tumwater 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (482) hide show
  1. package/README.md +77 -28
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +23 -0
  4. package/dist/src/agent/agent-backend.js +9 -7
  5. package/dist/src/agent/backend-traits.js +77 -0
  6. package/dist/src/agent/claude/claude-account.js +81 -161
  7. package/dist/src/agent/claude/claude-args.js +19 -19
  8. package/dist/src/agent/claude/claude-backend.js +58 -48
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +11 -19
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +112 -0
  25. package/dist/src/agent/notes-shim.js +40 -0
  26. package/dist/src/agent/opencode/opencode-args.js +60 -0
  27. package/dist/src/agent/opencode/opencode-backend.js +258 -0
  28. package/dist/src/agent/opencode/opencode-catalog.js +210 -0
  29. package/dist/src/agent/opencode/opencode-config.js +193 -0
  30. package/dist/src/agent/opencode/opencode-env.js +35 -0
  31. package/dist/src/agent/opencode/opencode-session-id.js +36 -0
  32. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +16 -33
  35. package/dist/src/agent/run-agent.js +475 -381
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +142 -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 +487 -0
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +384 -0
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +354 -132
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +132 -22
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +89 -11
  50. package/dist/src/backlog/backlog-structure.js +271 -125
  51. package/dist/src/backlog/backlog-write.js +121 -471
  52. package/dist/src/backlog/backlog.js +129 -17
  53. package/dist/src/backlog/question-answer.js +345 -0
  54. package/dist/src/baseline/main-baseline.js +157 -101
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +164 -55
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +41 -0
  61. package/dist/src/{brief.js → brief/brief.js} +38 -33
  62. package/dist/src/budget/budget.js +55 -28
  63. package/dist/src/budget/fallback-breaker.js +31 -31
  64. package/dist/src/build/build-check-detect.js +25 -16
  65. package/dist/src/build/build-check-events.js +87 -22
  66. package/dist/src/build/build-check-report.js +66 -27
  67. package/dist/src/build/build-check-scoped.js +64 -68
  68. package/dist/src/build/build-check.js +17 -15
  69. package/dist/src/build/build-info.js +3 -3
  70. package/dist/src/build/build-stage.js +31 -35
  71. package/dist/src/build/dep-install.js +21 -21
  72. package/dist/src/build/host-sleep.js +1 -1
  73. package/dist/src/change/change-data.js +131 -37
  74. package/dist/src/change/change-render.js +56 -14
  75. package/dist/src/cli/brief-command.js +9 -8
  76. package/dist/src/cli/cli-args.js +133 -28
  77. package/dist/src/cli/cli-command-args.js +82 -21
  78. package/dist/src/cli/cli-editor.js +15 -2
  79. package/dist/src/cli/cli-flag-specs.js +180 -50
  80. package/dist/src/cli/cli-init.js +51 -0
  81. package/dist/src/cli/cli-marker-commands.js +14 -11
  82. package/dist/src/cli/cli-query-commands.js +325 -224
  83. package/dist/src/cli/cli-repo-gate.js +12 -2
  84. package/dist/src/cli/cli-role-command.js +420 -0
  85. package/dist/src/cli/cli-start-summary.js +160 -0
  86. package/dist/src/cli/cli-start.js +453 -0
  87. package/dist/src/cli/completion.js +202 -17
  88. package/dist/src/cli/config-commands.js +51 -25
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +413 -157
  91. package/dist/src/cli/log-commands.js +234 -62
  92. package/dist/src/cli/notify-command.js +33 -18
  93. package/dist/src/cli/question-commands.js +215 -231
  94. package/dist/src/cli.js +154 -49
  95. package/dist/src/collections.js +84 -18
  96. package/dist/src/concurrency/check-permit.js +35 -19
  97. package/dist/src/concurrency/lock.js +22 -17
  98. package/dist/src/concurrency/provider-lease.js +105 -0
  99. package/dist/src/concurrency/semaphore.js +344 -46
  100. package/dist/src/config/config-aliases.js +105 -0
  101. package/dist/src/config/config-editable-keys.js +16 -4
  102. package/dist/src/config/config-fallbacks.js +416 -0
  103. package/dist/src/config/config-field-checks.js +58 -19
  104. package/dist/src/config/config-keys.js +182 -0
  105. package/dist/src/config/config-live.js +113 -22
  106. package/dist/src/config/config-request.js +137 -0
  107. package/dist/src/config/config-schema.js +58 -39
  108. package/dist/src/config/config-selector-checks.js +123 -0
  109. package/dist/src/config/config-validation-agents.js +80 -0
  110. package/dist/src/config/config-validation-model.js +116 -0
  111. package/dist/src/config/config-validation.js +201 -175
  112. package/dist/src/config/config-views.js +202 -346
  113. package/dist/src/config/config-write-core.js +135 -0
  114. package/dist/src/config/config-write.js +219 -391
  115. package/dist/src/config/config.js +102 -81
  116. package/dist/src/config/model-selector.js +44 -18
  117. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  118. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  119. package/dist/src/doctor/doctor-backlog.js +191 -161
  120. package/dist/src/doctor/doctor-check-names.js +81 -0
  121. package/dist/src/doctor/doctor-checks.js +229 -305
  122. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  123. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  124. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  125. package/dist/src/doctor/doctor-holds.js +181 -104
  126. package/dist/src/doctor/doctor-install.js +31 -0
  127. package/dist/src/doctor/doctor-launch-services.js +2 -2
  128. package/dist/src/doctor/doctor-model-checks.js +496 -270
  129. package/dist/src/doctor/doctor-notify.js +5 -7
  130. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  131. package/dist/src/doctor/doctor-orphans.js +189 -69
  132. package/dist/src/doctor/doctor-project-checks.js +303 -0
  133. package/dist/src/doctor/doctor-render.js +36 -9
  134. package/dist/src/doctor/doctor.js +145 -50
  135. package/dist/src/doctor/image-probe.js +257 -0
  136. package/dist/src/doctor/outcome.js +3 -2
  137. package/dist/src/doctor/probe.js +60 -0
  138. package/dist/src/events/event-format.js +363 -155
  139. package/dist/src/events/event-labels.js +123 -0
  140. package/dist/src/events/event-legacy.js +164 -0
  141. package/dist/src/events/event-read.js +74 -38
  142. package/dist/src/events/event-tally.js +16 -0
  143. package/dist/src/events/event-window.js +83 -27
  144. package/dist/src/events/events.js +47 -26
  145. package/dist/src/events/notify.js +85 -35
  146. package/dist/src/failure/failure-cluster.js +23 -20
  147. package/dist/src/failure/failure-data.js +140 -92
  148. package/dist/src/failure/failure-render.js +97 -78
  149. package/dist/src/failure/failure-state-change.js +45 -33
  150. package/dist/src/failure/time-spend.js +166 -82
  151. package/dist/src/files/file-queue.js +16 -4
  152. package/dist/src/files/files.js +129 -41
  153. package/dist/src/files/json-files.js +3 -3
  154. package/dist/src/files/json-object.js +45 -9
  155. package/dist/src/files/stat-cache.js +2 -2
  156. package/dist/src/files/tail.js +2 -2
  157. package/dist/src/gates/bootstrap-gates.js +8 -8
  158. package/dist/src/gates/budget-gates.js +361 -52
  159. package/dist/src/gates/disk-gate.js +18 -18
  160. package/dist/src/gates/gate-polls.js +201 -103
  161. package/dist/src/gates/maintenance-quota.js +34 -37
  162. package/dist/src/gates/pause-gates.js +33 -15
  163. package/dist/src/gates/readiness.js +8 -3
  164. package/dist/src/gates/role-cap-gates.js +23 -28
  165. package/dist/src/gates/startup-gate.js +154 -48
  166. package/dist/src/gates/streak-gate.js +67 -41
  167. package/dist/src/git/commit-message.js +80 -48
  168. package/dist/src/git/git-diff.js +29 -23
  169. package/dist/src/git/git-run.js +87 -46
  170. package/dist/src/git/git.js +114 -92
  171. package/dist/src/git/slot-salvage.js +253 -0
  172. package/dist/src/git/slots-state.js +18 -9
  173. package/dist/src/git/worktree-pool.js +150 -49
  174. package/dist/src/git/worktree-use.js +14 -20
  175. package/dist/src/git/worktree.js +108 -51
  176. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  177. package/dist/src/gui/gui-args.js +88 -21
  178. package/dist/src/gui/gui-backlog-commands.js +297 -0
  179. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  180. package/dist/src/gui/gui-command.js +9 -14
  181. package/dist/src/gui/gui-config-commands.js +462 -0
  182. package/dist/src/gui/gui-endpoint-commands.js +17 -716
  183. package/dist/src/gui/gui-endpoint-common.js +49 -0
  184. package/dist/src/gui/gui-endpoints.js +13 -331
  185. package/dist/src/gui/gui-history-endpoints.js +301 -0
  186. package/dist/src/gui/gui-operator-commands.js +240 -0
  187. package/dist/src/gui/gui-prompt-commands.js +231 -0
  188. package/dist/src/gui/gui-report-endpoints.js +90 -0
  189. package/dist/src/gui/gui-server.js +33 -9
  190. package/dist/src/gui/gui-team-commands.js +164 -0
  191. package/dist/src/gui/http-body.js +17 -1
  192. package/dist/src/history/history-data.js +166 -203
  193. package/dist/src/history/history-queued.js +147 -0
  194. package/dist/src/history/history-render.js +96 -0
  195. package/dist/src/history/history.js +206 -103
  196. package/dist/src/inbox/inbox-attachments.js +94 -39
  197. package/dist/src/inbox/inbox-cancel.js +16 -15
  198. package/dist/src/inbox/inbox-edit.js +10 -10
  199. package/dist/src/inbox/inbox-submit.js +54 -14
  200. package/dist/src/inbox/inbox-top.js +99 -0
  201. package/dist/src/inbox/inbox.js +49 -44
  202. package/dist/src/inbox/pending-prompt.js +27 -27
  203. package/dist/src/inbox/prompt-commands.js +154 -100
  204. package/dist/src/inbox/prompt-not-before.js +2 -2
  205. package/dist/src/init/init-templates.js +15 -14
  206. package/dist/src/init/init.js +35 -29
  207. package/dist/src/merge/backlog-conflicts.js +335 -0
  208. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +100 -93
  209. package/dist/src/merge/merge-cancel.js +53 -0
  210. package/dist/src/merge/merge-check-failures.js +209 -0
  211. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +183 -110
  212. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +6 -5
  213. package/dist/src/merge/merge-drain.js +351 -0
  214. package/dist/src/merge/merge-git.js +403 -0
  215. package/dist/src/merge/merge-pipeline.js +111 -0
  216. package/dist/src/merge/merge-post-review.js +90 -0
  217. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  218. package/dist/src/merge/merge-queue-render.js +113 -0
  219. package/dist/src/merge/merge-queue.js +224 -0
  220. package/dist/src/merge/merge-requeue.js +102 -0
  221. package/dist/src/merge/merge-slot.js +385 -0
  222. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +97 -71
  223. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +151 -82
  224. package/dist/src/merge/merge-top.js +69 -0
  225. package/dist/src/merge/merge-verify.js +214 -0
  226. package/dist/src/notes-cli.js +112 -0
  227. package/dist/src/operator/operator-commands.js +268 -89
  228. package/dist/src/operator/operator-intent.js +130 -82
  229. package/dist/src/operator/operator-requests.js +82 -87
  230. package/dist/src/operator/retire.js +49 -31
  231. package/dist/src/operator/revert.js +179 -0
  232. package/dist/src/orchestrator/orchestrator-launch.js +116 -48
  233. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  234. package/dist/src/orchestrator/orchestrator-scheduling.js +78 -348
  235. package/dist/src/orchestrator/orchestrator.js +173 -361
  236. package/dist/src/orchestrator/retention.js +66 -15
  237. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  238. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  239. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  240. package/dist/src/orchestrator/startup-migration-steps.js +52 -0
  241. package/dist/src/orchestrator/startup-migrations.js +44 -0
  242. package/dist/src/paths.js +228 -75
  243. package/dist/src/pi/command-shape.js +12 -1
  244. package/dist/src/pi/pi-args.js +30 -5
  245. package/dist/src/pi/pi-backend.js +19 -27
  246. package/dist/src/pi/pi-bin.js +64 -67
  247. package/dist/src/pi/pi-event-line.js +17 -1
  248. package/dist/src/pi/pi-failure.js +87 -0
  249. package/dist/src/pi/pi-listed-models.js +53 -0
  250. package/dist/src/pi/pi-models.js +163 -42
  251. package/dist/src/pi/pi-run-result.js +1 -1
  252. package/dist/src/pi/pi-settings.js +86 -0
  253. package/dist/src/pi/pi-stream.js +137 -113
  254. package/dist/src/pi/pi-watchdogs.js +20 -20
  255. package/dist/src/pi-extension/bounded-output.js +25 -8
  256. package/dist/src/pi-extension/context-budget.js +5 -3
  257. package/dist/src/pi-extension/context-shake.js +54 -21
  258. package/dist/src/pi-extension/context-window.js +36 -0
  259. package/dist/src/pi-extension/full-output.js +5 -12
  260. package/dist/src/pi-extension/strip-thinking.js +79 -0
  261. package/dist/src/pi-extension/trim-system-prompt.js +37 -0
  262. package/dist/src/pi-extension/write-guard.js +37 -0
  263. package/dist/src/process/launch-services.js +11 -11
  264. package/dist/src/process/node-bin.js +16 -0
  265. package/dist/src/process/process-group.js +8 -18
  266. package/dist/src/process/process-table.js +1 -1
  267. package/dist/src/process/process.js +48 -9
  268. package/dist/src/process/run-marker.js +42 -7
  269. package/dist/src/process/supervisor.js +47 -35
  270. package/dist/src/prompt/dialect.js +46 -11
  271. package/dist/src/prompt/gate-notes.js +203 -0
  272. package/dist/src/prompt/gate-prompts.js +201 -188
  273. package/dist/src/prompt/notes-backlog.js +181 -0
  274. package/dist/src/prompt/principles.js +1 -1
  275. package/dist/src/prompt/prompt-followup.js +43 -29
  276. package/dist/src/prompt/prompt.js +255 -172
  277. package/dist/src/redeploy/redeploy-policy.js +29 -24
  278. package/dist/src/redeploy/redeploy-probes.js +1 -1
  279. package/dist/src/redeploy/redeploy.js +7 -6
  280. package/dist/src/redeploy/redeployer.js +78 -70
  281. package/dist/src/redeploy/self-reload.js +3 -2
  282. package/dist/src/report/report-data.js +374 -285
  283. package/dist/src/report/report-fold.js +430 -0
  284. package/dist/src/report/report-main-red.js +55 -0
  285. package/dist/src/report/report-render.js +321 -61
  286. package/dist/src/report/report-types.js +1 -0
  287. package/dist/src/report/report.js +26 -9
  288. package/dist/src/request-timeouts.js +2 -2
  289. package/dist/src/review/known-flakes.js +133 -14
  290. package/dist/src/review/post-merge-review.js +106 -0
  291. package/dist/src/review/protected-edits.js +7 -7
  292. package/dist/src/review/review-followup.js +21 -9
  293. package/dist/src/review/review-precheck.js +77 -33
  294. package/dist/src/review/review-run.js +395 -0
  295. package/dist/src/review/review-verdict.js +16 -2
  296. package/dist/src/review/review.js +172 -356
  297. package/dist/src/review/secret-leaks.js +147 -0
  298. package/dist/src/review/suite-rerun.js +3 -3
  299. package/dist/src/roles/legacy-role-ids.js +21 -0
  300. package/dist/src/roles/legacy-role-migration.js +135 -0
  301. package/dist/src/roles/role-catalog.js +172 -137
  302. package/dist/src/roles/role-guidance.js +112 -44
  303. package/dist/src/roles/role-labels.js +9 -0
  304. package/dist/src/roles/role-render.js +221 -28
  305. package/dist/src/roles/role-view.js +244 -33
  306. package/dist/src/roles/roles.js +37 -35
  307. package/dist/src/roles/runner-ids.js +97 -0
  308. package/dist/src/run/acceptance-greps.js +119 -0
  309. package/dist/src/run/feedback-filings.js +51 -0
  310. package/dist/src/{tick → run}/qa-coverage.js +10 -27
  311. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +166 -136
  312. package/dist/src/run/run-detail-data.js +179 -0
  313. package/dist/src/run/run-detail-render.js +98 -0
  314. package/dist/src/run/run-detail.js +168 -0
  315. package/dist/src/run/run-finalize.js +154 -0
  316. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -6
  317. package/dist/src/run/run-prompt.js +304 -0
  318. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +80 -35
  319. package/dist/src/run/run-stage.js +295 -0
  320. package/dist/src/run/run-timing.js +198 -0
  321. package/dist/src/run/run-usage.js +180 -0
  322. package/dist/src/run/run-verdict.js +301 -0
  323. package/dist/src/run/selfcheck-digest.js +40 -0
  324. package/dist/src/run/stage-backlog-checks.js +249 -0
  325. package/dist/src/run/stage-check.js +172 -0
  326. package/dist/src/run/stage-path-checks.js +730 -0
  327. package/dist/src/runner/leftover.js +185 -0
  328. package/dist/src/{loop → runner}/model-fallback.js +13 -12
  329. package/dist/src/runner/revision.js +202 -0
  330. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +10 -10
  331. package/dist/src/runner/runner-outcome.js +107 -0
  332. package/dist/src/runner/runner-pi.js +433 -0
  333. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +34 -28
  334. package/dist/src/runner/runner-run.js +625 -0
  335. package/dist/src/runner/runner-state-load.js +268 -0
  336. package/dist/src/runner/runner-state.js +102 -0
  337. package/dist/src/runner/runner.js +350 -0
  338. package/dist/src/scheduling/backoff.js +64 -54
  339. package/dist/src/scheduling/claims.js +46 -26
  340. package/dist/src/scheduling/once-round.js +30 -30
  341. package/dist/src/scheduling/quiet-hours.js +26 -34
  342. package/dist/src/scheduling/scheduling.js +87 -74
  343. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  344. package/dist/src/status/status-budget.js +123 -0
  345. package/dist/src/status/status-data.js +106 -191
  346. package/dist/src/status/status-inbox.js +47 -0
  347. package/dist/src/status/status-install.js +26 -0
  348. package/dist/src/status/status-polls.js +70 -59
  349. package/dist/src/status/status-roles.js +131 -0
  350. package/dist/src/{fleet → team}/error-storm.js +16 -16
  351. package/dist/src/{fleet → team}/failure-spread.js +10 -10
  352. package/dist/src/{fleet → team}/orchestrator-info.js +97 -25
  353. package/dist/src/{fleet → team}/reclaim.js +9 -9
  354. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +60 -26
  355. package/dist/src/team/team-polls.js +299 -0
  356. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +47 -40
  357. package/dist/src/text/datetime.js +102 -16
  358. package/dist/src/text/format.js +49 -3
  359. package/dist/src/text/markdown.js +8 -5
  360. package/dist/src/text/phrases.js +89 -54
  361. package/dist/src/text/suggest.js +3 -2
  362. package/dist/src/text/text.js +68 -14
  363. package/dist/src/ui/badges.js +165 -93
  364. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  365. package/dist/src/ui/gui/gui-client-boot.js +221 -42
  366. package/dist/src/ui/gui/gui-client-composer.js +105 -27
  367. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  368. package/dist/src/ui/gui/gui-client-drawer.js +226 -130
  369. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  370. package/dist/src/ui/gui/gui-client-history.js +492 -68
  371. package/dist/src/ui/gui/gui-client-labels.js +23 -0
  372. package/dist/src/ui/gui/gui-client-markdown.js +1 -1
  373. package/dist/src/ui/gui/gui-client-model.js +84 -76
  374. package/dist/src/ui/gui/gui-client-operator.js +112 -78
  375. package/dist/src/ui/gui/gui-client-pending.js +248 -41
  376. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  377. package/dist/src/ui/gui/gui-client-report.js +122 -179
  378. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +118 -76
  379. package/dist/src/ui/gui/gui-client-settings.js +180 -20
  380. package/dist/src/ui/gui/gui-client-team.js +252 -0
  381. package/dist/src/ui/gui/gui-client.js +48 -40
  382. package/dist/src/ui/gui/gui-icons.js +0 -2
  383. package/dist/src/ui/gui/gui-page.js +25 -24
  384. package/dist/src/ui/gui/gui-styles.js +46 -38
  385. package/dist/src/ui/progress-data.js +11 -11
  386. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +40 -40
  387. package/dist/src/ui/status-model.js +276 -194
  388. package/dist/src/ui/status-payload.js +227 -62
  389. package/dist/src/ui/status-render.js +196 -108
  390. package/dist/src/ui/team-alerts.js +532 -0
  391. package/dist/src/ui/tone.js +11 -11
  392. package/dist/src/ui/transcript-run.js +58 -0
  393. package/dist/src/ui/transcript-tail.js +28 -19
  394. package/dist/src/ui/transcript.js +85 -38
  395. package/dist/src/ui/tui/tui-backlog.js +132 -21
  396. package/dist/src/ui/tui/tui-editor-answer.js +93 -0
  397. package/dist/src/ui/tui/tui-editor-attach.js +105 -0
  398. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  399. package/dist/src/ui/tui/tui-editor-brief.js +105 -0
  400. package/dist/src/ui/tui/tui-editor-budget.js +159 -0
  401. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  402. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  403. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  404. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  405. package/dist/src/ui/tui/tui-editor-entry.js +219 -0
  406. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  407. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  408. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  409. package/dist/src/ui/tui/tui-editor-interval.js +124 -0
  410. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  411. package/dist/src/ui/tui/tui-editor-notes.js +112 -0
  412. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  413. package/dist/src/ui/tui/tui-editor-pause.js +255 -0
  414. package/dist/src/ui/tui/tui-editor-prompt.js +90 -0
  415. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  416. package/dist/src/ui/tui/tui-editor-queued.js +94 -0
  417. package/dist/src/ui/tui/tui-editor-quiet.js +170 -0
  418. package/dist/src/ui/tui/tui-editor-retire.js +81 -0
  419. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  420. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  421. package/dist/src/ui/tui/tui-editor-search.js +85 -0
  422. package/dist/src/ui/tui/tui-editor-session.js +123 -0
  423. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  424. package/dist/src/ui/tui/tui-editors.js +185 -0
  425. package/dist/src/ui/tui/tui-frame.js +107 -58
  426. package/dist/src/ui/tui/tui-history.js +16 -12
  427. package/dist/src/ui/tui/tui-input.js +56 -60
  428. package/dist/src/ui/tui/tui-keys.js +106 -1825
  429. package/dist/src/ui/tui/tui-pane-activity.js +102 -0
  430. package/dist/src/ui/tui/tui-pane-backlog.js +269 -0
  431. package/dist/src/ui/tui/tui-pane-brief.js +29 -0
  432. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  433. package/dist/src/ui/tui/tui-pane-health.js +174 -0
  434. package/dist/src/ui/tui/tui-pane-history.js +636 -0
  435. package/dist/src/ui/tui/tui-pane-pending.js +369 -0
  436. package/dist/src/ui/tui/tui-pane-queued.js +159 -0
  437. package/dist/src/ui/tui/tui-pane-report.js +154 -0
  438. package/dist/src/ui/tui/tui-pane-session.js +120 -0
  439. package/dist/src/ui/tui/tui-pane-transcript.js +224 -0
  440. package/dist/src/ui/tui/tui-pane.js +125 -44
  441. package/dist/src/ui/tui/tui-panes.js +364 -0
  442. package/dist/src/ui/tui/tui.js +52 -34
  443. package/dist/src/verdict/fix-claim.js +135 -62
  444. package/dist/src/verdict/no-change.js +19 -0
  445. package/dist/src/verdict/refusal.js +80 -24
  446. package/dist/src/verdict/reply-contract.js +51 -28
  447. package/dist/src/version.js +2 -2
  448. package/package.json +5 -2
  449. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  450. package/dist/src/brief-edit.js +0 -42
  451. package/dist/src/cli/cli-run.js +0 -469
  452. package/dist/src/fleet/fleet-polls.js +0 -207
  453. package/dist/src/landing/backlog-conflicts.js +0 -116
  454. package/dist/src/landing/landing-check-failures.js +0 -141
  455. package/dist/src/landing/landing-drain.js +0 -204
  456. package/dist/src/landing/landing-git.js +0 -190
  457. package/dist/src/landing/landing-pipeline.js +0 -96
  458. package/dist/src/landing/landing-queue-render.js +0 -56
  459. package/dist/src/landing/landing-queue.js +0 -149
  460. package/dist/src/landing/landing-slot.js +0 -256
  461. package/dist/src/landing/landing-vetting.js +0 -196
  462. package/dist/src/loop/leftover.js +0 -129
  463. package/dist/src/loop/loop-pi.js +0 -370
  464. package/dist/src/loop/loop-state.js +0 -204
  465. package/dist/src/loop/loop-tick.js +0 -545
  466. package/dist/src/loop/loop.js +0 -315
  467. package/dist/src/loop/revision.js +0 -61
  468. package/dist/src/pi-extension/role-notes.js +0 -40
  469. package/dist/src/roles/loop-ids.js +0 -60
  470. package/dist/src/tick/stage-check.js +0 -302
  471. package/dist/src/tick/telemetry-digest.js +0 -24
  472. package/dist/src/tick/tick-detail-data.js +0 -107
  473. package/dist/src/tick/tick-detail.js +0 -150
  474. package/dist/src/tick/tick-finalize.js +0 -135
  475. package/dist/src/tick/tick-prompt.js +0 -201
  476. package/dist/src/tick/tick-stage.js +0 -223
  477. package/dist/src/tick/tick-timing.js +0 -195
  478. package/dist/src/tick/tick-usage.js +0 -142
  479. package/dist/src/tick/tick-verdict.js +0 -201
  480. package/dist/src/ui/fleet-alerts.js +0 -174
  481. package/dist/src/ui/gui/gui-client-fleet.js +0 -576
  482. package/dist/src/ui/gui/gui-client-sound.js +0 -52
@@ -1,190 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { COMMIT_IDENT, git, gitTry, runGit } from "../git/git-run.js";
4
- import { writeBytesAtomic } from "../files/files.js";
5
- import { currentBranch, gitLines, headOf } from "../git/git.js";
6
- import { unquotePorcelainPath } from "../git/git-diff.js";
7
- import { CONFIG_BASENAME, configPath, primaryWorktreeLockPath } from "../paths.js";
8
- import { withStateLockAsync } from "../concurrency/lock.js";
9
- /** Git plumbing for the landing flow: rebasing a worktree branch onto main, inspecting and
10
- * finishing a conflicted rebase, and fast-forwarding main — the mechanics, with no landing policy
11
- * of their own. Landing policy (lock → rebase → verify → ff, conflict resolution, batch ff)
12
- * lives in landing-merge.ts on top of these. Moved here from landing-merge.ts: the cluster started
13
- * as landing-merge.ts-only helpers (organize tick 78, moving them out of git/git.ts), but the
14
- * lander and the batch lander grew to call them directly, leaving landing-merge.ts exporting
15
- * generic rebase/ff plumbing that had nothing to do with merging. */
16
- /** Paths currently in conflict (unmerged) in the worktree. C-quoted names are decoded to
17
- * real paths — a non-ASCII conflicted file arrives as `"h\303\251llo.ts"` (core.quotePath is on
18
- * by default), and undecoded it does not exist on disk: hasConflictMarkers could never read
19
- * it, so an unresolved conflict in such a file passed the marker check and continueRebase
20
- * committed its markers to main. */
21
- export async function conflictedFiles(wt) {
22
- const out = await gitTry(wt, "diff", "--name-only", "--diff-filter=U");
23
- return gitLines(out).map(unquotePorcelainPath);
24
- }
25
- /** How many main commits the resolver prompt lists, and how many body lines it keeps per
26
- * commit: enough to explain a deliberate removal without filling the window. */
27
- const MAX_INTENT_COMMITS = 15;
28
- const MAX_INTENT_BODY_LINES = 20;
29
- function capIntentBody(body) {
30
- const lines = body.split("\n");
31
- if (lines.length <= MAX_INTENT_BODY_LINES)
32
- return body;
33
- const kept = lines.slice(0, MAX_INTENT_BODY_LINES).join("\n");
34
- return `${kept}\n… (${lines.length - MAX_INTENT_BODY_LINES} more lines)`;
35
- }
36
- /** The commits on `mainBranch` since `since` (the change's merge-base with main) that touched
37
- * any of `files`, newest first. resolveConflict threads this into the resolver prompt so pi
38
- * reads what main deliberately changed instead of guessing from the markers. The list is
39
- * capped at MAX_INTENT_COMMITS and each body at MAX_INTENT_BODY_LINES; `omitted` counts the
40
- * commits the cap dropped so the prompt can say so. An empty result is the normal case of a
41
- * fork point with no later main commits touching the files. */
42
- export async function mainCommitsTouching(wt, since, mainBranch, files) {
43
- if (files.length === 0)
44
- return { commits: [], omitted: 0 };
45
- const out = await gitTry(wt, "log", "--format=%h%x1f%s%x1f%b%x1e", `${since}..${mainBranch}`, "--", ...files);
46
- const parsed = (out ?? "")
47
- .split("\x1e")
48
- .map((record) => record.trim())
49
- .filter(Boolean)
50
- .map((record) => {
51
- const [sha = "", subject = "", ...rest] = record.split("\x1f");
52
- return { sha, subject, body: capIntentBody(rest.join("\x1f").trim()) };
53
- });
54
- return {
55
- commits: parsed.slice(0, MAX_INTENT_COMMITS),
56
- omitted: Math.max(0, parsed.length - MAX_INTENT_COMMITS),
57
- };
58
- }
59
- /** Attempt to rebase the worktree branch onto main and classify the outcome WITHOUT
60
- * cleaning up: "rebased" (including already up to date), "conflict" (the rebase stopped on
61
- * unmerged paths, which remain in the worktree for a resolver), or "other" (any other
62
- * failure). Shared by the two rebase wrappers below, which differ only in cleanup policy.
63
- * Rebase — not merge — keeps main's history linear: each tick lands as its own commit on top
64
- * of whatever main holds. */
65
- async function attemptRebase(wt, mainBranch) {
66
- try {
67
- // The -c ident is needed for the rewritten committer identity. diff3 carries the base
68
- // section of every conflict, which the deterministic backlog resolver reads
69
- // (plans/parallel-work-instances.md, part 3/7); rebaseOntoMain aborts on conflict, so the
70
- // marker style only matters to its leave-conflicts sibling.
71
- await runGit(wt, [...COMMIT_IDENT, "-c", "merge.conflictStyle=diff3", "rebase", mainBranch]);
72
- return "rebased";
73
- }
74
- catch {
75
- return (await conflictedFiles(wt)).length > 0 ? "conflict" : "other";
76
- }
77
- }
78
- /** Rebase the worktree branch onto main (main may have advanced during the tick).
79
- * Returns false and aborts the rebase on conflict. */
80
- export async function rebaseOntoMain(wt, mainBranch) {
81
- const state = await attemptRebase(wt, mainBranch);
82
- if (state !== "rebased")
83
- await gitTry(wt, "rebase", "--abort");
84
- return state === "rebased";
85
- }
86
- /** Rebase the worktree branch onto main, leaving conflict markers in place for a
87
- * resolver to work on. "clean" = rebased (or already up to date); "conflict" = the rebase
88
- * stopped on conflicts and the worktree holds them mid-rebase; "failed" = anything else
89
- * (aborted and cleaned up). */
90
- export async function rebaseOntoMainLeaveConflicts(wt, mainBranch) {
91
- const state = await attemptRebase(wt, mainBranch);
92
- if (state === "other")
93
- await gitTry(wt, "rebase", "--abort");
94
- return state === "rebased" ? "clean" : state === "conflict" ? "conflict" : "failed";
95
- }
96
- /** True if any of the given files still contains a git conflict marker.
97
- * A deleted file counts as resolved (the resolver chose the deletion). */
98
- export function hasConflictMarkers(wt, files) {
99
- // Only start/end markers are checked: every real conflict block carries them, while a bare
100
- // `=======` line is legitimate content (a markdown setext or RST underline of exactly seven
101
- // characters), and flagging it would reject clean resolutions forever. A resolver that
102
- // leaves only a separator line behind is treated as resolved; its stray line is content the
103
- // project's own tests can catch.
104
- const marker = /^(<{7}|>{7})( |$)/m;
105
- return files.some((f) => {
106
- const p = path.join(wt, f);
107
- try {
108
- return marker.test(fs.readFileSync(p, "utf8"));
109
- }
110
- catch {
111
- return false;
112
- }
113
- });
114
- }
115
- /** The reason naming every changed file that still carries a conflict marker, or undefined when
116
- * none do — the one home of the wording, shared by the stage check's pre-queue finding
117
- * (stage-check.ts) and the review gate's hard rejection (review.ts), so the two surfaces cannot
118
- * name the same leftover marker differently. Committing or landing one would put raw `<<<<<<<`
119
- * lines on main (PLANS.md "Robust conflict landing, part 2/2"). Builds on hasConflictMarkers
120
- * above, the shared definition of what a marker is. */
121
- export function conflictMarkerReason(wt, files) {
122
- const marked = files.filter((f) => hasConflictMarkers(wt, [f]));
123
- return marked.length > 0 ? `conflict markers remain in: ${marked.join(", ")}` : undefined;
124
- }
125
- /** Conclude an in-progress rebase with everything in the worktree as the resolution.
126
- * GIT_EDITOR=true so `rebase --continue` can never block on a commit-message prompt. A
127
- * resolution that leaves no unique content (the branch's change was fully superseded by
128
- * main) is skipped automatically by git, finishing the rebase cleanly. Throws when the
129
- * rebase stops again — e.g. on a second conflict from a later commit the resolution run added; the caller aborts and reports merge_conflict. */
130
- export async function continueRebase(wt) {
131
- await git(wt, "add", "-A");
132
- await runGit(wt, [...COMMIT_IDENT, "rebase", "--continue"], { GIT_EDITOR: "true" });
133
- return headOf(wt, "HEAD");
134
- }
135
- /** The live config's bytes, when the landing about to fast-forward `ref` would delete it
136
- * (plans/portability.md §4b/7): the config file exists, is tracked in the current index (and
137
- * HEAD — a staged-or-dirty config makes `git merge --ff-only` refuse below, so the two agree
138
- * on every merge that can succeed), and is absent from the incoming tree. Anything else —
139
- * already untracked, already absent, present in `ref` — needs no preserve step. */
140
- export async function configBytesToPreserve(root, ref) {
141
- const cfg = configPath(root);
142
- if (!fs.existsSync(cfg))
143
- return null;
144
- if ((await gitTry(root, "ls-files", "--error-unmatch", CONFIG_BASENAME)) === null)
145
- return null;
146
- if ((await gitTry(root, "cat-file", "-e", `${ref}:${CONFIG_BASENAME}`)) !== null)
147
- return null;
148
- return fs.readFileSync(cfg);
149
- }
150
- /** The preserve step's write-back, restore-only-when-absent: write the saved bytes only when
151
- * the live config is absent at write-back time. The merge deletes the file mid-window, so a
152
- * config request applied in that window (3/7's applyConfigRequest runs outside the merge lock)
153
- * recreates it — the newer bytes win (latest instruction wins), which also makes the step
154
- * idempotent. Called only after a successful merge: a refused merge leaves the file untouched.
155
- * Written through writeBytesAtomic (tmp+rename), like every other tumwater.json writer, so a
156
- * crash mid-write cannot leave the live config torn for the ~2 s config poll or the next
157
- * generation's startup load. */
158
- export function restoreConfigBytes(root, saved) {
159
- if (!saved)
160
- return;
161
- const cfg = configPath(root);
162
- if (fs.existsSync(cfg))
163
- return;
164
- writeBytesAtomic(cfg, saved);
165
- }
166
- /** Fast-forward main to `ref`, without touching any remote. Callers pass the worktree's
167
- * post-rebase HEAD — a bare sha, which both arms accept (`merge --ff-only <sha>` and
168
- * `push . <sha>:<main>`). Uses a working-tree merge when the primary checkout is on main (so its
169
- * files update), otherwise a local ref push. Returns true on success. The working-tree arm
170
- * preserves the live config across a landing that untracks it (plans/portability.md §4b/7):
171
- * without the write-back, the commit that removes the tracked config would delete the fleet's
172
- * running config out from under it and the next config poll would fall back to defaults. */
173
- export async function ffMainTo(root, ref, mainBranch) {
174
- const primaryBranch = await currentBranch(root);
175
- if (primaryBranch === mainBranch) {
176
- // The working-tree arm rewrites the same tracked files the CLI backlog writers read and
177
- // rewrite, so it shares their primary-worktree lock (paths.ts): without it a `tumwater bug`
178
- // that read before this merge can rename its stale whole-file write over the landed change.
179
- // The ref-push arm below touches no working tree and needs no lock; the save/merge/restore
180
- // stays one short section, never held across a build re-check.
181
- return withStateLockAsync(primaryWorktreeLockPath(root), async () => {
182
- const saved = await configBytesToPreserve(root, ref);
183
- if ((await gitTry(root, "merge", "--ff-only", ref)) === null)
184
- return false;
185
- restoreConfigBytes(root, saved);
186
- return true;
187
- });
188
- }
189
- return (await gitTry(root, "push", ".", `${ref}:${mainBranch}`)) !== null;
190
- }
@@ -1,96 +0,0 @@
1
- import { runOnAbort } from "../process/process.js";
2
- import { warnEventBestEffort } from "../events/events.js";
3
- import { errorMessage } from "../text/text.js";
4
- import { deleteRef } from "../git/git.js";
5
- import { removeLandingChange, writeLandingOutcome } from "./landing-slot.js";
6
- import { landingRefName, rejectedRefName } from "../paths.js";
7
- /** A fresh pipeline: nothing in flight anywhere — no vet running, no approved change waiting,
8
- * no merge holding main. The orchestrator builds one per fleet start (orchestrator.ts). */
9
- export function newLandingPipeline() {
10
- return { vetting: new Map(), vetted: new Map(), merge: null };
11
- }
12
- /** Every landing task in flight — each vet holding its permit, and the merge — for the shutdown
13
- * and restart waits. A parked vet is not one of them: it has started nothing, and a shutdown
14
- * (its controller) or a closed start gate (at its grant) settles it without a run. */
15
- export function landingTasks(p) {
16
- return [...[...p.vetting.values()].filter((v) => !v.parked), ...(p.merge ? [p.merge] : [])];
17
- }
18
- /** Everything `tumwater abort --role` can reach: every task in flight, plus each vetted change
19
- * waiting with no task of its own. A parked vet is out of reach, as a plainly queued entry
20
- * always was: its change has not started landing. */
21
- export function abortableLandings(p) {
22
- return [...landingTasks(p), ...p.vetted.values()];
23
- }
24
- /** Wire harness shutdown to one task's own controller — through the shared register/abort
25
- * race helper, so an already-fired signal aborts at once. Returns the detach function. The
26
- * fleet shutdown signal outlives every landing, so each task that settles while the signal is
27
- * still live must detach in its own finally; without that, the one long-lived signal
28
- * accumulates a listener (and the retained controller behind it) for every landing the fleet
29
- * ever runs, growing without bound until the shutdown it was waiting for. */
30
- export function abortOnShutdown(signal, controller) {
31
- return runOnAbort(signal, () => controller.abort());
32
- }
33
- /** Wrap a landing task's stored promise so it can never reject: the tasks are fire-and-forget
34
- * during normal operation (only the shutdown/restart drain awaits them), and an unexpected
35
- * throw out of their bookkeeping — an unwritable state file, event feed, or marker while
36
- * settling an outcome — would otherwise surface as an unhandled rejection and take the
37
- * orchestrator down. The throw is reported best-effort; the task's own finally has already
38
- * released its slot and controller, and the surviving queue entry and pin recover on the next
39
- * poll. */
40
- export function guardLandingTask(root, role, promise) {
41
- return promise.catch((err) => {
42
- warnEventBestEffort(root, "harness", `landing task for ${role} could not settle: ${errorMessage(err)}`);
43
- });
44
- }
45
- /** Take a permit at `tier` unless `signal` fires first: resolves to its release, or to null
46
- * when the signal won — a grant that arrives after that is handed straight back (a hop, never
47
- * a leak), so a parked vet that a shutdown settles holds nothing. */
48
- export async function acquireUnlessAborted(semaphore, tier, signal) {
49
- if (signal.aborted)
50
- return null;
51
- const granted = semaphore.acquire(tier).then(() => true);
52
- const stopped = new Promise((resolve) => signal.addEventListener("abort", () => resolve(false), { once: true }));
53
- if (await Promise.race([granted, stopped]))
54
- return () => semaphore.release();
55
- void granted.then(() => semaphore.release());
56
- return null;
57
- }
58
- /** The roles the pipeline holds right now — vetting (parked or not), vetted, or merging. */
59
- export function liveRoles(p) {
60
- return new Set([...p.vetting.keys(), ...p.vetted.keys(), ...(p.merge?.roles ?? [])]);
61
- }
62
- /** Discard the pinned landing refs of every role in a deliberately-aborted landing.
63
- * `tumwater abort --role` throws the committed work away, and the pin is what would otherwise
64
- * recover it, so the ref must go; a shutdown abort leaves `userAborted` unset and every ref
65
- * survives for recovery. Shared by every task's settling code so their discard semantics
66
- * cannot drift. A ref that is already gone is not an error. */
67
- export async function discardPinnedRefs(root, roles) {
68
- for (const role of roles) {
69
- try {
70
- await deleteRef(root, landingRefName(role));
71
- }
72
- catch {
73
- /* already gone */
74
- }
75
- }
76
- }
77
- /** Settle a landing queue entry that reached a terminal outcome — the one home of the
78
- * write-plus-drop pairing every settled landing goes through: the result lands in the role's
79
- * state and the landed/land_failed event (writeLandingOutcome), then the change's marker record
80
- * is dropped (removeLandingChange — landing-slot leaves the marker to the caller). Shared by
81
- * settleAbortedVetted, the vet's final-verdict branch, and the merge's per-change write-back, so
82
- * no settled entry can keep a stale marker or lose its outcome write. The pinned-ref discard
83
- * stays with the callers: only a user abort throws work away, and the merge discards its whole
84
- * stack at once rather than change by change. */
85
- export async function settleLandingOutcome(root, entry, state, result, durationMs, usage, file) {
86
- writeLandingOutcome(root, entry, state, result, durationMs, usage, file);
87
- // A revision that lands ends the rejected change's life: drop the rejected ref (plans/revise-
88
- // rejected.md). A rejection repoints or deletes it in landing-core.ts, so this is the only
89
- // ref cleanup a landed revision needs here. Guarded on the entry's revisionRound, not just a
90
- // `changed` result: a fresh change can land while a different rejection's revision is still
91
- // pending, and its landing must leave that pending revision's ref alone.
92
- if (result === "changed" && entry.revisionRound !== undefined) {
93
- await deleteRef(root, rejectedRefName(entry.role));
94
- }
95
- removeLandingChange(root, entry.role);
96
- }
@@ -1,56 +0,0 @@
1
- /** The renderer behind `tumwater queue` — the operator-facing text for the land queue view the
2
- * reader gathers (landing-queue.ts's readLandingQueue, beside it): one line per queued landing
3
- * in execution order, plus the count of queue files the reader skipped. Pure function of the
4
- * collected view and a `now` instant — no I/O, no clock reads — so the age it prints can be
5
- * pinned by a test. */
6
- import { humanSeconds, secondsSince } from "../text/datetime.js";
7
- import { shortSha } from "../text/format.js";
8
- import { plural } from "../text/phrases.js";
9
- import { landingChanges, LANDING_STAGE_LABELS } from "./landing-slot.js";
10
- /** The bracketed-label text for one queue entry, in the wording the loop rows use (status-model's
11
- * loopPhase): `landing <elapsed> · <stage>` while a vet or the merge holds the change,
12
- * `vetted, awaiting merge` once vetted, and "" when the marker is absent, names no record for
13
- * `sha`, or the record is `done`. Reads the marker through landingChanges, so an older-generation
14
- * single-change marker counts as one `landing` record. */
15
- export function queueEntryLandingLabel(sha, inFlight, now) {
16
- if (!inFlight)
17
- return "";
18
- const change = landingChanges(inFlight).find((c) => c.sha === sha);
19
- if (!change || change.status === "done")
20
- return "";
21
- if (change.status === "vetted")
22
- return "vetted, awaiting merge";
23
- let label = "landing";
24
- if (change.startedAt)
25
- label += ` ${humanSeconds(secondsSince(change.startedAt, now))}`;
26
- if (change.stage)
27
- label += ` · ${LANDING_STAGE_LABELS[change.stage]}`;
28
- return label;
29
- }
30
- /** Render the queue as the text `tumwater queue` prints: a `land queue (<depth>):` header then
31
- * one 1-based line per readable entry, oldest first, carrying the loop, tick number, short
32
- * pinned sha, age, and summary. An empty queue (no entries and nothing skipped) prints
33
- * `land queue empty`; a non-zero skip count appends one `(+N unreadable queue file(s) skipped)`
34
- * line so a torn or foreign file the reader dropped is visible rather than silent. An entry the
35
- * optional in-flight marker has a live record for carries ` · [<label>]` after its age
36
- * (queueEntryLandingLabel). */
37
- export function renderLandingQueue(view, now, inFlight) {
38
- const depth = view.entries.length + view.unreadable;
39
- const lines = [];
40
- if (depth === 0) {
41
- lines.push("land queue empty");
42
- }
43
- else {
44
- lines.push(`land queue (${depth}):`);
45
- view.entries.forEach((e, i) => {
46
- const age = humanSeconds(secondsSince(e.enqueuedAt, now));
47
- const label = queueEntryLandingLabel(e.sha, inFlight, now);
48
- const tag = label ? ` · [${label}]` : "";
49
- lines.push(` ${i + 1}. ${e.role} tick #${e.tick} · ${shortSha(e.sha)} · ${age} ago${tag} · ${e.summary}`);
50
- });
51
- }
52
- if (view.unreadable > 0) {
53
- lines.push(` (+${plural(view.unreadable, "unreadable queue file")} skipped)`);
54
- }
55
- return lines.join("\n");
56
- }
@@ -1,149 +0,0 @@
1
- import path from "node:path";
2
- import { listQueueFiles, queueFileName, removeQueueFile } from "../files/file-queue.js";
3
- import { isJsonObject, nonNegativeNumber } from "../files/json-object.js";
4
- import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
5
- import { cachedByStat } from "../files/stat-cache.js";
6
- import { landQueueDir } from "../paths.js";
7
- let seq = 0;
8
- /** Append one landing to the queue as a single timestamped file (creating the queue dir if
9
- * needed). The filename orders entries across processes by wall-clock time; the per-process
10
- * counter and pid break ties within one process. Written atomically (tmp + rename): the
11
- * enqueue runs while the process can be hard-killed mid-write (the tick deadline's
12
- * process-group kill, a shutdown), and a torn entry the crash left behind is not always
13
- * recoverable — the drain repairs only an unreadable HEAD (staleHeadFile), so a torn file
14
- * that sorts after a healthy entry would sit in the queue dir unread forever. */
15
- export function enqueueLanding(root, entry) {
16
- const dir = landQueueDir(root);
17
- const name = queueFileName(entry.enqueuedAt, seq++, ".json");
18
- writeJsonAtomic(path.join(dir, name), entry);
19
- }
20
- /** Every queue file in execution order (oldest first) — the same filename sort queuedLandings,
21
- * headLanding, and the orchestrator's per-poll queued-roles set read. A missing queue dir
22
- * reads as an empty queue. */
23
- function queueFiles(root) {
24
- return listQueueFiles(landQueueDir(root), ".json");
25
- }
26
- /** Number of landings currently queued — a directory listing only; no file content is read.
27
- * A missing queue dir reads as 0, like every other reader here. */
28
- export function queueDepth(root) {
29
- return queueFiles(root).length;
30
- }
31
- // Per-poll entry-content cache (stat-cache.cachedByStat): dashboard polls must not re-parse
32
- // every queued entry — each file is written once by enqueueLanding and only deleted on drop,
33
- // the same write-once discipline the inbox caches for prompts.
34
- const entryCache = new Map();
35
- /** True when `v` carries every field a LandingEntry requires, with the right type. The land
36
- * queue is one JSON object per file, so a file that parses to anything else — a scalar, an
37
- * array, an object missing a required field — is foreign or torn and reads as no entry.
38
- * Checking only `role` and `sha` left the `as LandingEntry` cast unsound: a foreign file that
39
- * happened to carry those two fields was handed downstream with `tick` and `summary`
40
- * undefined, which surface as bogus session names (`tumwater-<role>-undefined-review`) and
41
- * `undefined` in the reviewer's prompt. Optional fields are checked only when present.
42
- *
43
- * The three numeric fields go through nonNegativeNumber, not a bare `typeof === "number"`:
44
- * JSON's `1e999` parses to Infinity, so a hand-edited file could carry a non-finite tick or
45
- * enqueuedAt past a typeof check, and both feed plumbing downstream (session names, queue
46
- * ordering, the status payload). */
47
- function isPriorReview(v) {
48
- if (!isJsonObject(v))
49
- return false;
50
- return (typeof v.sha === "string" &&
51
- Array.isArray(v.reasons) &&
52
- v.reasons.every((r) => typeof r === "string"));
53
- }
54
- function isLandingEntry(v) {
55
- if (!isJsonObject(v))
56
- return false;
57
- const e = v;
58
- return (typeof e.role === "string" &&
59
- typeof e.sha === "string" &&
60
- nonNegativeNumber(e.tick, null) !== null &&
61
- typeof e.summary === "string" &&
62
- nonNegativeNumber(e.enqueuedAt, null) !== null &&
63
- (e.body === undefined || typeof e.body === "string") &&
64
- (e.highFriction === undefined || typeof e.highFriction === "boolean") &&
65
- (e.revisionRound === undefined || nonNegativeNumber(e.revisionRound, null) !== null) &&
66
- (e.priorReview === undefined || isPriorReview(e.priorReview)));
67
- }
68
- /** Read one queue file through the stat-keyed cache: null when it vanishes mid-listing (a
69
- * concurrent drop — the inbox.ts race policy), is unreadable, or fails to parse or to carry
70
- * every required LandingEntry field (a foreign or torn file is skipped, never thrown on). A
71
- * shallow copy out: the entry's fields are all scalars and callers treat the result as
72
- * read-only. */
73
- function readEntry(file) {
74
- return cachedByStat(entryCache, file, file, () => {
75
- // readJsonFile is the shared "missing or torn reads as no data" policy; a foreign shape
76
- // is still rejected by isLandingEntry.
77
- const parsed = readJsonFile(file);
78
- return isLandingEntry(parsed) ? parsed : null;
79
- }, (e) => ({ ...e }));
80
- }
81
- /** Every queued landing in execution order (oldest first) paired with the file to drop on
82
- * completion. The batch drain (merge queue 5/5) reads this slice because `dropLanding` needs
83
- * the file, which the bare-entry readers never return. A missing queue directory reads as an
84
- * empty queue. */
85
- export function queuedLandingFiles(root) {
86
- const out = [];
87
- for (const file of queueFiles(root)) {
88
- const entry = readEntry(file);
89
- if (entry)
90
- out.push({ entry, file });
91
- }
92
- return out;
93
- }
94
- /** All queued landings in execution order (oldest first). A missing queue directory reads as
95
- * an empty queue. */
96
- export function queuedLandings(root) {
97
- return queuedLandingFiles(root).map((q) => q.entry);
98
- }
99
- /** The roles currently holding a queued landing — the maintenance quota's and the permit
100
- * reserve's "already in flight" input, and the scheduler's per-poll merge-queue interlock.
101
- * A missing queue directory reads as an empty set, like every reader here. */
102
- export function queuedLandingRoles(root) {
103
- return new Set(queuedLandingFiles(root).map((q) => q.entry.role));
104
- }
105
- /** Read every queue file once through the stat-keyed cache, collecting the readable entries
106
- * (filename order) and counting the rest — the torn/foreign file policy readEntry already
107
- * applies, surfacing the skip count instead of hiding it. */
108
- export function readLandingQueue(root) {
109
- const entries = [];
110
- let unreadable = 0;
111
- for (const file of queueFiles(root)) {
112
- const entry = readEntry(file);
113
- if (entry)
114
- entries.push(entry);
115
- else
116
- unreadable++;
117
- }
118
- return { entries, unreadable };
119
- }
120
- /** The head of the queue — its oldest entry — plus the file to drop on completion, or null when
121
- * the queue is empty or its oldest file is a vanished/torn entry. */
122
- export function headLanding(root) {
123
- const file = queueFiles(root)[0];
124
- if (!file)
125
- return null;
126
- const entry = readEntry(file);
127
- return entry ? { entry, file } : null;
128
- }
129
- /** The oldest queue file when the queue is non-empty but its head is unreadable — torn or
130
- * foreign (a partial write an older build left, a hand-placed file) — i.e. when headLanding
131
- * reads null while the queue holds files; null when the queue is empty or its head is healthy.
132
- * The drain drops this file with a warning so the queue can drain: headLanding reads null for
133
- * it forever and nothing else will remove it, stranding every live entry behind it (each
134
- * author's interlock, the per-poll queued-roles set, pins its ticks along with them). The
135
- * crashed entry's commit, if any, still lives in its landing ref — next-tick leftover recovery
136
- * re-lands it — so the drop loses nothing recoverable from the file (BUGS.md 2026-09-16). */
137
- export function staleHeadFile(root) {
138
- const file = queueFiles(root)[0];
139
- if (!file)
140
- return null;
141
- return readEntry(file) === null ? file : null;
142
- }
143
- /** Remove one queued landing's file after its landing outcome — terminal or not (a
144
- * non-terminal outcome keeps the landing ref; the retry is the role's next fresh tick, not a
145
- * queue re-drain). ENOENT is a no-op: a concurrent drop between listing and removal is a
146
- * normal race (the inbox.ts policy), not an error. */
147
- export function dropLanding(file) {
148
- removeQueueFile(file);
149
- }