repo-mailbox/skills/dispatch/SKILL.md
Kjell Tore Guttormsen cde185979c feat(board): nested repos enter on a STATE.md, and the scan reports its nevner
Order 20260903T190201Z-238406410-from-.claude (.claude, operator decision
2026-09-03). A git repo nested under a depth-1 REPO was invisible: discovery
adds a depth-1 repo and stops, and the else-branch container scan - the only
place children are ever looked at - is unreachable for an entry that is itself
a repo. Measured before writing anything: 12 nested repos across the real tree,
exactly 1 with a STATE.md (from-ai-to-chitta/content-sadhguru), which had been
running work and reporting to nobody.

Admission is criterion (a) and nothing wider. add_nested_repos() sits beside
add_dot_repos(), never a widening of the `*` loops - the same argument ordre
20260818T124828Z made for dot repos, and the order made it again before this
repo wrote a line: routing a depth-1 repo into the container branch would admit
every vendored clone under claude-code-100x/. One level only; depth 3 is pinned
as NOT admitted. A dot-prefixed depth-1 repo gets the same nested scan, since
nothing in the criterion distinguishes it.

A nested repo now carries TWO names. The board KEY is <parent>/<child> as the
order specifies. The MAILBOX name is not that key: a mailbox is addressed by
basename(git toplevel), so $COORD/<parent>/<child>/inbox finds no directory and
INN/ORDRE/FLY would print 0 for a repo that may have mail - a failed
measurement wearing the reassuring value, in three columns at once. The record
loop carries `mbox` beside `name`; --voyage's order lookup takes
basename($vy_dir) for the same reason. Which dirs are nested is RECORDED by
discovery (NESTED_LIST), not re-derived from "is my parent a repo?", which
would prefix every depth-1 repo if a scan root were ever a checkout.

The denominator line is independent of all of that and went in regardless:
"undersoekt: N katalog(er) depth 1, M polyrepo-container(e), K nestede repo
(J med STATE.md tatt med)". The header count answers how many were found and
nothing about how many were looked at, so a criterion excluding 11 of 12 was
invisible at the surface built to show it. Real tree 2026-09-04: 43 / 5 / 12
(1 tatt med). A non-repo dot-dir counts in N and never in M - it is a
denominator, not a partition.

Its wording broke an existing check: "polyrepo container itself is not listed
as a repo" grepped the whole output for `polyrepo` and matched the footer's own
`polyrepo-container(e)` - the same class as a grep reading a comment that
EXPLAINS a pattern as an instance of it. Now anchored at column 1, which is
what it always meant.

Verified with board.sh, not from memory: from-ai-to-chitta/content-sadhguru
in-progress, Sonnet 5/high, and --plan gives it a real tab with paste=.
Mutation-verified: reverting the eight $COORD/$mbox reads turns exactly the
three mailbox checks red with the rest of the section green.

Bounded gaps, stated rather than closed: the mailbox-keyed JOINS ($OWED,
brief_orphans, --inbox-plan) still key on the board name - neither created nor
worsened here, since the repo was previously absent from RECORDS entirely; a
34-char key overflows the table's %-32s REPO column (same class as Fable
5.1/xhigh in KOST, and widening moves three cut -c89- helpers); a dot-prefixed
NESTED repo is not looked for.

board-selftest 368 -> 393. Suites: coord 257, board 393, route 73, orders 116,
guard 54 = 893. npm test 11/11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:35:55 +02:00

11 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.33.1

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:

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

"$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

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:

"$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:

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).