tumwater 0.1.3 → 0.2.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 (359) hide show
  1. package/README.md +63 -28
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-eligibility.js +332 -43
  26. package/dist/src/backlog/backlog-md.js +79 -17
  27. package/dist/src/backlog/backlog-render.js +62 -11
  28. package/dist/src/backlog/backlog-structure.js +261 -58
  29. package/dist/src/backlog/backlog-write.js +178 -441
  30. package/dist/src/backlog/backlog.js +113 -16
  31. package/dist/src/backlog/question-answer.js +339 -0
  32. package/dist/src/baseline/main-baseline.js +75 -88
  33. package/dist/src/baseline/main-red.js +32 -30
  34. package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
  35. package/dist/src/{brief.js → brief/brief.js} +33 -8
  36. package/dist/src/budget/budget.js +32 -15
  37. package/dist/src/budget/fallback-breaker.js +8 -7
  38. package/dist/src/build/build-check-detect.js +8 -7
  39. package/dist/src/build/build-check-events.js +68 -22
  40. package/dist/src/build/build-check-report.js +22 -23
  41. package/dist/src/build/build-check-scoped.js +51 -62
  42. package/dist/src/build/build-check.js +15 -14
  43. package/dist/src/build/build-info.js +2 -1
  44. package/dist/src/build/build-stage.js +23 -22
  45. package/dist/src/build/dep-install.js +22 -21
  46. package/dist/src/build/host-sleep.js +1 -1
  47. package/dist/src/change/change-data.js +13 -11
  48. package/dist/src/change/change-render.js +11 -10
  49. package/dist/src/cli/brief-command.js +9 -7
  50. package/dist/src/cli/cli-args.js +25 -5
  51. package/dist/src/cli/cli-command-args.js +43 -10
  52. package/dist/src/cli/cli-editor.js +13 -1
  53. package/dist/src/cli/cli-flag-specs.js +31 -6
  54. package/dist/src/cli/cli-marker-commands.js +13 -10
  55. package/dist/src/cli/cli-query-commands.js +439 -56
  56. package/dist/src/cli/cli-repo-gate.js +1 -1
  57. package/dist/src/cli/cli-run.js +124 -68
  58. package/dist/src/cli/completion.js +70 -12
  59. package/dist/src/cli/config-commands.js +14 -11
  60. package/dist/src/cli/help.js +264 -124
  61. package/dist/src/cli/log-commands.js +127 -21
  62. package/dist/src/cli/notify-command.js +33 -18
  63. package/dist/src/cli/question-commands.js +13 -224
  64. package/dist/src/cli.js +78 -36
  65. package/dist/src/collections.js +66 -16
  66. package/dist/src/concurrency/check-permit.js +16 -17
  67. package/dist/src/concurrency/lock.js +6 -5
  68. package/dist/src/concurrency/provider-lease.js +69 -0
  69. package/dist/src/concurrency/semaphore.js +258 -36
  70. package/dist/src/config/config-aliases.js +106 -0
  71. package/dist/src/config/config-editable-keys.js +3 -3
  72. package/dist/src/config/config-fallbacks.js +379 -0
  73. package/dist/src/config/config-field-checks.js +19 -10
  74. package/dist/src/config/config-keys.js +160 -0
  75. package/dist/src/config/config-live.js +64 -12
  76. package/dist/src/config/config-schema.js +34 -22
  77. package/dist/src/config/config-validation.js +132 -48
  78. package/dist/src/config/config-views.js +107 -325
  79. package/dist/src/config/config-write.js +138 -177
  80. package/dist/src/config/config.js +60 -49
  81. package/dist/src/config/model-selector.js +28 -2
  82. package/dist/src/doctor/doctor-backlog.js +494 -68
  83. package/dist/src/doctor/doctor-checks.js +509 -58
  84. package/dist/src/doctor/doctor-holds.js +139 -78
  85. package/dist/src/doctor/doctor-install.js +30 -0
  86. package/dist/src/doctor/doctor-launch-services.js +2 -2
  87. package/dist/src/doctor/doctor-model-checks.js +549 -157
  88. package/dist/src/doctor/doctor-notify.js +2 -3
  89. package/dist/src/doctor/doctor-orphans.js +10 -9
  90. package/dist/src/doctor/doctor-render.js +36 -9
  91. package/dist/src/doctor/doctor.js +76 -25
  92. package/dist/src/doctor/image-probe.js +244 -0
  93. package/dist/src/doctor/probe.js +64 -0
  94. package/dist/src/events/event-format.js +219 -106
  95. package/dist/src/events/event-labels.js +116 -0
  96. package/dist/src/events/event-legacy.js +140 -0
  97. package/dist/src/events/event-read.js +58 -22
  98. package/dist/src/events/event-window.js +27 -7
  99. package/dist/src/events/events.js +18 -14
  100. package/dist/src/events/notify.js +80 -30
  101. package/dist/src/failure/failure-cluster.js +12 -9
  102. package/dist/src/failure/failure-data.js +96 -48
  103. package/dist/src/failure/failure-render.js +69 -58
  104. package/dist/src/failure/failure-state-change.js +33 -26
  105. package/dist/src/failure/time-spend.js +79 -59
  106. package/dist/src/files/file-queue.js +16 -4
  107. package/dist/src/files/files.js +18 -6
  108. package/dist/src/files/json-object.js +18 -3
  109. package/dist/src/files/tail.js +1 -1
  110. package/dist/src/gates/bootstrap-gates.js +5 -4
  111. package/dist/src/gates/budget-gates.js +250 -34
  112. package/dist/src/gates/disk-gate.js +18 -18
  113. package/dist/src/gates/gate-polls.js +117 -69
  114. package/dist/src/gates/maintenance-quota.js +29 -29
  115. package/dist/src/gates/pause-gates.js +11 -11
  116. package/dist/src/gates/readiness.js +2 -1
  117. package/dist/src/gates/role-cap-gates.js +16 -21
  118. package/dist/src/gates/startup-gate.js +75 -31
  119. package/dist/src/gates/streak-gate.js +18 -16
  120. package/dist/src/git/commit-message.js +6 -5
  121. package/dist/src/git/git-diff.js +16 -16
  122. package/dist/src/git/git-run.js +8 -8
  123. package/dist/src/git/git.js +79 -59
  124. package/dist/src/git/slots-state.js +14 -5
  125. package/dist/src/git/worktree-pool.js +25 -18
  126. package/dist/src/git/worktree-use.js +15 -14
  127. package/dist/src/git/worktree.js +18 -18
  128. package/dist/src/gui/gui-args.js +10 -14
  129. package/dist/src/gui/gui-backlog-commands.js +273 -0
  130. package/dist/src/gui/gui-command.js +1 -1
  131. package/dist/src/gui/gui-config-commands.js +440 -0
  132. package/dist/src/gui/gui-endpoint-commands.js +16 -716
  133. package/dist/src/gui/gui-endpoint-common.js +49 -0
  134. package/dist/src/gui/gui-endpoints.js +105 -54
  135. package/dist/src/gui/gui-operator-commands.js +145 -0
  136. package/dist/src/gui/gui-prompt-commands.js +234 -0
  137. package/dist/src/gui/gui-server.js +25 -8
  138. package/dist/src/gui/gui-team-commands.js +164 -0
  139. package/dist/src/gui/http-body.js +17 -1
  140. package/dist/src/history/history-data.js +179 -104
  141. package/dist/src/history/history-render.js +76 -0
  142. package/dist/src/history/history.js +29 -70
  143. package/dist/src/inbox/inbox-attachments.js +66 -10
  144. package/dist/src/inbox/inbox-cancel.js +7 -5
  145. package/dist/src/inbox/inbox-edit.js +1 -1
  146. package/dist/src/inbox/inbox-submit.js +44 -4
  147. package/dist/src/inbox/inbox-top.js +83 -0
  148. package/dist/src/inbox/inbox.js +8 -2
  149. package/dist/src/inbox/pending-prompt.js +1 -1
  150. package/dist/src/inbox/prompt-commands.js +114 -73
  151. package/dist/src/init/init-templates.js +16 -14
  152. package/dist/src/init/init.js +12 -11
  153. package/dist/src/loop/leftover.js +28 -27
  154. package/dist/src/loop/loop-fallback.js +2 -2
  155. package/dist/src/loop/loop-pi.js +104 -50
  156. package/dist/src/loop/loop-recovery.js +15 -15
  157. package/dist/src/loop/loop-state.js +130 -13
  158. package/dist/src/loop/loop-tick.js +190 -88
  159. package/dist/src/loop/loop.js +68 -36
  160. package/dist/src/loop/model-fallback.js +3 -2
  161. package/dist/src/loop/revision.js +16 -7
  162. package/dist/src/merge/backlog-conflicts.js +155 -0
  163. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  164. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  165. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  166. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  167. package/dist/src/merge/merge-drain.js +212 -0
  168. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
  169. package/dist/src/merge/merge-pipeline.js +99 -0
  170. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  171. package/dist/src/merge/merge-queue-render.js +75 -0
  172. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  173. package/dist/src/merge/merge-slot.js +340 -0
  174. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  175. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  176. package/dist/src/merge/merge-verify.js +212 -0
  177. package/dist/src/notes-cli.js +97 -0
  178. package/dist/src/operator/operator-commands.js +113 -45
  179. package/dist/src/operator/operator-intent.js +74 -53
  180. package/dist/src/operator/operator-requests.js +52 -52
  181. package/dist/src/operator/retire.js +47 -28
  182. package/dist/src/operator/revert.js +71 -0
  183. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  184. package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
  185. package/dist/src/orchestrator/orchestrator.js +149 -96
  186. package/dist/src/orchestrator/retention.js +29 -12
  187. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  188. package/dist/src/orchestrator/startup-migrations.js +40 -0
  189. package/dist/src/paths.js +124 -38
  190. package/dist/src/pi/pi-args.js +26 -3
  191. package/dist/src/pi/pi-backend.js +15 -23
  192. package/dist/src/pi/pi-bin.js +18 -12
  193. package/dist/src/pi/pi-event-line.js +17 -1
  194. package/dist/src/pi/pi-failure.js +87 -0
  195. package/dist/src/pi/pi-models.js +126 -38
  196. package/dist/src/pi/pi-run-result.js +1 -1
  197. package/dist/src/pi/pi-settings.js +86 -0
  198. package/dist/src/pi/pi-stream.js +131 -107
  199. package/dist/src/pi/pi-watchdogs.js +4 -3
  200. package/dist/src/pi-extension/bounded-output.js +20 -5
  201. package/dist/src/pi-extension/context-budget.js +3 -1
  202. package/dist/src/pi-extension/context-shake.js +54 -21
  203. package/dist/src/pi-extension/context-window.js +37 -0
  204. package/dist/src/pi-extension/full-output.js +2 -2
  205. package/dist/src/pi-extension/strip-thinking.js +80 -0
  206. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  207. package/dist/src/process/launch-services.js +11 -10
  208. package/dist/src/process/process-table.js +1 -1
  209. package/dist/src/process/process.js +7 -7
  210. package/dist/src/process/run-marker.js +29 -6
  211. package/dist/src/process/supervisor.js +36 -25
  212. package/dist/src/prompt/dialect.js +30 -10
  213. package/dist/src/prompt/gate-prompts.js +170 -67
  214. package/dist/src/prompt/principles.js +1 -0
  215. package/dist/src/prompt/prompt-followup.js +29 -20
  216. package/dist/src/prompt/prompt.js +249 -91
  217. package/dist/src/redeploy/redeploy-policy.js +10 -10
  218. package/dist/src/redeploy/redeploy.js +6 -4
  219. package/dist/src/redeploy/redeployer.js +63 -55
  220. package/dist/src/redeploy/self-reload.js +1 -1
  221. package/dist/src/report/report-data.js +259 -254
  222. package/dist/src/report/report-fold.js +353 -0
  223. package/dist/src/report/report-render.js +235 -44
  224. package/dist/src/request-timeouts.js +1 -1
  225. package/dist/src/review/known-flakes.js +34 -14
  226. package/dist/src/review/protected-edits.js +4 -3
  227. package/dist/src/review/review-followup.js +16 -4
  228. package/dist/src/review/review-precheck.js +14 -13
  229. package/dist/src/review/review-verdict.js +14 -0
  230. package/dist/src/review/review.js +55 -34
  231. package/dist/src/review/secret-leaks.js +127 -0
  232. package/dist/src/review/suite-rerun.js +4 -3
  233. package/dist/src/roles/legacy-role-ids.js +21 -0
  234. package/dist/src/roles/legacy-role-migration.js +62 -0
  235. package/dist/src/roles/loop-ids.js +19 -2
  236. package/dist/src/roles/role-catalog.js +109 -131
  237. package/dist/src/roles/role-guidance.js +64 -31
  238. package/dist/src/roles/role-labels.js +10 -0
  239. package/dist/src/roles/role-render.js +194 -18
  240. package/dist/src/roles/role-view.js +216 -17
  241. package/dist/src/roles/roles.js +16 -14
  242. package/dist/src/scheduling/backoff.js +29 -19
  243. package/dist/src/scheduling/claims.js +15 -8
  244. package/dist/src/scheduling/once-round.js +11 -11
  245. package/dist/src/scheduling/quiet-hours.js +25 -32
  246. package/dist/src/scheduling/scheduling.js +28 -25
  247. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  248. package/dist/src/status/status-budget.js +115 -0
  249. package/dist/src/status/status-data.js +72 -184
  250. package/dist/src/status/status-inbox.js +47 -0
  251. package/dist/src/status/status-install.js +27 -0
  252. package/dist/src/status/status-polls.js +50 -39
  253. package/dist/src/status/status-roles.js +105 -0
  254. package/dist/src/{fleet → team}/error-storm.js +10 -10
  255. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  256. package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
  257. package/dist/src/{fleet → team}/reclaim.js +5 -4
  258. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  259. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  260. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
  261. package/dist/src/text/datetime.js +42 -2
  262. package/dist/src/text/format.js +10 -1
  263. package/dist/src/text/markdown.js +1 -1
  264. package/dist/src/text/phrases.js +35 -34
  265. package/dist/src/text/suggest.js +3 -2
  266. package/dist/src/text/text.js +2 -2
  267. package/dist/src/tick/qa-coverage.js +2 -1
  268. package/dist/src/tick/stage-check.js +142 -25
  269. package/dist/src/tick/telemetry-digest.js +10 -9
  270. package/dist/src/tick/tick-apply.js +101 -72
  271. package/dist/src/tick/tick-detail-data.js +23 -22
  272. package/dist/src/tick/tick-detail.js +37 -24
  273. package/dist/src/tick/tick-finalize.js +29 -22
  274. package/dist/src/tick/tick-outcome.js +2 -1
  275. package/dist/src/tick/tick-prompt.js +94 -26
  276. package/dist/src/tick/tick-resume.js +29 -13
  277. package/dist/src/tick/tick-stage.js +67 -53
  278. package/dist/src/tick/tick-timing.js +60 -59
  279. package/dist/src/tick/tick-usage.js +53 -23
  280. package/dist/src/tick/tick-verdict.js +75 -10
  281. package/dist/src/ui/badges.js +113 -69
  282. package/dist/src/ui/gui/gui-client-boot.js +202 -29
  283. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  284. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  285. package/dist/src/ui/gui/gui-client-history.js +94 -37
  286. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  287. package/dist/src/ui/gui/gui-client-loops.js +90 -47
  288. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  289. package/dist/src/ui/gui/gui-client-model.js +60 -43
  290. package/dist/src/ui/gui/gui-client-operator.js +73 -43
  291. package/dist/src/ui/gui/gui-client-pending.js +34 -31
  292. package/dist/src/ui/gui/gui-client-report.js +330 -47
  293. package/dist/src/ui/gui/gui-client-settings.js +126 -18
  294. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  295. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
  296. package/dist/src/ui/gui/gui-client.js +26 -20
  297. package/dist/src/ui/gui/gui-page.js +18 -19
  298. package/dist/src/ui/gui/gui-styles.js +12 -4
  299. package/dist/src/ui/progress-data.js +5 -3
  300. package/dist/src/ui/status-model.js +176 -102
  301. package/dist/src/ui/status-payload.js +114 -36
  302. package/dist/src/ui/status-render.js +121 -52
  303. package/dist/src/ui/team-alerts.js +341 -0
  304. package/dist/src/ui/tick-progress-model.js +14 -14
  305. package/dist/src/ui/tone.js +9 -8
  306. package/dist/src/ui/transcript-tail.js +1 -0
  307. package/dist/src/ui/transcript.js +23 -16
  308. package/dist/src/ui/tui/tui-app.js +1 -0
  309. package/dist/src/ui/tui/tui-backlog.js +85 -11
  310. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  311. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  312. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  313. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  314. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  315. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  316. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  317. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  318. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  319. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  320. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  321. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  322. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  323. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  324. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  325. package/dist/src/ui/tui/tui-editors.js +152 -0
  326. package/dist/src/ui/tui/tui-frame.js +61 -57
  327. package/dist/src/ui/tui/tui-history.js +12 -7
  328. package/dist/src/ui/tui/tui-input.js +44 -50
  329. package/dist/src/ui/tui/tui-keymap.js +1 -0
  330. package/dist/src/ui/tui/tui-keys.js +87 -1821
  331. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  332. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  333. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  334. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  335. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  336. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  337. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  338. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  339. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  340. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  341. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  342. package/dist/src/ui/tui/tui-pane.js +52 -28
  343. package/dist/src/ui/tui/tui-panes.js +294 -0
  344. package/dist/src/ui/tui/tui.js +44 -26
  345. package/dist/src/verdict/fix-claim.js +56 -33
  346. package/dist/src/verdict/no-change.js +19 -0
  347. package/dist/src/verdict/refusal.js +43 -11
  348. package/dist/src/verdict/reply-contract.js +17 -5
  349. package/dist/src/version.js +3 -2
  350. package/package.json +4 -2
  351. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  352. package/dist/src/landing/backlog-conflicts.js +0 -116
  353. package/dist/src/landing/landing-drain.js +0 -204
  354. package/dist/src/landing/landing-pipeline.js +0 -96
  355. package/dist/src/landing/landing-queue-render.js +0 -56
  356. package/dist/src/landing/landing-slot.js +0 -256
  357. package/dist/src/landing/landing-vetting.js +0 -196
  358. package/dist/src/pi-extension/role-notes.js +0 -40
  359. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  /** The review gate's deterministic build pre-check — split out of review.ts so the gate keeps
2
3
  * one responsibility (running the model reviewer and acting on its verdict) while this module
3
4
  * owns the pre-review verification: the one scoped build-check run, its flake re-run, the
@@ -13,7 +14,7 @@ import { sampleSleepClock } from "../build/host-sleep.js";
13
14
  import { recordReview } from "../tick/tick-apply.js";
14
15
  import { mainRedNotMine } from "../text/phrases.js";
15
16
  import { shortSha } from "../text/format.js";
16
- import { checkWaitStage, setLandingStage } from "../landing/landing-slot.js";
17
+ import { checkWaitStage, setMergeStage } from "../merge/merge-slot.js";
17
18
  import { mainTipVerdict } from "../baseline/main-red.js";
18
19
  import { FLAKY_WARNING_PREFIX, isKnownFlake } from "./known-flakes.js";
19
20
  /** Run the gate's deterministic build pre-check. `reject` is the gate's one shared rejection
@@ -22,10 +23,10 @@ import { FLAKY_WARNING_PREFIX, isKnownFlake } from "./known-flakes.js";
22
23
  * model-verdict path. */
23
24
  export async function gateBuildPrecheck(ctx, state, head, reject) {
24
25
  const { root, role, wt, mainBranch, config } = ctx;
25
- // The landing cell's stage (a no-op outside a queued landing — see setLandingStage): the
26
+ // The merge cell's stage (a no-op outside a queued merge — see setMergeStage): the
26
27
  // pre-check, its one re-run, and any attribution check behind them are the gate's first
27
28
  // long phase.
28
- setLandingStage(root, role, "build-check");
29
+ setMergeStage(root, role, "build-check");
29
30
  // The gate's one check invocation — the pre-check and its one flake re-run below are the
30
31
  // same run in the same scope, worktree, and config with the same resolved timeout, so the
31
32
  // scope and timeout resolution live here once and cannot drift between the two calls.
@@ -65,9 +66,9 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
65
66
  // The run spanned a host sleep past the tolerance and no clean attempt judged the tree
66
67
  // (runScopedBuildCheck already retried once): nothing here is the change's failure, so
67
68
  // the gate attributes nothing — it keeps the commit and reports the sleep, and the
68
- // landing path keeps the pin for a re-land (BUGS.md 2026-09-30).
69
+ // merge path keeps the pin for a re-merge (BUGS.md 2026-09-30).
69
70
  const detail = sleptPhrase("gate check", outcome.run.sleptMs, UNVERIFIED_TREE);
70
- warnEventBestEffort(root, role, `${detail}; keeping the commit for a re-land`);
71
+ warnEventBestEffort(root, role, `${detail}; keeping the commit for another merge attempt`);
71
72
  return { resolved: { decision: "failed", detail, unverified: true } };
72
73
  }
73
74
  if (outcome.status === "failed") {
@@ -75,23 +76,23 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
75
76
  // re-run passed on this exact headline) says nothing about this change: attribute nothing
76
77
  // — keep the commit and the pin, count no strike — and name the flake, exactly as the
77
78
  // host-sleep path above does. Before this, the repeat was attributed through main's
78
- // per-SHA baseline cache (usually a green seeded by an earlier landing), so a load- or
79
+ // per-SHA baseline cache (usually a green seeded by an earlier merge), so a load- or
79
80
  // clock-sensitive test rejected whichever unrelated change hit it (BUGS.md 2026-10-06).
80
81
  const flaky = failureHeadline(outcome.outputTail) ?? describeCheck(check);
81
82
  if (isKnownFlake(root, flaky)) {
82
- const detail = `known-flaky failure, landing kept: ${flaky}`;
83
+ const detail = `known-flaky failure, merge kept: ${flaky}`;
83
84
  recordReview(state, "failed", [detail], head);
84
85
  warnEventBestEffort(root, role, detail);
85
86
  return { resolved: { decision: "failed", detail, unverified: true } };
86
87
  }
87
88
  const reasons = checkFailureReasons(check, outcome);
88
- // A failure that reproduced is attributed, never fixed here: the one landing slot the
89
+ // A failure that reproduced is attributed, never fixed here: the one merge slot the
89
90
  // whole queue waits on is no place for a model run (the in-slot build-fix run this
90
- // replaces held it for hours and never once led to a landing — PLANS.md "Land-queue
91
+ // replaces held it for hours and never once led to a merge — PLANS.md "Land-queue
91
92
  // speed 1/3"). The question is whose failure it is, and main's own verdict at its tip
92
93
  // answers it. A failure matching no recorded flake bypasses the per-SHA cache and runs
93
94
  // main's check now (forceFresh): a clock- or load-sensitive test can pass on an immutable
94
- // SHA early and fail later, so a cached green — usually seeded by an earlier landing —
95
+ // SHA early and fail later, so a cached green — usually seeded by an earlier merge —
95
96
  // may no longer be true, and trusting it rejected an unrelated change (BUGS.md
96
97
  // 2026-10-06). A shutdown already under way skips the question and fails closed like any
97
98
  // abort.
@@ -105,10 +106,10 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
105
106
  // Main fails too: not this change's failure. Leave the commit (and with it the pin) and
106
107
  // do not advance the discard counter — the transport-failure rule (BUGS.md 2026-09-20):
107
108
  // nothing judged this diff. main-red.ts's gate and the bugfix handoff own the repair,
108
- // and the author's next tick re-lands the change once main is green again.
109
+ // and the author's next tick re-merges the change once main is green again.
109
110
  const detail = mainRedNotMine(main.sha);
110
111
  recordReview(state, "failed", [detail], head);
111
- warnEventBestEffort(root, role, `gate check failed on ${shortSha(head)}, but ${detail}; landing kept`);
112
+ warnEventBestEffort(root, role, `gate check failed on ${shortSha(head)}, but ${detail}; merge kept`);
112
113
  return { resolved: { decision: "failed", detail, mainRed: true } };
113
114
  }
114
115
  // Main green: the change broke the check — rejected deterministically through the shared
@@ -123,7 +124,7 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
123
124
  }
124
125
  if (outcome.status === "passed") {
125
126
  // Passed: this exact tree just went green under the project's own declared check. Hand
126
- // the verdict to the landing path via verifiedHead — it seeds the red-main baseline with
127
+ // the verdict to the merge path via verifiedHead — it seeds the red-main baseline with
127
128
  // the SHA that actually becomes main (the rebased head, which may differ from `head`),
128
129
  // so every role's next fresh tick is a cache hit instead of one redundant full-suite
129
130
  // re-run on an already-verified tree.
@@ -7,6 +7,20 @@ import { verdictLines } from "../verdict/reply-contract.js";
7
7
  import { clipReason } from "../build/build-check-report.js";
8
8
  /** Cap on recorded reasons, so a chatty reviewer cannot bloat persisted state. */
9
9
  const MAX_REASONS = 10;
10
+ /** The prior-objection number a reviewer reason is tagged with at its start — `(prior N)`, N
11
+ * the objection's 1-based number in the prior list the re-review prompt showed — or null when
12
+ * the reason is a newly raised (untagged) objection. The repeat-finality policy (BUGS.md
13
+ * 2026-10-14) asks a re-review to restate a still-open prior objection with this tag so the
14
+ * harness can tell a repeat from a new finding and end a chain whose objection has stopped
15
+ * converging. Anything else that merely begins with the word "prior" (e.g. "prior to the
16
+ * change…") is not a tag and reads as an untagged reason. */
17
+ export function priorObjectionNumber(reason) {
18
+ const m = /^\(prior\s+(\d+)\)/i.exec(reason.trim());
19
+ if (!m)
20
+ return null;
21
+ const n = Number(m[1]);
22
+ return Number.isInteger(n) && n >= 1 ? n : null;
23
+ }
10
24
  /** Parse the reviewer's reply: the LAST VERDICT line wins (the prompt asks for exactly one),
11
25
  * followed by its reasons — numbered/bulleted lines first, any other non-empty prose as a
12
26
  * fallback. Null when no parseable verdict exists: that is a FAILED review, never an approval
@@ -1,5 +1,5 @@
1
1
  import { REVIEW_RESUME_LIMIT, saveLoopState } from "../loop/loop-state.js";
2
- import { modelSelectorField, reviewRunConfig } from "../config/config-views.js";
2
+ import { modelSelectorField, resolvedAgentConfig, reviewRunConfig } from "../config/config-views.js";
3
3
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
4
4
  import { git } from "../git/git-run.js";
5
5
  import { changeBaseRev, headOf, patchId } from "../git/git.js";
@@ -16,17 +16,18 @@ import { shortSha } from "../text/format.js";
16
16
  import { firstReason } from "../text/phrases.js";
17
17
  import { isExemptDiff } from "./exemptions.js";
18
18
  import { falseFixReason } from "../verdict/fix-claim.js";
19
- import { conflictMarkerReason } from "../landing/landing-git.js";
19
+ import { conflictMarkerReason } from "../merge/merge-git.js";
20
20
  import { protectedEditReason } from "./protected-edits.js";
21
+ import { secretLeakReason } from "./secret-leaks.js";
21
22
  import { backlogStructureReason } from "../backlog/backlog-structure.js";
22
23
  import { suiteRerunWarning } from "./suite-rerun.js";
23
- import { setLandingStage } from "../landing/landing-slot.js";
24
+ import { setMergeStage } from "../merge/merge-slot.js";
24
25
  import { gateBuildPrecheck } from "./review-precheck.js";
25
26
  /** Attach the review gate's pi runs onto a partial GateResult — `run` plus the optional
26
27
  * verdict-recovery follow-up (`followUpRun`) and no-re-run nudge (`nudgeRun`) fields, each
27
28
  * present only when its turn actually ran. Spelled once so every return site of
28
29
  * reviewAheadOfMain that carries runs shares one shape for the usage-folding caller
29
- * (landing-core.ts folds run, then followUpRun, then nudgeRun), instead of one conditional
30
+ * (merge-core.ts folds run, then followUpRun, then nudgeRun), instead of one conditional
30
31
  * spread per site that a new run kind would have to update everywhere. */
31
32
  function withRuns(r, run, followUp, nudge) {
32
33
  return {
@@ -37,7 +38,7 @@ function withRuns(r, run, followUp, nudge) {
37
38
  };
38
39
  }
39
40
  /** Put the worktree back on the exact head the gate reviewed: the reviewer's only output is its
40
- * verdict, so a commit it made must never reach the landing pin or the next re-review. */
41
+ * verdict, so a commit it made must never reach the merge pin or the next re-review. */
41
42
  async function restoreReviewedHead(wt, head, root, role) {
42
43
  const now = await headOf(wt, "HEAD").catch(() => null);
43
44
  if (now && now !== head) {
@@ -94,7 +95,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
94
95
  state.unreviewFailures = 0;
95
96
  await resetWorktreeToMain(wt, mainBranch);
96
97
  logEventBestEffort(root, {
97
- loop: role,
98
+ source: role,
98
99
  type: "review_rejected",
99
100
  head,
100
101
  reasons,
@@ -109,28 +110,32 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
109
110
  }
110
111
  const files = await aheadOfMainFiles(wt, mainBranch);
111
112
  // A change handed back to its author with the conflict markers left in place (PLANS.md
112
- // "Robust conflict landing, part 2/2") must not land raw `<<<<<<<` lines. The stage check
113
+ // "Robust conflict landing, part 2/2") must not merge raw `<<<<<<<` lines. The stage check
113
114
  // flags them for one fix-up turn, but only this deterministic gate rejection makes it a
114
115
  // hard stop — an md-only diff skips the reviewer entirely, so nothing else would catch it.
115
116
  const markerReason = conflictMarkerReason(wt, files);
116
117
  if (markerReason !== undefined)
117
118
  return reject([markerReason]);
118
- // The prompt block and PRINCIPLES.md are markdown, so the exempt fast path below would land
119
+ // The prompt block and PRINCIPLES.md are markdown, so the exempt fast path below would merge
119
120
  // an edit to either unreviewed; reject it here, for md-only and code diffs alike.
120
121
  const protectedReason = await protectedEditReason(wt, mainBranch, files, role);
121
122
  if (protectedReason !== undefined)
122
123
  return reject([protectedReason]);
124
+ // A leaked credential is just as invisible to the exempt fast path: reject it here too.
125
+ const secretReason = await secretLeakReason(wt, mainBranch, files);
126
+ if (secretReason !== undefined)
127
+ return reject([secretReason]);
123
128
  if (isExemptDiff(files, config.review.exemptPaths)) {
124
129
  // The exemption is a fast path, not a blind eye: an md-only diff that moves a bug to
125
130
  // Fixed must have its fix's symbols on this tree, or it records code that does not
126
- // exist (BUGS.md 2026-09-22 — 9cea8c3 landed a Fix paragraph naming runScriptGroup
131
+ // exist (BUGS.md 2026-09-22 — 9cea8c3 merged a Fix paragraph naming runScriptGroup
127
132
  // and signalTree with no source behind it). A deterministic rejection, like the
128
133
  // pre-check below: no pi run consumed, reasons injected into the author's next tick.
129
134
  const falseFix = await falseFixReason(wt, mainBranch, files);
130
135
  if (falseFix)
131
136
  return reject([falseFix]);
132
- // Same cross-check as the landing path (backlog-structure.ts): an md-only diff that
133
- // duplicates or drops a `## ` section heading would land unreviewed — md-only edits skip
137
+ // Same cross-check as the merge path (backlog-structure.ts): an md-only diff that
138
+ // duplicates or drops a `## ` section heading would merge unreviewed — md-only edits skip
134
139
  // the reviewer by design, and a malformed backlog stays invisible until an entry ends up
135
140
  // on the wrong side of a heading (PLANS.md 2026-09-25, 52cbadd1).
136
141
  const structure = await backlogStructureReason(wt, mainBranch, files);
@@ -141,7 +146,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
141
146
  }
142
147
  // The backlog-structure check for code diffs too, before the build pre-check: a change that
143
148
  // duplicates or drops a `## ` section heading in PLANS.md, BUGS.md, or QUESTIONS.md cannot
144
- // land, and the rejection is deterministic — no pi run and no check run spent on a tree the
149
+ // merge, and the rejection is deterministic — no pi run and no check run spent on a tree the
145
150
  // heading set already condemns.
146
151
  const structure = await backlogStructureReason(wt, mainBranch, files);
147
152
  if (structure)
@@ -151,9 +156,9 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
151
156
  // "reviewing": a deterministic rejection never shows as reviewing on the dashboards. Both
152
157
  // gate callers (the tick path and leftover.ts's recoverLeftover) get it for free. Its run,
153
158
  // flake re-run, and attribution live in review-precheck.ts (the check run itself lives in
154
- // runScopedBuildCheck, shared with the landing path's in-lock re-check); a resolved result
155
- // here is returned verbatim and a pass hands the verified head to the landing path below.
156
- // The pre-check sets the landing cell's stage (a no-op outside a queued landing) —
159
+ // runScopedBuildCheck, shared with the merge path's in-lock re-check); a resolved result
160
+ // here is returned verbatim and a pass hands the verified head to the merge path below.
161
+ // The pre-check sets the merge cell's stage (a no-op outside a queued merge) —
157
162
  // "build-check" while it runs, "reviewing" when the reviewer run starts below.
158
163
  const pre = await gateBuildPrecheck(ctx, state, head, reject);
159
164
  if (pre.resolved) {
@@ -178,6 +183,10 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
178
183
  // same config the run below gets, so the event names what the reviewer actually runs on —
179
184
  // the budget fallback included. Omitted when no model is configured (pi's own default).
180
185
  const reviewCfg = reviewRunConfig(config);
186
+ // The team claude config dir (plans/notes-command.md "Keeping funes out of team runs"), so
187
+ // a claude review's resume guard looks under the same dir the review run writes its transcript
188
+ // to — a team that set one must resume under it, not the default ~/.claude.
189
+ const claudeConfigDir = resolvedAgentConfig(config, "claude").configDir;
181
190
  // A progressing timeout left this run's session for the next attempt (LoopState.reviewResume):
182
191
  // resume it only while the patch is unchanged (the same diff), the resume budget lasts, and
183
192
  // the directory still holds a gate session to `--continue`. Otherwise the stale marker is
@@ -189,7 +198,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
189
198
  agentSessionResumable(priorResume.sessionDir, {
190
199
  kind: "gate",
191
200
  agent: reviewCfg.agent ?? "pi",
192
- });
201
+ }, claudeConfigDir);
193
202
  if (resuming) {
194
203
  // Count the resume before the run: a crash must not let the same session be resumed
195
204
  // forever without ever spending the budget.
@@ -209,7 +218,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
209
218
  // needs the override.
210
219
  const followupCtx = reviewSessionDir === ctx.sessionDir ? ctx : { ...ctx, sessionDir: reviewSessionDir };
211
220
  logEventBestEffort(root, {
212
- loop: role,
221
+ source: role,
213
222
  type: "review_start",
214
223
  head,
215
224
  ...(ctx.revisionRound !== undefined ? { revision: ctx.revisionRound } : {}),
@@ -222,8 +231,8 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
222
231
  state.phase = "review";
223
232
  saveLoopState(root, state);
224
233
  // Freeze the revision the diff is measured from once, before the run: main moves while a
225
- // landing is vetted, and a reviewer comparing against the live `main` ref reads another loop's
226
- // just-landed work as this change reverting it (BUGS.md 2026-10-06). The prompt names this SHA
234
+ // merge is approved, and a reviewer comparing against the live `main` ref reads another loop's
235
+ // just-merged work as this change reverting it (BUGS.md 2026-10-06). The prompt names this SHA
227
236
  // and tells the reviewer to compare against it instead.
228
237
  const base = await changeBaseRev(wt, mainBranch);
229
238
  const diff = await aheadOfMainDiff(wt, mainBranch);
@@ -239,17 +248,17 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
239
248
  }
240
249
  : undefined;
241
250
  // The reviewer run's wall time rides on its verdict event: a reviewer that takes an hour per
242
- // merge on local hardware is a fleet-level cost an operator must be able to see.
251
+ // merge on local hardware is a team-level cost an operator must be able to see.
243
252
  const reviewStartedAt = Date.now();
244
253
  // The reviewer's started tool calls, collected only when the no-re-run rule stands (a verified
245
254
  // pre-check) — the one case the suite-rerun tripwire below can fire.
246
255
  const reviewerCalls = [];
247
- // The landing cell switches to the reviewer run's live detail (no-op outside a queued
248
- // landing). runPi writes the run's `tumwater_run` label line before spawning pi, and that
256
+ // The merge cell switches to the reviewer run's live detail (no-op outside a queued
257
+ // merge). runPi writes the run's `tumwater_run` label line before spawning pi, and that
249
258
  // line resets the gate progress accumulator, so the cell never shows a previous run's counts.
250
- setLandingStage(root, role, "reviewing");
259
+ setMergeStage(root, role, "reviewing");
251
260
  // The author's claimed WHY/RISK/VERIFIED ride along when present — checking those claims
252
- // against the actual diff is exactly the adversarial angle (recovery landings reconstruct
261
+ // against the actual diff is exactly the adversarial angle (recovery merges reconstruct
253
262
  // them from the pinned commit's message).
254
263
  const pi = await ctx.runGatePi({
255
264
  cwd: wt,
@@ -259,8 +268,8 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
259
268
  : buildReviewPrompt(diff, summary, commitBody, readPrinciples(root), highFriction, verifiedByHarness, base, undefined, priorReview),
260
269
  // The reviewer runs on its own time budget (review.timeoutSeconds), never longer than a
261
270
  // tick's: a timed-out review is a FAILED run (pi.ok false), so it takes the dead-backend
262
- // path below — commit kept, no strike — and re-lands through the author's next tick
263
- // instead of holding the land queue for a whole authoring tick.
271
+ // path below — commit kept, no strike — and re-merges through the author's next tick
272
+ // instead of holding the merge queue for a whole authoring tick.
264
273
  config: reviewCfg,
265
274
  // This run's own session directory (per-run, keyed by tick and pinned sha): a follow-up
266
275
  // turn, or a later attempt that resumes this review, continues exactly this run instead
@@ -284,8 +293,20 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
284
293
  reviewerCalls.push({ toolName, args });
285
294
  }
286
295
  : undefined,
296
+ // A gate run that read an image its model cannot see is logged, not killed (plans/
297
+ // image-input.md "Detecting a blind read"): the reviewer keeps working from what it CAN
298
+ // see, and the image_blind line names the file, the model, and this tick so the operator
299
+ // knows the diff was judged under a blind read.
300
+ onBlindRead: (path, action) => logEventBestEffort(root, {
301
+ source: role,
302
+ type: "image_blind",
303
+ run: ctx.tick,
304
+ path,
305
+ model: reviewCfg.model,
306
+ action,
307
+ }),
287
308
  });
288
- // A reviewer told the pre-check passed that re-ran the full suite anyway held the landing
309
+ // A reviewer told the pre-check passed that re-ran the full suite anyway held the merge
289
310
  // slot and loaded the shared host for a result the harness already had (BUGS.md 2026-09-23:
290
311
  // scratch copies under /tmp, `npm ci` and `npm test` there). Warned on every outcome, abort
291
312
  // included — the run happened either way; the verdict itself is not touched.
@@ -319,7 +340,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
319
340
  }
320
341
  nudgeRepeat = nudge ? (suiteRerunWarning(nudgeCalls) ?? null) : null;
321
342
  if (nudgeRepeat) {
322
- warnEventBestEffort(root, role, `${nudgeRepeat} the repeat fails this review: the commit stays, and the landing re-reviews it without a third suite run`);
343
+ warnEventBestEffort(root, role, `${nudgeRepeat} the repeat fails this review: the commit stays, and the merge re-reviews it without a third suite run`);
323
344
  }
324
345
  }
325
346
  let verdict = parseVerdict(pi.verdictText ?? "");
@@ -353,11 +374,11 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
353
374
  }
354
375
  // A repeat after the nudge (BUGS.md 2026-10-02): the reviewer re-ran the suite on the nudge
355
376
  // turn too. Fail the review — strike-free like a dead backend (BUGS.md 2026-09-20): the
356
- // commit stays for the next landing's re-review, the discard counter never advances, and no
377
+ // commit stays for the next merge's re-review, the discard counter never advances, and no
357
378
  // rejection is recorded against the author's diff.
358
379
  if (nudgeRepeat) {
359
- const message = `${nudgeRepeat} The review is failed on this repeat: the commit is kept, and the landing re-reviews it under the normal path.`;
360
- logEventBestEffort(root, { loop: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
380
+ const message = `${nudgeRepeat} The review is failed on this repeat: the commit is kept, and the merge re-reviews it under the normal path.`;
381
+ logEventBestEffort(root, { source: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
361
382
  state.reviewResume = undefined;
362
383
  recordReview(state, "failed", [message], head);
363
384
  return keep(withRuns({ decision: "failed", detail: message }, pi, followUp, nudge));
@@ -389,7 +410,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
389
410
  agentSessionResumable(reviewSessionDir, {
390
411
  kind: "gate",
391
412
  agent: reviewCfg.agent ?? "pi",
392
- });
413
+ }, claudeConfigDir);
393
414
  const reviewTimeoutRewrite = (message) => message.replace(" — session and worktree edits preserved for resume", resumeRemains
394
415
  ? " — the commit is kept; the next attempt continues this review"
395
416
  : " — the commit is kept; the next attempt reviews it from scratch");
@@ -401,7 +422,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
401
422
  : followUp
402
423
  ? ", even after a follow-up turn on its session"
403
424
  : ""}`);
404
- logEventBestEffort(root, { loop: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
425
+ logEventBestEffort(root, { source: role, type: "review_failed", head, message, durationMs: Date.now() - reviewStartedAt });
405
426
  // A dead reviewer must never destroy committed work (BUGS.md 2026-09-20): leave the commit
406
427
  // for the next tick's re-review and do not advance the per-HEAD discard counter. That
407
428
  // holds for the review run itself (BUGS.md 2026-09-20), for the verdict-recovery
@@ -458,7 +479,7 @@ export async function reviewAheadOfMain(ctx, state, summary, commitBody, highFri
458
479
  state.unreviewFailures = 0;
459
480
  await restoreReviewedHead(wt, head, root, role);
460
481
  logEventBestEffort(root, {
461
- loop: role,
482
+ source: role,
462
483
  type: "review_verdict",
463
484
  head,
464
485
  reason: verdict.reasons[0],
@@ -0,0 +1,127 @@
1
+ // vocabulary: plain
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { readTextOrNull } from "../files/files.js";
5
+ import { readJsonFile } from "../files/json-files.js";
6
+ import { changeBaseRev, fileContentAt } from "../git/git.js";
7
+ import { piModelsPath } from "../pi/pi-models.js";
8
+ import { moreSuffix } from "../text/phrases.js";
9
+ /** Environment variable names that hold credentials. */
10
+ const SECRET_NAME = /(TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|API_?KEY|ACCESS_?KEY|PRIVATE_?KEY)/i;
11
+ /** A value shorter than this is a setting (a token limit), not a credential. */
12
+ const MIN_SECRET_LENGTH = 16;
13
+ /** Files larger than this are not scanned. */
14
+ const MAX_SCAN_BYTES = 8 * 1024 * 1024;
15
+ /** Most files named in one reason. */
16
+ const MAX_NAMED = 10;
17
+ /** A credential-shaped value: at least 16 characters, no whitespace, not a path. */
18
+ function looksLikeSecret(value) {
19
+ return value.length >= MIN_SECRET_LENGTH && !/\s/.test(value) && !value.startsWith("/");
20
+ }
21
+ /** The environment entries that look like credentials: a secret-like name and a value of at
22
+ * least 16 characters with no whitespace that is not a path (so `GOOGLE_APPLICATION_CREDENTIALS`
23
+ * pointing at a key file is not a secret). */
24
+ export function secretEnvValues(env) {
25
+ const out = [];
26
+ for (const [name, value] of Object.entries(env)) {
27
+ if (value === undefined || !SECRET_NAME.test(name))
28
+ continue;
29
+ if (!looksLikeSecret(value))
30
+ continue;
31
+ out.push({ name, value });
32
+ }
33
+ return out;
34
+ }
35
+ function isRecord(v) {
36
+ return typeof v === "object" && v !== null && !Array.isArray(v);
37
+ }
38
+ /** The credentials pi stores: literal `apiKey`s and header values in `models.json`, and the
39
+ * `key`/`access`/`refresh` fields (plus secret-named `env` values) of the sibling `auth.json`.
40
+ * `$NAME` interpolations (secretEnvValues covers those) and `!command` values are skipped. A
41
+ * missing or torn file yields nothing; never throws. */
42
+ export function piCredentialValues(modelsPath = piModelsPath()) {
43
+ const out = [];
44
+ const add = (name, value) => {
45
+ if (typeof value !== "string" || value.startsWith("$") || value.startsWith("!"))
46
+ return;
47
+ if (looksLikeSecret(value))
48
+ out.push({ name, value });
49
+ };
50
+ const models = readJsonFile(modelsPath);
51
+ if (models && isRecord(models.providers)) {
52
+ for (const [p, cfg] of Object.entries(models.providers)) {
53
+ if (!isRecord(cfg))
54
+ continue;
55
+ add(`models.json ${p}.apiKey`, cfg.apiKey);
56
+ if (isRecord(cfg.headers)) {
57
+ for (const [h, v] of Object.entries(cfg.headers))
58
+ add(`models.json ${p}.headers.${h}`, v);
59
+ }
60
+ }
61
+ }
62
+ const auth = readJsonFile(path.join(path.dirname(modelsPath), "auth.json"));
63
+ if (auth) {
64
+ for (const [p, cred] of Object.entries(auth)) {
65
+ if (!isRecord(cred))
66
+ continue;
67
+ for (const f of ["key", "access", "refresh"])
68
+ add(`auth.json ${p}.${f}`, cred[f]);
69
+ if (isRecord(cred.env)) {
70
+ for (const [n, v] of Object.entries(cred.env)) {
71
+ if (SECRET_NAME.test(n))
72
+ add(`auth.json ${p}.env.${n}`, v);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+ /** Every secret the gate knows: secret-named environment values (named `$NAME`) then pi's stored
80
+ * credentials (named `pi <file> <path>`), the first entry winning when two share a value. */
81
+ export function knownSecrets(env = process.env, modelsPath = piModelsPath()) {
82
+ const all = [
83
+ ...secretEnvValues(env).map((s) => ({ name: `$${s.name}`, value: s.value })),
84
+ ...piCredentialValues(modelsPath).map((s) => ({ name: `pi ${s.name}`, value: s.value })),
85
+ ];
86
+ return all.filter((s, i) => all.findIndex((o) => o.value === s.value) === i);
87
+ }
88
+ /** The deterministic gate/stage finding for a change that writes the value of a secret
89
+ * environment variable or stored pi credential into a file (a Node diagnostic report dumps
90
+ * the whole environment). A value already present in the file at the merge-base is not this
91
+ * change's leak. The reason
92
+ * names files and variables, never the value. undefined when nothing leaked; never throws on
93
+ * an unreadable file. */
94
+ export async function secretLeakReason(wt, mainBranch, files, secrets = knownSecrets()) {
95
+ if (secrets.length === 0)
96
+ return undefined;
97
+ let baseRev;
98
+ const leaks = [];
99
+ for (const file of files) {
100
+ const full = path.join(wt, file);
101
+ let stat;
102
+ try {
103
+ stat = fs.statSync(full);
104
+ }
105
+ catch {
106
+ continue;
107
+ }
108
+ if (!stat.isFile() || stat.size > MAX_SCAN_BYTES)
109
+ continue;
110
+ const head = readTextOrNull(full);
111
+ if (head === null)
112
+ continue;
113
+ const hits = secrets.filter((s) => head.includes(s.value));
114
+ if (hits.length === 0)
115
+ continue;
116
+ baseRev ??= await changeBaseRev(wt, mainBranch);
117
+ const base = await fileContentAt(wt, baseRev, file);
118
+ for (const s of hits) {
119
+ if (!base.includes(s.value))
120
+ leaks.push(`\`${file}\` contains the value of ${s.name}`);
121
+ }
122
+ }
123
+ if (leaks.length === 0)
124
+ return undefined;
125
+ const shown = leaks.slice(0, MAX_NAMED).join("; ");
126
+ return `${shown}${moreSuffix(leaks.length - MAX_NAMED)} — delete the file or remove the value; never commit credentials (a Node diagnostic report, report.*.json, dumps the whole environment)`;
127
+ }
@@ -1,8 +1,9 @@
1
+ // vocabulary: plain
1
2
  /** Detecting a reviewer run that re-ran the full suite on a tree the gate's pre-check already
2
3
  * verified — pure string logic over the bash command lines a pi run started, no subprocess or
3
4
  * file I/O. The review prompt tells such a reviewer not to (src/prompt/gate-prompts.ts), but on
4
- * 2026-09-23 two reviewers that got the instruction copied their lander worktree to /tmp and ran
5
- * `npm ci` and `npm test` there anyway, holding the landing slot and loading the shared host;
5
+ * 2026-09-23 two reviewers that got the instruction copied their merger worktree to /tmp and ran
6
+ * `npm ci` and `npm test` there anyway, holding the merge slot and loading the shared host;
6
7
  * 102 of 429 retained review sessions ran a suite or `npm ci` themselves (BUGS.md 2026-09-23).
7
8
  * The prompt is the fix, this is the tripwire: review.ts feeds the reviewer's tool calls through
8
9
  * suiteRerunWarning and logs a warning when the rule was broken, so the digest can count it.
@@ -174,5 +175,5 @@ export function suiteRerunWarning(calls) {
174
175
  if (first === undefined)
175
176
  return undefined;
176
177
  const more = moreSuffix(reruns.length - 1);
177
- return `reviewer re-ran the suite the harness's pre-check already verified: ${squash(first, 160)}${more}`;
178
+ return `reviewer re-ran the suite tumwater's pre-check already verified: ${squash(first, 160)}${more}`;
178
179
  }
@@ -0,0 +1,21 @@
1
+ /** The one table of retired role ids, old id → new id. The switch parts of the plain-terms role
2
+ * rename add a row each; a row is never removed, because old events, state files and typed
3
+ * flags keep naming the old id. */
4
+ export const LEGACY_ROLE_IDS = {
5
+ dry: "dedupe",
6
+ telemetry: "selfcheck",
7
+ };
8
+ /** Today's id for a role id a person typed or an old file holds; any other id comes back
9
+ * unchanged. `legacy` is a test seam. */
10
+ export function canonicalRoleId(id, legacy = LEGACY_ROLE_IDS) {
11
+ return Object.hasOwn(legacy, id) ? legacy[id] : id;
12
+ }
13
+ /** The retired ids that now map to `id`. */
14
+ export function legacyRoleIdsOf(id, legacy = LEGACY_ROLE_IDS) {
15
+ return Object.entries(legacy).filter(([, next]) => next === id).map(([old]) => old);
16
+ }
17
+ /** `--grep` arguments matching a role's commit subjects under its current id and every retired
18
+ * one. git ORs repeated `--grep`s, so a history search sees both. */
19
+ export function roleCommitGrep(id, legacy = LEGACY_ROLE_IDS) {
20
+ return [id, ...legacyRoleIdsOf(id, legacy)].map((r) => `--grep="tumwater(${r})"`).join(" ");
21
+ }
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs";
2
+ import { deleteRefIfAt, refSha } from "../git/git.js";
3
+ import { gitTry, runGit } from "../git/git-run.js";
4
+ import { branchName, mergeRefName, rejectedRefName, reviewSessionDir, roleInboxDir, roleNotesPath, sessionDir, statePath } from "../paths.js";
5
+ import { errorMessage } from "../text/text.js";
6
+ import { LEGACY_ROLE_IDS } from "./legacy-role-ids.js";
7
+ /** 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. */
10
+ export async function migrateLegacyRoleIds(root, legacy = LEGACY_ROLE_IDS) {
11
+ const notes = [];
12
+ for (const [oldId, newId] of Object.entries(legacy)) {
13
+ const attempt = async (what, move) => {
14
+ try {
15
+ const note = await move();
16
+ if (note !== null)
17
+ notes.push(note);
18
+ }
19
+ catch (err) {
20
+ notes.push(`could not move ${what}: ${errorMessage(err)}`);
21
+ }
22
+ };
23
+ await attempt("branch", async () => {
24
+ const from = branchName(oldId);
25
+ const to = branchName(newId);
26
+ if ((await refSha(root, `refs/heads/${from}`)) === null)
27
+ return null;
28
+ if ((await refSha(root, `refs/heads/${to}`)) !== null)
29
+ return `kept branch ${from} and ${to}: both exist`;
30
+ await runGit(root, ["branch", "-m", from, to]);
31
+ return `renamed branch ${from} to ${to}`;
32
+ });
33
+ for (const refFor of [mergeRefName, rejectedRefName]) {
34
+ const from = refFor(oldId);
35
+ const to = refFor(newId);
36
+ await attempt(from, async () => {
37
+ const sha = await refSha(root, from);
38
+ if (sha === null)
39
+ return null;
40
+ if ((await refSha(root, to)) !== null)
41
+ return `kept ${from} and ${to}: both exist`;
42
+ if ((await gitTry(root, "update-ref", to, sha)) === null)
43
+ throw new Error(`update-ref ${to} failed`);
44
+ await deleteRefIfAt(root, from, sha);
45
+ return `moved ${from} to ${to}`;
46
+ });
47
+ }
48
+ for (const pathFor of [statePath, roleNotesPath, roleInboxDir, sessionDir, reviewSessionDir]) {
49
+ const from = pathFor(root, oldId);
50
+ const to = pathFor(root, newId);
51
+ await attempt(from, async () => {
52
+ if (!fs.existsSync(from))
53
+ return null;
54
+ if (fs.existsSync(to))
55
+ return `kept ${from} and ${to}: both exist`;
56
+ fs.renameSync(from, to);
57
+ return `moved ${from} to ${to}`;
58
+ });
59
+ }
60
+ }
61
+ return notes;
62
+ }
@@ -9,8 +9,10 @@
9
9
  /** The base roles that may run more than one instance. */
10
10
  export const INSTANCE_ROLES = new Set(["feature", "bugfix"]);
11
11
  /** The instance-id shape: a base role, a dash, and an index of 2 or more (`-1` is not an
12
- * instance — the bare id already means instance 1). */
13
- const INSTANCE_RE = /^(feature|bugfix)-([2-9]|[1-9][0-9]+)$/;
12
+ * instance — the bare id already means instance 1). The source string is also interpolated into
13
+ * the dashboard script (gui-client-labels.ts). */
14
+ export const INSTANCE_ID_SOURCE = "^(feature|bugfix)-([2-9]|[1-9][0-9]+)$";
15
+ const INSTANCE_RE = new RegExp(INSTANCE_ID_SOURCE);
14
16
  /** The base role a loop id belongs to: `feature-2` → `feature`, `bugfix-10` → `bugfix`; any
15
17
  * other id — a bare role, a custom loop, `feature-1` — is returned unchanged. */
16
18
  export function baseRoleOf(id) {
@@ -58,3 +60,18 @@ export function loopIds(config) {
58
60
  }
59
61
  return ids;
60
62
  }
63
+ /** The pause-marker key for a loop id. Instance 1's loop id is the bare role id, which as a pause
64
+ * key would mean the whole role, so while the role runs several instances instance 1 pauses under
65
+ * `<role>-1`. `-1` is a pause key only, never a loop id. Every other id is its own key. */
66
+ export function instancePauseKey(config, loopId) {
67
+ return INSTANCE_ROLES.has(loopId) && configuredInstances(config, loopId) > 1
68
+ ? `${loopId}-1`
69
+ : loopId;
70
+ }
71
+ /** Does the per-role pause marker stop this loop id? True for its own id, its base role's id (a
72
+ * whole-role pause), or — for the bare id of an instance role — the `<role>-1` pause key. */
73
+ export function loopPaused(paused, loopId) {
74
+ return (paused.has(loopId) ||
75
+ paused.has(baseRoleOf(loopId)) ||
76
+ (INSTANCE_ROLES.has(loopId) && paused.has(`${loopId}-1`)));
77
+ }