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:
parent
459c9feec0
commit
9dd24c3446
11 changed files with 435 additions and 18 deletions
149
scripts/board.sh
149
scripts/board.sh
|
|
@ -51,7 +51,21 @@
|
|||
# the register, not here. Conflating the two axes is a real defect class - the
|
||||
# board reads only its own axis, so keep them separate.
|
||||
#
|
||||
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain]
|
||||
# --brief is a SECOND RENDERING of the same scan, never a second scan. The
|
||||
# table answers "what is the state of every repo"; the briefing answers the
|
||||
# narrower question an unattended nightly 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 there. The 38-char cut is the table
|
||||
# column's property, not the record's, so the briefing prints NESTE uncut. 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. A repo with no route line is told so rather than handed a guess.
|
||||
#
|
||||
# --brief is still read-only: it writes nothing. The file write lives in
|
||||
# brief-nightly.sh, which renders to a temp file and renames it into place, and
|
||||
# refuses to overwrite a good briefing with an empty render.
|
||||
#
|
||||
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain] [--brief]
|
||||
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
|
||||
# BOARD_ROOTS overrides the default scan roots.
|
||||
# ASCII only, bash 3.2 safe.
|
||||
|
|
@ -61,12 +75,20 @@ export LC_ALL=C
|
|||
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
|
||||
ROOTS="${BOARD_ROOTS:-$HOME/repos}"
|
||||
NESTE_WIDTH=38
|
||||
BRIEF=0
|
||||
|
||||
# Sibling calculator, invoked rather than reimplemented: the rubric that turns
|
||||
# four traits into a model has exactly one copy, and it is route.sh's row
|
||||
# table. Bare form on purpose - a ${VAR:-fallback} here is the 0.12.1 defect.
|
||||
SELFDIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROUTE="$SELFDIR/route.sh"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
# bash 3.2: `shift 2` past the end of $# is a no-op -> would loop forever.
|
||||
--roots) [ $# -ge 2 ] || { echo "board: --roots requires a value" >&2; exit 2; }
|
||||
ROOTS="$2"; shift 2 ;;
|
||||
--brief) BRIEF=1; shift ;;
|
||||
--plain) shift ;;
|
||||
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "board: unknown argument: $1 (ignored)" >&2; shift ;;
|
||||
|
|
@ -148,8 +170,8 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
# No plan file, so no plan age: ALDER is blank rather than quietly showing
|
||||
# the commit age under a heading that means something else everywhere else
|
||||
# in the table. The sort key keeps using it - order is unchanged.
|
||||
printf '5|%06d|%s|-|-|%s|%s|-|%s|(ingen STATE.md)\n' \
|
||||
"$lastd" "$name" "$inbox" "$dirty" "$lastcol"
|
||||
printf '5|%06d|%s|-|-|%s|%s|-|%s|%s|(ingen STATE.md)\n' \
|
||||
"$lastd" "$name" "$inbox" "$dirty" "$lastcol" "$d"
|
||||
continue
|
||||
fi
|
||||
|
||||
|
|
@ -190,8 +212,11 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
}' "$state" 2>/dev/null \
|
||||
| sed -e 's/^[[:space:]]*>[[:space:]]*//' -e 's/\*\*//g' \
|
||||
-e 's/^[[:space:]]*[-*][[:space:]]*//' -e 's/^[[:space:]]*//' -e 's/`//g')"
|
||||
# Stored WHOLE. Truncation is a property of the table's 38-char column, so it
|
||||
# belongs to that renderer alone - the briefing is a second rendering of this
|
||||
# same record and exists precisely to carry the line uncut. Cutting here once
|
||||
# meant the only copy of the text was the cut one.
|
||||
[ -n "$neste" ] || neste="(tom NESTE-blokk)"
|
||||
neste="$(trunc "$neste" "$NESTE_WIDTH")"
|
||||
|
||||
disp="$status"
|
||||
if [ "$status" = "blocked" ] && [ -n "$blockedon" ] && [ "$blockedon" != "-" ]; then
|
||||
|
|
@ -207,8 +232,8 @@ printf '%s\n' "$REPOS" | while IFS= read -r d; do
|
|||
*) bucket=2 ;;
|
||||
esac
|
||||
|
||||
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%s\n' \
|
||||
"$bucket" "$age" "$name" "$disp" "$cost" "$inbox" "$dirty" "$age" "$lastcol" "$neste"
|
||||
printf '%s|%06d|%s|%s|%s|%s|%s|%sd|%s|%s|%s\n' \
|
||||
"$bucket" "$age" "$name" "$disp" "$cost" "$inbox" "$dirty" "$age" "$lastcol" "$d" "$neste"
|
||||
done > "${TMPDIR:-/tmp}/board.$$"
|
||||
|
||||
RECORDS="${TMPDIR:-/tmp}/board.$$"
|
||||
|
|
@ -222,12 +247,120 @@ hdr() {
|
|||
|
||||
rows() {
|
||||
awk -F'|' -v b="$1" '$1==b' "$RECORDS" | sort -t'|' -k2,2n | \
|
||||
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last neste; do
|
||||
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir neste; do
|
||||
printf '%-32s %-34s %-14s %4s %4s %6s %6s %s\n' \
|
||||
"$name" "$status" "$cost" "$inbox" "$dirty" "$age" "$last" "$neste"
|
||||
"$name" "$status" "$cost" "$inbox" "$dirty" "$age" "$last" "$(trunc "$neste" "$NESTE_WIDTH")"
|
||||
done
|
||||
}
|
||||
|
||||
# --- Briefing rendering (--brief) ------------------------------------------
|
||||
# The startup command for one repo, derived from that repo's OWN route line by
|
||||
# calling route.sh. Deriving it from next-cost instead would not work even in
|
||||
# principle: 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 gets its
|
||||
# next-cost printed and is told where the command comes from - a guessed
|
||||
# command would read as authoritative while being a guess, which is worse than
|
||||
# no command at all.
|
||||
brief_cmd() {
|
||||
bc_state="$1/STATE.md"
|
||||
bc_line="$(grep -m1 '^<!-- route:' "$bc_state" 2>/dev/null)"
|
||||
if [ -z "$bc_line" ]; then
|
||||
printf 'KOST: %s (ingen route-linje - kjor route-skillen i det repoet)' "$2"
|
||||
return
|
||||
fi
|
||||
bc_p="$(printf '%s' "$bc_line" | sed -n 's/.*path=\([a-z-]*\).*/\1/p')"
|
||||
bc_v="$(printf '%s' "$bc_line" | sed -n 's/.*verification=\([a-z-]*\).*/\1/p')"
|
||||
bc_r="$(printf '%s' "$bc_line" | sed -n 's/.*reversibility=\([a-z-]*\).*/\1/p')"
|
||||
bc_s="$(printf '%s' "$bc_line" | sed -n 's/.*scope=\([a-z-]*\).*/\1/p')"
|
||||
bc_cmd="$(bash "$ROUTE" --path "$bc_p" --verification "$bc_v" \
|
||||
--reversibility "$bc_r" --scope "$bc_s" --rationale brief 2>/dev/null \
|
||||
| sed -n 's/^command=//p')"
|
||||
if [ -n "$bc_cmd" ]; then
|
||||
printf '$ %s' "$bc_cmd"
|
||||
else
|
||||
printf 'KOST: %s (route-linjen kunne ikke tolkes)' "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
# The repo scan and the mailbox are two different populations, and the gap
|
||||
# between them is silent by default. board.sh discovers git REPOS; 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.
|
||||
# Such a mailbox is invisible in every column this script prints, so a briefing
|
||||
# that only walks the scan answers "who is waiting on you" with a number it
|
||||
# quietly knows is short. Measured on the real mailbox: 11 repos / 21 messages
|
||||
# against coord-count's 12 mailboxes / 22 pending, the missing one being the
|
||||
# declared surface `repos`.
|
||||
#
|
||||
# coord-count.sh is the right source and the only safe one: it counts without
|
||||
# delivering, where coord-inbox.sh would mark broadcasts seen just by looking.
|
||||
brief_orphans() {
|
||||
bo_count="$SELFDIR/coord-count.sh"
|
||||
[ -f "$bo_count" ] || return 0
|
||||
bo_tab="$(printf '\t')"
|
||||
bo_out="$(bash "$bo_count" 2>/dev/null \
|
||||
| awk -F"$bo_tab" '$2+0>0 {print $1"'"$bo_tab"'"$2}' \
|
||||
| while IFS="$bo_tab" read -r bo_name bo_n; do
|
||||
[ -n "$bo_name" ] || continue
|
||||
awk -F'|' -v n="$bo_name" '$3==n {f=1} END{exit !f}' "$RECORDS" \
|
||||
|| printf ' %-32s INN %s\n' "$bo_name" "$bo_n"
|
||||
done)"
|
||||
[ -n "$bo_out" ] || return 0
|
||||
echo ""
|
||||
echo "UTENFOR REPO-SKANNEN - postkasser uten et repo i treet:"
|
||||
printf '%s\n' "$bo_out"
|
||||
echo "Disse har ingen STATE.md og derfor intet neste steg. En deklarert flate"
|
||||
echo "(CLAUDE_COORD_REPO) eller et checkout utenfor scan-roten."
|
||||
}
|
||||
|
||||
brief() {
|
||||
n_owe="$(awk -F'|' '$6+0 > 0' "$RECORDS" | wc -l | tr -d ' ')"
|
||||
tot_msg="$(awk -F'|' '$6+0 > 0 {s+=$6} END{print s+0}' "$RECORDS")"
|
||||
|
||||
echo "BRIEFING $(date '+%Y-%m-%d %H:%M') - repo som skylder et svar"
|
||||
echo "Kilder: STATE.md (NESTE + route-linje), git, coord-innboks. 0 modellkall."
|
||||
echo ""
|
||||
|
||||
if [ "${n_owe:-0}" -eq 0 ]; then
|
||||
echo "Ingen repo har uhaandtert innboks. Ingen skylder noen et svar i dag."
|
||||
# Still checked: "no repo owes" and "no mailbox is pending" are different
|
||||
# claims, and only the second one is the good news it reads as.
|
||||
brief_orphans
|
||||
echo ""
|
||||
echo "MERK: INN teller hva ANDRE venter paa fra deg. Hva et repo venter PAA"
|
||||
echo "staar kun i dets egen board-linje (blocked-on) - postkassen har ikke"
|
||||
echo "noe reply-to-felt, saa utgaaende venting er ikke utledbar derfra."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Most-owed first: the repo holding up the most other sessions is read first.
|
||||
awk -F'|' '$6+0 > 0' "$RECORDS" | sort -t'|' -k6,6nr | \
|
||||
while IFS='|' read -r bucket sortkey name status cost inbox dirty age last dir neste; do
|
||||
printf ' %-32s INN %-4s %s\n' "$name" "$inbox" "$status"
|
||||
# Wrapped, not cut - the whole line is the point, but a 500-character one
|
||||
# is unreadable in a file nobody watched being written. Locale is set for
|
||||
# the same reason trunc sets it: under LC_ALL=C fold counts BYTES and can
|
||||
# split a multibyte character into mojibake.
|
||||
# Trailing newline via '%s\n': fold copies its input's lack of one, and the
|
||||
# command line below would then start on the tail of the NESTE text.
|
||||
printf '%s\n' "$neste" | LC_ALL=en_US.UTF-8 fold -s -w 84 \
|
||||
| sed -e '1s/^/ NESTE: /' -e '2,$s/^/ /'
|
||||
printf ' %s\n\n' "$(brief_cmd "$dir" "$cost")"
|
||||
done
|
||||
|
||||
printf '%s repo skylder svar, %s meldinger totalt.\n' "$n_owe" "$tot_msg"
|
||||
brief_orphans
|
||||
echo ""
|
||||
echo "MERK: INN teller hva ANDRE venter paa fra deg. Hva et repo venter PAA"
|
||||
echo "staar kun i dets egen board-linje (blocked-on) - postkassen har ikke"
|
||||
echo "noe reply-to-felt, saa utgaaende venting er ikke utledbar derfra."
|
||||
}
|
||||
|
||||
if [ "$BRIEF" -eq 1 ]; then
|
||||
brief
|
||||
exit 0
|
||||
fi
|
||||
|
||||
count() { awk -F'|' -v b="$1" '$1==b' "$RECORDS" | wc -l | tr -d ' '; }
|
||||
|
||||
echo "BOARD - tverr-repo oppmerksomhetstavle ($(awk 'END{print NR}' "$RECORDS") repo)"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue