feat(state-line-guard): deny status=done while commits are unpushed
ORDRE 42 (operator, 2026-08-16). Two sessions had their push refused by the UFW rate limit on port 22, reported that honestly in the coord inbox, and wrote status=done anyway: board line green, one commit unpushed, published surface 404. `done` meant "the session finished" where every reader takes it to mean "the work landed" -- and since `done` drops a repo from the board plan, `morning --say <repo>` could not reach either of them. The deny sits on the WRITE, not on session end. Measured against the official hooks docs rather than assumed: Stop fires "once per turn" with no signal marking the last one, and its exit 2 "prevents Claude from stopping", so a repo that genuinely cannot push would get a session that will not end; SessionEnd is the once-per-session event and cannot block at all. Fails open on every git uncertainty (no upstream, detached HEAD, missing remote-tracking ref, not a repo) -- 8 of 44 repos on the real tree have no upstream, one already status=done. Compares against the branch's own upstream, never a hardcoded origin/main (three repos sit on master). Selects the board line with board.sh's own anchor, so prose saying status=done never triggers it. status=blocked and status=in-progress stay writable in the same single edit, so the deny can never wedge a session. state-line-guard-selftest.sh section 10, 17 checks (23 -> 40), including the mandatory known-positive: status=done with everything pushed still allows. Both outcomes also verified against real repos -- app-creator (1 unpushed) denied, repo-mailbox (clean) allowed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P4LMWBQGmufmBU6UdvJZ2E
This commit is contained in:
parent
95ac7101ea
commit
d16a3f57e7
3 changed files with 400 additions and 2 deletions
53
CLAUDE.md
53
CLAUDE.md
|
|
@ -170,6 +170,57 @@ marketplace plugin. Three components, one boundary:
|
|||
never affected - `join` does not interpret its argument as a pattern.
|
||||
Pinned by state-line-guard-selftest.sh section 9 (`$\`` as the real repro,
|
||||
`$&` as a second sequence proving the fix is general).
|
||||
|
||||
**Since ORDRE 42 (operator, 2026-08-16) it carries a SECOND invariant: the
|
||||
projected content may not claim `status=done` in its board line while the
|
||||
repo holds commits the branch's upstream does not have.** Measured that day:
|
||||
two sessions had their push refused by the UFW rate limit on port 22, said so
|
||||
honestly in the coord inbox, and wrote `status=done` regardless - board line
|
||||
green, one commit unpushed, published surface 404. `done` meant "the session
|
||||
finished" where every reader takes it to mean "the work landed", and since
|
||||
`done` drops a repo from the board plan, `morning --say <repo>` could not
|
||||
reach either of them: one defect hid the other.
|
||||
|
||||
**The order recommended a session-end hook and that direction does not exist
|
||||
in the form it assumes - measured against the official hooks docs, not
|
||||
reasoned.** `Stop` fires "once per turn", not once when the session ends, with
|
||||
no signal marking the last turn; its exit 2 "prevents Claude from stopping,
|
||||
continues the conversation", so a repo that genuinely cannot push (the very
|
||||
rate limit that caused the incident) would get a session that will not end.
|
||||
`SessionEnd` is the once-per-session event and cannot block at all
|
||||
("Can block? No" - exit 2 "shows stderr to user only"), which is the
|
||||
after-the-fact nagging the order explicitly refused. Warn-on-write plus
|
||||
deny-at-session-end inherits the broken half and buys nothing. So the deny
|
||||
sits on the write, where the false claim is actually made.
|
||||
|
||||
**The false-positive trap is real but bounded, and the deny is escapable by
|
||||
telling the truth.** STATE.md is written BEFORE the session's final commit, so
|
||||
a session that batches its pushes does hold unpushed commits at that moment -
|
||||
but the global git rule already requires a push immediately after every
|
||||
commit, and the real tree bears that out (2026-08-16: 43 of 44 repos carrying
|
||||
a STATE.md had nothing unpushed; the one exception was `status=blocked` and
|
||||
honest). `status=blocked` and `status=in-progress` stay writable in the same
|
||||
single edit, so a session that cannot push is never wedged - only stopped from
|
||||
claiming otherwise.
|
||||
|
||||
**No ratchet here, unlike the line limit above, and the asymmetry is the
|
||||
reason.** An oversized file needs many writes to come back under the limit, so
|
||||
denying the intermediate steps would make trimming impossible; a false `done`
|
||||
is corrected by changing one token in the write already being made. A
|
||||
"deny only the transition into done" variant was rejected outright: the
|
||||
common shape is a repo that ended `done` last session and writes `done` again
|
||||
this session, which such a rule waves straight through. It **fails OPEN** on
|
||||
every git uncertainty - no upstream, detached HEAD, missing remote-tracking
|
||||
ref, not a repo, git slow or absent - because 8 of those 44 repos have no
|
||||
upstream at all (one already `status=done`), and a confident denial resting on
|
||||
a measurement that never happened is the worse error. The board line is
|
||||
selected with `board.sh`'s own anchor (`^<!-- board:`) and the status token
|
||||
compared exactly, so prose saying `status=done` never triggers it (a STATE.md
|
||||
documenting this guard writes that string routinely) and `done2` is not `done`
|
||||
here even though board.sh's F3+F4 prefix defect still reads it as one.
|
||||
Selftest section 10, 17 checks, including the mandatory known-positive: a
|
||||
`status=done` with everything pushed must still go through, or the guard is a
|
||||
gate that denies everything and proves nothing.
|
||||
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
|
||||
next-step blocks + board lines, `git status`, and mailbox pending counts, and
|
||||
prints one line per repo. Read-only by construction: it writes to no repo, no
|
||||
|
|
@ -535,7 +586,7 @@ obligations in another repo.
|
|||
`bash scripts/coord-selftest.sh` must exit 0 (220/220),
|
||||
`bash scripts/board-selftest.sh` must exit 0 (217/217),
|
||||
`bash scripts/route-selftest.sh` must exit 0 (69/69) and
|
||||
`bash scripts/state-line-guard-selftest.sh` must exit 0 (23/23).
|
||||
`bash scripts/state-line-guard-selftest.sh` must exit 0 (40/40).
|
||||
- English for all code, docs, and commit messages (public repo). Norwegian
|
||||
trigger aliases in the skill description are deliberate.
|
||||
- Conventional Commits: `type(scope): description`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue