Files
msd-core/docs/how-to/read-the-statusline-freshness-marker.md
Tom Boucher adb46cdd85 feat(#2734): surface STATE.md commit-age on the statusline (#3700)
* 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>
2026-08-20 00:35:01 -04:00

5.4 KiB

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

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:

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.