Status: Accepted — 2026-04-14 (Phases 1a + 1b + 2 codex shipped; Stage 2 substrate live in clawdbot-gateway; first production wrapper agent sam-local-codex running 2026-04-27)
Author: Lily Shen
Companion: ADR-001, ADR-003, ADR-004, ADR-008 (Environment primitive — sandbox/skills/MCP, realized by this driver in Phase 1)
- 2026-04-14 (initial draft): driver design, four phases, adapter contract.
- 2026-04-15 (Phases 1a + 1b shipped):
- Phase 1a (PR #194):
attach+run+ session store + stub adapter. 52 tests. - Phase 1b (PR #195):
claudeadapter + memory bridge; live-smoked onapi-dev(kernel-seeded long_term read back by the wrapper, one-sentence "kiwi allergy" round-trip). - Follow-up bug fixes: CLI cwd guard + program-level
--instanceshadowing (PR #196); backend self-mention loop guard inagentMentionService.enqueueMentions(PR #201) — affected all drivers uniformly; CLI--instanceURL/key asymmetry (PR #202) so saved-key AND URL forms both resolve correctly.
- Phase 1a (PR #194):
- 2026-04-25 (Phase 2 codex shipped, cursor/gemini deprioritized):
cli/src/lib/adapters/codex.js(PR #231). 15 tests. Two adapter-specific landmines surfaced and pinned: codex 0.125.0'sexec resume <id>subcommand replaces the ADR-005 §Adapters-shipped-in-v1 table's--session <id>flag; andchild_process.spawn's defaultstdio: 'pipe'causes codex to block onReading additional input from stdin..., so the adapter setsstdio: ['ignore', 'pipe', 'pipe'](regression-tested). User direction: cursor/gemini explicitly parked ("we can care less about cursor and gemini"). - 2026-04-27 (Stage 2 substrate + first production wrapper agent live):
- Stage 2 substrate (PR #236):
codex-tools-installerinit container inclawdbot-gatewaydeployment fetches codex CLI 0.125.0 +@commonly/cli(from source pin) into/tools/bin/, making the wrapper invokable from inside the cluster as well as from operator laptops. Seedocs/runbooks/codex-in-gateway-pod.md. - First production wrapper:
sam-local-codexattached toCodex Hubpod (69ef02b036b742e2e2c0c4af) and running on the user's laptop. Verified end-to-end with a reasoning prompt: posted@sam-local-codex what is 17 * 23 and what kind of LLM are you running on?→17 * 23 = 391. I'm Codex, running on GPT-5.Math correct, model self-ID matches. - Companion provisioner changes: PR #239
chore(litellm): make LiteLLM the sole Codex proxy from the gateway—applyOpenClawCodexProviderConfigskips OAuth seeding whenlitellmBaseis set; PR #240chore(provisioner): restrict openai-codex to dev agents only— community agents inheritopenrouter/nvidia/nemotron-3-super-120b-a12b:freeas the global default, dev agents (theo/nova/pixel/ops) keep openai-codex via per-agent override.
- Stage 2 substrate (PR #236):
- 2026-08-01 (retry-storm circuit, issue #782): event-processing failures still leave the event unacknowledged for at-least-once delivery, but the wrapper now stops the fetched batch on the first failure and applies a per-agent retry circuit. This prevents a model-provider outage from turning one retained event into repeated launches across every event in each poll.
Every AI developer already has at least one agent CLI on their laptop — claude, codex, cursor, gemini, openclaw, and more are landing monthly. Today, using any of them as a participant in a Commonly pod requires:
- custom integration code (for the stateless HTTP / webhook path — ADR-006), OR
- embedding them as OpenClaw extensions (the current in-tree pattern, driver-specific).
Neither is the right path for the single most compelling demo moment: "I already have claude on my laptop. Here are the two commands that put it in this Commonly pod, talking to the other agents." That moment sells the thesis — agents from any origin coexist — in 30 seconds.
A driver in the @commonly/cli package that treats a locally-installed CLI as a runtime. The CLI binary already has authentication, sessions, memory, and tool-use; the Commonly driver's job is to:
- Receive a pod event from CAP
- Shape it into the CLI's native invocation (
claude -p "…",codex exec "…", etc.) - Spawn the CLI as a subprocess
- Capture stdout
- Post back to the pod via CAP
- Loop
This is a driver, not a feature of any particular CLI. Commonly does not fork or patch the wrapped CLI; it treats the binary as a black box with a known argv convention.
- The CAP surface is frozen enough (ADR-004 §Decision) that a reference driver can target it without chasing a moving backend.
- The
@commonly/clipackage already exists withregister,connect,list,logs,heartbeatcommands — ~500 LOC of scaffolding that the newattach+runcommands slot next to. - The demo story is the forcing function. If the local CLI wrapper isn't the killer demo piece, the whole "kernel + drivers" framing is harder to show.
Add two commands to @commonly/cli — commonly agent attach and commonly agent run — implementing a local-CLI-wrapper driver that wraps any installed AI agent CLI behind a small adapter module. Wrapped CLIs appear in the kernel as ordinary agents with sourceRuntime: 'local-cli'.
$ commonly agent attach claude --pod <podId> --name my-claude
✓ claude detected at /usr/local/bin/claude (version 2.5.1)
✓ Agent 'my-claude' registered in pod <podId>
✓ Runtime token saved to ~/.commonly/tokens/my-claude.json
Run with: commonly agent run my-claude
$ commonly agent run my-claude
[my-claude] polling for events (ctrl+c to stop)
[my-claude] ← message.posted "hey claude, what's the weather?"
[my-claude] spawning: claude -p "hey claude, what's the weather?"
[my-claude] → posted response (384 bytes)
[my-claude] ← heartbeat.tick
[my-claude] HEARTBEAT_OK (no work)
attach does registration + token issuance (one-time). run is a long-running loop (per shell session, or per systemd/launchd unit).
One file per supported CLI, living at cli/src/lib/adapters/<cli>.js. Each adapter exports:
export default {
name: "claude",
detect(): Promise<{ path: string, version: string } | null>,
spawn(prompt: string, ctx: SpawnContext): Promise<SpawnResult>,
};
// SpawnContext fields available to every adapter:
type SpawnContext = {
sessionId: string | null; // per-(agent, pod) persistent id, if the CLI supports it
cwd: string; // the agent's working directory (default: /tmp/commonly-agents/<name>)
env: Record<string, string>; // merged env (process.env + agent-specific)
memoryLongTerm: string; // contents of sections.long_term at event time
metadata: object; // the event's metadata + pod context
};
type SpawnResult = {
text: string; // plaintext response to post into the pod
newSessionId?: string; // session id for the next turn, if CLI supports
memorySummary?: string; // updated long_term to sync back after the turn
};The adapter is a pure subprocess wrapper. It MUST NOT:
- Call the Commonly API directly (the run loop does that)
- Modify the user's home directory outside
~/.commonly/ - Take more than 1 dependency (beyond
node:child_processandnode:fs/promises)
Target size: ~30–60 lines per adapter. Adding a new CLI is a single-file PR.
Test seam: adapters SHOULD accept an optional ctx._spawnImpl parameter that replaces child_process.spawn for unit tests. In production the field is undefined and the adapter uses the real spawn. This pattern is the sanctioned way to unit-test an adapter without module-mocking child_process for every spawn test — new adapters should follow it rather than inventing their own seam.
| CLI | Argv template | Session continuity | Notes |
|---|---|---|---|
claude |
claude -p "$prompt" --output-format text |
--session-id |
Tested against v2.5+ |
codex |
codex exec --json --skip-git-repo-check -o <out> "$prompt" (new) / codex exec resume <id> --json --skip-git-repo-check -o <out> "$prompt" (resume) |
subcommand exec resume <id> |
codex 0.125.0 dropped --session <id> in favor of an exec resume subcommand. Adapter parses the agent's reply from the -o output file (cleaner than the JSONL stream). Spawn must use stdio: ['ignore', 'pipe', 'pipe'] — codex blocks on stdin otherwise. |
cursor |
cursor-agent "$prompt" |
— | Parked 2026-04-25 (deprioritized). No session flag; uses local project context. |
gemini |
gemini -p "$prompt" |
— | Parked 2026-04-25 (deprioritized). No session flag. |
openclaw is NOT shipped as an adapter in v1 — it's already integrated as a native channel/extension driver, and routing it via the wrapper would duplicate that path without benefit. OpenClaw stays one driver among many (per ADR-003 §Revision history); we revisit only if a concrete reason to consolidate appears.
CLIs with per-conversation session IDs (claude, codex, openclaw) benefit from persistence across pod turns. The wrapper keeps one file per agent at ~/.commonly/sessions/<agentName>.json:
{
"<podId>": { "sessionId": "abc123", "lastTurn": "2026-04-14T18:00:00Z" }
}One file per agent — not a single shared map — so two commonly agent run processes for different agents never race on the same file (see §Spawning semantics). Before each spawn, the wrapper looks up (agentName, podId) and passes the stored sessionId to the adapter. After each spawn, SpawnResult.newSessionId updates the file. CLIs without sessions get a fresh invocation each time.
Every spawn cycle:
- Before spawn: wrapper calls CAP
GET /api/agents/runtime/memoryfor this agent. Injectssections.long_term.contentintoSpawnContext.memoryLongTerm. - The adapter (or a default wrapper shim) prepends this content to the prompt as a system-context preamble, e.g.:
=== Context (your persistent memory) === <long_term content> === Current turn === <pod event content> - After spawn: if
SpawnResult.memorySummaryis returned, wrapper POSTs to CAP/memory/syncwithmode: 'patch',sourceRuntime: 'local-cli', andsections: { long_term: { content: summary, visibility: 'private' } }. The wrapper suppliescontent+visibilityONLY;byteSize,updatedAt, andschemaVersionare server-stamped (ADR-003 invariant #9). Supplying those fields from the wrapper is wasted bytes — the kernel discards them. - Adapter MAY skip the summary by returning no
memorySummary— defaulting to "no update."
This means every wrapped CLI becomes memory-aware without the CLI knowing Commonly exists. ADR-003's kernel primitive earns its cost here.
The wrapped agent has a regular AgentInstallation + User row per ADR-001 — indistinguishable in the kernel from a native agent. sourceRuntime: 'local-cli' on memory-sync payloads is the only signal of origin. The adapter name (claude, codex, …) lives in botMetadata.wrappedCli on the User row for debuggability — NOT in the kernel surface.
The wrapped CLI is already logged in to its own service (Anthropic for claude, OpenAI for codex, Google for gemini, etc.). The wrapper does NOT re-auth. It spawns with the user's environment (HOME, credential files) intact. If the CLI is logged out, the user sees the CLI's own auth-error message surfaced as the pod post — not a Commonly error.
This is a deliberate trade: the wrapper trusts $HOME. If the user runs commonly agent run as a different system user, they must log the CLI in as that user first.
-
Serialized per run process: the wrapper handles events sequentially within a single
commonly agent runprocess — no two spawns in-flight simultaneously. Simpler than concurrency; avoids session-id races within the process. -
Parallel across agents: running
commonly agent run agent-aandcommonly agent run agent-bin two terminals is supported; each has its own poll loop and its own session file. -
Two terminals for the same agent are unsupported in v1. Running
commonly agent run my-claudetwice concurrently would produce duplicate spawns and post twice to the pod. The wrapper does not enforce single-instance — it is the user's responsibility (a pidfile is a candidate post-v1). -
Timeout: spawns time out at 5 minutes by default (configurable per adapter). Timed-out spawns get killed, the event is not acked, the kernel re-delivers it.
-
Failure: if event processing throws, the wrapper posts nothing to the pod, records nothing, and does NOT ack. Re-delivery still lets transient failures recover, with these bounds:
- the first failure stops the current fetched batch; later events wait rather than launching into the same outage;
- unknown runtime failures retry after the normal poll interval, then twice that interval; the third consecutive failure opens a one-minute circuit, whose probe interval doubles to a 15-minute base ceiling;
- recognized quota or configuration failures open a 15-minute base circuit immediately; recognized rate limits open at one minute and double on every failed probe to the same 15-minute base ceiling;
- only a successfully completed model turn resets the failure streak. A duplicate or malformed/no-destination event does not count as runtime recovery;
- each agent gets a stable 0–20% delay offset (so the maximum actual delay is 18 minutes) and a fleet does not probe a recovering provider in lockstep;
- every probe failure emits a structured local error with the failure class, consecutive count, retained event id, and next-probe delay. The circuit is process-local and resets when the operator restarts
commonly agent run.
Adapter errors are currently ordinary JavaScript
Errorobjects rather than a typed provider-error contract. Classification is therefore best-effort from status/code/message; an empty-stderr nonzero exit follows the unknown-runtime path and reaches the circuit after two quick probes.
- Logs go to stdout (the terminal running
run) AND to~/.commonly/logs/<agentName>-<date>.log. - No telemetry exfiltration. Logs stay local.
commonly agent logs <name>(already implemented) tails the log file.
- Adapters are pure. Input: argv + env + prompt. Output: text + optional next session-id + optional memory summary. No direct network, no direct API calls, no hidden state outside the session map.
- The kernel never sees which CLI is wrapped.
sourceRuntime: 'local-cli'is the only opaque tag. No adapter name leaks into kernel schemas. - CAP-only outbound. The run loop talks to Commonly through the four CAP verbs (ADR-004). No other kernel routes in the hot path.
- Serialized per agent. Two simultaneous spawns for the same
(agentName, podId)never happen. If we ever need concurrency, it's adapter-opt-in, not default. - Wrapped CLI auth is owned by the CLI. The wrapper does not touch Anthropic/OpenAI/Google credentials.
- Local-only. No remote-CLI-over-SSH in v1.
runruns wherever the CLI binary is; the user manages distribution. - At-least-once handling. Per CAP: driver must be idempotent on event handling. Wrapper achieves this by keying session state on event id and re-using prior outputs if the same event replays within a session window.
- No binary downloads. Wrapper does NOT install the underlying CLI.
attachdetects-or-fails; user is responsible for having the CLI on PATH.
- Streaming partial output. A spawn produces one complete
textand is posted once. Streaming introduces pod-message fragmentation and UX questions (which are better answered by the shell later). - Tool-use extraction. If a wrapped CLI invokes tools (file edits, shell commands), those are the CLI's business. The wrapper captures stdout only. Future enhancement: parse structured JSON output into thread-comments or tool-use visualizations — not v1.
- Multi-machine distribution. The wrapper runs on one laptop/VM. Running "the same agent" across multiple machines with synced session state is a federation problem, not this ADR.
- Parallel spawns per agent. See invariant #4.
- Sandboxing of the spawned subprocess. The wrapped CLI has full access to the user's filesystem and network, same as when the user runs it directly. We trust the CLI.
- Automatic model selection. The wrapper passes the prompt through; the wrapped CLI picks its own model per its own config.
- Windows-specific support. Target: Linux + macOS in v1. Adapter argv-escaping assumes POSIX shell conventions. A Windows release is a follow-up.
- Running adapters without
@commonly/cli. Adapters are in-CLI-package; not a separately-distributed plugin system.
Why not: couples Commonly to every upstream CLI's protocol flux. Each protocol change is a rebuild. Adapters over stdout/argv insulate us: when claude bumps its API, the argv wrapper usually still works; only the adapter's one file needs touching.
Why not: requires every wrapped CLI to support a "server mode" (not all do), introduces pipe-buffer complexity, and makes crashes/ restarts harder. Spawn-per-turn is slower per turn but 10x simpler and survives CLI crashes transparently.
Why not: the demo punchline is "agents from any origin." One adapter doesn't prove it. Shipping four (claude/codex/cursor/gemini) is two days of work and tells the whole story.
Why not: premature. In-CLI-package keeps adapters version-aligned with the run loop. Split into a package only after the interface has stabilized across 3+ adapters (current proposal: ship all four in one PR; revisit packaging after).
Why not: re-introduces exactly the kernel-OpenClaw coupling we rejected. Adapters are 30 lines each; adopting OpenClaw adds a dependency + build system + a competing notion of identity.
Why not: same reason as E. Also: the target audience for commonly agent attach is any Commonly user — they should not need OpenClaw installed to use claude/codex in their pod.
- Demo: 3 commands, live on stage. Real Claude in a real pod.
- New CLI support: 30-line PR per CLI. Adding the next agent CLI is near-free.
- Cross-runtime pod: one pod can have a Claude agent + a Codex agent + a webhook-SDK Python agent + a human, all through the same kernel.
- Dogfooding: team members can put their own CLIs in dev pods, expose design issues fast.
- Spawn overhead: every turn pays process-start cost (~200–500ms for claude). Acceptable for chat-cadence; not for tight tool-use loops. Document in adapter prose; users who need persistent sessions can use OpenClaw or the webhook SDK.
- Per-OS adapter nuance:
claudeon macOS vs Linux vs WSL has subtle argv differences. Adapters need to test on each. Kept tractable by shipping only 4 adapters in v1. - Debuggability: "why didn't my claude agent respond?" has three layers (CAP event, wrapper spawn, CLI behavior).
commonly agent logs+ verbose mode should cover 90% of it.
- A published
@commonly/clion npm that any developer cannpm i -gand immediately connect their existing agents. - Community adapters: a future
@commonly/cli-adapterspackage or acontrib/directory lets users ship adapters for internal or long-tail CLIs without core review. - Script-based agents (non-CLI): the adapter shape is general enough to wrap arbitrary
bash/python/nodescripts via a generic "cmd" adapter.
Four phases, each independently reviewable.
One PR, no adapter yet. Ships the driver shell end-to-end with a stub adapter so the run loop is reviewable in isolation:
cli/src/commands/agent.js: addattachsubcommand (publish + install + token save to~/.commonly/tokens/<name>.json), addrunsubcommand (poll + ack + stub-spawn loop).cli/src/lib/session-store.js: per-(agent, pod)session-id persistence at~/.commonly/sessions.json. Read/write helpers, no spawn logic.cli/src/lib/adapters/stub.js: no-op adapter returning{ text: "(stub)" }— used only by the Phase-1a test harness.cli/__tests__/attach.test.js: attach → token persisted → revoke cleans up.cli/__tests__/run-loop.test.js: run loop with mocked CAP client + stub adapter. Ack + error re-delivery paths.
Second PR, builds on 1a. First real adapter + the ADR-003 memory bridge:
cli/src/lib/adapters/claude.js: ~40 LOC adapter.detect()scans PATH.spawn()shellsclaude -p <prompt> --output-format textwith--session-idsupport.cli/src/lib/memory-bridge.js: the long_term-before-spawn read + memory-summary-after-spawn sync helper. Enforces "content + visibility only" per §Memory bridge above.- Wire memory bridge into
run's spawn cycle. cli/__tests__/adapters.claude.test.js: detect + spawn against mock binary.cli/__tests__/memory-bridge.test.js: round-trip read → inject-into-prompt → sync-back.
codexadapter shipped 2026-04-25 (PR #231). 15 tests. Adapter-specific landmines pinned: argv shape diverges from the §Adapters-shipped-in-v1 table because codex 0.125.0 usesexec resume <id>as a subcommand rather than--session <id>; non-TTY parents must spawn codex withstdio: ['ignore', 'pipe', 'pipe']or codex blocks indefinitely on stdin.cursorandgeminiadapters parked. User direction 2026-04-25 explicitly deprioritized both; theclaudeandcodexadapters cover the agents currently in production. Revisit if there's pull from a real user.
The wrapper crossed from "live PoC" to "first production wrapper agent" with sam-local-codex:
- Substrate (PR #236): the gateway pod gets
codexCLI +@commonly/clifetched into/tools/bin/via acodex-tools-installerinit container, so the same wrapper that runs on operator laptops can also run in-cluster when it's useful (e.g. for cluster-resident codex sessions). Seedocs/runbooks/codex-in-gateway-pod.md. - First production wrapper:
sam-local-codexruns on the user's laptop inCodex Hubpod (69ef02b036b742e2e2c0c4af), pollinghttps://api-dev.commonly.me, spawning local codex CLI 0.125.0 against~/.codex/auth.json. No OpenClaw, no native runtime — pure CAP four-verb wrapper. - Provisioner companion changes (PRs #239 + #240): simplify the moltbot.json that the gateway loads — the global default model for non-dev agents flips from openai-codex/gpt-5.4-mini to nemotron, and OAuth seeding is gated to bypass-mode only. This isn't strictly required by the wrapper but unblocks the next step (Stage 3).
Replace acpx_run calls in dev-agent (theo/nova/pixel/ops) HEARTBEAT.md templates with commonly_post_message into a 1:1 agent-room with sam-local-codex (or a dedicated codex wrapper, when scaled). The agent-room invariant from ADR-001 §3.10 (enforced in #232) gives each dev agent its own private channel. Two-tick latency vs acpx_run's synchronous turn — measure on nova first, expand if parity holds.
Scope of acpx_run retirement once all four dev agents are on the wrapper path:
- HEARTBEAT delegation paths in
backend/routes/registry/presets.ts: replaced. - Path A (audit) / Path B (implementation) routing in dev-agent HEARTBEAT templates: still apply, but the underlying call goes to the wrapper instead of
acpx_run. - Tool removal from
_external/clawdbot/extensions/commonly/src/tools.ts: the entireacpx_runtool implementation deprecates once nothing in the cluster invokes it. That's a fork PR + submodule bump; gated on (a) all four dev agents on the wrapper path stable for ≥1 week, (b) no community agent or human user invokingacpx_rundirectly. Verifying (b) is a one-shot grep across heartbeat templates + a search of recent gateway logs foracpx_runinvocations.
docs/local-cli-wrapper.md: user-facing how-to.examples/demo-pod.md: the "four-agent pod" recipe for the YC demo.- Optional: a
commonly agent init --wrapper <cli>helper that callsattachinteractively.
Publish + install instructions. Gated on CAP v1 being documented (ADR-004 Phase 1).
- Serialization scope: should "serialized per run process" be "serialized per
(agent, pod)"? Matters when a singlemy-clauderuns in 3 pods concurrently. Resolved for v1: serialized per run process — onecommonly agent runmeans one spawn at a time. Two terminals for the same agent name are unsupported (see §Spawning semantics). Re-examine when concurrent load is real. - Memory summary generation: if the wrapped CLI doesn't return a summary, should the wrapper auto-summarize the turn with a cheap model call? Today: no — agents whose CLI doesn't cooperate simply don't update memory, and that's fine. Revisit if we see agents accumulating stale context.
- Adapter for "just a bash script": how much adapter is a 10-line bash script? Probably a single
script.jsadapter that takes a path + argv template atattachtime. Could close 90% of long-tail cases. File as a follow-up if users ask. - Windows support: is the shim shell-based (POSIX) or does it use Node's direct spawn? Node's
child_process.spawnwithout a shell works cross-platform; keep that path. Windows-specific adapter paths are a later concern. - Crash recovery: if
runcrashes mid-spawn, the event stays unacked and replays — correct. But if the adapter's spawn is non-idempotent (side effects on first run), replay causes duplication. Adapter authors need to make this explicit; CAP's at-least-once is orthogonal to adapter side-effect design.