tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -9,7 +9,7 @@ import { POST_REVIEW_FAILURE_LIMIT, addPostReview, readPostReviews, reviewMerged
9
9
  import { shortSha } from "../text/format.js";
10
10
  import { formatDate } from "../text/datetime.js";
11
11
  import { collapseWhitespace, errorMessage } from "../text/text.js";
12
- import { fileBugsOnMain } from "../backlog/backlog-archive.js";
12
+ import { fileBugsOnMain } from "../backlog/backlog-file-bugs.js";
13
13
  import { abortOnShutdown, acquireUnlessAborted, guardMergeTask } from "./merge-pipeline.js";
14
14
  import { resolveWaitStage } from "./merge-slot.js";
15
15
  import { mergeToMain } from "./merge-to-main.js";
@@ -1,6 +1,7 @@
1
1
  import { errCode } from "../errno.js";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { ensureDir } from "../files/files.js";
4
5
  import { listQueueFiles, queueFileName, removeQueueFile } from "../files/file-queue.js";
5
6
  import { isJsonObject, nonNegativeNumber } from "../files/json-object.js";
6
7
  import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
@@ -49,7 +50,7 @@ export function legacyMigrateLandQueueDir(root) {
49
50
  const notes = { moved: [], kept: [], failed: [] };
50
51
  const files = listQueueFiles(from, ".json");
51
52
  if (files.length > 0)
52
- fs.mkdirSync(to, { recursive: true });
53
+ ensureDir(to);
53
54
  for (const file of files) {
54
55
  const name = path.basename(file);
55
56
  const dest = path.join(to, name);
@@ -4,7 +4,7 @@ import { changeBaseRev, commitMessage, headOf } from "../git/git.js";
4
4
  import { aheadOfMainFiles, treeDiffFiles } from "../git/git-diff.js";
5
5
  import { resolvedDiffDiverges } from "./merge-diff.js";
6
6
  import { conflictedFileHunks, conflictedFiles, continueRebase, ffMainToOrWhy, overwriteBlockedReason, primaryCheckoutBlockers, hasConflictMarkers, mainCommitsTouching, rebaseOntoMain, rebaseOntoMainLeaveConflicts, } from "./merge-git.js";
7
- import { resolveBacklogInsertConflicts } from "./backlog-conflicts.js";
7
+ import { resolveBacklogInsertConflicts } from "./merge-backlog-conflicts.js";
8
8
  import { abortSync } from "../git/worktree.js";
9
9
  import { runScopedBuildCheck } from "../build/build-check-scoped.js";
10
10
  import { detectBuildCheck, gateCommandOf, mergeCommandOf } from "../build/build-check-detect.js";
@@ -24,8 +24,9 @@ export const VERIFY_TIER = -1;
24
24
  * verification parked for a permit, because the merge is the one serial step every queued change
25
25
  * waits on. (Nothing that holds a permit ever waits on the merge: no verification takes the merge
26
26
  * lock, and the resolver runs outside it — merge-to-main.ts's mergeToMain — so this cannot
27
- * deadlock.) Shared by merge-drain.ts's merge slot and merge-post-review.ts's fix landing, so the
28
- * reviewer's fix-merge resolver rides the same tier and the two cannot drift. */
27
+ * deadlock.) Shared by merge-drain.ts's merge slot and merge-post-review-drain.ts's fix
28
+ * landing, so the reviewer's fix-merge resolver rides the same tier and the two cannot
29
+ * drift. */
29
30
  export const MERGE_TIER = VERIFY_TIER - 1;
30
31
  /** How many verifications may be in flight (parked or running) at once: every `maxConcurrent`
31
32
  * permit but one, so a deep merge queue never takes the last slot from the roles that could
@@ -18,7 +18,7 @@ import { normalizePauseReason, pauseTeam, pauseRole, pausedRoles, resumeTeam, re
18
18
  import { readOrchestratorInfo } from "../team/orchestrator-info.js";
19
19
  import { pidAlive } from "../process/process.js";
20
20
  import { inspectReclaimCandidates } from "../team/reclaim.js";
21
- import { markerApplyNote, NO_HARNESS_ERROR, PAUSE_FOR_MAX_MS, cancelScheduledStopMessage, cancelWakes, listWakes, renderWakeList, requestAbort, requestResetCounters, requestRestart, requestScheduledStop, requestWake, rolePauseMessage, roleResumeMessage, runManualReclaim, timedPauseBits, } from "./operator-intent.js";
21
+ import { markerApplyNote, NO_HARNESS_ERROR, PAUSE_FOR_MAX_MS, cancelScheduledStopMessage, cancelWakes, listWakes, renderWakeList, requestAbort, requestAbortAll, requestResetCounters, requestRestart, requestScheduledStop, requestWake, rolePauseMessage, roleResumeMessage, runManualReclaim, timedPauseBits, } from "./operator-intent.js";
22
22
  /** The CLI layer of the operator commands: the `reset-counters`, `wake`, `reclaim`, `abort`,
23
23
  * `pause`, `resume`, `stop`, and `retire` commands, split out of cli.ts so the command bodies
24
24
  * live beside their shared `--role` resolution. The marker-writing cores and shared
@@ -182,17 +182,28 @@ export async function cmdReclaim(root, args) {
182
182
  }
183
183
  say(await runManualReclaim(root));
184
184
  }
185
- /** `tumwater abort --role <id>[,<id>...]`: kill the named roles' in-flight runs right now (a
186
- * comma list writes one marker per role). The CLI cannot
185
+ /** `tumwater abort [--role <id>[,<id>...]] [--all]`: kill the named roles' in-flight runs
186
+ * right now (a comma list writes one marker per role), or with `--all` every enabled role's in
187
+ * flight run in one request (one marker per enabled runner). The CLI cannot
187
188
  * reach into the orchestrator process, so the request rides on disk like reset-counters':
188
189
  * a per-role marker file a running team consumes within one poll cycle (the runner's
189
190
  * abortRun kills the pi child and resets the worktree to main). Requires a live harness —
190
- * with no team there is nothing to consume the marker. The role stays enabled: it backs
191
+ * with no team there is nothing to consume the marker. The roles stay enabled: they back
191
192
  * off normally and later runs proceed as usual. */
192
193
  export async function cmdAbort(root, args) {
194
+ if (args.includes("--all")) {
195
+ if (flagValue(args, "--role") !== null) {
196
+ fail("abort takes either --role <id>[,<id>...] or --all, not both");
197
+ }
198
+ const result = requestAbortAll(root);
199
+ if (!result.ok)
200
+ fail(result.error);
201
+ say(result.message);
202
+ return;
203
+ }
193
204
  // As in cmdLogs: the config exists only to validate the id against built-ins plus
194
205
  // user-defined roles; a missing --role fails before it is ever needed.
195
- const roles = namedRoleList(root, args) ?? fail("abort requires --role <id> (e.g. `--role feature`)");
206
+ const roles = namedRoleList(root, args) ?? fail("abort requires --role <id>[,<id>...] or --all (e.g. `--role feature`)");
196
207
  for (const role of roles) {
197
208
  const result = requestAbort(root, role);
198
209
  if (!result.ok)
@@ -436,37 +447,10 @@ export async function cmdRetire(root, args) {
436
447
  say(`nothing to remove for role ${role}`);
437
448
  }
438
449
  const REVERT_USAGE = "usage: tumwater revert <sha>|<role> <n>|<role> --last [--dry-run] [--json]";
439
- /** The sha the run `<role> <n>` (or the newest completed run of `<role>` under `last`) merged
440
- * under, resolved the way `tumwater history <role> <n>` and the dashboard's revert resolve it
441
- * (`runMergedSha`). Fails, exit 1, when the run is unknown or did not merge: revert is an
442
- * action, so a miss is an error rather than history's exit-0 "not found". */
443
- function revertRunSha(root, positionals, last) {
444
- const role = canonicalRoleId(positionals[0]);
445
- const ids = knownRoleIdsCached(root);
446
- if (!ids.includes(role))
447
- fail(`${unknownRoleMessage(role, ids)} (${REVERT_USAGE})`);
448
- const run = last
449
- ? (readRunRows(root, 1, role)[0]?.run ?? null)
450
- : parseCountFlag("revert <n>", positionals[1]);
451
- if (run === null)
452
- fail(`no completed run of ${role} in the event log`);
453
- const detail = readRunDetail(root, role, run);
454
- if (detail === null)
455
- fail(runNotFoundMessage(role, run));
456
- const found = runRevertSha(role, run, detail);
457
- if (!found.ok)
458
- fail(found.error);
459
- const sha = found.sha;
460
- return { sha, line: `run ${role} #${run} merged as ${shortSha(sha)}`, role, run };
461
- }
462
- /** `tumwater revert <sha>|<role> <n>|<role> --last`: undo one merged change on main,
463
- * committing the inverse as `tumwater(operator): Revert <short sha>` under the primary-worktree
464
- * lock. The lock, branch guard, and fail-closed ref/conflict handling live in
465
- * src/operator/revert.ts; this renderer names the new main head and names the role
466
- * that made the reverted commit, when one did, as the one that may redo the work.
467
- * `--json` prints the preview or the revert as one JSON object instead (refusals still fail on
468
- * stderr). */
469
- export async function cmdRevert(root, args) {
450
+ /** Scans `args` for `--dry-run`, `--last` and `--json`, refuses a flag-shaped token as a
451
+ * dropped flag and checks the positional arity, returning the flags and the remaining
452
+ * positionals. */
453
+ function parseRevertArgs(args) {
470
454
  // `--dry-run` and `--last` are the only flags; the positionals select a sha (one), a run
471
455
  // (`<role> <n>`) or the newest run of a role (`<role>` with --last). A flag or any other
472
456
  // shape fails before anything is looked up or written.
@@ -483,20 +467,24 @@ export async function cmdRevert(root, args) {
483
467
  fail(`revert takes only --dry-run, --last and --json (got ${flagged}); pass the commit or run to undo as the argument`);
484
468
  if (last ? rest.length !== 1 : rest.length < 1 || rest.length > 2)
485
469
  fail(`revert needs a commit or a run (${REVERT_USAGE})`);
470
+ return { dryRun, last, json, rest };
471
+ }
472
+ /** Resolves the target sha, its run line and its run ref (`revertRunSha` for `<role> <n>` and
473
+ * `<role> --last`, else the sha positional directly). */
474
+ function revertTarget(root, rest, last) {
486
475
  const run = last || rest.length === 2 ? revertRunSha(root, rest, last) : null;
487
476
  const sha = run?.sha ?? rest[0];
488
477
  const runLine = run?.line ?? null;
489
478
  const runRef = run === null ? null : { role: run.role, run: run.run };
490
- if (dryRun) {
491
- const p = await previewRevert(root, sha);
492
- if (!p.ok)
493
- fail(p.error);
494
- if (json) {
495
- sayJson({ dryRun: true, run: runRef, sha: p.sha, subject: p.subject, paths: p.paths, head: p.head, primary: p.primary, applies: p.applies, dirty: p.dirty });
496
- if (!p.applies || p.dirty)
497
- process.exitCode = 1;
498
- return;
499
- }
479
+ return { sha, runLine, runRef };
480
+ }
481
+ /** Renders the `--dry-run` preview (the run line, `would revert …`, the path list,
482
+ * `applies cleanly`/`would conflict`/the dirty-checkout line) and its exit code. */
483
+ function sayRevertPreview(p, runLine, runRef, json) {
484
+ if (json) {
485
+ sayJson({ dryRun: true, run: runRef, sha: p.sha, subject: p.subject, paths: p.paths, head: p.head, primary: p.primary, applies: p.applies, dirty: p.dirty });
486
+ }
487
+ else {
500
488
  if (runLine !== null)
501
489
  say(runLine);
502
490
  say(`would revert ${shortSha(p.sha)} ${p.subject}`);
@@ -509,13 +497,12 @@ export async function cmdRevert(root, args) {
509
497
  say(`would conflict ${on}`);
510
498
  if (p.dirty)
511
499
  say("the checkout has staged or unstaged changes, so tumwater revert would refuse until they are committed or stashed; nothing was written");
512
- if (!p.applies || p.dirty)
513
- process.exitCode = 1;
514
- return;
515
500
  }
516
- const result = await revertMergedCommit(root, sha);
517
- if (!result.ok)
518
- fail(result.error);
501
+ if (!p.applies || p.dirty)
502
+ process.exitCode = 1;
503
+ }
504
+ /** Renders the committed revert (`main is now …`, the role follow-up) or its `--json` payload. */
505
+ function sayRevertOutcome(result, runLine, runRef, json) {
519
506
  if (json) {
520
507
  sayJson({ dryRun: false, run: runRef, subject: result.subject, headSha: result.headSha, beforeSha: result.beforeSha, followUp: revertFollowUp(result.subject) });
521
508
  return;
@@ -527,3 +514,48 @@ export async function cmdRevert(root, args) {
527
514
  if (followUp !== null)
528
515
  say(followUp);
529
516
  }
517
+ /** The sha the run `<role> <n>` (or the newest completed run of `<role>` under `last`) merged
518
+ * under, resolved the way `tumwater history <role> <n>` and the dashboard's revert resolve it
519
+ * (`runMergedSha`). Fails, exit 1, when the run is unknown or did not merge: revert is an
520
+ * action, so a miss is an error rather than history's exit-0 "not found". */
521
+ function revertRunSha(root, positionals, last) {
522
+ const role = canonicalRoleId(positionals[0]);
523
+ const ids = knownRoleIdsCached(root);
524
+ if (!ids.includes(role))
525
+ fail(`${unknownRoleMessage(role, ids)} (${REVERT_USAGE})`);
526
+ const run = last
527
+ ? (readRunRows(root, 1, role)[0]?.run ?? null)
528
+ : parseCountFlag("revert <n>", positionals[1]);
529
+ if (run === null)
530
+ fail(`no completed run of ${role} in the event log`);
531
+ const detail = readRunDetail(root, role, run);
532
+ if (detail === null)
533
+ fail(runNotFoundMessage(role, run));
534
+ const found = runRevertSha(role, run, detail);
535
+ if (!found.ok)
536
+ fail(found.error);
537
+ const sha = found.sha;
538
+ return { sha, line: `run ${role} #${run} merged as ${shortSha(sha)}`, role, run };
539
+ }
540
+ /** `tumwater revert <sha>|<role> <n>|<role> --last`: undo one merged change on main,
541
+ * committing the inverse as `tumwater(operator): Revert <short sha>` under the primary-worktree
542
+ * lock. The lock, branch guard, and fail-closed ref/conflict handling live in
543
+ * src/operator/revert.ts; this renderer names the new main head and names the role
544
+ * that made the reverted commit, when one did, as the one that may redo the work.
545
+ * `--json` prints the preview or the revert as one JSON object instead (refusals still fail on
546
+ * stderr). */
547
+ export async function cmdRevert(root, args) {
548
+ const { dryRun, last, json, rest } = parseRevertArgs(args);
549
+ const { sha, runLine, runRef } = revertTarget(root, rest, last);
550
+ if (dryRun) {
551
+ const p = await previewRevert(root, sha);
552
+ if (!p.ok)
553
+ fail(p.error);
554
+ sayRevertPreview(p, runLine, runRef, json);
555
+ return;
556
+ }
557
+ const result = await revertMergedCommit(root, sha);
558
+ if (!result.ok)
559
+ fail(result.error);
560
+ sayRevertOutcome(result, runLine, runRef, json);
561
+ }
@@ -1,5 +1,4 @@
1
1
  import crypto from "node:crypto";
2
- import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { cancelScheduledStop, isTeamPaused, scheduleStop } from "../team/team-state.js";
5
4
  import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
@@ -8,12 +7,13 @@ import { formatDate, formatTime, humanUntil, DAY_MS } from "../text/datetime.js"
8
7
  import { submitRolePrompt } from "../inbox/inbox-submit.js";
9
8
  import { questionAsker } from "../backlog/question-answer.js";
10
9
  import { knownRoleIdsCached, loadConfigCached } from "../config/config.js";
11
- import { baseRoleOf } from "../roles/runner-ids.js";
10
+ import { baseRoleOf, runnerIds } from "../roles/runner-ids.js";
12
11
  import { DIRECTOR_ROLE } from "../roles/roles.js";
13
12
  import { saveRunnerState, zeroCounters } from "../runner/runner-state.js";
14
13
  import { loadRunnerState } from "../runner/runner-state-load.js";
15
14
  import { clearBackoff } from "../scheduling/backoff.js";
16
15
  import { writeJsonAtomic } from "../files/json-files.js";
16
+ import { ensureDir } from "../files/files.js";
17
17
  import { reclaimPass } from "../team/reclaim.js";
18
18
  import { formatGB } from "../text/format.js";
19
19
  import { plural } from "../text/phrases.js";
@@ -134,7 +134,7 @@ export function requestWake(root, roles, inMs) {
134
134
  // runner immediately, dropping a scheduled wake's notBeforeMs on the floor. A scheduled wake
135
135
  // gets its own file so a later wake request, immediate or scheduled, cannot overwrite it.
136
136
  if (deferred) {
137
- fs.mkdirSync(scheduledWakeDir(root), { recursive: true });
137
+ ensureDir(scheduledWakeDir(root));
138
138
  writeJsonAtomic(path.join(scheduledWakeDir(root), `${now}-${process.pid}-${crypto.randomBytes(4).toString("hex")}.json`), { at: now, roles, notBeforeMs: now + inMs });
139
139
  }
140
140
  else
@@ -251,6 +251,29 @@ export function requestAbort(root, role) {
251
251
  }
252
252
  return { ok: true, message: confirmation };
253
253
  }
254
+ /** The marker-writing core of `tumwater abort --all`, shared with the GUI's POST /api/abort-all:
255
+ * drop one per-role marker per enabled runner (the same ids `runnerIds` names, each enabled role's
256
+ * bare id plus its `-2`… instances) that a running team consumes within one poll cycle — the
257
+ * team-wide twin of `requestAbort`'s one marker for one role. Idle roles' markers are swept by
258
+ * `consumeAbortRequests`' per-marker pass without aborting anything (`removeQuiet` runs even when
259
+ * no runner matches), so passing every enabled id is the same safe no-op `abort --role` of an idle
260
+ * role is. Reports the liveness gate structurally ({ok, message|error}) like requestAbort so the
261
+ * CLI can fail() and the GUI can shape its 409/200. */
262
+ export function requestAbortAll(root) {
263
+ if (!orchestratorAlive(root)) {
264
+ return { ok: false, error: NO_HARNESS_ERROR };
265
+ }
266
+ const { config } = loadConfigCached(root);
267
+ const ids = config ? runnerIds(config) : [];
268
+ if (ids.length === 0)
269
+ return { ok: false, error: "no roles are enabled to abort" };
270
+ for (const id of ids)
271
+ writeJsonAtomic(abortRequestPath(root, id), { at: Date.now() });
272
+ return {
273
+ ok: true,
274
+ message: `abort requested for ${plural(ids.length, "role", "roles")} — the running team applies each within ~2s`,
275
+ };
276
+ }
254
277
  /** The timed-pause clauses both pause confirmations share, in the plan's "… paused for
255
278
  * 30m — resumes automatically at 14:05" shape. The duration phrase comes from the parsed
256
279
  * value, not from until minus the message's own now — a few ms of clock skew between the two
@@ -5,6 +5,7 @@ import { DIRECTOR_ROLE } from "../roles/roles.js";
5
5
  import { runnerEnabled } from "../roles/runner-ids.js";
6
6
  import { branchExists, countCommitsInRange, currentBranch, deleteRef, isDirty, isMergedInto, refSha, targetBranch, } from "../git/git.js";
7
7
  import { errorMessage } from "../text/text.js";
8
+ import { plural } from "../text/phrases.js";
8
9
  import { loadRunnerState } from "../runner/runner-state-load.js";
9
10
  import { branchName, mergeRefName, legacyLandingRefName, worktreePath } from "../paths.js";
10
11
  import { isUsableWorktree, removeWorktree } from "../git/worktree.js";
@@ -77,7 +78,7 @@ function objections(status) {
77
78
  if (status.enabledInConfig)
78
79
  reasons.push(`role ${status.role} is still enabled in tumwater.json — disable it first`);
79
80
  if (status.aheadOfMain > 0)
80
- reasons.push(`branch ${branchName(status.role)} holds ${status.aheadOfMain} unmerged commit(s) — merge or discard them first`);
81
+ reasons.push(`branch ${branchName(status.role)} holds ${plural(status.aheadOfMain, "unmerged commit")} — merge or discard them first`);
81
82
  if (status.worktreePresent && !status.worktreeUsable)
82
83
  reasons.push("the worktree is unusable and cannot be checked for uncommitted changes — remove it manually or use --force");
83
84
  if (status.dirty)
@@ -52,6 +52,20 @@ export async function resolveRevertTarget(root, ref) {
52
52
  return { ok: false, error: `${shortSha(full)} is not on ${primary}` };
53
53
  return { ok: true, full, primary };
54
54
  }
55
+ /** The shared read-only prologue of a revert and its preview, so the two cannot drift: resolve
56
+ * `ref` to a merged single-parent commit (the resolveRevertTarget/parentRefusal refusals), then
57
+ * read its subject. The subject is what the reverted-role follow-up and the event log read, and
58
+ * both callers needed exactly these three steps before diverging into their own work. */
59
+ async function resolveRevertSubject(root, ref) {
60
+ const target = await resolveRevertTarget(root, ref);
61
+ if (!target.ok)
62
+ return target;
63
+ const refusal = await parentRefusal(root, target.full);
64
+ if (refusal !== null)
65
+ return { ok: false, error: refusal };
66
+ const subject = await runGit(root, ["log", "-1", "--format=%s", target.full]);
67
+ return { ok: true, full: target.full, primary: target.primary, subject };
68
+ }
55
69
  /** The refusal for a commit without exactly one parent (a root or a merge), or null. Shared by
56
70
  * the revert and its preview: `git revert` of a root deletes the whole tree, of a merge needs
57
71
  * `-m`. */
@@ -78,14 +92,10 @@ export function runRevertSha(role, run, detail) {
78
92
  * touches the index, the worktree or a ref. Never throws. */
79
93
  export async function previewRevert(root, ref) {
80
94
  try {
81
- const target = await resolveRevertTarget(root, ref);
95
+ const target = await resolveRevertSubject(root, ref);
82
96
  if (!target.ok)
83
97
  return target;
84
- const { full, primary } = target;
85
- const refusal = await parentRefusal(root, full);
86
- if (refusal !== null)
87
- return { ok: false, error: refusal };
88
- const subject = await runGit(root, ["log", "-1", "--format=%s", full]);
98
+ const { full, primary, subject } = target;
89
99
  const paths = (await runGit(root, ["diff", "--name-only", "--no-renames", full + "^", full]))
90
100
  .split("\n")
91
101
  .filter((p) => p.length > 0);
@@ -137,16 +147,12 @@ export async function revertMergedCommit(root, ref) {
137
147
  // awaits would block its event loop until the lock times out.
138
148
  await new Promise((resolve) => setImmediate(resolve));
139
149
  return await withPrimaryWorktreeLockAsync(root, async () => {
140
- const target = await resolveRevertTarget(root, ref);
150
+ const target = await resolveRevertSubject(root, ref);
141
151
  if (!target.ok)
142
152
  return target;
143
- const { full } = target;
144
- const refusal = await parentRefusal(root, full);
145
- if (refusal !== null)
146
- return { ok: false, error: refusal };
153
+ const { full, subject } = target;
147
154
  // `head` is captured before the revert so a future caller can compare before/after.
148
155
  const head = await headOf(root, "HEAD");
149
- const subject = await runGit(root, ["log", "-1", "--format=%s", full]);
150
156
  // Fail-closed on any staged or unstaged change to a tracked path: we refuse to revert a
151
157
  // checkout that has one (untracked files are tolerated — the lock dir we just created is
152
158
  // one). This keeps the inverse commit free of unrelated content and, just as important,
@@ -13,7 +13,7 @@ import { TUMWATER_SOURCE } from "../events/events.js";
13
13
  import { consumeAbortRequests, consumeReclaimRequest, consumeRestartRequest, consumeResetRequest, consumeWakeRequest, } from "../operator/operator-requests.js";
14
14
  import { checkPermitSnapshot } from "../concurrency/check-permit.js";
15
15
  import { slotPoolSnapshot } from "../git/worktree-pool.js";
16
- import { redeployTarget } from "../baseline/main-baseline.js";
16
+ import { redeployTarget, watchVerifiesMain } from "../baseline/main-baseline.js";
17
17
  import { p75RunDurationMs } from "../run/run-timing.js";
18
18
  import { pollTeamGates } from "../gates/gate-polls.js";
19
19
  import { heldProviders, windowHeldProviders } from "../team/team-hold.js";
@@ -175,7 +175,7 @@ export async function pollRedeploy(ctx, state, { liveConfig, mainHead, now }) {
175
175
  ctx.mainWatch.poll(mainHead, state.holdForRestart || state.diskHeld, liveConfig);
176
176
  if (!redeploy)
177
177
  return false;
178
- const action = await redeploy.poll(await redeployTarget(ctx.root, mainHead, redeploy.build.sha), {
178
+ const action = await redeploy.poll(await redeployTarget(ctx.root, mainHead, redeploy.build.sha, watchVerifiesMain(ctx.root, liveConfig)), {
179
179
  roleInFlight: ctx.rolePermitHolders.size,
180
180
  directorInFlight: ctx.directorInFlight.size,
181
181
  roleRunP75Ms: p75RunDurationMs(ctx.roleRunDurationsMs),
@@ -5,7 +5,7 @@ import { FALLBACK_BREAKER_POLICY, } from "../budget/fallback-breaker.js";
5
5
  import { newPollState, pollGatesAndHolds, pollMergeDrain, pollRedeploy, pollRequestsAndWatch, pollReserveAndExit, pollSchedule, } from "./orchestrator-poll.js";
6
6
  import { RoleRunner } from "../runner/runner.js";
7
7
  import { dropDanglingTumwaterPins, removeLegacyLandWorktrees } from "../git/worktree.js";
8
- import { pinCrashedRuns, retireLegacyRoleWorktrees } from "../git/worktree-pool.js";
8
+ import { pinCrashedRuns, retireLegacyRoleWorktrees } from "../git/worktree-pool-startup.js";
9
9
  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";
@@ -6,6 +6,7 @@
6
6
  * and the orchestrator only hands it the (possibly live-reloaded) window. */
7
7
  import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
8
8
  import { DAY_MS } from "../text/datetime.js";
9
+ import { plural } from "../text/phrases.js";
9
10
  import { ensureParentDir, pruneOldFiles } from "../files/files.js";
10
11
  import { pruneSessionLedgers, pruneOpencodeSessions, pruneCodexSessions } from "../agent/session-ledger.js";
11
12
  import { loadConfigSafe } from "../config/config.js";
@@ -90,7 +91,7 @@ export class RetentionPruner {
90
91
  spillDir: opencodeSpillDir(root),
91
92
  });
92
93
  if (deleted > 0)
93
- warnEventBestEffort(root, TUMWATER_SOURCE, `deleted ${deleted} expired opencode session(s)`);
94
+ warnEventBestEffort(root, TUMWATER_SOURCE, `deleted ${plural(deleted, "expired opencode session")}`);
94
95
  // Codex's sessions also live outside .tumwater — under the team's CODEX_HOME (part 8/9).
95
96
  // Resolve its binary and home exactly as a run's invocation would and purge its expired
96
97
  // sessions from the ledger BEFORE the file pass below ages it out. Like the OpenCode twin,
@@ -106,21 +107,21 @@ export class RetentionPruner {
106
107
  env: { ...process.env, CODEX_HOME: codexHomeDir },
107
108
  });
108
109
  if (deletedCodex > 0)
109
- warnEventBestEffort(root, TUMWATER_SOURCE, `deleted ${deletedCodex} expired codex session(s)`);
110
+ warnEventBestEffort(root, TUMWATER_SOURCE, `deleted ${plural(deletedCodex, "expired codex session")}`);
110
111
  // Age out session/tool-output files now: an entirely stale dir is gone before the line
111
112
  // pass reads it. A running role's ledger mtime stays young, so pruneOldFiles never ages
112
113
  // that file out; each warning must name the unit it pruned.
113
114
  const pruned = pruneOldFiles(sessions, days) + pruneOldFiles(toolOutputDir(root), days);
114
115
  if (pruned > 0)
115
- warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${pruned} old agent session/tool-output file(s)`);
116
+ warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${plural(pruned, "old agent session/tool-output file")}`);
116
117
  const prunedLines = pruneSessionLedgers(sessions, days);
117
118
  if (prunedLines > 0)
118
- warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${prunedLines} stale session ledger line(s)`);
119
+ warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${plural(prunedLines, "stale session ledger line")}`);
119
120
  // Salvaged slot leftovers (src/git/slot-salvage.ts) age out on the same window as the
120
121
  // sessions that made them: a patch still waiting by then belongs to a role that has not run
121
122
  // since, and the operator had the window's length to take a kept one.
122
123
  const prunedPatches = pruneOldFiles(salvageDir(root), days);
123
124
  if (prunedPatches > 0)
124
- warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${prunedPatches} old salvaged workspace patch(es)`);
125
+ warnEventBestEffort(root, TUMWATER_SOURCE, `pruned ${plural(prunedPatches, "old salvaged workspace patch", "old salvaged workspace patches")}`);
125
126
  }
126
127
  }
package/dist/src/paths.js CHANGED
@@ -123,12 +123,6 @@ export function worktreeUseLockPath(root) {
123
123
  export function orchestratorStatePath(root) {
124
124
  return path.join(tumwaterDir(root), "state", "orchestrator.json");
125
125
  }
126
- /** The doctor's image-probe verdict cache (src/doctor/image-probe.ts): how each claimed
127
- * image-input model actually answered a live probe, keyed agent/provider/model. Runtime state
128
- * under .tumwater/state/, never committed. */
129
- export function imageProbesPath(root) {
130
- return path.join(tumwaterDir(root), "state", "image-probes.json");
131
- }
132
126
  /** The images column `tumwater doctor` last read from `pi --list-models`, keyed provider/model.
133
127
  * Runtime state under .tumwater/state/, never committed. */
134
128
  export function piListedModelsPath(root) {
@@ -313,7 +307,7 @@ export function sessionDir(root, role) {
313
307
  return path.join(sessionsRootDir(root), role);
314
308
  }
315
309
  /** `sessionDir`'s real team root — the nearest ancestor holding a `.tumwater` directory — or
316
- * undefined when none does. run-agent installs the notes shim from THIS (never from teamRootOf,
310
+ * undefined when none does. run-spawn installs the notes shim from THIS (never from teamRootOf,
317
311
  * whose no-ancestor fallback can be a shared directory): writing `.tumwater` at the fallback
318
312
  * would mint a phantom harness root every other suite on the host then found — BUGS.md
319
313
  * 2026-10-18's `$TMPDIR/.tumwater/bin/tumwater-notes`. */
@@ -347,15 +341,15 @@ export function owningTeamRoot(dir) {
347
341
  * unreachable in a harness run and used only by OpenCode's harness-only read-back lookups
348
342
  * (opencodeDbPath, loadConfigSafe), which always see a real root. Never use this to decide
349
343
  * where to WRITE a `.tumwater`: the fallback can be a shared directory, and creating `.tumwater`
350
- * there mints a phantom root (BUGS.md 2026-10-18); run-agent's shim install uses
344
+ * there mints a phantom root (BUGS.md 2026-10-18); run-spawn's shim install uses
351
345
  * nearestTumwaterRoot instead. Moved here from the opencode adapter (plans/notes-command.md) so
352
- * run-agent's shim install — which every backend's run needs — shares the one lookup instead of
346
+ * run-spawn's shim install — which every backend's run needs — shares the one lookup instead of
353
347
  * the opencode adapter keeping it private. */
354
348
  export function teamRootOf(sessionDir) {
355
349
  return nearestTumwaterRoot(sessionDir) ?? path.dirname(path.dirname(sessionDir));
356
350
  }
357
351
  /** The directory the notes shim lands in, under a team root: `<root>/.tumwater/bin`.
358
- * run-agent installs the `tumwater-notes` command there (src/agent/notes-shim.ts) and
352
+ * run-spawn installs the `tumwater-notes` command there (src/agent/notes-shim.ts) and
359
353
  * run-marker's backendBaseEnv prepends it to every run's PATH, so a run on any backend finds
360
354
  * the command (plans/notes-command.md "Reaching it from a run"). */
361
355
  export function notesShimDir(root) {
@@ -450,6 +444,12 @@ export function witnessWorktreePath(root) {
450
444
  export function slotWorktreePath(root, n) {
451
445
  return path.join(worktreesDir(root), `_slot-${n}`);
452
446
  }
447
+ /** A slot dir is canonical when it is exactly a `_slot-<n>` checkout under the worktrees dir.
448
+ * A migrated legacy `<role>` dir is not, which marks it for retirement once its resume finishes
449
+ * (retireLegacyRoleWorktrees in worktree-pool-startup.ts and releaseSlot in worktree-pool.ts). */
450
+ export function isCanonicalSlotDir(dir) {
451
+ return /^_slot-\d+$/.test(path.basename(dir));
452
+ }
453
453
  /** The worktree pool's dedicated checkout for the merge drain's merge-side work
454
454
  * (plans/worktree-pool.md, "Layout"): a single change, a stack and its bisect, the conflict
455
455
  * resolver and the post-resolve re-review. Kept out of the pool so a permit-waiting lease can
@@ -5,7 +5,7 @@ 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-agent.ts) resolves through each backend's resolveBin; the startup
8
+ * The spawn (src/agent/run-spawn.ts) resolves through each backend's resolveBin; the startup
9
9
  * gate (gates/startup-gate.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
@@ -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-agent.ts's spawn errors and doctor's checks quote the same
18
+ * report. Exported so src/agent/run-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
@@ -141,6 +141,15 @@ export function seamContextWindow(seam, projectRoot, modelsPath = piModelsPath()
141
141
  return null;
142
142
  }
143
143
  }
144
+ /** The resolved primary seam's declared context window for a role — `configForRole` resolves
145
+ * the role's seam (the view the role page, status table/drawer and run history all print), then
146
+ * `seamContextWindow` names the window that seam declares, or null when it cannot be known. The
147
+ * one home of that resolution so the role page, the status table/drawer and a run's history
148
+ * header cannot disagree about the peak-ctx denominator. `modelsPath` defaults to pi's own
149
+ * catalog path, as `seamContextWindow` does, for read-only callers that have no seam handle. */
150
+ export function seamContextWindowForRole(cfg, role, projectRoot, modelsPath = piModelsPath()) {
151
+ return seamContextWindow(configForRole(cfg, role), projectRoot, modelsPath);
152
+ }
144
153
  /** A provider's `baseUrl` from pi's models.json, or null when the file, the provider or the
145
154
  * field is missing — what doctor probes to tell a local server that is down from one that is up. */
146
155
  export function piProviderBaseUrl(modelsPath, provider) {