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:
parent
21e2873e21
commit
1ee003328c
5 changed files with 580 additions and 10 deletions
174
skills/dispatch/SKILL.md
Normal file
174
skills/dispatch/SKILL.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
---
|
||||
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.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:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```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.
|
||||
|
||||
### 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:
|
||||
|
||||
```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.
|
||||
|
||||
## 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue