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-done.sh` archives, `coord-count.sh` counts without delivering,
`coord-sweep.sh` closes the aged FYI backlog machine-wide. `coord-sweep.sh` closes the aged FYI backlog machine-wide.
Everything is pinned by `coord-selftest.sh` 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 **`ktg-plugin-marketplace` is a RETIRED `--to` address (operator decision
2026-08-15), rejected rather than redirected.** It is a polyrepo directory, 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 field means a reply IS owed, because every message written before 0.11.0
lacks it. 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 **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 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 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 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 `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 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 fixture where only counting every `replace_all` occurrence produces the
correct denial. Anything the hook cannot project with confidence — a correct denial. Anything the hook cannot project with confidence — a
missing file, an `old_string` that is not present, fields of the wrong 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, Pinned by state-line-guard-selftest.sh section 9 (`$\`` as the real repro,
`$&` as a second sequence proving the fix is general). `$&` 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 **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 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: 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 - **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
next-step blocks + board lines, `git status`, and mailbox pending counts, and 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 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 **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 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 find a live one when planted - never behaviourally, because there is no
behaviour to pin.** 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 **Ordre 20260818T124828Z (2026-08-18): repo discovery had no dotglob, so a
dot-prefixed repo (`.profile`, the Forgejo/GitHub org-profile convention) dot-prefixed repo (`.profile`, the Forgejo/GitHub org-profile convention)
was permanently invisible to the board - not listed, and `--dispatch 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:` - Zero dependencies everywhere: bash + coreutils in the engine, `node:`
builtins only in hook and tests. builtins only in hook and tests.
- TDD: no behavior change without a failing selftest check first. - TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (230/230), `bash scripts/coord-selftest.sh` must exit 0 (242/242),
`bash scripts/board-selftest.sh` must exit 0 (281/281), `bash scripts/board-selftest.sh` must exit 0 (300/300),
`bash scripts/route-selftest.sh` must exit 0 (69/69), `bash scripts/route-selftest.sh` must exit 0 (69/69),
`bash scripts/orders-selftest.sh` must exit 0 (110/110) and `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 - English for all code, docs, and commit messages (public repo). Norwegian
trigger aliases in the skill description are deliberate. trigger aliases in the skill description are deliberate.
- Conventional Commits: `type(scope): description`. - Conventional Commits: `type(scope): description`.

View file

@ -5,6 +5,10 @@
// ~120-line convention (global CLAUDE.md's Kontinuitets-system section; // ~120-line convention (global CLAUDE.md's Kontinuitets-system section;
// raised from ~60 by operator decision 2026-08-14). // raised from ~60 by operator decision 2026-08-14).
// //
// Env: CLAUDE_STATE_MAX_LINES overrides that limit (positive integer). An
// UNUSABLE value is refused by name, never silently ignored - see
// resolveMaxLines() below for why that direction is the safe one.
//
// PreToolUse, not PostToolUse: org-ops' work order (20260814T144553Z) asked // PreToolUse, not PostToolUse: org-ops' work order (20260814T144553Z) asked
// for a PostToolUse hook, but PostToolUse fires AFTER the tool already ran // for a PostToolUse hook, but PostToolUse fires AFTER the tool already ran
// and cannot undo the write (confirmed against the official hooks docs, // and cannot undo the write (confirmed against the official hooks docs,
@ -112,7 +116,34 @@ import { readFileSync } from 'node:fs';
import { basename, dirname } from 'node:path'; import { basename, dirname } from 'node:path';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
const MAX_LINES = 120; const DEFAULT_MAX_LINES = 120;
// F13: the limit was a bare constant, so a selftest of the BOUNDARY had to
// hardcode the same number the code carries - two copies of one policy, and
// every fixture had to be rewritten by hand the last time the operator moved
// it (60 -> 120, 2026-08-14). CLAUDE_STATE_MAX_LINES is the same kind of knob
// CLAUDE_COORD_DIR is for the mailbox root: it lets a test pin the boundary at
// a cheap value, and it lets the operator move the limit without a release.
//
// It is not a bypass claim. This guard has always been escapable by writing
// the file another way (Bash, an editor), exactly as the sibling pathguard is.
// The one thing it must never do is silently fail to take effect, which is why
// an UNUSABLE value returns null and is refused by name below rather than
// falling back to the default: a caller who set the variable and got 120
// anyway would be reading a limit that was never in force - the same
// positive-looking null this whole class of fix exists to close.
//
// Refused: "" (a variable expanded from something unset - a value was meant),
// "0" and negatives (a limit no write can satisfy), and anything not made of
// digits ("abc", "12.5", "1e3"). Unset is NOT unusable; it is the normal case.
function resolveMaxLines() {
const raw = process.env.CLAUDE_STATE_MAX_LINES;
if (raw === undefined) return DEFAULT_MAX_LINES;
if (!/^[0-9]+$/.test(raw)) return null;
const n = Number(raw);
if (!Number.isSafeInteger(n) || n < 1) return null;
return n;
}
function allow() { function allow() {
process.exit(0); process.exit(0);
@ -150,6 +181,22 @@ if (
allow(); allow();
} }
// Resolved here, AFTER the STATE.md gate above: an unusable override must not
// block a Write this guard would never have judged in the first place.
const MAX_LINES = resolveMaxLines();
if (MAX_LINES === null) {
process.stderr.write(
`\n[repo-mailbox] STATE LINE GUARD: ${toolName} blocked\n` +
` File: ${filePath}\n` +
` CLAUDE_STATE_MAX_LINES is set to ${JSON.stringify(process.env.CLAUDE_STATE_MAX_LINES)}, ` +
`which is not a positive whole number of lines.\n\n` +
`The limit was NOT applied and the write was NOT judged. Set ` +
`CLAUDE_STATE_MAX_LINES to a positive integer, or unset it to use the ` +
`default of ${DEFAULT_MAX_LINES}.\n`
);
process.exit(2);
}
let projected; let projected;
let currentLines; let currentLines;
if (toolName === 'Write') { if (toolName === 'Write') {

View file

@ -498,8 +498,13 @@ check "NESTE block of only a multi-line comment reports an empty block" $?
"$BOARD" --help >/dev/null 2>&1; check "--help exits 0" $? "$BOARD" --help >/dev/null 2>&1; check "--help exits 0" $?
# Unreadable root is a no-op, not a crash. # A root that does not exist is NOT a clean no-op - it is an unmeasured scan,
"$BOARD" --roots "$ROOT/does-not-exist" >/dev/null 2>&1; check "missing root is a clean no-op" $? # and it used to be indistinguishable from an empty tree. Pinned in full by
# section 26 below (F10); asserted here only as "it does not crash".
mr_rc=0
"$BOARD" --roots "$ROOT/does-not-exist" >/dev/null 2>&1 || mr_rc=$?
[ "$mr_rc" -eq 3 ]
check "missing root exits nonzero (3) without crashing" $?
# --- 8. The documented next-cost form IS the convention -------------------- # --- 8. The documented next-cost form IS the convention --------------------
# The cost field is free text on purpose: the parser reads to ';' or '-->' so # The cost field is free text on purpose: the parser reads to ';' or '-->' so
@ -2283,6 +2288,164 @@ check "known-positive: the process-inspection grep can find a planted pgrep call
/bin/rm -rf "$FLY_ROOT" 2>/dev/null /bin/rm -rf "$FLY_ROOT" 2>/dev/null
# --- 25. F6: a git FAILURE must not read as a clean tree -------------------
# `git -C "$d" status --porcelain 2>/dev/null | wc -l` yields 0 lines whether
# the tree is clean or git refused to answer at all, so DRT printed 0 and every
# reader - the table, the SUM, and --plan's free-capacity test - took that as
# "nothing uncommitted here". Verifiseringsloven ansikt 4 on our own tooling.
#
# The fixture is a REAL reachable shape, not a contrived one: a `.git` FILE
# pointing at a worktree parent that no longer exists. Discovery tests `.git`
# with -e precisely so worktrees are found (see board.sh's own comment), so
# this directory IS scanned as a repo and `git status` exits 128 on it.
# Measured before the fixture was written: rc=128, zero lines on stdout.
F6ROOT="$(mktemp -d)"
F6COORD="$(mktemp -d)"
# The known-positive controls come first and are the reason this section can
# fail in both directions: if the fix widened "unknown" to cover healthy repos,
# these go red while every negative check below stays green.
mkrepo "$F6ROOT/f6-clean"
{
echo "# STATE - f6-clean"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=done; blocked-on=-; next-cost=Sonnet 5/low -->"
echo "Ingenting."
} > "$F6ROOT/f6-clean/STATE.md"
git -C "$F6ROOT/f6-clean" add -A >/dev/null 2>&1
git -C "$F6ROOT/f6-clean" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1
mkrepo "$F6ROOT/f6-dirty"
{
echo "# STATE - f6-dirty"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=done; blocked-on=-; next-cost=Sonnet 5/low -->"
echo "Ingenting."
} > "$F6ROOT/f6-dirty/STATE.md"
git -C "$F6ROOT/f6-dirty" add -A >/dev/null 2>&1
git -C "$F6ROOT/f6-dirty" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1
echo "uncommitted" > "$F6ROOT/f6-dirty/scratch.txt"
# The defect case: scanned as a repo, but git cannot answer.
mkdir -p "$F6ROOT/f6-broken"
echo "gitdir: $F6ROOT/f6-broken-parent-that-does-not-exist/.git/worktrees/x" \
> "$F6ROOT/f6-broken/.git"
{
echo "# STATE - f6-broken"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=done; blocked-on=-; next-cost=Sonnet 5/low -->"
echo "Ingenting."
} > "$F6ROOT/f6-broken/STATE.md"
# Ground truth for the fixture itself, so a later git version that starts
# answering here fails THIS check rather than silently disarming the section.
git -C "$F6ROOT/f6-broken" status --porcelain >/dev/null 2>&1
[ $? -ne 0 ]
check "F6: fixture ground truth - git really does fail on the broken worktree" $?
F6TBL="$(CLAUDE_COORD_DIR="$F6COORD" "$BOARD" --roots "$F6ROOT" 2>/dev/null)"
# DRT is counted from the RIGHT (NF-3: DRT ALDER SISTE NESTE), never from the
# left: the KOST field is free text and its realistic spelling ("Sonnet 5/low")
# contains a space, so $7 reads FLY instead. Measured, not assumed - the first
# cut of this section passed its clean-tree control by accident that way.
printf '%s' "$F6TBL" | grep -E '^f6-clean[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '0'
check "F6: control - a healthy CLEAN tree still reads DRT 0" $?
printf '%s' "$F6TBL" | grep -E '^f6-dirty[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '1'
check "F6: control - a healthy DIRTY tree still reads DRT 1" $?
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '0'
[ $? -ne 0 ]
check "F6: a git failure does NOT read as DRT 0 (clean tree)" $?
printf '%s' "$F6TBL" | grep -E '^f6-broken[[:space:]]' | awk '{print $(NF-3)}' | grep -qx '?'
check "F6: a git failure reads as ? (not measured), the same token coord-count uses" $?
# The load-bearing consequence. All three fixtures are status=done with no mail
# and no orders, so the ONLY thing separating them is the tree reading. A repo
# whose tree could not be measured is not provably free, and certifying it free
# is how an unmeasured null becomes a fact about the world.
F6PLAN="$(CLAUDE_COORD_DIR="$F6COORD" "$BOARD" --roots "$F6ROOT" --plan 2>/dev/null)"
printf '%s' "$F6PLAN" | grep -q '^ledig=f6-clean '
check "F6: control - the clean done repo IS still reported as free capacity" $?
printf '%s' "$F6PLAN" | grep -q '^ledig=f6-dirty '
[ $? -ne 0 ]
check "F6: control - the dirty done repo is still NOT free capacity" $?
printf '%s' "$F6PLAN" | grep -q '^ledig=f6-broken '
[ $? -ne 0 ]
check "F6: an unmeasurable tree is NOT certified as free capacity" $?
# The SUM line adds field 7 across every repo. An unknown adds 0 there, which
# silently understates the total under a heading the operator reads as counted.
# It must be NAMED, on the same principle --plan already applies to repos with
# no board line: labelled, never silently dropped.
printf '%s' "$F6TBL" | grep -q 'MERK: 1 repo har umaalbart arbeidstre'
check "F6: the SUM roll-up names the repo whose tree could not be measured" $?
printf '%s' "$F6TBL" | grep -qE 'SUM: [0-9]+ uhaandterte innboks-meldinger, 1 ukommiterte filer'
check "F6: control - the SUM still counts the one real uncommitted file" $?
/bin/rm -rf "$F6ROOT" "$F6COORD" 2>/dev/null
# --- 26. F10: a scan root that does not exist is not an empty tree ----------
# `[ -d "$root" ] || continue` skipped a bad root silently, and a scan that
# found no repo at all then exited 0 printing nothing - so a typo in --roots,
# a moved home directory and a genuinely empty tree were one single output.
# This section REPLACES the older "missing root is a clean no-op" assertion,
# which encoded exactly this defect as a passing test.
F10ROOT="$(mktemp -d)"
F10COORD="$(mktemp -d)"
mkrepo "$F10ROOT/f10-repo"
{
echo "# STATE - f10-repo"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=planned; blocked-on=-; next-cost=Sonnet 5/low -->"
echo "Et steg."
} > "$F10ROOT/f10-repo/STATE.md"
# Known-positive control FIRST: a valid root still scans, still exits 0, and -
# critically - still says NOTHING on stderr. A warning on the ordinary path
# would train every reader to ignore the channel the defect case needs.
f10a_err="${TMPDIR:-/tmp}/f10a.err.$$"
f10a_out="$(CLAUDE_COORD_DIR="$F10COORD" "$BOARD" --roots "$F10ROOT" 2>"$f10a_err")"; f10a_rc=$?
[ "$f10a_rc" -eq 0 ] && printf '%s' "$f10a_out" | grep -q 'f10-repo' && [ ! -s "$f10a_err" ]
check "F10: control - a valid root scans, exits 0, and warns about nothing" $?
# An EXISTING but empty root is the genuine "no repos here" answer and must
# stay a silent, clean 0 - it is the reading the defect case used to be
# indistinguishable from.
F10EMPTY="$(mktemp -d)"
f10b_err="${TMPDIR:-/tmp}/f10b.err.$$"
f10b_out="$(CLAUDE_COORD_DIR="$F10COORD" "$BOARD" --roots "$F10EMPTY" 2>"$f10b_err")"; f10b_rc=$?
[ "$f10b_rc" -eq 0 ] && [ -z "$f10b_out" ] && [ ! -s "$f10b_err" ]
check "F10: control - an existing but EMPTY root is a silent, clean 0" $?
# The defect: the root is not there at all.
f10c_err="${TMPDIR:-/tmp}/f10c.err.$$"
f10c_out="$(CLAUDE_COORD_DIR="$F10COORD" "$BOARD" --roots "$F10ROOT/no-such-dir" 2>"$f10c_err")"; f10c_rc=$?
[ "$f10c_rc" -eq 3 ]
check "F10: a scan root that does not exist exits 3, not 0" $?
[ -z "$f10c_out" ]
check "F10: a missing root prints no board (3 is not an empty board)" $?
grep -q 'no-such-dir' "$f10c_err"
check "F10: the missing root is NAMED on stderr, not merely counted" $?
[ "$f10b_rc" -ne "$f10c_rc" ]
check "F10: empty root and missing root no longer report the same status" $?
# A MIX is the case that decides whether this is a warning or a failure: some
# repos really were scanned, so the board is real and must be printed - but the
# reader must still learn that one root contributed nothing because it is not
# there, rather than because it is empty.
f10d_err="${TMPDIR:-/tmp}/f10d.err.$$"
f10d_out="$(CLAUDE_COORD_DIR="$F10COORD" "$BOARD" --roots "$F10ROOT,$F10ROOT/no-such-dir" 2>"$f10d_err")"; f10d_rc=$?
[ "$f10d_rc" -eq 0 ]
check "F10: one good root among bad ones still exits 0 - the board is real" $?
printf '%s' "$f10d_out" | grep -q 'f10-repo'
check "F10: the good root's repos are still scanned when a sibling root is bad" $?
grep -q 'no-such-dir' "$f10d_err"
check "F10: the bad root is still named on stderr in the mixed case" $?
/bin/rm -rf "$F10ROOT" "$F10COORD" "$F10EMPTY" "$f10a_err" "$f10b_err" \
"$f10c_err" "$f10d_err" 2>/dev/null
echo "" echo ""
echo "board-selftest: $PASS passed, $FAIL failed" echo "board-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1 [ "$FAIL" -eq 0 ] || exit 1

View file

@ -313,8 +313,20 @@ $dotentry"
OLD_IFS="$IFS"; IFS=',' OLD_IFS="$IFS"; IFS=','
set -- $ROOTS set -- $ROOTS
IFS="$OLD_IFS" IFS="$OLD_IFS"
# F10: `[ -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.
# Verifiseringsloven ansikt 4: the nevner was never reported, so "no repos"
# could not be told apart from "nothing was looked at". Every bad root is now
# NAMED on stderr; the exit status only changes when NO root was scanned at all
# (below), because a mix still produces a real board that must be printed.
ROOTS_OK=0
for root in "$@"; do for root in "$@"; do
[ -d "$root" ] || continue if [ ! -d "$root" ]; then
echo "board: scan root does not exist: $root (contributed nothing - not scanned, not empty)" >&2
continue
fi
ROOTS_OK=$((ROOTS_OK + 1))
add_dot_repos "$root" add_dot_repos "$root"
for entry in "$root"/*; do for entry in "$root"/*; do
[ -d "$entry" ] || continue [ -d "$entry" ] || continue
@ -334,6 +346,17 @@ $child"
done done
done done
# Status 3 rather than 2, matching coord-count.sh's F5 fix: 2 already means
# "you called me wrong" everywhere in this script (a flag missing its value, an
# unknown --repo), and this is "the world you named is not there". stdout stays
# empty on purpose - 3 is not an empty board, it is the absence of a scan.
if [ "$ROOTS_OK" -eq 0 ]; then
echo "board: no scan root exists (roots: $ROOTS) - nothing was scanned, and that is NOT an empty board" >&2
exit 3
fi
# Roots existed and were scanned; finding no repo in them is a real, measured
# answer and stays a silent, clean 0.
[ -n "$(printf '%s' "$REPOS" | tr -d '[:space:]')" ] || exit 0 [ -n "$(printf '%s' "$REPOS" | tr -d '[:space:]')" ] || exit 0
# --- Collect one record per repo ------------------------------------------- # --- Collect one record per repo -------------------------------------------
@ -346,8 +369,28 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
name="$(basename "$d")" name="$(basename "$d")"
state="$d/STATE.md" state="$d/STATE.md"
dirty="$(git -C "$d" status --porcelain 2>/dev/null | wc -l | tr -d ' ')" # F6: `git status | wc -l` yields 0 lines whether the tree is CLEAN or git
[ -n "$dirty" ] || dirty=0 # refused to answer at all, so a failure used to print DRT=0 and every reader
# took it as "nothing uncommitted here" - Verifiseringsloven ansikt 4 on our
# own tooling. It is reachable, not theoretical: discovery tests .git with -e
# so worktrees are found, and a worktree whose parent checkout was deleted
# exits 128 here (measured). 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, and for the same fail-safe reason: not
# measured must never render as the reassuring value.
#
# Every shell consumer of this field prints it with %s and is unaffected. The
# two awk consumers are NOT, and both are handled where they read it: the
# free-capacity test (a "?" is not free) and the SUM roll-up (a "?" is named,
# never silently added as 0).
dirty_out="$(git -C "$d" status --porcelain 2>/dev/null)"; dirty_rc=$?
if [ "$dirty_rc" -ne 0 ]; then
dirty="?"
elif [ -z "$dirty_out" ]; then
dirty=0
else
dirty="$(printf '%s\n' "$dirty_out" | wc -l | tr -d ' ')"
fi
inbox=0 inbox=0
if [ -d "$COORD/$name/inbox" ]; then if [ -d "$COORD/$name/inbox" ]; then
@ -1363,10 +1406,15 @@ plan() {
awk -F'|' -v OWF="$OWED" ' awk -F'|' -v OWF="$OWED" '
FILENAME==OWF { ow[$1] = $2 + 0; next } FILENAME==OWF { ow[$1] = $2 + 0; next }
{ {
name = $3; status = $4; dirty = $7 + 0; orders = $12 + 0; claimed = $13 + 0 name = $3; status = $4; dirty = $7; orders = $12 + 0; claimed = $13 + 0
owed = (name in ow) ? ow[name] : 0 owed = (name in ow) ? ow[name] : 0
if (status != "done" && status != "deferred") next if (status != "done" && status != "deferred") next
if (owed > 0 || orders > 0 || claimed > 0 || dirty > 0) next # dirty is compared as a STRING against exactly "0", never as $7 + 0: a
# "?" (git could not answer, see F6 above) coerces to 0 in arithmetic and
# would certify an UNMEASURED tree as free capacity - the precise shape
# of the defect this whole column change exists to close. Only a tree
# measured clean is clean.
if (owed > 0 || orders > 0 || claimed > 0 || dirty != "0") next
print name " (" status ")" print name " (" status ")"
} }
' "$OWED" "$RECORDS" | sort > "$lf" ' "$OWED" "$RECORDS" | sort > "$lf"
@ -1564,9 +1612,15 @@ tot_in="$(awk -F'|' '{s+=$6} END{print s+0}' "$RECORDS")"
tot_dirty="$(awk -F'|' '{s+=$7} END{print s+0}' "$RECORDS")" tot_dirty="$(awk -F'|' '{s+=$7} END{print s+0}' "$RECORDS")"
n_mal="$(grep -c 'MALFORMED' "$RECORDS" 2>/dev/null | tr -d ' ')" n_mal="$(grep -c 'MALFORMED' "$RECORDS" 2>/dev/null | tr -d ' ')"
n_nofield="$(awk -F'|' '$4=="?"' "$RECORDS" | wc -l | tr -d ' ')" n_nofield="$(awk -F'|' '$4=="?"' "$RECORDS" | wc -l | tr -d ' ')"
# tot_dirty above sums field 7, where a "?" contributes 0 - so the total is
# short by an unknown amount whenever git failed anywhere. Labelled rather than
# silently dropped, the same rule --plan already applies to repos with no board
# line: the number stays honest about what it did NOT include.
n_unmeas="$(awk -F'|' '$7=="?"' "$RECORDS" | wc -l | tr -d ' ')"
printf '\nSUM: %s uhaandterte innboks-meldinger, %s ukommiterte filer.\n' "$tot_in" "$tot_dirty" printf '\nSUM: %s uhaandterte innboks-meldinger, %s ukommiterte filer.\n' "$tot_in" "$tot_dirty"
[ "${n_mal:-0}" -gt 0 ] && printf 'ADVARSEL: %s repo har MALFORMED status-token (utenfor det lukkede settet).\n' "$n_mal" [ "${n_mal:-0}" -gt 0 ] && printf 'ADVARSEL: %s repo har MALFORMED status-token (utenfor det lukkede settet).\n' "$n_mal"
[ "${n_nofield:-0}" -gt 0 ] && printf 'MERK: %s repo mangler board-linje - status/kost er ukjent (?), NESTE-utdrag brukes.\n' "$n_nofield" [ "${n_nofield:-0}" -gt 0 ] && printf 'MERK: %s repo mangler board-linje - status/kost er ukjent (?), NESTE-utdrag brukes.\n' "$n_nofield"
[ "${n_unmeas:-0}" -gt 0 ] && printf 'MERK: %s repo har umaalbart arbeidstre - git svarte ikke, DRT er ? og IKKE med i summen over.\n' "$n_unmeas"
exit 0 exit 0

View file

@ -35,7 +35,20 @@
# --exclude <mailbox> omit one mailbox (the caller's own, whose inbox is # --exclude <mailbox> omit one mailbox (the caller's own, whose inbox is
# already injected in full). # already injected in full).
# Env: CLAUDE_COORD_DIR overrides the mailbox root. # Env: CLAUDE_COORD_DIR overrides the mailbox root.
# Exit: always 0 - this runs at session start and must never fail one. # Exit: 0 = counted (zero or more mailboxes have pending mail)
# 2 = usage error, nothing counted
# 3 = mailbox root does not exist, nothing counted
#
# The header used to promise exit 0 unconditionally, on the grounds that this
# runs at session start and must never fail one - and that was false in both
# directions (F14). It exited 2 on a usage error already, and - worse - it exited 0 with zero lines when the mailbox root
# was ABSENT, which is byte-identical to "no mailbox has pending mail" on every
# channel a consumer can read (F5). board.sh consumes this TSV. That is
# Verifiseringsloven ansikt 4: a broken query returning a positive-looking null.
# What the old claim was protecting is kept and made precise: no state OF THE
# MAILBOX can ever produce a nonzero exit - not an empty root, not a malformed
# message, not an unreadable date. Only the caller (2) or a missing root (3)
# can, and both print nothing on stdout, so neither can be mistaken for a count.
# ASCII only, bash 3.2 safe. # ASCII only, bash 3.2 safe.
set -u set -u
export LC_ALL=C export LC_ALL=C
@ -56,7 +69,15 @@ while [ $# -gt 0 ]; do
esac esac
done done
[ -d "$COORD" ] || exit 0 # Not `|| exit 0`: see the F5 paragraph in the header. Status 3 rather than 2
# because 2 is already "you called me wrong" and this is "the world you named
# is not there" - two different repairs, and a consumer that only ever sees one
# integer cannot tell them apart. stdout stays empty on purpose: 3 is not a
# count of zero, it is the absence of a count.
if [ ! -d "$COORD" ]; then
echo "coord-count: mailbox root does not exist: $COORD (not counted, not zero)" >&2
exit 3
fi
# GNU/BSD date flavor, detected once per run (not per mailbox): BSD date # GNU/BSD date flavor, detected once per run (not per mailbox): BSD date
# rejects --version outright (exit nonzero, "illegal option" - measured on # rejects --version outright (exit nonzero, "illegal option" - measured on

View file

@ -1159,6 +1159,97 @@ check "F7(f): control - a dot-prefixed target name still delivers" $?
/bin/rm -rf "$F7DIR" 2>/dev/null /bin/rm -rf "$F7DIR" 2>/dev/null
# 36. F5 - a missing mailbox ROOT and an empty one are two different facts, and
# coord-count.sh used to report them identically: `[ -d "$COORD" ] || exit 0`,
# zero lines on stdout, exit 0, nothing on stderr. A consumer reading that TSV
# (board.sh does) cannot tell "no mailbox has pending mail" from "the root I
# was pointed at is not there" - Verifiseringsloven ansikt 4 exactly: a broken
# query returning a positive-looking null, consumed as a fact about the world.
# The two are now distinguishable by EXIT STATUS and a stderr line. Status 3 is
# new and deliberately not 2: 2 already means "you called me wrong" (a usage
# error, nothing counted), 3 means "the world you named is not there". Both
# print nothing on stdout, so no consumer can mistake either for a count.
#
# The three cases below are the whole point - they must not collapse into one.
F5DIR="$(mktemp -d)"
# (a) The known-positive control FIRST: an EXISTING root holding real mail must
# still count it. A guard that exits 3 on everything would pass every negative
# check below while having destroyed the script, and this is what proves the
# query can still find.
CLAUDE_COORD_DIR="$F5DIR" "$SEND" --to f5box --from tester --subject s --message m >/dev/null 2>&1
f5a_err="$F5DIR/../f5a.err"
f5a_out="$(CLAUDE_COORD_DIR="$F5DIR" "$COUNT" 2>"$f5a_err")"; f5a_rc=$?
[ "$f5a_rc" -eq 0 ] && printf '%s' "$f5a_out" | grep -q '^f5box 1 1 '
check "F5(a): control - an existing root with mail still counts it and exits 0" $?
# (b) An EXISTING but EMPTY root: the genuine "nobody has pending mail" answer.
# Exit 0, no stdout, and NOTHING on stderr - a warning here would make the
# ordinary case noisy and train every reader to ignore the channel that case
# (c) needs.
F5EMPTY="$(mktemp -d)"
f5b_err="$F5DIR/../f5b.err"
f5b_out="$(CLAUDE_COORD_DIR="$F5EMPTY" "$COUNT" 2>"$f5b_err")"; f5b_rc=$?
[ "$f5b_rc" -eq 0 ] && [ -z "$f5b_out" ] && [ ! -s "$f5b_err" ]
check "F5(b): an existing but empty root is a silent, clean zero (exit 0)" $?
# (c) The defect: a root that does not exist. Pre-fix this was byte-identical
# to (b) on every channel a consumer can read.
f5c_err="$F5DIR/../f5c.err"
f5c_out="$(CLAUDE_COORD_DIR="$F5DIR/no/such/root" "$COUNT" 2>"$f5c_err")"; f5c_rc=$?
[ "$f5c_rc" -eq 3 ]
check "F5(c): a missing mailbox root exits 3, not 0" $?
[ -z "$f5c_out" ]
check "F5(c): a missing root still prints NO count line (3 is not a count)" $?
[ -s "$f5c_err" ] && grep -q 'coord-count' "$f5c_err" && grep -q 'not counted' "$f5c_err"
check "F5(c): a missing root says so on stderr, naming what was not measured" $?
# (d) Ground truth that (b) and (c) really are distinguishable now. This is the
# defect stated as one predicate rather than as three separate assertions: it
# fails if any future change collapses the two readings again, including one
# that keeps both exit codes but drops the stderr line.
[ "$f5b_rc" -ne "$f5c_rc" ]
check "F5(d): empty root and missing root no longer report the same status" $?
/bin/rm -rf "$F5EMPTY" "$F5DIR/../f5a.err" "$F5DIR/../f5b.err" "$F5DIR/../f5c.err" 2>/dev/null
# 37. F14 - the header's exit contract said "always 0" while the script exited
# 2 on `--exclude` with no value, and now exits 3 on a missing root (36 above).
# The header IS the contract: it is what `-h` prints, so a consumer that reads
# it and trusts it is reading a false claim. Pinned as a check on the HELP TEXT
# for the same reason board-selftest pins the FLY legend wording - the text is
# engine behavior, not prose, and a doc line nothing tests is a doc line that
# drifts.
f14_help="$(CLAUDE_COORD_DIR="$F5DIR" "$COUNT" --help 2>/dev/null)"; f14_rc=$?
[ "$f14_rc" -eq 0 ] && [ -n "$f14_help" ] && printf '%s' "$f14_help" | grep -q '^Exit:'
check "F14: control - --help still exits 0 and prints an Exit: contract" $?
# Ground truth for the claim the old header contradicted.
f14b_rc=0
CLAUDE_COORD_DIR="$F5DIR" "$COUNT" --exclude >/dev/null 2>&1 || f14b_rc=$?
[ "$f14b_rc" -eq 2 ]
check "F14: ground truth - --exclude with no value really does exit 2" $?
printf '%s' "$f14_help" | grep -q 'always 0'
[ $? -ne 0 ]
check "F14: the header no longer claims the script always exits 0" $?
printf '%s' "$f14_help" | grep -q '2 *= *usage error'
check "F14: the header documents exit 2 (usage error, nothing counted)" $?
printf '%s' "$f14_help" | grep -q '3 *= *mailbox root'
check "F14: the header documents exit 3 (missing root, nothing counted)" $?
# The reason the "always 0" claim existed at all must survive its removal: the
# session-start path must still not be failable by mailbox STATE. Every exit
# above 0 is a caller/world error, never "this mailbox has awkward contents".
f14c_rc=0
CLAUDE_COORD_DIR="$F5DIR" "$COUNT" --exclude f5box >/dev/null 2>&1 || f14c_rc=$?
[ "$f14c_rc" -eq 0 ]
check "F14: control - a correct call over a real root still exits 0" $?
/bin/rm -rf "$F5DIR" 2>/dev/null
echo "----" echo "----"
echo "PASS=$PASS FAIL=$FAIL" echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ] [ "$FAIL" -eq 0 ]

View file

@ -565,6 +565,114 @@ run_hook "$P"
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: correcting done -> blocked is allowed with unpushed commits" $? [ "$HOOK_EXIT" -eq 0 ]; check "Edit: correcting done -> blocked is allowed with unpushed commits" $?
unset SG_PATH SG_CONTENT unset SG_PATH SG_CONTENT
# --- 11. F13: the line limit is overridable, and an unusable override is loud
# The limit was a bare `const MAX_LINES = 120`, so a test of the BOUNDARY had
# no choice but to hardcode 120 in every fixture - which means the tests and
# the code encoded the same number twice, and section 1's boundary fixtures
# would have to be rewritten by hand the next time the operator moves it (they
# already were once, 60 -> 120). CLAUDE_STATE_MAX_LINES makes the boundary
# testable at a cheap value AND gives the operator the same knob
# CLAUDE_COORD_DIR gives them over the mailbox root.
#
# The override is not a bypass claim: this guard has always been escapable by
# writing the file some other way (Bash, an editor), exactly as the sibling
# pathguard is. What it must never do is silently NOT take effect.
# Control first, at the DEFAULT: with no override set, the shipped limit still
# governs. This is what proves the denials below come from the override rather
# than from the guard having become stricter for everyone.
unset CLAUDE_STATE_MAX_LINES
P="$(payload '
const content = "x\n".repeat(6);
process.stdout.write(JSON.stringify({
tool_name: "Write",
tool_input: { file_path: "/tmp/f13/STATE.md", content }
}));
')"
run_hook "$P"
[ "$HOOK_EXIT" -eq 0 ]
check "F13: control - a 6-line new STATE.md is allowed at the default limit" $?
# The override takes effect, in BOTH directions. One assertion alone would not
# do: a broken parse that clamped everything to 0 would deny the 6-line file
# and look like a working override.
CLAUDE_STATE_MAX_LINES=5
export CLAUDE_STATE_MAX_LINES
run_hook "$P"
[ "$HOOK_EXIT" -eq 2 ]
check "F13: an override of 5 denies a 6-line new STATE.md" $?
printf '%s' "$HOOK_STDERR" | grep -q 'max 5'
check "F13: the denial message quotes the OVERRIDDEN limit, not the default" $?
P5="$(payload '
const content = "x\n".repeat(5);
process.stdout.write(JSON.stringify({
tool_name: "Write",
tool_input: { file_path: "/tmp/f13/STATE.md", content }
}));
')"
run_hook "$P5"
[ "$HOOK_EXIT" -eq 0 ]
check "F13: exactly-at-the-override is still allowed (boundary, not off by one)" $?
# The ratchet is a property of the guard, not of the constant, so it must
# survive the override: an already-oversized file can still be edited toward
# compliance. Same rule section 8 pins at the default.
# The file must be named exactly STATE.md and the variable must be exported
# BEFORE node reads it. Both were wrong in this section's first cut, and the
# check went GREEN anyway - the basename gate let the write through without
# measuring a thing, and the fixture file was never created. A vacuous pass
# is the very defect this order is closing, so the fixture asserts its own
# ground truth before the check that depends on it.
mkdir -p "$TMPDIR/f13-ratchet"
export SG_BIG="$TMPDIR/f13-ratchet/STATE.md"
node -e 'require("fs").writeFileSync(process.env.SG_BIG, "y\n".repeat(40))'
[ "$(wc -l < "$SG_BIG" | tr -d ' ')" = "40" ]
check "F13: fixture ground truth - the oversized STATE.md really is 40 lines" $?
P="$(payload '
process.stdout.write(JSON.stringify({
tool_name: "Write",
tool_input: { file_path: process.env.SG_BIG, content: "y\n".repeat(20) }
}));
')"
run_hook "$P"
[ "$HOOK_EXIT" -eq 0 ]
check "F13: the ratchet survives the override (40 -> 20, still over 5, allowed)" $?
# The inverse, so the check above cannot pass by the guard simply never firing
# on this path: GROWING the same oversized file is still denied at 5.
P="$(payload '
process.stdout.write(JSON.stringify({
tool_name: "Write",
tool_input: { file_path: process.env.SG_BIG, content: "y\n".repeat(60) }
}));
')"
run_hook "$P"
[ "$HOOK_EXIT" -eq 2 ]
check "F13: growing that same oversized file is still denied under the override" $?
# An override that cannot be used is DENIED, never silently ignored. A silent
# fallback to 120 is the exact defect class this whole order is closing: the
# caller would believe a limit was in force that never was, and a selftest
# would go green having measured the default while claiming to measure 5.
# Failing here is recoverable in one action (unset the variable) and the
# message says which one.
for bad in "0" "-3" "abc" "" "12.5" "1e3"; do
CLAUDE_STATE_MAX_LINES="$bad"
export CLAUDE_STATE_MAX_LINES
run_hook "$P5"
[ "$HOOK_EXIT" -eq 2 ] && printf '%s' "$HOOK_STDERR" | grep -q 'CLAUDE_STATE_MAX_LINES'
check "F13: an unusable override ('$bad') is refused by name, not ignored" $?
done
# ...and an UNSET variable is not an unusable one. Without this the check above
# would pass against a guard that refused every write on the planet.
unset CLAUDE_STATE_MAX_LINES
run_hook "$P5"
[ "$HOOK_EXIT" -eq 0 ]
check "F13: control - an UNSET override is the normal case, not a refusal" $?
unset SG_BIG
echo "" echo ""
echo "state-line-guard-selftest: $PASS passed, $FAIL failed" echo "state-line-guard-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1 [ "$FAIL" -eq 0 ] || exit 1