feat(gate): session-gate.sh - R3/R4/R6 checks before a session starts

Order 20260926T070037Z-680859139 (.claude). A read-only, zero-model-call
check any session starter can call first: exit 0 allows, exit 2 denies with
one stderr line naming the rule and what is missing.

- R4: PLAN.md holds exactly one gate line,
  <!-- gate: gjenfortelling=<time>; dekning=komplett; r1-proeve=<time|->;
  foerste-maaling=<time|->; retning=<name> -->
- R3: the Logg table's Bevegelse (ja|nei) and Kvote (N%) columns. Deny at
  >= 10% of the weekly quota without movement since the last row that moved.
- R6: cap 1 live session per repo (claude agents --json --cwd <repo>) until
  r1-proeve and foerste-maaling are both times, then 2. Night 23:00-07:00 or
  --night needs r1-proeve.

Fail-closed: unmeasured rows, a missing claude binary, a non-JSON listing,
duplicate or unknown keys and usage errors all deny.

Measured: claude agents --cwd matches the directory a session was STARTED
in, not its printed cwd, so --repo must be the main checkout root.

Chose NOT to call it from board.sh --dispatch, because a dispatch line is
generated before the start (a live count would be stale), and live-session
reading is the process axis board.sh refuses by design.

session-gate-selftest.sh: 0 -> 97 checks. It was red 91 of 95 before the
script existed, then 97/97 green. Ten mutants all die. It is wired into npm
test as the sixth suite, and the README badge goes from 1006 to 1103.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-26 09:24:12 +02:00
commit 02d4df2dfb
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
5 changed files with 794 additions and 10 deletions

View file

@ -74,6 +74,13 @@ test('orders bash selftest passes', () => {
runSuite('orders');
});
// session-gate.sh is the check a session starter calls before it starts one.
// Its suite feeds live sessions from a fake `claude` on PATH, so it proves the
// gate without depending on what happens to be running on this machine.
test('session-gate bash selftest passes', () => {
runSuite('session-gate');
});
// The engine refuses to invent an identity from the cwd, but the hook is the
// FOURTH place repo identity is derived, and a rule enforced in three of four
// places is not a rule: as long as the hook resolved the name itself and passed
@ -255,7 +262,7 @@ function suiteTotal(name) {
test('README states the selftest counts the suites actually reported', () => {
const readme = readFileSync(join(root, 'README.md'), 'utf8');
const suites = ['coord', 'board', 'route', 'orders', 'state-line-guard'];
const suites = ['coord', 'board', 'route', 'orders', 'state-line-guard', 'session-gate'];
let sum = 0;
for (const name of suites) {
@ -275,5 +282,5 @@ test('README states the selftest counts the suites actually reported', () => {
const badge = readme.match(/badge\/selftest_checks-(\d+)-/);
assert.ok(badge, 'README has no selftest_checks badge to check');
assert.equal(Number(badge[1]), sum,
`README's badge says ${badge[1]} selftest checks; the five suites reported ${sum}`);
`README's badge says ${badge[1]} selftest checks; the ${suites.length} suites reported ${sum}`);
});