fix(hooks): state-line-guard ratchets against current size, not a flat gate

Advisor review caught this before the v0.23.0 tag landed: the guard
compared the projected line count only against the fixed 60-line max,
never against the file's current size, so trimming an already-oversized
STATE.md (e.g. 156 -> 100 lines, still over 60 but smaller) was denied
exactly like growing it would be.

Verified against the real tree: 23 of the machine's STATE.md files are
already over 60 lines today, one at 1405. Shipped as a flat gate, this
hook would have made most of them un-editable except by a single write
landing at <=60 in one shot -- backwards for a guard meant to make
trimming possible.

Fixed with a ratchet: deny only when the projection is over the max AND
larger than the file's current line count (0 for a file that doesn't
exist yet), for both Write and Edit. A compliant file still cannot grow
past the limit and a new file still cannot be created oversized, but an
oversized file can now be edited toward compliance one write at a time.

state-line-guard-selftest.sh: 16 -> 21 checks (new section 8: shrink
allows, same-size allows, grow-while-oversized still denies, new-oversized
still denies). Suite total: 191 + 152 + 69 + 21 = 433.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186kZGKddxfA9N84HqMLbb2
This commit is contained in:
Kjell Tore Guttormsen 2026-08-14 17:10:04 +02:00
commit c1dabf109d
5 changed files with 154 additions and 10 deletions

View file

@ -75,7 +75,7 @@ marketplace plugin. Three components, one boundary:
otherwise only the first, mirroring what the real Edit tool does. Getting
`replace_all` wrong in either direction is not a hypothetical: a hook that
only ever replaced the first occurrence would silently pass a bulk edit that
balloons the file, so `state-line-guard-selftest.sh` (16 checks) pins a
balloons the file, so `state-line-guard-selftest.sh` (21 checks) pins a
fixture where only counting every `replace_all` occurrence produces the
correct denial. Anything the hook cannot project with confidence — a
missing file, an `old_string` that is not present, fields of the wrong
@ -83,6 +83,29 @@ marketplace plugin. Three components, one boundary:
here would; the guard only ever touches files named exactly `STATE.md`, at
any depth, matching the same basename rule the global session-start hook's
nearest-STATE-wins search already uses.
**It is a RATCHET against the file's current size, not a flat gate at 60 —
found by advisor review before the tag landed, not by the selftest, which
had no fixture for it.** The first cut compared the projected line count
only against `MAX_LINES`, never against what the file already was, so
trimming an oversized STATE.md from, say, 156 to 100 lines — still over 60,
but strictly smaller — was denied exactly like growing it would have been.
Verified empirically against the real tree (2026-08-14):
`wc -l ~/repos/*/STATE.md ~/repos/*/*/STATE.md | awk '$1 > 60'` found 23
files already over 60 lines, one at 1405. Shipped as a flat gate, this hook
would have made most of the machine's STATE.md files un-editable except by
a single write landing at `<=60` in one shot — backwards for a guard whose
whole point is making the trim the /insights finding asked for actually
possible. The fix reads the file's current line count for BOTH tool types
(previously only `Edit` read the file at all) and denies only when the
projection is over `MAX_LINES` **and** larger than that current count: a
compliant file still cannot grow past the limit, a brand-new file still
cannot be created oversized (current defaults to 0), but an already-oversized
file can always be edited toward compliance, one write at a time, without
ever making it worse. Section 8 of the selftest pins all four cases:
shrink-while-still-over-limit allows, same-size-rewrite allows, grow-an-
already-oversized-file still denies, and create-new-oversized-file still
denies.
- **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
@ -395,7 +418,7 @@ obligations in another repo.
`bash scripts/coord-selftest.sh` must exit 0 (191/191),
`bash scripts/board-selftest.sh` must exit 0 (152/152),
`bash scripts/route-selftest.sh` must exit 0 (69/69) and
`bash scripts/state-line-guard-selftest.sh` must exit 0 (16/16).
`bash scripts/state-line-guard-selftest.sh` must exit 0 (21/21).
- English for all code, docs, and commit messages (public repo). Norwegian
trigger aliases in the skill description are deliberate.
- Conventional Commits: `type(scope): description`.