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,19 +1,18 @@
1
- // vocabulary: plain
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { briefCandidates } from "../paths.js";
5
4
  import { errorMessage, truncateWithNote } from "../text/text.js";
6
5
  /** The managed sections of the project brief (TUMWATER.md, with README.md as the
7
6
  * compatibility path — plans/portability.md §7a/7): marker constants, the templates a fresh
8
- * repo gets, and reading the project's initial prompt back out. */
7
+ * repo gets, and reading the project's brief text back out. */
9
8
  export const PROMPT_START = "<!-- tumwater:prompt:start -->";
10
9
  export const PROMPT_END = "<!-- tumwater:prompt:end -->";
11
- /** Cap on the initial prompt (see readInitialPrompt). The prompt is the project's reason to
12
- * exist and rides into EVERY tick and director prompt, so unbounded text would be a standing
13
- * per-tick prefill cost — the same budget customRoles.task and roles.<id>.instructions are
10
+ /** Cap on the brief text (see readBriefText). The prompt is the project's reason to
11
+ * exist and rides into EVERY run and director prompt, so unbounded text would be a standing
12
+ * per-run prefill cost — the same budget customRoles.task and roles.<id>.instructions are
14
13
  * held to. `tumwater init` rejects an over-long prompt before it is committed (init.ts); this
15
14
  * constant is the shared bound so the read path and the init check cannot drift. */
16
- export const INITIAL_PROMPT_MAX_CHARS = 4096;
15
+ export const BRIEF_TEXT_MAX_CHARS = 4096;
17
16
  // The status markers stay exported too: the readme role targets the resolved brief file, not
18
17
  // a hardcoded README.md, and doctor's brief check reports which file holds the sections. The
19
18
  // prompt markers above stay exported because init.ts names them in its error message.
@@ -21,21 +20,21 @@ export const STATUS_START = "<!-- tumwater:status:start -->";
21
20
  export const STATUS_END = "<!-- tumwater:status:end -->";
22
21
  /** The placeholder every fresh template seeds between the status markers. A brief still
23
22
  * carrying exactly this body (or an empty one) is "description incomplete"; once the readme
24
- * role replaces it with a real status the tight-loop rule turns itself off. Hoisted so the
25
- * template and briefStatusIncomplete cannot drift on the wording. */
26
- export const EMPTY_STATUS = "_No status yet. The readme role keeps this section up to date._";
27
- /** The two managed sections a fresh brief carries — the initial prompt in its prompt
23
+ * role replaces it with a real status the project's description is complete. Hoisted so the
24
+ * template and the readme role's real-status replacement cannot drift on the wording. */
25
+ const EMPTY_STATUS = "_No status yet. The readme role keeps this section up to date._";
26
+ /** The two managed sections a fresh brief carries — the brief text in its prompt
28
27
  * markers, an empty status in its status markers — under the given H1. Both homes render
29
28
  * the identical body (README.md for a fresh repo, TUMWATER.md for an adopted one), so the
30
29
  * body lives here once and the two templates cannot drift on section wording or marker
31
30
  * placement; the H1 is the only difference between them. */
32
- function managedSections(heading, initialPrompt) {
31
+ function managedSections(heading, briefText) {
33
32
  return `# ${heading}
34
33
 
35
34
  ## Brief
36
35
 
37
36
  ${PROMPT_START}
38
- ${initialPrompt.trim()}
37
+ ${briefText.trim()}
39
38
  ${PROMPT_END}
40
39
 
41
40
  ## Status
@@ -45,19 +44,19 @@ ${EMPTY_STATUS}
45
44
  ${STATUS_END}
46
45
  `;
47
46
  }
48
- /** The README.md a fresh repo starts with: the initial prompt and an empty status,
47
+ /** The README.md a fresh repo starts with: the brief text and an empty status,
49
48
  * each in its managed section. */
50
- export function readmeTemplate(projectName, initialPrompt) {
51
- return managedSections(projectName, initialPrompt);
49
+ export function readmeTemplate(projectName, briefText) {
50
+ return managedSections(projectName, briefText);
52
51
  }
53
52
  /** The TUMWATER.md an adopted repo gets (plans/portability.md §7a/7, written by 7b's adopt
54
53
  * path): the same two managed sections as the README template, under a brief heading —
55
54
  * so the resolution logic in this module is the only difference between the two homes. */
56
- export function briefTemplate(projectName, initialPrompt) {
57
- return managedSections(`${projectName} — project brief`, initialPrompt);
55
+ export function briefTemplate(projectName, briefText) {
56
+ return managedSections(`${projectName} — project brief`, briefText);
58
57
  }
59
58
  /** True when `text` carries a well-formed managed prompt block: opening marker, then a
60
- * closing one strictly after it (readInitialPrompt's ordering guard). */
59
+ * closing one strictly after it (readBriefText's ordering guard). */
61
60
  function ownsBrief(text) {
62
61
  const start = text.indexOf(PROMPT_START);
63
62
  if (start < 0)
@@ -72,8 +71,8 @@ function ownsBrief(text) {
72
71
  *
73
72
  * A candidate that exists but cannot be read (README.md created as a directory, a
74
73
  * permission-lost file) is an operator-visible break, not a silent skip: the brief is
75
- * every loop's reason to exist, read each tick, so falling through to the next candidate
76
- * could run the team on the wrong brief and throwing raw would crash every tick with an
74
+ * every role's reason to exist, read each run, so falling through to the next candidate
75
+ * could run the team on the wrong brief and throwing raw would crash every run with an
77
76
  * errno that names neither the file nor the fix — the message below names both. */
78
77
  function firstBriefText(root, accepts) {
79
78
  for (const candidate of briefCandidates(root)) {
@@ -93,37 +92,18 @@ function firstBriefText(root, accepts) {
93
92
  }
94
93
  /** Which file owns the project's managed sections — the basename of the first candidate
95
94
  * that carries a well-formed prompt block, or null when none does. Callers that only need
96
- * the prompt use readInitialPrompt; this exists for the prompt builders (which name the
95
+ * the prompt use readBriefText; this exists for the prompt builders (which name the
97
96
  * actual file) and doctor's brief check. */
98
97
  export function briefFile(root) {
99
98
  return readBrief(root)?.file ?? null;
100
99
  }
101
- /** False when there is no resolved brief, or the resolved brief carries no well-formed
102
- * STATUS_START/STATUS_END pair (a malformed brief is doctor's warning, not a tight loop);
103
- * otherwise true when the status body between the markers is empty or still the EMPTY_STATUS
104
- * placeholder. The one machine-detectable "description not yet complete" signal: the initial
105
- * prompt half of the description is always present (init rejects an empty one). One small
106
- * single-file read, like readInitialPrompt. */
107
- export function briefStatusIncomplete(root) {
108
- const found = firstBriefText(root, ownsBrief);
109
- if (!found)
110
- return false;
111
- const bodyStart = found.text.indexOf(STATUS_START);
112
- if (bodyStart < 0)
113
- return false;
114
- const bodyEnd = found.text.indexOf(STATUS_END, bodyStart + STATUS_START.length);
115
- if (bodyEnd < 0)
116
- return false;
117
- const body = found.text.slice(bodyStart + STATUS_START.length, bodyEnd).trim();
118
- return body === "" || body === EMPTY_STATUS;
119
- }
120
- /** The project's initial prompt, extracted from the resolved brief file's managed section —
100
+ /** The project's brief text, extracted from the resolved brief file's managed section —
121
101
  * TUMWATER.md when it exists with markers, else README.md (plans/portability.md §7a/7). The
122
102
  * closing marker is searched for only AFTER the opening one: an end marker mentioned in prose
123
103
  * before the real block (e.g. README docs explaining how to edit the prompt) must not make a
124
- * valid later block unreadable — every loop would then run without its project prompt, and
104
+ * valid later block unreadable — every role would then run without its project prompt, and
125
105
  * init's guard would reject re-init as if the section were missing. */
126
- export function readInitialPrompt(root) {
106
+ export function readBriefText(root) {
127
107
  return readBrief(root)?.prompt ?? "";
128
108
  }
129
109
  /** The full path of the file that owns the prompt block (the same candidate readBrief
@@ -135,7 +115,7 @@ export function briefPath(root) {
135
115
  /** `text` with the prompt block's content replaced by `prompt` (trimmed), every byte outside
136
116
  * the two markers kept; null when the text has no well-formed block. Finds the block by
137
117
  * extractPrompt's rule (first start marker, then the first end marker after it). */
138
- export function replaceInitialPrompt(text, prompt) {
118
+ export function replaceBriefText(text, prompt) {
139
119
  const start = text.indexOf(PROMPT_START);
140
120
  if (start < 0)
141
121
  return null;
@@ -161,22 +141,22 @@ function extractPrompt(text) {
161
141
  if (prompt === null)
162
142
  return null;
163
143
  // A hand-edited brief can carry an over-long prompt past init's length check. Truncate it
164
- // for every tick instead of injecting the whole thing (the same defensive cap readPrinciples
144
+ // for every run instead of injecting the whole thing (the same defensive cap readPrinciples
165
145
  // applies, through text.ts's truncateWithNote), with a visible note so the loss is not
166
146
  // silent. init rejects the normal path before it is committed, so this is the backstop.
167
- return truncateWithNote(prompt, INITIAL_PROMPT_MAX_CHARS, "brief");
147
+ return truncateWithNote(prompt, BRIEF_TEXT_MAX_CHARS, "brief");
168
148
  }
169
149
  /** The resolved brief as one value: the owning file's basename (briefFile) and its extracted
170
- * initial prompt (readInitialPrompt) from a SINGLE firstBriefText scan. Tick prompt assembly
171
- * asks for both on every tick, and briefFile + readInitialPrompt each re-ran the candidate
172
- * scan and re-read the file — two whole-file reads per tick for one file's worth of content.
173
- * readInitialPrompt and briefFile now derive from this, so there is one read path; null when
150
+ * brief text (readBriefText) from a SINGLE firstBriefText scan. Run prompt assembly
151
+ * asks for both on every run, and briefFile + readBriefText each re-ran the candidate
152
+ * scan and re-read the file — two whole-file reads per run for one file's worth of content.
153
+ * readBriefText and briefFile now derive from this, so there is one read path; null when
174
154
  * no candidate carries a well-formed prompt block (both derived values are then empty/null). */
175
155
  export function readBrief(root) {
176
156
  const found = firstBriefText(root, ownsBrief);
177
157
  return found ? { file: found.file, prompt: extractPrompt(found.text) ?? "" } : null;
178
158
  }
179
- /** The brief's plain-text form: its file name, then the initial prompt — what `tumwater brief`
159
+ /** The brief's plain-text form: its file name, then the brief text — what `tumwater brief`
180
160
  * prints and the TUI's Brief tab shows, so the two cannot drift. */
181
161
  export function renderBrief(brief) {
182
162
  return `${brief.file}\n${brief.prompt}`;
@@ -1,17 +1,17 @@
1
- /** The daily-cost budget: a loop's and the team's local-day spend and the gate those figures
1
+ /** The daily-cost budget: a role's and the team's local-day spend and the gate those figures
2
2
  * drive (plans/daily-cost-budget.md, plans/fallback-model.md). The circuit breaker that judges
3
3
  * whether the gate's fallback can actually serve lives beside it in fallback-breaker.ts. Split
4
- * out of loop-state.ts, which owns
5
- * LoopState persistence and scheduling bookkeeping: the budget is POLICY both the scheduler and
4
+ * out of runner-state.ts, which owns
5
+ * RunnerState persistence and scheduling bookkeeping: the budget is POLICY both the scheduler and
6
6
  * every observer evaluate from the same definitions. Kept free of the scheduler and of pi's
7
7
  * model catalog (fallback readiness arrives as plain booleans), so observers can depend on it
8
8
  * without importing either — the one-way rule that previously kept these functions in
9
- * loop-state.ts. */
9
+ * runner-state.ts. */
10
10
  import { MODEL_TIERS } from "../config/config-schema.js";
11
- import { nonNegativeNumber } from "../files/json-object.js";
11
+ import { nonNegativeNumber, setOwn } from "../files/json-object.js";
12
12
  import { dayAt, dayKey } from "../text/datetime.js";
13
13
  /** The local calendar day as YYYY-MM-DD — the same local-time convention as every other
14
- * wall-clock display in the harness (lastTickCell). */
14
+ * wall-clock display in the harness (lastRunCell). */
15
15
  export function todayStamp(now = Date.now()) {
16
16
  return dayKey(now);
17
17
  }
@@ -22,10 +22,10 @@ export function todayStamp(now = Date.now()) {
22
22
  function spendNumber(value) {
23
23
  return nonNegativeNumber(value, 0);
24
24
  }
25
- /** This loop's spend for the local day (the daily cost budget window): $0 when its stamp is
26
- * stale or missing — a loop that hasn't ticked since yesterday reads as $0 today with no save
25
+ /** This role's spend for the local day (the daily cost budget window): $0 when its stamp is
26
+ * stale or missing — a role that hasn't ran since yesterday reads as $0 today with no save
27
27
  * required, and spend recorded before this field existed is unknown. A non-number,
28
- * non-finite, or negative value reads as $0 too: `loadLoopState` heals the field on read, but
28
+ * non-finite, or negative value reads as $0 too: `loadRunnerState` heals the field on read, but
29
29
  * this is the budget gate's own guard, and a hand-edited or torn `dayCostUsd` (or an in-memory
30
30
  * state) would otherwise concatenate into
31
31
  * teamDailyCost's sum (or make every `>= cap` comparison false) and silently defeat the cap,
@@ -33,11 +33,11 @@ function spendNumber(value) {
33
33
  export function dailyCost(s, now = Date.now()) {
34
34
  return s.dayStamp === todayStamp(now) ? spendNumber(s.dayCostUsd) : 0;
35
35
  }
36
- /** Record a pi run's cost into the loop's daily window, rolling over at local midnight:
37
- * when `now` is on a different day than the recorded stamp the window resets first, so a tick
36
+ /** Record a pi run's cost into the role's daily window, rolling over at local midnight:
37
+ * when `now` is on a different day than the recorded stamp the window resets first, so a run
38
38
  * that crosses midnight attributes its spend to the correct day. Mutates `s` in place — like
39
- * applyTickOutcome and foldUsage's other counter updates, the caller's state object is
40
- * authoritative across an in-flight tick; the value persists at tick end with the rest of the
39
+ * applyRunOutcome and foldUsage's other counter updates, the caller's state object is
40
+ * authoritative across an in-flight run; the value persists at run end with the rest of the
41
41
  * state. Both operands are guarded through spendNumber so a corrupt stored value or a
42
42
  * non-finite/negative run cost cannot poison the window with a string/NaN/negative sum. */
43
43
  export function recordDailyCost(s, usd, now = Date.now(), pair) {
@@ -53,10 +53,10 @@ export function recordDailyCost(s, usd, now = Date.now(), pair) {
53
53
  if (pair !== undefined) {
54
54
  const by = (s.dayCostByPair ??= {});
55
55
  const sum = spendNumber(Object.hasOwn(by, pair) ? by[pair] : 0) + spendNumber(usd);
56
- Object.defineProperty(by, pair, { value: sum, enumerable: true, writable: true, configurable: true });
56
+ setOwn(by, pair, sum);
57
57
  }
58
58
  }
59
- /** One model pair's spend for the local day, summed over every loop's per-pair window
59
+ /** One model pair's spend for the local day, summed over every role's per-pair window
60
60
  * (`dayCostByPair`): $0 for a stale day stamp or a pair no run has charged. */
61
61
  export function pairDailyCost(states, pair, now = Date.now()) {
62
62
  const today = todayStamp(now);
@@ -65,14 +65,24 @@ export function pairDailyCost(states, pair, now = Date.now()) {
65
65
  return sum + (by !== undefined && Object.hasOwn(by, pair) ? spendNumber(by[pair]) : 0);
66
66
  }, 0);
67
67
  }
68
- /** The team's spend for the local day: every loop's daily window summed. */
68
+ /** The team's spend for the local day: every role's daily window summed. */
69
69
  export function teamDailyCost(states, now = Date.now()) {
70
70
  return states.reduce((sum, s) => sum + dailyCost(s, now), 0);
71
71
  }
72
+ /** The roles that spent most today, highest first (ties by role name), at most `limit`; roles
73
+ * with no spend today (zero or a stale day window) are left out. Each state is its own role, so
74
+ * `feature` and `feature-2` are listed apart. */
75
+ export function topDailySpenders(states, now = Date.now(), limit = 3) {
76
+ return states
77
+ .map((s) => ({ role: s.role, spentUsd: dailyCost(s, now) }))
78
+ .filter((r) => r.spentUsd > 0)
79
+ .sort((a, b) => b.spentUsd - a.spentUsd || (a.role < b.role ? -1 : a.role > b.role ? 1 : 0))
80
+ .slice(0, limit);
81
+ }
72
82
  /** True when today's spend has reached the daily cost budget — the view is non-null, its cap is
73
83
  * enabled (capUsd > 0; a cap of 0 disables the gate regardless of spend), and spend sits at or
74
84
  * above it. The single definition of "the budget gate is on": the orchestrator evaluates it from
75
- * live loop states via budgetPaused, and both dashboards evaluate it from the snapshot's
85
+ * live runner states via budgetPaused, and both dashboards evaluate it from the snapshot's
76
86
  * materialized budget field (status/status-data.ts) — which carries the object even while
77
87
  * disabled, so the capUsd > 0 term is what keeps a disabled team out of "budget paused". The
78
88
  * comparison cannot drift between what the scheduler enforces and what users see. */
@@ -84,7 +94,7 @@ export function budgetReached(budget) {
84
94
  export const BUDGET_WARNING_FRACTION = 0.8;
85
95
  /** True when today's spend has crossed `BUDGET_WARNING_FRACTION` of the daily cap while the
86
96
  * budget gate is still open — the early heads-up beside budgetReached, which fires only once
87
- * the cap itself is reached and the team has already stopped (or demoted) its ticks. Same
97
+ * the cap itself is reached and the team has already stopped (or demoted) its runs. Same
88
98
  * guard terms as budgetReached (non-null view, a cap of 0 disables the budget), scaled by the
89
99
  * warning fraction, so the two views can never disagree about whether the budget is on. */
90
100
  export function budgetWarning(budget) {
@@ -98,7 +108,7 @@ export function budgetWarning(budget) {
98
108
  export function budgetPaused(states, config, now = Date.now()) {
99
109
  return budgetReached(budgetSpend(states, config, now));
100
110
  }
101
- /** The team's daily-budget figures from live loop states — the exact pair the gate evaluates:
111
+ /** The team's daily-budget figures from live runner states — the exact pair the gate evaluates:
102
112
  * today's summed spend and the configured cap (0 when disabled). budgetReached's own capUsd > 0
103
113
  * term makes a disabled cap read unreached, so the pair needs no null ceremony. Returning the
104
114
  * figures (not just the verdict) lets the scheduler publish WHAT it tripped on, not only THAT it
@@ -141,7 +151,7 @@ export function modelPairName(p) {
141
151
  * `strong` tiers resolved to a usable free pair (resolveTierFallbacks over the full usable
142
152
  * predicate — price AND breaker serving) — plus whether the review gate is on. The gate is
143
153
  * `paused` when `default` resolves to pause (nothing for the team's bulk to run on), or when
144
- * review is on and `strong` does (nothing could merge, so every authoring tick is wasted work);
154
+ * review is on and `strong` does (nothing could merge, so every authoring run is wasted work);
145
155
  * `fallback` otherwise, with the tiers that did resolve running their pairs and the rest held
146
156
  * per role. The single definition of the gate: the orchestrator enforces it and both dashboards
147
157
  * display it, so what an operator sees is what the scheduler is doing. */
@@ -1,16 +1,15 @@
1
- // vocabulary: plain
2
1
  /** The fallback's circuit breaker (BUGS.md 2026-09-20): whether the engaged free fallback can
3
2
  * actually SERVE. Split out of budget.ts, which keeps the daily-cost figures and the gate they
4
3
  * drive; the breaker is a self-contained pure state machine — no scheduler, no pi model
5
- * catalog, no persistence — that depends on nothing but the TickResult vocabulary, so the
6
- * orchestrator can hold it in memory and fold tick outcomes into it without importing the
4
+ * catalog, no persistence — that depends on nothing but the RunResult vocabulary, so the
5
+ * orchestrator can hold it in memory and fold run outcomes into it without importing the
7
6
  * budget. The gate reads its verdict through fallbackServing (budget.ts's budgetGate). */
8
- /** failureLimit 3 is a single loop's failing-streak threshold (tick-apply.ts ERROR_STREAK_WARN)
9
- * applied team-wide: consecutive across every role, so any tick that served in between resets
10
- * it — a healthy backend with one flaky tick never trips it, while a dead one trips on its
11
- * first three ticks (on 2026-09-19 the third failure landed 24 minutes into the hour; the 30
12
- * ticks after it were the waste). A 5-minute first cool-down because a failed probe costs one
13
- * tick — about half of that hour's failures came back in under 10 s, the rest ran minutes
7
+ /** failureLimit 3 is a single role's failing-streak threshold (run-apply.ts ERROR_STREAK_WARN)
8
+ * applied team-wide: consecutive across every role, so any run that served in between resets
9
+ * it — a healthy backend with one flaky run never trips it, while a dead one trips on its
10
+ * first three runs (on 2026-09-19 the third failure landed 24 minutes into the hour; the 30
11
+ * runs after it were the waste). A 5-minute first cool-down because a failed probe costs one
12
+ * run — about half of that hour's failures came back in under 10 s, the rest ran minutes
14
13
  * before the 400 — which is cheap next to idling until midnight; doubling to at most 30 minutes
15
14
  * bounds how long an operator who fixed the backend waits for the team to notice (editing the
16
15
  * cap or the pair re-trusts it at once — see rekeyFallbackBreaker). */
@@ -44,34 +43,34 @@ export function fallbackServing(b) {
44
43
  return b.probeAt === null;
45
44
  }
46
45
  /** Half-open: the demoted fallback's cool-down has elapsed and no probe is in flight, so one role
47
- * tick may start on it despite the `paused` gate. */
46
+ * run may start on it despite the `paused` gate. */
48
47
  export function fallbackProbeDue(b, now) {
49
48
  return b.pair !== null && b.probeAt !== null && !b.probing && now >= b.probeAt;
50
49
  }
51
- /** The breaker once the probe tick has been admitted: probing until recordFallbackTick hears
52
- * back, so every other due role waits for its verdict — one tick's evidence per cool-down, not
50
+ /** The breaker once the probe run has been admitted: probing until recordFallbackRun hears
51
+ * back, so every other due role waits for its verdict — one run's evidence per cool-down, not
53
52
  * a whole maxConcurrent wave of failures. */
54
53
  export function startFallbackProbe(b) {
55
54
  return { ...b, probing: true };
56
55
  }
57
56
  /** The breaker when an admitted probe never ran — turned away at its permit by the restart
58
57
  * hold's start gate: no evidence either way, so the claim is handed back and the probe is due
59
- * again at the next poll. Without this the breaker would wait on a verdict no tick will ever
58
+ * again at the next poll. Without this the breaker would wait on a verdict no run will ever
60
59
  * deliver, and the demoted pause could only clear on a rekey. */
61
60
  export function abandonFallbackProbe(b) {
62
61
  return { ...b, probing: false };
63
62
  }
64
- /** What one role tick that ran on the fallback says about its backend. `failed` is `error`
63
+ /** What one role run that ran on the fallback says about its backend. `failed` is `error`
65
64
  * alone: every way a backend cannot serve — an HTTP 4xx/5xx like oMLX's prefill guard, a
66
- * refused connection, a request that never returns (the tick timeout) — ends a tick as `error`,
65
+ * refused connection, a request that never returns (the run timeout) — ends a run as `error`,
67
66
  * and 33 of 33 did on 2026-09-19. (An `error` can also be local, e.g. a worktree that will not
68
67
  * reset; failureLimit of those in a row team-wide is still a team that cannot work, and the
69
68
  * probe wins the pause back.) `served` is a result only a model reply produces: a change bound
70
69
  * for merge (`queued`, `changed`), an explicit decision (`no_change`, `refused`), or a
71
70
  * reviewer's rejection. Everything else is no evidence: harness and operator kills (`aborted`,
72
- * `user_aborted`), ticks that never reached the model (`skipped`, `main_red`), a quiet kill
71
+ * `user_aborted`), runs that never reached the model (`skipped`, `main_red`), a quiet kill
73
72
  * (the model had answered before the stall — a tool call or the stream stopped), and the
74
- * merge results (`merge_conflict`, `merge_blocked`, `review_error`) — a tick no longer
73
+ * merge results (`merge_conflict`, `merge_blocked`, `review_error`) — a run no longer
75
74
  * returns them since leftover recovery queues its pin instead of merging it, and they would
76
75
  * not say whether the backend was involved. */
77
76
  export function fallbackEvidence(result) {
@@ -88,15 +87,15 @@ export function fallbackEvidence(result) {
88
87
  return "none";
89
88
  }
90
89
  }
91
- /** Fold one finished role tick that ran on the fallback into the breaker. `ranOn` is the breaker as
92
- * the tick started under it — evidence about a subject that is no longer engaged is dropped —
93
- * and `probe` says whether the orchestrator admitted it as the half-open probe. A served tick
90
+ /** Fold one finished role run that ran on the fallback into the breaker. `ranOn` is the breaker as
91
+ * the run started under it — evidence about a subject that is no longer engaged is dropped —
92
+ * and `probe` says whether the orchestrator admitted it as the half-open probe. A served run
94
93
  * closes the breaker from any state (the backend demonstrably works). A failed probe re-opens it
95
- * with the cool-down doubled up to maxCooldownMs; a failed tick while closed counts toward
94
+ * with the cool-down doubled up to maxCooldownMs; a failed run while closed counts toward
96
95
  * failureLimit and trips the breaker at it; a failed straggler that started before the trip leaves
97
96
  * the running cool-down alone. A probe with no evidence frees the probe slot, so the next due role
98
97
  * probes instead. */
99
- export function recordFallbackTick(b, ranOn, result, probe, now, policy = FALLBACK_BREAKER_POLICY) {
98
+ export function recordFallbackRun(b, ranOn, result, probe, now, policy = FALLBACK_BREAKER_POLICY) {
100
99
  if (ranOn.pair === null || ranOn.pair !== b.pair || ranOn.capUsd !== b.capUsd)
101
100
  return b;
102
101
  const isProbe = probe && b.probing;
@@ -143,7 +142,7 @@ export function fallbackServingPair(map, pair) {
143
142
  }
144
143
  /** The pair whose half-open window is due, or null when none is. Several pairs can carry a
145
144
  * demotion at once (one per resolved tier), so the walk returns the first due entry in map
146
- * order and the caller probes that single pair this tick. */
145
+ * order and the caller probes that single pair this run. */
147
146
  export function fallbackProbeDuePair(map, now) {
148
147
  for (const [pair, b] of Object.entries(map)) {
149
148
  if (fallbackProbeDue(b, now))
@@ -153,7 +152,7 @@ export function fallbackProbeDuePair(map, now) {
153
152
  }
154
153
  /** Apply `f` to the pair's entry when the map holds one, else return the map unchanged — the
155
154
  * shape shared by the map-scoped breaker transitions below (claim/hand back a probe, fold a
156
- * tick): a pair the resolution dropped has no judgment to touch. */
155
+ * run): a pair the resolution dropped has no judgment to touch. */
157
156
  function withPair(map, pair, f) {
158
157
  const b = map[pair];
159
158
  return b === undefined ? map : { ...map, [pair]: f(b) };
@@ -168,9 +167,9 @@ export function startFallbackProbeAt(map, pair) {
168
167
  export function abandonFallbackProbeAt(map, pair) {
169
168
  return withPair(map, pair, abandonFallbackProbe);
170
169
  }
171
- /** Fold one finished role tick into the pair named `pair`'s entry (recordFallbackTick's
170
+ /** Fold one finished role run into the pair named `pair`'s entry (recordFallbackRun's
172
171
  * semantics, scoped to the map): an absent entry has no judgment to fold into and is left
173
172
  * alone. */
174
- export function recordFallbackTickAt(map, pair, ranOn, result, probe, now, policy = FALLBACK_BREAKER_POLICY) {
175
- return withPair(map, pair, (b) => recordFallbackTick(b, ranOn, result, probe, now, policy));
173
+ export function recordFallbackRunAt(map, pair, ranOn, result, probe, now, policy = FALLBACK_BREAKER_POLICY) {
174
+ return withPair(map, pair, (b) => recordFallbackRun(b, ranOn, result, probe, now, policy));
176
175
  }
@@ -1,11 +1,10 @@
1
- // vocabulary: plain
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { isJsonObject } from "../files/json-object.js";
5
4
  import { readJsonFile } from "../files/json-files.js";
6
- import { walkUp } from "../files/files.js";
5
+ import { isDirectory, statOrNull, walkUp } from "../files/files.js";
7
6
  import { isNonBlankString } from "../text/text.js";
8
- /** Hard cap on one build check run — a hung script (watch mode) must not wedge the tick, and
7
+ /** Hard cap on one build check run — a hung script (watch mode) must not wedge the run, and
9
8
  * a timeout is environmental, never fail-closed. The default `check.timeoutSeconds` resolves
10
9
  * to this (via ms in detectBuildCheck); runBuildCheck uses it as its default timeout
11
10
  * parameter so tests can shorten it. */
@@ -18,13 +17,7 @@ const WALK_UP_LEVELS = 5;
18
17
  /** True when `dir` holds both a package.json and a node_modules/ directory — the structural
19
18
  * signature of an installed JS project root. */
20
19
  function hasInstall(dir) {
21
- try {
22
- fs.statSync(path.join(dir, "package.json"));
23
- return fs.statSync(path.join(dir, "node_modules")).isDirectory();
24
- }
25
- catch {
26
- return false;
27
- }
20
+ return statOrNull(path.join(dir, "package.json")) !== null && isDirectory(path.join(dir, "node_modules"));
28
21
  }
29
22
  /** Read the check script from `dir`'s package.json: prefer test — npm convention makes
30
23
  * `npm test` the canonical verify command — then typecheck, then build; null when none is
@@ -95,15 +88,30 @@ export function detectBuildCheck(startDir, config, maxLevels = WALK_UP_LEVELS) {
95
88
  return found !== null && found.kind === "npm" && configuredMs !== undefined ? { ...found, timeoutMs: configuredMs } : found;
96
89
  }
97
90
  /** The configured per-change gate command (`check.gateCommand`, PLANS.md Land-queue speed 3e) when
98
- * it is a non-blank string, else undefined — off. Opt-in: a repo that sets it trades a cheaper
99
- * review-gate pre-check for a full check.command run that happens once per stack at the batch/merge
100
- * scope instead. One reading shared by runScopedBuildCheck (which runs it at scope "gate") and
101
- * merge-to-main.ts's verifyMerge (which must then never treat the gate's green as the full
102
- * check's), so the two cannot disagree about whether it is on. */
91
+ * it is a non-blank string, else undefined — off. Opt-in: when no `mergeCommand` is declared, a
92
+ * project that sets one merges on it, not the full check (mergeCommandOf's fallback, PLANS.md
93
+ * "Fast merge" "Defaults"). One reading shared by runScopedBuildCheck (which runs it at scope
94
+ * "gate") and merge-to-main.ts's verifyMerge (which must then never treat the gate's green as the
95
+ * full check's), so the two cannot disagree about whether it is on. */
103
96
  export function gateCommandOf(config) {
104
97
  const g = config?.check?.gateCommand;
105
98
  return isNonBlankString(g) ? g : undefined;
106
99
  }
100
+ /** The configured in-lock merge command (`check.mergeCommand`, PLANS.md "Fast merge" part 6/14)
101
+ * when it is a non-blank string, else undefined — off. An absent `mergeCommand` falls back to
102
+ * the gate command (`gateCommandOf`, PLANS.md "Fast merge" "Defaults"): a project that declared
103
+ * a `check.gateCommand` merges on it. With it set, the merge and batch scopes run it instead of
104
+ * the full check, and main's full-check verdict comes from the main watch. A blank string
105
+ * (`""`) is the explicit opt-out that keeps the full check in the merge lock even when a gate
106
+ * command is set. One reading shared by runScopedBuildCheck, the merge paths and the main-red
107
+ * verdict reads, so they cannot disagree about whether it is on. */
108
+ export function mergeCommandOf(config) {
109
+ const c = config?.check;
110
+ const m = c?.mergeCommand;
111
+ if (m === undefined)
112
+ return gateCommandOf(config);
113
+ return isNonBlankString(m) ? m : undefined;
114
+ }
107
115
  /** Resolve `node_modules/<rel>` by walking UP from `startDir` — the same climb detectBuildCheck
108
116
  * makes, and the same one npm's run-script PATH walk makes: a tumwater worktree has no install
109
117
  * of its own (node_modules is gitignored, so it exists only where someone ran npm install), and
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /** The presentation half of the build check: the scope vocabulary, the environmental-skip
3
2
  * warning's wording, and the build_check event's shape. Split out of build-check.ts — which
4
3
  * keeps the deterministic execution and classification (detect → spawn → classify) — because
@@ -8,21 +7,27 @@
8
7
  * running a check. This module imports only types from build-check.js, so no runtime cycle.
9
8
  * runScopedBuildCheck (build-check-scoped.ts) is the only caller that both runs a check and
10
9
  * uses this wording; the baseline surfaces use the event/warning helpers alone. */
10
+ import { logEventBestEffort } from "../events/events.js";
11
11
  /** Does this build_check scope stand for a full-suite verdict — merge, batch, or the red-main
12
12
  * baseline? A gate check can run a subset of the tree, so a scope outside this set is never a
13
13
  * full-suite check. The one vocabulary the three predicates that recognize a full-suite check
14
14
  * select on — isPassingFullCheck, isRedFullCheck, and the status polls' newest-merge-check
15
15
  * scan (status-polls.ts) — so they cannot drift on which scopes count. The deadline-killed
16
16
  * failure costing in time-spend.ts is a narrower pair (merge/batch only, with a status and a
17
- * timeout required), so it keeps its own check. */
18
- export function isFullCheckScope(scope) {
17
+ * timeout required), so it keeps its own check. `full` is the event's `full` field: false
18
+ * (a check.mergeCommand run) is never a full-suite check. */
19
+ export function isFullCheckScope(scope, full) {
20
+ // A merge or batch run of check.mergeCommand carries `full: false` (buildCheckEvent): it ran
21
+ // the cheaper command, so it is no full-suite verdict whatever its scope.
22
+ if (full === false)
23
+ return false;
19
24
  return scope === "merge" || scope === "batch" || scope === "baseline";
20
25
  }
21
26
  /** Is this a passing build_check over the full suite? The scopes doctor's check row counts —
22
27
  * merge, batch and the red-main baseline — are isFullCheckScope's set; a gate check can run a
23
28
  * subset, so it is left out. */
24
29
  export function isPassingFullCheck(ev) {
25
- return ev.type === "build_check" && ev.status === "passed" && isFullCheckScope(ev.scope);
30
+ return ev.type === "build_check" && ev.status === "passed" && isFullCheckScope(ev.scope, ev.full);
26
31
  }
27
32
  /** The red sibling of isPassingFullCheck: a failed build_check over the same three scopes
28
33
  * (isFullCheckScope's set). A `skipped` check made no verdict about the tree, and a merge-scope
@@ -30,7 +35,7 @@ export function isPassingFullCheck(ev) {
30
35
  * alone is the rule. The two predicates are mutually exclusive on `status`, so a fold branch on
31
36
  * them never sees the other's events. */
32
37
  export function isRedFullCheck(ev) {
33
- return ev.type === "build_check" && ev.status === "failed" && isFullCheckScope(ev.scope);
38
+ return ev.type === "build_check" && ev.status === "failed" && isFullCheckScope(ev.scope, ev.full);
34
39
  }
35
40
  /** How late a check's deadline timer may fire before the timeout's wording stops naming the
36
41
  * configured bound alone. Normal timer lag is milliseconds; past this the warning and the
@@ -236,11 +241,14 @@ function buildCheckRunFields(outcome) {
236
241
  * runScopedBuildCheck's two priced events in build-check-scoped.ts (the killed retry and the
237
242
  * final verdict) and the red-main baseline loggers (main-red.ts's baselineCheckLogger,
238
243
  * redeploy.ts's redeploy mirror), which all paid the run whose timings the event carries. */
239
- export function buildCheckEvent(source, scope, outcome, durationMs) {
244
+ export function buildCheckEvent(source, scope, outcome, durationMs,
245
+ /** True for a merge/batch run of check.mergeCommand: the event then carries `full: false`. */
246
+ cheaper = false) {
240
247
  return {
241
248
  source,
242
249
  type: "build_check",
243
250
  scope,
251
+ ...(cheaper ? { full: false } : {}),
244
252
  status: outcome.status,
245
253
  script: outcome.script,
246
254
  durationMs,
@@ -259,6 +267,17 @@ export function buildCheckEvent(source, scope, outcome, durationMs) {
259
267
  : {}),
260
268
  };
261
269
  }
270
+ /** A scoped check that waited at least this long for a process-wide check permit is worth an
271
+ * event: it is the operator's signal that `maxConcurrentChecks` is too small for the team. */
272
+ export const CHECK_WAIT_EVENT_MS = 30_000;
273
+ /** Log one `check_wait` when a scoped check waited at least CHECK_WAIT_EVENT_MS for a check
274
+ * permit; a shorter wait is ordinary queueing and stays silent. The one home of the event's
275
+ * shape, beside buildCheckEvent. Best-effort: an unwritable feed must not fail the check. */
276
+ export function logCheckWaitEvent(root, role, scope, waitedMs, parked) {
277
+ if (waitedMs < CHECK_WAIT_EVENT_MS)
278
+ return;
279
+ logEventBestEffort(root, { source: role, type: "check_wait", role, scope, waitedMs, ...parked });
280
+ }
262
281
  /** The onRun hook that logs one baseline check's build_check event through `log` under `source`:
263
282
  * main-red.ts's gate logger (logEventBestEffort) and redeploy.ts's green-check and witness hooks
264
283
  * all turn the same run shape into the same baseline event, so that mapping lives here once