feat(board): add --inbox-plan, a fourth rendering independent of --plan's admission

morning-driver's own regression (0.22.0 narrowed --plan's admission gate,
and morning --innboks derives its population from --plan, so 21 mailboxes
with unhandled post produced only 3 openable tabs): --plan answers "which
repos deserve a tab today" (admission, ranking, a cap), --inbox-plan answers
"which repos have unhandled post" (population, no judgement, no cap) - a
superset of --plan's candidates, not a complement.

One block per name with pending>0 in the mailbox, classified against
board.sh's own RECORDS scan (no new discovery logic): class=repo (STATE.md
present), no-state (scanned repo, no STATE.md), orphan-mailbox (no matching
directory at all). pending=/owed= surface coord-count.sh's two counts
directly. TDD-first: board-selftest.sh section 15 (6 fixtures, isolated
root+mailbox) written and confirmed red before implementation.

Accepted work order: morning-driver 20260814T175317Z, corrected 20260814T180854Z.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mKC7v8gytdsota36tL7oP
This commit is contained in:
Kjell Tore Guttormsen 2026-08-14 20:30:22 +02:00
commit 09fd4b74fa
2 changed files with 303 additions and 5 deletions

View file

@ -125,7 +125,33 @@
# plan plus fokus_ikke_brukt= - never an empty one, since the prose arrives
# verbatim from the operator and a typo must not empty the morning.
#
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain] [--brief|--plan]
# --inbox-plan is a FOURTH rendering, ordered by morning-driver
# (20260814T175317Z, corrected 20260814T180854Z) as a fix for its OWN
# regression, not a feature request: --plan answers "which repos deserve a
# tab today" (admission, ranking, a cap); --inbox-plan answers "which repos
# have unhandled post" (population, no judgement, no cap) - and deriving the
# second from the first was always a shortcut, one that broke the moment
# --plan's admission narrowed in 0.22.0. It is a superset of --plan's
# candidates, not a complement: an already-admitted repo still gets a block.
#
# ONE BLOCK PER NAME WITH PENDING MAIL, independent of --plan's admission
# gate entirely. Each block carries `class=` distinguishing the three reasons
# a block might be unopenable, per the work order's explicit ask - a repo
# without STATE.md and a mailbox without a directory are NOT the same fact
# for the operator, and collapsing them into one undifferentiated "kan ikke
# aapnes" is the defect this mode exists to fix:
# repo - dir, .git and STATE.md all present; may still lack a
# route line, in which case command_missing= says so
# (same route_cmd_for() every other rendering uses).
# no-state - a scanned git repo with no STATE.md at all, so no NESTE
# and no route line can ever exist for it.
# orphan-mailbox - a mailbox name with no matching directory anywhere in
# the scanned roots; there is no dir= to cd into.
# `pending=` and `owed=` surface coord-count.sh's two counts directly (its
# debt field, not re-derived) so a caller can tell a request from a notice
# without recomputing it - the second explicit ask.
#
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain] [--brief|--plan|--inbox-plan]
# [--focus "<prose>"]
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
# BOARD_ROOTS overrides the default scan roots.
@ -138,6 +164,7 @@ ROOTS="${BOARD_ROOTS:-$HOME/repos}"
NESTE_WIDTH=38
BRIEF=0
PLAN=0
INBOX_PLAN=0
FOCUS=""
# Sibling calculator, invoked rather than reimplemented: the rubric that turns
@ -151,9 +178,10 @@ while [ $# -gt 0 ]; do
# 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 ;;
# Three renderings of one scan, so exactly one may be selected: last wins.
--brief) BRIEF=1; PLAN=0; shift ;;
--plan) PLAN=1; BRIEF=0; shift ;;
# Four renderings of one scan, so exactly one may be selected: last wins.
--brief) BRIEF=1; PLAN=0; INBOX_PLAN=0; shift ;;
--plan) PLAN=1; BRIEF=0; INBOX_PLAN=0; shift ;;
--inbox-plan) INBOX_PLAN=1; BRIEF=0; PLAN=0; shift ;;
# Raw operator prose, forwarded verbatim by the driver: it does not
# tokenize, match or normalize, so every bit of that work is here. Same
# `shift 2` guard as --roots, for the same bash 3.2 reason.
@ -328,7 +356,8 @@ done > "${TMPDIR:-/tmp}/board.$$"
RECORDS="${TMPDIR:-/tmp}/board.$$"
UNBLOCKS="${TMPDIR:-/tmp}/board-unblocks.$$"
OWED="${TMPDIR:-/tmp}/board-owed.$$"
trap '/bin/rm -f "$RECORDS" "$UNBLOCKS" "$OWED" 2>/dev/null' EXIT
ALLMAIL="${TMPDIR:-/tmp}/board-allmail.$$"
trap '/bin/rm -f "$RECORDS" "$UNBLOCKS" "$OWED" "$ALLMAIL" 2>/dev/null' EXIT
# --- Owed lookup (name -> messages that actually owe a reply) --------------
# `inbox` (field 6 of RECORDS) is raw pending-file count and stays that way -
@ -354,6 +383,20 @@ if { [ "$BRIEF" -eq 1 ] || [ "$PLAN" -eq 1 ]; } && [ -f "$SELFDIR/coord-count.sh
| awk -F"$ow_tab" -v OFS='|' '{print $1, $3}' > "$OWED"
fi
# --- All-mail lookup (name -> pending, owed), --inbox-plan only ------------
# --inbox-plan's whole population is "every name coord-count.sh reports", so
# unlike $OWED (which only needs the debt column) this keeps BOTH integers -
# pending is the population test, owed is the per-block ask #2 marker. Built
# only for --inbox-plan, the only consumer, for the same reason $OWED is
# gated: an extra coord-count.sh subprocess on every table/plain invocation
# would tax a path that never reads it.
: > "$ALLMAIL"
if [ "$INBOX_PLAN" -eq 1 ] && [ -f "$SELFDIR/coord-count.sh" ]; then
am_tab="$(printf '\t')"
bash "$SELFDIR/coord-count.sh" 2>/dev/null \
| awk -F"$am_tab" -v OFS='|' '{print $1, $2, $3}' > "$ALLMAIL"
fi
# --- Chain-root credit -----------------------------------------------------
# For every blocked repo, walk `blocked-on` up to the ROOT of the chain - the
# first repo that is not itself blocked - and credit that root with one repo
@ -906,6 +949,82 @@ if [ "$PLAN" -eq 1 ]; then
exit 0
fi
# --- Inbox-plan rendering (--inbox-plan) ------------------------------------
# See the file-header comment for the full rationale. One block per NAME with
# pending>0 in $ALLMAIL, joined against $RECORDS by FILENAME (never NR==FNR -
# the plan()'s own regression comment above explains why: the first file being
# empty silently misroutes the entire second file's lines, and $RECORDS here
# is never empty but the general idiom is worth staying consistent about).
#
# class is read off $RECORDS' own bucket, not recomputed: bucket 5 is exactly
# "no STATE.md" (see the discovery loop's own `printf '5|...'` line above), and
# a name absent from $RECORDS entirely never had a directory in the scan.
inbox_plan() {
ipf="${TMPDIR:-/tmp}/board-inboxplan.$$"
awk -F'|' -v OFS='|' -v RCF="$RECORDS" '
FILENAME==RCF {
name = $3
rc_bucket[name] = $1; rc_status[name] = $4; rc_cost[name] = $5
rc_dir[name] = $10; rc_neste[name] = $12
next
}
{
name = $1; pend = $2 + 0; owed = $3 + 0
if (name in rc_bucket) {
cls = (rc_bucket[name] == 5) ? "no-state" : "repo"
dir = rc_dir[name]; status = rc_status[name]; cost = rc_cost[name]
neste = rc_neste[name]
} else {
cls = "orphan-mailbox"; dir = "-"; status = "-"; cost = "-"; neste = "-"
}
print owed, pend, name, cls, dir, status, cost, neste
}
' "$RECORDS" "$ALLMAIL" | sort -t'|' -k1,1nr -k2,2nr -k3,3 > "$ipf"
echo "# INBOX-PLAN $(date '+%Y-%m-%d %H:%M') - en blokk per repo/mailbox med uhaandtert post"
echo "# Kilder: coord-count.sh (pending+owed), STATE.md, git. 0 modellkall."
echo "# Populasjon: alt med pending>0, uavhengig av --plan sin admisjon - ingen tak,"
echo "# ingen dom. Et repo --plan alt admitterer er fortsatt med (overmengde, ikke komplement)."
echo "# class=repo (STATE.md finnes) | no-state (repo finnes, STATE.md mangler) |"
echo "# orphan-mailbox (ingen katalog for postkassen i det skannede omraadet)."
echo "# pending=meldinger totalt, owed=derav skylder svar (resten er FYI)."
echo ""
ipn=0
while IFS='|' read -r owed pend name cls dir status cost neste; do
[ -n "$name" ] || continue
ipn=$((ipn + 1))
printf 'tab=%s\n' "$ipn"
printf 'repo=%s\n' "$name"
printf 'dir=%s\n' "$dir"
printf 'class=%s\n' "$cls"
printf 'pending=%s\n' "$pend"
printf 'owed=%s\n' "$owed"
printf 'status=%s\n' "$status"
printf 'neste=%s\n' "$neste"
case "$cls" in
# Same route_cmd_for()/plan_cmd() every other rendering uses - one copy
# of "how a command is derived from a route line", not a second guess.
repo) plan_cmd "$dir" "$cost" ;;
# No STATE.md means no route line can exist - naming that plainly is
# the whole point of a separate class, not a guessed command.
no-state) printf 'command_missing=ingen STATE.md - repoet finnes, men har ingen plan\n' ;;
orphan-mailbox) printf 'command_missing=ingen katalog for denne postkassen i det skannede omraadet\n' ;;
esac
echo ""
done < "$ipf"
/bin/rm -f "$ipf" 2>/dev/null
printf '# %s blokker.\n' "$ipn"
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)."
}
if [ "$INBOX_PLAN" -eq 1 ]; then
inbox_plan
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)"