repo-mailbox/scripts/brief-nightly.sh
Kjell Tore Guttormsen 9dd24c3446 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
2026-08-01 22:38:46 +02:00

67 lines
2.6 KiB
Bash
Executable file

#!/bin/bash
# brief-nightly.sh - render the cross-repo briefing to a file, atomically.
#
# This is the ONLY writer in the briefing path, and it exists so that board.sh
# does not become one. board.sh is read-only by construction - it writes to no
# repo, no STATE.md and no mailbox - and a `--brief --out FILE` flag would have
# ended that for the sake of one redirect.
#
# Why not just `board.sh --brief > file` from launchd:
#
# 1. A plain redirect TRUNCATES the target before the renderer has produced
# a byte. An unattended job that fails, or is read mid-run, then leaves
# the operator an empty or half-written briefing - and the briefing is
# read exactly when nobody is watching it being made. Rendering to a temp
# file in the same directory and rename()-ing it into place means a reader
# sees either the old briefing or the new one, never a partial one.
# 2. An EMPTY render is treated as a FAILED render and never replaces a good
# briefing. Board prints nothing at all when its scan roots do not exist,
# which is exactly what a mistyped path or a moved home directory looks
# like - silent truncation to zero would destroy yesterday's briefing on
# a bad launchd environment. A repo tree where nobody owes anything is a
# different case entirely: that renders a valid, non-empty briefing saying
# so, and is written normally.
#
# Zero model calls, by construction: it runs two shell scripts. That is the
# whole point - a nightly job on subscription auth draws from the same quota
# pool as interactive work, and every turn it would spend is a turn the
# operator does not get. Measured floor for one headless turn on
# claude-opus-5[1m]: ~0.25 USD-equivalent, which --max-budget-usd cannot
# prevent (it aborts AFTER turn one, never before it).
#
# ASCII only, bash 3.2 safe.
set -u
SELFDIR="$(cd "$(dirname "$0")" && pwd)"
BOARD="$SELFDIR/board.sh"
OUT="${CLAUDE_BRIEF_FILE:-$HOME/.claude/briefing.md}"
case "${1:-}" in
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
esac
OUTDIR="$(dirname "$OUT")"
mkdir -p "$OUTDIR" 2>/dev/null || {
echo "brief-nightly: cannot create $OUTDIR" >&2; exit 1; }
# Same directory as the target: rename() is only atomic within one filesystem.
TMP="$OUT.tmp.$$"
trap '/bin/rm -f "$TMP" 2>/dev/null' EXIT
bash "$BOARD" --brief "$@" > "$TMP" 2>/dev/null
rc=$?
if [ "$rc" -ne 0 ]; then
echo "brief-nightly: board.sh --brief exited $rc, keeping previous briefing" >&2
exit 1
fi
if [ ! -s "$TMP" ]; then
echo "brief-nightly: empty render, keeping previous briefing" >&2
exit 1
fi
mv -f "$TMP" "$OUT" || {
echo "brief-nightly: could not install $OUT" >&2; exit 1; }
exit 0