@enderfga/claw-orchestrator 7.14.0 → 7.16.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 (145) hide show
  1. package/README.md +162 -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 +84 -46
  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 +72 -9
  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/claude-goal-session.d.ts +147 -0
  22. package/dist/src/claude-goal-session.js +355 -0
  23. package/dist/src/claude-goal-session.js.map +1 -0
  24. package/dist/src/council.d.ts +8 -0
  25. package/dist/src/council.js +45 -12
  26. package/dist/src/council.js.map +1 -1
  27. package/dist/src/dashboard/index.html +28 -14
  28. package/dist/src/embedded-server.js +27 -8
  29. package/dist/src/embedded-server.js.map +1 -1
  30. package/dist/src/engine-spawn.js +3 -1
  31. package/dist/src/engine-spawn.js.map +1 -1
  32. package/dist/src/fanout.d.ts +13 -3
  33. package/dist/src/fanout.js +10 -0
  34. package/dist/src/fanout.js.map +1 -1
  35. package/dist/src/inbox-manager.d.ts +9 -0
  36. package/dist/src/inbox-manager.js +8 -2
  37. package/dist/src/inbox-manager.js.map +1 -1
  38. package/dist/src/index.d.ts +2 -0
  39. package/dist/src/index.js +135 -53
  40. package/dist/src/index.js.map +1 -1
  41. package/dist/src/kernel/agent-step.d.ts +6 -0
  42. package/dist/src/kernel/agent-step.js.map +1 -1
  43. package/dist/src/kernel/conditions.d.ts +1 -1
  44. package/dist/src/kernel/conditions.js +1 -1
  45. package/dist/src/kernel/engine.d.ts +28 -8
  46. package/dist/src/kernel/engine.js +93 -19
  47. package/dist/src/kernel/engine.js.map +1 -1
  48. package/dist/src/kernel/nodes/agent.js +2 -0
  49. package/dist/src/kernel/nodes/agent.js.map +1 -1
  50. package/dist/src/kernel/nodes/council.js +9 -4
  51. package/dist/src/kernel/nodes/council.js.map +1 -1
  52. package/dist/src/kernel/nodes/fanout.js +5 -0
  53. package/dist/src/kernel/nodes/fanout.js.map +1 -1
  54. package/dist/src/kernel/nodes/goal.d.ts +19 -5
  55. package/dist/src/kernel/nodes/goal.js +81 -34
  56. package/dist/src/kernel/nodes/goal.js.map +1 -1
  57. package/dist/src/kernel/nodes/subflow.js +9 -1
  58. package/dist/src/kernel/nodes/subflow.js.map +1 -1
  59. package/dist/src/kernel/nodes/ultraapp.d.ts +1 -0
  60. package/dist/src/kernel/nodes/ultraapp.js +1 -0
  61. package/dist/src/kernel/nodes/ultraapp.js.map +1 -1
  62. package/dist/src/kernel/nodes/verifier.js +5 -1
  63. package/dist/src/kernel/nodes/verifier.js.map +1 -1
  64. package/dist/src/kernel/projections.d.ts +1 -0
  65. package/dist/src/kernel/projections.js +7 -1
  66. package/dist/src/kernel/projections.js.map +1 -1
  67. package/dist/src/kernel/templates/index.d.ts +16 -11
  68. package/dist/src/kernel/templates/index.js +14 -8
  69. package/dist/src/kernel/templates/index.js.map +1 -1
  70. package/dist/src/kernel/types.d.ts +37 -13
  71. package/dist/src/kernel/types.js.map +1 -1
  72. package/dist/src/logger.d.ts +6 -0
  73. package/dist/src/logger.js +14 -0
  74. package/dist/src/logger.js.map +1 -1
  75. package/dist/src/mcp-annotations.d.ts +13 -0
  76. package/dist/src/mcp-annotations.js +100 -0
  77. package/dist/src/mcp-annotations.js.map +1 -0
  78. package/dist/src/models.d.ts +7 -0
  79. package/dist/src/models.js +9 -0
  80. package/dist/src/models.js.map +1 -1
  81. package/dist/src/openai-compat.d.ts +11 -5
  82. package/dist/src/openai-compat.js +55 -54
  83. package/dist/src/openai-compat.js.map +1 -1
  84. package/dist/src/persistent-acp-session.d.ts +7 -0
  85. package/dist/src/persistent-acp-session.js +26 -2
  86. package/dist/src/persistent-acp-session.js.map +1 -1
  87. package/dist/src/persistent-codex-app-session.d.ts +2 -0
  88. package/dist/src/persistent-codex-app-session.js +18 -9
  89. package/dist/src/persistent-codex-app-session.js.map +1 -1
  90. package/dist/src/persistent-codex-session.d.ts +14 -3
  91. package/dist/src/persistent-codex-session.js +22 -19
  92. package/dist/src/persistent-codex-session.js.map +1 -1
  93. package/dist/src/persistent-custom-session.d.ts +9 -0
  94. package/dist/src/persistent-custom-session.js +41 -21
  95. package/dist/src/persistent-custom-session.js.map +1 -1
  96. package/dist/src/persistent-grok-session.js +1 -1
  97. package/dist/src/persistent-grok-session.js.map +1 -1
  98. package/dist/src/persistent-session.d.ts +9 -0
  99. package/dist/src/persistent-session.js +23 -0
  100. package/dist/src/persistent-session.js.map +1 -1
  101. package/dist/src/run-ledger.d.ts +3 -3
  102. package/dist/src/session-manager.d.ts +45 -1
  103. package/dist/src/session-manager.js +199 -34
  104. package/dist/src/session-manager.js.map +1 -1
  105. package/dist/src/types.d.ts +34 -11
  106. package/dist/src/types.js.map +1 -1
  107. package/dist/src/ultraapp/build.d.ts +5 -11
  108. package/dist/src/ultraapp/build.js +5 -11
  109. package/dist/src/ultraapp/build.js.map +1 -1
  110. package/dist/src/ultraapp/council-adapter.d.ts +5 -2
  111. package/dist/src/ultraapp/council-adapter.js +6 -6
  112. package/dist/src/ultraapp/council-adapter.js.map +1 -1
  113. package/dist/src/ultraapp/files.d.ts +7 -0
  114. package/dist/src/ultraapp/files.js +7 -1
  115. package/dist/src/ultraapp/files.js.map +1 -1
  116. package/dist/src/ultraapp/manager.d.ts +23 -3
  117. package/dist/src/ultraapp/manager.js +170 -72
  118. package/dist/src/ultraapp/manager.js.map +1 -1
  119. package/dist/src/ultraapp/narrator.js +1 -1
  120. package/dist/src/ultraapp/narrator.js.map +1 -1
  121. package/dist/src/ultraapp/versions.d.ts +27 -5
  122. package/dist/src/ultraapp/versions.js +58 -13
  123. package/dist/src/ultraapp/versions.js.map +1 -1
  124. package/openclaw.plugin.json +4 -13
  125. package/package.json +13 -6
  126. package/skills/SKILL.md +52 -24
  127. package/skills/references/acp.md +140 -61
  128. package/skills/references/autoloop.md +148 -38
  129. package/skills/references/claude-cli-tracking.md +9 -6
  130. package/skills/references/cli.md +139 -108
  131. package/skills/references/council.md +44 -22
  132. package/skills/references/dashboard.md +37 -21
  133. package/skills/references/getting-started.md +49 -15
  134. package/skills/references/inbox.md +13 -8
  135. package/skills/references/mcp.md +25 -17
  136. package/skills/references/multi-engine.md +161 -145
  137. package/skills/references/observability.md +90 -58
  138. package/skills/references/openai-compat.md +172 -82
  139. package/skills/references/sessions.md +101 -65
  140. package/skills/references/tools.md +217 -158
  141. package/skills/references/ultra.md +7 -7
  142. package/skills/references/ultraapp.md +101 -56
  143. package/skills/references/verification.md +112 -56
  144. package/skills/references/workflow.md +275 -97
  145. package/assets/banner.jpg +0 -0
@@ -1,29 +1,30 @@
1
1
  # Agent Client Protocol (ACP)
2
2
 
3
- Claw Orchestrator can run **as an ACP agent**: a coding agent that any ACP client
4
- drives over JSON-RPC stdio. Zed, JetBrains IDEs, Neovim, Emacs and the VS Code ACP
5
- extension are clients; so is `dsh`, whose `@deepseek-ai/dsh-subagent-acp` provider
6
- spawns an arbitrary command as a subagent.
7
-
8
- The model selector spans several engines, and session modes select the
9
- orchestration shape: a single agent, a council, a planning pass, or a review.
10
-
11
- The other direction, the orchestrator as an ACP **client** driving an agent such
12
- as `kimi acp` or `dsh --profile acp`, is the `acp` engine: see
13
- [multi-engine.md](./multi-engine.md#acp-agents-engine-acp).
3
+ Claw Orchestrator speaks ACP in both directions:
4
+
5
+ - **As an ACP agent** (`clawo acp`): a coding agent that any ACP client drives over
6
+ JSON-RPC stdio. Zed, JetBrains IDEs, Neovim, Emacs and the VS Code ACP extension
7
+ are clients; so is `dsh`, whose `@deepseek-ai/dsh-subagent-acp` provider spawns an
8
+ arbitrary command as a subagent. The model selector spans several engines, and
9
+ session modes select the orchestration shape: a single agent, a council, a
10
+ planning pass, or a review. Most of this page covers this direction.
11
+ - **As an ACP client** (the `acp` engine): the orchestrator drives an agent that
12
+ serves ACP, such as `kimi acp` or `dsh --profile acp`, as one of its own sessions.
13
+ See [The `acp` engine](#the-acp-engine-driving-other-acp-agents) below.
14
14
 
15
15
  ```bash
16
16
  clawo acp # or the dedicated binary:
17
17
  clawo-acp
18
18
  # without a global install:
19
- npx -y @enderfga/claw-orchestrator acp
19
+ npx -y -p @enderfga/claw-orchestrator clawo-acp
20
20
  ```
21
21
 
22
22
  Requires Node ≥ 22 and at least one engine CLI (`claude`, `codex`, `agy`,
23
23
  `grok`) installed and logged in.
24
24
 
25
25
  Both read the protocol from stdin and write it to stdout. **Stdout carries nothing
26
- but protocol frames** — all logging goes to stderr.
26
+ but protocol frames** — all logging goes to stderr. The agent exits when the client
27
+ closes its stdin, stopping the engine processes it started.
27
28
 
28
29
  ## Protocol version
29
30
 
@@ -35,13 +36,25 @@ Implemented: `initialize`, `authenticate`, `session/new`, `session/resume`,
35
36
  `session/list`, `session/prompt`, `session/set_mode`, `session/set_config_option`,
36
37
  `session/cancel`.
37
38
 
39
+ `session/new` requires an absolute `cwd`.
40
+
38
41
  ## Configuration
39
42
 
40
- | Env var | Default | Meaning |
41
- | ---------------------- | ------------------- | ----------------------------------------------------------------------------------------------- |
42
- | `CLAWO_ACP_MODEL` | `claude-sonnet-4-6` | Model (and therefore engine) for a new session |
43
- | `CLAWO_ACP_PERMISSION` | `acceptEdits` | `plan` \| `acceptEdits` \| `bypassPermissions`; an unrecognised value is ignored with a warning |
44
- | `OPENCLAW_LOG_LEVEL` | `info` | Set `warn` to quieten the stderr channel |
43
+ | Env var | Default | Meaning |
44
+ | ----------------------------- | ------------------- | ----------------------------------------------------------------------------------------------- |
45
+ | `CLAWO_ACP_MODEL` | `claude-sonnet-4-6` | Model (and therefore engine) for a new session |
46
+ | `CLAWO_ACP_PERMISSION` | `acceptEdits` | `plan` \| `acceptEdits` \| `bypassPermissions`; an unrecognised value is ignored with a warning |
47
+ | `OPENCLAW_SERVE_MAX_SESSIONS` | `32` | Maximum number of engine sessions running at once |
48
+ | `OPENCLAW_SERVE_TTL_MINUTES` | `60` | Idle minutes before an engine session is stopped |
49
+ | `OPENCLAW_LOG_LEVEL` | `info` | Set `warn` to quieten the stderr channel |
50
+
51
+ ## Prompt content
52
+
53
+ Only `text` and `resource_link` blocks are read from a prompt. `text` blocks are
54
+ concatenated; a `resource_link` becomes a textual `[resource_link name=… uri=…]`
55
+ reference that the engine may open with its own tools; other blocks are dropped
56
+ (`initialize` advertises no image, audio or embedded-context support). A prompt
57
+ that yields no text this way is refused with `invalid params`.
45
58
 
46
59
  ## Session modes
47
60
 
@@ -56,12 +69,15 @@ slash command, advertised through `available_commands_update` when the session o
56
69
 
57
70
  That is the only way to reach a mode in a client that does not draw the picker.
58
71
 
59
- | Mode | What a turn does |
60
- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
61
- | `single` | One engine answers. Text streams as `agent_message_chunk`; tool use becomes `tool_call`. |
62
- | `council` | Several engines debate in isolated git worktrees. Each agent becomes a `tool_call` the client can collapse, each round emits a `plan`, and the synthesis arrives as text. |
63
- | `ultraplan` | Long-horizon planning pass. Result arrives as a `plan` update and as text. |
64
- | `ultrareview` | Parallel reviewers sweep the working tree; findings arrive the same way. |
72
+ | Mode | What a turn does |
73
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
74
+ | `single` | One engine answers. Text streams as `agent_message_chunk`; tool use becomes `tool_call`. The turn ends with a `usage_update` (context used and size, session cost) when the model's context window is known. |
75
+ | `council` | Several engines debate in isolated git worktrees. Each agent becomes a `tool_call` the client can collapse, each round emits a `plan`, and the synthesis arrives as text. |
76
+ | `ultraplan` | Long-horizon planning pass. Result arrives as a `plan` update and as text. |
77
+ | `ultrareview` | Parallel reviewers sweep the working tree; findings arrive the same way. |
78
+
79
+ A `council`, `ultraplan` or `ultrareview` turn gives up after 30 minutes; a council
80
+ is aborted at that point.
65
81
 
66
82
  ### Council
67
83
 
@@ -76,28 +92,37 @@ Treat council as a deliberate action, not a default.
76
92
  Agent deltas are buffered per agent and delivered on that agent's `tool_call_update`
77
93
  rather than streamed into the text channel. Several agents speak at once, and a
78
94
  consumer that only reads text — `dsh`'s ACP subagent reads nothing else — would
79
- otherwise receive them interleaved into one unreadable blob.
80
-
81
- Council requires a git repository at the session `cwd`; it creates one worktree and
82
- branch per agent. Its guardrails (non-git directory, too-short task, and others)
83
- surface as an `invalid params` error naming the reason.
95
+ otherwise receive them interleaved into one unreadable blob. An agent that fails a
96
+ round has its `tool_call_update` marked `failed` with the error; the council carries
97
+ on with the others.
98
+
99
+ Council works in git at the session `cwd`: a directory that is not a repository, or
100
+ has no commits yet, is initialised and given an initial commit of what it holds,
101
+ and each agent gets its own worktree under `.worktrees/` and a `council/<agent>`
102
+ branch. A run that fails before any agent answers — a too-short task, a `cwd`
103
+ inside the orchestrator's own install directory, a worktree that cannot be
104
+ created — ends the prompt with an `invalid params` error naming the reason. A run
105
+ that fails later reports the reason as text and ends with stop reason `refusal`.
84
106
 
85
107
  **Consensus parks the run rather than finishing it.** The turn ends at the gate, and
86
- the decision becomes a slash command, advertised through `available_commands_update`:
108
+ the decision becomes a slash command, advertised through `available_commands_update`.
109
+ Neither command merges anything: the agents merge their own work into the main branch
110
+ during the rounds.
87
111
 
88
- | Command | Effect |
89
- | ---------------------------- | ----------------------------------------------------------- |
90
- | `/council_accept` | Merge the winning agent's worktree. |
91
- | `/council_reject <feedback>` | Discard the result; the text is passed through as feedback. |
112
+ | Command | Effect |
113
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
114
+ | `/council_accept` | Accept the work the agents have merged on the main branch, and clean up: the worktrees, the `council/*` branches, `plan.md` and `reviews/` are removed. |
115
+ | `/council_reject [<feedback>]` | Reject the work: `plan.md` is rewritten with the feedback and committed. Worktrees and branches are kept so the council can retry. Feedback is optional. |
92
116
 
93
- Any other prompt while a council is parked is refused, so a second run cannot start
94
- over the same worktrees.
117
+ Any other prompt while a council is parked is answered with a reminder of these two
118
+ commands and runs nothing, so a second run cannot start over the same worktrees.
119
+ Either command with no council parked is refused with `invalid params`.
95
120
 
96
121
  ### Ultraplan and ultrareview
97
122
 
98
123
  Neither emits events — they are started and then polled — so progress is reported as
99
- `agent_thought_chunk` heartbeats. Ultraplan has **no abort path at all**, so
100
- cancelling one abandons the poll rather than stopping the work.
124
+ `agent_thought_chunk` heartbeats. The agent does not stop either run on a cancel:
125
+ cancelling one abandons the poll, and the run carries on.
101
126
 
102
127
  ## Cross-engine model selector
103
128
 
@@ -110,20 +135,22 @@ another engine starts a new one, since one engine cannot read another's.
110
135
 
111
136
  Other engines are absent for different reasons:
112
137
 
113
- - **`gemini`** — the Gemini CLI is sunset and superseded by Antigravity. It still
114
- works for callers that name it directly; it is not offered in a new picker.
115
- - **`cursor`** — a legacy engine, superseded by Grok Build in this lineup. Same
116
- treatment: callers that name it still work.
117
- - **`opencode`** — its models are open-ended `provider/model` strings passed
118
- straight through, so there is nothing in the registry to enumerate. An opencode
119
- session is reachable by naming the model, just not by picking it from this list.
138
+ - **`gemini`** and **`cursor`** — legacy engines (the Gemini CLI is sunset and
139
+ superseded by Antigravity; Cursor is superseded by Grok Build in this lineup).
140
+ Their models are filtered out of the list; a model name that routes to one, set
141
+ through `CLAWO_ACP_MODEL`, still works.
142
+ - **`opencode`** — its models are open-ended `provider/model` strings with nothing
143
+ in the registry to enumerate, and no model name routes to it (a `provider/model`
144
+ string resolves to the Claude Code engine), so the ACP agent does not start
145
+ opencode sessions.
146
+ - **`acp`** — an ACP agent picks its own model.
120
147
 
121
148
  ## Generated images
122
149
 
123
150
  When the engine generates an image in a turn (Codex does, when asked for one), the agent names the
124
151
  file's path in a text chunk and then sends the image itself as an `image` content block, so a client
125
- that renders images shows it and one that does not still gets the path. Files over 8 MB are named
126
- only.
152
+ that renders images shows it and one that does not still gets the path. Files over 8 MB, and files
153
+ that are not PNG, JPEG, WebP or GIF, are named only.
127
154
 
128
155
  ## Resuming sessions
129
156
 
@@ -135,7 +162,9 @@ Sessions outlive the agent process. `initialize` advertises
135
162
  - **`session/resume`** reattaches a session by id after the editor or the agent
136
163
  restarts, on the engine and model it was using, and the next prompt continues
137
164
  its conversation. The `cwd` must be the one the session was created in — the
138
- engines keep conversations per directory.
165
+ engines keep conversations per directory. A session resumed into a new agent
166
+ process starts from the default permission setting and mode again
167
+ (`CLAWO_ACP_PERMISSION`, `single`).
139
168
 
140
169
  `session/load` is not offered (`loadSession: false`): it requires replaying the
141
170
  earlier conversation to the client, and no engine returns its history in a form
@@ -143,6 +172,11 @@ that can be replayed. A resumed session therefore opens with an empty view in th
143
172
  client while the engine still remembers it. Sessions are kept for 7 days after
144
173
  their last use.
145
174
 
175
+ Within a running agent, the session layer stops an engine session that has sat
176
+ idle for `OPENCLAW_SERVE_TTL_MINUTES` (default 60) and keeps its resume handle. The
177
+ next prompt or `session/resume` on that ACP session starts it again on the same
178
+ engine, model and permission, and the conversation continues.
179
+
146
180
  ## Permission
147
181
 
148
182
  ACP can ask the user mid-turn through `session/request_permission`. This agent does
@@ -151,20 +185,36 @@ permission is resolved once into engine CLI flags at session start, and
151
185
  `permissionPromptTool` routes to an MCP tool the caller hosts rather than back
152
186
  through the manager. Offering a prompt that cannot be honoured would be worse than
153
187
  saying so, so the choice is a session config option instead — which also suits
154
- `dsh-subagent-acp`, whose default is to auto-reject permission requests.
188
+ `dsh-subagent-acp`, whose default is to auto-reject permission requests. The option
189
+ accepts `plan`, `acceptEdits` and `bypassPermissions`; any other value is refused
190
+ with `invalid params` and the session is left as it was. Changing it restarts the
191
+ underlying session on the same engine, which continues the conversation.
155
192
 
156
193
  ## Cancellation
157
194
 
158
- `session/cancel` settles the in-flight prompt as `cancelled` immediately, then tears
159
- the underlying session down and recreates it. The recreated session resumes the
160
- engine's conversation from the saved resume handle. Codex, codex-app, agy, cursor and
161
- opencode report theirs during a turn, so a cancel in the very first turn of a session
162
- can still start a fresh conversation. A cancel with **nothing in flight does
163
- nothing**.
195
+ What `session/cancel` does depends on the mode:
196
+
197
+ - **`single`** — the in-flight prompt settles as `cancelled` immediately. The
198
+ underlying session is torn down and recreated, and the recreated session
199
+ resumes the engine's conversation from the saved resume handle. The handle
200
+ exists only once the engine has reported it, so a cancel in the very first turn
201
+ of a session can leave the next prompt on a fresh conversation.
202
+ - **`council`** — the council run is aborted, and the turn ends with the
203
+ council's status message and stop reason `end_turn` or `refusal`, not
204
+ `cancelled`.
205
+ - **`ultraplan`** / **`ultrareview`** — the poll, which checks every 3 seconds,
206
+ stops at its next check and the prompt settles as `cancelled`; the run carries
207
+ on.
208
+
209
+ A cancel with **nothing in flight does nothing**.
210
+
211
+ In council mode a cancel aborts the run, which removes its worktrees, and the prompt
212
+ ends with `cancelled`. In ultraplan and ultrareview mode it ends the prompt with
213
+ `cancelled` and abandons the poll.
164
214
 
165
215
  The session layer has **no mid-turn cancel** — `stopSession()` is the only lever and
166
- it destroys the session rather than pausing the turn. So the ACP turn returns
167
- promptly while the engine subprocess may take a moment longer to die, and any
216
+ it destroys the session rather than pausing the turn. So in `single` mode the ACP
217
+ turn returns promptly while the engine subprocess may take a moment longer to die, and any
168
218
  partial work in that turn is lost.
169
219
 
170
220
  ## Use from `dsh`
@@ -178,20 +228,49 @@ needed:
178
228
  config:
179
229
  providerName: clawo
180
230
  command: npx
181
- args: ['-y', '@enderfga/claw-orchestrator', 'acp']
231
+ args: ['-y', '-p', '@enderfga/claw-orchestrator', 'clawo-acp']
182
232
  permission: reject
183
233
  ```
184
234
 
235
+ With a global install, `command: clawo-acp` and `args: []` does the same.
236
+
185
237
  That provider collects **only `agent_message_chunk` text** into its result — it
186
238
  ignores tool calls and other updates — and runs a fresh subprocess per run with no
187
239
  parent context. The adapter therefore keeps the text stream self-sufficient: an
188
240
  engine that never streams still gets its whole answer emitted as one final chunk.
189
241
 
242
+ ## The `acp` engine: driving other ACP agents
243
+
244
+ In the other direction, `engine: 'acp'` starts an ACP agent as a session and drives
245
+ it as a client. One wrapper covers every such agent, with no per-agent parsing:
246
+
247
+ ```bash
248
+ clawo engines # lists the bundled presets
249
+ clawo session-start kimi -e acp --acp-agent kimi-code -d ./project
250
+ clawo session-send kimi "Fix the failing test in tests/test_io.py"
251
+ ```
252
+
253
+ - `acpAgent` names the agent: a bundled preset id (`kimi-code`, `dsh`), or, from a
254
+ local caller only, an inline config (`name`, `command`, and optionally
255
+ `commandEnv`, `args`, `env`, `contextWindow`, `pricing`).
256
+ - One agent process per session; each send is one `session/prompt` on one ACP
257
+ session. The client advertises no `fs` or `terminal` capability, so the agent
258
+ reads, writes and runs commands with its own tools.
259
+ - `session/request_permission` is answered at once from the session's permission
260
+ mode: `bypassPermissions`, `acceptEdits`, `auto` and `dontAsk` allow, any other
261
+ mode (such as `plan`) rejects.
262
+ - A resumed session reattaches the agent's own session id with `session/resume`, or
263
+ `session/load` when that is all the agent offers.
264
+
265
+ The full reference — turn outcomes, cost, each preset's environment — is in
266
+ [multi-engine.md](./multi-engine.md#acp-agents-engine-acp).
267
+
190
268
  ## Development notes
191
269
 
192
- Unit tests cover the translation helpers and the config-option shapes
193
- (`src/__tests__/acp-server.test.ts`). They cannot catch a corrupted frame
194
- stream, so also drive the real binary:
270
+ Unit tests cover the translation helpers, the config-option shapes, and session
271
+ and council behaviour over an in-process client connection
272
+ (`src/__tests__/acp-server.test.ts`, `src/__tests__/acp-session-resume.test.ts`).
273
+ They cannot catch a corrupted frame stream, so also drive the real binary:
195
274
 
196
275
  ```bash
197
276
  npm run build
@@ -28,12 +28,14 @@ This page is the operator reference.
28
28
  | **Coder** | claude / sonnet | workspace | code changes, eval execution |
29
29
  | **Reviewer** | claude / sonnet | `<workspace>/tasks/<run_id>/reviewer_sandbox/` | distrust audit; advance / hold / rollback |
30
30
 
31
- Each role can use any built-in engine, or a `custom` engine config supplied by a
32
- local caller (custom engines name an executable, so the HTTP API does not accept
33
- them — see [tools.md](./tools.md)). If a non-Claude role omits `model`, that CLI
34
- uses its own default model rather than receiving the Claude `opus` / `sonnet`
35
- defaults. Role instructions are included in-band for engines that do not expose a
36
- native system-prompt flag.
31
+ Each role can use any built-in engine except `acp`, or a `custom` engine config
32
+ supplied by a local caller (custom engines name an executable, so the HTTP API
33
+ does not accept them — see [tools.md](./tools.md)). `acp` is refused for every
34
+ role: a role is configured by engine name alone and has no field for the ACP
35
+ agent to start. If a non-Claude role omits `model`, that CLI uses its own default
36
+ model rather than receiving the Claude `opus` / `sonnet` defaults. Claude receives
37
+ each role's instructions as its system prompt; every other engine receives them
38
+ in-band, at the top of each message to that role.
37
39
 
38
40
  Each role also accepts an optional fixed reasoning effort at run start:
39
41
  `planner_effort`, `coder_effort`, and `reviewer_effort`. Accepted values are
@@ -46,14 +48,15 @@ their existing behavior.
46
48
  Engines without native multi-turn conversation (one-shot custom engines) spawn a
47
49
  fresh process per send with nothing to resume, so the dispatcher replays that
48
50
  role's transcript in-band as a `<conversation_history>` block, oldest turns dropped
49
- past a character budget. Claude, Codex, Antigravity, Grok and OpenCode each
50
- resume their own conversation by id and get no replay — see
51
+ past a character budget. Claude, Codex (`codex` and `codex-app`), Antigravity,
52
+ Grok and OpenCode each resume their own conversation and get no replay — see
51
53
  `engineHasNativeConversation` in `types.ts`, which is the single source of truth for
52
54
  this and is checked with a two-turn recall test per engine.
53
55
 
54
56
  The Planner runs read-only so strategy cannot turn into source edits, and that is
55
57
  enforced by the engine rather than requested politely: Claude uses plan mode,
56
- Antigravity uses its plan mode, and OpenCode gets a generated
58
+ Antigravity uses its plan mode, Codex and Grok run in their own read-only
59
+ sandbox, and OpenCode gets a generated
57
60
  `clawo-readonly` agent that denies `edit`/`bash`/`external_directory` (its built-in
58
61
  `plan` agent is a user-overridable preset that denies neither, so a "read-only"
59
62
  session could otherwise still author files through a shell heredoc). A custom
@@ -90,7 +93,9 @@ failing with "Session not found".
90
93
  ## UX flow
91
94
 
92
95
  ```
93
- 1. autoloop_start { run_id, workspace } → Planner session ready (refuses if the workspace Git tree already has tracked, staged, untracked, or submodule changes — including when repository status configuration hides them, `status.showUntrackedFiles=no`, or the porcelain listing is too large to buffer; unexpected `git status` failures also refuse; untracked (`??`) or index-clean worktree modifications (` M`) under `tasks/<id>/` with a regular `tasks/<id>/goal.json` are ignored so a second start can proceed after a committed ledger is appended, including when workspace is a nested repo subdirectory after stripping a verified `show-prefix`; dirt outside the workspace still refuses; ignored paths and non-repo workspaces still start). On a Git workspace, after that dirty check passes, start creates `tasks/<runId>/.gitignore` with `*` as the first ledger file (accepting an existing exact `*`, `*\n`, or `*\r\n` without overwrite), proves the ledger is ignored via `git check-ignore`, and refuses with one fixed generic error when the ledger is tracked, the ignore file is partial/deceptive, or `tasks`/`tasks/<runId>` is not a real in-workspace directory (e.g. a committed `tasks` symlink). Failed proofs and later start failures retain the run-local dirs/`.gitignore` (never deleted). Does not write `info/exclude` or any repository-wide ignore. The same fresh-start boundary runs on the first boot of a workflow `autoloop` node (`visits === 1`); `autoloopResume` / `workflowResume` and later visits skip it. Non-Git starts are unchanged.
96
+ 1. autoloop_start { run_id, workspace } → Planner session ready
97
+ (a Git workspace must be clean,
98
+ see below)
94
99
  2. autoloop_chat { run_id, "<your goal>" } → Planner reads workspace,
95
100
  drafts plan.md + goal.json,
96
101
  asks "ready to spawn?"
@@ -105,6 +110,38 @@ failing with "Session not found".
105
110
  Stopping at a target or at `max_iters` from `goal.json` is the Planner's
106
111
  decision: the runtime does not evaluate `goal.json`.
107
112
 
113
+ ### Starting on a Git workspace
114
+
115
+ On a Git workspace, `autoloop_start` refuses with _Autoloop workspace has
116
+ uncommitted changes; commit or stash them before starting_ when the worktree has
117
+ any tracked, staged, untracked or submodule change. Repository configuration
118
+ cannot hide a change from this check (`status.showUntrackedFiles=no` included),
119
+ and a status listing too large to buffer, or any unexpected `git status` failure,
120
+ also refuses. The one exception is an earlier run's ledger: untracked (`??`) or
121
+ index-clean modified (` M`) files under `tasks/<id>/` are ignored when
122
+ `tasks/<id>/goal.json` is a regular file, so a second run can start after a
123
+ committed ledger was appended to. This also holds when the workspace is a
124
+ subdirectory of the repository; changes outside the workspace still refuse.
125
+ Ignored paths and non-Git workspaces start normally.
126
+
127
+ Once that check passes, start keeps the run's ledger out of Git before writing
128
+ anything else to it:
129
+
130
+ - It creates `tasks/<runId>/.gitignore` containing `*` as the ledger's first
131
+ file. An existing file whose content is exactly `*`, `*\n` or `*\r\n` is
132
+ accepted without being overwritten.
133
+ - It proves with `git check-ignore` that the ledger is ignored.
134
+ - It refuses with the fixed error _Autoloop could not isolate the run ledger
135
+ from Git_ when the ledger is tracked, the ignore file holds anything else, or
136
+ `tasks` / `tasks/<runId>` is not a real directory inside the workspace (for
137
+ example a committed `tasks` symlink).
138
+
139
+ Nothing is deleted on failure: a failed proof or a later start failure leaves the
140
+ run's directories and `.gitignore` in place. Start never writes
141
+ `.git/info/exclude` or any repository-wide ignore. The same boundary runs on the
142
+ first boot of a workflow `autoloop` node (`visits === 1`); `autoloopResume` /
143
+ `workflowResume` and later visits skip it.
144
+
108
145
  ## Timeout hierarchy and recoverable sends
109
146
 
110
147
  Autoloop has three independent start-time controls. Their bounds are inclusive,
@@ -261,7 +298,8 @@ for the `CustomEngineConfig` shape.
261
298
  | on_decision_needed | decision / both |
262
299
 
263
300
  The runtime fires five of these itself: `on_stall_30min` (30 minutes without
264
- activity), `on_metric_regression_2`, `on_reviewer_reject_2` (two in a row),
301
+ activity), `on_metric_regression_2` (two `rollback` verdicts in a row),
302
+ `on_reviewer_reject_2` (two `hold` or `rollback` verdicts in a row),
265
303
  `on_phase_error`, and `on_target_hit` (only when an acceptance contract passes,
266
304
  see [Acceptance contracts](#acceptance-contracts)). `on_start`,
267
305
  `on_iter_done_ok` and `on_decision_needed` are policy entries for the Planner's
@@ -302,16 +340,21 @@ CLIs expose no such command. The threshold is still meaningful there because
302
340
  `contextPercent` tracks real occupancy, but crossing it cannot free space:
303
341
  the session emits a single warning on its log channel the first time compaction
304
342
  is requested, then the thread keeps growing until the CLI refuses the request.
305
- Treat that warning as the signal to start a fresh session.
343
+ Treat that warning as the signal to start a fresh session. `codex-app` does
344
+ compact, through the app-server's own thread compaction.
306
345
 
307
346
  ## Phase-error circuit
308
347
 
309
348
  Subprocess deaths (Claude session lost), failed `git commit` in an iter, and
310
349
  other phase-bound failures surface as `phase_error` messages instead of
311
350
  silently masquerading as a "clarification request". An empty or whitespace-only
312
- Coder reply is also a phase error. Only a non-empty reply without
313
- `iter_complete` is treated as a clarification request. The runner counts
314
- consecutive `phase_error`s and:
351
+ Coder reply is also a phase error. A non-empty Coder reply without a valid
352
+ `iter_complete` returns `directive_ack` (`understood: false`) without review or
353
+ iteration advance. Structured `request_clarification` keeps a short
354
+ `clarification` question. Any other incomplete turn puts fixed status/decision
355
+ wording, a labelled sanitized truncated reply tail, and a write-time persisted
356
+ path (`iter/<n>/coder_reply_<dispatch_id>.txt`) or generic not-persisted result
357
+ into `clarification`. The runner counts consecutive `phase_error`s and:
315
358
 
316
359
  1. Fires `on_phase_error` on each one (defaults to error / both channels).
317
360
  2. After `phaseErrorCircuit` consecutive errors (default **3**) emits a
@@ -344,6 +387,8 @@ decisions:
344
387
  | `update_push_policy` | Planner mutates the policy |
345
388
  | `policy_silence_blocked` | Planner tried to silence a critical channel |
346
389
  | `phase_error` | Surfaced from dispatcher to runner |
390
+ | `send_timeout` | A role's send hit `send_timeout_ms`; run paused |
391
+ | `timeout_migration` | A resume raised `send_timeout_ms` (see above) |
347
392
  | `terminate` | Run ends (planner reason or `phase_error_circuit`) |
348
393
 
349
394
  JSONL, one entry per line, ts-prefixed.
@@ -353,19 +398,18 @@ JSONL, one entry per line, ts-prefixed.
353
398
  ```
354
399
  <workspace>/tasks/<run_id>/
355
400
  ├── .gitignore # run-local `*`; first file on Git start; keeps the ledger out of status/add
356
- ├── plan.md # Planner-authored (workspace-root copies are what iters commit)
357
- ├── goal.json # Planner-authored (workspace-root copies are what iters commit)
358
401
  ├── push_log.jsonl # every notify_user attempt + channel used
359
402
  ├── decisions.jsonl # runner / dispatcher audit trail (see above)
360
- ├── chat.jsonl # Planner-pane conversation, replayed by /chat_history
403
+ ├── chat.jsonl # chat for all three panes (user, Planner, Coder, Reviewer), replayed by /chat_history
404
+ ├── coder_log.jsonl # one row per Coder `coder_log` call: { ts, iter, message }
361
405
  ├── evidence/iter-<n>/ # acceptance-contract bundle, when a contract is configured
362
406
  ├── reviewer_sandbox/ # Reviewer cwd; restaged per iter
363
- │ ├── plan.md # copy
364
- │ ├── goal.json # copy
407
+ │ ├── plan.md # copy of the workspace-root file
408
+ │ ├── goal.json # copy of the workspace-root file
365
409
  │ ├── iter-N/ # this iter's directive + diff + eval
366
410
  │ ├── prior_verdict.json
367
411
  │ ├── reviewer_memory.md # persistent (frozen-injected at session start)
368
- │ └── reviewer_log.jsonl # persistent (Reviewer's append-only audit log)
412
+ │ └── reviewer_log.jsonl # persistent; one row per Reviewer `reviewer_log` call: { ts, iter, message }
369
413
  └── iter/<n>/
370
414
  ├── directive.json # Planner → Coder (schema_version: 1)
371
415
  ├── eval_output.json # what Coder reported (schema_version: 1)
@@ -374,9 +418,10 @@ JSONL, one entry per line, ts-prefixed.
374
418
  └── coder_summary.txt
375
419
  ```
376
420
 
377
- On a Git workspace, `autoloop_start` writes `.gitignore` before other ledger
378
- files and fails closed unless `git check-ignore` confirms the ledger is
379
- ignored. The run-local ignore is self-contained: it does not rely on
421
+ `plan.md` and `goal.json` live at the workspace root, where `write_plan` /
422
+ `write_goal` put them; the Reviewer gets copies in its sandbox. On a Git
423
+ workspace the ledger is kept out of Git by its own `.gitignore` (see
424
+ [Starting on a Git workspace](#starting-on-a-git-workspace)); it does not rely on
380
425
  `.git/info/exclude` or a root `.gitignore` entry for `tasks/`.
381
426
 
382
427
  The orchestrator git-commits each iter automatically. Coder must NOT call
@@ -393,18 +438,83 @@ Every JSON artifact in the ledger carries a `schema_version` field (currently
393
438
 
394
439
  ## Backend HTTP / SSE
395
440
 
396
- | Endpoint | Returns |
397
- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
398
- | `GET /autoloop/list` | `{ ok, runs: AutoloopState[] }` |
399
- | `POST /autoloop/new` | `{ ok, run_id, planner_session }` — body `{ workspace, run_id?, planner_engine?, planner_model?, coder_engine?, coder_model?, reviewer_engine?, reviewer_model?, send_timeout_ms?, activity_lease_ms?, autoloop_hard_timeout_ms? }`. Timeout fields use the defaults and inclusive bounds documented above; malformed or out-of-range values return 400 before a run starts. |
400
- | `GET /autoloop/<id>/state` | `{ ok, state: AutoloopState, live }` — `live` is `true` only when the run is running in this process. For a run that is not live here, `state` is the last state recorded in the run store, so historical runs open with their real iteration count. 404 when there is no such run. |
401
- | `GET /autoloop/<id>/push_log` | `{ ok, entries: PushLogEntry[] }` — served from the ledger via `autoloopStatus`, so historical runs work the same as live ones. |
402
- | `GET /autoloop/<id>/chat_history` | `{ ok, entries: ChatEntry[] }` — replays `<ledger>/chat.jsonl`. The dashboard fetches this when opening a run so the Planner-pane conversation survives a page refresh / cross-process / re-opening a terminated run. Returns `[]` when the file doesn't exist. |
403
- | `GET /autoloop/<id>/events` | SSE: `snapshot` / `message` / `state` / `push` / `iter_done` / `planner_reply` / `planner_error` / `coder_reply` / `reviewer_reply` / `terminated`. For runs that are NOT in this process's memory (terminated, or live in another process), the endpoint emits a single-shot `snapshot` + `terminated` then closes — the dashboard's existing handlers render history without hanging. A run still in memory that has already reached `terminated` or `crashed` gets the same single-shot pair instead of an open stream that would never receive another event. Every such stream sets `retry: 864000000`, so an `EventSource` does not keep reconnecting to a stream that can only end again. |
404
- | `POST /autoloop/<id>/chat` | **202** `{ ok, queued: true }` — body `{ text }`. Fire-and-forget: the Planner's reply streams back via the `/events` SSE channel as a `planner_reply` event (or `planner_error` on failure); the HTTP response intentionally does NOT wait for it, because first-contact replies routinely exceed reverse-proxy idle limits (e.g. Cloudflare Tunnel cuts at ~100s → 524). 400 on empty text. 404 when the run is not in this process's memory: if the store still holds it, the error says so and names `POST /autoloop/<id>/resume`; an unknown or malformed id is plain `not found`. The MCP `autoloop_chat` tool path keeps the synchronous await-and-return-reply semantics (it runs in-process). |
405
- | `GET /autoloop/<id>/resume-requirements` | `{ ok, runId, rolesNeedingCustomEngine }` — the roles whose engine was `custom`, so a caller knows which secret references a resume needs. Role names only; nothing sensitive. 404 when there is no such run. |
406
- | `POST /autoloop/<id>/resume` | `{ ok, state }` — for a live ordinary or activity-lease pause (`pending_dispatch` null), an empty timeout body continues that run: the response returns once status is `planning` or `running` again, the lease re-arms, the hard deadline stays put, and parked Planner input is then delivered once, in order. A live `awaiting_resume` pause is unchanged unless the body increases the send timeout. Otherwise restore the role engine/model choices from the run's spec and re-create dispatcher + runner. For recoverable send timeouts, body fields `send_timeout_ms` and `pending_dispatch_id` apply the increase-only migration described above; `allow_decrease`, lease overrides, and hard-cap overrides are rejected. A custom-engine config is never persisted and is never accepted over HTTP, so a role using `custom` is re-supplied by **reference**: `plannerCustomEngineRef` / `coderCustomEngineRef` / `reviewerCustomEngineRef` name an environment variable `CLAWO_CUSTOM_ENGINE_<NAME>` on the orchestrator host, which the server reads and resolves. The name is not sensitive, the value never crosses the wire, and an unknown name is an error rather than a silent start without credentials. Existing engine-specific conversation resume behavior is reused where supported; `chat.jsonl` remains the visual history fallback. 404 when there is no such run. |
407
- | `POST /autoloop/<id>/delete` | `{ ok }` — stops the loop if still live, deletes the run record from the run store, and purges the role sessions' persisted resume ids so the run cannot be resumed. The ledger directory under `<workspace>/tasks/<run_id>/` is kept on disk. 404 when there is no such run. |
441
+ | Endpoint | Returns |
442
+ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
443
+ | `GET /autoloop/list` | `{ ok, runs: AutoloopState[] }` |
444
+ | `POST /autoloop/new` | `{ ok, run_id, planner_session }` — see [Starting a run](#starting-a-run). 409 when the workspace refuses the start (see [Starting on a Git workspace](#starting-on-a-git-workspace)). |
445
+ | `GET /autoloop/<id>/state` | `{ ok, state: AutoloopState, live }` — `live` is `true` only when the run is running in this process. For a run that is not live here, `state` is the last state recorded in the run store, so historical runs open with their real iteration count. 404 when there is no such run. |
446
+ | `GET /autoloop/<id>/push_log` | `{ ok, entries: PushLogEntry[] }` — served from the ledger via `autoloopStatus`, so historical runs work the same as live ones. |
447
+ | `GET /autoloop/<id>/chat_history` | `{ ok, entries: ChatEntry[] }` — replays `<ledger>/chat.jsonl`, so the dashboard's panes survive a page refresh, another process, or re-opening a terminated run. `[]` when the file doesn't exist. |
448
+ | `GET /autoloop/<id>/events` | SSE — see [Event stream](#event-stream). |
449
+ | `POST /autoloop/<id>/chat` | **202** `{ ok, queued: true }` — see [Chat over HTTP](#chat-over-http). |
450
+ | `GET /autoloop/<id>/resume-requirements` | `{ ok, runId, rolesNeedingCustomEngine }` — the roles whose engine was `custom`, so a caller knows which secret references a resume needs. Role names only; nothing sensitive. 404 when there is no such run. |
451
+ | `POST /autoloop/<id>/resume` | `{ ok, state }` — see [Resume over HTTP](#resume-over-http). 404 when there is no such run. |
452
+ | `POST /autoloop/<id>/delete` | `{ ok }` — stops the loop if still live, deletes the run record from the run store, and purges the role sessions' persisted resume ids so the run cannot be resumed. The ledger directory under `<workspace>/tasks/<run_id>/` is kept on disk. 404 when there is no such run. |
453
+
454
+ ### Starting a run
455
+
456
+ `POST /autoloop/new` takes `{ workspace, run_id?, planner_engine?, planner_model?,
457
+ planner_effort?, coder_engine?, coder_model?, coder_effort?, reviewer_engine?,
458
+ reviewer_model?, reviewer_effort?, send_timeout_ms?, activity_lease_ms?,
459
+ autoloop_hard_timeout_ms? }`. A `run_id` that is missing or does not match
460
+ `^[a-zA-Z0-9_-]{1,64}$` is replaced by a generated `auto-<timestamp>-<hex>`.
461
+ Timeout fields use the defaults and inclusive bounds documented above; malformed
462
+ or out-of-range values return 400 before a run starts. Custom-engine configs are
463
+ not accepted over HTTP, so a `custom` role can only be started by a local caller.
464
+
465
+ ### Event stream
466
+
467
+ `GET /autoloop/<id>/events` emits `snapshot` / `message` / `state` / `push` /
468
+ `iter_done` / `planner_reply` / `planner_error` / `coder_reply` /
469
+ `reviewer_reply` / `compact` / `terminated`.
470
+
471
+ For a run that is NOT in this process's memory (terminated, or live in another
472
+ process), the endpoint emits a single-shot `snapshot` + `terminated` then closes —
473
+ the dashboard's existing handlers render history without hanging. A run still in
474
+ memory that has already reached `terminated` or `crashed` gets the same
475
+ single-shot pair instead of an open stream that would never receive another
476
+ event. Every such stream sets `retry: 864000000`, so an `EventSource` does not
477
+ keep reconnecting to a stream that can only end again.
478
+
479
+ ### Chat over HTTP
480
+
481
+ `POST /autoloop/<id>/chat` takes `{ text }` and is fire-and-forget: the
482
+ Planner's reply streams back on `/events` as a `planner_reply` event (or
483
+ `planner_error` on failure). The HTTP response intentionally does not wait for
484
+ it, because first-contact replies routinely exceed reverse-proxy idle limits
485
+ (e.g. Cloudflare Tunnel cuts at ~100s → 524). The MCP `autoloop_chat` tool keeps
486
+ the synchronous await-and-return-reply semantics (it runs in-process).
487
+
488
+ 400 on empty text. 404 when the run is not in this process's memory: if the
489
+ store still holds it, the error says so and names `POST /autoloop/<id>/resume`;
490
+ an unknown or malformed id is plain `not found`.
491
+
492
+ ### Resume over HTTP
493
+
494
+ `POST /autoloop/<id>/resume` returns `{ ok, state }`.
495
+
496
+ - **A live ordinary or activity-lease pause** (`pending_dispatch` null): an empty
497
+ timeout body continues that run. The response returns once status is
498
+ `planning` or `running` again, the lease re-arms, the hard deadline stays put,
499
+ and parked Planner input is then delivered once, in order.
500
+ - **A live `awaiting_resume` pause** is unchanged unless the body increases the
501
+ send timeout.
502
+ - **Otherwise** the role engine/model choices are restored from the run's spec
503
+ and the dispatcher and runner are re-created. Existing engine-specific
504
+ conversation resume is reused where supported; `chat.jsonl` remains the visual
505
+ history fallback.
506
+
507
+ For recoverable send timeouts, body fields `send_timeout_ms` and
508
+ `pending_dispatch_id` apply the increase-only migration described above;
509
+ `allow_decrease`, lease overrides, and hard-cap overrides are rejected.
510
+
511
+ A custom-engine config is never persisted and never accepted over HTTP, so a role
512
+ using `custom` is re-supplied by **reference**: `plannerCustomEngineRef` /
513
+ `coderCustomEngineRef` / `reviewerCustomEngineRef` name an environment variable
514
+ `CLAWO_CUSTOM_ENGINE_<NAME>` on the orchestrator host, whose value is the JSON
515
+ custom-engine config; the server reads and resolves it. The name is not
516
+ sensitive, the value never crosses the wire, and an unknown name is an error
517
+ rather than a silent start without credentials.
408
518
 
409
519
  The dashboard's 3-pane autoloop view (`/dashboard`) consumes these endpoints:
410
520
 
@@ -519,8 +629,8 @@ retries it.
519
629
  - **One-way push.** Replies to a push are not routed back to the Planner;
520
630
  answer with `autoloop_chat`.
521
631
  - **No fork / population mode.** Single linear iter trajectory per run.
522
- - **Cross-run knowledge isolated.** Each run's `reviewer_memory.md` and
523
- `coder_notes.md` live in that run's ledger; there is no shared store.
632
+ - **Cross-run knowledge isolated.** Each run's `reviewer_memory.md` lives in
633
+ that run's ledger; there is no shared store.
524
634
  - **No cost cap.** `maxBudgetUsd` is not exposed for autoloop. Wall-clock time
525
635
  is bounded by `autoloop_hard_timeout_ms` (default 24 h); the iteration count
526
636
  is bounded only by the Planner honouring `max_iters` in `goal.json`.