# Parlay **Talk to your background coding agents from your phone.** Parlay gives every long-running AI agent its own chat channel — dictate a message while you're walking the dog, the agent picks it up, works, and replies to the same thread. This is the project behind the **"Voice First: Code Anywhere"** demo: voice dictation on a phone, driving autonomous terminal coding agents on a machine somewhere else. > Status: **alpha**, single-owner. Interfaces move fast. Built for one person's > workflow first — it works, but it has sharp edges and assumes you're comfortable > running your own server. --- > **Phone → laptop in four steps:** [docs/quickstart.md](docs/quickstart.md) — install the Talon > actions, start the server, scan the QR it prints, tap. (The Quickstart below is the local-only > CLI path.) ## What you actually get - **A per-agent chat channel.** Every enrolled agent gets its own tab. Messages route only to the agent they're addressed to. - **Voice-first control.** A compiled phrase engine turns spoken commands into panel actions, and agent replies can be read back aloud with per-passage playback — so a whole review cycle can happen without a keyboard. - **Durable identity + memory.** `identity` and `scratchpad` persist across restarts, so an agent that blows its context window recovers *who it is* and *what it was doing*. Both are plain files under `~/.parlay/agents//` and work standalone on any clone. (The full `identity → handoff → scratchpad` chain adds a `handoff` leg from the author's private beads-store tooling — `handoff` is not a `parlay` verb and is not installed by this repo, so a clone gets the portable two legs. See the note below.) - **Spawn + supervise.** Launch a background agent that auto-enrols as a live tab, and drive event-based follow-ups. - **Reachable from anywhere you can reach the host** — over your LAN, a [Tailscale](https://tailscale.com) tailnet, or just `localhost`. ## Requirements, honestly **The CLI + server work standalone.** Start the server, point the CLI at it — no other services, no accounts, no tunnel. That's the path in the Quickstart below and it is the one this repo fully supports. ([Bun](https://bun.sh) is only needed if you also want the chat panel or the git hooks.) **The chat panel needs Bun and nothing else.** `packages/client` is a browser bundle, and the Go server *is* its host: it serves the bundle same-origin from the same `:4242` it serves `/api/chat/*` on, so there is no reverse proxy to wire and no second service to run. Build the bundle (`cd packages/client && bun run build`) and open `http://localhost:4242/` — Quickstart step 5. The author's own install hosts the panel behind a private page host called Pulse, which is **not open source and not available here**, but nothing in this repo requires it; it is a distribution choice, not a dependency. A second, separate bundle — the fleet dashboard (`packages/webview`) — is served at `/fleet/` and is not part of the Quickstart. **Tailscale is optional.** Nothing requires it. It is simply how the author reaches the host from a phone; a LAN address or any other private tunnel works the same way. ## Quickstart (local only — no Pulse, no tailnet) ### Step 0 — get the code, then check the machine before it wastes your time ```sh git clone https://github.com/trillium/parlay && cd parlay # the first command of this path ./bin/parlay-preflight ``` The clone is the one step nothing below can do for you. The preflight then checks the whole prerequisite set at once — Go (and its version against the floor `tools/cli/go.mod` declares), `git`, `curl`, the chat-server port, the eval-engine port, the state directories, and write permission on each — and it reports **every** problem it finds in one run, each with the exact command that fixes it. It needs nothing installed to run: it is bash and nothing else, on purpose (a checker that cannot run on a broken machine is useless precisely when it is needed). **Success looks like** `0 failure(s).` and exit 0, followed by the next command to run. **If it fails**, fix exactly what it names and run it again — the `fix:` line under each `FAIL` is the remedy, not a hint. It exits 1 while anything is blocking; `warn` lines never block, and each one says which optional feature it limits (the chat panel, the sandbox example). It is also the command to reach for when something that *used to* work stops working, or when a later step in this file fails: it is the single prerequisite surface, so the newcomer's step 0 and the operator's "what is wrong with this machine" are one command and not two. Its counterpart for a *running* instance is `./bin/parlay health`, named where you first have one in step 4 — the same two commands in both roles, deliberately, and not a second checklist to find. Steps 1–5 below are the path. When one of them bites — and a few of them will, in ways no prerequisite check can see — [`docs/traps.md`](docs/traps.md) is the same knowledge ordered by **when you will meet it** rather than by when the incident happened, so you can read the stage you are in instead of the whole record. ### The five steps (step 0 has just checked everything they need) Prereqs: [Go](https://go.dev) **1.26.5+** — the CLI and server are both Go, and `bin/parlay` builds the CLI for you on first run. (1.26.5, not 1.26, because `tools/cli/go.mod` declares `go 1.26.5`; with the default `GOTOOLCHAIN=auto` Go downloads the newer toolchain for you, but with `GOTOOLCHAIN=local` it is a hard error.) Step 0 checks all of this for you; this paragraph is what it is checking. [Bun](https://bun.sh) is needed **only** if you want the chat panel (`packages/client`, step 5) or the git hooks. Steps 0–4 are all Go, so you can skip `bun install` entirely unless you want the git hooks or the panel. > **This repo is four separate Go modules** — `packages/go-server`, `tools/cli`, > `tools/relay`, `packages/spawn-profiles` — and there is **no root `go.work`**. > So every `go` command has to be run from *inside* the module it names. A > repo-root-relative path such as `go run ./packages/go-server/cmd/parlay-server` > fails with `go.mod file not found in current directory or any parent directory`. > Each step below says which directory it runs in; every command from here on is > relative to the root of the clone unless it says otherwise. ```sh bun install # optional: also wires the git hooks (core.hooksPath tools/hooks) ``` **1. Prove the whole thing works first, in a sandbox.** One command that builds the CLI and the server, starts that server on a free port with `HOME` redirected and every path it can write pinned inside a throwaway temp directory (`-state-dir` for history and registry, `-pai-dir` for the TTS cache, `-assets-dir` for the panel), seeds two agents from [`examples/parlay-state`](examples/parlay-state) and a little history from [`examples/data-dir`](examples/data-dir), and then exercises the round-trip against it — send, history, the agent-role reply path, `identity`, `launch` and `doctor`: ```sh ./examples/bootstrap-sandbox.sh ``` **Success looks like** `all checks passed`, exit 0, and ten `PASS` lines covering the seeded registry, a message round-tripped *and* persisted into the state dir, the reply routed onto the right agent's channel, the server URL resolved from the sandbox's own `config.json`, and `doctor` passing identity, registry membership and reachability. It then prints a `LIMITS` block saying exactly what it did **not** prove — read it, because "all checks passed" is not "everything works". **If it fails**, it prints the failing check — and the server's log, if the server was the thing that did not come up: re-run step 0 first, since the failures it can have are usually prerequisite failures, and if step 0 is clean the failing check names the layer that is broken. It needs `go` and `curl`, and it removes its sandbox on exit (`--keep` leaves it on disk). This is how you run a *fleet* in isolation, and step 2's note below is why it matters: the server's store is only the first of the hardcoded paths, and the listener layer is one nothing redirects at all. [`examples/README.md`](examples/README.md) is the file-by-file recipe, and [`docs/traps.md`](docs/traps.md) stage 4 lists the couplings a single environment variable does not cover. It never touches your own files or a server you already have running. **2. Start the server.** It's `packages/go-server`, a single Go binary that listens on `:4242` (default `PARLAY_SERVER_ADDR=127.0.0.1:4242`) and owns `/api/chat/*`: ```sh cd packages/go-server && go run ./cmd/parlay-server ``` **Success looks like** the line it prints on boot — `parlay-server: listening on http://127.0.0.1:4242 (state dir: …, assets: …)` — and the process staying in the foreground. **If it fails**, a port somebody already holds exits immediately with `listen failed: listen tcp 127.0.0.1:4242: bind: address already in use`: run step 0, which says whether something is already there and whether it is parlay. The `assets:` value on that boot line is decided **once, when the server starts**: with no `-assets-dir` it walks up from its own directory and then from the directory you started it in looking for a checkout's `packages/client/dist`, and falls back to a bare `dist` when there is none. On a fresh clone there is none — the bundle is gitignored — so this process serves `503` on `/` for the rest of its life *even after* step 5 builds the bundle, and step 5 tells you to restart it. Start the server again after building, or build the panel first, and it finds the bundle itself. It persists state under `$PARLAY_STATE_HOME` (default `~/.parlay`) — messages/agents/ drafts/settings/uploads live there. To keep a dev run's *server store* out of a live install: ```sh cd packages/go-server && go run ./cmd/parlay-server -state-dir ~/.parlay/dev-data ``` > **⚠️ The server reads and writes its persisted store from `~/.parlay` by default.** > If you're running it alongside a live install, point `-state-dir` (or > `PARLAY_STATE_HOME`) at a scratch directory so you don't collide with or clobber > existing state. The chat history, agent registry, drafts, settings and uploads all > live there. `-state-dir` moves that one store and nothing else. `HOME`, the agent store the CLI enumerates (`PARLAY_AGENT_HOME`), the TTS cache and the host-wide listener layer are untouched, so a second server started this way still shares the machine's agent ids, its relay and its hooks. Step 1's sandbox is the version that isolates a whole fleet; [`docs/traps.md`](docs/traps.md) stage 4 names each coupling and the variable that covers it. **3. Point the CLI at it**, in another shell. Every command from here on is written relative to the **root of the clone**: ```sh cd /path/to/parlay # the directory you cloned into above ``` No `PARLAY_SERVER` export is needed: the CLI's coded default is `http://localhost:4242` — exactly where step 2 put the server — and the `bin/parlay` wrapper adds no environment of its own. If your server lives somewhere else, either export `PARLAY_SERVER` (the environment always wins) or run `parlay remote set ` (persists to `~/.parlay/config.json`, which beats the coded default but loses to the env var). **Success looks like** nothing visible yet — this step only fixes the address step 4 uses. **If a later command cannot reach the server**, `./bin/parlay remote` prints the URL it resolved and where that came from (`source: env`, `config` or `default`), and step 0 reports whether anything is answering there. **4. Talk to it, then confirm it is healthy:** ```sh ./bin/parlay # live snapshot: subscribers, agents, last messages ./bin/parlay send --demo --force "hello" # message the 'demo' channel ./bin/parlay history 5 # read it back ./bin/parlay health # the running instance's vitals: up? memory? history size? ./bin/parlay doctor # an AGENT's self-check — from this shell it exits 1 (see below) ./bin/parlay doctor --json # same checks as one JSON document (schema parlay.doctor/v1), for scripts/LLMs ./bin/parlay doctor deploy # deployment-level sweep: launchd, ports, logs, pins ``` **Success looks like** `sent to demo — id …`, your text in `history`, and `health` printing your server as up. `health` still exits 1 here — its second line is the *optional* voice engine, which you have not installed; that red line and the exit code are the engine's, not your install's (the note under this block says it at length). The check to run is the same one as step 0: `./bin/parlay-preflight` is the *machine* half of the health surface and `./bin/parlay health` is the *running instance* half — the first now reports the port as a parlay instance answering `/health`, the second reports what is inside it. Those two commands, in both roles, are the whole surface: there is no separate newcomer checklist and no separate operator one. `doctor deploy` is for a machine running the launchd services (`packages/go-server/deploy/install.sh`); on a fresh clone it has nothing to inventory and says so — and it still exits 1 there, for the same optional voice engine that makes `health` red: that FAIL is the engine you have not installed, not a deployment finding. Its health probes follow the same server/engine resolution as every other verb, so it reports on the server your CLI is actually pointed at — never on a hardcoded `:4242`. `send` normally refuses a target that isn't in the live agent registry; `--force` seeds a channel before its agent has registered, which is exactly the case here. `doctor` is an **agent's** self-check, so run from a plain host shell it reports `FAIL PARLAY_AGENT_ID is not set` and exits 1 — by design, not a broken install. That is the one check the Quickstart's shell cannot satisfy; `health` is the host-level equivalent. Run `doctor` inside a spawned agent to see the rest (registry enrolment, monitor, identity, scratchpad, spawn credentials). `health` reports the chat **server** (labelled `server`, not `relay` — parlay ships a separate relay daemon that only `parlay monitor` needs) and then the optional **voice engine**. You have not installed a voice engine at this point, so that line is red and `health` exits 1; that is the engine, not your install. Start one with `nohup ./bin/parlay eval serve &` only if you want spoken or typed phrase commands — the CLI, the API and the panel's text chat do not need it. The engine is the one component with no per-instance identity: it has no state directory and no `config.json` key, so its *address* is its identity, and `127.0.0.1:4343` is a host-wide slot shared by every parlay instance on the machine. That is fine for the single-instance Quickstart above. If you run a second instance — `parlay-dev`, a `-state-dir` server, or `parlay remote set` — the engine needs two knobs moved together, because it both *answers* probes and *pushes* actions: - `PARLAY_EVAL_ENGINE_URL` — where `parlay health` / `parlay doctor` probe it. Leave it at the default with a non-default server and those two annotate the line as the host's engine rather than let a green tick describe another instance's. - `parlay eval serve --push-url` (or `PARLAY_EVAL_PUSH_URL`) — where the engine delivers computed panel actions. Its default is the **default** server's `http://127.0.0.1:4242/api/chat/eval-push`, so an engine started for a second instance without this would drive the *first* instance's panel. There is a third, quieter cross-instance coupling, in the CLI rather than the engine: **`parlay listen --agent ` is a host-wide takeover.** It finds any other live `listen`/`monitor` on the same agent *id* in this host's process table and ends it — it does not distinguish instances, servers or state dirs. So a second instance's `listen --agent demo` kills the first instance's `demo` listener and leaves that instance registered but deaf. Give each instance its own agent ids (`demo` vs `demo-dev`); `--name`/`--color` do not scope it. If you deliberately want two instances sharing one channel name, set `PARLAY_LISTEN_NO_SINGLETON=1` in the one that must not evict (duplicate delivery becomes possible, and the skip is announced on stderr). `parlay shutdown ` reaps by the same id-based match, so it reaches across instances too. Only a process whose own `argv[0]` is `parlay` or `parlay-cli` is ever a candidate, so a script, shell or agent harness that merely *contains* that command line is never the victim — arming from a wrapper cannot kill the wrapper (this repo's guard got that wrong until 2026-10-05). The flip side is that a renamed copy of the binary is not detected at all, so duplicate delivery comes back silently. And a fourth, in the relay itself: **the relay is a per-user singleton that binds one upstream server for life.** `tools/relay` runs one process per user on the host-wide `$TMPDIR/parlay` runtime dir, started with a single `-server`. A second instance shares that process, so `listen`/`monitor` without `--legacy-poll` would enroll into a relay that is polling the *other* instance's chat server — the enroll succeeds, the tab looks live, and nothing you send to your own server ever arrives. `parlay monitor`/`listen` now refuse this before registering anything: `preflight OK` means the relay is up **and** polling the server your CLI is pointed at, and a mismatch exits 1 naming both (a relay too old to report which server it polls is let through — the check cannot guess). Three ways out — use `--legacy-poll`, give the instance its own relay (a `PARLAY_RELAY_RUNTIME=` plus a relay started with `-server $PARLAY_SERVER`), or point `PARLAY_SERVER` at whatever the existing relay is already polling. ### Your first routed message Step 4 put a message onto a channel, but nothing has *received* it yet — and step 1's sandbox says so from the other side: its `LIMITS` line names delivery as the one thing that run does not cover. One command closes that gap on a fresh clone, because `--legacy-poll` polls the server natively in Go and needs no relay binary: ```sh ./bin/parlay listen --agent demo --name Demo --legacy-poll # register, announce, then stream; runs until Ctrl-C, so use a second shell ``` **Success looks like** those three lines in the listening shell — ``` parlay listen: registering 'demo' … parlay listen: announced — arming monitor … parlay monitor (legacy poll) — server http://localhost:4242 channel demo ``` — and then, when you send from the first shell, the message itself arriving as one `CHAT_MSG` line: ```sh ./bin/parlay send --demo "first routed message" # no --force needed now: listen just registered demo ``` ``` CHAT_MSG|m1|user|first routed message ← the id is the one `send` just printed ``` That line **is** delivery — the same text an agent process would read. The send also shows up in `parlay history`, so a message that is in history but never arrives as a `CHAT_MSG` line is a listener problem, not a routing one. Ctrl-C ends it when you are done. **If it fails**, the listener's own output names the layer: `relay is not up and could not be started` means `--legacy-poll` was dropped (the relay path needs a binary a fresh clone does not have — the two cases are spelled out below), and a listener that starts but stays silent while `parlay history` shows your message is the registered-but-deaf shape [`docs/traps.md`](docs/traps.md) stage 3 opens with. Two things to know before you reach for it: the listener is a **takeover**, not an addition (a second `listen`/`monitor` for the same agent id ends the first on this host, whatever instance it belongs to — stage 4), and this path is one process that leaves nothing behind, because `--legacy-poll` neither enrolls in the relay nor writes a spool into `$TMPDIR/parlay`. **5. Open the panel (optional — this is the only step that needs Bun):** ```sh cd packages/client && bun run build # writes dist/index.html + dist/parlay-agent.js ``` Then open . **The server from step 2 cannot see this build** until it restarts: it resolved its assets directory at startup, when there was nothing to find, so it is still serving the `dist` fallback and answers `503`. Ctrl-C that server and run step 2 again — it walks up from the directory you started it in, finds `packages/client/dist` on its own, and needs no flag. (Building before step 2 works too, and so does `-assets-dir `, which is resolved from the directory you start the server in — give it an absolute path if you are the `cd packages/go-server` shell.) Until a server can see a bundle, `GET /` answers `503` with those instructions on its body — every `/api/chat/*` route works regardless, and none of the CLI in step 4 ever needed the panel. The bundle is gitignored, so this is a once-per-clone build. **Success looks like** the panel loading with the `demo` tab from step 4 — no browser needed to check the half that was just broken: `curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:4242/` prints `200`. **If it fails**, `curl -sS -i http://localhost:4242/` says which half is missing: `503` with the `bun run build` line means the server cannot see a bundle — it was never built, or it was built *after* that server started, so restart it; `404` means the assets directory the server was pointed at has no `index.html`. `/fleet/` is a *different* app (`packages/webview`, React) served from `/fleet`; `packages/go-server/deploy/install.sh --build` builds and copies it there. It is not needed for anything above. That round-trip is the whole substrate. From here: ```sh ./bin/parlay reply --agent demo "on it" # posts an agent-role message into history; channel routing needs a spawned agent's context ./bin/parlay alert "heads up" # broadcast to every agent ./bin/parlay help # every verb ./bin/parlay --help # one verb, in detail (every verb answers this) ./bin/parlay monitor --legacy-poll --agent demo # stream a channel; runs until Ctrl-C, so give it a second shell ``` `--legacy-poll` polls natively in Go and needs nothing beyond the server. `listen` — one-call self-enrolment: register, announce, then stream — accepts the same flag and takes the same native path, so `listen --agent demo --name Demo --legacy-poll` gives you a live enrolled agent on a fresh clone too. *Without* that flag, both verbs go through the relay binary — gitignored, built by neither `bun install` nor `bin/parlay` — and what happens depends on whether a relay is already running on this host: - **No relay up** (a machine with no parlay install): a clean exit 1, `relay is not up and could not be started`, and **nothing enrolled** (`NOT registered, so nothing is deaf`) — every enrolling entry point, `listen`, `monitor` and `claim` alike, probes the relay *before* it registers, so a missing relay never leaves a tab that looks live in the panel. Run `tools/relay/build.sh` first to take the relay path. - **A relay already up** (any machine that has parlay installed): the enroll succeeds through that host-wide relay, and your spool then carries whatever server *that* relay was started with — not necessarily yours. The preflight line is the whole check, so read it: `preflight OK … and polling ` naming your server means it verified its upstream; a `preflight OK` with **no** `and polling` is a relay too old to report one, and that is deliberately let through, because the check cannot guess. If you point an instance anywhere but that relay's server — every second instance — use `--legacy-poll`, or give the instance its own `PARLAY_RELAY_RUNTIME` plus a relay started with `-server $PARLAY_SERVER` (step 1's note; [`docs/traps.md`](docs/traps.md) stage 4). Launch a background agent that shows up as a live tab (needs a [Claude Code](https://claude.com/claude-code) install and the [herdr](https://github.com/trillium/herdr) terminal it spawns into): ```sh parlay spawn code-reviewer "Code Reviewer" "#c084fc" \ "Review the diff in ~/code/foo and report findings." --cwd ~/code/foo --model sonnet ``` **`--model` is mandatory and there is no default.** Omit it and `parlay spawn` refuses with exit 2 and *`refusing to spawn — no model was chosen`*: the launching session's model is never inherited and there is no silent sonnet fallback. Three things satisfy the gate — `--model ` (what the example does), a `--profile ` that carries a model ([`packages/spawn-profiles`](packages/spawn-profiles)), or `--no-pii`, which auto-routes to a free model. `parlay spawn --list` renders the profile catalog. One thing to know before your first spawn: for the default `claude` harness the launcher starts it with `--dangerously-skip-permissions` (plus a `--strict-mcp-config` and a sonnet fallback), deliberately — a phone-driven agent cannot answer a permission prompt. Every other harness gets only its explicit `--model` and uses its own permission config. Details in [`docs/launcher.md`](docs/launcher.md). `parlay spawn` is the sole entry point for spawning, and the only one there is: the launcher runs in-process (`tools/cli/internal/spawn`). The bash spawner and its `PARLAY_SPAWN_IMPL=bash` escape hatch are gone (task-42qot), so the mandatory-model gate (task-21d36) cannot be routed around. To reach it from your phone, expose the host — Tailscale, LAN IP, or a private tunnel — and export `PARLAY_SERVER` as that address instead of `localhost`. > **One honest caveat about context recovery.** A spawned agent that exhausts its > context is supposed to recover through an `identity → handoff → scratchpad` chain. > Two of those three legs are yours: `parlay identity` and `parlay scratchpad` are > plain files under `~/.parlay/agents//` and work on any clone. The middle leg, > `handoff`, is **not a `parlay` verb** — it is a beads-store wrapper from the > author's own federation tooling (the same family as `task`/`inbox`), and this repo > neither ships nor installs it. So the flags that lean on it — > `identity --submit`, `--park`, `--complete`, and `parlay drawdown`'s closing > recipe — either need an id passed explicitly (`parlay identity --submit `, > which works with no store installed) or are simply skipped. Every command involved > detects this and says so rather than pointing you at a command you do not have. **The chat API is unauthenticated by design** (that is how the CLI and plain `curl` work — see the origin guard in `packages/go-server/internal/guard`), so anything that can reach the port can post into a live agent's turn. Expose it only over a private network — a tailnet, a VPN, or a LAN you control — never a public tunnel or a port forwarded to the internet. ### When a step fails, or something that worked stops working Run step 0 again. `./bin/parlay-preflight` is the prerequisite surface for a newcomer and for an operator, and it is deliberately the same command in both roles — there is no second checklist to find. It answers whether the toolchain is present and new enough, whether the state directories and the CLI build directory are writable, whether the port the CLI talks to is free, and — if something is already listening there — whether it is parlay (`GET /health` answers with `"ok":true`) or a stranger holding the port. Each `FAIL` prints the exact command that fixes it. If the machine checks out and you still cannot tell which layer is broken, step 1's `./examples/bootstrap-sandbox.sh` exercises the whole stack against a throwaway instance and names the layer that fails. Two other verbs answer questions this one deliberately does not, both only once a server exists: - `./bin/parlay health` — the running instance's own vitals on the server your CLI is pointed at: subscribers, pollers, registered agents, server memory and history size; then the optional voice engine. It exits 1 on a fresh clone because the voice engine is not installed — that line is the engine, not your install, and the output says so. - `./bin/parlay doctor deploy` — the machine-level sweep for a host running the launchd services: service inventory, binary-path sanity, log freshness, pin-vs-doc consistency. On a fresh clone it has nothing to inventory and says so, and it exits 1 on the same not-installed voice engine `health` reports. (`./bin/parlay doctor` with no subcommand is a *different* thing: it is one agent's self-diagnosis — is `PARLAY_AGENT_ID` set, is that agent registered and listening, are its identity files readable. Run from a plain shell it fails on the first question by design. It is for an agent inside a spawned turn, not for you.) Everything else that has already bitten someone is in [`AGENTS.md`](AGENTS.md), the incident record, and the per-fact notes it links to under [`docs/agent-notes/`](docs/agent-notes/) — both ordered by when the incident happened. [`docs/traps.md`](docs/traps.md) is that same material ordered by when *you* will hit it, which is the better first read and the better one to search. ## Fleet layer and `parlay-dev` The core product (`packages/*`, `tools/cli`, `bin/parlay`, the server) installs and runs as above. Separate from that is a **fleet layer** — the inbox tooling, the pi-inbox bridge, and the agent skills that the author's personal fleet runs. It is personal glue, deliberately kept out of `tools/` and `skills/` of the core repo; it lives instead under `examples/fleet/`. **Install the fleet layer** with one command (idempotent, reversible): ```sh examples/fleet/install.sh # install everything (or --status / --uninstall) ``` What it wires up: | Thing | Home | How | |---|---|---| | `inbox`, `inbox-dispatch` | `~/.local/bin/` | backup-then-copy via each tool's own `install.sh` | | pi-inbox bridge | `~/.pi/agent/extensions/parlay-pi-inbox/` | copied dir (so `./src` imports resolve) | | fleet skills (`inbox-handler`, `parlay-spawn`, `voice-command-consulting`) | `~/.claude/skills/` | symlink to `examples/fleet/skills/` (edits apply immediately) | **`parlay` vs `parlay-dev`** — one wrapper script (`bin/parlay`), two names: - `parlay` — production/fleet mode. State in `~/.parlay`, talks to the running server. - `parlay-dev` — development mode. Same checkout, but `PARLAY_STATE_HOME` is redirected to `~/.parlay-dev` so building/testing `main` never touches live fleet state (`config.json`, identity, scratchpads, spawn defaults). Both are symlinks to `bin/parlay`; the invoked basename selects. `parlay-dev` isolates **client-side** state only — it still reaches the same server (`:4242` by default). For a fully isolated dev environment, also point `PARLAY_SERVER` at a scratch server, since agent registration is server-side. **Fresh-checkout note:** `bin/parlay-dev` is a tracked symlink in the repo, so any checkout has it automatically; `~/.local/bin/parlay` and `~/.local/bin/parlay-dev` on a live host are created by the install step (see the wrapper header / install docs). ## Layout A [Bun](https://bun.sh) workspace monorepo for the client packages, plus standalone Go modules for the server and CLI. This table is a newcomer's map of the parts you need first, not a complete index of every module in the repo: | Package | What it is | |---|---| | `packages/go-server` | The Go server that owns `/api/chat/*`: chat history, SSE, the long-poll feed the relay consumes, the server-side-eval relay, upload/link handling, drafts/settings. Runs standalone on `:4242` (`cd packages/go-server && go run ./cmd/parlay-server`). The contract it implements lives in [`docs/api-contract.md`](docs/api-contract.md). | | `tools/relay` | The standalone per-agent relay daemon — its own Go module, built by `tools/relay/build.sh`. Fans the server's `/api/chat/poll` feed out to enrolled agents; `parlay monitor`/`listen` need it unless you pass `--legacy-poll`. | | `packages/client` | The chat panel — tabs, presence, message rendering, TTS/speech playback, annotations. Built as a browser bundle with `cd packages/client && bun run build`; the Go server serves it same-origin from `-assets-dir` (`packages/client/dist`), so it needs no separate host or proxy. | | `tools/cli` | The Go `parlay` command surface — `reply`/`say`, `monitor`, `identity`/`scratchpad`, `alert`, `doctor`/`health`, `shutdown`, and more. Also embeds the compiled Go (RE2) eval-engine — the voice layer that matches spoken/typed phrases to a closed set of panel actions — as `parlay eval serve` (`internal/evalengine`). `bin/parlay` builds and execs this binary. | | `packages/input` | `parlay-input` — a self-contained, framework-agnostic DOM input wrapper for wiring your own UI input to a parlay server. The one publishable npm package; no dependencies. | | `packages/sandbox` | The **sandbox test site** — a react/vite page that previews what a string would do under each of your configurations (real eval engine, delivery removed), fires the real action on an explicit click, and carries the command log with its per-row off switch. Runs with `cd packages/sandbox && bun run dev`; no new endpoints. See [`packages/sandbox/README.md`](packages/sandbox/README.md) and [`docs/action-log.md`](docs/action-log.md). | | `examples/fleet` | The author's personal **fleet layer** — inbox dispatcher/emit, pi-inbox bridge, and the agent skills. Not core product; installs via `examples/fleet/install.sh`. | Agent-facing entry points live in `bin/` (`parlay`, `parlay-treehouse-guard`, …). ## System map Every load-bearing part of parlay, verified against the current code (2026-09-03), with a link to a deeper-dive doc. This is not a diagram of an idealized architecture — it shows a real, in-transition system, including the parts that are only half-wired today (see [`command-server.md`](docs/command-server.md) and [`monitor.md`](docs/monitor.md) for the specifics). ```mermaid flowchart LR subgraph client_side["Client side"] input["Input\n(packages/input)"] panel["Panel\n(packages/client)"] end server["Command/chat server — :4242\npackages/go-server (Go)"] hist["Events / history JSONL\nmessages.jsonl"] registry["Agent registry & presence\nagents.json"] relay["Relay\ntools/relay — per-agent spool fan-out"] monitor["Monitor / listen\ntools/cli/internal/monitor"] launcher["Launcher\ntools/cli/internal/spawn"] agent["A spawned agent process\n(herdr terminal)"] input -- "POST edits, evaluated by\nthe Go eval engine" --> server panel -- "SSE + REST" --> server server --> hist server --> registry server -- "/api/chat/poll" --> relay relay -- "spool file, tail -F" --> monitor monitor --> agent launcher -- "spawns + registers" --> agent launcher -- "register-agent, hello" --> registry agent -- "reply/say" --> server ``` | Part | What it does | Deep dive | |---|---|---| | **Input** | DOM wrapper that turns edits in a composer element into evaluated phrase-engine actions. | [`docs/input.md`](docs/input.md) | | **Command/chat server** | Owns `/api/chat/*` — a single Go implementation (`packages/go-server`), the sole server; the TS server it replaced was deleted with the Bun→Go cutover. | [`docs/command-server.md`](docs/command-server.md) | | **Events / history (JSONL)** | The one append-only chat-history file in the server's state dir (`messages.jsonl`), and the out-of-process hook/tool producers that post into it over HTTP. | [`docs/events-history.md`](docs/events-history.md) | | **Agent registry & presence** | Who is enrolled as a chat tab, and transient (in-memory-only) connection counts. | [`docs/agent-registry.md`](docs/agent-registry.md) | | **Monitor / listen** | How an enrolled agent actually receives messages — relay-backed by default, `--legacy-poll` as a no-relay fallback with a documented dead-tab gap. | [`docs/monitor.md`](docs/monitor.md) | | **Launcher (spawn)** | Launches a new background agent into a live chat tab — one in-process implementation (`tools/cli/internal/spawn`), so the model and beads gates cannot be routed around. | [`docs/launcher.md`](docs/launcher.md) | | **Relay** | Single fan-out daemon between the server's long-poll feed and every enrolled agent's monitor; a per-runtime-dir singleton, not built by default. | [`docs/relay.md`](docs/relay.md) | | **Live-command registry** | A separate registry from agent enrollment — tracks running `parlay` CLI invocations for `parlay commands` and the panel's live-commands view. | [`docs/live-commands.md`](docs/live-commands.md) | | **CLI** | The `parlay` Go command surface and the embedded voice/phrase eval engine. | [`tools/cli`](tools/cli) — start with `parlay help`, then `parlay --help`. The authoring doc ([`docs/CLI_VERBS_AND_EVENTS.md`](docs/CLI_VERBS_AND_EVENTS.md)) is TS-era design, not the live surface. | | **Panel** | The browser chat UI — tabs, presence, TTS, annotations. A gitignored bundle (`packages/client/dist`) that the Go server serves same-origin from `-assets-dir`, so it is the server's own host rather than a separate one. | [`packages/client`](packages/client) | ## A worked config [`examples/`](examples/) is a complete two-agent setup — every file a configured parlay actually needs, with notes on what to change. `examples/bootstrap-sandbox.sh` instantiates it in a throwaway sandbox and exercises it, leaving your own files and your running server alone — read its limits in [`examples/`](examples/) before you run it. ### Talk demo rehearsal `bin/parlay-demo-rehearsal` is the one-command pre-talk check for `/demo` (server up, page served, iframe headers, a scripted fake-agent exchange, PASS/FAIL with a fix per step). See [`docs/talk-demo.md`](docs/talk-demo.md). ## Development ```sh cd packages/go-server && go test ./... # the Go server cd packages/client && bun test # a TS client package, from inside it — see note below cd tools/cli && CGO_ENABLED=0 go test ./... # the Go CLI — see the cgo note below ``` The `CGO_ENABLED=0` on the CLI line is required, not decoration. `tools/cli`'s beads dependency carries an embedded Dolt tree whose ICU binding needs C++ headers that a stock macOS toolchain does not ship, so the same command with cgo on fails to build with `fatal error: 'unicode/regex.h' file not found` — for `go test`, for `go build`, and for the plain `go build .` that `bin/parlay` runs (which is why the wrapper pins the flag itself). Nothing in the CLI needs cgo. On Linux, or with a full Xcode/ICU toolchain installed, the flag is harmless either way. Every committed `deploy/install.sh` carries it for the same reason, and `tools/cli/deploy_build_flags_gate_test.go` fails the build if one regresses. There is no root `bunfig.toml`, so `bun test` at the repo root does not load the happy-dom preload some client packages need: DOM-touching suites fail there with `ReferenceError: document is not defined` even though they pass in-package — always run a suite from inside its own package. CI (`.github/workflows/ci.yml`) runs on every pull request and on pushes to `main`, and does exactly that for the Go modules, the Bun client packages, and the hermetic shell harnesses. Repo conventions worth knowing: - **`bun install` installs this repo's git hooks.** The root `package.json` `prepare` script runs `git config core.hooksPath tools/hooks`. `pre-commit` runs for everyone: it enforces the 250-line limit below and auto-bumps `PA_VERSION`. `post-commit`/`post-merge` rebuild and deliver the panel bundle — a build plus a POST to the local server — and **do nothing at all unless you opt in**: ```sh git config --bool parlay.autobuild true # enable; omit or set false to stay off ``` That setting lands in the clone's shared `.git/config`, so **one `git config` covers every linked worktree of that clone** — it does not matter which checkout you run it from, and you never run it per worktree. Opted out, the skip is not silent: on a commit that would otherwise have delivered, the hook prints one line saying delivery was skipped and one giving the command above. Enabling is not delivering — two different things. Opted in, the hooks still deliver only from the repo's primary checkout; a commit in a linked worktree logs a skip and delivers nothing. Set `PARLAY_MAIN_CHECKOUT` to point them at a different tree. To drop all the hooks including `pre-commit`: `git config --unset core.hooksPath` — but note that escape hatch is not durable, because the `prepare` script above re-runs `git config core.hooksPath tools/hooks` on the next `bun install` and silently puts them back. - **250-line file limit** (pre-commit) — split a module into a subfolder + barrel index past the limit. - **Two version axes** — the repo release `vX.Y.Z` git tag and the panel build `PA_VERSION` (`packages/client/src/version.ts`, auto-bumped per client change). - **Build the panel bundle with `cd packages/client && bun run build`** (that runs `build.ts`, which writes `dist/parlay-agent.js`). On success it also POSTs a best-effort reload beacon to `$PARLAY_RELOAD_TARGET` (default `127.0.0.1:4242`); if nothing is listening there it just logs that and moves on. If you *are* running a live server on that port, the build will force-reload its connected clients — use `bun test` or a scoped `bun build src/.ts --outdir=` when you only want to validate a change. Docs of note: [`docs/api-contract.md`](docs/api-contract.md) (the HTTP contract between client, CLI, and server), [`docs/COMMAND_DESIGN_CONTRACT.md`](docs/COMMAND_DESIGN_CONTRACT.md) (the voice engine), and [`docs/CLI_VERBS_AND_EVENTS.md`](docs/CLI_VERBS_AND_EVENTS.md) (CLI verb authoring — sound mechanics, but written against the retired TS CLI; `tools/cli` is the live surface). Several docs in `docs/` describe integration with the author's own agent fleet, some of it public and some not — [`docs/README.md`](docs/README.md) says which is which. ## Publishing **The `parlay` package name on npm is taken by an unrelated third party**, so no package in this repo can be published under the bare name. That single fact drives every naming decision here, and it is not a style preference: - **Publishable packages use flat, unscoped `parlay-` names** — `parlay-input`, and any future `parlay-`. Only `packages/input` is public today. - **The `@parlay` scope is never published.** It has no packages under it, and adding some would mean claiming a scope this project does not own. If you are tempted to "fix" a package name by moving it under `@parlay/…`, don't — that is the one direction that is closed. If you hit an install that resolves to something unexpected, check the version you actually got. `parlay-input` on the npm registry is a **0.1.0 alias stub** that points at the unpublished `@parlay/input`; the real implementation here is **0.2.0** and has not been published. Consumers who need the working package today vendor `dist/` from this repo — see `packages/input/README.md`. ## Contributing This is an alpha, single-owner project moving fast, so there's no formal contribution process yet. Issues and PRs are welcome, but expect interfaces to shift under you. ## License [MIT](LICENSE) © Trillium Smith