* test(#2734): failing-first suite for the statusline STATE.md freshness marker Binds the contract before any hook change exists: a `state ~N commits back` segment gated on the state_head stamp landed by #2622, firing at the same advisory threshold /gsd-health's W024 uses rather than at > 0. Covers all five acceptance criteria — threshold parity (19/20/21 boundaries), both renderers including formatGsdStateCompact, an exact spawn-count assertion, repo-pinning and sub_repos degradation, and behavioral parity against readStateHeadFreshness rather than a source-grep of the two fence copies. 52 example-based tests plus 5 seeded fast-check properties. Red now by design. * feat(#2734): surface STATE.md commit-age on the statusline Adds an opt-in `state ~N commits back` marker to the GSD-state segment, consuming the `state_head` stamp and freshness contract landed by #2622. A solo developer returning to a project reads "Phase 4, executing" in STATE.md and acts on it, without noticing the codebase moved 40 commits since that line was written. /gsd-health reports it as W024, but only if you think to run it; the statusline is the surface you see without asking. Fires at STATE_HEAD_ADVISORY_COMMITS (20), the same threshold W024 uses, not at > 0: with commit_docs:true the commit carrying a STATE.md sync advances HEAD by one, so > 0 would alarm permanently on a fresh project. Costs exactly one bounded git subprocess per render and none when disabled. `rev-list --left-right --count` answers ancestry and distance together, and repo pinning is a filesystem check mirroring projectOwnsItsRepo rather than a --show-toplevel compare, which is unreliable on macOS /private/var and Windows 8.3 paths. Every unresolvable input degrades to the tri-state unknown -- the marker is absent, never a "fresh" claim the project cannot substantiate: a malformed stamp, a root that does not own its .git, a sub_repos workspace, history rewound past the stamp, or git being unavailable. Also collapses statusline config resolution onto one resolveStatuslineOptions() seam. runStatusline() and renderStatusline() duplicated it byte-for-byte; one copy is what keeps a newly-added key from reaching only one of them. * test(#2734): route the e2e spawn through the process seam and fix fixture leaks Review findings from the two orthogonal passes: - `bothEntryPointsResolveOptionsIdentically` spawned a child and substring-matched its stdout to test a pure function. It now calls resolveStatuslineOptions() directly — no subprocess, no text matching. - `skipsFreshnessWorkWhenTodoTaskActive` genuinely needs a child (the !task gate lives in runStatusline, which reads stdin), so it now spawns through tests/helpers/process-seam.cjs and proves the negative with a filesystem fact: the git shim appends to a marker file on every invocation, and the assertion is that the marker never appears. Stronger than asserting text is missing, and it drops the last stdout substring match in the block. - Every fixture-creating test now registers `t.after(() => cleanup(dir))` instead of a trailing cleanup(dir), which leaked the temp repo on assertion failure. derivationAgreesWithStateModule reassigns `dir` across five fixtures, so it binds each directory at scheduling time rather than cleaning only the last. Also corrects markerCoexistsWithMilestoneComplete, which asserted the wrong expectation rather than finding a code defect: `percent` drives the progress bar too, so the milestone segment reads "v1.9 [##########] 100%". The marker appends after it, which is what the test exists to prove. CONTEXT.md's opt-in statusline key list was missing statusline.show_git as well as the new key; both are now enumerated. * docs(#2734): backfill changeset PR number (#3700) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -613,6 +613,7 @@ for a worked example.
|
||||
| `statusline.show_context_tokens` | boolean | `false` | Append the absolute token count (e.g. `(156k)`) after the context meter's percentage. Sums input, cache-creation, cache-read, and output tokens from the hook payload — a broader basis than the meter's percentage (which excludes output tokens), so the two figures can diverge slightly. Opt-in; the meter is unchanged when the flag is absent |
|
||||
| `statusline.state_format` | string | `"full"` | Format of the GSD-state segment. `"full"` (default) is the existing rendering with milestone name and progress bar. `"compact"` renders `<version> · P<phase>/<total> · <status>` (e.g. `v1.12 · P7/12 · executing`) — drops the milestone name and bar, and collapses narrative statuses to the canonical keyword set from `normalizeStateStatus()` (`paused` — the canonical stuck state — renders uppercase as `PAUSED`) |
|
||||
| `statusline.show_git` | boolean | `false` | Append a git segment after the directory: current branch plus compact work-state markers (`+staged` `~unstaged` `?untracked` `↑ahead` `↓behind`, or `✓` when clean and in sync). One `git status --porcelain=v2` call per render; the segment is absent outside a git repo or when git is unavailable |
|
||||
| `statusline.show_state_freshness` | boolean | `false` | Append `state ~N commits back` to the GSD-state segment when STATE.md carries a `state_head` stamp and the codebase has moved at least 20 commits past it (the same advisory threshold `/gsd-health`'s W024 uses). Exactly one `git rev-list` call per render, and only when enabled and a stamp is present; the marker is absent below the threshold, outside a git repo, when the project root does not own its `.git`, and in `planning.sub_repos` workspaces |
|
||||
|
||||
The prompt injection guard hook (`gsd-prompt-guard.js`) is always active and cannot be disabled — it's a security feature, not a workflow toggle.
|
||||
|
||||
|
||||
@@ -185,6 +185,8 @@
|
||||
- [Broken-Windows Ledger](#158-broken-windows-ledger)
|
||||
- [Complexity-Triggered Refactor](#159-complexity-triggered-refactor)
|
||||
- [Archive Quick Tasks at Milestone Close](#160-archive-quick-tasks-at-milestone-close)
|
||||
- [Verify-Command Path Grounding](#161-verify-command-path-grounding)
|
||||
- [Statusline STATE.md Freshness Marker](#162-statusline-statemd-freshness-marker)
|
||||
|
||||
---
|
||||
|
||||
@@ -3449,3 +3451,23 @@ See [Archiving quick tasks](how-to/handle-quick-and-fast-tasks.md#archiving-quic
|
||||
- `script_missing` is advisory only — this phase may be adding the script — so a genuinely mistyped npm script still reaches the executor.
|
||||
|
||||
See [Resolve verify-command path findings](how-to/resolve-verify-command-path-findings.md) and [`gsd-tools check verify-command-paths`](COMMANDS.md#gsd-tools-check-verify-command-paths).
|
||||
|
||||
---
|
||||
|
||||
### 162. Statusline STATE.md Freshness Marker
|
||||
|
||||
**Config key:** `statusline.show_state_freshness` (default `false`)
|
||||
|
||||
**Purpose:** A solo developer returning to a project after time away reads "Phase 4, executing" in `STATE.md` and acts on it — without noticing the codebase has moved 40 commits since that line was written. `/gsd-health` reports this as `W024`, but only if the user thinks to run it. The statusline is the one surface seen continuously without asking (#2734).
|
||||
|
||||
**Behavior:** Renders `state ~N commits back` inside the GSD-state segment when `STATE.md` carries a `state_head` stamp (#2573) and `HEAD` is at least `STATE_HEAD_ADVISORY_COMMITS` (20) commits past it. Both statusline formats carry it — the default renderer and the compact `statusline.state_format` one.
|
||||
|
||||
**The threshold is 20, deliberately not 1.** With `commit_docs: true` (the default) the commit carrying a `STATE.md` sync advances `HEAD` by one, so a `> 0` threshold would render `state ~1 commits back` permanently on a project that is by construction fresh — alarm fatigue on the one always-visible surface.
|
||||
|
||||
**It degrades to silence rather than to a wrong answer.** The marker is absent — never "fresh" — when the stamp is malformed, when the project root does not own its `.git` (an enclosing unrelated repo would otherwise answer), in a `planning.sub_repos` workspace (the outer `HEAD` never advances when code lands in children), when history was rewound past the stamp, and when git is unavailable or slow. A freshness claim the project cannot substantiate degrades to *unknown*.
|
||||
|
||||
**Cost:** exactly one bounded `git rev-list` call per render, and only when enabled *and* a stamp is present — `rev-list --left-right --count` answers ancestry and distance together, and repo pinning is a filesystem check rather than a subprocess. Disabled (the default) it adds none.
|
||||
|
||||
**A proxy, never a drift measurement.** The count includes commits that touched nothing `STATE.md` describes, and the stamp restamps on every state write — so a low count means "something wrote STATE recently", not "STATE is accurate". Rendered with a `~`; never gate on it.
|
||||
|
||||
**Reference:** [Configuration](CONFIGURATION.md) · [Read the statusline freshness marker](how-to/read-the-statusline-freshness-marker.md) · [ADR-2164](adr/2164-statusline-scope-boundary.md)
|
||||
|
||||
@@ -28,6 +28,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md)
|
||||
- [Resolve a contract-drift finding](how-to/resolve-contract-drift-findings.md) — bring an agent's completion contract, read-tag gate, or deleted-file test reference back into agreement with the registry
|
||||
- [Resolve unreachable-guard findings](how-to/resolve-unreachable-guard-findings.md) — fix shell guards whose fallback arm cannot run, and tell "nothing to report" apart from "could not look"
|
||||
- [Resolve an ESLint glob-coverage finding](how-to/resolve-eslint-coverage-findings.md) — bring a source file that matches no lint rule under coverage, or record a reasoned exemption
|
||||
- [Read the statusline freshness marker](how-to/read-the-statusline-freshness-marker.md) — turn on `state ~N commits back`, and tell "STATE.md is fresh" apart from "freshness could not be established"
|
||||
- [Keep planning docs out of a shared repo](how-to/keep-planning-docs-private.md) — make `.planning/` local-only, including untracking files git already tracks (the step `.gitignore` alone cannot do)
|
||||
- [Plan a phase](how-to/plan-a-phase.md) — run research, decompose work, and verify plan quality
|
||||
- [Execute a phase](how-to/execute-a-phase.md) — run plans in parallel waves with fresh-context subagents
|
||||
|
||||
111
docs/how-to/read-the-statusline-freshness-marker.md
Normal file
111
docs/how-to/read-the-statusline-freshness-marker.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Read the statusline STATE.md freshness marker
|
||||
|
||||
The statusline can tell you, at a glance, that `STATE.md` is describing a codebase that has
|
||||
moved on without it:
|
||||
|
||||
```
|
||||
Claude │ v2.0 Auth Rework · executing · state ~34 commits back │ my-project
|
||||
```
|
||||
|
||||
`state ~34 commits back` means `STATE.md` was last written against a commit that is now 34
|
||||
commits behind `HEAD`. You come back to a project after two weeks, the state file still says
|
||||
"Phase 4, executing", and this is the thing that tells you that sentence is stale before you
|
||||
act on it.
|
||||
|
||||
The marker is **advisory and approximate**. It never blocks anything.
|
||||
|
||||
## Turn it on
|
||||
|
||||
```bash
|
||||
gsd-tools config-set statusline.show_state_freshness true
|
||||
```
|
||||
|
||||
It is off by default. That is the only step — the `state_head` stamp it reads is written
|
||||
automatically every time GSD syncs `STATE.md`, so an active project already has one.
|
||||
|
||||
To turn it off again:
|
||||
|
||||
```bash
|
||||
gsd-tools config-set statusline.show_state_freshness false
|
||||
```
|
||||
|
||||
The marker also appears in the compact statusline format
|
||||
(`statusline.state_format: "compact"`), rendered identically.
|
||||
|
||||
## When it appears
|
||||
|
||||
Only when **all** of these hold:
|
||||
|
||||
1. `statusline.show_state_freshness` is `true`.
|
||||
2. `.planning/STATE.md` carries a `state_head:` stamp.
|
||||
3. The project root owns its own `.git`.
|
||||
4. The stamp is an ancestor of the current `HEAD`.
|
||||
5. `HEAD` is at least **20 commits** past the stamp.
|
||||
|
||||
Twenty is the same advisory threshold `/gsd-health` uses for its `W024` warning, and it is
|
||||
deliberately not `1`. With `commit_docs: true` (the default) the commit that carries a
|
||||
`STATE.md` sync advances `HEAD` by one, so a threshold of `> 0` would show
|
||||
`state ~1 commits back` permanently on a project that is, by construction, perfectly fresh.
|
||||
|
||||
## I turned it on and see nothing
|
||||
|
||||
That is usually correct behavior rather than a fault, but "nothing to report" and "could not
|
||||
look" render identically — both are simply absent. Work down this table to tell them apart.
|
||||
|
||||
| Reason | How to confirm | Is it a problem? |
|
||||
|---|---|---|
|
||||
| **Fewer than 20 commits behind** | `git rev-list --count $(grep '^state_head:' .planning/STATE.md \| cut -d' ' -f2)..HEAD` | No — this is the healthy case |
|
||||
| **No `state_head` stamp** | `grep '^state_head:' .planning/STATE.md` returns nothing | No — the stamp appears on the next state write |
|
||||
| **Project root does not own its `.git`** | `ls -d .git` at the directory holding `.planning/` | No — deliberate. See below |
|
||||
| **`planning.sub_repos` is set** | `gsd-tools config-get planning.sub_repos` | No — deliberate. See below |
|
||||
| **History was rewound past the stamp** | `git merge-base --is-ancestor <stamp> HEAD; echo $?` prints `1` | No — reported as unknown on purpose |
|
||||
| **The stamp is not a commit in this repo** | `git cat-file -e <stamp>` fails | Possibly — a hand-edited `STATE.md` |
|
||||
| **`git` unavailable, or the repo is enormous** | `git --version`; the read is abandoned after 1.5s | Rarely — the marker yields rather than stall your prompt |
|
||||
| **A todo task is showing instead** | The middle segment shows a task, not GSD state | No — the whole GSD-state segment is replaced |
|
||||
|
||||
Run `/gsd-health` for the same signal in a form that always explains itself — it reports `W024`
|
||||
with the count, and it is not subject to the statusline's silence.
|
||||
|
||||
### Why it stays quiet instead of guessing
|
||||
|
||||
Two cases deserve spelling out, because in both the marker *could* print a number and that
|
||||
number would be a confident lie:
|
||||
|
||||
- **A GSD project nested inside an unrelated checkout.** `git` resolves `HEAD` from the nearest
|
||||
enclosing `.git`, which might belong to a dotfiles or notes repo that has nothing to do with
|
||||
your project. Rather than report that repo's history as your project's freshness, GSD checks
|
||||
that the directory holding `.planning/` owns its own `.git` and otherwise says nothing.
|
||||
- **A `planning.sub_repos` workspace.** The outer directory legitimately owns both `.planning/`
|
||||
and its own repo, while every code commit lands in a nested child. The outer `HEAD` never
|
||||
advances, so the marker would read "fresh" forever while the code moved arbitrarily far.
|
||||
Per-child freshness would require choosing one `HEAD` out of several unrelated histories, so
|
||||
GSD declines to answer instead.
|
||||
|
||||
The rule in both: a freshness claim the project cannot substantiate degrades to *unknown*,
|
||||
never to *fresh*.
|
||||
|
||||
## What the number does and does not mean
|
||||
|
||||
`~34` counts **every** commit between the stamp and `HEAD`, including commits that touched
|
||||
nothing `STATE.md` describes. And because `state_head` is restamped on every state write, a
|
||||
*low* count means "something wrote `STATE.md` recently" — not "`STATE.md` is accurate."
|
||||
|
||||
So read it as a prompt to look, not as a measurement of drift:
|
||||
|
||||
- **A high count** is a reliable signal that the state file is worth re-reading.
|
||||
- **A low count** is not evidence that the state file is correct.
|
||||
|
||||
Do not build automation on it. It is a proxy, deliberately rendered with a `~`.
|
||||
|
||||
## Cost
|
||||
|
||||
One `git rev-list` call per statusline render, and only when the marker is enabled *and* a
|
||||
`state_head` stamp is present. With the feature off — the default — it adds no subprocess and
|
||||
no measurable work. The call is abandoned after 1.5 seconds, so a slow or huge repository
|
||||
costs you a missing marker rather than a stalled prompt.
|
||||
|
||||
## Related
|
||||
|
||||
- [Configuration reference](../CONFIGURATION.md) — `statusline.show_state_freshness` and the
|
||||
other `statusline.*` keys
|
||||
- [`/gsd-health`](../COMMANDS.md) — the `W024` warning that thresholds on the same constant
|
||||
Reference in New Issue
Block a user