tumwater 0.1.2 → 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 (361) hide show
  1. package/README.md +74 -108
  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-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -16,32 +16,47 @@
16
16
  import fs from "node:fs";
17
17
  import os from "node:os";
18
18
  import path from "node:path";
19
- import { fileURLToPath } from "node:url";
20
19
  import { randomUUID } from "node:crypto";
21
20
  import { resolvedAgentConfig } from "../../config/config-views.js";
22
21
  import { MODEL_TIERS } from "../../config/config-schema.js";
23
22
  import { routeModel } from "../../config/model-selector.js";
24
- import { agentBaseEnv } from "../../process/run-marker.js";
23
+ import { agentBaseEnv, withNotesPath } from "../../process/run-marker.js";
25
24
  import { resolveBackendBin } from "../../pi/pi-bin.js";
25
+ import { expandHomeTilde } from "../../files/files.js";
26
26
  import { claudeArgs } from "./claude-args.js";
27
27
  import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
28
28
  import { claudeTranslator } from "./claude-stream.js";
29
29
  import { resumableSession } from "../session-ledger.js";
30
- /** claude's per-cwd project directory name under `~/.claude/projects/`: every character that
31
- * is not a letter or digit becomes `-`. Exported with `claudeTranscriptPath` so the PATH fake
32
- * and the tests create and look for exactly the file the resume check reads. */
30
+ /** claude's per-cwd project directory name under the claude config dir's `projects/`:
31
+ * every character that is not a letter or digit becomes `-`. Exported with
32
+ * `claudeTranscriptPath` so the PATH fake and the tests create and look for exactly the file
33
+ * the resume check reads. */
33
34
  export function claudeProjectSlug(cwd) {
34
35
  return cwd.replace(/[^A-Za-z0-9]/g, "-");
35
36
  }
37
+ /** claude's config directory: `$CLAUDE_CONFIG_DIR` when non-empty (a leading `~` expands
38
+ * against the home dir), else `~/.claude`. A configured team `configDir` (the expanded
39
+ * `backends.claude.configDir`) wins over any inherited env var — the team's own directory is
40
+ * never overridden by the operator's shell. This mirrors claude's own lookup and pi's
41
+ * `piAgentDir`; exported so doctor reads the same dir the adapter writes transcripts into. */
42
+ export function claudeConfigDir(env = process.env, homeDir = os.homedir(), configured) {
43
+ if (configured)
44
+ return expandHomeTilde(configured, homeDir);
45
+ const dir = env.CLAUDE_CONFIG_DIR;
46
+ if (!dir)
47
+ return path.join(homeDir, ".claude");
48
+ return expandHomeTilde(dir, homeDir);
49
+ }
36
50
  /** The transcript file claude writes for a session started in `cwd`: its resume check must find
37
51
  * a real file before passing `--resume`. claude's own layout is
38
- * `~/.claude/projects/<slug>/<sessionId>.jsonl`; exported so the fake and the resume logic
39
- * cannot drift. */
40
- export function claudeTranscriptPath(cwd, id) {
41
- return path.join(os.homedir(), ".claude", "projects", claudeProjectSlug(cwd), `${id}.jsonl`);
52
+ * `<config>/projects/<slug>/<sessionId>.jsonl`; exported so the fake and the resume logic
53
+ * cannot drift. `configDir` is the configured team dir, threaded through so the resume check
54
+ * looks where the run's own env will write. */
55
+ export function claudeTranscriptPath(cwd, id, env = process.env, homeDir = os.homedir(), configDir) {
56
+ return path.join(claudeConfigDir(env, homeDir, configDir), "projects", claudeProjectSlug(cwd), `${id}.jsonl`);
42
57
  }
43
58
  /** The CLI version every gated claude model needs (plans/agent-backends.md finding 9): on
44
- * 2.1.216 Opus 5.5 is refused outright and Haiku 5.5 is mispriced about 250×, and fleet runs
59
+ * 2.1.216 Opus 5.5 is refused outright and Haiku 5.5 is mispriced about 250×, and team runs
45
60
  * set DISABLE_AUTOUPDATER so they never update themselves. The gated models are the three
46
61
  * tier defaults, derived from AGENT_DEFAULT_MODELS through the same routing the config uses
47
62
  * rather than listed again, so this table cannot drift from the models a seam resolves. */
@@ -65,37 +80,27 @@ function classifyClaudeExit(info) {
65
80
  }
66
81
  return {};
67
82
  }
68
- /** The inline `--mcp-config` JSON that runs the bundled role-notes server (part 5/7): the
69
- * same dist module pi's extension resolves to, spawned by the same node running this harness
70
- * so it needs no PATH lookup. */
71
- function roleNotesMcpConfig() {
72
- const server = fileURLToPath(new URL("../mcp/role-notes-server.js", import.meta.url));
73
- return JSON.stringify({ mcpServers: { tumwater: { command: process.execPath, args: [server] } } });
74
- }
75
83
  /** The ledger record this run may resume, or null: the newest record of `opts.kind` for
76
84
  * claude, at `opts.cwd` when one is given, whose transcript file exists. Shared by
77
85
  * `resolveClaudeSession` and the backend's `resumable` hook, so the resume guard and the
78
86
  * invocation can never drift on what counts as resumable. */
79
- function claudeResumableRecord(dir, opts) {
87
+ function claudeResumableRecord(dir, opts, configDir) {
80
88
  const record = resumableSession(dir, opts);
81
89
  if (record === null)
82
90
  return null;
83
91
  if (opts.cwd !== undefined && record.cwd !== opts.cwd)
84
92
  return null;
85
- if (!fs.existsSync(claudeTranscriptPath(record.cwd, record.id)))
93
+ if (!fs.existsSync(claudeTranscriptPath(record.cwd, record.id, process.env, os.homedir(), configDir)))
86
94
  return null;
87
95
  return record;
88
96
  }
89
97
  /** The session id this run uses: for a continue, the ledger's newest record of this kind when
90
98
  * it is claude's, this cwd's, and its transcript file exists; otherwise a fresh UUID. Returns
91
- * the id and the `--resume` id when resuming. */
92
- function resolveClaudeSession(run) {
99
+ * the id and the `--resume` id when resuming. `configDir` is the configured team dir, so the
100
+ * resume check looks where the run's own env will write transcripts. */
101
+ function resolveClaudeSession(run, configDir) {
93
102
  if (run.continueSession) {
94
- const record = claudeResumableRecord(run.sessionDir, {
95
- kind: run.kind,
96
- agent: "claude",
97
- cwd: run.cwd,
98
- });
103
+ const record = claudeResumableRecord(run.sessionDir, { kind: run.kind, agent: "claude", cwd: run.cwd }, configDir);
99
104
  if (record !== null)
100
105
  return { sessionId: record.id, resumeSessionId: record.id };
101
106
  }
@@ -117,35 +122,35 @@ export const claudeBackend = {
117
122
  if (run.config.model === undefined) {
118
123
  throw new Error("claude backend: no model resolved for this run");
119
124
  }
120
- const { sessionId, resumeSessionId } = resolveClaudeSession(run);
125
+ // The configured team config dir (plans/notes-command.md "Keeping funes out of team runs"):
126
+ // the run's env, its resume lookup, and the account seed all follow it; unset, nothing changes.
127
+ const claudeCfg = resolvedAgentConfig(run.config, "claude");
128
+ const configDir = claudeCfg.configDir;
129
+ const expandedConfigDir = configDir !== undefined ? claudeConfigDir(process.env, os.homedir(), configDir) : undefined;
130
+ const { sessionId, resumeSessionId } = resolveClaudeSession(run, configDir);
121
131
  const { args, stdin } = claudeArgs({
122
132
  cwd: run.cwd,
123
133
  model: run.config.model,
124
134
  thinking: run.config.thinking,
125
135
  ...(resumeSessionId !== undefined ? { resumeSessionId } : { sessionId }),
126
- // The operator's `agents.claude.args` passthrough, appended after the harness flags (the
136
+ // The operator's `backends.claude.args` passthrough, appended after the harness flags (the
127
137
  // builder rejects any entry that repeats a harness-managed flag). resolvedAgentConfig
128
138
  // owns the view so a caller cannot mutate the loaded config through the returned array.
129
- args: resolvedAgentConfig(run.config, "claude").args,
139
+ args: claudeCfg.args,
130
140
  prompt: run.prompt,
131
141
  // The caller computes the remaining daily budget for an API-billed claude run and the
132
142
  // adapter forwards it; a subscription run or the director passes none (part 7a/7).
133
143
  ...(run.maxBudgetUsd !== undefined ? { maxBudgetUsd: run.maxBudgetUsd } : {}),
134
- // claude does not load the bundled pi extensions, so a run carrying a notebook gets the
135
- // role_notes MCP server instead. Passed on EVERY claude invocation that carries a
136
- // notebook — fresh or resumed, a transient retry, a SUMMARY follow-up — because claude
137
- // keeps no flags across --resume, so a resume without this would lose the tool.
138
- ...(run.notesPath !== undefined ? { mcpConfig: roleNotesMcpConfig() } : {}),
139
144
  });
140
145
  // agentBaseEnv applies the run marker and the macOS preload (shared with the pi adapter);
141
146
  // claudeEnv layers the backend's own scrub-and-set on top. The notebook path rides beside
142
- // the marker the same way pi's adapter carries it (pi-backend.ts withNotesPath), so the
143
- // spawned MCP server — which inherits this environment — reads the same notes file.
144
- const base = run.notesPath === undefined
145
- ? agentBaseEnv(run.runMarker)
146
- : { ...agentBaseEnv(run.runMarker), TUMWATER_NOTES_PATH: run.notesPath };
147
+ // the marker the same way pi's adapter carries it (withNotesPath); from part 2/4 the run
148
+ // saves its note through the `tumwater-notes` command on PATH, so no MCP server reaches the
149
+ // run and `--strict-mcp-config` (always in claude-args.ts) keeps it that way.
150
+ const base = withNotesPath(agentBaseEnv(run.runMarker, undefined, run.binDir), run.notesPath);
147
151
  const env = claudeEnv(base, {
148
152
  quietTimeoutSeconds: run.config.quietTimeoutSeconds,
153
+ ...(expandedConfigDir !== undefined ? { configDir: expandedConfigDir } : {}),
149
154
  });
150
155
  return { args, stdin, env, sessionId };
151
156
  },
@@ -155,8 +160,13 @@ export const claudeBackend = {
155
160
  // seed the account file, so both must classify it the same way.
156
161
  return claudeTranslator({ thirdPartyProvider: claudeThirdPartyProvider(process.env) });
157
162
  },
158
- resumable(dir, opts) {
159
- return claudeResumableRecord(dir, opts) !== null;
163
+ resumable(dir, opts, configDir) {
164
+ return claudeResumableRecord(dir, opts, configDir) !== null;
165
+ },
166
+ // Every Claude model reads images (Claude Code's Read tool returns them as image blocks), so
167
+ // a claude seam is always capable — the doctor probes nothing and never works around it.
168
+ imageInput() {
169
+ return true;
160
170
  },
161
171
  classifyExit: classifyClaudeExit,
162
172
  };
@@ -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
+ }