repo-mailbox/scripts/board.sh
Kjell Tore Guttormsen f9d2c927ab feat(board): give --plan a focus cutoff that reports what it held back
--focus "<prose>" narrows the day plan to repos whose STATE.md DECLARES a
matching topic marker. Measured on the real tree: 26 blocks to 6.

The filter alone was never the feature. --plan documents that it takes one
position (the order), hides nothing, and labels what it cannot rank rather
than dropping it, so a silent cutoff would break a property the format had
already written down. The same run prints the slugs the prose resolved to,
how many blocks it removed, the repos that MENTION a resolved slug with no
marker line - named, not counted - and how many STATE.md it searched. Each
surviving block carries the declaration it survived on.

Enumerated rather than counted because the decisive find behind this feature
was invisible to every string measurement until the held-back population was
listed. "nevner", never "dekker": this reports text found, and board.sh has
no grounds for a claim about relevance. The slug vocabulary comes from the
scanned STATE.md themselves, so the "no other file" invariant survives.

Emitted as key=value, not '#' comments: the consumer's parser runs in
paragraph mode and drops any block without tab=, so a comment would reach the
operator on the terminal path and vanish on the driver path - the one case
where the cutoff is applied unseen.

board-selftest 89 -> 113. The narrowing check is comparative, since every
presence-only assertion passes against a no-op filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186vKCzuUEN5WcJB82kddzF
2026-08-02 19:58:25 +02:00

692 lines
32 KiB
Bash
Executable file

#!/bin/bash
# board.sh - cross-repo attention board. Answers the one question no single
# repo's STATE.md can: across ALL repos, which have a live next step, which are
# blocked and on whom, which owe someone a reply, and what each costs to
# advance. Read-only: never writes to a repo, a STATE.md, or the mailbox.
#
# Sources (all pre-existing, nothing invented):
# STATE.md "NESTE" block - the next step, per repo (canonical)
# STATE.md board line - optional machine-readable field (see below)
# git status --porcelain - uncommitted risk
# git log -1 --format=%ct - when anything last landed (the SISTE column)
# ~/.claude/coord/<repo>/inbox - UNHANDLED INBOUND: others addressed this
# repo and it has not processed them. This is an
# obligation the repo owes outward - NOT evidence
# that the repo is waiting on anyone. The mailbox
# format has no reply-to/thread field, so
# outbound waiting is not derivable from it at
# all; that is exactly what blocked-on carries.
#
# Board line (optional, one per STATE.md, directly under the NESTE heading):
#
# <!-- board: status=in-progress; blocked-on=-; next-cost=Sonnet 5/xhigh -->
#
# status planned | in-progress | blocked | deferred | done
# blocked-on <repo-name> or - (only meaningful with status=blocked)
# next-cost <model>/<effort>, the model spelled EXACTLY as the global rubric
# spells it: Sonnet 5/xhigh, Opus 5/high. The parser below accepts
# any spelling, but this field is compared across repos by eye, so
# one form is the whole point - and a second spelling documented
# here is how a field with no write path drifts.
# The field now HAS a write path: route.sh emits it, and the set of
# legal values is that script's row table - not this comment, which
# shows the form only. `route.sh --help` is the authority; the two
# ends are pinned together by route-selftest.sh section 6.
#
# TWO AGE COLUMNS, ONE MEANING EACH. ALDER is the STATE.md mtime - when the
# plan was last touched - and is blank for a repo that has none. SISTE is the
# last commit, read for every repo. They were one column once, and it meant
# whichever of the two the repo's branch happened to compute: a repo WITH a
# STATE.md showed only its plan's age, so one that had not committed in a year
# was indistinguishable from one worked on this morning. Both are evidence for
# the reader and neither is a ranking input - the buckets and the sort are
# unchanged by this column.
#
# ATTENTION AXIS, NOT A TOPIC AXIS. This status vocabulary is deliberately NOT
# the vocabulary a cross-repo TOPIC register uses. A topic register answers
# "what is this repo's status on subject X (has it adopted convention Y?)";
# this line answers "does this REPO's own next step need me?". Topic tokens do
# not transfer: `not-applicable` is meaningless about a repo's next step, and a
# topic-level `partial` carries an ownership-and-next-step rule that belongs to
# the register, not here. Conflating the two axes is a real defect class - the
# board reads only its own axis, so keep them separate.
#
# --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.
#
# --plan is a THIRD rendering of that same scan, and the only one that takes a
# position: it answers which repos to open a tab for today and in what order.
# It prints key=value blocks, not prose, because it has two consumers - the
# operator pasting commands, and a separate repo driving a terminal from it.
# Ordering is deterministic (debt, then in-progress, then planned, then repos
# with no declared status) and there is no cutoff, so nothing is hidden.
# Read-only like the rest: --plan writes nothing, in the repo or the mailbox.
#
# --focus "<prose>" narrows --plan to the repos whose STATE.md DECLARES a
# matching topic marker (`<slug>: <status>`), and is the only cutoff this
# format has. It is therefore required to report what it held back: the same
# run prints fokus= (the slugs the prose resolved to), fokus_droppet= (how
# many blocks the cutoff removed), fokus_utenfor= (the repos that MENTION a
# resolved slug with no marker line, named - that class is where the decisive
# find came from), and fokus_rekkevidde= (how many STATE.md were searched;
# board opens no other file). Each surviving block carries fokus_treff=, the
# declaration it survived on. Prose matching no declared slug prints the FULL
# 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]
# [--focus "<prose>"]
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
# BOARD_ROOTS overrides the default scan roots.
# ASCII only, bash 3.2 safe.
set -u
export LC_ALL=C
COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
ROOTS="${BOARD_ROOTS:-$HOME/repos}"
NESTE_WIDTH=38
BRIEF=0
PLAN=0
FOCUS=""
# 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 ;;
# 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 ;;
# 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.
--focus) [ $# -ge 2 ] || { echo "board: --focus requires a value" >&2; exit 2; }
FOCUS="$2"; shift 2 ;;
--plain) shift ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "board: unknown argument: $1 (ignored)" >&2; shift ;;
esac
done
NOW="$(date +%s)"
# Truncate to N CHARACTERS (not bytes). A byte cut splits multibyte prose and
# emits mojibake; macOS `cut -c` is character-aware under a UTF-8 locale.
trunc() { printf '%s' "$1" | LC_ALL=en_US.UTF-8 cut -c1-"$2"; }
# --- Discovery: git repos at depth 1, plus depth 2 under polyrepo dirs ------
# A directory that is itself a git repo is one repo; a directory that is not
# but contains git repos is a polyrepo container (the plugin marketplace) and
# contributes its children, never itself.
#
# "Is a repo" tests .git with -e, not -d: a worktree or submodule has .git as a
# FILE. A plain `git worktree add <root>/feature-x` lands a depth-1 sibling that
# can CARRY its own STATE.md - a -d test drops it silently. Kept identical in
# the rollup builder (catalog) on purpose: two readers, one name.
REPOS=""
# Split on comma via IFS + `set --` rather than an unquoted $(...) expansion:
# unquoted word-splitting would also split roots containing spaces. Arg parsing
# is finished above, so clobbering the positional parameters is safe here.
OLD_IFS="$IFS"; IFS=','
set -- $ROOTS
IFS="$OLD_IFS"
for root in "$@"; do
[ -d "$root" ] || continue
for entry in "$root"/*; do
[ -d "$entry" ] || continue
if [ -e "$entry/.git" ]; then
REPOS="$REPOS
$entry"
else
for child in "$entry"/*; do
[ -e "$child/.git" ] || continue
REPOS="$REPOS
$child"
done
fi
done
done
[ -n "$(printf '%s' "$REPOS" | tr -d '[:space:]')" ] || exit 0
# --- Collect one record per repo -------------------------------------------
# Record: bucket|sortkey|name|status|cost|inbox|dirty|age|neste
RECORDS=""
MALFORMED=""
printf '%s\n' "$REPOS" | while IFS= read -r d; do
[ -n "$d" ] || continue
name="$(basename "$d")"
state="$d/STATE.md"
dirty="$(git -C "$d" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
[ -n "$dirty" ] || dirty=0
inbox=0
if [ -d "$COORD/$name/inbox" ]; then
inbox="$(ls "$COORD/$name/inbox"/*.md 2>/dev/null | wc -l | tr -d ' ')"
[ -n "$inbox" ] || inbox=0
fi
# Read for EVERY repo, not just the STATE-less ones: a repo whose plan file
# is fresh can still have been silent for a year, and that is precisely the
# repo no other column reports. A repo with no commits at all has no reading
# to give - printing a day count there would be a fabricated one.
lastct="$(git -C "$d" log -1 --format=%ct 2>/dev/null)"
if [ -n "$lastct" ]; then
lastd=$(( (NOW - lastct) / 86400 )); lastcol="${lastd}d"
else
lastd=-1; lastcol="-"
fi
if [ ! -f "$state" ]; then
# 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|%s|(ingen STATE.md)\n' \
"$lastd" "$name" "$inbox" "$dirty" "$lastcol" "$d"
continue
fi
mtime="$(stat -f %m "$state" 2>/dev/null)"
if [ -n "$mtime" ]; then age=$(( (NOW - mtime) / 86400 )); else age=0; fi
# Anchored to the exact comment form, NOT a substring search: unanchored
# 'board:' also matches prose like "dashboard: ..." and -m1 would let a
# lookalike higher up the file win over the real line.
line="$(grep -m1 '^<!-- board:' "$state" 2>/dev/null)"
status=""; blockedon=""; cost=""
if [ -n "$line" ]; then
status="$(printf '%s' "$line" | sed -n 's/.*status=\([a-z-]*\).*/\1/p')"
blockedon="$(printf '%s' "$line" | sed -n 's/.*blocked-on=\([A-Za-z0-9._-]*\).*/\1/p')"
# Value runs to the next ';' or the closing '-->', NOT to the first
# non-lowercase byte: the rubric names models "Sonnet 5 / xhigh", so a
# lowercase-only class silently drops spec-conformant values to "?".
cost="$(printf '%s' "$line" | sed -n 's/.*next-cost=\([^;>]*\).*/\1/p' \
| sed -e 's/--$//' -e 's/[[:space:]]*$//' -e 's/^[[:space:]]*//')"
fi
case "$status" in
planned|in-progress|blocked|deferred|done) ;;
"") status="?" ;;
*) status="MALFORMED:$status" ;;
esac
[ -n "$cost" ] || cost="?"
# First content line under the NESTE heading: skip blanks, HTML comments and
# the heading itself; strip markdown bold/bullet noise.
neste="$(awk '
/NESTE/ { flag=1; next }
flag {
if ($0 ~ /^[[:space:]]*$/) next
if ($0 ~ /^[[:space:]]*<!--/) next
if ($0 ~ /^#/) next
print; exit
}' "$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)"
disp="$status"
if [ "$status" = "blocked" ] && [ -n "$blockedon" ] && [ "$blockedon" != "-" ]; then
disp="blocked>$blockedon"
fi
case "$status" in
blocked) bucket=1 ;;
MALFORMED:*) bucket=2 ;;
in-progress|planned|"?") bucket=2 ;;
deferred) bucket=3 ;;
done) bucket=4 ;;
*) bucket=2 ;;
esac
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.$$"
trap '/bin/rm -f "$RECORDS" 2>/dev/null' EXIT
hdr() {
printf '\n%s\n' "$1"
printf '%-32s %-34s %-14s %4s %4s %6s %6s %s\n' \
"REPO" "STATUS" "KOST" "INN" "DRT" "ALDER" "SISTE" "NESTE"
}
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 dir neste; do
printf '%-32s %-34s %-14s %4s %4s %6s %6s %s\n' \
"$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.
# Shared by --brief and --plan, because there is one route line grammar and it
# gets one reader. Two no-command cases, and callers must keep them apart:
# exit 1 - no route line at all
# exit 0, empty out - a route line route.sh rejects (a typo'd trait value)
# Neither may become a guessed command, and neither may become a bare command
# marker: a driver reading `command=` would type an empty line into a live pane.
route_cmd_for() {
rc_line="$(grep -m1 '^<!-- route:' "$1/STATE.md" 2>/dev/null)"
[ -n "$rc_line" ] || return 1
rc_p="$(printf '%s' "$rc_line" | sed -n 's/.*path=\([a-z-]*\).*/\1/p')"
rc_v="$(printf '%s' "$rc_line" | sed -n 's/.*verification=\([a-z-]*\).*/\1/p')"
rc_r="$(printf '%s' "$rc_line" | sed -n 's/.*reversibility=\([a-z-]*\).*/\1/p')"
rc_s="$(printf '%s' "$rc_line" | sed -n 's/.*scope=\([a-z-]*\).*/\1/p')"
bash "$ROUTE" --path "$rc_p" --verification "$rc_v" \
--reversibility "$rc_r" --scope "$rc_s" --rationale brief 2>/dev/null \
| sed -n 's/^command=//p'
return 0
}
brief_cmd() {
if bc_cmd="$(route_cmd_for "$1")"; then
if [ -n "$bc_cmd" ]; then
printf '$ %s' "$bc_cmd"
else
printf 'KOST: %s (route-linjen kunne ikke tolkes)' "$2"
fi
else
printf 'KOST: %s (ingen route-linje - kjor route-skillen i det repoet)' "$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
# --- Focus resolution (--focus) --------------------------------------------
# --focus is the first CUTOFF this format has ever had, and the plan documents
# at length that it has none: it takes one position (the order), it hides
# nothing, and it LABELS what it cannot rank rather than dropping it. A filter
# that stayed silent about what fell outside it would break that property
# outright, so the report is not a refinement of the feature - it is the
# condition the feature was allowed to exist under.
#
# The topic marker grammar belongs to the register, not here. board.sh only
# ever READS a declaration, and reads it from STATE.md alone: the slug
# vocabulary is whatever the scanned STATE.md files themselves declare, so no
# new file is opened and the "STATE.md and nothing else" invariant survives.
# It also means the reader must accept the marker as operators actually write
# it - bold, backticked, bulleted - because the strict form is what the
# register's own grep looks for, and the single most consequential repo in the
# measurement behind this feature was invisible to exactly that grep.
FOCUS_STATUS='planned|in-progress|partial|blocked|deferred|done|not-applicable'
focus_marker_re() {
printf '^[[:space:]]*[-*]?[[:space:]]*\**`?%s`?\**:[[:space:]]+(%s)([[:space:]]|$)' \
"$1" "$FOCUS_STATUS"
}
# Every slug declared anywhere in the scanned tree. Field 10 of RECORDS is the
# repo directory; a repo with no STATE.md simply contributes nothing.
focus_slugs() {
awk -F'|' '{print $10}' "$RECORDS" | while read -r fs_d; do
[ -n "$fs_d" ] && [ -f "$fs_d/STATE.md" ] || continue
grep -hE "$(focus_marker_re '[a-z0-9][a-z0-9-]*')" "$fs_d/STATE.md" 2>/dev/null
done | sed -E 's/^[[:space:]]*[-*]?[[:space:]]*\**`?([a-z0-9][a-z0-9-]*)`?\**:.*/\1/' \
| sort -u
}
# Prose -> slugs. Two normalisations, because both readings occur: the operator
# types a slug ("llm-ingestion-guard") or a bare word out of their own day
# ("guard"). Matching is never narrowed to one winner - an ambiguous phrase
# widens the answer and every slug it resolved to is named in the output,
# because silently picking one would make the cutoff lie about its own size.
focus_resolve() {
fr_words=" $(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' ' ') "
fr_parts=" $(printf '%s' "$fr_words" | tr '-' ' ') "
focus_slugs | while read -r fr_s; do
[ -n "$fr_s" ] || continue
case "$fr_words" in
*" $fr_s "*) printf '%s\n' "$fr_s"; continue ;;
esac
for fr_p in $(printf '%s' "$fr_s" | tr '-' ' '); do
# Two-character parts match far too much prose to be evidence of intent.
[ "${#fr_p}" -ge 3 ] || continue
case "$fr_parts" in
*" $fr_p "*) printf '%s\n' "$fr_s"; break ;;
esac
done
done | sort -u
}
# Does this repo DECLARE any of the resolved slugs? (survives the cutoff)
focus_declares() {
for fd_s in $FOCUS_SLUGS; do
grep -qE "$(focus_marker_re "$fd_s")" "$1/STATE.md" 2>/dev/null && return 0
done
return 1
}
# What a surviving block survived ON. This is the per-block evidence that
# closed `topics=`: the need was real, but a field in all 27 blocks on every
# day the operator has no focus is noise, while the same fact inside a focused
# run is the reason the block is there.
focus_evidence() {
for fe_s in $FOCUS_SLUGS; do
fe_line="$(grep -m1 -E "$(focus_marker_re "$fe_s")" "$1/STATE.md" 2>/dev/null)"
if [ -n "$fe_line" ]; then
fe_st="$(printf '%s' "$fe_line" | sed -E "s/.*:[[:space:]]+($FOCUS_STATUS).*/\1/")"
printf '%s: %s\n' "$fe_s" "$fe_st"
return 0
fi
done
return 1
}
# The held-back population: STATE.md MENTIONS a resolved slug and declares no
# marker line for it. The wording is a constraint, not a style choice - this
# is a fact about text found in a file, and board.sh has no grounds whatever
# for a claim about relevance, so it says "nevner" and never "dekker". The
# class is NAMED rather than counted, because in the measurement that produced
# this feature the decisive find - a heavy consumer pinning the library in its
# build file - appeared only once the population was enumerated. Reasoning
# about it had missed it entirely.
focus_heldback() {
awk -F'|' '{print $3 "|" $10}' "$RECORDS" | while IFS='|' read -r fh_n fh_d; do
[ -n "$fh_d" ] && [ -f "$fh_d/STATE.md" ] || continue
focus_declares "$fh_d" && continue
for fh_s in $FOCUS_SLUGS; do
if grep -qF -- "$fh_s" "$fh_d/STATE.md" 2>/dev/null; then
printf '%s\n' "$fh_n"
break
fi
done
done
}
# --- Day-plan rendering (--plan) -------------------------------------------
# A THIRD rendering of the same scan, built on exactly the argument --brief was:
# it is a lookup over data the scan already holds, it costs zero model calls,
# and route.sh already derives the per-repo command. The table says what the
# state of every repo is; the briefing says who is waiting; the plan says which
# repos to open a tab for today, in what order, with which command.
#
# key=value blocks, not prose, because the plan has TWO consumers: the operator
# pasting commands, and a separate repo driving a terminal from it. Prose would
# make the rendered FORMAT an API, and no test in this repo could hold it stable
# for a consumer living in another one. Comment lines all start with '#', so a
# consumer drops them with one rule.
#
# ORDER IS THE POSITION THIS RENDERING TAKES, and it is the only one it takes -
# there is no cutoff, so nothing is hidden. Four rules, all deterministic over
# fields the scan already read:
# 1. INN > 0, most-owed first, WHATEVER the status. A message owed is an
# obligation to another session. Excluding `blocked` is about a repo's own
# next step, which by definition cannot be moved; answering is a different
# axis, and is frequently what unblocks it.
# 2. in-progress - live work, oldest plan first.
# 3. planned.
# 4. '?' and MALFORMED - no declared status. Planned LAST, and labelled. The
# table already prints a MERK line about repos with no board line; a plan
# that dropped them silently would repeat exactly that defect.
# Excluded: done, deferred, blocked-without-debt, and repos with no STATE.md and
# no debt - a tab that cannot be moved is not a plan entry.
# Two lines for two consumers, and they are not redundant. A driver cd's the
# pane itself and then types the command, so it needs them apart; the operator
# needs ONE thing to select and paste, because assembling `cd <dir>` from one
# field and the command from another is precisely where a tab ends up started
# in the wrong repo. paste= is emitted only alongside command= - `paste=cd X && `
# with nothing after it would run the cd and then a bare newline, leaving the
# operator in the right directory with no session and no error.
plan_cmd() {
if pc_cmd="$(route_cmd_for "$1")"; then
if [ -n "$pc_cmd" ]; then
printf 'command=%s\n' "$pc_cmd"
printf 'paste=cd %s && %s\n' "$1" "$pc_cmd"
else
printf 'command_missing=route-linjen kunne ikke tolkes (kost: %s)\n' "$2"
fi
else
printf 'command_missing=ingen route-linje - kjor route-skillen der (kost: %s)\n' "$2"
fi
}
plan() {
# Groups are appended in rank order to one file, then numbered in a single
# loop: `while ... < file` keeps the counter in THIS shell, where a pipe into
# while would run it in a subshell and reset every tab number to 1.
#
# Each group prefixes its `why` as a new FIRST field, so the sort keys shift
# by one: inbox 6->7, sortkey (ALDER) 2->3. Prefixing rather than appending is
# deliberate - `neste` is the last field and is free prose, so anything added
# after it could be swallowed by a stray separator in a STATE.md.
pf="${TMPDIR:-/tmp}/board-plan.$$"
: > "$pf"
awk -F'|' -v OFS='|' '$6+0 > 0 {print "inbox:" $6, $0}' "$RECORDS" \
| sort -t'|' -k7,7nr -k3,3n >> "$pf"
awk -F'|' -v OFS='|' '$6+0 == 0 && $4 == "in-progress" {print "in-progress", $0}' "$RECORDS" \
| sort -t'|' -k3,3n >> "$pf"
awk -F'|' -v OFS='|' '$6+0 == 0 && $4 == "planned" {print "planned", $0}' "$RECORDS" \
| sort -t'|' -k3,3n >> "$pf"
awk -F'|' -v OFS='|' '$6+0 == 0 && ($4 == "?" || $4 ~ /^MALFORMED:/) {print "uavklart", $0}' "$RECORDS" \
| sort -t'|' -k3,3n >> "$pf"
# The cutoff, and its disclosure, computed together - they are one feature.
FOCUS_SLUGS=""
fp_applied=0
fp_before="$(awk 'END{print NR+0}' "$pf")"
if [ -n "$FOCUS" ]; then
FOCUS_SLUGS="$(focus_resolve "$FOCUS" | tr '\n' ' ')"
[ -n "$(printf '%s' "$FOCUS_SLUGS" | tr -d ' ')" ] && fp_applied=1
fi
if [ "$fp_applied" -eq 1 ]; then
fp_names="${TMPDIR:-/tmp}/board-focusnames.$$"
fp_kept="${TMPDIR:-/tmp}/board-planfocus.$$"
awk -F'|' '{print $3 "|" $10}' "$RECORDS" | while IFS='|' read -r fp_n fp_d; do
[ -n "$fp_d" ] && [ -f "$fp_d/STATE.md" ] || continue
focus_declares "$fp_d" && printf '%s\n' "$fp_n"
done > "$fp_names"
awk -F'|' 'NR==FNR{keep[$0]=1;next} keep[$4]' "$fp_names" "$pf" > "$fp_kept"
/bin/rm -f "$fp_names" 2>/dev/null
mv "$fp_kept" "$pf"
fi
echo "# PLAN $(date '+%Y-%m-%d %H:%M') - en blokk per tab, i den rekkefolgen"
echo "# Kilder: STATE.md (NESTE + route-linje), git, coord-innboks. 0 modellkall."
echo "# Rekkefolge: innboksgjeld (INN desc), sa in-progress, sa planned, sa uavklart."
# "uten gjeld" governs the WHOLE list, not just the token nearest to it: a
# done or deferred repo that owes mail IS planned, and the real tree has two.
# Read the other way this line calls its own tab 4 a bug.
echo "# Utelatt naar repoet ikke skylder svar: done, deferred, blocked, uten STATE.md."
# Emitted as key=value, not as a '#' comment, because the format's second
# consumer drops every comment line by rule - a disclosure written as a
# comment would reach the operator on the terminal path and vanish on the
# driver path, which is the one case where the cutoff is applied unseen.
if [ -n "$FOCUS" ]; then
fp_state="$(awk -F'|' '{print $10}' "$RECORDS" | while read -r fp_sd; do
[ -n "$fp_sd" ] && [ -f "$fp_sd/STATE.md" ] && echo x
done | wc -l | tr -d ' ')"
if [ "$fp_applied" -eq 1 ]; then
printf 'fokus=%s\n' "$(printf '%s' "$FOCUS_SLUGS" | sed 's/[[:space:]]*$//' | tr ' ' ',')"
printf 'fokus_droppet=%s av %s blokker\n' \
"$((fp_before - $(awk 'END{print NR+0}' "$pf")))" "$fp_before"
fp_hb="$(focus_heldback | sort -u)"
fp_hbn="$(printf '%s' "$fp_hb" | grep -c . | tr -d ' ')"
# "nevner", never "dekker": this states what was found in a file, and
# says which files were searched rather than implying it searched repos.
# One repo in the real measurement carries its strongest evidence in a
# README, which this scan never opens.
printf 'fokus_utenfor=%s repo nevner %s uten markorlinje: %s\n' \
"$fp_hbn" "$(printf '%s' "$FOCUS_SLUGS" | sed 's/[[:space:]]*$//' | tr ' ' ',')" \
"$(printf '%s' "$fp_hb" | tr '\n' ' ' | sed -e 's/[[:space:]]*$//' -e 's/^$/-/')"
printf 'fokus_rekkevidde=sokt i %s STATE.md - board leser ingen andre filer\n' "$fp_state"
else
# No declared slug matched. The full plan is printed: the driver forwards
# operator prose verbatim, so a typo must not silently produce a morning
# with no tabs at all.
printf 'fokus_ikke_brukt=%s traff ingen deklarert slug i %s STATE.md - hele planen vises\n' \
"$FOCUS" "$fp_state"
fi
fi
echo ""
pn=0
while IFS='|' read -r why bucket sortkey name status cost inbox dirty age last dir neste; do
[ -n "$name" ] || continue
pn=$((pn + 1))
printf 'tab=%s\n' "$pn"
printf 'repo=%s\n' "$name"
# Absolute, and the driver must cd into it explicitly: a new terminal pane
# inherits its anchor's working directory, so a plan that omitted this would
# look right and point at the wrong repo.
printf 'dir=%s\n' "$dir"
printf 'why=%s\n' "$why"
if [ "$fp_applied" -eq 1 ]; then
fp_ev="$(focus_evidence "$dir")" && printf 'fokus_treff=%s\n' "$fp_ev"
fi
printf 'status=%s\n' "$status"
printf 'neste=%s\n' "$neste"
plan_cmd "$dir" "$cost"
echo ""
done < "$pf"
/bin/rm -f "$pf" 2>/dev/null
printf '# %s tabber.\n' "$pn"
# An orphan mailbox has no directory to cd into, so it cannot BE a tab - but
# omitting it lets the plan claim a completeness it knows it lacks. Reported
# as commentary, reusing the briefing's single copy of that cross-check.
po="$(brief_orphans)"
if [ -n "$po" ]; then
printf '%s\n' "$po" | sed -e 's/^/# /' -e 's/^# *$/#/'
fi
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 [ "$PLAN" -eq 1 ]; then
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)"
echo "INN = uhaandtert innboks (andre venter paa DEG). DRT = ukommiterte filer."
echo "ALDER = dager siden STATE.md endret. SISTE = dager siden siste commit."
[ "$(count 1)" -gt 0 ] && { hdr "BLOKKERT (venter paa ekstern avhengighet)"; rows 1; }
[ "$(count 2)" -gt 0 ] && { hdr "AKTIV (reelt neste steg)"; rows 2; }
[ "$(count 3)" -gt 0 ] && { hdr "UTSATT (deferred - bevisst valg, ikke venting)"; rows 3; }
[ "$(count 4)" -gt 0 ] && { hdr "FERDIG"; rows 4; }
[ "$(count 5)" -gt 0 ] && { hdr "UTEN STATE.md (sovende / ubestemt tilstand)"; rows 5; }
# Obligations and risk read across buckets, so they get their own roll-up.
tot_in="$(awk -F'|' '{s+=$6} END{print s+0}' "$RECORDS")"
tot_dirty="$(awk -F'|' '{s+=$7} END{print s+0}' "$RECORDS")"
n_mal="$(grep -c 'MALFORMED' "$RECORDS" 2>/dev/null | tr -d ' ')"
n_nofield="$(awk -F'|' '$4=="?"' "$RECORDS" | wc -l | tr -d ' ')"
printf '\nSUM: %s uhaandterte innboks-meldinger, %s ukommiterte filer.\n' "$tot_in" "$tot_dirty"
[ "${n_mal:-0}" -gt 0 ] && printf 'ADVARSEL: %s repo har MALFORMED status-token (utenfor det lukkede settet).\n' "$n_mal"
[ "${n_nofield:-0}" -gt 0 ] && printf 'MERK: %s repo mangler board-linje - status/kost er ukjent (?), NESTE-utdrag brukes.\n' "$n_nofield"
exit 0