tumwater 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (464) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +55 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +6 -4
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +404 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +65 -3
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +181 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +183 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,15 +1,73 @@
1
- // vocabulary: plain
2
- import { readMergeQueue } from "../../merge/merge-queue.js";
3
- import { renderMergeQueue } from "../../merge/merge-queue-render.js";
1
+ import { readMergeQueue, queuedMergeFiles } from "../../merge/merge-queue.js";
2
+ import { renderMergeQueue, revisionNote, retryNote, scopeMergeQueue } from "../../merge/merge-queue-render.js";
3
+ import { cancelQueuedMerge } from "../../merge/merge-cancel.js";
4
+ import { moveQueuedMergeToTop } from "../../merge/merge-top.js";
5
+ import { mergeChanges } from "../../merge/merge-slot.js";
4
6
  import { orchestratorAlive } from "../../team/orchestrator-info.js";
5
7
  import { queueInFlight } from "../../status/status-polls.js";
6
- import { renderTeamChange } from "../../change/change-render.js";
8
+ import { renderChangeOverlaps, renderRoleChanges, renderTeamChange } from "../../change/change-render.js";
9
+ import { scopeChangeOverlaps } from "../../change/change-data.js";
10
+ import { roleLabel } from "../../roles/role-labels.js";
11
+ import { shortSha } from "../../text/format.js";
12
+ import { humanAgo } from "../../text/datetime.js";
13
+ import { readPostReviews } from "../../review/post-merge-review.js";
14
+ import { renderPostReviews, postReviewStatusLabel, scopePostReviews, POST_REVIEW_STATES, postReviewState } from "../../review/post-merge-review-render.js";
15
+ import { acceptPostReview } from "../../review/post-merge-review-accept.js";
16
+ import { squash } from "../../text/text.js";
7
17
  import { promptLineFree } from "./tui-pane-ctx.js";
8
- /** The Pending pane (view roleIds.length + 5) of the TUI (src/ui/tui/tui.tsx): the durable
9
- * merge queue rendered up front plus the git-backed change roster the async collector appends
10
- * when it resolves. Split out of the pane-navigation monolith; its only key arm is the Ctrl+R
11
- * in-place refresh, and it has no render state of its own — aside from the enter half of the
12
- * Ctrl+T cycle. */
18
+ import { arrowDir } from "./tui-keymap.js";
19
+ import { moveEntrySelection } from "./tui-backlog.js";
20
+ /** The overlapping-files block under the roster: empty for a no-base view (the roster above
21
+ * already says the main branch is missing), else the CLI's renderChangeOverlaps under a
22
+ * "overlapping files (N):" heading shaped like the queue's. */
23
+ export function pendingOverlapsText(view, scope) {
24
+ if (view.state === "no-base")
25
+ return "";
26
+ if (scope !== undefined) {
27
+ const narrowed = scopeChangeOverlaps(view, [scope]);
28
+ if (narrowed.overlaps.length === 0)
29
+ return renderChangeOverlaps(narrowed, [scope]);
30
+ return `overlapping files held by ${scope} (${narrowed.overlaps.length}):\n` + renderChangeOverlaps(narrowed);
31
+ }
32
+ if (view.overlaps.length === 0)
33
+ return renderChangeOverlaps(view);
34
+ return `overlapping files (${view.overlaps.length}):\n` + renderChangeOverlaps(view);
35
+ }
36
+ /** The distinct roles named across the overlaps, in first-seen order; empty for a no-base view. */
37
+ export function overlapHolders(view) {
38
+ if (view.state === "no-base")
39
+ return [];
40
+ const seen = [];
41
+ for (const o of view.overlaps)
42
+ for (const r of o.roles)
43
+ if (!seen.includes(r))
44
+ seen.push(r);
45
+ return seen;
46
+ }
47
+ /** The distinct roles named across the merge queue, in first-seen (queue) order; empty for an
48
+ * empty queue. The queue's own roles lead the pane's Ctrl+G scope cycle, before the overlap
49
+ * holders — the queue scope is the `tumwater queue --role <id>` twin. */
50
+ export function mergeQueueRoles(view) {
51
+ const seen = [];
52
+ for (const e of view.entries)
53
+ if (!seen.includes(e.role))
54
+ seen.push(e.role);
55
+ return seen;
56
+ }
57
+ /** The distinct roles named across the post-merge review queue, in first-seen (FIFO) order;
58
+ * empty for an empty queue. The review block's contribution to the pane's Ctrl+G scope cycle,
59
+ * after the queue's roles — the review scope is the `tumwater reviews --role <id>` twin. */
60
+ export function postReviewRoles(entries) {
61
+ const seen = [];
62
+ for (const e of entries)
63
+ if (!seen.includes(e.role))
64
+ seen.push(e.role);
65
+ return seen;
66
+ }
67
+ /** The heading over one overlap's drill-down: its place in the list and the path. */
68
+ export function overlapDrillHeading(index, total, path) {
69
+ return `overlap ${index + 1}/${total}: ${path} (Ctrl+N next)`;
70
+ }
13
71
  export function createPendingPane(session) {
14
72
  // Pending is the cycle's one async collector: collectTeamChanges awaits git plumbing,
15
73
  // so kick it off fire-and-forget, cache the rendered text when it resolves, and repaint.
@@ -20,21 +78,225 @@ export function createPendingPane(session) {
20
78
  // Guard on the PENDING pane still being current — view === roleIds.length + 5 — not just
21
79
  // on the raw index: an operator who cycled away, or a roster change that shifted which
22
80
  // pane the same index names, must not have this resolution overwrite the pane now shown.
81
+ // The overlaps are read by the same collectChangeOverlaps that `tumwater diff --overlaps`
82
+ // and `GET /api/diff?overlaps=1` serve, and rendered through the CLI's renderChangeOverlaps.
23
83
  // The default collector degrades rather than throws, so the rejection arm only covers an
24
84
  // injected seam (or an unexpected throw).
85
+ // Ctrl+N steps through the overlapping files, painting each holder's change limited to that
86
+ // file (the `tumwater diff --role a,b --path <path>` view); `base` is the pane text to
87
+ // return to after the last one. Ctrl+G cycles roleScope through each role the queue, the
88
+ // post-merge reviews and the overlaps name and back to all (the `queue --role <id>`,
89
+ // `reviews --role <id>` and `diff --overlaps --role <id>` twins, one scope covering all three
90
+ // surfaces); Ctrl+N then steps through only the scoped files,
91
+ // and the scope survives a refresh. The queue's roles lead the cycle, the review-only roles
92
+ // follow, then the overlap holders that are in neither, and each kept merge and each kept
93
+ // review holds its whole-queue position.
25
94
  let refreshGen = 0;
95
+ let base = "";
96
+ let lastOverlaps = null;
97
+ let lastQueue = null;
98
+ let lastReviews = [];
99
+ let rosterText = "";
100
+ let overlapIndex = -1;
101
+ let drillGen = 0;
102
+ let roleScope = null;
103
+ // The pane-local `--status` filter (the Ctrl+O twin of `tumwater reviews --status` and the
104
+ // dashboard State picker): null means all, otherwise one of waiting/failing/rejected. It
105
+ // composes with the Ctrl+G role scope in the review block's filter text but is surfaced only
106
+ // by that header, and it never moves the History pane's outcome filter (the Pending arm in
107
+ // tui-panes.ts is routed ahead of History's).
108
+ let reviewState = null;
109
+ let head = "";
110
+ // The Pending pane's row selection (null = list mode): the index into queueRows of the
111
+ // merge shown in full, cleared on every Ctrl+T cycle like selectedQueued. Each row pairs a
112
+ // kept entry with its 1-based whole-queue position (or the scope's kept positions when a
113
+ // Ctrl+G role scope is active), so a cancel addresses the entry `tumwater queue --cancel`
114
+ // numbers — the position the CLI's queue line printed.
115
+ let selectedQueue = null;
116
+ // The Pending pane's cached rows — the same whole-queue traversal renderMergeQueue painted,
117
+ // plus the post-merge review rows appended after them — so a selection addresses an entry's
118
+ // position without re-reading the queue on every frame.
119
+ let queueRows = [];
120
+ // The rendered detail of the selected row (the header, summary, and when present the
121
+ // entry's body and revision note); the pane body windows it in place of the queue list
122
+ // while a row is selected. Empty in list mode.
123
+ let queueDetail = "";
124
+ const compose = () => {
125
+ const block = lastOverlaps ? pendingOverlapsText(lastOverlaps, roleScope ?? undefined) : "";
126
+ base = head + (block ? "\n\n" + block : "");
127
+ };
128
+ const queueHead = () => {
129
+ if (lastQueue === null)
130
+ return "";
131
+ const inFlight = queueInFlight(session.root, lastQueue.entries, orchestratorAlive(session.root));
132
+ if (roleScope === null)
133
+ return renderMergeQueue(lastQueue, session.now(), inFlight);
134
+ const { view, scope } = scopeMergeQueue(lastQueue, [roleScope], (r) => r === roleScope);
135
+ return renderMergeQueue(view, session.now(), inFlight, scope);
136
+ };
137
+ // The one scoped traversal the review block's painting and the row selection both use, so
138
+ // the painted block and the selected row's accept position cannot drift: the whole list with
139
+ // no scope when neither the Ctrl+G role scope nor the Ctrl+O state filter is active, else
140
+ // `scopePostReviews`'s kept entries and scope (each kept review keeping its whole-queue
141
+ // position, the `tumwater reviews --role <id>` / `--status <state>` / composed twin). The
142
+ // whole-list path stays byte-identical so an unfiltered pane reads unchanged.
143
+ const scopedReviews = () => {
144
+ if (roleScope === null && reviewState === null)
145
+ return { entries: lastReviews };
146
+ const keep = (r) => (roleScope === null || r.role === roleScope) && (reviewState === null || postReviewState(r) === reviewState);
147
+ // The `--status <state>` filter text (or `--role <id> --status <state>` when composed with a
148
+ // Ctrl+G scope), passed only when a state filter is active: a role-only scope passes no filter
149
+ // so it keeps the `tumwater reviews --role <id>` twin's `no post-merge reviews for <id> …`
150
+ // no-match line (the CLI shares this path and passes no filter for `--role` alone).
151
+ const filterText = reviewState === null ? undefined : [roleScope !== null ? `--role ${roleScope}` : "", `--status ${reviewState}`].filter(Boolean).join(" ");
152
+ return scopePostReviews(lastReviews, roleScope === null ? [] : [roleScope], keep, filterText);
153
+ };
154
+ // The post-merge review block the pane paints under the merge queue and above the change
155
+ // roster: `tumwater reviews`' own renderer over the queue `readPostReviews` reads, so the
156
+ // TUI, the CLI, and the dashboard cannot drift on order or wording. Under a Ctrl+G scope it
157
+ // narrows to the scoped role's entries, each kept under its whole-queue position. Empty only
158
+ // when the whole queue is empty (no block, no header); a scope that keeps nothing still
159
+ // prints `renderPostReviews`'s no-match line — `no post-merge reviews for <id> …` for a
160
+ // role-only scope, `no post-merge reviews matching <filter> …` once a state filter joins.
161
+ const reviewsHead = () => {
162
+ if (lastReviews.length === 0)
163
+ return "";
164
+ const scoped = scopedReviews();
165
+ return renderPostReviews(scoped.entries, scoped.scope, session.now());
166
+ };
167
+ const queueAndReviews = () => {
168
+ const q = queueHead();
169
+ const r = reviewsHead();
170
+ return r === "" ? q : q + "\n\n" + r;
171
+ };
172
+ // The rendered detail of one selected row — the headline naming the entry with the position
173
+ // its kind's CLI command takes, then the summary and (for a merge) the body/revision/retry
174
+ // notes, so an operator deciding whether to cancel or accept sees what the entry is about
175
+ // before the second Ctrl+X/Ctrl+A. A review row never has a body or revision note: it names
176
+ // the accept action, the status suffix, and for a rejected entry why it cannot be accepted.
177
+ const rowDetail = (row) => {
178
+ if (row.kind === "merge") {
179
+ const e = row.entry;
180
+ const note = revisionNote(e);
181
+ const retry = retryNote(e, session.now());
182
+ const lines = [
183
+ `merge #${row.position} (Ctrl+X to cancel, Ctrl+V to top): ${roleLabel(e.role)} run #${e.run} · ${shortSha(e.sha)} · ${humanAgo(e.enqueuedAt, session.now())}`,
184
+ e.summary,
185
+ ];
186
+ if (e.body)
187
+ lines.push("", e.body);
188
+ if (note)
189
+ lines.push("", note);
190
+ if (retry)
191
+ lines.push("", retry);
192
+ return lines.join("\n");
193
+ }
194
+ const e = row.entry;
195
+ const status = postReviewStatusLabel(e);
196
+ const lines = [
197
+ `review #${row.position} (Ctrl+A to accept): ${roleLabel(e.role)} ${shortSha(e.commit)} · ${squash(e.summary, 80)}` + (status === "" ? "" : ` · ${status}`),
198
+ ];
199
+ return lines.join("\n");
200
+ };
201
+ // Resolve the selected row for a pending-pane action (Ctrl+X cancel, Ctrl+V top, Ctrl+A
202
+ // accept), or null when nothing is selected: a list-mode press just tells the operator how
203
+ // to select. Shared by the cancel, top, and accept handlers so the prompt and the row bound
204
+ // stay in one place.
205
+ const selectedRow = () => {
206
+ if (selectedQueue === null || queueRows.length === 0) {
207
+ session.flashMessage("select a merge with ↑↓ first");
208
+ session.requestRender();
209
+ return null;
210
+ }
211
+ return queueRows[Math.min(selectedQueue, queueRows.length - 1)];
212
+ };
213
+ // True when the queue shifted under the selection (a concurrent drain or another cancel)
214
+ // moved this entry away from `row.position`: refuse rather than act on the wrong entry.
215
+ // Shared by the cancel and top handlers for the one "the merge queue changed" wording.
216
+ const queueShiftedAway = (row) => {
217
+ if (queuedMergeFiles(session.root)[row.position - 1]?.entry.sha !== row.entry.sha) {
218
+ session.flashMessage("the merge queue changed — try again");
219
+ session.requestRender();
220
+ return true;
221
+ }
222
+ return false;
223
+ };
224
+ // Guard the cancel/top actions against racing a merge that just entered the slot: the
225
+ // selected entry (or one its changes touch) is heading into the merge right now, so acting
226
+ // on it would race the drain. Flash the pane's "being merged" message and return true so
227
+ // the caller aborts; otherwise false.
228
+ const guardMergeInFlight = (row) => {
229
+ const inFlight = queueInFlight(session.root, lastQueue?.entries ?? [], orchestratorAlive(session.root));
230
+ if (inFlight && (inFlight.sha === row.entry.sha || mergeChanges(inFlight).some((c) => c.sha === row.entry.sha))) {
231
+ session.flashMessage(`merge #${row.position} is being merged right now — wait for it to land, then \`tumwater revert ${shortSha(row.entry.sha)}\``);
232
+ session.requestRender();
233
+ return true;
234
+ }
235
+ return false;
236
+ };
237
+ // Re-read the pane's rows after the queue or the Ctrl+G scope changed (the same traversal
238
+ // queueHead painted) and clamp the row selection to the new list — a selection past the new
239
+ // end lands on the last remaining entry, never on a stale role/position; an emptied queue
240
+ // clears it back to list mode. The detail is then rebuilt to match the clamped row.
241
+ const rebuildQueueRows = () => {
242
+ if (lastQueue === null) {
243
+ queueRows = [];
244
+ selectedQueue = null;
245
+ queueDetail = "";
246
+ return;
247
+ }
248
+ if (roleScope === null) {
249
+ queueRows = lastQueue.entries.map((entry, i) => ({ kind: "merge", entry, position: i + 1 }));
250
+ }
251
+ else {
252
+ const { view, scope } = scopeMergeQueue(lastQueue, [roleScope], (r) => r === roleScope);
253
+ queueRows = view.entries.map((entry, i) => ({ kind: "merge", entry, position: scope.positions[i] }));
254
+ }
255
+ // The review block follows the same scope the painted head used, so a review row's
256
+ // position is its 1-based whole-queue position — the one `tumwater reviews` and
257
+ // `tumwater reviews --accept <n>` number — whether unscoped (i + 1) or kept by the scope's
258
+ // positions. Reviews follow the merge rows in the pane, exactly as the traversal painted.
259
+ const reviews = scopedReviews();
260
+ if (reviews.scope) {
261
+ reviews.entries.forEach((entry, i) => queueRows.push({ kind: "review", entry, position: reviews.scope.positions[i] }));
262
+ }
263
+ else {
264
+ reviews.entries.forEach((entry, i) => queueRows.push({ kind: "review", entry, position: i + 1 }));
265
+ }
266
+ if (selectedQueue !== null) {
267
+ if (queueRows.length === 0) {
268
+ selectedQueue = null;
269
+ queueDetail = "";
270
+ }
271
+ else {
272
+ selectedQueue = Math.min(selectedQueue, queueRows.length - 1);
273
+ queueDetail = rowDetail(queueRows[selectedQueue]);
274
+ }
275
+ }
276
+ };
26
277
  const refresh = () => {
27
278
  // Generation guard: each refresh bumps the counter, and a resolution paints only if it
28
279
  // is the latest request's — Ctrl+R held twice, or a refresh raced with a re-entry,
29
280
  // must not let an out-of-order older resolution overwrite the newer result.
30
281
  const gen = ++refreshGen;
31
- const queueView = readMergeQueue(session.root);
32
- const queueText = renderMergeQueue(queueView, session.now(), queueInFlight(session.root, queueView.entries, orchestratorAlive(session.root)));
33
- session.paneCache = queueText;
34
- void session.collectPending(session.root).then((team) => {
282
+ overlapIndex = -1;
283
+ lastOverlaps = null;
284
+ ++drillGen;
285
+ lastQueue = readMergeQueue(session.root);
286
+ lastReviews = readPostReviews(session.root);
287
+ rosterText = "";
288
+ rebuildQueueRows();
289
+ const queueText = queueAndReviews();
290
+ head = queueText;
291
+ session.paneCache = base = head;
292
+ void Promise.all([session.collectPending(session.root), session.collectOverlaps(session.root)]).then(([team, overlaps]) => {
35
293
  if (gen !== refreshGen || session.view !== session.roleIds.length + 5)
36
294
  return;
37
- session.paneCache = queueText + "\n\n" + renderTeamChange(team);
295
+ lastOverlaps = overlaps;
296
+ rosterText = "\n\n" + renderTeamChange(team);
297
+ head = queueAndReviews() + rosterText;
298
+ compose();
299
+ session.paneCache = base;
38
300
  session.requestRender();
39
301
  }, (err) => {
40
302
  if (gen !== refreshGen || session.view !== session.roleIds.length + 5)
@@ -57,6 +319,263 @@ export function createPendingPane(session) {
57
319
  session.requestRender();
58
320
  return true;
59
321
  }
322
+ if (key.ctrl && key.name === "g" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
323
+ const queueRoles = lastQueue ? mergeQueueRoles(lastQueue) : [];
324
+ const reviewRoles = postReviewRoles(lastReviews);
325
+ const holders = lastOverlaps?.state === "ready" ? overlapHolders(lastOverlaps) : [];
326
+ const roles = [
327
+ ...queueRoles,
328
+ ...reviewRoles.filter((r) => !queueRoles.includes(r)),
329
+ ...holders.filter((h) => !queueRoles.includes(h) && !reviewRoles.includes(h)),
330
+ ];
331
+ if (roles.length === 0) {
332
+ roleScope = null;
333
+ rebuildQueueRows();
334
+ head = queueAndReviews() + rosterText;
335
+ compose();
336
+ session.paneCache = base;
337
+ session.flashMessage("no queued merges, post-merge reviews or overlapping files to scope");
338
+ return true;
339
+ }
340
+ const slots = [null, ...roles];
341
+ const at = roleScope === null ? 0 : slots.indexOf(roleScope);
342
+ roleScope = slots[(at + 1) % slots.length] ?? null;
343
+ rebuildQueueRows();
344
+ overlapIndex = -1;
345
+ ++drillGen;
346
+ head = queueAndReviews() + rosterText;
347
+ compose();
348
+ session.paneCache = base;
349
+ session.paneScroll = 0;
350
+ session.requestRender();
351
+ return true;
352
+ }
353
+ if (key.ctrl && key.name === "o" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
354
+ // Ctrl+O on the Pending pane cycles the post-merge review state filter through all,
355
+ // waiting, failing and rejected and back — the `tumwater reviews --status` twin and the
356
+ // dashboard Post-merge reviews State picker, meaning the full state vocabulary even when
357
+ // the queue holds none of a state (the renderer's `no post-merge reviews matching
358
+ // --status …` line is the empty-selection state). Each kept review keeps its whole-queue
359
+ // position, so Ctrl+A still accepts the right entry under any filter. The pane's own arm
360
+ // is routed ahead of History's in tui-panes.ts, so it never advances the History outcome
361
+ // filter; the Ctrl+G role scope composes with this filter in the block's header.
362
+ const slots = [null, ...POST_REVIEW_STATES];
363
+ const at = reviewState === null ? 0 : slots.indexOf(reviewState);
364
+ reviewState = slots[(at + 1) % slots.length] ?? null;
365
+ rebuildQueueRows();
366
+ overlapIndex = -1;
367
+ ++drillGen;
368
+ head = queueAndReviews() + rosterText;
369
+ compose();
370
+ session.paneCache = base;
371
+ session.paneScroll = 0;
372
+ session.requestRender();
373
+ return true;
374
+ }
375
+ if (key.ctrl && key.name === "n" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
376
+ const overlaps = lastOverlaps && roleScope !== null ? scopeChangeOverlaps(lastOverlaps, [roleScope]) : lastOverlaps;
377
+ if (overlaps?.state !== "ready" || overlaps.overlaps.length === 0) {
378
+ session.flashMessage("no overlapping files to step through");
379
+ return true;
380
+ }
381
+ if (++overlapIndex >= overlaps.overlaps.length) {
382
+ overlapIndex = -1;
383
+ ++drillGen;
384
+ session.paneCache = base;
385
+ session.paneScroll = 0;
386
+ session.requestRender();
387
+ return true;
388
+ }
389
+ const { path, roles } = overlaps.overlaps[overlapIndex];
390
+ const heading = overlapDrillHeading(overlapIndex, overlaps.overlaps.length, path);
391
+ session.paneCache = heading + "\n\nloading…";
392
+ session.paneScroll = 0;
393
+ session.requestRender();
394
+ const gen = ++drillGen;
395
+ const refresh0 = refreshGen;
396
+ const current = () => gen === drillGen && refresh0 === refreshGen && session.view === session.roleIds.length + 5;
397
+ void Promise.all(roles.map((r) => session.collectChange(session.root, r, { path }))).then((views) => {
398
+ if (!current())
399
+ return;
400
+ session.paneCache = heading + "\n\n" + renderRoleChanges({ mainBranch: overlaps.mainBranch, roles: views });
401
+ session.requestRender();
402
+ }, (err) => {
403
+ if (!current())
404
+ return;
405
+ session.flashError(err);
406
+ session.requestRender();
407
+ });
408
+ return true;
409
+ }
410
+ const arrow = arrowDir(key.name);
411
+ if (session.view === session.roleIds.length + 5 && arrow !== null && promptLineFree(ctx.es)) {
412
+ // In the Pending pane, up/down select a queued merge row in full (wrapping, the backlog
413
+ // pane's rule) instead of editing the prompt; an empty queue has nothing to select, so
414
+ // moveEntrySelection clears the selection. The selected detail shares paneScroll with the
415
+ // list, so a newly opened row must start at its head, or it renders scrolled into the
416
+ // middle of the previous row's body.
417
+ selectedQueue = moveEntrySelection(queueRows.length, selectedQueue, arrow);
418
+ const selRow = selectedQueue === null ? undefined : queueRows[selectedQueue];
419
+ queueDetail = selRow ? rowDetail(selRow) : "";
420
+ session.paneScroll = 0;
421
+ session.requestRender();
422
+ return true;
423
+ }
424
+ if (key.name === "escape" && session.view === session.roleIds.length + 5 && selectedQueue !== null && promptLineFree(ctx.es)) {
425
+ // Esc on the Pending pane returns it to list mode; the cached list is untouched (nothing
426
+ // was cancelled), so the pane shows the same renderMergeQueue it entered with. The shared
427
+ // paneScroll resets with the body switch, like a selection move.
428
+ selectedQueue = null;
429
+ queueDetail = "";
430
+ session.paneScroll = 0;
431
+ session.requestRender();
432
+ return true;
433
+ }
434
+ if (key.ctrl && key.name === "x" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
435
+ const row = selectedRow();
436
+ if (row === null)
437
+ return true;
438
+ if (row.kind === "review") {
439
+ session.flashMessage("that row is a post-merge review — Ctrl+A accepts it");
440
+ session.requestRender();
441
+ return true;
442
+ }
443
+ if (session.armedQueue?.sha === row.entry.sha && session.armedQueue?.position === row.position) {
444
+ // Second press on the same entry: re-read the queue, guard the in-flight race, then
445
+ // remove it through the CLI's own core (`tumwater queue --cancel <n>`), so the pane,
446
+ // the CLI, and the dashboard cancel cannot drift.
447
+ const confirm = async () => {
448
+ if (queueShiftedAway(row))
449
+ return;
450
+ if (guardMergeInFlight(row))
451
+ return;
452
+ const outcome = await cancelQueuedMerge(session.root, row.position);
453
+ if (outcome.status === "cancelled") {
454
+ selectedQueue = null;
455
+ queueDetail = "";
456
+ session.flashMessage(`cancelled queue entry ${row.position}: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
457
+ refresh();
458
+ session.requestRender();
459
+ }
460
+ else {
461
+ selectedQueue = null;
462
+ queueDetail = "";
463
+ session.flashMessage("that merge already left the queue");
464
+ refresh();
465
+ session.requestRender();
466
+ }
467
+ };
468
+ void confirm();
469
+ }
470
+ else {
471
+ session.queueArmed = { position: row.position, sha: row.entry.sha };
472
+ session.flashMessage(`press Ctrl+X again to cancel merge #${row.position} — any other key keeps it`);
473
+ session.requestRender();
474
+ }
475
+ return true;
476
+ }
477
+ if (key.ctrl && key.name === "a" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
478
+ // Ctrl+A on the Pending pane accepts the selected post-merge review unreviewed (the
479
+ // terminal twin of `tumwater reviews --accept <n>`), once selected and twice pressed
480
+ // like the queue's Ctrl+X cancel. A merge row names its own action instead; an unarmed
481
+ // press sets reviewArmed, and the second press re-reads the queue and settles the entry
482
+ // through the CLI's own core, so the pane and the CLI cannot drift.
483
+ const review = selectedRow();
484
+ if (review === null)
485
+ return true;
486
+ if (review.kind === "merge") {
487
+ session.flashMessage("that row is a queued merge — Ctrl+X cancels it");
488
+ session.requestRender();
489
+ return true;
490
+ }
491
+ if (session.armedReview?.position === review.position && session.armedReview?.commit === review.entry.commit) {
492
+ const confirm = async () => {
493
+ const entries = readPostReviews(session.root);
494
+ if (entries[review.position - 1]?.commit !== review.entry.commit) {
495
+ session.flashMessage("the review queue changed — try again");
496
+ refresh();
497
+ session.requestRender();
498
+ return;
499
+ }
500
+ const outcome = acceptPostReview(session.root, review.position);
501
+ if (outcome.status === "accepted") {
502
+ selectedQueue = null;
503
+ queueDetail = "";
504
+ session.flashMessage(`accepted post-merge review of ${shortSha(outcome.commit)} unreviewed — ${outcome.summary}`);
505
+ refresh();
506
+ session.requestRender();
507
+ }
508
+ else if (outcome.status === "settled") {
509
+ session.flashMessage(`review #${review.position} already merged its fix on main — nothing to accept`);
510
+ refresh();
511
+ session.requestRender();
512
+ }
513
+ else {
514
+ session.flashMessage("the review queue changed — try again");
515
+ refresh();
516
+ session.requestRender();
517
+ }
518
+ };
519
+ void confirm();
520
+ }
521
+ else {
522
+ session.reviewArmed = { position: review.position, commit: review.entry.commit };
523
+ session.flashMessage(`press Ctrl+A again to accept review #${review.position} — any other key keeps it`);
524
+ session.requestRender();
525
+ }
526
+ return true;
527
+ }
528
+ if (key.ctrl && key.name === "v" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
529
+ // Move the selected merge to the head of the queue — the pane's twin of `tumwater
530
+ // queue --top <n>`, through the same core the CLI and the dashboard use, so the three
531
+ // cannot drift on the rename or the already-first/out-of-range outcomes. A single
532
+ // keypress is enough because the move is reversible and non-destructive (unlike
533
+ // Ctrl+X's two-step cancel arm).
534
+ const row = selectedRow();
535
+ if (row === null)
536
+ return true;
537
+ if (row.kind === "review") {
538
+ session.flashMessage("that row is a post-merge review — Ctrl+A accepts it");
539
+ session.requestRender();
540
+ return true;
541
+ }
542
+ const top = async () => {
543
+ if (row.position === 1) {
544
+ session.flashMessage("merge #1 is already at the front");
545
+ session.requestRender();
546
+ return;
547
+ }
548
+ if (queueShiftedAway(row))
549
+ return;
550
+ if (guardMergeInFlight(row))
551
+ return;
552
+ const outcome = moveQueuedMergeToTop(session.root, row.position);
553
+ if (outcome.status === "topped") {
554
+ selectedQueue = null;
555
+ queueDetail = "";
556
+ session.flashMessage(`moved merge #${row.position} to the front: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
557
+ refresh();
558
+ session.requestRender();
559
+ }
560
+ else if (outcome.status === "already-first") {
561
+ session.flashMessage(`merge #${row.position} is already at the front`);
562
+ refresh();
563
+ session.requestRender();
564
+ }
565
+ else if (outcome.status === "out-of-range") {
566
+ session.flashMessage("the merge queue changed — try again");
567
+ refresh();
568
+ session.requestRender();
569
+ }
570
+ else {
571
+ session.flashMessage("that merge already left the queue");
572
+ refresh();
573
+ session.requestRender();
574
+ }
575
+ };
576
+ void top();
577
+ return true;
578
+ }
60
579
  return false;
61
580
  };
62
581
  return {
@@ -67,8 +586,13 @@ export function createPendingPane(session) {
67
586
  refresh();
68
587
  return true;
69
588
  },
589
+ reset() {
590
+ // Leaving a view drops any queued-merge selection; re-entering starts at the list.
591
+ selectedQueue = null;
592
+ queueDetail = "";
593
+ },
70
594
  state() {
71
- return {};
595
+ return { selectedQueue, queueDetail };
72
596
  },
73
597
  };
74
598
  }