Feature lifecycle: harness features listing, receipts, self-allowing add, and remove with purge #144

Closed
opened 2026-10-04 17:31:12 +02:00 by dominik.polakovics · 0 comments

Summary

Give feature bundles a full, machine-readable lifecycle: a harness features listing, a harness add that leaves the feature usable and records what it did, and a new harness remove that undoes it. deckle-hosted will drive all three (at site creation and at any later point) and render its Features UI from the listing, so it stops hardcoding anything about news.

Current behavior

  • Feature bundles are embedded directories under scaffold/features/<name> with no metadata. addcmd.Features() lists their names; the only way to see them from the CLI is the usage error of harness add (stderr, exit 2).
  • harness add <feature> copies the bundle into the site (all-or-nothing, never overwriting) and runs Go config steps keyed by feature name (featureSteps; for news: buildFuture, the categories taxonomy edits in hugo.toml).
  • Add deliberately never touches the site's own page-type sidecars. Every scaffolded page type has children: [page, section], so after harness add news the editor has nowhere to offer a news section until someone hand-edits e.g. editor/pagetypes/home.yaml (featureNextSteps prints that as a manual step).
  • Add leaves no record. A second harness add news fails with a ConflictError; nothing can tell whether a site has a feature installed.
  • There is no remove, and no harness command emits JSON.

Desired behavior

Bundle manifest

Each bundle carries one manifest file at its root (e.g. scaffold/features/news/feature.yaml) with: label, description (plain English, shown next to a checkbox), an integer version, and the page-type allowance it wants (for news: allow page type news as a child of page type home). The manifest is never copied into a site and is validated by a test for every embedded bundle.

harness features [--site <dir>] [--json]

  • Without --site: the catalogue — every embedded feature's name, label, description, version.
  • With --site: the same, plus per feature whether it is installed on that site and at which version (from the receipt, see below).
  • Default output is human-readable text; --json prints one JSON object on stdout.

harness add <feature> [--site <dir>] [--json]

On top of today's vendoring and config steps:

  • Allows the section itself. Applies the manifest's allowance: adds the feature's page type to the target page type's children list in the site's sidecar. The edit preserves the file's comments and formatting and is idempotent. If the target sidecar is missing, unparsable, or has no explicit children list to extend, add still succeeds, reports the allowance as skipped, and prints the manual step. When the allowance was applied, the "allow the section somewhere" lines disappear from the printed next steps.
  • Writes a receipt at editor/features/<name>.yaml in the site, recording: the feature name and bundle version, every vendored path with a content hash of the bytes written, which config edits actually changed something (an edit that found its result already in place is not recorded), and which allowance was applied.
  • Already installed is success. If the receipt exists, add changes nothing, reports "already installed" and exits 0.
  • Collisions refuse as today. No receipt and any destination path exists (including the case where every bundle path exists): nothing is written, the conflicting paths are named, exit 1.

harness remove <feature> [--site <dir>] [--purge] [--json]

Driven by the receipt.

  • Not installed (no receipt): nothing changes, exit 0.
  • In use, without --purge: nothing changes at all — not one byte of the tree. The command reports what a purge would delete and exits 1 (status blocked). "In use" means any of:
    • pages whose effective page type is one of the feature's page types (for news: news sections and news-item pages), including everything beneath them;
    • elements of the feature's element types placed on other pages (for news: news-list), at any nesting depth;
    • vendored files whose current content no longer matches the receipt hash (customised by the owner).
  • Not in use, or with --purge: remove
    • deletes the in-use pages (whole bundles, with their page-local resources) and strips the feature's elements out of other pages, writing those pages back through the engine's normal content serialization;
    • deletes every vendored path in the receipt, customised or not, and prunes directories that became empty;
    • reverts each recorded config edit only if the setting is still exactly as add left it; otherwise leaves it and reports it as kept. Example: add re-enabled the taxonomy/term kinds and declared categories; if the owner has since added a tags taxonomy, remove takes out only the categories entry and leaves taxonomies enabled. A config edit that is not in the receipt (it was already in place before the add) is never reverted;
    • removes the feature's page types from the children list of every page-type sidecar in the site (not only the one add edited), comment-preserving;
    • deletes the receipt.
  • The preview and the result both list pages that link to purged pages (text links and page-reference params). Those links are left as they are — same as deleting a page in the editor.
  • Files in the shared media library (assets/media/) are never deleted.

JSON contract (the surface deckle-hosted consumes)

With --json, stdout carries exactly one JSON object and nothing else. Every object has a top-level format version so fields can be added without breaking consumers. Exit codes keep the existing convention: 0 the requested change happened or was already in place, 1 it did not (the JSON says why), 2 usage error (stderr, no JSON).

Illustrative shapes — field names are the implementer's call, the information content is the contract:

{"formatVersion": 1, "features": [
  {"name": "news", "label": "News", "description": "…", "version": 1,
   "installed": true, "installedVersion": 1}
]}
{"formatVersion": 1, "feature": "news", "status": "added",
 "allowance": "applied", "nextSteps": ["…"]}
{"formatVersion": 1, "feature": "news", "status": "blocked",
 "usage": {
   "pages": ["content/news/_index.md", "content/news/launch/_index.md"],
   "elements": [{"page": "content/_index.md", "element": "news-list", "count": 1}],
   "customizedFiles": ["layouts/news/list.html"],
   "linkedFrom": ["content/about/_index.md"]},
 "keptConfig": []}
  • add statuses: added / already installed / conflict (with the paths) / error (with a message); plus the allowance outcome (applied / already present / skipped, with the reason).
  • remove statuses: removed / not installed / blocked / error. blocked carries the usage preview; removed carries what was actually purged and which config edits were kept and why.
  • installed/installedVersion appear only when --site was given.

Decisions

  • harness add edits the site's page-type sidecar — reverses the old "never touches the site's OWN page-type sidecars" rule. "Vendored but nowhere to create it" is a half-finished state nobody wants; hosted was about to automate the same edit from outside. On by default for CLI users too.
  • Allowance failure is soft — a customised or missing home.yaml must not make an otherwise valid add fail; the manual step is printed instead.
  • Installed state is a receipt, not inferred from files — inference breaks when an owner renames one vendored layout, and cannot tell remove whether e.g. buildFuture = true predates the add.
  • One receipt file per feature, committed with the site — add creates it, remove deletes it, nothing to merge; every clone agrees.
  • Receipt hashes, not a comparison against the embedded bundle — a newer harness may ship a changed bundle, which would make every file look modified.
  • Remove purges content, behind an explicit flag — the content is still in git history, which is the owner's way back; the flag plus preview protects a developer with uncommitted work and gives hosted its confirmation dialog (it mirrors the editor's count-then-delete page deletion).
  • Customised vendored files go with the purge — keeping them would leave half a feature (a list template whose page type no longer exists), and hosted users cannot clean up files by hand.
  • Config reverts only what is still exactly as add left it — remove takes out what belongs to the feature and leaves alone anything the owner made their own.
  • The commands never touch git — they only change files in the working tree; the caller commits. Consequently remove (preview and purge) must behave identically on a clean tree and on one with uncommitted changes, and must never refuse because of git state. Hosted relies on this to run the read-only preview and the purge directly on an editor working copy that holds unpublished drafts.
  • Bundle version is recorded now — an update command is out of scope, but it cannot exist later without knowing which version a site has.
  • Config steps stay Go code keyed by feature name — no declarative config-edit language; there is one bundle to design against. Each config step gains a matching revert.
  • Label and description are English only — sidecar labels are plain English strings today.
  • features is its own command, not a flag on add — it needs no feature argument and has a per-site mode.

Key interfaces

  • Starting hints: internal/addcmd (vendoring, Features(), featureSteps, featureNextSteps, the hugo.toml transforms), cmd/harness/main.go (subcommand wiring, usage text, exit-code convention), scaffold/features/news, scaffold/embed.go.
  • Bundle manifest shape (label, description, version, allowance) and the receipt shape (version, files with hashes, applied config edits, applied allowance) — new, both YAML.
  • buildPlan must not vendor the manifest; harness init keeps skipping features/.
  • Effective page type must come from the engine's existing rule (internal/pagepath, used by internal/pageops), not a raw front-matter match.
  • Element stripping should go through the content model (internal/content, internal/elemops) so canonicalization rules hold; inbound-link discovery exists in internal/linkrefs (References).
  • The feature's page types and element types are derived from the sidecar paths in the receipt (editor/pagetypes/*.yaml, editor/elements/*.yaml).
  • scaffold/site/AGENTS.md and README.md document harness add today; both must describe the three commands, the receipt, the allowance and the purge semantics (project rule: the blueprint AGENTS.md must track the engine in the same change).

Acceptance criteria

  • Every embedded bundle has a valid manifest; a test fails for a bundle without one. The manifest never appears in a site after add.
  • harness features prints name, label, description and version for every bundle; --json prints one versioned JSON object; --site adds installed state and installed version per feature.
  • harness add with no feature still exits 2 and still names the available features.
  • After harness init + harness add news, editor/pagetypes/home.yaml lists news in children, its comments are intact, and a news section can be created at the content root without any manual edit.
  • Running the allowance twice produces no change; a home.yaml that is missing, or has no editable children list, yields a successful add that reports the allowance as skipped and prints the manual step.
  • Add writes editor/features/news.yaml listing every vendored path with its hash, the bundle version, the config edits that changed something, and the applied allowance. On a site that already had buildFuture = true, the receipt does not record that edit.
  • A second harness add news exits 0, reports already installed, and leaves the tree byte-identical.
  • Add on a site without a receipt where any bundle path exists writes nothing, names the conflicts and exits 1.
  • harness remove news on a site without the receipt exits 0 and changes nothing.
  • harness remove news right after harness init + harness add news (nothing in use) restores the tree byte-for-byte to its pre-add state, including hugo.toml and home.yaml.
  • With news pages, a news-list element on another page, or a customised vendored file present, harness remove news exits 1 with status blocked, lists each of them, and leaves the tree byte-identical.
  • harness remove news --purge deletes the news pages and their subtrees, strips news-list elements (including nested ones) while leaving the rest of those pages intact, deletes customised vendored files, and the resulting site builds with Hugo.
  • A setting changed by the owner after the add (e.g. an added tags taxonomy) survives remove and is reported as kept; a setting that predates the add survives remove.
  • After remove, no page-type sidecar in the site lists a removed page type in children, including sidecars the owner edited by hand.
  • Pages linking to purged pages are reported and left unchanged; nothing under assets/media/ is deleted.
  • Preview and purge give the same results on a tree with uncommitted changes and in a directory that is not a git repository.
  • --json output is a single JSON object on stdout for every status of add and remove, with the documented exit codes.
  • scaffold/site/AGENTS.md and README.md describe the new commands, flags, receipt, allowance and purge behaviour; the drift test still passes.

How to verify

  • make test (builds the web bundle, then go test ./...).
  • End to end in a scratch directory: harness init, harness features --json, harness add news --json, harness features --site . --json, create a news section with an item and place a "Latest news" element on the home page, harness remove news --json (blocked, tree unchanged), harness remove news --purge --json, then a Hugo build of the result.
  • Round trip: snapshot the tree after harness init, add then remove with nothing in use, diff against the snapshot — empty.

Assumptions to check

  • What an omitted children key means in a page-type sidecar (everything allowed vs nothing) — it decides when the allowance is "nothing to do" versus "skipped".
  • Whether the page-type loader rejects or ignores a children entry naming a type with no sidecar — it decides how strictly remove must clean every list.
  • That nothing in harness serve or the sidecar loaders treats a new editor/features/ directory as sidecars or trips over it.
  • That a missing shortcode template fails the Hugo build (the reason elements must be stripped rather than left) on the pinned Hugo version.
  • That the existing hugo.toml transforms can each report precisely whether they changed something, and that each has a well-defined inverse.
  • That a comment-preserving YAML edit of a flow-style list (children: [page, section]) is achievable with the YAML library already in use.

Out of scope

  • Loading bundles from outside the binary (hosted-only or third-party features).
  • Updating an installed feature to a newer bundle version.
  • Adopting sites that added a feature before receipts existed — they are treated as a collision.
  • Any git operation by these commands (commit, push, draft handling) — the caller's job.
  • A features screen in the standalone editor UI.
  • Translating labels and descriptions.
  • Restoring purged content.
  • The deckle-hosted side (tracked separately).

Blocked by

None - can start immediately

## Summary Give feature bundles a full, machine-readable lifecycle: a `harness features` listing, a `harness add` that leaves the feature usable and records what it did, and a new `harness remove` that undoes it. deckle-hosted will drive all three (at site creation and at any later point) and render its Features UI from the listing, so it stops hardcoding anything about `news`. ## Current behavior - Feature bundles are embedded directories under `scaffold/features/<name>` with no metadata. `addcmd.Features()` lists their names; the only way to see them from the CLI is the usage error of `harness add` (stderr, exit 2). - `harness add <feature>` copies the bundle into the site (all-or-nothing, never overwriting) and runs Go config steps keyed by feature name (`featureSteps`; for news: `buildFuture`, the categories taxonomy edits in `hugo.toml`). - Add deliberately never touches the site's own page-type sidecars. Every scaffolded page type has `children: [page, section]`, so after `harness add news` the editor has nowhere to offer a news section until someone hand-edits e.g. `editor/pagetypes/home.yaml` (`featureNextSteps` prints that as a manual step). - Add leaves no record. A second `harness add news` fails with a `ConflictError`; nothing can tell whether a site has a feature installed. - There is no remove, and no harness command emits JSON. ## Desired behavior ### Bundle manifest Each bundle carries one manifest file at its root (e.g. `scaffold/features/news/feature.yaml`) with: `label`, `description` (plain English, shown next to a checkbox), an integer `version`, and the page-type allowance it wants (for news: allow page type `news` as a child of page type `home`). The manifest is never copied into a site and is validated by a test for every embedded bundle. ### `harness features [--site <dir>] [--json]` - Without `--site`: the catalogue — every embedded feature's name, label, description, version. - With `--site`: the same, plus per feature whether it is installed on that site and at which version (from the receipt, see below). - Default output is human-readable text; `--json` prints one JSON object on stdout. ### `harness add <feature> [--site <dir>] [--json]` On top of today's vendoring and config steps: - **Allows the section itself.** Applies the manifest's allowance: adds the feature's page type to the target page type's `children` list in the site's sidecar. The edit preserves the file's comments and formatting and is idempotent. If the target sidecar is missing, unparsable, or has no explicit `children` list to extend, add still succeeds, reports the allowance as skipped, and prints the manual step. When the allowance was applied, the "allow the section somewhere" lines disappear from the printed next steps. - **Writes a receipt** at `editor/features/<name>.yaml` in the site, recording: the feature name and bundle version, every vendored path with a content hash of the bytes written, which config edits actually changed something (an edit that found its result already in place is not recorded), and which allowance was applied. - **Already installed is success.** If the receipt exists, add changes nothing, reports "already installed" and exits 0. - **Collisions refuse as today.** No receipt and any destination path exists (including the case where every bundle path exists): nothing is written, the conflicting paths are named, exit 1. ### `harness remove <feature> [--site <dir>] [--purge] [--json]` Driven by the receipt. - **Not installed** (no receipt): nothing changes, exit 0. - **In use, without `--purge`**: nothing changes at all — not one byte of the tree. The command reports what a purge would delete and exits 1 (status `blocked`). "In use" means any of: - pages whose effective page type is one of the feature's page types (for news: `news` sections and `news-item` pages), including everything beneath them; - elements of the feature's element types placed on other pages (for news: `news-list`), at any nesting depth; - vendored files whose current content no longer matches the receipt hash (customised by the owner). - **Not in use, or with `--purge`**: remove - deletes the in-use pages (whole bundles, with their page-local resources) and strips the feature's elements out of other pages, writing those pages back through the engine's normal content serialization; - deletes every vendored path in the receipt, customised or not, and prunes directories that became empty; - reverts each recorded config edit only if the setting is still exactly as add left it; otherwise leaves it and reports it as kept. Example: add re-enabled the `taxonomy`/`term` kinds and declared `categories`; if the owner has since added a `tags` taxonomy, remove takes out only the `categories` entry and leaves taxonomies enabled. A config edit that is not in the receipt (it was already in place before the add) is never reverted; - removes the feature's page types from the `children` list of **every** page-type sidecar in the site (not only the one add edited), comment-preserving; - deletes the receipt. - The preview and the result both list pages that link to purged pages (text links and page-reference params). Those links are left as they are — same as deleting a page in the editor. - Files in the shared media library (`assets/media/`) are never deleted. ### JSON contract (the surface deckle-hosted consumes) With `--json`, stdout carries exactly one JSON object and nothing else. Every object has a top-level format version so fields can be added without breaking consumers. Exit codes keep the existing convention: `0` the requested change happened or was already in place, `1` it did not (the JSON says why), `2` usage error (stderr, no JSON). Illustrative shapes — field names are the implementer's call, the information content is the contract: ```json {"formatVersion": 1, "features": [ {"name": "news", "label": "News", "description": "…", "version": 1, "installed": true, "installedVersion": 1} ]} ``` ```json {"formatVersion": 1, "feature": "news", "status": "added", "allowance": "applied", "nextSteps": ["…"]} ``` ```json {"formatVersion": 1, "feature": "news", "status": "blocked", "usage": { "pages": ["content/news/_index.md", "content/news/launch/_index.md"], "elements": [{"page": "content/_index.md", "element": "news-list", "count": 1}], "customizedFiles": ["layouts/news/list.html"], "linkedFrom": ["content/about/_index.md"]}, "keptConfig": []} ``` - add statuses: added / already installed / conflict (with the paths) / error (with a message); plus the allowance outcome (applied / already present / skipped, with the reason). - remove statuses: removed / not installed / blocked / error. `blocked` carries the usage preview; `removed` carries what was actually purged and which config edits were kept and why. - `installed`/`installedVersion` appear only when `--site` was given. ## Decisions - **`harness add` edits the site's page-type sidecar** — reverses the old "never touches the site's OWN page-type sidecars" rule. "Vendored but nowhere to create it" is a half-finished state nobody wants; hosted was about to automate the same edit from outside. On by default for CLI users too. - **Allowance failure is soft** — a customised or missing `home.yaml` must not make an otherwise valid add fail; the manual step is printed instead. - **Installed state is a receipt, not inferred from files** — inference breaks when an owner renames one vendored layout, and cannot tell remove whether e.g. `buildFuture = true` predates the add. - **One receipt file per feature, committed with the site** — add creates it, remove deletes it, nothing to merge; every clone agrees. - **Receipt hashes, not a comparison against the embedded bundle** — a newer harness may ship a changed bundle, which would make every file look modified. - **Remove purges content, behind an explicit flag** — the content is still in git history, which is the owner's way back; the flag plus preview protects a developer with uncommitted work and gives hosted its confirmation dialog (it mirrors the editor's count-then-delete page deletion). - **Customised vendored files go with the purge** — keeping them would leave half a feature (a list template whose page type no longer exists), and hosted users cannot clean up files by hand. - **Config reverts only what is still exactly as add left it** — remove takes out what belongs to the feature and leaves alone anything the owner made their own. - **The commands never touch git** — they only change files in the working tree; the caller commits. Consequently remove (preview and purge) must behave identically on a clean tree and on one with uncommitted changes, and must never refuse because of git state. Hosted relies on this to run the read-only preview and the purge directly on an editor working copy that holds unpublished drafts. - **Bundle version is recorded now** — an update command is out of scope, but it cannot exist later without knowing which version a site has. - **Config steps stay Go code keyed by feature name** — no declarative config-edit language; there is one bundle to design against. Each config step gains a matching revert. - **Label and description are English only** — sidecar labels are plain English strings today. - **`features` is its own command, not a flag on `add`** — it needs no feature argument and has a per-site mode. ## Key interfaces - Starting hints: `internal/addcmd` (vendoring, `Features()`, `featureSteps`, `featureNextSteps`, the `hugo.toml` transforms), `cmd/harness/main.go` (subcommand wiring, usage text, exit-code convention), `scaffold/features/news`, `scaffold/embed.go`. - Bundle manifest shape (label, description, version, allowance) and the receipt shape (version, files with hashes, applied config edits, applied allowance) — new, both YAML. - `buildPlan` must not vendor the manifest; `harness init` keeps skipping `features/`. - Effective page type must come from the engine's existing rule (`internal/pagepath`, used by `internal/pageops`), not a raw front-matter match. - Element stripping should go through the content model (`internal/content`, `internal/elemops`) so canonicalization rules hold; inbound-link discovery exists in `internal/linkrefs` (`References`). - The feature's page types and element types are derived from the sidecar paths in the receipt (`editor/pagetypes/*.yaml`, `editor/elements/*.yaml`). - `scaffold/site/AGENTS.md` and `README.md` document `harness add` today; both must describe the three commands, the receipt, the allowance and the purge semantics (project rule: the blueprint AGENTS.md must track the engine in the same change). ## Acceptance criteria - [ ] Every embedded bundle has a valid manifest; a test fails for a bundle without one. The manifest never appears in a site after add. - [ ] `harness features` prints name, label, description and version for every bundle; `--json` prints one versioned JSON object; `--site` adds installed state and installed version per feature. - [ ] `harness add` with no feature still exits 2 and still names the available features. - [ ] After `harness init` + `harness add news`, `editor/pagetypes/home.yaml` lists `news` in `children`, its comments are intact, and a news section can be created at the content root without any manual edit. - [ ] Running the allowance twice produces no change; a `home.yaml` that is missing, or has no editable `children` list, yields a successful add that reports the allowance as skipped and prints the manual step. - [ ] Add writes `editor/features/news.yaml` listing every vendored path with its hash, the bundle version, the config edits that changed something, and the applied allowance. On a site that already had `buildFuture = true`, the receipt does not record that edit. - [ ] A second `harness add news` exits 0, reports already installed, and leaves the tree byte-identical. - [ ] Add on a site without a receipt where any bundle path exists writes nothing, names the conflicts and exits 1. - [ ] `harness remove news` on a site without the receipt exits 0 and changes nothing. - [ ] `harness remove news` right after `harness init` + `harness add news` (nothing in use) restores the tree byte-for-byte to its pre-add state, including `hugo.toml` and `home.yaml`. - [ ] With news pages, a `news-list` element on another page, or a customised vendored file present, `harness remove news` exits 1 with status blocked, lists each of them, and leaves the tree byte-identical. - [ ] `harness remove news --purge` deletes the news pages and their subtrees, strips `news-list` elements (including nested ones) while leaving the rest of those pages intact, deletes customised vendored files, and the resulting site builds with Hugo. - [ ] A setting changed by the owner after the add (e.g. an added `tags` taxonomy) survives remove and is reported as kept; a setting that predates the add survives remove. - [ ] After remove, no page-type sidecar in the site lists a removed page type in `children`, including sidecars the owner edited by hand. - [ ] Pages linking to purged pages are reported and left unchanged; nothing under `assets/media/` is deleted. - [ ] Preview and purge give the same results on a tree with uncommitted changes and in a directory that is not a git repository. - [ ] `--json` output is a single JSON object on stdout for every status of add and remove, with the documented exit codes. - [ ] `scaffold/site/AGENTS.md` and `README.md` describe the new commands, flags, receipt, allowance and purge behaviour; the drift test still passes. ## How to verify - `make test` (builds the web bundle, then `go test ./...`). - End to end in a scratch directory: `harness init`, `harness features --json`, `harness add news --json`, `harness features --site . --json`, create a news section with an item and place a "Latest news" element on the home page, `harness remove news --json` (blocked, tree unchanged), `harness remove news --purge --json`, then a Hugo build of the result. - Round trip: snapshot the tree after `harness init`, add then remove with nothing in use, diff against the snapshot — empty. ## Assumptions to check - What an omitted `children` key means in a page-type sidecar (everything allowed vs nothing) — it decides when the allowance is "nothing to do" versus "skipped". - Whether the page-type loader rejects or ignores a `children` entry naming a type with no sidecar — it decides how strictly remove must clean every list. - That nothing in `harness serve` or the sidecar loaders treats a new `editor/features/` directory as sidecars or trips over it. - That a missing shortcode template fails the Hugo build (the reason elements must be stripped rather than left) on the pinned Hugo version. - That the existing `hugo.toml` transforms can each report precisely whether they changed something, and that each has a well-defined inverse. - That a comment-preserving YAML edit of a flow-style list (`children: [page, section]`) is achievable with the YAML library already in use. ## Out of scope - Loading bundles from outside the binary (hosted-only or third-party features). - Updating an installed feature to a newer bundle version. - Adopting sites that added a feature before receipts existed — they are treated as a collision. - Any git operation by these commands (commit, push, draft handling) — the caller's job. - A features screen in the standalone editor UI. - Translating labels and descriptions. - Restoring purged content. - The deckle-hosted side (tracked separately). ## Blocked by None - can start immediately
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#144
No description provided.