Two commits landed after v0.29.0 and neither is reachable: every entry point on this machine runs the version-pinned plugin cache, not the tree. Minor rather than patch because the behaviour is EXTENDED, not merely repaired - coord-count.sh and board.sh gained a new exit status (3, "the world you named is not there"), and pre-state-line-guard.mjs gained CLAUDE_STATE_MAX_LINES. Exit codes are a contract, and the installed cache still runs the old one. Version synced across all seven version-bearing files - plugin.json, package.json, the README badge, and the four SKILL.md frontmatters - so release-plugin.mjs's plugin.json == README-badge == target check passes. The three remaining 0.29.0 mentions (CLAUDE.md:915, CLAUDE.md:960, board.sh:1388) are historical prose about when things shipped and must keep saying 0.29.0. CHANGELOG covers BOTH unreleased commits, not just the newest: d8fdeaa's five defects were one class - a failed measurement rendering as a reassuring value - and the entry carries what that session found about itself, that --plan's free-capacity test had to move from "$7 + 0" to a string comparison against "0", because "?" coerces to 0 in arithmetic and would have certified an UNMEASURED tree as free capacity. The defect reappearing one layer down, wearing the fix as a disguise. No tag and no catalog change: that is the catalog session's order, which runs release-plugin.mjs to do the tag and the ref bump atomically. Doing both here would take a decision on the catalog's behalf without its context. Suites verified under system bash 3.2, not Homebrew 5.3: coord 242, board 300, route 69, orders 110, guard 54; npm test 11/11. Order 20260826T115026Z-998059640-from-.claude
223 lines
11 KiB
Markdown
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.30.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).
|