tumwater 0.1.3 → 0.2.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 (359) hide show
  1. package/README.md +63 -28
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-eligibility.js +332 -43
  26. package/dist/src/backlog/backlog-md.js +79 -17
  27. package/dist/src/backlog/backlog-render.js +62 -11
  28. package/dist/src/backlog/backlog-structure.js +261 -58
  29. package/dist/src/backlog/backlog-write.js +178 -441
  30. package/dist/src/backlog/backlog.js +113 -16
  31. package/dist/src/backlog/question-answer.js +339 -0
  32. package/dist/src/baseline/main-baseline.js +75 -88
  33. package/dist/src/baseline/main-red.js +32 -30
  34. package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
  35. package/dist/src/{brief.js → brief/brief.js} +33 -8
  36. package/dist/src/budget/budget.js +32 -15
  37. package/dist/src/budget/fallback-breaker.js +8 -7
  38. package/dist/src/build/build-check-detect.js +8 -7
  39. package/dist/src/build/build-check-events.js +68 -22
  40. package/dist/src/build/build-check-report.js +22 -23
  41. package/dist/src/build/build-check-scoped.js +51 -62
  42. package/dist/src/build/build-check.js +15 -14
  43. package/dist/src/build/build-info.js +2 -1
  44. package/dist/src/build/build-stage.js +23 -22
  45. package/dist/src/build/dep-install.js +22 -21
  46. package/dist/src/build/host-sleep.js +1 -1
  47. package/dist/src/change/change-data.js +13 -11
  48. package/dist/src/change/change-render.js +11 -10
  49. package/dist/src/cli/brief-command.js +9 -7
  50. package/dist/src/cli/cli-args.js +25 -5
  51. package/dist/src/cli/cli-command-args.js +43 -10
  52. package/dist/src/cli/cli-editor.js +13 -1
  53. package/dist/src/cli/cli-flag-specs.js +31 -6
  54. package/dist/src/cli/cli-marker-commands.js +13 -10
  55. package/dist/src/cli/cli-query-commands.js +439 -56
  56. package/dist/src/cli/cli-repo-gate.js +1 -1
  57. package/dist/src/cli/cli-run.js +124 -68
  58. package/dist/src/cli/completion.js +70 -12
  59. package/dist/src/cli/config-commands.js +14 -11
  60. package/dist/src/cli/help.js +264 -124
  61. package/dist/src/cli/log-commands.js +127 -21
  62. package/dist/src/cli/notify-command.js +33 -18
  63. package/dist/src/cli/question-commands.js +13 -224
  64. package/dist/src/cli.js +78 -36
  65. package/dist/src/collections.js +66 -16
  66. package/dist/src/concurrency/check-permit.js +16 -17
  67. package/dist/src/concurrency/lock.js +6 -5
  68. package/dist/src/concurrency/provider-lease.js +69 -0
  69. package/dist/src/concurrency/semaphore.js +258 -36
  70. package/dist/src/config/config-aliases.js +106 -0
  71. package/dist/src/config/config-editable-keys.js +3 -3
  72. package/dist/src/config/config-fallbacks.js +379 -0
  73. package/dist/src/config/config-field-checks.js +19 -10
  74. package/dist/src/config/config-keys.js +160 -0
  75. package/dist/src/config/config-live.js +64 -12
  76. package/dist/src/config/config-schema.js +34 -22
  77. package/dist/src/config/config-validation.js +132 -48
  78. package/dist/src/config/config-views.js +107 -325
  79. package/dist/src/config/config-write.js +138 -177
  80. package/dist/src/config/config.js +60 -49
  81. package/dist/src/config/model-selector.js +28 -2
  82. package/dist/src/doctor/doctor-backlog.js +494 -68
  83. package/dist/src/doctor/doctor-checks.js +509 -58
  84. package/dist/src/doctor/doctor-holds.js +139 -78
  85. package/dist/src/doctor/doctor-install.js +30 -0
  86. package/dist/src/doctor/doctor-launch-services.js +2 -2
  87. package/dist/src/doctor/doctor-model-checks.js +549 -157
  88. package/dist/src/doctor/doctor-notify.js +2 -3
  89. package/dist/src/doctor/doctor-orphans.js +10 -9
  90. package/dist/src/doctor/doctor-render.js +36 -9
  91. package/dist/src/doctor/doctor.js +76 -25
  92. package/dist/src/doctor/image-probe.js +244 -0
  93. package/dist/src/doctor/probe.js +64 -0
  94. package/dist/src/events/event-format.js +219 -106
  95. package/dist/src/events/event-labels.js +116 -0
  96. package/dist/src/events/event-legacy.js +140 -0
  97. package/dist/src/events/event-read.js +58 -22
  98. package/dist/src/events/event-window.js +27 -7
  99. package/dist/src/events/events.js +18 -14
  100. package/dist/src/events/notify.js +80 -30
  101. package/dist/src/failure/failure-cluster.js +12 -9
  102. package/dist/src/failure/failure-data.js +96 -48
  103. package/dist/src/failure/failure-render.js +69 -58
  104. package/dist/src/failure/failure-state-change.js +33 -26
  105. package/dist/src/failure/time-spend.js +79 -59
  106. package/dist/src/files/file-queue.js +16 -4
  107. package/dist/src/files/files.js +18 -6
  108. package/dist/src/files/json-object.js +18 -3
  109. package/dist/src/files/tail.js +1 -1
  110. package/dist/src/gates/bootstrap-gates.js +5 -4
  111. package/dist/src/gates/budget-gates.js +250 -34
  112. package/dist/src/gates/disk-gate.js +18 -18
  113. package/dist/src/gates/gate-polls.js +117 -69
  114. package/dist/src/gates/maintenance-quota.js +29 -29
  115. package/dist/src/gates/pause-gates.js +11 -11
  116. package/dist/src/gates/readiness.js +2 -1
  117. package/dist/src/gates/role-cap-gates.js +16 -21
  118. package/dist/src/gates/startup-gate.js +75 -31
  119. package/dist/src/gates/streak-gate.js +18 -16
  120. package/dist/src/git/commit-message.js +6 -5
  121. package/dist/src/git/git-diff.js +16 -16
  122. package/dist/src/git/git-run.js +8 -8
  123. package/dist/src/git/git.js +79 -59
  124. package/dist/src/git/slots-state.js +14 -5
  125. package/dist/src/git/worktree-pool.js +25 -18
  126. package/dist/src/git/worktree-use.js +15 -14
  127. package/dist/src/git/worktree.js +18 -18
  128. package/dist/src/gui/gui-args.js +10 -14
  129. package/dist/src/gui/gui-backlog-commands.js +273 -0
  130. package/dist/src/gui/gui-command.js +1 -1
  131. package/dist/src/gui/gui-config-commands.js +440 -0
  132. package/dist/src/gui/gui-endpoint-commands.js +16 -716
  133. package/dist/src/gui/gui-endpoint-common.js +49 -0
  134. package/dist/src/gui/gui-endpoints.js +105 -54
  135. package/dist/src/gui/gui-operator-commands.js +145 -0
  136. package/dist/src/gui/gui-prompt-commands.js +234 -0
  137. package/dist/src/gui/gui-server.js +25 -8
  138. package/dist/src/gui/gui-team-commands.js +164 -0
  139. package/dist/src/gui/http-body.js +17 -1
  140. package/dist/src/history/history-data.js +179 -104
  141. package/dist/src/history/history-render.js +76 -0
  142. package/dist/src/history/history.js +29 -70
  143. package/dist/src/inbox/inbox-attachments.js +66 -10
  144. package/dist/src/inbox/inbox-cancel.js +7 -5
  145. package/dist/src/inbox/inbox-edit.js +1 -1
  146. package/dist/src/inbox/inbox-submit.js +44 -4
  147. package/dist/src/inbox/inbox-top.js +83 -0
  148. package/dist/src/inbox/inbox.js +8 -2
  149. package/dist/src/inbox/pending-prompt.js +1 -1
  150. package/dist/src/inbox/prompt-commands.js +114 -73
  151. package/dist/src/init/init-templates.js +16 -14
  152. package/dist/src/init/init.js +12 -11
  153. package/dist/src/loop/leftover.js +28 -27
  154. package/dist/src/loop/loop-fallback.js +2 -2
  155. package/dist/src/loop/loop-pi.js +104 -50
  156. package/dist/src/loop/loop-recovery.js +15 -15
  157. package/dist/src/loop/loop-state.js +130 -13
  158. package/dist/src/loop/loop-tick.js +190 -88
  159. package/dist/src/loop/loop.js +68 -36
  160. package/dist/src/loop/model-fallback.js +3 -2
  161. package/dist/src/loop/revision.js +16 -7
  162. package/dist/src/merge/backlog-conflicts.js +155 -0
  163. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  164. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  165. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  166. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  167. package/dist/src/merge/merge-drain.js +212 -0
  168. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
  169. package/dist/src/merge/merge-pipeline.js +99 -0
  170. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  171. package/dist/src/merge/merge-queue-render.js +75 -0
  172. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  173. package/dist/src/merge/merge-slot.js +340 -0
  174. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  175. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  176. package/dist/src/merge/merge-verify.js +212 -0
  177. package/dist/src/notes-cli.js +97 -0
  178. package/dist/src/operator/operator-commands.js +113 -45
  179. package/dist/src/operator/operator-intent.js +74 -53
  180. package/dist/src/operator/operator-requests.js +52 -52
  181. package/dist/src/operator/retire.js +47 -28
  182. package/dist/src/operator/revert.js +71 -0
  183. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  184. package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
  185. package/dist/src/orchestrator/orchestrator.js +149 -96
  186. package/dist/src/orchestrator/retention.js +29 -12
  187. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  188. package/dist/src/orchestrator/startup-migrations.js +40 -0
  189. package/dist/src/paths.js +124 -38
  190. package/dist/src/pi/pi-args.js +26 -3
  191. package/dist/src/pi/pi-backend.js +15 -23
  192. package/dist/src/pi/pi-bin.js +18 -12
  193. package/dist/src/pi/pi-event-line.js +17 -1
  194. package/dist/src/pi/pi-failure.js +87 -0
  195. package/dist/src/pi/pi-models.js +126 -38
  196. package/dist/src/pi/pi-run-result.js +1 -1
  197. package/dist/src/pi/pi-settings.js +86 -0
  198. package/dist/src/pi/pi-stream.js +131 -107
  199. package/dist/src/pi/pi-watchdogs.js +4 -3
  200. package/dist/src/pi-extension/bounded-output.js +20 -5
  201. package/dist/src/pi-extension/context-budget.js +3 -1
  202. package/dist/src/pi-extension/context-shake.js +54 -21
  203. package/dist/src/pi-extension/context-window.js +37 -0
  204. package/dist/src/pi-extension/full-output.js +2 -2
  205. package/dist/src/pi-extension/strip-thinking.js +80 -0
  206. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  207. package/dist/src/process/launch-services.js +11 -10
  208. package/dist/src/process/process-table.js +1 -1
  209. package/dist/src/process/process.js +7 -7
  210. package/dist/src/process/run-marker.js +29 -6
  211. package/dist/src/process/supervisor.js +36 -25
  212. package/dist/src/prompt/dialect.js +30 -10
  213. package/dist/src/prompt/gate-prompts.js +170 -67
  214. package/dist/src/prompt/principles.js +1 -0
  215. package/dist/src/prompt/prompt-followup.js +29 -20
  216. package/dist/src/prompt/prompt.js +249 -91
  217. package/dist/src/redeploy/redeploy-policy.js +10 -10
  218. package/dist/src/redeploy/redeploy.js +6 -4
  219. package/dist/src/redeploy/redeployer.js +63 -55
  220. package/dist/src/redeploy/self-reload.js +1 -1
  221. package/dist/src/report/report-data.js +259 -254
  222. package/dist/src/report/report-fold.js +353 -0
  223. package/dist/src/report/report-render.js +235 -44
  224. package/dist/src/request-timeouts.js +1 -1
  225. package/dist/src/review/known-flakes.js +34 -14
  226. package/dist/src/review/protected-edits.js +4 -3
  227. package/dist/src/review/review-followup.js +16 -4
  228. package/dist/src/review/review-precheck.js +14 -13
  229. package/dist/src/review/review-verdict.js +14 -0
  230. package/dist/src/review/review.js +55 -34
  231. package/dist/src/review/secret-leaks.js +127 -0
  232. package/dist/src/review/suite-rerun.js +4 -3
  233. package/dist/src/roles/legacy-role-ids.js +21 -0
  234. package/dist/src/roles/legacy-role-migration.js +62 -0
  235. package/dist/src/roles/loop-ids.js +19 -2
  236. package/dist/src/roles/role-catalog.js +109 -131
  237. package/dist/src/roles/role-guidance.js +64 -31
  238. package/dist/src/roles/role-labels.js +10 -0
  239. package/dist/src/roles/role-render.js +194 -18
  240. package/dist/src/roles/role-view.js +216 -17
  241. package/dist/src/roles/roles.js +16 -14
  242. package/dist/src/scheduling/backoff.js +29 -19
  243. package/dist/src/scheduling/claims.js +15 -8
  244. package/dist/src/scheduling/once-round.js +11 -11
  245. package/dist/src/scheduling/quiet-hours.js +25 -32
  246. package/dist/src/scheduling/scheduling.js +28 -25
  247. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  248. package/dist/src/status/status-budget.js +115 -0
  249. package/dist/src/status/status-data.js +72 -184
  250. package/dist/src/status/status-inbox.js +47 -0
  251. package/dist/src/status/status-install.js +27 -0
  252. package/dist/src/status/status-polls.js +50 -39
  253. package/dist/src/status/status-roles.js +105 -0
  254. package/dist/src/{fleet → team}/error-storm.js +10 -10
  255. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  256. package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
  257. package/dist/src/{fleet → team}/reclaim.js +5 -4
  258. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  259. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  260. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
  261. package/dist/src/text/datetime.js +42 -2
  262. package/dist/src/text/format.js +10 -1
  263. package/dist/src/text/markdown.js +1 -1
  264. package/dist/src/text/phrases.js +35 -34
  265. package/dist/src/text/suggest.js +3 -2
  266. package/dist/src/text/text.js +2 -2
  267. package/dist/src/tick/qa-coverage.js +2 -1
  268. package/dist/src/tick/stage-check.js +142 -25
  269. package/dist/src/tick/telemetry-digest.js +10 -9
  270. package/dist/src/tick/tick-apply.js +101 -72
  271. package/dist/src/tick/tick-detail-data.js +23 -22
  272. package/dist/src/tick/tick-detail.js +37 -24
  273. package/dist/src/tick/tick-finalize.js +29 -22
  274. package/dist/src/tick/tick-outcome.js +2 -1
  275. package/dist/src/tick/tick-prompt.js +94 -26
  276. package/dist/src/tick/tick-resume.js +29 -13
  277. package/dist/src/tick/tick-stage.js +67 -53
  278. package/dist/src/tick/tick-timing.js +60 -59
  279. package/dist/src/tick/tick-usage.js +53 -23
  280. package/dist/src/tick/tick-verdict.js +75 -10
  281. package/dist/src/ui/badges.js +113 -69
  282. package/dist/src/ui/gui/gui-client-boot.js +202 -29
  283. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  284. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  285. package/dist/src/ui/gui/gui-client-history.js +94 -37
  286. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  287. package/dist/src/ui/gui/gui-client-loops.js +90 -47
  288. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  289. package/dist/src/ui/gui/gui-client-model.js +60 -43
  290. package/dist/src/ui/gui/gui-client-operator.js +73 -43
  291. package/dist/src/ui/gui/gui-client-pending.js +34 -31
  292. package/dist/src/ui/gui/gui-client-report.js +330 -47
  293. package/dist/src/ui/gui/gui-client-settings.js +126 -18
  294. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  295. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
  296. package/dist/src/ui/gui/gui-client.js +26 -20
  297. package/dist/src/ui/gui/gui-page.js +18 -19
  298. package/dist/src/ui/gui/gui-styles.js +12 -4
  299. package/dist/src/ui/progress-data.js +5 -3
  300. package/dist/src/ui/status-model.js +176 -102
  301. package/dist/src/ui/status-payload.js +114 -36
  302. package/dist/src/ui/status-render.js +121 -52
  303. package/dist/src/ui/team-alerts.js +341 -0
  304. package/dist/src/ui/tick-progress-model.js +14 -14
  305. package/dist/src/ui/tone.js +9 -8
  306. package/dist/src/ui/transcript-tail.js +1 -0
  307. package/dist/src/ui/transcript.js +23 -16
  308. package/dist/src/ui/tui/tui-app.js +1 -0
  309. package/dist/src/ui/tui/tui-backlog.js +85 -11
  310. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  311. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  312. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  313. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  314. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  315. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  316. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  317. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  318. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  319. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  320. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  321. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  322. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  323. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  324. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  325. package/dist/src/ui/tui/tui-editors.js +152 -0
  326. package/dist/src/ui/tui/tui-frame.js +61 -57
  327. package/dist/src/ui/tui/tui-history.js +12 -7
  328. package/dist/src/ui/tui/tui-input.js +44 -50
  329. package/dist/src/ui/tui/tui-keymap.js +1 -0
  330. package/dist/src/ui/tui/tui-keys.js +87 -1821
  331. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  332. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  333. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  334. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  335. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  336. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  337. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  338. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  339. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  340. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  341. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  342. package/dist/src/ui/tui/tui-pane.js +52 -28
  343. package/dist/src/ui/tui/tui-panes.js +294 -0
  344. package/dist/src/ui/tui/tui.js +44 -26
  345. package/dist/src/verdict/fix-claim.js +56 -33
  346. package/dist/src/verdict/no-change.js +19 -0
  347. package/dist/src/verdict/refusal.js +43 -11
  348. package/dist/src/verdict/reply-contract.js +17 -5
  349. package/dist/src/version.js +3 -2
  350. package/package.json +4 -2
  351. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  352. package/dist/src/landing/backlog-conflicts.js +0 -116
  353. package/dist/src/landing/landing-drain.js +0 -204
  354. package/dist/src/landing/landing-pipeline.js +0 -96
  355. package/dist/src/landing/landing-queue-render.js +0 -56
  356. package/dist/src/landing/landing-slot.js +0 -256
  357. package/dist/src/landing/landing-vetting.js +0 -196
  358. package/dist/src/pi-extension/role-notes.js +0 -40
  359. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,8 +1,7 @@
1
1
  /** Building the environment for one claude run (plans/agent-backends.md "The claude
2
2
  * invocation", "Findings" 2 and 4) — pure scrub-and-set over a base environment, with no
3
3
  * subprocess. The caller layers the run-marker, the macOS LaunchServices preload, and the
4
- * notebook on first (src/pi/pi-backend.ts's `withNotesPath` shows the pi adapter doing the
5
- * same), then hands the result here.
4
+ * notebook on first (src/process/run-marker.ts's `withNotesPath`), then hands the result here.
6
5
  *
7
6
  * Inherited Claude Code session variables break auth: spawned from inside a Claude Code
8
7
  * session, a child that keeps `CLAUDECODE`, `CLAUDE_CODE_*`, `CLAUDE_AGENT_SDK_*` or the host's
@@ -13,7 +12,7 @@
13
12
  *
14
13
  * The set half disables auto-memory, backgrounded Bash tasks (finding 4: claude refuses a long
15
14
  * foreground command, backgrounds it, and abandons it when `-p` ends, which would report work
16
- * never verified), the auto-updater (a mid-fleet self-update must not swap the binary under a
15
+ * never verified), the auto-updater (a mid-team self-update must not swap the binary under a
17
16
  * run), telemetry, and error reporting; and raises the Bash timeouts so a slow suite runs in
18
17
  * the foreground. `BASH_MAX_TIMEOUT_MS` is derived from `quietTimeoutSeconds` so claude's own
19
18
  * Bash timeout fires before tumwater's quiet kill and the model sees a tool error rather than a
@@ -80,5 +79,10 @@ export function claudeEnv(base, opts) {
80
79
  env.DISABLE_AUTOUPDATER = "1";
81
80
  env.DISABLE_TELEMETRY = "1";
82
81
  env.DISABLE_ERROR_REPORTING = "1";
82
+ // A team config dir (plans/notes-command.md "Keeping funes out of team runs"): force claude's
83
+ // own config and transcripts under it, so a funes hook on ~/.claude/projects never indexes
84
+ // team runs. Unset leaves any inherited CLAUDE_CONFIG_DIR (or claude's default) untouched.
85
+ if (opts.configDir)
86
+ env.CLAUDE_CONFIG_DIR = opts.configDir;
83
87
  return env;
84
88
  }
@@ -14,7 +14,7 @@
14
14
  * on the seam's optional `warnings()` — src/agent/run-agent.ts forwards each as an
15
15
  * `agent_unknown_event` warning after the run. `cost_unknown` is recorded the same way when a
16
16
  * run produced no authoritative `run_cost`. */
17
- import { isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
17
+ import { canonicalUsageBlock, isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
18
18
  /** claude's tool names → the canonical pi names the readers already know. Glob/Grep are kept
19
19
  * lower-cased; MultiEdit collapses onto edit (pi has one edit tool). Anything else is
20
20
  * lower-cased so a future tool still reads as a name rather than as `undefined`. */
@@ -37,20 +37,10 @@ function str(value) {
37
37
  }
38
38
  /** claude's usage block → pi's canonical usage block, with `cost.total` 0 because the run's
39
39
  * authoritative cost rides the final `run_cost` event (a split-block message repeats its usage
40
- * on every event, so summing per event would double-count — the fold takes it once). */
40
+ * on every event, so summing per event would double-count — the fold takes it once). The
41
+ * wire-field reads stay here; the block's assembly is the shared canonicalUsageBlock. */
41
42
  function canonicalUsage(usage) {
42
- const input = nonNegativeNumber(usage?.input_tokens, 0);
43
- const cacheRead = nonNegativeNumber(usage?.cache_read_input_tokens, 0);
44
- const cacheWrite = nonNegativeNumber(usage?.cache_creation_input_tokens, 0);
45
- const output = nonNegativeNumber(usage?.output_tokens, 0);
46
- return {
47
- input,
48
- cacheRead,
49
- cacheWrite,
50
- output,
51
- totalTokens: input + cacheRead + cacheWrite + output,
52
- cost: { total: 0 },
53
- };
43
+ return canonicalUsageBlock(nonNegativeNumber(usage?.input_tokens, 0), nonNegativeNumber(usage?.cache_read_input_tokens, 0), nonNegativeNumber(usage?.cache_creation_input_tokens, 0), nonNegativeNumber(usage?.output_tokens, 0), 0);
54
44
  }
55
45
  /** One claude content block → one pi content block, or undefined for a block pi has no
56
46
  * vocabulary for (dropped, like any other unmapped native shape). `tool_use` becomes pi's
@@ -76,7 +66,7 @@ function canonicalBlock(block) {
76
66
  return {
77
67
  type: "toolCall",
78
68
  id: str(block.id) ?? "",
79
- name: Object.hasOwn(TOOL_NAMES, name) ? TOOL_NAMES[name] : name.toLowerCase(),
69
+ name: Object.hasOwn(TOOL_NAMES, name) ? (TOOL_NAMES[name]) : name.toLowerCase(),
80
70
  arguments: args,
81
71
  };
82
72
  }
@@ -347,7 +337,8 @@ export function claudeTranslator(opts = {}) {
347
337
  return out;
348
338
  }
349
339
  case "tool_progress":
350
- // A 30 s heartbeat of a running tool call: known, and the open call already tracks liveness.
340
+ // A 30 s heartbeat of a running tool call: known, and the open call already tracks
341
+ // liveness.
351
342
  return [];
352
343
  default:
353
344
  record(type ?? "unknown");
@@ -0,0 +1,107 @@
1
+ /** The seam's answer to "who in this team can read images" (plans/image-input.md
2
+ * "Capability"). One resolver, `seamImageInput`, is the only place that rule lives; the doctor
3
+ * (src/doctor/doctor-model-checks.ts) and the later reroute parts both read it, so the answer
4
+ * is never reached two ways. It lives at runtime level because it needs the backend registry:
5
+ * a claude seam is always capable through the AgentBackend.imageInput hook, a pi seam reads the
6
+ * model's `input` in models.json, and any other backend answers null unless it declares a hook. */
7
+ import { configForRole, roleSeamTier, withModelOverrides } from "../config/config-views.js";
8
+ import { BORROW_ORDER, resolveTierFallbacks } from "../config/config-fallbacks.js";
9
+ import { seamAgent } from "../config/model-selector.js";
10
+ import { modelLabel } from "./backend-traits.js";
11
+ import { piModelImageInput, piPairFree, piModelsPath, readPiProviders } from "../pi/pi-models.js";
12
+ import { agentBackend } from "./agent-backend.js";
13
+ import { imageProbeEntryFresh, imageProbeKey } from "../doctor/image-probe.js";
14
+ /** One seam's image-input capability (plans/image-input.md "Capability"): `true` when the
15
+ * seam's backend declares image support, `false` when it is text-only, `null` when it cannot
16
+ * be answered from what the team knows (an unknown pair, a backend with no hook and no
17
+ * catalog entry — the doctor's `tier models` check already fails such pairs).
18
+ *
19
+ * The backend bookends: a backend with an `imageInput` hook (claude → true) owns the answer;
20
+ * the default pi backend leaves it to models.json's `input` field, where absent means text-only
21
+ * (pi's own default for a custom model). `probes` is the doctor's live-probe cache (part 2/5): a
22
+ * claimed pi seam whose probe FAILED reads false — a claim is only as good as the verified
23
+ * answer — while a cached pass (or an unprobed claim) stands on the catalog. claude seams are
24
+ * never probed, so the hook answer is never overridden. */
25
+ export function seamImageInput(seam, providers, probes) {
26
+ const agent = seamAgent(seam);
27
+ const backend = agentBackend(agent);
28
+ // A backend with an imageInput hook owns the answer outright (claude: true for every model).
29
+ if (backend?.imageInput !== undefined)
30
+ return backend.imageInput(seam.model);
31
+ // An unknown backend without a hook has no catalog to consult — unknown, never assume.
32
+ if (agent !== "pi")
33
+ return null;
34
+ const base = piModelImageInput(providers, seam.provider, seam.model);
35
+ if (base !== true)
36
+ return base;
37
+ // A claimed pi seam whose live probe failed (fresh, claim unchanged) reads false. Only pi is
38
+ // probed, so the probe key carries the pi agent.
39
+ const entry = probes?.[imageProbeKey(agent, seam.provider, seam.model)];
40
+ if (entry?.verdict === "fail" && imageProbeEntryFresh(entry))
41
+ return false;
42
+ return true;
43
+ }
44
+ /** The image-capable reroute target for `role`, or null when no peer tier can read images
45
+ * (plans/image-input.md "Reroute target"). Walks the role's seam tier through BORROW_ORDER,
46
+ * skipping the tier itself — small → default → strong, default → strong → small, strong →
47
+ * default (a strong-tier seam never drops to small, the same rule as the budget fallback) —
48
+ * and takes the first tier whose model reads images. `regime` picks the pair set (see
49
+ * RerouteRegime); `modelsPath` and `account` are injectable so tests need no real pi install
50
+ * or account. */
51
+ export function imageReroute(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, probes, providers = null) {
52
+ const tier = roleSeamTier(config, role);
53
+ const candidates = BORROW_ORDER[tier].filter((t) => t !== tier);
54
+ if (candidates.length === 0)
55
+ return null;
56
+ const providerMap = providers ?? readPiProviders(modelsPath);
57
+ const capable = (rc) => seamImageInput(rc, providerMap, probes) === true;
58
+ if (regime === "primary") {
59
+ for (const t of candidates) {
60
+ const rc = withModelOverrides(config, t, {});
61
+ if (capable(rc))
62
+ return { tier: t, config: rc };
63
+ }
64
+ return null;
65
+ }
66
+ // Fallback regime: only the tiers' free fallback pairs qualify (resolveTierFallbacks over the
67
+ // price check), never a priced primary model — a reroute must not spend past the cap.
68
+ const resolved = resolveTierFallbacks(config, (p) => piPairFree(providerMap, p.provider, p.model, account));
69
+ for (const t of candidates) {
70
+ const pair = resolved[t].pair;
71
+ if (!pair)
72
+ continue;
73
+ const rc = withModelOverrides(config, t, {
74
+ provider: pair.provider,
75
+ model: pair.model,
76
+ thinking: pair.thinking,
77
+ });
78
+ if (capable(rc))
79
+ return { tier: t, config: rc };
80
+ }
81
+ return null;
82
+ }
83
+ /** The config of imageReroute, for callers that want only the target model. */
84
+ export function imageRerouteTarget(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, probes, providers = null) {
85
+ return imageReroute(config, role, regime, modelsPath, account, probes, providers)?.config ?? null;
86
+ }
87
+ /** A seam's model as one readable token, the same label the doctor's `image input` check uses
88
+ * (`claude/<id>` for a claude seam, `provider/model` for pi). The one home of the label, so
89
+ * the surface strings (the images alert, the role page, the status rows) and the doctor report
90
+ * cannot drift, and delegates to backend-traits' modelLabel. */
91
+ export function seamImageLabel(agent, provider, model) {
92
+ return modelLabel(agent, provider, model);
93
+ }
94
+ /** Compute RoleImageInfo for one loop. `modelsPath` and `probes` are injectable; the providers
95
+ * catalog is read (stat-cached) here and also handed to imageReroute so a poll that asks many
96
+ * loops still parses the catalog once. All reads mirror the doctor's `image input` check, so
97
+ * the surface and the check agree about capability. */
98
+ export function roleImageInfo(config, role, modelsPath = piModelsPath(), probes) {
99
+ const providers = readPiProviders(modelsPath);
100
+ const rc = configForRole(config, role);
101
+ const imageInput = seamImageInput(rc, providers, probes);
102
+ const rr = imageReroute(config, role, "primary", modelsPath, null, probes, providers);
103
+ const imageRerouteStr = rr
104
+ ? `${rr.tier} (${seamImageLabel(rr.config.agent, rr.config.provider, rr.config.model)})`
105
+ : null;
106
+ return { imageInput, imageReroute: imageRerouteStr };
107
+ }
@@ -0,0 +1,41 @@
1
+ // vocabulary: plain
2
+ /** Installing the per-team `tumwater-notes` shim every backend's run finds on its PATH
3
+ * (plans/notes-command.md "Reaching it from a run"): run-agent calls `ensureNotesShim(root)`
4
+ * before building each invocation, and the backend prepends the returned dir to the run's
5
+ * PATH (agentBaseEnv's `binDir`, src/process/run-marker.ts). The shim is a two-line `sh`
6
+ * script that execs the compiled notes CLI, so a model's shell — pi's, Claude Code's or
7
+ * OpenCode's — runs the same command on every backend.
8
+ *
9
+ * The `<dist>` in the exec path is the shared distDir (src/build/build-info.ts), derived from
10
+ * this module's own location so a staged build (.tumwater/build/<sha>) points its shim at its
11
+ * own copy rather than the installed package's. The script is written
12
+ * atomically and made executable, only when its content differs, so a redeploy re-points it
13
+ * without churning the file. */
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { distDir } from "../build/build-info.js";
17
+ import { writeTextAtomic } from "../files/files.js";
18
+ import { notesShimDir } from "../paths.js";
19
+ /** The shim's file name — the command name every run sees on its PATH. */
20
+ export const NOTES_SHIM_NAME = "tumwater-notes";
21
+ /** Install the `tumwater-notes` shim under `<root>/.tumwater/bin` and return that directory.
22
+ * The shim is written atomically with mode 0755, only when its content differs from what is
23
+ * already there (a redeploy that changes `<dist>` rewrites it). Never throws: a failure to
24
+ * write is the caller's to absorb — run-agent logs one warning and runs without the shim. */
25
+ export function ensureNotesShim(root) {
26
+ const dir = notesShimDir(root);
27
+ const target = path.join(dir, NOTES_SHIM_NAME);
28
+ const content = `#!/bin/sh\nexec "${process.execPath}" "${path.join(distDir(), "src", "notes-cli.js")}" "$@"\n`;
29
+ let needsWrite = true;
30
+ try {
31
+ needsWrite = fs.readFileSync(target, "utf8") !== content;
32
+ }
33
+ catch {
34
+ // Absent or unreadable — write it.
35
+ }
36
+ if (needsWrite) {
37
+ writeTextAtomic(target, content);
38
+ }
39
+ fs.chmodSync(target, 0o755);
40
+ return dir;
41
+ }
@@ -0,0 +1,61 @@
1
+ // vocabulary: plain
2
+ /** Building one OpenCode run's command line (plans/opencode-backend.md "The invocation") —
3
+ * pure argv construction plus the prompt-on-stdin pairing, with no subprocess, backend
4
+ * registration, or session database. Part 5/7 wires this into the adapter; this module turns
5
+ * its inputs (the resolved model, the catalog's variant, the session choice, the session
6
+ * name, and the configured passthrough args) into the invocation's argv and stdin.
7
+ *
8
+ * The environment is built separately (opencode-env.ts) so each concern has one module.
9
+ *
10
+ * `.args` passthrough: a passthrough arg equal to a flag the harness itself sets would be
11
+ * appended AFTER the harness's own copy and OpenCode's parser is last-wins, so it would
12
+ * silently override the resolved model, the session choice, or the isolation flags. Each is
13
+ * rejected by opencodeArgs with the offending arg named, matching the same rejection in
14
+ * claudeArgs and the reserved set from plans/opencode-backend.md "Config and routing". */
15
+ /** Flags the harness invocation sets. A passthrough arg equal to one of these is rejected by
16
+ * opencodeArgs with the offending arg named, so an operator's `backends.opencode.args` can
17
+ * never silently override the resolved model, the session choice, or the isolation flags. This
18
+ * is the reserved set from plans/opencode-backend.md "Config and routing"; part 3 adds
19
+ * `--title`, which tumwater now sets (finding 6). */
20
+ import { appendPassthroughArgs } from "../agent-args.js";
21
+ export const HARNESS_OPENCODE_FLAGS = new Set([
22
+ "run",
23
+ "--format",
24
+ "--model",
25
+ "-m",
26
+ "--session",
27
+ "-s",
28
+ "--continue",
29
+ "-c",
30
+ "--fork",
31
+ "--standalone",
32
+ "--server",
33
+ "--attach",
34
+ "--auto",
35
+ "--yolo",
36
+ "--dangerously-skip-permissions",
37
+ "--agent",
38
+ "--thinking",
39
+ "--dir",
40
+ "--title",
41
+ ]);
42
+ /** Build one run's opencode argv and stdin. The prompt is never an argv element (OpenCode
43
+ * reads a non-TTY stdin to EOF, and the runner closes it). A model with no `/` names a model
44
+ * on OpenCode's own gateway and gets the `opencode/` provider; anything with a slash is the
45
+ * provider-qualified id and passes as is. Throws on a passthrough arg that repeats a harness
46
+ * flag, since OpenCode's last-wins parser would otherwise silently override what the harness
47
+ * asked for. */
48
+ /** The opencode-namespaced model id: a bare id (no `/`) names a model on OpenCode's own
49
+ * gateway and gets the `opencode/` provider; an id bearing a slash is provider-qualified and
50
+ * passes as-is. The single home of this routing rule — shared by the argv builder
51
+ * (opencodeArgs, here) and the catalog's cache key (opencode-catalog.ts's opencodeModelKey) —
52
+ * so the id a run is invoked with always matches the id a run is priced by. */
53
+ export function opencodeQualifiedModel(model) {
54
+ return model.includes("/") ? model : `opencode/${model}`;
55
+ }
56
+ export function opencodeArgs(opts) {
57
+ const model = opencodeQualifiedModel(opts.model) + (opts.variant !== undefined ? `#${opts.variant}` : "");
58
+ const args = ["run", "--standalone", "--format", "json", "--auto", "--model", model, "--session", opts.sessionId, "--title", opts.title];
59
+ appendPassthroughArgs(args, opts.args, HARNESS_OPENCODE_FLAGS, "opencodeArgs");
60
+ return { args, stdin: opts.prompt };
61
+ }
@@ -0,0 +1,255 @@
1
+ // vocabulary: plain
2
+ /** The opencode adapter for the agent seam (plans/opencode-backend.md "The invocation",
3
+ * "Sessions", §6). It shapes one run's argv/stdin/env, translates opencode's `run --format
4
+ * json` into the canonical stream, runs the authoritative session export after the process
5
+ * exits, and classifies the exit's stderr — everything backend-specific. src/agent/run-agent.ts
6
+ * owns the spawn and records this run in the session ledger; opencode mints its own session id
7
+ * (a `ses_…` id, or the resumed record's id), which `invocation` returns so the ledger can
8
+ * carry it.
9
+ *
10
+ * Sessions resume by id from the cwd the session started in, out of the run's shared database
11
+ * (opencodeDbPath). A continue takes the ledger's newest record of this run's kind and resumes
12
+ * only when it is opencode's, has this cwd, and `opencode session export <id> --standalone`
13
+ * actually finds it — `--session` silently creates a missing session, so the existence check is
14
+ * the only thing that stops a follow-up running against an empty session. Anything else mints a
15
+ * fresh id (design §4).
16
+ *
17
+ * Registration lives in src/agent/run-agent.ts beside the pi and claude adapters; a test selects
18
+ * opencode by setting `config.agent = "opencode"` on a run. A run carrying a notebook gets the
19
+ * notes path in its environment (the `tumwater-notes` command on PATH reads it), as the pi and
20
+ * claude adapters do. */
21
+ import { spawnSync } from "node:child_process";
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+ import { loadConfigSafe } from "../../config/config.js";
25
+ import { resolvedAgentConfig } from "../../config/config-views.js";
26
+ import { agentBaseEnv, withNotesPath } from "../../process/run-marker.js";
27
+ import { resolveBackendBin } from "../../pi/pi-bin.js";
28
+ import { opencodeDbPath, teamRootOf } from "../../paths.js";
29
+ import { resumableSession } from "../session-ledger.js";
30
+ import { opencodeArgs } from "./opencode-args.js";
31
+ import { opencodeConfigContent, operatorMcpNames } from "./opencode-config.js";
32
+ import { outputLimits } from "../../pi-extension/bounded-output.js";
33
+ import { seamContextWindow } from "../../pi/pi-models.js";
34
+ import { opencodeModelKey, readOpencodeCache } from "./opencode-catalog.js";
35
+ import { opencodeEnv } from "./opencode-env.js";
36
+ import { mintOpencodeSessionId } from "./opencode-session-id.js";
37
+ import { opencodeExportLines, opencodeTranslator } from "./opencode-stream.js";
38
+ /** How long a post-run `opencode session export` may take before it gives up (design §6): the
39
+ * export measured 0.18 s live, so 10 s is a generous bound on a slow or wedged CLI. Used for
40
+ * both the resume-existence check and the reconciling export. */
41
+ const EXPORT_TIMEOUT_MS = 10_000;
42
+ /** The minimum OpenCode CLI version this harness accepts (plans/opencode-backend.md "Which
43
+ * OpenCode", part 7/7): 2.0.32, the version H1 verified live. V1 is maintenance-only and has
44
+ * the git-repo hang (issue 55037), so doctor fails an installed CLI below this and names
45
+ * `brew install opencode`. */
46
+ export const OPENCODE_MIN_VERSION = "2.0.32";
47
+ /** The repository's root (initial) commit hash, or undefined when git cannot answer (not a
48
+ * repo, or git missing). `git rev-list --max-parents=0 HEAD` is the same read OpenCode makes
49
+ * to key its shell spill by the tree's root commit. Read-only. */
50
+ function rootCommitOf(root) {
51
+ try {
52
+ const res = spawnSync("git", ["-C", root, "rev-list", "--max-parents=0", "HEAD"], {
53
+ encoding: "utf8",
54
+ timeout: 5_000,
55
+ stdio: ["ignore", "pipe", "ignore"],
56
+ });
57
+ const line = res.stdout?.trim();
58
+ return line !== undefined && /^[0-9a-f]{40}$/i.test(line) ? line : undefined;
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ /** The tree's shell spill dir OpenCode writes (finding 17, part 7/7):
65
+ * `~/.local/share/opencode/shell/<root commit>/`, keyed by the repository's root commit hash and
66
+ * OUTSIDE `.tumwater/`, so only a `session delete` or an explicit spill prune reaches it. Empty
67
+ * string when the root commit cannot be read — callers then skip spill work (doctor reports no
68
+ * spill dir; retention prunes no spill files). */
69
+ export function opencodeSpillDir(root) {
70
+ const commit = rootCommitOf(root);
71
+ if (commit === undefined)
72
+ return "";
73
+ return path.join(os.homedir(), ".local", "share", "opencode", "shell", commit);
74
+ }
75
+ /** The catalog cache key for a run's resolved model id (part 6d/7): a bare gateway id gets the
76
+ * `opencode/` provider prefix, a provider-qualified id passes as is — the same rule
77
+ * opencodeArgs uses to build the `--model` arg and opencode-catalog.ts uses to key the cache,
78
+ * so an invocation's lookup always matches the key a run is priced and billed by. */
79
+ function catalogModelKey(model) {
80
+ return opencodeModelKey(model);
81
+ }
82
+ /** Run `opencode session export <id> --standalone` (the same database the run used, bounded at
83
+ * 10 s) and return whether it reported success — the existence check that decides a resume.
84
+ * Nonzero exit, a timeout, or a spawn failure all read as "no such session", which is the safe
85
+ * direction: `--session` would otherwise silently create a missing session. */
86
+ function opencodeSessionExists(id, env, bin, cwd) {
87
+ try {
88
+ const res = spawnSync(bin, ["session", "export", id, "--standalone"], {
89
+ env,
90
+ cwd,
91
+ timeout: EXPORT_TIMEOUT_MS,
92
+ stdio: "ignore",
93
+ });
94
+ return res.status === 0;
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
100
+ /** Run the reconciling export after a successful run and return its two canonical lines
101
+ * (`run_cost`, `run_usage`) from the session's `info`. Anything that goes wrong — a spawn or
102
+ * timeout, a nonzero exit, unparseable or malformed output — returns `[]`, and the caller
103
+ * (afterRun) reports `opencode_usage_partial` so the run keeps its streamed numbers. */
104
+ function opencodeExport(result) {
105
+ let res;
106
+ try {
107
+ res = spawnSync(result.bin, ["session", "export", result.id, "--standalone"], {
108
+ env: result.env,
109
+ cwd: result.cwd,
110
+ timeout: EXPORT_TIMEOUT_MS,
111
+ encoding: "utf8",
112
+ });
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ if (res.status !== 0 || !res.stdout)
118
+ return [];
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(res.stdout);
122
+ }
123
+ catch {
124
+ return [];
125
+ }
126
+ return opencodeExportLines(parsed);
127
+ }
128
+ /** The session id this run uses: for a continue, the ledger's newest record of this kind when it
129
+ * is opencode's, has this cwd, and the export finds the session; otherwise a freshly minted id.
130
+ * Returns the id; the runner records it in the ledger. */
131
+ function resolveOpencodeSession(run, env) {
132
+ if (run.continueSession) {
133
+ const record = resumableSession(run.sessionDir, {
134
+ kind: run.kind,
135
+ agent: "opencode",
136
+ cwd: run.cwd,
137
+ });
138
+ if (record !== null && opencodeSessionExists(record.id, env, run.agentBin, run.cwd)) {
139
+ return { sessionId: record.id };
140
+ }
141
+ }
142
+ return { sessionId: mintOpencodeSessionId() };
143
+ }
144
+ /** Classify opencode's stderr: on a nonzero exit, a free-tier refusal is a provider failure
145
+ * (moves the job to its fallback) and an unknown model or variant is a permanent config error.
146
+ * A run the harness killed has its own cause and is never classified here. The structured
147
+ * signals (the stream's `error` lines) are classified by the translator; this is the stderr
148
+ * half for a CLI that died before emitting one. */
149
+ function classifyOpencodeExit(info) {
150
+ if (info.killed)
151
+ return {};
152
+ if (info.code !== 0 && /FreeTierError/i.test(info.stderr)) {
153
+ return { transientBackend: true, backendKind: "server" };
154
+ }
155
+ if (info.code !== 0 && /Model unavailable|Variant unavailable/i.test(info.stderr)) {
156
+ return { configError: true };
157
+ }
158
+ return {};
159
+ }
160
+ /** The agent-backend seam's opencode adapter. */
161
+ export const opencodeBackend = {
162
+ id: "opencode",
163
+ label: "OpenCode",
164
+ defaultBin: "opencode",
165
+ binEnv: "TUMWATER_OPENCODE_BIN",
166
+ resolveBin(config) {
167
+ return resolveBackendBin(config, this);
168
+ },
169
+ invocation(run) {
170
+ // The shared database and injected config are per-root, built the same way for every run of
171
+ // this team so a resume's export check reads the same OPENCODE_DB the original run wrote.
172
+ const root = teamRootOf(run.sessionDir);
173
+ const dbPath = opencodeDbPath(root);
174
+ // Window-scaled tool-output caps (part 6f/7): OpenCode's `tool_output` default is about
175
+ // three times pi's tuned budgets, so every turn would re-send far more tool output than a
176
+ // pi run — critical on the 200K free model's window. Cap it at pi's `outputLimits(window)
177
+ // .bash` (16,000 at 128k and above, scaled down below), with the ~40 chars/line line cap
178
+ // pi's "12k chars ≈ 300 lines" read budget implies. An unknown window (no catalog entry)
179
+ // keeps the 128k values. The window is part 6e/7's `seamContextWindow` for this run's
180
+ // model.
181
+ const maxBytes = outputLimits(seamContextWindow({ agent: "opencode", model: run.config.model }, root)).bash;
182
+ const configContent = opencodeConfigContent({
183
+ cwd: run.cwd,
184
+ operatorMcpNames: operatorMcpNames(process.env, os.homedir()),
185
+ toolOutput: { maxLines: Math.round(maxBytes / 40), maxBytes },
186
+ });
187
+ // The notebook path rides beside the run marker, as the pi and claude adapters' envs do
188
+ // (withNotesPath); from part 2/4 the run saves its note through the `tumwater-notes` command
189
+ // on PATH, so OpenCode configures no tumwater MCP server for notes.
190
+ const base = withNotesPath(agentBaseEnv(run.runMarker, undefined, run.binDir), run.notesPath);
191
+ const env = opencodeEnv(base, { dbPath, configContent });
192
+ const { sessionId } = resolveOpencodeSession(run, env);
193
+ // The thinking variant (part 6d/7): ride the catalog's effort levels for the resolved
194
+ // model as a `#level` suffix, and drop a level the catalog does not list — an unlisted
195
+ // variant would otherwise be a permanent config error from OpenCode (finding 9).
196
+ const model = run.config.model ?? "";
197
+ const thinking = run.config.thinking;
198
+ const entry = readOpencodeCache(root).models[catalogModelKey(model)];
199
+ const variant = thinking !== undefined && thinking !== "" && entry?.efforts?.includes(thinking) ? thinking : undefined;
200
+ const { args, stdin } = opencodeArgs({
201
+ model,
202
+ ...(variant !== undefined ? { variant } : {}),
203
+ sessionId,
204
+ title: run.sessionName,
205
+ args: resolvedAgentConfig(run.config, "opencode").args,
206
+ prompt: run.prompt,
207
+ });
208
+ return { args, stdin, env, sessionId };
209
+ },
210
+ translator() {
211
+ return opencodeTranslator();
212
+ },
213
+ imageInput(model) {
214
+ // Answer from the catalog cache (part 6d/7): a model reads images when its catalog entry
215
+ // says so; a model missing from the cache answers false — the safe direction, so a blind
216
+ // image read is never silently assumed readable. The cache is per-root, found by walking up
217
+ // from the process's working directory to its `.tumwater`, the same root the runs' shared
218
+ // database and cache live under.
219
+ if (model === undefined)
220
+ return false;
221
+ return readOpencodeCache(teamRootOf(process.cwd())).models[catalogModelKey(model)]?.images === true;
222
+ },
223
+ resumable(dir, opts) {
224
+ const record = resumableSession(dir, { kind: opts.kind, agent: "opencode", cwd: opts.cwd });
225
+ if (record === null)
226
+ return false;
227
+ const root = teamRootOf(dir);
228
+ const dbPath = opencodeDbPath(root);
229
+ const env = { ...process.env, OPENCODE_DB: dbPath };
230
+ // Resolve the binary exactly as the run's invocation does — same env → backends.opencode.bin
231
+ // → default precedence — so a configured custom binary's resume guard agrees with the
232
+ // `run.agentBin` the invocation runs (they must not disagree, or a follow-up is silently
233
+ // skipped). The team's config is loaded from the root `dir` lives under.
234
+ const { config } = loadConfigSafe(root);
235
+ const bin = config !== undefined ? opencodeBackend.resolveBin(config).bin : "opencode";
236
+ return opencodeSessionExists(record.id, env, bin, opts.cwd ?? path.dirname(dir));
237
+ },
238
+ afterRun(run, invocation, translator) {
239
+ if (invocation.sessionId === undefined)
240
+ return [];
241
+ const lines = opencodeExport({
242
+ id: invocation.sessionId,
243
+ env: invocation.env,
244
+ bin: run.agentBin,
245
+ cwd: run.cwd,
246
+ });
247
+ if (lines.length === 0) {
248
+ // The export is authoritative, so its loss is worth one warning: the run keeps its
249
+ // streamed numbers and the caller surfaces `opencode_usage_partial` in the event feed.
250
+ translator.noteOpencodeUsagePartial();
251
+ }
252
+ return lines;
253
+ },
254
+ classifyExit: classifyOpencodeExit,
255
+ };