tumwater 0.4.0 → 0.4.1

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 (231) hide show
  1. package/README.md +1 -1
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +7 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +169 -166
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +10 -21
  11. package/dist/src/agent/codex/codex-stream.js +199 -178
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/notes-shim.js +2 -2
  15. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  16. package/dist/src/agent/opencode/opencode-config.js +2 -2
  17. package/dist/src/agent/opencode/opencode-stream.js +97 -88
  18. package/dist/src/agent/run-agent.js +66 -381
  19. package/dist/src/agent/run-settle.js +3 -2
  20. package/dist/src/agent/run-spawn.js +235 -0
  21. package/dist/src/agent/run-stream.js +193 -0
  22. package/dist/src/agent/write-guard.js +5 -3
  23. package/dist/src/backlog/backlog-archive.js +28 -11
  24. package/dist/src/backlog/backlog-cli-view.js +89 -70
  25. package/dist/src/backlog/backlog-eligibility.js +1 -1
  26. package/dist/src/backlog/backlog-index-data.js +159 -0
  27. package/dist/src/backlog/backlog-index.js +7 -5
  28. package/dist/src/backlog/backlog-md.js +1 -1
  29. package/dist/src/backlog/backlog-release.js +10 -17
  30. package/dist/src/backlog/backlog-render.js +1 -1
  31. package/dist/src/backlog/backlog-structure.js +0 -144
  32. package/dist/src/backlog/backlog-write.js +30 -17
  33. package/dist/src/backlog/backlog.js +9 -0
  34. package/dist/src/backlog/question-answer.js +3 -1
  35. package/dist/src/baseline/main-baseline.js +50 -15
  36. package/dist/src/baseline/main-guardrails.js +29 -5
  37. package/dist/src/baseline/main-red.js +45 -94
  38. package/dist/src/baseline/main-watch.js +8 -2
  39. package/dist/src/brief/brief.js +40 -0
  40. package/dist/src/budget/budget-fallback-view.js +50 -8
  41. package/dist/src/budget/budget-wiring.js +15 -6
  42. package/dist/src/budget/budget.js +1 -1
  43. package/dist/src/build/build-check-events.js +4 -2
  44. package/dist/src/build/build-check-report.js +2 -1
  45. package/dist/src/cli/cli-args.js +9 -0
  46. package/dist/src/cli/cli-flag-specs.js +5 -0
  47. package/dist/src/cli/cli-marker-commands.js +2 -2
  48. package/dist/src/cli/cli-query-commands.js +41 -6
  49. package/dist/src/cli/cli-role-command.js +26 -49
  50. package/dist/src/cli/cli-start.js +65 -39
  51. package/dist/src/cli/help.js +13 -5
  52. package/dist/src/cli/log-commands.js +125 -106
  53. package/dist/src/cli/question-commands.js +82 -63
  54. package/dist/src/cli.js +399 -342
  55. package/dist/src/collections.js +26 -3
  56. package/dist/src/config/config-request.js +176 -51
  57. package/dist/src/config/config-validation-roles.js +271 -0
  58. package/dist/src/config/config-validation.js +10 -205
  59. package/dist/src/config/config-write.js +194 -157
  60. package/dist/src/config/config.js +2 -1
  61. package/dist/src/doctor/doctor-backlog-bugs.js +14 -14
  62. package/dist/src/doctor/doctor-backlog-plans.js +17 -18
  63. package/dist/src/doctor/doctor-backlog.js +19 -13
  64. package/dist/src/doctor/doctor-check-names.js +2 -0
  65. package/dist/src/doctor/doctor-checks.js +16 -4
  66. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  67. package/dist/src/doctor/doctor-contention-checks.js +51 -18
  68. package/dist/src/doctor/doctor-holds.js +9 -9
  69. package/dist/src/doctor/doctor-notify.js +1 -1
  70. package/dist/src/doctor/doctor-opencode-checks.js +16 -4
  71. package/dist/src/doctor/doctor-principles.js +26 -0
  72. package/dist/src/doctor/doctor-project-checks.js +4 -3
  73. package/dist/src/doctor/doctor-role-notes.js +51 -0
  74. package/dist/src/doctor/doctor.js +7 -0
  75. package/dist/src/events/event-format.js +309 -154
  76. package/dist/src/events/notify.js +1 -1
  77. package/dist/src/failure/failure-cluster.js +4 -3
  78. package/dist/src/failure/failure-data.js +126 -94
  79. package/dist/src/failure/failure-render.js +58 -17
  80. package/dist/src/failure/failure-state-change.js +174 -8
  81. package/dist/src/failure/time-spend.js +139 -106
  82. package/dist/src/files/files.js +7 -0
  83. package/dist/src/gates/budget-gates.js +90 -68
  84. package/dist/src/gates/gate-polls.js +92 -61
  85. package/dist/src/gates/role-cap-gates.js +1 -1
  86. package/dist/src/gates/streak-gate.js +1 -1
  87. package/dist/src/git/git.js +1 -1
  88. package/dist/src/git/slot-salvage.js +4 -5
  89. package/dist/src/git/slots-state.js +12 -3
  90. package/dist/src/git/worktree-pool-startup.js +127 -0
  91. package/dist/src/git/worktree-pool.js +1 -120
  92. package/dist/src/gui/gui-admin-endpoints.js +1 -1
  93. package/dist/src/gui/gui-args.js +1 -1
  94. package/dist/src/gui/gui-backlog-commands.js +19 -10
  95. package/dist/src/gui/gui-backlog-endpoints.js +1 -1
  96. package/dist/src/gui/gui-config-commands.js +51 -76
  97. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  98. package/dist/src/gui/gui-endpoint-common.js +4 -3
  99. package/dist/src/gui/gui-history-endpoints.js +36 -27
  100. package/dist/src/gui/gui-operator-commands.js +43 -5
  101. package/dist/src/gui/gui-prompt-commands.js +1 -1
  102. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  103. package/dist/src/gui/gui-report-endpoints.js +11 -8
  104. package/dist/src/gui/gui-server.js +5 -2
  105. package/dist/src/gui/gui-team-commands.js +1 -1
  106. package/dist/src/history/history-data.js +7 -1
  107. package/dist/src/history/history-render.js +15 -10
  108. package/dist/src/history/history.js +2 -2
  109. package/dist/src/inbox/pending-prompt.js +3 -2
  110. package/dist/src/inbox/prompt-commands.js +189 -161
  111. package/dist/src/init/init.js +43 -17
  112. package/dist/src/merge/merge-drain.js +1 -1
  113. package/dist/src/merge/merge-git.js +5 -5
  114. package/dist/src/merge/merge-queue.js +2 -1
  115. package/dist/src/merge/merge-to-main.js +1 -1
  116. package/dist/src/merge/merge-verify.js +3 -2
  117. package/dist/src/operator/operator-commands.js +16 -5
  118. package/dist/src/operator/operator-intent.js +26 -3
  119. package/dist/src/operator/revert.js +18 -12
  120. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  121. package/dist/src/orchestrator/orchestrator.js +1 -1
  122. package/dist/src/paths.js +10 -4
  123. package/dist/src/pi/pi-bin.js +2 -2
  124. package/dist/src/pi/pi-models.js +9 -0
  125. package/dist/src/pi/pi-stream.js +63 -41
  126. package/dist/src/pi/pi-watchdogs.js +65 -49
  127. package/dist/src/pi-extension/full-output.js +2 -3
  128. package/dist/src/process/process.js +1 -1
  129. package/dist/src/process/run-marker.js +2 -2
  130. package/dist/src/process/supervisor.js +7 -4
  131. package/dist/src/prompt/gate-notes.js +34 -9
  132. package/dist/src/prompt/notes-backlog.js +11 -5
  133. package/dist/src/prompt/principles.js +11 -0
  134. package/dist/src/prompt/prompt.js +11 -7
  135. package/dist/src/redeploy/redeploy-probes.js +11 -1
  136. package/dist/src/redeploy/redeployer.js +54 -7
  137. package/dist/src/redeploy/self-reload.js +7 -2
  138. package/dist/src/report/report-data.js +298 -179
  139. package/dist/src/report/report-fold.js +251 -184
  140. package/dist/src/report/report-main-red.js +64 -0
  141. package/dist/src/report/report-render.js +127 -21
  142. package/dist/src/review/known-flakes.js +20 -1
  143. package/dist/src/review/post-merge-review.js +11 -9
  144. package/dist/src/review/quarantine-render.js +27 -10
  145. package/dist/src/review/review-precheck.js +2 -1
  146. package/dist/src/roles/role-catalog.js +14 -1
  147. package/dist/src/roles/role-guidance.js +8 -0
  148. package/dist/src/roles/role-render.js +19 -0
  149. package/dist/src/roles/role-view.js +18 -3
  150. package/dist/src/roles/roles.js +2 -22
  151. package/dist/src/run/feedback-filings.js +10 -1
  152. package/dist/src/run/run-apply.js +14 -2
  153. package/dist/src/run/run-detail-data.js +27 -3
  154. package/dist/src/run/run-detail-render.js +26 -1
  155. package/dist/src/run/run-finalize.js +75 -46
  156. package/dist/src/run/run-prompt.js +229 -189
  157. package/dist/src/run/run-stage.js +112 -43
  158. package/dist/src/run/run-verdict.js +113 -12
  159. package/dist/src/run/stage-backlog-checks.js +2 -2
  160. package/dist/src/run/stage-checks-shared.js +15 -0
  161. package/dist/src/run/stage-name-checks.js +3 -13
  162. package/dist/src/run/stage-path-checks.js +26 -29
  163. package/dist/src/runner/leftover.js +13 -4
  164. package/dist/src/runner/revision.js +2 -2
  165. package/dist/src/runner/runner-fallback.js +1 -1
  166. package/dist/src/runner/runner-pi.js +72 -49
  167. package/dist/src/runner/runner-run.js +17 -23
  168. package/dist/src/scheduling/claims.js +1 -1
  169. package/dist/src/status/status-data.js +140 -87
  170. package/dist/src/status/status-polls.js +41 -5
  171. package/dist/src/status/status-role-notes.js +49 -0
  172. package/dist/src/status/status-roles.js +8 -2
  173. package/dist/src/text/format.js +13 -1
  174. package/dist/src/text/phrases.js +16 -2
  175. package/dist/src/ui/alert-feeds.js +320 -0
  176. package/dist/src/ui/badges.js +1 -1
  177. package/dist/src/ui/gui/gui-client-boot.js +59 -53
  178. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  179. package/dist/src/ui/gui/gui-client-drawer.js +17 -1
  180. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  181. package/dist/src/ui/gui/gui-client-history.js +16 -16
  182. package/dist/src/ui/gui/gui-client-model.js +9 -9
  183. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  184. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  185. package/dist/src/ui/gui/gui-client-report-chart.js +103 -4
  186. package/dist/src/ui/gui/gui-client-report.js +70 -10
  187. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  188. package/dist/src/ui/gui/gui-client-settings.js +31 -3
  189. package/dist/src/ui/gui/gui-client-team.js +2 -2
  190. package/dist/src/ui/gui/gui-client.js +33 -4
  191. package/dist/src/ui/progress-data.js +2 -2
  192. package/dist/src/ui/run-progress-model.js +2 -2
  193. package/dist/src/ui/status-model.js +24 -14
  194. package/dist/src/ui/status-payload.js +149 -326
  195. package/dist/src/ui/status-render.js +130 -79
  196. package/dist/src/ui/team-alert-backlog-feed.js +181 -0
  197. package/dist/src/ui/team-alert-backlog.js +267 -0
  198. package/dist/src/ui/team-alert-merge.js +183 -0
  199. package/dist/src/ui/team-alert-shared.js +79 -0
  200. package/dist/src/ui/team-alert-status.js +321 -0
  201. package/dist/src/ui/team-alert-tail.js +251 -0
  202. package/dist/src/ui/team-alerts.js +15 -972
  203. package/dist/src/ui/tui/tui-backlog.js +1 -1
  204. package/dist/src/ui/tui/tui-editor-budget.js +9 -4
  205. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  206. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  207. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  208. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  209. package/dist/src/ui/tui/tui-editor-pause.js +2 -1
  210. package/dist/src/ui/tui/tui-editor-prompt.js +2 -1
  211. package/dist/src/ui/tui/tui-editor-quiet.js +2 -1
  212. package/dist/src/ui/tui/tui-editor-release.js +77 -0
  213. package/dist/src/ui/tui/tui-editor-role-file.js +75 -0
  214. package/dist/src/ui/tui/tui-editor-role-key.js +2 -1
  215. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  216. package/dist/src/ui/tui/tui-editors.js +4 -1
  217. package/dist/src/ui/tui/tui-frame.js +7 -3
  218. package/dist/src/ui/tui/tui-history.js +12 -10
  219. package/dist/src/ui/tui/tui-keys.js +5 -0
  220. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  221. package/dist/src/ui/tui/tui-pane-health.js +43 -3
  222. package/dist/src/ui/tui/tui-pane-history.js +299 -263
  223. package/dist/src/ui/tui/tui-pane-session.js +3 -0
  224. package/dist/src/ui/tui/tui-pane-transcript.js +12 -8
  225. package/dist/src/ui/tui/tui-panes.js +209 -145
  226. package/dist/src/ui/tui/tui.js +3 -3
  227. package/dist/src/verdict/refusal.js +2 -1
  228. package/package.json +1 -1
  229. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  230. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
  231. /package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +0 -0
@@ -10,12 +10,13 @@
10
10
  * state (the open tool calls, the turn's usage fallback, the unmapped native types seen), so
11
11
  * call it once per run. Unmapped native types, the distinct text of `error` items, and a
12
12
  * command abandoned at the end of its turn are recorded once each on the seam's optional
13
- * `warnings()` — src/agent/run-agent.ts forwards each as an `agent_unknown_event` warning
13
+ * `warnings()` — src/agent/run-stream.ts forwards each as an `agent_unknown_event` warning
14
14
  * after the run. `sessionId()` reports the thread id once `thread.started` has named it.
15
15
  */
16
16
  import { nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
17
17
  import { boundText, outputLimits } from "../../pi-extension/bounded-output.js";
18
18
  import { secondsUntil } from "../../text/datetime.js";
19
+ import { SERVER_TRANSIENT_STATUS } from "../backend-traits.js";
19
20
  /** How codex wraps every shell command — the prefix to strip (with its quoting) so the
20
21
  * canonical `bash` call carries the model's own command, which suite-rerun detection matches. */
21
22
  const SHELL_WRAPPER = /^\/(?:bin\/)?(?:zsh|bash) -lc '(.*)'$/s;
@@ -25,9 +26,6 @@ const RECONNECT = /^Reconnecting\.\.\. (\d+)\/(\d+) /;
25
26
  /** The item types dropped silently, with no canonical event and no `warnings()` entry
26
27
  * (design §5's `item.*` row): `todo_list` and `web_search`. */
27
28
  const DROPPED_ITEM_TYPES = new Set(["todo_list", "web_search"]);
28
- /** HTTP status classes that mean a transient backend failure (shared with the claude and
29
- * codex classifiers' reading of `codex exec`'s JSON error). */
30
- const SERVER_STATUS = new Set([500, 502, 503, 504, 529]);
31
29
  /** Strip codex's `/bin/zsh -lc '<cmd>'` (or `bash -lc`) wrapper and its quoting, returning
32
30
  * the model's own command — the argument suite-rerun detection matches. Passes a command the
33
31
  * capture did not wrap through unchanged. */
@@ -96,7 +94,7 @@ function classifyTurnFailed(parsed) {
96
94
  kind.kind = "configError";
97
95
  return kind;
98
96
  }
99
- if (status !== undefined && SERVER_STATUS.has(status)) {
97
+ if (status !== undefined && SERVER_TRANSIENT_STATUS.has(status)) {
100
98
  kind.kind = "transientBackend";
101
99
  kind.backendKind = "server";
102
100
  return kind;
@@ -185,187 +183,210 @@ export function codexTranslator(opts = {}) {
185
183
  }
186
184
  return out;
187
185
  };
186
+ /** The `thread.started` arm: announces the session and the agent start, or records the
187
+ * unmapped event once when the thread id is missing. */
188
+ const handleThreadStarted = (event) => {
189
+ const id = typeof event.thread_id === "string" ? event.thread_id : undefined;
190
+ if (id === undefined) {
191
+ recordEvent("thread.started");
192
+ return [];
193
+ }
194
+ sessionIdValue = id;
195
+ const out = [];
196
+ out.push(JSON.stringify({
197
+ type: "session",
198
+ id,
199
+ ...(cwd !== undefined ? { cwd } : {}),
200
+ }));
201
+ out.push(JSON.stringify({ type: "agent_start" }));
202
+ return out;
203
+ };
204
+ /** The `turn.completed` arm: folds the usage block and abandons the open calls. */
205
+ const handleTurnCompleted = (event) => {
206
+ sawTurnCompleted = true;
207
+ const usage = objectOrEmpty(event.usage);
208
+ turnUsage =
209
+ usage.input_tokens !== undefined || usage.output_tokens !== undefined
210
+ ? {
211
+ input: nonNegativeNumber(usage.input_tokens, 0),
212
+ cacheRead: nonNegativeNumber(usage.cached_input_tokens, 0),
213
+ cacheWrite: nonNegativeNumber(usage.cache_write_input_tokens, 0),
214
+ output: nonNegativeNumber(usage.output_tokens, 0),
215
+ }
216
+ : undefined;
217
+ return abandonOpenCalls();
218
+ };
219
+ /** The `turn.failed` arm: classifies the inner-JSON error and closes the turn. */
220
+ const handleTurnFailed = (event) => {
221
+ sawTurnFailed = true;
222
+ // The error message is itself a JSON string (finding 11); fall back to the held
223
+ // top-level error text when it is not.
224
+ const raw = typeof event.error === "object" && event.error !== null && !Array.isArray(event.error)
225
+ ? event.error.message
226
+ : undefined;
227
+ const inner = typeof raw === "string" ? parseJsonObject(raw) : undefined;
228
+ const cls = inner ? classifyTurnFailed(inner) : {};
229
+ const errorMessage = cls.errorMessage ?? (typeof raw === "string" ? raw : pendingFailureText);
230
+ const closes = abandonOpenCalls();
231
+ const message = { role: "assistant", content: [], stopReason: "error" };
232
+ if (errorMessage !== undefined)
233
+ message.errorMessage = errorMessage;
234
+ const end = {
235
+ type: "message_end",
236
+ message,
237
+ ...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
238
+ ...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
239
+ };
240
+ if (cls.kind === "transientRateLimit" && typeof cls.rateLimit?.retryAfterSeconds === "number")
241
+ end.retryAfterSeconds = cls.rateLimit.retryAfterSeconds;
242
+ return [...(cls.rateLimit !== undefined ? [JSON.stringify(cls.rateLimit)] : []), ...closes, JSON.stringify(end)];
243
+ };
244
+ /** The top-level `error` arm: a reconnect text emits an `auto_retry_start`; any other
245
+ * message is held as the next `turn.failed`'s fallback text. */
246
+ const handleErrorLine = (event) => {
247
+ const message = typeof event.message === "string" ? event.message : undefined;
248
+ const reconnect = message === undefined ? undefined : RECONNECT.exec(message);
249
+ if (reconnect) {
250
+ return [
251
+ JSON.stringify({
252
+ type: "auto_retry_start",
253
+ attempt: Number(reconnect[1]),
254
+ maxAttempts: Number(reconnect[2]),
255
+ errorMessage: message,
256
+ }),
257
+ ];
258
+ }
259
+ // Other top-level `error` lines are not fatal (design §5): held as the fallback text
260
+ // for a following `turn.failed`.
261
+ if (message !== undefined)
262
+ pendingFailureText = message;
263
+ return [];
264
+ };
265
+ /** The `item.started`/`item.completed` arm: reads the item and delegates by type to the
266
+ * item handlers, dropping the silent types and warning once on the unknown tail. */
267
+ const handleItem = (event, started) => {
268
+ const item = objectOrEmpty(event.item);
269
+ const itemType = typeof item.type === "string" ? item.type : "unknown";
270
+ if (itemType === "command_execution")
271
+ return handleCommandItem(item, started);
272
+ if (itemType === "file_change")
273
+ return handleFileChangeItem(item, started);
274
+ if (itemType === "error")
275
+ return handleErrorItem(item);
276
+ if (itemType === "agent_message")
277
+ return handleAgentMessageItem(item);
278
+ if (DROPPED_ITEM_TYPES.has(itemType))
279
+ return [];
280
+ // mcp_tool_call / reasoning and any unknown item shape map to nothing canonical; a
281
+ // once-per-type warning only (design §5).
282
+ recordItem(item);
283
+ return [];
284
+ };
285
+ /** The `command_execution` item arm: opens or closes the bash call, capping the result. */
286
+ const handleCommandItem = (item, started) => {
287
+ const command = typeof item.command === "string" ? item.command : "";
288
+ const out = [];
289
+ if (started) {
290
+ const id = typeof item.id === "string" ? item.id : `cmd-${openCalls.size}`;
291
+ openCalls.set(id, "command_execution");
292
+ const inner = innerCommand(command);
293
+ out.push(JSON.stringify({
294
+ type: "message_end",
295
+ message: {
296
+ role: "assistant",
297
+ content: [{ type: "toolCall", id, name: "bash", arguments: { command: inner } }],
298
+ },
299
+ }));
300
+ out.push(JSON.stringify({
301
+ type: "tool_execution_start",
302
+ toolCallId: id,
303
+ toolName: "bash",
304
+ args: { command: inner },
305
+ }));
306
+ }
307
+ else {
308
+ const id = typeof item.id === "string" ? item.id : "";
309
+ openCalls.delete(id);
310
+ const aggregated = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
311
+ const exitCode = nonNegativeNumber(item.exit_code, undefined);
312
+ out.push(JSON.stringify({
313
+ type: "tool_execution_end",
314
+ toolCallId: id,
315
+ isError: exitCode !== undefined ? exitCode !== 0 : item.status !== "completed",
316
+ result: boundText(aggregated, bashLimit),
317
+ }));
318
+ }
319
+ return out;
320
+ };
321
+ /** The `file_change` item arm: mirrors each change as an `edit` tool call. */
322
+ const handleFileChangeItem = (item, started) => {
323
+ const changes = Array.isArray(item.changes) ? item.changes.filter((c) => typeof c === "object" && c !== null) : [];
324
+ const id = typeof item.id === "string" ? item.id : "";
325
+ const out = [];
326
+ if (!started) {
327
+ const isError = typeof item.status === "string" && item.status !== "completed";
328
+ for (let i = 0; i < changes.length; i++) {
329
+ const callId = `${id}-${i}`;
330
+ openCalls.delete(callId);
331
+ out.push(JSON.stringify({ type: "tool_execution_end", toolCallId: callId, isError }));
332
+ }
333
+ return out;
334
+ }
335
+ const content = changes.map((c, i) => ({
336
+ type: "toolCall",
337
+ id: `${id}-${i}`,
338
+ name: "edit",
339
+ arguments: {
340
+ path: typeof c.path === "string" ? c.path : undefined,
341
+ kind: typeof c.kind === "string" ? c.kind : undefined,
342
+ },
343
+ }));
344
+ if (content.length === 0)
345
+ return out; // no changes to mirror, nothing else to emit
346
+ out.push(JSON.stringify({ type: "message_end", message: { role: "assistant", content } }));
347
+ for (let i = 0; i < content.length; i++) {
348
+ const callId = `${id}-${i}`;
349
+ openCalls.set(callId, "file_change");
350
+ out.push(JSON.stringify({
351
+ type: "tool_execution_start",
352
+ toolCallId: callId,
353
+ toolName: "edit",
354
+ args: content[i].arguments,
355
+ }));
356
+ }
357
+ return out;
358
+ };
359
+ /** The `item.type === "error"` arm: a non-fatal notice warned once per distinct message. */
360
+ const handleErrorItem = (item) => {
361
+ const message = typeof item.message === "string" ? item.message : undefined;
362
+ if (message !== undefined)
363
+ recordErrorItem(message);
364
+ return [];
365
+ };
366
+ /** The `agent_message` arm: the model's reply as one canonical `message_end`. */
367
+ const handleAgentMessageItem = (item) => {
368
+ const text = typeof item.text === "string" ? item.text : "";
369
+ return [
370
+ JSON.stringify({
371
+ type: "message_end",
372
+ message: { role: "assistant", content: [{ type: "text", text }] },
373
+ }),
374
+ ];
375
+ };
188
376
  const translate = (line) => {
189
377
  const parsed = parseJsonObject(line);
190
378
  if (!parsed)
191
379
  return []; // Non-JSON noise on stdout; ignore, as pi's parser does.
192
380
  const event = parsed;
193
381
  const type = typeof event.type === "string" ? event.type : "unknown";
194
- const out = [];
195
382
  switch (type) {
196
- case "thread.started": {
197
- const id = typeof event.thread_id === "string" ? event.thread_id : undefined;
198
- if (id === undefined) {
199
- recordEvent(type);
200
- return [];
201
- }
202
- sessionIdValue = id;
203
- out.push(JSON.stringify({
204
- type: "session",
205
- id,
206
- ...(cwd !== undefined ? { cwd } : {}),
207
- }));
208
- out.push(JSON.stringify({ type: "agent_start" }));
209
- return out;
210
- }
211
- case "turn.started":
212
- return [];
213
- case "turn.completed": {
214
- sawTurnCompleted = true;
215
- const usage = objectOrEmpty(event.usage);
216
- turnUsage =
217
- usage.input_tokens !== undefined || usage.output_tokens !== undefined
218
- ? {
219
- input: nonNegativeNumber(usage.input_tokens, 0),
220
- cacheRead: nonNegativeNumber(usage.cached_input_tokens, 0),
221
- cacheWrite: nonNegativeNumber(usage.cache_write_input_tokens, 0),
222
- output: nonNegativeNumber(usage.output_tokens, 0),
223
- }
224
- : undefined;
225
- return abandonOpenCalls();
226
- }
227
- case "turn.failed": {
228
- sawTurnFailed = true;
229
- // The error message is itself a JSON string (finding 11); fall back to the held
230
- // top-level error text when it is not.
231
- const raw = typeof event.error === "object" && event.error !== null && !Array.isArray(event.error)
232
- ? event.error.message
233
- : undefined;
234
- const inner = typeof raw === "string" ? parseJsonObject(raw) : undefined;
235
- const cls = inner ? classifyTurnFailed(inner) : {};
236
- const errorMessage = cls.errorMessage ?? (typeof raw === "string" ? raw : pendingFailureText);
237
- const closes = abandonOpenCalls();
238
- const message = { role: "assistant", content: [], stopReason: "error" };
239
- if (errorMessage !== undefined)
240
- message.errorMessage = errorMessage;
241
- const end = {
242
- type: "message_end",
243
- message,
244
- ...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
245
- ...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
246
- };
247
- if (cls.kind === "transientRateLimit" && typeof cls.rateLimit?.retryAfterSeconds === "number")
248
- end.retryAfterSeconds = cls.rateLimit.retryAfterSeconds;
249
- return [...(cls.rateLimit !== undefined ? [JSON.stringify(cls.rateLimit)] : []), ...closes, JSON.stringify(end)];
250
- }
251
- case "error": {
252
- const message = typeof event.message === "string" ? event.message : undefined;
253
- const reconnect = message === undefined ? undefined : RECONNECT.exec(message);
254
- if (reconnect) {
255
- return [
256
- JSON.stringify({
257
- type: "auto_retry_start",
258
- attempt: Number(reconnect[1]),
259
- maxAttempts: Number(reconnect[2]),
260
- errorMessage: message,
261
- }),
262
- ];
263
- }
264
- // Other top-level `error` lines are not fatal (design §5): held as the fallback text
265
- // for a following `turn.failed`.
266
- if (message !== undefined)
267
- pendingFailureText = message;
268
- return [];
269
- }
383
+ case "thread.started": return handleThreadStarted(event);
384
+ case "turn.started": return [];
385
+ case "turn.completed": return handleTurnCompleted(event);
386
+ case "turn.failed": return handleTurnFailed(event);
387
+ case "error": return handleErrorLine(event);
270
388
  case "item.started":
271
- case "item.completed": {
272
- const item = objectOrEmpty(event.item);
273
- const itemType = typeof item.type === "string" ? item.type : "unknown";
274
- const started = type === "item.started";
275
- if (itemType === "command_execution") {
276
- const command = typeof item.command === "string" ? item.command : "";
277
- if (started) {
278
- const id = typeof item.id === "string" ? item.id : `cmd-${openCalls.size}`;
279
- openCalls.set(id, "command_execution");
280
- const inner = innerCommand(command);
281
- out.push(JSON.stringify({
282
- type: "message_end",
283
- message: {
284
- role: "assistant",
285
- content: [{ type: "toolCall", id, name: "bash", arguments: { command: inner } }],
286
- },
287
- }));
288
- out.push(JSON.stringify({
289
- type: "tool_execution_start",
290
- toolCallId: id,
291
- toolName: "bash",
292
- args: { command: inner },
293
- }));
294
- }
295
- else {
296
- const id = typeof item.id === "string" ? item.id : "";
297
- openCalls.delete(id);
298
- const aggregated = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
299
- const exitCode = nonNegativeNumber(item.exit_code, undefined);
300
- out.push(JSON.stringify({
301
- type: "tool_execution_end",
302
- toolCallId: id,
303
- isError: exitCode !== undefined ? exitCode !== 0 : item.status !== "completed",
304
- result: boundText(aggregated, bashLimit),
305
- }));
306
- }
307
- return out;
308
- }
309
- if (itemType === "file_change") {
310
- const changes = Array.isArray(item.changes) ? item.changes.filter((c) => typeof c === "object" && c !== null) : [];
311
- const id = typeof item.id === "string" ? item.id : "";
312
- if (!started) {
313
- const isError = typeof item.status === "string" && item.status !== "completed";
314
- for (let i = 0; i < changes.length; i++) {
315
- const callId = `${id}-${i}`;
316
- openCalls.delete(callId);
317
- out.push(JSON.stringify({ type: "tool_execution_end", toolCallId: callId, isError }));
318
- }
319
- return out;
320
- }
321
- const content = changes.map((c, i) => ({
322
- type: "toolCall",
323
- id: `${id}-${i}`,
324
- name: "edit",
325
- arguments: {
326
- path: typeof c.path === "string" ? c.path : undefined,
327
- kind: typeof c.kind === "string" ? c.kind : undefined,
328
- },
329
- }));
330
- if (content.length === 0)
331
- return out; // no changes to mirror, nothing else to emit
332
- out.push(JSON.stringify({ type: "message_end", message: { role: "assistant", content } }));
333
- for (let i = 0; i < content.length; i++) {
334
- const callId = `${id}-${i}`;
335
- openCalls.set(callId, "file_change");
336
- out.push(JSON.stringify({
337
- type: "tool_execution_start",
338
- toolCallId: callId,
339
- toolName: "edit",
340
- args: content[i].arguments,
341
- }));
342
- }
343
- return out;
344
- }
345
- if (itemType === "error") {
346
- // An error item is a non-fatal notice (finding 11); warned once per distinct message.
347
- const message = typeof item.message === "string" ? item.message : undefined;
348
- if (message !== undefined)
349
- recordErrorItem(message);
350
- return [];
351
- }
352
- if (itemType === "agent_message") {
353
- // The model's reply: one canonical `message_end` with one text block.
354
- const text = typeof item.text === "string" ? item.text : "";
355
- return [
356
- JSON.stringify({
357
- type: "message_end",
358
- message: { role: "assistant", content: [{ type: "text", text }] },
359
- }),
360
- ];
361
- }
362
- if (DROPPED_ITEM_TYPES.has(itemType))
363
- return [];
364
- // mcp_tool_call / reasoning and any unknown item shape map to nothing canonical; a
365
- // once-per-type warning only (design §5).
366
- recordItem(item);
367
- return [];
368
- }
389
+ case "item.completed": return handleItem(event, type === "item.started");
369
390
  default:
370
391
  recordEvent(type);
371
392
  return [];
@@ -1,5 +1,6 @@
1
1
  /** The PreToolUse hook codex runs before each `apply_patch` in a team run (part 6/9 installs it
2
- * through `-c`): `node write-guard-hook.js` with codex's hook payload on stdin. To block the patch
2
+ * through `-c`): `node codex-write-guard-hook.js` with codex's hook payload on stdin. To block
3
+ * the patch
3
4
  * it prints a `deny` decision with the refusal as its reason, in the shape finding 5 showed codex
4
5
  * accepts: the model reads only the reason, so a denial by exit code would not carry it. It prints
5
6
  * nothing to let the patch run, and exits 0 either way. Any failure of its own (bad JSON, a throw)
@@ -1,6 +1,6 @@
1
1
  /** codex's half of the write guard (src/agent/write-guard.ts holds the rule): a PreToolUse hook on
2
2
  * `apply_patch`, installed by part 6/9, refuses an `apply_patch` that writes under the team root
3
- * but outside the run's worktree. The entry point is write-guard-hook.ts beside this file.
3
+ * but outside the run's worktree. The entry point is codex-write-guard-hook.ts beside this file.
4
4
  *
5
5
  * Codex's hook takes no argument naming the worktree, unlike claude's: codex trusts a hook by a
6
6
  * hash of its whole definition, so a command constant per build needs one trust lookup, while a
@@ -63,11 +63,11 @@ export function tomlString(s) {
63
63
  return out + '"';
64
64
  }
65
65
  /** The codex write guard's definition: the PreToolUse hook on `apply_patch` that runs the
66
- * compiled write-guard-hook.js with this process's node, falling back to `node` on PATH when
66
+ * compiled codex-write-guard-hook.js with this process's node, falling back to `node` on PATH when
67
67
  * that binary has moved (as claude's and the notes shim do). The hook path resolves from this
68
68
  * module, so a staged build (.tumwater/build/<sha>) runs its own copy and gets its own hash. */
69
69
  export function codexWriteGuardDefinition(nodeBin = process.execPath) {
70
- const hook = fileURLToPath(new URL("./write-guard-hook.js", import.meta.url));
70
+ const hook = fileURLToPath(new URL("./codex-write-guard-hook.js", import.meta.url));
71
71
  const command = `n=${shellQuote(nodeBin)}; [ -x "$n" ] || n=node; exec "$n" ${shellQuote(hook)}`;
72
72
  return { matcher: HOOK_MATCHER, command, timeout: HOOK_TIMEOUT_SECONDS };
73
73
  }
@@ -1,5 +1,5 @@
1
1
  /** Installing the per-team `tumwater-notes` shim every backend's run finds on its PATH
2
- * (plans/notes-command.md "Reaching it from a run"): run-agent calls `ensureNotesShim(root)`
2
+ * (plans/notes-command.md "Reaching it from a run"): run-spawn calls `ensureNotesShim(root)`
3
3
  * before building each invocation, and the backend prepends the returned dir to the run's
4
4
  * PATH (backendBaseEnv's `binDir`, src/process/run-marker.ts). The shim is a two-line `sh`
5
5
  * script that execs the compiled notes CLI, so a model's shell — pi's, Claude Code's or
@@ -20,7 +20,7 @@ export const NOTES_SHIM_NAME = "tumwater-notes";
20
20
  /** Install the `tumwater-notes` shim under `<root>/.tumwater/bin` and return that directory.
21
21
  * The shim is written atomically with mode 0755, only when its content differs from what is
22
22
  * already there (a redeploy that changes `<dist>` rewrites it). Never throws: a failure to
23
- * write is the caller's to absorb — run-agent logs one warning and runs without the shim. */
23
+ * write is the caller's to absorb — run-spawn logs one warning and runs without the shim. */
24
24
  export function ensureNotesShim(root, execPath = process.execPath) {
25
25
  const dir = notesShimDir(root);
26
26
  const target = path.join(dir, NOTES_SHIM_NAME);
@@ -1,8 +1,8 @@
1
1
  /** The opencode adapter for the agent seam (plans/opencode-backend.md "The invocation",
2
2
  * "Sessions", §6). It shapes one run's argv/stdin/env, translates opencode's `run --format
3
3
  * json` into the canonical stream, runs the authoritative session export after the process
4
- * exits, and classifies the exit's stderr — everything backend-specific. src/agent/run-agent.ts
5
- * owns the spawn and records this run in the session ledger; opencode mints its own session id
4
+ * exits, and classifies the exit's stderr — everything backend-specific. src/agent/run-spawn.ts
5
+ * spawns the run and records this run in the session ledger; opencode mints its own session id
6
6
  * (a `ses_…` id, or the resumed record's id), which `invocation` returns so the ledger can
7
7
  * carry it.
8
8
  *
@@ -18,7 +18,7 @@
18
18
  import fs from "node:fs";
19
19
  import path from "node:path";
20
20
  import { fileURLToPath, pathToFileURL } from "node:url";
21
- import { readTextOrNull } from "../../files/files.js";
21
+ import { readTextOrNull, xdgConfigHomeDir } from "../../files/files.js";
22
22
  import { isJsonObject } from "../../files/json-object.js";
23
23
  /** The operator's global opencode config lives in `$XDG_CONFIG_HOME/opencode/` (or
24
24
  * `~/.config/opencode/`), in `config.json`, `opencode.json` or `opencode.jsonc`. */
@@ -123,7 +123,7 @@ function stripJsonc(src) {
123
123
  * parse; doctor reports a broken one in part 7/7. `homeDir` is the operator's home directory;
124
124
  * the process's own `XDG_CONFIG_HOME` env, when set, wins over `~/.config` (the XDG default). */
125
125
  export function operatorMcpNames(env, homeDir) {
126
- const configHome = env.XDG_CONFIG_HOME ?? path.join(homeDir, ".config");
126
+ const configHome = xdgConfigHomeDir(env, homeDir);
127
127
  const dir = path.join(configHome, "opencode");
128
128
  for (const file of OPERATOR_CONFIG_FILES) {
129
129
  const text = readTextOrNull(path.join(dir, file));