tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -47,6 +47,8 @@ export function createTuiPanes(deps) {
47
47
  session.dropBrokenPinsArmed = false;
48
48
  session.armedRetireLeftovers = session.retireLeftoversArmed;
49
49
  session.retireLeftoversArmed = false;
50
+ session.armedArchiveBacklog = session.archiveBacklogArmed;
51
+ session.archiveBacklogArmed = false;
50
52
  session.armedRevert = session.revertArmed;
51
53
  session.revertArmed = null;
52
54
  session.armedQueue = session.queueArmed;
@@ -114,57 +116,47 @@ export function createTuiPanes(deps) {
114
116
  }
115
117
  session.requestRender();
116
118
  };
117
- /** Run the pane arms of the key dispatch; true when one consumed the key. `es` is the
118
- * editors' state, so the exclusion guards read the flags the editors hold. Each pane module
119
- * checks its own view/guard and returns true only when it consumed the key; this router keeps
120
- * the keys that multiple panes serve in today's priority order. */
121
- const handleKey = (key, es) => {
122
- // The editor flags below are all false here — the editors consume every key that reads or
123
- // writes the line and return true whenever one is open — but each module restates its
124
- // mutual-exclusion guard so it reads on its own instead of leaning on the routing order.
125
- const ctx = { es };
126
- if (key.ctrl && key.name === "n") {
127
- // Ctrl+N means four different things. The Health arm is routed first so its notify-test
128
- // action is reachable on Health; the History trail arm second, which falls through
129
- // (returns false) unless the History view is selected; the Pending overlap drill-down
130
- // third, with the same fall-through; the transcript pane's notes/change arm last.
131
- // The order among them is NOT immaterial: the transcript arm
132
- // consumes Ctrl+N whenever it is reached — it returns true with a render even off a
133
- // transcript view — so it must sit last, as the fallback, or it would swallow the key
134
- // before the Health or History arm ever saw it.
135
- if (health.handleKey?.(key, ctx))
136
- return true;
137
- if (history.handleKey?.(key, ctx))
138
- return true;
139
- if (pending.handleKey?.(key, ctx))
140
- return true;
141
- return transcript.handleKey?.(key, ctx) ?? false;
142
- }
143
- if (key.ctrl && key.name === "g") {
144
- // Ctrl+G's scope arm is routed by view: Activity on 0, the History exact-role scope on
145
- // N+4, the Pending overlap scope on N+5, and the report team/base-role scope
146
- // everywhere else — the priority the original else-branch chain kept, so the
147
- // report arm never steals the History view's slot.
148
- if (session.view === 0)
149
- return activity.handleKey?.(key, ctx) ?? false;
150
- if (session.view === session.roleIds.length + 4)
151
- return history.handleKey?.(key, ctx) ?? false;
152
- if (session.view === session.roleIds.length + 5)
153
- return pending.handleKey?.(key, ctx) ?? false;
154
- return report.handleKey?.(key, ctx) ?? false;
155
- }
156
- if (key.ctrl && key.name === "e") {
157
- // Ctrl+E's Health reclaim arm is routed ahead of the report panes' trailing-window
158
- // cycle; the Health arm's own view guard keeps Usage/Failures on their cycle, and the
159
- // Backlog/Inbox edit arms (which live in the editors) are untouched.
160
- if (health.handleKey?.(key, ctx))
161
- return true;
162
- return report.handleKey?.(key, ctx) ?? false;
163
- }
164
- if (key.ctrl && key.name === "k")
165
- return health.handleKey?.(key, ctx) ?? false;
166
- if (key.ctrl && key.name === "w")
119
+ const routeCtrlN = (key, ctx) => {
120
+ // Ctrl+N means four different things. The Health arm is routed first so its notify-test
121
+ // action is reachable on Health; the History trail arm second, which falls through
122
+ // (returns false) unless the History view is selected; the Pending overlap drill-down
123
+ // third, with the same fall-through; the transcript pane's notes/change arm last.
124
+ // The order among them is NOT immaterial: the transcript arm
125
+ // consumes Ctrl+N whenever it is reached — it returns true with a render even off a
126
+ // transcript view — so it must sit last, as the fallback, or it would swallow the key
127
+ // before the Health or History arm ever saw it.
128
+ if (health.handleKey?.(key, ctx))
129
+ return true;
130
+ if (history.handleKey?.(key, ctx))
131
+ return true;
132
+ if (pending.handleKey?.(key, ctx))
133
+ return true;
134
+ return transcript.handleKey?.(key, ctx) ?? false;
135
+ };
136
+ const routeCtrlG = (key, ctx) => {
137
+ // Ctrl+G's scope arm is routed by view: Activity on 0, the History exact-role scope on
138
+ // N+4, the Pending overlap scope on N+5, and the report team/base-role scope
139
+ // everywhere else — the priority the original else-branch chain kept, so the
140
+ // report arm never steals the History view's slot.
141
+ if (session.view === 0)
142
+ return activity.handleKey?.(key, ctx) ?? false;
143
+ if (session.view === session.roleIds.length + 4)
167
144
  return history.handleKey?.(key, ctx) ?? false;
145
+ if (session.view === session.roleIds.length + 5)
146
+ return pending.handleKey?.(key, ctx) ?? false;
147
+ return report.handleKey?.(key, ctx) ?? false;
148
+ };
149
+ const routeCtrlE = (key, ctx) => {
150
+ // Ctrl+E's Health reclaim arm is routed ahead of the report panes' trailing-window
151
+ // cycle; the Health arm's own view guard keeps Usage/Failures on their cycle, and the
152
+ // Backlog/Inbox edit arms (which live in the editors) are untouched.
153
+ if (health.handleKey?.(key, ctx))
154
+ return true;
155
+ return report.handleKey?.(key, ctx) ?? false;
156
+ };
157
+ const routeCtrlK = (key, ctx) => health.handleKey?.(key, ctx) ?? false;
158
+ const routeCtrlW = (key, ctx) => history.handleKey?.(key, ctx) ?? false;
159
+ const routeCtrlS = (key, ctx) => {
168
160
  // Health owns Ctrl+S (drop broken pins) ahead of the pane router's fallthrough; off the
169
161
  // Health view it falls through so the quiet-hours (Activity), trailing-window (History)
170
162
  // and min-run-interval (Transcript) arms keep their current meanings.
@@ -177,15 +169,20 @@ export function createTuiPanes(deps) {
177
169
  }
178
170
  if (key.ctrl && key.name === "s")
179
171
  return history.handleKey?.(key, ctx) ?? false;
172
+ return false;
173
+ };
174
+ const routeCtrlP = (key, ctx) => {
180
175
  // Ctrl+P on Usage/Failures steps the report window back; every other view is History's.
181
176
  if (key.ctrl && key.name === "p" && (session.view === session.roleIds.length + 2 || session.view === session.roleIds.length + 3)) {
182
177
  return report.handleKey?.(key, ctx) ?? false;
183
178
  }
184
- // Health owns Ctrl+P (re-probe image-input models) ahead of History's fallback.
185
- if (key.ctrl && key.name === "p" && session.view === session.roleIds.length + 7)
186
- return health.handleKey?.(key, ctx) ?? false;
179
+ // Health owns none of the Ctrl+P cycle now that the image-model re-probe is gone; it
180
+ // falls through to History's Ctrl+P below.
187
181
  if (key.ctrl && key.name === "p")
188
182
  return history.handleKey?.(key, ctx) ?? false;
183
+ return false;
184
+ };
185
+ const routeCtrlA = (key, ctx) => {
189
186
  // Health owns Ctrl+A (commit-backlog) ahead of the pane router's fallthrough; the Backlog
190
187
  // view's Ctrl+A (the answer editor) is handled by tui-editor-answer.ts, which the key
191
188
  // dispatcher consults before this router, so this case never shadows it.
@@ -195,10 +192,24 @@ export function createTuiPanes(deps) {
195
192
  // Health's so the Health commit-backlog arm keeps its view's slot.
196
193
  if (key.ctrl && key.name === "a" && session.view === session.roleIds.length + 5)
197
194
  return pending.handleKey?.(key, ctx) ?? false;
195
+ return false;
196
+ };
197
+ const routeCtrlU = (key, ctx) => {
198
198
  // Health owns Ctrl+U (re-sync install) ahead of the pane router's fallthrough; off the
199
199
  // Health view it falls through so the prompt-line editor keeps Ctrl+U (kill-to-start).
200
200
  if (key.ctrl && key.name === "u" && session.view === session.roleIds.length + 7)
201
201
  return health.handleKey?.(key, ctx) ?? false;
202
+ return false;
203
+ };
204
+ const routeCtrlB = (key, ctx) => {
205
+ // Health owns Ctrl+B (archive backlog) ahead of the pane router's fallthrough; the prompt-
206
+ // line editors are consulted first, so the budget editor keeps Ctrl+B where it applies,
207
+ // and off the Health view Ctrl+B is otherwise unbound.
208
+ if (key.ctrl && key.name === "b" && session.view === session.roleIds.length + 7)
209
+ return health.handleKey?.(key, ctx) ?? false;
210
+ return false;
211
+ };
212
+ const routeCtrlL = (key, ctx) => {
202
213
  // Health owns Ctrl+L (retire leftovers) ahead of the pane router's fallthrough; off the
203
214
  // Health view it falls through so the Activity fallback shift (view 0) and the
204
215
  // instructions editor on a role's Transcript keep their current meanings.
@@ -209,104 +220,153 @@ export function createTuiPanes(deps) {
209
220
  // (handled in the editors ahead of this router) keep their current meanings.
210
221
  if (key.ctrl && key.name === "l")
211
222
  return history.handleKey?.(key, ctx) ?? false;
212
- if (key.ctrl && key.name === "y") {
213
- // Ctrl+Y's transcript change-step arm (the diffstat toggle) is routed first and falls
214
- // through off it, so History keeps its Summary cycle on every other view.
215
- if (transcript.handleKey?.(key, ctx))
216
- return true;
217
- return history.handleKey?.(key, ctx) ?? false;
218
- }
219
- if (key.ctrl && key.name === "o") {
220
- // The Activity type cycle is routed first on its own view (0); on the Health view the
221
- // problems-only toggle precedes the History outcome cycle; on the Pending view the
222
- // post-merge review state filter is routed ahead of History's outcome cycle (its own arm
223
- // consumes the key on Pending); on the Backlog view the Ready-only filter is routed
224
- // ahead of History's outcome cycle, and every other view keeps the History outcome cycle.
225
- if (session.view === 0)
226
- return activity.handleKey?.(key, ctx) ?? false;
227
- if (session.view === session.roleIds.length + 7)
228
- return health.handleKey?.(key, ctx) ?? false;
229
- if (session.view === session.roleIds.length + 5)
230
- return pending.handleKey?.(key, ctx) ?? false;
231
- if (session.view === session.roleIds.length + 1)
232
- return backlog.handleKey?.(key, ctx) ?? false;
233
- return history.handleKey?.(key, ctx) ?? false;
234
- }
235
- if (key.ctrl && key.name === "x") {
236
- // The armed-clear Ctrl+X arms: the transcript notes first, then the History model filter,
237
- // then the queued cancel, then the Pending merge cancel, then the backlog cancel — each
238
- // returns false when its view/selection does not apply.
239
- if (transcript.handleKey?.(key, ctx))
240
- return true;
241
- if (history.handleKey?.(key, ctx))
242
- return true;
243
- if (queued.handleKey?.(key, ctx))
244
- return true;
245
- if (pending.handleKey?.(key, ctx))
246
- return true;
247
- return backlog.handleKey?.(key, ctx) ?? false;
248
- }
249
- if (key.ctrl && key.name === "r") {
250
- // The Ctrl+R arms: the Pending pane's queue/roster re-read, the Health pane's doctor
251
- // re-run, the History trail's transcript cycle, then the backlog's release — each returns
252
- // false when its view does not apply, so the order among them is immaterial.
253
- if (pending.handleKey?.(key, ctx))
254
- return true;
255
- if (health.handleKey?.(key, ctx))
256
- return true;
257
- if (history.handleKey?.(key, ctx))
258
- return true;
259
- return backlog.handleKey?.(key, ctx) ?? false;
260
- }
261
- if (key.ctrl && key.name === "v") {
262
- // The Ctrl+V arms, routed like Ctrl+X's: the History trail's revert, the queued
263
- // prompt's move-to-top, then the Pending pane's merge move-to-top, and the backlog
264
- // entry's move-to-top last — each returns false when its view/selection does not
265
- // apply. The Activity attach editor's own Ctrl+V arm is guarded to view 0, so it
266
- // still fires there ahead of this router.
267
- if (history.handleKey?.(key, ctx))
268
- return true;
269
- if (queued.handleKey?.(key, ctx))
270
- return true;
271
- if (pending.handleKey?.(key, ctx))
272
- return true;
223
+ return false;
224
+ };
225
+ const routeCtrlY = (key, ctx) => {
226
+ // Ctrl+Y's transcript change-step arm (the diffstat toggle) is routed first and falls
227
+ // through off it, so History keeps its Summary cycle on every other view.
228
+ if (transcript.handleKey?.(key, ctx))
229
+ return true;
230
+ return history.handleKey?.(key, ctx) ?? false;
231
+ };
232
+ const routeCtrlO = (key, ctx) => {
233
+ // The Activity type cycle is routed first on its own view (0); on the Health view the
234
+ // problems-only toggle precedes the History outcome cycle; on the Pending view the
235
+ // post-merge review state filter is routed ahead of History's outcome cycle (its own arm
236
+ // consumes the key on Pending); on the Backlog view the Ready-only filter is routed
237
+ // ahead of History's outcome cycle, and every other view keeps the History outcome cycle.
238
+ if (session.view === 0)
239
+ return activity.handleKey?.(key, ctx) ?? false;
240
+ if (session.view === session.roleIds.length + 7)
241
+ return health.handleKey?.(key, ctx) ?? false;
242
+ if (session.view === session.roleIds.length + 5)
243
+ return pending.handleKey?.(key, ctx) ?? false;
244
+ if (session.view === session.roleIds.length + 1)
273
245
  return backlog.handleKey?.(key, ctx) ?? false;
274
- }
246
+ return history.handleKey?.(key, ctx) ?? false;
247
+ };
248
+ const routeCtrlX = (key, ctx) => {
249
+ // The armed-clear Ctrl+X arms: the transcript notes first, then the History model filter,
250
+ // then the queued cancel, then the Pending merge cancel, then the backlog cancel — each
251
+ // returns false when its view/selection does not apply.
252
+ if (transcript.handleKey?.(key, ctx))
253
+ return true;
254
+ if (history.handleKey?.(key, ctx))
255
+ return true;
256
+ if (queued.handleKey?.(key, ctx))
257
+ return true;
258
+ if (pending.handleKey?.(key, ctx))
259
+ return true;
260
+ return backlog.handleKey?.(key, ctx) ?? false;
261
+ };
262
+ const routeCtrlR = (key, ctx) => {
263
+ // The Ctrl+R arms: the Pending pane's queue/roster re-read, the Health pane's doctor
264
+ // re-run, the History trail's transcript cycle, then the backlog's release — each returns
265
+ // false when its view does not apply, so the order among them is immaterial.
266
+ if (pending.handleKey?.(key, ctx))
267
+ return true;
268
+ if (health.handleKey?.(key, ctx))
269
+ return true;
270
+ if (history.handleKey?.(key, ctx))
271
+ return true;
272
+ return backlog.handleKey?.(key, ctx) ?? false;
273
+ };
274
+ const routeCtrlV = (key, ctx) => {
275
+ // The Ctrl+V arms, routed like Ctrl+X's: the History trail's revert, the queued
276
+ // prompt's move-to-top, then the Pending pane's merge move-to-top, and the backlog
277
+ // entry's move-to-top last — each returns false when its view/selection does not
278
+ // apply. The Activity attach editor's own Ctrl+V arm is guarded to view 0, so it
279
+ // still fires there ahead of this router.
280
+ if (history.handleKey?.(key, ctx))
281
+ return true;
282
+ if (queued.handleKey?.(key, ctx))
283
+ return true;
284
+ if (pending.handleKey?.(key, ctx))
285
+ return true;
286
+ return backlog.handleKey?.(key, ctx) ?? false;
287
+ };
288
+ const routeArrow = (key, ctx) => {
289
+ // Entry-browse arrows: the backlog pane, then the Pending pane (merge-row selection),
290
+ // then the queued pane; every other view falls through so the editors' prompt-history
291
+ // recall keeps the arrows.
292
+ if (backlog.handleKey?.(key, ctx))
293
+ return true;
294
+ if (pending.handleKey?.(key, ctx))
295
+ return true;
296
+ if (queued.handleKey?.(key, ctx))
297
+ return true;
298
+ return false;
299
+ };
300
+ const routePage = (key, ctx) => {
301
+ // Within-body page routing in today's order: the Activity tail, the notes/change
302
+ // (transcript) and run-trail (history) bodies, the Markdown panes from report on, then
303
+ // the backlog entry body.
304
+ if (activity.handleKey?.(key, ctx))
305
+ return true;
306
+ if (transcript.handleKey?.(key, ctx))
307
+ return true;
308
+ if (history.handleKey?.(key, ctx))
309
+ return true;
310
+ if (report.handleKey?.(key, ctx))
311
+ return true;
312
+ if (backlog.handleKey?.(key, ctx))
313
+ return true;
314
+ return false;
315
+ };
316
+ const routeEscape = (key, ctx) => {
317
+ if (pending.handleKey?.(key, ctx))
318
+ return true;
319
+ return queued.handleKey?.(key, ctx) ?? false;
320
+ };
321
+ /** Run the pane arms of the key dispatch; true when one consumed the key. `es` is the
322
+ * editors' state, so the exclusion guards read the flags the editors hold. Each pane module
323
+ * checks its own view/guard and returns true only when it consumed the key; this router keeps
324
+ * the keys that multiple panes serve in today's priority order. */
325
+ const handleKey = (key, es) => {
326
+ // The editor flags below are all false here — the editors consume every key that reads or
327
+ // writes the line and return true whenever one is open — but each module restates its
328
+ // mutual-exclusion guard so it reads on its own instead of leaning on the routing order.
329
+ const ctx = { es };
330
+ if (key.ctrl && key.name === "n")
331
+ return routeCtrlN(key, ctx);
332
+ if (key.ctrl && key.name === "g")
333
+ return routeCtrlG(key, ctx);
334
+ if (key.ctrl && key.name === "e")
335
+ return routeCtrlE(key, ctx);
336
+ if (key.ctrl && key.name === "k")
337
+ return routeCtrlK(key, ctx);
338
+ if (key.ctrl && key.name === "w")
339
+ return routeCtrlW(key, ctx);
340
+ if (key.ctrl && key.name === "s")
341
+ return routeCtrlS(key, ctx);
342
+ if (key.ctrl && key.name === "p")
343
+ return routeCtrlP(key, ctx);
344
+ if (key.ctrl && key.name === "a")
345
+ return routeCtrlA(key, ctx);
346
+ if (key.ctrl && key.name === "u")
347
+ return routeCtrlU(key, ctx);
348
+ if (key.ctrl && key.name === "b")
349
+ return routeCtrlB(key, ctx);
350
+ if (key.ctrl && key.name === "l")
351
+ return routeCtrlL(key, ctx);
352
+ if (key.ctrl && key.name === "y")
353
+ return routeCtrlY(key, ctx);
354
+ if (key.ctrl && key.name === "o")
355
+ return routeCtrlO(key, ctx);
356
+ if (key.ctrl && key.name === "x")
357
+ return routeCtrlX(key, ctx);
358
+ if (key.ctrl && key.name === "r")
359
+ return routeCtrlR(key, ctx);
360
+ if (key.ctrl && key.name === "v")
361
+ return routeCtrlV(key, ctx);
275
362
  const arrow = arrowDir(key.name);
276
- if (arrow !== null) {
277
- // Entry-browse arrows: the backlog pane, then the Pending pane (merge-row selection),
278
- // then the queued pane; every other view falls through so the editors' prompt-history
279
- // recall keeps the arrows.
280
- if (backlog.handleKey?.(key, ctx))
281
- return true;
282
- if (pending.handleKey?.(key, ctx))
283
- return true;
284
- if (queued.handleKey?.(key, ctx))
285
- return true;
286
- return false;
287
- }
363
+ if (arrow !== null)
364
+ return routeArrow(key, ctx);
288
365
  const page = pageDir(key.name);
289
- if (page !== null) {
290
- // Within-body page routing in today's order: the Activity tail, the notes/change
291
- // (transcript) and run-trail (history) bodies, the Markdown panes from report on, then
292
- // the backlog entry body.
293
- if (activity.handleKey?.(key, ctx))
294
- return true;
295
- if (transcript.handleKey?.(key, ctx))
296
- return true;
297
- if (history.handleKey?.(key, ctx))
298
- return true;
299
- if (report.handleKey?.(key, ctx))
300
- return true;
301
- if (backlog.handleKey?.(key, ctx))
302
- return true;
303
- return false;
304
- }
305
- if (key.name === "escape") {
306
- if (pending.handleKey?.(key, ctx))
307
- return true;
308
- return queued.handleKey?.(key, ctx) ?? false;
309
- }
366
+ if (page !== null)
367
+ return routePage(key, ctx);
368
+ if (key.name === "escape")
369
+ return routeEscape(key, ctx);
310
370
  return false;
311
371
  };
312
372
  /** Shift, clamp, and prune the pane state against a new roster. */
@@ -396,6 +456,9 @@ export function createTuiPanes(deps) {
396
456
  refreshQueued: queued.refreshQueued,
397
457
  refreshNotes: (role) => transcript.refreshNotes?.(role),
398
458
  refreshBrief: () => brief.refreshBrief?.(),
459
+ refreshReport: () => {
460
+ report.enter?.();
461
+ },
399
462
  clearBacklogSelection: backlog.clearBacklogSelection,
400
463
  applyHistorySearch: history.applySearch,
401
464
  applyActivitySearch: activity.applySearch,
@@ -2,8 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render as inkRender } from "ink";
3
3
  import { TuiApp } from "./tui-app.js";
4
4
  import { openQuestions } from "../../backlog/backlog.js";
5
- import { operatorHeldEntries, refusedEntries, stalledEntries } from "../../backlog/backlog-eligibility.js";
6
- import { capacityContention, droppedPostReviewsAlert, flappedChecksAlert, mainRuleFailures, notifyFailures, planDependencyAlert, postMergeChurnAlert, postReviewRiskAlert, primaryOverwriteBlocks, qaCoverageAlert, quarantineAlert, regressedFixesAlert, skippedMergeFiles, staleMerges, staleOpenBugs, staleOpenQuestions, stalePrompts, staleWaitingBugs, staleWorkspacePins } from "../status-payload.js";
5
+ import { refusedEntries, stalledEntries } from "../../backlog/backlog-eligibility.js";
6
+ import { capacityContention, instancePoolProblems, planDependencyAlert, providerCapProblems, regressedFixesAlert, staleOpenQuestions, staleWorkspacePins } from "../alert-feeds.js";
7
7
  import { strandedPlanEntriesFor } from "../../backlog/backlog-structure.js";
8
8
  import { duplicatedBacklogHeadings, fixClaimProblems, liveFinishedEntriesFor, validationGapProblems } from "../../doctor/doctor-backlog-plans.js";
9
9
  import { snapshot } from "../../status/status-data.js";
@@ -93,7 +93,7 @@ export async function runTui(root, seams = {}) {
93
93
  const view = s.view;
94
94
  // What needs the operator — the dashboard's alert banners (team-alerts.ts's teamAlerts), as
95
95
  // attention lines under the header. Each consumes one line of the height budget.
96
- const attention = alertLines(teamAlerts(snap, openQuestions(root), status.roles, Date.now(), refusedEntries(root), operatorHeldEntries(root), staleOpenBugs(root), stalePrompts(root), staleMerges(root), planDependencyAlert(root), staleWaitingBugs(root), skippedMergeFiles(root), stalledEntries(root), quarantineAlert(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root), notifyFailures(root), primaryOverwriteBlocks(root), capacityContention(root), flappedChecksAlert(root), regressedFixesAlert(root, Date.now()), postMergeChurnAlert(root), postReviewRiskAlert(root), droppedPostReviewsAlert(root), mainRuleFailures(root, Date.now()), staleClaimEntries(root, status.roles.map((r) => r.role), Date.now()), staleOpenQuestions(root), fixClaimProblems(root), qaCoverageAlert(root, Date.now()), staleWorkspacePins(root, Date.now()), liveFinishedEntriesFor(root)), width);
96
+ const attention = alertLines(teamAlerts(snap, openQuestions(root), status.roles, Date.now(), refusedEntries(root), planDependencyAlert(root), stalledEntries(root), strandedPlanEntriesFor(root), duplicatedBacklogHeadings(root), validationGapProblems(root), capacityContention(root), regressedFixesAlert(root, Date.now()), staleClaimEntries(root, status.roles.map((r) => r.role), Date.now()), staleOpenQuestions(root), fixClaimProblems(root), staleWorkspacePins(root, Date.now()), liveFinishedEntriesFor(root), instancePoolProblems(root), providerCapProblems(root)), width);
97
97
  const statusLines = [status.lines[0] ?? [], ...attention, ...status.lines.slice(1)];
98
98
  // Numbered previews of prompts queued for the director, between the table and the
99
99
  // activity pane: what will run next, in execution order. Each line consumes exactly
@@ -124,7 +124,7 @@ export async function runTui(root, seams = {}) {
124
124
  // The frame as the ink tree draws it (tui-app.tsx): the same composed lines the
125
125
  // hand-painted string frame joined, as clipped StatusLines — one column row per line,
126
126
  // one styled Text per span, colors through tui-app.tsx's toneColor.
127
- const mode = { budget: s.budgetMode, concurrencyMode: s.concurrencyMode, checksMode: s.checksMode, checkMode: s.checkMode, fallbackMode: s.fallbackMode, modelMode: s.modelMode, rolePromptFor: s.rolePromptFor, attachMode: s.attachMode, pauseEditMode: s.pauseEditMode, pauseEditRole: s.pauseEditRole, pauseEditWake: s.pauseEditWake, pauseEditStop: s.pauseEditStop, queuedEditFor: s.queuedEditFor, answerFor: s.answerFor, entryEditFor: s.entryEditFor, notesEditFor: s.notesEditFor, briefEditMode: s.briefEditMode, retireMode: s.retireMode, roleEnabledMode: s.roleEnabledMode, quietMode: s.quietMode, quietTeam: s.quietTeam, providerCapsMode: s.providerCapsMode, claudeReserveMode: s.claudeReserveMode, intervalMode: s.intervalMode, roleModelMode: s.roleModelMode, instructionsEditFor: s.instructionsEditFor, slotsMode: s.slotsMode, images: s.images, notifyMode: s.notifyMode, backendsMode: s.backendsMode, instancesMode: s.instancesMode };
127
+ const mode = { budget: s.budgetMode, concurrencyMode: s.concurrencyMode, checksMode: s.checksMode, checkMode: s.checkMode, fallbackMode: s.fallbackMode, modelMode: s.modelMode, rolePromptFor: s.rolePromptFor, attachMode: s.attachMode, pauseEditMode: s.pauseEditMode, pauseEditRole: s.pauseEditRole, pauseEditWake: s.pauseEditWake, pauseEditStop: s.pauseEditStop, queuedEditFor: s.queuedEditFor, answerFor: s.answerFor, entryEditFor: s.entryEditFor, notesEditFor: s.notesEditFor, briefEditMode: s.briefEditMode, retireMode: s.retireMode, releaseMode: s.releaseMode, roleEnabledMode: s.roleEnabledMode, quietMode: s.quietMode, quietTeam: s.quietTeam, providerCapsMode: s.providerCapsMode, claudeReserveMode: s.claudeReserveMode, intervalMode: s.intervalMode, roleModelMode: s.roleModelMode, instructionsEditFor: s.instructionsEditFor, slotsMode: s.slotsMode, images: s.images, notifyMode: s.notifyMode, backendsMode: s.backendsMode, instancesMode: s.instancesMode };
128
128
  const lines = [...statusLines, []]; // a blank row below the header
129
129
  for (const [i, preview] of queued.entries())
130
130
  lines.push(toneLine(`${i + 1}. ${preview}`, width));
@@ -43,7 +43,7 @@
43
43
  * - A `path:line` location (`status-roles.ts:66`) was checked as a symbol, and no file is named
44
44
  * that. A location now claims its file, a bare line reference (`:368`) claims nothing, and a
45
45
  * bare file name is backed by a file of that name anywhere in the walked source dirs. */
46
- import { existsSync, readFileSync, statSync } from "node:fs";
46
+ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
47
47
  import { extname, join } from "node:path";
48
48
  import { readTextOrNull, statOrNull, walkFiles } from "../files/files.js";
49
49
  import { fenceTracker, finishedArchiveOf, parseEntryDetails, sectionLines, trailingParenthetical, } from "../backlog/backlog-md.js";
@@ -307,11 +307,23 @@ export async function falseFixReason(wt, mainBranch, files) {
307
307
  // is no claim at all. Both sides of the comparison read both files.
308
308
  if (!files.includes("BUGS.md") && !files.includes(BUGS_ARCHIVE))
309
309
  return undefined;
310
- // A deleted or unreadable file reads as empty: there is no Fixed transition to verify,
311
- // so this check passes and lets the gate's sibling structural check (backlog-structure.ts)
312
- // report the dropped section. Reading it raw would throw ENOENT out of the gate instead —
313
- // and a file the change deletes is exactly the input the sibling check already handles.
314
- const heads = ["BUGS.md", BUGS_ARCHIVE].map((f) => readTextOrNull(join(wt, f)) ?? "");
310
+ // A deleted, unreadable, or non-regular (symlinked) file reads as empty: there is no Fixed
311
+ // transition to verify, so this check passes and lets the gate's sibling structural check
312
+ // (backlog-structure.ts) report the dropped section. Reading a symlink raw would pull an
313
+ // outside host file's `## Fixed` heading into this gate's verdict (the same lstat policy the
314
+ // backlog-conflict resolver and backlogStructureReason pin under INV-G3), so a head is read
315
+ // only when lstat shows a regular file; ENOENT and symlinks both read as empty.
316
+ const heads = ["BUGS.md", BUGS_ARCHIVE].map((f) => {
317
+ const full = join(wt, f);
318
+ try {
319
+ if (!lstatSync(full).isFile())
320
+ return "";
321
+ }
322
+ catch {
323
+ return "";
324
+ }
325
+ return readTextOrNull(full) ?? "";
326
+ });
315
327
  if (!heads.some((head) => head.includes("## Fixed")))
316
328
  return undefined;
317
329
  // The diff's own base, not main's tip: the gate diffs `mainBranch...HEAD` against the
@@ -3,7 +3,8 @@ import path from "node:path";
3
3
  import { commitPathsAndDiscardRest, fileContentAt } from "../git/git.js";
4
4
  import { writeTextAtomic } from "../files/files.js";
5
5
  import { parseEntryDetails } from "../backlog/backlog-md.js";
6
- import { actionableEntryRanges, stripEntryStamp } from "../backlog/backlog-structure.js";
6
+ import { actionableEntryRanges } from "../backlog/backlog-index-data.js";
7
+ import { stripEntryStamp } from "../backlog/backlog-structure.js";
7
8
  import { entryHoldAgainst, refusalNote, withoutRefusalNotes } from "../backlog/backlog-eligibility.js";
8
9
  import { entryKey, plannedPartKeys } from "../backlog/backlog-parts.js";
9
10
  import { baseRoleOf } from "../roles/runner-ids.js";
@@ -87,6 +88,15 @@ export function newlyRefusedTitles(baseMd, headMd, section) {
87
88
  * logs a warning and leaves PLANS.md out of the commit, so the other notes still merge. */
88
89
  export async function handleRefusal(ctx, state, wt, pi) {
89
90
  const reason = (pi.refusedReason ?? "").trim() || "no reason given";
91
+ const plansUnstripped = await stripBlockedPlanRefusalNotes(ctx, wt);
92
+ const notes = await collectRefusalNotes(wt, plansUnstripped);
93
+ const entries = await collectRefusedEntryTitles(ctx, wt, notes);
94
+ return mergeRefusalNotes(ctx, state, wt, reason, notes, entries);
95
+ }
96
+ /** Strip this run's newly added Refused notes from any `## Planned` entry main still holds as
97
+ * blocked by a planned prerequisite. Returns whether the strip write failed: a throw leaves the
98
+ * flag set and PLANS.md out of the commit, so the other notes still merge. */
99
+ async function stripBlockedPlanRefusalNotes(ctx, wt) {
90
100
  const plansFile = path.join(wt, "PLANS.md");
91
101
  let plansUnstripped = false;
92
102
  if (fs.existsSync(plansFile)) {
@@ -117,9 +127,17 @@ export async function handleRefusal(ctx, state, wt, pi) {
117
127
  }
118
128
  }
119
129
  }
120
- const notes = (await changedFiles(wt)).filter((f) => f.toLowerCase().endsWith(".md") && !(plansUnstripped && f === "PLANS.md"));
130
+ return plansUnstripped;
131
+ }
132
+ /** The markdown notes this refusal may commit: changed `.md` files, PLANS.md dropped only when
133
+ * the blocked-plan strip write failed. */
134
+ async function collectRefusalNotes(wt, plansUnstripped) {
135
+ return (await changedFiles(wt)).filter((f) => f.toLowerCase().endsWith(".md") && !(plansUnstripped && f === "PLANS.md"));
136
+ }
137
+ /** Best-effort names for the entry_refused event, per changed file's section — it must never
138
+ * fail the refusal, so a read error swallows and leaves that file unnamed. */
139
+ async function collectRefusedEntryTitles(ctx, wt, notes) {
121
140
  const entries = [];
122
- // Best-effort naming for the entry_refused event; it must never fail the refusal.
123
141
  for (const { file, section } of [{ file: "PLANS.md", section: "Planned" }, { file: "BUGS.md", section: "Open" }]) {
124
142
  if (!notes.includes(file))
125
143
  continue;
@@ -130,10 +148,19 @@ export async function handleRefusal(ctx, state, wt, pi) {
130
148
  // Unnamed is fine: the event still carries reason and files.
131
149
  }
132
150
  }
151
+ return entries;
152
+ }
153
+ /** Merge this refusal's notes onto main: commit only the markdown notes (subject + trailer only —
154
+ * a refusal carries no WHY/RISK/VERIFIED body; the reason is the subject, and the trailer's turn
155
+ * count is the same field the friction flag reads), then merge directly. A failed note merge (a
156
+ * throw such as a lock timeout, or a non-"changed" result) is recorded in `state.lastError` and
157
+ * reported through the "refused" result like any other non-merge. A merged refusal note supersedes
158
+ * a stale rejection exactly as applyMergeOutcome's merged branch does (this path merges directly,
159
+ * bypassing that fold), then pages the operator: the entry stays held until a human or the
160
+ * director lifts it. */
161
+ async function mergeRefusalNotes(ctx, state, wt, reason, notes, entries) {
133
162
  let commit;
134
163
  if (notes.length > 0) {
135
- // Subject + trailer only — a refusal carries no WHY/RISK/VERIFIED body; the reason is
136
- // the subject, and the trailer's turn count is the same field the friction flag reads.
137
164
  const message = buildCommitMessage(stampedSubject(baseRoleOf(ctx.role), `refuse — ${reason}`), null, commitTrailer(ctx.role, state.runs, ctx.turns, state.peakContextTokens, undefined, undefined, ctx.modelLine));
138
165
  commit = (await commitPathsAndDiscardRest(wt, message, notes)) ?? undefined;
139
166
  }
@@ -155,8 +182,6 @@ export async function handleRefusal(ctx, state, wt, pi) {
155
182
  }
156
183
  if (result !== "changed")
157
184
  state.lastError = `refusal note merge failed: ${result}`;
158
- // A merged refusal note is a merged change too: it supersedes a stale rejection exactly as
159
- // applyMergeOutcome's merged branch does (this path merges directly, bypassing that fold).
160
185
  else {
161
186
  clearSupersededRejection(state);
162
187
  // Page the operator: the entry stays held until a human or the director lifts it. Best-effort —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tumwater",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Opinionated autonomous development harness built on pi. You write the initial prompt; role-driven loops build the project.",
5
5
  "type": "module",
6
6
  "license": "MIT",