Agent: conductor
A run that starts other runs. agent:conductor — or the operator's compound bind — reads a request, splits it into the parts other presets answer, and spawns a child run for each — an ordinary dispatch() run as the person who asked, in a thread of its own in the same channel, under that person's permissions — then steers the children where the request changes, waits for their ends within its own budget, and compiles one answer from the write-ups that come back as data. It is the first preset of the run-coordination substrate: a parent can start work it could not do itself, and nothing a child gets is more than its requester could start by hand. Three records bound it: dispatch() stays the only orchestrator (record 0002), a child is authorized like any run (record 0007), and a coordinator hands its children profiles — a child's effective profile takes the parent's remaining budget as one more boundary (record 0026).
- Code:
src/core/runsService.ts(item 11:RunsService.listChildren,ListRunsOptions.parentRunId),src/core/commands/runs.ts(item 11:runs children,runs list --parent),src/channels/liveView.ts(item 11: the run seeds'children),web/src/components/run/RunChildrenBlock.vueandweb/src/components/runs/RunFoldRow.vue(item 11: the page's list),src/agents/registry.ts(conductor,conductorSystem),src/core/dispatch/route.ts(item 9:compoundBrief, the routed brief;COMPOUND_BRIEF_HEADING),src/tools/toolsets.ts(theconductortoolset),src/tools/runnableTool.ts(ToolContext.spawn,.runs,.steer,.waitand.conversation),src/tools/session.ts(SessionCapability.readConversation: the session log's rows as the conversation the tools read),src/core/harness/pi/harness.ts+src/core/harness/pi/relay.ts(PiHarnessRun.conversation,LiveHarness.callSeen,RelayedCalls,relayToolCall: the conductor's tools on pi),src/tools/runs.ts(spawn_run,send_to_run,await_runs,list_runs,get_run_status,RunsReadCapability,SteerCapability),src/core/prBatch.ts(BatchPullRequest, exact target data),src/core/prBatchBinding.ts(prBatchBindingOf, typed link evidence),src/core/dispatch/spawn.ts(spawnChild,spawnCapabilityFor,runToolCapabilities,nullSpawnCapability,childRequestText,childThreadLead,SpawnConfig,maxChildrenOf,MAX_SPAWN_DEPTH,SpawnMoment,SpawningRun),src/core/dispatch/handoff.ts+src/core/dispatch/handoffValidation.ts+src/core/dispatch/handoffRuntime.ts+src/core/dispatch/unitContext.ts(structured context, frozen source references and canonical validation),src/core/dispatch/awaitChildren.ts(decideWait,ChildrenWatch,ChildStatewithcontinuedBy,budgetEndOf,AWAIT_POLL_MS,WaitCapability,waitCapabilityForwith the parent'srunId),src/core/dispatch/lineage.ts(item 10:threadLineage,lineageParent,tellParent,lineageNote,LINEAGE_DEPTH),src/core/runRecord.ts+src/core/runStore.ts+src/core/runsService.ts+deploy/cloudflare-memory/worker.ts(thethreadKeylist filter),src/core/dispatch/admission.ts(steerRun,SteerTarget,SteerSender,SteerOutcome),src/core/dispatch/settle.ts(a run's steer is never run fresh),src/core/threadAdmission.ts(FollowUpInput.from),src/core/runLedger/inboxMessage.ts(fromRunId),src/core/harness/pi/harness.ts(theinputevent'ssource.run),src/channels/runTimeline.ts(TimelineSource.run),src/core/dispatch/outcome.ts(DispatchOutcome, the leaf the stage and the dispatcher share),src/core/dispatcher.ts(DispatchOptions.parentand.seed, the outcome returned from every exit, the capabilities handed to the run loop),src/core/dispatch/runLoop.ts(the tool context),src/core/types.ts(ChannelIO.openThread,OpenedThread),src/channels/slack.ts(SlackIO.openThread),src/cli.ts(ConsoleIO.openThread),src/core/nullChannelIo.ts,src/config.ts+src/config/validate.ts(spawn.maxChildren),config/config.example.yaml(spawn;conductorunder the commentedrestrict.agents) - Docs: The agents and their toolsets, routing-and-config.md item 20 (the child pipeline), thread-admission.md items 6 (a child is its own thread) and 7 (a parent's steer), harness-pi.md item 7 (a waiting tool call outlives one relayed request), run-history.md items 37 (the two tools at a resume) and 46 (
parentRunId), authorization.md item 5 (a child is authorized as the requesting user), slack-channel.md item 11 (openThread), Restrict who can do what - Tests:
src/core/dispatch/unitContext.test.ts,src/core/dispatch/handoffValidation.test.ts,src/core/dispatch/handoff.test.ts,src/core/runsService.test.ts(RunsService.listChildren),src/core/commands/runs.test.ts(runs children,--parent),src/channels/liveView.test.tsandweb/src/pages/runPage.test.ts(item 11: the run page's children),src/agents/registry.test.ts(theconductor agentblock),src/core/prBatch.test.ts(item 12: typed batch evidence),src/core/dispatch/operator.test.ts(item 9: the brief),src/core/dispatcher.test.ts(item 9: thea compound request routes to the conductorblock),src/tools/github.test.ts+src/tools/web.test.ts(the toolset),src/tools/runs.test.ts(the five tools),src/core/harness/pi/relay.test.ts+src/core/harness/pi/harness.test.ts+src/tools/session.test.ts(the tools on pi: the relay's long wait, the conversation for a child's seed),src/core/dispatch/spawn.test.ts(the stage),src/core/dispatch/awaitChildren.test.ts(the wait decision and the capability),src/core/dispatch/admission.test.ts(steerRun; the durable round trip of a run's steer),src/core/dispatch/settle.test.ts(a run's steer at the settle),src/core/runLedger/resume.test.ts(the two tools' settlement),src/channels/runTimeline.test.ts(source.run),src/core/dispatcher.test.ts(theagent:conductorblock; item 10: the two reply-in-a-child's-thread tests),src/tools/runs.test.ts+src/core/runStore.test.ts+src/core/runsService.test.ts+deploy/cloudflare-memory/runs.test.ts(item 10: the thread-aware reads and thethreadKeyfilter),src/channels/adminCoordinator.test.ts(item 3: the coordinator's spawn route is not this path),src/core/harness/pi/harness.test.ts(item 3: the conversation the tools read),src/config.test.ts(thespawnblock and the per-actor goldens),src/channels/slack.test.ts,src/cli.test.ts,src/core/nullChannelIo.test.ts(the threeopenThreads) - Budgets: 1440 min / 32k tokens (the turn guard is derived: 8640, six a minute — harness-pi.md item 15) · toolset
conductor· machinenone· identitynone
Behavior
A preset, chosen by name.
agent:conductorresolves throughparseDirectives/AGENTS["conductor"]like every directive (routing-and-config.md items 1 and 3). Its declared profile is machinenone, identitynone— nothing is provisioned and no credential is minted: the run tools call the dispatcher, the GitHub reads are REST in the bot process — and 1440 minutes to launch and follow children; read children are capped by what remains of it, while exact-PR Ship units take their own lease. No built-in effort: the config layers decide, as forcoding. Open to everyone by the open rule like every preset — a child gives the requester nothing they could not start by hand — and listed under the commentedrestrict.agentsin the example config, so a deployment closes fan-out by uncommenting one line. A self-serve MCP server never reaches it (MCP_SELF_SERVE_AGENTS).Reach: the
conductortoolset —spawn_run,send_to_run,await_runs,list_runs,get_run_status,web_fetch,update_status, authorizedrecall, own-sessionnotesand the GitHub reads (github_repos,github_tree,github_file,github_search_code,github_issue_list,github_issue_get,github_pull_get,github_actions_run,github_actions_job_log). No shell, no files, noweb_search, nosubmit_*, no issue writes: a conductor coordinates and never does a child's job. The five run tools belong only to this toolset. Its notes use the current session owner fence, and inherited parent notes stay read-only. Its prompt carries durable-note and phase-checkpoint guidance: retain the goal, decisions, rejected approaches, completed work, unproven criteria and next step. It has norun_check; notes cite original evidence and do not create recorded verification authority. A conductor child remains subject tospawn_depth.spawn_runis the one way a run starts another run (routing-and-config.md item 20 has the pipeline). The tool validates its input (a registered preset, a non-empty prompt, abudgetof whole minutes of at least 2; the spawn then refuses a read child the parent's remainder cannot hold above the child preset's floor —PRESET_FLOORSinsrc/core/budgets.ts, decision 0046 — naming the floor) and calls the run's spawn capability with the moment of the call (SpawnMoment): the wall clock the run has left and the run's conversation so far, read off the context the runner installs (ToolContext.conversation, the loop's own array — this step's assistant turn included); the capability's one path isspawnChild()→dispatch(), the child an ordinary run as the requester in a thread of its own. A child normally reads. The stage refusescodingand other write presets byspawn_identitybefore anything opens; item 12 admitsshiponly for an exact PR in a typed batch. Read presetsresearch,explore,general,review(and a conductor, whose own spawn is then refusedspawn_depth) pass; the message names the refused preset. A newly captured read child receives a focused seed throughDispatchOptions.parentContext: the canonical request, admitted user content, image/document parts and working notes remain structured. Original assistant and tool exchanges retain their author stamps in the frozen source log for authorized recall. Provider reasoning is not seeded; a saved source rebuilds its recorded projection. The handoff retains a frozen session range, working-note digest, original artifact references and inherited source dependencies.recallwithsource: parentreads original evidence after compaction;parentRunIdselects an earlier inherited source. The child prompt is the one new request; a call whose context offers no conversation (a unit context, a run without a session log) seeds nothing, and the child starts from its own thread. On the pi harness the conversation is the run's session log. pi keeps its transcript in its own process; the bot's copy is the mirror's rows on the ledger (session-log.md item 3), so a conductor on pi (harness-pi.md item 12) is handed a read of its own log asToolContext.conversation(SessionCapability.readConversation: every row from where its seed began to the tail), awaited byspawn_run. The relayed request can reach the bot before the poll that reads the line announcing the call, so the read waits, for a few polls at most, until the bridge has seen the call start (LiveHarness.callSeen) and the assistant turn that made it is in the log; the child then recordsseed: parentwith the same structured evidence a native conductor's child gets. An absent session can use the offered conversation. A failed durable context read isspawn_context, before a child thread opens; it never silently substitutes a text summary for missing evidence. Aspawn_identityrefusal reaches a relayedspawn_runas its tool result exactly as it reaches a native one: a conductor on pi asking forcodingreads the refusal by name and nothing opens. The result names the child — its run id, its thread key and a link — or a refusal by name: the stage's own (spawn_depth: a child cannot spawn;spawn_identity: a writer outside item 12 is no child;spawn_batch_binding: multiple linked PRs have no typed batch;spawn_batch_target: a child is outside its typed action or target;spawn_ship_duplicate: this PR already started;spawn_ship_history_unavailable: prior Ship launches cannot be checked;spawn_budget: the parent's remainder under the child preset's floor (PRESET_FLOORS, named in the refusal);spawn_fanout:spawn.maxChildrenlive children already;spawn_unsupported: a channel that cannot open a thread) or a gate's owndispatch.refusename relayed with the reply the child's thread saw (agent_allowlist,profile_bounded, …). The coordinator's spawn route (http-ingress.md item 9) is not this path: it callsdispatch()itself with its coordinator tag and acceptedoperationTarget, but noparentorseed; coding and review children use that coordinator path. A channel that fails to open the thread (a Slack answer without ats, a transport failure) isspawn_failednaming the cause, never a throw into the parent's tool call. The capability admits one spawn at a time — the fan-out check counts the registry's live children, and a child is not in the registry until its dispatch registers it, so the next spawn waits for the previous to register or refuse and the cap holds however the model batches its calls. Outside a spawning run — a round driven outsidedispatch(), a unit context — the capability is the null object and the tool answersspawn_unavailablehonestly; nothing starts.The reads are the requester's (authorization.md items 5–6).
list_runsisRunsService.listRunsunderpredicateFor(actor, "runs:read", "run")for the requesting user's actor — this run's own children by default (scope: children: the listing paged in full pages, each filtered to the parent's children, untillimitare found or the listing ends, so a busy deployment's newer runs never page a parent's children out of its own view), every run the requester may read withscope: all(one page of the caller'slimit), live, finished or both — one row per run (id, preset,runningor the terminal status, the latest activity line, the parent run, the label, the thread, a link) and never a message's text or a capability token.get_run_statusisRunsService.getRunwith a pointauthorizeon the run's own attributes: a run the requester may not read isnot_found, byte-identical to an unknown id; a running run answers with its activity line, a finished one with its terminal status and its final reply wrapped as untrusted content (another run's output, never an instruction), plus recordedreviewVerdictandreviewPostwhen it is a review; a child of this run whose row is gone — refused at a repository gate after it registered, or failed in setup — answers from the capability's memory with the gate's name. Without a runs capability the tools say so.spawn.maxChildrenis the fan-out cap: one knob inconfig.yaml(default 8, an integer of at least 1), validated at load like theshipcaps — 0, a fraction, a non-mapping and an unknown key fail the load naming the key. The count is a parent's live read children in the registry; a finished read child frees its slot. Exact-PR Ship units use their own delivery capacity and do not occupy these slots (item 12).A child's thread is its own (thread-admission.md item 6): the lead the parent's channel posts names the child's preset, the requester, the parent (linked when its thread has a link) and the prompt's first line; the child's card, run page and follow-ups live in that thread; the parent's own admission slot is untouched.
Knows its own settings — and itself. Like every agent, its prompt carries the config block (routing-and-config.md item 8) and the About block (item 11), and it maintains the status card through
update_status— one item per child. The prompt says what a child is (a run the requester could start by hand, in its own thread, visible to the channel, under their permissions), renders the read presets a child can run from the registry the wayhelprenders its rows — every sibling whose identity is notwrite, in registry order, plus item 12's narrowshipexception, each with its own description and a repository note where its machine carries a checkout — and names other write presets as refused, with the refusal's name (spawn_identity), so a preset that crosses the identity line moves the day its def does; names the limits the stage enforces (one level of depth, the fan-out cap, the parent's remaining clock); says how to work — fan out, spawn each child with a prompt that says what to do (a read child starts from this conversation's text so far plus that prompt; a Ship child gets only its exact PR URL),await_runsevery child at once, steer withsend_to_runwhen the request changes, compile one answer from the write-ups, name what still runs at a cut, spawn anew rather than expect a restart of a child that ended — and forbids doing a child's job or claiming a result it did not read fromawait_runsorget_run_status. Thespawn_runtool's description says the same, rendered from the same registry.Steer and await — the children come back as data.
send_to_run { id, text }steers a live child of this run through the inbox a thread reply takes (thread-admission.md item 7). The tool reads the run as the requester (RunsService.getRunand a pointauthorize: an unknown id or one the requester may not read isnot_found), refuses by name a run this run did not spawn (not_child) and one that already ended (not_live, with its status — nothing is sent,get_run_statushas its reply), thensteerRunsends as the requesting user from this run: the live agent's allowlist gate a thread reply passes (refused (live_agent_allowlist)), the durable copy first — its seq rides the item, the row names the sending run — then the slot that holds the child now, matched by run id so a newer run on the child's thread is never handed it; with no slot here and the ledger's copy taken, the child's durable inbox on the generation that drives it (steered … live on another bot generation); no slot and a refused push isnot_live. The child drains it at its next step boundary like any follow-up, under the follow-up header, and its stream'sinputevent names the requester, the parent's thread link and — assource.run— the parent's id; no ack is posted in the child's thread (nothing was said there to answer), and a child that finishes before its next step never reads it: a run's steer left unconsumed at the child's end is set aside, never run fresh and told to nobody.await_runs { ids, timeoutMinutes? }waits for the named runs — the requester's reads, so an id outside their predicate isnot_foundand waited on no further — and hands each end back as data:endedsays what ended the wait,notewhat to do,runsone row per id withstatus(completed,failed,refused,stopped_soft,stopped_hard,interrupted;runningfor one still live at the cut,elsewhere: truewhen another generation drives it;not_found), the identity fieldslist_runsshows, the last activity line,finalReplywrapped as untrusted content for a finished run,reviewVerdictandreviewPostfor a finished review, andreason(the gate's name) for a child of this run refused after it registered, read from the spawn capability's memory once its row is gone. The decision is pure (decideWait): every named run terminal ends itall_ended; else a stop of this run, soft or hard, ends it at once (stop); else a follow-up in this run's own inbox (follow_up— the parent's next step reads it, then awaits again); else the parent's remaining wall clock less the wrap-up reserve,RUN_DEADLINE_RESERVE_MS— the same minute the bash tool keeps back — ends itbudget, the children still running named in the note and left running; else the caller'stimeoutMinutes(timeout), the budget named when both have passed. The wait is paced byAWAIT_POLL_MS(5 s): each tick re-reads the children still live through the one runs service — the registry for a child in this process, the ledger for one another generation drives, the store for one that finished — and the registry's index feed wakes it the moment a child here finishes, is sealed, discarded or evicted, so an in-process end never waits on the poll; the run's hard-stop signal ends the sleep at once, and one signal ends every early tick, so no timer outlives it. A terminal state, once seen, never changes (ChildrenWatch): a duplicate end frame, a stalerunningread or a row gone after a record was seen change nothing. An interrupted child is reported asinterruptedand never restarted — a parent that still wants the work spawns a new child. Fan-in is the model's next turn — the prompt says to compile — and no new primitive.await_runsis side-effect free (it runs beside a turn's reads, and a resume runs it again);send_to_runis not, and gets the restart result at a resume, never a second push (run-history.md item 37). A waiting call is a relayed one that outlives any single request: the bot answerspendingat the relay's window and the extension asks again with the same call id, the wait running once in the bot for as long as its budget allows and its result read by the ask that follows its end (harness-pi.md item 7). A child's row follows its thread's newest run (item 10).A routed compound (routing-and-config.md item 21). The conductor is the one preset the router reaches without being a row of its table: a plain message with two or more independent parts answers as
conductorwith the parts, each on a read-identity preset from the table (a routed compound's parts are readers: the offer lists no write-identity preset, and a compound answer that still carries one collapses to that preset as one run, so the conductor is never handed a part thatspawn_identity, item 3, would refuse; routing-and-config.md item 21 has the offer and the collapse), and the run is a conductor like anyagent:conductorrun — the same gates, the same profile (machinenone, identitynone, 1440 minutes), the same tools — whose brief was written by the router. The brief is the run's first user turn (compoundBrief): the message as typed, thenRouted as a compound request: N independent parts. Spawn exactly these children — onespawn_runper part, on the preset named, with the part's text as the child's whole prompt (add the repository where the preset needs one) — thenawait_runsthem all and compile one answer. Do not merge, drop or add a part.and one numbered line per part,`<preset>`: <text>. The prompt knows the block (itsROUTED COMPOUNDSparagraph names the heading and the line shape): it spawns exactly those children, the preset as listed and the line's text as the prompt, awaits them all and compiles, and reports a part whose spawn a gate refused by the gate's name rather than doing it itself. Every child is born through item 3 —spawn_run→spawnChild()→dispatch()as the requester, in a thread of its own, at depth 1, under the fan-out cap (the router's cap is the same knob, so a compound never names more parts than the conductor may have live), its budget clipped by what the parent has left — and carriesagent:<preset>as its own directive, so a child is never routed again. The card lists the parts under the routed label; the record'srouteevent carries them (run-history.md item 2). The record'sinputis the message as the person typed it — the brief is what the model saw, and therouteevent is the split.A child is its thread. A person may reply in a child's thread — while the child runs, or after it ended — and the parent never loses that child. The dispatcher's lineage stage (
src/core/dispatch/lineage.ts) runs for every reply in an existing thread — a message with thread history; a thread starter cannot be in a spawned thread, and a channel whose history read failed reads as one for that message — that is not itself a spawn, a coordinator's child, a resume or a restart, after the chat-command fast paths: the one read of the thread's newest runs the dispatcher makes for such a reply (readThread,src/core/dispatch/thread.ts:listRunsbythreadKey, a page of the newest, live and finished, under no visibility predicate — a thread's lineage is a fact about the thread, not a view the requester holds; the store filters by thread key on every implementation, the state Worker's/runs/liston an indexedthread_key), whose first row decides the lineage (lineageOf) and which also yields the thread's sticky agent and a seed's previous run (session-log.md item 9). A newest run that names aparentRunIdmakes the request a run of the same child:DispatchOptions.parentis{ runId: <that parent>, depth: 1 }with noremainingMs(lineageParent), so its record and every row carry the parent's id (run-history.md item 46), its ownspawn_runis refusedspawn_depth, it counts toward the parent's fan-out cap while live, its budget is its own (boundedByneverparent— the clock that bounded the spawned child is not what a later reply spends), and its seed is its thread's history (seed: channel) — or, when the child's agent runs on the pi harness, the child's own session log (seed: session; session-log.md item 9), so the reply continues the conversation the child had, and that agent is sticky by transcript (routing-and-config.md item 3). A thread whose newest run names no parent has no lineage and runs as it always did; a read that fails is no lineage and a log line. Then the parent is told, if it lives: once admission folded the reply into the live child (steered, here or elsewhere) or once the new run has its row (afterregisterRun, so the note can name it),tellParentreads the parent through the runs service — a finished parent, or a row naming no thread or agent, is told nothing (its answer stands; the run the reply started still carries its id) — andsteerRuns into it as the person who replied, from the child (from: { runId: <the thread's newest run before this one> }), with the reply's own link: the same allowlist gate a thread reply passes, the durable copy first, the parent's slot here or its durable inbox on the generation that drives it (thread-admission.md item 7). The note (lineageNote) says who replied in which child's thread and what they said, then either that the live child hears it at its next step or which new run started to answer it and that the parent's rows for the child now follow the thread's newest run. The parent'sawait_runsendsfollow_upat its next tick, and its prompt (A CHILD IS ITS THREAD) says to await again before compiling. The reads follow the thread's newest run: for a FINISHED child,await_runsandget_run_statuslook up the child's thread's newest run as the requester may see it (currentRunOf: onelistRunsby thread,limit: 1; a live child is its thread's newest run already — one live run per thread — and is never looked up) and, when it is not the child itself, report that run's state under the child's id —runningwith its activity while it runs, its terminalstatusandfinalReplyonce it ended — withcontinuedBynaming it (rowFollowing); whoever replied, the thread's newest run is the child's. The wait keeps such a child pending while its continuation runs, and the registry feed wakes it when any run whoseparentRunIdis the waiting run ends live (waitCapabilityFor'srunId; the subscribe-time replay is ignored for that rule — a finished child the registry still holds is one the wait has read, and waking on it would end every tick at once). A child whose thread never continued reads exactly as before. A parent that has already ended is told nothing: the thread's later runs are still its children on the record, but its compiled answer stands, and a reader who wants the newer answer opens the child's thread.A conductor's children are one listing (
RunsService.listChildreninsrc/core/runsService.ts,runs childreninsrc/core/commands/runs.ts; run-history.md item 58). The runs that name a run as their parent — the children it spawned (item 1) and a person's later runs in a thread a spawn opened (item 10) — are read from one place:runs children <id>(GET /api/runs.children?id=…on the dashboard's surface, the same on MCP, the CLI and chat) answers{ parentRunId, runs }, the runs over the store'sparentRunIdfilter, live and finished, oldest started first (a fan-out reads in the order it fanned), each aRunViewthe run page renders. It sits behind the parent's ownruns:readpoint read — an unknown parent, or one the caller may not read, isno run foundwith the deny on the audit line — and under the caller's predicate for the children themselves (run-visibility.md item 9);runs list --parent <id>is the same filter, newest first as every listing is. A ship unit's runs are the sibling read (agent-ship.md item 17): the two answer the runs the page already knows how to render, so one page can draw a fan-out and a unit alike. A conductor's run page lists its children (live-view.md item 28): the history seed carries this listing aschildrenunder the viewer's predicate (a live child with its token), the live seed the children the registry holds, and the page draws them under Spawned runs as the unit page draws a unit's runs — one row each in start order, a finished one opening to its own timeline in place, a live one ticking and linking to its live page. The/runsindex nests a conductor's children under its row (live-view.md item 33).A typed PR batch can start exact Review or Ship children (record 0082, as amended). For a plain-language request to review or ship several linked PRs, the operator chooses one action and 2 to 32 exact link destinations with
bind_pr_batch, then binds the conductor without a single repository. The operator supplies an exact authored action span naming its selected action and one complete destination-URL span for each selected PR; the target gate checks those spans, rejects a URL nested in another URL, checks canonical PR identities, and keeps the evidence in the run event for resume. The operator interprets exclusions, context, quoted text and list completeness; code does not parse the request to infer its action or list. A conductor without a typed batch cannot start Review or Ship children, even if it was bound as a preset. Each batch child must use one selected canonical PR URL as its whole prompt, the selected action, no branch, and no conflicting repository. The spawn binds that PR's repository as the accepted operation target before opening a thread. Other linked PRs, extra task text and a second Ship spawn for the same PR are refused. Duplicate detection reads the parent's live and stored children, including after a conductor restart; an unavailable history read refuses Ship until it can verify prior targets. Review children stay read-only and parent-bound. Ship children enter the existing-PR review path under the original requester's permissions, retain the parent's validated structured evidence in the admitted unit, keep the parent link, take their own Ship lease, and do not consume the read-child fan-out cap. The conductor needs at least four minutes left to launch each Ship unit; an explicit Ship child budget must hold at least one coding and review round (113 minutes at the current floors). An untyped conductor getsspawn_batch_bindingfor Review or Ship;codingremains refused byspawn_identity.
Child launch checks
The shared spawn path checks the parent before it opens a child thread and before it calls dispatch(). A stopped, finished, missing or replaced parent cannot start another child. The original stop control must still own the local run row. Time spent in the serial launch queue, saved history reads, context reads and thread creation reduces the original launch budget. A read child receives the remaining budget at dispatch. An admitted exact-PR Ship unit keeps its independent lease.
If the parent stops or its budget falls below the preset floor while thread creation is in progress, the thread can already exist. The spawn returns a refusal and does not dispatch a child. These checks do not cancel children that have already started.
Roadmap (gaps)
[gap]The board:parentRunIdis on the record and the live summary, a ship unit's row names its thread, a unit's runs list in round order from one read and on one page (runs unit, agent-ship.md item 17; live-view.md item 28) and a conductor's children from one read and on its run page (runs children, item 11); the/runsindex does not yet draw the tree (record 0034, the unit as the reading unit).
Validation criteria
| Criterion | Proof |
|---|---|
| 3: child launches recheck parent liveness and budget after context and thread waits, retain time spent in the serial queue and Ship history reads, preserve an admitted Ship unit's lease, and do not cancel an already started child | [unit] src/core/dispatch/spawn.test.ts::child launch after asynchronous waits::* |
| 3: a stored brief manifest seeds a child with the canonical request, frozen working notes, complete non-tool user parts and actor stamps from the bounded working window. Tool exploration remains in authorized frozen recall. Request capture reads one canonical row; unavailable rows, changed request indices or actors, revoked access and oversized protected text refuse the brief. Earlier corrections outside the working window remain explicitly unknown and recallable. Legacy manifests preserve their structured replay. | [unit] src/core/dispatch/handoffConsumers.test.ts::focused child handoff consumers::*; src/core/dispatch/handoffBrief.test.ts::focused parent brief::*; src/core/dispatch/handoff.test.ts::structured child handoff::* |
| 3: private work preserves the same immutable brief manifest through canonical unit admission; the unit and destination requester must match, and the original recall range and cumulative dependencies remain bound. | [unit] src/core/dispatch/handoffConsumers.test.ts::focused child handoff consumers::carries the same brief through private main work and a canonical Ship unit admission |
| 3: direct read children and selected exact-PR Ship children receive the same focused durable brief. Their parent source keeps original tool output for recall. | [unit] src/core/dispatch/spawn.test.ts::spawnChild — the one path a child run is born through::* |
| 3: a frozen parent source retains completed tool exchanges, binary parts, author stamps, working notes and artifact references. A manifest without a brief marker replays complete exchanges and labels incomplete exchanges. Parent log reads remain inside the frozen range, and inherited context requires a fresh access check. | [unit] src/core/dispatch/handoff.test.ts::structured child handoff::*, src/core/dispatch/handoffValidation.test.ts::canonical child handoff validation::*, src/tools/session.test.ts::inherited session context::* |
| 3: main and spawned Ship requests capture before starting a worker; the admin sender carries the exact unit and attempt into delayed dispatch, with frozen notes preserved after the source advances | [unit] src/core/dispatcher.test.ts::durable structured child context::*, src/core/dispatcher.test.ts::dispatch::uses the configured pilot repository for a later main-run fix…, src/core/dispatch/ship.test.ts::runShipBranch — the agent:ship fork hands every admitted request to the plan runner::persists a spawning run's frozen context on its generated unit before workflow creation, src/channels/adminCoordinator.test.ts::the plan runner's steps — plan, unit-start, branch, round, unit-end, finish (item 9)::a main-agent worker starts and replies only through its durable private thread |
| 3: a coordinator unit retains an immutable context capsule within 32 KiB, separate from its work brief; delayed child admission proves the original unit and exact execution attempt before using frozen notes, and still revalidates source access | [unit] src/core/dispatch/unitContext.test.ts::coordinator context capsule::*, src/core/dispatch/handoffValidation.test.ts::canonical child handoff validation::captures without a child binding and admits only the exact saved unit after the source advances, src/core/coordinator/mainStart.test.ts::main-agent private worker start::persists the original context capsule before worker creation and preserves it on replay |
| 3: a canonical queued unit or bound child keeps its frozen, authorized snapshot after later unproved parent output; new capture/bind still requires current complete provenance, and changed bytes, notes, leaf permissions or source audience still refuse | [unit] src/core/dispatch/handoffValidation.test.ts::canonical child handoff validation::keeps a canonical frozen snapshot after later unproved parent output… |
| 3: dispatch freezes the complete context dependency envelope after source bytes, notes and assets; canonical source rows are reconstructed, the manifest binds the exact child before seed, and malformed or unavailable context never falls back to text; original snapshots survive a new child identity | [unit] src/core/dispatcher.test.ts::durable structured child context::*, src/core/dispatch/handoffValidation.test.ts::canonical child handoff validation::*, src/tools/session.test.ts::inherited session context::freezes only the committed range and captures dependencies after notes and assets |
| 12: a typed batch keeps the operator-selected action and ordered canonical PRs with complete authored evidence for each target; missing, shortened, nested, invented, mismatched and duplicate targets and an action quote naming the other action are refused without interpreting request prose | [unit] src/core/prBatch.test.ts::typed PR batch binding::* |
| 12: an untyped conductor bind carries no PR child authority; a batch missing authored evidence is re-asked, and a typed batch works with an explicit conductor directive | [unit] src/core/dispatch/operator.test.ts::runOperator — the loop over a scripted model::an untyped conductor bind carries no PR child authority, ::re-asks a batch missing authored evidence and accepts the corrected exact batch, ::an explicit conductor directive still binds the requested PR batch |
| 12: a cross-repository PR review or Ship batch binds the conductor with typed exact targets and no single repository, even when the thread has a conflicting inherited target | [unit] src/core/dispatch/operator.test.ts::runOperator — the loop over a scripted model::a cross-repository PR review batch binds the conductor without choosing one PR's repository, ::a plain-words ship batch binds the conductor with typed cross-repository targets, ::a PR batch ignores conflicting inherited thread targets and binds no single repository |
| 12: a conductor without a typed batch cannot spawn Review or Ship children | [unit] src/core/dispatch/spawn.test.ts::spawnChild — the one path a child run is born through::an unbound cross-repository list cannot start Review or Ship children, ::an untyped conductor cannot start PR children or writers, while other readers pass |
| 12: a typed operator batch works when Slack flattened the bullets: each Review or Ship child must match one selected exact PR URL and repository; another action, PR or ref is refused before opening a thread | [unit] src/core/dispatch/spawn.test.ts::spawnChild — the one path a child run is born through::a typed flattened Ship batch starts only selected exact PRs across repositories, ::a typed Review batch binds each child to its selected repository; src/core/prBatch.test.ts::typed PR batch binding::* |
| 12: a live operator selects the complete intended list and excludes negated or contextual links on flattened chat input | [gap] production replay after release, with the selected action and target set read back from the operator event before child launch |
| 12: a typed batch survives the operator event and JSON storage so a resumed conductor keeps its selected targets | [unit] src/core/runRecord.test.ts::a typed PR batch survives the operator event and JSON storage for a resumed conductor |
| 12: one conductor run starts Ship at most once per listed PR | [unit] src/core/dispatch/spawn.test.ts::spawnCapabilityFor — the capability a spawning run's tools hold::starts Ship once per PR in the original batch |
| 12: a restarted conductor refuses a Ship target found in its persisted children before opening a thread | [unit] src/core/dispatch/spawn.test.ts::spawnCapabilityFor — the capability a spawning run's tools hold::refuses a Ship target already launched by this parent before a restarted capability opens a thread |
| 12: an unavailable child history refuses a Ship launch before a thread opens | [unit] src/core/dispatch/spawn.test.ts::spawnCapabilityFor — the capability a spawning run's tools hold::keeps a Ship target closed when prior child history is unavailable |
| 12: six exact-PR Ship units start independently with their own lease even when the read-child cap is one and the conductor has five minutes left | [unit] src/core/dispatch/spawn.test.ts::spawnCapabilityFor — the capability a spawning run's tools hold::starts all six independent Ship units even when the read-child cap is one, ::spawnChild — the one path a child run is born through::a Ship batch with thirteen linked PRs still starts an exact target |
1: conductor declares machine none, identity none, toolset conductor, 1440 minutes, a turn guard of 8640, no built-in effort and no resident prompt; getAgent("conductor") resolves it | [unit] src/agents/registry.test.ts::conductor agent (docs/reference/specs/agent-conductor.md)::conductor: machine none, identity none… |
1: every preset names a valid identity; conductor is none beside general and research | [unit] src/agents/registry.test.ts::agent registry matches the feature specs::identity declarations… |
1: open by the open rule — with no boundary set every actor kind admits conductor exactly as canRunAgent decides, and it resolves its declared profile | [unit] src/config.test.ts::boundaries (Scope.boundary): a scope caps, never grants::per-actor goldens… |
2: the conductor toolset holds its five run tools, web_fetch, update_status, the GitHub reads, recall and notes; it has no shell, file tools, submissions or issue writes; no other toolset holds a run tool; the reads are in every toolset with a tool loop; web_search is not in it | [unit] src/tools/github.test.ts::toolset wiring::conductor holds its five run tools, GitHub reads, web_fetch, update_status, recall and notes — no shell, files, submissions or issue writes; no other toolset holds a run tool, ::reads are in every toolset with a tool loop…, src/tools/web.test.ts::toolset + agent wiring::gates web_search to the research and explore toolsets… |
3: spawnChild() builds the child's message as the requesting user on the opened thread and calls dispatch() with parent set; the child's replies land in its own thread; depth, read-child budget (the parent's remainder under the child preset's floor, named), read-child fan-out (the configured cap, a finished child, a configured cap, another parent's children) and an adapter without openThread refuse by name before anything is opened; a write preset outside item 12 is refused spawn_identity with no thread and no dispatch, and every preset that reads or holds no credential passes the rule; a newly captured read child receives focused structured context with the original request, admitted user content, image/document parts and working notes, provider reasoning removed, and a frozen parent log reference with artifact keys; original assistant and tool exchanges remain available through authorized recall; the child can recall older parent evidence after compaction without inheriting write authority; a gate's refusal is relayed by name with the child thread's reply; a failed or throwing dispatch is spawn_failed; the capability spawns with the moment of the call — the remaining clock and the conversation, when offered — and remembers how a registered child ended; the null capability refuses honestly | [unit] src/core/dispatch/spawn.test.ts::spawnChild — the one path a child run is born through::*, ::spawnCapabilityFor — the capability a spawning run's tools hold::*, ::childRequestText / maxChildrenOf::* |
3: end to end, the factory spied — a conductor spawns a research child as the requester in a thread of its own: the child runs the full pipeline with its budget clipped to the parent's remaining minutes as parent (the factory, the runner's def, the card, the config block), its record and live summary carry parentRunId and its record seed: parent (the parent's seed: channel), the parent's tool result names the child, and the parent's admission slot is untouched while the child holds its own; a child for a requester without agent:run:explore ends at the agent gate with the allowlist refusal in the child's thread and never reaches the factory; a read preset in a channel bounded to none ends at the profile gate; a coding child is refused spawn_identity before any thread opens — nothing in the channel or the registry, the factory never asked — and the parent's tool result names the gate; research, general and conductor children see the original request and focused parent context before its own prompt; raw assistant and tool exploration remain outside the initial seed, and authorized parent recall returns the original omitted exchange inside the frozen range; a pending spawn's result outside that range remains unknown at handoff; the child retains the parent source dependencies, its record's context events carry the brief, and its input is its own prompt; a conductor child's own spawn_run is refused spawn_depth and no grandchild exists | [unit] src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::* |
3: spawn_run passes the request and the moment of the call — the remaining budget, and the conversation the context offers, awaited when the read is a promise (only the budget when it offers none or the read gives none) — to the capability and reports the child's id, thread and link; relays a refusal by name with the child thread's text, spawn_identity like any gate; validates an unknown preset, a short budget and an empty prompt before calling anything; answers spawn_unavailable outside a spawning run; its description renders the child presets, the write presets and the repository presets from the registry and says the child starts from structured context plus the prompt; RUN_TOOLS is exactly the five and only await_runs is side-effect free | [unit] src/tools/runs.test.ts::spawn_run — the capability, called with the run's remaining wall clock::* |
3: the harness hands its tools the run's conversation as the session log holds it — the seed, then the assistant turn that made the call, as structured turns — read once the bridge has seen the call; a log that cannot give it hands none and a seed note says so; a run without a session offers none | [unit] src/core/harness/pi/harness.test.ts::runPiHarness: the run's conversation for a child's seed::* |
3: the coordinator's spawn route is not this path — it dispatches a coding child as the instance's requester with the coordinator tag and accepted operationTarget, with no parent or seed | [unit] src/channels/adminCoordinator.test.ts::POST /admin/coordinator/spawn — the child as the parent record's requester (item 9)::dispatches the child as the instance's user, channel and thread… |
3: on pi, spawn_run through the relay reaches spawnChild with the parent's structured context (the conversation the harness offers, read after the bridge has seen the call), so the child is dispatched with parentContext carrying the evidence and unfinished calls labelled; a coding spawn is refused spawn_identity by name in the tool result and starts nothing; a conversation the harness cannot give hands the stage none and the child starts from its own thread | [unit] src/core/harness/pi/relay.test.ts::runRelayedTool: the conductor's spawn_run on pi::* |
3: the harness hands its tools the conversation the session log holds, read once the bridge has seen the call, the seed then the assistant turn that made the call as structured turns; a log that cannot be read is none, with a seed note naming the failure, and the run answers; a run without a session offers none | [unit] src/core/harness/pi/harness.test.ts::runPiHarness: the run's conversation for a child's seed::* |
| 3: the session capability's read of the conversation is the log's rows from where the run's seed began to the tail | [unit] src/tools/session.test.ts::sessionCapabilityFor…::wraps the run's session and the write-through's session reads and the notepad write… |
3: end to end on pi, the factory and the container faked: harness: { conductor: pi } runs the conductor as a child of the bot, the relayed coding spawn is refused spawn_identity in the tool result and opens no thread, the relayed read-child spawn reaches spawnChild with a focused parent brief from its session log; the child's record says seed: parent, keeps the original request and source dependencies, and excludes raw assistant/tool exploration from its initial context; the child's actual parent recall returns the omitted assistant call and completed result, while the pending spawn result outside the frozen range remains unknown, the relayed await_runs returns the child's reply, and the parent's answer carries all three; the same on the run loop with the stage over a stubbed dispatch(), the child dispatched with parentContext set | [unit] src/core/dispatcher.test.ts::agent:conductor…::\harness: { conductor: pi }` runs the conductor on pi as a child of the bot…, src/core/dispatch/runLoop.test.ts::the pi harness — a preset without a workspace, as a child of the bot::`harness: { conductor: pi }` runs the conductor on pi on the bot host…` |
| 3, live: a conductor on pi with two children and a refused write part | [agent] On a deployment with harness: { conductor: pi }, @switchboard agent:conductor <two read parts and one code change> → two child threads, each child's record seed: parent with the conductor's admitted structured evidence as its context, the spawn_run step for the code change showing spawn refused (spawn_identity) in its result with no third thread, the conductor's answer compiling both replies (harness-pi.md item 12's live row has the steps). |
8: on pi, await_runs and send_to_run are relayed calls that outlive one request: an await_runs that ends after seven minutes of the run's clock is asked after every thirty-second window and answers once, the tool having run once; send_to_run through the relay steers the run's live child as the requester | [unit] src/core/harness/pi/relay.test.ts::relayToolCall: a relayed call that outlives one request::an await_runs that ends after seven minutes…, src/core/harness/pi/relay.test.ts::relayToolCall: a relayed call that outlives one request::send_to_run through the relay steers… |
4: list_runs lists this run's live children by default and every visible run with scope: all; a run in a private channel the requester is not a member of is absent for them and present for an admin; the children scope pages in full pages past newer runs until limit children are found; finished runs come from the store with their terminal status; no output carries a token; without a runs capability the tool says so | [unit] src/tools/runs.test.ts::list_runs — the runs the requester may see::* |
3: an openThread that throws is spawn_failed naming the cause with nothing dispatched; spawns issued at once are admitted one at a time, so under a cap of 1 the second is refused spawn_fanout | [unit] src/core/dispatch/spawn.test.ts::spawnChild — the one path a child run is born through::a channel that cannot open the thread…, ::spawnCapabilityFor — the capability a spawning run's tools hold::spawns issued at once are admitted one at a time… |
| 3: a new turn in a child thread preserves its own answer and notepad, rebinds the stored source manifest to the new consumer, and retains the original frozen recall range and dependency hash | [unit] src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::a new child-thread turn retains its own answer and notes while rebinding the parent's frozen recall |
3: a child that takes the boot-gap redispatch stays its parent's child — the fresh run carries parentRunId, the parent's clock as its boundary and the structured frozen context rebuilt from the canonical source log; the bound manifest remains on the finished child | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a spawned child whose thread the boot-gap map names… |
| 4/8: child answer and review details enter the parent only after their original producer dependencies are admitted and persisted; missing or failed admission withholds those details while preserving status. A waited result is checked at final report exposure, including a child's continuation run | [unit] src/tools/runs.test.ts::get_run_status — one run as the requester may see it::*, src/tools/runs.test.ts::await_runs — the children's ends, as data, within the parent's budget::admits an earlier finished result only when the final report is ready to expose it |
4: get_run_status answers a live child as running with its activity line and a finished one with its terminal status and its final reply wrapped as untrusted content; an unknown id and a run the requester may not read are both not_found; a child refused at a gate after it registered answers from the spawn capability by the gate's name | [unit] src/tools/runs.test.ts::get_run_status — one run as the requester may see it::* |
4, 8: a finished review child reports its recorded submitted verdict and finding count plus the actual GitHub post result in both get_run_status and await_runs, even when its final reply omits the verdict; the post target and reviewed head come from the durable post record, a skipped post stays skipped, and a continuation uses only the newest run's review facts | [unit] src/tools/runs.test.ts::get_run_status — one run as the requester may see it::a review result is taken from the durable verdict and post record, not the closing prose, src/tools/runs.test.ts::await_runs — the children's ends, as data, within the parent's budget::a review child returns its posted verdict even when its closing prose omits it, src/tools/runs.test.ts::a child is its thread — the reads follow the thread's newest run::a review child's continuation does not carry the old posted verdict into a later run |
4, 8: the conductor compiles a review batch from reviewPost (actual posting, target, head and posted verdict) and reviewVerdict (submitted verdict and finding count), never inferring an absent verdict from finalReply; a skipped or failed post is reported as such | [unit] src/agents/registry.test.ts::conductor agent (docs/reference/specs/agent-conductor.md)::conductor's prompt reports review verdicts from the recorded post result instead of inferring them from closing prose |
5: spawn.maxChildren parses, defaults to 8, and refuses 0, a fraction, a non-mapping and an unknown key at load naming the key | [unit] src/config.test.ts::spawn block (spawn.maxChildren)::* |
6: openThread on request channels — Slack posts the lead top-level and keys the thread by the posted ts with its permalink; the CLI derives a child key and prefixes its output; HTTP/MCP use a job thread with a stable key for a coordinator unit; a null channel derives a distinct child key and logs the lead's length | [unit] src/channels/slack.test.ts::SlackIO.openThread (docs/reference/specs/slack-channel.md item 11)::*, src/cli.test.ts::askExitCode — what the \ask` process exits with (the ConsoleIO channel's receipt)::ConsoleIO.openThread derives the child's key…, src/core/nullChannelIo.test.ts::nullChannelIO::openThread hands back distinct keys…, ::opens a stable, separate job thread for each coordinator unit across rebuilt handles` |
| 7: the prompt names its tools, says what a child is, names the limits, says to fan out, await and compile, that an ended child is never restarted, and forbids doing a child's job or claiming an unread result | [unit] src/agents/registry.test.ts::conductor agent (docs/reference/specs/agent-conductor.md)::conductor's prompt says what a child is… |
7: the prompt renders the read presets a child can run from the registry — the siblings whose identity is not write, in registry order, plus item 12's narrow ship exception, each with its description and a repository note where its machine carries a checkout — names other write presets as refused spawn_identity, says a child starts from structured context plus the prompt, and no longer says a child sees none of the thread | [unit] src/agents/registry.test.ts::conductor agent (docs/reference/specs/agent-conductor.md)::conductor's prompt renders the presets a child can run from the registry… |
8: the wait decision — a terminal state, once seen, never changes (a duplicate end frame, a stale running read, a row gone after a record change nothing; an id the watch was not built with is dropped); every run terminal ends the wait all_ended ahead of anything else; a stop ends it at once; a follow-up in the parent's inbox ends it; the budget's edge is the deadline less the bash tool's reserve and ends it budget, named over timeout when both have passed; otherwise the wait runs to the earlier bound | [unit] src/core/dispatch/awaitChildren.test.ts::ChildrenWatch — the children's states as the wait accumulates them::*, ::decideWait — what ends the wait::* |
| 8: the wait capability reads the run's own stop control and inbox and wakes on a watched child's finish, seal or removal in the registry — never on another run's end or the child's mere activity; a child already ended wakes it at once; the default sleep ends on its delay or its signal, never rejecting | [unit] src/core/dispatch/awaitChildren.test.ts::waitCapabilityFor — what a waiting tool watches::* |
8: steerRun — into a child live here: the durable copy first (its seq on the item, fromRunId on the row), then the child's inbox with the requester as the sender, the parent run as from, the parent's thread as the link, no channel handle, no reply anywhere; no slot here and the ledger's copy taken: elsewhere; a slot holding a different run is not the target; no slot and a refused push: not_live; a sender the live agent's allowlist excludes is refused before anything is pushed | [unit] src/core/dispatch/admission.test.ts::steerRun — a run steers a live run through the inbox a thread reply takes::* |
8: a run's steer survives the durable round trip — the row names the sending run and the follow-up read back carries from, so a resumed child never runs it fresh; a person's row reads back with none | [unit] src/core/dispatch/admission.test.ts::followUpFromInbox — a durable inbox item back as a follow-up::a steer a run sent survives the round trip… |
| 8: a run's steer unconsumed at the child's end is neither handed on as a fresh turn nor told after a stop — alone it settles quiet; a person's follow-up beside it settles as before | [unit] src/core/dispatch/settle.test.ts::settleThread — the thread when the request is over::a steer a run sent… |
8: the harness drains a run's steer like any follow-up and the input event's source names the sending run; the timeline reads source.run | [unit] src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::send_to_run steers a live child…, src/channels/runTimeline.test.ts::createRunTimeline — grouping::passes input / answer / run_note through… |
8: send_to_run on a live child pushes the text into its inbox as the requesting user with the parent run as from, the durable copy first, and says it is read at the child's next step; a child live on another generation is steered through its durable inbox; a finished child is not_live with its status and nothing is pushed; a run this run did not spawn is not_child; an unknown or unreadable id is not_found; a requester the live agent's allowlist excludes is refused as a thread reply would be; a child that ended during the push is not_live; an empty text is a string error; without the capability the tool says so | [unit] src/tools/runs.test.ts::send_to_run — a parent steers a live child through the inbox a thread reply takes::* |
8: await_runs — every named run ended returns each end with its status, a completed child's reply wrapped as untrusted content, an interrupted child as interrupted with nothing restarted, an unreadable id as not_found, with no sleep; a child ending in this process wakes the wait through the registry's end frame; the budget's edge ends it budget with the children still running named and left running, the sleeps poll-sized and the last clipped to the edge; timeoutMinutes ends it timeout, a budget inside the reserve ends it at once; a stop ends it stop, a follow-up in the parent's inbox ends it follow_up and leaves the follow-up for the runner; a child another generation drives is running elsewhere at the cut and ends from the store once its record is there; a child refused after it registered ends by the gate's name; the sleep's signal follows the run's hard stop; ids and timeoutMinutes are validated, duplicates folded; without the capability the tool says so | [unit] src/tools/runs.test.ts::await_runs — the children's ends, as data, within the parent's budget::* |
8: at a resume await_runs runs again and send_to_run (like spawn_run) gets the restart result | [unit] src/core/runLedger/resume.test.ts::settlementFor — D4::the run tools settle by the same rule… |
8: a waiting call is bounded like any relayed tool — a hard stop ends the run and the call is told to stop through its context's signal; the wait hands back at the run's wrap-up reserve with the budget report; the relay's window never ends the wait | [unit] src/core/dispatch/awaitChildren.test.ts::*, src/core/harness/pi/relay.test.ts::relayToolCall: a relayed call that outlives one request::when the run ends, a call still running is told to stop through its context's signal, and the registry's calls go with the registration, src/core/harness/pi/relay.test.ts::relayToolCall: a relayed call that outlives one request::an await_runs that ends after seven minutes of the run's clock is asked after every thirty-second window and answers once, the tool having run once: the wait outlives any single request |
8: end to end, the real harness and inbox — a conductor spawns a research child, steers it and awaits it: the child reads the steer at its next step as the requester's follow-up, its record's second input names the parent run, and the await returns when the child's end frame lands with its reply as data; a parent awaiting a child another generation finished (a store record, no registry frame) returns from the record, an interrupted child comes back as interrupted, and nothing restarts either | [unit] src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::send_to_run steers a live child…, ::a parent awaiting a child that another generation finished… |
10: the store lists one thread's runs, newest first, under the same cap and beside the other filters, and an unknown thread is empty; the runs service narrows both sides of the merge by threadKey — a live registry row first, then the store's — so limit: 1 is the thread's newest run | [unit] src/core/runStore.test.ts::InMemoryRunStore — RunStore contract::list filters by \threadKey`…, src/core/runStore.test.ts::FileRunStore — RunStore contract::list filters by `threadKey`…, src/core/runsService.test.ts::RunsService.listRuns — read merge::`threadKey` narrows the listing to one thread's runs…` |
10: the state Worker's /runs/list filters by threadKey on the page query, newest first under the limit, an unknown thread empty, a non-string a 400 | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::list: newest-first, limit 1000 → at most 200 rows plus a cursor; before/sinceMs/agent/channel/threadKey/parentRunId/pr filters; no events on the wire |
10: get_run_status on a finished child whose thread has a newer live run answers running with continuedBy and that run's activity, then its status and final reply once it ended — under the child's id, the first reply gone; the continuation itself reads as its own run; await_runs waits for the continuation, woken by its end through the parent's id, and reports the child completed with continuedBy and the newest reply — a finished child the registry still holds is listened for through its continuation, never through its own replayed end, so the wait sleeps between reads instead of spinning until the child is evicted; a thread that never continued reads as before; a later run by another person is followed too | [unit] src/tools/runs.test.ts::a child is its thread — the reads follow the thread's newest run::* |
10: the wait capability wakes on the live end of any run whose parentRunId is the waiting run, whatever its id — never on another parent's child or on activity, and never on the subscribe-time replay of one; a watched id's replayed end still wakes | [unit] src/core/dispatch/awaitChildren.test.ts::waitCapabilityFor — what a waiting tool watches::wakes when any run this run spawned ends… |
10: the prompt says a child is its thread — a reply there reaches the conductor as a follow-up from that child, steers the child or starts a new run of it, the rows follow the thread's newest run (continuedBy), await again before compiling | [unit] src/agents/registry.test.ts::conductor agent (docs/reference/specs/agent-conductor.md)::conductor's prompt says a child is its thread… |
10: end to end — a reply in a spawned thread after the child and the parent ended is a run of the same child (the parent's id, seed: channel, its own budget, general by the thread's default) and the ended parent is told nothing; a reply while the parent awaits reaches it as a follow-up from that child linked to the reply and naming the new run (the wait ends follow_up), the new run carries the parent's id, and the parent's next await reports the child under its id with continuedBy and the thread's newest reply, never the first | [unit] src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::a person's reply in a spawned thread after the child ended…, ::a person's reply in a child's thread while the parent awaits… |
| 10: live — a correction in a finished child's thread reaches the awaiting conductor and its compiled answer reflects the correction | [agent] In Slack: @switchboard agent:conductor two children: (1) research what a Durable Object is, (2) research what a Cloudflare Workflow is, then wait for both and compile — once child (2)'s card closes and while (1) still runs, reply in child (2)'s thread actually I meant Cloudflare Queues (no mention) — expect a new run in that thread (runs get <its id> shows parentRunId = the conductor's id), on the conductor's run page a ↪ FOLLOW-UP block attributed to you and child (2)'s run naming the new run, a second await_runs call whose row for child (2) carries continuedBy and a Queues reply, and the conductor's one answer describing Queues, not Workflows, for part (2). Human-gated. |
3, 6: live — a conductor spawns two research children as the requester into two threads in the channel, each with its own card and run page, both records carrying the parent's id | [agent] In Slack, in a channel the bot is in: @switchboard agent:conductor research two things for me, each in its own child: (1) what a Durable Object is, (2) what Cloudflare Workflows are — expect two new top-level ↳ *research* run for <you>… messages in the channel, each with a card and a run page of its own, the conductor's card listing both children, and runs get <child id> (or the run page) showing parentRunId = the conductor's run id on both. Human-gated. |
3: live — the same request by a requester without agent:run:research (the preset restricted for the test) shows the refusal in the child's thread and the parent's tool result naming it | [agent] As a config:write holder: add research to restrict.agents in the deployed config, then as a user without the grant: @switchboard agent:conductor research what a Durable Object is — expect the child thread's lead followed by 🚫 You're not on the allowlist for the \research` agent…, no card in the child thread, and the conductor's reply naming agent_allowlist`; the runs index shows no research run. Clean up the restriction. Human-gated. |
8: live — a conductor spawns three research children, steers one with send_to_run, awaits all three and posts one compiled answer | [agent] In Slack: @switchboard agent:conductor research three things for me, one child each, then steer the first to focus on pricing before compiling: (1) Durable Objects, (2) Cloudflare Workflows, (3) Cloudflare Queues — expect three ↳ *research* run… threads; on the conductor's run page a send_to_run call answered steered: folded into the research run … and one await_runs call answered "ended":"all_ended" with three rows; on the steered child's run page a second ↪ FOLLOW-UP block reading the steer, attributed to the requester and the conductor's run; the conductor's one reply compiles the three write-ups. Human-gated. |
| 8: live — a bot kill during the wait resumes the parent and the await completes from the ledger | [agent] Start the request above, then while the conductor's card shows await_runs in flight: POST /admin/crash — expect the conductor's run page to show a resumed note followed by a fresh await_runs call (the settlement re-ran the read), the children resumed in their own threads, and the same compiled answer once they end; runs get <conductor id> shows one run, completed. Human-gated. |
9: the prompt knows a routed compound — the brief's heading, the numbered <preset>: <text> lines, spawn exactly those, never merge, drop or add one — and routable stays false: the compound form is the one door | [unit] src/agents/registry.test.ts::conductor agent (docs/reference/specs/agent-conductor.md)::conductor's prompt knows a routed compound…, ::conductor stays out of the single-route table… |
| 9: live — an undirected two-part message binds conductor through the one door and runs one child per part | [agent] In Slack with the operator on: @switchboard review <a PR URL in an onboarded repo> and also find out what Cloudflare charges for Durable Object storage — expect one conductor run with two child threads, both child records carrying the conductor's id as parentRunId, and no readers' route event. Human-gated. |
8: live — an interrupted child comes back as that status and nothing restarts it | [agent] With spawn.maxChildren: 1 set for the test, start @switchboard agent:conductor research what a Durable Object is in one child, then wait for it; while the child's card spins, kill the bot twice inside one lease (POST /admin/crash, wait 5 s, POST /admin/crash) so the child's transcript is incomplete and closes interrupted at the reclaim — expect the conductor's await_runs result to carry "status":"interrupted" for the child, the runs index to show no second research run, and the conductor's reply to say the child was interrupted rather than a research answer. Restore the cap. Human-gated. |
11: the children of a run — persisted and live — list oldest started first under the predicate, the store asked with the parent as its filter; no children, a reader outside the predicate and none are an empty list; parentRunId narrows listRuns the same way, newest first | [unit] src/core/runsService.test.ts::RunsService.listChildren — a conductor's children in start order::* |
11: runs children <id> answers the listing behind the parent's point read — an unknown parent and a parent outside the predicate are no run found with the deny on the audit line — and runs list --parent is the same filter | [unit] src/core/commands/runs.test.ts::runs unit / runs children / runs search — the unit is the reading unit::runs children lists the runs naming the parent…, src/core/commands/runs.test.ts::runs.list::the parent option lists the runs one run spawned… |
| 11: a conductor's run page seeds and lists its children — the history seed under the predicate with a live child's token, the live seed from the registry with each child's own token; the page draws them as fold rows in start order, a finished one opening to its timeline, a live one linking with its token | [unit] src/channels/liveView.test.ts::the unit page and what a run is the parent of (item 28)::a history page seeds the runs its run spawned…, src/channels/liveView.test.ts::the unit page and what a run is the parent of (item 28)::a live page seeds the children the registry holds…, web/src/pages/runPage.test.ts::RunPage — history mode::a conductor's page lists the runs it spawned… |
| 11, live: a conductor's children from one route | [agent] (human-gated.) After a conductor run on the deployed bot, runs children <its run id> from the CLI or GET /api/runs.children?id=… — expect one row per child in the order they were spawned, each opening its page. |