tumwater 0.2.0 → 0.3.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 (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { loadConfigSafe } from "../config/config.js";
@@ -6,11 +5,16 @@ import { seamsInUse } from "../config/config-fallbacks.js";
6
5
  import { findOnPath } from "../files/files.js";
7
6
  import { GIT_MISSING_MESSAGE } from "../git/git-run.js";
8
7
  import { branchesPhrase, branchExists, currentBranch, hasCommits, isGitRepo } from "../git/git.js";
9
- import { DETACHED_HEAD_MESSAGE, NOT_A_REPO_MESSAGE, NOT_INITIALIZED_MESSAGE, NO_COMMITS_MESSAGE, } from "./readiness.js";
10
- import { agentMissingMessage, findAgentBinary, resolveAgentBin } from "../pi/pi-bin.js";
8
+ import { DETACHED_HEAD_MESSAGE, NOT_A_REPO_MESSAGE, NOT_INITIALIZED_MESSAGE, NO_COMMITS_MESSAGE, teamWorkspaceMessage, } from "./readiness.js";
9
+ import { owningTeamRoot } from "../paths.js";
10
+ import { backendMissingMessage, findBackendBinary, resolveBackendBin } from "../pi/pi-bin.js";
11
11
  import { seedClaudeAccount } from "../agent/claude/claude-account.js";
12
- import { refreshOpencodeCatalog } from "../agent/opencode/opencode-catalog.js";
13
- import { agentBackend } from "../agent/agent-backend.js";
12
+ import { seedCodexAccount } from "../agent/codex/codex-account.js";
13
+ import { opencodeCatalogStale, readOpencodeCache, refreshOpencodeCatalog, } from "../agent/opencode/opencode-catalog.js";
14
+ import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
15
+ import { HOUR_MS } from "../text/datetime.js";
16
+ import { errorMessage } from "../text/text.js";
17
+ import { backendById } from "../agent/agent-backend.js";
14
18
  // Register the built-in pi and claude adapters into the registry this module reads (see
15
19
  // unregisteredBackendInUse) — importing run-agent.ts is the one place they register.
16
20
  import "../agent/run-agent.js";
@@ -26,13 +30,15 @@ export async function repoNotReady(root) {
26
30
  return GIT_MISSING_MESSAGE;
27
31
  if (!(await isGitRepo(root)))
28
32
  return NOT_A_REPO_MESSAGE;
29
- if (!fs.existsSync(path.join(root, "tumwater.json")))
30
- return NOT_INITIALIZED_MESSAGE;
33
+ if (!fs.existsSync(path.join(root, "tumwater.json"))) {
34
+ const owner = owningTeamRoot(root);
35
+ return owner === undefined ? NOT_INITIALIZED_MESSAGE : teamWorkspaceMessage(owner);
36
+ }
31
37
  if (!(await hasCommits(root)))
32
38
  return NO_COMMITS_MESSAGE;
33
39
  return null;
34
40
  }
35
- /** Every precondition a `tumwater run` generation checks before its orchestrator starts, in the
41
+ /** Every precondition a `tumwater start` generation checks before its orchestrator starts, in the
36
42
  * order it checks them: the repo gate, a tumwater.json that loads and validates, a resolvable
37
43
  * agent binary, and a main branch it can target. Either the values the generation starts with,
38
44
  * or the first unmet precondition's message — never a throw, so a poll can ask it safely. */
@@ -57,18 +63,18 @@ opencodeSeed = {}) {
57
63
  if (unregistered !== undefined) {
58
64
  return { problem: `backend ${unregistered} is configured but this build has no adapter for it` };
59
65
  }
60
- // Fail fast instead of starting loops whose every tick dies with "spawn <bin> ENOENT". Every
61
- // enabled seam's agent — the top-level `agent` plus each enabled loop's resolved view and the
62
- // reviewer — resolves through the same resolveAgentBin the spawn uses (and doctor's pi check);
63
- // a missing one names its own agent's binary, not pi's. resolveAgentBin normalizes path-shaped
66
+ // Fail fast instead of starting roles whose every run dies with "spawn <bin> ENOENT". Every
67
+ // enabled seam's agent — the top-level `agent` plus each enabled role's resolved view and the
68
+ // reviewer — resolves through the same resolveBackendBin the spawn uses (and doctor's pi check);
69
+ // a missing one names its own agent's binary, not pi's. resolveBackendBin normalizes path-shaped
64
70
  // values against THIS process's cwd, which every generation inherits, so what is checked here
65
- // is exactly what the ticks spawn.
66
- for (const agent of agentsInUse(config)) {
67
- const resolved = resolveAgentBin(config, agent);
68
- // The shared executability rule (pi/pi-bin.ts's findAgentBinary — the one doctor's pi check
71
+ // is exactly what the runs spawn.
72
+ for (const agent of backendsInUse(config)) {
73
+ const resolved = resolveBackendBin(config, agent);
74
+ // The shared executability rule (pi/pi-bin.ts's findBackendBinary — the one doctor's pi check
69
75
  // asks too), so a seam's missing binary reads the same way on both surfaces.
70
- if (!findAgentBinary(resolved))
71
- return { problem: agentMissingMessage(resolved, agent) };
76
+ if (!findBackendBinary(resolved))
77
+ return { problem: backendMissingMessage(resolved, agent) };
72
78
  }
73
79
  // Seed the claude account state before the first run (plans/agent-backends.md part 6a):
74
80
  // `claude auth status` decides subscription vs API key, and an existing file is left alone.
@@ -76,24 +82,36 @@ opencodeSeed = {}) {
76
82
  // process: runStartupCheck is also the redeploy/supervisor poll's ask (runStartupProblem),
77
83
  // and a logged-out claude would otherwise spawn a bounded probe on every poll. If the seed
78
84
  // fails, the first claude run's observation fills the file in.
79
- if (!claudeSeedAttempted.has(root) && agentsInUse(config).includes("claude")) {
85
+ if (!claudeSeedAttempted.has(root) && backendsInUse(config).includes("claude")) {
80
86
  claudeSeedAttempted.add(root);
81
87
  seedClaudeAccount(root, config);
82
88
  }
89
+ // Seed the codex account state before the first codex run (plans/codex-backend.md §7):
90
+ // `codex login status` decides subscription vs API key, and an existing file is left alone.
91
+ // Gated on a codex seam like the claude seed above, and attempted once per root per process.
92
+ // API-key billing is refused (decision 5): Codex reports no cost, so per-token billing would
93
+ // spend money the cap cannot see. If the seed fails or reads something else, the first codex
94
+ // run's observation fills the file in.
95
+ if (!codexSeedAttempted.has(root) && backendsInUse(config).includes("codex")) {
96
+ codexSeedAttempted.add(root);
97
+ const codexAccount = seedCodexAccount(root, config);
98
+ if (codexAccount?.billing === "api") {
99
+ return {
100
+ problem: "Codex is logged in with an API key; tumwater runs Codex on a ChatGPT subscription only — run `CODEX_HOME=<home> codex login --device-auth`",
101
+ };
102
+ }
103
+ }
83
104
  // Seed the opencode models catalog (part 6b/7) before the first run: the poll surfaces price
84
105
  // opencode seams from the cached `.tumwater/state/opencode-models.json`, and an absent cache
85
106
  // reads every opencode seam NOT free (the safe direction for the budget gate). Mirroring the
86
107
  // claude seed above, it runs once per root per process when a model job routes to opencode;
87
- // a failed fetch keeps the last good cache (or an empty one) and never blocks startup.
88
- if (!opencodeSeedAttempted.has(root) && agentsInUse(config).includes("opencode")) {
108
+ // a failed fetch keeps the last good cache (or an empty one) and never blocks startup; a
109
+ // failed fetch or cache write logs one warning event.
110
+ if (!opencodeSeedAttempted.has(root) && backendsInUse(config).includes("opencode")) {
89
111
  opencodeSeedAttempted.add(root);
90
- await refreshOpencodeCatalog(root, {
91
- providers: opencodeProviders(config),
92
- // Probe the configured opencode binary (not a different one on PATH) for the version, so
93
- // the freshness check compares against the binary the team actually runs.
94
- cliBin: resolveAgentBin(config, "opencode").bin,
95
- fetch: opencodeSeed.fetch,
96
- now: opencodeSeed.now,
112
+ await seedOpencodeCatalog(root, config, {
113
+ ...opencodeSeed,
114
+ warn: (message) => warnEventBestEffort(root, TUMWATER_SOURCE, message),
97
115
  });
98
116
  }
99
117
  const branch = await resolveMainBranch(root, config, branchArg);
@@ -101,10 +119,54 @@ opencodeSeed = {}) {
101
119
  return branch;
102
120
  return { config, mainBranch: branch.mainBranch };
103
121
  }
122
+ /** Refresh the opencode models catalog for the config's opencode seams: the one seed both the
123
+ * startup check and a live edit that first routes a seam to opencode run. `warn` receives the
124
+ * refresh's own warnings; every caller passes one (startup logs through warnEventBestEffort). */
125
+ export function seedOpencodeCatalog(root, config, opts = {}) {
126
+ return refreshOpencodeCatalog(root, {
127
+ providers: opencodeProviders(config),
128
+ // Probe the configured opencode binary (not a different one on PATH) for the version, so
129
+ // the freshness check compares against the binary the team actually runs.
130
+ cliBin: resolveBackendBin(config, "opencode").bin,
131
+ fetch: opts.fetch,
132
+ now: opts.now,
133
+ warn: opts.warn,
134
+ });
135
+ }
136
+ /** How long a running team waits between attempts to refetch a missing or stale opencode
137
+ * catalog. */
138
+ export const OPENCODE_CATALOG_RETRY_MS = HOUR_MS;
139
+ /** The poll-driven catalog refresh: a running team refetches a missing or stale catalog, so a
140
+ * failed startup fetch or a days-long run does not price by nothing or by an old copy. The poll
141
+ * path checks only the cache's age. It skips the `opencode --version` comparison, so no binary is
142
+ * spawned on every poll; the version check still runs inside refreshOpencodeCatalog once a
143
+ * refresh is due. The throttle starts at construction (the startup check has just tried) and
144
+ * moves on every attempt, so a fetch that keeps failing warns at most once per
145
+ * OPENCODE_CATALOG_RETRY_MS, through refreshOpencodeCatalog's own fetch-failed warning. */
146
+ export function newOpencodeCatalogRefresh(root, seed, warn) {
147
+ const clock = seed.now ?? Date.now;
148
+ let lastAttempt = clock();
149
+ return {
150
+ poll(config) {
151
+ if (!backendsInUse(config).includes("opencode"))
152
+ return;
153
+ if (clock() - lastAttempt < OPENCODE_CATALOG_RETRY_MS)
154
+ return;
155
+ if (!opencodeCatalogStale(readOpencodeCache(root), clock()))
156
+ return;
157
+ lastAttempt = clock();
158
+ void seedOpencodeCatalog(root, config, { ...seed, warn }).catch((err) => warn(`open code model catalog refresh failed: ${errorMessage(err)}`));
159
+ },
160
+ };
161
+ }
104
162
  /** Roots this process has already attempted a claude-account seed for (see runStartupCheck),
105
163
  * so the supervisor's and redeploy's repeated startup checks do not respawn `claude auth
106
164
  * status` on every poll when the probe fails. */
107
165
  const claudeSeedAttempted = new Set();
166
+ /** Roots this process has already attempted a codex-account seed for (see runStartupCheck),
167
+ * so the supervisor's and redeploy's repeated startup checks do not respawn `codex login
168
+ * status` on every poll when the probe fails. */
169
+ const codexSeedAttempted = new Set();
108
170
  /** Roots this process has already attempted an opencode-catalog seed for (see runStartupCheck),
109
171
  * so the supervisor's and redeploy's repeated startup checks do not respawn the models.dev
110
172
  * fetch on every poll when it fails. */
@@ -131,7 +193,7 @@ export function opencodeProviders(config) {
131
193
  * as a fallback, the resolver, or the director is checked exactly like a primary seam.
132
194
  * One list the startup gate and doctor's binary check share, so a route the harness would
133
195
  * spawn is checked on both surfaces. */
134
- export function agentsInUse(config) {
196
+ export function backendsInUse(config) {
135
197
  return [...new Set(seamsInUse(config).map((s) => s.agent ?? "pi"))];
136
198
  }
137
199
  /** The first backend an enabled seam routes to that this build has no registered adapter for,
@@ -139,10 +201,10 @@ export function agentsInUse(config) {
139
201
  * are registered by importing src/agent/run-agent.ts (see the import above); a seam backend
140
202
  * that is not in the registry is one this build cannot run. Doctor's binary check asks the
141
203
  * same question, so the startup gate and doctor cannot disagree about which backend a build
142
- * can serve. After the opencode adapter lands (part 5/7) this stays the general rule for any
143
- * future id. */
204
+ * can serve. opencode's and codex's adapters now register like pi's and claude's, so only a
205
+ * future id without an adapter trips this rule. */
144
206
  export function unregisteredBackendInUse(config) {
145
- return agentsInUse(config).find((agent) => agentBackend(agent) === undefined);
207
+ return backendsInUse(config).find((agent) => backendById(agent) === undefined);
146
208
  }
147
209
  /** runStartupCheck reduced to its verdict: the message a generation would fail on right now, or
148
210
  * null when one would boot — what the redeploy and the supervisor ask. */
@@ -1,11 +1,11 @@
1
- /** The error-streak circuit breaker (PLANS.md 2026-09-30): a role whose ticks keep failing
2
- * consecutively is warned at ERROR_STREAK_WARN (src/tick/tick-apply.ts) and named by the team-wide
1
+ /** The error-streak circuit breaker (PLANS.md 2026-09-30): a role whose runs keep failing
2
+ * consecutively is warned at ERROR_STREAK_WARN (src/run/run-apply.ts) and named by the team-wide
3
3
  * storm alarms (src/team/error-storm.ts, src/team/failure-spread.ts), but both only talk — the
4
- * role keeps ticking on the error ladder's 600 s max backoff forever, burning a model slot and
5
- * spend on a loop that cannot succeed. Past ERROR_STREAK_BREAKER consecutive failed ticks this gate
4
+ * role keeps running on the error ladder's 600 s max backoff forever, burning a model slot and
5
+ * spend on a role that cannot succeed. Past ERROR_STREAK_BREAKER consecutive failed runs this gate
6
6
  * pauses the role through the same per-role pause marker the operator's `tumwater pause --role
7
7
  * <id>` writes (src/team/team-state.ts's pauseRole — the marker, lock, and idempotence come
8
- * free), so the scheduler's existing pausedRolesSet skip blocks new ticks with zero scheduler
8
+ * free), so the scheduler's existing pausedRolesSet skip blocks new runs with zero scheduler
9
9
  * changes. The director is NOT exempt: a failing director cannot process prompts anyway, and one
10
10
  * uniform rule needs no carve-out. `tumwater resume --role <id>` (or the dashboard's per-row
11
11
  * toggle) lifts it, as today.
@@ -14,27 +14,33 @@
14
14
  * here so it is unit-testable without a team, and the module owns the only event emission — the
15
15
  * orchestrator's poll owns only the wiring.
16
16
  *
17
- * Ack bookkeeping. The marker itself is the ack carrier: every poll, each role present in the
18
- * paused set has its streak recorded in the in-memory `acked` map, so after the operator resumes a
19
- * breaker-paused role, re-tripping requires ERROR_STREAK_BREAKER *more* consecutive failures, not
20
- * one — the same grace an operator-paused failing role gets (its streak is acked while the pause
21
- * stands, so resuming it does not instantly re-trip on the pre-pause streak), and the same
22
- * durability a harness restart mid-pause gets (the in-memory state is lost, but the standing marker
23
- * freezes the ack on the first poll after the restart). If the operator had already resumed before
24
- * a restart, a still-live streak at or past the breaker re-trips once — the cause is durable, and
25
- * one event is the honest report. A streak that resets to 0 through the role's own success clears
26
- * the ack: the next episode trips on its own fresh ERROR_STREAK_BREAKER failures.
17
+ * Ack bookkeeping. The marker is the ack carrier within a run, and the durable copy of the
18
+ * ack lives in each runner's persisted `RunnerState.streakAcked`: every poll, each role present
19
+ * in the paused set has its streak recorded in the in-memory `acked` cache (and flushed to the
20
+ * runner's state), so after the operator resumes a breaker-paused role, re-tripping
21
+ * requires ERROR_STREAK_BREAKER *more* consecutive failures, not one — the same grace an
22
+ * operator-paused failing role gets (its streak is acked while the pause stands, so resuming
23
+ * it does not instantly re-trip on the pre-pause streak). Because the ack is
24
+ * persisted, a harness restart after a resume does NOT re-trip a role whose frozen streak
25
+ * never ran down — the durable cause's first poll finds the persisted ack and leaves the role
26
+ * alone (BUGS.md 2026-10-23: the old in-memory-only ack re-paused a resumed role on every
27
+ * auto-redeploy). A streak that resets to 0 through the role's own success clears the ack
28
+ * (from the cache and the runner's state): the next episode trips on its own fresh
29
+ * ERROR_STREAK_BREAKER failures.
27
30
  *
28
- * Accepted behavior, stated here and not fixed in this change: the dashboards' "failing tick after
29
- * tick" alert (src/ui/team-alerts.ts) reads consecutiveErrors regardless of pause, so it keeps
30
- * listing a breaker-paused role until the streak clears; the loop cell's paused badge explains why
31
- * it is not ticking, and the role_streak_paused event tells the operator why. */
32
- import { ERROR_STREAK_BREAKER } from "../tick/tick-apply.js";
33
- import { loopPaused } from "../roles/loop-ids.js";
31
+ * Accepted behavior, stated here and not fixed in this change: the dashboards' "failing run after
32
+ * run" alert (src/ui/team-alerts.ts) reads consecutiveErrors regardless of pause, so it keeps
33
+ * listing a breaker-paused role until the streak clears; the role cell's paused badge explains why
34
+ * it is not running, and the role_streak_paused event tells the operator why. */
35
+ import { ERROR_STREAK_BREAKER } from "../run/run-apply.js";
36
+ import { saveRunnerStateBestEffort } from "../runner/runner-state.js";
37
+ import { runnerPaused } from "../roles/runner-ids.js";
34
38
  import { pauseRole } from "../team/team-state.js";
35
39
  import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
36
- /** A fresh gate state: nothing acked, so a live streak at or past the breaker trips on the
37
- * first poll (the durable-cause case a restart after an operator resume relies on). */
40
+ /** A fresh gate state: nothing cached yet, so the first poll of a runner whose persisted
41
+ * `streakAcked` is set seeds it from there — a restart does NOT re-trip a role the breaker
42
+ * already excused (the bug this state exists to fix). A runner with no persisted ack that
43
+ * is at or past the breaker trips on the first poll, as before. */
38
44
  export function newStreakGateState() {
39
45
  return { acked: new Map() };
40
46
  }
@@ -42,7 +48,7 @@ export function newStreakGateState() {
42
48
  * has reached ERROR_STREAK_BREAKER plus what its ack already excused, and whose role is not
43
49
  * in `pausedRoles`: write the per-role pause marker and log one `role_streak_paused` event
44
50
  * carrying the role, the streak, and the last error. Returns the pause keys this poll wrote
45
- * (`pauseKey` maps a loop id to its marker key; instance 1 of a multi-instance role pauses as
51
+ * (`pauseKey` maps a role id to its marker key; instance 1 of a multi-instance role pauses as
46
52
  * `<role>-1`), so the wiring can fold them into the poll's paused-roles view and the scheduler
47
53
  * blocks them on this very poll, not the next one.
48
54
  *
@@ -54,16 +60,30 @@ export function pollStreakGate(root, state, runners, pausedRoles, pauseKey = (r)
54
60
  const tripped = [];
55
61
  for (const { role, state: s } of runners) {
56
62
  const streak = s.consecutiveErrors ?? 0;
63
+ // Seed a fresh in-run ack cache from the persisted excused streak, so a restart keeps the
64
+ // grace the breaker (and a later operator resume) relies on instead of re-pausing a role
65
+ // whose frozen streak never ran down (BUGS.md 2026-10-23).
66
+ if (!state.acked.has(role) && s.streakAcked !== undefined) {
67
+ state.acked.set(role, s.streakAcked);
68
+ }
57
69
  // Ack while paused: the marker is the ack carrier. The streak is frozen while a role is
58
- // paused (no ticks run), so recording it here is exactly the level the resume excuses.
59
- if (loopPaused(pausedRoles, role)) {
60
- state.acked.set(role, streak);
70
+ // paused (no new runs start), so recording it here is exactly the level the resume excuses.
71
+ if (runnerPaused(pausedRoles, role)) {
72
+ if ((state.acked.get(role) ?? 0) !== streak) {
73
+ state.acked.set(role, streak);
74
+ s.streakAcked = streak;
75
+ saveRunnerStateBestEffort(root, s);
76
+ }
61
77
  continue;
62
78
  }
63
79
  if (streak === 0) {
64
80
  // A streak that reset through the role's own success ends the excused episode: the
65
- // next one must climb a fresh ERROR_STREAK_BREAKER of its own.
66
- state.acked.delete(role);
81
+ // next one must climb a fresh ERROR_STREAK_BREAKER of its own, so drop the durable ack too.
82
+ if (state.acked.get(role) !== undefined) {
83
+ state.acked.delete(role);
84
+ delete s.streakAcked;
85
+ saveRunnerStateBestEffort(root, s);
86
+ }
67
87
  continue;
68
88
  }
69
89
  const ack = state.acked.get(role) ?? 0;
@@ -71,9 +91,13 @@ export function pollStreakGate(root, state, runners, pausedRoles, pauseKey = (r)
71
91
  continue;
72
92
  // Ack at trip time, before the pause is observed: the bar for the next trip is
73
93
  // ERROR_STREAK_BREAKER more failures from this level, whether or not any poll sees the
74
- // role paused in between (an operator resume racing the next poll must not re-trip).
94
+ // role paused in between (an operator resume racing the next poll must not re-trip). The
95
+ // durable copy is written only once the pause is confirmed below — a failed or crashed
96
+ // pauseRole must not leave a permanent excused ack with no marker behind it.
75
97
  state.acked.set(role, streak);
76
98
  if (pauseRole(root, pauseKey(role))) {
99
+ s.streakAcked = streak;
100
+ saveRunnerStateBestEffort(root, s);
77
101
  logEventBestEffort(root, {
78
102
  source: TUMWATER_SOURCE,
79
103
  type: "role_streak_paused",
@@ -1,13 +1,13 @@
1
- // vocabulary: plain
2
- import { truncate } from "../text/text.js";
1
+ import { truncate, joinParts } from "../text/text.js";
3
2
  import { compactTokens } from "../text/format.js";
3
+ import { modelLabel } from "../agent/backend-traits.js";
4
4
  import { labeledLine } from "../verdict/reply-contract.js";
5
- /** Assembling tick commit messages from pi's final reply: the SUMMARY line becomes the
5
+ /** Assembling run commit messages from pi's final reply: the SUMMARY line becomes the
6
6
  * subject, the WHY/RISK/VERIFIED block becomes the body, and the harness stamps a trailer
7
- * with tick metadata. Split out of prompt.ts — which keeps building the prompts that declare
7
+ * with run metadata. Split out of prompt.ts — which keeps building the prompts that declare
8
8
  * this contract (the machine-detectable half — sentinel and verdict-line detection — lives in
9
9
  * reply-contract.ts) — because turning a reply into what `git commit` receives is the git side
10
- * of the contract, consumed by the tick pipeline (tick-stage.ts, tick-verdict.ts) and
10
+ * of the contract, consumed by the run pipeline (run-stage.ts, run-verdict.ts) and
11
11
  * refusal.ts. */
12
12
  /** Cap on the commit subject's summary portion (the SUMMARY line), so a verbose model cannot
13
13
  * bloat every commit subject. Truncated with an ellipsis — like the body fields below — rather
@@ -25,23 +25,39 @@ export function extractSummary(finalText) {
25
25
  const summary = labeledLine(finalText, "SUMMARY");
26
26
  return summary === null ? null : truncate(summary, COMMIT_SUMMARY_MAX);
27
27
  }
28
- /** The subject for a changed tick whose reply — and whose follow-up request — carried no
28
+ /** The subject for a changed run whose reply — and whose follow-up request — carried no
29
29
  * SUMMARY line: named from what actually changed, so the log still says what the commit touched
30
- * instead of only which loop made it. Up to three repo-relative paths (worktree order), then a
30
+ * instead of only which role made it. Up to three repo-relative paths (worktree order), then a
31
31
  * count; capped like a real summary. Falls back to the bare "<role> run N" only when nothing is
32
32
  * known. */
33
- export function fallbackSummary(files, role, tick) {
33
+ export function fallbackSummary(files, role, runNumber) {
34
34
  if (files.length === 0)
35
- return `${role} run ${tick}`;
35
+ return `${role} run ${runNumber}`;
36
36
  const shown = files.slice(0, 3).join(", ");
37
37
  const rest = files.length > 3 ? ` and ${files.length - 3} more` : "";
38
38
  return truncate(`Update ${shown}${rest}`, COMMIT_SUMMARY_MAX);
39
39
  }
40
- /** Cap on each commit-body field, so a verbose model cannot bloat every commit. */
41
- const COMMIT_BODY_FIELD_MAX = 200;
40
+ /** Cap on each commit-body field. It only bounds a runaway reply: at 200 it cut almost half of
41
+ * all body lines, which reviewers rejected as incomplete claims. */
42
+ const COMMIT_BODY_FIELD_MAX = 1000;
43
+ /** A numbered `Objection N: <disposition>` line a revision run's reply carries (the closing
44
+ * contract buildRevisionNote teaches, gate-notes.ts) — the author's answer to the prior
45
+ * review's Nth numbered objection. Anchored at line start so prose that merely mentions an
46
+ * objection mid-sentence cannot be read as one. */
47
+ const OBJECTION_LINE = /^\s*Objection (\d+):\s*(.+)\s*$/gm;
48
+ /** Pull the numbered Objection lines out of a reply, each capped like a body field. */
49
+ function extractObjections(finalText) {
50
+ const out = [];
51
+ for (const m of finalText.matchAll(OBJECTION_LINE)) {
52
+ out.push({ n: Number(m[1]), text: truncate(m[2] ?? "", COMMIT_BODY_FIELD_MAX) });
53
+ }
54
+ // Preserve the numbered order regardless of where the author placed each line.
55
+ out.sort((a, b) => a.n - b.n);
56
+ return out;
57
+ }
42
58
  /** Pull the WHY:/RISK:/VERIFIED: lines out of a pi final reply via the shared labeled-line
43
59
  * parser, tolerant of any subset being absent (a non-compliant reply still commits), each field
44
- * capped at 200 chars (truncate + ellipsis). Null when none were found. */
60
+ * capped at COMMIT_BODY_FIELD_MAX (truncate + ellipsis). Null when none were found. */
45
61
  export function extractCommitBody(finalText) {
46
62
  const pick = (label) => {
47
63
  const v = labeledLine(finalText, label);
@@ -52,50 +68,59 @@ export function extractCommitBody(finalText) {
52
68
  risk: pick("RISK"),
53
69
  verified: pick("VERIFIED"),
54
70
  };
55
- return body.why || body.risk || body.verified ? body : null;
56
- }
57
- /** Merge a follow-up reply's body over the tick's original body, field by field: a follow-up that
58
- * restates only some of WHY/RISK/VERIFIED overrides those fields and leaves the others intact. A
59
- * wholesale replacement would silently drop the fields a partial reply did not restate, losing the
60
- * authoring run's recorded reasoning; returns `base` unchanged when `update` carries no field. */
61
- export function mergeCommitBody(base, update) {
62
- if (!update)
71
+ const objections = extractObjections(finalText);
72
+ if (objections.length > 0)
73
+ body.objections = objections;
74
+ return body.why || body.risk || body.verified || objections.length > 0 ? body : null;
75
+ }
76
+ /** Add the pre-queue self-check fix-up turn's RISK and VERIFIED lines to the run's body, each
77
+ * after the authoring run's own line as `(self-check: …)`. The fix-up turn answers the
78
+ * self-check's findings, not the change. When its lines replaced the authoring run's, the commit
79
+ * described that answer over the whole diff ("No code changed" on a full feature), and review
80
+ * rejected it as claims-vs-diff (BUGS.md 2026-10-21). Its WHY is dropped for the same reason.
81
+ * Returns `base` unchanged when the reply has neither line. */
82
+ export function appendSelfCheckNotes(base, fixUp) {
83
+ if (!fixUp?.risk && !fixUp?.verified)
63
84
  return base;
64
- const merged = {
65
- why: update.why ?? base?.why,
66
- risk: update.risk ?? base?.risk,
67
- verified: update.verified ?? base?.verified,
68
- };
69
- return merged.why || merged.risk || merged.verified ? merged : null;
85
+ const note = (own, added) => added === undefined ? own : own === undefined ? `self-check: ${added}` : `${own} (self-check: ${added})`;
86
+ return { ...base, risk: note(base?.risk, fixUp.risk), verified: note(base?.verified, fixUp.verified) };
70
87
  }
71
- /** The body's lines in commit order ("WHY: …\nRISK: …\nVERIFIED: …"); "" when the body is
72
- * empty. */
88
+ /** The body's lines in commit order ("WHY: …\nRISK: …\nVERIFIED: …\nObjection N: …"); "" when
89
+ * the body has no fields. Objection lines follow the VERIFIED line, numbered as the author
90
+ * wrote them. Empty when the body is empty. */
73
91
  export function formatCommitBody(body) {
74
- return [
92
+ return joinParts([
75
93
  body.why && `WHY: ${body.why}`,
76
94
  body.risk && `RISK: ${body.risk}`,
77
95
  body.verified && `VERIFIED: ${body.verified}`,
78
- ]
79
- .filter(Boolean)
80
- .join("\n");
96
+ ...(body.objections ?? []).map((o) => `Objection ${o.n}: ${o.text}`),
97
+ ], "\n");
81
98
  }
82
- /** The harness-stamped trailer line of every tick commit — truth from the run's counters, not
83
- * model claims. `turns` sums this tick's pre-commit pi runs (main + transient retry); `peakCtx`
84
- * is their largest single-request context. A changed tick flagged high-friction (plans/
99
+ /** The harness-stamped trailer line of every run commit — truth from the run's counters, not
100
+ * model claims. `turns` sums this run's pre-commit pi runs (main + transient retry); `peakCtx`
101
+ * is their largest single-request context. A changed run flagged high-friction (plans/
85
102
  * refusal-and-thrash.md) appends a sibling Friction line after the Run line — minutes rounded,
86
- * e.g. `Friction: high (41 turns / 62m)`; only changed ticks carry it, so the refusal path
87
- * never passes the argument and the Run line's asserted format stays stable. */
88
- export function commitTrailer(role, tick, turns, peakCtx, highFrictionMinutes, revisionRound) {
89
- const base = `Run: ${role} #${tick} · turns ${turns} · ctx ${compactTokens(peakCtx)}`;
103
+ * e.g. `Friction: high (41 turns / 62m)`; only changed runs carry it, so the refusal path
104
+ * never passes the argument and the Run line's asserted format stays stable. A `modelLine` (see
105
+ * modelTrailerLine) goes on its own line directly after the Run line, before Friction and
106
+ * Revision, so Revision stays last. */
107
+ export function commitTrailer(role, runNumber, turns, peakCtx, highFrictionMinutes, revisionRound, modelLine) {
108
+ const base = `Run: ${role} #${runNumber} · turns ${turns} · ctx ${compactTokens(peakCtx)}`;
109
+ const model = modelLine === undefined ? "" : `\n${modelLine}`;
90
110
  const friction = highFrictionMinutes === undefined
91
111
  ? ""
92
112
  : `\nFriction: high (${turns} turns / ${Math.round(highFrictionMinutes)}m)`;
93
113
  // The revision line is last and absent on a fresh change: leftover recovery reads it back to
94
114
  // keep the rejected change's round when the queue marker is lost (plans/revise-rejected.md).
95
115
  const revision = revisionRound === undefined ? "" : `\nRevision: ${revisionRound}`;
96
- return `${base}${friction}${revision}`;
116
+ return `${base}${model}${friction}${revision}`;
97
117
  }
98
- /** The high-friction trailer line a flagged tick's commit carries (see commitTrailer) — the
118
+ /** The trailer's Model line: the model and backend a run's seam started on, e.g.
119
+ * `Model: claude-opus-5-5 · backend claude`; `default` when the seam names no model. */
120
+ export function modelTrailerLine(agent, provider, model) {
121
+ return `Model: ${model === undefined ? "default" : modelLabel(agent, provider, model)} · backend ${agent}`;
122
+ }
123
+ /** The high-friction trailer line a flagged run's commit carries (see commitTrailer) — the
99
124
  * durable record recovery reads back. Anchored to the whole line so prose that merely mentions
100
125
  * friction cannot set the flag. */
101
126
  const FRICTION_TRAILER = /^Friction: high \(\d+ turns \/ \d+m\)$/m;
@@ -110,15 +135,15 @@ const REVISION_TRAILER = /^Revision: (\d+)$/m;
110
135
  * undefined for a fresh change. Leftover recovery reads this off the pinned commit so a
111
136
  * retriable revision keeps its round (and its rejected ref's cleanup) across a lost queue
112
137
  * marker (plans/revise-rejected.md). */
113
- function revisionRoundOf(message) {
138
+ export function revisionRoundOf(message) {
114
139
  const m = REVISION_TRAILER.exec(message);
115
140
  return m ? Number(m[1]) : undefined;
116
141
  }
117
- /** The harness-stamped commit-subject prefix — the `tumwater(<role>):` part tick-stage.ts (and
142
+ /** The harness-stamped commit-subject prefix — the `tumwater(<role>):` part run-stage.ts (and
118
143
  * refusal.ts) adds before the SUMMARY line, space included when a summary follows. */
119
- const SUBJECT_PREFIX = /^tumwater\([^)]+\):(?:\s|$)/;
144
+ const SUBJECT_PREFIX = /^tumwater\(([^)]+)\):(?:\s|$)/;
120
145
  /** Build a harness-stamped commit subject: `tumwater(<role>): <summary>` — the exact shape
121
- * SUBJECT_PREFIX below matches, so the stamp's writers (tick-stage.ts's normal tick commit and
146
+ * SUBJECT_PREFIX below matches, so the stamp's writers (run-stage.ts's normal run commit and
122
147
  * refusal.ts's refusal note) and its parser live in one file and cannot drift apart. */
123
148
  export function stampedSubject(role, summary) {
124
149
  return `tumwater(${role}): ${summary}`;
@@ -137,6 +162,12 @@ export function commitSubject(message) {
137
162
  const rest = line.slice(m[0].length).trim();
138
163
  return rest === "" ? undefined : rest;
139
164
  }
165
+ /** The `<role>` of a commit subject the harness stamped (`tumwater(<role>): …`); undefined for an
166
+ * unstamped line. */
167
+ export function stampedRole(message) {
168
+ const line = (message.split("\n", 1)[0] ?? "").trim();
169
+ return SUBJECT_PREFIX.exec(line)?.[1];
170
+ }
140
171
  /** Reconstruct the review-gate metadata from a commit message the harness stamped itself.
141
172
  * Leftover recovery has no authoring run to read from, so the pinned commit's message is the
142
173
  * only source of truth: without this a recovered high-friction change loses both its flag and
@@ -153,7 +184,7 @@ export function parseCommitMetadata(message) {
153
184
  ...(revisionRound !== undefined ? { revisionRound } : {}),
154
185
  };
155
186
  }
156
- /** Assemble a tick's full commit message — the single place that builds one. The subject is
187
+ /** Assemble a run's full commit message — the single place that builds one. The subject is
157
188
  * the existing "tumwater(<role>): <summary>" line; the author's body (omitted when absent)
158
189
  * and the harness-stamped trailer follow as separate paragraphs. Refusal commits route
159
190
  * through here too (subject + trailer only). */
@@ -3,7 +3,7 @@
3
3
  * ahead-of-main diff extraction (including its oversized-diff truncation). Everything here
4
4
  * interprets git output; spawning git stays in git.ts. */
5
5
  import { changeBaseRev, gitLines } from "./git.js";
6
- import { gitTry } from "./git-run.js";
6
+ import { git, gitTry } from "./git-run.js";
7
7
  import { cutSplitsSurrogatePair } from "../text/text.js";
8
8
  /** Git's C-quoting short escapes (its `sq_lookup` table) and the byte each decodes to. The
9
9
  * mapping was a nine-case switch that repeated `bytes += …; break;` per entry; keeping it as
@@ -68,7 +68,10 @@ export function unquotePorcelainPath(p) {
68
68
  }
69
69
  /** Repo-relative paths of every change in the worktree — modified, untracked, and deleted, parsed
70
70
  * from `git status --porcelain -z` (paths only). The refusal path uses this to classify what a
71
- * refusing run left behind: markdown notes may be kept, everything else is discarded.
71
+ * refusing run left behind: markdown notes may be kept, everything else is discarded. A failed
72
+ * `git status` throws: read as "no changes", it once sent a refusal's or a self-check's
73
+ * decision down the path that drops the run's work (2026-10-21's spawn wedge), so a caller that
74
+ * only reports (a view, a warning) catches it itself.
72
75
  *
73
76
  * The `-z` format is what git recommends for machine parsing: records are NUL-terminated and
74
77
  * paths are emitted verbatim, never C-quoted, so a path with whitespace, a quote, a control
@@ -79,7 +82,7 @@ export function unquotePorcelainPath(p) {
79
82
  * `-z` a rename/copy is two records — `XY <to>\0<from>\0`, destination first — so this reads
80
83
  * the destination (the path that exists now) and skips the extra origin record. */
81
84
  export async function changedFiles(wt) {
82
- const out = await gitTry(wt, "status", "--porcelain", "-z");
85
+ const out = await git(wt, "status", "--porcelain", "-z");
83
86
  if (!out)
84
87
  return [];
85
88
  const files = [];
@@ -101,9 +104,11 @@ export async function changedFiles(wt) {
101
104
  }
102
105
  /** Repo-relative paths changed between the branch's fork point from main and its HEAD —
103
106
  * everything a merge of this branch would bring in (the three-dot range diffs against the
104
- * merge-base, so commits main gained during the tick are not included). */
107
+ * merge-base, so commits main gained during the run are not included). A failed `git diff`
108
+ * throws: an empty list is vacuously review-exempt (isExemptDiff), so the gate skipped its
109
+ * reviewer and the in-lock re-check its build check. A view catches it itself. */
105
110
  export async function aheadOfMainFiles(wt, mainBranch) {
106
- const out = await gitTry(wt, "diff", "--name-only", `${mainBranch}...HEAD`);
111
+ const out = await git(wt, "diff", "--name-only", `${mainBranch}...HEAD`);
107
112
  // Decode git's C-quoted names (same convention as `git status --porcelain`): every caller
108
113
  // uses these as real worktree paths or matches them against plain HEAD-provided names, and
109
114
  // an undecoded `"h\303\251llo.md"` reads as a nonexistent file (hasConflictMarkers) and
@@ -143,19 +148,20 @@ function splitDiffByFile(diff) {
143
148
  * oversized diff is itself reviewable information, and the reviewer can read any file in the
144
149
  * worktree directly). The per-file sections are split out of the one full diff already fetched —
145
150
  * spawning a diff per file would re-diff the entire range once per file for text we already hold in
146
- * memory (measured ~0.5 s of redundant git work on a 530 KB / 39-file tick). Section byte length
151
+ * memory (measured ~0.5 s of redundant git work on a 530 KB / 39-file run). Section byte length
147
152
  * ranks files at least as well as numstat's added+deleted: it is exactly what the reviewer will
148
153
  * see, and a binary file's short "Binary files differ" stub naturally ranks below any real text
149
- * change. */
150
- export async function aheadOfMainDiff(wt, mainBranch, maxBytes = 200_000) {
154
+ * change. A failed `git diff` throws: the reviewer handed an empty diff judged no change at
155
+ * all. A view catches it itself. */
156
+ export async function aheadOfMainDiff(wt, mainBranch, maxBytes = 200_000, pathspec = []) {
151
157
  const range = `${mainBranch}...HEAD`;
152
- const full = (await gitTry(wt, "diff", range)) ?? "";
158
+ const full = await git(wt, "diff", range, ...pathspec);
153
159
  if (full.length <= maxBytes)
154
160
  return full;
155
161
  // Over the cap: rank files by change size and include the largest while budget allows.
156
162
  const ranked = splitDiffByFile(full).sort((a, b) => b.length - a.length);
157
163
  let out = `[diff truncated: the full ahead-of-main diff is ${full.length} bytes; ` +
158
- `showing --stat plus the largest files]\n\n` + ((await gitTry(wt, "diff", "--stat", range)) ?? "") + "\n";
164
+ `showing --stat plus the largest files]\n\n` + (await git(wt, "diff", "--stat", range, ...pathspec)) + "\n";
159
165
  for (const d of ranked) {
160
166
  // Account for the surrounding newlines in the budget check so the output never exceeds maxBytes
161
167
  // even when a section ends exactly on the boundary.