Merge pull request #3118 from gsd-build/fix/3063-state-complete-phase-corrupts-state-md-b
fix: prevent state complete-phase from resolving literal 'Phase' token
This commit is contained in:
5
.changeset/pr-3118-release-note.md
Normal file
5
.changeset/pr-3118-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3118
|
||||
---
|
||||
Fixes for issue #3118 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
@@ -71,7 +71,8 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
|
||||
const { 'keep-recent': keepRecent, 'dry-run': dryRun } = parseNamedArgs(args, ['keep-recent'], ['dry-run']);
|
||||
state.cmdStatePrune(cwd, { keepRecent: keepRecent || '3', dryRun: !!dryRun }, raw);
|
||||
} else if (subcommand === 'complete-phase') {
|
||||
state.cmdStateCompletePhase(cwd, raw);
|
||||
const { phase: p } = parseNamedArgs(args, ['phase']);
|
||||
state.cmdStateCompletePhase(cwd, raw, p || args[2]);
|
||||
} else if (subcommand === 'milestone-switch') {
|
||||
const { milestone, name } = parseNamedArgs(args, ['milestone', 'name']);
|
||||
state.cmdStateMilestoneSwitch(cwd, milestone, name, raw);
|
||||
|
||||
@@ -1690,32 +1690,36 @@ function cmdStatePrune(cwd, options, raw) {
|
||||
* that the phase execution is finished and the project is ready for the next phase.
|
||||
* Implements the `gsd state complete-phase` subcommand (issue #2735).
|
||||
*/
|
||||
function cmdStateCompletePhase(cwd, raw) {
|
||||
function resolvePhaseIdForCompletePhase(content, overridePhase) {
|
||||
const candidate = overridePhase ||
|
||||
stateExtractField(content, 'Current Phase') ||
|
||||
stateExtractField(content, 'Phase') ||
|
||||
'';
|
||||
|
||||
// Accept canonical phase token only (e.g. 3, 03, 3A, 3.3, 10.2)
|
||||
const phaseMatch = String(candidate).match(/(\d+[A-Z]?(?:\.\d+)*)/i);
|
||||
return phaseMatch ? phaseMatch[1] : null;
|
||||
}
|
||||
|
||||
function cmdStateCompletePhase(cwd, raw, overridePhase) {
|
||||
const statePath = planningPaths(cwd).state;
|
||||
if (!fs.existsSync(statePath)) {
|
||||
output({ error: 'STATE.md not found' }, raw);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(statePath, 'utf-8');
|
||||
const resolvedPhase = resolvePhaseIdForCompletePhase(content, overridePhase);
|
||||
if (!resolvedPhase || /^phase$/i.test(resolvedPhase)) {
|
||||
output({ error: 'Unable to resolve current phase. Pass an explicit phase: state complete-phase --phase <N>' }, raw);
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const updated = [];
|
||||
let resolvedPhase = '?';
|
||||
|
||||
readModifyWriteStateMd(statePath, (content) => {
|
||||
// Read the current phase number for descriptive messages.
|
||||
//
|
||||
// The 'Phase' fallback can match the decorated body line under
|
||||
// `## Current Position` (e.g. `Phase: 01 (Foo) — EXECUTING`), which would
|
||||
// flow downstream into messy `Status: Phase 01 (Foo) — EXECUTING complete`
|
||||
// output. Strip everything past the leading numeric/decimal token so the
|
||||
// fallback path produces a clean phase identifier matching the canonical
|
||||
// `Current Phase` field. CodeRabbit nitpick on PR #2761.
|
||||
const rawPhase = stateExtractField(content, 'Current Phase') ||
|
||||
stateExtractField(content, 'Phase') ||
|
||||
'';
|
||||
const phaseToken = rawPhase.match(/^\s*([\w.-]+)/);
|
||||
const currentPhase = phaseToken ? phaseToken[1] : '?';
|
||||
resolvedPhase = currentPhase;
|
||||
const currentPhase = resolvedPhase;
|
||||
|
||||
// Update Status field
|
||||
const statusValue = `Phase ${currentPhase} complete`;
|
||||
|
||||
@@ -2471,6 +2471,61 @@ describe('state complete-phase: decorated Phase fallback (#2761 nitpick)', () =>
|
||||
`Status should reference canonical Current Phase (03), got: ${updated}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects unresolved literal Phase token and does not corrupt STATE.md (#3063)', () => {
|
||||
const stateMd = [
|
||||
'---',
|
||||
'milestone: v1.0',
|
||||
'---',
|
||||
'',
|
||||
'# State',
|
||||
'',
|
||||
'**Status:** Executing',
|
||||
'**Last Activity:** 2024-01-15',
|
||||
'',
|
||||
'## Current Position',
|
||||
'',
|
||||
'Phase: narrative only',
|
||||
'',
|
||||
].join('\n');
|
||||
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
|
||||
fs.writeFileSync(statePath, stateMd);
|
||||
|
||||
const result = runGsdTools('state complete-phase', tmpDir);
|
||||
assert.ok(result.success, 'command should return JSON error payload, not crash');
|
||||
const output = JSON.parse(result.output);
|
||||
assert.ok(output.error, 'expected clear resolution error');
|
||||
|
||||
const after = fs.readFileSync(statePath, 'utf-8');
|
||||
assert.ok(!after.includes('Phase: Phase — COMPLETE'));
|
||||
assert.ok(!after.includes('Status: Phase Phase complete'));
|
||||
});
|
||||
|
||||
test('supports explicit phase override for complete-phase disambiguation (#3063)', () => {
|
||||
const stateMd = [
|
||||
'---',
|
||||
'milestone: v1.0',
|
||||
'---',
|
||||
'',
|
||||
'# State',
|
||||
'',
|
||||
'**Status:** Executing',
|
||||
'**Last Activity:** 2024-01-15',
|
||||
'',
|
||||
'## Current Position',
|
||||
'',
|
||||
'Phase: narrative only',
|
||||
'',
|
||||
].join('\n');
|
||||
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
|
||||
fs.writeFileSync(statePath, stateMd);
|
||||
|
||||
const result = runGsdTools('state complete-phase --phase 3.3', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const after = fs.readFileSync(statePath, 'utf-8');
|
||||
assert.ok(after.includes('**Status:** Phase 3.3 complete'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user