repo-mailbox/skills/dispatch/SKILL.md
Kjell Tore Guttormsen 1ee003328c 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
2026-08-16 16:05:53 +02:00

8.4 KiB

name description version
dispatch Turn "start a session in repo X, on order Y, at cost Z" into a complete, verified startup command — the prompt written to a file, the model and effort derived from the rubric, and the output form chosen by whether the target repo already has a terminal pane. Use whenever work is being handed to a session in another repo, or to this repo's own next session: "dispatch a session in repo X", "start a session there with this order", "give me the startup command for repo Y", "hand this work to <repo>", "write the plan file for that session", "open a tab for X with this task", "how do I launch the next session on this". Also triggers on Norwegian phrasings: "dispatch en økt i repo X", "start en økt der med denne ordren", "gi meg oppstartskommandoen for Y", "send arbeidet til <repo>", "skriv planfila for den økten", "åpne en tab for X med denne oppgaven", "hvordan starter jeg neste økt på dette". Trigger even when no tool is named — producing a runnable startup command for another session IS this skill. Not for choosing WHICH repo deserves the next session (that is `board`), not for scoring model and effort alone (that is `route`), and not for sending a message to another repo (that is `coord-send`). 0.24.0

dispatch — hand a session a task it can actually start on

A dispatch is one line the operator pastes and one file that line reads. Both halves are easy to get wrong in ways that look right: a command with no prompt in it, a plan file the driver silently discards, a --no-go that does not stop what everyone assumed it stopped. All three were measured on 2026-08-16 — four separate misfires in one day, by two different repos — and this skill exists so they are not re-derived a fifth time.

You produce the command. You never run it. Starting a session in another repo spends the operator's quota and takes an action inside a repo this session does not own. Hand back the finished command and stop.

The engine

BOARD="${CLAUDE_PLUGIN_ROOT}/scripts/board.sh"

"$BOARD" --dispatch --repo <name> \
         --prompt-file <absolute path> \
         --target-pane <yes|no> \
         --path <known|partial|undetermined> \
         --verification <strong|weak|none> \
         --reversibility <cheap|costly|one-way> \
         --scope <local|multi-file|cross-cutting> \
         --rationale "why these four scores"

It writes nothing and prints one block. Exit 2 means it refused — read stderr and fix the call; every refusal is a case where a command would have been wrong rather than merely imperfect.

It is board.sh and not a script of its own because the block format (tab=/repo=/dir=/command=/paste=) has exactly one generator. Two emitters of one file format is the drift defect this repo's CLAUDE.md warns about.

The four steps, in order

1. Write the prompt file

The order goes in a file, in plain prose, and the file is what the session actually receives:

mkdir -p "$HOME/.claude/dispatch"
PF="$HOME/.claude/dispatch/<repo>-$(date -u +%Y%m%dT%H%M%SZ).prompt"

Runtime scratch, not repo content — but it must survive until the operator pastes the line, so never write it to a directory that gets cleaned between sessions. The path must be absolute and shell-clean (letters, digits, /, ., _, -); --dispatch refuses anything else, because the path is expanded inside the command while the contents are not.

Write the whole order: the concrete task, which discipline applies (Iron Law, TDD, which file the test goes in), and what must not be triggered. A prompt that says only "continue" forces the receiving session to guess its task out of STATE.md, which is the thing handing over a prompt is supposed to prevent.

Norwegian prose, æøå, quotes, $ and backticks are all fine in the file. That is measured, not assumed: "$(cat file)" hands the bytes to the session as one argv element with no re-evaluation. The ASCII rule applies to shell and hook code, not to a prompt travelling through argv.

2. Measure whether the target already has a pane

morning --probe-panes | grep "<absolute dir of the target repo>"

A hit means --target-pane yes. Measure it; never assume it. This is the one input --dispatch refuses to default, for the same reason route.sh refuses to default --last-effort: it is a fact about the world, and guessing it produces a dispatch that verifies green and opens nothing.

Two facts about this measurement, both verified 2026-08-16 against the installed morning:

  • --probe-panes works from a Claude session, without a tty. It cannot identify the anchor pane, but the DIR column — the part you need — is there.
  • A session dispatching its own next session is always --target-pane yes. That is not a special case for one repo; it is what self-dispatch is, and it is where all four of the day's misfires landed.

3. Score the four traits and call --dispatch

Scoring is judgement and it is yours; the model, effort and advisor flag are a lookup and are route.sh's. Score the task the dispatched session will do, using the route skill's trait table.

--dispatch deliberately takes no --model/--effort. --advisor opus is a property of the rubric row — two rows share a model/effort pair while differing on it, and the CLI accepts a wrong advisor silently — so a dispatch that took the model directly would have no honest source for that flag. If the right call is a Fable row, the rubric cannot produce it: write that command by hand, and say in the handover that it is a recorded override, running without an advisor.

4. Verify, then hand it over

--target-pane no (plan-file form). Write the whole output to a file and dry-run it:

"$BOARD" --dispatch ... > "$HOME/.claude/dispatch/<repo>-<ts>.plan"
morning --plan-file "$HOME/.claude/dispatch/<repo>-<ts>.plan" --dry-run

Expect opening: 1 of 1. Then hand back:

morning --plan-file <path to the plan file> --no-go

Know what that dry-run does not prove. It proves the block parses and yields a command. It does not answer the pane question: run from a Claude session there is no tty, so morning reports window: unknown ... assuming an empty window and plan_drop_open never fires. A gate built on the dry-run would pass the self-dispatch case every single time — the one case it would exist to catch. Step 2 is the measurement; this is a parse check.

--target-pane yes (paste-only form). There is no plan file, deliberately: morning's plan_drop_open (morning:1788) drops a block whose repo already has a pane. Hand back the paste= line, and say it goes in the existing tab after /exit. Never prefix it with cd — one repo per terminal tab, and the operator is already standing in that one. morning --relaunch bypasses the filter but opens a second tab beside the existing one, which is rarely wanted.

Three things that must reach the operator

Say these in the handover, not only in the plan file. Each was a real correction, not a hypothetical:

  1. The command carries the prompt in argv. claude --model X --effort Y on its own is not a dispatch; it is a session waiting for someone to tell it what to do. Delivered bare twice on 2026-08-16, corrected by the operator with "gi meg alltid komplette oppstartskommandoer for nye sesjoner".
  2. --no-go does not make the session wait. It suppresses only the follow-up Go message — morning:806 is exact: "--no-go says nothing is typed once the startup command is in". The startup command, prompt and all, is typed regardless, so the dispatched session starts working on its own. An operator decision was once taken on the opposite premise and had to be corrected before the run.
  3. Which form you produced, and why. "Plan file, because <repo> has no pane" or "paste line, because <repo> already has one and a plan block for it would be dropped". The form is a consequence of a measurement, and the operator should be able to see the measurement.

Where a dispatch is not enough

The prompt file is scratch: it carries the order to one session and nothing records it afterwards. When the receiving repo needs a durable record of the order — something its next session will still see after this one is gone — send it a coord-send message as well. The dispatch starts the session; the mailbox is what makes the order survive it.