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,11 +1,11 @@
1
- // vocabulary: plain
2
1
  import { loadConfigSafe } from "../config/config.js";
3
- import { withStateLockAsync } from "../concurrency/lock.js";
2
+ import { withPrimaryWorktreeLockAsync } from "../concurrency/lock.js";
4
3
  import { currentBranch, headOf, isMergedInto, targetBranch } from "../git/git.js";
5
- import { COMMIT_IDENT, gitTry } from "../git/git-run.js";
6
- import { stampedSubject } from "../git/commit-message.js";
7
- import { primaryWorktreeLockPath } from "../paths.js";
4
+ import { GitError, commitAuthorArgs, committerIdentArgs, gitTry, runGit } from "../git/git-run.js";
5
+ import { stampedRole, stampedSubject } from "../git/commit-message.js";
8
6
  import { shortSha } from "../text/format.js";
7
+ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
8
+ import { runMergedSha } from "../run/run-detail-data.js";
9
9
  /** `tumwater revert <sha>`: undo one merged change on main, committing the inverse as
10
10
  * `tumwater(operator): Revert <short sha>` on the primary checkout. Mirrors
11
11
  * `commitOperatorBacklogEdit`'s lock-and-branch discipline (src/backlog/backlog-commit.ts): the
@@ -13,6 +13,118 @@ import { shortSha } from "../text/format.js";
13
13
  * fast-forward, and it refuses — writing nothing — when the primary is not on the base branch.
14
14
  * Fail-closed: an unknown ref, a ref not on the base branch, or a revert that conflicts writes
15
15
  * no commit and leaves the checkout as it was. */
16
+ /** The shared git steps of a revert: `git revert --no-commit <sha>` in `wt`, then commit just the
17
+ * inverse as `message`. `wt` must have a clean tracked tree (so `--abort` after a conflict has
18
+ * nothing to discard). Never throws; a conflict or a failed commit aborts the revert and comes
19
+ * back as `{ ok: false, error }` with `wt` as it was. */
20
+ export async function revertCommitIn(wt, sha, message) {
21
+ if ((await gitTry(wt, "revert", "--no-commit", sha)) === null) {
22
+ // The tree was clean when the revert started, so --abort restores exactly that state.
23
+ await gitTry(wt, "revert", "--abort");
24
+ return { ok: false, error: `reverting ${shortSha(sha)} conflicted; nothing was committed and the checkout is unchanged` };
25
+ }
26
+ // `--only` with the revert's own staged paths commits just the inverse, so it cannot pick up any
27
+ // unrelated change of another path staged after the clean check. `--no-renames` keeps a rename's
28
+ // two halves as separate path lines: the default name-only collapses a detected rename to its
29
+ // destination, which would commit only half and leave the other half staged with a dirty tree.
30
+ const pathsSpec = ((await gitTry(wt, "diff", "--cached", "--name-only", "--no-renames")) ?? "")
31
+ .split("\n")
32
+ .filter((p) => p.length > 0);
33
+ if ((await gitTry(wt, ...(await committerIdentArgs(wt)), "commit", ...commitAuthorArgs(), "--only", "-m", message, "--", ...pathsSpec)) === null) {
34
+ await gitTry(wt, "revert", "--abort");
35
+ return { ok: false, error: `the revert of ${shortSha(sha)} did not commit; nothing was committed` };
36
+ }
37
+ return { ok: true, sha: await headOf(wt, "HEAD") };
38
+ }
39
+ /** The read-only refusals shared by a revert and its preview: the primary checkout must be on the
40
+ * base branch, `ref` must resolve to a commit, and that commit must be merged into the branch.
41
+ * The dashboard's `handleRun` also calls it, to decide whether a run's commit is on the base
42
+ * branch. */
43
+ export async function resolveRevertTarget(root, ref) {
44
+ const primary = await currentBranch(root);
45
+ const base = targetBranch(loadConfigSafe(root).config?.baseBranch, primary);
46
+ if (primary === null || base !== primary)
47
+ return { ok: false, error: `the primary checkout is not on ${base} — run from a checkout of it` };
48
+ const full = await gitTry(root, "rev-parse", "--verify", ref + "^{commit}");
49
+ if (full === null)
50
+ return { ok: false, error: `no such commit: ${ref}` };
51
+ if (!(await isMergedInto(root, full, primary)))
52
+ return { ok: false, error: `${shortSha(full)} is not on ${primary}` };
53
+ return { ok: true, full, primary };
54
+ }
55
+ /** The refusal for a commit without exactly one parent (a root or a merge), or null. Shared by
56
+ * the revert and its preview: `git revert` of a root deletes the whole tree, of a merge needs
57
+ * `-m`. */
58
+ async function parentRefusal(root, full) {
59
+ const parents = (await runGit(root, ["rev-list", "--parents", "-n", "1", full])).split(" ").length - 1;
60
+ if (parents === 1)
61
+ return null;
62
+ return `${shortSha(full)} has ${parents} parents; tumwater revert undoes a single-parent commit only, so nothing was written`;
63
+ }
64
+ /** The commit a run's merge put on main, or the CLI's refusal: the run did not merge, or merged
65
+ * but the event log no longer names its commit. Shared by `tumwater revert <role> <n>` and the
66
+ * TUI History trail's Ctrl+V so the two cannot drift. */
67
+ export function runRevertSha(role, run, detail) {
68
+ if (detail.result !== "merged")
69
+ return { ok: false, error: `${role} run #${run} did not merge (result: ${detail.result ?? "unknown"}); there is nothing to revert` };
70
+ const sha = runMergedSha(detail);
71
+ if (sha === null)
72
+ return { ok: false, error: `${role} run #${run} merged but the event log no longer names its commit; pass the sha from git log instead` };
73
+ return { ok: true, sha };
74
+ }
75
+ /** `tumwater revert <sha> --dry-run`: what a revert of `ref` would undo and whether it applies
76
+ * cleanly, from git's object store alone — a `git merge-tree` three-way merge (base = the commit,
77
+ * ours = HEAD, theirs = its parent, the merge `git revert` performs). Takes no lock and never
78
+ * touches the index, the worktree or a ref. Never throws. */
79
+ export async function previewRevert(root, ref) {
80
+ try {
81
+ const target = await resolveRevertTarget(root, ref);
82
+ if (!target.ok)
83
+ return target;
84
+ const { full, primary } = target;
85
+ const refusal = await parentRefusal(root, full);
86
+ if (refusal !== null)
87
+ return { ok: false, error: refusal };
88
+ const subject = await runGit(root, ["log", "-1", "--format=%s", full]);
89
+ const paths = (await runGit(root, ["diff", "--name-only", "--no-renames", full + "^", full]))
90
+ .split("\n")
91
+ .filter((p) => p.length > 0);
92
+ const head = await headOf(root, "HEAD");
93
+ let applies = true;
94
+ try {
95
+ await runGit(root, ["merge-tree", "--write-tree", "--merge-base=" + full, "HEAD", full + "^"]);
96
+ }
97
+ catch (err) {
98
+ if (!(err instanceof GitError))
99
+ throw err;
100
+ if (err.code !== 1)
101
+ return { ok: false, error: `could not preview the revert: ${err.message}` };
102
+ applies = false;
103
+ }
104
+ const dirty = (await runGit(root, ["status", "--porcelain", "--untracked-files=no"])) !== "";
105
+ return { ok: true, sha: full, subject, paths, head, primary, applies, dirty };
106
+ }
107
+ catch {
108
+ return { ok: false, error: "the revert preview did not complete (git failed); nothing was written" };
109
+ }
110
+ }
111
+ /** The role a revert of a commit with this subject undoes: the stamped role when it is a working
112
+ * role, else null — an unstamped (hand-made) commit, the operator's or the harness's own. */
113
+ function revertedRole(subject) {
114
+ const role = stampedRole(subject);
115
+ if (role === undefined || role === "operator" || role === "tumwater")
116
+ return null; // vocabulary-ok: commit stamp strings
117
+ return role;
118
+ }
119
+ /** What to tell the operator after reverting a commit with this subject: the role that made it may
120
+ * redo the work. Null when no role made it — an unstamped (hand-made) commit, the operator's or
121
+ * the harness's own. */
122
+ export function revertFollowUp(subject) {
123
+ const role = revertedRole(subject);
124
+ if (role === null)
125
+ return null;
126
+ return `the ${role} role may redo the work, so cancel or edit its PLANS.md/BUGS.md entry if it should not`;
127
+ }
16
128
  /** Revert one already-merged commit (`ref` resolved to a commit) and commit the inverse on the
17
129
  * primary checkout under the primary-worktree lock. Never throws; every refusal and every git
18
130
  * failure comes back as `{ ok: false, error }` after restoring the checkout. `head` is captured
@@ -24,18 +136,17 @@ export async function revertMergedCommit(root, ref) {
24
136
  // call, and that write takes the same lock synchronously — a lock already held across our
25
137
  // awaits would block its event loop until the lock times out.
26
138
  await new Promise((resolve) => setImmediate(resolve));
27
- return await withStateLockAsync(primaryWorktreeLockPath(root), async () => {
28
- const primary = await currentBranch(root);
29
- const base = targetBranch(loadConfigSafe(root).config?.baseBranch, primary);
30
- if (primary === null || base !== primary)
31
- return { ok: false, error: `the primary checkout is not on ${base} — run from a checkout of it` };
32
- const full = await gitTry(root, "rev-parse", "--verify", ref + "^{commit}");
33
- if (full === null)
34
- return { ok: false, error: `no such commit: ${ref}` };
139
+ return await withPrimaryWorktreeLockAsync(root, async () => {
140
+ const target = await resolveRevertTarget(root, ref);
141
+ if (!target.ok)
142
+ return target;
143
+ const { full } = target;
144
+ const refusal = await parentRefusal(root, full);
145
+ if (refusal !== null)
146
+ return { ok: false, error: refusal };
35
147
  // `head` is captured before the revert so a future caller can compare before/after.
36
148
  const head = await headOf(root, "HEAD");
37
- if (!(await isMergedInto(root, full, primary)))
38
- return { ok: false, error: `${shortSha(full)} is not on ${primary}` };
149
+ const subject = await runGit(root, ["log", "-1", "--format=%s", full]);
39
150
  // Fail-closed on any staged or unstaged change to a tracked path: we refuse to revert a
40
151
  // checkout that has one (untracked files are tolerated — the lock dir we just created is
41
152
  // one). This keeps the inverse commit free of unrelated content and, just as important,
@@ -44,25 +155,22 @@ export async function revertMergedCommit(root, ref) {
44
155
  // pre-existing work can never be lost.
45
156
  if ((await gitTry(root, "status", "--porcelain", "--untracked-files=no")) !== "")
46
157
  return { ok: false, error: "the worktree has staged or unstaged changes; commit or stash them first, nothing was committed" };
47
- if ((await gitTry(root, "revert", "--no-commit", full)) === null) {
48
- // The tree was clean when the revert started, so --abort restores exactly that state.
49
- await gitTry(root, "revert", "--abort");
50
- return { ok: false, error: `reverting ${shortSha(full)} conflicted; nothing was committed and the checkout is unchanged` };
51
- }
52
- const commitMsg = stampedSubject("operator", "Revert " + shortSha(full)); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
53
- // Mirror commitOperatorBacklogEdit: `--only` with the revert's own staged paths commits
54
- // just the inverse, so it cannot pick up any unrelated change of another path staged
55
- // after the clean check above. `--no-renames` keeps a rename's two halves as separate
56
- // path lines: the default name-only collapses a detected rename to its destination, which
57
- // would commit only half and leave the other half staged with a dirty tree.
58
- const pathsSpec = ((await gitTry(root, "diff", "--cached", "--name-only", "--no-renames")) ?? "")
59
- .split("\n")
60
- .filter((p) => p.length > 0);
61
- if ((await gitTry(root, ...COMMIT_IDENT, "commit", "--only", "-m", commitMsg, "--", ...pathsSpec)) === null) {
62
- await gitTry(root, "revert", "--abort");
63
- return { ok: false, error: `the revert of ${shortSha(full)} did not commit; nothing was committed` };
64
- }
65
- return { ok: true, headSha: await headOf(root, "HEAD"), beforeSha: head };
158
+ const reverted = await revertCommitIn(root, full, stampedSubject("operator", "Revert " + shortSha(full))); // vocabulary-ok: the tumwater(operator): commit prefix is a machine string
159
+ if (!reverted.ok)
160
+ return reverted;
161
+ // Log the operator's deliberate revert while the primary lock is held, so the inverse
162
+ // commit and its trail land together. Best-effort: a failed event write must never turn a
163
+ // successful revert into a refusal.
164
+ const role = revertedRole(subject);
165
+ logEventBestEffort(root, {
166
+ source: TUMWATER_SOURCE,
167
+ type: "operator_revert",
168
+ commit: full,
169
+ revert: reverted.sha,
170
+ subject,
171
+ ...(role === null ? {} : { runner: role }),
172
+ });
173
+ return { ok: true, headSha: await headOf(root, "HEAD"), beforeSha: head, subject };
66
174
  });
67
175
  }
68
176
  catch {
@@ -1,18 +1,17 @@
1
- // vocabulary: plain
2
1
  /** The orchestrator's launch pass: the half of the per-poll runner sweep that admits due
3
- * roles to their ticks. The decision half (isEligible, the pause/backoff/deferral gates,
2
+ * roles to their runs. The decision half (isEligible, the pause/backoff/deferral gates,
4
3
  * once-mode settling) stays in runOrchestrator (src/orchestrator/orchestrator.ts) — it reads
5
4
  * the poll's gate verdicts and the backlog state; this module takes its output, the
6
5
  * `reasons` map of due runners, and turns each into a reserved, semaphore-gated, in-flight
7
- * tick: the probe admission for the fallback breaker, the wake event, the
8
- * reservation/parking bookkeeping, runTimedRoleTick's wiring (slot acquisition, the start
6
+ * run: the probe admission for the fallback breaker, the wake event, the
7
+ * reservation/parking bookkeeping, runTimedRoleRun's wiring (slot acquisition, the start
9
8
  * gate, breaker evidence), and the in-flight-bucket bookkeeping the drain and the p75
10
9
  * duration sample read. Split out of orchestrator.ts — whose poll loop otherwise reads as
11
10
  * one decision after another — so the launch mechanics read as one step of that loop. The
12
11
  * shared in-flight state (buckets, permit holders, duration samples, gateStates) is passed
13
12
  * in and mutated in place, exactly as the inline version did: the drain's `finally` and the
14
13
  * redeployer read it between polls. */
15
- import { abandonFallbackProbeAt, recordFallbackTickAt, startFallbackProbeAt, } from "../budget/fallback-breaker.js";
14
+ import { abandonFallbackProbeAt, recordFallbackRunAt, startFallbackProbeAt, } from "../budget/fallback-breaker.js";
16
15
  import { seamHoldKey } from "../config/config-views.js";
17
16
  import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
18
17
  import { errorMessage } from "../text/text.js";
@@ -21,20 +20,20 @@ import { modelPairName } from "../budget/budget.js";
21
20
  import { withProviderLease } from "../concurrency/provider-lease.js";
22
21
  import { seamConcurrencyKey } from "../config/config-views.js";
23
22
  import { DIRECTOR_ROLE, roleTier } from "../roles/roles.js";
24
- import { runTimedRoleTick } from "../tick/tick-timing.js";
25
- /** How many recent work-bearing tick durations the p75 sample keeps (drainInFlightWork's
26
- * HANDOFF_MERGE_WINDOW_MS input): the drain window tracks recent tick pace, not all-time
23
+ import { runTimedRoleRun } from "../run/run-timing.js";
24
+ /** How many recent work-bearing run durations the p75 sample keeps (drainInFlightWork's
25
+ * HANDOFF_MERGE_WINDOW_MS input): the drain window tracks recent run pace, not all-time
27
26
  * pace, so the sample is a ring buffer. */
28
- export const ROLE_TICK_DURATION_SAMPLES = 50;
29
- /** The admission wait threshold below which a role tick's semaphore wait logs no event — a
30
- * tick that starts at once stays silent. Mirrors SLOT_WAIT_EVENT_MS in src/git/worktree-pool.ts:
27
+ export const ROLE_RUN_DURATION_SAMPLES = 50;
28
+ /** The admission wait threshold below which a role run's semaphore wait logs no event — a
29
+ * run that starts at once stays silent. Mirrors SLOT_WAIT_EVENT_MS in src/git/worktree-pool.ts:
31
30
  * the wait that signals maxConcurrentPerProvider (or maxConcurrent) caps the team. */
32
31
  export const PROVIDER_WAIT_EVENT_MS = 30_000;
33
- /** Launch one due runner's tick: reserve it, park it in the semaphore, and start it under
34
- * runTimedRoleTick with the breaker-evidence wiring. Returns the task promise already added
32
+ /** Launch one due runner's run: reserve it, park it in the semaphore, and start it under
33
+ * runTimedRoleRun with the breaker-evidence wiring. Returns the task promise already added
35
34
  * to its in-flight bucket (its `finally` self-removes and folds the duration sample). */
36
- export function launchDueTicks(ctx) {
37
- const { root, reasons, signal, gateStates, breakerPolicy, breakerNow, probePair, probeRoles, budgetActive, semaphore, piDefault, rolePermitHolders, roleInFlight, directorInFlight, roleTickDurationsMs, startHeld, } = ctx;
35
+ export function launchDueRuns(ctx) {
36
+ const { root, reasons, signal, gateStates, breakerPolicy, breakerNow, probePair, probeRoles, budgetActive, windowMovedRoles, semaphore, piDefault, rolePermitHolders, roleInFlight, directorInFlight, roleRunDurationsMs, startHeld, } = ctx;
38
37
  let admittedProbe = false;
39
38
  for (const runner of fairOrder([...reasons.keys()])) {
40
39
  if (signal.aborted)
@@ -58,10 +57,10 @@ export function launchDueTicks(ctx) {
58
57
  logEventBestEffort(root, { source: runner.role, type: "wake", reason });
59
58
  }
60
59
  runner.state.running = true; // Reserve before the semaphore wait so we don't double-schedule.
61
- // The director never queues behind role loops: a user prompt starts immediately,
60
+ // The director never queues behind roles: a user prompt starts immediately,
62
61
  // even when maxConcurrent slots are busy. Parked waiters keep fairOrder's tier order
63
- // across polls too: a work-role arrival jumps ahead of maintenance ticks that queued
64
- // in an earlier poll (in-flight ticks always run to completion).
62
+ // across polls too: a work-role arrival jumps ahead of maintenance runs that queued
63
+ // in an earlier poll (in-flight runs always finish).
65
64
  const usesSlot = runner.role !== DIRECTOR_ROLE;
66
65
  // Mark the parked waiter while it waits: it holds no permit yet, so the dashboards
67
66
  // render `awaiting slot` (an inactive state) and the active rows keep tracking
@@ -73,36 +72,44 @@ export function launchDueTicks(ctx) {
73
72
  const parkKey = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(Date.now());
74
73
  runner.state.parkedSince = usesSlot ? Date.now() : undefined;
75
74
  runner.state.parkedOn = usesSlot && parkKey ? seamConcurrencyKey(parkKey, piDefault) : undefined;
76
- // The tick's own run time (null when it never ran, was cut off, or ended without
77
- // work): the drain-window sample is taken only for a work-bearing tick that finished
78
- // on its own (runTimedRoleTick; BUGS.md 2026-09-30).
75
+ // The run's own duration (null when it never started, was cut off, or ended without
76
+ // work): the drain-window sample is taken only for a work-bearing run that finished
77
+ // on its own (runTimedRoleRun; BUGS.md 2026-09-30).
79
78
  let durationMs = null;
80
- // Whether runner.tick() was ever called — false when the start gate (or a shutdown)
81
- // turned the tick away at its permit.
79
+ // Whether runner.run() was ever called — false when the start gate (or a shutdown)
80
+ // turned the run away at its permit.
82
81
  let started = false;
83
- // The tick's keyed semaphore lease (plans/provider-concurrency.md §4): acquired on the
82
+ // The run's keyed semaphore lease (plans/provider-concurrency.md §4): acquired on the
84
83
  // authoring seam's provider key and released in place of the bare team release, so the
85
- // cap is real for role ticks from part 2/5 on. The director never queues, so its lease is
84
+ // cap is real for role runs from part 2/5 on. The director never queues, so its lease is
86
85
  // never created here.
87
86
  let lease = null;
88
87
  // The director's forced provider lease (plans/provider-concurrency.md §4): never queues and
89
- // never counts toward maxConcurrent, but DOES count toward its provider key while its tick
88
+ // never counts toward maxConcurrent, but DOES count toward its provider key while its run
90
89
  // runs — so a full provider admits no further grants until the director releases.
91
90
  let directorLease = null;
92
91
  const task = (async () => {
93
- durationMs = await runTimedRoleTick(signal, usesSlot
92
+ durationMs = await runTimedRoleRun(signal, usesSlot
94
93
  ? async () => {
95
94
  // The key is the authoring seam's concurrency key, computed here at admission
96
95
  // (runConfig applies any budget or model-failure fallback already in force).
97
96
  const cfg = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(Date.now());
98
97
  const key = cfg ? seamConcurrencyKey(cfg, piDefault) : undefined;
99
98
  const admittedAt = Date.now();
99
+ // Which cap would hold the run, read once at park time (not revised if the caps
100
+ // change mid-wait): the run's own provider cap when full, else the team total.
101
+ const pool = semaphore.snapshot();
102
+ const keyed = key !== undefined ? pool.byKey[key] : undefined;
103
+ const providerFull = keyed?.limit !== undefined && keyed.inUse >= keyed.limit;
104
+ const heldBy = providerFull
105
+ ? { cap: "provider", limit: keyed.limit }
106
+ : { cap: "team", limit: pool.limit };
100
107
  lease = await semaphore.acquireLease(roleTier(runner.role), key);
101
108
  // A wait of PROVIDER_WAIT_EVENT_MS or more for the provider/team slot is
102
109
  // provider contention worth surfacing: name it so `tumwater doctor`'s provider
103
110
  // contention row can sum it and the operator can size maxConcurrentPerProvider
104
111
  // (or maxConcurrent) from real waits. Best-effort like the launch pass's other
105
- // events, so an unwritable log never blocks a start; a tick admitted at once
112
+ // events, so an unwritable log never blocks a start; a run admitted at once
106
113
  // stays silent.
107
114
  if (Date.now() - admittedAt >= (ctx.providerWaitMs ?? PROVIDER_WAIT_EVENT_MS)) {
108
115
  logEventBestEffort(ctx.root, {
@@ -111,18 +118,19 @@ export function launchDueTicks(ctx) {
111
118
  role: runner.role,
112
119
  waitedMs: Date.now() - admittedAt,
113
120
  ...(key !== undefined ? { key } : {}),
121
+ ...heldBy,
114
122
  });
115
123
  }
116
- // Permit granted: the tick is now an active, permit-holding state until the
117
- // release below. A tick the start gate turns away releases within the same
124
+ // Permit granted: the run is now an active, permit-holding state until the
125
+ // release below. A run the start gate turns away releases within the same
118
126
  // microtask chain, so no poll ever counts it as a permit holder.
119
127
  runner.state.parkedSince = undefined;
120
128
  runner.state.parkedOn = undefined;
121
129
  rolePermitHolders.add(runner);
122
130
  }
123
131
  : async () => {
124
- // The director wraps its tick in a forced lease on its run's provider key, never
125
- // queuing behind role loops: a user prompt starts at once even on a full provider,
132
+ // The director wraps its run in a forced lease on its provider key, never
133
+ // queuing behind roles: a user prompt starts at once even on a full provider,
126
134
  // and the key's in-use rises by one — overshooting by one — so no further grant on
127
135
  // that key happens until its release drains it back under the cap.
128
136
  const cfg = runner.runConfig(Date.now());
@@ -136,55 +144,55 @@ export function launchDueTicks(ctx) {
136
144
  directorLease.release();
137
145
  }, async () => {
138
146
  started = true;
139
- // A role tick that starts while the budget holds runs on the fallback pair its tier
147
+ // A role run that starts while the budget holds uses the fallback pair its tier
140
148
  // resolved to (the gate's per-tier view was installed in its config), or — when a
141
149
  // model-fallback episode is active — on the tier's model-fallback pair, which can
142
150
  // differ from the budget pair. Its outcome is the evidence for the pair it ACTUALLY
143
151
  // ran on, so read it from runConfig (config + episode), never from configForRole
144
- // alone: a per-pair breaker fed a fallback tick under the primary's name would
152
+ // alone: a per-pair breaker fed a fallback run under the primary's name would
145
153
  // misprice a healthy pair and miss a dead one (PLANS.md "Model failure fallback,
146
154
  // part 1/2").
147
155
  const now = Date.now();
148
156
  const cfg = runner.role === DIRECTOR_ROLE ? null : runner.runConfig(now);
149
- // The breaker is keyed by the seam that ran (seamHoldKey), like the tick pair and
157
+ // The breaker is keyed by the seam that ran (seamHoldKey), like the run pair and
150
158
  // the team-hold stamps (PLANS.md "Mixed claude + pi fleets, part 2/4"): a claude
151
- // fallback tick demotes the `claude/<model>` breaker entry, never `?/…`.
152
- const ranPair = budgetActive && cfg?.model !== undefined
159
+ // fallback run demotes the `claude/<model>` breaker entry, never `?/…`.
160
+ const ranPair = (budgetActive || windowMovedRoles.has(runner.role)) && cfg?.model !== undefined
153
161
  ? modelPairName({ provider: seamHoldKey(cfg), model: cfg.model })
154
162
  : null;
155
163
  const ranOn = ranPair !== null ? gateStates.budget.breakers[ranPair] : undefined;
156
- // The tick's lease rides in the provider-lease store for the length of the tick, so
157
- // every run it makes re-keys it to the provider that run actually uses
158
- // (plans/provider-concurrency.md §5) — a budget or model-failure switch that moves a
159
- // mid-tick run to another provider moves the lease with it. The director's forced
164
+ // The run's lease rides in the provider-lease store for the length of the run, so
165
+ // every agent run it makes re-keys it to the provider that agent run actually uses
166
+ // (plans/provider-concurrency.md §5) — a budget or model-failure switch that moves an
167
+ // agent run to another provider mid-run moves the lease with it. The director's forced
160
168
  // lease re-keys the same way, and its rekey never waits.
161
- const outcome = await withProviderLease(runner.role === DIRECTOR_ROLE ? directorLease : lease, semaphore, () => runner.tick(), { piDefault });
169
+ const outcome = await withProviderLease(runner.role === DIRECTOR_ROLE ? directorLease : lease, semaphore, () => runner.run(), { piDefault });
162
170
  if (ranPair !== null && ranOn !== undefined) {
163
171
  const at = breakerNow();
164
172
  const evidence = outcome.recoveredLeftover ? "skipped" : outcome.result;
165
- gateStates.budget.breakers = recordFallbackTickAt(gateStates.budget.breakers, ranPair, ranOn, evidence, probe, at, breakerPolicy);
173
+ gateStates.budget.breakers = recordFallbackRunAt(gateStates.budget.breakers, ranPair, ranOn, evidence, probe, at, breakerPolicy);
166
174
  }
167
175
  else if (probe && probePair !== null) {
168
- // A probe whose tick started on no engaged pair — the budget gate reopened
176
+ // A probe whose run started on no engaged pair — the budget gate reopened
169
177
  // between admission and start (the cap raised, or midnight reset spend) — folds
170
178
  // no evidence, so its claim would hang on the pair's entry forever: probing
171
179
  // blocks both fallbackProbeDuePair and startFallbackProbeAt, and nothing but a
172
180
  // rekey clears it, so the demoted fallback could never earn another probe. Hand
173
- // the claim back exactly as the !started path below does for a tick turned away
181
+ // the claim back exactly as the !started path below does for a run turned away
174
182
  // at its permit.
175
183
  gateStates.budget.breakers = abandonFallbackProbeAt(gateStates.budget.breakers, probePair);
176
184
  }
177
185
  return outcome;
178
186
  }, Date.now,
179
- // The restart start gate, plus the failure hold for role ticks (the director is
187
+ // The restart start gate, plus the failure hold for role runs (the director is
180
188
  // exempt, as at scheduling): a waiter granted its permit mid-storm must not start
181
189
  // into it.
182
190
  () => startHeld(runner.role));
183
- // A reservation whose tick never started hands itself back, so the role re-schedules
191
+ // A reservation whose run never started hands itself back, so the role re-schedules
184
192
  // once the hold lifts (or on the new build) instead of sitting `running` forever with
185
193
  // nothing in flight. Memory only, on purpose: nothing started, so no state write and
186
194
  // no run_start/run_end — the persisted state still reads exactly as the last real
187
- // tick left it, and the next generation schedules the role from that.
195
+ // run left it, and the next generation schedules the role from that.
188
196
  if (!started)
189
197
  runner.state.running = false;
190
198
  // A probe turned away the same way answered nothing: hand its claim back (see
@@ -194,7 +202,7 @@ export function launchDueTicks(ctx) {
194
202
  })();
195
203
  const bucket = runner.role === DIRECTOR_ROLE ? directorInFlight : roleInFlight;
196
204
  bucket.add(task);
197
- // A LoopRunner.tick folds every failure it handles into an `error` outcome, so this task
205
+ // A RoleRunner.run folds every failure it handles into an `error` outcome, so this task
198
206
  // should resolve. A throw outside that fold (a finalize-time file write, a runner bug)
199
207
  // rejects it instead; the drain awaits the bucket with allSettled, but chaining cleanup
200
208
  // off a bare `void task.finally(...)` would leave the derived promise's rejection
@@ -210,9 +218,9 @@ export function launchDueTicks(ctx) {
210
218
  .finally(() => {
211
219
  bucket.delete(task);
212
220
  if (bucket === roleInFlight && durationMs !== null) {
213
- roleTickDurationsMs.push(durationMs);
214
- if (roleTickDurationsMs.length > ROLE_TICK_DURATION_SAMPLES)
215
- roleTickDurationsMs.shift();
221
+ roleRunDurationsMs.push(durationMs);
222
+ if (roleRunDurationsMs.length > ROLE_RUN_DURATION_SAMPLES)
223
+ roleRunDurationsMs.shift();
216
224
  }
217
225
  });
218
226
  }