tumwater 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) 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 +42 -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 +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +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 +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -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 +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -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-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -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 +1 -2
  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 +118 -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 +150 -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 +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,4 +1,4 @@
1
- import { claudeUtilizationPct, claudeWindowLabel } from "../agent/claude/claude-account.js";
1
+ import { utilizationPct, windowLabel } from "../agent/subscription-account.js";
2
2
  import { MODEL_TIERS } from "../config/config-schema.js";
3
3
  import { budgetGate, budgetReached, fallbackTierEntries } from "../budget/budget.js";
4
4
  import { fallbackDisplayPair } from "../config/config-views.js";
@@ -9,24 +9,29 @@ import { formatGB, shortSha, usd, usdCap } from "../text/format.js";
9
9
  /** The status header's BADGE fragments, phrased once for both observer surfaces: the terminal
10
10
  * table (status-render.ts's renderStatus header) and the JSON/GUI payload
11
11
  * (status-payload.ts's preformatted badge fields). Split from status-model.ts, which keeps the
12
- * per-loop display model (phase, rows, token metrics) — the badges are header-scale team
13
- * facts (build, main check, budget, merge queue, timed pause) rather than per-loop state, and
12
+ * per-role display model (phase, rows, token metrics) — the badges are header-scale team
13
+ * facts (build, main check, budget, merge queue, timed pause) rather than per-role state, and
14
14
  * their one-home rule ("the page cannot re-derive and drift from these strings") is easier to
15
15
  * see when the module is exactly the badges. The duration phrasing they share (humanSeconds)
16
16
  * lives in datetime.ts — core modules need it too, so it sits below the ui layer. */
17
17
  /** The header's build fragment: which commit the running harness was compiled from and, when
18
18
  * main's build inputs have moved past it, how far — the team is then executing code main no
19
19
  * longer describes (auto-restart lands the new build; off, restart `tumwater run` by hand).
20
- * A stale build also says what auto-restart is doing about it: `restart pending` resolves
21
- * itself, `restart BLOCKED` never will until main moves, and telling them apart at a glance is
22
- * the whole point (see BuildStatus.restartBlocked). Empty when the dist carries no stamp.
23
- * Shared by the TUI/status header and the GUI's. */
20
+ * A stale build also says what auto-restart is doing about it: a cooldown-deferred restart
21
+ * (the reason literally opens with "cooldown until …") scopes the wait to the redeploy — the
22
+ * team keeps running the stale build, so the badge reads "work continues" and keeps the
23
+ * deadline; `restart pending` (the drain that precedes the swap, which actually holds new runs
24
+ * and merges) resolves itself; `restart BLOCKED` never will until main moves. Telling them apart
25
+ * at a glance is the whole point (see BuildStatus.restartBlocked). Empty when the dist carries
26
+ * no stamp. Shared by the TUI/status header and the GUI's. */
24
27
  export function buildBadge(build) {
25
28
  if (!build)
26
29
  return "";
27
30
  const stale = build.stale ? ` — update available (${plural(build.aheadCommits ?? 0, "commit")} behind)` : "";
28
31
  const restart = build.restartBlocked
29
- ? `; restart blocked: ${build.restartBlocked}`
32
+ ? build.restartBlocked.startsWith("cooldown until ")
33
+ ? ` — restarts after cooldown; work continues (${build.restartBlocked})`
34
+ : `; restart blocked: ${build.restartBlocked}`
30
35
  : build.restartPending
31
36
  ? " — restarts when idle"
32
37
  : "";
@@ -84,7 +89,7 @@ export function mainCheckBadge(mainCheck) {
84
89
  }
85
90
  /** The team's current budget-gate state, derived from a snapshot's budget block — the header
86
91
  * badge's default-tier story (part 5c/8): the badge names the team's bulk fallback, so it
87
- * reads the `default` tier's verdict alone (the per-tier pause lives on the loop rows, via
92
+ * reads the `default` tier's verdict alone (the per-tier pause lives on the role rows, via
88
93
  * status-data.ts's budgetPausedRoles). */
89
94
  function teamBudgetGate(budget) {
90
95
  return budgetGate(budgetReached(budget), { default: budget.fallback !== null, strong: budget.fallback !== null }, false);
@@ -120,17 +125,21 @@ function untilReset(resetsAt, now) {
120
125
  * `· Claude 5h · 64% · resets in 4h 12m` (`resets now` once the deadline has passed), with
121
126
  * the status word when no `utilization` was reported and an `overage` marker while the
122
127
  * account spends into overage. A rejected window reads
123
- * `claude model jobs wait until 14:30` in place of `resets` while a future deadline holds the
124
- * seams; a reset reading names neither. The one home of the string, so the TUI header and the
125
- * payload's preformatted badge cannot drift. */
126
- function claudeWindowBadge(claudeWindow, now, moved = false) {
127
- const w = claudeWindow.primary;
128
- const parts = [`Claude ${claudeWindowLabel(w.type)}`];
129
- parts.push(typeof w.utilization === "number" ? `${claudeUtilizationPct(w.utilization)}%` : claudeWindowStatusWord(w.status));
128
+ * `<backend> model jobs wait until 14:30` in place of `resets` while a future deadline holds
129
+ * the seams; a reset reading names neither. Reads the most-constrained block (`blocks[0]`,
130
+ * already most-constrained-first), rendering under that backend's own display label. The
131
+ * hold clause names the backend by its lowercase id (not the display label), so a claude-only
132
+ * team keeps the byte-identical `claude model jobs wait` the pre-3b badge rendered. The one
133
+ * home of the string, so the TUI header and the payload's preformatted badge cannot drift. */
134
+ function subscriptionWindowBadge(blocks, now, moved = false) {
135
+ const block = blocks[0];
136
+ const w = block.primary;
137
+ const parts = [`${block.label} ${windowLabel(w.type)}`];
138
+ parts.push(typeof w.utilization === "number" ? `${utilizationPct(w.utilization)}%` : claudeWindowStatusWord(w.status));
130
139
  if (w.isUsingOverage === true)
131
140
  parts.push("overage");
132
- if (w.status === "rejected" && claudeWindow.heldUntil !== null) {
133
- parts.push(`${moved ? "on fallback" : "claude model jobs wait"} until ${clockHm(claudeWindow.heldUntil)}`);
141
+ if (w.status === "rejected" && block.heldUntil !== null) {
142
+ parts.push(`${moved ? "on fallback" : `${block.backend} model jobs wait`} until ${clockHm(block.heldUntil)}`);
134
143
  }
135
144
  else if (w.status !== "reset" && typeof w.resetsAt === "number") {
136
145
  parts.push(w.resetsAt - now < 60_000 ? "resets now" : `resets in ${untilReset(w.resetsAt, now)}`);
@@ -163,13 +172,14 @@ export function chainBadge(chain) {
163
172
  return text ? ` · ${text}` : "";
164
173
  }
165
174
  /** The window fragment's tone rank, for the header badge and the GUI meter: a rejected window
166
- * is red, an allowed_warning amber, anything else no tone. */
167
- export function claudeWindowTone(claudeWindow) {
168
- if (!claudeWindow)
175
+ * is red, an allowed_warning amber, anything else no tone. Reads the most-constrained block
176
+ * (`blocks[0]`), so a multi-backend team's tone follows its tightest window. */
177
+ export function subscriptionWindowTone(blocks) {
178
+ if (!blocks || blocks.length === 0)
169
179
  return undefined;
170
- if (claudeWindow.primary.status === "rejected")
180
+ if (blocks[0].primary.status === "rejected")
171
181
  return "red";
172
- if (claudeWindow.primary.status === "allowed_warning")
182
+ if (blocks[0].primary.status === "allowed_warning")
173
183
  return "yellow";
174
184
  return undefined;
175
185
  }
@@ -193,11 +203,15 @@ export function claudeWindowTone(claudeWindow) {
193
203
  * needs no forecast slot: a disabled cap never carries a capHitAt (projectCapHit returns null
194
204
  * for capUsd <= 0), and the fallback fragment only stands once the cap IS reached — the one
195
205
  * state where the forecast is itself null — so the fragments can never compete for the line. */
196
- export function budgetBadge(budget, claudeWindow, now = Date.now(), chain) {
197
- const window = (claudeWindow ? claudeWindowBadge(claudeWindow, now, chainMovedByWindow(chain)) : "") + chainBadge(chain);
206
+ export function budgetBadge(budget, windows, only = false, now = Date.now(), chain) {
207
+ const blocks = windows && windows.length > 0 ? windows : undefined;
208
+ const window = (blocks ? subscriptionWindowBadge(blocks, now, chainMovedByWindow(chain)) : "") + chainBadge(chain);
198
209
  // Subscription-only: the window stands IN PLACE of the daily spend — there is no priced
199
- // spend for a dollar figure to report. Mixed and none fall through to the daily spend.
200
- if (claudeWindow?.only)
210
+ // spend for a dollar figure to report. This holds only while a window block actually
211
+ // exists: an all-free subscription-only team (only and free, no windows) reads ` · budget: n/a`
212
+ // like any other all-free team, so the free branch below stays reachable. Mixed and none
213
+ // fall through to the daily spend.
214
+ if (only && blocks)
201
215
  return window;
202
216
  if (budget.free)
203
217
  return ` · budget: n/a${window}`;
@@ -211,13 +225,12 @@ export function budgetBadge(budget, claudeWindow, now = Date.now(), chain) {
211
225
  // single-pair text byte-identical to the pre-tier badge. Off-gate the badge is byte-
212
226
  // identical to before either way.
213
227
  const tierEntries = fallbackTierEntries(budget.tiers);
214
- // The backend-default label when the engaged pair names neither half: `<agent> default`
215
- // (plans/agent-backends.md part 4f/7), `pi default` for a pi team as before.
216
- const agentDefault = `${budget.backend && budget.backend !== "pi" ? budget.backend : "pi"} default`;
228
+ // The label when the engaged pair names neither half: pi's own default.
229
+ const backendDefault = "pi default";
217
230
  const fallback = teamBudgetGate(budget) === "fallback"
218
231
  ? tierEntries.length >= 2
219
232
  ? ` · over-cap model: ${tierEntries.join(", ")} (cost n/a)`
220
- : ` · over-cap model: ${fallbackDisplayPair(budget.fallback, agentDefault)} (cost n/a)`
233
+ : ` · over-cap model: ${fallbackDisplayPair(budget.fallback, backendDefault)} (cost n/a)`
221
234
  : "";
222
235
  const forecast = budget.capHitAt !== null
223
236
  ? ` · ~cap at ${clockHm(budget.capHitAt)}`
@@ -249,11 +262,11 @@ export function wakeBadge(wake, now) {
249
262
  return "";
250
263
  return ` · wake scheduled in ${humanUntil(wake.atMs, now)} (${wake.roles.join(", ")})`;
251
264
  }
252
- /** One loop's own pending-wake marker for its state cell: ` · wake in <duration>` for the
265
+ /** One role's own pending-wake marker for its state cell: ` · wake in <duration>` for the
253
266
  * earliest future wake naming `role` (or `instanceOf`, the base role an extra instance runs),
254
- * empty when none does. Same match rule as the GUI's `scheduledWakesFor` (gui-client-loops.ts
267
+ * empty when none does. Same match rule as the GUI's `scheduledWakesFor` (gui-client-roles.ts
255
268
  * keeps its own JS copy). `wakes` need not be sorted. */
256
- export function loopWakeMarker(wakes, role, instanceOf, now) {
269
+ export function roleWakeMarker(wakes, role, instanceOf, now) {
257
270
  let at;
258
271
  for (const w of wakes ?? []) {
259
272
  if (w.atMs <= now || !(w.roles.includes(role) || (instanceOf !== undefined && w.roles.includes(instanceOf))))
@@ -272,7 +285,7 @@ export function loopWakeMarker(wakes, role, instanceOf, now) {
272
285
  * or already-expired deadline renders nothing — the read side treats an expired marker as
273
286
  * unpaused, so the badge never claims a countdown that is over. The duration goes through
274
287
  * pauseCountdown, the one home of that guard and rounding. renderStatus renders it in
275
- * the TUI/status header after the budget badge; the GUI recomputes the ticking number
288
+ * the TUI/status header after the budget badge; the GUI recomputes the counting-down number
276
289
  * client-side from the payload's raw `pausedUntil` (status-payload.ts) using its own
277
290
  * humanSeconds copy, pinned against this one by test. `reason` (the operator's
278
291
  * `pause --reason <text>` note) rides after the countdown — and makes the untimed pause
@@ -295,7 +308,7 @@ export function pauseBadge(pausedUntil, now, reason) {
295
308
  * countdowns, shared by pauseBadge's header fragment and team-alerts.ts's paused alert
296
309
  * title, so the two surfaces cannot disagree about how much time remains. The guard is this
297
310
  * function's own; the ms→seconds rounding rides datetime.ts's secondsUntil. The GUI's pause
298
- * control recomputes the ticking number client-side from the payload's raw `pausedUntil`
311
+ * control recomputes the counting-down number client-side from the payload's raw `pausedUntil`
299
312
  * (gui-client-operator.ts) — a separate runtime that cannot import TypeScript. */
300
313
  export function pauseCountdown(pausedUntil, now) {
301
314
  if (pausedUntil === undefined || pausedUntil <= now)
@@ -304,7 +317,7 @@ export function pauseCountdown(pausedUntil, now) {
304
317
  }
305
318
  /** The header's quiet-hours fragment (plans: "Quiet hours … part 2/2, observability"): the
306
319
  * schedule as standing information in every configured state — `· quiet until 07:00` while
307
- * the local clock sits inside the window (the active reading answers "why are the loops
320
+ * the local clock sits inside the window (the active reading answers "why are the roles
308
321
  * idle?" with when they start again) and `· quiet 23:00-07:00` otherwise — and empty when
309
322
  * the snapshot carries no window, so a team without the schedule keeps its header
310
323
  * byte-identical. The window end comes from quiet-hours.ts's quietWindowEnd — read back out of
@@ -354,3 +367,18 @@ export function permitsBadge(permits) {
354
367
  return "";
355
368
  return ` · running ${permits.inUse} of ${permits.limit} · ${providers}`;
356
369
  }
370
+ /** The check-permit pool's reading, ` · checks 1/2` plus `, 3 waiting` while checks queue. Empty
371
+ * when no check runs or waits, so an idle check pool leaves the header byte-identical. */
372
+ export function checksBadge(checks) {
373
+ if (!checks || (checks.inUse === 0 && checks.waiting === 0))
374
+ return "";
375
+ return ` · checks ${checks.inUse}/${checks.limit}${checks.waiting > 0 ? `, ${checks.waiting} waiting` : ""}`;
376
+ }
377
+ /** The workspace pool's header fragment: ` · workspaces 8/8`, plus `, 2 waiting` when runs queue
378
+ * for a slot. Shown only while the pool is full or something waits; a pool with a free workspace
379
+ * leaves the header byte-identical. */
380
+ export function workspacesBadge(pool) {
381
+ if (!pool || (pool.waiting === 0 && pool.leased < pool.limit))
382
+ return "";
383
+ return ` · workspaces ${pool.leased}/${pool.limit}${pool.waiting > 0 ? `, ${pool.waiting} waiting` : ""}`;
384
+ }
@@ -0,0 +1,292 @@
1
+ /** The dashboard's backlog card, browser-side: the questions/plans/bugs/queued tabs with
2
+ * their text Search box, the Queued tab's in-place prompt editor, and the New plan / File a
3
+ * bug forms. renderBacklog repaints it from each /api/status poll (renderTeam in
4
+ * gui-client-team.ts calls it). Split out of gui-client-team.ts; spliced into gui-client.ts's
5
+ * script right after it, reaching its helpers ($, esc, recall, store, getJson, postJson,
6
+ * paintPanel, showFlash, clickClosest) and lastStatus through that concatenation. */
7
+ export const GUI_CLIENT_BACKLOG_JS = String.raw `
8
+ // ---- backlog: questions, plans, bugs, and queued prompts ----
9
+ let backlogTab = recall("backlog") || "";
10
+ let backlogGrep = recall("backlog-grep") || "";
11
+ let backlogBodyHits = null;
12
+ // backlog:start
13
+ // The Queued tab's in-place editor state, beside the tab choice its rows paint from:
14
+ // backlogStatus is the last payload renderBacklog saw, queuedEdit is the open editor
15
+ // ({ role, file, text } or null), and queuedEditPainted marks that its textarea is on
16
+ // screen so the standing 1s poll cannot repaint over what the operator is typing.
17
+ let backlogStatus = null;
18
+ let backlogMatchAt = 0;
19
+ let queuedEdit = null;
20
+ let queuedEditPainted = false;
21
+ // The Plans/Bugs tabs' New form: { kind: "plan" | "bug" } or null, with the same painted
22
+ // guard so a poll does not repaint over the typed title.
23
+ let backlogNew = null;
24
+ let backlogNewPainted = false;
25
+ // Names of the blocked series whose parts the Plans tab currently shows.
26
+ let openSeries = new Set();
27
+ function setBacklogTab(tab) {
28
+ backlogTab = tab;
29
+ store("backlog", tab);
30
+ queuedEdit = null;
31
+ queuedEditPainted = false;
32
+ backlogNew = null;
33
+ backlogNewPainted = false;
34
+ if (lastStatus) renderBacklog(lastStatus);
35
+ }
36
+ // The Search box's text: trimmed, remembered, and the last payload repainted through it.
37
+ function setBacklogGrep(text) {
38
+ backlogGrep = text.trim();
39
+ store("backlog-grep", backlogGrep);
40
+ if (backlogStatus) renderBacklog(backlogStatus);
41
+ fetchBacklogMatch();
42
+ }
43
+ // The server's body matches for the current Search text: { needle, plans: [index…], … } from
44
+ // /api/backlog-match. A stale answer (the box changed meanwhile) is dropped, and a failed
45
+ // fetch leaves the shown-text matching in place.
46
+ async function fetchBacklogMatch() {
47
+ if (!backlogGrep) { backlogBodyHits = null; return; }
48
+ const needle = backlogGrep;
49
+ backlogMatchAt = Date.now();
50
+ try {
51
+ const d = await getJson("/api/backlog-match?grep=" + encodeURIComponent(needle));
52
+ if (backlogGrep !== needle) return;
53
+ backlogBodyHits = { needle, ...d };
54
+ if (backlogStatus) renderBacklog(backlogStatus);
55
+ } catch (e) { /* keep shown-text matching */ }
56
+ }
57
+ function beginBacklogNew(kind) {
58
+ backlogNew = { kind };
59
+ backlogNewPainted = false;
60
+ renderBacklog(backlogStatus);
61
+ }
62
+ function toggleBacklogSeries(name) {
63
+ if (openSeries.has(name)) openSeries.delete(name); else openSeries.add(name);
64
+ renderBacklog(backlogStatus);
65
+ }
66
+ function endBacklogNew() {
67
+ backlogNew = null;
68
+ backlogNewPainted = false;
69
+ renderBacklog(backlogStatus);
70
+ }
71
+ function beginQueuedEdit(role, file, text) {
72
+ backlogTab = "queued";
73
+ queuedEdit = { role, file, text };
74
+ queuedEditPainted = false;
75
+ renderBacklog(backlogStatus);
76
+ }
77
+ function endQueuedEdit() {
78
+ queuedEdit = null;
79
+ queuedEditPainted = false;
80
+ renderBacklog(backlogStatus);
81
+ }
82
+ const BACKLOG = [
83
+ ["questions", "Questions", "question", "indigo", "No open questions", "Roles post here when they need a decision from you."],
84
+ ["plans", "Plans", "plan", "blue", "Nothing planned", "The plan role files features in PLANS.md."],
85
+ ["bugs", "Bugs", "bug", "red", "No open bugs", "Bugs filed in BUGS.md show up here."],
86
+ ["queued", "Inbox", "chat", "gray", "Inbox empty", "Messages you send wait here until their role's next run."], // vocabulary-ok: internal tab key
87
+ ];
88
+ function renderBacklog(d) {
89
+ backlogStatus = d;
90
+ if (backlogGrep && Date.now() - backlogMatchAt > 5000) fetchBacklogMatch();
91
+ if (queuedEdit && queuedEditPainted) return; // an open editor owns the panel until Save or Cancel
92
+ if (backlogNew && backlogNewPainted) return; // likewise the New form
93
+ const items = { questions: d.questions || [], plans: d.plans || [], bugs: d.bugs || [], queued: queuedPrompts(d) };
94
+ if (!items[backlogTab]) backlogTab = items.questions.length ? "questions" : "plans";
95
+ paintPanel("backlogtabs", BACKLOG.map(([k, label]) => "<button type='button' role='tab' data-tab='" + k + "' aria-selected='" + (k === backlogTab) + "'" +
96
+ (k === backlogTab ? " class='active'" : "") + ">" + label + "<span class='badge" + (k === "questions" && items.questions.length ? " t-indigo" : "") + "'>" +
97
+ items[k].length + "</span></button>").join(""));
98
+ const [key, , ic, tone, emptyTitle, emptyText] = BACKLOG.find((s) => s[0] === backlogTab);
99
+ const list = items[key];
100
+ // Search narrows rows by the text each one shows, or by a body match from /api/backlog-match;
101
+ // indexes stay the payload's own. Inbox rows also match their full text there (the queued key).
102
+ const needle = backlogGrep.toLowerCase();
103
+ const hit = (text, file, i) => !needle || String(text).toLowerCase().includes(needle) ||
104
+ (!!file && !!backlogBodyHits && backlogBodyHits.needle === backlogGrep && (backlogBodyHits[file] || []).includes(i));
105
+ const queuedHit = (q) => !!backlogBodyHits && backlogBodyHits.needle === backlogGrep &&
106
+ (backlogBodyHits.queued || []).some((m) => m.role === q.role && m.file === q.file);
107
+ let shown = 0;
108
+ let html;
109
+ if (!list.length) html = "<div class='empty'><strong>" + esc(emptyTitle) + "</strong>" + esc(emptyText) + "</div>";
110
+ else if (key === "queued") {
111
+ // Each role's queue head carries no Move to top (a no-op there); every later row of
112
+ // that role does. The rows arrive grouped by role — director first, then each role in
113
+ // id order — so a Set of roles already painted marks each group's first row.
114
+ const paintedFirst = new Set();
115
+ html = list.map((q) => {
116
+ const addr = "data-file='" + esc(q.file) + "' data-role='" + esc(q.role) + "'";
117
+ // Mark this role's group head before the edit-row early return, so an edited head row
118
+ // still leaves the Move to top button on that role's later rows.
119
+ const alreadyFirst = paintedFirst.has(q.role);
120
+ paintedFirst.add(q.role);
121
+ // The row being edited swaps its preview for a textarea and a Save/Cancel pair; the
122
+ // address stays the queue-file basename, the same one /api/prompt-cancel uses.
123
+ if (queuedEdit && q.file === queuedEdit.file && q.role === queuedEdit.role) {
124
+ queuedEditPainted = true;
125
+ shown++; // the open editor always shows, so a search never replaces it with "Nothing matches"
126
+ return "<div class='list-item'><span class='li-icon'>" + icon("chat") + "</span>" +
127
+ "<textarea id='queuedit' class='queuedit'>" + esc(queuedEdit.text) + "</textarea>" +
128
+ "<button type='button' class='btn btn-sm rowaction' data-action='prompteditsave' " + addr + ">Save</button>" +
129
+ "<button type='button' class='btn btn-sm rowaction' data-action='prompteditcancel' " + addr + ">Cancel</button></div>";
130
+ }
131
+ if (!hit(q.preview + " " + q.role) && !queuedHit(q)) return "";
132
+ shown++;
133
+ const topBtn = alreadyFirst
134
+ ? "<button type='button' class='btn btn-sm rowaction' data-action='prompttop' " + addr + ">Move to top</button>"
135
+ : "";
136
+ return "<div class='list-item'><span class='li-icon'>" + icon("chat") + "</span><div class='li-main'><span class='li-title'>" + esc(q.preview) +
137
+ "</span><span class='li-meta'>" + esc((q.role === "director" ? "for the director" : "for the " + roleLabel(q.role) + " role") +
138
+ (q.notBeforeMs && q.notBeforeMs > Date.now() ? " · delivers in " + humanSeconds(Math.round((q.notBeforeMs - Date.now()) / 1000))
139
+ : (q.queuedAtMs ? " · queued " + fmtAgo(q.queuedAtMs) : ""))) + "</span></div>" +
140
+ topBtn +
141
+ "<button type='button' class='btn btn-sm rowaction' data-action='promptcancel' " + addr + ">Cancel</button>" +
142
+ "<button type='button' class='btn btn-sm rowaction' data-action='promptedit' " + addr + ">Edit</button></div>";
143
+ }).join("");
144
+ } else {
145
+ const open = openEntryKey();
146
+ // The eligibility holds beside the title, from the payload's parallel arrays (indexed by
147
+ // the same tab position the click sends): empty for eligible rows so their markup is
148
+ // unchanged, and the Questions tab carries no holds.
149
+ const holds = key === "plans" ? (d.planHolds || []) : key === "bugs" ? (d.bugHolds || []) : [];
150
+ // Blocked entries (a hold of any kind) paint after the pickable ones under one divider;
151
+ // each row keeps its original data-index so the drawer and Answer still address it.
152
+ const blocked = key === "plans" ? (d.planBlocked || []) : key === "bugs" ? (d.bugBlocked || []) : [];
153
+ const rows = list.map((title, i) => {
154
+ const t = splitTitle(title);
155
+ const mark = holds[i] || "";
156
+ if (!hit(t.title + " " + t.meta + " " + mark, key, i)) return "";
157
+ shown++;
158
+ return "<div class='list-item t-" + tone + (open === key + ":" + i ? " active" : "") + "'><span class='li-icon'>" + icon(ic) + "</span>" +
159
+ "<button type='button' class='li-open backloglink' data-file='" + key + "' data-index='" + i + "'><span class='li-title'>" + esc(t.title) + "</span>" +
160
+ (t.meta ? "<span class='li-meta'>" + esc(t.meta) + "</span>" : "") +
161
+ (mark ? "<span class='li-meta'>" + esc(mark.trim()) + "</span>" : "") + "</button>" +
162
+ (key === "questions" ? "<button type='button' class='btn btn-sm backloglink' data-file='questions' data-index='" + i + "'>Answer</button>" : "") + "</div>";
163
+ });
164
+ const ready = rows.filter((r, i) => r && !blocked[i]);
165
+ const series = key === "plans" ? (d.planSeries || []) : [];
166
+ let heldCount = 0;
167
+ const held = [];
168
+ const seen = new Set();
169
+ rows.forEach((r, i) => {
170
+ if (!blocked[i] || !r) return;
171
+ heldCount++;
172
+ const s = series[i];
173
+ if (!s || needle) { held.push(r); return; }
174
+ if (seen.has(s.name)) return;
175
+ seen.add(s.name);
176
+ held.push("<div class='list-item list-series'><button type='button' class='li-open' data-action='seriestoggle' data-series='" + esc(s.name) + "'><span class='li-title'>" + esc(s.label) + "</span><span class='li-meta'>blocked: each waits on an earlier part</span></button></div>");
177
+ if (openSeries.has(s.name)) rows.forEach((r2, j) => { if (blocked[j] && series[j] && series[j].name === s.name) held.push(r2); });
178
+ });
179
+ html = ready.join("") + (heldCount ? "<div class='list-item list-divider'><span class='li-meta'>Blocked (" + heldCount + ")</span></div>" + held.join("") : "");
180
+ }
181
+ const answered = (key === "questions" ? (d.answeredQuestions || []) : []).map((a, i) => ({ a, i })).filter((x) => hit(x.a.title + " " + (x.a.decision || ""), "answered", x.i));
182
+ shown += answered.length;
183
+ if (answered.length) {
184
+ html += "<div class='list-item list-divider'><span class='li-meta'>Answered recently</span></div>" + answered.map(({ a, i }) =>
185
+ "<div class='list-item t-gray" + (openEntryKey() === "answered:" + i ? " active" : "") + "'><span class='li-icon'>" + icon("check") + "</span><button type='button' class='li-open backloglink' data-file='answered' data-index='" + i + "'><span class='li-title'>" + esc(a.title) + "</span>" +
186
+ (a.decision ? "<span class='li-meta'>" + esc(a.decision) + "</span>" : "") + "</button></div>").join("");
187
+ }
188
+ const finished = (key === "plans" ? (d.donePlans || []) : key === "bugs" ? (d.fixedBugs || []) : []).map((f, i) => ({ f, i })).filter((x) => hit(x.f.title, key === "plans" ? "done" : "fixed", x.i));
189
+ shown += finished.length;
190
+ if (finished.length) {
191
+ html += "<div class='list-item list-divider'><span class='li-meta'>" + (key === "plans" ? "Done recently" : "Fixed recently") + "</span></div>" + finished.map(({ f, i }) => {
192
+ const doneFile = key === "plans" ? "done" : "fixed";
193
+ return "<div class='list-item t-gray" + (openEntryKey() === doneFile + ":" + i ? " active" : "") + "'><span class='li-icon'>" + icon("check") + "</span>" +
194
+ "<button type='button' class='li-open backloglink' data-file='" + doneFile + "' data-index='" + i + "'><span class='li-title'>" + esc(f.title) + "</span>" +
195
+ (f.completedOn ? "<span class='li-meta'>" + esc((key === "plans" ? "done " : "fixed ") + f.completedOn) + "</span>" : "") + "</button></div>";
196
+ }).join("");
197
+ }
198
+ if (needle && list.length && !shown) html = "<div class='empty'><strong>Nothing matches this search</strong>Clear the search box to see every entry.</div>";
199
+ if (key === "plans" || key === "bugs") {
200
+ const kind = key === "plans" ? "plan" : "bug";
201
+ if (backlogNew && backlogNew.kind === kind) {
202
+ backlogNewPainted = true;
203
+ html = "<div class='list-item'><input id='backlognew' type='text' placeholder='" + (kind === "plan" ? "Plan title" : "Bug symptom") + "'>" +
204
+ "<textarea id='backlognewbody' rows='6' placeholder='Details (optional, markdown)'></textarea>" +
205
+ "<button type='button' class='btn btn-sm rowaction' data-action='backlognewsave' data-kind='" + kind + "'>Save</button>" +
206
+ "<button type='button' class='btn btn-sm rowaction' data-action='backlognewcancel'>Cancel</button></div>" + html;
207
+ } else {
208
+ html = "<div class='list-item'><button type='button' class='btn btn-sm' data-action='backlognew' data-kind='" + kind + "'>" +
209
+ (kind === "plan" ? "New plan" : "File a bug") + "</button></div>" + html;
210
+ }
211
+ }
212
+ paintPanel("backlog", html);
213
+ }
214
+ // backlog:end
215
+ $("backlogtabs").addEventListener("click", (ev) => {
216
+ const b = clickClosest(ev, "button[data-tab]");
217
+ if (b) setBacklogTab(b.dataset.tab);
218
+ });
219
+ // Like #feedgrep, the box is static markup set once at load: the 1s poll repaints only the rows,
220
+ // never the box, so it cannot overwrite what the operator is typing.
221
+ $("backloggrep").value = backlogGrep;
222
+ $("backloggrep").addEventListener("input", (ev) => setBacklogGrep(ev.target.value));
223
+ $("backlog").addEventListener("click", async (ev) => {
224
+ const ser = clickClosest(ev, "button[data-action='seriestoggle']");
225
+ if (ser) { toggleBacklogSeries(ser.dataset.series); return; }
226
+ const fresh = clickClosest(ev, "button[data-action='backlognew']");
227
+ if (fresh) { beginBacklogNew(fresh.dataset.kind); return; }
228
+ if (clickClosest(ev, "button[data-action='backlognewcancel']")) { endBacklogNew(); return; }
229
+ const freshSave = clickClosest(ev, "button[data-action='backlognewsave']");
230
+ if (freshSave) {
231
+ const input = $("backlognew");
232
+ const title = input ? input.value.trim() : "";
233
+ if (!title) return;
234
+ const bodyBox = $("backlognewbody");
235
+ await postAction("/api/backlog-file", { kind: freshSave.dataset.kind, title, body: bodyBox ? bodyBox.value : "" }, (d) => "Filed: " + d.title + uncommittedTail(d));
236
+ endBacklogNew();
237
+ return;
238
+ }
239
+ const top = clickClosest(ev, "button.rowaction[data-action='prompttop']");
240
+ if (top) {
241
+ await postAction("/api/prompt-top", { role: top.dataset.role, file: top.dataset.file }, (d) =>
242
+ d && d.status === "topped"
243
+ ? "Moved to the front: " + (d.preview || "")
244
+ : d && d.status === "already_first"
245
+ ? "Already first"
246
+ : "No longer in the inbox — " + (top.dataset.role ? roleLabel(top.dataset.role) : "the director") + " already took it");
247
+ return;
248
+ }
249
+ const cancel = clickClosest(ev, "button.rowaction[data-action='promptcancel']");
250
+ if (cancel) {
251
+ await postAction("/api/prompt-cancel", { role: cancel.dataset.role, file: cancel.dataset.file }, (d) =>
252
+ d && d.status === "cancelled"
253
+ ? "Cancelled: " + (d.preview || "")
254
+ : "No longer in the inbox — " + (cancel.dataset.role ? roleLabel(cancel.dataset.role) : "the director") + " already took it");
255
+ return;
256
+ }
257
+ const edit = clickClosest(ev, "button.rowaction[data-action='promptedit']");
258
+ if (edit) {
259
+ try {
260
+ const d = await getJson("/api/prompt-text?role=" + encodeURIComponent(edit.dataset.role) + "&file=" + encodeURIComponent(edit.dataset.file));
261
+ if (d && d.status === "gone") {
262
+ showFlash("No longer in the inbox — " + (edit.dataset.role ? roleLabel(edit.dataset.role) : "the director") + " already took it");
263
+ refresh();
264
+ } else {
265
+ beginQueuedEdit(edit.dataset.role, edit.dataset.file, (d && d.text) || "");
266
+ }
267
+ } catch (err) {
268
+ showFlash("error: " + err.message);
269
+ }
270
+ return;
271
+ }
272
+ const save = clickClosest(ev, "button.rowaction[data-action='prompteditsave']");
273
+ if (save) {
274
+ const area = $("queuedit");
275
+ const text = area ? area.value.trim() : "";
276
+ if (!text) { showFlash("error: prompt text is empty"); return; }
277
+ await postAction("/api/prompt-edit", { role: save.dataset.role, file: save.dataset.file, text }, (d) =>
278
+ d && d.status === "edited"
279
+ ? "Edited: " + (d.newText || "")
280
+ : "No longer in the inbox — " + (save.dataset.role ? roleLabel(save.dataset.role) : "the director") + " already took it");
281
+ endQueuedEdit();
282
+ return;
283
+ }
284
+ const editCancel = clickClosest(ev, "button.rowaction[data-action='prompteditcancel']");
285
+ if (editCancel) {
286
+ endQueuedEdit();
287
+ return;
288
+ }
289
+ const entry = clickClosest(ev, ".backloglink");
290
+ if (entry) toggleEntry(entry.dataset.file, Number(entry.dataset.index));
291
+ });
292
+ `;