feat(brief): render the nightly cross-repo briefing without a model

The operator has more repos than they can hold in their head, and the
question that actually costs them is "who is waiting on me, and what does
answering cost". board.sh already scans for it; nothing rendered it in a
form an unattended job could leave behind.

--brief is a second RENDERING of that scan, never a second scan. It prints
NESTE uncut, because the 38-character cut is the table column's property
and not the record's - the value used to be truncated at record-build time,
which left the cut string as the only copy. Each startup command is derived
by CALLING route.sh with that repo's own four traits; next-cost alone
cannot produce it, since the advisor flag is a property of the ROW and two
rows can share a model/effort pair while differing on it. A repo with no
route line is told so rather than handed a guess.

It cross-checks itself against coord-count.sh, and that is the substance of
the change rather than a nicety. The repo scan and the mailbox are two
different populations: a mailbox can carry a name no scan will ever produce
- a declared non-git surface (CLAUDE_COORD_REPO, e.g. ~/repos) or a
checkout outside the roots. Measured on the real mailbox: 11 repos / 21
messages in the briefing against coord-count's 12 / 22, the missing one
being the declared surface `repos`. A briefing that only walked the scan
would answer "who is waiting on you" with a number it quietly knew was
short.

Zero model calls, which was the deciding property. Measured against 2.1.220
under subscription auth: --max-budget-usd DOES bite (terminal_reason
budget_exhausted, exit 1), but it aborts AFTER turn one - floor ~0.25
USD-equivalent per turn on claude-opus-5[1m]. It is a runaway brake, not a
pre-flight gate, so a nightly claude -p job would draw on the same quota
pool as interactive work every night. Determinism removes the question.

board.sh stays read-only: the file write lives in brief-nightly.sh, which
renders to a temp file and renames it into place, and treats an EMPTY
render as a FAILED one - board prints nothing when its scan roots do not
exist, which is what a mistyped path or a moved home looks like, and a
plain `> file` redirect would destroy yesterday's briefing on a bad launchd
environment.

The launchd template carries placeholders, not absolute paths: this repo is
mirrored publicly and a plist is the one file here that would need a home
directory in it. It points at a checkout rather than the plugin cache,
which is version-pinned and would break silently on the next bump.

board-selftest 36 -> 49. One check pins a defect caught only by eye against
the real tree: fold copies its input's missing trailing newline, which ran
the command onto the tail of the NESTE prose and produced a briefing whose
commands could not be copied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017orCFDkmp88fLnqDR3chdJ
This commit is contained in:
Kjell Tore Guttormsen 2026-08-01 22:38:46 +02:00
commit 9dd24c3446
11 changed files with 435 additions and 18 deletions

View file

@ -54,7 +54,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` (36 checks).
STATE.md and no mailbox. Pinned by `board-selftest.sh` (49 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
@ -69,6 +69,46 @@ marketplace plugin. Three components, one boundary:
one measured effect was an accidental fallback target for Claude sessions'
own Bash tool calls (the bug 0.12.1 fixed) — once that fallback was gone they
had no remaining function and were deleted.
**`board.sh --brief` is a second RENDERING of that scan, never a second
scan, and `brief-nightly.sh` is the only writer in that path.** The briefing
answers the narrower question an unattended job can answer without judgement:
which repos have an unhandled inbox, what their next step says in full, and
the exact command to start a session in each. It prints NESTE uncut because
the 38-character cut is the table column's property, not the record's — the
value used to be truncated at record-build time, which left the cut string as
the only copy. Each command is derived by CALLING `route.sh` with that repo's
own four traits; `next-cost` alone cannot yield it, since the advisor flag is
a property of the ROW and two rows can share a model/effort pair while
differing on it. A repo with no route line is told so rather than handed a
guess, because a guessed command reads as authoritative.
It also cross-checks itself against `coord-count.sh`, and that is not
belt-and-braces. The repo scan and the mailbox are two different populations:
a mailbox can carry a name no scan will ever produce — a declared non-git
surface (`CLAUDE_COORD_REPO`, e.g. `~/repos` itself) or a checkout outside the
roots. Measured on the real mailbox at 0.15.0: the briefing found 11 repos /
21 messages where `coord-count` saw 12 mailboxes / 22 pending, the missing one
being the declared surface `repos`. A briefing that only walks the scan
answers "who is waiting on you" with a number it quietly knows is short.
**Zero model calls, and that is the load-bearing property, not an
implementation detail.** The operator authenticates by subscription, so a
headless `claude -p` job draws from the same quota pool as interactive work.
Measured against 2.1.220: `--max-budget-usd` DOES bite under subscription auth
(`terminal_reason: budget_exhausted`, exit 1), but it aborts AFTER turn one,
never before it — floor ~0.25 USD-equivalent per turn on `claude-opus-5[1m]`.
It is a runaway brake, not a pre-flight gate. Making the briefing deterministic
removes the question entirely.
`board.sh` stays read-only, which is why the file write lives in the wrapper
instead of behind a `--brief --out FILE` flag. The wrapper renders to a temp
file in the target directory and renames it into place, and treats an EMPTY
render as a FAILED one: board prints nothing at all when its scan roots do not
exist, which is what a mistyped path or a moved home directory looks like, and
a plain `> file` redirect would destroy yesterday's briefing on a bad launchd
environment. A tree where nobody owes anything is a different case — that is a
valid, non-empty briefing saying so, and is written normally.
- **Route (`scripts/route.sh`):** pure calculator for the next session's model
and effort. Takes four scored traits of the next task plus a required
rationale, and prints one block of `key=value` lines: the rubric row, the rule
@ -169,7 +209,7 @@ obligations in another repo.
builtins only in hook and tests.
- TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (182/182),
`bash scripts/board-selftest.sh` must exit 0 (36/36) and
`bash scripts/board-selftest.sh` must exit 0 (49/49) and
`bash scripts/route-selftest.sh` must exit 0 (73/73).
- English for all code, docs, and commit messages (public repo). Norwegian
trigger aliases in the skill description are deliberate.
@ -181,6 +221,11 @@ obligations in another repo.
`bash scripts/route-selftest.sh` (or `npm test`, the Node wrapper around all three)
- Hook smoke test: `node hooks/scripts/session-start.mjs` (expects JSON on stdout)
- Board smoke test: `bash scripts/board.sh` (read-only, ~3s over the real tree)
- Briefing smoke test: `bash scripts/board.sh --brief` (read-only, writes
nothing). `brief-nightly.sh` DOES write — it overwrites `$CLAUDE_BRIEF_FILE`
(default `~/.claude/briefing.md`), so point that at a scratch path when
testing. Installed as a launchd agent from `launchd/`, which points at the
SOURCE repo, never the version-pinned plugin cache.
- Route smoke test: `bash scripts/route.sh --path known --verification strong
--reversibility cheap --scope local --rationale x` (writes nothing, instant)
- Sweep smoke test: `bash scripts/coord-sweep.sh` (dry-run is the default, so