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:
Kjell Tore Guttormsen 2026-08-02 06:40:22 +02:00
commit 6d0fe14d0f
10 changed files with 485 additions and 31 deletions

View file

@ -1,6 +1,6 @@
{
"name": "repo-mailbox",
"version": "0.15.0",
"version": "0.16.0",
"description": "Local mailbox for coordination between Claude Code sessions in different repositories. Directed messages and broadcasts as plain Markdown files on your own disk, injected as context at session start. Local, private, no network.",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -5,6 +5,71 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.16.0] - 2026-08-02
### Added
- **`board.sh --plan` — the day plan, a THIRD rendering of the same scan.** 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 to start each. 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 order is the position it takes, and the only one.** There is no cutoff,
so nothing is hidden, and four deterministic rules decide the ranking:
1. **Repos owing mail come first, most-owed first — whatever their status.**
This is the rule most likely to be "fixed" into a defect. Excluding
`blocked` or `done` is a claim about a repo's *own next step*, which by
definition cannot be moved; owing an answer is a different axis entirely,
and answering is frequently what unblocks it. Measured on the real tree at
this version, two of 26 planned repos were `done` with an unhandled inbox,
which is why the fixture tree now pins both `done`-with-debt and
`blocked`-with-debt rather than the general rule alone.
2. `in-progress` — live work, oldest plan first.
3. `planned`.
4. Repos with no board line, ranked last and **labelled** `why=uavklart`. The
table already prints a note about these; a plan that dropped them silently
would repeat exactly that defect.
Excluded, and only when the repo owes nothing: `done`, `deferred`, `blocked`,
and repos with no STATE.md. A tab that cannot be moved is not a plan entry.
- **`key=value` blocks rather than prose, because the plan has two consumers:**
the operator pasting a command, and a separate repo driving a terminal from
it. Prose would make the rendered format an API that no test in this repo
could hold stable for a consumer living in another one. Comment lines all
start with `#`, so a consumer drops them with one rule.
Two fields carry measured constraints rather than preferences. `dir=` is
absolute because a new terminal pane inherits its anchor's working directory,
so a plan that omitted it would look right and point at the wrong repo. And a
repo whose route line is missing or unparseable gets `command_missing=` with
the reason, never an empty `command=`: a driver reading `^command=` would type
a blank line into whatever that pane is running.
### Changed
- `route_cmd_for()` is now the single reader of the route-line grammar, shared
by `--brief` and `--plan`. It distinguishes "no route line" from "a route line
`route.sh` rejects" by **exit code** rather than by an empty string, because
both must degrade to a marker and neither may become a guess. `--brief` output
is unchanged.
- `board-selftest.sh`: 51 -> 86 checks.
### Notes
- **Driving a terminal from this plan deliberately does not live here.** The
measurement in `docs/ghostty-orchestration-measurement.md` is the argument:
it would be a version-pinned, undocumented composition over a preview API
whose documented path is already broken upstream and whose regression was
closed as *not planned*, with a blast radius reaching into other repos' live
sessions. The dependency runs one way — the driver consumes the plan, the plan
never knows a terminal exists — so if that API breaks, the plan still prints.
- The `0.15.0` entry is missing from this file; the release itself shipped and is
tagged. This is a gap in the log, not in the code.
## [0.14.0] - 2026-08-01
### Added

View file

@ -54,7 +54,7 @@ marketplace plugin. Three components, one boundary:
- **Board (`scripts/board.sh`):** cross-repo attention board. Reads STATE.md
next-step blocks + board lines, `git status`, and mailbox pending counts, and
prints one line per repo. Read-only by construction: it writes to no repo, no
STATE.md and no mailbox. Pinned by `board-selftest.sh` (51 checks).
STATE.md and no mailbox. Pinned by `board-selftest.sh` (86 checks).
**It lives here because the mailbox is one of its three inputs, and it carries
the same axis distinction the mailbox does.** A pending count means *others
@ -82,6 +82,38 @@ marketplace plugin. Three components, one boundary:
differing on it. A repo with no route line is told so rather than handed a
guess, because a guessed command reads as authoritative.
**`board.sh --plan` is the THIRD rendering, and the only one that takes a
position.** It answers which repos to open a tab for today, in what order,
with which command. The position it takes is the ORDER and nothing else -
there is no cutoff, so the plan hides nothing, and every rule is a lookup over
fields the scan already read. Debt ranks first regardless of status, and that
is the rule most likely to be "fixed" into a defect: excluding `blocked` or
`done` is a claim about a repo's OWN next step, which by definition cannot be
moved, while owing a reply is the other axis entirely - answering is often
what unblocks it. Measured on the real tree at 0.16.0, two of 26 planned repos
were `done` with an unhandled inbox. Repos with no board line rank last and
are LABELLED rather than dropped, because the table already prints a MERK line
about them and a plan that omitted them silently would repeat that defect.
It renders `key=value` blocks, not prose, because it has two consumers: the
operator, and a driver repo consuming the plan. Prose would make the rendered
format an API no test in THIS repo could hold stable for a consumer in
another. `command_missing=` carries both no-command causes (no route line, and
a route line route.sh rejects) because a bare `command=` is the shape of a
runnable command carrying nothing - a driver reading `^command=` would type an
empty line into a live pane. `route_cmd_for()` is the single reader of the
route-line grammar, shared with `--brief`, and distinguishes the two causes by
exit code rather than by an empty string.
**Driving a terminal from the plan does NOT belong here, and the measurement
in `docs/ghostty-orchestration-measurement.md` is the argument, not taste.**
It is a version-pinned undocumented composition over a preview API whose
documented path is already broken upstream and whose regression was closed as
not planned, with a blast radius reaching into other repos' live sessions.
None of that is mailbox transport, and none of it may be able to break
`coord-inbox` or `board`. The dependency runs one way: the driver consumes the
plan, the plan never knows a terminal exists.
It also cross-checks itself against `coord-count.sh`, and that is not
belt-and-braces. 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
@ -209,7 +241,7 @@ obligations in another repo.
builtins only in hook and tests.
- TDD: no behavior change without a failing selftest check first.
`bash scripts/coord-selftest.sh` must exit 0 (183/183),
`bash scripts/board-selftest.sh` must exit 0 (51/51) and
`bash scripts/board-selftest.sh` must exit 0 (86/86) and
`bash scripts/route-selftest.sh` must exit 0 (73/73).
- English for all code, docs, and commit messages (public repo). Norwegian
trigger aliases in the skill description are deliberate.

View file

@ -8,7 +8,7 @@
*AI-generated: all code produced by Claude Code through dialog-driven development.*
![Version](https://img.shields.io/badge/version-0.15.0-blue)
![Version](https://img.shields.io/badge/version-0.16.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Hooks](https://img.shields.io/badge/hooks-1-green)
![Skills](https://img.shields.io/badge/skills-3-orange)
@ -94,7 +94,7 @@ Scoring is judgement and belongs to the skill; turning scores into a row is a lo
coord-done.sh <filename>... | --all # archive without replying
coord-count.sh [--exclude <mailbox>] # per mailbox: pending + replies owed, delivering nothing
coord-sweep.sh [--write] [--days <n>] [--log <path>] # close aged notices machine-wide (dry-run by default)
board.sh [--roots <dir>[,<dir>...]] [--brief] # cross-repo attention board (read-only)
board.sh [--roots <dir>[,<dir>...]] [--brief|--plan] # cross-repo attention board (read-only)
brief-nightly.sh # render the briefing to a file, atomically
route.sh --path <v> --verification <v> --reversibility <v> \
--scope <v> --rationale "<why>" # model + effort for the next session
@ -107,6 +107,12 @@ The reply/resolve hints the hook injects (`-> reply: coord-send --reply-to …
It makes **zero model calls**, which is the point rather than a detail. Under subscription auth a headless session draws from the same quota pool as interactive work, and `--max-budget-usd` is a runaway brake rather than a pre-flight gate — measured against 2.1.220, it aborts *after* the first turn, never before it. `board.sh --brief` writes nothing; the file write lives in `brief-nightly.sh`, which renders to a temp file and renames it into place, and refuses to replace a good briefing with an empty render. `launchd/` holds a sample agent that runs it nightly; it points at a checkout, never at the version-pinned plugin cache.
**`board.sh --plan` renders the day plan**, a *third* rendering of that same scan and the only one that takes a position: which repos to open a tab for today, in what order, and the command to start each. The order is the position, and there is no cutoff — nothing is hidden, and four deterministic rules decide it. Repos owing mail come first, most-owed first, *whatever their status*: excluding `blocked` or `done` is a statement about a repo's own next step, which by definition cannot be moved, while owing an answer is a different axis and answering is often what unblocks it. Then live work (`in-progress`), then `planned`, then last and labelled, the repos with no board line at all — the table already prints a note about those, so a plan that dropped them silently would repeat exactly that defect. Still zero model calls, still read-only, and still cross-checked against `coord-count.sh`.
It prints `key=value` blocks rather than prose because it has **two consumers**: the operator pasting a command, and a separate repo driving a terminal from it. Prose would make the rendered format an API that no test here could hold stable for a consumer living in another repo. Each block carries `dir=` as an absolute path — a new terminal pane inherits its anchor's working directory, so a plan that omitted it would look right and point at the wrong repo. A repo whose route line is missing or unparseable gets `command_missing=` with the reason, never an empty `command=`: a driver reading `^command=` would type a blank line into a live pane.
Driving a terminal from this plan deliberately lives **outside this repo**. That work is a version-pinned, undocumented composition on top of a preview API whose documented path is already broken upstream, and its blast radius reaches into other repos' running sessions. The dependency runs one way — the driver consumes the plan, the plan never knows a terminal exists — so if the terminal API breaks, the plan still prints and the operator still pastes.
## Security Model
Cross-repo message content is untrusted input by design:
@ -141,7 +147,7 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
## Development
bash scripts/coord-selftest.sh # 183 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 36 checks against a throwaway repo tree
bash scripts/board-selftest.sh # 86 checks against a throwaway repo tree
bash scripts/route-selftest.sh # 73 checks, incl. the route->board round trip
npm test # all three selftests via node --test

View file

@ -1,6 +1,6 @@
{
"name": "repo-mailbox",
"version": "0.15.0",
"version": "0.16.0",
"private": true,
"type": "module",
"engines": {

View file

@ -428,6 +428,199 @@ CLAUDE_BRIEF_FILE="$OUTFILE" BOARD_ROOTS="$ROOT/does-not-exist" \
grep -q 'FORRIGE BRIEFING' "$OUTFILE"
check "empty render never overwrites the previous briefing" $?
# --- 10. Day-plan rendering (--plan) ---------------------------------------
# A THIRD rendering of the same scan, on exactly the argument --brief was built
# on. The table answers "what is the state of every repo"; the briefing answers
# "who is waiting on me"; the plan answers "which repos do I open a tab for
# today, in what order, with which command". It has TWO consumers - the operator
# pasting commands, and a separate driver repo consuming the plan - which is why
# it renders key=value blocks rather than prose: prose would make the FORMAT an
# API that no test in this repo could hold stable.
#
# repo-blocked-owes: blocked AND owing mail. The exclusion of `blocked` is about
# a repo's OWN next step, which by definition cannot be moved; answering a
# message is a different axis, and is often precisely what unblocks it. Without
# this fixture the debt-first rule and the status exclusion never meet, and
# whichever one was written second would silently win.
mkrepo "$ROOT/repo-blocked-owes"
{
echo "# STATE - repo-blocked-owes"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=blocked; blocked-on=repo-a; next-cost=Opus 5/high -->"
printf 'Venter p%s repo-a, men skylder to svar.\n' "$EMDASH"
} > "$ROOT/repo-blocked-owes/STATE.md"
mkdir -p "$CLAUDE_COORD_DIR/repo-blocked-owes/inbox"
for n in 1 2; do
echo "msg" > "$CLAUDE_COORD_DIR/repo-blocked-owes/inbox/2026-msg$n-from-w.md"
done
# repo-done-owes: done AND owing mail. Same rule as repo-blocked-owes, but this
# is the shape that actually occurs - measured on the real tree at 0.16.0, two
# of the 26 planned repos were `done` with an unhandled inbox, and none were
# blocked-with-debt. A repo whose own work is finished can still owe an answer.
mkrepo "$ROOT/repo-done-owes"
{
echo "# STATE - repo-done-owes"
printf '## %s NESTE %s START HER\n' "$HAND" "$EMDASH"
echo "<!-- board: status=done; blocked-on=-; next-cost=Sonnet 5/high -->"
printf 'Arbeidet er ferdig %s men innboksen er ikke gjort opp.\n' "$EMDASH"
} > "$ROOT/repo-done-owes/STATE.md"
mkdir -p "$CLAUDE_COORD_DIR/repo-done-owes/inbox"
echo "msg" > "$CLAUDE_COORD_DIR/repo-done-owes/inbox/2026-msg1-from-w.md"
PLAN="$("$BOARD" --roots "$ROOT" --plan 2>/dev/null)"; rc=$?
check "--plan exits 0" "$rc"
printf '%s' "$PLAN" | grep -q '^# PLAN '
check "plan carries a header naming what it is" $?
# Machine-readable by construction: the driver repo reads these keys.
printf '%s' "$PLAN" | grep -q '^tab=1$'
check "plan numbers tabs starting at 1" $?
# Debt outranks everything, most-owed first. repo-a owes 3, repo-blocked-owes 2,
# repo-owes and repo-typo 1 each.
[ "$(printf '%s\n' "$PLAN" | grep -A1 '^tab=1$' | grep '^repo=' | cut -d= -f2)" = "repo-a" ]
check "the repo owing the most messages is tab 1" $?
n_a="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-a$' | cut -d: -f1)"
n_bo="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-blocked-owes$' | cut -d: -f1)"
n_ow="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-owes$' | cut -d: -f1)"
[ -n "$n_a" ] && [ -n "$n_bo" ] && [ -n "$n_ow" ] \
&& [ "$n_a" -lt "$n_bo" ] && [ "$n_bo" -lt "$n_ow" ]
check "debt group is ordered by message count, descending" $?
# The status exclusion is about a repo's own next step, never about its debt.
printf '%s' "$PLAN" | grep -q '^repo=repo-blocked-owes$'
check "a BLOCKED repo that owes mail is still planned (debt is the other axis)" $?
printf '%s\n' "$PLAN" | grep -A4 '^repo=repo-blocked-owes$' | grep -q '^why=inbox:2$'
check "a blocked repo in the plan says debt, not status, is why it is there" $?
# Debt before live work: repo-h is in-progress and owes nothing.
n_h="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-h$' | cut -d: -f1)"
[ -n "$n_h" ] && [ "$n_ow" -lt "$n_h" ]
check "every repo owing mail is planned before any that owes none" $?
# repo-a is BOTH in-progress and owing. It must appear exactly once - a repo
# listed twice is two tabs for one repo, which is the plan failing at its job.
[ "$(printf '%s\n' "$PLAN" | grep -c '^repo=repo-a$')" -eq 1 ]
check "a repo qualifying under two rules is planned exactly once" $?
printf '%s\n' "$PLAN" | grep -A3 '^repo=repo-a$' | grep -q '^why=inbox:3$'
check "why= names the rule that put the repo in the plan" $?
# in-progress outranks planned among the repos owing nothing.
n_g="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-g$' | cut -d: -f1)"
[ -n "$n_g" ] && [ "$n_h" -lt "$n_g" ]
check "live work (in-progress) is planned before merely planned work" $?
# Excluded classes. A tab for a done or deferred repo is noise; a tab for a
# blocked repo that owes nothing cannot be moved at all.
printf '%s' "$PLAN" | grep -q '^repo=repo-wt$'; [ $? -ne 0 ]
check "plan excludes a done repo that owes nothing" $?
# ...but the exclusion is conditional on owing nothing, and the header says so.
# Read the other way, the plan's own header would call these entries a bug.
printf '%s' "$PLAN" | grep -q '^repo=repo-done-owes$'
check "a DONE repo that owes mail is still planned" $?
printf '%s\n' "$PLAN" | grep -A4 '^repo=repo-done-owes$' | grep -q '^status=done$'
check "a done repo in the plan still reports its real status" $?
printf '%s\n' "$PLAN" | grep -m1 '^# Utelatt' | grep -q 'skylder svar'
check "the header states the exclusions are conditional on owing nothing" $?
printf '%s' "$PLAN" | grep -q '^repo=repo-d$'; [ $? -ne 0 ]
check "plan excludes a deferred repo" $?
printf '%s' "$PLAN" | grep -q '^repo=plug-x$'; [ $? -ne 0 ]
check "plan excludes a blocked repo that owes nothing" $?
printf '%s' "$PLAN" | grep -q '^repo=repo-c$'; [ $? -ne 0 ]
check "plan excludes a repo with no STATE.md and no debt" $?
# A repo with no board line has no declared next step, but dropping it SILENTLY
# is what the table's own MERK line exists to prevent. It is planned last and
# labelled, never omitted.
printf '%s' "$PLAN" | grep -q '^repo=repo-b$'
check "a repo lacking a board line is planned, not silently dropped" $?
printf '%s\n' "$PLAN" | grep -A3 '^repo=repo-b$' | grep -q '^why=uavklart$'
check "a repo with unknown status is labelled uavklart, not guessed into a rule" $?
n_b="$(printf '%s\n' "$PLAN" | grep -n '^repo=repo-b$' | cut -d: -f1)"
[ -n "$n_b" ] && [ "$n_g" -lt "$n_b" ]
check "uavklart repos rank below every repo that declared a status" $?
# The command comes from route.sh, same single copy of the rubric the briefing
# uses - advisor flag included, since that is a property of the ROW.
printf '%s' "$PLAN" | grep -q '^command=claude --model sonnet --effort high --advisor opus$'
check "plan derives the exact startup command from the repo's route line" $?
# Both no-command causes must degrade to a marker. A bare `command=` would be
# the shape of a runnable command carrying nothing - worse than none, because a
# driver reading `^command=` would type an empty line into a live pane.
printf '%s' "$PLAN" | grep -q '^command=$'; [ $? -ne 0 ]
check "plan never emits an empty command= line" $?
printf '%s\n' "$PLAN" | grep -A6 '^repo=repo-typo$' | grep -q '^command_missing='
check "unparseable route line degrades to command_missing=, not to a guess" $?
printf '%s\n' "$PLAN" | grep -A6 '^repo=repo-a$' | grep -q '^command_missing='
check "repo owing mail but lacking a route line is marked, never guessed at" $?
# Same argument as the briefing: the 38-char cut is the TABLE column's property.
printf '%s' "$PLAN" | grep -q '^neste=.*check-versions'
check "plan prints the full NESTE line, not the 38-char table excerpt" $?
# The driver cds into this, so a relative or missing path lands a session in the
# wrong repo - constraint 2 of the Ghostty measurement, in field form.
pdir="$(printf '%s\n' "$PLAN" | grep -m1 '^dir=' | cut -d= -f2-)"
[ -n "$pdir" ] && [ -d "$pdir" ] && [ "${pdir#/}" != "$pdir" ]
check "dir= is an absolute path that exists" $?
# Every block must be complete: a driver that reads a block missing dir= or
# command= has no safe default, and guessing one types into a live pane.
n_tab="$(printf '%s\n' "$PLAN" | grep -c '^tab=')"
n_rep="$(printf '%s\n' "$PLAN" | grep -c '^repo=')"
n_dir="$(printf '%s\n' "$PLAN" | grep -c '^dir=')"
n_why="$(printf '%s\n' "$PLAN" | grep -c '^why=')"
n_sta="$(printf '%s\n' "$PLAN" | grep -c '^status=')"
n_nes="$(printf '%s\n' "$PLAN" | grep -c '^neste=')"
n_cmd="$(printf '%s\n' "$PLAN" | grep -c '^command=')"
n_cms="$(printf '%s\n' "$PLAN" | grep -c '^command_missing=')"
[ "$n_tab" -gt 0 ] && [ "$n_rep" -eq "$n_tab" ] && [ "$n_dir" -eq "$n_tab" ] \
&& [ "$n_why" -eq "$n_tab" ] && [ "$n_sta" -eq "$n_tab" ] \
&& [ "$n_nes" -eq "$n_tab" ] && [ $(( n_cmd + n_cms )) -eq "$n_tab" ]
check "every planned block carries every field exactly once" $?
# Numbering must be dense: a driver opening tab 4 of 3 is a real failure mode.
seq_ok=0
printf '%s\n' "$PLAN" | grep '^tab=' | cut -d= -f2 > "$ROOT/tabs.txt"
# An empty tab list satisfies "every number is its index" vacuously, which is
# the check passing by finding nothing rather than by reading the numbering.
[ -s "$ROOT/tabs.txt" ] || seq_ok=1
i=0; while read -r t; do i=$((i+1)); [ "$t" = "$i" ] || seq_ok=1; done < "$ROOT/tabs.txt"
check "tab numbers are dense and consecutive" "$seq_ok"
# Read-only, same as every other rendering. Reading IS delivering in this
# engine, so a plan that consumed a backlog would be the core defect.
p4="$(snap "$CLAUDE_COORD_DIR")"
"$BOARD" --roots "$ROOT" --plan >/dev/null 2>&1
[ "$p4" = "$(snap "$CLAUDE_COORD_DIR")" ]
check "plan writes nothing to the mailbox (reading is delivering)" $?
# The scan and the mailbox are two populations. An orphan mailbox has no repo to
# cd into, so it cannot BE a tab - but dropping it makes the plan claim a
# completeness it knows it lacks. It is reported as commentary.
printf '%s' "$PLAN" | grep -q 'orphan-surface'
check "plan surfaces a pending mailbox with no repo in the scan tree" $?
printf '%s' "$PLAN" | grep -q '^repo=orphan-surface$'; [ $? -ne 0 ]
check "orphan mailbox is commentary, never a tab block (there is no dir to cd to)" $?
# Comment lines are '#'-prefixed so a consumer can drop them with one rule.
printf '%s\n' "$PLAN" | grep -vE '^#|^$|^[a-z_]+=' | grep -q .; [ $? -ne 0 ]
check "every plan line is a comment, a blank, or a key=value pair" $?
printf '%s' "$PLAN" | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1
check "plan output is valid UTF-8" $?
"$BOARD" --help 2>/dev/null | grep -q '\-\-plan'
check "--help documents --plan" $?
echo ""
echo "board-selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1

View file

@ -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)"

View file

@ -8,13 +8,16 @@ description: >-
for a cross-repo status read: "what should I work on", "who is waiting on me",
"what unblocks the most", "show the board", "what is cheapest to move",
"cross-repo status", "where is the leverage", "which repo is blocked". Also
triggers on Norwegian phrasings: "hva skal jeg jobbe med", "hvem venter på meg",
"hva løsner mest", "vis tavlen", "hva er billigst å flytte", "hvor bør jeg
begynne", "status på tvers av repo", "hva er blokkert". Trigger even when the
covers planning a whole day rather than picking one repo: "plan my day", "which
repos should I open tabs for", "day plan". Also triggers on Norwegian phrasings:
"hva skal jeg jobbe med", "hvem venter på meg", "hva løsner mest", "vis tavlen",
"hva er billigst å flytte", "hvor bør jeg begynne", "status på tvers av repo",
"hva er blokkert", "lag en dagsplan", "planlegg dagen", "hvilke repo skal jeg
åpne i dag". Trigger even when the
user names no repo and no tool — choosing *between* repos is this skill. Not for
"where were we" inside the current repo: that is this repo's own STATE.md,
already injected at session start.
version: "0.15.0"
version: "0.16.0"
---
# board — which repo deserves the next session
@ -122,6 +125,36 @@ If the user asked a narrower question ("who is waiting on me", "what is cheapest
answer that question directly from the same run instead of forcing the full
ranking onto them.
## When the ask is a day, not a repo
"Hvilke repo skal jeg åpne tabber for i dag", "lag en dagsplan", "planlegg dagen",
"hva står på programmet" — that is a different question from "which repo wins",
and it has its own rendering:
"$BOARD" --plan
This is the one case where a list *is* the answer and the no-dumping rule does not
apply — the user asked for the day, and a day has more than one repo in it. Pass
the plan through with a short framing line; do not re-rank it, re-order it, or trim
it. The order is the engine's position and it is deterministic: debt first
(most-owed first, whatever the status), then `in-progress`, then `planned`, then
repos with no board line. Substituting your own judgement for that order makes the
plan unreproducible and costs the property that makes it trustworthy.
Two things to say out loud when you hand it over:
- **A `done` or `blocked` repo in the plan is not a bug.** It is there because it
owes mail, and owing an answer is a different axis from whether its own next step
can move. `why=inbox:N` says exactly that.
- **`command_missing=` means that repo has no usable route line**, so there is no
command to paste. Name those repos rather than letting the operator discover it
per tab. Fixing them is the `route` skill's job, in *that* repo — never a side
quest here.
Blocks are `key=value` and comment lines start with `#`, because a separate driver
repo consumes this output. That format is a contract with a consumer outside this
repo: never reformat it, and never hand-edit a block before passing it on.
## Coverage gaps you must name
A ranking is only as honest as its inputs, and two gaps are invisible in the answer

View file

@ -15,7 +15,7 @@ description: >-
covers retiring a broadcast that has become wrong or obsolete: "retract that
broadcast", "that announcement is outdated, pull it", "trekk tilbake kringkastingen",
"den broadcasten er utdatert".
version: "0.15.0"
version: "0.16.0"
---
# coord-send — natural-language front door for inter-repo messages

View file

@ -14,7 +14,7 @@ description: >-
the operator names no model and no tool — choosing the model for the next
session IS this skill. Not for choosing which REPO gets the next session:
that is the `board` skill.
version: "0.15.0"
version: "0.16.0"
---
# route — what the next session should run with