fix: preserve Status/LastActivity/Progress in Current Position during begin-phase
cmdStateBeginPhase replaced the entire ## Current Position section with only Phase and Plan lines, destroying Status, Last activity, and Progress fields. cmdStateAdvancePlan then failed to update these fields since they no longer existed. Now begin-phase updates individual lines within Current Position instead of replacing the whole section. Also adds updateCurrentPositionFields helper so advance-plan keeps the Current Position body in sync with bold frontmatter fields. Fixes #1365 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -239,6 +239,32 @@ function stateReplaceFieldWithFallback(content, primary, fallback, value) {
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update fields within the ## Current Position section of STATE.md.
|
||||
* This keeps the Current Position body in sync with the bold frontmatter fields.
|
||||
* Only updates fields that already exist in the section; does not add new lines.
|
||||
* Fixes #1365: advance-plan could not update Status/Last activity after begin-phase.
|
||||
*/
|
||||
function updateCurrentPositionFields(content, fields) {
|
||||
const posPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
const posMatch = content.match(posPattern);
|
||||
if (!posMatch) return content;
|
||||
|
||||
let posBody = posMatch[2];
|
||||
|
||||
if (fields.status && /^Status:/m.test(posBody)) {
|
||||
posBody = posBody.replace(/^Status:.*$/m, `Status: ${fields.status}`);
|
||||
}
|
||||
if (fields.lastActivity && /^Last activity:/im.test(posBody)) {
|
||||
posBody = posBody.replace(/^Last activity:.*$/im, `Last activity: ${fields.lastActivity}`);
|
||||
}
|
||||
if (fields.plan && /^Plan:/m.test(posBody)) {
|
||||
posBody = posBody.replace(/^Plan:.*$/m, `Plan: ${fields.plan}`);
|
||||
}
|
||||
|
||||
return content.replace(posPattern, `${posMatch[1]}${posBody}`);
|
||||
}
|
||||
|
||||
function cmdStateAdvancePlan(cwd, raw) {
|
||||
const statePath = planningPaths(cwd).state;
|
||||
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
||||
@@ -273,19 +299,23 @@ function cmdStateAdvancePlan(cwd, raw) {
|
||||
if (currentPlan >= totalPlans) {
|
||||
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Phase complete — ready for verification');
|
||||
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
||||
content = updateCurrentPositionFields(content, { status: 'Phase complete — ready for verification', lastActivity: today });
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ advanced: false, reason: 'last_plan', current_plan: currentPlan, total_plans: totalPlans, status: 'ready_for_verification' }, raw, 'false');
|
||||
} else {
|
||||
const newPlan = currentPlan + 1;
|
||||
let planDisplayValue;
|
||||
if (useCompoundFormat) {
|
||||
// Preserve compound format: "X of Y in current phase" → replace X only
|
||||
const newPlanValue = planField.replace(/^\d+/, String(newPlan));
|
||||
content = stateReplaceField(content, 'Plan', newPlanValue) || content;
|
||||
planDisplayValue = planField.replace(/^\d+/, String(newPlan));
|
||||
content = stateReplaceField(content, 'Plan', planDisplayValue) || content;
|
||||
} else {
|
||||
planDisplayValue = `${newPlan} of ${totalPlans}`;
|
||||
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
|
||||
}
|
||||
content = stateReplaceFieldWithFallback(content, 'Status', null, 'Ready to execute');
|
||||
content = stateReplaceFieldWithFallback(content, 'Last Activity', 'Last activity', today);
|
||||
content = updateCurrentPositionFields(content, { status: 'Ready to execute', lastActivity: today, plan: planDisplayValue });
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
|
||||
}
|
||||
@@ -882,12 +912,44 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
||||
updated.push('Current focus');
|
||||
}
|
||||
|
||||
// Update ## Current Position section (#1104)
|
||||
// Update ## Current Position section (#1104, #1365)
|
||||
// Update individual fields within Current Position instead of replacing the
|
||||
// entire section, so that Status, Last activity, and Progress are preserved.
|
||||
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
const positionMatch = content.match(positionPattern);
|
||||
if (positionMatch) {
|
||||
const newPosition = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING\nPlan: 1 of ${planCount || '?'}\n`;
|
||||
content = content.replace(positionPattern, (_match, header) => `${header}${newPosition}`);
|
||||
const header = positionMatch[1];
|
||||
let posBody = positionMatch[2];
|
||||
|
||||
// Update or insert Phase line
|
||||
const newPhase = `Phase: ${phaseNumber}${phaseName ? ` (${phaseName})` : ''} — EXECUTING`;
|
||||
if (/^Phase:/m.test(posBody)) {
|
||||
posBody = posBody.replace(/^Phase:.*$/m, newPhase);
|
||||
} else {
|
||||
posBody = newPhase + '\n' + posBody;
|
||||
}
|
||||
|
||||
// Update or insert Plan line
|
||||
const newPlan = `Plan: 1 of ${planCount || '?'}`;
|
||||
if (/^Plan:/m.test(posBody)) {
|
||||
posBody = posBody.replace(/^Plan:.*$/m, newPlan);
|
||||
} else {
|
||||
posBody = posBody.replace(/^(Phase:.*$)/m, `$1\n${newPlan}`);
|
||||
}
|
||||
|
||||
// Update Status line if present
|
||||
const newStatus = `Status: Executing Phase ${phaseNumber}`;
|
||||
if (/^Status:/m.test(posBody)) {
|
||||
posBody = posBody.replace(/^Status:.*$/m, newStatus);
|
||||
}
|
||||
|
||||
// Update Last activity line if present
|
||||
const newActivity = `Last activity: ${today} -- Phase ${phaseNumber} execution started`;
|
||||
if (/^Last activity:/im.test(posBody)) {
|
||||
posBody = posBody.replace(/^Last activity:.*$/im, newActivity);
|
||||
}
|
||||
|
||||
content = content.replace(positionPattern, `${header}${posBody}`);
|
||||
updated.push('Current Position');
|
||||
}
|
||||
|
||||
|
||||
@@ -1482,6 +1482,130 @@ describe('milestone-scoped phase counting in frontmatter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// begin-phase — field preservation (#1365)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('state begin-phase preserves Current Position fields (#1365)', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('begin-phase preserves Status, Last activity, and Progress in Current Position', () => {
|
||||
const stateMd = `# Project State
|
||||
|
||||
**Current Phase:** 1
|
||||
**Current Phase Name:** setup
|
||||
**Total Phases:** 5
|
||||
**Current Plan:** 0
|
||||
**Total Plans in Phase:** 0
|
||||
**Status:** Ready to plan
|
||||
**Last Activity:** 2026-03-20
|
||||
**Last Activity Description:** Roadmap created
|
||||
|
||||
## Current Position
|
||||
Phase: 1 of 5 (setup)
|
||||
Plan: 0 of ? in current phase
|
||||
Status: Ready to plan
|
||||
Last activity: 2026-03-20 -- Roadmap created
|
||||
Progress: [..........] 0%
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Phase | Decision | Rationale |
|
||||
|-------|----------|-----------|
|
||||
`;
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd);
|
||||
|
||||
const result = runGsdTools(
|
||||
['state', 'begin-phase', '--phase', '1', '--name', 'setup', '--plans', '4'],
|
||||
tmpDir
|
||||
);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const content = fs.readFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'
|
||||
);
|
||||
|
||||
// Extract the Current Position section
|
||||
const posMatch = content.match(/## Current Position\s*\n([\s\S]*?)(?=\n##|$)/i);
|
||||
assert.ok(posMatch, 'Current Position section should exist');
|
||||
const posSection = posMatch[1];
|
||||
|
||||
// Phase and Plan lines should be updated
|
||||
assert.ok(/^Phase:.*EXECUTING/m.test(posSection), 'Phase line should say EXECUTING');
|
||||
assert.ok(/^Plan:.*1 of 4/m.test(posSection), 'Plan line should show 1 of 4');
|
||||
|
||||
// Status, Last activity, and Progress must still be present (the bug destroys these)
|
||||
assert.ok(/^Status:/m.test(posSection),
|
||||
'Status field must be preserved in Current Position');
|
||||
assert.ok(/^Last activity:/m.test(posSection),
|
||||
'Last activity field must be preserved in Current Position');
|
||||
assert.ok(/^Progress:/m.test(posSection),
|
||||
'Progress field must be preserved in Current Position');
|
||||
});
|
||||
|
||||
test('advance-plan can update Status after begin-phase', () => {
|
||||
// Simulates the full workflow: begin-phase then advance through all plans
|
||||
const stateMd = `# Project State
|
||||
|
||||
**Current Phase:** 1
|
||||
**Current Phase Name:** setup
|
||||
**Total Phases:** 5
|
||||
**Current Plan:** 0
|
||||
**Total Plans in Phase:** 0
|
||||
**Status:** Ready to plan
|
||||
**Last Activity:** 2026-03-20
|
||||
**Last Activity Description:** Roadmap created
|
||||
|
||||
## Current Position
|
||||
Phase: 1 of 5 (setup)
|
||||
Plan: 0 of ? in current phase
|
||||
Status: Ready to plan
|
||||
Last activity: 2026-03-20 -- Roadmap created
|
||||
Progress: [..........] 0%
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Phase | Decision | Rationale |
|
||||
|-------|----------|-----------|
|
||||
`;
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd);
|
||||
|
||||
// Step 1: begin-phase
|
||||
const beginResult = runGsdTools(
|
||||
['state', 'begin-phase', '--phase', '1', '--name', 'setup', '--plans', '2'],
|
||||
tmpDir
|
||||
);
|
||||
assert.ok(beginResult.success, `begin-phase failed: ${beginResult.error}`);
|
||||
|
||||
// Step 2: advance-plan to go from plan 1 to plan 2
|
||||
const adv1 = runGsdTools(['state', 'advance-plan'], tmpDir);
|
||||
assert.ok(adv1.success, `advance-plan 1 failed: ${adv1.error}`);
|
||||
|
||||
// Step 3: advance-plan again — plan 2 of 2 is the last, should set "Phase complete"
|
||||
const adv2 = runGsdTools(['state', 'advance-plan'], tmpDir);
|
||||
assert.ok(adv2.success, `advance-plan 2 failed: ${adv2.error}`);
|
||||
|
||||
const content = fs.readFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'
|
||||
);
|
||||
const posMatch = content.match(/## Current Position\s*\n([\s\S]*?)(?=\n##|$)/i);
|
||||
assert.ok(posMatch, 'Current Position section should exist after advance-plan');
|
||||
const posSection = posMatch[1];
|
||||
|
||||
// After advancing past all plans, Status should say "Phase complete"
|
||||
assert.ok(/Status:.*Phase complete/i.test(posSection),
|
||||
'Status should be updated to "Phase complete" after last advance-plan');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// summary-extract command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user