scripts/check-doc-checkpoint.py enforces CLAUDE.md's rule that docs/STATUS.md, docs/BENCHMARKS.md and the moved row spec's ## Now are owed when a row changes lifecycle state (CLAUDE.md:409-414). It recognises the state by matching a backticked token against a fixed tuple (:56-66):
STATES = ("TODO", "READY", "ACTIVE", "GATING", "BLOCKED", "DONE", "DROPPED", "N/A")
STATE_CELL = re.compile(r"`(" + "|".join(re.escape(s) for s in STATES) + r")`")
PARTIAL and ANCHOR-BACKFILL are not in that tuple, and both are lifecycle states in active use.
.agents/feature-matrix.md:20 declares them explicitly: "PARTIAL, BLOCKED, and ANCHOR-BACKFILL as explicit non-done states." BLOCKED is in the tuple. The other two are not.
Scale
Counting backticked state cells across .agents/*-matrix.md, using the checker's own STATE_CELL shape:
| state |
cells |
in STATES? |
PARTIAL |
115 |
no |
ANCHOR-BACKFILL |
73 |
no |
DONE |
73 |
yes |
BLOCKED |
9 |
yes |
PARTIAL — 115 cells across six matrices (engine 30, model 28, feature 25, quantization 14, backend 11, kernel 7) — is more used than DONE, and it is the one the gate cannot see. Together the two unguarded states cover 188 rows.
The consequence
A row moving into or out of PARTIAL or ANCHOR-BACKFILL owes docs/STATUS.md and its spec's ## Now under the prose rule, and no gate can observe that it moved. The state cell never matches STATE_CELL, so the change is not a lifecycle change as far as the checker is concerned, and the projection surfaces silently go stale.
This is not hypothetical. It surfaced during the fresh review of LOAD-GGUF-MMPROJ (#821 W1), whose row moves READY → PARTIAL. The reviewer proved the instrument was live — flipping the row to DONE in a scratch commit reds it (rc 1, naming docs/BENCHMARKS.md and the spec's ## Now) — then reverted docs/STATUS.md, then docs/USAGE.md, then all of docs/, and got rc 0 every time. The green was honest (no checker was weakened, and no scripts/ change was in that diff) but vacuous: the row owed the checker nothing because its destination state is invisible.
That is the shape CLAUDE.md's "Changing the rules or a checker" section warns about — the checker's behaviour and the prose rule diverging, with no gate over the divergence.
Why this is filed rather than fixed in flow
Adding two strings to a tuple is a one-line edit, but it is a semantic checker change: it moves 188 rows into the population the gate enforces against, and the first change to touch any of them then owes STATUS/BENCHMARKS/## Now where it previously did not. That needs a spec, a red-before test, and a decision about whether ANCHOR-BACKFILL — which reads more like a maintenance marker than a lifecycle state — belongs in the same tuple as PARTIAL at all.
It is also exactly the ratchet situation that has already reddened main once: .agents/issue-index.md records #1376, where filling a spec's ## Gates moved a row into the runnable population and RUNNABLE_BASELINE was not re-pinned in the same change. Widening STATES has the same shape and wants the same care.
Suggested check when this is fixed
Take a row currently in PARTIAL, move it to ACTIVE in a scratch commit without touching docs/STATUS.md, and confirm the checker reds. Today it does not.
Verified at 63d87805c.
scripts/check-doc-checkpoint.pyenforces CLAUDE.md's rule thatdocs/STATUS.md,docs/BENCHMARKS.mdand the moved row spec's## Noware owed when a row changes lifecycle state (CLAUDE.md:409-414). It recognises the state by matching a backticked token against a fixed tuple (:56-66):PARTIALandANCHOR-BACKFILLare not in that tuple, and both are lifecycle states in active use..agents/feature-matrix.md:20declares them explicitly: "PARTIAL,BLOCKED, andANCHOR-BACKFILLas explicit non-done states."BLOCKEDis in the tuple. The other two are not.Scale
Counting backticked state cells across
.agents/*-matrix.md, using the checker's ownSTATE_CELLshape:STATES?PARTIALANCHOR-BACKFILLDONEBLOCKEDPARTIAL— 115 cells across six matrices (engine30,model28,feature25,quantization14,backend11,kernel7) — is more used thanDONE, and it is the one the gate cannot see. Together the two unguarded states cover 188 rows.The consequence
A row moving into or out of
PARTIALorANCHOR-BACKFILLowesdocs/STATUS.mdand its spec's## Nowunder the prose rule, and no gate can observe that it moved. The state cell never matchesSTATE_CELL, so the change is not a lifecycle change as far as the checker is concerned, and the projection surfaces silently go stale.This is not hypothetical. It surfaced during the fresh review of
LOAD-GGUF-MMPROJ(#821 W1), whose row movesREADY→PARTIAL. The reviewer proved the instrument was live — flipping the row toDONEin a scratch commit reds it (rc 1, namingdocs/BENCHMARKS.mdand the spec's## Now) — then reverteddocs/STATUS.md, thendocs/USAGE.md, then all ofdocs/, and got rc 0 every time. The green was honest (no checker was weakened, and noscripts/change was in that diff) but vacuous: the row owed the checker nothing because its destination state is invisible.That is the shape CLAUDE.md's "Changing the rules or a checker" section warns about — the checker's behaviour and the prose rule diverging, with no gate over the divergence.
Why this is filed rather than fixed in flow
Adding two strings to a tuple is a one-line edit, but it is a semantic checker change: it moves 188 rows into the population the gate enforces against, and the first change to touch any of them then owes
STATUS/BENCHMARKS/## Nowwhere it previously did not. That needs a spec, a red-before test, and a decision about whetherANCHOR-BACKFILL— which reads more like a maintenance marker than a lifecycle state — belongs in the same tuple asPARTIALat all.It is also exactly the ratchet situation that has already reddened
mainonce:.agents/issue-index.mdrecords#1376, where filling a spec's## Gatesmoved a row into the runnable population andRUNNABLE_BASELINEwas not re-pinned in the same change. WideningSTATEShas the same shape and wants the same care.Suggested check when this is fixed
Take a row currently in
PARTIAL, move it toACTIVEin a scratch commit without touchingdocs/STATUS.md, and confirm the checker reds. Today it does not.Verified at
63d87805c.