The router offers every command and the ship loop through one front door; coding leaves the table; a repository's auto-merge setting no longer refuses ship
The ask. Decide (the maintainer, before the plan is written): adopt this shape for the request router and the ship preset. Written for an engineer who knows the dispatch pipeline (record 0024), the command registry (record 0008) and the router as record 0026 left it, and has not read the ship spec. The frame is the maintainer's, 2026-09-15: every command reachable through the router, new commands reachable with no new work, and "fix it should be ship; the coding agent becomes a step of ship since ship is what people really want".
Success criteria: (1) a plain message reaches any chat-exposed command with its arguments bound, or the coding, review, fix loop to LGTM, with no per-command routing code; (2) a command added to the registry is reachable by plain words the day it lands, and verify fails when it lacks routing fixtures; (3) "fix it" in a Slack thread bound to a pull request drives that pull request to a posted LGTM with no directive typed, in a repository whose settings allow auto-merge; (4) no new merge path: the runner squashes only what a plan instance's merge: runner names, no routed run is a seeded plan or merge: runner, ship enables auto-merge on nothing, and a pull request merges on ship's approval only where its author had already enabled auto-merge, as it merges today on an agent:review approval; (5) a wrong route writes what a routed coding run writes today, a pull request or commits on the thread's own branch, never a merge, and costs at most a pipeline's compute, three review rounds in 120 minutes against a coding run's 45.
TL;DR
Today a plain message can mean a preset, never one of the 29 chat commands or the review loop, so people type config set channel --models.coding … and agent:ship by hand. The bet: the router's answer is already a forced tool call and the registry already derives a tool schema per command, so the menu becomes the preset tool plus one tool per command, and ship takes coding's seat as the table's write preset. The cost is about 5.5k more cached input tokens per routed message, one hand-back flag on commands, and ship's entry rewritten for a caller who typed no directive. Decided: the menu, ship in and coding out, a generated plan with merge: person for a routed ship (every ship request is a plan instance, seeded or generated: the amendment in the ship section), adopting the thread's pull request whoever authored it, five commands handed back, the auto-merge refusal replaced by the per-pull-request fact. Doing nothing leaves two regexes as the only natural-language path to a command and the loop behind a directive help tells people not to learn.
Today at 8078f3e3
Only what the design depends on, as the delta from what a reader of records 0008 and 0026 expects. The full survey is the appendix.
| You would expect | What is true | Proof |
|---|---|---|
| The router's menu is everything a message can mean | The menu is the preset registry alone; the 29 chat-exposed commands, each with a JSON Schema derived from its own definition that the MCP adapter already serves as a tool, never reach it | src/core/dispatch/route.ts routablePresets, routeTool; src/core/commandSurface.ts jsonSchemaFor |
| Natural language reaches commands through the router | It reaches two, through two hand-written regular expressions: "run the tests on X in Y" and "build X in Y" | src/core/operations.ts:67-68; src/core/dispatch/fastPath.ts answerOperation |
ship is kept out of the table because it merges | On a task string ship never merges: the runner squashes only a plan/<id>/<unit> branch; every other unit ends merge_ready for a person | src/agents/registry.ts:586; src/core/coordinator/driver.ts:429; src/core/ship/coordinator.ts:771 |
| The auto-merge refusal prevents an unattended merge | It reads the repository's allow_auto_merge setting, which only lets people with write permission enable auto-merge on a pull request; whether anyone did is the pull request's own auto_merge field, which ship never reads | src/core/ship/preflight.ts:157; src/execution/githubPulls.ts:302-331; GitHub docs |
| The two write presets agree on what "fix it" means in a pull request's thread | A coding run works on the branch the thread was bound to at its first attach, the pull request's head, and its post-step updates that pull request; ship with the same words forks a fresh ship/ branch off the default branch, because a person's pull request "is not ship's to drive" | src/core/dispatch/provision.ts:670; src/core/codingPrPostStep.ts:401-425; src/core/ship/preflight.ts (fallThrough) |
Numbers, measured at this sha: the router's prompt for "fix it" is 8,656 characters, about 2.5k tokens at 3.5 characters a token; the 29 command tools serialize to about 19.3k characters, about 5.5k tokens. The last replay of the router on production history scored 93.0 percent (172 of 185) against typed presets, under record 0026's 95 percent bar and attributed to label noise, while every rule row met and the look-alike row held at 0 of 10 (#1039 in the tracker); 29 routed runs landed in the twelve hours after the default flipped, their misroutes uncounted. Ship has no live median yet (#388); its ceiling is three review rounds in 120 minutes.
The shape
The front door is the route stage's one forced model call, and its menu is a tool set: the route tool exactly as today, whose preset enum is the table, plus one command tool per command the registry exposes to chat and enables in this process, named mcpToolName(id), described by the command's own describe, with jsonSchemaFor(cmd) as its input schema. The model must call one tool. A route call is a preset, gated and dispatched as today. A command call is a routed command: its input goes through namedToInput and the registry's invoke as the message's user, authorize before parse, the path a typed <group> <verb> takes, recorded as an inline run where the command does work; the reply opens with the receipt line, the bound command in chat form, the analogue of the card's routed: line. Each registry carries one structural switch: routable: false on an AgentDef keeps a preset out of the table, and handBack: true on a CommandDef keeps the command in the menu but the door never runs it, answering a hand-back instead, the bound command line to paste. On the preset side ship loses its opt-out and coding gains one, so the table has one write preset, and the imperative rule, rendered from the table's write presets, names ship without anyone editing it. Ship's entry gains one door: a task in a thread bound to a pull request adopts that pull request, whoever authored it, and runs a work round on its head branch before the review loop; any task, not only a fix, since a thread works on whatever context it carries.
The closest known shape is an MCP client: the router reads tools/list from Switchboard's own registry and calls one tool. The one difference is that the client is a classifier under a strict parse, not an agent: one call per message, it never sees a tool's result, and every call is re-checked against the requester's grants and the registry's schema before anything runs.
One trace: "fix the failing test" in a person's pull request thread
The case most likely to be wrong is where ship's directive-era rules meet the routed world: a plain imperative in a thread a person's pull request already binds.
- The maintainer replies in the Slack thread of PR 1203, a pull request they authored, with "fix the failing test". No directive, no sticky preset, no forced channel agent. The thread holds no live run, so the router is asked.
- The route stage builds the front door: the rules and table, the
routetool withpreset ∈ {general, review, research, explore, ship, conductor}, and up to 29 command tools. The imperative rule, rendered from the table's write presets, reads "for it, answership". - The model answers
route { preset: "ship", reason: "terse order to repair a failure" }in about a second, the time the live row in #1039 took. The parse checksshipagainst the presets the requester may run. - The triple re-resolves with
shipas the request's agent; the agent gate asksagent:run:ship; the profile gate intersects ship's 120 minutes and write identity with the channel's boundary; the thread is claimed. The card paints*ship* on <model> · routed: terse order to repair a failure. dispatch()forks into the ship branch withagentSource: route. The preflight runs: Slack channel, the compound gate (ship,coding,reviewallowed), the repository from the thread, and the pull request's facts: open, same-repo headrefactor/u29-d-types-out-of-providers, basemain,auto_mergenull.- Entry: the thread carries the pull request, this message did not name it, and the message has task text. Today this falls through to round 0 on a fresh
ship/fix-the-failing-test-<hash>offmain. Under this record it adopts: the unit's branch is the pull request's head, its base the pull request's base, round 0 a work round. - The routed ship is held to the task form: the text is not
plan <path>.md …, so the hand-off writes a one-unit task instance withmerge: personon it. A plan reference would have refused with "a plan runs when you nameagent:ship". - The runner's pre-check finds 1203 open; its branch create finds the head branch already there and is a no-op. The coding child runs as the requester on that branch with the task and the unit contract, fixes the test, pushes; the post-step finds the open pull request by head branch and edits its description with the one the child submitted.
- The review child runs at the new head, submits
approve, and the post-step posts a review whose body startsLGTM:. The repository's opt-in workflow turns that into an approval pinned to the reviewed commit. merge: person: the unit endsmerge_ready.auto_mergewas null, so GitHub does not merge; had the author enabled it, the card would have said so at step 5 and the approval would merge the pull request once checks pass, by their own instruction. The card closes with the pull request link, one review round, "a person merges" as the remaining gate, and the routed footer.- The maintainer merges from the merge queue as today.
The property: a routed imperative in a pull request's thread drives that pull request to a posted LGTM through ship, and the only writes are the ones a routed coding run and an agent:review already make there.
Second trace: a command with arguments
- In the prompting channel the maintainer writes "use opus for coding in this channel". Stage A: prose. The thread is idle, so the router is asked.
- The model calls
config_set { scope: "channel", models: { coding: "anthropic/claude-opus-5" } }; the ref's shape came from the option's own description,per-agent model: --models.<agent> provider/model. namedToInputyields{ args: ["channel"], options: { models: { coding: "anthropic/claude-opus-5" } } }: a nested object is the option the dotted flag--models.codingbuilds.config.setcarries no hand-back flag.invokeruns as the requester:config:writeonconfig-scope { channel }is theirs, the options parse, the handler writes the override.- The reply:
routed: config set channel --models.coding anthropic/claude-opus-5, then the command's ownUpdated channel scope. Now: …. No run record and no claimed thread:config.setis not an inline-run command, and a typed one claims nothing either. - Had the model bound
"opus", the handler's own check would answer the existinginvalid_inputline naming the option and the expected shape, and the person would correct it in one reply. A bad bind is corrected, never guessed at, the rule the chat grammar already holds.
The difficulty map
- Ship as the routed write preset: entry checks written for a typed directive, the plan form behind the same text, a refusal on the wrong GitHub field. Ship at the front door.
- Binding command arguments from prose to the bar, and holding it as commands are added. Commands at the front door (most work).
- Which routed writes run at once and which are handed back. Inside the same section.
- The provider seam: "any tool" without a second call, and the wider cacheable half. One paragraph there.
Ship at the front door
The constraint: ship's entry was designed for a person who typed agent:ship and knew what it does, so every ambiguity resolved toward refusal or a fresh branch, and the router now hands it people who typed "fix it". Three rules break under that.
Ship is routable and coding is not, because the message never says whether the person wants a review loop after the fix. "Fix it" carries no signal for that, so a table with both write presets asks the model to read intent the text does not hold, on every imperative; one write preset removes the question, and the maintainer's call is that the loop is what people want. The imperative rule and the compound form's write-ask clause are rendered from the table's identity: write rows, so they name ship the moment routable: false moves from ship's def to coding's; presetDoor, the registry's word for how a plain message reaches a preset, flips help the same way, so coding reads "never picked for you, name it", and the About block and the docs generator follow the registry. Coding stays a preset: the pipeline spawns it as its child and agent:coding still runs a one-pass change; nothing in its def changes but the flag. A wrong route now costs a pipeline of at most three review rounds inside ship's 120-minute budget instead of one 45-minute coding run, and it still ends in a pull request a person merges; the card's routed: line and footer remain the affordance for catching it, the flag swap is the one-commit reversal, and the first twenty routed runs are the check. A routed ship is never sticky, since a ship run has no session for the thread to continue, so the next plain message in the thread routes again on its own words.
A routed ship runs the task form only, and a task instance says who merges. The plan form, plan <path>.md [units …], is where the runner squashes under plan:merge, the runner's merge grant, and it is told from a task by the text alone (parseShipPlanRequest). The ship branch refuses the plan form when agentSource is route, naming the directive that runs it, and accepts it from a directive as today. The hand-off writes merge: person on every task instance and merge: runner on a plan's, and the driver reads that field instead of deriving it from the branch name; today the derivation would give a task whose adopted head is a plan/ branch the runner's mode, and the runner would ask a merge the instance guard then refuses. The merge grant's line is unchanged: the runner squashes a plan's unit and nothing else, and no routed run reaches one.
Amendment, 2026-09-15, before acceptance. The task form and the plan form collapse into one. Every ship request is a plan instance: seeded from a plan file named as today (
plan <path>.md [units …], read at the base ref), or generated by the hand-off from the request text as a plan of one unit whose section is the text, its id derived from the thread as the ship branch name is today, so a re-issue in the thread is the next attempt of the same plan. One id scheme (plan-<id>[-n]), one branch scheme (plan/<id>/<unit>, or the adopted pull request's own head), one contract builder (contractFromPlanover the generated graph, so a task gets the guards a unit gets, andcontractFromTaskgoes), one re-issue rule (the next attempt reruns the unmerged units; a bare pull request reference is the unit row withresume).mergeis an instance field the hand-off writes from the request and the plan, never derived from a branch name:personunless a seeded plan or the request saysrunner; the driver reads it, and the runner's door checks theplan:mergegrant, the field, and that the pull request heads the unit's row. The guard in this paragraph becomes: a routed ship runs a generated plan withmerge: person; the seeded form needs the directive. The maintainer asked for this on 2026-09-15 ("you either seed it a plan or it creates one for you at the start of the process so that the steps are the same"); record 0031 carries the matching note under its merge grant.
A task in a pull request's thread adopts the pull request; a bare reference resumes at review; a pull request quoted beside a new task stays context. The repository resolver already tells the cases apart with two flags: prFromMessage, set when this message itself named the pull request rather than the thread carrying it, and refFromPr, set when the target ref came from a pull request's head. Adopt: the thread carries the pull request and the message has task text. The unit's branch is the pull request's head and its base the pull request's base; the runner's branch create finds that branch and is a no-op, as it is today when the ref exists; round 0 is a work round, a coding child on that branch with the task and the unit contract, then the review loop as today, and the child's post-step updates the pull request's description when the child submits one, the ready-state contract the loop exists to hold. Resume: no task text, so a review round first, as ship spec item 10 has it today, now for a pull request of any author. Context: this message named the pull request and carries task text, so round 0 starts on a fresh ship/ branch off the default branch, today's rule. Who can trigger an adoption is who can run ship, coding and review in that channel and use that repository, the compound gate and the per-repository gate the preflight already asks: the same people who can type agent:coding in the thread today and push to the branch, or agent:review and post the LGTM the workflow approves. The bot-authorship check is therefore removed from every path: it protected a stranger's branch from a directive-holder's thread, and the same words push to that branch as a coding run anyway. The fork-head check moves ahead of the three cases so each meets it; a closed pull request as a resume target, and a target whose facts cannot be fetched, still refuse.
The auto-merge refusal is replaced by the fact it should have read. GitHub's repository setting allow_auto_merge lets people with write permission enable auto-merge on a pull request; a pull request merges automatically only after someone did that to it, and the pull request's own auto_merge field says whether anyone has. Ship enables auto-merge on nothing, so in an allowing repository a posted LGTM: yields an approval and a pull request that waits for a person, the state the refusal's own message wanted. The one case the old check named and never tested is a pull request whose author already enabled auto-merge: ship's approval then merges it. That path exists today without ship: a repository that set the auto-approve variables already turns the bot's LGTM: into an approval on any pull request anyone asks agent:review to look at, and an author who enabled auto-merge in that repository armed it against exactly that approval. PullRequestFacts gains autoMergeEnabled from the GET the entry checks already make, the card and the merge-ready report name it ("auto-merge is on for this pull request: the approval merges it once checks pass"), and ship refuses nothing. The repository-level field and its extraction go; the repository lookup stays for the default branch. The option the maintainer named, ship enabling auto-merge on its own pull request at the approving verdict, is a later record: a new GitHub write under a grant of its own.
Invariants: (a) every message the router routes to ship produces a generated plan instance whose merge is person, written by the hand-off and read by the driver, whatever its head branch is named; (b) an adopted unit runs on the pull request's head branch and creates no new ref; (c) with no agent:ship typed in the thread, no seeded plan runs and plan:merge is never asked; (d) ship enables auto-merge on no pull request, and its bot-process GitHub writes stay the branch create, the pull request open or edit, the pinned review post, the board comments, and on a plan branch the one guarded squash.
Failure modes: a question routed to ship. The coding child asks its clarifying question and stops, the review child reviews the unchanged head, and the unit ends with the card naming both; in a thread whose pull request has auto-merge on, an approve then merges it by the author's instruction, the card having named that state before the review ran, and the look-alike fixtures hold that route at 0 of 10 on the last replay. A person's pull request gets a work round they did not want: the push is to their branch from a request in that branch's own thread, and the commits are theirs to revert; the description edit is the more intrusive write, and the open question below owns it.
The alternative it beat: a second write preset, build, routable and never merging, beside a directive-only ship. On a task string ship already never merges, so build would be ship under another name, and the table would again hold two write presets for one intent.
Commands at the front door
The constraint is accuracy with zero per-command work. The router scores 93.0 percent against typed presets today; commands add arguments, and an argument bound wrong is a write to the wrong scope or a refusal that reads as a bug.
The menu is derived, never listed. routableCommands(commands) is every command the bound registry lists with surfaces.chat !== false and enabledWhen(caps) true, in registry order, rendered by mcpToolName, describe and jsonSchemaFor, three derivations of the command definition itself (record 0008) that the MCP adapter, one thin consumer among the surfaces, already uses; 29 in a process with every capability on, 12 in one with none. A command's describe and option descriptions are therefore its routing prompt, the lever record 0026 found for presets, and they already do that work for help and for MCP clients. The route tool keeps its schema and rules; the compound form keeps its readers-only parts and never carries a command part in this record.
The call and the answer. CompletionRequest.toolChoice (src/core/provider.ts since #1203) gains { type: "any" } beside the forced single tool, and piStreamOptions (src/core/harness/piAi.ts), the one place the choice is spelled per API through pi's model library, maps it to Anthropic's { type: "any" } and Chat Completions' "required", with parallel calls switched off where the library exposes the switch; routeMaxOutputTokens grows by the largest command input the schemas admit. The seam hands back the one call, and an answer that carries two is refused by name as no route, whatever the switch did. A route call takes today's strict parse. A command call is checked against the offered set, so an invented name or a hidden command is no route and the request runs on defaults.agent with the reason recorded; an offered command's input goes through namedToInput(cmd, input, "camel") and then invokeChatCommand, the fast path's own function moved to a shared module, with the message's user as caller and the same inline-run rule. routing: { answer: text } offers presets only; that mode exists for a provider that cannot take a forced tool call at all. A routed command runs at the route stage, before the agent gate and before the thread is claimed, exactly where a typed command answers: it claims no thread and starts no agent run, so two plain messages in one idle thread run their commands unserialized as two typed ones would, and a thread with a live run never routes, so a command ask there is a follow-up steered into that run while the typed grammar still answers. A command that answers not_found or unavailable, a repository with no resident for repo_test, is re-routed once over the presets alone, so "run the tests on main" still reaches the sandbox preset as the regex path's fall-through does today, and the record names both decisions.
The receipt line is the bound command. Every routed command reply opens with routed: <chat form of the bound input>, printed by the grammar from the parsed { args, options }, so a person sees exactly what ran and can paste it back corrected; an inline command run's record carries the same line as its route event. A bound input the registry refuses, invalid_input from the parse or the handler, answers the existing usage line, which names the argument or option and the expected shape and never echoes the value.
Which writes run at once. A routed command runs at once, read or write, the precedent set for routed coding: the exact grammar runs repo offboard acme/api with no confirmation today, so a confirmation would be a state machine built for prose alone. Five of the 16 write commands carry handBack: true, because a misfire costs more than a reply can undo: repo.offboard and repo.rebuild tear down or rebuild a resident, mcp.remove deletes a stored credential, runs.stop ends someone's run, memory.forget deletes a durable memory. They stay in the menu, so the door still binds the ask, and the route stage answers the hand-back for them, To run this: repo offboard acme/api, invoking nothing; the person pastes it and the exact grammar takes it as today. The set is decided and revisable by editing five flags.
Fixtures are the contract, and the conformance suite requires them. src/load/routeCommandFixtures.ts holds { id, text, command, input } rows and { id, text, notCommand } decoys beside the imperative and compound sets, and load -- route scores two new rows: the command named (bar 100 percent on the checked-in set, since a wrong command is the failure that costs) and the bound input equal to the fixture's after the registry's parse, that is, what the handler would receive (bar 90 percent to start, every miss printed with both inputs). The conformance suite gains one fence: every command routableCommands would offer has at least three fixtures, a happy path with every required argument, a paraphrase, and a decoy that must not bind to it; a command registered without them fails verify by name. That fence is what makes "new commands are handled automatically" a checked property. The bars are guesses until the first replay on the fast model; the lever if they do not hold is routing.model, as for presets.
Cost. The static half of the prompt grows from about 8.6k characters to about 28k, roughly 8k tokens; the per-message half stays the message. The static half is the cacheable prefix; the plan verifies that the route call sets the provider's cache breakpoint after the tools and system, and the replay reports input tokens per call from the provider's usage so the number is measured against the routed volume the run history shows.
Invariants: (a) the tool set shown equals routableCommands(commands) for that process, so a hidden command can never be called through the door, and a handBack command's call is never invoked; (b) a routed command invokes through commands.invoke and nothing else, so authorization precedes parsing on this surface as on every other; (c) no routed command claims a thread or starts an agent run, except the one re-route over presets on not_found or unavailable; (d) every routed command reply's first line is the chat form of exactly the bound input.
Failure modes: the wrong scope bound, me for channel. The write lands on the requester's own scope, the receipt line says so, config clear me undoes it. A command called for a question about it, "what does config set do?". The handler answers or refuses; the decoys exist to push that rate toward zero. The answer cut at the output cap: refused by name as today, the request runs on the default agent. A bot death mid-command: a routed command reserves no ledger row, as a typed one does not, so the reply is lost and nothing replays; the person asks again.
The alternative it beat: two calls, group first, arguments second over that group's tools. It halves the static prompt and doubles the latency and the failure surface, and it dies on the measurement: 29 tools are 5.5k tokens, and one call carries them.
Why not X
Why not give the general agent the commands as tools and let it call them? An agent loop runs over untrusted content for many turns and can call a write command from any of them; the front door makes one call per message under a strict parse and never sees a result. The exact grammar exists so a command is never guessed at.
Why not have the model write the chat-grammar line and hand it to the exact parser? The tool schema carries each field's description at the point of binding and the provider constrains the shape, while a grammar line asks the model to spell kebab flags, dotted keys and quoting from prose with no field descriptions beside them. The receipt line renders that same grammar line from the bound input, so the person gets the artifact without the model having to write it.
Why not keep coding routable and send only pull-request-thread imperatives to ship? The thread's binding says where the change lands, not whether the person wants a review loop after it, and a fresh-thread "add X to acme/api" wants the loop as much as a thread-bound "fix it" does. Two write presets would put that unreadable question back on every imperative.
Why not confirm every routed write? The typed grammar runs writes without confirmation today; a confirmation is new state for prose only, lost on a restart. Five commands are handed back instead, and the rest run with the receipt line as the undo handle.
Boundaries
Not in this record: a message that mixes a command with an ask, since compound parts are read presets only; pronoun asks such as "connect it" that need earlier thread turns to bind an argument, since the router sees the message and the thread's last directives alone; ship enabling auto-merge on its own pull request; the runner squashing anything off a plan branch; a scope's agent splitting into a force and a default (record 0026's follow-up), so a channel that pins agent: coding still never routes. Compatibility: agent:coding, agent:ship and every typed command behave as today; routing: { auto: false } still turns the whole door off; the imperative fixtures and the replay's imperative bar relabel from coding to ship. The living specs change in the same pull requests as the code: the ship spec's merge-ready and entry-check items, routing-and-config's router item, command-registry's chat-adapter and every-command items, and load-harness's replay item, with the tutorial and get-started lines that say ship "only ever runs when named" and that a plain change routes to coding.
What would change our mind
| Assumption | Cheapest evidence | When |
|---|---|---|
| The fast model binds command arguments at the bars | the command fixtures replayed twice, printed rows read | before unit 2 ships |
| People want every imperative on ship at its cost, three rounds and 120 minutes against coding's one pass and 45 | the first twenty routed ship runs: rounds used, minutes, how many were misroutes | after unit 1 is live |
| Adopting a person's pull request is welcome | the first ten adopted units, each in its author's own thread | after unit 1 is live |
| The static half caches | input tokens per route call from the provider's usage, before and after | the first replay on unit 2 |
Reversibility: the flags on ship and coding swap back in one commit; the command tools are one function's output and one branch of the route stage; the auto-merge field swap is one block; the adoption rule is one branch of the preflight beside today's fall-through.
Rollout
Unit 1, ship: the flag swap, one plan-instance entry (seeded or generated) with the routed guard on it, one id and branch scheme, contractFromTask deleted, merge on the instance and read by the driver and the door, adoption with the work round and the fork check ahead of it, the per-pull-request auto-merge fact, the fixtures and bar relabeled, the spec rows and docs lines. Unit 2, commands: the any tool choice, routableCommands and the tool set, the command branch of the route stage with the receipt line, the hand-back and the one re-route, handBack on the five defs, the regex path deleted, the command fixtures, the conformance fence, the replay rows. Each unit is its own plan through the review loop and ends on its live receipt below.
Open questions
| Question | Owner | Resolves it | Before |
|---|---|---|---|
| Does the work round edit an adopted pull request's description when the child submits one, whoever authored the pull request? Decided as yes, revisable | the maintainer | the first adopted unit on a pull request of their own | unit 1 live |
| The command bars, 100 percent on the name and 90 on the input, on the fast model | the maintainer runs the replay | two replay runs on the fixture set | unit 2 ships |
Validation
The criteria bind in the two plans, one row per invariant above with its test id, and in the spec rows the units change. Two receipts are human-gated: a routed "fix the failing test" in a pull request's Slack thread, no directive, ends merge_ready with a posted LGTM: in a repository that allows auto-merge; and "use opus for coding in this channel" lands the channel override with its receipt line and no run.
Sources
- The maintainer's direction, 2026-09-15, in the session that produced this record.
- 0008, 0024, 0026, 0031, 0034.
- Specs: routing-and-config item 21, agent-ship items 9 and 10, command-registry items 18 and 20, load-harness item 17.
- The tracker: #131 (the ship loop, whose "never merge" this record keeps on task strings), #388 (ship's live receipts), #1039 (the imperative rule, the 93.0 percent replay, the 29 routes and the one-second live row).
- GitHub, Automatically merging a pull request: "Before you use auto-merge, it must be enabled for the repository"; "People with write permissions to a repository can enable auto-merge for a pull request."
Appendix: survey at 8078f3e3
| Fact | Where |
|---|---|
| Stage order: stage A 320, the regex path 327, the route stage 393, the agent gate 418, the profile gate 434, the thread claimed 477, the ship fork 573, the ledger row reserved 763 | src/core/dispatcher.ts |
The route stage returns unrouted when the thread holds a live run; admission then steers the message into that run | src/core/dispatch/route.ts:660; src/core/dispatch/admission.ts |
CompletionRequest.toolChoice is { type: "tool"; name } only, spelled per API by each provider adapter at this sha and by piStreamOptions through pi's model library since #1203; the seam takes the first matching tool_use | src/providers/types.ts:95, src/providers/anthropic.ts:188, src/providers/openaiCompat.ts:51; src/core/provider.ts:46 and src/core/harness/piAi.ts:200-214 at 06a97079; src/core/dispatch/route.ts:552-556 |
Five routable presets; ship and conductor carry routable: false; conductor joins the enum with the compound offer; spawn.maxChildren defaults to 3 | src/agents/registry.ts:527-640, src/core/dispatch/route.ts:200, src/core/dispatch/spawn.ts:54 |
The router prompt for "fix it": system 5,998 characters, tool 2,590, user 68; 29 chat-exposed commands, 16 of them effect: write (`config.set | clear |
CommandDef has surfaces, enabledWhen, effect, describe, settle; no routing field | src/core/commandRegistry.ts:137-162 |
jsonSchemaFor is the MCP inputSchema; namedToInput(cmd, named, "camel") maps a JSON object back to { args, options }, nesting dotted keys | src/core/commandSurface.ts:161-177,304-320 |
runChatCommand is private to the fast path; isInlineRunCommand is exported; the regex path falls through to the agent on not_found and unavailable | src/core/dispatch/fastPath.ts:71,158-166,185 |
RepoShipInfo.allowAutoMerge is GitHub's allow_auto_merge, read by the same GET as default_branch; PullRequestFacts comes from GET /repos/{repo}/pulls/{n} and carries no auto_merge | src/execution/githubPulls.ts:250-331,398-410 |
The plan form is ^plan <path>.md [units U…]$ over the directive-stripped task text; the task instance carries no merge; the driver derives it from the branch name; the instance guard refuses a merge for an instance with no plan | src/core/ship/coordinator.ts:178-183, src/core/coordinator/handOff.ts:125-151, src/core/coordinator/driver.ts:429, src/channels/adminCoordinator.ts:1231-1235 |
| The runner's pre-check never skips the branch create; the create treats a 422 "already exists" as success | src/core/ship/coordinator.ts:578,897-905; src/execution/githubPulls.ts:208,238 |
| The fork-head check runs on the resume path only | src/core/ship/preflight.ts:232 |
The ship branch already accepts a route decision in its context and publishes it; a ship run has no session, so it is never the thread's sticky agent | src/core/dispatch/ship.ts:128,239; src/core/dispatch/thread.ts:44-55; src/core/dispatcher.ts:1036 |
The repository resolver sets prFromMessage and refFromPr; an inherited pull request rebinds the ref only from a run record | src/core/repoContext.ts:114,122,615-618 |
| Coding 45 minutes, ship 120, three review rounds by default | src/agents/registry.ts:538,589; src/core/shipPipeline.ts:34 |
| Fixture sets: 30 imperative rows, 25 compound rows; bars: accuracy ≥ 95 percent, a compound detection bar, an imperative hit bar labeled "routed to coding" | src/load/routeImperativeFixtures.ts, src/load/routeCompoundFixtures.ts, src/load/routeReplay.ts:579-616 |
The conformance suite fences the catalogue with a snapshot, a sample per field and a COMMAND_FIXTURES escape hatch | src/core/commandConformance.test.ts:194-207 |
The auto-approve workflow fires on a commented review by the configured bot whose body starts LGTM:; off until two repository variables are set | .github/workflows/auto-approve-review-lgtm.yml:49-56 |
The tutorial says ship "only ever runs when named" and shows a plain change routed to coding | docs/tutorials/first-request-in-slack.md:45,63; docs/tutorials/get-started.md:18 |