T-Y3D4-archive-closed-tasks-in-subdirectory
Status: closed/superseded · Impact: high · Complexity: large
Closed and active tasks share docs/planning/tasks/, which forces every
scanner (entities audit/migrate, task next, task resolve, the
corpus loader, project cleanup) to special-case “is this closed?” via a
frontmatter read. PR #101’s touchpoint migration scope-crept into 76
closed-task files (reverted) precisely because “closed files live
alongside active files but should be treated specially” is load-bearing
in every code path. Move closed tasks under archive/ and the
distinction becomes a filesystem fact — default scans read only the top
level — and the whole class of “did the scanner remember to special-case
closed?” bugs goes with it.
| Location | Role today |
|---|---|
docs/planning/tasks/ | one flat dir: 477 files, 322 with status: closed/* at time of review; no archive/ subdir exists |
apps/sdlc/lib/model/ops/audit.ts#discoverFiles | enumeration for sdlc entities audit and (via runAudit) entities migrate: top-level *.md glob; --include-archive already recurses archive//archived/ subdirs, which don’t exist yet |
apps/sdlc/lib/model/ops/audit.ts#auditFile | skips closed entities via frontmatter status (--include-closed opt-in); treats schema_version: 0 as intentional non-drift — a legacy tolerance nothing writes anymore (zero instances on disk) |
apps/sdlc/lib/model/ops/migrate.ts | skips closed entities at runtime, reporting them under “skipped (closed)”; --include-closed opt-in |
apps/sdlc/lib/model/entities/task/ops/next.ts | sdlc task next flat-readdirs the tasks dir and reads every file’s frontmatter to filter by status |
apps/sdlc/lib/model/entities/task/ops/resolve.ts | sdlc task resolve flat-readdirs the tasks dir (contract: file-resolution.md) |
apps/sdlc/lib/model/corpus/loader.ts#loadCorpus | wikilink/depends_on resolution corpus: flat-readdirs each docs/planning/<plural>/ dir; no archive awareness |
apps/sdlc/lib/model/entities/task/ops/close-commit.ts | sdlc task close-commit (driven by /sdlc:task-close-out) lands closing frontmatter via an ephemeral worktree; the file stays in place |
apps/sdlc/lib/services/project/ops/scan-candidates.ts | sdlc project cleanup scanner walks docs/planning/tasks/ for task linkage |
Proposed
Section titled “Proposed”- Closed tasks live at
docs/planning/tasks/archive/<basename>.md. Active tasks stay atdocs/planning/tasks/<basename>.md. - Close-out becomes a
git mv+ frontmatter edit inside the close-commit op (the move usesgit mvso file history follows). Closure commits show as renames ingit diff --stat. - Default scans read only top-level
*.mdand naturally skip the archive subdir. Archive access is explicit opt-in (--include-archive, already present onentities audit). - Enumeration is centralized in one
apps/sdlc/lib/util/module exposing active-only and include-archived listings. Every scanner consumes it; the helper is the one place that knows about the archive directory. - Wikilink and
depends_on:resolution union both dirs. Basenames are globally unique (mintedT-XXXX-<slug>), so resolution stays unambiguous. Where both files exist (botched-migration edge case), active wins; archive is consulted only on explicit opt-in. - The runtime closed-status special-casing retires for tasks: with
archive off the scan path, audit/migrate never see closed task files
unless
--include-archiveis passed. Audit’s legacyschema_version: 0non-drift tolerance is removed. Closed tasks keep whateverschema_versionthey had when they closed; audit never reports drift on archived files. - One-time migration
git mvs every closed task intoarchive/, preserving blame and history per file (git log --followwalks across the rename cleanly).
Approach
Section titled “Approach”Ordered for landability — each numbered step is intended to be a self-contained commit, and the multi-PR break points are called out inline.
- Add the enumeration helper to
apps/sdlc/lib/util/(module name open — decide at pickup), hoistingdiscoverFiles/globMdTopLevel/rglobMdout ofapps/sdlc/lib/model/ops/audit.ts. Expose an active-only listing and an include-archived listing; the archive concept is “any path witharchive/archivedas a component under the entity dir”. Co-locate tests underapps/sdlc/lib/util/tests/. (PR 1, alone, so downstream PRs rebase onto a stable helper.) - Sweep scanners onto the helper.
audit.ts#discoverFilescall sites (coversentities auditandentities migrate),task next(next.ts),task resolve(resolve.ts), the corpus loader (loader.ts), and the project-cleanup scanner (scan-candidates.ts). GrepreaddirSync|globunderapps/sdlc/lib/to catch stragglers. Behavior is unchanged on disk (archive doesn’t exist yet) — this is the API migration only. (PR 2.) - Union the resolvers. The corpus loader consults the
include-archived listing so a
T-XXXX-fooreference still resolves after the corpus migration. Add a unit test that seeds a file atarchive/<basename>.mdand asserts the resolver finds it. Decide and codify per-resolver whether archive participates — e.g. thedepends_ongraph’s “legit target of a new edge” enumeration stays active-only. (PR 2 if small, else PR 3.) git mvin close-out. Extendsdlc task close-committo move the file todocs/planning/tasks/archive/<basename>.mdin the same closing commit, before the frontmatter edit. Updatetask-close-out’s SKILL.md and the close-commit tests. (PR 3 or 4.)- One-time corpus migration.
git mvevery file underdocs/planning/tasks/whose frontmatterstatusstarts withclosed/(322 at time of review — re-enumerate at pickup) intodocs/planning/tasks/archive/. Single commit, zero content changes. Verifygit diff --stat HEAD~1shows only renames with zero insertions/deletions, andgit log --followon a sample file shows pre-migration history. (PR 5, alone, so the diff reviews as pure renames.) - Retire task closed special-casing. Remove audit’s
schema_version: 0non-drift branch. The generic--include-closedskip in audit/migrate stays — other entity types (epic, milestone, backlog) still mix closed and active in one dir — but for tasks it becomes vestigial: archive keeps closed tasks off the scan path entirely. Update tests. (PR 6.) - Document the convention. Add
apps/sdlc/lib/model/entities/task/archive.mdexplaining the directory layout, how close-out writes there, and how scanners and resolvers see (or don’t see) archived files. Updateapps/sdlc/lib/model/entities/task/file-resolution.md, which assumes one flat dir. (PR 6 or 7.) - Schema-bump checklist follow-up. Add a sixth question to
apps/sdlc/conventions/schema-bump-checklist.md— “what happens to closed entities during this migration?”. With archive in place the answer for task migrations becomes “nothing; archive is off the migration path”. (PR 6 or 7.) - Refresh per-skill docs. Update SKILL.md prose for skills whose
behavior changed (
task-close-out,entities-migrate,entities-audit, plus any surfaced by step 2’s grep) and re-runsdlc docs generate. (Final PR, after code lands.)
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
apps/sdlc/lib/util/ | modify | new enumeration module (name open) + tests: active-only and include-archived listings |
apps/sdlc/lib/model/ops/audit.ts | modify | discoverFiles/globMdTopLevel/rglobMd hoist to the helper; drop the schema_version: 0 non-drift branch |
apps/sdlc/lib/model/ops/migrate.ts | modify | enumeration via the helper; closed-skip stays for non-archived entity types |
apps/sdlc/lib/model/entities/task/ops/next.ts | modify | readdir swap to the helper |
apps/sdlc/lib/model/entities/task/ops/resolve.ts | modify | readdir swap; archive handling per the file-resolution contract |
apps/sdlc/lib/model/corpus/loader.ts | modify | union-glob for wikilink/depends_on resolution |
apps/sdlc/lib/services/project/ops/scan-candidates.ts | modify | task-linkage walk consumes the helper |
apps/sdlc/lib/model/entities/task/ops/close-commit.ts | modify | git mv to archive/ inside the closing commit |
apps/sdlc/lib/model/entities/task/ops/tests/ | modify | close-commit, next, resolve test updates |
apps/sdlc/skills/task-close-out/SKILL.md | modify | document the move step |
apps/sdlc/skills/entities-audit/SKILL.md | modify | archive/closed flag language updates |
apps/sdlc/skills/entities-migrate/SKILL.md | modify | archive/closed flag language updates |
apps/sdlc/lib/model/entities/task/archive.md | new | convention doc for the archive layout |
apps/sdlc/lib/model/entities/task/file-resolution.md | modify | drop the flat-dir assumption |
apps/sdlc/conventions/schema-bump-checklist.md | modify | add the closed-entity question |
docs/planning/tasks/T-*.md | modify | one-time git mv of all closed tasks to archive/ (pure renames) |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: The helper’s active-only listing returns exactly the set of
*.mdfiles directly underdocs/planning/tasks/whose path does not contain anarchive/part. Verified by a unit-test fixture with three active files and one archived file: the helper yields three. - AC-2: The include-archived listing returns all four files in the same fixture; the default (archive excluded) matches the active-only listing.
- AC-3:
sdlc entities auditwith default flags does not read any file underdocs/planning/tasks/archive/in the per-file pass. Verified by planting a known-malformed file atarchive/<basename>.mdand asserting audit returns zero drift entries against it. - AC-4:
/sdlc:task-close-outproduces a single commit containing onegit mv(rename detected bygit diff --find-renames) plus the closing frontmatter edit. Verified by a test fixture: anin-progresstask, close-out runs, the file is atdocs/planning/tasks/archive/<basename>.md, andgit show --stat HEADreports one rename and one status+completion_note delta. - AC-5: The corpus-migration commit shows every file that
grep -l '^status: closed/' docs/planning/tasks/*.mdenumerated at pickup as a rename ingit diff --stat <commit>~..<commit>, with zero insertions and zero deletions on the renamed paths. - AC-6:
git log --follow docs/planning/tasks/archive/<sample-closed-task>.mdshows the file’s full pre-archive history (commits dating before the corpus migration commit). Pick any one sample for the check. - AC-7:
apps/sdlc/lib/model/ops/audit.tsno longer contains theschema_version: 0non-drift branch. Verified by grep forsentinelin the file returning zero matches. - AC-8:
apps/sdlc/lib/model/entities/task/archive.mdexists and contains an H1 plus at least one paragraph each describing (a) the directory layout, (b) how close-out writes there, (c) how scanners and resolvers see archive contents. - AC-9: Wikilink resolution test: a fixture with a basename present
only at
tasks/archive/<basename>.mdresolves the wikilink<basename>to that archive path under the include-archived listing, and returns “unresolved” under the default listing. - AC-10:
apps/sdlc/conventions/schema-bump-checklist.mdcontains a question about closed-entity handling, phrased so the default answer for new task migrations is “nothing — archive is off the migration path”.
Out of scope
Section titled “Out of scope”- Extending the same archive treatment to other entity types (epic, milestone, backlog). They have similar lifecycles; that’s a follow-up once this pattern proves out for tasks. Audit/migrate’s generic closed-status skip stays in place for them.
- Changing the task
status:enum.closed/<reason>remains as informative metadata; the canonical “is this closed?” signal becomes the directory location, but the enum value still travels with the file for human readability and historical querying. - Restoring any of PR #101’s reverted closed-task changes. Those reverts stand; this task replaces the underlying mechanism rather than re-attempting the reverted sweep.
- Entity schema changes. The archive move is filesystem-level only; no
schema_versionbump. - A dedicated CLI for “show me my archived tasks”. The include-archived listing is the API; any UX surface on top of it is a separate task.
Dependencies
Section titled “Dependencies”- none — the op-substrate ports this task was sequenced behind (T-MBOK-lib-util-modules, T-IDW6-task-inflight-project-scans, T-NFSM-project-cleanup-port) have all closed, so the lib/CLI substrate this spec targets is in place.
Discovery context
Section titled “Discovery context”Surfaced from the PR #101 closed-task scope-creep incident: a
touchpoint migration intended only for active tasks instead stamped 76
closed tasks with a downgraded schema_version and re-serialized their
completion_note: YAML scalars as a rewrite side effect; the
closed-task scope was reverted. Root cause was an ambiguous spec phrase
about closed-task handling that a sub-agent interpreted as prescriptive
rather than descriptive. Globally unique task basenames make the
directory split nearly free at the resolver layer, which pushed the
structural fix from “expensive future cleanup” into “the right shape
now.”