fix(#4256): resolve todos from the root via todosDir everywhere (#4479)

* test(#4256): pin todos as root-scoped under workstreams (RED)

* fix(#4256): resolve todos from the root via todosDir everywhere

* chore(#4256): changeset fragment (pr number to backfill)

* chore(#4256): backfill PR number in changeset

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-07 08:24:14 -04:00
committed by GitHub
parent 6ebe6372ce
commit 0a0905705a
6 changed files with 410 additions and 17 deletions

View File

@@ -304,6 +304,7 @@ interface PlanningPaths {
requirements: string;
debug: string;
quick: string;
todos: string;
}
// #2142: the quick-task directory. Exported as its own function (not only as a
@@ -316,6 +317,33 @@ function quickDirFrom(planningBase: string): string {
return path.join(planningBase, 'quick');
}
// #4256: the todos directory — deliberately ROOT-SCOPED, unlike every other
// planningPaths key. Todos are shared project state by construction: the
// migrateToWorkstreams contract keeps them among the shared files that "stay
// in place" at .planning/todos/ (workstream.cts), and every workflow writer
// writes that literal cwd-relative root path. The six todos readers
// previously hand-composed `path.join(planningDir(cwd), 'todos', ...)`,
// which silently re-scoped to .planning/workstreams/<ws>/todos/ — a
// directory nothing creates — under a workstream, so todos went invisible
// and audit-open passed the milestone-close gate vacuously. Same
// two-composers-of-one-path shape the `debug` (#3149) and `quick` (#2142)
// keys were introduced to eliminate (DEFECT.GENERATIVE-FIX).
//
// Exported as its own function pair (not only as a `planningPaths` key)
// because `audit.cts`'s `scanTodos`/`cmdAuditAcknowledge` consume an
// already-resolved todos base rather than a `cwd`, mirroring how #2142
// exported `quickDirFrom` for `scanQuickTasks`. `todosDir` takes NO ws/project
// parameter — todos have no workstream- or project-scoped form anywhere, so
// there is no discriminator to thread. This is also the single root #4327's
// future filename-containment guard should enforce against.
function todosDirFrom(planningBase: string): string {
return path.join(planningBase, 'todos');
}
function todosDir(cwd: string): string {
return todosDirFrom(planningRoot(cwd));
}
function planningPaths(cwd: string, ws?: string | null): PlanningPaths {
const base = planningDir(cwd, ws);
return {
@@ -332,6 +360,11 @@ function planningPaths(cwd: string, ws?: string | null): PlanningPaths {
debug: path.join(base, 'debug'),
// #2142: quick-task directory, composed via the shared quickDirFrom helper.
quick: quickDirFrom(base),
// #4256: todos directory — deliberately ROOT-scoped while the rest of
// this record follows the active workstream/project (todos are shared
// project state per the migrateToWorkstreams contract), composed via the
// shared todosDir helper so this key and every direct caller agree.
todos: todosDir(cwd),
};
}
@@ -638,6 +671,8 @@ export = {
listAvailableWorkstreams,
planningPaths,
quickDirFrom,
todosDirFrom,
todosDir,
withPlanningLock,
getActiveWorkstream,
peekActiveWorkstream,