fix(3153): handle numeric 100 percent and block-list next_phases

This commit is contained in:
Tom Boucher
2026-05-05 18:34:42 -04:00
parent 8adfe4de01
commit dbc09d21a6
3 changed files with 44 additions and 7 deletions

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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({}), '');
});