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
408 lines
20 KiB
Bash
Executable file
408 lines
20 KiB
Bash
Executable file
#!/bin/bash
|
|
# board-selftest.sh - prove board.sh end-to-end against a throwaway repo tree
|
|
# and a throwaway mailbox (never touches ~/repos or ~/.claude/coord). Re-run
|
|
# after any edit to board.sh. ASCII only, bash 3.2 safe.
|
|
#
|
|
# Multibyte STATE.md content (em-dash, the NESTE pointing-hand) is generated
|
|
# with printf octal escapes so this script's own source stays pure ASCII - a
|
|
# literal em-dash in shell source has crashed bash 3.2 under `set -u` before.
|
|
set -u
|
|
export LC_ALL=C
|
|
|
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
BOARD="$DIR/board.sh"
|
|
|
|
ROOT="$(mktemp -d)"
|
|
CLAUDE_COORD_DIR="$(mktemp -d)"
|
|
export CLAUDE_COORD_DIR
|
|
cleanup() { /bin/rm -rf "$ROOT" "$CLAUDE_COORD_DIR" 2>/dev/null; }
|
|
trap cleanup EXIT
|
|
|
|
PASS=0; FAIL=0
|
|
check() { if [ "$2" -eq 0 ]; then PASS=$((PASS+1)); echo " ok - $1"; else FAIL=$((FAIL+1)); echo " FAIL - $1"; fi; }
|
|
|
|
# Multibyte building blocks (octal escapes keep this source ASCII).
|
|
EMDASH="$(printf '\342\200\224')"
|
|
HAND="$(printf '\360\237\221\211')"
|
|
OSLASH="$(printf '\303\270')"
|
|
|
|
mkrepo() { mkdir -p "$1" && git -C "$1" init -q 2>/dev/null; }
|
|
|
|
echo "board-selftest (root: $ROOT, mailbox: $CLAUDE_COORD_DIR)"
|
|
|
|
# --- 0. Empty root: no repos, still exits cleanly. ---
|
|
out0="$("$BOARD" --roots "$ROOT" 2>/dev/null)"; rc=$?
|
|
[ "$rc" -eq 0 ]; check "empty root exits 0" $?
|
|
|
|
# --- Fixture tree ---------------------------------------------------------
|
|
|
|
# repo-a: full board line, in-progress, cheap next step, unhandled inbox.
|
|
mkrepo "$ROOT/repo-a"
|
|
{
|
|
echo "# STATE - repo-a"
|
|
echo ""
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=sonnet/xhigh -->"
|
|
printf '**Lukk to MAJOR fra reviewen** %s deretter S3.3 concurrent fan-out.\n' "$EMDASH"
|
|
} > "$ROOT/repo-a/STATE.md"
|
|
|
|
# repo-b: STATE + NESTE prose but NO board line (the un-backfilled majority).
|
|
mkrepo "$ROOT/repo-b"
|
|
{
|
|
echo "# STATE - repo-b"
|
|
echo ""
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo ""
|
|
printf '> Kj%sr forskningstema 1 headless %s hard gate for motorens arkitektur.\n' "$OSLASH" "$EMDASH"
|
|
} > "$ROOT/repo-b/STATE.md"
|
|
|
|
# repo-c: no STATE.md at all (dormant class).
|
|
mkrepo "$ROOT/repo-c"
|
|
|
|
# repo-d: dirty working tree.
|
|
mkrepo "$ROOT/repo-d"
|
|
{
|
|
echo "# STATE - repo-d"
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=deferred; blocked-on=-; next-cost=sonnet/high -->"
|
|
echo "Parkert med vilje."
|
|
} > "$ROOT/repo-d/STATE.md"
|
|
# Commit STATE.md first, so the dirty count proves board counts UNCOMMITTED
|
|
# files only (2 junk files) rather than every file in a fresh tree.
|
|
git -C "$ROOT/repo-d" add STATE.md >/dev/null 2>&1
|
|
git -C "$ROOT/repo-d" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1
|
|
: > "$ROOT/repo-d/junk1.txt"
|
|
: > "$ROOT/repo-d/junk2.txt"
|
|
|
|
# repo-e: malformed status token (must be flagged, never silently accepted).
|
|
mkrepo "$ROOT/repo-e"
|
|
{
|
|
echo "# STATE - repo-e"
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=aktiv; blocked-on=-; next-cost=opus/high -->"
|
|
echo "Ugyldig token."
|
|
} > "$ROOT/repo-e/STATE.md"
|
|
|
|
# repo-g: prose containing a 'board:'-lookalike ABOVE the real board line.
|
|
# An unanchored substring grep would match 'dashboard:' first and mis-parse the
|
|
# whole repo; the board line is defined as living under the NESTE heading.
|
|
mkrepo "$ROOT/repo-g"
|
|
{
|
|
echo "# STATE - repo-g"
|
|
echo "Vi bygde et dashboard: status=done ble diskutert i forrige runde."
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=planned; blocked-on=-; next-cost=opus/xhigh -->"
|
|
echo "Ekte neste steg her."
|
|
} > "$ROOT/repo-g/STATE.md"
|
|
|
|
# repo-h: next-cost written the way the model rubric actually names the models,
|
|
# with a space and capitals ("Sonnet 5/xhigh"). This is spec-conformant
|
|
# (<modell>/<effort>) and must parse - a lowercase-only pattern silently drops
|
|
# it to "?" and hides the cost column exactly where it is needed.
|
|
mkrepo "$ROOT/repo-h"
|
|
{
|
|
echo "# STATE - repo-h"
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=Sonnet 5/xhigh -->"
|
|
echo "Neste steg her."
|
|
} > "$ROOT/repo-h/STATE.md"
|
|
|
|
# polyrepo/: NOT a git repo itself, but holds git repos one level down.
|
|
mkdir -p "$ROOT/polyrepo"
|
|
mkrepo "$ROOT/polyrepo/plug-x"
|
|
{
|
|
echo "# STATE - plug-x"
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=blocked; blocked-on=repo-a; next-cost=opus/high -->"
|
|
printf 'Venter p%s amendment-pakken.\n' "$EMDASH"
|
|
} > "$ROOT/polyrepo/plug-x/STATE.md"
|
|
|
|
# repo-wt + wt-feature: a git WORKTREE (and a submodule) has .git as a FILE,
|
|
# not a directory. A `git worktree add <root>/feature-x` yields a sibling
|
|
# directory that can CARRY its own STATE.md. Testing discovery for a .git
|
|
# DIRECTORY only drops it silently - the same silent-loss class as the V6
|
|
# marker fix.
|
|
mkrepo "$ROOT/repo-wt"
|
|
{
|
|
echo "# STATE - repo-wt"
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=done; blocked-on=-; next-cost=sonnet/high -->"
|
|
printf 'Hovedtreet %s ferdig.\n' "$EMDASH"
|
|
} > "$ROOT/repo-wt/STATE.md"
|
|
git -C "$ROOT/repo-wt" add STATE.md >/dev/null 2>&1
|
|
git -C "$ROOT/repo-wt" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1
|
|
git -C "$ROOT/repo-wt" -c user.email=t@t -c user.name=t \
|
|
worktree add -q -b feature-x "$ROOT/wt-feature" >/dev/null 2>&1
|
|
# Distinct board line, so the check proves board read the STATE INSIDE the
|
|
# worktree rather than matching the source repo's row by accident.
|
|
{
|
|
echo "# STATE - wt-feature"
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=fable/xhigh -->"
|
|
printf 'Arbeid i worktree %s eget neste steg.\n' "$EMDASH"
|
|
} > "$ROOT/wt-feature/STATE.md"
|
|
|
|
# repo-old: fresh STATE.md, last COMMIT 400 days old. This is the case the
|
|
# fixture tree was missing entirely: with age read off STATE.md's mtime, a repo
|
|
# whose work stopped long ago looks exactly as alive as one touched this
|
|
# morning, and no column contradicts it.
|
|
mkrepo "$ROOT/repo-old"
|
|
{
|
|
echo "# STATE - repo-old"
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=Opus 5/high -->"
|
|
echo "Ser levende ut, men ingen commit paa lenge."
|
|
} > "$ROOT/repo-old/STATE.md"
|
|
OLD_EPOCH=$(( $(date +%s) - 400 * 86400 ))
|
|
git -C "$ROOT/repo-old" add STATE.md >/dev/null 2>&1
|
|
# %ct is the COMMITTER date. `--date=` sets the AUTHOR date only and would
|
|
# leave %ct at now, so the fixture would be backdated in name only.
|
|
GIT_AUTHOR_DATE="@$OLD_EPOCH +0000" GIT_COMMITTER_DATE="@$OLD_EPOCH +0000" \
|
|
git -C "$ROOT/repo-old" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1
|
|
|
|
# repo-stale: the mirror image - STATE.md untouched since a fixed past date,
|
|
# last commit today. The pair pins that the two ages are independent readings
|
|
# and that neither of them silently became the sort key.
|
|
mkrepo "$ROOT/repo-stale"
|
|
{
|
|
echo "# STATE - repo-stale"
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=Opus 5/high -->"
|
|
echo "Gammel STATE, fersk commit."
|
|
} > "$ROOT/repo-stale/STATE.md"
|
|
git -C "$ROOT/repo-stale" add STATE.md >/dev/null 2>&1
|
|
git -C "$ROOT/repo-stale" -c user.email=t@t -c user.name=t commit -qm init >/dev/null 2>&1
|
|
touch -t 202601010000 "$ROOT/repo-stale/STATE.md"
|
|
|
|
# plain-dir/: no git repo anywhere under it - must be ignored entirely.
|
|
mkdir -p "$ROOT/plain-dir/sub"
|
|
echo "hei" > "$ROOT/plain-dir/sub/file.txt"
|
|
|
|
# Coord fixture: 3 unhandled for repo-a, 1 archived (must not be counted).
|
|
mkdir -p "$CLAUDE_COORD_DIR/repo-a/inbox" "$CLAUDE_COORD_DIR/repo-a/archive"
|
|
for n in 1 2 3; do echo "msg" > "$CLAUDE_COORD_DIR/repo-a/inbox/2026-msg$n-from-x.md"; done
|
|
echo "old" > "$CLAUDE_COORD_DIR/repo-a/archive/2026-old-from-x.md"
|
|
|
|
# repo-owes: unhandled inbox AND a route line, so the briefing can derive the
|
|
# EXACT startup command - advisor flag included - from the repo's own four
|
|
# traits instead of guessing from next-cost alone. Its NESTE runs deliberately
|
|
# past the table's 38-character column: carrying that line whole is the reason
|
|
# the briefing exists at all.
|
|
mkrepo "$ROOT/repo-owes"
|
|
{
|
|
echo "# STATE - repo-owes"
|
|
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
|
|
echo "<!-- board: status=in-progress; blocked-on=-; next-cost=Sonnet 5/high -->"
|
|
echo "<!-- route: path=known; verification=strong; reversibility=cheap; scope=local; rationale=x -->"
|
|
echo "Svar org-ops om badge-formatet, deretter bump katalogens ref og verifiser check-versions."
|
|
} > "$ROOT/repo-owes/STATE.md"
|
|
mkdir -p "$CLAUDE_COORD_DIR/repo-owes/inbox"
|
|
echo "msg" > "$CLAUDE_COORD_DIR/repo-owes/inbox/2026-msg1-from-y.md"
|
|
|
|
OUT="$("$BOARD" --roots "$ROOT" 2>/dev/null)"
|
|
|
|
# --- 1. Discovery ---------------------------------------------------------
|
|
printf '%s' "$OUT" | grep -q 'repo-a'; check "discovers top-level git repo" $?
|
|
printf '%s' "$OUT" | grep -q 'plug-x'; check "discovers nested polyrepo git repo (depth 2)" $?
|
|
printf '%s' "$OUT" | grep -q 'repo-c'; check "lists git repo without STATE.md" $?
|
|
printf '%s' "$OUT" | grep -q 'plain-dir'; [ $? -ne 0 ]; check "ignores non-git directory tree" $?
|
|
printf '%s' "$OUT" | grep -q 'polyrepo'; [ $? -ne 0 ]; check "polyrepo container itself is not listed as a repo" $?
|
|
# Guard the fixture itself: if git ever stops writing a .git FILE for
|
|
# worktrees, the next two checks would pass for the wrong reason.
|
|
[ -f "$ROOT/wt-feature/.git" ]; check "fixture: worktree .git is a FILE, not a dir" $?
|
|
printf '%s' "$OUT" | grep -q 'wt-feature'; check "discovers git worktree (.git is a file)" $?
|
|
printf '%s' "$OUT" | grep -qE 'wt-feature.*fable/xhigh'
|
|
check "reads STATE.md from inside the worktree, not the source repo" $?
|
|
|
|
# --- 2. Board line parsing (the B field) ----------------------------------
|
|
printf '%s' "$OUT" | grep -q 'in-progress'; check "parses status token from board line" $?
|
|
printf '%s' "$OUT" | grep -q 'sonnet/xhigh'; check "parses next-cost from board line" $?
|
|
printf '%s' "$OUT" | grep -qE 'repo-h.*Sonnet 5/xhigh'
|
|
check "next-cost accepts spaces and capitals (rubric model names)" $?
|
|
printf '%s' "$OUT" | grep -qE 'plug-x.*repo-a'
|
|
check "blocked repo names its blocker on its own row" $?
|
|
printf '%s' "$OUT" | grep -q 'deferred'; check "distinguishes deferred from blocked" $?
|
|
|
|
# --- 3. Malformed input is flagged, not swallowed -------------------------
|
|
printf '%s' "$OUT" | grep -qi 'malformed\|ugyldig\|invalid'; check "malformed status token is flagged" $?
|
|
|
|
# A 'dashboard:' lookalike earlier in the file must not win over the real line.
|
|
printf '%s' "$OUT" | grep -qE 'repo-g.*planned.*opus/xhigh'
|
|
check "board-line parse ignores 'board:' lookalikes in prose" $?
|
|
|
|
# --- 4. Heuristic fallback when board line is absent -----------------------
|
|
printf '%s' "$OUT" | grep -q 'repo-b'; check "repo without board line still listed" $?
|
|
printf '%s' "$OUT" | grep -qi 'forskningstema\|headless'; check "shows NESTE excerpt when board line absent" $?
|
|
# Markdown noise must be stripped: a blockquoted NESTE line renders as prose.
|
|
printf '%s' "$OUT" | grep -qE 'repo-b.*[[:space:]]>'; [ $? -ne 0 ]
|
|
check "blockquote marker stripped from NESTE excerpt" $?
|
|
|
|
# --- 5. Git + coord signals ------------------------------------------------
|
|
printf '%s' "$OUT" | grep -qE 'repo-d.*[^0-9]2([^0-9]|$)'; check "reports dirty file count" $?
|
|
printf '%s' "$OUT" | grep -qE 'repo-a.*[^0-9]3([^0-9]|$)'; check "reports unhandled coord inbox count (archive excluded)" $?
|
|
|
|
# --- 6. Bucketing ----------------------------------------------------------
|
|
printf '%s' "$OUT" | grep -qi 'blocked'; check "blocked repo surfaced" $?
|
|
printf '%s' "$OUT" | grep -qiE 'dormant|sovende|uten STATE'; check "STATE-less repos bucketed separately" $?
|
|
|
|
# --- 7. Robustness ---------------------------------------------------------
|
|
# Multibyte prose must not crash the reader nor emit split-character garbage.
|
|
[ -n "$OUT" ]; check "produces output over multibyte STATE prose" $?
|
|
printf '%s' "$OUT" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1
|
|
check "output is valid UTF-8 (no split multibyte truncation)" $?
|
|
|
|
# A STATE.md with a NESTE heading and nothing after it must not hang or crash.
|
|
mkrepo "$ROOT/repo-f"
|
|
{ echo "# STATE - repo-f"; printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"; } > "$ROOT/repo-f/STATE.md"
|
|
"$BOARD" --roots "$ROOT" >/dev/null 2>&1; check "empty NESTE block does not crash" $?
|
|
|
|
"$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" $?
|
|
|
|
# --- 8. The documented next-cost form IS the convention --------------------
|
|
# The cost field is free text on purpose: the parser reads to ';' or '-->' so
|
|
# the rubric's spacing survives, and the fixtures above deliberately vary the
|
|
# spelling to pin that leniency. But leniency on the read side means the ONLY
|
|
# thing holding the field uniform is what --help prescribes, and --help once
|
|
# prescribed a versionless `sonnet/xhigh` while the global rubric spelled
|
|
# `Sonnet 5 / xhigh`. Two specs for one field: measured across the real tree,
|
|
# not one board line used ours, and eight distinct strings were in circulation.
|
|
# Pinned here because cleaning the DATA cannot fix that - a spec that
|
|
# contradicts the rubric regenerates the divergence every session.
|
|
HELP="$("$BOARD" --help 2>/dev/null)"
|
|
printf '%s' "$HELP" | grep -q 'next-cost=Sonnet 5/xhigh'
|
|
check "--help prescribes the canonical next-cost form" $?
|
|
# The versionless form is what the rubric does NOT use; no example may show it.
|
|
if printf '%s' "$HELP" | grep -qE '(sonnet|opus|fable)/(high|xhigh)'; then rc=1; else rc=0; fi
|
|
check "--help shows no versionless model example" "$rc"
|
|
|
|
# --- 9. SISTE: last commit, read for EVERY repo ----------------------------
|
|
# One column, one meaning. ALDER answers "when was the plan last touched" and
|
|
# is blank where there is no plan; SISTE answers "when did anything last land"
|
|
# and is read for every repo. Before this, a single ALDER column carried both
|
|
# readings depending on which branch a repo fell into, so a repo with a STATE.md
|
|
# could go a year without a commit and show nothing at all.
|
|
[ "$(git -C "$ROOT/repo-old" log -1 --format=%ct 2>/dev/null)" = "$OLD_EPOCH" ]
|
|
check "fixture: repo-old's COMMITTER date is actually backdated" $?
|
|
|
|
printf '%s' "$OUT" | grep -q 'SISTE'; check "table has a SISTE column" $?
|
|
|
|
printf '%s\n' "$OUT" | grep -qE '^repo-old .*[[:space:]]0d[[:space:]]+40[01]d[[:space:]]'
|
|
check "fresh STATE.md + old commit: ALDER stays 0d, SISTE shows the 400 days" $?
|
|
|
|
printf '%s\n' "$OUT" | grep -qE '^repo-stale .*[[:space:]][1-9][0-9]*d[[:space:]]+0d[[:space:]]'
|
|
check "old STATE.md + fresh commit: the two ages read independently" $?
|
|
|
|
# A repo with no commits has no last commit to report. A day count there would
|
|
# be a fabricated reading, and the pre-existing -1d was exactly that.
|
|
# The -n guard is load-bearing: an absent row would leave crow empty, and an
|
|
# empty string trivially contains no day count - the check would pass by
|
|
# finding nothing rather than by reading the row.
|
|
crow="$(printf '%s\n' "$OUT" | grep '^repo-c ')"
|
|
if [ -n "$crow" ] && ! printf '%s' "$crow" | grep -qE '[0-9]+d'; then rc=0; else rc=1; fi
|
|
check "repo with no commits reports no day count in either age column" "$rc"
|
|
|
|
# The four ranking rules stay four: SISTE is evidence the operator reads, never
|
|
# an input to the order. If it had become the sort key, repo-old (400d) would
|
|
# fall behind repo-stale (0d) instead of ahead of it.
|
|
n_old="$(printf '%s\n' "$OUT" | grep -n '^repo-old ' | cut -d: -f1)"
|
|
n_stale="$(printf '%s\n' "$OUT" | grep -n '^repo-stale ' | cut -d: -f1)"
|
|
[ -n "$n_old" ] && [ -n "$n_stale" ] && [ "$n_old" -lt "$n_stale" ]
|
|
check "SISTE is evidence, not a ranking input (order still follows ALDER)" $?
|
|
|
|
# --- 8. Briefing rendering (--brief) --------------------------------------
|
|
# A second rendering of the SAME scan, never a second scan: the briefing
|
|
# answers "who is waiting on me and what does answering cost", where the table
|
|
# answers "what is the state of every repo".
|
|
BRIEF="$("$BOARD" --roots "$ROOT" --brief 2>/dev/null)"
|
|
|
|
printf '%s' "$BRIEF" | grep -q 'repo-owes'
|
|
check "brief includes a repo with an unhandled inbox" $?
|
|
|
|
printf '%s' "$BRIEF" | grep -q 'repo-b'; [ $? -ne 0 ]
|
|
check "brief excludes a repo that owes nothing" $?
|
|
|
|
# The table cuts NESTE at 38 characters. The briefing carries the whole line,
|
|
# so the tail past the cut is the thing being proved here.
|
|
printf '%s' "$BRIEF" | grep -q 'check-versions'
|
|
check "brief prints the full NESTE line, not the 38-char table excerpt" $?
|
|
|
|
# Derived by CALLING route.sh with the repo's own four traits - deliberately
|
|
# not spelled out in board.sh - so the rubric keeps exactly one copy. Row 1
|
|
# carries the advisor, which is what makes the quota fallback safe to take.
|
|
printf '%s' "$BRIEF" | grep -q 'claude --model sonnet --effort high --advisor opus'
|
|
check "brief derives the exact startup command from the repo's route line" $?
|
|
|
|
# The command must start its OWN line. `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 - caught by eye
|
|
# against the real tree, which is exactly what a check is for.
|
|
printf '%s' "$BRIEF" | grep -qE '^ \$ claude --model'
|
|
check "startup command starts on its own line, not appended to NESTE prose" $?
|
|
|
|
# repo-a owes 3 messages and has no route line. A fabricated command would be
|
|
# worse than none: it would read as authoritative while being a guess.
|
|
printf '%s' "$BRIEF" | grep -A4 'repo-a' | grep -qi 'route'
|
|
check "repo owing mail but lacking a route line is marked, never guessed at" $?
|
|
|
|
# Read-only by construction, and the briefing must not erode it. Reading IS
|
|
# delivering in this engine, so a survey that consumed a backlog would be
|
|
# precisely the defect the whole design forbids.
|
|
snap() { find "$1" -type f -exec stat -f '%N %m %z' {} \; 2>/dev/null | sort; }
|
|
b4="$(snap "$CLAUDE_COORD_DIR")"
|
|
"$BOARD" --roots "$ROOT" --brief >/dev/null 2>&1
|
|
[ "$b4" = "$(snap "$CLAUDE_COORD_DIR")" ]
|
|
check "brief writes nothing to the mailbox (reading is delivering)" $?
|
|
|
|
# A mailbox can carry a name the repo scan will NEVER produce: a declared
|
|
# non-git surface (CLAUDE_COORD_REPO, e.g. ~/repos itself) or a checkout
|
|
# outside the scan roots. board.sh discovers git repos, so such a mailbox is
|
|
# invisible to every column it prints - and the briefing exists to answer "who
|
|
# is waiting on you", where dropping one silently is the loss-wearing-the-
|
|
# shape-of-normal class this engine keeps naming. Measured against the real
|
|
# mailbox before this check existed: 11 repos / 21 messages in the briefing
|
|
# against 12 mailboxes / 22 pending in coord-count.
|
|
mkdir -p "$CLAUDE_COORD_DIR/orphan-surface/inbox"
|
|
echo "msg" > "$CLAUDE_COORD_DIR/orphan-surface/inbox/2026-msg1-from-z.md"
|
|
BRIEF2="$("$BOARD" --roots "$ROOT" --brief 2>/dev/null)"
|
|
|
|
printf '%s' "$BRIEF2" | grep -q 'orphan-surface'
|
|
check "brief surfaces a pending mailbox that has no repo in the scan tree" $?
|
|
|
|
# It must be reported as its own class, not silently folded in among repos the
|
|
# scan actually found - there is no STATE.md behind it and so no next step.
|
|
printf '%s' "$BRIEF2" | grep -qi 'utenfor repo-skannen\|uten repo'
|
|
check "orphan mailbox is reported as its own class, not as a scanned repo" $?
|
|
|
|
# --- 9. brief-nightly.sh: the only writer, and it writes atomically -------
|
|
# board.sh stays read-only, so the file write lives in a separate wrapper.
|
|
# Unattended overwrite must never expose a half-written briefing: the wrapper
|
|
# renders to a temp file in the same directory and renames it into place.
|
|
NIGHTLY="$DIR/brief-nightly.sh"
|
|
OUTFILE="$ROOT/briefing.md"
|
|
CLAUDE_BRIEF_FILE="$OUTFILE" BOARD_ROOTS="$ROOT" bash "$NIGHTLY" >/dev/null 2>&1
|
|
check "brief-nightly exits 0" $?
|
|
|
|
[ -f "$OUTFILE" ] && grep -q 'repo-owes' "$OUTFILE"
|
|
check "brief-nightly writes the briefing to CLAUDE_BRIEF_FILE" $?
|
|
|
|
ls "$OUTFILE".tmp.* >/dev/null 2>&1; [ $? -ne 0 ]
|
|
check "brief-nightly leaves no temp file behind" $?
|
|
|
|
# An EMPTY render is a failed render, and must leave the previous briefing
|
|
# standing rather than truncating it to nothing - the failure mode a plain
|
|
# `> file` redirect has, and the reason the temp-then-rename exists. A repo
|
|
# tree where nobody owes anything is NOT this case: that renders a valid,
|
|
# non-empty briefing saying so, and is written normally.
|
|
echo "FORRIGE BRIEFING" > "$OUTFILE"
|
|
CLAUDE_BRIEF_FILE="$OUTFILE" BOARD_ROOTS="$ROOT/does-not-exist" \
|
|
bash "$NIGHTLY" >/dev/null 2>&1
|
|
grep -q 'FORRIGE BRIEFING' "$OUTFILE"
|
|
check "empty render never overwrites the previous briefing" $?
|
|
|
|
echo ""
|
|
echo "board-selftest: $PASS passed, $FAIL failed"
|
|
[ "$FAIL" -eq 0 ] || exit 1
|
|
exit 0
|