"Claude Code-specific dispatch protocol. Covers the astragentic-dispatch mod (the pane records itself, runs its brief, reports its turn end), the launcher matrix, pre-dispatch verification, and Claude runtime facts. Read dispatch-ticket for the shared protocol."
Install
npx skillscat add thientranhung/astragentic/dispatch-ticket-claude Install via the SkillsCat registry.
Dispatch a ticket — Claude Code runtime
Read dispatch-ticket for the shared protocol (binding identity, inputs/resolution,
worktree law, brief format, simplify, cleanup). This skill adds the Claude Code launcher and
the mod that does three steps of a dispatch for you.
The mod does the steps that got skipped
Writing the dispatch record, arming the watch, and typing the slash command into the pane
were each a rule in prose, and each was skipped in production on the same day. A guessed tab
id closed a working Builder, and a hand-rolled while true; sleep loop stood in for the
watcher. A rule read at hour zero loses to the tool description in front of you at hour five
(AST-041, AST-069). So on a Claude root these steps are no longer yours. The mod.claude/skills/astragentic-dispatch/ ships in the payload, and every Claude session in the
project loads it with no flag and no setup:
| Step | Before | Now |
|---|---|---|
| Record tab and pane ids | copied by hand from the create output | the pane writes them at session start, from herdr's own environment |
| Deliver the slash command | SendMessage for the body, type the command into the pane, confirm the echo | one SendMessage; the pane runs the first line as a real command |
| Confirm delivery | read the pane for a phrase only the brief had | the pane answers RECEIVED |
| Watch the turn | a Monitor wrapping the watcher script, re-armed for every turn | the pane reports TURN-END at every turn end, and the mod wakes you with a prompt |
| Protect a working pane | nothing | herdr tab|pane|workspace close on a mid-turn pane is refused |
| Claim on the tracker before the brief | prose in thomas.md |
a brief for a ticket the tracker reports unclaimed is not sent |
| Tracker write-back after a merge | refused only at a push of the base | the merge result carries the note, the line turns red, and no brief is sent until scripts/ticket-done.sh has stamped it |
| Release a worktree's resources | release-worktree-resources.sh by hand before removal |
runs at git worktree remove, and the record entry goes with the worktree |
| See which step a Builder is at | read its transcript | the Builder pane's band shows flow <ticket>: ✓ implement · ✓ tdd · ○ review:matt · ○ review:built-in · ○ simplify · ○ arm, from the same record the gates read |
| Seed a new worktree | prose in the project's docs | .astraler/project/setup-worktree.sh <path> runs right after a successful git worktree add; a failure is named in the result and the worktree is not to be dispatched into |
| Know which flow steps ran | a Pass: line in a commit message, which anyone can type |
the Builder's pane records every Skill call as the engine expands it; TURN-END carries the list, and a merge names the steps with no record |
Measured 2026-10-05 on Claude Code 2.1.289 and herdr 0.9.1, with a Builder in its own
worktree. The record carried the right tab and pane ids. A phrase that existed only in the
brief body came back in the Builder's answer. Wake-up arrived under one second after the turn
ended, where pane-state polling was 67 s late (AST-097). A herdr tab close on the working
Builder's tab was refused.
Three preconditions, all checkable:
- Claude Code 2.1.294 or later.
check-requirements.shrefuses older versions. Below the
floor the mod does not load, and nothing else watches the pane. - The tab label is set before launch. The mod reads
ticket:/spec:/qa:/rin:
from its tab at session start. A pane labelled after launch never records itself. - The mod is committed on the branch the worktree checks out. The pane loads the copy in
its own worktree, so a payload that is not committed is not there (AST-036).
After launch, read the record back. .astraler/state/dispatch-record.json must carry this
key with pane_id equal to the pane you created. If it is absent, one of the three
preconditions failed. STOP: a pane the mod does not know about is unwatched, exactly like a
missing Monitor used to be.
Submitting: one SendMessage
SendMessage({
to: "<builder-session-name>", // from ListAgents
message: "/mattpocock-skills:implement ABC-123\n\nWorktree: … · Branch: … · Base: …\nAcceptance criteria: …"
})The first line is the slash command, and everything below it arrives as the command's
arguments. A peer message is not a user turn, so it cannot invoke a disable-model-invocation
skill (AST-112). The pane's mod takes the message and runs the first line through$.command.run, which is a real user command. No typing into the pane, no echo check.
Steering is a plain SendMessage too. To an idle pane it becomes a user turn. To a pane
mid-turn it is delivered the normal way, so the running turn can read it.
Do not arm a Monitor, type into the pane, or write tab and pane ids by hand. Each of those
is now a second copy of something the mod does, and a second copy is where the two drift.
What comes back
RECEIVEDis consumed silently. The line above your prompt moves the dispatch toworking.TURN-ENDarrives as a prompt in your session: "builder ABC-123 ended its turn
(answer). Its last words: …". It is a bell, not proof. A turn ending is not the work
finishing (AST-097). A Builder that backgrounds its work sends one TURN-END when it parks and
another when the background work lands, and both were measured. Verify by artifact.reason=command-failedmeans the pane refused the first line, usually an unknown command
name. Fix the brief and send again.- One ticket yields several
TURN-ENDs, and that is correct. A Builder parks and ends its
turn while its review sub-agents and the arm run in the background, then resumes; measured:
eight bells for one ticket, each a true turn end,Skills run:growing across them. Read the
last one; the count is not a defect. - A refused gate leaves a trace. Each refusal (the tdd gate, the arm gate) is appended to the
pane's record entry underrefusalswith its time, written to/tmp/harness-hook-events.log, and shown red on the Builder's band for two minutes. "Did the
gate fire" is answered from the record, not inferred from the order the steps ended up in. - A TURN-END the pane could not send still arrives, from the record, within 30 s. Measured
downstream: a pane toggled into auto mode (shift+tab) had its SendMessage classified with
no verdict, and the dispatcher learned of the finished turn from the owner ten minutes
later. The pane now writes the turn end into its record entry when the send fails, and your
mod submits the same prompt from there, marked read from the record. Do not toggle a
dispatched pane's permission mode: the launcher's--dangerously-skip-permissionsis what
keeps the message path open. - No
RECEIVEDwithin 90 s turns the line red, and your next prompt carries a note naming
the pane. Read that pane: the message may be held or lost. The sender's{"success": true}
is true even for a held message (measured), so it proves nothing. /dispatch-boardopens a pane listing every live dispatch with its state and age.- The band above the prompt is always on in the dispatcher's session:
⎇ <branch>, the
live dispatches (residents included), and one line the project composes in.astraler/project/status-line.sh— tracker counts, who holds the gate token, whatever the
project measured it needs on screen. The plug prints one line and exits 0; a failing plug
showsstatus-line.sh failed; absent, the band shows branch and dispatches only. AskUserQuestionis refused in a dispatched pane. No human is there; the model decides
from its evidence or reports the question in its handback.
The project's rules travel as overlays
.astraler/project/overlays/ is the project's, and no release writes into it. The mod reads it
on every Claude session in the project:
| File | Where it lands |
|---|---|
overlays/<role>.md (builder, shaper, qa, rin) |
appended to that role's system prompt, on every render, so it survives compaction |
overlays/thomas.md |
the same, in the dispatcher's own session (any Claude session in the repo that is not a dispatched pane) |
overlays/dispatch-brief.md |
appended to every brief the mod sends, after the brief's own text |
Measured 2026-10-09 on Claude Code 2.1.294: a section the mod added at prompt.compose was
read by the model on the first turn. An overlay adds to the contract; where it would
contradict one, the contract wins and the conflict goes upstream. A rule that must refuse
belongs in a plug script (ticket-done.sh, the pre-push hook), not in an overlay: prose can
instruct, only a hook can stop.
Two steps are refused, not noted
Measured on the first two real dispatches after the FLOW: line: one Builder skipped tdd,
the other the built-in review, neither named n/a, and the merge note made each visible after
the fact. So the Builder's pane now refuses the first content commit untilmattpocock-skills:tdd is in its record, and the arm (codex-arm) until the built-incode-review is. The exemption is yours and travels in the brief: a line TDD: n/a — <why> (docs-only, no seam) or REVIEW: n/a — <why>; the mod reads it from the brief it
delivers and never from the Builder's handback. A brief delivered outside the mod
(herdr agent prompt) carries no exemption the mod can see, so on a pane you had to prompt by
hand expect the gate to hold until the brief is re-sent through SendMessage.
The flow is read from what ran
The brief's FLOW: line names four Skill calls: mattpocock-skills:tdd,mattpocock-skills:code-review, the built-in code-review, simplify. The Builder's pane
records each one at the moment the engine expands it, under skills_run in its record entry.
Measured 2026-10-09 on Claude Code 2.1.294: a plugin skill is recorded qualified, a built-in
bare, so the two reviews that share a word are told apart.
TURN-ENDcarriesSkills run: …— read it before the handback's claims.- A merge names the gaps in its result: "ABC-123 merged with no record of: tdd, built-in
code-review". It is a note, not a refusal: a step the handback namesn/awith its reason
is legitimate, and the mod cannot read reasons. Any other gap is a step that did not run. - Only a pane the mod registered is judged. A Codex or OpenCode Builder has no
session_idin its entry and is not named; its evidence stays the marker script. - A fork's call is recorded under the Builder, the same attribution git gives its commits.
The tracker: claimed before the brief, closed before the next one
Both ends of a ticket's tracker state were remembered sometimes. A dispatched ticket stayed in
its ready state, and a merged one stayed in progress (AST-074). The brief is the one moment every
dispatch passes through, so the checks sit there, on the answer of the project's own plug.astraler/project/tracker-state.sh <id>: one line, <state> <assignee-or-dash>, the same plugticket-done.sh asks.
- Claimed before the brief. A brief whose first line names a ticket the plug reports with
assignee-, or with stateclosedorunclaimed, is not sent. The refusal says why. A
project whose claim is a status or a label makes its plug printunclaimeduntil that is
set; the vocabulary stays the project's. - Closed before the next brief. A Builder ticket whose branch has commits of its own and
reached the base, or that agh pr mergemerged, with no ticket-done stamp, holds every
brief. The note arrives in the merge command's own result, so it lands in the same turn as the
merge.scripts/ticket-done.sh <id>clears it, and it asks the same plug that the ticket is
closed and released. The base-push guard missedgh pr mergeand merge-and-hold, which is
where the write-back was forgotten (AST-057). - A harness upgrade still half-applied holds every brief.
install.sh --applythat stops on
conflicts leaves.astraler/state/apply-incomplete, and the unreconciled files are the role
contracts the next agent reads. While the marker exists, no brief is sent, and the refusal
lists the paths. Reconcile them, stampapplied-version, then delete the marker. - Payload edited but not committed holds every brief. A worktree checks out HEAD, so a
payload file edited in the main checkout (a role contract,orchestrator.md) reaches the
dispatcher and never the agent (AST-036). While any file of the applied release's payload is
modified or untracked here, no brief is sent, and the refusal lists them with what to do.
Commit or revert; or, when this brief does not depend on them, add a lineUncommitted-payload: <why this brief is unaffected>, which goes through and reaches the
agent with the brief. Measured downstream: about one commit in six passes through this state
in ordinary work, so a gate with no way through except committing half-written rules would
teach committing them.check-requirements.shreports the same state, but only at
adaptation: after the first upgrade it was measured letting the first dispatch through.
This check is a lint, not a boundary. When it cannot read the applied version, the
release tree orgit status, it lets the brief go rather than block every dispatch on a
transient failure. Do not cite it as proof that a dirty payload cannot be dispatched. Scoping it
to what each role reads was rejected: that source exists only as prose in each contract's Load
table, and a gate whose correctness depends on how a markdown table is formatted fails in a
direction nobody can predict. - No answer, no check. An absent plug, one that fails, and one that prints
unreachableare
an empty socket: briefs go, andscripts/ticket-done.shstamps the tracker half as unverified, as before.
A check that cannot answer must not refuse every dispatch.
Measured 2026-10-05: a brief for a ticket reporting open - was refused, and the Builder received
nothing. After the merge, the note arrived with the merge result, the line turned red, and the
next brief was refused until the dispatcher closed the ticket and ran scripts/ticket-done.sh. It did that
at once, in the turn that read the note.
Cleanup: the release runs at removal
Pass the worktree as a literal path. A path with $, a backtick or ~, or one that is not a
worktree git worktree list knows, is refused: this hook runs before the shell expands
variables, and git worktree remove $(pwd)/$W used to match no record and slip past every
protection below, including the mid-turn refusal (measured downstream).
git worktree remove <path> on a recorded worktree runs release-worktree-resources.sh <path>
first: it reaps processes rooted there, runs the project's teardown plug and stamps the path.
It runs only when the worktree's agent is not mid-turn and its tree is clean, or --force was
given. The release tears down live state, so it must never run for a removal the git guard is
about to refuse (AST-115). If the release fails, the removal is refused with its output.
The release reaps the agent in that worktree, and herdr closes a tab whose last pane exited. The
tab is usually gone before you close it, so a tab close answering not-found is expected. The
record entry is deleted once the worktree and the tab are both gone. Measured: release ran,
stamp written, Builder reaped, tab closed, entry gone, with no step besides the removal.
What the mod cannot see
- A pane whose process died. The mod dies with it. The workspace watchdog still covers this
(dispatch-ticket/WATCHING.md), and it stays mandatory. - A pane that never recorded itself. See the read-back above.
- A resident pane without a
resident:label. A long-lived pane the owner keeps (a deploy
worker, a design or harness partner) is recorded, shown on the board and refused a close
only when its tab is labelledresident:<name>before launch. Unlabelled, it is invisible to
the mod and aherdr tab closeon it goes through. - Codex and OpenCode panes. They run no Claude mod, so they keep the watcher script and its
protocol indispatch-ticket.
Trust boundary. A consumed message skips the receiver's crossSessionInbound hold. On the
dispatcher side the mod consumes only its own marked messages whose key and pane match the
record. On a dispatched pane it consumes every peer message, which is the same trust a pane
launched with --dangerously-skip-permissions already extends to its dispatcher.
Launcher matrix — Claude rows
builder → claude --dangerously-skip-permissions --agent builder --model <row: Model> <--effort only when the row sets one> <--advisor only when the row sets one>
shaper → claude --dangerously-skip-permissions --agent shaper --model <row: Model> <--effort only when the row sets one> <--advisor only when the row sets one>
rin → claude --agent rin --model <row: Model> <--effort only when the row sets one> <--advisor only when the row sets one>
qa → claude --agent qa --model <row: Model> <--effort only when the row sets one> <--advisor only when the row sets one>Model and effort come from the role's orchestrator.md row, never from memory.
Add --effort only when the row sets it (low|medium|high|xhigh|max); blank means the
runtime default. Add --advisor <value> only when the row's Advisor cell is set; the pairing
rules are in orchestrator.md. Not yet measured through herdr agent start: the flag is
undocumented in claude --help, so the first launch with it is the measurement — a session
that could not attach the advisor says cannot advise at launch and runs without it. Rin runs without --dangerously-skip-permissions: a gate runs under
permissions, and rin has no fallback row, so no other runtime is legal here.
Pre-dispatch verification
Verify the adapter exists in the worktree before launching:
test -f <worktree-path>/.claude/agents/<role>.md || echo "STOP: adapter missing"A missing adapter means the payload was not committed or was gitignored (AST-036). The
shared protocol's worktree-visibility check catches this too, but this is the exact fileclaude --agent <role> will try to load.
Verify the mod is in the worktree too. Without it the pane runs, but nothing records it,
delivers its command, or reports its turn end:
test -f <worktree-path>/.claude/skills/astragentic-dispatch/hooks/register.tsx || echo "STOP: dispatch mod missing"Launch
For builder and shaper (write roles):
herdr agent start "<role>-<ticket-id>" --kind claude --pane <pane-id> --timeout 60000 \
-- --dangerously-skip-permissions --agent <role> --model <row: Model> <--effort only when the row sets one> <--advisor only when the row sets one>For rin and qa (review roles — no --dangerously-skip-permissions):
herdr agent start "<role>-<artifact-key>" --kind claude --pane <pane-id> --timeout 60000 \
-- --agent <role> --model <row: Model> <--effort only when the row sets one> <--advisor only when the row sets one>Measured runtime facts
These describe how herdr reads a Claude pane. The mod does not depend on them, but the
workspace watchdog still does.
Claude idle rule. prompt_box_body at priority 950 matches "❯\n", so an empty
Claude composer reads as idle. This means:
- A multi-line brief sitting unsent in the composer reports
idle— the dispatcher trusts
it and concludes the Builder finished instantly (AST-032, AST-037). - A pane read as
idlehas not necessarily started. The watchdog'sNEVER_STARTEDexists for this.
Runtime detection quality. Claude tops out at osc_title 1100 then falls to text
regions. working and blocked are rule-backed; idle is rule-backed but its evidence is
weak (the empty-composer match above). Verify by artifact.