fix(#2237): fail loud on ambiguous bare-number phase directory collision (#2262)

* fix(#2237): fail loud on ambiguous bare-number phase directory collision

When two unrelated projects share a .planning/phases/ tree, a bare phase
number silently resolved to the first 0N-* directory found — risking
cross-project file writes. The fix detects multiple matches for the same
phase number and surfaces an ambiguous_matches result instead of silently
taking the first.

Changes:
- src/phase-locator.cts: searchPhaseInDir uses filter() + ambiguity check
- src/phase.cts: cmdFindPhase same pattern
- src/init.cts: cmdInitPhaseOp surfaces ambiguous_matches in the result
- tests/phase-locator.test.cjs: 3 regression tests

* docs: backfill changeset PR number (#2262)

* merge: keep up to date with next

* fix: regenerate stale capability-registry after next merge
This commit is contained in:
Tom Boucher
2026-07-14 15:17:14 -04:00
committed by GitHub
parent e35534c3ec
commit 5f609762ed
5 changed files with 103 additions and 4 deletions

View File

@@ -979,6 +979,22 @@ function cmdInitPhaseOp(cwd: string, phase: string, raw: boolean): void {
const config = loadConfig(cwd);
let phaseInfo = guardedFindPhase(cwd, phase, config.project_code);
// #2237: surface ambiguous phase-directory collisions instead of silently
// taking the first match when unrelated projects share a .planning/phases/ tree.
if (phaseInfo?.['ambiguous_matches']) {
const matches = phaseInfo['ambiguous_matches'] as string[];
const result: Record<string, unknown> = {
phase_found: false,
phase_dir: null,
phase_number: null,
phase_name: null,
ambiguous_matches: matches,
warning: `Phase ${phase} is ambiguous: ${matches.length} directories match (${matches.map((m: string) => `"${m}"`).join(', ')}). Set a distinct project_code in .planning/config.json to scope resolution.`,
};
output(withProjectRoot(cwd, result), raw);
return;
}
if (phaseInfo?.['archived']) {
const roadmapPhase = guardedGetRoadmapPhase(cwd, phase, config.project_code);
if (roadmapPhase?.['found']) {