tumwater 0.4.2 → 0.4.3

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 (305) 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} +25 -12
  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 +1 -8
  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 +9 -0
  14. package/dist/src/agent/codex/codex-backend.js +2 -2
  15. package/dist/src/agent/codex/codex-env.js +28 -15
  16. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  17. package/dist/src/agent/codex/codex-rollout.js +93 -56
  18. package/dist/src/agent/codex/codex-stream.js +1 -1
  19. package/dist/src/agent/env-scrub.js +18 -0
  20. package/dist/src/agent/image-input.js +68 -11
  21. package/dist/src/agent/notes-shim.js +2 -2
  22. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  23. package/dist/src/agent/opencode/opencode-catalog.js +50 -27
  24. package/dist/src/agent/opencode/opencode-config.js +32 -14
  25. package/dist/src/agent/opencode/opencode-env.js +12 -13
  26. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  27. package/dist/src/agent/session-ledger.js +1 -1
  28. package/dist/src/agent/subscription-account.js +3 -3
  29. package/dist/src/backlog/backlog-cli-view.js +81 -41
  30. package/dist/src/backlog/backlog-cli.js +57 -26
  31. package/dist/src/backlog/backlog-eligibility.js +58 -102
  32. package/dist/src/backlog/backlog-md.js +5 -5
  33. package/dist/src/backlog/backlog-parts.js +65 -27
  34. package/dist/src/backlog/backlog-release.js +12 -12
  35. package/dist/src/backlog/backlog-structure.js +114 -61
  36. package/dist/src/backlog/backlog.js +1 -1
  37. package/dist/src/baseline/main-baseline.js +148 -84
  38. package/dist/src/budget/budget-fallback-view.js +100 -243
  39. package/dist/src/budget/budget-wiring.js +29 -65
  40. package/dist/src/budget/fallback-breaker.js +2 -2
  41. package/dist/src/build/build-check-detect.js +43 -14
  42. package/dist/src/build/build-check-events.js +2 -2
  43. package/dist/src/build/build-check-scoped.js +40 -16
  44. package/dist/src/build/build-check.js +13 -5
  45. package/dist/src/cli/cli-command-args.js +89 -52
  46. package/dist/src/cli/cli-flag-specs.js +42 -22
  47. package/dist/src/cli/cli-init.js +34 -12
  48. package/dist/src/cli/cli-query-commands.js +1 -1
  49. package/dist/src/cli/cli-repo-gate.js +2 -2
  50. package/dist/src/cli/cli-start.js +91 -58
  51. package/dist/src/cli/completion.js +82 -46
  52. package/dist/src/cli/config-commands.js +82 -71
  53. package/dist/src/cli/help.js +4 -6
  54. package/dist/src/cli/log-commands.js +43 -23
  55. package/dist/src/cli/notify-command.js +2 -2
  56. package/dist/src/cli/question-commands.js +86 -60
  57. package/dist/src/cli.js +181 -135
  58. package/dist/src/collections.js +31 -16
  59. package/dist/src/concurrency/check-slots.js +70 -0
  60. package/dist/src/concurrency/lock.js +22 -0
  61. package/dist/src/concurrency/run-permit.js +49 -0
  62. package/dist/src/concurrency/semaphore.js +46 -250
  63. package/dist/src/config/config-editable-keys.js +4 -7
  64. package/dist/src/config/config-fallbacks.js +81 -101
  65. package/dist/src/config/config-field-checks.js +66 -37
  66. package/dist/src/config/config-keys.js +5 -20
  67. package/dist/src/config/config-live.js +4 -28
  68. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  69. package/dist/src/config/config-request.js +153 -102
  70. package/dist/src/config/config-schema.js +9 -11
  71. package/dist/src/config/config-selector-checks.js +1 -1
  72. package/dist/src/config/config-validation-agents.js +2 -2
  73. package/dist/src/config/config-validation-roles.js +4 -52
  74. package/dist/src/config/config-validation.js +4 -38
  75. package/dist/src/config/config-views.js +9 -34
  76. package/dist/src/config/config-write.js +6 -23
  77. package/dist/src/config/config.js +4 -5
  78. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  79. package/dist/src/doctor/doctor-backlog-plans.js +24 -24
  80. package/dist/src/doctor/doctor-backlog.js +7 -5
  81. package/dist/src/doctor/doctor-check-names.js +3 -6
  82. package/dist/src/doctor/doctor-checks.js +5 -5
  83. package/dist/src/doctor/doctor-claude-checks.js +44 -28
  84. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  85. package/dist/src/doctor/doctor-contention-checks.js +44 -112
  86. package/dist/src/doctor/doctor-fallback-checks.js +2 -2
  87. package/dist/src/doctor/doctor-holds.js +6 -45
  88. package/dist/src/doctor/doctor-install.js +1 -1
  89. package/dist/src/doctor/doctor-launch-services.js +1 -1
  90. package/dist/src/doctor/doctor-model-checks.js +178 -94
  91. package/dist/src/doctor/doctor-notify.js +2 -2
  92. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  93. package/dist/src/doctor/doctor-orphans.js +120 -20
  94. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  95. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  96. package/dist/src/doctor/doctor-principles.js +3 -3
  97. package/dist/src/doctor/doctor-project-checks.js +3 -3
  98. package/dist/src/doctor/doctor-role-notes.js +1 -1
  99. package/dist/src/doctor/doctor.js +8 -11
  100. package/dist/src/events/event-format.js +30 -54
  101. package/dist/src/events/event-labels.js +1 -5
  102. package/dist/src/events/event-legacy.js +1 -3
  103. package/dist/src/events/{notify.js → event-notify.js} +2 -2
  104. package/dist/src/events/event-read.js +1 -1
  105. package/dist/src/failure/failure-cluster.js +6 -6
  106. package/dist/src/failure/failure-data.js +8 -4
  107. package/dist/src/failure/failure-render.js +44 -34
  108. package/dist/src/failure/failure-state-change.js +4 -34
  109. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  110. package/dist/src/files/files.js +30 -0
  111. package/dist/src/files/json-object.js +8 -8
  112. package/dist/src/files/stat-cache.js +12 -8
  113. package/dist/src/gates/budget-gates.js +85 -84
  114. package/dist/src/gates/gate-polls.js +27 -68
  115. package/dist/src/gates/pause-gates.js +16 -2
  116. package/dist/src/gates/readiness.js +1 -1
  117. package/dist/src/gates/role-cap-gates.js +44 -35
  118. package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
  119. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  120. package/dist/src/git/git.js +75 -45
  121. package/dist/src/git/worktree-pool.js +3 -6
  122. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  123. package/dist/src/gui/gui-backlog-commands.js +4 -4
  124. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  125. package/dist/src/gui/gui-command.js +43 -24
  126. package/dist/src/gui/gui-history-endpoints.js +100 -52
  127. package/dist/src/gui/gui-operator-commands.js +22 -23
  128. package/dist/src/gui/gui-prompt-commands.js +23 -23
  129. package/dist/src/gui/gui-report-endpoints.js +1 -1
  130. package/dist/src/history/history-data.js +71 -44
  131. package/dist/src/history/history-queued.js +49 -27
  132. package/dist/src/history/history.js +51 -33
  133. package/dist/src/inbox/prompt-commands.js +59 -42
  134. package/dist/src/init/init.js +88 -40
  135. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  136. package/dist/src/merge/merge-core.js +1 -1
  137. package/dist/src/merge/merge-drain.js +8 -11
  138. package/dist/src/merge/merge-pipeline.js +6 -7
  139. package/dist/src/merge/merge-post-review-drain.js +10 -13
  140. package/dist/src/merge/merge-requeue.js +1 -1
  141. package/dist/src/merge/merge-slot.js +0 -8
  142. package/dist/src/merge/merge-verify.js +8 -18
  143. package/dist/src/notes-cli.js +52 -42
  144. package/dist/src/operator/operator-commands.js +76 -44
  145. package/dist/src/operator/operator-intent.js +3 -3
  146. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  147. package/dist/src/orchestrator/orchestrator-poll.js +27 -32
  148. package/dist/src/orchestrator/orchestrator.js +8 -21
  149. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  150. package/dist/src/orchestrator/scheduling-scope.js +1 -1
  151. package/dist/src/orchestrator/scheduling-skip.js +1 -1
  152. package/dist/src/orchestrator/startup-migrations.js +1 -1
  153. package/dist/src/paths.js +8 -7
  154. package/dist/src/pi/pi-args.js +5 -2
  155. package/dist/src/pi/pi-backend.js +2 -2
  156. package/dist/src/pi/pi-bin.js +3 -3
  157. package/dist/src/pi/pi-event-line.js +1 -1
  158. package/dist/src/pi/pi-models.js +43 -29
  159. package/dist/src/pi/pi-settings.js +43 -20
  160. package/dist/src/pi/pi-stream.js +32 -6
  161. package/dist/src/pi/pi-watchdogs.js +12 -5
  162. package/dist/src/pi/pi.js +2 -2
  163. package/dist/src/pi-extension/context-shake.js +32 -20
  164. package/dist/src/pi-extension/kill-guard.js +69 -0
  165. package/dist/src/process/process-group.js +49 -5
  166. package/dist/src/process/process.js +8 -8
  167. package/dist/src/process/run-marker.js +31 -13
  168. package/dist/src/process/supervisor.js +40 -28
  169. package/dist/src/prompt/dialect.js +1 -1
  170. package/dist/src/prompt/gate-notes.js +2 -2
  171. package/dist/src/prompt/notes-backlog.js +66 -37
  172. package/dist/src/prompt/principles.js +6 -4
  173. package/dist/src/prompt/prompt-followup.js +12 -4
  174. package/dist/src/prompt/prompt.js +88 -38
  175. package/dist/src/report/report-data.js +53 -37
  176. package/dist/src/report/report-fold.js +16 -9
  177. package/dist/src/report/report-render.js +21 -194
  178. package/dist/src/report/report-since-data.js +4 -3
  179. package/dist/src/report/report-since-render.js +173 -0
  180. package/dist/src/report/report-usage-fold.js +8 -8
  181. package/dist/src/report/report.js +46 -22
  182. package/dist/src/review/post-merge-review-render.js +9 -3
  183. package/dist/src/review/post-merge-review.js +1 -1
  184. package/dist/src/review/review-precheck.js +11 -1
  185. package/dist/src/review/review.js +40 -153
  186. package/dist/src/review/secret-leaks.js +26 -16
  187. package/dist/src/roles/role-catalog.js +11 -3
  188. package/dist/src/roles/role-guidance.js +16 -12
  189. package/dist/src/roles/role-render.js +1 -14
  190. package/dist/src/roles/role-view.js +9 -21
  191. package/dist/src/run/acceptance-greps.js +88 -45
  192. package/dist/src/run/run-apply.js +12 -6
  193. package/dist/src/run/run-detail-data.js +121 -68
  194. package/dist/src/run/run-detail-render.js +92 -35
  195. package/dist/src/run/run-detail.js +72 -41
  196. package/dist/src/run/run-prompt.js +63 -50
  197. package/dist/src/run/run-resume.js +76 -29
  198. package/dist/src/run/run-stage.js +135 -84
  199. package/dist/src/run/run-timing.js +8 -2
  200. package/dist/src/run/run-usage.js +31 -1
  201. package/dist/src/run/run-verdict.js +119 -50
  202. package/dist/src/run/stage-check.js +49 -32
  203. package/dist/src/run/stage-checks-shared.js +28 -0
  204. package/dist/src/run/stage-name-checks.js +111 -92
  205. package/dist/src/run/stage-path-checks.js +69 -42
  206. package/dist/src/run/stage-prose-checks.js +55 -40
  207. package/dist/src/runner/runner-outcome.js +6 -1
  208. package/dist/src/runner/runner-pi.js +3 -10
  209. package/dist/src/runner/runner-recovery.js +1 -1
  210. package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
  211. package/dist/src/runner/runner-run.js +97 -35
  212. package/dist/src/runner/runner-state-load.js +12 -9
  213. package/dist/src/runner/runner.js +41 -25
  214. package/dist/src/scheduling/claims.js +0 -4
  215. package/dist/src/scheduling/scheduling.js +53 -66
  216. package/dist/src/status/status-data.js +2 -2
  217. package/dist/src/status/status-role-notes.js +2 -2
  218. package/dist/src/status/status-roles.js +9 -17
  219. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  220. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  221. package/dist/src/team/team-hold.js +98 -65
  222. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
  223. package/dist/src/team/team-polls.js +28 -126
  224. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  225. package/dist/src/team/team-state.js +4 -13
  226. package/dist/src/text/markdown.js +1 -1
  227. package/dist/src/text/phrases.js +20 -15
  228. package/dist/src/text/text.js +3 -3
  229. package/dist/src/ui/alert-feeds.js +49 -73
  230. package/dist/src/ui/badges.js +3 -13
  231. package/dist/src/ui/gui/gui-client-boot.js +2 -2
  232. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  233. package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
  234. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  235. package/dist/src/ui/gui/gui-client-history.js +28 -14
  236. package/dist/src/ui/gui/gui-client-model.js +0 -12
  237. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  238. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  239. package/dist/src/ui/gui/gui-client-report.js +1 -1
  240. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  241. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  242. package/dist/src/ui/gui/gui-client-team.js +4 -4
  243. package/dist/src/ui/gui/gui-client.js +7 -1
  244. package/dist/src/ui/progress-data.js +53 -40
  245. package/dist/src/ui/run-progress-model.js +9 -2
  246. package/dist/src/ui/status-model.js +2 -24
  247. package/dist/src/ui/status-payload.js +3 -7
  248. package/dist/src/ui/status-render.js +5 -9
  249. package/dist/src/ui/team-alert-backlog.js +6 -6
  250. package/dist/src/ui/team-alert-status.js +9 -170
  251. package/dist/src/ui/team-alert-tail.js +19 -215
  252. package/dist/src/ui/team-alerts.js +7 -8
  253. package/dist/src/ui/transcript-run.js +27 -16
  254. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  255. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  256. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  257. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  258. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  259. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  260. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  261. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  262. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  263. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  264. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  265. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  266. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  267. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  268. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  269. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  270. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  271. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  272. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  273. package/dist/src/ui/tui/tui-editors.js +1 -7
  274. package/dist/src/ui/tui/tui-frame.js +182 -134
  275. package/dist/src/ui/tui/tui-guard.js +20 -0
  276. package/dist/src/ui/tui/tui-input.js +159 -92
  277. package/dist/src/ui/tui/tui-keymap.js +51 -29
  278. package/dist/src/ui/tui/tui-keys.js +1 -1
  279. package/dist/src/ui/tui/tui-pane-activity.js +3 -3
  280. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  281. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  282. package/dist/src/ui/tui/tui-pane-health.js +252 -228
  283. package/dist/src/ui/tui/tui-pane-history.js +61 -70
  284. package/dist/src/ui/tui/tui-pane-pending.js +284 -247
  285. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  286. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  287. package/dist/src/ui/tui/tui-pane-session.js +11 -7
  288. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  289. package/dist/src/ui/tui/tui-pane.js +14 -11
  290. package/dist/src/ui/tui/tui-panes.js +29 -32
  291. package/dist/src/ui/tui/tui.js +3 -4
  292. package/dist/src/verdict/fix-claim.js +76 -54
  293. package/dist/src/verdict/refusal.js +1 -1
  294. package/package.json +5 -5
  295. package/dist/src/concurrency/provider-lease.js +0 -105
  296. package/dist/src/runner/model-fallback.js +0 -76
  297. package/dist/src/runner/runner-fallback.js +0 -66
  298. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  299. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  300. /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
  301. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  302. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  303. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  304. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
  305. /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
@@ -1,8 +1,7 @@
1
- import { publishInfoFields } from "../team/orchestrator-info.js";
1
+ import { publishInfoFields } from "../team/team-orchestrator-info.js";
2
2
  import { baseRoleOf, runnerEnabled } from "../roles/runner-ids.js";
3
3
  import { DIRECTOR_ROLE } from "../roles/roles.js";
4
4
  import { reviewHoldKey, pollDeferralInputs, pollRunnerReasons } from "./orchestrator-scheduling.js";
5
- import { seamConcurrencyKey } from "../config/config-views.js";
6
5
  import { launchDueRuns } from "./orchestrator-launch.js";
7
6
  import { branchHead, currentBranch } from "../git/git.js";
8
7
  import { queuedMergeFiles, queuedMergeRoles } from "../merge/merge-queue.js";
@@ -15,11 +14,21 @@ import { checkPermitSnapshot } from "../concurrency/check-permit.js";
15
14
  import { slotPoolSnapshot } from "../git/worktree-pool.js";
16
15
  import { redeployTarget, watchVerifiesMain } from "../baseline/main-baseline.js";
17
16
  import { p75RunDurationMs } from "../run/run-timing.js";
17
+ import { reapAttributableOrphans } from "../doctor/doctor-orphans.js";
18
18
  import { pollTeamGates } from "../gates/gate-polls.js";
19
19
  import { heldProviders, windowHeldProviders } from "../team/team-hold.js";
20
20
  import { workReserveCount } from "../scheduling/claims.js";
21
21
  export function newPollState() {
22
- return { restart: false, holdForRestart: false, diskHeld: false, warnedBranchDivergence: false, lastReserve: 0 };
22
+ return { restart: false, holdForRestart: false, diskHeld: false, warnedBranchDivergence: false, lastReserve: 0, lastOrphanReapAt: 0 };
23
+ }
24
+ /** How often the orchestrator's poll loop stands down the orphan reap — a once-per-interval
25
+ * throttle so a slow process-table scan never runs on the busy per-poll hot path. */
26
+ export const ORPHAN_REAP_INTERVAL_MS = 15 * 60 * 1000;
27
+ /** Whether the orphan reap is due now, given the last poll it ran: true at and after
28
+ * `now >= last + intervalMs`. Purely a clock comparison so tests pin it from literal
29
+ * numbers. A fresh state's `last` of 0 is always due against a real `now`. */
30
+ export function dueForOrphanReap(last, now, intervalMs = ORPHAN_REAP_INTERVAL_MS) {
31
+ return now - last >= intervalMs;
23
32
  }
24
33
  /** The start gate every reserved run passes the moment its permit is granted
25
34
  * (runTimedRoleRun's `held`): closed while a restart is pending, and after one is decided so
@@ -52,7 +61,7 @@ export async function pollRequestsAndWatch(ctx, state) {
52
61
  // src/config/config-live.ts owns the last-known-good retention and the edge-triggered
53
62
  // warnings/events around it.
54
63
  const liveConfig = ctx.liveReload.poll();
55
- // The notify command rides the same last-known-good reload (src/events/notify.ts): a live
64
+ // The notify command rides the same last-known-good reload (src/events/event-notify.ts): a live
56
65
  // `config set notify` edit takes effect on the next poll, no restart.
57
66
  ctx.notifier.update(liveConfig);
58
67
  // Live session retention (the last restart-only setting): the edge-triggered
@@ -60,6 +69,15 @@ export async function pollRequestsAndWatch(ctx, state) {
60
69
  // src/orchestrator/retention.ts. The live config (last-known-good while the file is broken or
61
70
  // missing) drives the check, like the budget gate.
62
71
  ctx.retentionPruner.poll(root, liveConfig.sessionRetentionDays);
72
+ // The orphan reap (src/doctor/doctor-orphans.ts): at most once per interval, kill the
73
+ // orphans the doctor row already reports that are old enough to be certain a live run no
74
+ // longer owns them — the automatic counterpart to the human click `doctor
75
+ // --kill-orphans`/dashboard/TUI makes. The time is stamped before the await, so a slow
76
+ // scan cannot overlap the next poll's reap (and this await anyway holds the poll step).
77
+ if (dueForOrphanReap(state.lastOrphanReapAt, Date.now())) {
78
+ state.lastOrphanReapAt = Date.now();
79
+ await reapAttributableOrphans(root);
80
+ }
63
81
  // A missing or stale opencode catalog is refetched at most once an hour while opencode is routed.
64
82
  ctx.opencodeCatalogRefresh.poll(liveConfig);
65
83
  // launchservicesd's Mach-port budget (src/process/launch-services.ts owns it):
@@ -119,9 +137,8 @@ export function pollGatesAndHolds(ctx, state, { liveConfig, now }) {
119
137
  state.diskHeld = gates.diskHeld;
120
138
  // Publish the permit pool's live state (plans/provider-concurrency.md §8): observers
121
139
  // (status, TUI, GUI) read one file instead of coupling to this Semaphore, and the team
122
- // line shows per-provider counts only while a cap map is configured. Written only when
123
- // it changes, like budget and disk; a map-less team publishes `byKey` empty, which
124
- // renders exactly as before. The check-permit pool rides along, so status can show checks
140
+ // line shows the team total. Written only when it changes, like budget and disk. The
141
+ // check-permit pool rides along, so status can show checks
125
142
  // running and waiting while merges stall. The workspace pool's live state rides along too:
126
143
  // one small slots.json read per poll, and publishInfoFields rewrites the info file only
127
144
  // when a value changed.
@@ -150,7 +167,7 @@ export function pollGatesAndHolds(ctx, state, { liveConfig, now }) {
150
167
  teamHeldNow,
151
168
  reviewHeldNow: () => liveConfig.review.enabled && teamHeldNow().has(reviewSeamKey),
152
169
  reviewHeld: liveConfig.review.enabled && teamHeldProviders.has(reviewSeamKey),
153
- roleProviders: new Map(runners.map((r) => [r.role, r.runProvider(now)])),
170
+ roleProviders: new Map(runners.map((r) => [r.role, r.runProvider()])),
154
171
  };
155
172
  }
156
173
  /** Self-redeploy (src/redeploy/redeployer.ts): with main's head in hand, let the state
@@ -222,8 +239,7 @@ export async function pollMergeDrain(ctx, state, { liveConfig, mainHead }, gates
222
239
  runners: ctx.runners,
223
240
  liveConfig,
224
241
  roleConfig: gates.roleConfig,
225
- directorConfig: gates.directorConfig,
226
- piDefault: ctx.piDefault,
242
+ directorConfig: liveConfig,
227
243
  startHeld: (role) => mergeStartHeld(runStartHeld(state), gates.reviewHeldNow(), afterMergeRoles, role),
228
244
  }, ctx.merges);
229
245
  }
@@ -280,9 +296,7 @@ export async function pollSchedule(ctx, state, { liveConfig, mainHead, now }, ga
280
296
  probePair: inputs.probePair,
281
297
  probeRoles: inputs.probeRoles,
282
298
  budgetActive: gates.budgetActive,
283
- windowMovedRoles: gates.windowMovedRoles,
284
299
  semaphore: ctx.semaphore,
285
- piDefault: ctx.piDefault,
286
300
  rolePermitHolders: ctx.rolePermitHolders,
287
301
  roleInFlight: ctx.roleInFlight,
288
302
  directorInFlight: ctx.directorInFlight,
@@ -296,36 +310,17 @@ export async function pollSchedule(ctx, state, { liveConfig, mainHead, now }, ga
296
310
  }
297
311
  /** Priority headroom, then once mode's exit; returns whether the loop should stop. Writes
298
312
  * state.lastReserve and, in once mode, fires internalStop. */
299
- export function pollReserveAndExit(ctx, state, { liveConfig, now }) {
313
+ export function pollReserveAndExit(ctx, state, { liveConfig }) {
300
314
  const { root, runners, semaphore } = ctx;
301
315
  // Priority headroom (plans/parallel-work-instances.md part 7/7): withhold a few permits
302
316
  // from maintenance while a work role has work to take, so a due work run never waits out
303
317
  // a whole maintenance run. Sized after the launch pass, so a runner launched this poll
304
318
  // already reads as running and reserves nothing for itself.
305
- const poolSnapshot = semaphore.snapshot();
306
- // The provider-headroom predicate receives the structural ClaimRunner; the config that
307
- // names the provider lives on the real RoleRunners, so look the runner up by role.
308
- const runnerByRole = new Map(runners.map((r) => [r.role, r]));
309
319
  const reserve = workReserveCount(root, runners, {
310
320
  maxConcurrent: liveConfig.maxConcurrent,
311
321
  enabled: new Set(runners.filter((r) => runnerEnabled(r.config, r.role)).map((r) => r.role)),
312
322
  queuedMergeRoles: queuedMergeRoles(root),
313
323
  instancesOf: (base) => liveConfig.roles[base]?.instances ?? 1,
314
- // Per-provider headroom (plans/provider-concurrency.md §6): an idle work role reserves a
315
- // permit only when its own provider could use it — capped and full, it would only idle
316
- // the slot another provider's maintenance run could take. Read from the pool's live
317
- // snapshot; an uncapped key (or the team-total-only case) always has headroom.
318
- providerHasHeadroom: (r) => {
319
- const runner = runnerByRole.get(r.role);
320
- const cfg = runner && runner.role !== DIRECTOR_ROLE ? runner.runConfig(now) : null;
321
- const key = cfg ? seamConcurrencyKey(cfg, ctx.piDefault) : undefined;
322
- if (key === undefined)
323
- return true;
324
- const k = poolSnapshot.byKey[key];
325
- if (k?.limit === undefined)
326
- return true;
327
- return k.inUse < k.limit;
328
- },
329
324
  });
330
325
  semaphore.setReserve(reserve);
331
326
  if ((state.lastReserve === 0) !== (reserve === 0)) {
@@ -1,4 +1,4 @@
1
- import { readOrchestratorInfo, writeOrchestratorInfo } from "../team/orchestrator-info.js";
1
+ import { readOrchestratorInfo, writeOrchestratorInfo } from "../team/team-orchestrator-info.js";
2
2
  import { runnerIds } from "../roles/runner-ids.js";
3
3
  import { newLiveConfigReload } from "../config/config-live.js";
4
4
  import { FALLBACK_BREAKER_POLICY, } from "../budget/fallback-breaker.js";
@@ -10,20 +10,18 @@ import { mergeTasks, newMergePipeline } from "../merge/merge-pipeline.js";
10
10
  import { bestEffortEventLogger, logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
11
11
  import { removeQuiet } from "../files/files.js";
12
12
  import { OnceRound } from "../scheduling/once-round.js";
13
- import { newNotifier } from "../events/notify.js";
13
+ import { newNotifier } from "../events/event-notify.js";
14
14
  import { piModelsPath } from "../pi/pi-models.js";
15
- import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
16
15
  import { Semaphore } from "../concurrency/semaphore.js";
17
16
  import { orchestratorStatePath } from "../paths.js";
18
17
  import { createMainWatch } from "../baseline/main-watch.js";
19
18
  import { queueCulpritHeal } from "../baseline/main-red.js";
20
19
  import { RetentionPruner } from "./retention.js";
21
- import { newOpencodeCatalogRefresh } from "../gates/startup-gate.js";
20
+ import { newOpencodeCatalogRefresh } from "../gates/startup-gates.js";
22
21
  import { WorkMergedCache } from "../scheduling/work-merged-cache.js";
23
22
  import { drainInFlightWork, HANDOFF_MERGE_WINDOW_MS, sleepInterruptible, } from "../run/run-timing.js";
24
23
  import { newTeamGateStates } from "../gates/gate-polls.js";
25
- import { loggedWindowKeys } from "../team/team-polls.js";
26
- import { ReclaimController } from "../team/reclaim.js";
24
+ import { ReclaimController } from "../team/team-reclaim.js";
27
25
  import { reapPreviousTeamOrphans } from "../doctor/doctor-orphans.js";
28
26
  import { runStartupMigrations } from "./startup-migration-steps.js";
29
27
  import { archiveAtTeamStart } from "../backlog/backlog-archive.js";
@@ -34,9 +32,6 @@ export async function runOrchestrator(opts) {
34
32
  const { root, config, mainBranch, signal: externalSignal } = opts;
35
33
  const pollMs = opts.pollMs ?? POLL_MS;
36
34
  const modelsPath = opts.modelsPath ?? piModelsPath();
37
- // pi's saved default model — the same stat-cached read status-roles.ts makes — used as the
38
- // fallback provider key for a pi seam that names no model (plans/provider-concurrency.md §2).
39
- const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), root);
40
35
  const breakerPolicy = opts.fallbackBreakerPolicy ?? FALLBACK_BREAKER_POLICY;
41
36
  const breakerNow = opts.breakerNow ?? Date.now;
42
37
  // Parallel work instances (part 5b/7): the runner list is every runner id the config enables
@@ -77,10 +72,6 @@ export async function runOrchestrator(opts) {
77
72
  // resolver — merge-drain.ts) all hold a permit; see its MergePipelineContext for why a
78
73
  // merge is not exempt.
79
74
  const semaphore = new Semaphore(Math.max(1, config.maxConcurrent));
80
- // Seed the pool's per-provider caps from the startup config (plans/provider-concurrency.md
81
- // §7): a role run's lease is bounded by its provider key from the first launch onward, not
82
- // just after a live edit. A config without the key passes an empty map, the no-change case.
83
- semaphore.setKeyCaps(config.maxConcurrentPerProvider ?? {});
84
75
  const infoFile = orchestratorStatePath(root);
85
76
  const info = { pid: process.pid, startedAt: Date.now(), roles: enabled };
86
77
  if (redeploy)
@@ -174,9 +165,6 @@ export async function runOrchestrator(opts) {
174
165
  // the start gate reads gateStates.teamHold at permit time, so its closures always see
175
166
  // the latest poll's hold verdict rather than the poll that created them.
176
167
  const gateStates = newTeamGateStates(config);
177
- // A restart must not re-announce a window that is still open.
178
- gateStates.claudeWindowWarned = loggedWindowKeys(root, "subscription_window_warning", Date.now());
179
- gateStates.claudeWindowReserved = loggedWindowKeys(root, "subscription_window_reserved", Date.now());
180
168
  // The live config the last successful reload produced (last-known-good while the file is
181
169
  // broken or missing). The reload bookkeeping itself lives in src/config/config-live.ts.
182
170
  const liveReload = newLiveConfigReload({ root, config, mainBranch, runnerSignal: roleSignal, runners, semaphore, roleFilter: opts.roleFilter });
@@ -194,10 +182,10 @@ export async function runOrchestrator(opts) {
194
182
  // logs exactly once — on the transition in, never while merely not-due and never on exit.
195
183
  // In-memory only: a restart mid-episode can re-log at most one event.
196
184
  const deferredDue = new Map();
197
- // The operator notify hook (src/events/notify.ts owns the whole concern): one configured shell
198
- // command run on notable events (budget_paused, role_streak_paused, merge_failed,
199
- // restart_blocked). Subscribed here so it sees every event this process logs from start;
200
- // disposed beside the orchestrator_stop event below.
185
+ // The operator notify hook (src/events/event-notify.ts owns the whole concern): one
186
+ // configured shell command run on notable events (budget_paused, role_streak_paused,
187
+ // merge_failed, restart_blocked). Subscribed here so it sees every event this
188
+ // process logs from start; disposed beside the orchestrator_stop event below.
201
189
  const notifier = newNotifier(root);
202
190
  // Best-effort event logging in the poll loop (bestEffortEventLogger, events.ts): this loop
203
191
  // and its finally run with no catch, so an unwritable feed (ENOSPC, EACCES, the path replaced
@@ -221,7 +209,6 @@ export async function runOrchestrator(opts) {
221
209
  once,
222
210
  info,
223
211
  modelsPath,
224
- piDefault,
225
212
  redeploy,
226
213
  launchServicesWatch,
227
214
  reclaim,
@@ -19,52 +19,73 @@ import { noteSkip } from "./scheduling-scope.js";
19
19
  * runner's claim/heldElsewhere state, the claim group's held set, and once-mode settlement on
20
20
  * the skip paths. */
21
21
  export function admitRunner(scope, runner, reason) {
22
- // Parallel work instances (part 4/7): a multi-runner base role's claim assignment, after
23
- // every gate has admitted this runner. A runner that already holds a claim proceeds — its
24
- // revision, resume or retry is the same entry. An unclaimed runner with outstanding work
25
- // (a revision) is the mid-entry runner the group hold waits on: it proceeds unassigned —
26
- // holding it too would deadlock the whole group, since only its run can settle the hold.
27
- // Otherwise, when the run is not an inbox/resume run, it takes the first free entry; an
28
- // extra instance with none free is skipped without touching its backoff, and the primary
29
- // stands down too while siblings hold every eligible entry, unless an operator wake is fresh
30
- // (then it runs unassigned).
31
- // Stand-down: the feature charter says "If PLANS.md is empty or everything is done, there is
32
- // nothing to do" and the bugfix charter says "when every open entry carries [a Refused note],
33
- // there is nothing to do", so a feature runner (primary included) with no eligible plan at all,
34
- // or a bugfix runner whose open bugs are all held (no open bugs at all is search mode, which
35
- // still runs), skips its scheduled and main-moved runs instead of paying a model run that
36
- // can only end in
37
- // nothing-to-do. Startup, inbox and resume runs, a fresh operator wake, and a runner with a
38
- // claim or outstanding work still run. nextRunAt and backoff stay put, so the first poll
39
- // after a plan turns eligible starts a run for it.
40
- const standDownBase = baseRoleOf(runner.role);
41
- if ((standDownBase === "feature" || (standDownBase === BUGFIX_ROLE && scope.openBugsNow)) &&
22
+ if (standDownForNoWork(scope, runner, reason))
23
+ return "skipped";
24
+ return claimGroupAdmission(scope, runner, reason);
25
+ }
26
+ /** The candidate half of the stand-down test: true when a feature runner, or a bugfix runner
27
+ * whose open bugs are non-empty (no open bugs at all is search mode, which still runs), is due
28
+ * a scheduled or main-moved run, is not freshly woken, and holds no claim or outstanding work —
29
+ * so startup, inbox and resume runs, a fresh operator wake, and a claimed or mid-entry runner
30
+ * all pass through. The caller stands the runner down only when this holds and no entry is
31
+ * eligible. */
32
+ function noWorkScheduledRun(scope, runner, reason) {
33
+ const base = baseRoleOf(runner.role);
34
+ return ((base === "feature" || (base === BUGFIX_ROLE && scope.openBugsNow)) &&
42
35
  (reason === "scheduled" || reason === "main moved") &&
43
36
  !freshlyWoken(runner.state) &&
44
37
  runner.state.claim === undefined &&
45
- !hasOutstandingWork(runner.state)) {
46
- let eligible = scope.eligibleMemo.get(standDownBase);
47
- if (eligible === undefined) {
48
- eligible =
49
- scope.claimGroups.get(standDownBase)?.eligible ??
50
- eligibleEntries(scope.root, standDownBase);
51
- scope.eligibleMemo.set(standDownBase, eligible);
52
- }
53
- if (eligible.length === 0) {
54
- noteSkip(scope.root, runner, "no-free-entry");
55
- runner.state.heldElsewhere = undefined;
56
- if (scope.once.active)
57
- scope.once.settle(runner.role, "no free entry");
58
- return "skipped";
59
- }
38
+ !hasOutstandingWork(runner.state));
39
+ }
40
+ /** The eligible-entries list for a base role, computed once per poll and memoized on the
41
+ * scope so the stand-down and claim-group paths share it. */
42
+ function eligibleForBase(scope, base) {
43
+ let eligible = scope.eligibleMemo.get(base);
44
+ if (eligible === undefined) {
45
+ eligible =
46
+ scope.claimGroups.get(base)?.eligible ?? eligibleEntries(scope.root, base);
47
+ scope.eligibleMemo.set(base, eligible);
60
48
  }
49
+ return eligible;
50
+ }
51
+ /** Stand down a runner with no eligible entry at all on its scheduled and main-moved runs: the
52
+ * feature charter says "If PLANS.md is empty or everything is done, there is nothing to do" and
53
+ * the bugfix charter says "when every open entry carries [a Refused note], there is nothing to
54
+ * do", so a feature runner (primary included) with no eligible plan at all, or a bugfix runner
55
+ * whose open bugs are all held (no open bugs at all is search mode, which still runs), skips
56
+ * its scheduled and main-moved runs instead of paying a model run that can only end in
57
+ * nothing-to-do. nextRunAt and backoff stay put, so the first poll after a plan turns eligible
58
+ * starts a run for it. */
59
+ function standDownForNoWork(scope, runner, reason) {
60
+ if (!noWorkScheduledRun(scope, runner, reason))
61
+ return false;
62
+ if (eligibleForBase(scope, baseRoleOf(runner.role)).length > 0)
63
+ return false;
64
+ noteSkip(scope.root, runner, "no-free-entry");
65
+ runner.state.heldElsewhere = undefined;
66
+ if (scope.once.active)
67
+ scope.once.settle(runner.role, "no free entry");
68
+ return true;
69
+ }
70
+ /** Parallel work instances (part 4/7): a multi-runner base role's claim assignment, after
71
+ * every gate has admitted this runner. A runner that already holds a claim proceeds — its
72
+ * revision, resume or retry is the same entry. An unclaimed runner with outstanding work
73
+ * (a revision) is the mid-entry runner the group hold waits on: it proceeds unassigned —
74
+ * holding it too would deadlock the whole group, since only its run can settle the hold.
75
+ * Otherwise, when the run is not an inbox/resume run, it takes the first free entry; an
76
+ * extra instance with none free is skipped without touching its backoff, and the primary
77
+ * stands down too while siblings hold every eligible entry, unless an operator wake is fresh
78
+ * (then it runs unassigned). */
79
+ function claimGroupAdmission(scope, runner, reason) {
61
80
  const claimGroup = scope.claimGroups.get(baseRoleOf(runner.role));
62
81
  if (claimGroup) {
63
82
  if (runner.state.claim) {
64
83
  // keeps its claim
84
+ return "admitted";
65
85
  }
66
86
  else if (hasOutstandingWork(runner.state)) {
67
87
  // finishes its own unrecorded entry
88
+ return "admitted";
68
89
  }
69
90
  else if (reason !== "inbox" && reason !== "resume") {
70
91
  // The group holds an unclaimed mid-entry sibling: assign nothing this poll, or a
@@ -74,39 +95,7 @@ export function admitRunner(scope, runner, reason) {
74
95
  noteSkip(scope.root, runner, "claim-hold");
75
96
  return "skipped";
76
97
  }
77
- const free = claimGroup.eligible.filter((e) => !claimGroup.held.has(e.key));
78
- const pick = assignNext(free);
79
- // An unassigned runner is told which entries its siblings hold (cleared when none).
80
- const heldTitles = claimGroup.eligible
81
- .filter((e) => claimGroup.held.has(e.key))
82
- .map((e) => e.title);
83
- runner.state.heldElsewhere = !pick && heldTitles.length > 0 ? heldTitles : undefined;
84
- if (pick) {
85
- runner.state.claim = {
86
- file: baseRoleOf(runner.role) === BUGFIX_ROLE ? "BUGS.md" : "PLANS.md",
87
- key: pick.key,
88
- title: pick.title,
89
- at: scope.now,
90
- source: "assigned",
91
- };
92
- claimGroup.held.add(pick.key);
93
- logEventBestEffort(scope.root, {
94
- source: runner.role,
95
- type: "claim",
96
- action: "assigned",
97
- key: pick.key,
98
- title: pick.title,
99
- });
100
- }
101
- else if (instanceIndex(runner.role) >= 2 ||
102
- (heldTitles.length > 0 && !freshlyWoken(runner.state))) {
103
- // An idle extra instance, or a primary whose eligible entries siblings all hold
104
- // (unless an operator wake is fresh): no run, no backoff climb.
105
- noteSkip(scope.root, runner, "no-free-entry");
106
- if (scope.once.active)
107
- scope.once.settle(runner.role, "no free entry");
108
- return "skipped";
109
- }
98
+ return assignClaim(scope, runner, claimGroup.eligible, claimGroup.held);
110
99
  }
111
100
  }
112
101
  else {
@@ -114,3 +103,47 @@ export function admitRunner(scope, runner, reason) {
114
103
  }
115
104
  return "admitted";
116
105
  }
106
+ /** Assign the first free eligible entry to an unclaimed admitted runner: record the claim on
107
+ * the runner, add it to the group's held set, and log the assignment. With no free entry, an
108
+ * idle extra instance, or a primary whose eligible entries siblings all hold (unless an
109
+ * operator wake is fresh), stands down rather than run unassigned. */
110
+ function assignClaim(scope, runner, eligible, held) {
111
+ const free = eligible.filter((e) => !held.has(e.key));
112
+ const pick = assignNext(free);
113
+ // An unassigned runner is told which entries its siblings hold (cleared when none).
114
+ const heldTitles = eligible.filter((e) => held.has(e.key)).map((e) => e.title);
115
+ runner.state.heldElsewhere = !pick && heldTitles.length > 0 ? heldTitles : undefined;
116
+ if (pick) {
117
+ recordAssignedClaim(scope, runner, pick, held);
118
+ return "admitted";
119
+ }
120
+ else if (instanceIndex(runner.role) >= 2 ||
121
+ (heldTitles.length > 0 && !freshlyWoken(runner.state))) {
122
+ // An idle extra instance, or a primary whose eligible entries siblings all hold
123
+ // (unless an operator wake is fresh): no run, no backoff climb.
124
+ noteSkip(scope.root, runner, "no-free-entry");
125
+ if (scope.once.active)
126
+ scope.once.settle(runner.role, "no free entry");
127
+ return "skipped";
128
+ }
129
+ return "admitted";
130
+ }
131
+ /** Build the runner's claim record, add the picked entry to the group's held set, and log the
132
+ * assignment to the events feed. */
133
+ function recordAssignedClaim(scope, runner, pick, held) {
134
+ runner.state.claim = {
135
+ file: baseRoleOf(runner.role) === BUGFIX_ROLE ? "BUGS.md" : "PLANS.md",
136
+ key: pick.key,
137
+ title: pick.title,
138
+ at: scope.now,
139
+ source: "assigned",
140
+ };
141
+ held.add(pick.key);
142
+ logEventBestEffort(scope.root, {
143
+ source: runner.role,
144
+ type: "claim",
145
+ action: "assigned",
146
+ key: pick.key,
147
+ title: pick.title,
148
+ });
149
+ }
@@ -13,7 +13,7 @@ import { readMergeMarker, mergeChanges } from "../merge/merge-slot.js";
13
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";
@@ -5,7 +5,7 @@ import { isAfterMergeRole } from "../review/post-merge-review.js";
5
5
  import { deferRunReason, isEligible } from "../scheduling/scheduling.js";
6
6
  import { inboxSize } from "../inbox/inbox.js";
7
7
  import { freshlyWoken } from "../runner/runner-state.js";
8
- import { MERGE_CONFLICT_LIMIT } from "../runner/leftover.js";
8
+ import { MERGE_CONFLICT_LIMIT } from "../runner/runner-leftover.js";
9
9
  import { noteSkip } from "./scheduling-scope.js";
10
10
  /** Decide whether this runner is due this poll, running every skip gate and the once/deferral
11
11
  * bookkeeping against scheduling-scope.ts's scope. Returns skip:true when a gate declined the
@@ -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
  }
@@ -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