Skip to content

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.

LocationRole 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#discoverFilesenumeration 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#auditFileskips 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.tsskips closed entities at runtime, reporting them under “skipped (closed)”; --include-closed opt-in
apps/sdlc/lib/model/entities/task/ops/next.tssdlc 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.tssdlc task resolve flat-readdirs the tasks dir (contract: file-resolution.md)
apps/sdlc/lib/model/corpus/loader.ts#loadCorpuswikilink/depends_on resolution corpus: flat-readdirs each docs/planning/<plural>/ dir; no archive awareness
apps/sdlc/lib/model/entities/task/ops/close-commit.tssdlc 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.tssdlc project cleanup scanner walks docs/planning/tasks/ for task linkage
  • Closed tasks live at docs/planning/tasks/archive/<basename>.md. Active tasks stay at docs/planning/tasks/<basename>.md.
  • Close-out becomes a git mv + frontmatter edit inside the close-commit op (the move uses git mv so file history follows). Closure commits show as renames in git diff --stat.
  • Default scans read only top-level *.md and naturally skip the archive subdir. Archive access is explicit opt-in (--include-archive, already present on entities 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 (minted T-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-archive is passed. Audit’s legacy schema_version: 0 non-drift tolerance is removed. Closed tasks keep whatever schema_version they had when they closed; audit never reports drift on archived files.
  • One-time migration git mvs every closed task into archive/, preserving blame and history per file (git log --follow walks across the rename cleanly).

Ordered for landability — each numbered step is intended to be a self-contained commit, and the multi-PR break points are called out inline.

  1. Add the enumeration helper to apps/sdlc/lib/util/ (module name open — decide at pickup), hoisting discoverFiles / globMdTopLevel / rglobMd out of apps/sdlc/lib/model/ops/audit.ts. Expose an active-only listing and an include-archived listing; the archive concept is “any path with archive/archived as a component under the entity dir”. Co-locate tests under apps/sdlc/lib/util/tests/. (PR 1, alone, so downstream PRs rebase onto a stable helper.)
  2. Sweep scanners onto the helper. audit.ts#discoverFiles call sites (covers entities audit and entities migrate), task next (next.ts), task resolve (resolve.ts), the corpus loader (loader.ts), and the project-cleanup scanner (scan-candidates.ts). Grep readdirSync|glob under apps/sdlc/lib/ to catch stragglers. Behavior is unchanged on disk (archive doesn’t exist yet) — this is the API migration only. (PR 2.)
  3. Union the resolvers. The corpus loader consults the include-archived listing so a T-XXXX-foo reference still resolves after the corpus migration. Add a unit test that seeds a file at archive/<basename>.md and asserts the resolver finds it. Decide and codify per-resolver whether archive participates — e.g. the depends_on graph’s “legit target of a new edge” enumeration stays active-only. (PR 2 if small, else PR 3.)
  4. git mv in close-out. Extend sdlc task close-commit to move the file to docs/planning/tasks/archive/<basename>.md in the same closing commit, before the frontmatter edit. Update task-close-out’s SKILL.md and the close-commit tests. (PR 3 or 4.)
  5. One-time corpus migration. git mv every file under docs/planning/tasks/ whose frontmatter status starts with closed/ (322 at time of review — re-enumerate at pickup) into docs/planning/tasks/archive/. Single commit, zero content changes. Verify git diff --stat HEAD~1 shows only renames with zero insertions/deletions, and git log --follow on a sample file shows pre-migration history. (PR 5, alone, so the diff reviews as pure renames.)
  6. Retire task closed special-casing. Remove audit’s schema_version: 0 non-drift branch. The generic --include-closed skip 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.)
  7. Document the convention. Add apps/sdlc/lib/model/entities/task/archive.md explaining the directory layout, how close-out writes there, and how scanners and resolvers see (or don’t see) archived files. Update apps/sdlc/lib/model/entities/task/file-resolution.md, which assumes one flat dir. (PR 6 or 7.)
  8. 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.)
  9. 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-run sdlc docs generate. (Final PR, after code lands.)
LocationKindChange
apps/sdlc/lib/util/modifynew enumeration module (name open) + tests: active-only and include-archived listings
apps/sdlc/lib/model/ops/audit.tsmodifydiscoverFiles/globMdTopLevel/rglobMd hoist to the helper; drop the schema_version: 0 non-drift branch
apps/sdlc/lib/model/ops/migrate.tsmodifyenumeration via the helper; closed-skip stays for non-archived entity types
apps/sdlc/lib/model/entities/task/ops/next.tsmodifyreaddir swap to the helper
apps/sdlc/lib/model/entities/task/ops/resolve.tsmodifyreaddir swap; archive handling per the file-resolution contract
apps/sdlc/lib/model/corpus/loader.tsmodifyunion-glob for wikilink/depends_on resolution
apps/sdlc/lib/services/project/ops/scan-candidates.tsmodifytask-linkage walk consumes the helper
apps/sdlc/lib/model/entities/task/ops/close-commit.tsmodifygit mv to archive/ inside the closing commit
apps/sdlc/lib/model/entities/task/ops/tests/modifyclose-commit, next, resolve test updates
apps/sdlc/skills/task-close-out/SKILL.mdmodifydocument the move step
apps/sdlc/skills/entities-audit/SKILL.mdmodifyarchive/closed flag language updates
apps/sdlc/skills/entities-migrate/SKILL.mdmodifyarchive/closed flag language updates
apps/sdlc/lib/model/entities/task/archive.mdnewconvention doc for the archive layout
apps/sdlc/lib/model/entities/task/file-resolution.mdmodifydrop the flat-dir assumption
apps/sdlc/conventions/schema-bump-checklist.mdmodifyadd the closed-entity question
docs/planning/tasks/T-*.mdmodifyone-time git mv of all closed tasks to archive/ (pure renames)
  • AC-1: The helper’s active-only listing returns exactly the set of *.md files directly under docs/planning/tasks/ whose path does not contain an archive/ 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 audit with default flags does not read any file under docs/planning/tasks/archive/ in the per-file pass. Verified by planting a known-malformed file at archive/<basename>.md and asserting audit returns zero drift entries against it.
  • AC-4: /sdlc:task-close-out produces a single commit containing one git mv (rename detected by git diff --find-renames) plus the closing frontmatter edit. Verified by a test fixture: an in-progress task, close-out runs, the file is at docs/planning/tasks/archive/<basename>.md, and git show --stat HEAD reports 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/*.md enumerated at pickup as a rename in git 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>.md shows 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.ts no longer contains the schema_version: 0 non-drift branch. Verified by grep for sentinel in the file returning zero matches.
  • AC-8: apps/sdlc/lib/model/entities/task/archive.md exists 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>.md resolves 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.md contains a question about closed-entity handling, phrased so the default answer for new task migrations is “nothing — archive is off the migration path”.
  • 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_version bump.
  • 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.

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.”


← Back to Tasks