tumwater 0.2.0 → 0.4.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 (464) 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 +55 -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 +6 -4
  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 +404 -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 +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -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 +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -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-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -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 +65 -3
  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 +181 -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 +183 -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 +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,13 +1,13 @@
1
- import { eventRole, eventUsage, tickSpanMs, tickStartMap } from "../events/event-read.js";
1
+ import { eventRole, eventUsage, runSpanMs, runStartMap } from "../events/event-read.js";
2
2
  import { errorTextOrPlaceholder, normalizeClusterKey, poolTimeoutKey, sortedRoles, truncateExample } from "./failure-cluster.js";
3
- import { resolveQueuedResult, bucketMergeEvents } from "../history/history-data.js";
3
+ import { resolveQueuedResult, bucketMergeEvents } from "../history/history-queued.js";
4
4
  import { nonNegativeNumber, stringList } from "../files/json-object.js";
5
5
  import { getOrCreate, groupBy, rankByCount } from "../collections.js";
6
6
  /** How the Outcome table's results collapse for costing:
7
- * "merged" made progress, "no_change" spent a tick and merged nothing, and every remaining
7
+ * "merged" made progress, "no_change" spent a run and merged nothing, and every remaining
8
8
  * result is "error-class" — it burned agent time without merge, whether the cause was a
9
9
  * hard failure or the review gate. Typed as a full Record so a result added to
10
- * src/tick/tick-outcome.ts and forgotten here is a compile error, like RESULT_ORDER in
10
+ * src/run/run-outcome.ts and forgotten here is a compile error, like RESULT_ORDER in
11
11
  * src/failure/failure-render.ts. */
12
12
  export const OUTCOME_CLASS = {
13
13
  changed: "merged",
@@ -35,7 +35,7 @@ const LOSS_TOP = 5;
35
35
  /** The raw results whose loss attributes to a cluster — an error, an abort, a quiet kill, or
36
36
  * a red-main skip, each carrying its own message on the run_end (main_red's is the
37
37
  * redMainMessage the error-clusters section already itemizes, BUGS.md 2026-10-07). A queued
38
- * tick's merge outcomes are owned by the joins below instead: review_error through its
38
+ * run's merge outcomes are owned by the joins below instead: review_error through its
39
39
  * matching `review_failed` message, review-rejected through its review reasons, and
40
40
  * merge_conflict/merge_blocked by the result itself (BUGS.md 2026-10-10). (A raw
41
41
  * `user_aborted` run_end is also error-class but is not in this set, so it stays out of the
@@ -45,8 +45,8 @@ const CLUSTERED_RESULTS = new Set(["error", "aborted", "quiet_killed", "main_red
45
45
  * other can occupy. Shared by the review-rejected and review_error cause joins, so the two
46
46
  * agree on which event belongs to the attempt (BUGS.md 2026-10-10: the window, not the pin's
47
47
  * sha, is the match, since every verification rebases its pin before the gate). */
48
- function newestAttemptEvent(eventsByLoop, role, attempt) {
49
- return (eventsByLoop.get(role) ?? [])
48
+ function newestAttemptEvent(eventsByRunner, role, attempt) {
49
+ return (eventsByRunner.get(role) ?? [])
50
50
  .filter((e) => e.ts >= attempt.pin.ts && e.ts <= attempt.outcome.ts)
51
51
  .at(-1);
52
52
  }
@@ -64,14 +64,14 @@ function pooledClusterCause(source) {
64
64
  * written since 2026-09-29 does), else the start→end pairing over `starts` — the same
65
65
  * fallback history-data.ts renders, so old events read their span the one way it can still be
66
66
  * known. 0 when neither source has the start (rotation cut it); the fold prices no time it
67
- * cannot attest but still counts the tick. */
68
- function tickDurationMs(ev, starts) {
67
+ * cannot attest but still counts the run. */
68
+ function runDurationMs(ev, starts) {
69
69
  const own = nonNegativeNumber(ev.durationMs, null);
70
70
  if (own !== null)
71
71
  return own;
72
- return tickSpanMs(ev, starts) ?? 0;
72
+ return runSpanMs(ev, starts) ?? 0;
73
73
  }
74
- /** Add one tick to a {runs, ms, costUsd} accumulator — the single home of the fold shared
74
+ /** Add one run to a {runs, ms, costUsd} accumulator — the single home of the fold shared
75
75
  * by the time-and-spend table's outcome cells (SpendCell) and the loss-cause drafts
76
76
  * (LossDraft), so the two tallies cannot drift apart in what they count or price. The
77
77
  * caller passes one event's already-computed durationMs and usage, so folding an event
@@ -82,48 +82,48 @@ function tally(target, durationMs, usage) {
82
82
  target.costUsd += usage.costUsd;
83
83
  }
84
84
  /** Fold the window's run_ends into the time-and-spend table and the loss ranking. Pairing
85
- * runs over BOTH windows' events (the caller passes the whole read), so a tick that opened
85
+ * runs over BOTH windows' events (the caller passes the whole read), so a run that opened
86
86
  * in the preceding day and ended in the current one still gets its span. The merge-outcome
87
87
  * join runs over that same whole read too (BUGS.md 2026-09-30): a queued run_end resolves
88
88
  * through its own merge_queued pin to the later merge_done/merge_failed outcome for the same
89
- * loop+sha — history-data.ts's exact join — so an authoring tick whose merge was rejected
89
+ * role+sha — history-data.ts's exact join — so an authoring run whose merge was rejected
90
90
  * prices into the error-class cell its contract names instead of the merged cell its
91
91
  * transient queued result suggested. A leftover-recovery run resolves to the failed attempt it
92
92
  * re-queued (the history row's rule), so its near-zero span counts under that failure — a
93
93
  * review timeout's cluster, say — rather than adding a run to the change's final
94
94
  * review-rejected cause once per re-queue. A merge still in the pipeline, or an outcome event
95
95
  * matching no pin, keeps the queued→merged reading: the conservative fallback. */
96
- export function timeAndSpend(tickEvents, allEvents) {
97
- const starts = tickStartMap(allEvents);
96
+ export function timeAndSpend(runEvents, allEvents) {
97
+ const starts = runStartMap(allEvents);
98
98
  const emptyCell = () => ({ runs: 0, ms: 0, costUsd: 0 });
99
99
  const byRole = new Map();
100
100
  const losses = new Map();
101
- // The merge evidence rides history-data.ts's bucketMergeEvents — the same buckets
101
+ // The merge evidence rides history-queued.ts's bucketMergeEvents — the same buckets
102
102
  // history's rows read — so the digest's join cannot drift from the rows' join.
103
- const { mergeQueuedByLoop, outcomeByLoop } = bucketMergeEvents(allEvents);
104
- const rejectedByLoop = groupBy(allEvents.filter((e) => e.type === "review_rejected"), (e) => e.source);
103
+ const { mergeQueuedByRunner, outcomeByRunner } = bucketMergeEvents(allEvents);
104
+ const rejectedByRunner = groupBy(allEvents.filter((e) => e.type === "review_rejected"), (e) => e.source);
105
105
  // A review_error merge kept its pin but left no message on its own merge_failed; the
106
- // matching review_failed event for the same loop+sha carries it, so the span prices under
106
+ // matching review_failed event for the same role+sha carries it, so the span prices under
107
107
  // the same cluster key the digest's review-failure section already counts.
108
- const reviewFailedByLoop = groupBy(allEvents.filter((e) => e.type === "review_failed"), (e) => e.source);
109
- const deadlineKilledByLoop = groupBy(allEvents.filter((e) => e.type === "build_check" &&
108
+ const reviewFailedByRunner = groupBy(allEvents.filter((e) => e.type === "review_failed"), (e) => e.source);
109
+ const deadlineKilledByRunner = groupBy(allEvents.filter((e) => e.type === "build_check" &&
110
110
  e.status === "failed" &&
111
111
  (e.scope === "merge" || e.scope === "batch") &&
112
112
  typeof e.timeoutMs === "number"), (e) => e.source);
113
113
  // Queued run_ends resolve newest-first so each claims the newest pin at or before its end
114
- // that no newer tick has claimed — resolveQueuedResult's exact claim rule, shared with the
115
- // history rows. tickEvents is oldest-first, hence the backward walk.
114
+ // that no newer run has claimed — resolveQueuedResult's exact claim rule, shared with the
115
+ // history rows. runEvents is oldest-first, hence the backward walk.
116
116
  const claimTop = new Map();
117
117
  const resolved = new Map();
118
- for (let i = tickEvents.length - 1; i >= 0; i--) {
119
- const ev = tickEvents[i];
118
+ for (let i = runEvents.length - 1; i >= 0; i--) {
119
+ const ev = runEvents[i];
120
120
  if (ev.result !== "queued")
121
121
  continue;
122
- const joined = resolveQueuedResult(ev, mergeQueuedByLoop, outcomeByLoop, claimTop, new Map(), null);
122
+ const joined = resolveQueuedResult(ev, mergeQueuedByRunner, outcomeByRunner, claimTop, new Map(), null);
123
123
  if (joined !== null)
124
124
  resolved.set(ev, joined);
125
125
  }
126
- for (const ev of tickEvents) {
126
+ for (const ev of runEvents) {
127
127
  let cls = OUTCOME_CLASS[ev.result];
128
128
  if (cls === undefined)
129
129
  continue; // An unknown result is tallied in the Outcome table; costing it would need a class first.
@@ -138,7 +138,7 @@ export function timeAndSpend(tickEvents, allEvents) {
138
138
  }
139
139
  const role = eventRole(ev);
140
140
  const usage = eventUsage(ev);
141
- const durationMs = tickDurationMs(ev, starts);
141
+ const durationMs = runDurationMs(ev, starts);
142
142
  const row = getOrCreate(byRole, role, () => ({
143
143
  merged: emptyCell(),
144
144
  no_change: emptyCell(),
@@ -155,9 +155,9 @@ export function timeAndSpend(tickEvents, allEvents) {
155
155
  if (ev.result === "refused") {
156
156
  // A refusal is a deliberate objection that blocks its entry, not quiet idleness — the
157
157
  // reason live in the event + lastSummary (src/verdict/refusal.ts). Rank it as its own
158
- // per-role cause so the digest surfaces the objection instead of pricing the tick
158
+ // per-role cause so the digest surfaces the objection instead of pricing the run
159
159
  // exactly like a silently idle one; the branch sits BEFORE the no_change one because a
160
- // refused tick is also no_change-class in the outcome table. The example rides the
160
+ // refused run is also no_change-class in the outcome table. The example rides the
161
161
  // refusal's summary the way a review-rejected cause rides its reason.
162
162
  key = `refused\u0000${role}`;
163
163
  kind = "refused";
@@ -169,8 +169,8 @@ export function timeAndSpend(tickEvents, allEvents) {
169
169
  }
170
170
  else if (CLUSTERED_RESULTS.has(String(ev.result))) {
171
171
  // A clustered result with no error text still owns its time: an abort's run_end never
172
- // carries one (planTickStart clears s.lastError, tick-finalize logs error: undefined),
173
- // and a malformed error tick can lose it too, so both take the same placeholder cause the
172
+ // carries one (planRunStart clears s.lastError, run-finalize logs error: undefined),
173
+ // and a malformed error run can lose it too, so both take the same placeholder cause the
174
174
  // error-clusters section already itemizes rather than dropping the span itemized nowhere.
175
175
  ({ key, example } = pooledClusterCause(ev.error));
176
176
  }
@@ -182,7 +182,7 @@ export function timeAndSpend(tickEvents, allEvents) {
182
182
  // `merge_queued` keeps the authored one — exact-sha equality names no reason for a rebased
183
183
  // rejection (BUGS.md 2026-10-10). The rejection for this attempt is the one inside the
184
184
  // claimed pin and its outcome, the window no other attempt can occupy.
185
- const rej = newestAttemptEvent(rejectedByLoop, role, resolvedOutcome);
185
+ const rej = newestAttemptEvent(rejectedByRunner, role, resolvedOutcome);
186
186
  const reasons = stringList(rej?.reasons);
187
187
  example = reasons[0] !== undefined ? truncateExample(reasons[0]) : "";
188
188
  }
@@ -198,7 +198,7 @@ export function timeAndSpend(tickEvents, allEvents) {
198
198
  // keeps the authored one — exact-sha equality missed every rebased verification (BUGS.md
199
199
  // 2026-10-10). The review failure for this attempt is the one that falls inside the
200
200
  // claimed pin and its outcome, the window no other attempt can occupy.
201
- const failed = newestAttemptEvent(reviewFailedByLoop, role, resolvedOutcome);
201
+ const failed = newestAttemptEvent(reviewFailedByRunner, role, resolvedOutcome);
202
202
  ({ key, example } = pooledClusterCause(failed?.message));
203
203
  }
204
204
  else if (ev.result === "queued" &&
@@ -214,7 +214,7 @@ export function timeAndSpend(tickEvents, allEvents) {
214
214
  example = errorTextOrPlaceholder(undefined);
215
215
  // A merge-scope check the harness killed at its own deadline is the team's load, not a
216
216
  // verdict on the change: split it into its own cause so the ranking names it.
217
- const killed = newestAttemptEvent(deadlineKilledByLoop, role, resolvedOutcome);
217
+ const killed = newestAttemptEvent(deadlineKilledByRunner, role, resolvedOutcome);
218
218
  if (kind === "merge-blocked" && killed) {
219
219
  key = `merge_blocked_deadline\u0000${role}`;
220
220
  example = `${String(killed.scope)} build check killed at its ${Number(killed.timeoutMs) / 1000}s deadline (tumwater's own timeout, not a test failure)`;
@@ -44,6 +44,23 @@ export function queueFileStamp(name) {
44
44
  const m = /^(\d{13})-/.exec(name);
45
45
  return m ? Number(m[1]) : null;
46
46
  }
47
+ /** The queue-file name one stamp below `stamps`' minimum — naming an entry to sort before
48
+ * every listed one, the "move to head" rename both queues share. With no parseable stamps it
49
+ * falls back to just before now (before every hand-placed name). Decrements while the computed
50
+ * name exists on disk, because a concurrent writer may have just taken the name we computed;
51
+ * the pid in queueFileName keeps two concurrent tops' names apart too. `ext` is the queue's own
52
+ * entry extension. The caller hands in its already-listed stamps (parsed with queueFileStamp),
53
+ * not a fresh scan, so the smallest stamp reflects the same read that picked the entry being
54
+ * moved. */
55
+ export function lowestStampName(stamps, dir, ext) {
56
+ let stamp = (stamps.length > 0 ? Math.min(...stamps) : Date.now()) - 1;
57
+ let file = path.join(dir, queueFileName(stamp, 0, ext));
58
+ while (fs.existsSync(file)) {
59
+ stamp--;
60
+ file = path.join(dir, queueFileName(stamp, 0, ext));
61
+ }
62
+ return file;
63
+ }
47
64
  /** Remove one queue entry's file, reporting whether it was there: true when removed, false
48
65
  * when it had already vanished (ENOENT). Any other error is rethrown. Both queues treat a
49
66
  * vanished file as a normal race with a concurrent dequeue/cancel, never as a failure. */
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { errCode } from "../errno.js";
4
+ import { DAY_MS } from "../text/datetime.js";
4
5
  /** Generic file operations under the harness's error policy — missing is no data, cleanup
5
6
  * must not throw, directories are created before writes: stat-or-missing for log readers,
6
7
  * PATH lookup for the pi-installation preflight, size-based rotation, recursive directory
@@ -22,6 +23,22 @@ export function statOrNull(file) {
22
23
  return null; // Missing (or vanished) — no data yet.
23
24
  }
24
25
  }
26
+ /** Whether `file` exists and is a directory (a symlink is followed); false when it is missing
27
+ * or cannot be stat'd. */
28
+ export function isDirectory(file) {
29
+ return statOrNull(file)?.isDirectory() === true;
30
+ }
31
+ /** Whether `file` is a regular file the process may execute (a symlink is followed); false
32
+ * when it is missing, a directory (which passes X_OK), or lacks the executable bit. */
33
+ export function isExecutableFile(file) {
34
+ try {
35
+ fs.accessSync(file, fs.constants.X_OK);
36
+ return fs.statSync(file).isFile();
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ }
25
42
  /** Whether a freshly stat'd file is still the same file at the same bytes as a prior
26
43
  * observation: matching dev/ino (a replaced inode is a different file even at the same size)
27
44
  * plus matching size and, when the caller tracks it, mtime. The one home of the freshness
@@ -36,6 +53,12 @@ export function sameStatIdentity(st, prev) {
36
53
  return false;
37
54
  return prev.mtimeMs === undefined || st.mtimeMs === prev.mtimeMs;
38
55
  }
56
+ /** The identity fields sameStatIdentity compares, all zero when the file is missing (`null`):
57
+ * the "empty log" observation, so a log that has not been created yet reads as zero bytes of a
58
+ * file no real stat can match. */
59
+ export function statIdentityOrZero(st) {
60
+ return { dev: st?.dev ?? 0, ino: st?.ino ?? 0, size: st?.size ?? 0, mtimeMs: st?.mtimeMs ?? 0 };
61
+ }
39
62
  /** Open `file` for reading, or null when it vanished between a prior stat and this open — the
40
63
  * shared race-handling step for the incremental/tail log readers (forEachTailChunk,
41
64
  * terminateTornTail, readCompleteLines, readTranscriptTail), all of which treat a rotated-away
@@ -83,7 +106,7 @@ export function terminateTornTail(file, st) {
83
106
  /** Read a text file whole, or null when it does not exist (or cannot be read) — the harness's
84
107
  * "missing is no data" policy for the markdown and prompt text a render or prompt-building
85
108
  * path must never throw on: the backlog's PLANS/BUGS/QUESTIONS, the principles injected into
86
- * every tick prompt, the usage report's sources, and a queued prompt that vanished mid-
109
+ * every run prompt, the usage report's sources, and a queued prompt that vanished mid-
87
110
  * listing. The single home of that read-and-swallow step; callers pick their empty value
88
111
  * (`?? ""` when a blank default reads better than null). */
89
112
  export function readTextOrNull(file) {
@@ -148,29 +171,37 @@ export function envPath() {
148
171
  /** Locate an executable on PATH the same way spawn() would resolve it: a regular file
149
172
  * with the execute bit in some PATH directory. Returns its absolute path, or null when
150
173
  * missing (or not executable), so callers can fail fast with a clear message instead of
151
- * letting every tick die with "spawn <name> ENOENT". */
174
+ * letting every run die with "spawn <name> ENOENT". */
152
175
  export function findOnPath(name, pathEnv = envPath()) {
153
176
  for (const dir of pathEnv.split(path.delimiter)) {
154
177
  if (!dir)
155
178
  continue;
156
179
  const candidate = path.join(dir, name);
157
- try {
158
- fs.accessSync(candidate, fs.constants.X_OK); // Directories pass X_OK; require a file.
159
- if (fs.statSync(candidate).isFile())
160
- return candidate;
161
- }
162
- catch {
163
- // Not in this directory; keep looking.
164
- }
180
+ if (isExecutableFile(candidate))
181
+ return candidate;
165
182
  }
166
183
  return null;
167
184
  }
168
185
  /** Keep an append-only log bounded: over `maxBytes` it is renamed to `<file>.1`
169
- * (replacing any previous rotation) and a fresh file starts. Returns true if rotated. */
170
- export function rotateIfLarge(file, maxBytes) {
186
+ * (replacing any previous rotation) and a fresh file starts. Returns true if rotated.
187
+ * `keep` is how many archived generations to retain: over the size it drops `<file>.<keep>`,
188
+ * shifts `<file>.<k-1>` → `<file>.<k>` from oldest to newest (missing generations skipped),
189
+ * then moves the live file to `<file>.1`. With `keep` 1 it behaves as before, replacing the
190
+ * previous `.1`; role pi logs pass the default, the harness event log keeps five generations
191
+ * (EVENTS_KEEP). */
192
+ export function rotateIfLarge(file, maxBytes, keep = 1) {
171
193
  try {
172
194
  if (fs.statSync(file).size <= maxBytes)
173
195
  return false;
196
+ if (keep >= 1) {
197
+ // Drop the oldest generation first so a shift down cannot collide with it.
198
+ fs.rmSync(file + "." + keep, { force: true });
199
+ for (let gen = keep - 1; gen >= 1; gen--) {
200
+ const from = file + "." + gen;
201
+ if (fs.existsSync(from))
202
+ fs.renameSync(from, file + "." + (gen + 1));
203
+ }
204
+ }
174
205
  fs.renameSync(file, file + ".1");
175
206
  return true;
176
207
  }
@@ -217,13 +248,9 @@ export function walkUp(startDir, maxLevels, visit) {
217
248
  * cannot drift between them. (files.ts's own pruneOldFiles keeps its hand-rolled walk: it
218
249
  * removes emptied directories on the way back up, which this file-only visitor cannot do.) */
219
250
  export function walkFiles(dir, visit, skipDirs = new Set()) {
220
- let entries;
221
- try {
222
- entries = fs.readdirSync(dir, { withFileTypes: true });
223
- }
224
- catch {
251
+ const entries = tryReadDirEntries(dir);
252
+ if (!entries)
225
253
  return; // Vanished or unreadable — skip this directory.
226
- }
227
254
  for (const entry of entries) {
228
255
  const file = path.join(dir, entry.name);
229
256
  if (entry.isDirectory()) {
@@ -241,22 +268,44 @@ export function walkFiles(dir, visit, skipDirs = new Set()) {
241
268
  * with the same walk-and-swallow error policy, so it lives here once. (walkFiles above is the
242
269
  * recursive, file-only sibling.) */
243
270
  export function subdirectoryNames(dir, keep) {
244
- let entries;
271
+ const entries = tryReadDirEntries(dir);
272
+ if (!entries)
273
+ return []; // Missing or unreadable — no data.
274
+ const names = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
275
+ return keep ? names.filter(keep) : names;
276
+ }
277
+ /** The entry names directly under `dir`, in readdir order; `[]` when the directory is missing
278
+ * or unreadable. For the best-effort scans — a sweep, a count, an existence probe — where "no
279
+ * directory" and "nothing in it" are the same answer. A reader that must tell an unreadable
280
+ * directory from an empty one (listQueueFiles) keeps its own try/catch. */
281
+ export function dirEntryNames(dir) {
245
282
  try {
246
- entries = fs.readdirSync(dir, { withFileTypes: true });
283
+ return fs.readdirSync(dir);
247
284
  }
248
285
  catch {
249
- return []; // Missing or unreadable — no data.
286
+ return [];
287
+ }
288
+ }
289
+ /** The `Dirent` list directly under `dir`, or null when the directory is missing or
290
+ * unreadable — the walk-and-swallow prelude walkFiles, subdirectoryNames, pruneOldFiles, and
291
+ * treeBytes all began by spelling out, so the "a vanished or unreadable directory is skipped,
292
+ * never an error" policy lives in one place. `dirEntryNames` above is the plain-name sibling
293
+ * for scans that need no file/directory distinction. build-stage.ts's pruneStaleStagings reads
294
+ * the staging root the same way, and exports it the way dirEntryNames is exported.) */
295
+ export function tryReadDirEntries(dir) {
296
+ try {
297
+ return fs.readdirSync(dir, { withFileTypes: true });
298
+ }
299
+ catch {
300
+ return null;
250
301
  }
251
- const names = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
252
- return keep ? names.filter(keep) : names;
253
302
  }
254
303
  /** Write `data` — UTF-8 text or raw bytes — to `file` via a tmp file + rename, so a concurrent
255
304
  * reader of the same path sees either the old (absent) or the new content, never a partial
256
305
  * write: the prompt-inbox queue files (inbox.ts's enqueueRolePrompt) are written by one process
257
- * while dashboards poll, `--list` reads, and the loops dequeue in others, and a read that races
258
- * the write could run a tick on a truncated user request or flash one on the dashboard. The tmp
259
- * name carries the pid because several processes can write at once (CLI, TUI, GUI, a loop's
306
+ * while dashboards poll, `--list` reads, and the roles dequeue in others, and a read that races
307
+ * the write could start an agent run on a truncated user request or flash one on the dashboard.
308
+ * The tmp name carries the pid because several processes can write at once (CLI, TUI, GUI, a role's
260
309
  * re-queue); on failure the tmp is removed and the error rethrown, leaving the target untouched.
261
310
  * The harness's JSON writer (writeJsonAtomic in src/files/json-files.ts) and the config write-back
262
311
  * (writeBytesAtomic's caller, merge-git.ts's restoreConfigBytes) write through this, so the
@@ -304,6 +353,11 @@ export function removeQuiet(file) {
304
353
  export function removeTree(dir) {
305
354
  fs.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
306
355
  }
356
+ /** The epoch-ms instant `days` days before now: the age cutoff the retention prunes compare a
357
+ * file's mtime against (pruneOldFiles, pruneOldDirectory, and session-ledger.ts's two prunes). */
358
+ export function daysAgoMs(days) {
359
+ return Date.now() - days * DAY_MS;
360
+ }
307
361
  /** Delete regular files under `dir` (recursively) older than `days` days. Walks with a plain
308
362
  * per-directory readdir instead of {recursive: true} + entry.parentPath, so it stays within the
309
363
  * Node >= 20 floor declared in package.json — parentPath landed only in v20.12, and on earlier
@@ -324,16 +378,12 @@ export function removeTree(dir) {
324
378
  export function pruneOldFiles(dir, days) {
325
379
  if (!fs.existsSync(dir))
326
380
  return 0;
327
- const cutoff = Date.now() - days * 24 * 3600 * 1000;
381
+ const cutoff = daysAgoMs(days);
328
382
  let pruned = 0;
329
383
  const walk = (d) => {
330
- let entries;
331
- try {
332
- entries = fs.readdirSync(d, { withFileTypes: true });
333
- }
334
- catch {
384
+ const entries = tryReadDirEntries(d);
385
+ if (!entries)
335
386
  return; // Vanished (or unreadable) mid-scan; skip this directory.
336
- }
337
387
  for (const entry of entries) {
338
388
  const file = path.join(d, entry.name);
339
389
  if (entry.isDirectory()) {
@@ -369,14 +419,14 @@ export function pruneOldFiles(dir, days) {
369
419
  }
370
420
  /** Remove `dir` entirely when the directory itself is older than `days` days (its own mtime,
371
421
  * not its children's). Worktree builds leave a gitignored `dist/` dir that `git clean -fd`
372
- * does not drop; this prunes it at the next tick, bounded by the same age rule. Returns true
422
+ * does not drop; this prunes it at the next run, bounded by the same age rule. Returns true
373
423
  * when the directory was removed. Mirrors pruneOldFiles' vanish-tolerant style. */
374
424
  export function pruneOldDirectory(dir, days) {
375
425
  if (!fs.existsSync(dir))
376
426
  return false;
377
427
  let pruned = false;
378
428
  try {
379
- if (fs.statSync(dir).mtimeMs < Date.now() - days * 24 * 3600 * 1000) {
429
+ if (fs.statSync(dir).mtimeMs < daysAgoMs(days)) {
380
430
  removeTree(dir);
381
431
  pruned = true;
382
432
  }
@@ -386,3 +436,29 @@ export function pruneOldDirectory(dir, days) {
386
436
  }
387
437
  return pruned;
388
438
  }
439
+ /** The total bytes under one directory tree, counted iteratively and skipping unreadable
440
+ * entries: a permission-denied subdirectory must not fail doctor. */
441
+ export function treeBytes(dir) {
442
+ let total = 0;
443
+ const stack = [dir];
444
+ while (stack.length > 0) {
445
+ const current = stack.pop();
446
+ const entries = tryReadDirEntries(current);
447
+ if (!entries)
448
+ continue;
449
+ for (const entry of entries) {
450
+ const full = path.join(current, entry.name);
451
+ if (entry.isDirectory())
452
+ stack.push(full);
453
+ else if (entry.isFile()) {
454
+ try {
455
+ total += fs.statSync(full).size;
456
+ }
457
+ catch {
458
+ // A file that vanished mid-walk contributes nothing.
459
+ }
460
+ }
461
+ }
462
+ }
463
+ return total;
464
+ }
@@ -8,14 +8,14 @@ import { parseJsonObject } from "./json-object.js";
8
8
  * is "no data", never an error) shared by every reader and writer of those files. */
9
9
  /** Read and parse a JSON *object* file, returning null when it does not exist, cannot be read
10
10
  * or parsed, or parses to a valid JSON value that is not a plain object (a scalar, `null`, or
11
- * an array). The harness's state/marker/info files (loop state, orchestrator info, landing
11
+ * an array). The harness's state/marker/info files (runner state, orchestrator info, landing
12
12
  * marker, reset/wake/abort markers, restart stamp, build stamp, claude account state) are all
13
13
  * objects written by other processes and may be missing or torn mid-write — readers treat that
14
14
  * as "no data" rather than an error, so a crash in one process can never take down the
15
15
  * observers polling them. A wrong-shaped file reads the same way: casting a scalar/array to `T`
16
16
  * would hand
17
- * callers a value that looks like data but is not (loadLoopState would spread a string's
18
- * characters into the loop's state, readOrchestratorInfo's `pid` would read undefined off an
17
+ * callers a value that looks like data but is not (loadRunnerState would spread a string's
18
+ * characters into the role's state, readOrchestratorInfo's `pid` would read undefined off an
19
19
  * array), so the object check is part of the no-data policy, not an extra one. */
20
20
  export function readJsonFile(file) {
21
21
  try {
@@ -30,11 +30,11 @@ export function stringList(value) {
30
30
  * absent, not a number, or not finite (NaN/±Infinity poison arithmetic and comparisons: a
31
31
  * NaN costUsd makes every budget-cap comparison false, so the cap never trips) — the one
32
32
  * home of the "the field is the number or it is nothing" read. The call sites, by the
33
- * fallback each passes: event-read.ts's eventTick (tick → 0), eventUsage (tokens, costUsd →
34
- * 0), eventDayKey (ts → null), tickStartMap and tickSpanMs (both ts → null);
33
+ * fallback each passes: event-read.ts's eventRun (run → 0), eventUsage (tokens, costUsd →
34
+ * 0), eventDayKey (ts → null), runStartMap and runSpanMs (both ts → null);
35
35
  * event-format.ts's elapsed (ms → null), gigabytes (v → 0), minutes (ms → 0), and the
36
36
  * build_stale, slot_wait, restart, and permit_reserve renderers (aheadCommits, waitedMs,
37
- * abortedTicks, reserve → 0); phrases.ts's budgetPhrase (spentUsd, capUsd → 0); redeploy.ts's
37
+ * abortedRuns, reserve → 0); phrases.ts's budgetPhrase (spentUsd, capUsd → 0); redeploy.ts's
38
38
  * autoRestartRecord (lastAt → null); build/build-info.ts's readBuildInfo (builtAt → 0);
39
39
  * failure/failure-state-change.ts's model_fallback_ended (durationMs → 0) and build_stale
40
40
  * renderer (aheadCommits → 0); gates/maintenance-quota.ts's foldLine and seed (ev.ts → null);
@@ -54,13 +54,13 @@ export function finiteNumber(value, fallback) {
54
54
  * amount is as poisonous as NaN: it runs a spend total backwards and makes every `>= cap`
55
55
  * comparison false, so the daily budget's safety valve would never trip. The call sites, by
56
56
  * the fallback each passes: budget/budget.ts's spendNumber (dayCostUsd/usd → 0),
57
- * loop/loop-state.ts's healNumber (every numeric LoopState field → 0),
58
- * tick/tick-usage.ts's counter (each persisted usage count → 0), pi/pi-stream.ts's usageNumber
57
+ * runner/runner-state-load.ts's healNumber (every numeric RunnerState field → 0),
58
+ * run/run-usage.ts's counter (each persisted usage count → 0), pi/pi-stream.ts's usageNumber
59
59
  * (one wire usage number → 0), ui/progress-data.ts's message_end read (totalTokens → the
60
- * previous context, output and its peak floor → 0), time-spend.ts's tickDurationMs (an event's
60
+ * previous context, output and its peak floor → 0), time-spend.ts's runDurationMs (an event's
61
61
  * durationMs → null, the "no own duration" marker), pi-extension/context-budget.ts's
62
62
  * contextNote (usedTokens, usedWindow → null), pi-extension/context-shake.ts's percentOf
63
- * (tokens, contextWindow → null), merge/merge-queue.ts's isMergeEntry (tick, enqueuedAt,
63
+ * (tokens, contextWindow → null), merge/merge-queue.ts's isMergeEntry (run, enqueuedAt,
64
64
  * revisionRound → null), git/slots-state.ts's isSlotLease (since, pid → null); and
65
65
  * agent/claude/claude-stream.ts's canonicalUsage (each wire usage
66
66
  * number → 0) and its result/retry/rate-limit reads (api_error_status, attempt,
@@ -92,6 +92,27 @@ export function parseJsonObject(text) {
92
92
  export function objectOrEmpty(value) {
93
93
  return isJsonObject(value) ? value : {};
94
94
  }
95
+ /** The entries of `value` that pass `guard`, as a fresh record; `{}` when `value` is not a JSON
96
+ * object. The one home of "keep the well-formed entries of an untrusted map, drop the foreign
97
+ * ones" (the image-probe and opencode-catalog caches, the per-pair daily spend map). Built with
98
+ * `Object.fromEntries`, so a `__proto__` key stays an own property. */
99
+ export function validEntries(value, guard) {
100
+ if (!isJsonObject(value))
101
+ return {};
102
+ return Object.fromEntries(Object.entries(value).filter((kv) => guard(kv[1])));
103
+ }
104
+ /** Store one own property on `obj` under `key`. A plain `obj[key] = value` loses a key of
105
+ * `__proto__`: the assignment hits Object.prototype's setter and rewrites the map's prototype
106
+ * instead of adding an own entry, so the value vanishes from every reader and from the
107
+ * persisted JSON. Handling untrusted keys, or a model/flow name crossing an external boundary,
108
+ * means that case is real, so every site that stores under such a key defines the own property
109
+ * instead — the one home of the `enumerable/writable/configurable: true` descriptor, so the
110
+ * three flags cannot drift between sites. A named wrapper (`setWindow` on the account's
111
+ * windows) keeps its domain meaning across its call sites; callers keep `setOwn` where the
112
+ * key/value are generic, as the qa coverage ledger and budget.ts's per-pair spend do. */
113
+ export function setOwn(obj, key, value) {
114
+ Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
115
+ }
95
116
  /** Assemble a stream translator's canonical usage block from the four already-validated non-
96
117
  * negative counts and the USD cost — the one home of the shape both the claude and opencode
97
118
  * translators (agent/claude/claude-stream.ts, agent/opencode/opencode-stream.ts) build, so the
@@ -36,8 +36,8 @@ export function cachedByStat(cache, key, file, load, clone) {
36
36
  * reads it, and a null (no data) propagates through so a render path never throws on the file's
37
37
  * state. `parse` runs only on a cache miss and receives the file's text (never null); `clone`
38
38
  * hands each caller its own copy. The callers: backlog.ts's sectionEntries and
39
- * sectionCompletionDates, and backlog-structure.ts's strandedPlanEntriesFor,
40
- * actionableEntryRangesFor, and renderBacklogIndexBlock. */
39
+ * sectionCompletionDates, and backlog-structure.ts's strandedPlanEntriesFor and
40
+ * actionableEntryRangesFor, and backlog-index.ts's renderBacklogIndexBlock. */
41
41
  export function cachedTextByStat(cache, key, file, parse, clone) {
42
42
  return cachedByStat(cache, key, file, () => {
43
43
  const md = readTextOrNull(file); // Missing or unreadable — no data.
@@ -160,7 +160,7 @@ const MAX_TAILS = 64;
160
160
  * from `fresh()`'s offset and folding every line into its fresh value;
161
161
  * - append-only growth since the last visit → fold only the newly complete lines in.
162
162
  * A torn trailing line is left unconsumed until its newline lands (the offset advances to
163
- * the last complete line). Shared by every observer that polls a log once per tick —
163
+ * the last complete line). Shared by every observer that polls a log once per run —
164
164
  * src/ui/progress-data.ts's live tail and transcript.ts's rendered entries. */
165
165
  export function withTail(tails, file, st, fresh, feed) {
166
166
  let tail = tails.get(file);
@@ -1,8 +1,7 @@
1
- // vocabulary: plain
2
1
  /** New-project bootstrap (plans/work-ratio.md, "New-project bootstrap"; part 2/2): a fresh
3
2
  * project seeds `bootstrap.untilPlansDone` in tumwater.json (part 1/2), and this gate holds
4
- * every maintenance loop until that many plans have moved to PLANS.md's `## Done`. Only plan,
5
- * feature and director tick — plus bugfix while BUGS.md `## Open` is non-empty, since its
3
+ * every maintenance role until that many plans have moved to PLANS.md's `## Done`. Only plan,
4
+ * feature and director run — plus bugfix while BUGS.md `## Open` is non-empty, since its
6
5
  * latent-bug search is maintenance. The hold gives a young project a steady stream of plans and
7
6
  * code before hygiene roles start churning.
8
7
  *
@@ -28,7 +27,7 @@ import { writeJsonAtomic } from "../files/json-files.js";
28
27
  import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
29
28
  import { errorMessage, stripTerminalControls } from "../text/text.js";
30
29
  import { BUGFIX_ROLE, DIRECTOR_ROLE } from "../roles/roles.js";
31
- import { baseRoleOf } from "../roles/loop-ids.js";
30
+ import { baseRoleOf } from "../roles/runner-ids.js";
32
31
  /** The read-only bootstrap verdict: active while a positive `untilPlansDone` is configured and
33
32
  * the latch file is absent and fewer than that many plans are Done. No writes — the status
34
33
  * surfaces (src/status/status-data.ts) call this directly to show the same figure the
@@ -56,7 +55,7 @@ export function bootstrapHoldsRole(role, openBugsNow) {
56
55
  export function newBootstrapGateState() {
57
56
  return { active: false };
58
57
  }
59
- /** Step the gate by one orchestrator poll. Returns the loop ids held by bootstrap as of THIS
58
+ /** Step the gate by one orchestrator poll. Returns the role ids held by bootstrap as of THIS
60
59
  * poll, for the scheduler's `bootstrapHeld` view. When the target has been reached, writes the
61
60
  * latch and logs `bootstrap_complete` once (the now-present latch suppresses later logs, and
62
61
  * survives an orchestrator restart that resets `state`). An unwritable repo does not abort the