Skip to main content
Glama
Deploy-Forward

convoy

Official
README.md
# Convoy

Convoy is shared project memory for Grok Bot: attach one MCP endpoint, route work to your existing CLIs, and keep every neuron grounded in one durable thread state.

Public MCP remains one root: `https://convoy.bot/mcp`. A named thread is a `--root` binding, not a second MCP URL.

## Install

Python >= 3.11, standard library only (no runtime dependencies).

```bash
git clone https://github.com/Deploy-Forward/convoy.git
cd convoy
python -m pip install .
convoy --help
```

Alternative: `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.

### Grok Marketplace plugin

The xAI-compatible plugin root is [`plugin/convoy`](plugin/convoy): it ships
`.grok-plugin/plugin.json`, `.mcp.json`, and the `convoy` +
`convoy-wizard` skills. The official source of truth for Grok Marketplace
discovery is
[`xai-org/plugin-marketplace`](https://github.com/xai-org/plugin-marketplace);
its third-party entry must pin a reviewed full commit SHA from this repository
and set `path` to `plugin/convoy`. After that catalog PR merges, Grok Build
install is `/marketplace` → **convoy** → `i`, the same path
[`exa-labs/exa-grok-plugin`](https://github.com/exa-labs/exa-grok-plugin)
documents. There is no OAuth step: `.mcp.json` points at
`https://convoy.bot/mcp`. The Agent Plugins/Cursor manifests remain
compatibility surfaces, not the xAI catalog. A Grok Bot Settings path is
unverified here.

### OpenAI plugin

The OpenAI package lives at [`plugins/convoy`](plugins/convoy), with the
required `.codex-plugin/plugin.json`, a remote HTTP `.mcp.json`, the canonical
Deploy Forward logo, and a bundled fail-closed Convoy skill. The repository's
local Codex marketplace is [`.agents/plugins/marketplace.json`](.agents/plugins/marketplace.json).

```bash
codex plugin marketplace add <checkout-root>
codex plugin add convoy@convoy
```

This package is intentionally separate from `plugin/convoy`: OpenAI, Cursor,
xAI, and Agent Plugins use different discovery manifests. Installing the
OpenAI plugin is the revocable permission grant for its declared MCP connection
and skill; endpoint write gates and exact action consent still apply. Public
directory submission remains a publisher step after Deploy Forward approves
privacy and terms URLs and, if used, OpenAI issues a real connector ID. The
release gates and exact submission sequence are tracked in
[`docs/openai-plugin-publication.md`](docs/openai-plugin-publication.md).

The canonical local, plugin, and future hosted sequence is documented in
[`docs/convoy-happy-path.md`](docs/convoy-happy-path.md), including the exact
chair-addressed `send` path and the tenant-isolation requirement behind the
word "cloud."

### 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. Convoy therefore **probes** its own command line
before writing any hook file, and refuses to write one that would not run.
Check what it picked:

```bash
convoy --root <thread-root> skills --worktree <worktree>
```

The card's `hooks.resolved_via` is one of `console-script` (the installed
`convoy` is on PATH — the best case), `interpreter` (this Python can
`-m convoy`), `interpreter+src` (no install: the command carries the
checkout's `src` with it), or `kept-existing` (a hook already there still
works and was left alone). If nothing resolves, the card carries an install
hint and **no hook file is written**; install the console script and re-run.
The same command is the repair when a hook goes stale: it prunes Convoy's own
dead entries and leaves your own hooks untouched.

## 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):

- `threads [--prune]` — every Convoy thread this machine knows. `--prune` drops 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.
- `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 echoes `since_iso`.
- `relaunch [--thread <name>] [--timeout <s>] [--dry-run]` — after the panes died (shutdown): brings every chair up again from `seats.jsonl` in its own worktree, queues each chair an inbox row saying when it left off (`last_seen`, `unread`, the exact `feed --since <ts>` to run), stamps `kind=relaunch`, and proves connected only from seated acks stamped after the relaunch. Dry shows the timeline and spawns nothing.
- `rail [--since <window>]` — the strip under the panes: feed events, seats connected | pending | stale (from the seated acks), usage remaining per harness (`null` is unknown, never 0), 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 from `recent()`, expand chairs, click → `focus`; a stale chair shows a `nudge` button (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). `pin` toggles topmost; `x` hides to the tray where `pystray`+`PIL` import, else minimizes. `--service` starts one detached strip per machine behind `$CONVOY_HOME/widget.pid` (`already: true` when the pid is alive and its image is our interpreter; a reused pid respawns). `crew --launch` and `relaunch` start it unless `--no-widget`. Stdlib only.
- `focus --seat <chair>` — ask the pane host to highlight that chair. `{focused: false, reason}` until a host adapter is evidenced (tmux `select-pane -t` is tested; Windows Terminal `wt focus-pane` is not evidenced on this machine).

Write (thread state):

- `init` — create the thread layer at `--root`.
- `bind --thread <name>` — bind this root to a named thread.
- `start [<repo>] [--to <harness> ...] [--thread <name>] [--cancel]` — thin alias: git URL → clone once then `onboard --github yes`; local path → `onboard --github no`; no repo → picker from `recent()` (title + root + last activity, never auto-picks newest); empty index → ask to start a new thread; `--cancel` leaves unbound. Already-live harness on the root (whoami/roster) → `attach`, never a duplicate `bring_up`.
- Outside-harness join: a session started in no worktree runs `whoami` (chair null, an ask), `threads`, `start` (picker, never auto-picks), then `attach` + `join --to <harness>` on the chosen root and receives queued sends without stealing a pane. `docs/OUTSIDE_HARNESS_JOIN.md`.
- `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/` and `thread.md` go into that clone's `.git/info/exclude`). Whoever launched first conducts: the first harness named on the first onboard becomes `lead`; a later onboard reports it and never steals 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's `seated` snapshot says `pending`.
- `await-seated --seat <chair> [--seat ...] [--timeout <s>]` — observe the acks: per chair `connected` (its own `seated` row 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 --to <chair> --as <you>` — pass lead to a chair.
- `hook note "<text>" [--as-me] --to <chair>` — leave a note for a chair (or `grok-bot`).
- `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`); `--live` runs a fresh headless vendor session (`executed`).
- `inbox [--seat <chair>] [--drain | --hook-pretooluse]` — list or drain the live-seat inbox. The hook command is always `convoy inbox --hook-pretooluse` (never a baked interpreter path).
- `end [--summary <text>] [--push | --hook]` — explicit task completion, or the Codex/Claude Stop heartbeat. `--push` authorizes exactly one plain `git push` and refuses dirty, detached, or no-upstream state; `--hook` never 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 from `panes` plus a unique WT title or tmux target). Live send needs a consent card that names that pane and the exact keys. `delivery: nudged`, never `delivered`. Refuses when the pane cannot be proven. Write-gated on MCP.
- `bring-up` / `open [--thread <name>] [--dry-run]` — bulk show of seated neurons in one new terminal window.
- `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 MCP endpoint for one root.

### Run your own MCP

```bash
convoy mcp --root <thread-root> --port 8788
```

Then attach `http://127.0.0.1:8788/mcp` in your MCP client. Write tools are
off by default on the RPC layer: set `CONVOY_MCP_WRITE_TOOLS=1` on a
gated/loopback deploy to expose `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`. An ungated
public `tools/list` hides the write tools rather than
listing and refusing them, so a listed verb is a promise. Reads (`choices`,
`neurons`, `inbox` pending, `graph`) stay public, and a public inbox
read 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.
The public `https://convoy.bot/mcp` is bound to one root; a different thread
means running your own server with your own `--root`.

## 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.
- **Deploy-Forward/platform** — a closed sibling repo; not needed to run this repo.
- **Aether** — an internal demo host; not needed.

## 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`, or `pi`) on a thread.
- **synapse**: a native Convoy `send` into 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 `--root` binding (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.

| Harness | `onboard` / `roster` id | `resume_argv` shape | `ensure_first_run` behavior | `send --live` behavior |
| --- | --- | --- | --- | --- |
| `grok` | `grok` | `grok -m <model?> --agent <path?> --resume <vendor-id?>` | Writes PATH ungate block; installs `neuron-identity`; writes Convoy-owned `--agent` file; writes project PreToolUse hook (`convoy inbox --hook-pretooluse`). | Native CLI on PATH. Named live seats queue (`delivery: queued`); never steals `--resume`. |
| `claude` | `claude` | `claude --resume <vendor-id?>` | Writes PATH ungate block; installs neuron skills plus `.claude/commands/end.md`; writes project `.claude/settings.json` (inbox hooks + Stop heartbeat), merges user `~/.claude/settings.json` skip key, and writes `~/.claude.json` trust project keys. | Native CLI on PATH. Named live seats queue (`delivery: queued`); never steals `--resume`. |
| `codex` | `codex` | `codex resume <vendor-id?>` (**not** `--resume`) | Writes PATH ungate block; installs neuron skills, `.agents/skills/convoy-end`, and project `.codex/hooks.json` Stop heartbeat. No Claude permission-ungate writes. | Native CLI on PATH. Named live seats queue; may `codex queue` (`delivery: native-queued`). |
| `cursor-agent` | `cursor-agent` | `cursor-agent --resume <vendor-id?>` | Writes PATH ungate block; installs `neuron-identity`; writes Grok/Claude inbox hook files (swap-safe). Drain via `convoy inbox --drain` (no vendor hook proven). | Native CLI on PATH. Named live seats queue (`delivery: queued`); never steals `--resume`. |
| `agy` | `agy` | `agy --conversation <vendor-id?>` (live `--help` 2026-09-01: no `--resume`) | Writes PATH ungate block; installs `neuron-identity`; inbox hook files as above. | Native CLI on PATH. Named live seats queue (`delivery: queued`); never steals `--resume`. |
| `hermes` | `hermes` | `hermes --resume <vendor-id?>` (live `--help` 2026-09-01) | Writes PATH ungate block; installs `neuron-identity`; inbox hook files as above. | Native CLI on PATH. Named live seats queue (`delivery: queued`); never steals `--resume`. |
| `pi` | `pi` | `pi --resume <vendor-id?>` (flag verified live; `--resume` opens a session picker — direct-id resume unverified) | Writes PATH ungate block; installs `neuron-identity`; inbox hook files as above. | Native CLI on PATH. Named live seats queue (`delivery: queued`); never steals `--resume`. |

Notes tied to code/tests:

- `seat.session_id` and `seat.resume` are distinct: session key vs vendor resume token.
- First-run seats can omit vendor resume; then no resume token is passed.
- Live `send` is headless and never steals an active interactive neuron; refusal cards ask users to `bring_up` / open a pane or write `.convoy/handoff/<chair>-<ts>.md`.
- `context.pack` overlays home-layer `convoy_id` + `thread_key` onto seat-worktree pointers when present.
- `bring_up` / `open` are the bulk show commands; targeted `join --launch` is
  the explicit one-chair exception described below.

### Bring-up and pane invariants

- Isolated WT only: one `wt --window new` spawn, one tab, split panes joined with literal `";"` argv elements.
- Bulk bring-up never uses `-w 0` or `-w <thread>`; targeted launch may use
  `-w 0` only after the user explicitly requests `--launch` from an active
  Windows Terminal session. Never `--` before harness exe; never per-seat
  `CREATE_NEW_CONSOLE`; never close on fail with `WM_CLOSE`.
- Two same-harness seats on different worktrees are two panes; duplicates collapse by worktree/resume/session key.
- `Ctrl+Shift+W` should 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+Down` can 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`](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:

```bash
convoy join --to <harness> --worktree <path> --launch
```

This 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, `WT_SESSION`, and `wt` on PATH | `wt -w 0 split-pane`; targets the most-recent WT window and its active pane |
| tmux on macOS/Linux | `TMUX`, `TMUX_PANE`, and `tmux` on PATH | `tmux split-window -t <caller-pane>`; exact caller pane |

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.

## How it works

1. Attach `https://convoy.bot/mcp`.
2. Run `onboard` with harnesses you already installed.
3. Bind one thread at `--root`; Convoy writes/reads one durable `convoy_id`.
4. Use `send` for 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 use `codex queue`. Drain with
   `convoy inbox --drain` or the project hook `convoy inbox --hook-pretooluse`
   (Grok PreToolUse, Claude PreToolUse + UserPromptSubmit). Hook files never
   bake an absolute interpreter path.
5. Use `bring_up` / `open` only when you want visible interactive TUIs for seated neurons.

## End-to-end example

```bash
# (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: `PYTHONPATH=src python test/run.py` (discovers `test/demo/*_test.py`).  
License: MIT.

## Cloudflare split hosting (static site + MCP proxy)

This repo includes a Cloudflare Worker config that serves the landing page/static files at the edge while preserving the existing Python MCP transport.

- Config: `wrangler.jsonc`
- Worker entry: `workers-site.mjs`
- Static assets directory: `src/convoy/site`
- Production runbook: [`docs/deploy-convoy-bot-mcp.md`](docs/deploy-convoy-bot-mcp.md)

Routing behavior:

- `/mcp` and `/mcp/*` are proxied byte-for-byte to `MCP_ORIGIN` (the current Python MCP origin).
- all other paths are served from Worker static assets (`env.ASSETS.fetch(request)`).

Production currently uses a Worker Route on `convoy.bot/*`,
`MCP_ORIGIN=https://convoy.bot`, and a proxied Cloudflare Tunnel whose ingress
is the separate Python process on `127.0.0.1:8788`. A Worker deploy cannot
update that process. Follow the runbook to restart and prove the Python origin
first, then deploy the Worker only when its code/config/assets changed. Public
Gate 0 remains RED when the write gate is correctly closed; do not claim GREEN
from an updated build stamp or remembered tool count.