tumwater 0.1.2 → 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 (361) hide show
  1. package/README.md +74 -108
  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-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -0,0 +1,51 @@
1
+ import { legacyMigrateLandQueueDir } from "../merge/merge-queue.js";
2
+ import fs from "node:fs";
3
+ import { deleteRefIfAt, refSha, refsUnder, setRef } from "../git/git.js";
4
+ import { mergeRefName, legacyLandingStatePath } from "../paths.js";
5
+ import { migrateLegacyRoleIds } from "../roles/legacy-role-migration.js";
6
+ import { registerStartupMigration } from "./startup-migrations.js";
7
+ /** The one place that fixes startup-migration step order. Callers import the runners from here,
8
+ * so the steps are registered before anything can run. Add a step by appending one
9
+ * `registerStartupMigration(...)` call at the end. */
10
+ registerStartupMigration({ name: "mergeQueueDir", run: async (root) => legacyMigrateLandQueueDir(root) });
11
+ registerStartupMigration({
12
+ name: "mergingMarker",
13
+ run: async (root) => {
14
+ try {
15
+ fs.unlinkSync(legacyLandingStatePath(root));
16
+ return ["removed the old landing.json marker"]; // vocabulary-ok: names the legacy file
17
+ }
18
+ catch (err) {
19
+ if (err.code === "ENOENT")
20
+ return [];
21
+ throw err;
22
+ }
23
+ },
24
+ });
25
+ registerStartupMigration({
26
+ name: "mergeRefs",
27
+ run: async (root) => {
28
+ const legacyPrefix = "refs/tumwater/landing/"; // vocabulary-ok: legacy name read from old refs or commits
29
+ const notes = [];
30
+ for (const { ref, sha } of await refsUnder(root, legacyPrefix)) {
31
+ const role = ref.slice(legacyPrefix.length);
32
+ const target = mergeRefName(role);
33
+ const existing = await refSha(root, target);
34
+ if (existing === null) {
35
+ if (!(await setRef(root, target, sha)))
36
+ continue; // keep the legacy pin; the next start retries
37
+ await deleteRefIfAt(root, ref, sha);
38
+ notes.push(`moved ${ref} to ${target}`);
39
+ }
40
+ else if (existing === sha) {
41
+ await deleteRefIfAt(root, ref, sha);
42
+ notes.push(`removed ${ref}: ${target} already names the same commit`);
43
+ }
44
+ else
45
+ notes.push(`kept ${ref} and ${target}: they name different commits`);
46
+ }
47
+ return notes;
48
+ },
49
+ });
50
+ registerStartupMigration({ name: "legacyRoleIds", run: (root) => migrateLegacyRoleIds(root) });
51
+ export { runStartupMigrations, runStartupMigrationsIfIdle } from "./startup-migrations.js";
@@ -0,0 +1,40 @@
1
+ import { logEvent, TUMWATER_SOURCE, warnEventBestEffort } from "../events/events.js";
2
+ import { orchestratorAlive } from "../team/orchestrator-info.js";
3
+ import { errorMessage } from "../text/text.js";
4
+ const registry = [];
5
+ /** Append one step to the registry; a duplicate name throws. */
6
+ export function registerStartupMigration(step) {
7
+ if (registry.some((s) => s.name === step.name))
8
+ throw new Error(`startup migration "${step.name}" is already registered`);
9
+ registry.push(step);
10
+ }
11
+ /** Run every registered step in registration order. A step that moved something logs one
12
+ * `state_migrated` event; a step that throws logs one warning, the rest still run, and the failed
13
+ * step retries at the next start. A second call moves and logs nothing. */
14
+ export async function runStartupMigrations(root) {
15
+ for (const step of registry) {
16
+ try {
17
+ const notes = await step.run(root);
18
+ if (notes.length > 0) {
19
+ logEvent(root, {
20
+ source: TUMWATER_SOURCE,
21
+ type: "state_migrated",
22
+ step: step.name,
23
+ moved: notes.length,
24
+ detail: notes.join("; "),
25
+ });
26
+ }
27
+ }
28
+ catch (err) {
29
+ warnEventBestEffort(root, TUMWATER_SOURCE, `startup migration ${step.name} failed: ${errorMessage(err)}`);
30
+ }
31
+ }
32
+ }
33
+ /** The CLI-reader form: runs the steps only when no orchestrator is alive for `root`, so a CLI
34
+ * process never moves files under a running team, which may still be the old build. A live team
35
+ * has already migrated at its own start. */
36
+ export async function runStartupMigrationsIfIdle(root) {
37
+ if (orchestratorAlive(root))
38
+ return;
39
+ await runStartupMigrations(root);
40
+ }
package/dist/src/paths.js CHANGED
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import path from "node:path";
2
3
  import { DIRECTOR_ROLE } from "./roles/roles.js";
3
4
  /** All harness runtime state lives under <repo>/.tumwater (gitignored). */
@@ -17,13 +18,13 @@ export function briefCandidates(root) {
17
18
  function tumwaterDir(root) {
18
19
  return path.join(root, STATE_DIR);
19
20
  }
20
- /** The repo's tumwater.json — the fleet config (config.ts). */
21
+ /** The repo's tumwater.json — the team config (config.ts). */
21
22
  export function configPath(root) {
22
23
  return path.join(root, CONFIG_BASENAME);
23
24
  }
24
25
  /** The cross-process lock serializing read-modify-write updates of tumwater.json
25
26
  * (writeConfigMutation and applyConfigRequest in config/config-write.ts): the CLI's
26
- * `tumwater config set`, a standalone `tumwater gui` server, and the running fleet are separate
27
+ * `tumwater config set`, a standalone `tumwater gui` server, and the running team are separate
27
28
  * processes that can rewrite one config in the same instant, and without serialization each
28
29
  * one's whole-file write silently drops the others' change. A lock directory owned by the
29
30
  * shared mkdir-and-pid mutex (withSyncLock, src/concurrency/lock.ts), which creates and removes
@@ -33,10 +34,11 @@ export function configLockPath(root) {
33
34
  }
34
35
  /** The cross-process lock serializing writes to the primary checkout's tracked backlog files:
35
36
  * `tumwater bug`/`plan` (backlog-write.ts's appendEntry) and `tumwater questions answer`
36
- * (question-commands.ts's answerQuestion) each read a whole backlog file and rewrite it, while a
37
- * landing's working-tree fast-forward (landing-git.ts's ffMainTo) updates those same files in
37
+ * (backlog/question-answer.ts's tryAnswerQuestion) each read a whole backlog file and rewrite
38
+ * it, while a merge's working-tree fast-forward (merge-git.ts's ffMainTo) updates those
39
+ * same files in
38
40
  * the same checkout by `git merge --ff-only`. Without a shared lock the CLI's stale read can
39
- * overwrite the landed change, and vice versa. Same mkdir-and-pid mutex as configLockPath, under
41
+ * overwrite the merged change, and vice versa. Same mkdir-and-pid mutex as configLockPath, under
40
42
  * .tumwater/state so it never enters the tracked tree; its critical sections are one file read
41
43
  * plus one atomic rename, short enough to exclude a merge without stalling the build re-check. */
42
44
  export function primaryWorktreeLockPath(root) {
@@ -54,7 +56,7 @@ export function exampleConfigPath(root) {
54
56
  export function configRequestPath(wt) {
55
57
  return path.join(wt, ".tumwater-config-request.json");
56
58
  }
57
- /** The directory every harness worktree lives under — role, lander and mirror alike. The
59
+ /** The directory every harness worktree lives under — role, merger and mirror alike. The
58
60
  * single home of the tumwaterDir + worktrees pair: every worktree path helper below joins
59
61
  * its leaf through this, so the layout moves in one place. */
60
62
  export function worktreesDir(root) {
@@ -91,13 +93,19 @@ export function worktreeUseLockPath(root) {
91
93
  return path.join(tumwaterDir(root), "state", "worktree-use.lock");
92
94
  }
93
95
  /** The orchestrator's own info file (its pid, for liveness checks —
94
- * fleet/orchestrator-info.ts). */
96
+ * team/orchestrator-info.ts). */
95
97
  export function orchestratorStatePath(root) {
96
98
  return path.join(tumwaterDir(root), "state", "orchestrator.json");
97
99
  }
100
+ /** The doctor's image-probe verdict cache (src/doctor/image-probe.ts): how each claimed
101
+ * image-input model actually answered a live probe, keyed agent/provider/model. Runtime state
102
+ * under .tumwater/state/, never committed. */
103
+ export function imageProbesPath(root) {
104
+ return path.join(tumwaterDir(root), "state", "image-probes.json");
105
+ }
98
106
  /** The `qa` flow-coverage ledger (tick/qa-coverage.ts): which flow each observer tick exercised
99
107
  * and how it went. Runtime state under .tumwater/state/, never committed — a passing check
100
- * must leave the repo untouched, or every cheap pass would move main and wake the fleet. */
108
+ * must leave the repo untouched, or every cheap pass would move main and wake the team. */
101
109
  export function qaCoveragePath(root) {
102
110
  return path.join(tumwaterDir(root), "state", "qa-coverage.json");
103
111
  }
@@ -108,12 +116,17 @@ export function qaCoveragePath(root) {
108
116
  export function roleNotesPath(root, role) {
109
117
  return path.join(tumwaterDir(root), "state", "notes", `${role}.md`);
110
118
  }
111
- /** The in-flight landing marker (merge queue 4/5): the orchestrator's drain task
112
- * writes it (landing-slot.ts, merge queue 4/5) and removes it after every outcome — the observers
119
+ /** The in-flight merge marker (merge queue 4/5): the orchestrator's drain task
120
+ * writes it (merge-slot.ts, merge queue 4/5) and removes it after every outcome — the observers
113
121
  * (`status`, TUI, GUI) are separate processes that cannot see the drain's in-memory promise,
114
122
  * but they can read this file (plans/merge-queue.md). */
115
- export function landingStatePath(root) {
116
- return path.join(tumwaterDir(root), "state", "landing.json");
123
+ export function mergeStatePath(root) {
124
+ return path.join(tumwaterDir(root), "state", "merging.json");
125
+ }
126
+ /** The merge marker's old file, deleted by the startup migration (the marker is display-only,
127
+ * so a previous generation's is stale and is not read). */
128
+ export function legacyLandingStatePath(root) {
129
+ return path.join(tumwaterDir(root), "state", "landing.json"); // vocabulary-ok: legacy path read from old layouts
117
130
  }
118
131
  /** The last completed auto-restart's timestamp (epoch ms) — unlike orchestrator.json it must
119
132
  * survive the process exit that IS the restart, so it lives in its own file and is never removed
@@ -121,17 +134,18 @@ export function landingStatePath(root) {
121
134
  export function autoRestartStampPath(root) {
122
135
  return path.join(tumwaterDir(root), "state", "auto-restart.json");
123
136
  }
124
- /** The green main-baseline verdicts the fleet keeps across restarts (baseline/main-baseline.ts's
125
- * persistBaselineVerdicts): SHAs whose suite passed, so a new generation does not re-verify them. */
137
+ /** The green main-baseline verdicts the team keeps across restarts (baseline/main-baseline.ts's
138
+ * persistBaselineVerdicts): SHAs whose suite passed, so a new generation does not re-verify
139
+ * them. */
126
140
  export function baselineVerdictsPath(root) {
127
141
  return path.join(tumwaterDir(root), "state", "baseline-verdicts.json");
128
142
  }
129
- /** Marker file `tumwater reset-counters` drops for a running fleet to consume (it must also
143
+ /** Marker file `tumwater reset-counters` drops for a running team to consume (it must also
130
144
  * zero the runners' in-memory counters, or their next save resurrects the old values). */
131
145
  export function resetRequestPath(root) {
132
146
  return path.join(tumwaterDir(root), "reset-counters.json");
133
147
  }
134
- /** Marker file `tumwater wake [--role <id>]` drops for a running fleet to consume: like the
148
+ /** Marker file `tumwater wake [--role <id>]` drops for a running team to consume: like the
135
149
  * reset-counters marker it must name its targets because it can affect many loops, and like it
136
150
  * it must reach the runners' IN-MEMORY schedule — the poll loop reads eligibility from the
137
151
  * in-memory state, so clearing only the on-disk file would not wake anything. */
@@ -145,43 +159,49 @@ export function scheduledWakeDir(root) {
145
159
  return path.join(tumwaterDir(root), "wakes");
146
160
  }
147
161
  /** Marker file the dashboard's restart button (the build-stale alert's refresh icon) drops for
148
- * a running fleet to consume: force the pending self-redeploy past its cooldown now. Presence
162
+ * a running team to consume: force the pending self-redeploy past its cooldown now. Presence
149
163
  * is the request; the content is just `{ at }` — like the abort markers, one untyped flag. */
150
164
  export function restartRequestPath(root) {
151
165
  return path.join(tumwaterDir(root), "restart.json");
152
166
  }
153
- /** Marker file `tumwater reclaim` drops for a running fleet to consume: presence means "run
167
+ /** Marker file `tumwater reclaim` drops for a running team to consume: presence means "run
154
168
  * one manual reclaim pass over every candidate." Like the restart marker, presence is the
155
- * request and the content is just `{ at }` — one untyped flag. A fleet that is not running
169
+ * request and the content is just `{ at }` — one untyped flag. A team that is not running
156
170
  * cannot consume it, so `cmdReclaim` runs the pass in-process instead. */
157
171
  export function reclaimRequestPath(root) {
158
172
  return path.join(tumwaterDir(root), "reclaim.json");
159
173
  }
160
- /** Per-role marker file `tumwater abort --role <id>` drops for a running fleet to consume:
174
+ /** Per-role marker file `tumwater abort --role <id>` drops for a running team to consume:
161
175
  * kill that loop's in-flight tick. One file per role (presence = pending request; content is
162
176
  * just `{ at }`) keeps consumption race-free and needs no parsing — unlike the single shared
163
177
  * reset-counters marker, which must name its targets because it affects many loops. */
164
178
  export function abortRequestPath(root, role) {
165
179
  return path.join(tumwaterDir(root), `abort-${role}.json`);
166
180
  }
167
- /** Persistent operator-intent marker for `tumwater pause`: its presence means "the fleet is
181
+ /** Persistent operator-intent marker for `tumwater pause`: its presence means "the team is
168
182
  * paused" until `resume` removes it — unlike the abort/reset markers above, which are one-shot
169
183
  * requests consumed on pickup. Content is `{ at }` (the pause timestamp), read for display
170
184
  * only, never required. */
171
185
  export function pausedPath(root) {
172
186
  return path.join(tumwaterDir(root), "paused.json");
173
187
  }
188
+ /** Operator-intent marker for `tumwater stop --in <duration>`: `{ at, until }` in epoch ms. The
189
+ * running team's watch consumes it when `until` passes; a fresh `tumwater run` clears a
190
+ * stale one. */
191
+ export function scheduledStopPath(root) {
192
+ return path.join(tumwaterDir(root), "stop-at.json");
193
+ }
174
194
  /** Persistent operator-intent marker for `tumwater pause --role <id>` / `resume --role <id>`:
175
195
  * the set of individually paused roles, `{ roles: string[], at: number }` — the same
176
- * presence-means-paused persistence as the fleet marker above, so pausing a role before
196
+ * presence-means-paused persistence as the team marker above, so pausing a role before
177
197
  * startup starts it paused. Under state/ beside the orchestrator info file: unlike the
178
- * fleet marker it is structured state several writers rewrite (the orchestrator never does,
198
+ * team marker it is structured state several writers rewrite (the orchestrator never does,
179
199
  * but the shape is atomic-json state, not a presence flag). */
180
200
  export function pausedRolesPath(root) {
181
201
  return path.join(tumwaterDir(root), "state", "paused-roles.json");
182
202
  }
183
203
  /** The cross-process lock serializing read-modify-write updates of the paused-roles marker
184
- * (pauseRole/resumeRole in fleet/fleet-state.ts): the CLI and the GUI server are separate processes
204
+ * (pauseRole/resumeRole in team/team-state.ts): the CLI and the GUI server are separate processes
185
205
  * that can toggle different roles in the same instant, and without serialization the last
186
206
  * writer's whole-set overwrite silently drops the other's pause. A lock directory owned by the
187
207
  * shared mkdir-and-pid mutex (withSyncLock, src/concurrency/lock.ts), which creates and removes it;
@@ -231,6 +251,66 @@ export function sessionsRootDir(root) {
231
251
  export function sessionDir(root, role) {
232
252
  return path.join(sessionsRootDir(root), role);
233
253
  }
254
+ /** `sessionDir`'s real team root — the nearest ancestor holding a `.tumwater` directory — or
255
+ * undefined when none does. run-agent installs the notes shim from THIS (never from teamRootOf,
256
+ * whose no-ancestor fallback can be a shared directory): writing `.tumwater` at the fallback
257
+ * would mint a phantom harness root every other suite on the host then found — BUGS.md
258
+ * 2026-10-18's `$TMPDIR/.tumwater/bin/tumwater-notes`. */
259
+ export function nearestTumwaterRoot(sessionDir) {
260
+ let dir = sessionDir;
261
+ for (let i = 0; i < 64; i += 1) {
262
+ let tumwater;
263
+ try {
264
+ tumwater = fs.statSync(path.join(dir, ".tumwater"));
265
+ }
266
+ catch {
267
+ tumwater = undefined;
268
+ }
269
+ if (tumwater?.isDirectory())
270
+ return dir;
271
+ const parent = path.dirname(dir);
272
+ if (parent === dir)
273
+ break;
274
+ dir = parent;
275
+ }
276
+ return undefined;
277
+ }
278
+ /** The team root that owns `sessionDir`: the `.tumwater` directory that contains it. A run's
279
+ * session dir is always `<root>/.tumwater/sessions/…`, so the first ancestor holding a
280
+ * `.tumwater` directory is the root; opencodeDbPath reads back to that same `.tumwater` for
281
+ * the run's shared database. The no-ancestor fallback — a directory two levels up — is
282
+ * unreachable in a harness run and used only by OpenCode's harness-only read-back lookups
283
+ * (opencodeDbPath, loadConfigSafe), which always see a real root. Never use this to decide
284
+ * where to WRITE a `.tumwater`: the fallback can be a shared directory, and creating `.tumwater`
285
+ * there mints a phantom root (BUGS.md 2026-10-18); run-agent's shim install uses
286
+ * nearestTumwaterRoot instead. Moved here from the opencode adapter (plans/notes-command.md) so
287
+ * run-agent's shim install — which every backend's run needs — shares the one lookup instead of
288
+ * the opencode adapter keeping it private. */
289
+ export function teamRootOf(sessionDir) {
290
+ return nearestTumwaterRoot(sessionDir) ?? path.dirname(path.dirname(sessionDir));
291
+ }
292
+ /** The directory the notes shim lands in, under a team root: `<root>/.tumwater/bin`.
293
+ * run-agent installs the `tumwater-notes` command there (src/agent/notes-shim.ts) and
294
+ * run-marker's agentBaseEnv prepends it to every run's PATH, so a run on any backend finds
295
+ * the command (plans/notes-command.md "Reaching it from a run"). */
296
+ export function notesShimDir(root) {
297
+ return path.join(tumwaterDir(root), "bin");
298
+ }
299
+ /** The shared OpenCode session database for a team root (plans/opencode-backend.md §4): one
300
+ * file per root, under the same `.tumwater` the run's session dir lives in, so the 6.5 MB
301
+ * models catalog is downloaded once and six concurrent runs on one file work. The opencode
302
+ * adapter derives `root` from a run's session dir (the `.tumwater` that contains it) and
303
+ * points `OPENCODE_DB` here. */
304
+ export function opencodeDbPath(root) {
305
+ return path.join(tumwaterDir(root), "opencode", "opencode.db");
306
+ }
307
+ /** tumwater's cached models.dev catalog for OpenCode (plans/opencode-backend.md §7; part
308
+ * 6a/7 opencode-catalog.ts): the providers the config routes through OpenCode, priced from
309
+ * models.dev's `api.json` at most once a day. Runtime state under .tumwater/state/, never
310
+ * committed. */
311
+ export function opencodeModelsPath(root) {
312
+ return path.join(tumwaterDir(root), "state", "opencode-models.json");
313
+ }
234
314
  /** Session dir for a role's review-gate runs. Kept OUTSIDE the role's own session dir on
235
315
  * purpose: hasResumableSession (the resume-after-shutdown guard) must only ever see the
236
316
  * AUTHOR's sessions — a leftover reviewer session would make an interrupted tick "resume"
@@ -256,15 +336,20 @@ export function inboxDir(root) {
256
336
  export function roleInboxDir(root, role) {
257
337
  return role === DIRECTOR_ROLE ? inboxDir(root) : path.join(inboxDir(root), role);
258
338
  }
259
- /** The durable land queue (src/landing/landing-queue.ts): a changed tick's pinned commit waits here
260
- * as one JSON file for the orchestrator's landing pipeline (plans/merge-queue.md 3/5, land-queue
339
+ /** The durable merge queue (src/merge/merge-queue.ts): a changed tick's pinned commit waits here
340
+ * as one JSON file for the orchestrator's merge pipeline (plans/merge-queue.md 3/5, land-queue
261
341
  * speed 2c). Like the inbox it is a directory of timestamped files —
262
342
  * a crash between enqueue and drop loses nothing, and `tumwater status` can read it without the
263
343
  * scheduler. */
264
- export function landQueueDir(root) {
265
- return path.join(tumwaterDir(root), "land-queue");
344
+ export function mergeQueueDir(root) {
345
+ return path.join(tumwaterDir(root), "merge-queue");
346
+ }
347
+ /** The merge queue's old directory, still read by observers until the startup migration
348
+ * (src/orchestrator/startup-migration-steps.ts) moves its files into `mergeQueueDir`. */
349
+ export function legacyLandQueueDir(root) {
350
+ return path.join(tumwaterDir(root), "land-queue"); // vocabulary-ok: legacy path read from old layouts
266
351
  }
267
- /** The lock dir serializing merges to main (landing-merge.ts; doctor checks it). */
352
+ /** The lock dir serializing merges to main (merge-to-main.ts; doctor checks it). */
268
353
  export function mergeLockDir(root) {
269
354
  return path.join(tumwaterDir(root), "merge.lock");
270
355
  }
@@ -284,19 +369,19 @@ export function gateMainWorktreePath(root) {
284
369
  }
285
370
  /** Detached worktree pinned at the RUNNING build's own SHA for the redeployer's urgency
286
371
  * carve-out (redeploy.ts's buildRed): the baseline check that decides whether the build the
287
- * fleet is executing is red runs here — its own checkout, not the redeploy mirror above, so a
372
+ * team is executing is red runs here — its own checkout, not the redeploy mirror above, so a
288
373
  * witness check at an old SHA can never repoint the mirror a green check or compile is using.
289
374
  * The leading underscore follows the _main convention, so it can never collide with a role. */
290
375
  export function witnessWorktreePath(root) {
291
376
  return path.join(worktreesDir(root), "_build");
292
377
  }
293
378
  /** One slot of the worktree pool (plans/worktree-pool.md): a fixed `.tumwater/worktrees/_slot-<n>`
294
- * checkout that ticks and vets lease and switch commits in place, instead of one `_land-<role>`
379
+ * checkout that ticks and verifies lease and switch commits in place, instead of one `_land-<role>`
295
380
  * per role. The leading underscore keeps it from colliding with a role worktree. */
296
381
  export function slotWorktreePath(root, n) {
297
382
  return path.join(worktreesDir(root), `_slot-${n}`);
298
383
  }
299
- /** The worktree pool's dedicated checkout for the landing drain's merge-side work
384
+ /** The worktree pool's dedicated checkout for the merge drain's merge-side work
300
385
  * (plans/worktree-pool.md, "Layout"): a single change, a stack and its bisect, the conflict
301
386
  * resolver and the post-resolve re-review. Kept out of the pool so a permit-waiting lease can
302
387
  * never deadlock behind the merge lock, and the leading underscore keeps it from colliding with
@@ -316,15 +401,21 @@ export function slotsStatePath(root) {
316
401
  export function slotsLockPath(root) {
317
402
  return path.join(tumwaterDir(root), "state", "slots.lock");
318
403
  }
319
- /** The ref pinning a role's committed-but-unlanded sha (plans/merge-queue.md invariant 4):
404
+ /** The ref pinning a role's committed-but-not yet merged sha (plans/merge-queue.md invariant 4):
320
405
  * written right after the tick's commit, before its branch resets to main, so the reset cannot
321
- * orphan the work. Deleted on every terminal landing outcome; kept until re-landed otherwise. */
322
- export function landingRefName(role) {
323
- return `refs/tumwater/landing/${role}`;
406
+ * orphan the work. Deleted on every terminal merge outcome; kept until re-merged otherwise. */
407
+ export function mergeRefName(role) {
408
+ return `refs/tumwater/merge/${role}`;
409
+ }
410
+ /** The pre-rename pin name (`refs/tumwater/landing/<role>`): the startup step moves these to
411
+ * mergeRefName, and retire still reads and deletes it for a team stopped before its first
412
+ * start on this build. */
413
+ export function legacyLandingRefName(role) {
414
+ return `refs/tumwater/landing/${role}`; // vocabulary-ok: legacy name read from old refs or commits
324
415
  }
325
416
  /** The ref naming a role's rejected change while a revision is due (plans/revise-rejected.md):
326
417
  * written at the gate's rejection, read by the author's next tick to re-apply the rejected diff,
327
- * and deleted once the revision lands, is dropped, conflicts, or exhausts its rounds. Keeping
418
+ * and deleted once the revision merges, is dropped, conflicts, or exhausts its rounds. Keeping
328
419
  * the head reachable here stops git gc from collecting the one commit the revision re-applies. */
329
420
  export function rejectedRefName(role) {
330
421
  return `refs/tumwater/rejected/${role}`;
@@ -8,11 +8,27 @@ import { fileURLToPath } from "node:url";
8
8
  * location so staged builds (.tumwater/build/<sha>) load their own copies: bounded-output caps
9
9
  * oversized tool results, context-shake then reclaims old bulky results at 70% of the window,
10
10
  * context-budget appends its context-usage note to the (already bounded) result that crosses a
11
- * threshold, and role-notes registers the `role_notes` tool (only when TUMWATER_NOTES_PATH
12
- * names a notebook — see pi-extension/role-notes.ts). */
11
+ * threshold, strip-thinking drops the model's replayed thinking from each request on a
12
+ * small-window chat-completions model, and trim-system-prompt drops pi's docs section from the
13
+ * system prompt. Notes ride the `tumwater-notes` command on
14
+ * every backend from part 2/4 on, so the role-notes extension is no longer bundled. */
13
15
  export function bundledExtensionPaths() {
14
- return ["bounded-output.js", "context-shake.js", "context-budget.js", "role-notes.js"].map((file) => fileURLToPath(new URL(`../pi-extension/${file}`, import.meta.url)));
16
+ return [
17
+ "bounded-output.js",
18
+ "context-shake.js",
19
+ "context-budget.js",
20
+ "strip-thinking.js",
21
+ "trim-system-prompt.js",
22
+ ].map((file) => fileURLToPath(new URL(`../pi-extension/${file}`, import.meta.url)));
15
23
  }
24
+ /** pi 1.2.0's built-in extensions other than `mcp`, read from pi's own `dist/extensions/index.js`.
25
+ * Team runs skip pi's auto-discovery with `--no-extensions`, then re-enable exactly these with an
26
+ * explicit `-e builtin:` — so an installed pi package, a `~/.pi/agent/extensions` file, a target
27
+ * repo's own `.pi/extensions`, and pi's built-in MCP extension (which reads `~/.pi/agent/mcp.json`)
28
+ * can never smuggle an operator MCP server (e.g. funes) into a team run, while behavior is
29
+ * otherwise unchanged. A pi bump that renames or adds a built-in is a visible diff here; doctor
30
+ * prints the same names so a package meant for the team does not vanish silently. */
31
+ export const PI_KEPT_BUILTINS = ["llama.cpp", "codemode", "tool-search"];
16
32
  /** Build the pi argv for one tick: the harness's own flags, the bundled extensions, then the
17
33
  * user's config.piArgs (so a user flag still wins). */
18
34
  export function piArgs(opts) {
@@ -33,6 +49,13 @@ export function piArgs(opts) {
33
49
  // tumwater pi extension") and tell the model how full its window is at fixed thresholds.
34
50
  // Loaded before config.piArgs so a user flag still wins.
35
51
  if (path.basename(opts.agentBin ?? "pi") === "pi") {
52
+ // Disable pi's discovered extensions (packages, ~/.pi/agent/extensions, a repo's own
53
+ // .pi/extensions, and the built-in MCP reader for ~/.pi/agent/mcp.json), then re-enable only
54
+ // the named built-ins so operator MCP servers stay out of team runs. Kept before tumwater's
55
+ // own `-e` extensions; config.piArgs still wins (appended last).
56
+ args.push("--no-extensions");
57
+ for (const builtin of PI_KEPT_BUILTINS)
58
+ args.push("-e", `builtin:${builtin}`);
36
59
  for (const ext of bundledExtensionPaths())
37
60
  args.push("-e", ext);
38
61
  }
@@ -5,8 +5,8 @@
5
5
  * backend-neutral. The claude adapter registers alongside it in src/agent/run-agent.ts. */
6
6
  import { piArgs } from "./pi-args.js";
7
7
  import { resolveBackendBin } from "./pi-bin.js";
8
- import { agentBaseEnv } from "../process/run-marker.js";
9
- import { isPermanentConfigError, matchBackendFailure } from "./pi-stream.js";
8
+ import { agentBaseEnv, withNotesPath } from "../process/run-marker.js";
9
+ import { isPermanentConfigError, matchBackendFailure } from "./pi-failure.js";
10
10
  import { hasResumableSession, sessionResumable } from "../agent/session-ledger.js";
11
11
  /** pi crashing on malformed JSON, as Node's JSON.parse phrases it on pi's stderr — five ticks in
12
12
  * the first 18 days died this way (one of them 2 h 39 m of director work on a fresh steering
@@ -18,13 +18,6 @@ export const TRANSIENT_PI_CRASH = /Unexpected end of JSON input|is not valid JSO
18
18
  * mispricing of BUGS.md 2026-10-06. Matched against the child's stderr at exit; exported for
19
19
  * tests. */
20
20
  export const MODEL_FALLBACK_CLONE = /not found for provider[^\n]*Using custom model id/i;
21
- /** `base` plus TUMWATER_NOTES_PATH when this run carries a notebook. Kept out of runMarkerEnv so
22
- * the two environment concerns — cross-group attribution and the notebook — stay separate. */
23
- function withNotesPath(base, notesPath) {
24
- if (!notesPath)
25
- return base;
26
- return { ...base, TUMWATER_NOTES_PATH: notesPath };
27
- }
28
21
  /** pi's identity translator: every native line is already a canonical line. */
29
22
  const identityTranslator = {
30
23
  translate: (line) => [line],
@@ -39,20 +32,19 @@ export const piBackend = {
39
32
  return resolveBackendBin(config, this);
40
33
  },
41
34
  invocation(run) {
42
- // The prompt rides as the last argv element (pi's own reading of a bare trailing arg), so
43
- // pi delivers no stdin — the runner leaves stdio[0] "ignore" exactly as before.
44
- const args = [
45
- ...piArgs({
46
- config: run.config,
47
- sessionDir: run.sessionDir,
48
- sessionName: run.sessionName,
49
- continueSession: run.continueSession,
50
- agentBin: run.agentBin,
51
- }),
52
- run.prompt,
53
- ];
54
- const env = withNotesPath(agentBaseEnv(run.runMarker), run.notesPath);
55
- return { args, env };
35
+ // The prompt rides on stdin, never argv: pi --print reads a piped stdin to EOF as the first
36
+ // message (trimmed), and the runner writes and closes it. As the last argv element it sat
37
+ // in every pi's `ps` command line, so one agent's `pkill -f "npm run test"` matched — and
38
+ // SIGTERMed — every pi run on the host whose prompt mentions the check (BUGS.md 2026-10-18).
39
+ const args = piArgs({
40
+ config: run.config,
41
+ sessionDir: run.sessionDir,
42
+ sessionName: run.sessionName,
43
+ continueSession: run.continueSession,
44
+ agentBin: run.agentBin,
45
+ });
46
+ const env = withNotesPath(agentBaseEnv(run.runMarker, undefined, run.binDir), run.notesPath);
47
+ return { args, stdin: run.prompt, env };
56
48
  },
57
49
  translator() {
58
50
  return identityTranslator;
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { resolvedAgentConfig } from "../config/config-views.js";
4
4
  import { envPath, findOnPath } from "../files/files.js";
5
5
  /** Which agent binary the harness spawns and where that value came from — the per-agent
6
- * resolution `TUMWATER_<ID>_BIN` → `agents.<id>.bin` (pi's legacy `agentBin` alias) → the
6
+ * resolution `TUMWATER_<ID>_BIN` → `backends.<id>.bin` (pi's legacy `agentBin` alias) → the
7
7
  * backends' defaults (plans/agent-backends.md "Config"; plans/portability.md §5/7), the
8
8
  * executability rule the preflight askers share, and the missing-binary wording they quote.
9
9
  * The spawn (src/agent/run-agent.ts) resolves through each backend's resolveBin; the startup
@@ -19,7 +19,7 @@ const PI_MISSING_MESSAGE = "pi not found on PATH — install it (https://github.
19
19
  * report. Exported so src/agent/run-agent.ts's spawn errors and doctor's checks quote the same
20
20
  * string. `binEnv` is the environment variable a backend reads (pi's TUMWATER_PI_BIN by
21
21
  * default; claude's adapter passes TUMWATER_CLAUDE_BIN), and `configKey` is the tumwater.json
22
- * key the "config" source names (pi's legacy `agentBin`, or `agents.<id>.bin`), so a claude
22
+ * key the "config" source names (pi's legacy `agentBin`, or `backends.<id>.bin`), so a claude
23
23
  * failure names its own var and key rather than pi's. */
24
24
  export function agentBinSourceLabel(source, binEnv = "TUMWATER_PI_BIN", configKey = "agentBin") {
25
25
  switch (source) {
@@ -45,7 +45,7 @@ export function piMissingMessage(resolved) {
45
45
  /** The missing-binary message for any agent's resolved binary, used by the startup gate's walk
46
46
  * over every enabled seam's agent. pi delegates to piMissingMessage so its pinned wording is
47
47
  * untouched; any other agent names itself, the resolved value, and where it came from, so a
48
- * wrong `agents.<id>.bin` or `TUMWATER_<ID>_BIN` never reads as a missing pi. */
48
+ * wrong `backends.<id>.bin` or `TUMWATER_<ID>_BIN` never reads as a missing pi. */
49
49
  export function agentMissingMessage(resolved, agent) {
50
50
  if (agent === "pi")
51
51
  return piMissingMessage(resolved);
@@ -61,14 +61,14 @@ function agentBinEnv(agent) {
61
61
  return `TUMWATER_${agent.toUpperCase()}_BIN`;
62
62
  }
63
63
  /** The tumwater.json key that names one agent's binary: pi's legacy `agentBin`, or the
64
- * per-agent `agents.<id>.bin`. Shared by the missing-binary and spawn-error wording so a
64
+ * per-agent `backends.<id>.bin`. Shared by the missing-binary and spawn-error wording so a
65
65
  * non-pi failure never names pi's key. */
66
66
  export function agentBinConfigKey(agent) {
67
- return agent === "pi" ? "agentBin" : `agents.${agent}.bin`;
67
+ return agent === "pi" ? "agentBin" : `backends.${agent}.bin`;
68
68
  }
69
69
  /** The source label for one agent's resolved binary, deriving the env var and config key from
70
70
  * the agent id when the caller has no backend defaults in hand. pi keeps its pinned legacy
71
- * wording; any other agent names its own `TUMWATER_<ID>_BIN` and `agents.<id>.bin`. One home
71
+ * wording; any other agent names its own `TUMWATER_<ID>_BIN` and `backends.<id>.bin`. One home
72
72
  * for the convention agentMissingMessage and doctor's ok detail both read. */
73
73
  export function agentBinSourceLabelFor(agent, source) {
74
74
  return agent === "pi"
@@ -79,13 +79,19 @@ export function agentBinSourceLabelFor(agent, source) {
79
79
  * the startup gate should refuse to boot and doctor should fail its check. This is the whole
80
80
  * executability rule in one place so the two askers cannot drift: a path-shaped value (already
81
81
  * normalized against the process cwd by resolveAgentBin) is tested directly with
82
- * accessSync(X_OK); a bare name resolves through PATH. `pathEnv` is injectable so tests need
83
- * no PATH mutation — the gate and doctor both default it to the process PATH. */
82
+ * accessSync(X_OK) and required to be a regular file — a directory passes X_OK, but can never
83
+ * be spawned, so it is rejected here with exactly the file requirement findOnPath's bare-name
84
+ * branch applies (a bin pointed at a directory would otherwise pass the preflight and die
85
+ * mid-run at the spawn instead of failing the gate with the "not an executable" message). A
86
+ * bare name resolves through PATH. `pathEnv` is injectable so tests need no PATH mutation —
87
+ * the gate and doctor both default it to the process PATH. */
84
88
  export function findAgentBinary(resolved, pathEnv = envPath()) {
85
89
  if (resolved.bin.includes("/")) {
86
90
  try {
87
- fs.accessSync(resolved.bin, fs.constants.X_OK);
88
- return resolved.bin;
91
+ fs.accessSync(resolved.bin, fs.constants.X_OK); // Directories pass X_OK; require a file.
92
+ if (fs.statSync(resolved.bin).isFile())
93
+ return resolved.bin;
94
+ return null;
89
95
  }
90
96
  catch {
91
97
  return null;
@@ -106,7 +112,7 @@ function absBin(value) {
106
112
  /** The precedence every backend shares, parameterized by its own env override and default
107
113
  * binary name: `<binEnv>` env → the configured entry's bin → `defaultBin`. An empty or
108
114
  * whitespace value falls through to the next source, so `TUMWATER_PI_BIN= tumwater run` cannot
109
- * wedge the fleet on a typo'd export. This is precedence only — resolvability is checked at the
115
+ * wedge the team on a typo'd export. This is precedence only — resolvability is checked at the
110
116
  * preflight sites (cmdRun and doctor), never here. Private: the backends and the gate resolve
111
117
  * through resolveAgentBin, its one caller. */
112
118
  function resolveBinFrom(binEnv, defaultBin, configured) {
@@ -119,7 +125,7 @@ function resolveBinFrom(binEnv, defaultBin, configured) {
119
125
  return { bin: defaultBin, source: "default" };
120
126
  }
121
127
  /** Resolve which binary one agent spawns: `TUMWATER_<ID>_BIN` → the effective
122
- * `agents.<id>.bin` (config-views.ts resolvedAgentConfig, so pi's legacy `agentBin` still
128
+ * `backends.<id>.bin` (config-views.ts resolvedAgentConfig, so pi's legacy `agentBin` still
123
129
  * names pi) → the agent's default. `agent` defaults to pi, the historical backend; the
124
130
  * backends pass their own id, binEnv and defaultBin, while the startup gate derives the
125
131
  * reserved agents' conventional env name and default binary when it walks every enabled seam. */
@@ -51,7 +51,7 @@ export function parsePiEventLine(line, types) {
51
51
  * empty-content update right after start, and only updates with real text prove the command is
52
52
  * alive — so stalled-tool-call tracking (src/pi/pi-watchdogs.ts's warning,
53
53
  * src/ui/progress-data.ts's flag) moves its clock on these alone: a content-free keepalive must
54
- * not mask a hang, exactly as message_update deltas cannot reset the quiet watchdog. */
54
+ * not mask a hang — the same rule messageUpdateHasContent applies to streaming deltas. */
55
55
  export function toolUpdateHasContent(partialResult) {
56
56
  if (!isJsonObject(partialResult))
57
57
  return false;
@@ -60,6 +60,22 @@ export function toolUpdateHasContent(partialResult) {
60
60
  return false;
61
61
  return content.some((block) => isJsonObject(block) && isNonBlankString(block.text));
62
62
  }
63
+ /** True when a `message_update` carries new assistant content — a non-blank
64
+ * `assistantMessageEvent.delta`. pi's protocol strips the cumulative message snapshot from
65
+ * these lines, so the delta is all they carry: a content-free one (a zombie stream's
66
+ * keepalive, or the pre-strip shape with no delta event at all) proves nothing and must not
67
+ * reset the hang watchdog, exactly as a content-free tool_execution_update must not
68
+ * (toolUpdateHasContent). A content-bearing delta IS real generation, and the quiet watchdog
69
+ * counts it: a long thinking block on a slow model emits nothing else for its whole duration,
70
+ * so ignoring deltas killed live runs as hung (BUGS.md 2026-10-17). */
71
+ export function messageUpdateHasContent(event) {
72
+ if (!isJsonObject(event))
73
+ return false;
74
+ const update = event.assistantMessageEvent;
75
+ if (!isJsonObject(update))
76
+ return false;
77
+ return isNonBlankString(update.delta);
78
+ }
63
79
  /** The call's full raw command text from a tool-execution event's args (bash-like tools name
64
80
  * it `command`, some `cmd`; other tools yield ""). The two open-call trackers
65
81
  * (src/pi/pi-watchdogs.ts's stall warning, src/ui/progress-data.ts's dashboard flag) store this