Merge pull request #947 from j2h4u/fix/state-advance-plan-compound
fix(state): parse compound Plan field in advance-plan command
This commit is contained in:
@@ -206,25 +206,55 @@ function cmdStateAdvancePlan(cwd, raw) {
|
||||
if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
|
||||
|
||||
let content = fs.readFileSync(statePath, 'utf-8');
|
||||
const currentPlan = parseInt(stateExtractField(content, 'Current Plan'), 10);
|
||||
const totalPlans = parseInt(stateExtractField(content, 'Total Plans in Phase'), 10);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Try legacy separate fields first, then compound "Plan: X of Y" format
|
||||
const legacyPlan = stateExtractField(content, 'Current Plan');
|
||||
const legacyTotal = stateExtractField(content, 'Total Plans in Phase');
|
||||
const planField = stateExtractField(content, 'Plan');
|
||||
|
||||
let currentPlan, totalPlans;
|
||||
let useCompoundFormat = false;
|
||||
|
||||
if (legacyPlan && legacyTotal) {
|
||||
currentPlan = parseInt(legacyPlan, 10);
|
||||
totalPlans = parseInt(legacyTotal, 10);
|
||||
} else if (planField) {
|
||||
// Compound format: "2 of 6 in current phase" or "2 of 6"
|
||||
currentPlan = parseInt(planField, 10);
|
||||
const ofMatch = planField.match(/of\s+(\d+)/);
|
||||
totalPlans = ofMatch ? parseInt(ofMatch[1], 10) : NaN;
|
||||
useCompoundFormat = true;
|
||||
}
|
||||
|
||||
if (isNaN(currentPlan) || isNaN(totalPlans)) {
|
||||
output({ error: 'Cannot parse Current Plan or Total Plans in Phase from STATE.md' }, raw);
|
||||
return;
|
||||
}
|
||||
|
||||
const replaceField = (c, primary, fallback, value) => {
|
||||
let r = stateReplaceField(c, primary, value);
|
||||
if (r) return r;
|
||||
if (fallback) { r = stateReplaceField(c, fallback, value); if (r) return r; }
|
||||
return c;
|
||||
};
|
||||
|
||||
if (currentPlan >= totalPlans) {
|
||||
content = stateReplaceField(content, 'Status', 'Phase complete — ready for verification') || content;
|
||||
content = stateReplaceField(content, 'Last Activity', today) || content;
|
||||
content = replaceField(content, 'Status', null, 'Phase complete — ready for verification');
|
||||
content = replaceField(content, 'Last Activity', 'Last activity', 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;
|
||||
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
|
||||
content = stateReplaceField(content, 'Status', 'Ready to execute') || content;
|
||||
content = stateReplaceField(content, 'Last Activity', today) || content;
|
||||
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;
|
||||
} else {
|
||||
content = stateReplaceField(content, 'Current Plan', String(newPlan)) || content;
|
||||
}
|
||||
content = replaceField(content, 'Status', null, 'Ready to execute');
|
||||
content = replaceField(content, 'Last Activity', 'Last activity', today);
|
||||
writeStateMd(statePath, content, cwd);
|
||||
output({ advanced: true, previous_plan: currentPlan, current_plan: newPlan, total_plans: totalPlans }, raw, 'true');
|
||||
}
|
||||
|
||||
@@ -892,6 +892,45 @@ describe('cmdStateAdvancePlan (state advance-plan)', () => {
|
||||
assert.ok(output.error !== undefined, 'output should have error field');
|
||||
assert.ok(output.error.toLowerCase().includes('cannot parse'), 'error should mention Cannot parse');
|
||||
});
|
||||
|
||||
test('advances plan in compound "Plan: X of Y" format', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# Project State\n\nPlan: 2 of 5 in current phase\nStatus: In progress\nLast activity: 2025-01-01\n`
|
||||
);
|
||||
|
||||
const result = runGsdTools('state advance-plan', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.advanced, true, 'advanced should be true');
|
||||
assert.strictEqual(output.previous_plan, 2);
|
||||
assert.strictEqual(output.current_plan, 3);
|
||||
assert.strictEqual(output.total_plans, 5);
|
||||
|
||||
const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.ok(updated.includes('Plan: 3 of 5 in current phase'),
|
||||
'should preserve compound format with updated plan number');
|
||||
assert.ok(updated.includes('Status: Ready to execute'),
|
||||
'Status should be updated');
|
||||
});
|
||||
|
||||
test('marks phase complete on last plan in compound format', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
`# Project State\n\nPlan: 3 of 3 in current phase\nStatus: In progress\nLast activity: 2025-01-01\n`
|
||||
);
|
||||
|
||||
const result = runGsdTools('state advance-plan', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.advanced, false);
|
||||
assert.strictEqual(output.reason, 'last_plan');
|
||||
|
||||
const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.ok(updated.includes('Phase complete'), 'Status should contain Phase complete');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cmdStateRecordMetric (state record-metric)', () => {
|
||||
|
||||
Reference in New Issue
Block a user