feat(#2833): phase-lifecycle status-line — read-side (parseStateMd + formatGsdState scenes + tests + docs) (#2884)
* feat(#2833): parseStateMd reads phase-lifecycle frontmatter fields Extend parseStateMd() to parse 4 new STATE.md frontmatter fields that drive the phase-lifecycle status-line proposed in #2833: - active_phase : phase number when orchestrator is in-flight, null when idle - next_action : recommended next command when idle - next_phases : YAML flow array of phase numbers for next_action - progress : nested block with completed_phases / total_phases / percent All fields default to undefined when absent — formatGsdState() (next commit) degrades gracefully so existing STATE.md files keep rendering as before. YAML scope intentionally narrow: - Only top-level scalar keys (status, milestone, active_phase, next_action) - Only single-line flow array for next_phases ([...]) - progress block requires 2-space indent for nested keys Block sequences (- item over multiple lines) and inline comments inside nested blocks are NOT parsed — keeping the regex-based parser predictable. Comments outside frontmatter or after the closing --- still work. Tests: all 27 existing tests still pass (no behavior change for STATE.md files that don't carry the new fields). Refs #2833 * feat(#2833): formatGsdState renders phase-lifecycle scenes + opt-in progress bar Extend formatGsdState() with three lifecycle scenes that activate when the new STATE.md frontmatter fields (added in the previous commit) are present. Also append an opt-in progress bar to the milestone segment when progress.percent is available. Scenes (first match wins; falls through to the existing path otherwise): 1. active_phase set → 'v2.0 [██░] X% · Phase 4.5 executing' (status field carries the lifecycle stage: discussing / planning / executing / verifying) 2. active_phase null + → 'v2.0 [██░] X% · next execute-phase 4.5' next_action set (idle state — surfaces what the user should run next without opening STATE.md) 3. percent=100 (or → 'v2.0 [██████████] 100% · milestone complete' completed=total) 4. (default fallback) → 'v1.9 Code Quality · executing · ph (1/5)' (existing rendering, byte-for-byte preserved when none of the new fields are populated) Backward compat is the design priority: - STATE.md files without the new fields render identically to v1.38.x - progress bar is opt-in (empty string when percent absent) - Each new scene only activates when its specific fields are populated A new helper renderProgressBar() generates the 10-segment bar that matches the existing context meter style (so the two bars on the status-line are visually consistent). Tests: 27/27 existing tests still pass. Refs #2833 * test(#2833): cover parseStateMd lifecycle fields + formatGsdState scenes 26 new tests organized in 5 describe blocks, modeled after the existing enh-2538-statusline-last-command.test.cjs convention: parseStateMd #2833 lifecycle fields (7 tests) - reads active_phase / next_action / next_phases / progress.percent - 'null' literal handled correctly - YAML flow array parsing (1 item, multiple items) - progress nested block (3 fields) - absent fields return undefined formatGsdState #2833 lifecycle scenes (6 tests) - Scene 1: active_phase set → 'Phase X.Y <stage>' - Scene 2: idle + next_action → 'next <action> <phases>' (1+ phases) - Scene 3: percent=100 OR completed=total → 'milestone complete' formatGsdState #2833 backward compatibility (4 tests) — CRITICAL - Legacy STATE.md (no new fields) renders byte-for-byte unchanged - Empty state, partial state, progress-bar-opt-in all preserved progress bar rendering (6 tests) - 0% / 50% / 100% / clamping / opt-in absence formatGsdState #2833 scene priority (3 tests) - active_phase wins over next_action when both populated - next_action wins over fallback when active_phase null - percent=100 wins over fallback even with phase set Combined run: 53/53 tests pass (existing 27 + new 26). Refs #2833 * docs(#2833): describe phase-lifecycle frontmatter fields and rendering scenes Add docs/STATE-MD-LIFECYCLE.md as the canonical reference for the four new STATE.md frontmatter fields and the four status-line rendering scenes introduced by this proposal: - Frontmatter field reference (active_phase / next_action / next_phases / progress.percent) with type and population semantics - Why progress.percent is intentionally the phase dimension and not the plans dimension (plans dimension trends optimistic when future phases are unplanned) - The four rendering scenes including their priority order - Stage-label convention for Scene 1 (discussing / planning / executing / verifying matching the four phase orchestrators) - Frontmatter parsing constraints — frontmatter must start at file head, no comments inside nested blocks, next_phases is single-line flow only - Backward-compatibility guarantee (locked in by the test suite) - Cross-links to the foundation issue #1989 and the read-side issues this proposal helps close The document deliberately scopes itself to the read-side (what the hook parses, what it renders). Write-side SDK and workflow changes that auto-maintain the fields are out of scope for this PR so each piece can be reviewed independently — see the issue thread for the full proposal. Refs #2833 * test(#2833): simplify '0% renders 10 empty segments' assertion Address CodeRabbit nitpick — drop the convoluted assert.equal that built the expected value via .replace() and rely on the existing assert.ok includes-check. The behavior under test is unchanged; the assertion is just easier to read. Refs #2884 review comment
This commit is contained in:
177
docs/STATE-MD-LIFECYCLE.md
Normal file
177
docs/STATE-MD-LIFECYCLE.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# STATE.md Phase Lifecycle Frontmatter
|
||||
|
||||
> **Status:** Reference for the phase-lifecycle status-line proposed in
|
||||
> [issue #2833](https://github.com/gsd-build/get-shit-done/issues/2833).
|
||||
> The status-line hook (`hooks/gsd-statusline.js`) reads the fields below;
|
||||
> SDK write-side support to maintain them is tracked separately.
|
||||
|
||||
GSD's `STATE.md` carries YAML frontmatter that the status-line hook reads on
|
||||
every render. This document describes the **phase-lifecycle fields** and the
|
||||
rendering scenes they trigger.
|
||||
|
||||
All four lifecycle fields are **optional and additive**. Existing `STATE.md`
|
||||
files (without these fields) keep rendering exactly as they did before — no
|
||||
visual change, no migration required.
|
||||
|
||||
---
|
||||
|
||||
## Frontmatter fields
|
||||
|
||||
```yaml
|
||||
---
|
||||
gsd_state_version: 1.0
|
||||
milestone: v2.0 # existing
|
||||
milestone_name: Code Quality # existing
|
||||
status: in_progress # existing — see "status semantics" below
|
||||
|
||||
# Phase-lifecycle additions (issue #2833) — all optional
|
||||
active_phase: null # phase number when an orchestrator is in flight
|
||||
next_action: execute-phase # next recommended command when idle
|
||||
next_phases: ["4.5"] # phases that next_action applies to (1-2 ids)
|
||||
|
||||
progress: # nested block (existing key, percent now opt-in for the bar)
|
||||
total_phases: 17
|
||||
completed_phases: 10
|
||||
percent: 59
|
||||
---
|
||||
```
|
||||
|
||||
### Field reference
|
||||
|
||||
| Field | Type | When populated | When null/absent |
|
||||
|---|---|---|---|
|
||||
| `active_phase` | string (e.g. `"4.5"`) | An orchestrator command is in flight on this phase | Idle between phases |
|
||||
| `next_action` | string | Idle, with a recommended command (`discuss-phase` / `plan-phase` / `execute-phase` / `verify-phase`) | An orchestrator is in flight, OR no recommendation available |
|
||||
| `next_phases` | YAML flow array (e.g. `["4.5"]`) | Goes with `next_action` — phases the action applies to | Same as above |
|
||||
| `progress.percent` | integer 0-100 | Milestone progress in **phase dimension** (`completed_phases / total_phases`) | Bar rendering is opt-in — absent → no bar |
|
||||
|
||||
### `next_phases` parser scope
|
||||
|
||||
Only **single-line YAML flow** is parsed: `next_phases: ["4.5", "4.6"]`.
|
||||
|
||||
Block sequences over multiple lines (`- 4.5\n - 4.6`) are intentionally
|
||||
**not parsed** — the status-line only needs the primary recommendation, and a
|
||||
single-line array keeps the regex-based parser predictable. If a project needs
|
||||
to track many candidate next phases for documentation purposes, store the
|
||||
extra ones in the `STATE.md` body.
|
||||
|
||||
### `progress.percent` dimension
|
||||
|
||||
The bar rendered next to the milestone version reflects **phase completion**
|
||||
(`completed_phases / total_phases`), not plan completion.
|
||||
|
||||
Plan dimension (`completed_plans / total_plans`) trends optimistic for any
|
||||
project where future phases haven't been planned yet — `total_plans` only
|
||||
counts plans inside *already-planned* phases, so the denominator is
|
||||
structurally smaller than reality. Reporting that number to stakeholders
|
||||
overstates progress.
|
||||
|
||||
If a project wants to show plan-level progress somewhere, store it elsewhere
|
||||
in frontmatter or the body — the status-line bar is reserved for the
|
||||
phase-dimension number that matches `ROADMAP.md` progress tables and
|
||||
`MILESTONES.md`.
|
||||
|
||||
---
|
||||
|
||||
## Status-line rendering scenes
|
||||
|
||||
`formatGsdState()` checks the lifecycle fields in the order below and emits
|
||||
the **first matching scene**. If none match, the renderer falls through to
|
||||
the original `<status> · <phase>` format (byte-for-byte unchanged from
|
||||
v1.38.x).
|
||||
|
||||
| Scene | Trigger | Display |
|
||||
|---|---|---|
|
||||
| **1. Phase active** | `active_phase` populated | `v2.0 [██░░░] X% · Phase 4.5 executing` |
|
||||
| **2. Idle, next recommended** | `active_phase` null AND `next_action` + `next_phases` populated | `v2.0 [██░░░] X% · next execute-phase 4.5` |
|
||||
| **3. Milestone complete** | `percent: 100` OR `completed_phases == total_phases` | `v2.0 [██████████] 100% · milestone complete` |
|
||||
| **4. Default fallback** | None of the above | `v1.9 Code Quality · executing · ph (1/5)` (existing format) |
|
||||
|
||||
### Scene priority example
|
||||
|
||||
When both `active_phase` and `next_action` are populated, **Scene 1 wins** —
|
||||
an orchestrator is in flight, so any "next recommendation" would be misleading.
|
||||
This is enforced by check order in `formatGsdState()` and by tests in
|
||||
`tests/enh-2833-phase-lifecycle-statusline.test.cjs` (suite *"scene priority"*).
|
||||
|
||||
### Stage labels in Scene 1
|
||||
|
||||
In Scene 1, the second part of `Phase 4.5 <stage>` is whichever value is in
|
||||
the `status` field at that moment. The convention proposed in issue #2833
|
||||
is to use the lifecycle stage:
|
||||
|
||||
| Command | `status` value while in flight |
|
||||
|---|---|
|
||||
| `/gsd-discuss-phase` | `discussing` |
|
||||
| `/gsd-plan-phase` | `planning` |
|
||||
| `/gsd-execute-phase` | `executing` |
|
||||
| `/gsd-verify-phase` | `verifying` |
|
||||
|
||||
If `status` is left at `in_progress` (the milestone-level value), Scene 1
|
||||
renders just `Phase 4.5` without the stage suffix.
|
||||
|
||||
---
|
||||
|
||||
## Frontmatter parsing constraints
|
||||
|
||||
The status-line hook uses regex-based parsing (no full YAML library), so a
|
||||
few constraints apply:
|
||||
|
||||
1. **Frontmatter must start at the very first character of the file.**
|
||||
Anything (including comments) above the opening `---` invalidates the
|
||||
match. The opening `---` line must be exactly that — no trailing spaces.
|
||||
|
||||
2. **Comments inside nested blocks are not supported.**
|
||||
The parser for `progress:` requires the next line to be `[ \t]+\w+:` —
|
||||
inserting `# comment` between `progress:` and the first key breaks the
|
||||
match and the bar disappears. Put any documentation in the body of
|
||||
`STATE.md`, not inside frontmatter blocks.
|
||||
|
||||
3. **`next_phases` accepts only single-line flow format.**
|
||||
See the parser scope note above.
|
||||
|
||||
These constraints are tested in
|
||||
`tests/enh-2833-phase-lifecycle-statusline.test.cjs`. If a future change
|
||||
swaps the regex parser for a real YAML library, the constraints can be
|
||||
relaxed and the tests updated accordingly.
|
||||
|
||||
---
|
||||
|
||||
## Backward compatibility
|
||||
|
||||
This document describes additive fields. The promise is:
|
||||
|
||||
- A `STATE.md` file with **none** of the lifecycle fields populated renders
|
||||
**byte-for-byte identically** to v1.38.x and earlier.
|
||||
- Adding any lifecycle field is **opt-in per project** — the renderer falls
|
||||
through to the existing format when fields are absent.
|
||||
- The progress bar is opt-in even when `progress` block exists — only
|
||||
`progress.percent` triggers the bar; `total_phases` / `completed_phases`
|
||||
alone don't.
|
||||
|
||||
The `formatGsdState #2833 backward compatibility` test suite locks this
|
||||
guarantee in: any change that breaks legacy `STATE.md` rendering will fail
|
||||
the suite.
|
||||
|
||||
---
|
||||
|
||||
## Related issues / PRs
|
||||
|
||||
- **#1989** — *enhancement: surface GSD state in statusline.* The foundation
|
||||
this proposal extends. Established that `STATE.md` frontmatter drives the
|
||||
status-line.
|
||||
- **#2833** — *enhancement: phase-lifecycle status-line — auto-rotate
|
||||
STATE.md frontmatter as phase orchestrators progress.* This document
|
||||
describes the read-side spec from that issue. Write-side SDK / workflow
|
||||
changes to auto-maintain the fields are tracked separately so each piece
|
||||
can be reviewed independently.
|
||||
|
||||
Companion read-side issues this proposal also helps close (each fixed a
|
||||
specific symptom of the same gap):
|
||||
|
||||
- #1102 — STATE.md frontmatter plan counts only update on plan completion
|
||||
- #1103 — STATE.md status / last_activity not updated when a phase starts
|
||||
- #1446 / #1572 — phase complete doesn't update Plans column
|
||||
- #612 — ROADMAP.md not updating
|
||||
- #956 — planning document drift across core workflows
|
||||
- #2018 — verify-work doesn't auto-transition (fixed for verify only)
|
||||
Reference in New Issue
Block a user