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,24 +1,64 @@
1
- // vocabulary: plain
2
- /** Backlog eligibility: which PLANS.md / BUGS.md entries a work loop may take now (plans,
1
+ /** Backlog eligibility: which PLANS.md / BUGS.md entries a work role may take now (plans,
3
2
  * parallel-work-instances, part 2/7). An entry is held when its body carries a **Refused …**,
4
3
  * **Needs review …**, **Needs replan …** or **Waiting for the next failure …** note, or when
5
4
  * its heading's trailing parenthetical
6
5
  * (or a parenthetical left open by a wrapped heading) names a prerequisite (`requires parts
7
6
  * 1/5–3/5 landed`) that is itself still listed under `## Planned`. The parsers are pure over
8
7
  * markdown text; `eligibleEntries` is the stat-backed reader for a caller that wants the list.
9
- * The backlog index marks every held entry so a loop
8
+ * The backlog index marks every held entry so a role
10
9
  * skips it without re-deriving the rule. */
11
10
  import path from "node:path";
12
11
  import { statOrNull } from "../files/files.js";
13
- import { allLiveClaims } from "../loop/loop-state.js";
14
- import { collapseWhitespace } from "../text/text.js";
15
- import { NEEDS_REPLAN_PREFIX, NEEDS_REVIEW_PREFIX, WAITING_FOR_FAILURE_PREFIX } from "../roles/role-guidance.js";
16
- import { baseRoleOf } from "../roles/loop-ids.js";
12
+ import { allLiveClaims } from "../runner/runner-state-load.js";
13
+ import { firstTrimmedLineStartingWith } from "../text/text.js";
14
+ import { HELD_TAG_PREFIX, NEEDS_REPLAN_PREFIX, NEEDS_REVIEW_PREFIX, PRIOR_HELD_PREFIX, WAITING_FOR_FAILURE_PREFIX } from "../roles/role-guidance.js";
15
+ import { baseRoleOf } from "../roles/runner-ids.js";
17
16
  import { roleLabel } from "../roles/role-labels.js";
18
- import { openParenthetical, trailingParenthetical } from "./backlog-md.js";
19
- import { actionableEntryRangesFor, stripEntryStamp } from "./backlog-structure.js";
17
+ import { actionableEntryRangesFor } from "./backlog-structure.js";
18
+ import { entryKey, partKey, plannedPartKeys, plansWaitingOn, requiredParts, seriesPart } from "./backlog-parts.js";
20
19
  import { finishedFiles, openBugEntries, plannedPlanEntries } from "./backlog.js";
21
- /** The `**Refused …` note prefix a refusing tick writes. Every hold note — this one and the
20
+ /** The number of holds an entry's count must reach before a Needs-review/Needs-replan note
21
+ * becomes a `held-for-operator` hold (the operator's decision, surfaced by parts 2/3 and 3/3). */
22
+ export const OPERATOR_HOLD_THRESHOLD = 3;
23
+ /** The held count a body records: the integer in the first Needs-review or Needs-replan note
24
+ * line's `(held N)` tag, else the integer in a `**Previously held N**` line, else 0. A present
25
+ * but non-numeric `(held …)` tag is 0 (and stops the scan), so a malformed tag never throws.
26
+ * Pure over the body text — the data-layer read half of the convergence backstop. */
27
+ export function heldCount(body) {
28
+ const tagRe = new RegExp(`${HELD_TAG_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\d+)\\)`);
29
+ for (const raw of body.split("\n")) {
30
+ const line = raw.trimStart();
31
+ const note = line.startsWith(NEEDS_REVIEW_PREFIX) || line.startsWith(NEEDS_REPLAN_PREFIX);
32
+ if (!note)
33
+ continue;
34
+ const tag = tagRe.exec(line);
35
+ if (tag !== null)
36
+ return Number(tag[1]);
37
+ if (line.includes(HELD_TAG_PREFIX))
38
+ return 0;
39
+ }
40
+ for (const raw of body.split("\n")) {
41
+ const line = raw.trimStart();
42
+ if (!line.startsWith(PRIOR_HELD_PREFIX))
43
+ continue;
44
+ const count = /\d+/.exec(line.slice(PRIOR_HELD_PREFIX.length));
45
+ return count === null ? 0 : Number(count[0]);
46
+ }
47
+ return 0;
48
+ }
49
+ /** The `(held N)` count on a single Needs-review or Needs-replan note line, or undefined when the
50
+ * line is not such a note or carries no numeric tag. The per-line form of heldCount, so a writer
51
+ * that drops notes (withoutHeldForOperatorNotes) can gate on the same threshold without scanning
52
+ * the whole body. */
53
+ function lineHeldCount(line) {
54
+ const trimmed = line.trimStart();
55
+ const isNote = trimmed.startsWith(NEEDS_REVIEW_PREFIX) || trimmed.startsWith(NEEDS_REPLAN_PREFIX);
56
+ if (!isNote)
57
+ return undefined;
58
+ const tag = new RegExp(`${HELD_TAG_PREFIX.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\d+)\\)`).exec(trimmed);
59
+ return tag === null ? undefined : Number(tag[1]);
60
+ }
61
+ /** The `**Refused …` note prefix a refusing run writes. Every hold note — this one and the
22
62
  * imported Needs-review and Needs-replan prefixes — is matched as a line prefix (hasNoteLine)
23
63
  * so a mention in prose never holds an entry. */
24
64
  const REFUSED_PREFIX = "**Refused ";
@@ -32,7 +72,7 @@ function hasNoteLine(body, prefix) {
32
72
  * begins with it, trimmed, with the leading `**` and the first closing `**` removed. Null when
33
73
  * no line matches — the same line-prefix rule entryHold uses. */
34
74
  function noteText(body, prefix) {
35
- const line = body.split("\n").map((l) => l.trim()).find((l) => l.startsWith(prefix));
75
+ const line = firstTrimmedLineStartingWith(body, prefix);
36
76
  return line === undefined ? null : line.replace(/^\*\*/, "").replace("**", "");
37
77
  }
38
78
  /** The text of `body`'s Refused note, or null when it has none (entryHold's "refused" hold). */
@@ -69,53 +109,51 @@ export function withoutRefusalNotes(lines) {
69
109
  export function withoutWaitingNotes(lines) {
70
110
  return withoutNoteLines(lines, WAITING_FOR_FAILURE_PREFIX);
71
111
  }
72
- /** One prerequisite ref, resolved against the entry's own series when it names none. */
73
- function parseRef(chunk, ownSeries) {
74
- const text = chunk.trim().replace(/^parts?\s+/i, "");
75
- if (text === "")
76
- return [];
77
- const m = /^(?:([\s\S]+?)\s+)?([0-9]+[a-z]?)\/(\d+)(?:\s*[–-]\s*(\d+)\/(\d+))?$/i.exec(text);
78
- if (m === null)
79
- return [];
80
- const series = (m[1]?.trim() ?? "") || ownSeries;
81
- if (series === null || series === "")
82
- return [];
83
- const of = Number(m[3]);
84
- const start = m[2].toLowerCase();
85
- if (!Number.isInteger(of) || of < 1)
86
- return [];
87
- if (m[4] === undefined)
88
- return [{ series, part: start, of }];
89
- // A range is numeric on both ends (the lettered parts never range); expand it inclusively.
90
- const from = Number(start);
91
- const to = Number(m[4]);
92
- if (!Number.isInteger(from) || !Number.isInteger(to) || to < from)
93
- return [];
94
- const parts = [];
95
- for (let p = from; p <= to; p++)
96
- parts.push({ series, part: String(p), of });
97
- return parts;
98
- }
99
- /** The stable identity of an entry heading: its `(planned …)`/`(done …)` stamp suffix
100
- * removed through the shared stripEntryStamp, whitespace collapsed, lowercased. Adding a
101
- * done stamp or a Refused note therefore leaves the key unchanged. */
102
- export function entryKey(title) {
103
- return collapseWhitespace(stripEntryStamp(title)).toLowerCase();
104
- }
105
- /** An entry's own series and part from its heading, or null when the heading is not a
106
- * `<Series>, part i/n: …` title. The series is the text before `, part i/n:`. */
107
- export function seriesPart(title) {
108
- const m = /^(.+?),\s*part\s+([0-9]+[a-z]?)\/(\d+)\s*:/i.exec(stripEntryStamp(title));
109
- if (m === null)
110
- return null;
111
- const series = m[1].trim();
112
- if (series === "")
113
- return null;
114
- return { series, part: m[2].toLowerCase(), of: Number(m[3]) };
115
- }
116
- /** The one identity a PartRef matches by: lowercased series, verbatim part token. */
117
- function partKey(series, part) {
118
- return `${series.toLowerCase()}\u0000${part}`;
112
+ /** `lines` with every Needs-review/Needs-replan note whose `(held N)` count reached
113
+ * OPERATOR_HOLD_THRESHOLD removed, together with its `**Previously held N−1**` line (the prior
114
+ * count the increment rule recorded beside the live `(held N)`), so a `--release` lift leaves
115
+ * neither behind and the entry becomes eligible. Drops nothing when no note is at or past the
116
+ * threshold, so release still cannot lift a live first/second Needs-review hold. Consecutive
117
+ * dropped lines are collapsed to one blank line, the same rule withoutNoteLines applies to a
118
+ * single dropped note. */
119
+ export function withoutHeldForOperatorNotes(lines) {
120
+ const drop = new Set();
121
+ const priorHeld = new Set();
122
+ lines.forEach((raw, i) => {
123
+ const c = lineHeldCount(raw);
124
+ if (c !== undefined && c >= OPERATOR_HOLD_THRESHOLD) {
125
+ priorHeld.add(c - 1);
126
+ drop.add(i);
127
+ }
128
+ });
129
+ if (priorHeld.size === 0)
130
+ return lines;
131
+ lines.forEach((raw, i) => {
132
+ if (drop.has(i))
133
+ return;
134
+ const trimmed = raw.trimStart();
135
+ if (!trimmed.startsWith(PRIOR_HELD_PREFIX))
136
+ return;
137
+ const count = /\d+/.exec(trimmed.slice(PRIOR_HELD_PREFIX.length));
138
+ if (count !== null && priorHeld.has(Number(count[0])))
139
+ drop.add(i);
140
+ });
141
+ const out = [];
142
+ for (let i = 0; i < lines.length; i++) {
143
+ if (!drop.has(i)) {
144
+ out.push(lines[i]);
145
+ continue;
146
+ }
147
+ let j = i;
148
+ while (j < lines.length && drop.has(j))
149
+ j++;
150
+ // A dropped run between two blank lines drops the blank after it too, so the lift leaves
151
+ // one blank line where the note was (the same rule withoutNoteLines applies to one).
152
+ if (j < lines.length && (lines[i - 1] ?? "x").trim() === "" && (lines[j] ?? "x").trim() === "")
153
+ j++;
154
+ i = j - 1;
155
+ }
156
+ return out;
119
157
  }
120
158
  /** The Done entries of the parts `title` requires, in the order its requires clause names them:
121
159
  * for each requiredParts ref, the first `## Done` entry whose seriesPart has the same partKey,
@@ -164,178 +202,9 @@ export function waitingPartRanges(root, title) {
164
202
  }
165
203
  return out;
166
204
  }
167
- /** The prerequisite-set identities of the still-planned entries — seriesPart(title) of each,
168
- * keyed by partKey — built once for a batch and reused across every entry judged against it, so
169
- * judging n entries is n seriesPart parses rather than n² (entryHold rebuilt the set per entry).
170
- * Series compare case-insensitively and part tokens verbatim, exactly as entryHoldAgainst
171
- * matches them. */
172
- export function plannedPartKeys(planned) {
173
- const keys = new Set();
174
- for (const p of planned) {
175
- const part = seriesPart(p.title);
176
- if (part !== null)
177
- keys.add(partKey(part.series, part.part));
178
- }
179
- return keys;
180
- }
181
- /** The doctor's count of a refusal's stranded dependents: the ascending indexes into `planned` of
182
- * the entries that require `planned[index]`'s own part, directly or through a chain of parts.
183
- * Matching is by partKey, as entryHoldAgainst matches. [] when that entry has no series part. A
184
- * dependency cycle ends the walk, and `index` itself is never listed. */
185
- export function plansWaitingOn(planned, index) {
186
- const root = planned[index];
187
- const own = root === undefined ? null : seriesPart(root.title);
188
- if (own === null)
189
- return [];
190
- const dependents = new Map();
191
- planned.forEach((entry, i) => {
192
- if (seriesPart(entry.title) === null)
193
- return;
194
- for (const ref of requiredParts(entry.title)) {
195
- const key = partKey(ref.series, ref.part);
196
- dependents.set(key, [...(dependents.get(key) ?? []), i]);
197
- }
198
- });
199
- const seen = new Set([index]);
200
- const queue = [partKey(own.series, own.part)];
201
- for (let key = queue.shift(); key !== undefined; key = queue.shift()) {
202
- for (const i of dependents.get(key) ?? []) {
203
- if (seen.has(i))
204
- continue;
205
- seen.add(i);
206
- const part = seriesPart(planned[i].title);
207
- queue.push(partKey(part.series, part.part));
208
- }
209
- }
210
- seen.delete(index);
211
- return [...seen].sort((a, b) => a - b);
212
- }
213
- /** The prerequisite parts a plan heading's trailing parenthetical (or, when the heading wraps
214
- * and the parenthetical stays open past the line, its open parenthetical) names through the clause
215
- * `requires <ref>((, | and )<ref>)* landed`, with each range (e.g. `parts 1/5–3/5`) expanded.
216
- * Returns [] when the heading has no such clause or the clause does not parse — an agent judges
217
- * an unparseable clause, exactly as it does today. Only the heading is read, never the body. */
218
- export function requiredParts(title) {
219
- const clause = requiresClause(title);
220
- if (clause === null)
221
- return [];
222
- const ownSeries = seriesPart(title)?.series ?? null;
223
- const refs = [];
224
- for (const chunk of clause.split(/\s*,\s*|\s+and\s+/i))
225
- refs.push(...parseRef(chunk, ownSeries));
226
- return refs;
227
- }
228
- /** The ref text a heading's `requires <refs> landed` clause captures — read from the trailing
229
- * parenthetical, or the open one of a wrapped heading — or null when the heading has no such
230
- * clause. requiredParts parses it; planDependencyProblems uses it to spot a clause that parses to
231
- * nothing. */
232
- function requiresClause(title) {
233
- let meta = trailingParenthetical(title);
234
- if (meta === "")
235
- meta = openParenthetical(title);
236
- if (meta === "")
237
- return null;
238
- const m = /requires\s+(.+?)\s+landed\b/i.exec(meta); // vocabulary-ok: parses the requires … landed heading syntax
239
- return m === null ? null : m[1];
240
- }
241
- /** A dependency verb followed by a series part ref in a heading's parenthetical — the wording the
242
- * claim allocator ignores (BUGS.md 2026-10-14: a part headed "blocked by part 1/2 above" was handed
243
- * out six times while part 1/2 was in flight). planDependencyProblems reports each such ref. */
244
- const MISWORDED_DEPENDENCY_RE = /\b(?:blocked by|blocked on|after|follows|depends on|waits on|waits for|once)\s+(?:the\s+)?(parts?\s+[0-9]+[a-z]?\/[0-9]+)/gi;
245
- /** The mistakes in the planned entries' `requires … landed` clauses that the claim allocator
246
- * cannot see: `dangling` — a ref naming a part no planned or done heading carries (its hold is
247
- * void), `unparsed` — titles whose clause yields no refs, and `cycle` — the first dependency cycle
248
- * among planned `Series, part i/n:` entries as `Series i/n` names in walk order, first name
249
- * repeated last (a self-reference is a one-hop cycle), or null — and `misworded`: a series part
250
- * whose heading parenthetical names a still-planned part through a dependency verb ("blocked by
251
- * part 1/2") with no requires clause holding that part, so the allocator hands it out now.
252
- * Refused and needs-review entries count like the rest, since their clauses still decide when
253
- * they unblock. */
254
- export function planDependencyProblems(planned, doneTitles) {
255
- const known = plannedPartKeys(planned);
256
- for (const t of doneTitles) {
257
- const part = seriesPart(t);
258
- if (part !== null)
259
- known.add(partKey(part.series, part.part));
260
- }
261
- const dangling = [];
262
- const unparsed = [];
263
- const misworded = [];
264
- const plannedKeys = plannedPartKeys(planned);
265
- const nodes = new Map();
266
- for (const entry of planned) {
267
- const refs = requiredParts(entry.title);
268
- if (refs.length === 0 && requiresClause(entry.title) !== null)
269
- unparsed.push(entry.title);
270
- for (const ref of refs) {
271
- if (!known.has(partKey(ref.series, ref.part)))
272
- dangling.push({ title: entry.title, ref: `${ref.series} ${ref.part}/${ref.of}` });
273
- }
274
- const own = seriesPart(entry.title);
275
- if (own === null)
276
- continue;
277
- let meta = trailingParenthetical(entry.title);
278
- if (meta === "")
279
- meta = openParenthetical(entry.title);
280
- const held = new Set(refs.map((ref) => partKey(ref.series, ref.part)));
281
- for (const m of meta.matchAll(MISWORDED_DEPENDENCY_RE)) {
282
- for (const ref of parseRef(m[1], own.series)) {
283
- const key = partKey(ref.series, ref.part);
284
- if (!plannedKeys.has(key) || held.has(key))
285
- continue;
286
- const name = `${ref.series} ${ref.part}/${ref.of}`;
287
- if (!misworded.some((w) => w.title === entry.title && w.ref === name))
288
- misworded.push({ title: entry.title, ref: name });
289
- }
290
- }
291
- nodes.set(partKey(own.series, own.part), {
292
- name: `${own.series} ${own.part}/${own.of}`,
293
- deps: refs.map((ref) => partKey(ref.series, ref.part)),
294
- });
295
- }
296
- const state = new Map();
297
- const stack = [];
298
- const visit = (key) => {
299
- state.set(key, "open");
300
- stack.push(key);
301
- for (const dep of nodes.get(key).deps) {
302
- if (!nodes.has(dep))
303
- continue;
304
- const seen = state.get(dep);
305
- if (seen === "open") {
306
- return [...stack.slice(stack.indexOf(dep)), dep].map((k) => nodes.get(k).name);
307
- }
308
- if (seen === undefined) {
309
- const found = visit(dep);
310
- if (found !== null)
311
- return found;
312
- }
313
- }
314
- stack.pop();
315
- state.set(key, "done");
316
- return null;
317
- };
318
- let cycle = null;
319
- for (const key of nodes.keys()) {
320
- if (state.has(key))
321
- continue;
322
- cycle = visit(key);
323
- if (cycle !== null)
324
- break;
325
- }
326
- return { cycle, dangling, unparsed, misworded };
327
- }
328
- /** The prerequisite refs of `title`'s `requires … landed` clause when none of them is still in
329
- * `plannedParts` (built by plannedPartKeys), else []. A heading with no parseable clause is never
330
- * reported, so only a refusal whose named prerequisites have all left `## Planned` reads as
331
- * possibly stale. */
332
- export function prerequisitesMerged(title, plannedParts) {
333
- const refs = requiredParts(title);
334
- return refs.some((ref) => plannedParts.has(partKey(ref.series, ref.part))) ? [] : refs;
335
- }
336
205
  /** Why `entry` is held against the still-`planned` entries, or null when it may be taken: a
337
206
  * `**Refused …` line (refused), a Needs-review line (needs-review), a Needs-replan line
338
- * (needs-replan — the plan loop owns it), or a prerequisite `(series, part)` still among
207
+ * (needs-replan — the plan role owns it), or a prerequisite `(series, part)` still among
339
208
  * `planned` ({ blockedBy }). Series compare case-insensitively; part tokens compare verbatim.
340
209
  * Bodies mentioning "requires" are never consulted — only the heading's trailing or
341
210
  * still-open parenthetical. */
@@ -348,10 +217,15 @@ export function entryHold(entry, planned) {
348
217
  export function entryHoldAgainst(entry, plannedParts) {
349
218
  if (hasNoteLine(entry.body, REFUSED_PREFIX))
350
219
  return "refused";
351
- if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX))
352
- return "needs-review";
353
- if (hasNoteLine(entry.body, NEEDS_REPLAN_PREFIX))
354
- return "needs-replan";
220
+ if (hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX) || hasNoteLine(entry.body, NEEDS_REPLAN_PREFIX)) {
221
+ // A note whose held count reached OPERATOR_HOLD_THRESHOLD stops out of the role loop:
222
+ // neither the plan role (planBacklogFull tests only these two) nor this role takes it.
223
+ return heldCount(entry.body) >= OPERATOR_HOLD_THRESHOLD
224
+ ? "held-for-operator" // vocabulary-ok: the operator-held string names the operator hold decision
225
+ : hasNoteLine(entry.body, NEEDS_REVIEW_PREFIX)
226
+ ? "needs-review"
227
+ : "needs-replan";
228
+ }
355
229
  if (hasNoteLine(entry.body, WAITING_FOR_FAILURE_PREFIX))
356
230
  return "waiting-for-failure";
357
231
  const refs = requiredParts(entry.title);
@@ -375,12 +249,22 @@ export function holdMark(hold) {
375
249
  return " [blocked: needs review]";
376
250
  if (hold === "needs-replan")
377
251
  return " [blocked: needs replan]";
252
+ if (hold === "held-for-operator")
253
+ return " [blocked: held for operator]"; // vocabulary-ok: this hold names the operator-held wording for the operator decision
378
254
  if (hold === "waiting-for-failure")
379
255
  return " [blocked: waiting for the next failure]";
380
256
  return ` [blocked: requires ${hold.blockedBy.join(", ")}]`;
381
257
  }
258
+ /** The line `plan|bug --release` and `--top` print when the entry they touched is still held:
259
+ * empty for no hold, else the hold in holdMark wording and that no role takes the entry until it
260
+ * clears. */
261
+ export function stillHeldNote(hold) {
262
+ if (hold === null)
263
+ return "";
264
+ return `still held ${holdMark(hold).trim()}: no role takes it until that clears`;
265
+ }
382
266
  /** `/api/status` polls the holds on the 1-second dashboard poll and the TUI recomputes them each
383
- * frame while PLANS.md/BUGS.md change only when a loop merges an edit. The file reads beneath are
267
+ * frame while PLANS.md/BUGS.md change only when a role merges an edit. The file reads beneath are
384
268
  * already stat-cached (plannedPlanEntries/openBugEntries), but the per-entry eligibility walk
385
269
  * (entryHoldAgainst's body scan + heading clause) is pure CPU re-derived on every call, growing
386
270
  * with the append-only backlog. Cache the derived holds keyed on both files' stat identities — the
@@ -447,16 +331,57 @@ export function backlogMarks(root) {
447
331
  bugs: holds.bugs.map((h, i) => mark(h, working.bugs[i])),
448
332
  };
449
333
  }
450
- /** The verbatim titles of the planned plans, then the open bugs, whose hold is `"refused"`, in
451
- * file order: the entries that wait on an operator decision (the `refused` team alert's
452
- * input). Pairs backlogHolds' arrays with the entries the same readers return, like
334
+ /** The planned plans whose hold is `"held-for-operator"`, in file order: the plans parked
335
+ * until the operator lifts them with `tumwater plan --release` (the `operator-held` team
336
+ * alert's input), each carrying the backlog file and its 0-based position so a surface
337
+ * opening the row can land on it, the same RefusedEntry shape refusedEntries returns. Only
338
+ * plans — Needs-review/Needs-replan notes are a plan note, so a bug is never held this way.
339
+ * A missing file yields `[]`. */
340
+ export function operatorHeldEntries(root) {
341
+ const holds = backlogHolds(root);
342
+ const out = [];
343
+ plannedPlanEntries(root).forEach((e, i) => {
344
+ if (holds.plans[i] === "held-for-operator")
345
+ out.push({ file: "plans", index: i, title: e.title }); // vocabulary-ok: the held-for-operator hold's key
346
+ });
347
+ return out;
348
+ }
349
+ /** The planned plans whose hold is exactly `"needs-review"` or `"needs-replan"`, in file
350
+ * order: the plans parked by a Needs-review/Needs-replan note until the plan role splits or
351
+ * replans them (the `stalled-plans` team alert's input), each carrying the 0-based PLANS.md
352
+ * index and the hold. Same `backlogHolds(root)` + `plannedPlanEntries(root)` pair
353
+ * `operatorHeldEntries` uses, so the alert and the doctor's `stalled plans` row classify
354
+ * through the same `entryHold` and cannot disagree on which holds are role-resolvable. A
355
+ * held-for-operator plan (a note at the held threshold) is excluded — it feeds
356
+ * `operatorHeldEntries` and the `operator-held` alert instead. A missing file yields `[]`. */
357
+ export function stalledPlanEntries(root) {
358
+ const holds = backlogHolds(root);
359
+ const out = [];
360
+ plannedPlanEntries(root).forEach((e, i) => {
361
+ const hold = holds.plans[i];
362
+ if (hold === "needs-review" || hold === "needs-replan")
363
+ out.push({ index: i, title: e.title, hold });
364
+ });
365
+ return out;
366
+ }
367
+ /** The planned plans, then the open bugs, whose hold is `"refused"`, in file order: the
368
+ * entries that wait on an operator decision (the `refused` team alert's input), each carrying
369
+ * the backlog file and its 0-based position so a surface opening the row can land on it.
370
+ * Pairs backlogHolds' arrays with the entries the same readers return, like
453
371
  * backlogInProgress. A missing file yields `[]`. */
454
- export function refusedEntryTitles(root) {
372
+ export function refusedEntries(root) {
455
373
  const holds = backlogHolds(root);
456
- const titlesOf = (entries, held) => entries.filter((_, i) => held[i] === "refused").map((e) => e.title);
374
+ const entriesOf = (entries, held, file) => {
375
+ const out = [];
376
+ entries.forEach((e, i) => {
377
+ if (held[i] === "refused")
378
+ out.push({ file, index: i, title: e.title });
379
+ });
380
+ return out;
381
+ };
457
382
  return [
458
- ...titlesOf(plannedPlanEntries(root), holds.plans),
459
- ...titlesOf(openBugEntries(root), holds.bugs),
383
+ ...entriesOf(plannedPlanEntries(root), holds.plans, "plans"),
384
+ ...entriesOf(openBugEntries(root), holds.bugs, "bugs"),
460
385
  ];
461
386
  }
462
387
  /** The runner id holding a live claim on each planned plan and open bug, two arrays parallel
@@ -475,7 +400,15 @@ export function backlogInProgress(root) {
475
400
  export function inProgressMark(runner) {
476
401
  return runner === null ? "" : ` [working on it: ${roleLabel(runner)}]`;
477
402
  }
478
- /** The entries a `role` loop may take now, in file order: PLANS.md's `## Planned` entries for
403
+ /** The note a cancel surface adds when the cancelled entry was claimed: empty for `null`, else
404
+ * names the role (through roleLabel) and the `abort` that stops its run, which the cancel itself
405
+ * leaves going. */
406
+ export function cancelledClaimNote(runner) {
407
+ return runner === null
408
+ ? ""
409
+ : `${roleLabel(runner)} is still working on it — its run goes on; \`tumwater abort --role ${runner}\` stops it`;
410
+ }
411
+ /** The entries `role` may take now, in file order: PLANS.md's `## Planned` entries for
479
412
  * feature, BUGS.md's `## Open` entries for bugfix, minus every entry entryHold holds, each with
480
413
  * its stamp-free key, verbatim title and 1-based line range. Reads the primary checkout through
481
414
  * the same stat-cached readers the index uses; a missing or unreadable file yields []. */
@@ -491,7 +424,7 @@ export function eligibleEntries(root, role) {
491
424
  const parts = plannedPartKeys(planned);
492
425
  // The cross-role in-flight hold (BUGS.md 2026-10-09): an entry another role's base has
493
426
  // claimed is not eligible here. Same-base claims are excluded from the hold — the scheduling
494
- // pass's claim groups and tick-stage's staged claims already hold those — so the queried
427
+ // pass's claim groups and run-stage's staged claims already hold those — so the queried
495
428
  // role's own claim (and a sibling instance's) stays eligible and its release rules are
496
429
  // unchanged; only a plan another base currently owns is taken off this base's hands.
497
430
  const ownBase = baseRoleOf(role);
@@ -0,0 +1,149 @@
1
+ /** Renders the `<backlog-index>` prompt block every role prompt carries — each actionable
2
+ * entry (PLANS.md ## Planned, BUGS.md ## Open, QUESTIONS.md ## Open) plus the newest closed
3
+ * ones, each with the 1-based line range it occupies in its file — the bounded replacement for
4
+ * the per-run `grep -n '^##'` heading map. The entry-range parsing (actionableEntryRanges) and
5
+ * series collapsing (groupBlockedSeries, partsLabel) it builds on live in backlog-structure.ts
6
+ * alongside the stranded-plan checks; this module holds only the block's rendering and its
7
+ * per-file stat caches. Extracted from backlog-structure.ts (organized 2026-10-19) so that
8
+ * module no longer carries a second responsibility. */
9
+ import path from "node:path";
10
+ import { cachedTextByStat } from "../files/stat-cache.js";
11
+ import { FINISHED_ARCHIVES } from "./backlog-md.js";
12
+ import { allLiveClaims } from "../runner/runner-state-load.js";
13
+ import { collapseWhitespace, truncate } from "../text/text.js";
14
+ import { actionableEntryRanges, groupBlockedSeries, partsLabel, stripEntryStamp, } from "./backlog-structure.js";
15
+ import { entryHoldAgainst, holdMark } from "./backlog-eligibility.js";
16
+ import { plannedPartKeys } from "./backlog-parts.js";
17
+ import { plannedPlanEntries } from "./backlog.js";
18
+ /** Max characters an index title keeps before a trailing ellipsis, so one very long BUGS.md
19
+ * heading cannot dominate the block. */
20
+ const INDEX_TITLE_MAX = 160;
21
+ /** The index title for a `### ` heading: its stamp suffix (`(reported … 2026-…)`) removed via
22
+ * the shared stripEntryStamp and whitespace collapsed, then clipped to INDEX_TITLE_MAX
23
+ * through text.ts's truncate — the one ellipsis rule, so the cut cannot drift from the
24
+ * other trimmed labels and never splits a surrogate pair. */
25
+ function indexTitle(heading) {
26
+ return truncate(collapseWhitespace(stripEntryStamp(heading)), INDEX_TITLE_MAX);
27
+ }
28
+ /** The files and sections the actionable index covers, in the order roles read them: PLANS.md's
29
+ * planned features, BUGS.md's open bugs, QUESTIONS.md's open questions. */
30
+ const INDEX_SECTIONS = [
31
+ { file: "PLANS.md", section: "Planned" },
32
+ { file: "BUGS.md", section: "Open" },
33
+ { file: "QUESTIONS.md", section: "Open" },
34
+ ];
35
+ /** How many closed entries per closed section the index lists. */
36
+ const RECENT_CLOSED_MAX = 5;
37
+ /** The closed sections whose newest entries the index also lists, each read from the live file
38
+ * and then its archive (FINISHED_ARCHIVES, plans/backlog-archive.md); every one keeps its entries
39
+ * newest first, so the first entries in file order are the recent ones. */
40
+ const CLOSED_SECTIONS = FINISHED_ARCHIVES.map((a) => ({
41
+ files: [a.file, a.archive],
42
+ section: a.section,
43
+ }));
44
+ /** Per-file cache of the parsed actionable entries (indexSectionCache). */
45
+ const indexSectionCache = new Map();
46
+ /** Per-file cache of the newest closed entries' ranges; separate from indexSectionCache, which is
47
+ * keyed by the file path alone and PLANS.md would collide. */
48
+ const closedSectionCache = new Map();
49
+ /** One `IndexRow` as an ordinary index line: range, truncated stamp-free title, hold mark, and
50
+ * the in-flight suffix when a live claim names it. The collapsed-series line is built by
51
+ * collapseBlockedSeries, returned alongside these. */
52
+ function renderIndexRow(row) {
53
+ return `- ${row.start}-${row.end}: ${indexTitle(row.title)}${row.mark}${row.claimer ? ` [in flight by ${row.claimer}]` : ""}`;
54
+ }
55
+ function collapseBlockedSeries(rows) {
56
+ return groupBlockedSeries(rows).map((g) => {
57
+ if (g.kind === "row")
58
+ return renderIndexRow(g.row);
59
+ const first = g.rows[0];
60
+ const last = g.rows[g.rows.length - 1];
61
+ return (`- ${first.start}-${last.end}: ${indexTitle(g.series)}, ${g.rows.length} blocked parts ` +
62
+ `(${partsLabel(g.rows)}) [blocked: each waits on an earlier part]`);
63
+ });
64
+ }
65
+ /** The `<backlog-index>` prompt block: each actionable entry (PLANS.md ## Planned, BUGS.md
66
+ * ## Open, QUESTIONS.md ## Open) with its 1-based line range, rendered from the primary
67
+ * checkout `root`. Replaces the per-run `grep -n '^##'` heading map, whose pattern also matched
68
+ * every `###` title in BUGS.md's closed history and so re-sent ~108 KB on every turn (BUGS.md
69
+ * 2026-10-06). A missing or unreadable file contributes no sections; undefined when no section
70
+ * has entries, so an empty backlog leaves the prompt unchanged. The per-file actionable entries
71
+ * are stat-cached (indexSectionCache), so an unchanged backlog file is not re-read or re-walked. */
72
+ export function renderBacklogIndexBlock(root) {
73
+ // The still-planned set every section's holds are judged against. A prerequisite clause rides
74
+ // in the entry heading, and the eligibility gate (backlog-eligibility.ts's eligibleEntries)
75
+ // and `tumwater backlog`'s holds read it for BUGS.md's Open entries too, so the same set must
76
+ // reach this block's Open section as well — otherwise a held bug reads as eligible here while
77
+ // the gate skips it. plannedPlanEntries is stat-cached by sectionEntries, so an unchanged
78
+ // PLANS.md costs one stat per call.
79
+ const planned = plannedPlanEntries(root);
80
+ const plannedParts = plannedPartKeys(planned);
81
+ // The cross-role in-flight hold (BUGS.md 2026-10-09): every role's prompts share this one
82
+ // section-agnostic index, and the role it is assembled for is not available here, so an entry
83
+ // claimed by ANY role is marked in flight — the owner's own index marks it too, which is harmless
84
+ // (its assignment note is the instruction), and every other role reads it as taken instead of
85
+ // re-implementing a change someone's merge is already carrying to main.
86
+ const claimed = allLiveClaims(root);
87
+ const listed = [];
88
+ for (const { file, section } of INDEX_SECTIONS) {
89
+ const full = path.join(root, file);
90
+ const entries = cachedTextByStat(indexSectionCache, full, full, (md) => {
91
+ const ranges = actionableEntryRanges(md, section);
92
+ if (ranges.length === 0)
93
+ return [];
94
+ const lines = md.split(/\r?\n/);
95
+ // The body slice is the lines after the heading through the last body line, so a note
96
+ // is found whether it sits first or last (the heading is at 1-based line e.start).
97
+ return ranges.map((e) => ({
98
+ start: e.start,
99
+ end: e.end,
100
+ title: e.title,
101
+ body: lines.slice(e.start, e.end).join("\n"),
102
+ }));
103
+ }, (v) => v.slice());
104
+ if (entries === null || entries.length === 0)
105
+ continue;
106
+ listed.push(`${file} ## ${section}`);
107
+ // Build the rows once per section, then collapse a PLANS.md series' blocked parts (this
108
+ // plan) — BUGS.md and QUESTIONS.md sections keep every entry on its own line.
109
+ const rows = entries.map((e) => {
110
+ const hold = entryHoldAgainst({ title: e.title, body: e.body }, plannedParts);
111
+ const key = collapseWhitespace(stripEntryStamp(e.title)).toLowerCase();
112
+ return {
113
+ start: e.start,
114
+ end: e.end,
115
+ title: e.title,
116
+ hold,
117
+ mark: holdMark(hold),
118
+ claimer: claimed.get(key),
119
+ };
120
+ });
121
+ const isPlans = file === "PLANS.md" && section === "Planned";
122
+ listed.push(...(isPlans ? collapseBlockedSeries(rows) : rows.map(renderIndexRow)));
123
+ }
124
+ for (const { files, section } of CLOSED_SECTIONS) {
125
+ // The newest RECENT_CLOSED_MAX across the live section (normally empty) and the archive.
126
+ let room = RECENT_CLOSED_MAX;
127
+ for (const file of files) {
128
+ if (room <= 0)
129
+ break;
130
+ const full = path.join(root, file);
131
+ const entries = cachedTextByStat(closedSectionCache, full, full, (md) => actionableEntryRanges(md, section).slice(0, RECENT_CLOSED_MAX), (v) => v.slice())?.slice(0, room);
132
+ if (entries === undefined || entries.length === 0)
133
+ continue;
134
+ room -= entries.length;
135
+ listed.push(`${file} ## ${section} (newest ${entries.length})`);
136
+ for (const e of entries)
137
+ listed.push(`- ${e.start}-${e.end}: ${indexTitle(e.title)}`);
138
+ }
139
+ }
140
+ if (listed.length === 0)
141
+ return undefined;
142
+ return `Backlog index — each actionable entry, then the newest closed ones, with the 1-based line range it occupies in its file.
143
+ Read the entries you need by those ranges; the archive files (PLANS-DONE.md, BUGS-FIXED.md) grow
144
+ with every merge, so search them rather than reading them. Blocked parts of a multi-part series are
145
+ shown as one line per series.
146
+ <backlog-index>
147
+ ${listed.join("\n")}
148
+ </backlog-index>`;
149
+ }