diff --git a/.changeset/nimble-geese-zip.md b/.changeset/nimble-geese-zip.md new file mode 100644 index 000000000..0d61c0ed1 --- /dev/null +++ b/.changeset/nimble-geese-zip.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4543 +--- +**`/gsd-complete-milestone`'s safety commit and every `/gsd-init`-family command now correctly treat PROJECT.md as a file shared across workstreams, not a per-workstream file** — a #4455 follow-up regression (and one pre-existing, adjacent bug) resolved PROJECT.md through the workstream-scoped path instead of the documented shared root path, so under an active workstream the safety commit silently missed the real PROJECT.md and every init command's `project_title` field silently disappeared. diff --git a/src/init.cts b/src/init.cts index f6b9ee26f..733ecd12a 100644 --- a/src/init.cts +++ b/src/init.cts @@ -344,7 +344,15 @@ function withProjectRoot(cwd: string, result: Record): Record { const planningExists = fs.existsSync(dir); const requirementsExists = fs.existsSync(path.join(dir, 'REQUIREMENTS.md')); const milestonesExists = fs.existsSync(path.join(dir, 'MILESTONES.md')); + // #4455 follow-up (code-review finding): PROJECT.md is shared across + // workstreams (see cmdInitCompleteMilestone's projectPath comment for the + // full evidence) — checked at planningRoot(cwd), never the workstream-scoped + // `dir`, so a workstream whose own REQUIREMENTS/ROADMAP/STATE are all + // present isn't wrongly reported incomplete just because the shared + // PROJECT.md isn't ALSO duplicated under its own directory. const coreComplete = - fs.existsSync(path.join(dir, 'PROJECT.md')) && + fs.existsSync(path.join(planningDir(cwd, null), 'PROJECT.md')) && requirementsExists && fs.existsSync(path.join(dir, 'ROADMAP.md')) && fs.existsSync(path.join(dir, 'STATE.md')); @@ -1363,7 +1377,10 @@ function cmdInitNewProject(cwd: string, raw: boolean, options: Record = { - project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'PROJECT.md')))), + // #4455 follow-up (code-review finding): PROJECT.md is shared across + // workstreams — see cmdInitCompleteMilestone's projectPath comment for + // the full evidence. + project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd, null), 'PROJECT.md')))), planning_exists: fs.existsSync(planningRoot(cwd)), ...getInitGitState(cwd), // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase. The @@ -1625,7 +1650,8 @@ function cmdInitIngestDocs(cwd: string, raw: boolean): void { // hardcoded bare '.planning/intel/...', '.planning/PROJECT.md', etc. // literals into their Agent(prompt=...) blocks; those now interpolate // these fields instead. - project_path: toPosixPath(path.join(planningDir(cwd), 'PROJECT.md')), + // #4455 follow-up: PROJECT.md is shared across workstreams. + project_path: toPosixPath(path.join(planningDir(cwd, null), 'PROJECT.md')), requirements_path: toPosixPath(path.join(planningDir(cwd), 'REQUIREMENTS.md')), roadmap_path: toPosixPath(path.join(planningDir(cwd), 'ROADMAP.md')), state_path: toPosixPath(path.join(planningDir(cwd), 'STATE.md')), @@ -1672,13 +1698,17 @@ function cmdInitResume(cwd: string, raw: boolean): void { state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')), roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')), - project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'PROJECT.md')))), + // #4455 follow-up (code-review finding): PROJECT.md is shared across + // workstreams — see cmdInitCompleteMilestone's projectPath comment for + // the full evidence. + project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd, null), 'PROJECT.md')))), planning_exists: fs.existsSync(planningRoot(cwd)), // #2376: absolute — see comment on phase_dir in cmdInitExecutePhase. state_path: toPosixPath(path.join(planningDir(cwd), 'STATE.md')), roadmap_path: toPosixPath(path.join(planningDir(cwd), 'ROADMAP.md')), - project_path: toPosixPath(path.join(planningDir(cwd), 'PROJECT.md')), + // #4455 follow-up: PROJECT.md is shared across workstreams. + project_path: toPosixPath(path.join(planningDir(cwd, null), 'PROJECT.md')), has_interrupted_agent: !!interruptedAgentId, interrupted_agent_id: interruptedAgentId, @@ -2594,7 +2624,10 @@ function cmdInitMilestoneOp(cwd: string, raw: boolean): void { archived_milestones: archivedMilestones, archive_count: archivedMilestones.length, - project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'PROJECT.md')))), + // #4455 follow-up (code-review finding): PROJECT.md is shared across + // workstreams — see cmdInitCompleteMilestone's projectPath comment for + // the full evidence. + project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd, null), 'PROJECT.md')))), roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')), state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')), archive_exists: fs.existsSync(path.join(planningRoot(cwd), 'archive')), @@ -3039,7 +3072,10 @@ function cmdInitManager(cwd: string, raw: boolean): void { waiting_signal: waitingSignal, all_complete: completedCount === nonBacklogPhases.length && nonBacklogPhases.length > 0, - project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'PROJECT.md')))), + // #4455 follow-up (code-review finding): PROJECT.md is shared across + // workstreams — see cmdInitCompleteMilestone's projectPath comment for + // the full evidence. + project_exists: pathExistsInternal(cwd, toPosixPath(path.relative(cwd, path.join(planningDir(cwd, null), 'PROJECT.md')))), roadmap_exists: true, state_exists: true, manager_flags: managerFlags, @@ -3086,16 +3122,35 @@ function cmdInitCompleteMilestone( const statePath = path.join(planningBase, 'STATE.md'); const roadmapPath = path.join(planningBase, 'ROADMAP.md'); const archiveDir = path.join(planningBase, 'milestones'); - // #4455 follow-up (code-review finding): MILESTONES.md and PROJECT.md are - // workstream-scoped too — cmdMilestoneComplete (src/milestone.cts) writes - // MILESTONES.md via planningPaths(cwd).planning (the workstream base, not - // root), and planningPaths().project resolves PROJECT.md the same way. - // Neither is the deliberately-root-scoped exception `todos` is (#4256) — - // an earlier version of this fix wrongly treated both as shared root - // files, which would have made the safety commit below silently miss the - // actual files milestone.complete just wrote under an active workstream. + // #4455 follow-up (code-review finding): MILESTONES.md is workstream-scoped + // too — cmdMilestoneComplete (src/milestone.cts) writes it via + // planningPaths(cwd).planning (the workstream base, not root; #1911). It is + // not the deliberately-root-scoped exception `todos` is (#4256) — an + // earlier version of this fix wrongly treated it as a shared root file, + // which would have made the safety commit below silently miss the actual + // file milestone.complete just wrote under an active workstream. const milestonesPath = path.join(planningBase, 'MILESTONES.md'); - const projectPath = path.join(planningBase, 'PROJECT.md'); + // #4455 follow-up round 2 (self-discovered regression): PROJECT.md, unlike + // MILESTONES.md, is genuinely SHARED across a project's own workstreams — + // never cloned per workstream. gsd-core/references/workstream-flag.md's + // directory diagram marks it `# Shared`; new-milestone.md states it + // outright ("PROJECT.md is shared across workstreams") and explicitly + // SKIPS writing its `## Current Milestone` heading under an active + // workstream specifically to avoid clobbering the one shared file (#2308); + // cmdWorkstreamCreate (src/workstream.cts) never creates a PROJECT.md + // under a workstream directory. The first version of this #4455 follow-up + // wrongly generalized from planningPaths()'s structural shape (which + // composes `project` under the workstream base) without checking an + // actual PROJECT.md write path — resolved against planningRoot(cwd) + // (round 2), but that ALSO ignores the separate GSD_PROJECT dimension + // (multi-project namespacing, #3749: PROJECT.md legitimately lives at + // `.planning//PROJECT.md` when GSD_PROJECT is set — a real, + // tested, pre-existing feature planningRoot's blanket root-only read + // broke), caught by gsd-test on this fix's own first push. `planningDir` + // with `ws` explicitly nulled (never read from GSD_WORKSTREAM) but + // `project` left to default from GSD_PROJECT is the correct middle + // ground: respects project-namespacing, ignores workstream-namespacing. + const projectPath = path.join(planningDir(cwd, null), 'PROJECT.md'); // REQUIREMENTS.md is workstream-scoped the same way (planningPaths(cwd).requirements, // src/planning-workspace.cts) — the git-rm-after-archive step needs the // resolved path too, not the literal root file. @@ -3595,7 +3650,10 @@ function cmdInitProgress(cwd: string, raw: boolean, options: Record { + let tmpDir; + + beforeEach(() => { + tmpDir = require('fs').realpathSync(createTempProject()); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + function writeRootProjectMd(dir) { + fs.writeFileSync(path.join(dir, '.planning', 'PROJECT.md'), '# Test Project\n'); + } + + test('flat mode: milestones_path/requirements_path resolve to root, project_path resolves to root (regression guard)', () => { + writeState(tmpDir); + writeRoadmap(tmpDir, [{ number: '1', name: 'Setup' }]); + writeRootProjectMd(tmpDir); + fs.writeFileSync(path.join(tmpDir, '.planning', 'MILESTONES.md'), '# Milestones\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements\n'); + + const result = runGsdTools('init complete-milestone', tmpDir, { GSD_WORKSTREAM: '' }); + assert.ok(result.success, `Command failed: ${result.error}`); + const output = JSON.parse(result.output); + + assert.strictEqual(output.milestones_path, absPlanningPath(tmpDir, 'MILESTONES.md')); + assert.strictEqual(output.project_path, absPlanningPath(tmpDir, 'PROJECT.md')); + assert.strictEqual(output.requirements_path, absPlanningPath(tmpDir, 'REQUIREMENTS.md')); + }); + + test('GSD_WORKSTREAM=alpha: milestones_path/requirements_path resolve into the workstream, but project_path STAYS root (PROJECT.md is shared, #4455 follow-up regression)', () => { + seedWorkstream(tmpDir, { + name: 'alpha', + state: '---\nstatus: active\n---\n# State\n', + roadmap: '# Roadmap\n\n## Progress\n\n- [ ] **Phase 1: Setup**\n\n### Phase 1: Setup\n\n**Goal:** Bootstrap\n', + }); + // PROJECT.md is only ever written at root — cmdWorkstreamCreate never + // creates a per-workstream copy (it is a documented shared file, see + // gsd-core/references/workstream-flag.md's directory diagram). + writeRootProjectMd(tmpDir); + fs.mkdirSync(path.join(tmpDir, '.planning', 'workstreams', 'alpha'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'workstreams', 'alpha', 'MILESTONES.md'), '# Milestones\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'workstreams', 'alpha', 'REQUIREMENTS.md'), '# Requirements\n'); + + const result = runGsdTools('init complete-milestone', tmpDir, { GSD_WORKSTREAM: 'alpha' }); + assert.ok(result.success, `Command failed: ${result.error}`); + const output = JSON.parse(result.output); + + assert.strictEqual(output.milestones_path, absPlanningPath(tmpDir, 'workstreams', 'alpha', 'MILESTONES.md')); + assert.strictEqual(output.requirements_path, absPlanningPath(tmpDir, 'workstreams', 'alpha', 'REQUIREMENTS.md')); + // The regression this test guards against: project_path must be the + // ROOT PROJECT.md, never a workstream-scoped path that no writer ever + // populates. + assert.strictEqual(output.project_path, absPlanningPath(tmpDir, 'PROJECT.md')); + assert.notStrictEqual(output.project_path, absPlanningPath(tmpDir, 'workstreams', 'alpha', 'PROJECT.md')); + }); + + test('GSD_PROJECT=second-product: project_path resolves into the PROJECT namespace, not root (#3749 regression — round 1 of this fix broke this)', () => { + // The FIRST version of this #4455 follow-up resolved project_path via + // planningRoot(cwd), which ignores GSD_PROJECT entirely — gsd-test caught + // this immediately (tests/init.test.cjs's pre-existing #3749 coverage) on + // this fix's own first push. PROJECT.md is shared across a project's own + // WORKSTREAMS, but a DIFFERENT project (GSD_PROJECT) legitimately gets + // its own separate PROJECT.md at `.planning//PROJECT.md`. The + // correct resolution is planningDir(cwd, null) — `ws` explicitly nulled + // (never read from GSD_WORKSTREAM), `project` left to default from + // GSD_PROJECT. + fs.mkdirSync(path.join(tmpDir, '.planning', 'second-product'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'second-product', 'PROJECT.md'), '# Second Product\n'); + writeRootProjectMd(tmpDir); // an unrelated root PROJECT.md must not win + + const result = runGsdTools('init complete-milestone', tmpDir, { GSD_PROJECT: 'second-product' }); + assert.ok(result.success, `Command failed: ${result.error}`); + const output = JSON.parse(result.output); + + assert.strictEqual(output.project_path, absPlanningPath(tmpDir, 'second-product', 'PROJECT.md')); + assert.notStrictEqual(output.project_path, absPlanningPath(tmpDir, 'PROJECT.md')); + }); + + test('GSD_PROJECT=second-product AND GSD_WORKSTREAM=alpha together: project_path follows the PROJECT namespace, ignoring the workstream', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'second-product', 'workstreams', 'alpha'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'second-product', 'PROJECT.md'), '# Second Product\n'); + + const result = runGsdTools('init complete-milestone', tmpDir, { GSD_PROJECT: 'second-product', GSD_WORKSTREAM: 'alpha' }); + assert.ok(result.success, `Command failed: ${result.error}`); + const output = JSON.parse(result.output); + + assert.strictEqual(output.project_path, absPlanningPath(tmpDir, 'second-product', 'PROJECT.md')); + assert.notStrictEqual(output.project_path, + absPlanningPath(tmpDir, 'second-product', 'workstreams', 'alpha', 'PROJECT.md')); + }); +}); + +describe('withProjectRoot — project_title reads PROJECT.md from root even under a workstream (#4455 follow-up)', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = require('fs').realpathSync(createTempProject()); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + // Exercised via `init complete-milestone` rather than `init manager`: + // cmdInitManager has its own readiness guard requiring STATE.md/ROADMAP.md + // to already exist (see the "state_path/roadmap_path are null" test + // above), which would obscure whether THIS test is actually exercising + // withProjectRoot. Both commands share the same withProjectRoot helper. + test('flat mode: project_title is populated from the root PROJECT.md (regression guard)', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# My Test Project\n\nSome content.\n'); + + const result = runGsdTools('init complete-milestone', tmpDir, { GSD_WORKSTREAM: '' }); + assert.ok(result.success, `Command failed: ${result.error}`); + const output = JSON.parse(result.output); + assert.strictEqual(output.project_title, 'My Test Project'); + }); + + test('GSD_WORKSTREAM=alpha: project_title is STILL populated from the root PROJECT.md, not silently dropped (#4455 follow-up regression)', () => { + seedWorkstream(tmpDir, { + name: 'alpha', + state: '---\nstatus: active\n---\n# State\n', + roadmap: '# Roadmap\n\n## Progress\n\n- [ ] **Phase 1: Setup**\n\n### Phase 1: Setup\n\n**Goal:** Bootstrap\n', + }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# My Test Project\n\nSome content.\n'); + + const result = runGsdTools('init complete-milestone', tmpDir, { GSD_WORKSTREAM: 'alpha' }); + assert.ok(result.success, `Command failed: ${result.error}`); + const output = JSON.parse(result.output); + assert.strictEqual(output.project_title, 'My Test Project', + 'project_title must be read from the shared root PROJECT.md even when a workstream is active'); + }); +}); + +describe('init subcommands sharing the project_exists/project_path PROJECT.md pattern (#4455 follow-up)', () => { + // A code-review pass on this fix's first draft found the identical + // workstream-scoped-PROJECT.md bug (fixed above for cmdInitCompleteMilestone + // and withProjectRoot) repeated verbatim, via grep, in six more cmdInit* + // functions. Each is exercised here through its real CLI subcommand rather + // than re-asserting src/init.cts internals directly, so a regression in the + // router wiring would also be caught. `init manager` and `init new-milestone` + // are deliberately excluded: `manager` has its own STATE.md/ROADMAP.md + // readiness guard (covered separately above via `init complete-milestone`, + // which shares withProjectRoot); `new-milestone`'s project_path fix is + // real (see src/init.cts) but its dedicated coverage belongs with #4456's + // own new-milestone.md workstream-forwarding work, not duplicated here. + const SUBCOMMANDS = ['ingest-docs', 'resume', 'progress', 'new-project']; + + let tmpDir; + + beforeEach(() => { + tmpDir = require('fs').realpathSync(createTempProject()); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + for (const subcommand of SUBCOMMANDS) { + test(`GSD_WORKSTREAM=alpha: "init ${subcommand}" resolves project_path to the shared root PROJECT.md`, () => { + seedWorkstream(tmpDir, { + name: 'alpha', + state: '---\nstatus: active\n---\n# State\n', + roadmap: '# Roadmap\n\n## Progress\n\n- [ ] **Phase 1: Setup**\n\n### Phase 1: Setup\n\n**Goal:** Bootstrap\n', + }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Test Project\n'); + + const result = runGsdTools(`init ${subcommand}`, tmpDir, { GSD_WORKSTREAM: 'alpha' }); + assert.ok(result.success, `init ${subcommand} failed: ${result.error}`); + const output = JSON.parse(result.output); + + assert.strictEqual(output.project_exists, true, + `init ${subcommand}: project_exists must be true (root PROJECT.md exists), got: ${JSON.stringify(output.project_exists)}`); + if ('project_path' in output) { + assert.strictEqual(output.project_path, absPlanningPath(tmpDir, 'PROJECT.md'), + `init ${subcommand}: project_path must resolve to the shared root PROJECT.md, got: ${output.project_path}`); + assert.notStrictEqual(output.project_path, absPlanningPath(tmpDir, 'workstreams', 'alpha', 'PROJECT.md'), + `init ${subcommand}: project_path must not resolve into the workstream directory`); + } + }); + } + + test('milestone-op: GSD_WORKSTREAM=alpha does not report project_exists=false for a root-only PROJECT.md', () => { + // milestone-op does not expose a project_path field, only project_exists + // via buildInitCompletenessFields — the coreComplete/init_incomplete fix. + seedWorkstream(tmpDir, { + name: 'alpha', + state: '---\nstatus: active\n---\n# State\n', + roadmap: '# Roadmap\n\n## Progress\n\n- [ ] **Phase 1: Setup**\n\n### Phase 1: Setup\n\n**Goal:** Bootstrap\n', + }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Test Project\n'); + + const result = runGsdTools('init milestone-op', tmpDir, { GSD_WORKSTREAM: 'alpha' }); + assert.ok(result.success, `init milestone-op failed: ${result.error}`); + const output = JSON.parse(result.output); + assert.strictEqual(output.project_exists, true, + `init milestone-op: project_exists must be true for a root-only PROJECT.md under a workstream, got: ${JSON.stringify(output.project_exists)}`); + }); +}); + // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-3584-runtime-slash-emitters.test.cjs — consolidation epic #1969 (B2 #1971) diff --git a/tests/workstream-scoped-paths.test.cjs b/tests/workstream-scoped-paths.test.cjs index 419a75daa..00eb57216 100644 --- a/tests/workstream-scoped-paths.test.cjs +++ b/tests/workstream-scoped-paths.test.cjs @@ -324,37 +324,42 @@ describe('complete-milestone.md workstream-scoped paths (#4455)', () => { assert.ok(out.includes(projectPath), `expected root PROJECT.md in --files, got: ${out}`); }); - test('GSD_WORKSTREAM=alpha: --files lists workstream-scoped STATE/ROADMAP/archive/MILESTONES/PROJECT paths, not root (#4455 regression)', () => { + test('GSD_WORKSTREAM=alpha: --files lists workstream-scoped STATE/ROADMAP/archive/MILESTONES paths, but PROJECT.md STAYS root (#4455 regression)', () => { const wsStatePath = path.join(tmpDir, 'STATE-alpha.md'); const wsRoadmapPath = path.join(tmpDir, 'ROADMAP-alpha.md'); const wsArchiveDir = path.join(tmpDir, 'milestones-alpha'); const wsMilestonesPath = path.join(tmpDir, 'MILESTONES-alpha.md'); - const wsProjectPath = path.join(tmpDir, 'PROJECT-alpha.md'); + // PROJECT.md is genuinely SHARED across workstreams — never cloned per + // workstream (gsd-core/references/workstream-flag.md marks it + // `# Shared`; new-milestone.md states it outright; cmdWorkstreamCreate + // never creates one under a workstream dir). The real + // cmdInitCompleteMilestone therefore ALWAYS returns the root path for + // project_path regardless of GSD_WORKSTREAM — reflected here by + // passing the SAME root-shaped value the flat-mode test above uses, + // not a workstream-shaped one. An earlier version of this fix wrongly + // pinned MILESTONES.md and PROJECT.md as identically workstream-scoped + // (caught by isolated code review, then re-caught as a genuine + // regression after merge — see tests/init-manager.test.cjs's + // "milestones_path/project_path/requirements_path" describe block for + // the test that exercises the real init.cts function, not just this + // fence-mechanics stub). + const rootProjectPath = path.join(tmpDir, 'PROJECT-root.md'); const out = runCommitFence({ state_path: wsStatePath, roadmap_path: wsRoadmapPath, archive_dir: wsArchiveDir, - milestones_path: wsMilestonesPath, project_path: wsProjectPath, + milestones_path: wsMilestonesPath, project_path: rootProjectPath, }); assert.ok(out.includes(wsStatePath), `expected workstream STATE.md in --files, got: ${out}`); assert.ok(out.includes(wsRoadmapPath), `expected workstream ROADMAP.md in --files, got: ${out}`); assert.ok(out.includes(`${wsArchiveDir}/v[X.Y]-ROADMAP.md`), `expected workstream archive ROADMAP in --files, got: ${out}`); - // MILESTONES.md and PROJECT.md are workstream-scoped too — cmdMilestoneComplete - // (src/milestone.cts) writes MILESTONES.md via planningPaths(cwd).planning (the - // workstream base), and PROJECT.md resolves the same way (planningPaths().project). - // An earlier version of this fix wrongly pinned both as shared root files, which - // would have made this safety commit silently miss the actual files - // `milestone complete` just wrote under an active workstream (#4455 follow-up, - // caught by isolated code review). assert.ok(out.includes(wsMilestonesPath), `expected workstream MILESTONES.md in --files, got: ${out}`); - assert.ok(out.includes(wsProjectPath), `expected workstream PROJECT.md in --files, got: ${out}`); + assert.ok(out.includes(rootProjectPath), `expected root PROJECT.md in --files, got: ${out}`); assert.ok(!out.includes(path.join(tmpDir, '.planning', 'STATE.md')), `must not fall back to the flat root STATE.md path, got: ${out}`); assert.ok(!out.includes(path.join(tmpDir, '.planning', 'ROADMAP.md')), `must not fall back to the flat root ROADMAP.md path, got: ${out}`); assert.ok(!out.includes(path.join(tmpDir, '.planning', 'MILESTONES.md')), `must not fall back to the flat root MILESTONES.md path, got: ${out}`); - assert.ok(!out.includes(path.join(tmpDir, '.planning', 'PROJECT.md')), - `must not fall back to the flat root PROJECT.md path, got: ${out}`); }); });