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,12 +1,35 @@
1
- // vocabulary: plain
2
- /** The dashboard's Pending view, browser-side: every loop's unmerged change as /api/diff
1
+ /** The dashboard's Pending view, browser-side: every role's unmerged change as /api/diff
3
2
  * serves it (the same document `tumwater diff` prints from the shared change collectors). The
4
3
  * roster shows each role's branch, state, commits ahead of main with their subjects, and its
5
- * uncommitted-file count; a row opens that loop's drawer, whose Pending change section fetches
4
+ * uncommitted-file count; a row opens that role's drawer, whose Pending change section fetches
6
5
  * the full patch (`/api/diff?role=<id>`). It refetches when the view opens and when its
7
- * Refresh button is pressed — never on a timer of its own. Spliced into gui-client.ts's
8
- * script, reaching its helpers (esc, getJson, icon, errorPanel) through that concatenation. */
6
+ * Refresh button is pressed — never on a timer of its own. It also paints the overlapping-files
7
+ * card from `/api/diff?overlaps=1`, with Role and Folder pickers that narrow it client-side,
8
+ * and the Merge queue card from the status payload, with a Role picker that keeps only the
9
+ * chosen role's waiting merges under their whole-queue positions (the tumwater queue --role
10
+ * twin) and a per-row Move to top / two-click Cancel pair on every non-first entry, and the
11
+ * Post-merge reviews card from the status payload, with a Role picker that keeps only
12
+ * the chosen role's entries (`tumwater reviews --role` twin). Spliced
13
+ * into gui-client.ts's script, reaching its helpers (esc, getJson, icon,
14
+ * errorPanel) through that concatenation. */
9
15
  export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
16
+ let pendingOverlaps = null;
17
+ let pendingOverlapOpen = null;
18
+ let pendingOverlapRole = "";
19
+ let pendingOverlapDir = "";
20
+ let pendingQueueRole = "";
21
+ let pendingReviewRole = "";
22
+ let pendingReviewState = "";
23
+ let pendingCancelSha = "";
24
+ // The Merge queue card's per-row cancel, armed by a first click and fired by a second within
25
+ // 5 s — the same confirmSlot two-click factory the drawer's note-clear/entry-cancel use, so a
26
+ // stray click cannot remove an entry. Repaints the card (an armed row reads Confirm cancel).
27
+ const queueCancelSlot = confirmSlot(() => renderPendingQueue(lastStatus));
28
+ let pendingAcceptCommit = "";
29
+ // The Post-merge reviews card's per-row accept, armed by a first click and fired by a second
30
+ // within 5 s — the same confirmSlot two-click factory, so a stray click cannot accept an
31
+ // entry unreviewed. Repaints the card (an armed row reads Confirm accept).
32
+ const reviewAcceptSlot = confirmSlot(() => renderPendingReviews(lastStatus));
10
33
  // pending-table:start
11
34
  // One role's state cell: the collector's three states, with "ready" split into pending vs
12
35
  // idle work. Never colors a degraded state as an active one.
@@ -30,7 +53,7 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
30
53
  return "<span class='num' title='" + esc(files.join(", ")) + "'>" + esc(plural(files.length, "file")) + "</span>";
31
54
  }
32
55
  // The Pending roster: one clickable row per known role (the collector's config order), each
33
- // opening that loop's drawer. Pure — the view paints what this returns.
56
+ // opening that role's drawer. Pure — the view paints what this returns.
34
57
  function pendingTableHtml(team) {
35
58
  if (!team || !Array.isArray(team.roles) || team.roles.length === 0)
36
59
  return "<div class='empty'><strong>No roles yet</strong>Roles appear here once the team is configured.</div>";
@@ -44,37 +67,268 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
44
67
  "<td>" + pendingAheadHtml(r) + "</td>" +
45
68
  "<td class='num'>" + pendingDirtyHtml(r) + "</td></tr>",
46
69
  ).join("");
47
- return "<div class='table-wrap'><table class='table pending'><thead><tr><th class='c-loop'>Role</th><th>Branch</th>" + // vocabulary-ok: CSS class the stylesheet matches
70
+ return "<div class='table-wrap'><table class='table pending'><thead><tr><th class='c-role'>Role</th><th>Branch</th>" +
48
71
  "<th>State</th><th>Ahead of " + esc(team.mainBranch) + "</th><th class='c-dirty num'>Uncommitted</th></tr></thead><tbody>" + rows + "</tbody></table></div>";
49
72
  }
73
+ // One queued merge's line: the 1-based whole-queue position, role, run, short sha, age, an
74
+ // optional merge label, and summary. Shared by the Merge queue card and the role drawer so
75
+ // the two cannot drift; a malformed entry renders stand-ins rather than undefined/NaN.
76
+ function queueEntryLineHtml(e, position, now, label, cancel, top) {
77
+ const sec = Number.isFinite(e.enqueuedAt) ? Math.max(0, Math.round((now - e.enqueuedAt) / 1000)) : 0;
78
+ const run = Number.isFinite(e.run) ? e.run : "?";
79
+ const note = Number.isFinite(e.revisionRound) ? " · revision " + e.revisionRound + (Number.isFinite(e.priorObjections) && e.priorObjections > 0 ? " · " + plural(e.priorObjections, "prior objection") : "") : "";
80
+ const retry = typeof e.retry === "string" ? " · re-queued after " + e.retry + (Number.isFinite(e.retryCount) && e.retryCount > 1 ? " · ×" + e.retryCount : "") + (Number.isFinite(e.notBefore) && e.notBefore > now ? " · eligible in " + humanSeconds(Math.max(0, Math.round((e.notBefore - now) / 1000))) : "") : "";
81
+ let line = "<div class='mono clamp1' style='padding:3px 0'>" + position + ". " + esc(e.role ? roleLabel(e.role) : "?") +
82
+ " run #" + esc(run) + " · " + esc(shortSha(e.sha)) + " · " + esc(humanSeconds(sec)) +
83
+ " ago" + (label && typeof label === "string" ? " · [" + esc(label) + "]" : "") +
84
+ (e.highFriction === true ? " · high friction" : "") +
85
+ retry +
86
+ note +
87
+ " · " + esc(e.summary || "—") + "</div>";
88
+ // A reorder button (Move to top) on every non-first row of the Merge queue card, before
89
+ // the two-click cancel: the move is reversible and non-destructive, so a single click is
90
+ // enough (unlike Cancel's two-click arm). The drawer (this role's merges) passes no top,
91
+ // so its rows carry no reorder.
92
+ if (top && position > 1 && typeof e.sha === "string") {
93
+ line = "<button type='button' class='btn btn-sm' data-top='" + esc(e.sha) + "'>Move to top</button>" + line;
94
+ }
95
+ // A two-click cancel: armed by the first click, fired by the second within 5 s (the
96
+ // confirmSlot guard), so a stray click cannot remove an entry. A malformed entry carries
97
+ // no sha, so it renders no button.
98
+ if (cancel && typeof cancel.sha === "string") {
99
+ return line + "<button type='button' class='btn btn-sm' data-cancel='" + esc(cancel.sha) + "'>" + (cancel.armed ? "Confirm cancel" : "Cancel") + "</button>";
100
+ }
101
+ return line;
102
+ }
103
+ // The todo/queued merge entries of a status merge queue document, or [] when the document
104
+ // is null/absent or its entries are missing or malformed — so callers never guard the shape.
105
+ function mergeQueueEntries(mergeQueue) {
106
+ return mergeQueue && Array.isArray(mergeQueue.entries) ? mergeQueue.entries : [];
107
+ }
108
+ // The distinct role names a list of queue/review entries mentions (each entry's role when
109
+ // present; null/undefined entries yield nothing), as a Set for .has() picker checks. Every
110
+ // role picker here — the Merge queue card, the Post-merge reviews card, and the drawer's
111
+ // section — derives it, so one definition of "the roles present in this list" cannot drift.
112
+ // Sort into a checked array where a stable picker order is wanted:
113
+ // [...presentRoles(e)].sort().
114
+ function presentRoles(entries) {
115
+ return new Set(entries.map((e) => e && e.role).filter((r) => typeof r === "string"));
116
+ }
117
+ // The role drawer's Merge queue section: only this role's waiting merges, each under its
118
+ // whole-queue position (labels run parallel to the whole queue). "" when it has none.
119
+ function drawerQueueHtml(mergeQueue, role, now, labels) {
120
+ const entries = mergeQueueEntries(mergeQueue);
121
+ let kept = 0;
122
+ const lines = entries.map((e, i) => {
123
+ if (!e || e.role !== role) return "";
124
+ kept++;
125
+ return queueEntryLineHtml(e, i + 1, now, Array.isArray(labels) ? labels[i] : undefined);
126
+ }).join("");
127
+ if (!kept) return "";
128
+ return "<section><div class='sec-head'><h3>Merge queue</h3><span class='muted' style='font-size:13px'>" +
129
+ esc(kept + " of " + entries.length + " waiting") + "</span></div><div style='padding:4px 0'>" + lines + "</div></section>";
130
+ }
50
131
  // The Merge queue card's body: every queued merge in execution order (oldest first), one
51
- // line each mirroring tumwater queue's renderMergeQueue — 1-based position, loop, tick,
132
+ // line each mirroring tumwater queue's renderMergeQueue — 1-based position, role, run,
52
133
  // short sha, age, summary — so the page and the CLI cannot drift. Painted from the status
53
134
  // payload the page already polls; depth counts the unreadable queue files the status reader
54
135
  // skips, so a positive depth with no readable list states the count instead of inventing rows.
55
136
  // Pure: the age comes from the passed now, and a malformed entry renders a stand-in rather
56
- // than undefined/NaN. Null/absent/zero-depth reads as empty.
57
- function pendingQueueHtml(mergeQueue, now, labels) {
137
+ // than undefined/NaN. Null/absent/zero-depth reads as empty. Its Role picker keeps only the
138
+ // chosen role's waiting merges, each under its position in the whole queue (the
139
+ // tumwater queue --role twin); "All roles" (the default) shows every line.
140
+ function pendingQueueHtml(mergeQueue, now, labels, role = "", cancelArmedSha = "") {
58
141
  const depth = mergeQueue && Number.isFinite(mergeQueue.depth) ? mergeQueue.depth : 0;
59
- const entries = mergeQueue && Array.isArray(mergeQueue.entries) ? mergeQueue.entries : [];
142
+ const entries = mergeQueueEntries(mergeQueue);
143
+ const roles = [...presentRoles(entries)].sort();
144
+ const active = role && roles.includes(role) ? role : "";
145
+ let head = "<div class='card-head'><h2>Merge queue</h2></div>";
60
146
  let body;
61
147
  if (depth === 0) {
62
148
  body = "<div class='empty'>Merge queue empty</div>";
63
149
  } else if (entries.length === 0) {
64
150
  body = "<div class='empty'>merge queue: " + esc(depth) + " waiting</div>";
65
151
  } else {
66
- body = "<div style='padding:10px 16px 12px'>" + entries.map((e, i) => {
67
- const sec = Number.isFinite(e.enqueuedAt) ? Math.max(0, Math.round((now - e.enqueuedAt) / 1000)) : 0;
68
- const run = Number.isFinite(e.run) ? e.run : "?";
69
- const note = Number.isFinite(e.revisionRound) ? " · revision " + e.revisionRound + (Number.isFinite(e.priorObjections) && e.priorObjections > 0 ? " · " + plural(e.priorObjections, "prior objection") : "") : "";
70
- return "<div class='mono clamp1' style='padding:3px 0'>" + (i + 1) + ". " + esc(e.role ? roleLabel(e.role) : "?") +
71
- " run #" + esc(run) + " · " + esc(shortSha(e.sha)) + " · " + esc(humanSeconds(sec)) +
72
- " ago" + (Array.isArray(labels) && labels[i] && typeof labels[i] === "string" ? " · [" + esc(labels[i]) + "]" : "") +
73
- note +
74
- " · " + esc(e.summary || "—") + "</div>";
75
- }).join("") + "</div>";
152
+ if (roles.length) {
153
+ head = "<div class='card-head'><h2>Merge queue</h2><label><span class='sr-only'>Role</span><select class='field' id='pendingqueuerole'><option value=''>All roles</option>" +
154
+ roles.map((id) => "<option value='" + esc(id) + "'" + (id === active ? " selected" : "") + ">" + esc(roleLabel(id)) + "</option>").join("") + "</select></label></div>";
155
+ }
156
+ body = "<div style='padding:10px 16px 12px'>" + entries.map((e, i) => ({ e, i })).filter(({ e }) => !active || (e && e.role === active)).map(({ e, i }) =>
157
+ queueEntryLineHtml(e, i + 1, now, Array.isArray(labels) ? labels[i] : undefined, { sha: e && e.sha, armed: e && e.sha === cancelArmedSha }, true)).join("") + "</div>";
158
+ }
159
+ return head + body;
160
+ }
161
+ // The Overlapping files card: one "path: role, role" line per file two or more pending changes
162
+ // touch (tumwater diff --overlaps). Pure; a malformed document reads as no overlaps. Its Role
163
+ // picker keeps only the lines the chosen role holds (diff --overlaps --role), each line still
164
+ // naming every holder and keeping its index in the unfiltered list. Its Folder picker keeps only
165
+ // the lines at or under the chosen directory (diff --overlaps --path).
166
+ // The expanded detail under one overlap line: each holding role's path-scoped change, or its
167
+ // loading / error row. Pure.
168
+ function overlapDetailHtml(rows) {
169
+ return (Array.isArray(rows) ? rows : []).map((r) => {
170
+ let body;
171
+ if (r.state === "loading") body = "<p class='muted'>Loading…</p>";
172
+ else if (r.state === "error") body = "<p class='muted'>" + esc(r.message || "Pending change unavailable") + "</p>";
173
+ else body = drawerChangeHtml(r.view);
174
+ return "<h3 class='mono'>" + esc(roleLabel(r.role)) + "</h3>" + body;
175
+ }).join("");
176
+ }
177
+ // The sorted, de-duplicated role ids any overlap names; empty for a malformed document.
178
+ function pendingOverlapHolders(view) {
179
+ if (!view || typeof view !== "object" || !Array.isArray(view.overlaps)) return [];
180
+ const ids = new Set();
181
+ for (const o of view.overlaps) {
182
+ if (o && Array.isArray(o.roles)) for (const id of o.roles) if (typeof id === "string") ids.add(id);
76
183
  }
77
- return "<div class='card-head'><h2>Merge queue</h2></div>" + body;
184
+ return [...ids].sort();
185
+ }
186
+ // The sorted, de-duplicated directories holding any overlap path; empty for a malformed
187
+ // document. A trailing-slash path (an untracked directory) also adds itself.
188
+ function pendingOverlapDirs(view) {
189
+ if (!view || typeof view !== "object" || !Array.isArray(view.overlaps)) return [];
190
+ const dirs = new Set();
191
+ for (const o of view.overlaps) {
192
+ if (!o || typeof o.path !== "string") continue;
193
+ const dir = o.path.endsWith("/");
194
+ const parts = (dir ? o.path.slice(0, -1) : o.path).split("/");
195
+ const n = dir ? parts.length : parts.length - 1;
196
+ for (let k = 1; k <= n; k++) dirs.add(parts.slice(0, k).join("/"));
197
+ }
198
+ return [...dirs].sort();
199
+ }
200
+ function pendingOverlapsHtml(view, open, role, dir) {
201
+ const ok = view && typeof view === "object";
202
+ let body;
203
+ let head = "<div class='card-head'><h2>Overlapping files</h2></div>";
204
+ if (ok && view.state === "no-base") {
205
+ body = "<div class='empty'>Main branch " + esc(view.mainBranch) + " does not exist</div>";
206
+ } else if (!ok || !Array.isArray(view.overlaps) || view.overlaps.length === 0) {
207
+ body = "<div class='empty'>No file is touched by two pending changes</div>";
208
+ } else {
209
+ const holders = pendingOverlapHolders(view);
210
+ const active = role && holders.includes(role) ? role : "";
211
+ const dirs = pendingOverlapDirs(view);
212
+ const activeDir = dir && dirs.includes(dir) ? dir : "";
213
+ head = "<div class='card-head'><h2>Overlapping files</h2><label><span class='sr-only'>Role</span><select class='field' id='pendingoverlaprole'><option value=''>All roles</option>" +
214
+ holders.map((id) => "<option value='" + esc(id) + "'" + (id === active ? " selected" : "") + ">" + esc(roleLabel(id)) + "</option>").join("") + "</select></label>" +
215
+ (dirs.length ? "<label><span class='sr-only'>Folder</span><select class='field' id='pendingoverlapdir'><option value=''>All folders</option>" +
216
+ dirs.map((d) => "<option value='" + esc(d) + "'" + (d === activeDir ? " selected" : "") + ">" + esc(d + "/") + "</option>").join("") + "</select></label>" : "") + "</div>";
217
+ const lines = view.overlaps.map((o, i) => {
218
+ const roles = o && Array.isArray(o.roles) ? o.roles : [];
219
+ if (active && !roles.includes(active)) return "";
220
+ const path = o && typeof o.path === "string" ? o.path : "?";
221
+ if (activeDir && !path.startsWith(activeDir + "/")) return "";
222
+ const line = "<button type='button' class='li-open mono clamp1' data-overlap='" + i + "'>" + esc(path) + ": " + esc(roles.map(roleLabel).join(", ")) + "</button>";
223
+ return open && open.path === path ? line + "<div class='overlap-detail'>" + overlapDetailHtml(open.rows) + "</div>" : line;
224
+ }).join("");
225
+ body = lines ? "<div style='padding:10px 16px 12px'>" + lines + "</div>" : "<div class='empty'>No overlapping file matches the filters</div>";
226
+ }
227
+ return head + body;
228
+ }
229
+ // The Post-merge reviews card: every well-formed entry in .tumwater/state/post-review.json
230
+ // in FIFO order, one line each mirroring 'tumwater reviews' renderPostReviews — role, short
231
+ // commit, summary, and the status suffix from the browser twin of postReviewStatusLabel — so
232
+ // the page and the CLI cannot drift on order or wording. Painted from the status payload the
233
+ // page already polls. Empty (or a missing/malformed list) shows the empty line. A malformed
234
+ // entry renders a stand-in rather than undefined/NaN.
235
+ // The status suffix — the browser twin of postReviewStatusLabel
236
+ // (src/review/post-merge-review-render.ts):
237
+ // the reviewer's recorded rejected reasons when 'rejected' is set, else the
238
+ // verdict-less failed-review-run count when positive, else "". Hand-kept copy because the
239
+ // script cannot import the server renderer; pinned by the client test against the CLI wording.
240
+ const squashText = (s, max) => {
241
+ const t = String(s).replace(/\s+/g, " ").trim();
242
+ if (t.length <= max) return t;
243
+ return t.slice(0, max - 1).trimEnd() + "…";
244
+ };
245
+ // The states the Post-merge reviews card's State picker lists, in the order 'tumwater reviews
246
+ // --status' names them — the browser twin of POST_REVIEW_STATES
247
+ // (src/review/post-merge-review-render.ts), so the page and the CLI cannot drift on what a
248
+ // state means. Hand-kept because the inline script cannot import the server renderer.
249
+ const POST_REVIEW_STATE_ORDER = ["waiting", "failing", "rejected", "fixed"];
250
+ // One post-merge review entry's state — the browser twin of the server's 'postReviewState'
251
+ // (src/review/post-merge-review-render.ts), literal in its 'rejected' branch: 'fixed' when
252
+ // the repair already merged (the terminal Forward-never-back 1a/6 record — naming both shas,
253
+ // it is not pending anything), 'rejected' when 'rejected' is set (an empty array still
254
+ // counts; statusPayload's entries come from readPostReviews, which only ever stores an array
255
+ // or leaves it off), else 'failing' when the review runs keep ending without a verdict
256
+ // ('failures' positive), else 'waiting'. The precedence matches postReviewStatusLabel and
257
+ // 'tumwater reviews --status'; pinned against the server predicate by test so the browser and
258
+ // the server cannot disagree on what fixed/rejected/failing/waiting mean.
259
+ function postReviewState(e) {
260
+ if (e && e.fixed !== undefined) return "fixed";
261
+ if (e && e.rejected !== undefined) return "rejected";
262
+ if (e && typeof e.failures === "number" && e.failures > 0) return "failing";
263
+ return "waiting";
264
+ }
265
+ function postReviewStatusLabel(e) {
266
+ switch (postReviewState(e)) {
267
+ case "fixed":
268
+ return "[fixed — the repair " + shortSha(e.fixed) + " already merged on main]";
269
+ case "rejected":
270
+ return "[rejected — " + e.rejected.map((r) => squashText(r, 60)).join("; ") + "]";
271
+ case "failing":
272
+ if (e && typeof e.lastFailure === "string" && e.lastFailure.length > 0) {
273
+ return "[" + plural(e.failures, "failed review run") + " — last: " + squashText(e.lastFailure, 80) + "]";
274
+ }
275
+ return "[" + plural(e.failures, "failed review run") + "]";
276
+ default:
277
+ return "";
278
+ }
279
+ }
280
+ function postReviewsHtml(entries, armed = "", role = "", state = "", now = 0) {
281
+ if (!Array.isArray(entries) || entries.length === 0) {
282
+ return "<div class='card-head'><h2>Post-merge reviews</h2></div><div class='empty'>No merged commit is waiting for a post-merge review</div>";
283
+ }
284
+ const roles = [...presentRoles(entries)].sort();
285
+ const active = role && roles.includes(role) ? role : "";
286
+ const present = new Set(entries.map(postReviewState));
287
+ const states = POST_REVIEW_STATE_ORDER.filter((s) => present.has(s));
288
+ const activeState = state && states.includes(state) ? state : "";
289
+ let head = "<div class='card-head'><h2>Post-merge reviews</h2></div>";
290
+ if (roles.length) {
291
+ head = "<div class='card-head'><h2>Post-merge reviews</h2><label><span class='sr-only'>Role</span><select class='field' id='pendingreviewrole'><option value=''>All roles</option>" +
292
+ roles.map((id) => "<option value='" + esc(id) + "'" + (id === active ? " selected" : "") + ">" + esc(roleLabel(id)) + "</option>").join("") + "</select></label>" +
293
+ (states.length ? "<label><span class='sr-only'>State</span><select class='field' id='pendingreviewstate'><option value=''>All states</option>" +
294
+ states.map((s) => "<option value='" + s + "'" + (s === activeState ? " selected" : "") + ">" + s + "</option>").join("") + "</select></label>" : "") + "</div>";
295
+ }
296
+ const kept = entries.filter((e) => (!active || (e && e.role === active)) && (!activeState || postReviewState(e) === activeState));
297
+ const lines = kept.map((e) => {
298
+ const label = postReviewStatusLabel(e);
299
+ const age = typeof e.enqueuedAt === "number" && Number.isFinite(now)
300
+ ? " · " + esc(humanSeconds(Math.max(0, Math.round((now - e.enqueuedAt) / 1000)))) + " ago"
301
+ : "";
302
+ let line = "<div class='queue-entry mono clamp1'>" + esc(e.role ? roleLabel(e.role) : "?") + " " + esc(shortSha(e.commit)) +
303
+ age +
304
+ " · " + esc(e.summary || "—") +
305
+ (label ? " · <span class='mono'>" + esc(label) + "</span>" : "") + "</div>";
306
+ // A two-click Accept on every well-formed entry not already settled fixed (the server
307
+ // refuses a fixed entry, so it renders no button): armed by the first
308
+ // click, fired by the second within 5 s (the confirmSlot guard), so a stray click cannot
309
+ // accept an entry unreviewed. Threaded the way pendingQueueHtml threads cancelArmedSha.
310
+ if (e && e.fixed === undefined && typeof e.commit === "string") {
311
+ line += "<button type='button' class='btn btn-sm' data-accept='" + esc(e.commit) + "'>" + (armed === e.commit ? "Confirm accept" : "Accept") + "</button>";
312
+ }
313
+ return line;
314
+ }).join("");
315
+ if (kept.length === 0) {
316
+ return head + "<div class='queue-head'>post-merge reviews (0 of " + entries.length + "):</div><div class='empty'>No post-merge review matches the filters</div>";
317
+ }
318
+ return head +
319
+ "<div class='queue-head'>post-merge reviews (" + esc(active || activeState ? kept.length + " of " + entries.length : entries.length) + "):</div>" + lines;
320
+ }
321
+ // Paint the Post-merge reviews card from the polled status payload — the dashboard twin of
322
+ // 'tumwater reviews', driven by the raw postReviews list statusPayload already carries.
323
+ function renderPendingReviews(status) {
324
+ const card = $("pendingreviews");
325
+ if (!card) return;
326
+ const entries = status && Array.isArray(status.postReviews) ? status.postReviews : [];
327
+ const roles = presentRoles(entries);
328
+ if (!roles.has(pendingReviewRole)) pendingReviewRole = "";
329
+ const states = new Set(entries.map(postReviewState));
330
+ if (!states.has(pendingReviewState)) pendingReviewState = "";
331
+ card.innerHTML = postReviewsHtml(entries, reviewAcceptSlot.armed(pendingAcceptCommit) ? pendingAcceptCommit : "", pendingReviewRole, pendingReviewState, Date.now());
78
332
  }
79
333
  // pending-table:end
80
334
  // Paint the Merge queue card from the polled status payload, at paint-time age; no fetch of
@@ -82,19 +336,145 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
82
336
  function renderPendingQueue(status) {
83
337
  const card = $("pendingqueue");
84
338
  if (!card) return;
85
- card.innerHTML = pendingQueueHtml(status && status.mergeQueue, Date.now(), status && status.mergeQueueLabels);
339
+ const entries = mergeQueueEntries(status && status.mergeQueue);
340
+ const roles = presentRoles(entries);
341
+ if (!roles.has(pendingQueueRole)) pendingQueueRole = "";
342
+ card.innerHTML = pendingQueueHtml(status && status.mergeQueue, Date.now(), status && status.mergeQueueLabels, pendingQueueRole, queueCancelSlot.armed(pendingCancelSha) ? pendingCancelSha : "");
343
+ }
344
+ function paintPendingOverlaps() {
345
+ const card = $("pendingoverlaps");
346
+ if (card) card.innerHTML = pendingOverlapsHtml(pendingOverlaps, pendingOverlapOpen, pendingOverlapRole, pendingOverlapDir);
347
+ }
348
+ // Toggle one overlap line's detail: fetch each holding role's change scoped to the path. A
349
+ // reply is dropped once another line (or a Refresh) replaced the open detail.
350
+ async function togglePendingOverlap(index) {
351
+ const o = pendingOverlaps && Array.isArray(pendingOverlaps.overlaps) ? pendingOverlaps.overlaps[index] : null;
352
+ if (!o || typeof o.path !== "string") return;
353
+ const path = o.path;
354
+ if (pendingOverlapOpen && pendingOverlapOpen.path === path) {
355
+ pendingOverlapOpen = null;
356
+ paintPendingOverlaps();
357
+ return;
358
+ }
359
+ const roles = Array.isArray(o.roles) ? o.roles : [];
360
+ const open = { path, rows: roles.map((role) => ({ role, state: "loading" })) };
361
+ pendingOverlapOpen = open;
362
+ paintPendingOverlaps();
363
+ await Promise.all(roles.map(async (role, k) => {
364
+ let row;
365
+ try {
366
+ const view = await getJson("/api/diff?role=" + encodeURIComponent(role) + "&path=" + encodeURIComponent(path));
367
+ row = { role, state: "ok", view };
368
+ } catch (err) {
369
+ row = { role, state: "error", message: errMessage(err, "Pending change unavailable") };
370
+ }
371
+ if (pendingOverlapOpen !== open) return;
372
+ open.rows[k] = row;
373
+ paintPendingOverlaps();
374
+ }));
86
375
  }
87
376
  function buildPendingView() {
88
377
  if (!buildOnce("pending", "<div class='view-head'><div><h1>Pending</h1><p>Every role's unmerged change — its branch, commits ahead of main, and uncommitted files. Open a row for the full diff.</p></div>" +
89
378
  "<div class='toolbar'><button type='button' class='btn btn-sm' id='pendingrefresh'>" + icon("refresh") + "Refresh</button></div></div>" +
90
379
  "<div class='card' id='pendingqueue'><div class='empty'>Loading…</div></div>" +
380
+ "<div class='card' id='pendingreviews'><div class='empty'>Loading…</div></div>" +
381
+ "<div class='card' id='pendingoverlaps'><div class='empty'>Loading…</div></div>" +
91
382
  "<div class='card' id='pendingbody'><div class='empty'>Loading…</div></div>")) return;
92
383
  $("pendingrefresh").addEventListener("click", () => fetchPending());
384
+ // A Merge queue row's Cancel: the first click on an unarmed sha arms it (repainting the
385
+ // button as Confirm cancel), a second on the armed sha within 5 s posts /api/queue-cancel
386
+ // then clears the arm and refetches so the removed row disappears at once.
387
+ $("pendingqueue").addEventListener("click", async (e) => {
388
+ // A Merge queue row's Move to top: a single click posts /api/queue-top by sha, moving
389
+ // that entry to the head of the queue and refetching (the move is reversible and
390
+ // non-destructive, so it needs no two-step arm).
391
+ const t = e.target.closest("[data-top]");
392
+ if (t) {
393
+ const sha = t.getAttribute("data-top");
394
+ if (sha) {
395
+ await postAction("/api/queue-top", { sha }, (d) => d && d.status === "topped" ? "moved the merge of " + shortSha(d.commit) + " to the front" : d && d.status === "already-first" ? "that merge is already at the front" : "that merge already left the queue");
396
+ fetchPending();
397
+ }
398
+ return;
399
+ }
400
+ const b = e.target.closest("[data-cancel]");
401
+ if (!b) return;
402
+ const sha = b.getAttribute("data-cancel");
403
+ if (!sha) return;
404
+ if (queueCancelSlot.armed(sha)) {
405
+ await postAction("/api/queue-cancel", { sha }, (d) => d && d.status === "cancelled" ? "cancelled the merge of " + shortSha(d.commit) : "that merge already left the queue");
406
+ pendingCancelSha = "";
407
+ queueCancelSlot.disarm();
408
+ fetchPending();
409
+ } else {
410
+ pendingCancelSha = sha;
411
+ queueCancelSlot.arm(sha);
412
+ }
413
+ });
414
+ // A Post-merge reviews row's Accept: the first click on an unarmed commit arms it
415
+ // (repainting that button as Confirm accept), a second on the armed commit within 5 s posts
416
+ // /api/post-review-accept then clears the arm and refetches so the removed row disappears.
417
+ $("pendingreviews").addEventListener("click", async (e) => {
418
+ const b = e.target.closest("[data-accept]");
419
+ if (!b) return;
420
+ const commit = b.getAttribute("data-accept");
421
+ if (!commit) return;
422
+ if (reviewAcceptSlot.armed(commit)) {
423
+ await postAction("/api/post-review-accept", { sha: commit }, (d) => d && d.status === "accepted" ? "accepted the post-merge review of " + shortSha(d.commit) + " unreviewed — " + d.summary : "that review already left the queue");
424
+ pendingAcceptCommit = "";
425
+ reviewAcceptSlot.disarm();
426
+ fetchPending();
427
+ } else {
428
+ pendingAcceptCommit = commit;
429
+ reviewAcceptSlot.arm(commit);
430
+ }
431
+ });
432
+ $("pendingqueue").addEventListener("change", (e) => {
433
+ if (e.target.id === "pendingqueuerole") {
434
+ pendingQueueRole = e.target.value;
435
+ renderPendingQueue(lastStatus);
436
+ }
437
+ });
438
+ $("pendingreviews").addEventListener("change", (e) => {
439
+ if (e.target.id === "pendingreviewrole") {
440
+ pendingReviewRole = e.target.value;
441
+ renderPendingReviews(lastStatus);
442
+ } else if (e.target.id === "pendingreviewstate") {
443
+ pendingReviewState = e.target.value;
444
+ renderPendingReviews(lastStatus);
445
+ }
446
+ });
447
+ $("pendingoverlaps").addEventListener("click", (e) => {
448
+ const b = e.target.closest("[data-overlap]");
449
+ if (b) togglePendingOverlap(Number(b.dataset.overlap));
450
+ });
451
+ $("pendingoverlaps").addEventListener("change", (e) => {
452
+ if (e.target.id === "pendingoverlapdir") pendingOverlapDir = e.target.value;
453
+ else if (e.target.id === "pendingoverlaprole") pendingOverlapRole = e.target.value;
454
+ else return;
455
+ pendingOverlapOpen = null;
456
+ paintPendingOverlaps();
457
+ });
93
458
  }
94
459
  // Load the team-wide change roster. Called when the view opens and by the Refresh button;
95
- // no timer of its own, and the roster only changes when a loop merges or edits its worktree.
460
+ // no timer of its own, and the roster only changes when a role merges or edits its worktree.
461
+ // Load the overlapping-files card on its own, so neither read can blank the other's card.
462
+ async function fetchPendingOverlaps() {
463
+ pendingOverlapOpen = null;
464
+ try {
465
+ pendingOverlaps = await getJson("/api/diff?overlaps=1");
466
+ } catch (err) {
467
+ const card = $("pendingoverlaps");
468
+ if (card) card.innerHTML = errorPanel("Overlaps unavailable", err);
469
+ return;
470
+ }
471
+ if (!pendingOverlapHolders(pendingOverlaps).includes(pendingOverlapRole)) pendingOverlapRole = "";
472
+ if (!pendingOverlapDirs(pendingOverlaps).includes(pendingOverlapDir)) pendingOverlapDir = "";
473
+ paintPendingOverlaps();
474
+ }
96
475
  async function fetchPending() {
97
476
  buildPendingView();
477
+ fetchPendingOverlaps();
98
478
  try {
99
479
  pendingTeam = await getJson("/api/diff");
100
480
  } catch (err) {
@@ -104,6 +484,7 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
104
484
  }
105
485
  renderPending();
106
486
  renderPendingQueue(lastStatus);
487
+ renderPendingReviews(lastStatus);
107
488
  }
108
489
  function renderPending() {
109
490
  const body = $("pendingbody");