tumwater 0.4.2 → 0.4.4

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 (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -6,14 +6,14 @@
6
6
  * scope. */
7
7
  import { increment } from "../collections.js";
8
8
  import { DIRECTOR_ROLE, commitTier } from "../roles/roles.js";
9
+ import { HOUR_MS } from "../text/datetime.js";
9
10
  import { baseRoleOf, runnerEnabled, runnerPaused } from "../roles/runner-ids.js";
10
11
  import { hasOutstandingWork, saveRunnerStateBestEffort } from "../runner/runner-state.js";
11
12
  import { queuedMergeRoles } from "../merge/merge-queue.js";
12
13
  import { readMergeMarker, mergeChanges } from "../merge/merge-slot.js";
13
- import { isAfterMergeRole } from "../review/post-merge-review.js";
14
14
  import { aheadOfMainBranch, branchExists } from "../git/git.js";
15
15
  import { branchName } from "../paths.js";
16
- import { hasUnmergedPin } from "../runner/leftover.js";
16
+ import { hasUnmergedPin } from "../runner/runner-leftover.js";
17
17
  import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
18
18
  import { eligibleEntries } from "../backlog/backlog-eligibility.js";
19
19
  import { CLAIM_IDLE_MAX_MS, claimReleaseReason, heldKeys, listedKeys, } from "../scheduling/claims.js";
@@ -31,7 +31,7 @@ export function noteSkip(root, runner, reason) {
31
31
  * hasUnmergedPin director pin, the per-base claim-group build (with its claim-release loop,
32
32
  * which mutates runner state, persists and logs), and the writing count. */
33
33
  export async function buildSchedulingScope(ctx) {
34
- const { root, runners, mainBranch, merges, once, now, mainHead, userPaused, quietNow, pausedRoles, capPaused, maintenanceQuota, bootstrapHeld, budgetPausedRoles, probeRoles, reviewStrongPaused, afterMergeRoles, roleQuietHeld, heldProviders, windowHeldProviders, reviewHeld, roleProviders, openBugsNow, workBacklogOpen, planBacklogFull, holdForRestart, diskHeld, deferredDue, workMergedSince, } = ctx;
34
+ const { root, runners, mainBranch, merges, once, now, mainHead, userPaused, quietNow, pausedRoles, capPaused, maintenanceQuota, bootstrapHeld, budgetPausedRoles, probeRoles, roleQuietHeld, heldProviders, windowHeldProviders, roleProviders, openBugsNow, workBacklogOpen, planBacklogFull, holdForRestart, diskHeld, deferredDue, workMergedSince, } = ctx;
35
35
  // Merge-queue interlock data, listed ONCE per poll (was once per runner per poll — each
36
36
  // queuedMergeFiles() re-lists the merge-queue directory and re-stats every queued entry): the
37
37
  // check below only asks whether the runner's role has a queued entry. The queue changes
@@ -43,23 +43,22 @@ export async function buildSchedulingScope(ctx) {
43
43
  // clock, since its only other run is an operator prompt. A queued or in-flight pin is the
44
44
  // merge-queue interlock's to hold, so the leftover reason is not offered then.
45
45
  const pinnedDirector = !queuedRoles.has(DIRECTOR_ROLE) && (await hasUnmergedPin(root, DIRECTOR_ROLE));
46
- // Merge-queue interlock relief (parts 1-3/4): for a post-merge-review role — one whose own
47
- // config `review.afterMerge` names it — the queued/in-flight interlock is scoped to the stages
48
- // that actually race the author's branch. `interlockedHold` holds exactly the QUEUED post-merge
49
- // roles in a non-admissible stage: pre-approval (`verifying`, or queued but not yet picked up —
50
- // absent from the pipeline's approved/merge sets), conflict resolution (`resolve-wait`/
51
- // `resolving`, read from the live marker), or a branch already ahead of main (a prior relief
52
- // run's held commit). An approved-and-waiting or in-flight-merge queued change no longer parks
53
- // the author. Pre-merge-review roles keep the full interlock (their rejection must reach the
54
- // runner before it authors again), so they are never added here; scheduling-skip.ts skips a
55
- // queued role unless it is BOTH post-merge AND absent from interlockedHold.
46
+ // Merge-queue interlock relief (parts 1-3/4): the queued/in-flight interlock is scoped to
47
+ // the stages that actually race the author's branch. `interlockedHold` holds exactly the
48
+ // QUEUED roles in a non-admissible stage: pre-approval (`verifying`, or queued but not yet
49
+ // picked up — absent from the pipeline's approved/merge sets), conflict resolution
50
+ // (`resolve-wait`/`resolving`, read from the live marker), or a branch already ahead of main
51
+ // (a prior relief run's held commit). An approved-and-waiting or in-flight-merge queued
52
+ // change no longer parks the author. The relief applies team-wide — no role waits on a
53
+ // reviewer — so every queued role is held only while it is in interlockedHold;
54
+ // scheduling-skip.ts skips a queued role unless it is absent from that set.
56
55
  const interlockedHold = new Set();
57
56
  if (queuedRoles.size > 0) {
58
57
  const marker = readMergeMarker(root);
59
58
  const markerChanges = marker ? mergeChanges(marker) : [];
60
59
  for (const runner of runners) {
61
60
  const role = runner.role;
62
- if (!queuedRoles.has(role) || !isAfterMergeRole(runner.config, role))
61
+ if (!queuedRoles.has(role))
63
62
  continue;
64
63
  const inApproved = merges.approved.has(role);
65
64
  const inMerge = (merges.merge?.roles ?? []).includes(role);
@@ -133,7 +132,7 @@ export async function buildSchedulingScope(ctx) {
133
132
  const reason = released;
134
133
  logEventBestEffort(root, { source: r.role, type: "claim", action: "released", title, reason });
135
134
  if (reason === "stale") {
136
- warnEventBestEffort(root, r.role, `released "${title}", which a stopped instance was working on (idle ${Math.round(CLAIM_IDLE_MAX_MS / 3_600_000)}h)`);
135
+ warnEventBestEffort(root, r.role, `released "${title}", which a stopped instance was working on (idle ${Math.round(CLAIM_IDLE_MAX_MS / HOUR_MS)}h)`);
137
136
  }
138
137
  }
139
138
  claimGroups.set(base, {
@@ -151,15 +150,10 @@ export async function buildSchedulingScope(ctx) {
151
150
  // The per-role windows (quietHoursPerRole) fold in beside the team one as a fourth
152
151
  // disjunct — a role held by either window (or both, held once) starts no new runs;
153
152
  // the director stays exempt by the same role check that covers the other disjuncts.
154
- // The budget hold, per role (part 5c/8): a tier that resolved to pause holds its own roles,
155
- // and — with review on — an unresolvable strong tier holds every role a pre-merge review
156
- // gates (nothing could merge) — except a role on `review.afterMerge`, whose review runs
157
- // after merging, so it keeps authoring. The probe of a demoted pair pierces the hold for
158
- // EXACTLY the probed pair's tier's roles: the earlier draft's pair-blind pierce lifted the
159
- // pause team-wide on any due probe.
160
- const budgetHolds = (role) => (budgetPausedRoles.has(role) ||
161
- (reviewStrongPaused && !afterMergeRoles.has(baseRoleOf(role)))) &&
162
- !probeRoles.has(role);
153
+ // The budget hold, per role (part 5c/8): a tier that resolved to pause holds its own
154
+ // roles. The probe of a demoted pair pierces the hold for EXACTLY the probed pair's tier's
155
+ // roles: the earlier draft's pair-blind pierce lifted the pause team-wide on any due probe.
156
+ const budgetHolds = (role) => budgetPausedRoles.has(role) && !probeRoles.has(role);
163
157
  const operatorPauseBlocks = (role) => (userPaused || quietNow || roleQuietHeld.has(role) || budgetHolds(role)) &&
164
158
  role !== DIRECTOR_ROLE;
165
159
  // Fresh operator demand: a queued prompt or a wake newer than the role's last run end. The
@@ -213,12 +207,9 @@ export async function buildSchedulingScope(ctx) {
213
207
  bootstrapHeld,
214
208
  budgetPausedRoles,
215
209
  probeRoles,
216
- reviewStrongPaused,
217
- afterMergeRoles,
218
210
  roleQuietHeld,
219
211
  heldProviders,
220
212
  windowHeldProviders,
221
- reviewHeld,
222
213
  roleProviders,
223
214
  openBugsNow,
224
215
  workBacklogOpen,
@@ -1,11 +1,10 @@
1
1
  import { logEventBestEffort } from "../events/events.js";
2
2
  import { baseRoleOf, configuredInstances, runnerPaused } from "../roles/runner-ids.js";
3
3
  import { BUGFIX_ROLE, DIRECTOR_ROLE, commitTier } from "../roles/roles.js";
4
- import { isAfterMergeRole } from "../review/post-merge-review.js";
5
4
  import { deferRunReason, isEligible } from "../scheduling/scheduling.js";
6
5
  import { inboxSize } from "../inbox/inbox.js";
7
6
  import { freshlyWoken } from "../runner/runner-state.js";
8
- import { MERGE_CONFLICT_LIMIT } from "../runner/leftover.js";
7
+ import { MERGE_CONFLICT_LIMIT } from "../runner/runner-leftover.js";
9
8
  import { noteSkip } from "./scheduling-scope.js";
10
9
  /** Decide whether this runner is due this poll, running every skip gate and the once/deferral
11
10
  * bookkeeping against scheduling-scope.ts's scope. Returns skip:true when a gate declined the
@@ -14,7 +13,7 @@ import { noteSkip } from "./scheduling-scope.js";
14
13
  * deferredDue, quotaAdmitted, once-mode settlement — and writes skip reasons as its only
15
14
  * side effects. */
16
15
  export async function skipRunner(scope, runner) {
17
- const { root, once, now, mainHead, pausedRoles, capPaused, holdForRestart, diskHeld, windowHeldProviders, roleProviders, reviewHeld, afterMergeRoles, heldProviders, openBugsNow, workBacklogOpen, planBacklogFull, deferredDue, workMergedSince, queuedRoles, interlockedHold, pinnedDirector, claimGroups, writing, maintenanceQuota, maintenanceQuotaOverLine, bootstrapHeldNow, operatorPauseBlocks, } = scope;
16
+ const { root, once, now, mainHead, pausedRoles, capPaused, holdForRestart, diskHeld, windowHeldProviders, roleProviders, heldProviders, openBugsNow, workBacklogOpen, planBacklogFull, deferredDue, workMergedSince, queuedRoles, interlockedHold, pinnedDirector, claimGroups, writing, maintenanceQuota, maintenanceQuotaOverLine, bootstrapHeldNow, operatorPauseBlocks, } = scope;
18
17
  // The role's inbox size and operator demand, computed once per runner per poll: every
19
18
  // consumer below (the holds, isEligible's inbox count, the maintenance admission) shares
20
19
  // this one directory listing + per-file stat read instead of each re-scanning the queue.
@@ -73,10 +72,8 @@ export async function skipRunner(scope, runner) {
73
72
  // re-queue every poll until the repeated-failure pause stops it.
74
73
  if (runner.role === DIRECTOR_ROLE && windowHeldProviders.has(roleProviders.get(runner.role)))
75
74
  return { skip: true };
76
- if (runner.role !== DIRECTOR_ROLE &&
77
- (heldProviders.has(roleProviders.get(runner.role)) ||
78
- (reviewHeld && !afterMergeRoles.has(baseRoleOf(runner.role)))))
79
- return { skip: true }; // nor while ITS provider's failure storm holds — or the reviewer's, which a pre-merge review gates (a review.afterMerge role is exempt)
75
+ if (runner.role !== DIRECTOR_ROLE && heldProviders.has(roleProviders.get(runner.role)))
76
+ return { skip: true }; // nor while ITS provider's failure storm holds — the reviewer's hold parks only the post-merge review queue, never a role start
80
77
  // The role's OWN queue decides inbox due-ness: the director counts its historical
81
78
  // inbox, every other role its per-role queue (tumwater prompt --role <id>) — so a
82
79
  // queued prompt makes its role due by itself, no wake marker needed (isEligible).
@@ -113,14 +110,13 @@ export async function skipRunner(scope, runner) {
113
110
  // results — but the merge's outcome replaces it before the interlock lets the role back into
114
111
  // this pass), and skipping here saves that branch's workMergedSince git-range query for the
115
112
  // skipped role.
116
- // Interlock relief (parts 1-3/4): a post-merge-review role (its own config `review.afterMerge`
117
- // names it) with a queued change is held only while it is in a non-admissible stage — the
118
- // pre-approval/conflict/branch-ahead holds buildSchedulingScope put in `interlockedHold` — and
119
- // otherwise falls through to author (the merge no longer parks it). A pre-merge-review role,
120
- // and a post-merge role still in a held stage, keep the full interlock: their rejection must
121
- // reach the runner before it authors again.
113
+ // Interlock relief (parts 1-3/4): a queued role is held only while it is in a non-admissible
114
+ // stage — the pre-approval/conflict/branch-ahead holds buildSchedulingScope put in
115
+ // `interlockedHold` — and otherwise falls through to author (the merge no longer parks it).
116
+ // A role still in a held stage keeps the full interlock: its rejection must reach the runner
117
+ // before it authors again. The relief now applies team-wide: no role waits on a reviewer.
122
118
  if (queuedRoles.has(runner.role)) {
123
- if (!isAfterMergeRole(runner.config, runner.role) || interlockedHold.has(runner.role)) {
119
+ if (interlockedHold.has(runner.role)) {
124
120
  noteSkip(root, runner, undefined);
125
121
  return { skip: true };
126
122
  }
@@ -1,5 +1,5 @@
1
1
  import { logEvent, TUMWATER_SOURCE, warnEventBestEffort } from "../events/events.js";
2
- import { orchestratorAlive } from "../team/orchestrator-info.js";
2
+ import { orchestratorAlive } from "../team/team-orchestrator-info.js";
3
3
  import { errorMessage } from "../text/text.js";
4
4
  const registry = [];
5
5
  /** Append one step to the registry; a duplicate name throws. */
package/dist/src/paths.js CHANGED
@@ -36,7 +36,8 @@ export function configLockPath(root) {
36
36
  }
37
37
  /** The cross-process lock serializing writes to the primary checkout's tracked backlog files:
38
38
  * `tumwater bug`/`plan` (backlog-write.ts's appendEntry) and `tumwater questions answer`
39
- * (backlog/question-answer.ts's tryAnswerQuestion) each read a whole backlog file and rewrite
39
+ * (src/backlog/backlog-question-answer.ts's tryAnswerQuestion) each read a whole backlog file and
40
+ * rewrite
40
41
  * it, while a merge's working-tree fast-forward (merge-git.ts's ffMainTo) updates those
41
42
  * same files in
42
43
  * the same checkout by `git merge --ff-only`. Without a shared lock the CLI's stale read can
@@ -119,7 +120,7 @@ export function worktreeUseLockPath(root) {
119
120
  return path.join(tumwaterDir(root), "state", "worktree-use.lock");
120
121
  }
121
122
  /** The orchestrator's own info file (its pid, for liveness checks —
122
- * team/orchestrator-info.ts). */
123
+ * team/team-orchestrator-info.ts). */
123
124
  export function orchestratorStatePath(root) {
124
125
  return path.join(tumwaterDir(root), "state", "orchestrator.json");
125
126
  }
@@ -163,8 +164,8 @@ export function roleNotesPath(root, role) {
163
164
  export function mergeStatePath(root) {
164
165
  return path.join(tumwaterDir(root), "state", "merging.json");
165
166
  }
166
- /** Merged commits awaiting their review after merging (src/review/post-merge-review.ts), for roles
167
- * listed in `review.afterMerge`: `{ pending: PostReview[] }`. Runtime state, never committed. */
167
+ /** Merged commits awaiting their review after merging (src/review/post-merge-review.ts):
168
+ * `{ pending: PostReview[] }`. Runtime state, never committed. */
168
169
  export function postReviewPath(root) {
169
170
  return path.join(tumwaterDir(root), "state", "post-review.json");
170
171
  }
@@ -286,7 +287,7 @@ export function piLogPath(root, role) {
286
287
  /** A role's backend-native stdout log (.tumwater/log/<role>.<backend>.native.jsonl), written
287
288
  * beside the canonical raw log and kept for debugging only: no reader folds it, and the backend
288
289
  * is in the name because one role's runs may switch backends and the two dialects must not
289
- * mix. Written and rotated like the raw log (src/agent/run-agent.ts). */
290
+ * mix. Written and rotated like the raw log (src/agent/agent-run.ts). */
290
291
  export function nativeLogPath(root, role, backend) {
291
292
  return path.join(tumwaterDir(root), "log", `${role}.${backend}.native.jsonl`);
292
293
  }
@@ -307,7 +308,7 @@ export function sessionDir(root, role) {
307
308
  return path.join(sessionsRootDir(root), role);
308
309
  }
309
310
  /** `sessionDir`'s real team root — the nearest ancestor holding a `.tumwater` directory — or
310
- * undefined when none does. run-spawn installs the notes shim from THIS (never from teamRootOf,
311
+ * undefined when none does. agent-spawn installs the notes shim from THIS (never from teamRootOf,
311
312
  * whose no-ancestor fallback can be a shared directory): writing `.tumwater` at the fallback
312
313
  * would mint a phantom harness root every other suite on the host then found — BUGS.md
313
314
  * 2026-10-18's `$TMPDIR/.tumwater/bin/tumwater-notes`. */
@@ -341,15 +342,15 @@ export function owningTeamRoot(dir) {
341
342
  * unreachable in a harness run and used only by OpenCode's harness-only read-back lookups
342
343
  * (opencodeDbPath, loadConfigSafe), which always see a real root. Never use this to decide
343
344
  * where to WRITE a `.tumwater`: the fallback can be a shared directory, and creating `.tumwater`
344
- * there mints a phantom root (BUGS.md 2026-10-18); run-spawn's shim install uses
345
+ * there mints a phantom root (BUGS.md 2026-10-18); agent-spawn's shim install uses
345
346
  * nearestTumwaterRoot instead. Moved here from the opencode adapter (plans/notes-command.md) so
346
- * run-spawn's shim install — which every backend's run needs — shares the one lookup instead of
347
+ * agent-spawn's shim install — which every backend's run needs — shares the one lookup instead of
347
348
  * the opencode adapter keeping it private. */
348
349
  export function teamRootOf(sessionDir) {
349
350
  return nearestTumwaterRoot(sessionDir) ?? path.dirname(path.dirname(sessionDir));
350
351
  }
351
352
  /** The directory the notes shim lands in, under a team root: `<root>/.tumwater/bin`.
352
- * run-spawn installs the `tumwater-notes` command there (src/agent/notes-shim.ts) and
353
+ * agent-spawn installs the `tumwater-notes` command there (src/agent/notes-shim.ts) and
353
354
  * run-marker's backendBaseEnv prepends it to every run's PATH, so a run on any backend finds
354
355
  * the command (plans/notes-command.md "Reaching it from a run"). */
355
356
  export function notesShimDir(root) {
@@ -1,6 +1,6 @@
1
1
  /** Building the pi command line for one run — pure argv construction with no subprocess or
2
2
  * file I/O. Split out of pi.ts; the child-process integration (runPi) now lives in
3
- * src/agent/run-agent.ts and the run's result types in src/pi/pi-run-result.ts, while
3
+ * src/agent/agent-run.ts and the run's result types in src/pi/pi-run-result.ts, while
4
4
  * test/pi-args.test.ts pins how config and session choices shape the command line. */
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -10,7 +10,9 @@ import { fileURLToPath } from "node:url";
10
10
  * context-budget appends its context-usage note to the (already bounded) result that crosses a
11
11
  * threshold, strip-thinking drops the model's replayed thinking from each request on a
12
12
  * small-window chat-completions model, trim-system-prompt drops pi's docs section from the
13
- * system prompt, and write-guard refuses an edit or write outside the run's worktree. Notes ride
13
+ * system prompt, and write-guard refuses an edit or write outside the run's worktree, while
14
+ * kill-guard refuses a bash call that kills processes by pattern (pkill/killall and ps/pgrep fed
15
+ * into kill). Notes ride
14
16
  * the `tumwater-notes` command on
15
17
  * every backend from part 2/4 on, so the role-notes extension is no longer bundled. */
16
18
  export function bundledExtensionPaths() {
@@ -21,6 +23,7 @@ export function bundledExtensionPaths() {
21
23
  "strip-thinking.js",
22
24
  "trim-system-prompt.js",
23
25
  "write-guard.js",
26
+ "kill-guard.js",
24
27
  ].map((file) => fileURLToPath(new URL(`../pi-extension/${file}`, import.meta.url)));
25
28
  }
26
29
  /** pi 1.2.0's built-in extensions other than `mcp`, read from pi's own `dist/extensions/index.js`.
@@ -1,8 +1,8 @@
1
1
  /** The pi adapter for the agent seam (plans/agent-backends.md "The seam"). Every piece here
2
2
  * is today's pi-specific behavior moved behind the `AgentBackend` interface — argv via
3
3
  * pi-args, the identity translator (pi's event vocabulary IS the canonical stream), the
4
- * stderr exit rules, and the model-fallback-clone warning — so src/agent/run-agent.ts stays
5
- * backend-neutral. The claude adapter registers alongside it in src/agent/run-agent.ts. */
4
+ * stderr exit rules, and the model-fallback-clone warning — so src/agent/agent-run.ts stays
5
+ * backend-neutral. The claude adapter registers alongside it in src/agent/agent-run.ts. */
6
6
  import { piArgs } from "./pi-args.js";
7
7
  import { resolveBinForBackend } from "./pi-bin.js";
8
8
  import { backendRunEnv } from "../process/run-marker.js";
@@ -5,8 +5,8 @@ import { envPath, findOnPath, isExecutableFile } from "../files/files.js";
5
5
  * resolution `TUMWATER_<ID>_BIN` → `backends.<id>.bin` (pi's legacy `agentBin` alias) → the
6
6
  * backends' defaults (plans/agent-backends.md "Config"; plans/portability.md §5/7), the
7
7
  * executability rule the preflight askers share, and the missing-binary wording they quote.
8
- * The spawn (src/agent/run-spawn.ts) resolves through each backend's resolveBin; the startup
9
- * gate (gates/startup-gate.ts) walks every enabled seam's backend through the same resolution and
8
+ * The spawn (src/agent/agent-spawn.ts) resolves through each backend's resolveBin; the startup
9
+ * gate (gates/startup-gates.ts) walks every enabled seam's backend through the same resolution and
10
10
  * executability rule, and doctor (doctor/doctor-checks.ts) asks them for every backend in use
11
11
  * (backendsInUse, the startup gate's rule). Each surface quotes this module's wording, so the
12
12
  * executability verdict and the pi text cannot drift between them. Split out of
@@ -15,7 +15,7 @@ import { envPath, findOnPath, isExecutableFile } from "../files/files.js";
15
15
  * layer. */
16
16
  const PI_MISSING_MESSAGE = "pi not found on PATH — install it (https://github.com/badlogic/pi-mono) or add its bin directory to your PATH";
17
17
  /** The operator-facing name of each source, as it appears in failure text and doctor's
18
- * report. Exported so src/agent/run-settle.ts's spawn errors and doctor's checks quote the same
18
+ * report. Exported so src/agent/agent-settle.ts's spawn errors and doctor's checks quote the same
19
19
  * string. `binEnv` is the environment variable a backend reads (pi's TUMWATER_PI_BIN by
20
20
  * default; claude's adapter passes TUMWATER_CLAUDE_BIN), and `configKey` is the tumwater.json
21
21
  * key the "config" source names (pi's legacy `agentBin`, or `backends.<id>.bin`), so a claude
@@ -1,6 +1,6 @@
1
1
  /** Parsing one line of pi's JSONL event stream — pure string functions with no subprocess or file
2
2
  * I/O. Split out of pi.ts, whose child-process integration (runPi) now lives in
3
- * src/agent/run-agent.ts and whose PiStreamParser in pi-stream.ts folds those lines into a run
3
+ * src/agent/agent-run.ts and whose PiStreamParser in pi-stream.ts folds those lines into a run
4
4
  * result — because these are shared
5
5
  * by every observer that folds raw pi log lines into per-type state (src/ui/progress-data.ts's live
6
6
  * tail, transcript.ts's renderer), and those display modules should not import from the subprocess
@@ -76,6 +76,33 @@ export function messageUpdateHasContent(event) {
76
76
  return false;
77
77
  return isNonBlankString(update.delta);
78
78
  }
79
+ /** Fold one pi event into the pending-reply state and return the new state — the single
80
+ * definition both observers apply (see PendingReply). An assistant `message_start` opens a
81
+ * stretch stamped `now`, or counts one more attempt of an open one. A `message_update` whose
82
+ * delta carries content (`hasContent`: each observer decides for itself whether a delta is worth
83
+ * parsing) or an assistant `message_end` that did not fail closes it: the model answered. A failed
84
+ * or aborted end keeps it open, because pi's retry that follows is the same unanswered request. */
85
+ export function foldPendingReply(pending, event, { hasContent = false, now = Date.now() } = {}) {
86
+ const msg = event.message;
87
+ switch (event.type) {
88
+ case "message_start": {
89
+ if (msg?.role !== "assistant")
90
+ return pending;
91
+ if (pending)
92
+ return { ...pending, attempts: pending.attempts + 1 };
93
+ const target = [msg.provider, msg.model].filter(isNonBlankString).join("/");
94
+ return { since: now, target, attempts: 1 };
95
+ }
96
+ case "message_update":
97
+ return hasContent ? undefined : pending;
98
+ case "message_end":
99
+ if (msg?.role !== "assistant")
100
+ return pending;
101
+ return msg.stopReason === "error" || msg.stopReason === "aborted" ? pending : undefined;
102
+ default:
103
+ return pending;
104
+ }
105
+ }
79
106
  /** The call's full raw command text from a tool-execution event's args (bash-like tools name
80
107
  * it `command`, some `cmd`; other tools yield ""). The two open-call trackers
81
108
  * (src/pi/pi-watchdogs.ts's stall warning, src/ui/progress-data.ts's dashboard flag) store this
@@ -1,4 +1,5 @@
1
1
  import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
2
+ import { cachedByStat } from "../files/stat-cache.js";
2
3
  import { piListedModelsPath } from "../paths.js";
3
4
  /** The whitespace-split rows of the table `pi --list-models` prints, header first. Null unless
4
5
  * the first non-blank line is the `provider model …` header, so other text (a "No models
@@ -33,8 +34,20 @@ export function parsePiModelImages(stdout) {
33
34
  }
34
35
  return images;
35
36
  }
37
+ /** Per-poll cache of the parsed listing, keyed by file path: the status snapshot reads it once
38
+ * per poll through collectRoleRows (src/status/status-roles.ts) while the file changes only when
39
+ * doctor runs `pi --list-models` and rewrites it, so an unchanged file costs one stat per poll
40
+ * instead of a re-read plus JSON.parse. Any write invalidates via dev/ino/mtime/size
41
+ * (stat-cache.cachedByStat, same freshness check as the other polled files); a missing or
42
+ * malformed file yields null and is not cached, so a mid-edit broken file recovers on the next
43
+ * poll exactly like before. */
44
+ const listedCache = new Map();
36
45
  /** The saved listing, or null when the file is missing, torn, or has no `images` object. */
37
46
  export function readPiListedModels(root) {
47
+ return cachedByStat(listedCache, piListedModelsPath(root), // Keyed by path so distinct roots and test files never collide.
48
+ piListedModelsPath(root), () => readPiListedModelsUncached(root), (listed) => ({ ...listed, images: { ...listed.images } }));
49
+ }
50
+ function readPiListedModelsUncached(root) {
38
51
  const saved = readJsonFile(piListedModelsPath(root));
39
52
  const images = saved?.images;
40
53
  if (typeof images !== "object" || images === null || Array.isArray(images))
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { enabledRoleIds, loadConfigSafe } from "../config/config.js";
4
4
  import { configForRole, resolverConfig, resolvedBackendConfig, reviewConfig } from "../config/config-views.js";
5
5
  import { fallbackPair } from "../config/config-fallbacks.js";
6
- import { seamAgent } from "../config/model-selector.js";
6
+ import { seamAgent } from "../config/config-model-selector.js";
7
7
  import { cachedByStat } from "../files/stat-cache.js";
8
8
  import { isJsonObject } from "../files/json-object.js";
9
9
  import { readJsonFile } from "../files/json-files.js";
@@ -107,40 +107,54 @@ export function seamContextWindow(seam, projectRoot, modelsPath = piModelsPath()
107
107
  const backend = seamAgent(seam);
108
108
  switch (backend) {
109
109
  case "pi":
110
- let { provider, model } = seam;
111
- if (!provider || !model) {
112
- const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), projectRoot);
113
- provider = piDefault?.provider;
114
- model = piDefault?.model;
115
- }
116
- return declaredWindow(resolveModelDef(readPiProviders(modelsPath), provider, model)?.contextWindow);
110
+ return piSeamContextWindow(seam, projectRoot, modelsPath);
117
111
  case "opencode":
118
- // The models.dev cache (readOpencodeCache, an empty cache when the file is missing) holds
119
- // the window part 6a/7 parsed from `limit.context`, keyed by the gateway-qualified id the
120
- // run actually invokes (opencodeModelKey) — the same key and entry the free-ness check
121
- // prices by, so a seam's window never disagrees with the model it runs.
122
- if (seam.model === undefined)
123
- return null;
124
- return declaredWindow(readOpencodeCache(projectRoot).models[opencodeModelKey(seam.model)]?.context);
112
+ return opencodeSeamContextWindow(seam, projectRoot);
125
113
  case "codex":
126
- // The model catalog (codex-catalog.ts) holds the window part 8/9 parsed from
127
- // `context_window × effective_context_window_percent / 100`, read from the team's codex
128
- // home (backends.codex.configDir) at this project root — the same home the runs use. An
129
- // unknown model, an absent home or a missing catalog reads null (declaredWindow's safe
130
- // answer for an unknown window).
131
- if (seam.model === undefined)
132
- return null;
133
- {
134
- const { config } = loadConfigSafe(projectRoot);
135
- const home = codexHome(resolvedBackendConfig(config ?? {}, "codex"));
136
- if (home === undefined)
137
- return null;
138
- return declaredWindow(readCodexCatalog(home).get(seam.model)?.contextWindow);
139
- }
114
+ return codexSeamContextWindow(seam, projectRoot);
140
115
  default:
141
116
  return null;
142
117
  }
143
118
  }
119
+ /** The window a pi seam declares, or null when it cannot be known: reads the pair's
120
+ * `contextWindow` from pi's models.json, running on pi's saved default
121
+ * (readPiDefaultModel) when the seam names no provider/model — exactly the model pi will
122
+ * start on. An unresolvable pair or a missing/unusable window reads null. */
123
+ function piSeamContextWindow(seam, projectRoot, modelsPath) {
124
+ let { provider, model } = seam;
125
+ if (!provider || !model) {
126
+ const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), projectRoot);
127
+ provider = piDefault?.provider;
128
+ model = piDefault?.model;
129
+ }
130
+ return declaredWindow(resolveModelDef(readPiProviders(modelsPath), provider, model)?.contextWindow);
131
+ }
132
+ /** The window an opencode seam declares, or null when it cannot be known. The models.dev cache
133
+ * (readOpencodeCache, an empty cache when the file is missing) holds the window part 6a/7
134
+ * parsed from `limit.context`, keyed by the gateway-qualified id the run actually invokes
135
+ * (opencodeModelKey) — the same key and entry the free-ness check prices by, so a seam's
136
+ * window never disagrees with the model it runs. A missing cache, an unknown model or an
137
+ * unusable `context` reads null. */
138
+ function opencodeSeamContextWindow(seam, projectRoot) {
139
+ if (seam.model === undefined)
140
+ return null;
141
+ return declaredWindow(readOpencodeCache(projectRoot).models[opencodeModelKey(seam.model)]?.context);
142
+ }
143
+ /** The window a codex seam declares, or null when it cannot be known. The model catalog
144
+ * (codex-catalog.ts) holds the window part 8/9 parsed from
145
+ * `context_window × effective_context_window_percent / 100`, read from the team's codex home
146
+ * (backends.codex.configDir) at this project root — the same home the runs use. An unknown
147
+ * model, an absent home or a missing catalog reads null (declaredWindow's safe answer for an
148
+ * unknown window). */
149
+ function codexSeamContextWindow(seam, projectRoot) {
150
+ if (seam.model === undefined)
151
+ return null;
152
+ const { config } = loadConfigSafe(projectRoot);
153
+ const home = codexHome(resolvedBackendConfig(config ?? {}, "codex"));
154
+ if (home === undefined)
155
+ return null;
156
+ return declaredWindow(readCodexCatalog(home).get(seam.model)?.contextWindow);
157
+ }
144
158
  /** The resolved primary seam's declared context window for a role — `configForRole` resolves
145
159
  * the role's seam (the view the role page, status table/drawer and run history all print), then
146
160
  * `seamContextWindow` names the window that seam declares, or null when it cannot be known. The
@@ -50,37 +50,60 @@ export function readPiDefaultModel(settingsPath, projectRoot) {
50
50
  return cachedByStat(settingsCache, settingsPath, // Keyed by path so distinct roots and test files never collide.
51
51
  settingsPath, () => readPiDefaultModelUncached(settingsPath, projectRoot), (defaultModel) => ({ ...defaultModel }));
52
52
  }
53
- function readPiDefaultModelUncached(settingsPath, projectRoot) {
54
- // readJsonFile is the shared "missing or torn reads as no data" policy for a JSON object
55
- // file; the nested shapes get the same isJsonObject guard.
56
- const doc = readJsonFile(settingsPath);
57
- if (!doc)
58
- return null;
53
+ /** The saved default pair, or null when either half is missing or empty — pi uses a saved
54
+ * default only as a pair. */
55
+ function savedDefaultPair(doc) {
59
56
  const { defaultProvider, defaultModel } = doc;
60
57
  if (typeof defaultProvider !== "string" || defaultProvider === "")
61
58
  return null;
62
59
  if (typeof defaultModel !== "string" || defaultModel === "")
63
60
  return null;
64
- if (Array.isArray(doc.enabledModels) && doc.enabledModels.length > 0)
65
- return null;
61
+ return { provider: defaultProvider, model: defaultModel };
62
+ }
63
+ /** Whether pi starts on the first scoped model instead of the saved default — a pattern
64
+ * tumwater does not resolve, so the reader shows nothing. */
65
+ function startsWithScopedModels(doc) {
66
+ return Array.isArray(doc.enabledModels) && doc.enabledModels.length > 0;
67
+ }
68
+ /** Whether the project's own `.pi/settings.json` names any default key, which makes pi's final
69
+ * choice depend on whether it trusts the project — unknowable to tumwater, so show nothing. */
70
+ function projectOverridesSavedDefault(projectRoot) {
66
71
  // A project-local override that names any default key means pi's final choice depends on
67
72
  // whether it trusts the project — unknowable to tumwater, so show nothing.
68
73
  const proj = readJsonFile(path.join(projectRoot, ".pi", "settings.json"));
69
- if (proj) {
70
- for (const key of DEFAULT_KEYS) {
71
- if (proj[key] !== undefined)
72
- return null;
73
- }
74
+ if (!proj)
75
+ return false;
76
+ for (const key of DEFAULT_KEYS) {
77
+ if (proj[key] !== undefined)
78
+ return true;
74
79
  }
75
- const key = `${defaultProvider}/${defaultModel}`;
80
+ return false;
81
+ }
82
+ /** pi's thinking for the pair: `modelThinkingLevels[key]` first, else `defaultThinkingLevel`. */
83
+ function savedThinkingLevel(doc, key) {
76
84
  const levels = doc.modelThinkingLevels;
77
85
  const level = isJsonObject(levels) ? levels[key] : undefined;
78
- let thinking;
79
86
  if (typeof level === "string")
80
- thinking = level;
81
- else if (typeof doc.defaultThinkingLevel === "string")
82
- thinking = doc.defaultThinkingLevel;
87
+ return level;
88
+ if (typeof doc.defaultThinkingLevel === "string")
89
+ return doc.defaultThinkingLevel;
90
+ return undefined;
91
+ }
92
+ function readPiDefaultModelUncached(settingsPath, projectRoot) {
93
+ // readJsonFile is the shared "missing or torn reads as no data" policy for a JSON object
94
+ // file; the nested shapes get the same isJsonObject guard.
95
+ const doc = readJsonFile(settingsPath);
96
+ if (!doc)
97
+ return null;
98
+ const pair = savedDefaultPair(doc);
99
+ if (!pair)
100
+ return null;
101
+ if (startsWithScopedModels(doc))
102
+ return null;
103
+ if (projectOverridesSavedDefault(projectRoot))
104
+ return null;
105
+ const thinking = savedThinkingLevel(doc, `${pair.provider}/${pair.model}`);
83
106
  return thinking !== undefined
84
- ? { provider: defaultProvider, model: defaultModel, thinking }
85
- : { provider: defaultProvider, model: defaultModel };
107
+ ? { provider: pair.provider, model: pair.model, thinking }
108
+ : { provider: pair.provider, model: pair.model };
86
109
  }