tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -1,17 +1,14 @@
1
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";
2
+ * "Capability"). One resolver, `seamImageInput`, is the only place that rule lives, so the
3
+ * answer is never reached two ways. It lives at runtime level because it needs the backend
4
+ * registry: a claude seam is always capable through the AgentBackend.imageInput hook, a pi
5
+ * seam reads the model's `input` in models.json, and any other backend answers null unless it
6
+ * declares a hook. */
7
+ import { configForRole } from "../config/config-views.js";
9
8
  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";
9
+ import { piModelImageInput, piModelsPath, readPiProviders } from "../pi/pi-models.js";
12
10
  import { piListedImageInput } from "../pi/pi-listed-models.js";
13
11
  import { backendById } from "./agent-backend.js";
14
- import { imageProbeEntryFresh, imageProbeKey } from "../doctor/image-probe.js";
15
12
  /** One seam's image-input capability (plans/image-input.md "Capability"): `true` when the
16
13
  * seam's backend declares image support, `false` when it is text-only, `null` when it cannot
17
14
  * be answered from what the team knows (a pair neither models.json nor the pi listing names, a
@@ -21,11 +18,10 @@ import { imageProbeEntryFresh, imageProbeKey } from "../doctor/image-probe.js";
21
18
  * the default pi backend leaves it to models.json's `input` field, where absent means text-only
22
19
  * (pi's own default for a custom model); a pair absent from models.json answers what
23
20
  * `pi --list-models` last said (src/pi/pi-listed-models.ts), else null.
24
- * `probes` is the doctor's live-probe cache (part 2/5): a claimed pi seam whose probe FAILED
25
- * reads false — a claim is only as good as the verified
26
- * answer — while a cached pass (or an unprobed claim) stands on the catalog. claude seams are
27
- * never probed, so the hook answer is never overridden. */
28
- export function seamImageInput(seam, providers, probes, listed = null) {
21
+ * A claim in models.json is trusted without a live probe (the doctor's live probe was
22
+ * subtracted): a claimed pi seam reads true on the catalog. claude seams are answered by the
23
+ * hook. */
24
+ export function seamImageInput(seam, providers, listed = null) {
29
25
  const backendId = seamAgent(seam);
30
26
  const backend = backendById(backendId);
31
27
  // A backend with an imageInput hook owns the answer outright (claude: true for every model).
@@ -34,79 +30,14 @@ export function seamImageInput(seam, providers, probes, listed = null) {
34
30
  // An unknown backend without a hook has no catalog to consult — unknown, never assume.
35
31
  if (backendId !== "pi")
36
32
  return null;
37
- const base = piModelImageInput(providers, seam.provider, seam.model) ?? piListedImageInput(listed, seam.provider, seam.model);
38
- if (base !== true)
39
- return base;
40
- // A claimed pi seam whose live probe failed (fresh, claim unchanged) reads false. Only pi is
41
- // probed, so the probe key carries the pi backend.
42
- const entry = probes?.[imageProbeKey(backendId, seam.provider, seam.model)];
43
- if (entry?.verdict === "fail" && imageProbeEntryFresh(entry))
44
- return false;
45
- return true;
33
+ return piModelImageInput(providers, seam.provider, seam.model) ?? piListedImageInput(listed, seam.provider, seam.model);
46
34
  }
47
- /** The image-capable reroute target for `role`, or null when no peer tier can read images
48
- * (plans/image-input.md "Reroute target"). Walks the role's seam tier through BORROW_ORDER,
49
- * skipping the tier itself — small → default → strong, default → strong → small, strong →
50
- * default (a strong-tier seam never drops to small, the same rule as the budget fallback) —
51
- * and takes the first tier whose model reads images. `regime` picks the pair set (see
52
- * RerouteRegime); `modelsPath` and `account` are injectable so tests need no real pi install
53
- * or account. */
54
- export function imageReroute(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, probes, providers = null, listed = null) {
55
- const tier = roleSeamTier(config, role);
56
- const candidates = BORROW_ORDER[tier].filter((t) => t !== tier);
57
- if (candidates.length === 0)
58
- return null;
59
- const providerMap = providers ?? readPiProviders(modelsPath);
60
- const capable = (rc) => seamImageInput(rc, providerMap, probes, listed) === true;
61
- if (regime === "primary") {
62
- for (const t of candidates) {
63
- const rc = withModelOverrides(config, t, {});
64
- if (capable(rc))
65
- return { tier: t, config: rc };
66
- }
67
- return null;
68
- }
69
- // Fallback regime: only the tiers' free fallback pairs qualify (resolveTierFallbacks over the
70
- // price check), never a priced primary model — a reroute must not spend past the cap.
71
- const resolved = resolveTierFallbacks(config, (p) =>
72
- // The free-ness lookup reads the one claude account held here (part 2a/9).
73
- piPairFree(providerMap, p, (b) => (b === "claude" ? account : null)));
74
- for (const t of candidates) {
75
- const pair = resolved[t].pair;
76
- if (!pair)
77
- continue;
78
- const rc = withModelOverrides(config, t, {
79
- provider: pair.provider,
80
- model: pair.model,
81
- thinking: pair.thinking,
82
- });
83
- if (capable(rc))
84
- return { tier: t, config: rc };
85
- }
86
- return null;
87
- }
88
- /** The config of imageReroute, for callers that want only the target model. */
89
- export function imageRerouteTarget(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, probes, providers = null, listed = null) {
90
- return imageReroute(config, role, regime, modelsPath, account, probes, providers, listed)?.config ?? null;
91
- }
92
- /** A seam's model as one readable token, the same label the doctor's `image input` check uses
93
- * (`claude/<id>` for a claude seam, `provider/model` for pi). The one home of the label, so
94
- * the surface strings (the images alert, the role page, the status rows) and the doctor report
95
- * cannot drift, and delegates to backend-traits' modelLabel. */
96
- export function seamImageLabel(backend, provider, model) {
97
- return modelLabel(backend, provider, model);
98
- }
99
- /** Compute RoleImageInfo for one role. `modelsPath` and `probes` are injectable; the providers
100
- * catalog is read (stat-cached) here and also handed to imageReroute so a poll that asks many
101
- * roles still parses the catalog once. All reads mirror the doctor's `image input` check, so
102
- * the surface and the check agree about capability. */
103
- export function roleImageInfo(config, role, modelsPath = piModelsPath(), probes, listed = null) {
35
+ /** Compute RoleImageInfo for one role. `modelsPath` and `listed` are injectable; the providers
36
+ * catalog is read (stat-cached) here. The reroute field is a literal null — the peer-tier image
37
+ * reroute was subtracted (part 5g/6), so no role names a reroute target; 5h removes the field. */
38
+ export function roleImageInfo(config, role, modelsPath = piModelsPath(), listed = null) {
104
39
  const providers = readPiProviders(modelsPath);
105
40
  const rc = configForRole(config, role);
106
- const imageInput = seamImageInput(rc, providers, probes, listed);
107
- const rr = imageReroute(config, role, "primary", modelsPath, null, probes, providers, listed);
108
- const imageRerouteStr = rr
109
- ? `${rr.tier} (${seamImageLabel(rr.config.agent, rr.config.provider, rr.config.model)})`
110
- : null;
111
- return { imageInput, imageReroute: imageRerouteStr };
41
+ const imageInput = seamImageInput(rc, providers, listed);
42
+ return { imageInput, imageReroute: null };
112
43
  }
@@ -1,5 +1,5 @@
1
1
  /** Installing the per-team `tumwater-notes` shim every backend's run finds on its PATH
2
- * (plans/notes-command.md "Reaching it from a run"): run-agent calls `ensureNotesShim(root)`
2
+ * (plans/notes-command.md "Reaching it from a run"): run-spawn calls `ensureNotesShim(root)`
3
3
  * before building each invocation, and the backend prepends the returned dir to the run's
4
4
  * PATH (backendBaseEnv's `binDir`, src/process/run-marker.ts). The shim is a two-line `sh`
5
5
  * script that execs the compiled notes CLI, so a model's shell — pi's, Claude Code's or
@@ -20,7 +20,7 @@ export const NOTES_SHIM_NAME = "tumwater-notes";
20
20
  /** Install the `tumwater-notes` shim under `<root>/.tumwater/bin` and return that directory.
21
21
  * The shim is written atomically with mode 0755, only when its content differs from what is
22
22
  * already there (a redeploy that changes `<dist>` rewrites it). Never throws: a failure to
23
- * write is the caller's to absorb — run-agent logs one warning and runs without the shim. */
23
+ * write is the caller's to absorb — run-spawn logs one warning and runs without the shim. */
24
24
  export function ensureNotesShim(root, execPath = process.execPath) {
25
25
  const dir = notesShimDir(root);
26
26
  const target = path.join(dir, NOTES_SHIM_NAME);
@@ -1,8 +1,8 @@
1
1
  /** The opencode adapter for the agent seam (plans/opencode-backend.md "The invocation",
2
2
  * "Sessions", §6). It shapes one run's argv/stdin/env, translates opencode's `run --format
3
3
  * json` into the canonical stream, runs the authoritative session export after the process
4
- * exits, and classifies the exit's stderr — everything backend-specific. src/agent/run-agent.ts
5
- * owns the spawn and records this run in the session ledger; opencode mints its own session id
4
+ * exits, and classifies the exit's stderr — everything backend-specific. src/agent/run-spawn.ts
5
+ * spawns the run and records this run in the session ledger; opencode mints its own session id
6
6
  * (a `ses_…` id, or the resumed record's id), which `invocation` returns so the ledger can
7
7
  * carry it.
8
8
  *
@@ -4,8 +4,8 @@
4
4
  * `.tumwater/state/opencode-models.json` that survives a failed fetch, and
5
5
  * `opencodeModelFree` as the one predicate part 6b/7's seam reads. No seam is wired here: that
6
6
  * is part 6b/7 (the budget gate), part 6c/7 (a billedAt demotion on an observed cost) and
7
- * part 7/7 (a startup-cache seed and doctor), so this module's only callers today are its own
8
- * tests. */
7
+ * part 7/7 (a startup-cache seed and doctor), so this module only publishes the data and
8
+ * predicates those seam consumers read. */
9
9
  import { spawnSync } from "node:child_process";
10
10
  import { isJsonObject, validEntries } from "../../files/json-object.js";
11
11
  import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
@@ -18,7 +18,7 @@
18
18
  import fs from "node:fs";
19
19
  import path from "node:path";
20
20
  import { fileURLToPath, pathToFileURL } from "node:url";
21
- import { readTextOrNull } from "../../files/files.js";
21
+ import { readTextOrNull, xdgConfigHomeDir } from "../../files/files.js";
22
22
  import { isJsonObject } from "../../files/json-object.js";
23
23
  /** The operator's global opencode config lives in `$XDG_CONFIG_HOME/opencode/` (or
24
24
  * `~/.config/opencode/`), in `config.json`, `opencode.json` or `opencode.jsonc`. */
@@ -123,7 +123,7 @@ function stripJsonc(src) {
123
123
  * parse; doctor reports a broken one in part 7/7. `homeDir` is the operator's home directory;
124
124
  * the process's own `XDG_CONFIG_HOME` env, when set, wins over `~/.config` (the XDG default). */
125
125
  export function operatorMcpNames(env, homeDir) {
126
- const configHome = env.XDG_CONFIG_HOME ?? path.join(homeDir, ".config");
126
+ const configHome = xdgConfigHomeDir(env, homeDir);
127
127
  const dir = path.join(configHome, "opencode");
128
128
  for (const file of OPERATOR_CONFIG_FILES) {
129
129
  const text = readTextOrNull(path.join(dir, file));
@@ -16,6 +16,7 @@
16
16
  * `opencode_free_tier_refused` is added there once when any step was refused. `--thinking` is
17
17
  * never passed, so no `reasoning` event appears; the mapping below still recognizes one so a
18
18
  * future `--thinking` run degrades gracefully. */
19
+ import { transientProseKind } from "../backend-traits.js";
19
20
  import { canonicalUsageBlock, isJsonObject, nonNegativeNumber, objectOrEmpty, parseJsonObject } from "../../files/json-object.js";
20
21
  /** opencode's tool names → the canonical pi names the readers already know. `shell`→`bash` and
21
22
  * `patch`→`edit` (pi has one edit tool); anything else is lower-cased so a future tool still
@@ -66,10 +67,9 @@ function classifyError(err) {
66
67
  // Transient server, connection and timeout failures.
67
68
  if (/\b(500|501|502|503|504|529)\b|internal server error|bad gateway|service unavailable|gateway timeout|overloaded/i.test(message))
68
69
  return { kind: "transientBackend", backendKind: "server" };
69
- if (/connection|econn|refused|reset|terminated/i.test(message))
70
- return { kind: "transientBackend", backendKind: "connection" };
71
- if (/timed out|timeout/i.test(message))
72
- return { kind: "transientBackend", backendKind: "timeout" };
70
+ const prose = transientProseKind(message);
71
+ if (prose)
72
+ return prose;
73
73
  return {};
74
74
  }
75
75
  /** The step's token counts and cost → pi's canonical usage block: the four counts, their sum
@@ -117,14 +117,7 @@ export function opencodeTranslator(opts = {}) {
117
117
  }
118
118
  return out;
119
119
  };
120
- const translate = (line) => {
121
- const parsed = parseJsonObject(line);
122
- if (!parsed) {
123
- // A malformed / non-JSON line on stdout is skipped with one warning for the run.
124
- record("malformed");
125
- return [];
126
- }
127
- const event = parsed;
120
+ const announceSession = (event) => {
128
121
  // The first event with a non-empty `sessionID` announces the session and the agent start.
129
122
  // An error raised before a session exists has `sessionID: ""` (or none) and emits neither.
130
123
  let out = [];
@@ -140,92 +133,108 @@ export function opencodeTranslator(opts = {}) {
140
133
  out.push(JSON.stringify({ type: "agent_start" }));
141
134
  }
142
135
  }
136
+ return out;
137
+ };
138
+ const handleStepStart = () => {
139
+ step = { content: [], calls: [] };
140
+ return [];
141
+ };
142
+ const handleText = (event) => {
143
+ // A content block with no open buffer opens one: `step_start` is sometimes missing, and
144
+ // edit-run.jsonl's final `text` lands after its last `step_finish`, so a block dropped
145
+ // for want of a buffer would lose the run's last turn.
146
+ step ??= { content: [], calls: [] };
147
+ step.content.push({ type: "text", text: objectOrEmpty(event.part).text ?? "" });
148
+ return [];
149
+ };
150
+ const handleReasoning = (event) => {
151
+ step ??= { content: [], calls: [] };
152
+ const thinking = objectOrEmpty(event.part).text ?? "";
153
+ step.content.push({ type: "thinking", thinking: typeof thinking === "string" ? thinking : "" });
154
+ return [];
155
+ };
156
+ const handleToolUse = (event) => {
157
+ // As above, a tool reported with no open buffer opens one.
158
+ step ??= { content: [], calls: [] };
159
+ const partValue = objectOrEmpty(event.part);
160
+ const state = objectOrEmpty(partValue.state);
161
+ const input = objectOrEmpty(state.input);
162
+ // `arguments` is the tool's `input` plus a `path` alias of `filePath` —
163
+ // describeToolCall reads either.
164
+ const args = { ...input };
165
+ if (typeof input.filePath === "string")
166
+ args.path = input.filePath;
167
+ // V2 2.0.32 emits the native tool name in `part.tool` (not `part.name`); the live
168
+ // captures agree.
169
+ const name = typeof partValue.tool === "string" ? partValue.tool : "";
170
+ // Own-property test, not a bare index: a tool name like `constructor` or
171
+ // `__proto__` would otherwise inherit a function from the table's prototype chain and
172
+ // bypass the lower-cased fallback (the same guard claude-stream.ts applies).
173
+ const canonicalName = Object.hasOwn(TOOL_NAMES, name) ? (TOOL_NAMES[name] ?? name.toLowerCase()) : name.toLowerCase();
174
+ const id = typeof partValue.id === "string" ? partValue.id : "";
175
+ step.content.push({ type: "toolCall", id, name: canonicalName, arguments: args });
176
+ step.calls.push({ id, name: canonicalName, args, isError: state.status === "error" });
177
+ return [];
178
+ };
179
+ const handleStepFinish = (event) => {
180
+ if (!step)
181
+ return [];
182
+ const part = objectOrEmpty(event.part);
183
+ const tokens = objectOrEmpty(part.tokens);
184
+ const cache = objectOrEmpty(tokens.cache);
185
+ step.usage = canonicalUsage({
186
+ input: tokens.input,
187
+ output: tokens.output,
188
+ cache: { read: cache.read, write: cache.write },
189
+ }, nonNegativeNumber(part.cost, 0));
190
+ return flush(true);
191
+ };
192
+ const handleError = (event) => {
193
+ // Errors carry their payload in `event.error` (`{type, message, status?, response?}`,
194
+ // plan finding 8); classification reads type first, message second.
195
+ const err = objectOrEmpty(event.error);
196
+ const cls = classifyError(err);
197
+ if (cls.freeTier)
198
+ sawFreeTier = true;
199
+ const message = typeof err.message === "string" ? err.message : "";
200
+ // The open step (if any) is cut off by the error and flushed without usage first.
201
+ const out = flush(false);
202
+ const m = { role: "assistant", content: [], stopReason: "error" };
203
+ if (message !== "")
204
+ m.errorMessage = message;
205
+ // tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
206
+ // reads event.tumwaterKind and trusts it before its text regexes.
207
+ out.push(JSON.stringify({
208
+ type: "message_end",
209
+ message: m,
210
+ ...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
211
+ ...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
212
+ ...(cls.retryAfterSeconds !== undefined ? { retryAfterSeconds: cls.retryAfterSeconds } : {}),
213
+ }));
214
+ return out;
215
+ };
216
+ const translate = (line) => {
217
+ const parsed = parseJsonObject(line);
218
+ if (!parsed) {
219
+ // A malformed / non-JSON line on stdout is skipped with one warning for the run.
220
+ record("malformed");
221
+ return [];
222
+ }
223
+ const event = parsed;
224
+ const out = announceSession(event);
143
225
  const type = event.type;
144
226
  switch (type) {
145
- case "step_start":
146
- step = { content: [], calls: [] };
147
- break;
148
- case "text": {
149
- // A content block with no open buffer opens one: `step_start` is sometimes missing, and
150
- // edit-run.jsonl's final `text` lands after its last `step_finish`, so a block dropped
151
- // for want of a buffer would lose the run's last turn.
152
- step ??= { content: [], calls: [] };
153
- step.content.push({ type: "text", text: objectOrEmpty(event.part).text ?? "" });
154
- break;
155
- }
156
- case "reasoning": {
157
- step ??= { content: [], calls: [] };
158
- const thinking = objectOrEmpty(event.part).text ?? "";
159
- step.content.push({ type: "thinking", thinking: typeof thinking === "string" ? thinking : "" });
160
- break;
161
- }
162
- case "tool_use": {
163
- // As above, a tool reported with no open buffer opens one.
164
- step ??= { content: [], calls: [] };
165
- const partValue = objectOrEmpty(event.part);
166
- const state = objectOrEmpty(partValue.state);
167
- const input = objectOrEmpty(state.input);
168
- // `arguments` is the tool's `input` plus a `path` alias of `filePath` —
169
- // describeToolCall reads either.
170
- const args = { ...input };
171
- if (typeof input.filePath === "string")
172
- args.path = input.filePath;
173
- // V2 2.0.32 emits the native tool name in `part.tool` (not `part.name`); the live
174
- // captures agree.
175
- const name = typeof partValue.tool === "string" ? partValue.tool : "";
176
- // Own-property test, not a bare index: a tool name like `constructor` or
177
- // `__proto__` would otherwise inherit a function from the table's prototype chain and
178
- // bypass the lower-cased fallback (the same guard claude-stream.ts applies).
179
- const canonicalName = Object.hasOwn(TOOL_NAMES, name) ? (TOOL_NAMES[name] ?? name.toLowerCase()) : name.toLowerCase();
180
- const id = typeof partValue.id === "string" ? partValue.id : "";
181
- step.content.push({ type: "toolCall", id, name: canonicalName, arguments: args });
182
- step.calls.push({ id, name: canonicalName, args, isError: state.status === "error" });
183
- break;
184
- }
185
- case "step_finish": {
186
- if (!step)
187
- break;
188
- const part = objectOrEmpty(event.part);
189
- const tokens = objectOrEmpty(part.tokens);
190
- const cache = objectOrEmpty(tokens.cache);
191
- step.usage = canonicalUsage({
192
- input: tokens.input,
193
- output: tokens.output,
194
- cache: { read: cache.read, write: cache.write },
195
- }, nonNegativeNumber(part.cost, 0));
196
- out.push(...flush(true));
197
- break;
198
- }
199
- case "error": {
200
- // Errors carry their payload in `event.error` (`{type, message, status?, response?}`,
201
- // plan finding 8); classification reads type first, message second.
202
- const err = objectOrEmpty(event.error);
203
- const cls = classifyError(err);
204
- if (cls.freeTier)
205
- sawFreeTier = true;
206
- const message = typeof err.message === "string" ? err.message : "";
207
- // The open step (if any) is cut off by the error and flushed without usage first.
208
- out.push(...flush(false));
209
- const m = { role: "assistant", content: [], stopReason: "error" };
210
- if (message !== "")
211
- m.errorMessage = message;
212
- // tumwaterKind/backendKind ride the canonical EVENT (not its message): the parser
213
- // reads event.tumwaterKind and trusts it before its text regexes.
214
- out.push(JSON.stringify({
215
- type: "message_end",
216
- message: m,
217
- ...(cls.kind !== undefined ? { tumwaterKind: cls.kind } : {}),
218
- ...(cls.backendKind !== undefined ? { backendKind: cls.backendKind } : {}),
219
- ...(cls.retryAfterSeconds !== undefined ? { retryAfterSeconds: cls.retryAfterSeconds } : {}),
220
- }));
221
- break;
222
- }
227
+ case "step_start": return [...out, ...handleStepStart()];
228
+ case "text": return [...out, ...handleText(event)];
229
+ case "reasoning": return [...out, ...handleReasoning(event)];
230
+ case "tool_use": return [...out, ...handleToolUse(event)];
231
+ case "step_finish": return [...out, ...handleStepFinish(event)];
232
+ case "error": return [...out, ...handleError(event)];
223
233
  default:
224
234
  // Record the unknown type ("" for a type-less line), once, in first-seen order.
225
235
  record(typeof type === "string" ? type : "unknown");
226
- break;
236
+ return out;
227
237
  }
228
- return out;
229
238
  };
230
239
  return {
231
240
  translate,