feat(board): render the day plan as a third rendering of the same scan
The table says what the state of every repo is; the briefing says who is waiting. Neither says which repos to open a tab for today, in what order, and with which command - so a day plan was assembled by hand every morning from a table that deliberately takes no position. --plan is built on exactly the argument --brief was: a lookup over data the scan already holds, zero model calls, and route.sh already derives the per-repo command. The order IS the position it takes, and the only one - there is no cutoff, so nothing is hidden. The rule most likely to be "fixed" into a defect: repos owing mail rank first regardless of status. Excluding blocked or done is a claim about a repo's OWN next step, which by definition cannot be moved, while owing an answer is the other axis entirely - and answering is often what unblocks it. Measured on the real tree, two of 26 planned repos were done with an unhandled inbox, so the fixture tree pins done-with-debt and blocked-with-debt rather than the general rule alone. Repos with no board line rank last and are LABELLED, because the table already prints a note about them and dropping them silently would repeat that defect. key=value rather than prose, because the plan has two consumers: the operator, and a separate repo driving a terminal from it. Prose would make the rendered format an API no test here could hold stable for a consumer living in another repo. dir= is absolute because a new pane inherits its anchor's directory; command_missing= carries both no-command causes, because a driver reading ^command= would type a blank line into a live pane. route_cmd_for() becomes the single reader of the route-line grammar, shared with --brief, distinguishing the two failure causes by exit code rather than by an empty string. --brief output is unchanged. Driving a terminal from the plan stays OUT of this repo, on the measurement in docs/ghostty-orchestration-measurement.md rather than on taste. board-selftest: 51 -> 86 checks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011eKRzhD1RqY2MxvDEkrJAM
This commit is contained in:
parent
5d9c76763d
commit
6d0fe14d0f
10 changed files with 485 additions and 31 deletions
161
scripts/board.sh
161
scripts/board.sh
|
|
@ -65,7 +65,15 @@
|
|||
# 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]
|
||||
# --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.
|
||||
#
|
||||
# Usage: board.sh [--roots <dir>[,<dir>...]] [--plain] [--brief|--plan]
|
||||
# Env: CLAUDE_COORD_DIR overrides the mailbox root.
|
||||
# BOARD_ROOTS overrides the default scan roots.
|
||||
# ASCII only, bash 3.2 safe.
|
||||
|
|
@ -76,6 +84,7 @@ COORD="${CLAUDE_COORD_DIR:-$HOME/.claude/coord}"
|
|||
ROOTS="${BOARD_ROOTS:-$HOME/repos}"
|
||||
NESTE_WIDTH=38
|
||||
BRIEF=0
|
||||
PLAN=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
|
||||
|
|
@ -88,7 +97,9 @@ 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 ;;
|
||||
--brief) BRIEF=1; shift ;;
|
||||
# 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 ;;
|
||||
--plain) shift ;;
|
||||
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "board: unknown argument: $1 (ignored)" >&2; shift ;;
|
||||
|
|
@ -261,24 +272,34 @@ rows() {
|
|||
# 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() {
|
||||
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"
|
||||
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 (route-linjen kunne ikke tolkes)' "$2"
|
||||
printf 'KOST: %s (ingen route-linje - kjor route-skillen i det repoet)' "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -361,6 +382,110 @@ if [ "$BRIEF" -eq 1 ]; then
|
|||
exit 0
|
||||
fi
|
||||
|
||||
# --- 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.
|
||||
plan_cmd() {
|
||||
if pc_cmd="$(route_cmd_for "$1")"; then
|
||||
if [ -n "$pc_cmd" ]; then
|
||||
printf 'command=%s\n' "$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"
|
||||
|
||||
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."
|
||||
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"
|
||||
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)"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue