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:
parent
3beef2a603
commit
d8fdeaa991
7 changed files with 606 additions and 16 deletions
118
CLAUDE.md
118
CLAUDE.md
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
// ~120-line convention (global CLAUDE.md's Kontinuitets-system section;
|
||||
// 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
|
||||
// for a PostToolUse hook, but PostToolUse fires AFTER the tool already ran
|
||||
// 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 { 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() {
|
||||
process.exit(0);
|
||||
|
|
@ -150,6 +181,22 @@ if (
|
|||
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 currentLines;
|
||||
if (toolName === 'Write') {
|
||||
|
|
|
|||
|
|
@ -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" $?
|
||||
|
||||
# Unreadable root is a no-op, not a crash.
|
||||
"$BOARD" --roots "$ROOT/does-not-exist" >/dev/null 2>&1; check "missing root is a clean no-op" $?
|
||||
# A root that does not exist is NOT a clean no-op - it is an unmeasured scan,
|
||||
# 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 --------------------
|
||||
# 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
|
||||
|
||||
# --- 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 "board-selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
|
|
@ -313,8 +313,20 @@ $dotentry"
|
|||
OLD_IFS="$IFS"; IFS=','
|
||||
set -- $ROOTS
|
||||
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
|
||||
[ -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"
|
||||
for entry in "$root"/*; do
|
||||
[ -d "$entry" ] || continue
|
||||
|
|
@ -334,6 +346,17 @@ $child"
|
|||
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
|
||||
|
||||
# --- Collect one record per repo -------------------------------------------
|
||||
|
|
@ -346,8 +369,28 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
name="$(basename "$d")"
|
||||
state="$d/STATE.md"
|
||||
|
||||
dirty="$(git -C "$d" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
|
||||
[ -n "$dirty" ] || dirty=0
|
||||
# F6: `git status | wc -l` yields 0 lines whether the tree is CLEAN or git
|
||||
# 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
|
||||
if [ -d "$COORD/$name/inbox" ]; then
|
||||
|
|
@ -1363,10 +1406,15 @@ plan() {
|
|||
awk -F'|' -v OWF="$OWED" '
|
||||
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
|
||||
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 ")"
|
||||
}
|
||||
' "$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")"
|
||||
n_mal="$(grep -c 'MALFORMED' "$RECORDS" 2>/dev/null | 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"
|
||||
[ "${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_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
|
||||
|
|
|
|||
|
|
@ -35,7 +35,20 @@
|
|||
# --exclude <mailbox> omit one mailbox (the caller's own, whose inbox is
|
||||
# already injected in full).
|
||||
# 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.
|
||||
set -u
|
||||
export LC_ALL=C
|
||||
|
|
@ -56,7 +69,15 @@ while [ $# -gt 0 ]; do
|
|||
esac
|
||||
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
|
||||
# rejects --version outright (exit nonzero, "illegal option" - measured on
|
||||
|
|
|
|||
|
|
@ -1159,6 +1159,97 @@ check "F7(f): control - a dot-prefixed target name still delivers" $?
|
|||
|
||||
/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 "PASS=$PASS FAIL=$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
|
|
|
|||
|
|
@ -565,6 +565,114 @@ run_hook "$P"
|
|||
[ "$HOOK_EXIT" -eq 0 ]; check "Edit: correcting done -> blocked is allowed with unpushed commits" $?
|
||||
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 "state-line-guard-selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue