Dispatch Governance
The judgment the cyberlegion CLI deliberately does not carry. The CLI is dumb hands — it never
auto-routes, never picks a backend, never invokes a Task tool itself. This governance is the one
place that judgment lives, loaded in-session by the legate gateway on a dispatch intent, and
realized headless by the headless-legate agent. Both loads run the exact same procedure below.
Input: an intent to fulfill role R with brief B, optionally expecting a result that
satisfies verdict schema V.
1. Resolve the agent definition
npx cyberlegion@0.3.1 agent resolve <R> --format json
Read model, effort, harness, warm, interactive off the resolved def. warm marks a role
that should run as a live peer session (not a fire-and-forget unit); interactive marks a role that
may need to grill the caller or converse over multiple rounds, not just return one result.
2. Probe the environment
npx cyberlegion@0.3.1 mux doctor --format json
Read whether a multiplexer was ancestry-discovered (tmux/herdr) — a channel strategy needs one to open a pane in; its absence rules that strategy out regardless of what the agent def wants.
3. Pick exactly one strategy
warm |
interactive |
mux present | Strategy |
|---|---|---|---|
| true | true | true | channel |
| true | true | false | run-inline |
| false or true | false | — | subagent |
| false | true | — | error — unroutable def |
channel — warm, interactive, and a pane is available: spin up a live peer that can converse and mail back over rounds. Compose the primitives directly — no single CLI command does this:
mint a thread id, weave it (and an instruction to reply on it) into brief
B, then:npx cyberlegion@0.3.1 unit spawn --agent <R> --brief-file <B> --at <placement> npx cyberlegion@0.3.1 mail await --thread <the-minted-thread-id> [--timeout <ms>] [--max-wait <s>]unit spawnlaunches the peer (its brief tells it what thread to reply on);mail awaitthen blocks on that reply thread. Use--atto place the pane,--timeout/--max-waitto bound the wait; awaitingoutcome at--max-waitmeans re-runmail awaiton the same thread to keep polling, not a failure.The wake-matrix decision (which sub-mode of channel to run) belongs here too, since the CLI carries no
selectWakePathhelper of its own:Environment Wake mode Portable default (mux unknown, or nothing verified) Bounded await — poll mail awaitwith--max-wait, re-arming onwaiting("A-loop").Claude Code, with an observable background task Bounded await plus a background-task check-in ("A-prime") — same polling, but the harness's own background-task surface can short-circuit a wait. A live foreign session behind a verified multiplexer ( mux doctorreports tmux/herdr, notnone)Doorbell — unit nudgethe peer's pane, thenmail await("B").Multiplexer is noneNever pick doorbell (B) — there is no pane to ring. Fall back to bounded await (A). run-inline — the role is warm and interactive (needs a live back-and-forth), but there is no multiplexer to host a peer in. Do not delegate. Return a
run-inlineverdict and let the caller (the session that loaded this governance — e.g. the SDD conductor) do the work itself, in-session. A cold subagent is the wrong substitute here: it cold-reloads its whole context every round and holds no user channel, so a role that needs to converse cannot be served that way.subagent — the role is cold and one-shot (no live conversation expected, one result back). Realize it via
subagent-backend-governance: resolve the agent def → the caller's own harness Task/subagent tool → the subagent's Task-result (its own final returned message) is the verdict. The CLI cannot invoke a Task tool itself — that is always the caller's own tool, nevercyberlegion's — and it collects nothing on the caller's behalf; there is no result file.error (unroutable def) —
interactivewithwarmunset (false) has no strategy. A role that needs live back-and-forth (interactive) must be a live peer (warm); a cold one-shot must not beinteractive(subagent-backend-governanceis one-shot by design). So this pair is a malformed def, not a routing choice — fail loud here, naming the def and the contradictory tags, so the author fixes it. It is not aneedsInput(that is for an under-specified task, not a bad def), and it is never silently swept into subagent. This is the Legate's judgment, not the CLI's:agent resolvereports the raw tags; only this governance holds the table that knows the pair maps to nothing.
The CLI never chooses between these on its own — unit spawn + mail await only run in this
sequence when this governance picked channel; the caller's Task tool only runs when it picked
subagent; picking run-inline means neither runs at all.
The subagent | channel seam
This is the seam a dependent (e.g. SDD, ADR-0023) references by intent — "dispatch a role
fulfillment and expect a verdict" — never by pinning subagent or channel as a literal command
name. The dependent states its intent (role, brief, verdict schema) and this governance decides the
mechanism; a dependent that hardcodes "always subagent" or "always channel" has broken the seam and
coupled itself to a mechanism instead of an intent.
Result shape — DispatchResult
Every strategy resolves to one shape the caller can handle uniformly:
{
"strategy": "subagent" | "channel" | "run-inline",
"id": "<unit id, when one was spawned>", // omitted for run-inline
"verdict": "matched" | "waiting" | "timed-out" | "run-inline",
"result": { /* the subagent's Task-result, or the mail-await reply body, as-is */ },
"needsInput": ["<question>", "..."] // present only when the callee could not complete
}
result is carried through unvalidated today — structured verdict-schema checking against V is a
deferred capability (mail --verdict-schema), not present in this CLI.
waiting (channel path, --max-wait cap hit) and timed-out (channel path, --timeout elapsed)
are re-armable and terminal respectively — treat them the same way mail await's own three
outcomes are treated; never silently retry past a timed-out. A needsInput result on the subagent
path means the unit hit its own leash and returned a needs-input result rather than guessing.
How you relay that needsInput (or your own) is relay-governance, not this skill. Report/ask
transport is keyed on the reporting agent's own lifecycle — a framed callee returns needsInput to
its caller; a bare cron session with no frame pushes mail to the standing owner and exits. Load
relay-governance for the fork; this skill owns strategy choice (channel / run-inline /
subagent), relay owns how the result or an unanswerable question gets home.
Non-goals
- No auto-routing in the CLI. The CLI carries no
dispatchcommand group at all —unit spawn,mail await,unit nudge, and the caller's own Task tool are invoked deliberately by this governance (orsubagent-backend-governance), never by a--backend autoflag — no such flag exists. - No mid-flight strategy switch. Once a strategy is picked for one dispatch, it runs to its one of the three outcomes above; a failed subagent is not silently retried as a channel.