fix(#3865): init.* phase queries accept --phase <N> as the positional alias (#4054)

* test(#3865): init.* phase queries accept --phase as positional alias (failing first)

* fix(#3865): init.* phase queries accept --phase <N> as the positional alias

The phase-taking init.* queries read their phase token at args[2]
blindly: '--phase 60' made the literal '--phase' the phase, which the
locator can never resolve — the reported incident answered a well-formed
phase_found:false, plan_count:0 with exit 0 for a phase holding seven
committed plans (ADR-3473 §8.4's later strict validation turned the
paired form into a usage error instead, still not the alias).

normalizePhaseAlias (shared by all eight phase-taking handlers — the
issue's four plus phase-op, review, discuss-phase-assumptions, todos,
the same class) rewrites '--phase N'/'--phase=N' into the caller-owned
positional slot before flag parsing, so handlers and strict validation
see exactly the argv the positional form produces. A valueless --phase
is a usage error naming the flag. Any other flag-shaped args[2] now
resolves to undefined (the commands' designed use-the-current-phase
input) instead of passing flag text down as a phase name.

isFlagToken exported from command-arg-projection (single owner of the
flag-shape predicate).

* fix(#3865): review fold-ins — honest no-position-given comment, fail-closed throws

The helper's comment claimed flag-shaped args[2] resolves to 'the
commands' designed use-the-current-phase input' — no such cross-module
behavior exists: execute-phase/plan-phase/verify-work usage-error
'phase required', the find-based queries answer phase_found:false, and
todos drops its area filter. Reworded to what actually happens, so the
contract-grade comment cannot mislead a future edit. Adds the
parseNamedArgsOrExit-style throw after each error() call as a
fail-closed backstop against a returning fail().

* chore(#3865): changeset fragment (pr number backfilled after PR creation)

* chore(#3865): backfill changeset PR number (4054)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-29 15:22:34 -04:00
committed by GitHub
parent d9e906a744
commit 44ddc6dc46
4 changed files with 155 additions and 18 deletions

View File

@@ -918,6 +918,69 @@ describe('init commands', () => {
const output = JSON.parse(result.output);
assert.strictEqual(output.phase_found, true);
assert.strictEqual(output.phase_name, 'Details Block Regression');
});
// ─── #3865: --phase alias for the positional phase token ──────────────────
test('#3865: init execute-phase accepts --phase <N> as the positional alias', () => {
seedPhase(tmpDir, '03-api', { '03-01-PLAN.md': '# Plan' });
writePlanningDocs(tmpDir);
const result = runGsdTools(['init', 'execute-phase', '--phase', '03'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.phase_found, true, '--phase 03 must resolve the phase, not answer phase_found:false');
assert.strictEqual(output.plan_count, 1, 'the on-disk plan must be counted — the reported incident had 7 plans read as 0');
});
test('#3865: init execute-phase accepts the --phase=N form', () => {
seedPhase(tmpDir, '03-api', { '03-01-PLAN.md': '# Plan' });
writePlanningDocs(tmpDir);
const result = runGsdTools(['init', 'execute-phase', '--phase=03'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
assert.strictEqual(JSON.parse(result.output).phase_found, true);
});
test('#3865: the positional form still works (control)', () => {
seedPhase(tmpDir, '03-api', { '03-01-PLAN.md': '# Plan' });
writePlanningDocs(tmpDir);
const result = runGsdTools(['init', 'execute-phase', '03'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
assert.strictEqual(JSON.parse(result.output).phase_found, true);
});
test('#3865: init plan-phase accepts --phase <N>', () => {
seedPhase(tmpDir, '03-api', { '03-01-PLAN.md': '# Plan' });
writePlanningDocs(tmpDir);
const result = runGsdTools(['init', 'plan-phase', '--phase', '03'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
assert.strictEqual(JSON.parse(result.output).phase_found, true);
});
test('#3865: init verify-work accepts --phase <N>', () => {
seedPhase(tmpDir, '03-api', { '03-01-PLAN.md': '# Plan' });
writePlanningDocs(tmpDir);
const result = runGsdTools(['init', 'verify-work', '--phase', '03'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
assert.strictEqual(JSON.parse(result.output).phase_found, true);
});
test('#3865: init code-review accepts --phase <N>', () => {
seedPhase(tmpDir, '03-api', { '03-01-PLAN.md': '# Plan' });
writePlanningDocs(tmpDir);
const result = runGsdTools(['init', 'code-review', '--phase', '03'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
assert.strictEqual(JSON.parse(result.output).phase_found, true);
});
test('#3865: --phase with no value is a usage error, never a silent phase_found:false', () => {
seedPhase(tmpDir, '03-api', { '03-01-PLAN.md': '# Plan' });
writePlanningDocs(tmpDir);
const result = runGsdTools(['init', 'execute-phase', '--phase'], tmpDir);
assert.strictEqual(result.success, false, 'a valueless --phase must exit non-zero with a diagnostic');
assert.ok(
(result.error || '').includes('--phase'),
`the diagnostic must name the flag; got: ${result.error}`
);
});
});
@@ -4877,3 +4940,5 @@ describe('init — GSD_PROJECT scoping (#3964)', () => {
assert.equal(out['codebase_dir_exists'], true, 'unscoped probe of the root codebase dir');
});
});