tumwater 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,12 +1,13 @@
1
- // vocabulary: plain
1
+ import { joinParts } from "../text/text.js";
2
2
  import { agree, shortSpanPhrase } from "../text/phrases.js";
3
3
  import { compactTokens, usd } from "../text/format.js";
4
- import { clockHm, formatTimestamp, humanSeconds, nextRunPhrase, secondsSince, secondsUntil } from "../text/datetime.js";
4
+ import { clockHm, formatTimestamp, humanAgo, nextRunPhrase, secondsUntil } from "../text/datetime.js";
5
5
  import { claudeWindowLabel } from "../agent/claude/claude-account.js";
6
+ import { windowLabel } from "../agent/subscription-account.js";
7
+ import { backendDisplayName } from "../agent/backend-traits.js";
6
8
  import { yieldMultiplierSuffix } from "../scheduling/backoff.js";
7
9
  import { quietWindowEnd } from "../scheduling/quiet-hours.js";
8
- import { isAgentId } from "../config/model-selector.js";
9
- /** The terminal's view of one loop — what `tumwater role <id>` prints, rendered as Markdown
10
+ /** The terminal's view of one role — what `tumwater role <id>` prints, rendered as Markdown
10
11
  * from the payload role-view.ts's rolePayload collects (the same document `--json` prints).
11
12
  * Like backlog-render.ts, the renderer owns no collection of its own: the CLI's --json and
12
13
  * human branches share one collection through sayJsonOrRender's thunk, so the two surfaces
@@ -15,7 +16,7 @@ import { isAgentId } from "../config/model-selector.js";
15
16
  * prose-and-shell markdown the operator may copy verbatim — with the fence grown past any
16
17
  * backtick run the text itself contains, so a prompt that quotes a fence still round-trips.
17
18
  * Unset things render as explicit `_(none)_` lines rather than disappearing, so an
18
- * un-overridden loop reads as fully defaulted, not as a truncated report. */
19
+ * un-overridden role reads as fully defaulted, not as a truncated report. */
19
20
  /** A code fence one longer than any backtick run in the fenced text, so the block always
20
21
  * closes where the text itself would have ended it. */
21
22
  function fenceFor(text) {
@@ -31,22 +32,22 @@ function fencedOrNone(label, text, noneNote = "none") {
31
32
  const fence = fenceFor(text);
32
33
  return [`## ${label}`, "", fence, text.replace(/\n+$/, ""), fence, ""];
33
34
  }
34
- /** The Notebook and Next tick prompt blocks and the "more waits" disclosure, as Markdown
35
- * lines — the read-only inspection the TUI's notebook view and `tumwater role <id>` share,
36
- * so the terminal and the CLI show one loop's notebook and assembled prompt identically.
35
+ /** The Notes and Next run prompt blocks and the "more waits" disclosure, as Markdown
36
+ * lines — the read-only inspection the TUI's notes view and `tumwater role <id>` share,
37
+ * so the terminal and the CLI show one role's notes and assembled prompt identically.
37
38
  * `renderRoleMarkdown` splices these at the same position, so the extracted function and the
38
39
  * full document cannot drift. */
39
40
  export function inspectMarkdownLines(p) {
40
41
  const running = chainStepLine(p);
41
42
  return [...(running ? [running, ""] : []), ...inspectBodyLines(p)];
42
43
  }
43
- /** The notebook, prompt and disclosure blocks alone — what renderRoleMarkdown splices after
44
+ /** The notes, prompt and disclosure blocks alone — what renderRoleMarkdown splices after
44
45
  * its own Team lines, which already carry the fallback-step bullet. */
45
46
  function inspectBodyLines(p) {
46
47
  const lines = [];
47
48
  lines.push(...fencedOrNone("Notes", p.notes, "none yet — the role's own earlier runs write them, and the director has none"));
48
49
  lines.push(...fencedOrNone("Next run's prompt", p.nextPrompt, "nothing to do this run"));
49
- // tick-prompt.ts dequeues exactly one queued prompt per tick, so with more than one
50
+ // run-prompt.ts dequeues exactly one queued prompt per run, so with more than one
50
51
  // waiting the block above shows only the oldest — say so, so the count line and the
51
52
  // block never read as a contradiction (the 2026-10-04 BUGS.md entry under ## Fixed).
52
53
  if (p.inboxCount > 1 && p.nextPrompt !== null && p.nextPrompt.trim() !== "") {
@@ -59,11 +60,11 @@ function inspectBodyLines(p) {
59
60
  * `model` alone when only the model is (the join simply drops unset halves) — shared by the
60
61
  * main line and the budget-fallback line, so the two render the same wiring the same way. */
61
62
  function modelPair(provider, model) {
62
- return [provider, model].filter(Boolean).join("/");
63
+ return joinParts([provider, model], "/");
63
64
  }
64
65
  /** The backend's own-default label when no model resolves: `<agent> default` — pi's
65
66
  * historical `pi default` for the pi backend, `claude default` for claude (part 4f/7). */
66
- function agentDefault(p) {
67
+ function backendDefault(p) {
67
68
  return `${p.backend !== "pi" ? p.backend : "pi"} default`;
68
69
  }
69
70
  /** The provider/model wiring as one readable token: `provider/model` when both resolve,
@@ -75,11 +76,11 @@ function modelLine(p) {
75
76
  // level (piDefault is formatted with the effective thinking winning), so the separate
76
77
  // ` (thinking: …)` suffix is dropped for this case only.
77
78
  if (p.piDefault)
78
- return `${agentDefault(p)} (${p.piDefault})`;
79
+ return `${backendDefault(p)} (${p.piDefault})`;
79
80
  const pair = modelPair(p.provider, p.model);
80
81
  const thinking = p.thinking ? ` (thinking: ${p.thinking})` : "";
81
82
  if (!pair)
82
- return `${agentDefault(p)}${thinking}`;
83
+ return `${backendDefault(p)}${thinking}`;
83
84
  const agent = p.backend !== "pi" ? `${p.backend}/` : "";
84
85
  return `${agent}${pair}${thinking}`;
85
86
  }
@@ -89,9 +90,8 @@ function modelLine(p) {
89
90
  * backend-default label. One home for the Model fallback and Budget fallback lines, so a
90
91
  * claude fallback reads `claude/claude-sonnet-5-5`, not a bare id (PLANS.md "Mixed claude +
91
92
  * pi fleets, part 3/4" gap 7). */
92
- function fallbackPairLabel(fb, agentDefault) {
93
- const agentBackend = (fb.backend !== undefined && fb.backend !== "pi" ? fb.backend : undefined) ??
94
- (fb.provider !== undefined && isAgentId(fb.provider) ? fb.provider : undefined);
93
+ function fallbackPairLabel(fb, backendDefault) {
94
+ const agentBackend = fb.backend !== undefined && fb.backend !== "pi" ? fb.backend : undefined;
95
95
  if (fb.model !== undefined) {
96
96
  if (agentBackend !== undefined)
97
97
  return `${agentBackend}/${fb.model}`;
@@ -99,12 +99,27 @@ function fallbackPairLabel(fb, agentDefault) {
99
99
  }
100
100
  if (fb.provider !== undefined)
101
101
  return fb.provider;
102
- return agentDefault;
102
+ return backendDefault;
103
103
  }
104
- /** Why a tier moved down its fallback chain, as the role view words it. */
105
- function chainReasonPhrase(reason, windowType) {
104
+ /** The backend-and-window label the role view shows for a non-claude backend's window:
105
+ * `backendDisplayName(billingBackend)` beside `windowLabel(windowType)` — "Codex 7d" for a
106
+ * codex subscription (plans/codex-backend.md part 3c/9), so a reader knows whose window it
107
+ * is. Claude keeps today's bare window type ("5h") so its role view stays byte-identical. */
108
+ function billingWindowLabel(billingBackend, windowType) {
109
+ const type = windowType !== undefined ? windowLabel(windowType) : "";
110
+ const backend = billingBackend !== undefined && billingBackend !== "claude" ? backendDisplayName(billingBackend) : "";
111
+ return [backend, type].filter(Boolean).join(" ");
112
+ }
113
+ /** Why a tier moved down its fallback chain, as the role view words it. The window phrase
114
+ * names the backend in play ("Codex 7d window used up") when a non-claude backend moved the
115
+ * role; claude keeps today's wording byte-identical, and an unknown backend falls back to
116
+ * today's hardcoded "Claude". */
117
+ function chainReasonPhrase(reason, billingBackend, windowType) {
106
118
  switch (reason) {
107
119
  case "window":
120
+ if (billingBackend !== undefined && billingBackend !== "claude") {
121
+ return `${billingWindowLabel(billingBackend, windowType)} window used up`;
122
+ }
108
123
  return `${windowType !== undefined ? claudeWindowLabel(windowType) : "Claude"} window used up`;
109
124
  case "reserve":
110
125
  return "window reserve reached";
@@ -125,10 +140,10 @@ function chainStepLine(p) {
125
140
  if (!c)
126
141
  return null;
127
142
  const resets = c.returnsAt !== undefined ? `, resets ${clockHm(c.returnsAt)}` : "";
128
- return `- Running on fallback ${c.step} of ${c.of} (${chainReasonPhrase(c.reason, p.billingWindow)}${resets})`;
143
+ return `- Running on fallback ${c.step} of ${c.of} (${chainReasonPhrase(c.reason, p.billingBackend, p.billingWindow)}${resets})`;
129
144
  }
130
- /** One recent tick outcome as a Markdown bullet line — `<time> · <result> · <span> —
131
- * <summary>` — so `tumwater role <id>` and the TUI notebook view read a loop's discrete
145
+ /** One recent run outcome as a Markdown bullet line — `<time> · <result> · <span> —
146
+ * <summary>` — so `tumwater role <id>` and the TUI notes view read a role's discrete
132
147
  * outcomes at a glance. `time` is the same local `YYYY-MM-DD HH:MM:SS` stamp the model-
133
148
  * fallback `since` prints (formatTimestamp); `span` is the same short-duration phrasing
134
149
  * shortSpanPhrase renders for holdPhrase and the event feed's countdowns; the summary is
@@ -142,29 +157,29 @@ function recentOutcomeLine(o) {
142
157
  line += ` — ${o.summary}`;
143
158
  return line;
144
159
  }
145
- /** One quiet-hours bullet: the window, whose it is, and whether it holds the loop now — with
160
+ /** One quiet-hours bullet: the window, whose it is, and whether it holds the role now — with
146
161
  * the same `quiet until <end>` text the status table uses. */
147
162
  function quietHoursLine(w) {
148
163
  const whose = w.scope === "role" ? "own window" : "team window";
149
164
  const state = w.inWindow ? `holding now — quiet until ${quietWindowEnd(w.window)}` : "not holding now";
150
165
  return `- Quiet hours: ${w.window} (${whose}; ${state})`;
151
166
  }
152
- /** The loop's schedule as Markdown bullet lines — `Next tick`, `Last tick`, then one
167
+ /** The role's schedule as Markdown bullet lines — `Next run`, `Last run`, then one
153
168
  * `Quiet hours` line per governing window (none when the payload carries no windows) — in the
154
169
  * same words and wording family as the team surface's `next run` cell (status-render.ts's
155
- * nextRunCell), so the role view and the status table cannot drift on when a loop acts next:
156
- * a never-scheduled loop (nextRunAt 0) reads `not scheduled yet`; a due loop reads `due now`;
170
+ * nextRunCell), so the role view and the status table cannot drift on when a role acts next:
171
+ * a never-scheduled role (nextRunAt 0) reads `not scheduled yet`; a due role reads `due now`;
157
172
  * a future one reads `in <Ns>` through humanSeconds, prefixed `backoff ` while backoffSeconds
158
173
  * > 0 (the wake row-action clears exactly that) — the shared nextRunPhrase, so the role view
159
174
  * and the status `next run` cell cannot drift on the backoff prefix or the humanSeconds
160
- * bucketing. `Last tick` reuses formatTimestamp and the same `humanSeconds … ago` phrasing the
161
- * status `last tick` cell ages with, or `never` for a loop that has not ticked. `now` is
175
+ * bucketing. `Last run` reuses formatTimestamp and the same `humanSeconds … ago` phrasing the
176
+ * status `last run` cell ages with, or `never` for a role that has not run. `now` is
162
177
  * injected (default Date.now()) so the renderer stays a pure function of the payload and the
163
178
  * test can pin a fixed clock. */
164
179
  function scheduleLine(p, now) {
165
180
  // The yield multiplier rides the line as `×N` exactly like nextRunCell/fmtNextRun: a quiet
166
- // yield-scaled loop's effective gap is longer than nextRunAt's countdown shows (isEligible
167
- // gates on the multiplier too), so the Schedule must not understate when the loop acts next.
181
+ // yield-scaled role's effective gap is longer than nextRunAt's countdown shows (isEligible
182
+ // gates on the multiplier too), so the Schedule must not understate when the role acts next.
168
183
  const suffix = yieldMultiplierSuffix(p.yieldMultiplier);
169
184
  let next;
170
185
  if (p.nextRunAt <= 0) {
@@ -175,11 +190,11 @@ function scheduleLine(p, now) {
175
190
  next = nextRunPhrase(remain, p.backoffSeconds > 0, "due now", true) + suffix;
176
191
  }
177
192
  const last = p.lastRunEndedAt !== undefined
178
- ? `${formatTimestamp(p.lastRunEndedAt)} (${humanSeconds(secondsSince(p.lastRunEndedAt, now))} ago)`
193
+ ? `${formatTimestamp(p.lastRunEndedAt)} (${humanAgo(p.lastRunEndedAt, now)})`
179
194
  : "never";
180
195
  return [`- Next run: ${next}`, `- Last run: ${last}`, ...(p.quietHours ?? []).map(quietHoursLine)];
181
196
  }
182
- /** Render one loop's inspection payload as Markdown. `now` is injected (default Date.now())
197
+ /** Render one role's inspection payload as Markdown. `now` is injected (default Date.now())
183
198
  * so the schedule lines stay a pure function of the payload — the test pins a fixed clock. */
184
199
  export function renderRoleMarkdown(p, now = Date.now()) {
185
200
  const lines = [`# tumwater role: ${p.id}`, ""];
@@ -194,15 +209,15 @@ export function renderRoleMarkdown(p, now = Date.now()) {
194
209
  // unresolvable pair, a model with no declared window).
195
210
  lines.push(`- Context window: ${p.contextWindow === null ? "unknown" : `${compactTokens(p.contextWindow)} tokens`}`);
196
211
  // Per-provider concurrency cap (plans/provider-concurrency.md §8): name the key whose cap
197
- // bounds the role's concurrent ticks, plus the cap itself. Present only while
212
+ // bounds the role's concurrent runs, plus the cap itself. Present only while
198
213
  // `maxConcurrentPerProvider` maps this seam's key — a cap-less default team has no
199
214
  // resolvable key entry, so its role view renders no new line and stays byte-identical.
200
215
  if (p.providerKey !== undefined && p.providerCap !== undefined) {
201
216
  lines.push(`- Provider: ${p.providerKey} (cap ${p.providerCap})`);
202
217
  }
203
- // Image input (plans/image-input.md "Surfaces", part 5/5): whether this loop's own model
204
- // reads images, and — when it cannot — where a tick that reads an image reroutes. Not just
205
- // `yes`/`no`: a text-only loop that falls back names the target, so `tumwater role <id>`
218
+ // Image input (plans/image-input.md "Surfaces", part 5/5): whether this role's own model
219
+ // reads images, and — when it cannot — where a run that reads an image reroutes. Not just
220
+ // `yes`/`no`: a text-only role that falls back names the target, so `tumwater role <id>`
206
221
  // explains the team alert without running doctor. Unknown (null) renders `unknown` rather
207
222
  // than guessing.
208
223
  const images = p.imageInput === true
@@ -216,11 +231,11 @@ export function renderRoleMarkdown(p, now = Date.now()) {
216
231
  // Model failure fallback, part 2/2: name the off-model pair, when the episode began, and
217
232
  // the provider-class failure that tripped it, so `tumwater role <id>` explains the state.
218
233
  if (p.modelFallback) {
219
- const pair = fallbackPairLabel(p.modelFallback, agentDefault(p));
234
+ const pair = fallbackPairLabel(p.modelFallback, backendDefault(p));
220
235
  lines.push(`- Model fallback: ${pair} — on fallback since ${formatTimestamp(p.modelFallback.since)} (primary failing: ${p.modelFallback.reason})`);
221
236
  }
222
237
  if (p.fallback) {
223
- const pair = fallbackPairLabel(p.fallback, agentDefault(p));
238
+ const pair = fallbackPairLabel(p.fallback, backendDefault(p));
224
239
  lines.push(`- Over-cap model: ${pair} (${p.fallbackFree ? "free" : "priced"})`);
225
240
  }
226
241
  const running = chainStepLine(p);
@@ -229,17 +244,19 @@ export function renderRoleMarkdown(p, now = Date.now()) {
229
244
  // The claude seam's billing (plans/agent-backends.md §7/§8, PLANS.md part 6d/7): absent on
230
245
  // a pi seam, so its role view is byte-identical to before.
231
246
  if (p.billing) {
232
- const window = p.billing === "subscription" && p.billingWindow ? ` (${claudeWindowLabel(p.billingWindow)} window)` : "";
247
+ const window = p.billing === "subscription" && p.billingWindow
248
+ ? ` (${billingWindowLabel(p.billingBackend, p.billingWindow)} window)`
249
+ : "";
233
250
  lines.push(`- Billing: ${p.billing === "api" ? "API" : p.billing}${window}`);
234
251
  }
235
252
  lines.push(`- Min run interval: ${p.minRunIntervalSeconds}s`);
236
253
  if (p.instances !== undefined)
237
254
  lines.push(`- Instances: ${p.instances}`);
238
255
  lines.push(`- Inbox: ${p.inboxCount}`);
239
- // The loop's own spend over its retained event window (role-view.ts's usage, summed from the
240
- // same run_end events recentOutcomes reads): how much the loop burned on tokens and cost,
256
+ // The role's own spend over its retained event window (role-view.ts's usage, summed from the
257
+ // same run_end events recentOutcomes reads): how much the role burned on tokens and cost,
241
258
  // with a notional suffix only when a claude-subscription seam burned non-zero notional spend
242
- // (plans/agent-backends.md part 6a). Absent for a never-costly loop, so old `tumwater role`
259
+ // (plans/agent-backends.md part 6a). Absent for a never-costly role, so old `tumwater role`
243
260
  // goldens are byte-identical.
244
261
  if (p.usage) {
245
262
  let spend = `- Spend: ${usd(p.usage.costUsd)} · ${compactTokens(p.usage.tokens)} tokens`;
@@ -248,7 +265,7 @@ export function renderRoleMarkdown(p, now = Date.now()) {
248
265
  lines.push(spend);
249
266
  }
250
267
  // The per-role daily cap (role-view.ts's dailyCap): today's spend against the cap and whether
251
- // it holds the loop now. Absent without a cap, so old goldens are unchanged.
268
+ // it holds the role now. Absent without a cap, so old goldens are unchanged.
252
269
  if (p.dailyCap) {
253
270
  const dc = p.dailyCap;
254
271
  let cap = `- Daily cap: ${usd(dc.spentUsd)} of ${usd(dc.capUsd)} spent today (${dc.over ? "holding now — no new runs until local midnight" : "not holding"})`;
@@ -256,14 +273,14 @@ export function renderRoleMarkdown(p, now = Date.now()) {
256
273
  cap += ` · shared by ${dc.roleIds.join(", ")}`;
257
274
  lines.push(cap);
258
275
  }
259
- // The loop's schedule (role-view.ts's nextRunAt/backoffSeconds/lastRunEndedAt, read from
260
- // the same persisted state): when it may tick again, and when it last did — the one
261
- // place the team's `next run`/`last tick` vocabulary was absent. Rendered from the
276
+ // The role's schedule (role-view.ts's nextRunAt/backoffSeconds/lastRunEndedAt, read from
277
+ // the same persisted state): when it may run again, and when it last did — the one
278
+ // place the team's `next run`/`last run` vocabulary was absent. Rendered from the
262
279
  // injected `now`, so the lines stay a pure function of the payload.
263
280
  lines.push(...scheduleLine(p, now));
264
- // The loop's recent tick outcomes (role-view.ts's recentOutcomes, read from the event log):
265
- // one line per outcome so `tumwater role <id>` shows what the loop has been DOING, not just
266
- // what it is. Absent (never ticked) renders nothing new, so old goldens are byte-identical.
281
+ // The role's recent run outcomes (role-view.ts's recentOutcomes, read from the event log):
282
+ // one line per outcome so `tumwater role <id>` shows what the role has been DOING, not just
283
+ // what it is. Absent (never run) renders nothing new, so old goldens are byte-identical.
267
284
  if (p.recentOutcomes && p.recentOutcomes.length > 0) {
268
285
  lines.push("", "## Recent runs", "");
269
286
  for (const o of p.recentOutcomes)
@@ -1,8 +1,7 @@
1
- // vocabulary: plain
2
- /** One loop's read-only inspection payload — the collector behind `tumwater role <id>`:
3
- * what the loop IS (catalog entry or custom, enabled, paused, tier), what it RUNS ON
1
+ /** One role's read-only inspection payload — the collector behind `tumwater role <id>`:
2
+ * what the role IS (catalog entry or custom, enabled, paused, tier), what it RUNS ON
4
3
  * (resolved provider/model, the budget fallback pair), what it CARRIES (the instructions
5
- * override and the role's own find text verbatim), and what its NEXT tick's prompt will
4
+ * override and the role's own find text verbatim), and what its NEXT run's prompt will
6
5
  * read like, assembled through the preview seam so nothing queued is consumed. Split out
7
6
  * of cli.ts like backlogPayload and status/status-data.ts's snapshot so the collector is testable
8
7
  * without the CLI and one renderer consumes exactly this shape. Every read degrades like
@@ -13,21 +12,23 @@ import { defaultConfig, enabledRoleIds, isCustomRole, knownRoleIds, loadConfigSa
13
12
  import { DIRECTOR_ROLE, customRole, roleById, roleTier, unknownRoleMessage } from "./roles.js";
14
13
  import { queuedRolePromptCount } from "../inbox/inbox.js";
15
14
  import { pausedRoles } from "../team/team-state.js";
16
- import { loadLoopState } from "../loop/loop-state.js";
17
- import { baseRoleOf, loopIds, loopPaused } from "./loop-ids.js";
15
+ import { loadRunnerState } from "../runner/runner-state-load.js";
16
+ import { baseRoleOf, runnerIds, runnerPaused } from "./runner-ids.js";
18
17
  import { roleCapVerdicts } from "../gates/role-cap-gates.js";
19
18
  import { yieldMultiplierFor } from "../scheduling/backoff.js";
20
19
  import { quietHoursStatus } from "../scheduling/quiet-hours.js";
21
- import { assembleTickPrompt } from "../tick/tick-prompt.js";
20
+ import { assembleRunPrompt } from "../run/run-prompt.js";
22
21
  import { configForRole, resolvedModelFields, reviewRunConfig, roleSeamTier, seamConcurrencyKey } from "../config/config-views.js";
23
22
  import { fallbackPair, modelFallbackView } from "../config/config-fallbacks.js";
24
23
  import { fallbackModelPair, piModelsPath, seamContextWindow } from "../pi/pi-models.js";
25
24
  import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
26
25
  import { formatModelSelector } from "../config/model-selector.js";
27
26
  import { roleImageInfo } from "../agent/image-input.js";
27
+ import { readPiListedModels } from "../pi/pi-listed-models.js";
28
28
  import { readImageProbeCache } from "../doctor/image-probe.js";
29
29
  import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
30
- import { claudeWindowEntries, readClaudeAccount } from "../agent/claude/claude-account.js";
30
+ import { readClaudeAccount } from "../agent/claude/claude-account.js";
31
+ import { readAccount, windowEntries } from "../agent/subscription-account.js";
31
32
  import { CONFIG_BASENAME, configPath, roleNotesPath } from "../paths.js";
32
33
  import { readTextOrNull } from "../files/files.js";
33
34
  import { eventRole, eventUsage, readEvents } from "../events/event-read.js";
@@ -49,7 +50,7 @@ export function rawRoleField(root, roleId, field) {
49
50
  return undefined;
50
51
  return obj[field];
51
52
  }
52
- /** Collect one loop's inspection payload. `modelsPath` overrides pi's model definitions location
53
+ /** Collect one role's inspection payload. `modelsPath` overrides pi's model definitions location
53
54
  * — a test seam, like status/status-data.ts's snapshot — consulted only when a fallback pair is
54
55
  * configured (the freeness verdict needs the definitions; nothing else does). */
55
56
  export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now = new Date()) {
@@ -61,30 +62,37 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
61
62
  const cfg = config ?? defaultConfig();
62
63
  const catalog = roleById(role);
63
64
  const customEntry = cfg.customRoles.find((c) => c.name === role);
64
- // The director is inspectable like any loop but has no catalog entry: a pseudo-Role whose
65
+ // The director is inspectable like any role but has no catalog entry: a pseudo-Role whose
65
66
  // find text is empty (rolePayload nulls it below — the director's queued prompts ARE its
66
67
  // work), so the unknown-role throw only fires for ids nothing can answer.
67
68
  const resolved = catalog ??
68
69
  (customEntry ? customRole(customEntry.name, customEntry.task)
69
70
  : role === DIRECTOR_ROLE ? { id: DIRECTOR_ROLE, title: DIRECTOR_ROLE, find: "" }
70
71
  : undefined);
71
- if (!resolved) {
72
+ // roleById maps any `feature-N` to its base whatever the config runs, so an instance id
73
+ // counts only when knownRoleIds (the ids the config runs) holds it.
74
+ if (!resolved || !knownRoleIds(cfg).includes(role)) {
72
75
  // The one unknown-role answer (unknownRoleMessage): the CLI's main catch turns the throw
73
76
  // into `tumwater: <message>` with exit 1, so the collector keeps the wording and the CLI
74
77
  // stays a thin peel-parse-print layer.
75
78
  throw new Error(unknownRoleMessage(role, knownRoleIds(cfg)));
76
79
  }
77
80
  const effective = configForRole(cfg, role);
78
- // The claude billing answer, read when a claude seam is in play — the role's OWN seam, or
79
- // the review gate's when the reviewer runs claude (PLANS.md "Mixed claude + pi fleets, part
80
- // 3/4" gap 7): a pi author on a claude reviewer still shows where its changes' spend lands.
81
- // Its subscription window's primary type rides along for the renderer's label. The account
82
- // file is team-global, so either seam reads the same answer.
83
- const claudeInPlay = effective.agent === "claude" || (cfg.review.enabled && reviewRunConfig(cfg).agent === "claude");
84
- const account = claudeInPlay ? readClaudeAccount(root) : null;
81
+ // The subscription backend whose billing is in play — the role's OWN seam when it routes
82
+ // to a non-pi backend, else the review gate's backend when the reviewer runs a non-pi
83
+ // backend (today's claude precedence generalized off the literal "claude", part 3c/9;
84
+ // PLANS.md "Mixed claude + pi fleets, part 3/4" gap 7): a pi author on a claude/codex
85
+ // reviewer still shows where its changes' spend lands. Read that backend's account; the
86
+ // account file is team-global, so either seam reads the same answer. Its subscription
87
+ // window's primary type rides along for the renderer's label.
88
+ const roleBackend = effective.agent !== undefined && effective.agent !== "pi" ? effective.agent : undefined;
89
+ const reviewAgent = reviewRunConfig(cfg).agent;
90
+ const reviewBackend = cfg.review.enabled && reviewAgent !== undefined && reviewAgent !== "pi" ? reviewAgent : undefined;
91
+ const billingBackend = roleBackend ?? reviewBackend;
92
+ const account = billingBackend !== undefined ? readAccount(root, billingBackend) : null;
85
93
  const info = readOrchestratorInfo(root);
86
94
  const chainStep = (orchestratorAlive(root, info) && info?.chain?.[roleSeamTier(cfg, role)]) || null;
87
- const billingWindow = account?.billing === "subscription" ? claudeWindowEntries(account)[0]?.type : undefined;
95
+ const billingWindow = account?.billing === "subscription" ? windowEntries(account)[0]?.type : undefined;
88
96
  // pi's own saved default for a pi seam that resolves no model, read once beside the models
89
97
  // catalog this collector already takes as a seam (piSettingsPath). The config's `thinking`
90
98
  // wins over the default's because tumwater passes it to pi as `--thinking`, mirroring the
@@ -117,28 +125,34 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
117
125
  // gate does) keeps them on one answer. OpenCode's `catalog` is threaded the way claude's
118
126
  // `account` is: absent, an opencode pair reads NOT free.
119
127
  const opencodeCatalog = readOpencodeCache(root);
120
- const freeFallback = declaredFallback ? fallbackModelPair(cfg, modelsPath, readClaudeAccount(root), opencodeCatalog) : null;
128
+ // The claude account answer (6a/7), read the way the budget gate reads it — always, so the
129
+ // fallback price verdict sees the subscription even when claude is not this role's own seam
130
+ // (a pi role may still fall back to claude). The lookup returns it only for "claude" (part
131
+ // 2a/9) — no caller asks for another backend yet.
132
+ const claudeFallbackAccount = readClaudeAccount(root);
133
+ const accounts = (b) => (b === "claude" ? claudeFallbackAccount : null);
134
+ const freeFallback = declaredFallback ? fallbackModelPair(cfg, modelsPath, accounts, opencodeCatalog) : null;
121
135
  const fallback = freeFallback ?? declaredFallback;
122
- // One state read serves both the model-fallback episode and the next-tick preview below.
123
- const state = loadLoopState(root, role);
136
+ // One state read serves both the model-fallback episode and the next-run preview below.
137
+ const state = loadRunnerState(root, role);
124
138
  const episode = state.modelFallback;
125
- // The loop's recent tick outcomes, read from the event log (readEvents) so the command
139
+ // The role's recent run outcomes, read from the event log (readEvents) so the command
126
140
  // answers with the team stopped and never touches live state. readEvents returns the log's
127
141
  // newest events in file (chronological) order, so filtering to this role's run_end events
128
142
  // and taking the last 8 yields the newest 8, reversed to newest-first. Durations and
129
143
  // summaries ride along only when the event carried them (a torn or empty line drops them),
130
- // and an empty result is omitted so a never-ticked loop carries no `recentOutcomes` at all.
131
- // One event-log read serves both per-loop facts — the recent outcomes and the spend sum — so
144
+ // and an empty result is omitted so a never-run role carries no `recentOutcomes` at all.
145
+ // One event-log read serves both per-role facts — the recent outcomes and the spend sum — so
132
146
  // the two cannot drift on what they cover: filtered to this role's run_end events once, then
133
147
  // the outcome projection keeps the newest valid 8 while the usage fold sums the whole window.
134
148
  const events = readEvents(root);
135
- const tickEnds = events.filter((ev) => ev.type === "run_end" && eventRole(ev) === role);
149
+ const runEnds = events.filter((ev) => ev.type === "run_end" && eventRole(ev) === role);
136
150
  // The event log is read without a shape filter, so a torn or hand-edited run_end line can
137
151
  // carry a non-numeric ts/durationMs or an empty result — the same foreign shape every other
138
152
  // consumer guards with finiteNumber. A line with no usable ts or result is as unusable as
139
- // an absent one (the same "a torn line carries no usable timestamp" rule eventTick/tickStartMap
153
+ // an absent one (the same "a torn line carries no usable timestamp" rule eventRun/runStartMap
140
154
  // apply), so it contributes no outcome; a non-numeric durationMs is dropped like a missing one.
141
- const recentOutcomes = tickEnds
155
+ const recentOutcomes = runEnds
142
156
  .filter((ev) => {
143
157
  return finiteNumber(ev.ts, null) !== null && typeof ev.result === "string" && ev.result !== "";
144
158
  })
@@ -150,13 +164,13 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
150
164
  ...(typeof ev.summary === "string" && ev.summary !== "" ? { summary: ev.summary } : {}),
151
165
  }))
152
166
  .reverse();
153
- // The loop's own spend over the same retained window, summed per-run_end through the shared
167
+ // The role's own spend over the same retained window, summed per-run_end through the shared
154
168
  // eventUsage guard (a torn or hand-edited run_end line contributes nothing: a non-numeric
155
169
  // field reads as 0, and a line without a usable ts is as unusable as an absent one, matching
156
- // recentOutcomes's drop rule so a torn tick never silently spends). Omitted when all three
157
- // sums are 0, keeping a never-costly loop's JSON unchanged (the report's zero-means-absent
170
+ // recentOutcomes's drop rule so a torn run never silently spends). Omitted when all three
171
+ // sums are 0, keeping a never-costly role's JSON unchanged (the report's zero-means-absent
158
172
  // convention).
159
- const usage = tickEnds.reduce((acc, ev) => {
173
+ const usage = runEnds.reduce((acc, ev) => {
160
174
  if (finiteNumber(ev.ts, null) === null)
161
175
  return acc;
162
176
  const u = eventUsage(ev);
@@ -193,15 +207,15 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
193
207
  const capUsd = cfg.maxDailyCostUsdPerRole?.[base];
194
208
  let dailyCap;
195
209
  if (role !== DIRECTOR_ROLE && typeof capUsd === "number" && Number.isFinite(capUsd) && capUsd > 0) {
196
- const ids = loopIds(cfg).filter((id) => baseRoleOf(id) === base);
210
+ const ids = runnerIds(cfg).filter((id) => baseRoleOf(id) === base);
197
211
  if (!ids.includes(role))
198
212
  ids.push(role);
199
- const runners = ids.map((id) => ({ role: id, state: id === role ? state : loadLoopState(root, id) }));
213
+ const runners = ids.map((id) => ({ role: id, state: id === role ? state : loadRunnerState(root, id) }));
200
214
  const verdict = roleCapVerdicts(runners, cfg.maxDailyCostUsdPerRole, now.getTime()).get(base);
201
215
  if (verdict)
202
216
  dailyCap = { capUsd, spentUsd: verdict.spentUsd, over: verdict.over, roleIds: verdict.roleIds };
203
217
  }
204
- // The role's notebook, read for display. The same degradation as every other read here: a
218
+ // The role's notes, read for display. The same degradation as every other read here: a
205
219
  // missing, empty, or unreadable file is simply "no note yet". The director has none.
206
220
  let note = null;
207
221
  if (role !== DIRECTOR_ROLE) {
@@ -227,7 +241,7 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
227
241
  title: resolved.title,
228
242
  custom: isCustomRole(cfg, role),
229
243
  enabled: enabledRoleIds(cfg).includes(role),
230
- paused: loopPaused(new Set(pausedRoles(root)), role),
244
+ paused: runnerPaused(new Set(pausedRoles(root)), role),
231
245
  tier: roleTier(role),
232
246
  modelTier: roleSeamTier(cfg, role),
233
247
  ...resolvedModelFields(effective.provider, effective.model, effective.thinking),
@@ -245,10 +259,10 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
245
259
  fallback,
246
260
  fallbackFree: freeFallback !== null,
247
261
  modelFallback: modelFallbackView(episode, cfg, role),
248
- ...(account !== null ? { billing: account.billing } : {}),
262
+ ...(account !== null ? { billing: account.billing, billingBackend } : {}),
249
263
  ...(billingWindow !== undefined ? { billingWindow } : {}),
250
264
  chainStep,
251
- ...roleImageInfo(cfg, role, modelsPath, readImageProbeCache(root)),
265
+ ...roleImageInfo(cfg, role, modelsPath, readImageProbeCache(root), readPiListedModels(root)),
252
266
  minRunIntervalSeconds: effective.minRunIntervalSeconds,
253
267
  ...(minRunIntervalOverride !== undefined ? { minRunIntervalOverride } : {}),
254
268
  ...(providerOverride !== undefined ? { providerOverride } : {}),
@@ -261,7 +275,7 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
261
275
  ...(cfg.roles[base]?.instances !== undefined ? { instances: cfg.roles[base].instances } : {}),
262
276
  // The roles.<base>.instructions override verbatim — resolved through the base role like
263
277
  // the cap and quiet hours, so an instance id shows (and can edit) the override its runs
264
- // carry (tick-prompt reads config.roles[base]); null when unset.
278
+ // carry (run-prompt reads config.roles[base]); null when unset.
265
279
  instructions: cfg.roles[baseRoleOf(role)]?.instructions ?? null,
266
280
  find: role === DIRECTOR_ROLE ? null : resolved.find,
267
281
  notes: note,
@@ -271,20 +285,18 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
271
285
  // declared window) — the same seam `- Next run:` previews. Independent of the fallback.
272
286
  contextWindow,
273
287
  // The preview seam: the queued prompt (if any) is read, not consumed, so an inspection
274
- // never costs the loop its queued work — the property the tick-prompt test pins.
275
- nextPrompt: assembleTickPrompt({
288
+ // never costs the role its queued work — the property the run-prompt test pins.
289
+ nextPrompt: assembleRunPrompt({
276
290
  root,
277
291
  config: cfg,
278
292
  role,
279
293
  state,
280
294
  preview: true,
281
- // The compact prompt every run gets (loop-tick.ts), so the preview is the real prompt.
282
- compact: true,
283
295
  })?.prompt ?? null,
284
- // The loop's schedule — surfaced read-only from the same persisted state rolePayload
285
- // already loaded for the model-fallback episode and the next-tick preview, so no new
286
- // read path exists and the collector stays one loadLoopState per role. `lastRunEndedAt`
287
- // is omitted (JSON unchanged) until the loop has actually ticked.
296
+ // The role's schedule — surfaced read-only from the same persisted state rolePayload
297
+ // already loaded for the model-fallback episode and the next-run preview, so no new
298
+ // read path exists and the collector stays one loadRunnerState per role. `lastRunEndedAt`
299
+ // is omitted (JSON unchanged) until the role has actually run.
288
300
  nextRunAt: state.nextRunAt,
289
301
  backoffSeconds: state.backoffSeconds,
290
302
  yieldMultiplier: yieldMultiplierFor(state),