tumwater 0.1.2 → 0.2.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 (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
package/dist/src/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // vocabulary: plain
2
3
  /** The CLI's entry point and command dispatcher — the `bin` target package.json points at
3
4
  * (dist/src/cli.js). main() runs the Node-floor gate from package.json's `engines.node`, answers a
4
5
  * `--help` topic before any command parses arguments or gates on a ready repo, resolves the repo
@@ -7,23 +8,39 @@
7
8
  * statically loads only the argument parsing, output, repo gate and help it shares, so
8
9
  * `tumwater report` never pays for the orchestrator or ink, and `tumwater logs` never pays for
9
10
  * the status collectors. A process that starts, runs one command and exits (the CLI as the
10
- * fleet's agents and the test suite call it, ~700 spawns a suite run) otherwise compiled all
11
+ * team's agents and the test suite call it, ~700 spawns a suite run) otherwise compiled all
11
12
  * 169 modules every time. The work itself lives in the command
12
13
  * modules; this file owns the shared sequence, including each case's flag rejection and its
13
14
  * ready-repo gate. An error escaping main() funnels through its catch to fail(errorMessage(err)),
14
15
  * the CLI's one error path. */
15
16
  import { parseCountFlag, } from "./cli/cli-args.js";
16
- import { grepFlagSpec, GUI_FLAG_SPECS, JSON_FLAG, LAST_FLAG, N_FLAG, rejectUnknownArgs, resultFlagSpec, ROLE_FLAG, RUN_FLAG_SPECS, sinceFlagSpec, FORCE_FLAG, } from "./cli/cli-flag-specs.js";
17
+ import { grepFlagSpec, GUI_FLAG_SPECS, JSON_FLAG, LAST_FLAG, N_FLAG, rejectUnknownArgs, resultFlagSpec, ROLE_FLAG, RUN_FLAG_SPECS, sinceFlagSpec, STAT_FLAG, SUMMARY_FLAG, COUNT_FLAG, typeFlagSpec, DRY_RUN_FLAG, FORCE_FLAG, STOP_CANCEL_FLAG, STOP_IN_FLAG, REPROBE_FLAG, PROBLEMS_FLAG, } from "./cli/cli-flag-specs.js";
17
18
  import { fail, say, sayJsonOrRender } from "./cli/cli-output.js";
18
- import { parsePromptArgs, peelPositionals } from "./cli/cli-command-args.js";
19
+ import { historyTrailForm, parsePromptArgs, peelPositionals } from "./cli/cli-command-args.js";
19
20
  import { requireReadyRepo } from "./cli/cli-repo-gate.js";
20
21
  import { repoToplevel } from "./git/git.js";
21
22
  import { didYouMean } from "./text/suggest.js";
22
23
  import { errorMessage, gotSuffix, stripTerminalControls } from "./text/text.js";
24
+ import { typoSuffix } from "./text/suggest.js";
25
+ import { completionConfigKeys, completionRoleIds, completionScript, SUPPORTED_SHELLS } from "./cli/completion.js";
23
26
  import { HELP, helpTopic, helpTopicForArgs, suggestCommand } from "./cli/help.js";
24
27
  import { PACKAGE_JSON, nodeFloorProblem, packageEnginesNode, packageVersion } from "./version.js";
25
28
  // The CLI's help text and its per-command topic parser live in help.ts — importing cli.ts
26
29
  // would run main(), so tests pin the topics against help.ts directly.
30
+ /** One run's trail, shared by `history <role> <n>` and its hidden alias `tick` so the two cannot
31
+ * drift. Positional-first: the <role> <n> tokens peel off and the remaining flags go through
32
+ * rejectUnknownArgs (only --json, --last, --stat), so a listing flag such as --role is refused.
33
+ * Arity is gated before the ready-repo check so a malformed call fails with its usage in any
34
+ * directory; cmdTick owns the value validation and the --last/numbered rivals. */
35
+ async function runTrailCommand(root, args) {
36
+ const { cmdTick, TICK_USAGE } = await import("./tick/tick-detail.js");
37
+ const { positionals, rest } = peelPositionals(args);
38
+ rejectUnknownArgs("history", rest, [JSON_FLAG, LAST_FLAG, STAT_FLAG]);
39
+ if (positionals.length < 1 || positionals.length > 2)
40
+ fail(`usage: ${TICK_USAGE}`);
41
+ await requireReadyRepo(root);
42
+ await cmdTick(root, positionals, rest.includes("--json"), rest.includes("--last"), rest.includes("--stat"));
43
+ }
27
44
  async function main() {
28
45
  const [, , command, ...args] = process.argv;
29
46
  // The Node-floor gate, before any command does work: package.json's engines spec is the
@@ -55,14 +72,15 @@ async function main() {
55
72
  const root = (await repoToplevel(process.cwd())) ?? process.cwd();
56
73
  switch (command) {
57
74
  case "init":
58
- // Imported lazily: cli/cli-run.ts pulls the whole fleet-booting subgraph (orchestrator,
75
+ // Imported lazily: cli/cli-run.ts pulls the whole team-booting subgraph (orchestrator,
59
76
  // supervisor, redeploy, launch-services — the run path's machinery), and every other
60
77
  // command's spawn pays module compilation for it unless the dispatch defers that load
61
- // to the commands that actually boot or serve a fleet.
78
+ // to the commands that actually boot or serve a team.
62
79
  await (await import("./cli/cli-run.js")).cmdInit(root, args);
63
80
  break;
81
+ case "start":
64
82
  case "run":
65
- rejectUnknownArgs("run", args, RUN_FLAG_SPECS);
83
+ rejectUnknownArgs("start", args, RUN_FLAG_SPECS);
66
84
  await (await import("./cli/cli-run.js")).cmdRun(root, args);
67
85
  break;
68
86
  case "tui":
@@ -131,26 +149,37 @@ async function main() {
131
149
  case "doctor": {
132
150
  // No requireReadyRepo gate: doctor's job is to report WHY the environment isn't ready,
133
151
  // so it must run outside a git repo and print fail lines rather than throwing.
134
- rejectUnknownArgs("doctor", args, [JSON_FLAG]);
152
+ rejectUnknownArgs("doctor", args, [JSON_FLAG, REPROBE_FLAG, PROBLEMS_FLAG]);
135
153
  const { runDoctor } = await import("./doctor/doctor.js");
136
- const { renderDoctor } = await import("./doctor/doctor-render.js");
137
- const report = await runDoctor(root);
154
+ const { renderDoctor, renderDoctorProblems, problemsOnly } = await import("./doctor/doctor-render.js");
155
+ // The terminal probes unclaimed-verified models by default (7-day cache); --reprobe
156
+ // forces every claimed model. The TUI/GUI call runDoctor with the default "never", so a
157
+ // refresh never spends a model call.
158
+ const probeImages = args.includes("--reprobe") ? "all" : "missing";
159
+ const report = await runDoctor(root, undefined, undefined, probeImages);
138
160
  // --json prints the collector's own payload (the DoctorReport object), not a re-parse of
139
161
  // the render — the `report --json` precedent. The exit-code contract below holds in both
140
162
  // forms: 1 when any check fails, 0 otherwise; warnings never fail the exit.
141
- sayJsonOrRender(args, report, renderDoctor);
163
+ // --problems keeps the full report for the render (it counts the hidden ok rows) and
164
+ // hands --json the filtered one; the exit code below reads the full report either way.
165
+ if (args.includes("--problems"))
166
+ sayJsonOrRender(args, args.includes("--json") ? problemsOnly(report) : report, renderDoctorProblems);
167
+ else
168
+ sayJsonOrRender(args, report, renderDoctor);
142
169
  if (report.checks.some((c) => c.level === "fail"))
143
170
  process.exitCode = 1; // Warnings never fail the exit.
144
171
  break;
145
172
  }
146
173
  case "logs": {
147
- const { cmdLogs, GREP_VALUE_ERROR } = await import("./cli/log-commands.js");
174
+ const { cmdLogs, GREP_VALUE_ERROR, TYPE_VALUE_ERROR } = await import("./cli/log-commands.js");
148
175
  rejectUnknownArgs("logs", args, [
149
176
  { names: ["-f", "--follow"] },
150
177
  N_FLAG,
151
178
  sinceFlagSpec("logs --since"),
152
179
  grepFlagSpec(GREP_VALUE_ERROR),
180
+ typeFlagSpec(TYPE_VALUE_ERROR),
153
181
  JSON_FLAG,
182
+ COUNT_FLAG,
154
183
  ROLE_FLAG,
155
184
  { names: ["--prompt"] },
156
185
  ]);
@@ -165,6 +194,10 @@ async function main() {
165
194
  break;
166
195
  case "history": {
167
196
  const { cmdHistory, HISTORY_GREP_VALUE_ERROR, HISTORY_RESULT_VALUE_ERROR } = await import("./history/history.js");
197
+ if (historyTrailForm(args)) {
198
+ await runTrailCommand(root, args);
199
+ break;
200
+ }
168
201
  rejectUnknownArgs("history", args, [
169
202
  N_FLAG,
170
203
  sinceFlagSpec("history --since"),
@@ -172,30 +205,15 @@ async function main() {
172
205
  resultFlagSpec(HISTORY_RESULT_VALUE_ERROR),
173
206
  JSON_FLAG,
174
207
  ROLE_FLAG,
208
+ SUMMARY_FLAG,
175
209
  ]);
176
210
  await requireReadyRepo(root);
177
211
  await cmdHistory(root, args);
178
212
  break;
179
213
  }
180
- case "tick": {
181
- const { cmdTick, TICK_USAGE } = await import("./tick/tick-detail.js");
182
- // Positional-first command (the role command's pattern): the <role> <n> tokens peel off,
183
- // the flags that remain go through rejectUnknownArgs (only --json is admitted), and
184
- // cmdTick owns the arity and value validation — a missing role, an unknown id, a
185
- // non-positive or non-numeric n, or a stray extra positional fails there with the usage.
186
- const { positionals, rest } = peelPositionals(args);
187
- rejectUnknownArgs("tick", rest, [JSON_FLAG, LAST_FLAG]);
188
- // Arity before the ready-repo gate (the config subcommands' precedent): a malformed
189
- // invocation fails with its usage no matter the directory. cmdTick keeps the guard too —
190
- // in-process callers reach it without this dispatcher. One positional is the --last
191
- // form (<role>), two is the numbered pair (<role> <n>); cmdTick picks the arity by the
192
- // flag, so the gate stays loose here and the rivals check lives there.
193
- if (positionals.length < 1 || positionals.length > 2)
194
- fail(`usage: ${TICK_USAGE}`);
195
- await requireReadyRepo(root);
196
- await cmdTick(root, positionals, rest.includes("--json"), rest.includes("--last"));
214
+ case "tick": // vocabulary-ok: command name stays
215
+ await runTrailCommand(root, args);
197
216
  break;
198
- }
199
217
  case "diff":
200
218
  await (await import("./cli/cli-query-commands.js")).cmdDiff(root, args);
201
219
  break;
@@ -205,9 +223,19 @@ async function main() {
205
223
  case "backlog":
206
224
  await (await import("./cli/cli-query-commands.js")).cmdBacklog(root, args);
207
225
  break;
226
+ case "archive":
227
+ rejectUnknownArgs("archive", args, []);
228
+ await requireReadyRepo(root);
229
+ await (await import("./backlog/backlog-cli.js")).cmdArchive(root);
230
+ break;
208
231
  case "bug": {
209
- const { OPERATOR_FILE_KINDS, bugTitleOf, bugWakeText, cancelAndAnnounce, cancelBug, editAndAnnounce, editBug, editBugFromText, fileBug, fileBugFromText, fileAndAnnounce, listAndAnnounce } = await import("./backlog/backlog-write.js");
232
+ const { OPERATOR_FILE_KINDS, bugWakeText, cancelBug, moveEntryToTop, releaseEntry, editBug, editBugFromText, fileBug, fileBugFromText } = await import("./backlog/backlog-write.js");
233
+ const { bugTitleOf, cancelAndAnnounce, releaseAndAnnounce, topAndAnnounce, editAndAnnounce, fileAndAnnounce, listAndAnnounce, showAndAnnounce } = await import("./backlog/backlog-cli.js");
210
234
  const usage = 'tumwater bug "<symptom>"';
235
+ if (args.includes("--show")) {
236
+ showAndAnnounce(root, args, "bug");
237
+ break;
238
+ }
211
239
  if (args.includes("--list")) {
212
240
  listAndAnnounce(root, args, "bug", "BUGS.md", "Open");
213
241
  break;
@@ -220,12 +248,25 @@ async function main() {
220
248
  await cancelAndAnnounce(root, args, "bug", (n) => cancelBug(root, n));
221
249
  break;
222
250
  }
251
+ if (args.includes("--top")) {
252
+ await topAndAnnounce(root, args, "bug", (n) => moveEntryToTop(root, "bug", n));
253
+ break;
254
+ }
255
+ if (args.includes("--release")) {
256
+ await releaseAndAnnounce(root, args, "bug", (n) => releaseEntry(root, "bug", n), OPERATOR_FILE_KINDS.bug.role);
257
+ break;
258
+ }
223
259
  await fileAndAnnounce(root, args, "bug", (positionals) => fileBug(root, positionals.join(" "), usage), OPERATOR_FILE_KINDS.bug.role, bugWakeText, bugTitleOf, (text) => fileBugFromText(root, text, usage));
224
260
  break;
225
261
  }
226
262
  case "plan": {
227
- const { OPERATOR_FILE_KINDS, cancelAndAnnounce, cancelPlan, editAndAnnounce, editPlan, editPlanFromText, filePlan, filePlanFromText, fileAndAnnounce, listAndAnnounce, planTitleOf, planWakeText } = await import("./backlog/backlog-write.js");
263
+ const { OPERATOR_FILE_KINDS, cancelPlan, moveEntryToTop, releaseEntry, editPlan, editPlanFromText, filePlan, filePlanFromText, planWakeText } = await import("./backlog/backlog-write.js");
264
+ const { cancelAndAnnounce, releaseAndAnnounce, topAndAnnounce, editAndAnnounce, fileAndAnnounce, listAndAnnounce, showAndAnnounce, planTitleOf } = await import("./backlog/backlog-cli.js");
228
265
  const usage = 'tumwater plan "<title>" [body...]';
266
+ if (args.includes("--show")) {
267
+ showAndAnnounce(root, args, "plan");
268
+ break;
269
+ }
229
270
  if (args.includes("--list")) {
230
271
  listAndAnnounce(root, args, "plan", "PLANS.md", "Planned");
231
272
  break;
@@ -238,12 +279,23 @@ async function main() {
238
279
  await cancelAndAnnounce(root, args, "plan", (n) => cancelPlan(root, n));
239
280
  break;
240
281
  }
282
+ if (args.includes("--top")) {
283
+ await topAndAnnounce(root, args, "plan", (n) => moveEntryToTop(root, "plan", n));
284
+ break;
285
+ }
286
+ if (args.includes("--release")) {
287
+ await releaseAndAnnounce(root, args, "plan", (n) => releaseEntry(root, "plan", n), OPERATOR_FILE_KINDS.plan.role);
288
+ break;
289
+ }
241
290
  await fileAndAnnounce(root, args, "plan", (positionals) => filePlan(root, positionals[0] ?? "", positionals.slice(1).join(" "), usage), OPERATOR_FILE_KINDS.plan.role, planWakeText, planTitleOf, (text) => filePlanFromText(root, text, usage));
242
291
  break;
243
292
  }
244
293
  case "questions":
245
294
  await (await import("./cli/cli-query-commands.js")).cmdQuestions(root, args);
246
295
  break;
296
+ case "brief":
297
+ await (await import("./cli/brief-command.js")).cmdBrief(root, args);
298
+ break;
247
299
  case "role":
248
300
  await (await import("./cli/cli-query-commands.js")).cmdRole(root, args);
249
301
  break;
@@ -261,33 +313,62 @@ async function main() {
261
313
  await (await import("./inbox/prompt-commands.js")).cmdPrompt(root, args);
262
314
  break;
263
315
  case "reset-counters":
264
- // Fall through: the six marker commands share one guard+dispatch shape — unknown-args
316
+ // Fall through: the seven marker commands share one guard+dispatch shape — unknown-args
265
317
  // rejection against the command's flag set, the ready-repo gate, then the
266
- // operator-commands core — so runMarkerCommand holds it once instead of six copies
318
+ // operator-commands core — so runMarkerCommand holds it once instead of seven copies
267
319
  // drifting. The case labels here are exactly MarkerCommand's members, which is what
268
320
  // makes the cast below exhaustive.
269
321
  case "wake":
270
322
  case "abort":
271
323
  case "pause":
272
324
  case "resume":
273
- case "reclaim": {
325
+ case "reclaim":
326
+ case "restart": {
274
327
  await (await import("./cli/cli-marker-commands.js")).runMarkerCommand(root, command, args);
275
328
  break;
276
329
  }
277
330
  case "retire": {
278
331
  // Not a marker command (it acts immediately, not via a request file), but it shares the
279
332
  // marker commands' gate shape: unknown-args rejection, then the ready-repo gate.
280
- rejectUnknownArgs("retire", args, [ROLE_FLAG, FORCE_FLAG, JSON_FLAG]);
333
+ rejectUnknownArgs("retire", args, [ROLE_FLAG, FORCE_FLAG, DRY_RUN_FLAG, JSON_FLAG]);
281
334
  await requireReadyRepo(root);
282
335
  await (await import("./operator/operator-commands.js")).cmdRetire(root, args);
283
336
  break;
284
337
  }
338
+ case "revert": {
339
+ // Acts immediately on the primary checkout under the primary-worktree lock; the single
340
+ // positional (the commit to undo) is validated inside cmdRevert.
341
+ await requireReadyRepo(root);
342
+ await (await import("./operator/operator-commands.js")).cmdRevert(root, args);
343
+ break;
344
+ }
285
345
  case "stop": {
286
- // No flags: stop is a bare signal to the recorded orchestrator pid, so any argument is
287
- // a mistake and fails before a signal is ever sent.
288
- rejectUnknownArgs("stop", args, []);
346
+ // Bare stop signals the recorded orchestrator pid; `--in`/`--cancel` schedule or drop a
347
+ // deferred stop. Anything else is a mistake and fails before a signal is ever sent.
348
+ rejectUnknownArgs("stop", args, [STOP_IN_FLAG, STOP_CANCEL_FLAG]);
289
349
  await requireReadyRepo(root);
290
- await (await import("./operator/operator-commands.js")).cmdStop(root);
350
+ await (await import("./operator/operator-commands.js")).cmdStop(root, args);
351
+ break;
352
+ }
353
+ case "completion": {
354
+ // Works outside a repo: no requireReadyRepo.
355
+ if (args.length === 1 && args[0] === "--roles") {
356
+ for (const id of completionRoleIds(root))
357
+ say(id);
358
+ break;
359
+ }
360
+ if (args.length === 1 && args[0] === "--config-keys") {
361
+ for (const key of completionConfigKeys(root))
362
+ say(key);
363
+ break;
364
+ }
365
+ const shell = args[0];
366
+ if (args.length !== 1 || !SUPPORTED_SHELLS.includes(shell))
367
+ // A single near-miss shell word gets the same typoSuffix `logs --type`/`history --result`
368
+ // give, so `completion zs` reads "did you mean `zsh`?" while a far-off value (`tcsh`) and
369
+ // a wrong arity (none or several words, typed shown by gotSuffix) stay hint-free.
370
+ fail(`completion takes one shell: bash, zsh, or fish${gotSuffix(args.length > 1 ? args[1] : shell)}${args.length === 1 ? typoSuffix(shell ?? "", SUPPORTED_SHELLS) : ""}`);
371
+ say(completionScript(shell));
291
372
  break;
292
373
  }
293
374
  case "version":
@@ -301,7 +382,7 @@ async function main() {
301
382
  // raw stack trace, and a non-string version fails instead of printing "undefined".
302
383
  const { version, problem } = packageVersion(PACKAGE_JSON);
303
384
  if (problem !== undefined || version === undefined)
304
- fail(problem ?? "package.json carries no version field (the running harness's install looks broken)");
385
+ fail(problem ?? "package.json carries no version field (the running tumwater install looks broken)");
305
386
  say(version);
306
387
  break;
307
388
  }
@@ -4,8 +4,8 @@
4
4
  * utilities module. */
5
5
  /** Group items by a key, preserving first-seen key order and input order within each group.
6
6
  * The single home of the "append to a Map of arrays" step, shared by the event bucketers
7
- * (history-data.ts's bucketLandingEvents, failure/time-spend.ts's rejected-by-loop pass) and
8
- * the fleet-hold poll's observations-by-provider grouping (fleet-polls.ts's pollFleetHold), so
7
+ * (history-data.ts's bucketMergeEvents, failure/time-spend.ts's rejected-by-loop pass) and
8
+ * the team-hold poll's observations-by-provider grouping (team-polls.ts's pollTeamHold), so
9
9
  * those sites cannot drift on how a missing key is seeded. Returns a fresh Map; the input is
10
10
  * never mutated. */
11
11
  export function groupBy(items, key) {
@@ -18,8 +18,8 @@ export function groupBy(items, key) {
18
18
  /** Return `map`'s value for `key`, creating and storing `make()`'s result first when the key is
19
19
  * absent — the single home of the `map.get(key) ?? fresh; …; map.set(key, value)` step, shared
20
20
  * by the failure digest's per-role stats, outcome, and prompt-token accumulators (failure-data.ts),
21
- * its time-and-spend per-role row (time-spend.ts), the fleet hold's provider+kind grouping
22
- * (fleet-hold.ts), the error-storm reducer's roles-by-key set (src/fleet/error-storm.ts), and
21
+ * its time-and-spend per-role row (time-spend.ts), the team hold's provider+kind grouping
22
+ * (team-hold.ts), the error-storm reducer's roles-by-key set (src/team/error-storm.ts), and
23
23
  * `groupBy` above, so those sites cannot drift on how a missing key is seeded. `make` runs only
24
24
  * when the key is absent. A stored value of `undefined` reads as absent, matching the `??` idiom
25
25
  * this replaces; the callers above never store one. */
@@ -33,9 +33,12 @@ export function getOrCreate(map, key, make) {
33
33
  }
34
34
  /** Increment a Map counter, seeding a missing key at 1 — the single home of the
35
35
  * `map.set(k, (map.get(k) ?? 0) + 1)` step, shared by the failure-spread kind tally
36
- * (src/fleet/failure-spread.ts), the diff multiset's line tally (landing/landing-diff.ts), the
37
- * section-title tally (backlog/backlog-structure.ts), and the once-summary outcome tally
38
- * (cli/cli-run.ts), so those sites cannot drift on how a missing key is seeded. */
36
+ * (src/team/failure-spread.ts), the diff multiset's line tally (merge/merge-diff.ts), the
37
+ * section-title tally (backlog/backlog-structure.ts), the once-summary outcome tally
38
+ * (cli/cli-run.ts), the logs' per-type tally (cli/log-commands.ts), the flaky-check tally
39
+ * (doctor/doctor-checks.ts), and the multi-runner base tally
40
+ * (orchestrator/orchestrator-scheduling.ts), so those sites cannot drift on how a missing key is
41
+ * seeded. */
39
42
  export function increment(map, key) {
40
43
  map.set(key, (map.get(key) ?? 0) + 1);
41
44
  }
@@ -44,7 +47,7 @@ export function increment(map, key) {
44
47
  * step (the seed matters because `noUncheckedIndexedAccess` reads every entry as possibly
45
48
  * undefined), shared by the failure digest's outcome tally per role (failure-data.ts), the
46
49
  * usage report's window role totals (report-render.ts), and its per-day fold's tick and cost
47
- * tallies (report-data.ts), so those sites cannot drift on how a missing key is seeded. The
50
+ * tallies (report-fold.ts), so those sites cannot drift on how a missing key is seeded. The
48
51
  * test duration reporter (test/test-durations-reporter.ts) keeps its own copy: it imports no
49
52
  * tumwater module by design, since node --test loads it into its own parent process.
50
53
  *
@@ -56,22 +59,69 @@ export function addTo(record, key, amount) {
56
59
  const current = Object.hasOwn(record, key) ? record[key] : 0;
57
60
  record[key] = (current ?? 0) + amount;
58
61
  }
62
+ /** Set `key` to `value` in a bounded memo `cache`, moving an existing key to the LRU tail
63
+ * (delete + re-set, so a re-read at the cap does not evict itself) and, when the map would
64
+ * exceed `max` entries, dropping the oldest (first-inserted) one. The single home of the
65
+ * bounded-memo discipline — the `delete(key); set(key, …)` refresh and the
66
+ * `keys().next().value` oldest-eviction — shared by the report fold (report-fold.ts's
67
+ * reportFoldCache), the failure digest (failure-data.ts's failureDigestCache), and the
68
+ * baseline verdict cache (main-baseline.ts's baselineCache), so those sites cannot drift on
69
+ * how a hit is refreshed or an over-cap cache drained. After any call the map holds at most
70
+ * `max` entries. */
71
+ export function memoSetLru(cache, key, value, max) {
72
+ if (cache.has(key))
73
+ cache.delete(key);
74
+ if (cache.size >= max) {
75
+ const oldest = cache.keys().next().value;
76
+ if (oldest !== undefined)
77
+ cache.delete(oldest);
78
+ }
79
+ cache.set(key, value);
80
+ }
59
81
  /** One deterministic ranking rule: count descending, then key ascending, so equal counts always
60
82
  * order the same way. These two helpers are the single home of that rule; every surface that
61
- * ranks counters calls one of them rather than re-implementing the comparator —
83
+ * ranks counters by count calls one of them rather than re-implementing the comparator —
62
84
  * `rankCountEntries` serves the `[key, count]` entry shape (src/report/report-render.ts's
63
- * rankedRoleMap, src/fleet/failure-spread.ts's strongest-kind pick) and `rankByCount` serves
85
+ * rankedRoleMap, src/team/failure-spread.ts's strongest-kind pick) and `rankByCount` serves
64
86
  * callers whose counts are fields or computations on other shapes
65
- * (src/failure/failure-cluster.ts's clusters, src/fleet/error-storm.ts's strongest-cause pick,
66
- * failure-data.ts's outcome table, and time-spend.ts's time-and-spend table). The one deliberate
67
- * exception is ui/gui/gui-client-report.ts's reportRoleOrder, which keeps its own copy by design:
68
- * it runs in the browser, where harness modules cannot be imported. Both helpers return a fresh
69
- * sorted array; the input is never mutated. */
87
+ * (src/failure/failure-cluster.ts's clusters, src/team/error-storm.ts's strongest-cause pick,
88
+ * failure-data.ts's outcome table, and time-spend.ts's time-and-spend table). `sortByValueDesc`
89
+ * is the deliberate sibling for the same value-descending shape that wants a code-unit tiebreak
90
+ * instead of localeCompare, and unlike these two it sorts its input array in place. The one
91
+ * deliberate exception to both orderings is ui/gui/gui-client-report.ts's reportRoleOrder, which
92
+ * keeps its own copy by design: it runs in the browser, where harness modules cannot be imported.
93
+ * These two helpers return a fresh sorted array; their input is never mutated. */
70
94
  export function rankByCount(items, count, key) {
71
95
  return [...items].sort((a, b) => count(b) - count(a) || key(a).localeCompare(key(b)));
72
96
  }
97
+ /** The lower median of a non-empty numeric list: the middle value, or the smaller of the
98
+ * middle two for an even count. Returns a fresh sorted copy; the input is never mutated.
99
+ * Shared by report-data.ts's per-day medians, doctor-checks.ts's and doctor-backlog.ts's
100
+ * 24 h phrase medians. (failure-data.ts's `median` is deliberately different: the mean of
101
+ * the middle two for an even count.) */
102
+ export function lowerMedian(values) {
103
+ const sorted = [...values].sort((a, b) => a - b);
104
+ return sorted[Math.floor((sorted.length - 1) / 2)];
105
+ }
106
+ /** Ascending string comparator by UTF-16 code unit — the locale-independent order a bare `.sort()`
107
+ * uses, unlike `localeCompare` (which `rankByCount` uses for its tiebreak). Used directly by
108
+ * src/cli/cli-run.ts, src/cli/log-commands.ts and src/history/history-data.ts, and by
109
+ * `sortByValueDesc` for its tiebreak. */
110
+ export function compareCodeUnits(a, b) {
111
+ return a < b ? -1 : a > b ? 1 : 0;
112
+ }
113
+ /** Rank `[key, value, …]` pairs by value descending, breaking ties in code-unit order
114
+ * (compareCodeUnits — the locale-independent order, unlike rankByCount's localeCompare), so
115
+ * equal values always order the same way. The single home of that comparator, which
116
+ * src/cli/log-commands.ts's per-type event-count lines and src/report/report-render.ts's
117
+ * mergeDone sort hand-rolled before; `…rest` lets a site carry extra trailing fields
118
+ * (report-render's third column) through the sort untouched. Mutates and returns the input
119
+ * array, matching the `.sort()` the call sites replaced. */
120
+ export function sortByValueDesc(entries) {
121
+ return entries.sort((a, b) => b[1] - a[1] || compareCodeUnits(a[0], b[0]));
122
+ }
73
123
  /** rankByCount for `[key, count]` entry pairs (count first): src/report/report-render.ts's
74
- * rankedRoleMap and src/fleet/failure-spread.ts's strongest-kind pick. */
124
+ * rankedRoleMap and src/team/failure-spread.ts's strongest-kind pick. */
75
125
  export function rankCountEntries(entries) {
76
126
  return rankByCount(entries, (e) => e[1], (e) => e[0]);
77
127
  }
@@ -2,28 +2,27 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { defaultConfig } from "../config/config.js";
3
3
  import { Semaphore } from "./semaphore.js";
4
4
  /** One process-wide bound on concurrent runs of the declared check (config.maxConcurrentChecks):
5
- * every full suite the harness runs — runScopedBuildCheck's
6
- * gate/landing/batch scopes and main-baseline.ts's checkMainBaseline, which runs the suite directly
7
- * — takes a permit here first, so a burst of landings cannot stack suites on the host beside the
8
- * authors' own test runs (the suite has load-sensitive tests). Split out of build/build-check.ts
9
- * — which keeps the deterministic execution and classification (detect → spawn → classify)
10
- * — because permit accounting is scheduling policy, not check semantics: it changes when the
11
- * concurrency or fairness policy changes, not when the check's execution does. Separate from the
12
- * orchestrator's maxConcurrent semaphore: a role tick holds one of those while it waits here, but a
13
- * check permit is held only around the check process itself — never across a pi run, the merge
14
- * lock, or another check — so no permit holder waits on anything a waiter holds. Sized at each
15
- * acquire from the caller's live config (checkCap), so a tumwater.json edit applies to the next
16
- * check; Semaphore.setCapacity never preempts a running check on a shrink. */
5
+ * every full suite the harness runs — runScopedBuildCheck's gate/merge/batch scopes and
6
+ * main-baseline.ts's checkMainBaseline, which runs the suite directly — takes a permit here first,
7
+ * so a burst of merges cannot stack suites on the host beside the authors' own test runs (the suite
8
+ * has load-sensitive tests). Split out of build/build-check.ts — which keeps the deterministic
9
+ * execution and classification (detect → spawn → classify) — because permit accounting is
10
+ * scheduling policy, not check semantics: it changes when the concurrency or fairness policy
11
+ * changes, not when the check's execution does. Separate from the orchestrator's maxConcurrent
12
+ * semaphore: a role tick holds one of those while it waits here, but a check permit is held only
13
+ * around the check process itself — never across a pi run, the merge lock, or another check — so no
14
+ * permit holder waits on anything a waiter holds. Sized at each acquire from the caller's live
15
+ * config (checkCap), so a tumwater.json edit applies to the next check; Semaphore.setCapacity never
16
+ * preempts a running check on a shrink. */
17
17
  const checkPermits = new Semaphore(defaultConfig().maxConcurrentChecks);
18
18
  /** Set while the current async context holds a check permit: a nested withCheckPermit runs
19
19
  * inside the permit it already has instead of queueing for a second one — at a cap of 1 that
20
20
  * second wait could never be granted (its own holder is the one blocking it). No call path
21
- * nests today; this keeps a future one from deadlocking the fleet's checks. */
21
+ * nests today; this keeps a future one from deadlocking the team's checks. */
22
22
  const holdingPermit = new AsyncLocalStorage();
23
- /** Waiting-queue tiers (Semaphore.acquire): a merge-scope check runs inside the merge lock, so
24
- * it is granted the next free permit ahead of queued gate and baseline checks — every check it
25
- * waited behind would be lock-hold time for every other landing. A running check is never
26
- * preempted. */
23
+ /** Waiting-queue tiers (Semaphore.acquire): a merge-scope check runs inside the merge lock, so it
24
+ * is granted the next free permit ahead of queued gate and baseline checks — every check it waited
25
+ * behind would be lock-hold time for every other merge. A running check is never preempted. */
27
26
  export const CHECK_TIER = { merge: 0, other: 1 };
28
27
  /** The live cap: config.maxConcurrentChecks when it is a positive integer (validateConfig
29
28
  * enforces that for tumwater.json), the default otherwise — a caller passing a partial config
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  import { pidAlive } from "../process/process.js";
@@ -225,11 +226,11 @@ export function withStateLock(lock, fn, timeoutMs = 10_000) {
225
226
  ensureParentDir(lock);
226
227
  return withSyncLock(lock, fn, timeoutMs);
227
228
  }
228
- /** The async twin of withStateLock, for a critical section that must await (the landing's
229
- * working-tree fast-forward runs git). Same lock directory and protocol as withStateLock, so a
230
- * sync writer and an async one on the same path exclude each other; ensureParentDir runs first
231
- * for the same reason. The default timeout is withStateLock's, not withLock's longer merge-lock
232
- * budget: these sections are one file read-modify-write or one fast-forward. */
229
+ /** The async twin of withStateLock, for a critical section that must await (the merge's
230
+ * working-tree fast-forward runs git). Same lock directory and protocol as withStateLock, so a sync
231
+ * writer and an async one on the same path exclude each other; ensureParentDir runs first for the
232
+ * same reason. The default timeout is withStateLock's, not withLock's longer merge-lock budget:
233
+ * these sections are one file read-modify-write or one fast-forward. */
233
234
  export async function withStateLockAsync(lock, fn, timeoutMs = 10_000) {
234
235
  ensureParentDir(lock);
235
236
  return withLock(lock, fn, timeoutMs);
@@ -0,0 +1,69 @@
1
+ /** The AsyncLocalStorage store that lets a tick's lease follow its runs across providers
2
+ * (plans/provider-concurrency.md §5): a lease rides here for the length of a tick, a verify task,
3
+ * or a resolver run, and `LoopPi.runWithTransientRetry` re-keys it to the provider each run
4
+ * actually uses. The one choke point every run a loop makes (authoring, gate, merge and resolver)
5
+ * passes through, so a budget handback or a model-failure switch that moves a mid-tick run to
6
+ * another provider moves the lease with it instead of leaving it parked on the key it was admitted
7
+ * on. The check-permit.ts `holdingPermit` store is the pattern: nothing here is a separate permit,
8
+ * only the single lease the run path already holds.
9
+ *
10
+ * The store carries the `piDefault` (the caller's cached readPiDefaultModel result) beside the
11
+ * lease so `ensureProviderLease` can resolve a bare no-model pi seam's key exactly as the
12
+ * admission/review seam did — a run whose key were computed without it would re-key a default
13
+ * team's tick off pi's own provider. It also carries the semaphore and, optionally, wait
14
+ * hooks, so a re-key that has to park on a full target key reports its wait the way a parked
15
+ * resolver does (merge-verify.ts's providerWaitStage). */
16
+ import { AsyncLocalStorage } from "node:async_hooks";
17
+ import { seamConcurrencyKey } from "../config/config-views.js";
18
+ const providerLeaseStore = new AsyncLocalStorage();
19
+ /** Run `run` with `lease` in the provider-lease store, so any run it makes re-keys that lease to
20
+ * the provider it actually uses (ensureProviderLease). `opts.piDefault` is the caller's cached pi
21
+ * default model, so a bare no-model seam resolves to the same key the admission or review seam
22
+ * used; `opts.hooks` (the verify: providerWaitStage) hears a re-key wait, if there is one. The same
23
+ * lease object is released by the caller after the store is unwrapped — a re-key mutates it in
24
+ * place, so release() frees whatever key the run path ended on. */
25
+ export function withProviderLease(lease, semaphore, run, opts = {}) {
26
+ return providerLeaseStore.run({ lease, semaphore, piDefault: opts.piDefault, hooks: opts.hooks }, run);
27
+ }
28
+ /** Re-key the calling lease to the provider `cfg` runs on, if it is not there yet. No-op when
29
+ * the current async context carries no lease (a direct LoopPi unit test, `tumwater tick`) or
30
+ * when the run is already on the lease's key. When it does re-key and `hooks` are present and
31
+ * the target key is at its cap, the hooks hear `waiting` before the lease parks and `granted`
32
+ * once the key frees — a re-key never waits on the team total (it already holds a team
33
+ * permit), only on the target key's headroom. */
34
+ export async function ensureProviderLease(cfg) {
35
+ const ctx = providerLeaseStore.getStore();
36
+ if (!ctx)
37
+ return;
38
+ const key = seamConcurrencyKey(cfg, ctx.piDefault);
39
+ if (ctx.lease.key === key)
40
+ return;
41
+ // The leased currently holds `old` (or nothing); the target key's in-use does not include it,
42
+ // so the key being at its cap is exactly whether rekey() will park.
43
+ const parks = ctx.hooks !== undefined && keyAtCap(ctx, key);
44
+ if (parks)
45
+ ctx.hooks.waiting();
46
+ await ctx.lease.rekey(key);
47
+ if (parks)
48
+ ctx.hooks.granted();
49
+ }
50
+ /** Whether `key` is at its configured cap right now (an absent or uncapped key is not) — the
51
+ * same test semaphore.drive uses to withhold a re-key grant, decided from the live snapshot so
52
+ * a re-key wait can be announced before the lease parks (providerWaitStage). */
53
+ function keyAtCap(ctx, key) {
54
+ if (key === undefined)
55
+ return false;
56
+ const entry = ctx.semaphore.snapshot().byKey[key];
57
+ return entry?.limit !== undefined && entry.inUse >= entry.limit;
58
+ }
59
+ /** The pool-slot wait rule (plans/provider-concurrency.md §5 "No deadlock"): a lease never holds a
60
+ * provider key while it waits for a worktree pool slot. Called by leaseSlot just before it parks on
61
+ * a busy pool; it re-keys the calling lease to no key (which never waits), so a tick or verify
62
+ * waiting for a slot does not hold — and cannot deadlock — a provider slot whose in-use a re-key
63
+ * waiter is waiting to drain. No-op without a lease in context. */
64
+ export async function dropProviderLeaseKey() {
65
+ const ctx = providerLeaseStore.getStore();
66
+ if (!ctx)
67
+ return;
68
+ await ctx.lease.rekey(undefined);
69
+ }