T-T5RB-consolidate-task-status-enum
Status: closed/done · Impact: high · Complexity: large
The task status enum has accumulated semantically duplicated values
(done vs closed/done, superseded vs closed/superseded, bare
closed paired with legacy resolution_* fields, etc.) that exist
only because the schema grew incrementally. Collapse the enum to a
single canonical shape organized under four top-level majors —
planning/, open/, in-progress, closed/ — and migrate every
existing instance. This is also the first schema-version bump (v1 →
v2), so it establishes the per-version transform pattern that future
schema changes will follow.
plugin/entities/task/schema.json exposes 21 values in the status
enum, of which only 5 are in use across the 28 task files in
docs/planning/tasks/: draft (10), ready (3), in-progress (2),
closed/done (8), closed/superseded (1). The remaining 16 are a
mix of legacy bare forms (done, superseded, closed, open,
partially-superseded, relocated-upstream, investigated-no-repro)
and never-used closed reasons (closed/partially-superseded,
closed/obsoleted, closed/relocated, closed/no-repro,
closed/wontdo, closed/needs-definition, etc.). The duplicate
shapes force every reader (skill, audit, human) to think about whether
done and closed/done mean the same thing — they do, and they
shouldn’t both exist.
The allOf block carries a bare-closed-requires-resolution_*
branch purely to support files that nobody is writing anymore.
schema_version is at 1; no schema has ever bumped, so the
audit_entities.py / migrate_entities.py per-version handler chain
exists only in concept (see plugin/skills/entities-audit/SKILL.md
notes on stale-version drift being manual today).
Proposed
Section titled “Proposed”Schema version bumps to 2. The status enum contains exactly the
following values, in four majors:
planning/ — spec still being shaped, not pickable
planning/draft— captured idea; body still being writtenplanning/proposed— body filled in; awaiting refinement / triageplanning/needs-definition— refinement blocked on a missing piece (gap recorded indefinition_gap)planning/backlog— triaged; on the list but not next
open/ — available to be picked up right now
open/ready— implementation-ready; passestask-ensure-ready
in-progress — someone has it (bare; /blocked qualifier when applicable)
in-progressin-progress/blocked— work started, paused on external dependency
closed/ — terminal
closed/done— shippedclosed/superseded— fully replaced by another taskclosed/partially-superseded— partially replacedclosed/obsoleted— no longer relevantclosed/relocated— moved to another tracker / repoclosed/no-repro— investigated, couldn’t reproduceclosed/wontdo— decided not to do
Every existing task file in this repo has been rewritten to use the
new shape. Every plugin script and skill that references status values
has been updated. The migrate skill now dispatches stale
schema_version drift through a per-type version chain, with
plugin/entities/task/migrations/v1-to-v2.py as the first transform.
Approach
Section titled “Approach”-
Schema v2 — In
plugin/entities/task/schema.json: bumpversionto2, replace theenumwith the 14-entry list above, remove the bare-closed-requires-resolution_*branch fromallOf(the bareclosedstatus no longer exists), keep theclosed/*requirescompletion_notebranch. Update the schema description to drop the “legacy shape” language. -
Template — Update
plugin/entities/task/template.md: change the initialstatus:toplanning/draft, bumpschema_version: "2", update the HTML-comment workflow line to walkplanning/draft → planning/proposed → planning/backlog → open/ready. -
Creation script defaults — In
plugin/scripts/new_task.py: changeSTATUS_CHOICESto("planning/draft", "planning/proposed", "planning/backlog", "open/ready"), default toplanning/draft. Update the CLI help text. -
Skill references — Audit every
plugin/skills/*/SKILL.mdthat hardcodes status values. Known callers:task-work(flips status on pickup; current expectsready, moves toopen/ready),task-ensure-ready(downshifts toproposed/needs-definition; moves toplanning/needs-definition),task-review,task-define,task-new,backlog-triage. Each gets a search-and-replace for the literal status strings plus a re-read for any prose that mentions the old shape. -
v1→v2 migration transform — Create
plugin/entities/task/migrations/v1-to-v2.pyas a pure function that takes a parsed-frontmatter dict and returns the migrated dict. Mapping table is in the Migration mapping section below. For the bare-closed-with-resolutioncase, infer the target stage fromresolution, copy anycompletion_notealready present, and drop the legacyresolution,resolution_date,resolution_commitkeys. Raise a clearMigrationErrorif a bare-closedfile has noresolutionfield (shouldn’t happen given the v1 schema’sallOfbranch, but defensive). -
Audit + migrate wiring — In
audit_entities.py: when a file’sschema_versionis older than the schema’s currentversion, check whether a transform exists at<entities_dir>/<type>/migrations/v<file_ver>-to-v<curr_ver>.py; if yes, tag the driftauto_fixable: true. Inmigrate_entities.py: register a handler for theschema_version/older thandrift kind that loads and applies the transform chain (today only one hop, but the structure supports v1→v2→v3 chains later). The handler updates theschema_versionfield after applying so the file is now stamped at the new version. -
Apply to this repo — Run
/sdlc:entities-migrateagainst this repo. 13 task files should migrate (10 draft, 3 ready). Closed and in-progress files are unaffected by the status remapping but still need theirschema_versionbumped, so all 24 active task files will be touched in some way. Re-run the audit; confirm 0 drift. -
Tests — Add evals to
plugin/skills/entities-migrate/tests/covering the v1-to-v2 transform: one fixture per legacy status that requires migration (draft,ready,proposed,backlog,done,superseded,closedwith eachresolutionvariant). Add evals toplugin/skills/entities-audit/tests/confirming stale-version drift is nowauto_fixable: truewhen a transform exists.
Migration mapping
Section titled “Migration mapping”This is the canonical mapping the v1-to-v2 transform implements.
old status | → | new status | other field changes |
|---|---|---|---|
draft | → | planning/draft | — |
proposed | → | planning/proposed | — |
proposed/needs-definition | → | planning/needs-definition | — |
backlog | → | planning/backlog | — |
ready | → | open/ready | — |
in-progress | → | in-progress | unchanged |
in-progress/blocked | → | in-progress/blocked | unchanged |
closed/done | → | closed/done | unchanged |
closed/superseded | → | closed/superseded | unchanged |
closed/partially-superseded | → | closed/partially-superseded | unchanged |
closed/obsoleted | → | closed/obsoleted | unchanged |
closed/relocated | → | closed/relocated | unchanged |
closed/no-repro | → | closed/no-repro | unchanged |
closed/wontdo | → | closed/wontdo | unchanged |
closed (bare; resolution: fixed) | → | closed/done | drop resolution, resolution_date, resolution_commit; require completion_note (synthesize from resolution_commit if missing: “Resolved in |
closed (bare; resolution: wontfix) | → | closed/wontdo | same field cleanup |
closed (bare; resolution: superseded) | → | closed/superseded | same |
closed (bare; resolution: obsoleted) | → | closed/obsoleted | same |
open (bare legacy) | → | planning/proposed | best-guess default; no other field changes |
done (bare legacy) | → | closed/done | require completion_note (synthesize “Migrated from bare done status” if missing) |
superseded (bare legacy) | → | closed/superseded | same completion_note synthesis |
partially-superseded (legacy) | → | closed/partially-superseded | same |
relocated-upstream (legacy) | → | closed/relocated | same |
investigated-no-repro (legacy) | → | closed/no-repro | same |
Files to touch
Section titled “Files to touch”plugin/entities/task/schema.json— bumpversionto 2, replacestatusenum, remove bare-closedallOfbranchplugin/entities/task/template.md—schema_version: "2", initialstatus: planning/draft, comment block updateplugin/entities/task/migrations/v1-to-v2.py(new) — pure-function transform, importable by the migrate handlerplugin/scripts/new_task.py—STATUS_CHOICESupdate, default toplanning/draft, help textplugin/scripts/audit_entities.py— detect available transforms; flip stale-version drift toauto_fixable: truewhen a chain existsplugin/scripts/migrate_entities.py— register handler for stale-version drift; load and apply transform chain; updateschema_versionon successplugin/skills/task-work/SKILL.md— replaceready→open/ready,in-progressreferences audited, status-flip languageplugin/skills/task-ensure-ready/SKILL.md— downshift target becomesplanning/needs-definitionplugin/skills/task-new/SKILL.md— status choices in interview match new enumplugin/skills/task-define/SKILL.md— anyproposed/needs-definitionreferencesplugin/skills/task-review/SKILL.md— status references in triage proseplugin/skills/backlog-triage/SKILL.md— promotion creates tasks atplanning/draft(or whatever the new equivalent is)plugin/skills/entities-audit/SKILL.md— drift kinds table now listsschema_version older than currentas auto-fixable when transform existsplugin/skills/entities-migrate/SKILL.md— document the v1-to-v2 case; note that the apply path also bumpsschema_versionto currentplugin/skills/entities-migrate/tests/run_evals.py— new fixture cases for v1-to-v2 transformplugin/skills/entities-migrate/tests/fixtures/v1-to-v2-*/(new) — one fixture per legacy status that requires migrationplugin/skills/entities-audit/tests/run_evals.py— update thestale-schema-versioncase to assertauto_fixable: trueonce a real transform existsdocs/planning/tasks/*.md— every active task file migrated by/sdlc:entities-migrateat the end (13 status updates +schema_versionstamp on all 24 active files)
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
plugin/entities/task/schema.jsonvalidates as Draft 2020-12 withversion: 2and exactly the 14-value enum listed above; bare-closedallOfbranch is gone. - AC-2:
plugin/scripts/new_task.pydefaults toplanning/draft; running it against a fresh project produces a file that validates cleanly under the v2 schema. - AC-3:
plugin/entities/task/migrations/v1-to-v2.pyexists, is a pure function (no I/O), and every row of the migration table above has a unit-style fixture asserting the mapping. - AC-4: Running
/sdlc:entities-migrateagainst this repo’s main branch in a fresh worktree produces a diff that updates every active task file to v2 with the right status remapping;/sdlc:entities-auditafterwards exits 0 with no structural drift. - AC-5:
plugin/skills/entities-audit/tests/run_evals.pyandplugin/skills/entities-migrate/tests/run_evals.pyboth pass, including new cases for stale-version-with-transform and each migration row. - AC-6:
.claude/skills/project-check/check_entities.pyreports no drift across all entity directories. - AC-7: Every
plugin/skills/*/SKILL.mdthat references task statuses uses only values from the v2 enum; a grep for the removed values (\bdraft\b,\bready\bin status context,proposed/needs-definition, etc.) outside of the migration transform and this task file returns no results.
Out of scope
Section titled “Out of scope”- LLM-assisted prose migration. The H2-section drift still gets reported as manual after this lands; the section-content rewriter is a separate task.
- Milestone or backlog status reform. Those entities have their own state shapes; this task changes only the task enum. If similar duplication exists for milestones, it gets its own ticket.
- Cross-project migration tooling (e.g., a hosted migrator for projects that adopt the plugin
later). Once this lands, any v1 project that runs
/sdlc:entities-migrategets the v2 transform applied locally; that’s sufficient. - Renaming
definition_gap,resolution_*fields, or any frontmatter key beyond the enum cleanup. The bare-closedmigration removesresolution_*from migrated files; it does not remove the field definitions from the schema (deferred to a follow-up so an audit can confirm no instance still carries them first).
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Raised during the SDLC:Mini session on 2026-05-19, after /sdlc:setup
ran a fresh audit and surfaced the schema_version drift on this repo’s
two pre-versioning tasks. The conversation about semantic duplicates
in the status enum (done vs closed/done, etc.) led directly to
this consolidation. Establishes the v1→v2 transform pattern for
future schema changes.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-19. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
uv run --with jsonschemaDraft 2020-12 check + JSON read ofplugin/entities/task/schema.json;version: 2, 14-value enum, singleallOfbranch confirmed. - AC-2: auto —
plugin/scripts/new_task.pyinvoked against a fresh project root; output validated withplugin/validators/validate_frontmatter.py. - AC-3: auto —
plugin/entities/task/migrations/test_v1_to_v2.pyexercises every mapping table row plus error paths; 32/32 passing. - AC-4: auto — migration ran against this repo’s tasks + the M0001 milestone (30 files migrated);
plugin/scripts/audit_entities.pyexits 0 afterwards. - AC-5: auto —
plugin/skills/entities-audit/tests/run_evals.py8/8 andplugin/skills/entities-migrate/tests/run_evals.py8/8, including the new stale-version-with-transform cases. - AC-6: auto —
.claude/skills/project-check/check_entities.py --entities-dir plugin/entitiesclean across 4 entity dirs;.claude/skills/project-check/tests/run_evals.py7/7. - AC-7: auto —
grep -rEn '\(draft|ready|proposed|backlog)`|\bproposed/needs-definition\b’ plugin/skills/*/SKILL.mdreturns no live status references (only_LEGACY_BARE_CLOSED_STATUSES` backstop set inside the audit script, which is the v1-tolerance code path itself).
What worked
Section titled “What worked”- The sub-agent produced 7 atomic, logically-scoped commits — schema/template, transform module, audit+migrate wiring, SKILL.md sweep, pre-versioned-file fix, repo migration, follow-up cleanup. Each leaves the worktree buildable.
- Fixture-based eval pattern scaled cleanly from 4/4 to 8/8 cases on both audit and migrate suites without breaking the existing cases — the test layout supports incremental drift coverage.
validate_frontmatter.py+audit_entities.py+migrate_entities.pyformed a tight feedback loop during migration; structural problems surfaced as exit-1 from the audit before they could compound.
Friction and automation gaps
Section titled “Friction and automation gaps”- PyYAML auto-parses unquoted ISO 8601 datetime strings into Python
datetimeobjects, which then round-trip as2026-05-19T21:02:01+00:00and fail the schema’sZ-only pattern — task-ensure-ready hit this on first stamp attempt and the bad value landed in a commit before validation caught up. Two automation gaps fall out: (a)task-ensure-readyshould write the timestamp quoted (or pass it through the same_coerce_for_schemapath the validator uses) → T-DPO2-ensure-ready-stamps-quoted-iso-z, and (b) the validator’s pipe-to-tail in skill examples masks the exit code; skills that gate on the validator should invoke it without piping → T-XPG7-validator-skill-examples-no-pipe-tail. - The task spec implicitly assumed every pre-v2 file had
schema_version: 1set. In practice, two pre-existing tasks in this repo had noschema_versionfield at all (they predated that field), and the original migrate flow’s stamp path would have stampedschema_version: "2"onto a v1-shaped file, leaving it invalid against the v2 schema. The sub-agent had to add a generalization (commitc050d0b): if a file is missingschema_versionand a transform chain exists from v1, route it through the chain rather than the stamp path. Future schema bumps should specify this case upfront in the task spec rather than relying on implementer judgment. → T-U1GD-schema-bump-tasks-handle-missing-version yaml.safe_dumpre-emits files with keys in insertion order. The stamp path insertsschema_versionright aftertype:; the transform-chain path inserts it at the bottom of the dict. Functional but visually inconsistent across migrated files. A smallreorder_frontmatter_keys(...)helper called by both paths would close this. → T-NIPN-migrate-reorders-frontmatter-keys- The implementation-ready contract requires an
Out of scopesection “only when the surrounding scope is non-obvious.” The task-ensure-ready evaluator currently treats it as effectively-required (it caught this task because the spec includes one). Two reasonable readings of the contract — worth disambiguating inimplementation-ready.md. → T-E4ZB-clarify-out-of-scope-requirement - The
## Migration mappingtable sits between Approach and Files-to-touch in the task body, buttask-ensure-readylooks for the canonical contract sections by name. A task that introduces a domain-specific section breaks the linear flow assumption. Not blocking here, but if more tasks start carrying “reference tables” they’ll need a convention (subsection under Approach? appendix-style at the end?). → T-BFKL-ensure-ready-tolerates-domain-sections
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-DPO2-ensure-ready-stamps-quoted-iso-z — task-ensure-ready writes a quoted Z-only ISO 8601 stamp that round-trips through PyYAML cleanly, created
- T-XPG7-validator-skill-examples-no-pipe-tail — skill examples invoke validate_frontmatter.py without piping so exit codes propagate, created
- T-U1GD-schema-bump-tasks-handle-missing-version — schema-bump task spec template enumerates missing-schema_version handling upfront, created
- T-NIPN-migrate-reorders-frontmatter-keys — shared reorder_frontmatter_keys helper yields canonical key order across migrate paths, created
- T-E4ZB-clarify-out-of-scope-requirement — implementation-ready contract disambiguates whether Out of scope is required or conditional, created
- T-BFKL-ensure-ready-tolerates-domain-sections — task-ensure-ready tolerates domain-specific H2 sections between canonical ones, created