fix(infra): a failed measurement must never render as a reassuring value

Tier 3, all five the same defect class (Verifiseringsloven ansikt 4): a
broken or uninstrumented query returning a positive-looking null, consumed
as a fact about the world.

F5  coord-count.sh: a mailbox root that does not exist was byte-identical
    to one where nobody has pending mail - zero lines, exit 0, silent
    stderr. Now exit 3 + a named stderr line; an existing-but-empty root
    stays a silent, clean 0. 3 rather than 2 because 2 already means "you
    called me wrong" and this means "the world you named is not there".
F14 coord-count.sh: the header promised exit 0 unconditionally while
    --exclude with no value already exited 2. Contract restated as
    0/2/3 and pinned as a check on the help TEXT.
F6  board.sh: `git status | wc -l` yields 0 lines whether the tree is
    clean or git refused to answer, so a failure printed DRT=0. Now "?",
    and BOTH awk consumers handle it - --plan's free-capacity test
    compares the field as a string against "0" (a "?" coerces to 0 in
    arithmetic and would certify an unmeasured tree as free), and the SUM
    roll-up names what it could not add.
F10 board.sh: a scan root that does not exist was skipped in silence and
    the empty scan exited 0. Bad roots are now named on stderr; exit 3
    only when NO root was scanned. A mix still exits 0 and prints the
    board. Replaces an assertion that encoded this defect as a pass.
F13 pre-state-line-guard.mjs: MAX_LINES is overridable via
    CLAUDE_STATE_MAX_LINES so the boundary is testable without hardcoding
    120 twice. An unusable value denies by name rather than falling back
    to the default - a limit that silently did not take effect is the
    same defect one layer up.

Every design choice mutation-tested; every negative check carries a
known-positive control. Section 11's first cut was vacuously green (wrong
basename + unexported fixture path) - recorded in CLAUDE.md rather than
quietly fixed, and the section now asserts its own ground truth.

Denominator measured, not estimated: coord-inbox.sh:57 and
coord-order-inbox.sh:60/64 carry the same `|| exit 0` shape and are
deliberately left alone (injection path, prose output, must never fail a
SessionStart) - stated in CLAUDE.md as a bounded gap.

Suites: coord 230->242, board 281->300, guard 40->54, route 69, orders
110, npm 11/11. Verified under system bash 3.2, not just Homebrew 5.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-08-26 12:15:05 +02:00
commit d8fdeaa991
7 changed files with 606 additions and 16 deletions

118
CLAUDE.md
View file

@ -16,7 +16,7 @@ marketplace plugin. Three components, one boundary:
`coord-done.sh` archives, `coord-count.sh` counts without delivering,
`coord-sweep.sh` closes the aged FYI backlog machine-wide.
Everything is pinned by `coord-selftest.sh`
(230 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
(242 checks, throwaway mailbox via `CLAUDE_COORD_DIR`).
**`ktg-plugin-marketplace` is a RETIRED `--to` address (operator decision
2026-08-15), rejected rather than redirected.** It is a polyrepo directory,
@ -102,6 +102,39 @@ marketplace plugin. Three components, one boundary:
field means a reply IS owed, because every message written before 0.11.0
lacks it.
**F5+F14 (2026-08-26): the header promised exit 0 unconditionally, and both
halves of that were false - one of them silently.** `--exclude` with no value
already exited 2 (F14), so the documented contract contradicted the code a
reader could run. Worse, `[ -d "$COORD" ] || exit 0` (F5) made a mailbox root
that DOES NOT EXIST byte-identical to one where nobody has pending mail: zero
lines on stdout, exit 0, nothing on stderr. `board.sh` consumes this TSV.
That is Verifiseringsloven ansikt 4 in the transport itself - a broken query
returning a positive-looking null, read as a fact about the world. The
contract is now three statuses: 0 = counted, 2 = usage error, 3 = mailbox root
absent. **3 rather than 2 on purpose:** 2 already means "you called me wrong"
and this means "the world you named is not there" - two different repairs,
and a consumer that only ever sees one integer cannot tell them apart. Both
nonzero paths print NOTHING on stdout, so neither can be mistaken for a count
of zero. What the old "always 0" claim was protecting is kept and made
precise: no state OF THE MAILBOX can produce a nonzero exit - not an empty
root, not a malformed message, not an unreadable date - so a SessionStart
still cannot be failed by mail. An EXISTING but empty root stays a silent,
clean 0, and selftest section 36 pins that silence as hard as it pins the
failure: a warning on the ordinary path would train every reader to ignore
the channel the defect case needs. Section 37 pins the header TEXT itself
(the same technique board-selftest uses on the FLY legend) - a doc line
nothing tests is a doc line that drifts, which is how this one drifted.
**Bounded gap, stated rather than closed:** `coord-inbox.sh:57` and
`coord-order-inbox.sh:60/64` carry the identical `[ -d "$COORD" ] || exit 0`
shape. The denominator was measured, not estimated (`grep -n '|| exit 0'
scripts/*.sh`), and they were deliberately left alone: they are the
INJECTION path, their output is prose a session reads rather than a TSV a
program parses, and their contract really is "never fail a SessionStart".
Changing them would change what every session sees, in every repo, and that
was not ordered. Naming them here is the point - the next session that greps
for this shape finds a decision, not an oversight.
**Reading is delivering — counting is not.** `coord-inbox.sh` records a
broadcast as seen once it has printed it, so it can never be used to survey
other repos: doing so would consume each one's backlog silently, and the seen
@ -227,7 +260,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` (21 checks) pins a
balloons the file, so `state-line-guard-selftest.sh` 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
@ -291,6 +324,37 @@ marketplace plugin. Three components, one boundary:
Pinned by state-line-guard-selftest.sh section 9 (`$\`` as the real repro,
`$&` as a second sequence proving the fix is general).
**F13 (2026-08-26): `MAX_LINES` is now overridable by
`CLAUDE_STATE_MAX_LINES`, and an UNUSABLE override is refused by name rather
than ignored.** As a bare constant the limit forced every boundary fixture to
hardcode the same number the code carried - two copies of one policy, both
rewritten by hand the last time the operator moved it (60 -> 120). The knob is
the same kind `CLAUDE_COORD_DIR` is for the mailbox root. It is **not** a
bypass claim: this guard has always been escapable by writing the file another
way, exactly as the sibling pathguard is. The one thing it must never do is
silently fail to take effect, so an unusable value (`""`, `"0"`, negatives,
anything not made of digits) **denies with a message naming the variable**
instead of falling back to 120 - a caller who set it and got the default
anyway would be reading a limit that was never in force, which is the exact
positive-looking null this whole tier of work closes. Empty counts as
unusable, not as unset: it is what an expansion of an unset variable produces,
and a value was meant. Recovery is one `unset`, and the message says so.
Resolution happens AFTER the STATE.md basename gate, so a bad override can
never block a write this guard would not have judged anyway.
**Section 11's first cut was VACUOUSLY GREEN, and that is worth recording
rather than quietly fixing.** The ratchet fixture wrote to
`f13-ratchet-STATE.md` - a basename that is not `STATE.md`, so the guard
allowed the write without measuring anything - and its path variable was not
exported when `node` read it, so the fixture file was never created either.
Two independent faults, one passing check, testing nothing. It was caught by
an `ERR_INVALID_ARG_TYPE` trace in `npm test` output that a green summary
line would otherwise have buried. The section now asserts its own ground
truth (the oversized file really is 40 lines) before the check that depends
on it, and carries the inverse case (growing that same file is still denied),
so neither fault can recur silently. Mutation-verified: removing the ratchet
now turns the check red, which it could not do before.
**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:
@ -347,7 +411,7 @@ marketplace plugin. Three components, one boundary:
- **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
STATE.md and no mailbox. Pinned by `board-selftest.sh` (281 checks).
STATE.md and no mailbox. Pinned by `board-selftest.sh` (300 checks).
**It lives here because the mailbox is one of its three inputs, and it carries
the same axis distinction the mailbox does.** A pending count means *others
@ -651,6 +715,48 @@ marketplace plugin. Three components, one boundary:
find a live one when planted - never behaviourally, because there is no
behaviour to pin.**
**F6+F10 (2026-08-26): two of the board's own readings turned a FAILED
measurement into a reassuring value.** `git -C "$d" status --porcelain
2>/dev/null | wc -l` yields zero lines whether the tree is CLEAN or git
refused to answer at all, so DRT printed **0** - and every reader took that
as "nothing uncommitted here" (F6). It is reachable, not theoretical:
discovery tests `.git` with `-e` precisely so worktrees are found, and a
worktree whose parent checkout was deleted exits **128** here (measured
directly before the fixture was written, and the fixture asserts that ground
truth itself so a future git that starts answering fails the check rather
than silently disarming the section). The exit status is now the
discriminator and an unmeasured tree reads **`?`** - the same token
`coord-count.sh` already uses for an age it could not compute, for the same
fail-safe reason. **The fix is worthless without its two awk consumers, and
one of them is the whole point:** `--plan`'s free-capacity test compared
`$7 + 0`, and `"?"` coerces to **0** in arithmetic, so an UNMEASURED tree
would have been certified as free capacity - the defect reappearing one
layer down, wearing the fix as a disguise. It now compares the field as a
STRING against exactly `"0"`; only a tree measured clean is clean. Mutation
test: restoring the numeric comparison turns exactly that one check red
while both controls (clean repo still free, dirty repo still not) stay
green. The SUM roll-up sums the same field, where a `?` adds 0 and quietly
understates the total, so it now NAMES the repos it could not add -
labelled, never silently dropped, the same rule `--plan` already applies to
repos with no board line. Every SHELL consumer prints the field with `%s`
and was unaffected; that denominator was measured, not assumed.
**F10 is the same defect at the scan boundary.** `[ -d "$root" ] || continue`
skipped a bad root in silence, and a scan that then found nothing exited 0
printing nothing - so a typo in `--roots`, a moved home directory and a
genuinely empty tree were one single output, with the nevner never reported.
Every bad root is now NAMED on stderr; the exit status changes only when NO
root was scanned at all (**3**, matching `coord-count.sh`'s F5 fix for the
same reason). **A mix still exits 0 and still prints the board** - some repos
really were scanned, so the board is real; the reader just also learns that
one root contributed nothing because it is absent rather than because it is
empty. An EXISTING but empty root stays a silent, clean 0. This REPLACES an
older assertion reading `check "missing root is a clean no-op"` - the defect
encoded as a passing test, which is why nothing ever caught it.
`brief-nightly.sh` needed no change and gains from this: it already treats a
nonzero `--brief` as failure and keeps yesterday's briefing, so a mistyped
root now preserves the file instead of overwriting it with an empty render.
**Ordre 20260818T124828Z (2026-08-18): repo discovery had no dotglob, so a
dot-prefixed repo (`.profile`, the Forgejo/GitHub org-profile convention)
was permanently invisible to the board - not listed, and `--dispatch
@ -923,11 +1029,11 @@ obligations in another repo.
- Zero dependencies everywhere: bash + coreutils in the engine, `node:`
builtins only in hook and tests.
- TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (230/230),
`bash scripts/board-selftest.sh` must exit 0 (281/281),
`bash scripts/coord-selftest.sh` must exit 0 (242/242),
`bash scripts/board-selftest.sh` must exit 0 (300/300),
`bash scripts/route-selftest.sh` must exit 0 (69/69),
`bash scripts/orders-selftest.sh` must exit 0 (110/110) and
`bash scripts/state-line-guard-selftest.sh` must exit 0 (40/40).
`bash scripts/state-line-guard-selftest.sh` must exit 0 (54/54).
- English for all code, docs, and commit messages (public repo). Norwegian
trigger aliases in the skill description are deliberate.
- Conventional Commits: `type(scope): description`.