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,7 +1,7 @@
1
- // vocabulary: plain
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
- import { readTextOrNull } from "../files/files.js";
3
+ import { errCode } from "../errno.js";
4
+ import { isJsonObject } from "../files/json-object.js";
5
5
  import { readJsonFile } from "../files/json-files.js";
6
6
  import { changeBaseRev, fileContentAt } from "../git/git.js";
7
7
  import { piModelsPath } from "../pi/pi-models.js";
@@ -32,9 +32,6 @@ export function secretEnvValues(env) {
32
32
  }
33
33
  return out;
34
34
  }
35
- function isRecord(v) {
36
- return typeof v === "object" && v !== null && !Array.isArray(v);
37
- }
38
35
  /** The credentials pi stores: literal `apiKey`s and header values in `models.json`, and the
39
36
  * `key`/`access`/`refresh` fields (plus secret-named `env` values) of the sibling `auth.json`.
40
37
  * `$NAME` interpolations (secretEnvValues covers those) and `!command` values are skipped. A
@@ -48,12 +45,12 @@ export function piCredentialValues(modelsPath = piModelsPath()) {
48
45
  out.push({ name, value });
49
46
  };
50
47
  const models = readJsonFile(modelsPath);
51
- if (models && isRecord(models.providers)) {
48
+ if (models && isJsonObject(models.providers)) {
52
49
  for (const [p, cfg] of Object.entries(models.providers)) {
53
- if (!isRecord(cfg))
50
+ if (!isJsonObject(cfg))
54
51
  continue;
55
52
  add(`models.json ${p}.apiKey`, cfg.apiKey);
56
- if (isRecord(cfg.headers)) {
53
+ if (isJsonObject(cfg.headers)) {
57
54
  for (const [h, v] of Object.entries(cfg.headers))
58
55
  add(`models.json ${p}.headers.${h}`, v);
59
56
  }
@@ -62,11 +59,11 @@ export function piCredentialValues(modelsPath = piModelsPath()) {
62
59
  const auth = readJsonFile(path.join(path.dirname(modelsPath), "auth.json"));
63
60
  if (auth) {
64
61
  for (const [p, cred] of Object.entries(auth)) {
65
- if (!isRecord(cred))
62
+ if (!isJsonObject(cred))
66
63
  continue;
67
64
  for (const f of ["key", "access", "refresh"])
68
65
  add(`auth.json ${p}.${f}`, cred[f]);
69
- if (isRecord(cred.env)) {
66
+ if (isJsonObject(cred.env)) {
70
67
  for (const [n, v] of Object.entries(cred.env)) {
71
68
  if (SECRET_NAME.test(n))
72
69
  add(`auth.json ${p}.env.${n}`, v);
@@ -85,12 +82,21 @@ export function knownSecrets(env = process.env, modelsPath = piModelsPath()) {
85
82
  ];
86
83
  return all.filter((s, i) => all.findIndex((o) => o.value === s.value) === i);
87
84
  }
85
+ /** Whether `err` says the path holds no file (deleted, or a parent replaced). */
86
+ function noFile(err) {
87
+ const code = errCode(err);
88
+ return code === "ENOENT" || code === "ENOTDIR";
89
+ }
90
+ /** The finding for a changed file the gate could not read. */
91
+ function unreadable(file, err) {
92
+ return `\`${file}\` could not be read to check for leaked secrets (${errCode(err) ?? "error"})`;
93
+ }
88
94
  /** The deterministic gate/stage finding for a change that writes the value of a secret
89
95
  * environment variable or stored pi credential into a file (a Node diagnostic report dumps
90
96
  * the whole environment). A value already present in the file at the merge-base is not this
91
97
  * change's leak. The reason
92
- * names files and variables, never the value. undefined when nothing leaked; never throws on
93
- * an unreadable file. */
98
+ * names files and variables, never the value. undefined when nothing leaked. A changed file
99
+ * that exists but cannot be read counts as a finding; a deleted file does not. */
94
100
  export async function secretLeakReason(wt, mainBranch, files, secrets = knownSecrets()) {
95
101
  if (secrets.length === 0)
96
102
  return undefined;
@@ -100,16 +106,30 @@ export async function secretLeakReason(wt, mainBranch, files, secrets = knownSec
100
106
  const full = path.join(wt, file);
101
107
  let stat;
102
108
  try {
103
- stat = fs.statSync(full);
109
+ // lstat, not stat: a symlink in the change set is not followed. The gate scans the
110
+ // content each change writes, and a change that adds a symlink writes no content — its
111
+ // target (which lstat's isFile() leaves unreadable, unlike stat's, which follows it out
112
+ // of the worktree) is not what the change put there. Following it would read an
113
+ // arbitrary host path a malicious repo can point a link at, and would flag that outside
114
+ // file's contents as this change's leak.
115
+ stat = fs.lstatSync(full);
104
116
  }
105
- catch {
117
+ catch (err) {
118
+ if (!noFile(err))
119
+ leaks.push(unreadable(file, err));
106
120
  continue;
107
121
  }
108
122
  if (!stat.isFile() || stat.size > MAX_SCAN_BYTES)
109
123
  continue;
110
- const head = readTextOrNull(full);
111
- if (head === null)
124
+ let head;
125
+ try {
126
+ head = fs.readFileSync(full, "utf8");
127
+ }
128
+ catch (err) {
129
+ if (!noFile(err))
130
+ leaks.push(unreadable(file, err));
112
131
  continue;
132
+ }
113
133
  const hits = secrets.filter((s) => head.includes(s.value));
114
134
  if (hits.length === 0)
115
135
  continue;
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /** Detecting a reviewer run that re-ran the full suite on a tree the gate's pre-check already
3
2
  * verified — pure string logic over the bash command lines a pi run started, no subprocess or
4
3
  * file I/O. The review prompt tells such a reviewer not to (src/prompt/gate-prompts.ts), but on
@@ -1,23 +1,56 @@
1
1
  import fs from "node:fs";
2
+ import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
2
3
  import { deleteRefIfAt, refSha } from "../git/git.js";
3
4
  import { gitTry, runGit } from "../git/git-run.js";
4
5
  import { branchName, mergeRefName, rejectedRefName, reviewSessionDir, roleInboxDir, roleNotesPath, sessionDir, statePath } from "../paths.js";
5
6
  import { errorMessage } from "../text/text.js";
6
7
  import { LEGACY_ROLE_IDS } from "./legacy-role-ids.js";
8
+ /** Rewrite a JSON state file's `role` field to the id its file name now names; a non-JSON file
9
+ * (or one without a `role` field) is left untouched. `role` is the one field that must match
10
+ * the file name — saveRunnerState writes `statePath(root, state.role)` — so a file moved by
11
+ * migration must not keep its old id inside or it would be saved back to the retired file. */
12
+ function rewriteStateRole(file, role) {
13
+ try {
14
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
15
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
16
+ return;
17
+ const rec = parsed;
18
+ if (rec.role === role)
19
+ return;
20
+ rec.role = role;
21
+ // writeJsonAtomic, not a plain writeFileSync like every other production state writer
22
+ // (INV-AF4): the rewritten file is a role's runner state, and a tmp+rename keeps a crash
23
+ // mid-write (or a concurrent reader) from ever seeing a torn file. A migration that dies
24
+ // before the rename leaves the target at its pre-rewrite bytes — retried next startup —
25
+ // never a truncated state file the runner would read as no state.
26
+ writeJsonAtomic(file, rec);
27
+ }
28
+ catch {
29
+ /* not a JSON state file — leave it alone */
30
+ }
31
+ }
7
32
  /** Move each retired role id's branch, refs and files to the new id (plans/terminology.md "Role
8
- * names"). Returns one line per artifact moved or skipped. Nothing that exists under the new id
9
- * is overwritten, each artifact fails alone, and pi and native logs stay put as history. */
33
+ * names"). Returns one line per artifact, sorted into moved, kept (both
34
+ * ids exist) and failed. State files are the one exception to "kept": when both id-named copies
35
+ * exist and the old-id copy carries the newer end-of-run stamp (`lastRunEndedAt`, or the
36
+ * pre-rename `lastTickEndedAt` a frozen copy carries), that copy is folded over the stale
37
+ * new-name one under the new name (the earlier migration renamed only the file list, not the
38
+ * `role` field inside, so the runner kept writing the old-id name and the two copies drifted —
39
+ * BUGS.md 2026-10-23); otherwise both are left in place. Nothing other than a state file that
40
+ * exists under the new id is overwritten, each artifact fails alone, and pi and native logs stay
41
+ * put as history. */
10
42
  export async function migrateLegacyRoleIds(root, legacy = LEGACY_ROLE_IDS) {
11
- const notes = [];
43
+ const notes = { moved: [], kept: [], failed: [] };
12
44
  for (const [oldId, newId] of Object.entries(legacy)) {
13
45
  const attempt = async (what, move) => {
14
46
  try {
15
47
  const note = await move();
16
- if (note !== null)
17
- notes.push(note);
48
+ if (note === null)
49
+ return;
50
+ (note.startsWith("kept ") ? notes.kept : notes.moved).push(note);
18
51
  }
19
52
  catch (err) {
20
- notes.push(`could not move ${what}: ${errorMessage(err)}`);
53
+ notes.failed.push(`could not move ${what}: ${errorMessage(err)}`);
21
54
  }
22
55
  };
23
56
  await attempt("branch", async () => {
@@ -45,7 +78,47 @@ export async function migrateLegacyRoleIds(root, legacy = LEGACY_ROLE_IDS) {
45
78
  return `moved ${from} to ${to}`;
46
79
  });
47
80
  }
48
- for (const pathFor of [statePath, roleNotesPath, roleInboxDir, sessionDir, reviewSessionDir]) {
81
+ // State files get a one-time heal, not "kept": when both id-named copies exist (a state
82
+ // object the earlier migration renamed without rewriting `role`, so the runner kept saving
83
+ // under the old-id name and the two files drifted), fold them into one — when the old-id copy
84
+ // is the newer one, keep it under the new name and drop the stale new-name copy. A live copy
85
+ // carries `lastRunEndedAt`; a frozen copy (never saved since the earlier rename) carries only
86
+ // the pre-rename `lastTickEndedAt`, so the stamp reads either. When either copy is not a state
87
+ // object (unreadable/torn, or a non-object), their stamps tie, or the new-name copy is the
88
+ // newer one, keep both untouched, so the heal never fabricates or destroys a file it does not
89
+ // understand.
90
+ await attempt(`state ${statePath(root, oldId)}`, async () => {
91
+ const from = statePath(root, oldId);
92
+ const to = statePath(root, newId);
93
+ if (!fs.existsSync(from))
94
+ return null;
95
+ if (!fs.existsSync(to)) {
96
+ fs.renameSync(from, to);
97
+ rewriteStateRole(to, newId);
98
+ return `moved ${from} to ${to}`;
99
+ }
100
+ const stamp = (p) => {
101
+ const j = readJsonFile(p);
102
+ if (typeof j?.lastRunEndedAt === "number")
103
+ return j.lastRunEndedAt;
104
+ return typeof j?.lastTickEndedAt === "number" ? j.lastTickEndedAt : -2; // retired-name-ok: legacy state field the frozen copy carries
105
+ };
106
+ const oldStamp = stamp(from);
107
+ const newStamp = stamp(to);
108
+ if (oldStamp < 0 || newStamp < 0 || oldStamp === newStamp)
109
+ return `kept ${from} and ${to}: both exist`;
110
+ if (oldStamp < newStamp)
111
+ return `kept ${from} and ${to}: both exist`;
112
+ // The old-id file holds the newer state (the live runner kept saving under the old name
113
+ // after the earlier migration). Fold it into the new-name path atomically: renameSync
114
+ // replaces the destination and removes the source in one step, so a crash between the two
115
+ // cannot leave the new-name file torn over the still-present old copy (which would read as
116
+ // -2 next time and wedge the heal in "kept").
117
+ fs.renameSync(from, to);
118
+ rewriteStateRole(to, newId);
119
+ return `healed ${from} and ${to}: kept ${from}, newer`;
120
+ });
121
+ for (const pathFor of [roleNotesPath, roleInboxDir, sessionDir, reviewSessionDir]) {
49
122
  const from = pathFor(root, oldId);
50
123
  const to = pathFor(root, newId);
51
124
  await attempt(from, async () => {
@@ -1,11 +1,10 @@
1
- // vocabulary: plain
2
- /** The role catalog: one built-in loop's identity and its role-specific "find something to do"
1
+ /** The role catalog: one built-in role's identity and its role-specific "find something to do"
3
2
  * instruction text. Split out of roles.ts (2026-10-01, organize) so the catalog's embedded
4
3
  * prompt prose lives apart from the derived registries and classification helpers, which stayed
5
4
  * in roles.ts. Pure catalog data — the shared instruction fragments role prompts embed live in
6
5
  * role-guidance.ts (import-only downward, so prompt assembly depends on both without a cycle).
7
6
  * roles.ts re-exports ROLES and Role, so every existing importer keeps its import path. */
8
- import { backlogMoveGuidance, DECOMPOSITION_GUIDANCE, NEEDS_REPLAN_NOTE, NEEDS_REVIEW_NOTE, PLAN_SIZING, searchGuidance, VALIDATION_GAP_GUIDANCE, VALIDATION_GAP_QUERY, VALIDATION_GAP_TALLY, WAITING_FOR_FAILURE_NOTE, WAITING_FOR_FAILURE_PREFIX, } from "./role-guidance.js";
7
+ import { backlogMoveGuidance, DECOMPOSITION_GUIDANCE, HELD_TAG_PREFIX, NEEDS_REPLAN_NOTE, NEEDS_REVIEW_NOTE, PLAN_QUEUE_GUIDANCE, PLAN_SIZING, PRIOR_HELD_PREFIX, searchGuidance, VALIDATION_GAP_GUIDANCE, VALIDATION_GAP_QUERY, VALIDATION_GAP_TALLY, WAITING_FOR_FAILURE_NOTE, WAITING_FOR_FAILURE_PREFIX, } from "./role-guidance.js";
9
8
  import { roleCommitGrep } from "./legacy-role-ids.js";
10
9
  import { NOTHING_TO_DO } from "../verdict/reply-contract.js";
11
10
  /** The trailer the readme role's commits carry and its `git log --grep` reads back; git parses
@@ -14,10 +13,10 @@ import { NOTHING_TO_DO } from "../verdict/reply-contract.js";
14
13
  const README_TRAILER = "Run: readme #";
15
14
  const README_TRAILER_GREP = "^(Run|Tick): readme #"; // vocabulary-ok: legacy name read from old refs or commits
16
15
  const README_TRAILER_LEGACY = "Tick: readme #"; // vocabulary-ok: legacy name read from old refs or commits
17
- /** The opinionated role catalog. Every loop runs one role; a role's `find` text is
18
- * the role-specific "find something to do" half of the tick prompt.
16
+ /** The opinionated role catalog. Every role has one job; a role's `find` text is
17
+ * the role-specific "find something to do" half of the run prompt.
19
18
  *
20
- * Order matters: it is the scheduling priority when loops are otherwise tied
19
+ * Order matters: it is the scheduling priority when roles are otherwise tied
21
20
  * (e.g. the startup burst), so shipping work (feature, bugfix) outranks hygiene. */
22
21
  export const ROLES = [
23
22
  {
@@ -37,12 +36,18 @@ export const ROLES = [
37
36
  4. A plan too large to finish in this run is not split by you: append the note
38
37
  ${NEEDS_REVIEW_NOTE}
39
38
  under its heading, skip it, and implement the next available plan that fits. You may mark
40
- several oversized entries while scanning, but land exactly one plan.
39
+ several oversized entries while scanning, but land exactly one plan. The held tag you write
40
+ — \`${HELD_TAG_PREFIX}<N>)\` on a Needs-review note, \`${HELD_TAG_PREFIX}<H>)\` on a
41
+ Needs-replan note — is ONE past the count already on the entry: read your own or an earlier
42
+ mark's \`${HELD_TAG_PREFIX}N)\` tag, else the \`${PRIOR_HELD_PREFIX}<N>**\` line a lift left
43
+ behind, else start at 1 — never reset it while the same plan ping-pongs, so three holds
44
+ without a merge reach \`${HELD_TAG_PREFIX}3)\`. A plan that merged and left \`## Planned\`
45
+ carries no marker — the convergence backstop starts fresh there.
41
46
  5. Implement it completely: code, tests, and any docs. The reviewer checks your diff against
42
47
  the entry's files-touched list and acceptance criteria, so land everything the entry
43
48
  promises, or update the entry to say what changed and why. When a plan's anchors no longer
44
49
  match the code, correct the entry in the same change instead of refusing.
45
- 6. Mark the plan done in PLANS.md — ${backlogMoveGuidance("PLANS.md", "## Planned", "## Done", "PLANS-DONE.md")}
50
+ 6. Mark the plan done in PLANS.md — ${backlogMoveGuidance("PLANS.md", "## Planned", "## Done", "PLANS-DONE.md", "done")}
46
51
  7. Verify last, after the PLANS.md edit, so one run of the project's check covers everything
47
52
  you changed (see "Leave the project working" below).
48
53
  A plan that resists implementation is a finding: refuse it with the objection recorded rather
@@ -63,7 +68,7 @@ than forcing it.`,
63
68
  heading, and end the run. Do not keep investigating without new evidence.
64
69
  3. Fix it, and add a regression test that fails without the fix — run that test to watch it
65
70
  fail before the fix and pass after.
66
- 4. Mark it fixed in BUGS.md — ${backlogMoveGuidance("BUGS.md", "## Open", "## Fixed", "BUGS-FIXED.md")}
71
+ 4. Mark it fixed in BUGS.md — ${backlogMoveGuidance("BUGS.md", "## Open", "## Fixed", "BUGS-FIXED.md", "fixed")}
67
72
  5. In the moved entry, write the required validation-gap trace line (format below).
68
73
  6. Verify last, after the BUGS.md edits, so one run of the project's check covers everything
69
74
  you changed (see "Leave the project working" below).
@@ -95,6 +100,13 @@ within that budget, there is nothing to do.`,
95
100
  following it would answer them — correcting the approach or the anchors, tightening the
96
101
  acceptance criteria, splitting it per PLAN_SIZING, or refusing it with the objection
97
102
  recorded when it should not be done — and remove both the note and the quoted objections.
103
+ - When you lift a Needs-review or Needs-replan note (a split or a rewrite) and the change
104
+ has NOT merged, keep the held count instead of letting it reset: remove any existing
105
+ \`**Previously held N**\` line on the surviving parent and every child part the split
106
+ produces, then write \`${PRIOR_HELD_PREFIX}<N>**\` at the lifted note's count on the
107
+ parent and copy it to each child, so exactly one marker survives and heldCount reads the
108
+ lifted count everywhere — a merged parent (one whose plan left \`## Planned\`) leaves no
109
+ marker.
98
110
  - Otherwise, when PLANS.md \`## Planned\` already holds one more eligible plan than there
99
111
  are feature instances (one waiting per instance, plus one for the next planner run),
100
112
  end with ${NOTHING_TO_DO} — feature has work, and a waiting plan is refined by the
@@ -113,6 +125,7 @@ within that budget, there is nothing to do.`,
113
125
  at them in ranges.
114
126
  5. Write it as a short markdown section under \`## Planned\` in PLANS.md: goal, approach, files
115
127
  touched, acceptance criteria.
128
+ ${PLAN_QUEUE_GUIDANCE}
116
129
  ${PLAN_SIZING}
117
130
  ${DECOMPOSITION_GUIDANCE}`,
118
131
  },
@@ -154,7 +167,7 @@ edit the brief between the tumwater:prompt markers.`,
154
167
  title: "code organizer",
155
168
  // Written against the 2026-10-05/06 churn: with minor fixes exhausted and the shared
156
169
  // small-and-self-contained scope rule in force, organize reorganized the whole tree one
157
- // directory per tick (45 of 86 merges in four hours) with no target structure ever
170
+ // directory per run (45 of 86 merges in four hours) with no target structure ever
158
171
  // stated. Major refactors stay welcome; they must be considered whole and merge whole.
159
172
  find: `Find ONE way the code could be better organized, and restructure it.
160
173
  1. First rank the production source files (not the tests) by \`wc -l\`. A production module
@@ -163,7 +176,8 @@ edit the brief between the tumwater:prompt markers.`,
163
176
  file of similar size: a test file's shape follows the module it covers. An outlier that is
164
177
  mostly one large function or closure cannot be split in one run: do not start the split,
165
178
  write it as a plan in PLANS.md under ## Planned that states the target structure (which
166
- state and handlers move into which module). Then look for other minor improvements to
179
+ state and handlers move into which module).
180
+ ${PLAN_QUEUE_GUIDANCE} Then look for other minor improvements to
167
181
  the existing structure: a module in the wrong directory, a missing separation between
168
182
  layers, or inconsistent file naming.
169
183
  2. When the minor improvements are exhausted, a major refactor — new directories, a
@@ -233,7 +247,8 @@ scratch analysis scripts — that is a different, much larger task.`,
233
247
  this run's ONE task: move each listed entry to the section its heading's dates say it belongs in
234
248
  (cut and paste, under the existing \`## \` heading — never add, remove, or rename a \`## \`
235
249
  heading), leave its text verbatim, and change nothing else. An entry listed under PLANS-DONE.md's
236
- \`## Done\` with no done date goes back under PLANS.md's \`## Planned\`.
250
+ \`## Done\` with no done date is already finished and archived there: keep it in place and only add the
251
+ missing \`done YYYY-MM-DD\` to its \`### \` heading — never move it back under PLANS.md's \`## Planned\`.
237
252
 
238
253
  Otherwise, pick ONE kind of uncleanliness — dead code, misleading names, commented-out blocks,
239
254
  overly clever constructs, missing or wrong doc comments on public surfaces, inconsistent
@@ -330,6 +345,9 @@ the last day (per-role outcome counts, each role's time and spend by outcome, no
330
345
  warning clusters, review rejections, and what merged). It is your entire evidence base — do not go
331
346
  looking for the event log or per-role transcripts yourself; tumwater injects the digest because
332
347
  the live log lives outside your worktree.
348
+ The digest ends with a ## Fast merge guardrails section when the post-merge machinery is failing
349
+ quietly (main red over 30 minutes, quarantine aged or oversized). Each line there is tumwater's
350
+ own response failing, so it is fileable as it stands under rule 1.
333
351
  1. A cluster is a bug ONLY when tumwater's RESPONSE to it is wrong — a failure that raised no
334
352
  alarm, drove the wrong state or sleep ladder, latched a stale verdict, or hid itself from
335
353
  the user. A mere infrastructure failure (the model server was slow, pi exited non-zero)
@@ -349,6 +367,44 @@ the live log lives outside your worktree.
349
367
  entry, so the bugfix role can reproduce it.
350
368
  BUGS.md is your only write; never edit source, tests, or docs. If every cluster is ordinary
351
369
  infrastructure weather or already filed, there is nothing to do.`,
370
+ },
371
+ {
372
+ id: "feedback",
373
+ tier: "default",
374
+ title: "feedback: files tumwater bugs upstream on GitHub",
375
+ find: `File ONE bug per run as a GitHub issue on github.com/znation/tumwater, or declare
376
+ nothing-to-do, from the <failure-digest> block in your prompt: a deterministic digest of this
377
+ project's own event log over the last day (per-role outcome counts, each role's time and spend by
378
+ outcome, normalized error and warning clusters, review rejections, and what merged). It is your
379
+ entire evidence base — do not go looking for the event log or per-role transcripts yourself; tumwater
380
+ injects the digest because the live log lives outside your worktree.
381
+ The digest ends with a ## Fast merge guardrails section when the post-merge machinery is failing
382
+ quietly (main red over 30 minutes, quarantine aged or oversized). Each line there is tumwater's
383
+ own response failing, so it is fileable as it stands under rule 1.
384
+ 1. A cluster is a bug ONLY when tumwater's RESPONSE to it is wrong — a failure that raised no
385
+ alarm, drove the wrong state or sleep ladder, latched a stale verdict, or hid itself from
386
+ the user. A mere infrastructure failure (the model server was slow, pi exited non-zero)
387
+ is weather, not a bug: filing it produces an issue no maintainer can act on.
388
+ 2. Read the digest for that wrong response — 44 identical errors that raised no warning, a run
389
+ that failed in 200 ms then climbed the idle ladder, a cluster that begins on the date a
390
+ specific commit merged.
391
+ 3. Rank what you would file by LOSS, not by run count: the digest's time-and-spend table and
392
+ top loss causes price each cause in agent-hours and dollars, and a cluster that burned hours
393
+ outranks one that burned seconds — cite its cost when it is the reason you file.
394
+ 4. Before filing, dedupe: check the "Previously filed from this install" block in your prompt
395
+ and run \`gh issue list --repo znation/tumwater --search <cluster terms>\` — never file a
396
+ duplicate. When the same cluster is already filed, there is nothing to do.
397
+ 5. File it with \`gh issue create --repo znation/tumwater --title … --body …\` from the shell
398
+ and verify the issue URL in the command's output before you report it. The body cites the
399
+ cluster's normalized key, its count and roles, the correlated \`merged\` commit, and the
400
+ output of \`tumwater version\`; redact absolute paths and secrets — file only what the digest
401
+ itself already shows.
402
+ You never edit files in this repo — \`gh\` is your only write. End every run with the
403
+ ${NOTHING_TO_DO} sentinel; when you filed, one \`FILED: #<n> — <title>\` line rides just above it
404
+ (named as qa's FLOW line is). A run that cannot file because \`gh\` is missing, unauthenticated,
405
+ or failed ends WITHOUT the sentinel, naming the failure in your final prose so tumwater's
406
+ finished-without-changes warning surfaces it. If every cluster is ordinary infrastructure weather
407
+ or already filed, there is nothing to do.`,
352
408
  },
353
409
  {
354
410
  id: "security",
@@ -461,6 +517,7 @@ arguments, config files, environment), and rough edges you meet while running th
461
517
  - promote a recurring non-\`none\` \`gap:\` tag — three or more Fixed entries carrying it —
462
518
  into a PLANS.md entry for the infrastructure that would retire it, citing those entries.
463
519
  ${VALIDATION_GAP_TALLY}
520
+ ${PLAN_QUEUE_GUIDANCE}
464
521
  You edit only markdown — never source.
465
522
 
466
523
  Reading the backlog files: PLANS.md and BUGS.md hold open work, so read them whole when your move
@@ -1,11 +1,10 @@
1
- // vocabulary: plain
2
1
  /** Shared instruction fragments role prompts and prompt assembly embed verbatim. Pure
3
2
  * data: this module imports only the dependency-free legacy-role-ids, so prompt assembly
4
3
  * can depend on it without a cycle.
5
4
  * Split from roles.ts, whose catalog (now role-catalog.ts) find texts embed these fragments
6
5
  * while prompt.ts embeds them too — the wording is one concern, the catalog another. */
7
6
  import { roleCommitGrep } from "./legacy-role-ids.js";
8
- /** Shared guidance for any loop about to record a plan or bug: split independent parts
7
+ /** Shared guidance for any role about to record a plan or bug: split independent parts
9
8
  * into their own entries. Defined once so the director and role prompts cannot drift.
10
9
  * Lives here (not in prompt.ts) because two role `find` texts embed it — keeping this
11
10
  * module import-free, with the dependency running one way: prompt assembly → roles,
@@ -14,8 +13,8 @@ export const DECOMPOSITION_GUIDANCE = `Before recording a plan or bug, check whe
14
13
  into independent subparts (separate features, or separate bugs). If it does, record each part as
15
14
  its own PLANS.md/BUGS.md entry that cross-references its siblings, so roles can pick them up
16
15
  independently; if the parts are not truly independent, keep a single entry.`;
17
- /** Shared instruction for closing a backlog entry (the feature loop moving a plan to Done,
18
- * the bugfix loop moving a bug to Fixed). Written against the observed failure (PLANS.md
16
+ /** Shared instruction for closing a backlog entry (the feature role moving a plan to Done,
17
+ * the bugfix role moving a bug to Fixed). Written against the observed failure (PLANS.md
19
18
  * 2026-09-30, part 1/3): "move it to a Done section" reads as "create a section", and five
20
19
  * commits left PLANS.md with two `## Done` headings — so the wording names the file's EXISTING
21
20
  * resolved heading, forbids touching `## ` structure, and gives the verifiable invariant
@@ -23,11 +22,16 @@ independently; if the parts are not truly independent, keep a single entry.`;
23
22
  * numbered step of their task. The pasted entry does not stay there: tumwater moves it to the top
24
23
  * of `archive` when the change commits (plans/backlog-archive.md), which the last line says so no
25
24
  * run opens the archive to do it by hand. Takes the file, section and archive names so the feature
26
- * and bugfix texts cannot drift apart; lives here with the other shared fragments. */
27
- export function backlogMoveGuidance(file, openSection, resolvedSection, archive) {
25
+ * and bugfix texts cannot drift apart; lives here with the other shared fragments. Also takes
26
+ * the closing token ("done" for a finished plan, "fixed" for a BUGS.md record) chosen by the
27
+ * caller, so the verb and the heading-key rule are named instead of left for the role to guess. */
28
+ export function backlogMoveGuidance(file, openSection, resolvedSection, archive, token) {
28
29
  return `move the entry, step by step:
29
30
  a. cut the entry out of \`${openSection}\`;
30
- b. add the closing date to its \`### \` heading;
31
+ b. add the closing stamp to its \`### \` heading, at the end — \`— ${token} YYYY-MM-DD by the <role> role\`
32
+ (a finished plan appends it after its existing \`(planned …)\` parenthetical; the heading
33
+ text before its first \` (\` must not change, or the gate reads it as a brand-new entry
34
+ under \`${resolvedSection}\` rather than a move);
31
35
  c. paste it as the first entry under the file's existing \`${resolvedSection}\` heading;
32
36
  d. if it was the last entry under \`${openSection}\`, leave a \`_None yet._\` placeholder
33
37
  there.
@@ -39,7 +43,7 @@ export function backlogMoveGuidance(file, openSection, resolvedSection, archive)
39
43
  * a comment inside the template would become prompt text. */
40
44
  const REQUIRES_PART_FORM = "requires part N/M landed"; // vocabulary-ok: requiredParts parses this heading syntax
41
45
  /** Shared sizing rule for anyone writing a plan (the plan role and the director): the feature
42
- * loop is one mid-sized local model working alone in one run, and the review gate rejects a
46
+ * role is one mid-sized local model working alone in one run, and the review gate rejects a
43
47
  * change that lands less than its entry promises — so a plan must fit that run. Oversized plans
44
48
  * were the pattern behind "half-done against its own plan" rejections. The last sentence is the
45
49
  * self-hosting build lag (BUGS.md 2026-09-23): plan 4b bundled "the merge preserves the live
@@ -58,6 +62,17 @@ ranges, because line anchors go stale with every merge. When tumwater is buildin
58
62
  every commit merges under the build that predates it, so a
59
63
  change to how merges behave and any step that depends on it are separate sub-plans, the second
60
64
  merge only once the first is the running build.`;
65
+ /** Shared guidance for anyone writing a plan (the plan role, the organize role, and the
66
+ * director): a new plan joins the BACK of the `## Planned` queue, never the top. Part of the
67
+ * define-once pattern of DECOMPOSITION_GUIDANCE/PLAN_SIZING — file order under `## Planned` IS
68
+ * the work queue: claim assignment takes the first free entry in file order (`assignNext`,
69
+ * src/scheduling/claims.ts), so an entry set at the top jumps ahead of work already waiting.
70
+ * Lives here because three role `find` texts and the director's routing bullet embed it. */
71
+ export const PLAN_QUEUE_GUIDANCE = `A new plan joins the queue at the BACK: append the entry at the
72
+ END of \`## Planned\`, after every existing entry, never at the top. File order under
73
+ \`## Planned\` is the work queue — claim assignment takes the first free entry in that order
74
+ (\`assignNext\`, src/scheduling/claims.ts) — so an entry inserted at the top jumps ahead of work
75
+ already waiting.`;
61
76
  /** The closed vocabulary for the validation-gap trace (plans/repair-traces.md): what made a bug
62
77
  * hard to *confirm*, which is the only evidence of where this project's test infrastructure is
63
78
  * weakest. Closed on purpose so the traces aggregate, and `none` is a written value rather than an
@@ -109,36 +124,56 @@ export const VALIDATION_GAP_QUERY = `grep -snE '\\*\\*Validation gap:\\*\\* (${V
109
124
  * parsing the date or the wording out of the full template — the same split as
110
125
  * NEEDS_REPLAN_PREFIX. */
111
126
  export const NEEDS_REVIEW_PREFIX = `**Needs review `;
112
- /** The markdown note the feature loop appends to a plan it cannot finish in one run, so the plan
113
- * loop can split it. Mirrors the **Refused …** note convention: a note the next fresh tick reads,
127
+ /** The tag text before the held count, shared by the note templates and the parser
128
+ * (backlog-eligibility's heldCount) so the two cannot drift: the live count on a Needs-review
129
+ * or Needs-replan note is written inline as `(held N)`, with `N` one past the surviving prior
130
+ * count (the feature role appends it, the plan role's split carries it via PRIOR_HELD_NOTE). */
131
+ export const HELD_TAG_PREFIX = `(held `;
132
+ /** The markdown note the feature role appends to a plan it cannot finish in one run, so the plan
133
+ * role can split it. Mirrors the **Refused …** note convention: a note the next fresh run reads,
114
134
  * with no code parsing it. Defined once so the feature and plan texts and the director's routing
115
- * clause cannot drift. */
116
- export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run**`;
135
+ * clause cannot drift. The trailing `(held <N>)` tag (HELD_TAG_PREFIX) is the live held count,
136
+ * written as one past the count surviving on the entry; it sits inside the line, so
137
+ * NEEDS_REVIEW_PREFIX still recognizes the note. */
138
+ export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run ${HELD_TAG_PREFIX}<N>)**`;
139
+ /** The marker a lifted hold leaves behind, so a split or rewrite carries the count instead of
140
+ * letting it reset: `**Previously held N**` on the note's line. The live count is `(held N)` on
141
+ * the note; the surviving marker after a lift is `**Previously held N**` — heldCount reads the
142
+ * first as the priority, then the second. */
143
+ export const PRIOR_HELD_PREFIX = `**Previously held `;
144
+ /** The literal marker a lift leaves on a split or rewrite so the held count survives instead of
145
+ * resetting: a `**Previously held <N>**` line at the same count the lifted note carried (the
146
+ * <N> here is the value the note's `(held N)` tag held). heldCount reads this as the fallback
147
+ * when no live note is present, so the count round-trips a split. */
148
+ export const PRIOR_HELD_NOTE = `${PRIOR_HELD_PREFIX}<N>**`;
117
149
  /** The literal prefix every Needs-replan note starts with. Exported so a reader that only has to
118
150
  * recognize one (the backlog index's ` [blocked: needs replan]` mark) can match on it instead of
119
151
  * parsing the date or the round count out of the full template. */
120
152
  export const NEEDS_REPLAN_PREFIX = `**Needs replan `;
121
- /** The markdown note the feature loop appends under a plan whose change was rejected after its
122
- * final revision round, carrying the reviewer's objections back to the strong-tier plan loop
153
+ /** The markdown note the feature role appends under a plan whose change was rejected after its
154
+ * final revision round, carrying the reviewer's objections back to the strong-tier plan role
123
155
  * instead of letting feature re-author the same plan. Same convention as NEEDS_REVIEW_NOTE: a
124
- * note a later fresh tick reads, with no code parsing it. */
125
- export const NEEDS_REPLAN_NOTE = `${NEEDS_REPLAN_PREFIX}<YYYY-MM-DD> by feature: rejected after <N> review rounds**`;
156
+ * note a later fresh run reads, with no code parsing it. The trailing `(held <H>)` tag
157
+ * (HELD_TAG_PREFIX) is the live held count, one past the count surviving on the entry; the
158
+ * placeholder is `<H>` (for held) so it reads apart from the `<N>` review-round count earlier
159
+ * in the same line. It sits inside the line, so NEEDS_REPLAN_PREFIX still recognizes the note. */
160
+ export const NEEDS_REPLAN_NOTE = `${NEEDS_REPLAN_PREFIX}<YYYY-MM-DD> by feature: rejected after <N> review rounds ${HELD_TAG_PREFIX}<H>)**`;
126
161
  /** The literal prefix every Waiting-for-the-next-failure note starts with. Exported so a reader
127
162
  * that only has to recognize one (the backlog index's ` [blocked: waiting for the next failure]`
128
163
  * mark) can match on it instead of parsing the date or the wording out of the full template. */
129
164
  export const WAITING_FOR_FAILURE_PREFIX = `**Waiting for the next failure `;
130
- /** The markdown note the bugfix loop writes under an open bug it cannot reproduce once it has made
165
+ /** The markdown note the bugfix role writes under an open bug it cannot reproduce once it has made
131
166
  * the failure explain itself, so bugfix stops spending runs without new evidence. Same convention
132
- * as NEEDS_REPLAN_NOTE: a note a later fresh tick reads. It lifts when whoever records the next
167
+ * as NEEDS_REPLAN_NOTE: a note a later fresh run reads. It lifts when whoever records the next
133
168
  * occurrence deletes the line, or by `tumwater bug --release <n>`. */
134
169
  export const WAITING_FOR_FAILURE_NOTE = `${WAITING_FOR_FAILURE_PREFIX}<YYYY-MM-DD> by bugfix: <what the next failure's message will show>**`;
135
170
  /** How a role with no backlog (organize, clean, dedupe, perf, security, robustness, improve) finds
136
171
  * its one task, as numbered steps. Written against the observed failure: with nothing to point
137
172
  * at, a local model reads the codebase file by file — thirty whole-file reads, ~300 KB of tool
138
- * output — and then either fills its window or declares nothing-to-do (improve: 41 of 44 ticks in
173
+ * output — and then either fills its window or declares nothing-to-do (improve: 41 of 44 runs in
139
174
  * the first week of September landed nothing). Cheap signals shortlist candidates; a decision
140
175
  * deadline ends the search; the harness's own commit subjects (`tumwater(<role>): …`) are the
141
- * only cross-tick memory of what the role did lately, so the text names them. Takes the role id
176
+ * only cross-run memory of what the role did lately, so the text names them. Takes the role id
142
177
  * so the git command is literal, not a placeholder the model has to fill in. */
143
178
  export function searchGuidance(roleId) {
144
179
  return `How to search — do not read the codebase file by file: a whole-tree survey costs most of
@@ -1,7 +1,6 @@
1
- // vocabulary: plain
2
1
  /** How a role's id shows up in words: an instance id as `feature #2`. Pure, no I/O, so core
3
2
  * modules and the UI may both import it. */
4
- import { baseRoleOf, instanceIndex } from "./loop-ids.js";
3
+ import { baseRoleOf, instanceIndex } from "./runner-ids.js";
5
4
  /** An instance id as `feature #2`; any other id (a bare role, `director`, a custom role,
6
5
  * `feature-1`) unchanged. */
7
6
  export function roleLabel(runnerId) {