repo-mailbox/skills/dispatch/SKILL.md
Kjell Tore Guttormsen 2f8ceb3f97 feat(sweep): schedule the FYI sweep - invocation was the gap, not the mechanism
coord-sweep.sh shipped in 0.10.0 and had never run once against the real
mailbox. Measured 2026-09-03 with a denominator (docs/2026-09-03-coordination-
debt-measurement.md): 55 mailbox directories, 52 with an inbox/, 27 pending
directed messages - 23 of them pure notices, re-injected at every session start
in repos nobody had opened. The script was correct and unreachable.

WP5 (order 20260902T113745Z-1254925290) asked for a mechanism and named two
candidates. The measurement chose neither, and the first session returned the
order saying so: bulk-ack for pure notices was already built - it is this
script - so the second candidate would have been two copies of one policy, and
the broadcast class converges on its own (reading sets seen), with 34 of 263
unread pairs belonging to two mailboxes no session can hold, so a TTL would
have closed those rather than reduced them. The operator then chose the window
and authorized the schedule.

launchd/com.ktg.repo-mailbox-sweep.plist runs --write --days 14 daily at 05:30.
That is the entire behavioural change. The window is written out in the plist
rather than inherited from the script's default: it is a policy constant chosen
on a measured distribution (30d -> 0 messages, 14d -> 7, 7d -> 13), so a later
change to DAYS=14 must not silently change what an unattended job closes across
51 other repos. It runs BEFORE the 06:00 briefing agent, which scans the same
mailbox this mutates, so the morning briefing reports the debt that remains
rather than counting notices being closed underneath it.

coord-selftest.sh section 38 pins the launchd templates (242 -> 257 checks). A
wrong program path is the one defect here that nothing catches at runtime: the
agent loads cleanly and then silently never runs, with no output to be wrong
and no exit status to read. launchctl list proves an agent is LOADED, never
that it is RIGHT. The section covers every plist in launchd/, not only the new
one - the plist grammar gets one reader rather than one per agent - while
board-selftest.sh section 9 keeps owning brief-nightly.sh's behaviour. Each
plist must name a script that exists here, carry a Label matching its filename,
keep its __CHECKOUT__/__HOME__ placeholders (public mirror), and never point
into the version-pinned plugin cache.

The cache assertion runs on the extracted path, never the whole file - caught
by the check itself on its first run: the brief plist's header explains in
prose why it does not point at the cache, and a file-wide grep read that
explanation as the defect it warns about, the same shape as prose saying
status=done triggering the board's done-guard. Four controls present; mutation-
verified against the real file, where a one-letter typo (coord-sweeep.sh) turns
exactly that check red. XML well-formedness is deliberately not checked:
plutil is not coreutils, and malformed XML already fails loudly at launchctl
load - the opposite of the silent failure this section exists for.

Also fixes the README selftest-checks badge, stale at 529 since 0.25.0; the
real total is 868 (257 + 368 + 73 + 116 + 54).

Suites: coord 257, board 368, route 73, orders 116, guard 54. npm test 11/11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 08:20:33 +02:00

223 lines
11 KiB
Markdown

---
name: dispatch
description: >-
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`).
version: "0.33.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
ORDER="${CLAUDE_PLUGIN_ROOT}/scripts/coord-order-send.sh"
BOARD="${CLAUDE_PLUGIN_ROOT}/scripts/board.sh"
"$ORDER" --to <name> --subject "<one line>" --prompt-file <absolute path>
"$BOARD" --dispatch --repo <name> \
--order-id <the id the order engine printed> \
--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 five steps, in order
### 1. Write the prompt file
The order goes in a file, in plain prose:
```bash
mkdir -p "$HOME/.claude/dispatch"
PF="$HOME/.claude/dispatch/<repo>-$(date -u +%Y%m%dT%H%M%SZ).prompt"
```
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: the order body never passes through a shell.
### 2. Deliver it into the recipient's order queue
```bash
"$ORDER" --to <repo> --subject "<one line naming the task>" --prompt-file "$PF"
```
It prints `order-id=<id>`. **That id is the whole handle from here on.**
This step is what makes a dispatch survive the pane it was typed into. The
prompt file is scratch: it carries the order to one session and nothing records
it afterwards. Measured 2026-08-17: an order was dispatched three times over
90 minutes before it was worked, because the first two tabs ran something else
and the order left no trace anyone could find. In the queue it stays pending,
is re-injected at every session start in that repo, shows up in `board`'s ORDRE
column, and is closed only by a session that claims and finishes it.
The queue is `~/.claude/coord/<repo>/orders/` — the same private local
infrastructure as the mailbox, and a **different channel** from it. Mail is
untrusted cross-repo data that can never instruct a session; an order is
operator-authorized work. Never send an order as a `coord-send` message and
never send a message as an order.
`--to` refuses what `coord-send` refuses: `_`-prefixed names, path traversal,
and the retired `ktg-plugin-marketplace` address (send to `catalog`).
### 3. Measure whether the target already has a pane
```bash
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.
### 4. 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.
Pass `--order-id <id>`, not `--prompt-file`. The emitted command is then a thin
**starter**: it carries no order text at all, only the id and the four steps
the receiving session runs (claim, compare against STATE's NESTE, execute,
close). The order text has exactly one home, and a copy in argv would be free
to drift from it and would die with the pane. `--dispatch` refuses an
`--order-id` that is not in the target's pending queue — the same rule as the
empty prompt file, one level up.
`--prompt-file` still works and is the fallback when there is genuinely no
queue to write to. Passing both is refused: the session would be told two
things.
`--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.
### 5. Verify, then hand it over
**`--target-pane no` (plan-file form).** Write the whole output to a file and
dry-run it:
```bash
"$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.
## What happens at the far end
The dispatched session claims the order and owns it until it closes it:
```bash
coord-order-claim <id> # atomic; exactly one session wins
coord-order-done <id> --commit <hash> # executed, with a result pointer
coord-order-done <id> --no-commit --reason "<why>"
coord-order-done <id> --return --reason "<why>" # back to the queue, with the reason
```
The claim prints the order and tells that session to compare it against its own
STATE.md NESTE block and to **state any divergence in its first reply**. That is
the point of the D-check: a dispatch that displaces a live next step is a
decision, and it should be an uttered one rather than a silent one.
If the tab is never run, nothing is lost. The order sits pending, the next
session in that repo sees it at startup, and `board` counts it in ORDRE.
## Where a dispatch is still not enough
An order is a task. When what the other repo needs is a *notice* — something is
fixed, a premise changed, a question needs answering — that is `coord-send`, not
a dispatch. The test is whether you are asking for work to be done (order) or
telling them something they must decide what to do with (mail).