From dbc09d21a65373870c959b8cd27dd8aae70927c5 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 5 May 2026 18:34:42 -0400 Subject: [PATCH] fix(3153): handle numeric 100 percent and block-list next_phases --- ...fix-3153-statusline-percent-next-phases.md | 5 ++++ hooks/gsd-statusline.js | 23 +++++++++++++------ tests/gsd-statusline.test.cjs | 23 +++++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 .changeset/fix-3153-statusline-percent-next-phases.md diff --git a/.changeset/fix-3153-statusline-percent-next-phases.md b/.changeset/fix-3153-statusline-percent-next-phases.md new file mode 100644 index 000000000..ed0406f1c --- /dev/null +++ b/.changeset/fix-3153-statusline-percent-next-phases.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3153 +--- +**Statusline state rendering is now type-robust and YAML-list compatible** — milestone completion now renders for numeric and string `percent` values, and `next_phases` parsing supports both flow-array and block-list YAML forms. \ No newline at end of file diff --git a/hooks/gsd-statusline.js b/hooks/gsd-statusline.js index 910a9ec1a..ee8ec9f87 100755 --- a/hooks/gsd-statusline.js +++ b/hooks/gsd-statusline.js @@ -157,13 +157,22 @@ function parseStateMd(content) { // next_action: recommended command when idle (discuss-phase / plan-phase / execute-phase / verify-phase) if (key === 'next_action') state.nextAction = (v === 'null' || v === '') ? null : v; } - // next_phases YAML flow array: ["4.5", "4.6"] — single-line flow only - // Block sequences (- 4.5 / - 4.6 over multiple lines) are intentionally - // not parsed here; statusline only needs the primary recommendation. - const npMatch = fm.match(/^next_phases:\s*\[([^\]]*)\]/m); - if (npMatch) { - const items = npMatch[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean); + // next_phases supports both flow array and block-list YAML forms. + const npFlowMatch = fm.match(/^next_phases:\s*\[([^\]]*)\]/m); + if (npFlowMatch) { + const items = npFlowMatch[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean); state.nextPhases = items.length > 0 ? items : null; + } else { + const npBlockMatch = fm.match(/^next_phases:\s*\n((?:[ \t]*-[ \t]*[^\n]+\n?)*)/m); + if (npBlockMatch) { + const items = npBlockMatch[1] + .split('\n') + .map(line => line.match(/^[ \t]*-[ \t]*(.+)$/)) + .filter(Boolean) + .map(m => m[1].trim().replace(/^["']|["']$/g, '')) + .filter(Boolean); + state.nextPhases = items.length > 0 ? items : null; + } } // progress nested block: completed_phases / total_phases / percent (2-space indent) const progMatch = fm.match(/^progress:\s*\n((?:[ \t]+\w+:.+\n?)+)/m); @@ -256,7 +265,7 @@ function formatGsdState(s) { // Scene 2: idle + a recommended next command is visible to the user. // Surfaces "what to run next" without the user opening STATE.md. parts.push(`next ${s.nextAction} ${phasesStr}`); - } else if (s.percent === '100' || (s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases)) { + } else if (Number(s.percent) === 100 || (s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases)) { // Scene 3: milestone complete (every phase done). parts.push('milestone complete'); } else { diff --git a/tests/gsd-statusline.test.cjs b/tests/gsd-statusline.test.cjs index 070e27134..2335a6f51 100644 --- a/tests/gsd-statusline.test.cjs +++ b/tests/gsd-statusline.test.cjs @@ -125,6 +125,21 @@ describe('parseStateMd', () => { assert.equal(s.status, undefined); assert.equal(s.phaseNum, undefined); }); + + test('parses next_phases from YAML block-list form (#3153)', () => { + const content = [ + '---', + 'next_action: execute', + 'next_phases:', + ' - 4.5', + ' - 4.6', + '---', + ].join('\n'); + + const s = parseStateMd(content); + assert.equal(s.nextAction, 'execute'); + assert.deepEqual(s.nextPhases, ['4.5', '4.6']); + }); }); // ─── formatGsdState ───────────────────────────────────────────────────────── @@ -185,6 +200,14 @@ describe('formatGsdState', () => { assert.equal(out, 'Foundations · planning'); }); + test('treats numeric 100 percent as milestone complete (#3153)', () => { + const out = formatGsdState({ + milestone: 'v2.0', + percent: 100, + }); + assert.equal(out, 'v2.0 [██████████] 100% · milestone complete'); + }); + test('returns empty string for empty state', () => { assert.equal(formatGsdState({}), ''); });