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,7 +1,6 @@
1
- // vocabulary: plain
2
1
  /** Local date/time formatting and calendar-day arithmetic — the zero-padded clock and
3
2
  * calendar parts, local-midnight day math, and the report-window header every display and
4
- * report surface (transcript run separators, the status table's last-tick cell, daily-budget
3
+ * report surface (transcript run separators, the status table's last-run cell, daily-budget
5
4
  * day stamps, the usage report's and failure digest's per-day buckets and headers) render
6
5
  * through, so what counts as one day and how a timestamp prints cannot drift per consumer.
7
6
  * Split out of text.ts — which keeps the one-line text shaping — because calendar
@@ -9,14 +8,28 @@
9
8
  * string cutting, with its own tests. Presentation only: depends on node built-ins alone. */
10
9
  import { plural } from "./phrases.js";
11
10
  import { EVENTS_LOG_BASENAME } from "../paths.js";
11
+ /** Milliseconds in an hour — the shared unit for this repo's window and timeout
12
+ * constants (reclaim's idle threshold, the doctor's merge/stale-pin/wait checks, the
13
+ * claim/review/queue expiry, the scheduling defer cap, redeploy's cooldowns, the
14
+ * provider-hold window), so no two of them can spell the hour multiplier differently
15
+ * (`60 * 60 * 1000` vs `60 * 60_000` vs `3600_000`). Day-sized spans use `DAY_MS`
16
+ * (or `N * DAY_MS`) below. */
17
+ export const HOUR_MS = 60 * 60 * 1000;
18
+ /** Milliseconds in a day — the sibling of `HOUR_MS`: the shared day unit for the
19
+ * day-sized retention and lookback constants (the opencode-catalog and image-probe TTLs,
20
+ * the doctor's stale-pin/merge/prompt/wait checks, the known-flake and selfcheck
21
+ * windows, the dashboard-history since bound, the retention prune, the claude-window
22
+ * lookback) so one day cannot mean slightly different spans per consumer. */
23
+ export const DAY_MS = 24 * HOUR_MS;
12
24
  /** Zero-pad an integer to two digits — the clock and calendar components every local-time
13
25
  * display in the harness renders through (transcript run separators, the status table's last-
14
- * tick cell, the daily-budget day stamp), so zero-padding cannot drift per consumer. */
26
+ * run cell, the daily-budget day stamp), so zero-padding cannot drift per consumer. */
15
27
  export function pad2(n) {
16
28
  return String(n).padStart(2, "0");
17
29
  }
18
30
  /** The local wall-clock `HH:MM` of an instant, the hours/minutes-only shape the burn-rate
19
- * forecast, the window badge, and the role view's fallback line share. */
31
+ * forecast, the window badge, the role view's fallback line, and the failure digest's
32
+ * transition stamps (failure-render.ts's stateChangeStamp) share. */
20
33
  export function clockHm(ms) {
21
34
  const d = new Date(ms);
22
35
  return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
@@ -25,7 +38,7 @@ export function clockHm(ms) {
25
38
  * clamps at zero (an age never goes negative when the clock skews), `secondsUntil` clamps at
26
39
  * zero the same way for the future direction (a countdown never reads negative once its
27
40
  * deadline has passed). The one home of the clamp + rounding behind every elapsed age and
28
- * countdown — status-render.ts's lastTickCell and nextRunCell, the queued-prompt age and
41
+ * countdown — status-render.ts's lastRunCell and nextRunCell, the queued-prompt age and
29
42
  * deliver-in suffix, the team pause's auto-resume countdown, and status-model.ts's sleeping
30
43
  * phase label (which re-floors at 1s) all render through. The GUI page's browser
31
44
  * script keeps its own one-line twins (gui-client.ts's ageSec and its inline countdown
@@ -39,12 +52,6 @@ export function secondsSince(sinceMs, now) {
39
52
  export function secondsUntil(atMs, now) {
40
53
  return Math.max(0, Math.round((atMs - now) / 1000));
41
54
  }
42
- /** Whole elapsed seconds since an epoch-ms instant, clamped at zero and rounded to the
43
- * nearest second — the age rule status-render.ts's lastTickCell ages its `HH:MM:SS · Ns ago`
44
- * stamp through (secondsSince against the real clock). */
45
- export function elapsedSeconds(sinceMs) {
46
- return secondsSince(sinceMs, Date.now());
47
- }
48
55
  /** Calendar date as `YYYY-MM-DD` in local time — shared by the transcript run separators,
49
56
  * the daily-budget day stamp (todayStamp), and the usage report's per-day buckets, which must
50
57
  * all agree on what counts as one day. */
@@ -52,13 +59,23 @@ export function formatDate(d) {
52
59
  return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
53
60
  }
54
61
  /** Wall-clock time as zero-padded `HH:MM:SS` in local time — the clock half formatTimestamp
55
- * builds, and the direct rendering of the status table's last-tick cell (status-render.ts's
56
- * lastTickCell), doctor-checks's probe-at stamp, failure-render's state-change stamp, and
62
+ * builds, and the direct rendering of the status table's last-run cell (status-render.ts's
63
+ * lastRunCell), doctor-project-checks's probe-at stamp, and
57
64
  * operator-intent's pause/deadline wording, so a local time of day reads the same shape at
58
65
  * every surface. */
59
66
  export function formatTime(d) {
60
67
  return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
61
68
  }
69
+ /** Epoch ms as a local `HH:MM:SS` wall-clock string — the epoch-ms form of formatTime, the
70
+ * dayKey-to-formatDate sibling. It shares the clock-instant rendering every event renderer's
71
+ * pause/reset/resume suffix needs (event-format.ts's pauseSuffix and windowResetPhrase,
72
+ * failure-state-change.ts's team/role pause untils, doctor-model-checks.ts's probe time), so no
73
+ * site re-derives `formatTime(new Date(ms))`. A non-finite or absent `ms` renders the same NaN
74
+ * clock formatTime's Date would — callers guard with finiteNumber first, exactly as they did
75
+ * before. */
76
+ export function formatClock(ms) {
77
+ return formatTime(new Date(ms));
78
+ }
62
79
  /** Epoch ms as a local calendar-day key (`YYYY-MM-DD`) — the epoch-ms form of formatDate:
63
80
  * the day-bucketing rule the event log's day key (eventDayKey), the windowed readers' window
64
81
  * keys, the daily-budget stamp, and the failure digest's cluster dates all render through, so
@@ -89,7 +106,7 @@ export function monthDay(dayKey) {
89
106
  * the transcript's run separators, the history table's time cell, and the review-gate prompt's
90
107
  * rejection context, so a timestamp reads identically at every surface. A non-finite `ms` (a
91
108
  * corrupt event `ts`, a Date.parse of a malformed ISO match) renders as the em dash — the same
92
- * "no value" marker the history table uses for a tick with no reported duration, so a corrupt
109
+ * "no value" marker the history table uses for a run with no reported duration, so a corrupt
93
110
  * stamp reads as unknowable instead of printing `new Date(NaN)`'s `NaN-NaN-NaN NaN:NaN:NaN`
94
111
  * through every surface that renders events or rows. */
95
112
  export function formatTimestamp(ms) {
@@ -144,8 +161,33 @@ export function humanSeconds(s) {
144
161
  export function humanUntil(atMs, now) {
145
162
  return humanSeconds(secondsUntil(atMs, now));
146
163
  }
147
- /** The shared "when may this loop next act" phrase — the `next run` cell (status-render.ts's
148
- * nextRunCell) and the `Next tick` line (role-render.ts's scheduleLine) render their schedule
164
+ /** `humanUntil`'s past-direction twin: how long since an epoch-ms instant, as `NS ago` —
165
+ * the past-direction phrase five renderers each built as `{humanSeconds(secondsSince(...))} ago`
166
+ * (status-render.ts's ago against the real clock, role-render.ts's last-run and
167
+ * merge-queue-render.ts's enqueue ages, inbox.ts's queued-age suffix, and doctor-notify.ts's
168
+ * latest-failed-page age, which had inlined the clamp itself), so the clamp, the rounding,
169
+ * and the " ago" wording cannot drift per consumer. Lives here beside the other time
170
+ * phrasing so core modules (which may not import src/ui) share the same form. */
171
+ export function humanAgo(sinceMs, now) {
172
+ return `${humanSeconds(secondsSince(sinceMs, now))} ago`;
173
+ }
174
+ /** A compact age clamped at zero and floored to whole units — `12m`, `4h`, `6d` — the bare
175
+ * (no `ago`) parenthetical the `qa` coverage block and the `feedback` "previously filed" block
176
+ * render for each ledger entry, which each defined as their own local `formatAge`. Kept
177
+ * distinct from humanSeconds on purpose: this floors (a sub-minute age reads `0m`, never
178
+ * `Ns`) and skips the seconds bucket, so the two units/rounding rules cannot drift per
179
+ * consumer. Lives here beside the other time phrasing. */
180
+ export function formatAge(ms) {
181
+ const minutes = Math.max(0, Math.floor(ms / 60_000));
182
+ if (minutes < 60)
183
+ return `${minutes}m`;
184
+ const hours = Math.floor(minutes / 60);
185
+ if (hours < 24)
186
+ return `${hours}h`;
187
+ return `${Math.floor(hours / 24)}d`;
188
+ }
189
+ /** The shared "when may this role next act" phrase — the `next run` cell (status-render.ts's
190
+ * nextRunCell) and the `Next run` line (role-render.ts's scheduleLine) render their schedule
149
191
  * through it, so the two surfaces cannot drift on the backoff prefix or the humanSeconds
150
192
  * bucketing. `remainSeconds` is the seconds until the next run (non-positive = due); `backoff`
151
193
  * adds the `backoff ` prefix the wake action clears; `dueWord` names the due case and `withIn`
@@ -166,7 +208,7 @@ export function dayLabel(days) {
166
208
  return plural(days, "day");
167
209
  }
168
210
  /** The report header both renderers print: the window's day-key bounds, its length in days
169
- * (singular at 1), and the event log it was read from. The failure digest appends its own tick
211
+ * (singular at 1), and the event log it was read from. The failure digest appends its own run
170
212
  * count. The rotation phrase is a required argument — eventsRotationLabel() supplies it from
171
213
  * events.ts's EVENTS_MAX_BYTES, so this header cannot keep claiming "16 MB" after the actual
172
214
  * threshold moves (this module stays presentation-only, owning no log facts of its own). The
@@ -180,7 +222,12 @@ export function reportWindow(from, to, days, rotation) {
180
222
  * with the window's start instant in local time. `duration` is the window length already phrased
181
223
  * by cli-args.ts's durationLabel (this helper supplies the leading `last `) — taken as a
182
224
  * string so this presentation-only module avoids importing the CLI layer; the failure digest
183
- * appends its own tick count. One home so the two reports' window wording cannot drift. */
225
+ * appends its own run count. One home so the two reports' window wording cannot drift. */
184
226
  export function sinceWindowLine(duration, fromIso) {
185
227
  return `window: last ${duration} (since ${formatTimestamp(new Date(fromIso).getTime())})`;
186
228
  }
229
+ /** The bounded sibling of `sinceWindowLine`: the window line for a `--since`/`--until` span that
230
+ * ends before now. */
231
+ export function boundedWindowLine(since, until, fromIso, toIso) {
232
+ return `window: ${since} to ${until} ago (since ${formatTimestamp(new Date(fromIso).getTime())}, until ${formatTimestamp(new Date(toIso).getTime())})`;
233
+ }
@@ -6,20 +6,31 @@ import { textOr } from "./text.js";
6
6
  * (wording fragments). Presentation only: depends on node built-ins alone, so any layer
7
7
  * (harness or observer) can import it — and each format lives in exactly one place so the
8
8
  * abbreviation length, decimal width, and compaction thresholds cannot drift per consumer. */
9
+ /** Append a section note to a running prompt, separated from what precedes it by a blank line.
10
+ * The run and authoring-run prompt assemblers (run-prompt.ts, runner-run.ts) build a prompt by
11
+ * stacking nameless sections — assignment, held-entry, conflict-discard, conflict-handback and
12
+ * cut-off notes appended this way. The rejected-review and revision notes deliberately LEAD the
13
+ * run instead (prependPromptNote, plans/reviewer-as-role.md part 4/4) so the author cannot miss
14
+ * them. This is the single append so the `\n\n` section separator stays in one place. The note
15
+ * is appended unconditionally, exactly as the call sites appended the literal, so behavior is
16
+ * unchanged for any note. */
17
+ export function appendPromptNote(prompt, note) {
18
+ return `${prompt}\n\n${note}`;
19
+ }
20
+ /** Prepend a section note to a running prompt, separated from what follows by a blank line — the
21
+ * mirror of appendPromptNote, used where a note must LEAD the charter rather than trail it: the
22
+ * rejected-review note (run-prompt.ts) and the revision note (runner-run.ts) of
23
+ * plans/reviewer-as-role.md part 4/4, which the author must not miss. This is the single prepend
24
+ * so the `\n\n` section separator stays in one place, the same way appendPromptNote owns the
25
+ * append side. */
26
+ export function prependPromptNote(prompt, note) {
27
+ return `${note}\n\n${prompt}`;
28
+ }
9
29
  /** Compact token count for tight table cells and commit trailers — bare integers below
10
30
  * 10,000, one-decimal k at and above 10,000, one-decimal M at and above 1,000,000. The single
11
31
  * home of the token display format shared by the status table's gen/peak-ctx columns and the
12
32
  * commit trailer's ctx field — pinned here so those surfaces cannot drift even though they
13
33
  * live in different modules. */
14
- /** Append a section note to a running prompt, separated from what precedes it by a blank line.
15
- * The tick and authoring-tick prompt assemblers (tick-prompt.ts, loop-tick.ts) build a prompt by
16
- * stacking nameless sections — assignment, held-entry, rejected-review, conflict-discard,
17
- * conflict-handback, revision and cut-off notes — each appended the same way. This is the single
18
- * append so the `\n\n` section separator stays in one place. The note is appended unconditionally,
19
- * exactly as the call sites appended the literal, so behavior is unchanged for any note. */
20
- export function appendPromptNote(prompt, note) {
21
- return `${prompt}\n\n${note}`;
22
- }
23
34
  export function compactTokens(n) {
24
35
  if (n >= 1_000_000)
25
36
  return `${(n / 1_000_000).toFixed(1)}M`;
@@ -58,10 +69,36 @@ export function usdCap(n) {
58
69
  /** A disk size in GB for display: one decimal and the unit (`12.3 GB`) — the single home of
59
70
  * the disk-size format shared by the event feed's disk lines (event-format.ts), the status
60
71
  * header's disk badge (badges.ts's `diskBadge`), the team's low-disk alert (team-alerts.ts),
61
- * the doctor's disk check detail (doctor-checks.ts), and `tumwater reclaim`'s freed-size line
62
- * (operator-commands.ts), so the decimal width and unit cannot drift per consumer. A caller
72
+ * the doctor's disk check detail (doctor-project-checks.ts), and `tumwater reclaim`'s
73
+ * freed-size line (operator-commands.ts), so the decimal width and unit cannot drift per
74
+ * consumer. A caller
63
75
  * reading the value from an untrusted event field applies event-format.ts's `gigabytes`
64
76
  * wrapper (finiteNumber's corrupt-field rule) over this. */
65
77
  export function formatGB(n) {
66
78
  return `${n.toFixed(1)} GB`;
67
79
  }
80
+ /** A byte count for display, picking the largest of the B/KB/MB/GB tiers so the unit keeps the
81
+ * number small (`2.0 KB`, `1.5 MB`) — one decimal everywhere. The single home of the tiered
82
+ * byte format shared by the doctor's claude transcript size (doctor-checks.ts's `formatBytes`)
83
+ * and the opencode db/spill sizes (doctor-model-checks.ts's `opencodeBytes`), so the tiers and
84
+ * decimal width cannot drift per consumer. Distinct from `formatGB` above, which always renders
85
+ * a disk size in GB regardless of magnitude. The GUI's file-size label still formats from its
86
+ * own inline JS in gui-client-composer.ts: a separate runtime that cannot import TypeScript. */
87
+ export function formatBytes(n) {
88
+ if (n >= 1024 ** 3)
89
+ return `${(n / 1024 ** 3).toFixed(1)} GB`;
90
+ if (n >= 1024 ** 2)
91
+ return `${(n / 1024 ** 2).toFixed(1)} MB`;
92
+ if (n >= 1024)
93
+ return `${(n / 1024).toFixed(1)} KB`;
94
+ return `${n} B`;
95
+ }
96
+ /** A token-count context window with its unit, thousands-separated (`128,000-token window`) —
97
+ * the single home of this format shared by the doctor's codex and opencode model checks
98
+ * (doctor-codex-checks.ts, doctor-opencode-checks.ts), each of which renders a cataloged
99
+ * model's window this way, so the separator and the "-token window" unit cannot drift per
100
+ * consumer. The separating comma is the caller's to add when the label follows a price
101
+ * (opencode's `, ${tokenWindowLabel(…)}`). */
102
+ export function tokenWindowLabel(n) {
103
+ return `${n.toLocaleString("en-US")}-token window`;
104
+ }
@@ -18,13 +18,16 @@ export function markdownTable(header, rows, align = []) {
18
18
  ...rows.map((cells) => `| ${cells.join(" | ")} |`),
19
19
  ];
20
20
  }
21
- /** `items` as a `1. …` ordered list, one line each, joined with newlines — the single home of
21
+ /** `items` as a `1. …` ordered list, one line each, joined with newlines — the home of
22
22
  * the `map((item, i) => `${i + 1}. ${item}`).join("\n")` step, shared by the review prompt's
23
23
  * prior objections, its stage-fix findings, and the rejected/revision notes
24
- * (prompt/gate-prompts.ts) and `tumwater questions`' open-question list
25
- * (backlog/question-answer.ts), so the numbering shape cannot drift between the surfaces that
26
- * read a numbered list. `empty` is returned verbatim when `items` is empty (the reasons
27
- * sites pass `(no reasons recorded)`; the others keep the default empty string). */
24
+ * (prompt/gate-notes.ts), `tumwater questions`' open-question list
25
+ * (backlog/question-answer.ts) and `tumwater wake --list`'s wake lines
26
+ * (operator/operator-intent.ts `renderWakeList`), so the numbering shape cannot drift between
27
+ * those surfaces (the TUI queued-prompt rows and the merge queue rows number their own
28
+ * lines). `empty` is returned verbatim when `items` is empty
29
+ * (the reasons sites
30
+ * pass `(no reasons recorded)`; the others keep the default empty string). */
28
31
  export function numberedList(items, empty = "") {
29
32
  return items.length > 0 ? items.map((item, i) => `${i + 1}. ${item}`).join("\n") : empty;
30
33
  }
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  import path from "node:path";
3
2
  import { finiteNumber, isJsonObject, stringList } from "../files/json-object.js";
4
3
  import { squash } from "./text.js";
@@ -8,38 +7,40 @@ import { shortSha, usd } from "./format.js";
8
7
  * of $y", main's red-tip naming, a tool call's one-line label, the plural helper) shared by the
9
8
  * event feed (event-format.ts), the failure digest (src/failure/failure-render.ts and
10
9
  * src/failure/failure-state-change.ts), the status surfaces (ui/), and the CLI messages
11
- * (operator/operator-commands.ts, cli/cli-run.ts), so their phrasing cannot drift per consumer.
10
+ * (operator/operator-commands.ts, cli/cli-start-summary.ts), so their phrasing
11
+ * cannot drift per consumer.
12
12
  * Pure presentation: every phrase composes the shared formats format.ts pins (shortSha, usd,
13
13
  * squash) into words. Pure value formats — the token a number or hash renders as (compactTokens,
14
14
  * shortSha, usd, usdCap) — stay in format.ts; this module is where those tokens become words. */
15
15
  const NO_REASONS_GIVEN = "no reasons given";
16
- /** The reason an over-long prompt or config field carries: the text rides into a tick's
17
- * prefill, so a longer value bloats every tick's context. The single home of that tail,
16
+ /** The reason an over-long prompt or config field carries: the text rides into a run's
17
+ * prefill, so a longer value bloats every run's context. The single home of that tail,
18
18
  * shared by init's seed check (init/init.ts) and config-validation's customRoles task and
19
19
  * roles-instructions caps (config/config-validation.ts), so the three cannot drift on the
20
20
  * wording. inbox-submit's promptLengthProblem renders a role-scoped variant ("it rides into
21
- * the <role> tick's prefill") and keeps its own wording. */
21
+ * the <role> run's prefill") and keeps its own wording. */
22
22
  export const PREFILL_REASON = "it rides into every run's prefill";
23
23
  /** The reason a review rejection leads with — its first listed reason, or `no reasons given`
24
24
  * when the list is empty or its first entry is not a string. One home so the fallback cannot
25
25
  * drift across the four surfaces that show just that first reason: the review gate's returned
26
26
  * detail (review.ts), the event feed's one-line reject (event-format.ts), the failure digest's
27
- * rejection clusters (failure-data.ts), and the error a loop's next tick records (tick-apply.ts).
27
+ * rejection clusters (failure-data.ts), and the error a role's next run records (run-apply.ts).
28
28
  * Sites that render the whole reason list, or intentionally omit the fallback (time-spend.ts's
29
29
  * first-reason example, review.ts's rejection event `reason`), keep their own handling. */
30
30
  export function firstReason(reasons) {
31
31
  const first = reasons?.[0];
32
32
  return typeof first === "string" ? first : NO_REASONS_GIVEN;
33
33
  }
34
- /** A count and its noun as one phrase (`plural(3, "tick")` → `3 ticks`) — the single home of
35
- * the singular/plural selection the CLI's once summary (cli/cli-run.ts), the day window's day label
34
+ /** A count and its noun as one phrase (`plural(3, "run")` → `3 runs`) — the single home of the
35
+ * singular/plural selection the CLI's once summary (cli/cli-start-summary.ts), the
36
+ * day window's day label
36
37
  * (datetime.ts), the failure digest's loss-cause lines (src/failure/failure-render.ts), and the
37
38
  * team alerts' banner titles (ui/team-alerts.ts, whose local copy this replaces), and the
38
39
  * build_stale lines' "N commit(s) ahead" (event-format.ts, src/failure/failure-state-change.ts —
39
40
  * the singular/plural wording their tests pin, once "N commit(s)" with the old literal, now "N
40
41
  * commits"/"1 commit"), and the stage self-check and dropped-attachment notes
41
- * (src/tick/tick-stage.ts, inbox/inbox-attachments.ts) all rendered inline before. `many` accepts a
42
- * whole replacement form (`plural(n, "loop is", "loops are")`) so verb-agreement titles share the
42
+ * (src/run/run-stage.ts, inbox/inbox-attachments.ts) all rendered inline before. `many` accepts a
43
+ * whole replacement form (`plural(n, "role is", "roles are")`) so verb-agreement titles share the
43
44
  * helper; the plural-by-`s` default covers regular nouns. (The GUI keeps its own JS copy in
44
45
  * gui-client.ts: a separate runtime that cannot import TypeScript.) */
45
46
  export function plural(n, one, many = `${one}s`) {
@@ -86,16 +87,16 @@ export function mainRedNotMine(sha) {
86
87
  export function pauseReasonSuffix(reason) {
87
88
  return reason ? ` — "${reason}"` : "";
88
89
  }
89
- /** The harness warning a loop crossing the error-streak threshold raises — `3 consecutive
90
- * tick failures: <reason>` — shared by the tick-side crossing (tick-finalize.ts, an error or
90
+ /** The harness warning a role crossing the error-streak threshold raises — `3 consecutive
91
+ * run failures: <reason>` — shared by the run-side crossing (run-finalize.ts, an error or
91
92
  * recovery failure) and the merge-side crossing a review rejection resolves
92
93
  * (merge-slot.ts), the two sites that build the message, so the operator-facing wording
93
94
  * their tests pin cannot drift between them. */
94
95
  export function consecutiveFailuresWarning(count, reason) {
95
96
  return `${count} runs failed in a row: ${reason}`;
96
97
  }
97
- /** The base line for an operator team pause — `team paused — role loops stop starting new
98
- * ticks` — built here so the event feed (event-format.ts, which appends its own `(director
98
+ /** The base line for an operator team pause — `team paused — roles stop starting new
99
+ * runs` — built here so the event feed (event-format.ts, which appends its own `(director
99
100
  * keeps running)` consequence) and the failure digest's Team state changes lines
100
101
  * (src/failure/failure-state-change.ts, which keeps the short form) cannot drift on the base
101
102
  * wording. `tumwater pause`'s confirmation (operator/operator-commands.ts) interleaves its
@@ -112,6 +113,10 @@ export function teamPausePhrase() {
112
113
  export function teamResumePhrase() {
113
114
  return "team resumed — roles run again";
114
115
  }
116
+ /** Appended to a resume line whose `expired: true` event says the pause's deadline lifted it
117
+ * rather than `tumwater resume` — read by the event feed (event-format.ts) and the failure
118
+ * digest (src/failure/failure-state-change.ts). */
119
+ export const PAUSE_EXPIRED_SUFFIX = " (its timed pause ran out)";
115
120
  /** The base line for a per-role pause — `role <id> paused — it starts no new runs` —
116
121
  * shared by the event feed (event-format.ts, which appends `(the rest of the team keeps running)`)
117
122
  * and the failure digest's Team state changes lines (src/failure/failure-state-change.ts, which
@@ -129,7 +134,25 @@ export function roleResumePhrase(role) {
129
134
  }
130
135
  /** The line for an orchestrator shutdown — the event feed (event-format.ts) and the failure
131
136
  * digest's Team state changes lines (src/failure/failure-state-change.ts) both render it. */
132
- export const ORCHESTRATOR_STOPPED = "tumwater stopped";
137
+ const ORCHESTRATOR_STOPPED = "tumwater stopped";
138
+ /** The orchestrator shutdown line with its `cause` (restart, once, signal, error): the event feed
139
+ * (event-format.ts) and the failure digest (src/failure/failure-state-change.ts) both render it.
140
+ * A missing or unknown cause — every event logged before the field existed — renders the bare
141
+ * ORCHESTRATOR_STOPPED. */
142
+ export function orchestratorStopPhrase(cause) {
143
+ switch (cause) {
144
+ case "restart":
145
+ return `${ORCHESTRATOR_STOPPED} to restart onto the new build`;
146
+ case "once":
147
+ return `${ORCHESTRATOR_STOPPED} — the --once round settled`;
148
+ case "signal":
149
+ return `${ORCHESTRATOR_STOPPED} on a stop signal`;
150
+ case "error":
151
+ return `${ORCHESTRATOR_STOPPED} — its polling failed`;
152
+ default:
153
+ return ORCHESTRATOR_STOPPED;
154
+ }
155
+ }
133
156
  /** A HarnessEvent's loosely typed `roles` list as a `A, B, C` phrase — the single home of the
134
157
  * array coercion and join, shared by the event feed (event-format.ts: counters_reset's scope,
135
158
  * budget_handback's handed-back list, the rate_limit_hold line) and the failure digest's state-
@@ -197,17 +220,30 @@ export function backendKindPhrase(kind) {
197
220
  return "backend failure";
198
221
  }
199
222
  }
223
+ const LEADING_CD = /^\s*cd\s+(?:"[^"]*"|'[^']*'|\S+)\s*(?:&&|;)\s*/;
224
+ function withoutLeadingCd(command) {
225
+ let rest = command;
226
+ while (LEADING_CD.test(rest))
227
+ rest = rest.replace(LEADING_CD, "");
228
+ return rest.trim() ? rest : command;
229
+ }
200
230
  /** One-line description of a tool call from its name and args — shared by live progress data
201
231
  * collection (LiveProgress.lastTool), transcript rendering, and the harness's stalled-tool-call
202
232
  * warning (src/pi/pi-stream.ts builds the label src/pi/pi-watchdogs.ts renders). Path-like keys
203
- * reduce to the file name; the other candidate keys are shown verbatim. */
233
+ * reduce to the file name; the other candidate keys are shown verbatim. A leading `cd <dir> &&`
234
+ * is dropped from a command, because the run's cwd is implied and the path would use up the
235
+ * 32-character budget. */
204
236
  export function describeToolCall(toolName, args) {
205
237
  let detail = "";
206
238
  if (isJsonObject(args)) {
207
239
  const candidate = args.path ?? args.file_path ?? args.command ?? args.cmd ?? args.pattern ?? args.url;
208
240
  if (typeof candidate === "string") {
209
241
  detail =
210
- candidate === args.path || candidate === args.file_path ? path.basename(candidate) : candidate;
242
+ candidate === args.path || candidate === args.file_path
243
+ ? path.basename(candidate)
244
+ : candidate === args.command || candidate === args.cmd
245
+ ? withoutLeadingCd(candidate)
246
+ : candidate;
211
247
  }
212
248
  }
213
249
  detail = squash(detail, 32);
@@ -218,12 +254,13 @@ export function describeToolCall(toolName, args) {
218
254
  /** The ` (+N more)` remainder suffix a capped list appends when `count` entries were omitted —
219
255
  * empty when none were, so the item fits an already-built line either way. The single home of that
220
256
  * parenthesized remainder, shared by the reviewer suite-rerun warning (review/suite-rerun.ts) and
221
- * the stage self-check's missing-path and lost-final-newline findings (tick/stage-check.ts), so the
222
- * three cannot drift on the spacing or the parenthesized form. The other remainder phrasings keep
223
- * their own wording: the failure digest's bare `+N more` role list and its `_+N more …_` lines
224
- * (failure/failure-render.ts), doctor's `and N more` (doctor/doctor-orphans.ts), the TUI alert
225
- * pointer (ui/tui/tui-frame.ts), and the review-reject reasons, whose extra clause sits inside the
226
- * parentheses (events/event-format.ts). */
257
+ * the stage self-check's missing-path and lost-final-newline findings (run/stage-path-checks.ts
258
+ * and run/stage-prose-checks.ts),
259
+ * so the three cannot drift on the spacing or the parenthesized form. The other remainder
260
+ * phrasings keep their own wording: the failure digest's bare `+N more` role list and its `_+N
261
+ * more …_` lines (failure/failure-render.ts), doctor's `and N more` (doctor/doctor-orphans.ts),
262
+ * the TUI alert pointer (ui/tui/tui-frame.ts), and the review-reject reasons, whose extra clause
263
+ * sits inside the parentheses (events/event-format.ts). */
227
264
  export function moreSuffix(count) {
228
265
  return count > 0 ? ` (+${count} more)` : "";
229
266
  }
@@ -55,7 +55,7 @@ export function didYouMean(suggestion) {
55
55
  * `input` against `candidates`, the empty string when nothing is close enough to suggest. The
56
56
  * unknown-X error sites (roles.ts's unknownRoleMessage, config-write.ts's
57
57
  * unknownConfigKeyError and its role-field error, config-field-checks.ts's checkKnownKeys,
58
- * checkKnownRoleId and its thinking-level problem, config-validation.ts's backend problem,
58
+ * checkKnownRoleId and its thinking-level problem, config-validation-agents.ts's backend problem,
59
59
  * gui/gui-args.ts's rejectBadRole, init-templates.ts's
60
60
  * unknownTemplateError) render the hint
61
61
  * through this one composition so the two-step pairing cannot drift; callers still print the
@@ -1,5 +1,5 @@
1
1
  /** Shared text-shaping helpers for human-facing one-line text — the observability layer's
2
- * display labels (live progress, transcripts), tick commit subjects and body fields, and
2
+ * display labels (live progress, transcripts), run commit subjects and body fields, and
3
3
  * build-check reason lines: whitespace collapsing, ellipsis truncation, and plain-decimal
4
4
  * integer parsing. (Number, money, and hash formats — compactTokens, shortSha, usd, usdCap —
5
5
  * live in format.ts; local date/time formatting and calendar-day arithmetic live in
@@ -34,7 +34,7 @@ export function stripTerminalControls(s) {
34
34
  * The one home of the "a torn or hand-edited field reads as a visible placeholder, never
35
35
  * String(undefined)'s literal 'undefined'" coercion the event feed's free-text fields and
36
36
  * shortSha apply. Numbers stay admitted because events legitimately carry numeric fields
37
- * (a tick number, a pid) that a display line renders as text. */
37
+ * (a run number, a pid) that a display line renders as text. */
38
38
  export function textOr(value, fallback = "?") {
39
39
  if (typeof value === "string")
40
40
  return value;
@@ -49,6 +49,28 @@ export function textOr(value, fallback = "?") {
49
49
  export function errorMessage(err) {
50
50
  return err instanceof Error ? err.message : String(err);
51
51
  }
52
+ /** The first line of `errorMessage(err)`, trimmed: the one-line form a status reason carries
53
+ * when git's multi-line stderr would swamp it. Used by the fast-forward and archive-commit
54
+ * failure reasons. */
55
+ export function errorFirstLine(err) {
56
+ return errorMessage(err).trim().split("\n")[0];
57
+ }
58
+ /** `s` with every regex metacharacter escaped, so it can be dropped into a `RegExp` and match
59
+ * only its literal text — the one spelling of the standard `replace(/[.*+?^${}()|[\]\\]/g,
60
+ * "\\$&")` escape, previously repeated at every site that turned an untrusted string (a
61
+ * provider id, a series/held-tag prefix, a role-note token, a config fragment) into a
62
+ * word-bounded or anchored match. Careful callers build `new RegExp(escapeRegExp(s))` and then
63
+ * append the boundaries (`\\b`, `^`, `)`) they want on top; this helper escapes only, never
64
+ * anchors, so the caller keeps control of the surrounding pattern. The presentation-only
65
+ * contract is inherited from the module: node built-ins alone, so any layer can import it. */
66
+ export function escapeRegExp(s) {
67
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
68
+ }
69
+ /** The first line of `body` that begins with `prefix` once trimmed, trimmed; undefined when no
70
+ * line does. Used by the backlog's hold-note text and the answered-question preview. */
71
+ export function firstTrimmedLineStartingWith(body, prefix) {
72
+ return body.split("\n").map((l) => l.trim()).find((l) => l.startsWith(prefix));
73
+ }
52
74
  /** The error message's " (got …)" value suffix, spelled one way everywhere: JSON.stringify for
53
75
  * the value's debug form, wrapped in the established parenthetical. The human-facing
54
76
  * validation errors that echo back a rejected value with this suffix — the CLI flag
@@ -57,12 +79,11 @@ export function errorMessage(err) {
57
79
  * this one helper, so the "got" wording and its quoting cannot drift between surfaces.
58
80
  * (Deliberately different suffixes: config-field-checks's show() truncates long config values
59
81
  * before the parenthetical, and cli/cli-args.ts's duration cap renders the value through
60
- * durationLabel — neither fits a plain stringify.) Undefined values are the
61
- * caller's call: append the suffix only for a value that is actually present (see
62
- * gui/gui-args's string/boolean validators), since ` (got undefined)` would name a field the
63
- * request simply did not carry. */
82
+ * durationLabel — neither fits a plain stringify.) A missing value
83
+ * (undefined) appends nothing, since ` (got undefined)` would name a field the request never
84
+ * carried; null still prints ` (got null)`, a value the request did carry. */
64
85
  export function gotSuffix(v) {
65
- return ` (got ${JSON.stringify(v)})`;
86
+ return v === undefined ? "" : ` (got ${JSON.stringify(v)})`;
66
87
  }
67
88
  /** Is `v` a string with non-whitespace content — the "field carries something usable" guard every
68
89
  * structural read of an unknown-typed JSON field applies (a blank or whitespace-only string reads
@@ -70,7 +91,7 @@ export function gotSuffix(v) {
70
91
  * narrows `v` to `string` for the code that consumes it — the one spelling shared by
71
92
  * build/build-check-detect.ts's check-script/command/cwd/gateCommand reads, config validation's
72
93
  * thinking-level check and model-field guards (tier-map values, the legacy model/ fallback selector
73
- * and its legacy provider, exempt paths, custom-loop tasks), pi-event-line.ts's tool-result content
94
+ * and its legacy provider, exempt paths, custom-role tasks), pi-event-line.ts's tool-result content
74
95
  * test, quiet-hours.ts's per-role window lookup, and inbox-attachments.ts's image-name check, so
75
96
  * the blank-means-unset rule cannot drift per call site. Callers where a bare empty string means
76
97
  * unset rather than blank text (notify.ts, version.ts, inbox-cancel.ts) keep their own `=== ""`
@@ -79,6 +100,27 @@ export function gotSuffix(v) {
79
100
  export function isNonBlankString(v) {
80
101
  return typeof v === "string" && v.trim() !== "";
81
102
  }
103
+ /** Split a comma list into its trimmed, non-empty pieces, in order — the one spelling of the
104
+ * split-trim-drop-blanks step shared by the `--role` list flags (cli-args.ts and
105
+ * operator-commands.ts), `logs --type`, the history `--result` and `--model` filters, and the
106
+ * GUI's `role` and `model` query parameters. Deduplication and the no-piece-left error stay
107
+ * with each caller. */
108
+ export function splitCommaList(raw) {
109
+ return raw.split(",").map((p) => p.trim()).filter((p) => p !== "");
110
+ }
111
+ /** splitCommaList with repeated pieces dropped (first occurrence kept) — the `--model` history
112
+ * filter and the GUI's `role` and `model` query parameters, each of which treats a repeat as
113
+ * the same selector. */
114
+ export function splitCommaListUnique(raw) {
115
+ return [...new Set(splitCommaList(raw))];
116
+ }
117
+ /** A value or list of `T` as a list: a bare `T` is one item, an array stays itself. The
118
+ * single home of the string-or-list normalization (a `--role` selector is either one id or a
119
+ * comma-split list): report-fold.ts's scopeBases/roleSum/narrowToRole and history-data.ts's
120
+ * role filter each spelled the `typeof x === "string" ? [x] : x` wrap by hand before. */
121
+ export function asList(value) {
122
+ return typeof value === "string" ? [value] : value;
123
+ }
82
124
  /** Drop the blank lines at the start of `lines`, in place — the one home of the
83
125
  * leading-blank trim loop shared by the backlog writer's section-content rebuild and
84
126
  * `questions answer`'s tail slice (the lines after the moved answer). Blank means
@@ -112,6 +154,20 @@ function isLowSurrogate(code) {
112
154
  export function cutSplitsSurrogatePair(s, i) {
113
155
  return i > 0 && i < s.length && isHighSurrogate(s.charCodeAt(i - 1)) && isLowSurrogate(s.charCodeAt(i));
114
156
  }
157
+ /** Join the first `max` of `items` with `sep`, then `<sep>+N more` when others were left out. */
158
+ export function joinWithMore(items, max, sep = ", ") {
159
+ const shown = items.slice(0, max).join(sep);
160
+ return items.length > max ? `${shown}${sep}+${items.length - max} more` : shown;
161
+ }
162
+ /** Join the non-empty entries of `parts` with `sep`, dropping empty strings and nullish or
163
+ * falsy entries — the shape of a message assembled from optional fragments (a doctor verdict
164
+ * line, a commit body, a provider/model token, an error note). Each call site previously
165
+ * wrote `[a, ...b].filter(Boolean).join(sep)` by hand; feeding a fragment that is unset (an
166
+ * `undefined`/`false` from a guard) is fine, no `cond ? x : ""` placeholder needed. Returns
167
+ * the empty string when nothing is non-empty. */
168
+ export function joinParts(parts, sep = "; ") {
169
+ return parts.filter((p) => Boolean(p)).join(sep);
170
+ }
115
171
  /** Shorten `s` to at most `max` characters: unchanged when it fits, otherwise cut to
116
172
  * `max - 1`, drop any trailing space the cut may have left behind, and append an ellipsis.
117
173
  * The result is never longer than `max`. A cut that would split a surrogate pair (an astral
@@ -140,8 +196,8 @@ export function squash(s, max) {
140
196
  /** Text capped for injection into a prompt: past `max` characters it is cut and given a visible
141
197
  * `…[<label> truncated at <max> chars]` note naming what was capped and how much survived, so
142
198
  * the loss is never silent. The single home of that marker format — the PRINCIPLES.md cap
143
- * (principles.ts's readPrinciples), the initial-prompt backstop (brief.ts's extractPrompt), and
144
- * the role-notebook read (tick-prompt.ts) all render through it, so the defensive caps cannot
199
+ * (principles.ts's readPrinciples), the brief text backstop (brief.ts's extractPrompt), and
200
+ * the role-notes read (run-prompt.ts) all render through it, so the defensive caps cannot
145
201
  * drift apart in wording. A plain slice at `max` (not truncate's ellipsis-and-trim cut): the
146
202
  * marker carries the ellipsis, and the cut boundary must stay a stable, predictable prefix of
147
203
  * the original text. A cut that would split a surrogate pair (an astral character such as
@@ -189,11 +245,20 @@ export function parseNonNegativeInt(raw) {
189
245
  }
190
246
  /** The shared over-long-text message: `<subject> is <n> chars — shorten it to at most <max>`,
191
247
  * with `tail` naming where the text rides once accepted. The one home of that phrasing, used
192
- * by init's seed check (init.ts, "the initial prompt … every tick's prefill"), inbox-submit's
193
- * promptLengthProblem ("the prompt … the <role> tick's prefill"), and config-validation's
194
- * customRoles task and roles instructions caps (both "every tick's prefill"), so the four
248
+ * by init's seed check (init.ts, "the brief text … every run's prefill"), inbox-submit's
249
+ * promptLengthProblem ("the prompt … the <role> run's prefill"), and config-validation's
250
+ * customRoles task and roles instructions caps (both "every run's prefill"), so the four
195
251
  * too-long messages cannot drift apart in shape. Callers keep their own subject, cap, and
196
252
  * tail; this owns only the sentence around them. */
197
253
  export function tooLongMessage(subject, n, max, tail) {
198
254
  return `${subject} is ${n} chars — shorten it to at most ${max}: ${tail}`;
199
255
  }
256
+ /** Each distinct path-shaped token in `text`, in first-appearance order: at least one slash and
257
+ * a final `.<ext>` segment. Scanned inside the text (not whitespace-split) so surrounding
258
+ * backticks, brackets, and sentence punctuation fall away with the match. Shared by the staging
259
+ * self-check's added-nonexistent-path finding (run/stage-path-checks.ts) and run-prompt's
260
+ * role-note missing-path scan (run/run-prompt.ts), so the two agree on what reads as a path. */
261
+ export function pathTokens(text) {
262
+ const tokenRe = /[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+\.[A-Za-z0-9]+/g;
263
+ return [...new Set([...text.matchAll(tokenRe)].map((m) => m[0]))];
264
+ }