@enderfga/claw-orchestrator 7.14.0 → 7.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/README.md +160 -135
  2. package/configs/autoloop-reviewer-prompt.md +1 -1
  3. package/configs/engines/README.md +31 -11
  4. package/dist/bin/acp-server.js +7 -12
  5. package/dist/bin/acp-server.js.map +1 -1
  6. package/dist/bin/cli.js +71 -40
  7. package/dist/bin/cli.js.map +1 -1
  8. package/dist/bin/mcp-server.js +2 -54
  9. package/dist/bin/mcp-server.js.map +1 -1
  10. package/dist/src/acp-server.d.ts +17 -9
  11. package/dist/src/acp-server.js +149 -30
  12. package/dist/src/acp-server.js.map +1 -1
  13. package/dist/src/autoloop/agent-tools.d.ts +2 -0
  14. package/dist/src/autoloop/agent-tools.js +7 -0
  15. package/dist/src/autoloop/agent-tools.js.map +1 -1
  16. package/dist/src/autoloop/dispatcher.d.ts +6 -0
  17. package/dist/src/autoloop/dispatcher.js +23 -1
  18. package/dist/src/autoloop/dispatcher.js.map +1 -1
  19. package/dist/src/autoloop/planner-tools.js +2 -0
  20. package/dist/src/autoloop/planner-tools.js.map +1 -1
  21. package/dist/src/council.d.ts +8 -0
  22. package/dist/src/council.js +45 -12
  23. package/dist/src/council.js.map +1 -1
  24. package/dist/src/dashboard/index.html +28 -14
  25. package/dist/src/embedded-server.js +27 -8
  26. package/dist/src/embedded-server.js.map +1 -1
  27. package/dist/src/engine-spawn.js +3 -1
  28. package/dist/src/engine-spawn.js.map +1 -1
  29. package/dist/src/fanout.d.ts +13 -3
  30. package/dist/src/fanout.js +10 -0
  31. package/dist/src/fanout.js.map +1 -1
  32. package/dist/src/inbox-manager.d.ts +9 -0
  33. package/dist/src/inbox-manager.js +8 -2
  34. package/dist/src/inbox-manager.js.map +1 -1
  35. package/dist/src/index.d.ts +2 -0
  36. package/dist/src/index.js +117 -51
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/kernel/conditions.d.ts +1 -1
  39. package/dist/src/kernel/conditions.js +1 -1
  40. package/dist/src/kernel/engine.d.ts +28 -8
  41. package/dist/src/kernel/engine.js +93 -19
  42. package/dist/src/kernel/engine.js.map +1 -1
  43. package/dist/src/kernel/nodes/agent.js +2 -0
  44. package/dist/src/kernel/nodes/agent.js.map +1 -1
  45. package/dist/src/kernel/nodes/council.js +9 -4
  46. package/dist/src/kernel/nodes/council.js.map +1 -1
  47. package/dist/src/kernel/nodes/fanout.js +5 -0
  48. package/dist/src/kernel/nodes/fanout.js.map +1 -1
  49. package/dist/src/kernel/nodes/goal.d.ts +8 -1
  50. package/dist/src/kernel/nodes/goal.js +40 -19
  51. package/dist/src/kernel/nodes/goal.js.map +1 -1
  52. package/dist/src/kernel/nodes/subflow.js +9 -1
  53. package/dist/src/kernel/nodes/subflow.js.map +1 -1
  54. package/dist/src/kernel/nodes/ultraapp.d.ts +1 -0
  55. package/dist/src/kernel/nodes/ultraapp.js +1 -0
  56. package/dist/src/kernel/nodes/ultraapp.js.map +1 -1
  57. package/dist/src/kernel/nodes/verifier.js +5 -1
  58. package/dist/src/kernel/nodes/verifier.js.map +1 -1
  59. package/dist/src/kernel/projections.d.ts +1 -0
  60. package/dist/src/kernel/projections.js +7 -1
  61. package/dist/src/kernel/projections.js.map +1 -1
  62. package/dist/src/kernel/templates/index.d.ts +6 -3
  63. package/dist/src/kernel/templates/index.js +8 -3
  64. package/dist/src/kernel/templates/index.js.map +1 -1
  65. package/dist/src/kernel/types.d.ts +30 -10
  66. package/dist/src/kernel/types.js.map +1 -1
  67. package/dist/src/logger.d.ts +6 -0
  68. package/dist/src/logger.js +14 -0
  69. package/dist/src/logger.js.map +1 -1
  70. package/dist/src/mcp-annotations.d.ts +13 -0
  71. package/dist/src/mcp-annotations.js +100 -0
  72. package/dist/src/mcp-annotations.js.map +1 -0
  73. package/dist/src/models.d.ts +7 -0
  74. package/dist/src/models.js +9 -0
  75. package/dist/src/models.js.map +1 -1
  76. package/dist/src/openai-compat.d.ts +11 -5
  77. package/dist/src/openai-compat.js +55 -54
  78. package/dist/src/openai-compat.js.map +1 -1
  79. package/dist/src/persistent-acp-session.d.ts +7 -0
  80. package/dist/src/persistent-acp-session.js +26 -2
  81. package/dist/src/persistent-acp-session.js.map +1 -1
  82. package/dist/src/persistent-codex-app-session.d.ts +2 -0
  83. package/dist/src/persistent-codex-app-session.js +18 -9
  84. package/dist/src/persistent-codex-app-session.js.map +1 -1
  85. package/dist/src/persistent-codex-session.d.ts +14 -3
  86. package/dist/src/persistent-codex-session.js +22 -19
  87. package/dist/src/persistent-codex-session.js.map +1 -1
  88. package/dist/src/persistent-custom-session.d.ts +9 -0
  89. package/dist/src/persistent-custom-session.js +41 -21
  90. package/dist/src/persistent-custom-session.js.map +1 -1
  91. package/dist/src/persistent-grok-session.js +1 -1
  92. package/dist/src/persistent-grok-session.js.map +1 -1
  93. package/dist/src/run-ledger.d.ts +3 -3
  94. package/dist/src/session-manager.d.ts +34 -1
  95. package/dist/src/session-manager.js +160 -30
  96. package/dist/src/session-manager.js.map +1 -1
  97. package/dist/src/types.d.ts +34 -11
  98. package/dist/src/types.js.map +1 -1
  99. package/dist/src/ultraapp/build.d.ts +5 -11
  100. package/dist/src/ultraapp/build.js +5 -11
  101. package/dist/src/ultraapp/build.js.map +1 -1
  102. package/dist/src/ultraapp/council-adapter.d.ts +5 -2
  103. package/dist/src/ultraapp/council-adapter.js +6 -6
  104. package/dist/src/ultraapp/council-adapter.js.map +1 -1
  105. package/dist/src/ultraapp/files.d.ts +7 -0
  106. package/dist/src/ultraapp/files.js +7 -1
  107. package/dist/src/ultraapp/files.js.map +1 -1
  108. package/dist/src/ultraapp/manager.d.ts +23 -3
  109. package/dist/src/ultraapp/manager.js +170 -72
  110. package/dist/src/ultraapp/manager.js.map +1 -1
  111. package/dist/src/ultraapp/narrator.js +1 -1
  112. package/dist/src/ultraapp/narrator.js.map +1 -1
  113. package/dist/src/ultraapp/versions.d.ts +27 -5
  114. package/dist/src/ultraapp/versions.js +58 -13
  115. package/dist/src/ultraapp/versions.js.map +1 -1
  116. package/openclaw.plugin.json +4 -13
  117. package/package.json +13 -6
  118. package/skills/SKILL.md +52 -24
  119. package/skills/references/acp.md +140 -61
  120. package/skills/references/autoloop.md +141 -35
  121. package/skills/references/claude-cli-tracking.md +8 -6
  122. package/skills/references/cli.md +122 -93
  123. package/skills/references/council.md +44 -22
  124. package/skills/references/dashboard.md +37 -21
  125. package/skills/references/getting-started.md +49 -15
  126. package/skills/references/inbox.md +13 -8
  127. package/skills/references/mcp.md +25 -17
  128. package/skills/references/multi-engine.md +161 -145
  129. package/skills/references/observability.md +90 -58
  130. package/skills/references/openai-compat.md +172 -82
  131. package/skills/references/sessions.md +101 -65
  132. package/skills/references/tools.md +209 -158
  133. package/skills/references/ultra.md +7 -7
  134. package/skills/references/ultraapp.md +101 -56
  135. package/skills/references/verification.md +112 -56
  136. package/skills/references/workflow.md +227 -93
  137. package/assets/banner.jpg +0 -0
@@ -16,24 +16,44 @@ SessionManager
16
16
  │ └── Wraps: agy -p (Google Antigravity CLI, per-message spawning, stream-json output)
17
17
  ├── engine: 'grok' → PersistentGrokSession
18
18
  │ └── Wraps: grok -p --output-format json (xAI Grok Build, per-message spawning)
19
- ├── engine: 'cursor' → PersistentCursorSession (legacy)
20
- │ └── Wraps: cursor-agent -p --trust --output-format stream-json (per-message spawning)
21
19
  ├── engine: 'opencode' → PersistentOpencodeSession
22
20
  │ └── Wraps: opencode run --format json, message on stdin (per-message spawning)
21
+ ├── engine: 'acp' → PersistentAcpSession
22
+ │ └── Wraps: any Agent Client Protocol agent on stdio (long-running, one ACP session)
23
23
  └── engine: 'custom' → PersistentCustomSession
24
24
  └── Wraps: any CLI via user-provided CustomEngineConfig
25
25
  ```
26
26
 
27
+ The `gemini` and `cursor` engines are legacy: still wired for existing callers, not documented
28
+ as options (see the notes under [Antigravity](#google-antigravity-engine-agy) and
29
+ [Grok Build](#grok-build-engine-grok)).
30
+
31
+ ### At a glance
32
+
33
+ | Engine | Process | How the conversation carries over | Where cost comes from |
34
+ | ----------- | ----------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------- |
35
+ | `claude` | one long-running `claude -p` | the live process; `--resume <id>` when a session is restarted | the CLI's own `total_cost_usd` (registry estimate in proxy mode) |
36
+ | `codex` | one `codex exec` per send | `codex exec resume <thread_id>` | registry price × reported tokens |
37
+ | `codex-app` | one long-running `codex app-server` | the live thread; `thread/resume` when a session is restarted | registry price × reported tokens |
38
+ | `agy` | one `agy -p` per send | `--conversation <id>` | registry price × reported tokens |
39
+ | `grok` | one `grok -p` per send | `--resume <sessionId>` | grok's own `total_cost_usd` |
40
+ | `opencode` | one `opencode run` per send | `--session <id>` | opencode's own `step_finish` cost (registry estimate when none) |
41
+ | `acp` | one long-running agent process | one ACP session; `session/resume` or `session/load` on restart | what the agent reports (estimated from `pricing` when it reports none) |
42
+ | `custom` | persistent, or one process per send | persistent: the live process (`args.resume` on restart); one-shot: none | registry price × reported tokens (see `pricing` below) |
43
+
27
44
  ## Supported Engines
28
45
 
29
46
  ### Claude Code (`engine: 'claude'`)
30
47
 
31
- Default engine. Long-running subprocess with streaming JSON I/O. Tested with Claude Code CLI **2.1.307**.
48
+ Default engine. Long-running subprocess with streaming JSON I/O. Tested with Claude Code CLI **2.1.310**.
32
49
 
33
50
  - Persistent multi-turn conversations
34
51
  - Real-time streaming (text, tool_use, tool_result, system events)
35
52
  - Session resume via `--resume`
36
- - Full cost tracking from API usage data
53
+ - Cost is the CLI's own `total_cost_usd`, which prices cache writes. It is a running total for the
54
+ process, so spend advances by the difference between reports. The registry estimate is used
55
+ instead in proxy mode (the CLI is told it runs `opus`) and for the first turn of a process started
56
+ with `--resume`, whose first report includes the spend of the turns it resumed
37
57
  - Cross-session peer messaging (`crossSessionInbound`): this session's policy for messages from
38
58
  other Claude Code sessions on the same machine — `accept` delivers them, `hold` waits for approval
39
59
  in that session's terminal, `refuse` rejects them. It is a settings key, passed through
@@ -68,18 +88,16 @@ Wraps the `codex exec` subcommand. Each `send()` spawns a new process. Tested wi
68
88
  - Real `usage` from the `turn.completed` JSON event (input, output, cached, reasoning tokens). **These are cumulative over the thread, not per turn**, so they replace the session totals rather than being added to them; subtracting consecutive values gives one turn's prompt
69
89
  - `contextPercent` is the per-turn prompt measured against **codex's own context limit** (`model_context_window`, read from the thread's rollout file), not the model's published window in the registry. Resuming a thread also seeds the token baseline from the rollout, so the first send does not count the whole thread history as one prompt. This is best-effort: an unreadable or `--ephemeral` thread falls back to the registry window
70
90
  - `item.completed` parsing distinguishes `reasoning` / `todo_list` (logged, not counted) from real tool items (`command_execution`, `file_change`, `mcp_tool_call`, `web_search`, which increment `toolCalls`; a non-zero `command_execution.exit_code` increments `toolErrors`)
71
- - Reasoning effort: the engine-agnostic `effort` maps to `-c model_reasoning_effort=<level>` for `low|medium|high|xhigh|max|ultra`; `auto` and `ultracode` pass nothing. `-c` values are not validated at spawn, so an unknown level fails at the API rather than at the command line
91
+ - Reasoning effort: the engine-agnostic `effort` maps to `-c model_reasoning_effort=<level>` for `low|medium|high|xhigh|max|ultra`; `auto` passes nothing. Only those levels are passed, because codex does not validate `-c` values at spawn: an unlisted one would fail at the API rather than at the command line
72
92
  - `appendSystemPrompt`: codex has no system-prompt flag, so it is placed at the top of the first message of each new conversation (a resumed thread already carries it)
73
93
  - `jsonSchema` → `--output-schema <file>` (written to a temp file, accepted by `exec` and `exec resume`)
74
94
  - `noSessionPersistence` → `--ephemeral` (accepted by `exec` and `exec resume`); `ignoreUserConfig` → `--ignore-user-config`, which stops `$CODEX_HOME/config.toml` from choosing the model for an orchestrated run (auth still resolves from `CODEX_HOME`); `addDir` → `--add-dir` on the first turn only, since `exec resume` rejects it and the resumed thread keeps the roots it opened with
75
95
  - `codexProfile` → `--profile <name>` (named config profile from `~/.codex/config.toml`), first turn only
76
96
  - `--worktree` is not passed. With it, codex writes the turn's edits to `~/.codex/worktrees/<hash>/<repo>` instead of the session's `cwd`, while acceptance contracts, evidence diffs and the baseline change set all read the session's `cwd`. Council's per-agent git worktrees cover the isolation use case
77
- - Per-session continuity: the `thread_id` from the first turn's `thread.started` event is captured and reused via `codex exec resume <id>` for subsequent sends, so the model sees prior turns
78
- - `sandboxMode` maps to `--sandbox <mode>` on the first turn. A resumed thread does not keep it, and `codex exec resume` rejects `--sandbox`, so the policy is restated as `-c sandbox_mode="<mode>"` on every resumed turn
79
- - One-shot execution per message (no persistent subprocess between sends)
80
- - Captures the real Codex thread ID and persists it, so later sends and process-level session resume use `codex exec resume <thread_id>`
97
+ - Continuity: the `thread_id` from the first turn's `thread.started` event is captured and reused via `codex exec resume <id>` for later sends, so the model sees prior turns. The id is persisted too, so a restarted session (and `resumeSessionId`) resumes the same thread
98
+ - `sandboxMode` maps to `--sandbox <mode>` on the first turn (default `workspace-write`). A resumed thread does not keep it, and `codex exec resume` rejects `--sandbox`, so the policy is restated as `-c sandbox_mode="<mode>"` on every resumed turn
81
99
  - Working directory passed via `-C` on the first turn
82
- - Model: with no `model`, codex runs its own default — the `model` in `~/.codex/config.toml` if set. Cost is then priced as the model codex reports it ran (the rollout's `turn_context`; `thread/start` on `codex-app`), and as `gpt-5.5` only when that cannot be read
100
+ - Model: with no `model`, codex runs its own default — the `model` in `~/.codex/config.toml` if set. Cost is then priced as the model codex reports it ran (the rollout's `turn_context`; `thread/start` on `codex-app`), and as `gpt-5.5` only when that cannot be read. Cached reads are inside codex's `input_tokens`, so they are taken out and priced at the cached rate
83
101
  - Requires `codex` CLI >= 0.119 (for `exec resume`): `npm install -g @openai/codex`
84
102
  - The prompt is sent on stdin (`codex exec … -`), never on the command line.
85
103
  - **Windows:** the npm `.cmd` shim launches through `src/engine-spawn.ts` (cross-spawn), which escapes the remaining arguments for `cmd.exe`. Because the prompt is on stdin it arrives unchanged, newlines and quotes included. A timed-out turn ends the whole process tree (`taskkill /T /F`), not only `cmd.exe`.
@@ -109,11 +127,14 @@ Wraps `codex app-server --listen stdio:// --enable goals` as a long-running JSON
109
127
  - Real-time streaming via `item/agentMessage/delta` notifications
110
128
  - Cumulative token tracking from `thread/tokenUsage/updated` notifications. The same notification's `last` breakdown and `modelContextWindow` drive `contextPercent`, so it reports live occupancy against the window the server enforces rather than a running total over the model's published window
111
129
  - `appendSystemPrompt` is placed at the top of the first message of a new thread (app-server has no system-prompt flag); a resumed thread already carries it
130
+ - Reasoning effort: the session `effort` opens or resumes the thread with `config: { model_reasoning_effort }`, so the goal loop's own turns use it too, and each `turn/start` carries `effort` (the per-send override, else the session's), so `setEffort` reaches the next turn. The levels are the same as `codex`'s; `auto` passes nothing, and codex keeps the last effort it was sent for the turns after it
112
131
  - Goals through the typed RPCs `thread/goal/set` / `get` / `clear`: setting an objective starts the server's own turn loop, which runs until the goal is `complete`, `budgetLimited`, `blocked`, `usageLimited` or `paused`. Each change arrives as `thread/goal/updated`. The `codex_goal_*` tools wrap these; the kernel's `goal` node drives them for unattended runs ([workflow.md](./workflow.md#goal))
113
132
  - `error` notifications are read as `{ error: { message, codexErrorInfo }, willRetry }`. The session keeps the last one (`lastError`, with `codexErrorInfo` as a name such as `serverOverloaded`) and emits it as `codex:error`; an error notification never throws when nothing listens for errors
114
133
  - v2 RPC tools (Codex 0.137): `codex_interrupt` (`turn/interrupt`), `codex_steer` (`turn/steer`, including into a turn the goal loop started), `codex_fork` (`thread/fork`), `codex_rollback` (`thread/rollback`), `codex_models` (`model/list`), `codex_thread_list` (`thread/list`). A `turn/completed` with `status: 'failed'` rejects the turn and increments `toolErrors`.
115
134
  - Threads are opened and resumed with `approvalPolicy: "never"`, and a server request the session cannot handle (an approval, a request for user input) gets an error reply at once: nothing here can answer one, and an unanswered request blocks its turn. The sandbox mode is the boundary.
116
- - Thread resume: starting with `resumeSessionId` loads the existing thread via `thread/resume` instead of `thread/start`. The id the session manager stores and replays (model switch, restart) is the app-server's own thread id. A thread resumed with an active goal continues it on its own.
135
+ - Thread resume: starting with `resumeSessionId` loads the existing thread via `thread/resume` instead of `thread/start`. The id the session manager stores and replays (model switch, restart) is the app-server's own thread id. A thread resumed with an active goal continues it on its own. An id that `thread/resume` rejects falls back to a new thread (logged); the `goal` node refuses that case rather than start the goal over.
136
+ - `compact` asks the app-server to compact the thread (`thread/compact/start`).
137
+ - `clawo goal "<objective>" --check "npm test"` runs the `goal` workflow from the command line: one goal on one thread, then the runtime's own checks, then repair on the same thread ([cli.md](./cli.md#clawo-goal)).
117
138
 
118
139
  ```typescript
119
140
  await manager.startSession({
@@ -127,22 +148,11 @@ await manager.codexGoal('codex-goal-task', { action: 'set', objective: 'build a
127
148
  // await tool('codex_goal_set', { name: 'codex-goal-task', objective: 'build a tic-tac-toe app' });
128
149
  ```
129
150
 
130
- ### Running an engine under a launcher
131
-
132
- `SessionConfig.launcher` is an argv prefix the engine binary runs under, for every
133
- engine: `launcher: ['/opt/task/sandbox.sh', 'task', '0 1']` spawns
134
- `/opt/task/sandbox.sh task '0 1' codex app-server …`. This is how a caller puts
135
- the agent inside its own sandbox (bubblewrap, a container entry point, `taskset`)
136
- without a custom engine. The launcher is the caller's own command, at the same
137
- trust level as a contract's command check, which the HTTP API also accepts: set
138
- it through the SessionManager API, `/session/start` or a workflow spec (the `goal`
139
- node's `launcher`). It is not a parameter of the `session_start` tool.
140
-
141
151
  ### Google Antigravity (`engine: 'agy'`)
142
152
 
143
153
  Wraps Google's **Antigravity CLI** (`agy`) — the successor to Gemini CLI (consumer
144
154
  Gemini CLI tiers stopped serving 2026-06-18). Each `send()` spawns a new process
145
- in print mode. Tested with `agy` **1.4.4**.
155
+ in print mode, with the message as the `-p` argument. Tested with `agy` **1.4.6**.
146
156
 
147
157
  - One-shot execution per message (no persistent subprocess)
148
158
  - **Structured output and real usage** — `--output-format stream-json` emits an
@@ -178,8 +188,9 @@ in print mode. Tested with `agy` **1.4.4**.
178
188
  passes the requested effort through rather than substituting a tier the caller
179
189
  did not ask for; run `agy models` to see the tiers a slug actually exposes.
180
190
 
181
- - Permission modes: `bypassPermissions` → `--dangerously-skip-permissions`,
182
- `default` and `manual` → `--sandbox` (terminal-restricted), and
191
+ - Permission modes: `bypassPermissions` (or `dangerouslySkipPermissions`) →
192
+ `--dangerously-skip-permissions`, `default` and `manual` → `--sandbox`
193
+ (terminal-restricted), and
183
194
  `sandboxMode: 'read-only'` → `--mode plan` (takes precedence). Other modes
184
195
  run agy's own approval flow, which can block in headless print mode. A caller
185
196
  must explicitly choose `bypassPermissions` for a write-enabled session; it is
@@ -202,7 +213,8 @@ in print mode. Tested with `agy` **1.4.4**.
202
213
  pass through unregistered. The `agy/` prefix forces Antigravity routing for
203
214
  provider-like model strings.
204
215
  - Consumer auth is a one-time `agy` Google OAuth login (subscription quotas, no
205
- per-token billing — registry pricing mirrors Gemini API rates as a value proxy)
216
+ per-token billing — registry pricing mirrors Gemini API rates as a value proxy).
217
+ An effort-qualified slug such as `gemini-3.1-pro-high` is priced as its base model
206
218
  - Requires `agy` installed: `curl -fsSL https://antigravity.google/cli/install.sh | bash`
207
219
  - Binary: `agy` (set `AGY_BIN` env var to override)
208
220
 
@@ -218,16 +230,17 @@ await manager.startSession({
218
230
 
219
231
  > **Legacy: `engine: 'gemini'`.** Google sunset the consumer Gemini CLI (tiers stopped
220
232
  > serving 2026-06-18) in favour of Antigravity. The `gemini` engine still exists and
221
- > still works — existing callers are not broken, and `gemini-*` model strings outside
222
- > agy's registered slugs still route to it — but it is no longer a documented option,
223
- > is not version-tracked, and gets no new work. Use `agy` for Google. (Unrelated: the
224
- > multi-model **proxy** still talks to the Gemini **API**; that is a different
225
- > subsystem and is unaffected.)
233
+ > still works for existing callers, but it is no longer a documented option, is not
234
+ > version-tracked, and gets no new work. Use `agy` for Google. The OpenAI-compatible
235
+ > endpoint and the ACP agent pick the engine from the model name, and there `gemini-*`
236
+ > strings outside agy's registered slugs (including the `gemini-pro` and `gemini-flash`
237
+ > aliases) still route to the `gemini` engine. (Unrelated: the multi-model **proxy**
238
+ > still talks to the Gemini **API**; that is a different subsystem and is unaffected.)
226
239
 
227
240
  ### Grok Build (`engine: 'grok'`)
228
241
 
229
242
  Wraps xAI's **Grok Build** CLI. Each `send()` spawns `grok -p <msg> --output-format json`, which
230
- prints a single JSON object and exits. Tested with `grok` **1.0.67**.
243
+ prints a single JSON object and exits. Tested with `grok` **1.0.70**.
231
244
 
232
245
  - **Cost comes from the engine, not from the price table.** The result object carries
233
246
  `total_cost_usd`, and the wrapper writes it straight into the session's spend, so the run ledger
@@ -238,19 +251,21 @@ prints a single JSON object and exits. Tested with `grok` **1.0.67**.
238
251
  - **Real conversation continuity**: the `sessionId` from turn 1 is replayed as `--resume <id>`.
239
252
  `--continue` is not used — it means "the most recent session for this cwd", which collides
240
253
  between concurrent sessions.
241
- - Real token counts from `usage` (`input_tokens`, `output_tokens`, `cache_read_input_tokens`).
242
- These are **per-turn**, unlike codex, where the same field is cumulative.
254
+ - Real token counts from `usage` (`input_tokens`, `output_tokens`, `cache_read_input_tokens`,
255
+ `cache_creation_input_tokens`). These are **per-turn**, unlike codex, where the same field is
256
+ cumulative, and `input_tokens` excludes the cached reads.
243
257
  - Permission modes pass straight through: grok's `--permission-mode` takes the same vocabulary we
244
258
  use. The one exception is our `manual`, which grok spells `default`.
245
259
  - Reasoning effort maps to `--effort`; grok accepts `low|medium|high|xhigh`, so `max` and `ultra`
246
260
  clamp to `xhigh`.
247
- - Session options that reach grok: `appendSystemPrompt` → `--rules` (appends, unlike
248
- `systemPrompt` → `--system-prompt-override`, which replaces), `allowedTools` → `--tools`,
249
- `disallowedTools` → `--disallowed-tools`, `jsonSchema` → `--json-schema` (inline, and it implies the
250
- JSON output format already asked for), `agent` → `--agent`, `agents` → `--agents`,
251
- `dangerouslySkipPermissions` → `--always-approve`, `customSessionId` → `--session-id`, `forkSession`
252
- → `--fork-session`. **grok validates neither tool list**: a name that does not exist is ignored
253
- rather than rejected, so a typo in a denylist leaves the tool enabled. Prefer an allowlist.
261
+ - Session options that reach grok: `cwd` → `--cwd`, `appendSystemPrompt` → `--rules` (appends,
262
+ unlike `systemPrompt` → `--system-prompt-override`, which replaces), `maxTurns` → `--max-turns`,
263
+ `allowedTools` → `--tools`, `disallowedTools` → `--disallowed-tools`, `jsonSchema` →
264
+ `--json-schema` (inline, and it implies the JSON output format already asked for), `agent` →
265
+ `--agent`, `agents` → `--agents`, `dangerouslySkipPermissions` → `--always-approve`,
266
+ `forkSession` → `--fork-session` (on a resumed turn), `customSessionId` → `--session-id` (on a
267
+ first turn, or a forked one). **grok validates neither tool list**: a name that does not exist is
268
+ ignored rather than rejected, so a typo in a denylist leaves the tool enabled. Prefer an allowlist.
254
269
  - **`sandboxMode: 'read-only'` runs inside grok's `--sandbox read-only` profile**, which the OS
255
270
  enforces (Seatbelt on macOS, Landlock on Linux) for the whole process tree: the session reads
256
271
  anywhere and can write only to `~/.grok/` and the temp directory. Measured on 1.0.50 with
@@ -279,43 +294,49 @@ await manager.startSession({
279
294
  });
280
295
  ```
281
296
 
282
- ### Cursor Agent (`engine: 'cursor'`) — legacy
297
+ > **Legacy: `engine: 'cursor'`.** Superseded in this lineup by Grok Build. The `cursor` engine
298
+ > still exists and still works for existing callers — it runs `cursor-agent` (`CURSOR_BIN`
299
+ > overrides it, never the bare `agent` name) and resumes a chat with `--resume <chatId>` — but it
300
+ > is no longer a documented option, is not version-tracked, and gets no new work. It left the
301
+ > tracked set because its stream does not say which model ran, so its cost cannot be attributed
302
+ > to a model. On the OpenAI-compatible endpoint and the ACP agent,
303
+ > `composer-*` and `auto` model names still route to it.
283
304
 
284
- > **Legacy: `engine: 'cursor'`.** Superseded in this lineup by Grok Build (`engine: 'grok'`).
285
- > The `cursor` engine still exists and still works — existing callers are not broken — but it is
286
- > no longer a documented option, is not version-tracked, and gets no new work.
287
- >
288
- > Cursor itself is still maintained. It left the tracked set because it does not report which
289
- > model ran (its `system` init event says `"model": "Auto"`), so costs cannot be attributed to a
290
- > model, and because xAI's Grok installer also claims the bare `agent` binary name.
305
+ ### OpenCode (`engine: 'opencode'`)
291
306
 
292
- Wraps the Cursor Agent CLI with `-p --output-format stream-json`. Write-enabled sessions use `--force`. Each `send()` spawns a new process.
307
+ Wraps the [OpenCode](https://github.com/anomalyco/opencode) CLI with `run --format json`. Each `send()` spawns a new process. Tested with `opencode` **1.19.4**.
293
308
 
294
- - Conversation continuity: the chat id from the first turn's `system` event is captured and passed back as `--resume <chatId>` on later sends, so the model sees prior turns. `--continue` is deliberately not used: it resumes "the latest chat", which collides between concurrent sessions.
295
- - One-shot execution per message (no persistent subprocess)
296
- - Working directory via `--workspace` flag
297
- - Real token counts from stream-json `result` events (camelCase: `inputTokens`, `outputTokens`, `cacheReadTokens`)
298
- - `--force` enables auto-approval of file changes. `sandboxMode: 'read-only'` does **not** use `--force`:
299
- - read-only is enforced by a `.cursor/cli.json` deny config (`Write`/`Edit`/`Shell` denied), written into an isolated temp dir used as the process cwd, with `--workspace` pointing at the real project (the repo tree is never modified)
300
- - `--mode plan` is passed as well, but it only steers the model; the deny config is the boundary
301
- - `--sandbox` is not added, since it does not restrict in-workspace writes and overrides the mode
302
- - read/grep/search remain available
303
- - `--trust` auto-trusts the workspace without prompting
304
- - Cursor uses its own model routing (e.g., `sonnet-4`, `gpt-5`, `auto`)
305
- - Requires Cursor Agent CLI: `curl https://cursor.com/install -fsSL | bash`
306
- - Binary: `cursor-agent` (set `CURSOR_BIN` env var to override). The generic `agent` name is
307
- deliberately not used: xAI's Grok installer symlinks `agent` to its own binary, which rejects
308
- `--force`/`--trust`/`--workspace` and fails the turn with "unexpected argument"
309
+ - Conversation continuity: the session id from the event envelope is captured and passed back as `--session <id>` on later sends, so the model sees prior turns. `--continue` is deliberately not used: it means "the last session on this machine", which collides between concurrent sessions.
310
+ - NDJSON event stream with envelope `{ type, timestamp, sessionID, ... }`
311
+ - Event types: `text`, `reasoning`, `tool_use`, `step_start`, `step_finish`, `error`
312
+ - `text` and `tool_use` are **cumulative snapshots** keyed by `part.id` / `part.callID`; the wrapper diffs them to produce streaming deltas for `onText` callbacks and counts each tool invocation once
313
+ - Real token counts from `step_finish.part.tokens.{input,output,cache.{read,write}}`. **`input` is the uncached remainder only** — opencode's own `total` is `input + output + cache.read + cache.write` — so the cached part is billed on top of it, not carved out of it, and `contextPercent` is measured against the whole input side. On a resumed turn the uncached remainder is usually a small fraction of the cached part
314
+ - Cost is what opencode reports on each `step_finish` (`part.cost`), so a free model costs $0; the registry estimate applies only when no cost is reported. `opencode run --format json` does not name the model it ran
315
+ - Reasoning effort maps to `--variant`, opencode's provider-specific effort knob. opencode does not validate the value: a level the provider does not offer runs the turn at its default rather than failing
316
+ - `appendSystemPrompt`: opencode has no system-prompt flag, so it is placed at the top of the first message of each new conversation
317
+ - The message is written to the subprocess's stdin, which is then closed: opencode waits for EOF before it starts
318
+ - Provider-agnostic: opencode's `--model` expects `provider/model` form (e.g. `anthropic/claude-sonnet-4`). The wrapper passes `--model` through only when the value contains a `/`; otherwise opencode's own default applies
319
+ - `sandboxMode: 'read-only'` runs a generated `clawo-readonly` agent (`--agent clawo-readonly` plus an `OPENCODE_CONFIG_CONTENT` env var defining it):
320
+ - it denies `edit`, `bash`, `external_directory`, `webfetch` and `task` at the permission level, and also removes those tools via the agent's `tools` map
321
+ - denying `task` matters: otherwise the agent can hand a write to a subagent that runs under the default writable agent
322
+ - OpenCode's built-in `plan` agent is not used, because it permits both `bash` and `edit`
323
+ - if the `clawo-readonly` agent fails to load, the turn is refused rather than run with write access
324
+ - to test a change to this config, use adversarial prompts that include asking the agent to delegate; `opencode agent list` shows compiled rules that look the same for a safe and an unsafe agent
325
+ - Requires OpenCode 1.x: `npm install -g opencode-ai` or `brew install anomalyco/tap/opencode`. Auth via `opencode auth login` **or** any provider env var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, etc.) — opencode picks up either path
326
+ - **OpenCode 2 is refused at session start.** Its `run --format json` keeps the event envelope but drops `step_finish` (no tokens or cost) and the `--variant` flag, and the read-only agent could not be verified on it (measured on the `0.0.0-beta-17823` build). The session reads `opencode --version` once and refuses a 2.x or `0.0.0-beta` build with that reason; if the version cannot be read, it starts as before. OpenCode 2 is what `opencode upgrade --major`, the `@opencode/cli` npm package and the `opencode-v2` Homebrew formula install.
327
+ - Binary: `opencode` (set `OPENCODE_BIN` env var to override). **Windows:** the npm `.cmd` shim launches through `src/engine-spawn.ts` (cross-spawn), as for codex above.
309
328
 
310
329
  ```typescript
311
330
  await manager.startSession({
312
- name: 'cursor-task',
313
- engine: 'cursor',
314
- model: 'sonnet-4',
331
+ name: 'opencode-task',
332
+ engine: 'opencode',
333
+ model: 'anthropic/claude-sonnet-4',
315
334
  cwd: '/project',
316
335
  });
317
336
  ```
318
337
 
338
+ > **Schema stability:** opencode releases nearly daily and the JSON event schema is not formally documented. The parser tolerates unknown event types and missing fields — but pin a tested `opencode` version in CI if you depend on field names.
339
+
319
340
  ### ACP agents (`engine: 'acp'`)
320
341
 
321
342
  Drives any agent that serves the [Agent Client Protocol](https://agentclientprotocol.com) on stdio, with no per-agent code. Run through it so far: Kimi Code 2.3.1 (`kimi acp`), DeepSeek's dsh 0.2.1-rc.2 (`dsh --profile acp`) and OpenCode 1.19.4 (`opencode acp`). This is the client side of the protocol; [acp.md](./acp.md) is the other side, where the orchestrator is the agent.
@@ -326,12 +347,16 @@ Drives any agent that serves the [Agent Client Protocol](https://agentclientprot
326
347
  - `session/request_permission` is answered at once from `permissionMode`: `bypassPermissions`, `acceptEdits`, `auto` and `dontAsk` pick an allow option, the others a reject option. Nothing waits on a person.
327
348
  - Resume: the agent's session id is the resume id (`stats.acpSessionId`). A resumed session reattaches it with `session/resume`, or `session/load` when that is all the agent offers; an agent with neither fails the start rather than opening a new conversation.
328
349
  - Usage and cost are whatever the agent reports (`PromptResponse.usage`, `usage_update`). With `pricing` set and no reported cost, the cost is estimated from the reported tokens. Kimi Code 2.3.1 and dsh 0.2.1-rc.2 report neither, so their sessions show 0 tokens and $0. OpenCode reports tokens.
329
- - The model and credentials are the agent's own configuration, set in its environment:
350
+ - `sandboxMode: 'read-only'` is refused at start. The agent's tools run inside the agent process and this client sees only the permission requests the agent chooses to send, so nothing here can stop a write. `permissionMode: 'plan'` rejects every permission request the agent does send.
351
+ - The model and credentials are the agent's own configuration, set in its environment; the session's `model` and `effort` are not sent to the agent, and `compact` is left to it:
330
352
  - Kimi Code: `KIMI_MODEL_NAME`, `KIMI_MODEL_API_KEY`, `KIMI_MODEL_BASE_URL`, `KIMI_MODEL_MAX_CONTEXT_SIZE`, `KIMI_MODEL_CAPABILITIES`, or a `kimi` login. `KIMI_BIN` overrides the command.
331
353
  - dsh: `DEEPSEEK_API_KEY` (and `DEEPSEEK_BASE_URL`); its profile chooses the model. `DSH_BIN` overrides the command.
354
+ - ACP has no system-prompt channel, so `systemPrompt` and `appendSystemPrompt` go at the top of the first message of a new session (a council seat's charter travels this way); a resumed session already holds them.
332
355
  - `launcher` applies as for every engine.
356
+ - Where an ACP agent runs besides `session_start`: each fan-out and council agent, a workflow `agent` node, `solve`'s implementer and scouts, and a fan-out's synthesis pass (`synthesisAcpAgent`) name it by preset id in `acpAgent`; `session_handoff` takes `acpAgent` as `session_start` does. On the CLI an agent in a list is written `acp:<preset>`. Autoloop roles and ultrareview reviewers refuse `acp`: a role cannot name the agent, and a reviewer must be read-only.
333
357
 
334
358
  ```bash
359
+ clawo fanout "How would you cut this test suite's run time?" -e claude,acp:kimi-code,acp:dsh --synthesize --wait
335
360
  clawo session-start kimi -e acp --acp-agent kimi-code --permission-mode bypassPermissions -d ./project
336
361
  clawo session-send kimi "Fix the failing test in tests/test_io.py"
337
362
  ```
@@ -339,8 +364,13 @@ clawo session-send kimi "Fix the failing test in tests/test_io.py"
339
364
  ### Community engine presets
340
365
 
341
366
  `customEngine` accepts either an inline `CustomEngineConfig` or the id of a preset
342
- bundled in `configs/engines/`. The preset form exists so a third-party CLI is
343
- described once and shipped, rather than retyped by every caller.
367
+ bundled in `configs/engines/`, and `acpAgent` does the same for the `acp` engine. The
368
+ preset form exists so a third-party CLI is described once and shipped, rather than
369
+ retyped by every caller. A preset carries exactly one of an `engine` block (started
370
+ with `engine: 'custom'`) or an `acp` block (started with `engine: 'acp'`); naming one
371
+ with the other engine fails with a message that says which to use. Bundled today:
372
+ `zcode` (custom), `kimi-code` and `dsh` (ACP). The format is in
373
+ [configs/engines/README.md](../../configs/engines/README.md).
344
374
 
345
375
  Engines fall into three tiers, decided by how they are verified. Bundled presets
346
376
  are always `community`:
@@ -353,57 +383,40 @@ are always `community`:
353
383
 
354
384
  A community preset must name its maintainer and the engine version and date it
355
385
  was verified against (`provenance.maintainer`, `provenance.verifiedAgainst`,
356
- `provenance.verifiedOn`), with a link to the captured smoke-test transcript.
386
+ `provenance.verifiedOn`); a link to the captured smoke-test transcript
387
+ (`provenance.smokeUrl`) is expected in review but not enforced by the schema check.
357
388
 
358
389
  A preset never carries protocol translation. A CLI speaking its own wire format
359
390
  needs an adapter binary in its author's package, with the preset pointing `bin`
360
- at it — as `@enderfga/dsh-clawo` does.
361
-
362
- Over HTTP — which includes the `clawo` CLI — a custom engine may be given **only**
363
- as a preset id. An inline config names a binary and its arguments and is refused
364
- there; a preset id cannot introduce either, so it is the same class as naming a
365
- built-in engine, and `engine: 'codex'` already spawns an executable over that
366
- surface. Presets are therefore the supported way to reach a custom engine from
367
- the CLI: `clawo engines` lists what is bundled, and
368
- `clawo session-start <name> -e custom --custom-engine <id>` starts one.
391
+ at it — as the `zcode` preset does with `zcode-claw-adapter`. An agent that serves
392
+ ACP needs no adapter: its preset names the command.
393
+
394
+ Over HTTP — which includes the `clawo` CLI — a custom engine or an ACP agent may be
395
+ given **only** as a preset id. An inline config names a binary and its arguments and
396
+ is refused there; a preset id cannot introduce either, so it is the same class as
397
+ naming a built-in engine, and `engine: 'codex'` already spawns an executable over
398
+ that surface. Presets are therefore the supported way to reach either from the CLI:
399
+ `clawo engines` lists what is bundled, and
400
+ `clawo session-start <name> -e custom --custom-engine <id>` (or
401
+ `-e acp --acp-agent <id>` for an ACP preset) starts one.
369
402
 
370
403
  Contribution steps and the smoke script: [CONTRIBUTING.md](../../CONTRIBUTING.md#contributing-an-engine).
371
404
 
372
- ### OpenCode (`engine: 'opencode'`)
373
-
374
- Wraps the [sst/opencode](https://github.com/sst/opencode) CLI with `run --format json`. Each `send()` spawns a new process.
375
-
376
- - Conversation continuity: the session id from the event envelope is captured and passed back as `--session <id>` on later sends, so the model sees prior turns. `--continue` is deliberately not used: it means "the last session on this machine", which collides between concurrent sessions.
377
- - One-shot execution per message (no persistent subprocess)
378
- - NDJSON event stream with envelope `{ type, timestamp, sessionID, ... }`
379
- - Event types: `text`, `reasoning`, `tool_use`, `step_start`, `step_finish`, `error`
380
- - `text` and `tool_use` are **cumulative snapshots** keyed by `part.id` / `part.callID`; the wrapper diffs them to produce streaming deltas for `onText` callbacks and counts each tool invocation once
381
- - Real token counts from `step_finish.part.tokens.{input,output,cache.{read,write}}`. **`input` is the uncached remainder only** — opencode's own `total` is `input + output + cache.read + cache.write` — so the cached part is billed on top of it, not carved out of it, and `contextPercent` is measured against the whole input side. On a resumed turn the uncached remainder is usually a small fraction of the cached part
382
- - Cost is what opencode reports on each `step_finish` (`part.cost`), so a free model costs $0; the registry estimate applies only when no cost is reported. `opencode run --format json` does not name the model it ran
383
- - Reasoning effort maps to `--variant`, opencode's provider-specific effort knob. opencode does not validate the value: a level the provider does not offer runs the turn at its default rather than failing
384
- - `appendSystemPrompt`: opencode has no system-prompt flag, so it is placed at the top of the first message of each new conversation
385
- - The wrapper closes the subprocess's stdin immediately after spawn (opencode otherwise reads stdin and blocks on EOF, hanging the call)
386
- - Provider-agnostic: opencode's `--model` expects `provider/model` form (e.g. `anthropic/claude-sonnet-4`). The wrapper passes `--model` through only when the value contains a `/`; otherwise opencode's own default applies
387
- - `sandboxMode: 'read-only'` runs a generated `clawo-readonly` agent (`--agent clawo-readonly` plus an `OPENCODE_CONFIG_CONTENT` env var defining it):
388
- - it denies `edit`, `bash`, `external_directory`, `webfetch` and `task` at the permission level, and also removes those tools via the agent's `tools` map
389
- - denying `task` matters: otherwise the agent can hand a write to a subagent that runs under the default writable agent
390
- - OpenCode's built-in `plan` agent is not used, because it permits both `bash` and `edit`
391
- - if the `clawo-readonly` agent fails to load, the turn is refused rather than run with write access
392
- - to test a change to this config, use adversarial prompts that include asking the agent to delegate; `opencode agent list` shows compiled rules that look the same for a safe and an unsafe agent
393
- - Requires opencode installed: `brew install sst/tap/opencode` or `npm install -g opencode-ai`. Auth via `opencode auth login` **or** any provider env var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, etc.) — opencode picks up either path
394
- - **OpenCode 2 is refused at session start.** Its `run --format json` keeps the event envelope but drops `step_finish` (no tokens or cost) and the `--variant` flag, and the read-only agent could not be verified on it (measured on `0.0.0-beta-17823`, the build `opencode upgrade --major` installs as of October 2026). The session reads `opencode --version` once and refuses a 2.x or `0.0.0-beta` build with that reason; if the version cannot be read, it starts as before.
395
- - Binary: `opencode` (set `OPENCODE_BIN` env var to override). The message is sent on stdin, never on the command line. **Windows:** the npm `.cmd` shim launches through `src/engine-spawn.ts` (cross-spawn), as for codex above.
396
-
397
- ```typescript
398
- await manager.startSession({
399
- name: 'opencode-task',
400
- engine: 'opencode',
401
- model: 'anthropic/claude-sonnet-4',
402
- cwd: '/project',
403
- });
404
- ```
405
+ ### Running an engine under a launcher
405
406
 
406
- > **Schema stability:** opencode releases nearly daily and the JSON event schema is not formally documented. The parser tolerates unknown event types and missing fields — but pin a tested `opencode` version in CI if you depend on field names.
407
+ `SessionConfig.launcher` is an argv prefix the engine binary runs under, for every
408
+ engine: `launcher: ['/opt/task/sandbox.sh', 'task', '0 1']` spawns
409
+ `/opt/task/sandbox.sh task '0 1' codex app-server …`. This is how a caller puts
410
+ the agent inside its own sandbox (bubblewrap, a container entry point, `taskset`)
411
+ without a custom engine. The launcher is the caller's own command, at the same
412
+ trust level as a contract's command check, which the HTTP API also accepts: set
413
+ it through the SessionManager API, the `session_start` tool, `/session/start` or a
414
+ workflow spec (the `goal` node's `launcher`, which `workflow_start` and
415
+ `clawo goal --launcher` also take). On Windows, a launcher that runs through
416
+ `cmd.exe` (a `.cmd` or `.bat`) is held to the same argv rule as an engine shim: an
417
+ argument with a newline or an unbalanced double quote is refused before spawning,
418
+ so an engine that takes its prompt on the command line needs a launcher that is a
419
+ real executable.
407
420
 
408
421
  ## ISession Interface
409
422
 
@@ -411,8 +424,11 @@ All engines implement `ISession`, making them interchangeable at the `SessionMan
411
424
 
412
425
  ```typescript
413
426
  interface ISession {
414
- // State
427
+ // Identity
415
428
  sessionId?: string;
429
+ readonly pid?: number;
430
+
431
+ // State
416
432
  readonly isReady: boolean;
417
433
  readonly isPaused: boolean;
418
434
  readonly isBusy: boolean;
@@ -441,26 +457,19 @@ interface ISession {
441
457
 
442
458
  // Events (EventEmitter)
443
459
  on(event, listener): this;
460
+ once(event, listener): this;
444
461
  emit(event, ...args): boolean;
462
+ removeListener(event, listener): this;
445
463
  }
446
464
  ```
447
465
 
448
466
  `SessionStats` requires `turnsSucceeded` as well as `turns`, so an engine that
449
467
  implements this interface has to say which of its turns succeeded — the exit code
450
- is not the answer on every engine. See "Stats & Monitoring" in `sessions.md`.
468
+ is not the answer on every engine. See [Stats & Monitoring](./sessions.md#stats--monitoring).
451
469
 
452
470
  ## Team Tools Across Engines
453
471
 
454
- Team tools (`team_list`, `team_send`) operate on the same virtual-team layer for **every** engine: the "team" is the set of all active sessions managed by SessionManager.
455
-
456
- | Engine | `team_list` | `team_send` |
457
- | ------------------- | ------------------------------------------ | ------------------------------ |
458
- | Claude | Lists other active SessionManager sessions | Routes via cross-session inbox |
459
- | Codex / `codex-app` | Lists other active SessionManager sessions | Routes via cross-session inbox |
460
- | Antigravity | Lists other active SessionManager sessions | Routes via cross-session inbox |
461
- | Grok | Lists other active SessionManager sessions | Routes via cross-session inbox |
462
- | OpenCode | Lists other active SessionManager sessions | Routes via cross-session inbox |
463
- | Custom | Lists other active SessionManager sessions | Routes via cross-session inbox |
472
+ Team tools (`team_list`, `team_send`) operate on the same virtual-team layer for **every** engine: the "team" is the set of all active sessions managed by SessionManager, whatever engine each one runs. `team_list` lists the other active sessions with their engine and status; `team_send` routes a message through the cross-session inbox.
464
473
 
465
474
  Messages are delivered via the inbox system — idle sessions receive immediately, busy sessions queue for later delivery.
466
475
 
@@ -486,20 +495,20 @@ await manager.startSession({
486
495
 
487
496
  What happens behind the scenes:
488
497
 
489
- 1. Plugin reads `~/.openclaw/openclaw.json` for gateway port + auth
490
- 2. Starts a local proxy server (random port, auto-managed)
498
+ 1. The session manager reads `~/.openclaw/openclaw.json` for the gateway port + auth (in plugin and `clawo serve` mode alike)
499
+ 2. Starts a local proxy server on `127.0.0.1` (random port, auto-managed) the first time a Claude session names a non-Claude model, provided a gateway URL was found
491
500
  3. Claude Code CLI sends Anthropic-format requests → proxy converts to OpenAI → gateway → any model
492
501
 
493
502
  ### Manual Config (optional)
494
503
 
495
504
  Override with environment variables if needed:
496
505
 
497
- | Variable | Default | Description |
498
- | ---------------- | -------------------------------- | --------------------------------------------------- |
499
- | `GATEWAY_URL` | Auto-detected from openclaw.json | Gateway endpoint (e.g. `http://127.0.0.1:18789/v1`) |
500
- | `GATEWAY_KEY` | Auto-detected from openclaw.json | Gateway auth password/token |
501
- | `GEMINI_API_KEY` | - | Direct Gemini API access (bypasses gateway) |
502
- | `OPENAI_API_KEY` | - | Direct OpenAI API access (bypasses gateway) |
506
+ | Variable | Default | Description |
507
+ | ---------------- | -------------------------------- | --------------------------------------------------------- |
508
+ | `GATEWAY_URL` | Auto-detected from openclaw.json | Gateway endpoint (e.g. `http://127.0.0.1:18789/v1`) |
509
+ | `GATEWAY_KEY` | Auto-detected from openclaw.json | Gateway auth password/token |
510
+ | `GEMINI_API_KEY` | - | Direct Gemini API access, used when no gateway key is set |
511
+ | `OPENAI_API_KEY` | - | Direct OpenAI API access, used when no gateway key is set |
503
512
 
504
513
  ### Architecture
505
514
 
@@ -516,8 +525,12 @@ Integrate **any** coding agent CLI without writing engine-specific code. You pro
516
525
 
517
526
  Two protocol modes:
518
527
 
519
- - **Persistent** (`persistent: true`) — long-running subprocess with stream-json I/O over stdin/stdout (like Claude Code)
520
- - **One-shot** (`persistent: false`, default) — new process spawned per `send()` (like Codex/Antigravity)
528
+ - **Persistent** (`persistent: true`) — long-running subprocess with stream-json I/O over stdin/stdout (like Claude Code). It keeps the conversation; `args.resume` passes `resumeSessionId` when a session is restarted
529
+ - **One-shot** (`persistent: false`, default) — new process spawned per `send()`, with the message on the command line right after `args.print`. Nothing carries over between sends. On Windows, a CLI that runs through `cmd.exe` (an npm `.cmd` shim) cannot take a message containing a newline or an unbalanced double quote this way; use persistent mode
530
+
531
+ `appendSystemPrompt` goes through `args.appendSystemPrompt` when the config maps it. Without that flag it is put at the top of the first message of a conversation: every message in one-shot mode, the first one in persistent mode unless the session resumed through `args.resume`.
532
+
533
+ `sandboxMode: 'read-only'` needs both `args.permissionMode` and a `permissionModes` entry for the session's permission mode that maps to the CLI's read-only or plan value. Without them the CLI is never run writable: a persistent session refuses to start, and a one-shot session refuses every send.
521
534
 
522
535
  ### CustomEngineConfig
523
536
 
@@ -529,11 +542,17 @@ Two protocol modes:
529
542
  | `persistent` | boolean | | `true` = persistent subprocess, `false` = one-shot (default) |
530
543
  | `args` | object | yes | CLI flag mappings (see below) |
531
544
  | `permissionModes` | object | | Maps OpenClaw mode names to CLI-specific values |
532
- | `pricing` | object | | `{ input, output, cached? }` per 1M tokens |
545
+ | `pricing` | object | | `{ input, output, cached? }` per 1M tokens (see below) |
533
546
  | `contextWindow` | number | | Context window size (default: 200,000) |
534
547
  | `env` | object | | Extra environment variables for the CLI process |
535
548
  | `sanitizePatterns` | string[] | | Regex patterns to redact from stderr |
536
549
 
550
+ Cost is the reported tokens priced at the registry rate for the session's `model`,
551
+ or at its `pricingOverrides` entry when it has one. An unregistered model, or no
552
+ model, is priced at `pricing`, and at the `claude-sonnet-4-6` rate when the engine
553
+ sets none. With no model, a `pricingOverrides` entry for `claude-sonnet-4-6` still
554
+ takes precedence over `pricing`.
555
+
537
556
  ### args field
538
557
 
539
558
  | Key | Example | Description |
@@ -543,14 +562,14 @@ Two protocol modes:
543
562
  | `outputFormatValue` | `"stream-json"` | Value for stream-json output |
544
563
  | `inputFormat` | `"--input-format"` | Input format flag (persistent only) |
545
564
  | `inputFormatValue` | `"stream-json"` | Value for stream-json input |
546
- | `skipPermissions` | `"-y"` | Skip all permissions flag |
565
+ | `skipPermissions` | `"-y"` | Passed when `dangerouslySkipPermissions` |
547
566
  | `permissionMode` | `"--permission-mode"` | Permission mode flag |
548
567
  | `model` | `"--model"` | Model selection flag |
549
568
  | `systemPrompt` | `"--system-prompt"` | System prompt override flag |
550
569
  | `appendSystemPrompt` | `"--append-system-prompt"` | Append system prompt flag |
551
570
  | `maxTurns` | `"--max-turns"` | Max agent turns flag |
552
571
  | `resume` | `"--resume"` | Session resume flag (persistent only) |
553
- | `verbose` | `"--verbose"` | Verbose output flag |
572
+ | `verbose` | `"--verbose"` | Verbose output flag (persistent only) |
554
573
  | `replayUserMessages` | `"--replay-user-messages"` | Replay user messages (persistent only) |
555
574
  | `includePartialMessages` | `"--include-partial-messages"` | Include partial messages (persistent only) |
556
575
  | `effort` | `"--effort"` | Effort level flag |
@@ -622,10 +641,6 @@ await manager.startSession({
622
641
  });
623
642
  ```
624
643
 
625
- ### Example: Google Antigravity CLI (`agy`)
626
-
627
- Use the built-in [`engine: 'agy'`](#google-antigravity-engine-agy) instead of a custom config.
628
-
629
644
  ### Custom Engine in Council
630
645
 
631
646
  Custom engines work in council by setting `engine: 'custom'` and `customEngine` on the agent persona:
@@ -653,9 +668,10 @@ To add a built-in engine (for CLIs that need custom protocol handling beyond wha
653
668
 
654
669
  1. Create `src/persistent-<engine>-session.ts` implementing `ISession`
655
670
  2. Add the engine name to `ENGINE_TYPES` (from which `EngineType` is derived) and its binary to `ENGINE_BINARY_NAMES` in `src/types.ts`
656
- 3. Add a case to `engineHasNativeConversation()` in `src/types.ts` and, if the engine resumes a conversation by id, to `nativeThreadIsLive()` in `src/openai-compat.ts`
671
+ 3. Add a case to `engineHasNativeConversation()` in `src/types.ts` and, if the engine resumes a conversation by id, to `nativeThreadIsLive()` in `src/openai-compat.ts` and to `_sessionResumeId()` / `_storedResumeId()` in `src/session-manager.ts`, so a restart replays an id the engine can resume rather than a synthetic wrapper id
657
672
  4. Add a case to `SessionManager._createSession()`
658
673
  5. Add model pricing to `MODELS[]` in `src/models.ts`
674
+ 6. For a core engine: add it to `ENGINES` in `scripts/sweep.ts` and pin its tested version in the engine table of `AGENTS.md`
659
675
 
660
676
  The `ISession` interface is deliberately minimal — each engine handles its own subprocess bootstrapping, I/O protocol, and cleanup internally.
661
677