convoy
OfficialAllows Convoy to onboard Hermes CLI sessions as neurons, manage resume tokens, and send headless synapses to Hermes for tasks on a shared thread.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@convoybring up an isolated Windows Terminal window with split panes for threads T0, T1, and T2"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Convoy
Convoy: local-first multi-agent orchestration. Run Claude Code, Codex, Cursor, Grok and agy side by side on your machine, on one durable thread.
Convoy's MCP runs on your machine at http://127.0.0.1:8788/mcp (convoy mcp). There is no hosted Convoy endpoint. One local server serves every thread on the machine; each call names its thread, and --root pins one.
Remote access to your loopback MCP is not a Convoy feature; if you build it, put it behind your own access control.
Install
Python >= 3.11, standard library only (no runtime dependencies).
git clone https://github.com/Deploy-Forward/convoy.git
cd convoy
python -m pip install .
convoy --helpAlternative: pipx install . (not verified on Windows in this pass).
The convoy console script and python -m convoy both work only after
install. To run from a checkout without installing, put src on the path:
PYTHONPATH=src python test/run.py on bash, or
$env:PYTHONPATH='src'; python test/run.py in PowerShell.
Skills and plugins
Skills ship from Deploy-Forward/plugins
(github.com/Deploy-Forward/plugins):
the convoy and worklanes plugins carry the Convoy skills and the MCP connection
to your own Convoy (http://127.0.0.1:8788/mcp). The convoy-wizard skill is
not in the plugins repository; convoy preflight still checks the verbs a guided
setup needs, from the list in src/convoy/wizard_preflight.py. This repository is the CLI and
the MCP server. It ships no skill text and no plugin pack, so there is one source
for the skills a neuron reads. Claude Code installs with
claude plugin install convoy@deploy-forward; the plugins repository's README
has the install for every other harness.
A launch writes one Convoy file a neuron reads, the AGENTS.md pointer (see
below): one paragraph naming the convoy plugin's convoy-operate skill and
convoy-dictionary. Convoy replaces that block only when it is a block an
earlier Convoy wrote; a block you edited is left as it is and the card names the
file. Convoy deletes nothing in a worktree: skill copies, agent files or prompts
an earlier version wrote stay where they are, and removing them is your call.
convoy --version prints the version, the executable that ran, the package
source it imported and whether that source is an editable install. Run it first
when a skill and the CLI seem to disagree.
Host rendering
Host rendering: unverified. Whether Grok Bot surfaces Convoy as
@convoy or /convoy, and whether it renders the card tool's
structuredContent as one card with a drill-down or only shows the text copy,
is a fact about the host that nothing in this repository observes. The server
declares card with an MCP outputSchema and answers through
structuredContent so a host that renders cards can; the claim that it does
stays unverified until a maintainer records a live run in
test/demo/fixtures/host_rendering.json (date + verbatim evidence), which flips
host_rendering_contract_test from skipped to asserting.
The canonical local and plugin sequence is documented in
docs/convoy-happy-path.md, including the exact
chair-addressed send path and the tenant-isolation requirement any future
hosted product would have to meet before the word "cloud" applies.
Receiving messages needs a command that resolves
Neurons receive through a harness hook, and a hook runs in its own shell that
inherits nothing from yours. A hook file only ever carries the bare
convoy inbox --hook-pretooluse or convoy end --hook, never an interpreter
path, and Convoy probes that command before writing any hook file. Check
what it found:
convoy --root <thread-root> skills --worktree <worktree>In a worktree Convoy did not mint, skills refreshes only the Convoy-named
files; add --write-repo-files for AGENTS.md. Convoy writes no
.codex/hooks.json: Codex runs Convoy's hooks from the convoy plugin, once the
person trusts them with /hooks in Codex.
The card's hooks.resolved_via is console-script when the installed
convoy answers in the hook shell. When it does not (it is not on PATH, or an
unrelated convoy shim shadows it), the card names the PATH problem with an
install hint, no hook file is written or removed, and a launch refuses
rather than start a pane that cannot receive. Put the console script first on
PATH (convoy --version names the one that runs) and re-run. The same command
is the repair when a hook goes stale: it replaces Convoy's own older entries,
an interpreter-pinned one included, with the bare command and leaves your own
hooks untouched.
Related MCP server: wmux
CLI reference
One line per verb; flags shown are the ones you will reach for (see
convoy <verb> --help for the full set). Every verb accepts a global
--root <thread-root> before the verb name.
Read (no writes to thread state):
list [--json] [--all] [--since <window>]— deterministic numbered thread blocks and full neuron rows. Default: present threads updated/active within 14 days. Temp and absent roots are always named as skipped, never removed.--allincludes hidden/older usable roots with reasons. Unknown fields stay unknown; detached is not dead. The read-only MCPlistexposes the same card to authenticated callers, not the anonymous product edge.threads [--prune]— every Convoy thread this machine knows.--prunedrops rows whose root is under the OS temp dir or is absent and reports every dropped row (never silent).panes— every body of every neuron on this thread, from the OS process table; never a token.whoami— which chair is this process? Walks process ancestry to the harness. On a seated chair it addsleadandlaunched_by({chair, neuron_id}or null).report "<text>"— send your result to the chair that launched you (launched_by), else to the lead; refuses withwhywhen neither exists. Needs environment, token or pane-host proof of your session.A neuron launched over MCP (
crew,join,launch,bring_up) records the conductor its bearer proves as its launcher (launched_by: {kind: conductor, name}); itsconvoy reportis a proven note addressed to that conductor, read with the conductor'srepliescursor. An MCP launch without a bearer identity refuses.adopt --id <neuron id>(or--seat <chair>) — make yourself the launcher of an existing neuron that has none (or whose launcher is gone or detached), then send it its report and reply commands. Needs environment, token or pane-host proof; an unseated caller is attached first. A live recorded launcher is replaced only by the thread's lead.reply <token> "<text>"— answer one send: a note to its proven sender citing the token, which is the delivery receiptrepliescounts. Refuses an unknown token or a caller that was not the recipient.graph [--neuron <chair>] [--html [--out <file>]]— read-only ontology of the thread.seats [--convoy-id <id>]— seat rows.feed --since <10m|2h|1d|45s|ISO>— events in a window; the card echoessince_iso.relaunch [--thread <name>] [--timeout <s>] [--dry-run]— after the panes died (shutdown): brings every chair up again fromseats.jsonlin its own worktree, queues each chair an inbox row saying when it left off (last_seen,unread, the exactfeed --since <ts>to run), stampskind=relaunch, and proves connected only from seated acks stamped after the relaunch. Dry shows the timeline and spawns nothing.rail [--since <window>] [--probe]— the strip under the panes: feed events, seats connected | pending | stale (from the seated acks), usage remaining per harness (nullis unknown, never 0; read by running each harness's CLI only with--probe), last stamp, lead. Reads only the thread; from a chair's worktree it finds its thread through the machine index, so every neuron sees one rail.context [--instance-id <chair>]— pointer pack for a neuron.glance [--thread <name>] [--tray]— one-screen status.resume --neuron <chair>— dry: prints native argv + cwd, spawns nothing.choices— installed harnesses, known worktrees, chairs, terminal adapter; no resume tokens.probe --to <harness>,id,terminals.widget [--topmost/--no-topmost] [--refresh 3] [--service]— always-on-top tkinter strip: one dot per thread fromrecent(), expand chairs, click →focus; a stale chair shows anudgebutton (dry card first, keys typed only on confirm, then the feed is polled 60 s for the chair's own row — that row alone means delivered).pintoggles topmost;xhides to the tray wherepystray+PILimport, else minimizes.--servicestarts one detached strip per machine behind$CONVOY_HOME/widget.pid(already: truewhen the pid is alive and its image is our interpreter; a reused pid respawns).crew --launchandrelaunchstart it unless--no-widget. A start with seats from the widget records the thread's held lead chair as each neuron's launcher (the card sayslauncher.source: "widget-lead"), so the neurons report to the lead; a thread with no held lead refuses until an agent session attaches (convoy attach <thread>). Stdlib only.focus --seat <chair>— ask the pane host to highlight that chair.{focused: false, reason}until a host adapter is evidenced (tmuxselect-pane -tis tested; Windows Terminalwt focus-paneis not evidenced).
Write (thread state):
init— create the thread layer at--root.bind --thread <name>— bind this root to a named thread.start [<target>] [--to <harness> ...] [--thread <name>] [--cancel]— resolve an existing local path without network access; a git URL orowner/reporeuses a checkout by normalized remote identity before cloning; a bare name searches indexed/local repos and the authenticated GitHub user's and organizations' repositories. Only exact case-insensitive repository names or folder basenames can resolve automatically; fuzzy suggestions return a numbered picker. Among complete same-remote matches, prefer an indexed thread root (latest indexed update), then a unique main checkout. Otherwise ask, with main checkouts first and linked worktrees collapsed to a count;--allexpands them. No match asks local folder versus GitHub creation, unless discovery failed (unknown, never absence). Only explicit--createcreates a private GitHub repo. Missing/logged-outghstays local; an exact local match also works offline without refresh.--search-root <dir>repeats and scans one level deep, excluding Convoy state folders except the owned checkout store;--scan-budget <seconds>defaults to 5 and includes local candidate dates. Incomplete scans carry warnings and never auto-resolve. Ordinary remotes use config file reads, including shared worktree configs and once-per-resolution global insteadOf rules; conditional includes/overrides fall back to bounded Git reads. GitHub candidate date metadata comes from the single listing'supdatedAt(marked as that source, not a commit timestamp). Cloud-backed checkouts fetch non-interactively and fast-forward only if clean, behind, and without local commits or ignored/untracked files that would be overwritten; dirty/ahead/diverged/detached work is kept, never stashed, reset, rebased or merged. Fetch and ls-remote alone disable interactive credentials; clone/worktree keep their existing policy. Helper timeouts terminate their process trees without waiting on inherited output pipes. No target returns the existing thread picker. No pane launches. Repo writes stay under.convoy/unless--write-repo-files; cards include the resolution andpulledoutcome, with credentials redacted and non-secret Convoy receipt tokens retained.
The ignored-file guard checks incoming paths immediately before fast-forward. An ignored file created concurrently after that check can still be overwritten by Git; this remaining race is not an atomic preservation guarantee. No stash/reset or implicit file deletion is used.
An indexed thread root outranks the main checkout only when its known index update is within 14 days. Older, future or unknown dates do not prove freshness. When only linked worktrees match, the picker shows their actual numbered rows, newest first, rather than an empty collapsed list. Refresh performs one non-interactive fetch followed by local git merge --ff-only @{upstream}; it never creates a merge commit or performs a second network round trip through pull.
attach [<thread>] [--as-harness H]— link the calling native session to the chosen thread using the same process/native identity proof aswhoami.<thread>is the exactcvy_id, a unique prefix of it of at least 8 characters (cvy_counts), the exact thread name, or the thread's root path; an ambiguous prefix refuses withmatches N threads: <ids>.detach --threadandlead --threadresolve the same way. Pick numbers are display-only: pass the block'scvy_id. No choice prints the list and refuses to guess; unavailable/conflicting identity refuses without creating a chair. A native session can sit on several threads at once, one chair on each: the card lists the others inalso_on, the hooks deliver every thread's inbox (each row labelled with its thread), and a command that needs one thread (whoamiwith no root,detachwithout--thread) refuses with the list. Repeating returnsalready: trueand per-chair catch-up; a bare local self-join reuses the chair. Explicit names, titles, other worktrees andjoin --launchstill provision a new neuron. An unavailable non-temp indexed root is reported inownership_skippedand never refuses the attach; temp roots remain in the index but are excluded from attachment, with reasons inownership_skipped. No pane or vendor usage probe is launched by attach.--as-harnessasserts, never overrides, the proven harness. Legacy catch-up-only callers useattach [<cvy_id>] --read-only(still records an attach event, but never seats the caller).detach [--thread <thread>]— prove the calling chair, write its rolling handoff and append detached state. No history deletion, pane close or session kill. Pending rows remain on disk but cannot wake the detached chair; sends refusedetached; attach again. Attach reactivates the same proven chair.onboard --to <harness> [--to ...] [--thread <name>] [--checkout-root <path|git-url>] [--github yes|no]— name installed harnesses and bind; a URL is cloned once under$CONVOY_HOME/checkouts/<owner>/<repo>(.convoy/andthread.mdgo into that clone's.git/info/exclude). Onboard names no lead: a new thread'sleadis unset until a session attaches (it takes the lead) or a seated chair passes it; a later onboard reports a standing lead and never changes it.seat --to <harness> --session-id <chair> [--worktree <path>] [--model M] [--resume <vendor-id>] [--title T] [--effort E]— register a seated neuron.join --to <harness> [--worktree <path>] [--title T] [--as <chair>] [--launch] [--consent <id>]— register one fresh chair.crew --seat <harness>[,model=M][,effort=E][,where=local|cloud][,title=T] [--seat ...] [--checkout <path>] [--launch]— N neurons at once: validates every seat first, mints one worktree per local seat, joins every chair with a boot prompt, and (with--launch) brings them up in ONE window. Launched is not connected: the card'sseatedsnapshot sayspending.--count Nrepeats one--seatN times (1..16; titles<harness>-<i>or<title>-<i>):convoy crew --seat codex --count 6 --launch.--brief TEXT|@pathsends every neuron the same task once it has seated (--brief-timeout, default 300 s), each copy prefixed with "You are neuron of ...", throughsend: one token per neuron in the card'sbriefs. A neuron that does not seat gets no send and a warning. Without--launchthe card shows the planned briefs.More than four panes open in tabs of four, each a 2x2 grid, in the thread's window.
--heresplits your own window and takes at most four.Canary first: a launch of several neurons starts the first alone and launches the rest only once it is seated (
--canary-timeout, default 120 s). If it does not seat or its harness exits, the rest stay joined and unlaunched (reason: "canary_failed") andokis false.--no-canarylaunches all at once.
add <harness> [<model>|auto] [--effort E] [--title T] [--checkout <path>] [--dry-run]— one neuron: mints its worktree, joins its chair, and launches it inside tmux as a split of your exact pane; on Windows into the thread's own Windows Terminal window (wt -w convoy-<8 hex>: the first neuron opens it, later ones split inside it, never your working window); on POSIX outside tmux into the thread's one detached tmux session. It refuses before any write when there is no terminal to use, and when it cannot prove who is launching (run it from an agent session such as Claude Code or Codex, orconvoy attach <thread>first): every neuron records its launcher. Model and effort are auto unless given: no flag, the harness picks. The card'splacementissplit,thread-window,detached,hereornone; a failed launch leaves the chair joined with arecoveryverb that works on that path.--hereis the opt-in to split the window you are working in (Windows:wt -w 0 split-pane; inside tmux: a split of your pane); outside both it refuses and namesthread-window/detached. It is never the default.await-seated --seat <chair> [--seat ...] [--timeout <s>]— observe the acks: per chairconnected(its ownseatedrow cites the minted token) |pending|stale, with the seconds waited.swap --seat <chair> --to <harness> --handoff <.convoy/handoff/<chair>-<ts>.md> --as <chair>— replace the occupant, keep the chair.seated --seat <chair> --token <token>— proof-of-life echo from the new occupant.lead [--thread <thread>] [--to <chair> [--as <you>]]— read the lead (lead,lead_chair,dangling,reachable_id), or pass it to a seated chair. The author is the proven calling chair (environment or token proof);--asasserts it and refuses when it disagrees. Only the current lead passes it;--to <harness>passes to that harness's one seated chair under the same rule; while the lead is unset or dangling (it names a harness with no seated chair), any seated chair may take it.convoy attachtakes an unset or dangling lead for the attaching chair. On the lead and attach cards,conductoris the lead chair or null; the hostedgrok-botis named only where that hosted conductor is meant.hook note "<text>" [--as-me] --to <chair>— leave a note for a chair (orgrok-bot). A receipt that cites a send token (re token <t>ortoken=<t>) sent to you, with no--to, goes to that send's proven sender (a third party citing it stays unaddressed); one addressed to its own author refuses (a receipt goes to the sender: --to <chair>).stamp "<summary>" [--agent A] [--model M] [--effort E] [--transcript <pointer>]— conductor stamp.send --to <chair|harness> "<body>" [--live] [--dry-run] [--instance-id <chair>]— synapse.--to <chair>(a session_id, e.g.codex-1-demo) queues into that chair's inbox in its own worktree (delivery: queued,delivered: false);--to <harness>with a chair already on that harness refuses (naming a vendor is not naming a neuron); default runner records a feed row (delivery: recorded);--liveruns a fresh headless vendor session (executed).inbox [--seat <chair>] [--drain | --hook-pretooluse]— list or drain the live-seat inbox. The hook command is alwaysconvoy inbox --hook-pretooluse(never a baked interpreter path).end [--summary <text>] [--push | --hook]— explicit task completion, or the Codex/Claude Stop heartbeat.--pushauthorizes exactly one plaingit pushand refuses dirty, detached, or no-upstream state;--hooknever pushes.install --to <harness> --opt-in [--live]— cataloged installer; dry-run by default.
Launch / panes:
choices— see above; run it first.launch --seat <chair> [--dry-run] [--consent <id>]— split one already-joined fresh chair into the active pane host.consent --grant <request-id>— grant a prior consent request after the user explicitly approves it.close --seat <chair> [--consent <id>]— request closure of one Convoy-managed pane.nudge --seat <chair> [--keys <exact>] [--target <tmux-pane>] [--dry-run] [--consent <id>]— wake an idle chair on this machine. Identifies the pane first (live body frompanesplus a unique WT title or tmux target). Live send needs a consent card that names that pane and the exact keys.delivery: nudged, neverdelivered. Refuses when the pane cannot be proven. Write-gated on MCP.bring-up/open [--thread <name>] [--dry-run]— bulk show of seated neurons in the thread's own terminal window (wt -w convoy-<8 hex>).hide/minimize/background [--dry-run]— bulk hide.resume --neuron <chair> --go— spawn once in the chair's worktree; refuses when a live body holds the chair.
MCP:
mcp [--root <thread-root>] [--host 127.0.0.1] [--port 8788]— serve the loopback MCP endpoint for every thread in the machine index;--rootpins one.
Run your own MCP
convoy mcp --port 8788Then attach http://127.0.0.1:8788/mcp in your MCP client. The server binds only a
loopback address (--host accepts 127.0.0.1, localhost or ::1 and refuses
anything else) and answers loopback requests only: the connecting peer must be
this machine's loopback, the Host must be 127.0.0.1, localhost or [::1]
on its port, and an Origin, when a browser sends one, must be an http://
loopback origin on that same port. Anything else is a 403, which keeps a web
page (or a DNS-rebinding name) from driving your MCP. No response carries CORS
headers. The desktop widget's own local server applies the same checks.
Writes need a conductor bearer: run convoy conductor mint on this machine and
send it as Authorization: Bearer <bearer> on every request. The bearer opens
the write gate for send, stamp, note, join, seat, launch,
crew, seated, consent, await_seated, focus, nudge, onboard, clone, mint,
repos, resume with go=true, and inbox with drain=true. There is no
process-wide switch: CONVOY_MCP_WRITE_TOOLS was removed in 1.3.2. A
tools/list on a machine with no bearer minted hides the write tools. Once a
bearer exists they are listed for every caller and refused at tools/call
without it, so a listed verb is a promise only to the caller holding the
bearer. Reads (choices,
neurons, inbox pending, graph) need no bearer, and an inbox read
without one never echoes the row token. repos wraps gh repo list on the MCP
process PATH (name, url, private, updated_at; gh absent is an install hint);
it lists the gh login on the MCP host, the conductor's account, which is why
it sits behind the gate rather than handing that inventory to strangers.
clone puts a URL under $CONVOY_HOME/checkouts/<owner>/<repo>; mint
derives one worktree per seat from that checkout as <checkout>-wt-<name>
on branch convoy/<name>, so nobody hand-makes worktrees for N neurons. crew
does the whole walk for N seats (validate, mint, join each with a boot prompt,
one window) and await_seated reads the acks back, so "they all connected" is
observed, never assumed. convoy preflight tells you which of the wizard's
verbs a live tools/list is missing and why.
One local server serves every thread on the machine; each call names its
thread. A server started with --root is pinned to that thread, and a call
that names another thread still wins.
Wake service
Wake dispatch is off for a root until convoy --root <root> wake enable.
Read convoy --root <root> wake status for enabled state, routes, faults and
held alerts; convoy --root <root> wake disable opts that root out without
restarting the origin. The supervised MCP origin runs one dispatcher per enabled
root under an OS lock.
The waiter route is dispatcher-managed. A session starts its own background waiter using the command printed by its Stop hook; it receives a token pointer, drains only its proven inbox, answers with a token-citing receipt and re-arms. A detached waiter started by a hook is not a session wake. Do not run legacy inbox polling alongside the enabled waiter protocol or infer delivery from a queued/fired card.
A detached chair retains history, handoff and pending rows but cannot drain, pulse or wake; new sends refuse until the same proven session attaches again. Detach does not close its pane or terminate its harness.
Names you will see
Grok Bot — the xAI desktop conductor chat that attaches the MCP; not a neuron.
ola-brain — a private predecessor wrapper; refused by
install, not needed.platform — a closed sibling repo; not needed to run this repo.
Terms
Grok Bot: the conductor in this chat; not a neuron and not a window.
neuron: one BYO harness session (
grok,claude,codex,cursor-agent,agy/antigravity,hermes, orpi) on a thread.synapse: a native Convoy
sendinto one neuron; one harness, one meter, compact card back.Convoy: source of truth (
feed, seats,convoy_id).thread: durable circuit keyed by
convoy_id.named thread: a
--rootbinding (not a second MCP URL).grok-bot-local vs grok-bot-cloud: neuron host (user machine vs cloud agent), not a second source of truth.
Product wording retires hop in favor of neuron/synapse/thread.
The problem
Single-harness chat is weak project memory: context windows bloat, meter state drifts, and another agent cannot safely rehydrate shared state without copy/paste loss.
Wrapper stacks (one vendor CLI inside another) add indirection and contention instead of shared truth.
The solution
Convoy keeps a slim pointer/stamp layer while Grok Bot remains conductor. Synapses run on native vendor CLIs, return compact cards, and keep session ownership separated.
Contract: feed + seats + convoy_id. Unknown values stay JSON null; no invented usage/session numbers.
Machine-readable contract: src/convoy/harness_effort.json (loaded by MCP-facing code).
Keyed effort language (locked)
Effort keys are harness-scoped and must not be merged. The locked key space
lives in CANON.md ("Effort keys are harness-scoped") and the
machine-readable source of truth is src/convoy/harness_effort.json — this
README deliberately does not restate the table, so there is exactly one place
for it to drift from the code: none.
Supported neurons (code-true contract)
grok, claude, codex, cursor-agent, and agy have a cataloged installer
(convoy install --to); hermes and pi are BYO-only (install refuses them)
and their direct-id resume is unverified.
Where a first run writes: in a worktree Convoy minted (.convoy/minted.json,
written by crew / mint when they create it, naming that worktree and its
checkout) every file in the table goes in. Anywhere else, often your own repo, a
launch and skills write only the Convoy-named files git excludes
(.claude/settings.local.json, the convoy-root pointers,
.grok/hooks/convoy-inbox.json). AGENTS.md is written there
only after an opt-in: --write-repo-files on the CLI, write_repo_files: true
on MCP bring_up / open / launch / crew behind the write gate (never on a
dry run: a dry bring-up / open / relaunch on the CLI, or a dry MCP
bring_up / open, refuses it and writes nothing). The opt-in is kept, bound to that folder and kept out
of git, in .convoy/repo-files.json, so later launches there refresh them.
Withdraw it with convoy --root <root> skills --worktree <worktree> --no-write-repo-files; the files already written stay, and are yours to keep or
delete. Until
then the card lists what is missing on disk as would_write, and a Codex seat
without Convoy's hook cannot receive; the note names the route that works where
it is read. A .claude/settings.local.json that git tracks is never written.
The card names each home trust store a launch wrote (trust_stores_written).
start and onboard write nothing outside .convoy/ without the flag;
terminals writes nothing.
Harness |
|
|
|
|
|
|
| Writes PATH ungate block; writes the | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; writes the | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; writes the | Native CLI on PATH. A send |
|
|
| Writes PATH ungate block; writes the | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; writes the | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; writes the | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; writes the | Native CLI on PATH. Named live seats queue ( |
Notes tied to code/tests:
seat.session_idandseat.resumeare distinct: session key vs vendor resume token.First-run seats can omit vendor resume; then no resume token is passed.
Live
sendis headless and never steals an active interactive neuron; refusal cards ask users tobring_up/ open a pane or write.convoy/handoff/<chair>-<ts>.md.context.packoverlays home-layerconvoy_id+thread_keyonto seat-worktree pointers when present.bring_up/openare the bulk show commands; targetedjoin --launchis the explicit one-chair exception described below.
Bring-up and pane invariants
Every WT launch on a thread targets the thread's own window,
-w convoy-<8 hex of sha256(convoy_id)>:new-tabfor the first neuron,split-pane -Vinside it after that, panes in one spawn joined with literal";"argv elements. Never-w 0, never the caller's window, never--before the harness exe; never per-seatCREATE_NEW_CONSOLE; never close on fail withWM_CLOSE.Two same-harness seats on different worktrees are two panes; duplicates collapse by worktree/resume/session key.
Ctrl+Shift+Wshould only drop one split pane at a time (or no-op when there is no split pane left).Codex TUI conflict: while a Codex pane is focused,
Shift+Up/Shift+Downcan change reasoning level and fight pane navigation. Do not use those shortcuts for pane selection in that focus state.
Targeted one-chair launch
Full contract and DoD: docs/targeted-launch.md.
convoy choices lists installed harnesses, known Git/registered worktrees,
existing chair identifiers, and the detected terminal adapter. It deliberately
omits every vendor resume token. A model or user can then invoke:
convoy join --to <harness> --worktree <path> --launchThis registers and launches exactly one fresh chair. Harness argv construction is independent of terminal placement, so all harnesses use the same terminal adapter contract. A persistent atomic launch claim refuses duplicate launchers; existing/resumable chairs are not eligible and a failed terminal spawn leaves the fresh chair pending for an explicit retry.
Supported active-pane adapters:
Host | Detection | Targeting |
Windows Terminal | Windows and |
|
tmux on macOS/Linux |
|
|
Windows Terminal, | Windows, |
|
On macOS or Linux outside tmux, with tmux installed, launch and join --launch
join the thread's one detached tmux session (convoy-<8 hex>; the first neuron
creates it with new-session -d, later ones split-window -t =<name>:) instead of
refusing. The card says placement: detached and gives attach
(tmux attach -t =<session>). The session is created synchronously, so a
refused new-session is reported as not launched. The MCP launch tool, behind
the write gate, does the same.
Other terminal hosts fail closed with a manual-pane instruction. Convoy never
injects keystrokes or guesses an iTerm, Terminal.app, WezTerm, kitty, or shell
API. Source installs currently require Python 3.11+; the project exposes a
cross-platform convoy console entry point, but a machine without Python still
needs a packaged executable/runtime before a skill can invoke it.
Creation and closure are separate capabilities. The Windows Terminal CLI can
create a split but does not expose its closePane action. A killed TUI may leave
an exited pane visible under graceful closeOnExit, so absent process IDs are
not pane-close proof. New targeted launches therefore use a Convoy lifecycle
host: after a separate, scoped close-chair consent it terminates only its owned
child tree and exits zero. Legacy panes still require Ctrl+D or the configured
closePane binding. First-run harness trust prompts are also user decisions;
Convoy returns an awaiting-user-consent card and never auto-accepts them.
Resume: what works today, and what is being improved
Resume is an area to improve. Convoy only resumes a neuron's conversation when it knows the harness's own id for it, and today it learns that id for two harnesses only. For the others, the id is recorded only if someone writes it on the neuron's row. So a relaunch may start a fresh conversation instead of continuing the old one.
Before you promise a person that a neuron will resume, read the dry plan:
convoy --root <root> resume --neuron <id> (without --go it launches nothing). If the argv carries
the harness's resume form with an id, it will resume. If it carries a declare flag (--session-id,
-s) or no id at all, it will start a new conversation. Say so.
Harness | Resume command | Where the id comes from, and when | Evidence |
|
| Convoy mints the id before the first launch and declares it with | ran: a session declared with |
|
| The same minting: declared with | help: |
|
| Not captured. No launch flag sets the id; record it with | help |
|
| Not captured. No launch flag sets the id; record it with | help |
|
| Not captured. No launch flag sets the id; record it with | help |
|
| Not captured; record it by hand as above. | help |
|
| Not captured. | help |
What is being improved:
capturing the harness's own id automatically for every harness, not only the two that let Convoy choose it at launch;
recording each harness's resume command, and the evidence for it, in the harness contract.
How it works
Start Convoy on your machine (
convoy mcp) and attachhttp://127.0.0.1:8788/mcp.Run
onboardwith harnesses you already installed.Bind one thread at
--root; Convoy writes/reads one durableconvoy_id.Use
sendfor synapses. A send that names a live seat queues the body (delivery: queued,delivered: false); it does not type into the TUI and does not spawn a second--resume. Codex may usecodex queue. Drain withconvoy inbox --drainor the project hookconvoy inbox --hook-pretooluse(Grok PreToolUse, Claude PreToolUse + UserPromptSubmit). Hook files never bake an absolute interpreter path.Use
bring_up/openonly when you want visible interactive TUIs for seated neurons.
End-to-end example
# (after `python -m pip install .`, see Install)
# 1) Name installed harnesses and bind this root to one thread
convoy onboard --to grok --to claude --to codex --thread demo
# 2) Register seated neurons (session key + optional vendor resume token)
convoy seat --to grok --session-id seat-grok --worktree ../wt-grok --model gpt-5.6-sol --resume vendor-grok-uuid
convoy seat --to codex --session-id seat-codex --worktree ../wt-codex --resume vendor-codex-uuid
# 3) Dry-run bring-up shows native argv (Codex uses "resume" subcommand)
convoy bring-up --dry-run
# 4) Headless synapse (safe default)
convoy send --to claude "Summarize open payment retry bugs and propose a fix plan."
# 5) Optional live headless run in a fresh native session (no resume token)
convoy send --to codex --live "Draft unit tests for the retry planner."Development: the supported source-checkout test entrypoints are
PYTHONPATH=src python test/run.py (PowerShell: $env:PYTHONPATH='src'; python test/run.py)
and python -m unittest from the checkout (for example,
python -m unittest discover -s test/demo -p '*_test.py'). Both discover
test/demo/*_test.py and isolate CONVOY_HOME under
the OS temporary directory, even if it was set to a non-temporary path.
Other test adapters, including direct top-level discovery by IDEs or pytest,
must be given their own throwaway CONVOY_HOME; their startup shapes are not
covered by this guard.
License: MIT.
This server cannot be deployed
Maintenance
Related MCP Connectors
Eyes and hands on real Windows PCs — observe, click, type via Glasswarp API.
Develop, manage, and debug Railway projects, services, and deployments from within agents.
Securely control computers you explicitly pair through files, terminals, processes, screenshots, desktop UI/input, clipboard, browser automation, diagnostics, and document tools.
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables management of visible, interactive terminal sessions across platforms (macOS, Windows, Linux, WSL). Supports creating, executing commands, capturing output, and managing multiple terminal windows simultaneously.51MIT
- AlicenseNot gradedqualityAmaintenanceA native Windows terminal multiplexer with MCP bridge for AI agents, enabling browser automation, multi-agent coordination, and terminal control.424MIT
- AlicenseBqualityCmaintenanceEnables stateful subprocess sessions (REPLs) and tmux-backed TUI program control for interactive tools like Python, r2, or shells.13AGPL 3.0
- FlicenseNot gradedqualityDmaintenanceEnables orchestration of multiple AI coding agents (Claude Code, Gemini CLI, etc.) in isolated Wezterm panes, allowing one agent to spawn, monitor, and communicate with others across projects.7 npm1-