feat(board): --dispatch, the startup command a dispatched session can act on

"Start a session in repo X, on order Y, at cost Z" was produced by hand, and
it misfired four times on 2026-08-16 across two repos. Three distinct holes,
all measured, all closed here:

1. A bare `claude --model X --effort Y` forces the operator to type Go, and
   the session then guesses its task out of STATE.md. The emitted command
   carries the prompt in argv: `... "$(cat <file>)"`. Verified directly that
   this passes the file's bytes as ONE argv element with no re-evaluation, so
   $(...), backticks, quotes and UTF-8 in the prompt BODY are inert - only the
   PATH is expanded, so it must be absolute and shell-clean.
2. --no-go stops only the follow-up Go message, never the work (morning:806).
   The plan-file form says so in its own output, not just in a comment.
3. A session dispatching its own next session gets an empty plan: morning's
   plan_drop_open (morning:1788) drops a block whose repo already has a pane,
   and --dry-run says "0 of 1", which reads as a broken plan file. --dispatch
   therefore emits two forms, chosen by --target-pane: a plan block, or a
   bare paste line for the tab that already exists (and no tab= key at all,
   so it can never be fed to morning as a plan).

Generator ownership, the question left open for two sessions: it goes in
board.sh, which already owns the block format including paste=. A second
emitter of tab=/repo=/dir=/command=/paste= would be two copies of one file
format. Read-only survives - the prompt file and the plan file are written by
the caller, the brief-nightly.sh split unchanged.

--target-pane yes|no is REQUIRED with no default, the same rule --last-effort
carries: it is a measurement (morning --probe-panes, which works without a
tty), and the dry-run cannot substitute for it - run from a Claude session
morning reports "window: unknown ... assuming an empty window" and
plan_drop_open never fires, so a dry-run gate would pass the self-dispatch
case every time.

Cost comes from route.sh's row table; --dispatch deliberately takes no
--model/--effort, because --advisor opus is a property of the ROW and a
dispatch taking the model directly has no honest source for that flag.

New skills/dispatch/SKILL.md is the front door. board-selftest 183 -> 217.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ett8uHMDLir6trFaMzrYRu
This commit is contained in:
Kjell Tore Guttormsen 2026-08-16 16:05:53 +02:00
commit 1ee003328c
5 changed files with 580 additions and 10 deletions

View file

@ -12,7 +12,7 @@ Session A in repo X leaves a message for repo Y; the next session in repo Y gets
![Version](https://img.shields.io/badge/version-0.24.0-blue)
![Hooks](https://img.shields.io/badge/hooks-1-green)
![Skills](https://img.shields.io/badge/skills-3-orange)
![Skills](https://img.shields.io/badge/skills-4-orange)
![CLI scripts](https://img.shields.io/badge/CLI_scripts-8-blue)
![Selftest checks](https://img.shields.io/badge/selftest_checks-433-blue)
@ -94,6 +94,8 @@ It lives here because it is the **writer** for the cost field the board already
Scoring is judgement and belongs to the skill; turning scores into a row is a lookup and costs no model calls. One deliberate side effect is worth more than the tokens saved: a next step that cannot be scored `known` or `partial`, with no design phase planned, is an **underspecified task description** — the answer is to rewrite the step, not to upgrade the model.
**Handing a task to another session (the `dispatch` skill).** "Start a session in repo X on this order." `board.sh --dispatch` turns that into one line the operator can paste: the order written to a prompt file, the model and effort looked up from the same row table `route.sh` uses, and the prompt passed **in argv**`claude --model … --effort … "$(cat <file>)"` — so the session is handed its task instead of having to guess it out of STATE.md. It emits one of two forms, and which one is a measurement rather than a preference: a repo with no terminal pane gets a plan block (`morning --plan-file <f> --no-go`), while a repo that already has one gets a bare paste line for that tab, because a plan block for an already-open repo is silently dropped by the driver and reads as a broken plan file. `--target-pane yes|no` is therefore required with no default, exactly as `route.sh` refuses to default `--last-effort`: it is a fact about the world, and this plugin never looks for a terminal itself. Read-only holds — the prompt file and the plan file are written by the caller, never by `board.sh`.
**CLI.** The engine is eight user-facing bash scripts in the plugin's `scripts/` directory (plus four selftests); resolve them as `"${CLAUDE_PLUGIN_ROOT:-$HOME/.claude}/scripts/coord-<name>.sh"` (from a terminal, use the plugin's install path):
coord-send.sh --to <repo> --subject "<subject>" [--message "<text>"] # or body on stdin
@ -106,6 +108,8 @@ Scoring is judgement and belongs to the skill; turning scores into a row is a lo
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|--plan] [--focus "<prose>"] # cross-repo attention board (read-only)
board.sh --dispatch --repo <name> --prompt-file <abs path> \
--target-pane <yes|no> --path <v> ... --rationale "<why>" # startup command for a session in <name>
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
@ -163,10 +167,10 @@ Note that raising the inbox's priority (Rule 7) deliberately does **not** widen
## Development
bash scripts/coord-selftest.sh # 197 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 178 checks against a throwaway repo tree
bash scripts/coord-selftest.sh # 220 checks against a throwaway mailbox
bash scripts/board-selftest.sh # 217 checks against a throwaway repo tree
bash scripts/route-selftest.sh # 69 checks, incl. the route->board round trip
bash scripts/state-line-guard-selftest.sh # 21 checks, incl. the Edit replace_all projection and the ratchet
bash scripts/state-line-guard-selftest.sh # 23 checks, incl. the Edit replace_all projection and the ratchet
npm test # all four selftests via node --test
TDD is the house rule: every behavior change lands with a failing selftest check first.