From 85a65fd384885baf96f8ba498762f327d169d9a7 Mon Sep 17 00:00:00 2001 From: j2h4u <39818683+j2h4u@users.noreply.github.com> Date: Thu, 5 Mar 2026 02:58:49 +0500 Subject: [PATCH] fix(state): parse compound Plan field in advance-plan command `cmdStateAdvancePlan` expected separate `Current Plan` and `Total Plans in Phase` fields, but the current STATE.md template uses a single compound field: `Plan: X of Y in current phase`. Now tries legacy separate fields first, then falls back to parsing the compound format. Preserves the compound format when writing back (replaces only the plan number). Also handles `Last activity` (lowercase) field name from current template. Co-Authored-By: Claude Opus 4.6 --- get-shit-done/bin/lib/state.cjs | 44 +++++++++++++++++++++++++++------ tests/state.test.cjs | 39 +++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 0d7387855..3a91006b9 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -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'); } diff --git a/tests/state.test.cjs b/tests/state.test.cjs index 14ea38982..fe3b84670 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -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)', () => {