tumwater 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/README.md +63 -28
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-eligibility.js +332 -43
  26. package/dist/src/backlog/backlog-md.js +79 -17
  27. package/dist/src/backlog/backlog-render.js +62 -11
  28. package/dist/src/backlog/backlog-structure.js +261 -58
  29. package/dist/src/backlog/backlog-write.js +178 -441
  30. package/dist/src/backlog/backlog.js +113 -16
  31. package/dist/src/backlog/question-answer.js +339 -0
  32. package/dist/src/baseline/main-baseline.js +75 -88
  33. package/dist/src/baseline/main-red.js +32 -30
  34. package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
  35. package/dist/src/{brief.js → brief/brief.js} +33 -8
  36. package/dist/src/budget/budget.js +32 -15
  37. package/dist/src/budget/fallback-breaker.js +8 -7
  38. package/dist/src/build/build-check-detect.js +8 -7
  39. package/dist/src/build/build-check-events.js +68 -22
  40. package/dist/src/build/build-check-report.js +22 -23
  41. package/dist/src/build/build-check-scoped.js +51 -62
  42. package/dist/src/build/build-check.js +15 -14
  43. package/dist/src/build/build-info.js +2 -1
  44. package/dist/src/build/build-stage.js +23 -22
  45. package/dist/src/build/dep-install.js +22 -21
  46. package/dist/src/build/host-sleep.js +1 -1
  47. package/dist/src/change/change-data.js +13 -11
  48. package/dist/src/change/change-render.js +11 -10
  49. package/dist/src/cli/brief-command.js +9 -7
  50. package/dist/src/cli/cli-args.js +25 -5
  51. package/dist/src/cli/cli-command-args.js +43 -10
  52. package/dist/src/cli/cli-editor.js +13 -1
  53. package/dist/src/cli/cli-flag-specs.js +31 -6
  54. package/dist/src/cli/cli-marker-commands.js +13 -10
  55. package/dist/src/cli/cli-query-commands.js +439 -56
  56. package/dist/src/cli/cli-repo-gate.js +1 -1
  57. package/dist/src/cli/cli-run.js +124 -68
  58. package/dist/src/cli/completion.js +70 -12
  59. package/dist/src/cli/config-commands.js +14 -11
  60. package/dist/src/cli/help.js +264 -124
  61. package/dist/src/cli/log-commands.js +127 -21
  62. package/dist/src/cli/notify-command.js +33 -18
  63. package/dist/src/cli/question-commands.js +13 -224
  64. package/dist/src/cli.js +78 -36
  65. package/dist/src/collections.js +66 -16
  66. package/dist/src/concurrency/check-permit.js +16 -17
  67. package/dist/src/concurrency/lock.js +6 -5
  68. package/dist/src/concurrency/provider-lease.js +69 -0
  69. package/dist/src/concurrency/semaphore.js +258 -36
  70. package/dist/src/config/config-aliases.js +106 -0
  71. package/dist/src/config/config-editable-keys.js +3 -3
  72. package/dist/src/config/config-fallbacks.js +379 -0
  73. package/dist/src/config/config-field-checks.js +19 -10
  74. package/dist/src/config/config-keys.js +160 -0
  75. package/dist/src/config/config-live.js +64 -12
  76. package/dist/src/config/config-schema.js +34 -22
  77. package/dist/src/config/config-validation.js +132 -48
  78. package/dist/src/config/config-views.js +107 -325
  79. package/dist/src/config/config-write.js +138 -177
  80. package/dist/src/config/config.js +60 -49
  81. package/dist/src/config/model-selector.js +28 -2
  82. package/dist/src/doctor/doctor-backlog.js +494 -68
  83. package/dist/src/doctor/doctor-checks.js +509 -58
  84. package/dist/src/doctor/doctor-holds.js +139 -78
  85. package/dist/src/doctor/doctor-install.js +30 -0
  86. package/dist/src/doctor/doctor-launch-services.js +2 -2
  87. package/dist/src/doctor/doctor-model-checks.js +549 -157
  88. package/dist/src/doctor/doctor-notify.js +2 -3
  89. package/dist/src/doctor/doctor-orphans.js +10 -9
  90. package/dist/src/doctor/doctor-render.js +36 -9
  91. package/dist/src/doctor/doctor.js +76 -25
  92. package/dist/src/doctor/image-probe.js +244 -0
  93. package/dist/src/doctor/probe.js +64 -0
  94. package/dist/src/events/event-format.js +219 -106
  95. package/dist/src/events/event-labels.js +116 -0
  96. package/dist/src/events/event-legacy.js +140 -0
  97. package/dist/src/events/event-read.js +58 -22
  98. package/dist/src/events/event-window.js +27 -7
  99. package/dist/src/events/events.js +18 -14
  100. package/dist/src/events/notify.js +80 -30
  101. package/dist/src/failure/failure-cluster.js +12 -9
  102. package/dist/src/failure/failure-data.js +96 -48
  103. package/dist/src/failure/failure-render.js +69 -58
  104. package/dist/src/failure/failure-state-change.js +33 -26
  105. package/dist/src/failure/time-spend.js +79 -59
  106. package/dist/src/files/file-queue.js +16 -4
  107. package/dist/src/files/files.js +18 -6
  108. package/dist/src/files/json-object.js +18 -3
  109. package/dist/src/files/tail.js +1 -1
  110. package/dist/src/gates/bootstrap-gates.js +5 -4
  111. package/dist/src/gates/budget-gates.js +250 -34
  112. package/dist/src/gates/disk-gate.js +18 -18
  113. package/dist/src/gates/gate-polls.js +117 -69
  114. package/dist/src/gates/maintenance-quota.js +29 -29
  115. package/dist/src/gates/pause-gates.js +11 -11
  116. package/dist/src/gates/readiness.js +2 -1
  117. package/dist/src/gates/role-cap-gates.js +16 -21
  118. package/dist/src/gates/startup-gate.js +75 -31
  119. package/dist/src/gates/streak-gate.js +18 -16
  120. package/dist/src/git/commit-message.js +6 -5
  121. package/dist/src/git/git-diff.js +16 -16
  122. package/dist/src/git/git-run.js +8 -8
  123. package/dist/src/git/git.js +79 -59
  124. package/dist/src/git/slots-state.js +14 -5
  125. package/dist/src/git/worktree-pool.js +25 -18
  126. package/dist/src/git/worktree-use.js +15 -14
  127. package/dist/src/git/worktree.js +18 -18
  128. package/dist/src/gui/gui-args.js +10 -14
  129. package/dist/src/gui/gui-backlog-commands.js +273 -0
  130. package/dist/src/gui/gui-command.js +1 -1
  131. package/dist/src/gui/gui-config-commands.js +440 -0
  132. package/dist/src/gui/gui-endpoint-commands.js +16 -716
  133. package/dist/src/gui/gui-endpoint-common.js +49 -0
  134. package/dist/src/gui/gui-endpoints.js +105 -54
  135. package/dist/src/gui/gui-operator-commands.js +145 -0
  136. package/dist/src/gui/gui-prompt-commands.js +234 -0
  137. package/dist/src/gui/gui-server.js +25 -8
  138. package/dist/src/gui/gui-team-commands.js +164 -0
  139. package/dist/src/gui/http-body.js +17 -1
  140. package/dist/src/history/history-data.js +179 -104
  141. package/dist/src/history/history-render.js +76 -0
  142. package/dist/src/history/history.js +29 -70
  143. package/dist/src/inbox/inbox-attachments.js +66 -10
  144. package/dist/src/inbox/inbox-cancel.js +7 -5
  145. package/dist/src/inbox/inbox-edit.js +1 -1
  146. package/dist/src/inbox/inbox-submit.js +44 -4
  147. package/dist/src/inbox/inbox-top.js +83 -0
  148. package/dist/src/inbox/inbox.js +8 -2
  149. package/dist/src/inbox/pending-prompt.js +1 -1
  150. package/dist/src/inbox/prompt-commands.js +114 -73
  151. package/dist/src/init/init-templates.js +16 -14
  152. package/dist/src/init/init.js +12 -11
  153. package/dist/src/loop/leftover.js +28 -27
  154. package/dist/src/loop/loop-fallback.js +2 -2
  155. package/dist/src/loop/loop-pi.js +104 -50
  156. package/dist/src/loop/loop-recovery.js +15 -15
  157. package/dist/src/loop/loop-state.js +130 -13
  158. package/dist/src/loop/loop-tick.js +190 -88
  159. package/dist/src/loop/loop.js +68 -36
  160. package/dist/src/loop/model-fallback.js +3 -2
  161. package/dist/src/loop/revision.js +16 -7
  162. package/dist/src/merge/backlog-conflicts.js +155 -0
  163. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  164. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  165. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  166. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  167. package/dist/src/merge/merge-drain.js +212 -0
  168. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
  169. package/dist/src/merge/merge-pipeline.js +99 -0
  170. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  171. package/dist/src/merge/merge-queue-render.js +75 -0
  172. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  173. package/dist/src/merge/merge-slot.js +340 -0
  174. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  175. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  176. package/dist/src/merge/merge-verify.js +212 -0
  177. package/dist/src/notes-cli.js +97 -0
  178. package/dist/src/operator/operator-commands.js +113 -45
  179. package/dist/src/operator/operator-intent.js +74 -53
  180. package/dist/src/operator/operator-requests.js +52 -52
  181. package/dist/src/operator/retire.js +47 -28
  182. package/dist/src/operator/revert.js +71 -0
  183. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  184. package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
  185. package/dist/src/orchestrator/orchestrator.js +149 -96
  186. package/dist/src/orchestrator/retention.js +29 -12
  187. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  188. package/dist/src/orchestrator/startup-migrations.js +40 -0
  189. package/dist/src/paths.js +124 -38
  190. package/dist/src/pi/pi-args.js +26 -3
  191. package/dist/src/pi/pi-backend.js +15 -23
  192. package/dist/src/pi/pi-bin.js +18 -12
  193. package/dist/src/pi/pi-event-line.js +17 -1
  194. package/dist/src/pi/pi-failure.js +87 -0
  195. package/dist/src/pi/pi-models.js +126 -38
  196. package/dist/src/pi/pi-run-result.js +1 -1
  197. package/dist/src/pi/pi-settings.js +86 -0
  198. package/dist/src/pi/pi-stream.js +131 -107
  199. package/dist/src/pi/pi-watchdogs.js +4 -3
  200. package/dist/src/pi-extension/bounded-output.js +20 -5
  201. package/dist/src/pi-extension/context-budget.js +3 -1
  202. package/dist/src/pi-extension/context-shake.js +54 -21
  203. package/dist/src/pi-extension/context-window.js +37 -0
  204. package/dist/src/pi-extension/full-output.js +2 -2
  205. package/dist/src/pi-extension/strip-thinking.js +80 -0
  206. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  207. package/dist/src/process/launch-services.js +11 -10
  208. package/dist/src/process/process-table.js +1 -1
  209. package/dist/src/process/process.js +7 -7
  210. package/dist/src/process/run-marker.js +29 -6
  211. package/dist/src/process/supervisor.js +36 -25
  212. package/dist/src/prompt/dialect.js +30 -10
  213. package/dist/src/prompt/gate-prompts.js +170 -67
  214. package/dist/src/prompt/principles.js +1 -0
  215. package/dist/src/prompt/prompt-followup.js +29 -20
  216. package/dist/src/prompt/prompt.js +249 -91
  217. package/dist/src/redeploy/redeploy-policy.js +10 -10
  218. package/dist/src/redeploy/redeploy.js +6 -4
  219. package/dist/src/redeploy/redeployer.js +63 -55
  220. package/dist/src/redeploy/self-reload.js +1 -1
  221. package/dist/src/report/report-data.js +259 -254
  222. package/dist/src/report/report-fold.js +353 -0
  223. package/dist/src/report/report-render.js +235 -44
  224. package/dist/src/request-timeouts.js +1 -1
  225. package/dist/src/review/known-flakes.js +34 -14
  226. package/dist/src/review/protected-edits.js +4 -3
  227. package/dist/src/review/review-followup.js +16 -4
  228. package/dist/src/review/review-precheck.js +14 -13
  229. package/dist/src/review/review-verdict.js +14 -0
  230. package/dist/src/review/review.js +55 -34
  231. package/dist/src/review/secret-leaks.js +127 -0
  232. package/dist/src/review/suite-rerun.js +4 -3
  233. package/dist/src/roles/legacy-role-ids.js +21 -0
  234. package/dist/src/roles/legacy-role-migration.js +62 -0
  235. package/dist/src/roles/loop-ids.js +19 -2
  236. package/dist/src/roles/role-catalog.js +109 -131
  237. package/dist/src/roles/role-guidance.js +64 -31
  238. package/dist/src/roles/role-labels.js +10 -0
  239. package/dist/src/roles/role-render.js +194 -18
  240. package/dist/src/roles/role-view.js +216 -17
  241. package/dist/src/roles/roles.js +16 -14
  242. package/dist/src/scheduling/backoff.js +29 -19
  243. package/dist/src/scheduling/claims.js +15 -8
  244. package/dist/src/scheduling/once-round.js +11 -11
  245. package/dist/src/scheduling/quiet-hours.js +25 -32
  246. package/dist/src/scheduling/scheduling.js +28 -25
  247. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  248. package/dist/src/status/status-budget.js +115 -0
  249. package/dist/src/status/status-data.js +72 -184
  250. package/dist/src/status/status-inbox.js +47 -0
  251. package/dist/src/status/status-install.js +27 -0
  252. package/dist/src/status/status-polls.js +50 -39
  253. package/dist/src/status/status-roles.js +105 -0
  254. package/dist/src/{fleet → team}/error-storm.js +10 -10
  255. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  256. package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
  257. package/dist/src/{fleet → team}/reclaim.js +5 -4
  258. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  259. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  260. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
  261. package/dist/src/text/datetime.js +42 -2
  262. package/dist/src/text/format.js +10 -1
  263. package/dist/src/text/markdown.js +1 -1
  264. package/dist/src/text/phrases.js +35 -34
  265. package/dist/src/text/suggest.js +3 -2
  266. package/dist/src/text/text.js +2 -2
  267. package/dist/src/tick/qa-coverage.js +2 -1
  268. package/dist/src/tick/stage-check.js +142 -25
  269. package/dist/src/tick/telemetry-digest.js +10 -9
  270. package/dist/src/tick/tick-apply.js +101 -72
  271. package/dist/src/tick/tick-detail-data.js +23 -22
  272. package/dist/src/tick/tick-detail.js +37 -24
  273. package/dist/src/tick/tick-finalize.js +29 -22
  274. package/dist/src/tick/tick-outcome.js +2 -1
  275. package/dist/src/tick/tick-prompt.js +94 -26
  276. package/dist/src/tick/tick-resume.js +29 -13
  277. package/dist/src/tick/tick-stage.js +67 -53
  278. package/dist/src/tick/tick-timing.js +60 -59
  279. package/dist/src/tick/tick-usage.js +53 -23
  280. package/dist/src/tick/tick-verdict.js +75 -10
  281. package/dist/src/ui/badges.js +113 -69
  282. package/dist/src/ui/gui/gui-client-boot.js +202 -29
  283. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  284. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  285. package/dist/src/ui/gui/gui-client-history.js +94 -37
  286. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  287. package/dist/src/ui/gui/gui-client-loops.js +90 -47
  288. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  289. package/dist/src/ui/gui/gui-client-model.js +60 -43
  290. package/dist/src/ui/gui/gui-client-operator.js +73 -43
  291. package/dist/src/ui/gui/gui-client-pending.js +34 -31
  292. package/dist/src/ui/gui/gui-client-report.js +330 -47
  293. package/dist/src/ui/gui/gui-client-settings.js +126 -18
  294. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  295. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
  296. package/dist/src/ui/gui/gui-client.js +26 -20
  297. package/dist/src/ui/gui/gui-page.js +18 -19
  298. package/dist/src/ui/gui/gui-styles.js +12 -4
  299. package/dist/src/ui/progress-data.js +5 -3
  300. package/dist/src/ui/status-model.js +176 -102
  301. package/dist/src/ui/status-payload.js +114 -36
  302. package/dist/src/ui/status-render.js +121 -52
  303. package/dist/src/ui/team-alerts.js +341 -0
  304. package/dist/src/ui/tick-progress-model.js +14 -14
  305. package/dist/src/ui/tone.js +9 -8
  306. package/dist/src/ui/transcript-tail.js +1 -0
  307. package/dist/src/ui/transcript.js +23 -16
  308. package/dist/src/ui/tui/tui-app.js +1 -0
  309. package/dist/src/ui/tui/tui-backlog.js +85 -11
  310. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  311. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  312. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  313. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  314. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  315. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  316. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  317. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  318. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  319. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  320. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  321. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  322. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  323. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  324. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  325. package/dist/src/ui/tui/tui-editors.js +152 -0
  326. package/dist/src/ui/tui/tui-frame.js +61 -57
  327. package/dist/src/ui/tui/tui-history.js +12 -7
  328. package/dist/src/ui/tui/tui-input.js +44 -50
  329. package/dist/src/ui/tui/tui-keymap.js +1 -0
  330. package/dist/src/ui/tui/tui-keys.js +87 -1821
  331. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  332. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  333. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  334. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  335. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  336. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  337. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  338. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  339. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  340. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  341. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  342. package/dist/src/ui/tui/tui-pane.js +52 -28
  343. package/dist/src/ui/tui/tui-panes.js +294 -0
  344. package/dist/src/ui/tui/tui.js +44 -26
  345. package/dist/src/verdict/fix-claim.js +56 -33
  346. package/dist/src/verdict/no-change.js +19 -0
  347. package/dist/src/verdict/refusal.js +43 -11
  348. package/dist/src/verdict/reply-contract.js +17 -5
  349. package/dist/src/version.js +3 -2
  350. package/package.json +4 -2
  351. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  352. package/dist/src/landing/backlog-conflicts.js +0 -116
  353. package/dist/src/landing/landing-drain.js +0 -204
  354. package/dist/src/landing/landing-pipeline.js +0 -96
  355. package/dist/src/landing/landing-queue-render.js +0 -56
  356. package/dist/src/landing/landing-slot.js +0 -256
  357. package/dist/src/landing/landing-vetting.js +0 -196
  358. package/dist/src/pi-extension/role-notes.js +0 -40
  359. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,7 +1,10 @@
1
+ // vocabulary: plain
1
2
  /** Shared instruction fragments role prompts and prompt assembly embed verbatim. Pure
2
- * data: this module imports nothing, so prompt assembly can depend on it without a cycle.
3
+ * data: this module imports only the dependency-free legacy-role-ids, so prompt assembly
4
+ * can depend on it without a cycle.
3
5
  * Split from roles.ts, whose catalog (now role-catalog.ts) find texts embed these fragments
4
6
  * while prompt.ts embeds them too — the wording is one concern, the catalog another. */
7
+ import { roleCommitGrep } from "./legacy-role-ids.js";
5
8
  /** Shared guidance for any loop about to record a plan or bug: split independent parts
6
9
  * into their own entries. Defined once so the director and role prompts cannot drift.
7
10
  * Lives here (not in prompt.ts) because two role `find` texts embed it — keeping this
@@ -9,7 +12,7 @@
9
12
  * and the catalog → here. */
10
13
  export const DECOMPOSITION_GUIDANCE = `Before recording a plan or bug, check whether it decomposes
11
14
  into independent subparts (separate features, or separate bugs). If it does, record each part as
12
- its own PLANS.md/BUGS.md entry that cross-references its siblings, so loops can pick them up
15
+ its own PLANS.md/BUGS.md entry that cross-references its siblings, so roles can pick them up
13
16
  independently; if the parts are not truly independent, keep a single entry.`;
14
17
  /** Shared instruction for closing a backlog entry (the feature loop moving a plan to Done,
15
18
  * the bugfix loop moving a bug to Fixed). Written against the observed failure (PLANS.md
@@ -17,9 +20,11 @@ independently; if the parts are not truly independent, keep a single entry.`;
17
20
  * commits left PLANS.md with two `## Done` headings — so the wording names the file's EXISTING
18
21
  * resolved heading, forbids touching `## ` structure, and gives the verifiable invariant
19
22
  * (the same headings before and after). Lettered sub-steps, because the callers embed it as one
20
- * numbered step of their task. Takes the file and section names so the feature and bugfix texts
21
- * cannot drift apart; lives here with the other shared fragments. */
22
- export function backlogMoveGuidance(file, openSection, resolvedSection) {
23
+ * numbered step of their task. The pasted entry does not stay there: tumwater moves it to the top
24
+ * of `archive` when the change commits (plans/backlog-archive.md), which the last line says so no
25
+ * run opens the archive to do it by hand. Takes the file, section and archive names so the feature
26
+ * and bugfix texts cannot drift apart; lives here with the other shared fragments. */
27
+ export function backlogMoveGuidance(file, openSection, resolvedSection, archive) {
23
28
  return `move the entry, step by step:
24
29
  a. cut the entry out of \`${openSection}\`;
25
30
  b. add the closing date to its \`### \` heading;
@@ -27,8 +32,12 @@ export function backlogMoveGuidance(file, openSection, resolvedSection) {
27
32
  d. if it was the last entry under \`${openSection}\`, leave a \`_None yet._\` placeholder
28
33
  there.
29
34
  Never add, remove, or rename a \`## \` heading: \`grep -n '^## ' ${file}\` must list the
30
- same headings before and after the edit.`;
35
+ same headings before and after the edit. When your change commits, tumwater moves the
36
+ pasted entry to the top of ${archive}; do not open or edit ${archive} yourself.`;
31
37
  }
38
+ /** The heading clause requiredParts parses, spelled as the parser reads it. Interpolated because
39
+ * a comment inside the template would become prompt text. */
40
+ const REQUIRES_PART_FORM = "requires part N/M landed"; // vocabulary-ok: requiredParts parses this heading syntax
32
41
  /** Shared sizing rule for anyone writing a plan (the plan role and the director): the feature
33
42
  * loop is one mid-sized local model working alone in one run, and the review gate rejects a
34
43
  * change that lands less than its entry promises — so a plan must fit that run. Oversized plans
@@ -38,13 +47,17 @@ export function backlogMoveGuidance(file, openSection, resolvedSection) {
38
47
  * and its own fast-forward deleted the config. */
39
48
  export const PLAN_SIZING = `Size every plan to ONE implementation run by a mid-sized model working
40
49
  alone: a handful of files, at most a few hundred lines of change including tests, and no design
41
- question left open for the implementer. Anything larger is split into independently landable
42
- sub-plans that cross-reference each other, each with its own acceptance criteria. Anchor plans on
50
+ question left open for the implementer. Anything larger is split into independently mergeable
51
+ sub-plans that cross-reference each other, each with its own acceptance criteria. A sub-plan that
52
+ depends on a sibling spells that dependency as ${REQUIRES_PART_FORM}, in quotes, in its
53
+ heading's trailing parenthetical; it is the only form the claim allocator reads, so a dependency
54
+ worded any other way (such as blocked by part 1/2, after part 1/2, or follows part 1/2) does not
55
+ hold the part until its prerequisite leaves ## Planned. Anchor plans on
43
56
  file paths and symbol names — functions, types, constants, test names — never on line numbers or
44
- ranges, because line anchors go stale with every landing. When the project is the harness running
45
- the fleet, every commit lands under the build that predates it, so a
46
- change to how landings behave and any step that depends on it are separate sub-plans, the second
47
- landing only once the first is the running build.`;
57
+ ranges, because line anchors go stale with every merge. When tumwater is building itself,
58
+ every commit merges under the build that predates it, so a
59
+ change to how merges behave and any step that depends on it are separate sub-plans, the second
60
+ merge only once the first is the running build.`;
48
61
  /** The closed vocabulary for the validation-gap trace (plans/repair-traces.md): what made a bug
49
62
  * hard to *confirm*, which is the only evidence of where this project's test infrastructure is
50
63
  * weakest. Closed on purpose so the traces aggregate, and `none` is a written value rather than an
@@ -71,17 +84,28 @@ made the bug hard to CONFIRM — not to fix — as one line, \`**Validation gap:
71
84
  - real-run-needed — no offline path covered it; a real bounded run was required
72
85
  - no-observability — the failure left no trace
73
86
  - slow-check — the only verification was slow enough to shape the fix
74
- - unclear-invariant — had to reconstruct what the code was supposed to guarantee first
87
+ - unclear-invariant — had to reconstruct what the code was supposed to guarantee first; record the reconstructed invariant in docs/invariants.md in the same commit so a later bug in that area does not reconstruct it again
75
88
  When nothing fits exactly, use the closest tag and say so in the sentence — never invent a
76
89
  tag. \`none\` is written, never omitted, so a tally has an honest denominator.`;
77
- /** The one query that tallies validation-gap tags across BUGS.md, counting both the verbatim
78
- * \`**Validation gap:**\` line bugfix writes and the steward's compressed \`gap: <tag>\` suffix.
90
+ /** The one query that tallies validation-gap tags across BUGS.md and BUGS-FIXED.md (where fixed
91
+ * bugs are archived), counting both the verbatim \`**Validation gap:**\` line bugfix writes and
92
+ * the \`gap: <tag>\` suffix of the one-line records the steward used to compress older entries to.
79
93
  * Split from VALIDATION_GAP_GUIDANCE so only the steward — the one role that aggregates the
80
94
  * tags — carries it; a bugfix run writes one line and never needs the tally. */
81
- export const VALIDATION_GAP_TALLY = `One query counts both the verbatim line and the compressed
82
- \`gap: <tag>\` suffix: \`grep -oE 'gap:[*]{0,2} ?[a-z-]+' BUGS.md | sed -E 's/^gap:[*]{0,2} ?//' | sort | uniq -c\`.`;
95
+ export const VALIDATION_GAP_TALLY = `One query counts both the verbatim line and the older compressed
96
+ \`gap: <tag>\` suffix across BUGS.md and its archive: \`grep -sohE 'gap:[*]{0,2} ?[a-z-]+' BUGS.md BUGS-FIXED.md | sed -E 's/^gap:[*]{0,2} ?//' | sort | uniq -c\`.`;
97
+ /** The one query a *consumer* of the validation-gap traces — a role like coverage that picks its
98
+ * next target from the record of what made past bugs hard to confirm — runs to pull the
99
+ * recent, still-full-body entries. Split from VALIDATION_GAP_TALLY (the steward's aggregate
100
+ * count) because this one lists the *entries*, newest-first: \`head\` not \`tail\`, since
101
+ * \`## Fixed\` is newest-first, so the recent full-body entries sit at the top, for a role to
102
+ * read each hit's free text. It reads BUGS.md's own section first, then BUGS-FIXED.md, where
103
+ * fixed bugs are archived (plans/backlog-archive.md); \`-s\` keeps a missing archive quiet, and
104
+ * each hit names its file and line. Built from the closed vocabulary (minus \`none\`, which
105
+ * names nothing to build) so it cannot drift. */
106
+ export const VALIDATION_GAP_QUERY = `grep -snE '\\*\\*Validation gap:\\*\\* (${VALIDATION_GAP_TAGS.filter((t) => t !== "none").join("|")})' BUGS.md BUGS-FIXED.md | head -20`;
83
107
  /** The literal prefix every Needs-review note starts with. Exported so a reader that only has
84
- * to recognize one (the backlog index's ` [needs review]` mark) can match on it instead of
108
+ * to recognize one (the backlog index's ` [blocked: needs review]` mark) can match on it instead of
85
109
  * parsing the date or the wording out of the full template — the same split as
86
110
  * NEEDS_REPLAN_PREFIX. */
87
111
  export const NEEDS_REVIEW_PREFIX = `**Needs review `;
@@ -91,22 +115,31 @@ export const NEEDS_REVIEW_PREFIX = `**Needs review `;
91
115
  * clause cannot drift. */
92
116
  export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run**`;
93
117
  /** The literal prefix every Needs-replan note starts with. Exported so a reader that only has to
94
- * recognize one (the backlog index's ` [needs replan]` mark) can match on it instead of parsing
95
- * the date or the round count out of the full template. */
118
+ * recognize one (the backlog index's ` [blocked: needs replan]` mark) can match on it instead of
119
+ * parsing the date or the round count out of the full template. */
96
120
  export const NEEDS_REPLAN_PREFIX = `**Needs replan `;
97
121
  /** The markdown note the feature loop appends under a plan whose change was rejected after its
98
122
  * final revision round, carrying the reviewer's objections back to the strong-tier plan loop
99
123
  * instead of letting feature re-author the same plan. Same convention as NEEDS_REVIEW_NOTE: a
100
124
  * note a later fresh tick reads, with no code parsing it. */
101
125
  export const NEEDS_REPLAN_NOTE = `${NEEDS_REPLAN_PREFIX}<YYYY-MM-DD> by feature: rejected after <N> review rounds**`;
102
- /** How a role with no backlog (organize, clean, dry, perf, security, robustness, improve) finds its
103
- * one task, as numbered steps. Written against the observed failure: with nothing to point at, a
104
- * local model reads the codebase file by file — thirty whole-file reads, ~300 KB of tool output
105
- * — and then either fills its window or declares nothing-to-do (improve: 41 of 44 ticks in the
106
- * first week of September landed nothing). Cheap signals shortlist candidates; a decision deadline
107
- * ends the search; the harness's own commit subjects (`tumwater(<role>): …`) are the only
108
- * cross-tick memory of what the role did lately, so the text names them. Takes the role id so the
109
- * git command is literal, not a placeholder the model has to fill in. */
126
+ /** The literal prefix every Waiting-for-the-next-failure note starts with. Exported so a reader
127
+ * that only has to recognize one (the backlog index's ` [blocked: waiting for the next failure]`
128
+ * mark) can match on it instead of parsing the date or the wording out of the full template. */
129
+ export const WAITING_FOR_FAILURE_PREFIX = `**Waiting for the next failure `;
130
+ /** The markdown note the bugfix loop writes under an open bug it cannot reproduce once it has made
131
+ * the failure explain itself, so bugfix stops spending runs without new evidence. Same convention
132
+ * as NEEDS_REPLAN_NOTE: a note a later fresh tick reads. It lifts when whoever records the next
133
+ * occurrence deletes the line, or by `tumwater bug --release <n>`. */
134
+ export const WAITING_FOR_FAILURE_NOTE = `${WAITING_FOR_FAILURE_PREFIX}<YYYY-MM-DD> by bugfix: <what the next failure's message will show>**`;
135
+ /** How a role with no backlog (organize, clean, dedupe, perf, security, robustness, improve) finds
136
+ * its one task, as numbered steps. Written against the observed failure: with nothing to point
137
+ * at, a local model reads the codebase file by file — thirty whole-file reads, ~300 KB of tool
138
+ * output — and then either fills its window or declares nothing-to-do (improve: 41 of 44 ticks in
139
+ * the first week of September landed nothing). Cheap signals shortlist candidates; a decision
140
+ * deadline ends the search; the harness's own commit subjects (`tumwater(<role>): …`) are the
141
+ * only cross-tick memory of what the role did lately, so the text names them. Takes the role id
142
+ * so the git command is literal, not a placeholder the model has to fill in. */
110
143
  export function searchGuidance(roleId) {
111
144
  return `How to search — do not read the codebase file by file: a whole-tree survey costs most of
112
145
  the run and rarely finds anything a targeted look would not.
@@ -114,10 +147,10 @@ the run and rarely finds anything a targeted look would not.
114
147
  the expensive unit: \`git log --stat -15\` (recent churn is where problems accumulate),
115
148
  \`wc -l\` over the source files (size outliers), and \`grep -rn\` for the specific pattern
116
149
  you are after.
117
- 2. Skip what your own role handled lately: \`git log --oneline -15 --grep="tumwater(${roleId})"\`
118
- lists it — the harness's commit subjects say what each loop did.
150
+ 2. Skip what your own role handled lately: \`git log --oneline -15 ${roleCommitGrep(roleId)}\`
151
+ lists it — tumwater's commit subjects say what each role did.
119
152
  3. Vary the slice you look at across runs (a different directory, module, or recency window)
120
- so successive ticks do not all converge on the same files.
153
+ so successive runs do not all converge on the same files.
121
154
  4. Shortlist at most five candidate files and inspect them with \`grep -n\` and ranged reads;
122
155
  open a file whole only when it is under ~300 lines.
123
156
  5. Decide within ~15 tool calls, in a handful of turns: if no candidate clearly clears the bar
@@ -0,0 +1,10 @@
1
+ // vocabulary: plain
2
+ /** How a role's id shows up in words: an instance id as `feature #2`. Pure, no I/O, so core
3
+ * modules and the UI may both import it. */
4
+ import { baseRoleOf, instanceIndex } from "./loop-ids.js";
5
+ /** An instance id as `feature #2`; any other id (a bare role, `director`, a custom role,
6
+ * `feature-1`) unchanged. */
7
+ export function roleLabel(runnerId) {
8
+ const base = baseRoleOf(runnerId);
9
+ return base === runnerId ? runnerId : `${base} #${instanceIndex(runnerId)}`;
10
+ }
@@ -1,6 +1,11 @@
1
- import { agree } from "../text/phrases.js";
2
- import { formatTimestamp } from "../text/datetime.js";
1
+ // vocabulary: plain
2
+ import { agree, shortSpanPhrase } from "../text/phrases.js";
3
+ import { compactTokens, usd } from "../text/format.js";
4
+ import { clockHm, formatTimestamp, humanSeconds, nextRunPhrase, secondsSince, secondsUntil } from "../text/datetime.js";
3
5
  import { claudeWindowLabel } from "../agent/claude/claude-account.js";
6
+ import { yieldMultiplierSuffix } from "../scheduling/backoff.js";
7
+ import { quietWindowEnd } from "../scheduling/quiet-hours.js";
8
+ import { isAgentId } from "../config/model-selector.js";
4
9
  /** The terminal's view of one loop — what `tumwater role <id>` prints, rendered as Markdown
5
10
  * from the payload role-view.ts's rolePayload collects (the same document `--json` prints).
6
11
  * Like backlog-render.ts, the renderer owns no collection of its own: the CLI's --json and
@@ -32,15 +37,21 @@ function fencedOrNone(label, text, noneNote = "none") {
32
37
  * `renderRoleMarkdown` splices these at the same position, so the extracted function and the
33
38
  * full document cannot drift. */
34
39
  export function inspectMarkdownLines(p) {
40
+ const running = chainStepLine(p);
41
+ return [...(running ? [running, ""] : []), ...inspectBodyLines(p)];
42
+ }
43
+ /** The notebook, prompt and disclosure blocks alone — what renderRoleMarkdown splices after
44
+ * its own Team lines, which already carry the fallback-step bullet. */
45
+ function inspectBodyLines(p) {
35
46
  const lines = [];
36
- lines.push(...fencedOrNone("Notebook", p.note, "none yet — the role's own earlier ticks write it, and the director has none"));
37
- lines.push(...fencedOrNone("Next tick prompt", p.nextPrompt, "nothing to run this tick"));
47
+ lines.push(...fencedOrNone("Notes", p.notes, "none yet — the role's own earlier runs write them, and the director has none"));
48
+ lines.push(...fencedOrNone("Next run's prompt", p.nextPrompt, "nothing to do this run"));
38
49
  // tick-prompt.ts dequeues exactly one queued prompt per tick, so with more than one
39
50
  // waiting the block above shows only the oldest — say so, so the count line and the
40
51
  // block never read as a contradiction (the 2026-10-04 BUGS.md entry under ## Fixed).
41
52
  if (p.inboxCount > 1 && p.nextPrompt !== null && p.nextPrompt.trim() !== "") {
42
53
  const more = p.inboxCount - 1;
43
- lines.push(`_(the Next tick prompt embeds the oldest queued prompt — one is consumed per tick; ${more} more ${agree(more, "waits", "wait")})_`);
54
+ lines.push(`_(the next run's prompt embeds the oldest inbox message — one is taken per run; ${more} more ${agree(more, "waits", "wait")})_`);
44
55
  }
45
56
  return lines;
46
57
  }
@@ -53,49 +64,214 @@ function modelPair(provider, model) {
53
64
  /** The backend's own-default label when no model resolves: `<agent> default` — pi's
54
65
  * historical `pi default` for the pi backend, `claude default` for claude (part 4f/7). */
55
66
  function agentDefault(p) {
56
- return `${p.agent !== "pi" ? p.agent : "pi"} default`;
67
+ return `${p.backend !== "pi" ? p.backend : "pi"} default`;
57
68
  }
58
69
  /** The provider/model wiring as one readable token: `provider/model` when both resolve,
59
70
  * `model` alone when only the model is named, and the backend-default note when neither is.
60
71
  * A non-pi backend is named as the selector's provider namespace (`claude/claude-sonnet-5-5`),
61
72
  * matching the string the events and status rows carry; a pi seam is unchanged. */
62
73
  function modelLine(p) {
74
+ // pi's own saved default (part 2/2): the formatted selector already carries its thinking
75
+ // level (piDefault is formatted with the effective thinking winning), so the separate
76
+ // ` (thinking: …)` suffix is dropped for this case only.
77
+ if (p.piDefault)
78
+ return `${agentDefault(p)} (${p.piDefault})`;
63
79
  const pair = modelPair(p.provider, p.model);
64
80
  const thinking = p.thinking ? ` (thinking: ${p.thinking})` : "";
65
81
  if (!pair)
66
82
  return `${agentDefault(p)}${thinking}`;
67
- const agent = p.agent !== "pi" ? `${p.agent}/` : "";
83
+ const agent = p.backend !== "pi" ? `${p.backend}/` : "";
68
84
  return `${agent}${pair}${thinking}`;
69
85
  }
70
- /** Render one loop's inspection payload as Markdown. */
71
- export function renderRoleMarkdown(p) {
86
+ /** A fallback pair as one readable token, agent-qualified exactly like modelLine — a non-pi
87
+ * backend (its `agent`, or a provider that names one, `claude`) is named as the selector's
88
+ * namespace; a pi pair keeps `provider/model`; neither half present falls back to the
89
+ * backend-default label. One home for the Model fallback and Budget fallback lines, so a
90
+ * claude fallback reads `claude/claude-sonnet-5-5`, not a bare id (PLANS.md "Mixed claude +
91
+ * 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);
95
+ if (fb.model !== undefined) {
96
+ if (agentBackend !== undefined)
97
+ return `${agentBackend}/${fb.model}`;
98
+ return fb.provider !== undefined ? `${fb.provider}/${fb.model}` : fb.model;
99
+ }
100
+ if (fb.provider !== undefined)
101
+ return fb.provider;
102
+ return agentDefault;
103
+ }
104
+ /** Why a tier moved down its fallback chain, as the role view words it. */
105
+ function chainReasonPhrase(reason, windowType) {
106
+ switch (reason) {
107
+ case "window":
108
+ return `${windowType !== undefined ? claudeWindowLabel(windowType) : "Claude"} window used up`;
109
+ case "reserve":
110
+ return "window reserve reached";
111
+ case "cap":
112
+ return "daily cap reached";
113
+ case "step-cap":
114
+ return "previous step's cap reached";
115
+ case "overage":
116
+ return "extra usage refused";
117
+ case "demoted":
118
+ return "previous step demoted";
119
+ }
120
+ }
121
+ /** The `Running on fallback <step> of <of> (<why>, resets HH:MM)` bullet, or null when the
122
+ * role's tier is on its primary. */
123
+ function chainStepLine(p) {
124
+ const c = p.chainStep;
125
+ if (!c)
126
+ return null;
127
+ 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})`;
129
+ }
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
132
+ * outcomes at a glance. `time` is the same local `YYYY-MM-DD HH:MM:SS` stamp the model-
133
+ * fallback `since` prints (formatTimestamp); `span` is the same short-duration phrasing
134
+ * shortSpanPhrase renders for holdPhrase and the event feed's countdowns; the summary is
135
+ * appended when the event carried one. Unset halves are dropped, never printed as a literal
136
+ * `undefined`. */
137
+ function recentOutcomeLine(o) {
138
+ let line = `${formatTimestamp(o.ts)} · ${o.result}`;
139
+ if (o.durationMs !== undefined)
140
+ line += ` · ${shortSpanPhrase(o.durationMs)}`;
141
+ if (o.summary)
142
+ line += ` — ${o.summary}`;
143
+ return line;
144
+ }
145
+ /** One quiet-hours bullet: the window, whose it is, and whether it holds the loop now — with
146
+ * the same `quiet until <end>` text the status table uses. */
147
+ function quietHoursLine(w) {
148
+ const whose = w.scope === "role" ? "own window" : "team window";
149
+ const state = w.inWindow ? `holding now — quiet until ${quietWindowEnd(w.window)}` : "not holding now";
150
+ return `- Quiet hours: ${w.window} (${whose}; ${state})`;
151
+ }
152
+ /** The loop's schedule as Markdown bullet lines — `Next tick`, `Last tick`, then one
153
+ * `Quiet hours` line per governing window (none when the payload carries no windows) — in the
154
+ * 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`;
157
+ * a future one reads `in <Ns>` through humanSeconds, prefixed `backoff ` while backoffSeconds
158
+ * > 0 (the wake row-action clears exactly that) — the shared nextRunPhrase, so the role view
159
+ * 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
162
+ * injected (default Date.now()) so the renderer stays a pure function of the payload and the
163
+ * test can pin a fixed clock. */
164
+ function scheduleLine(p, now) {
165
+ // 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.
168
+ const suffix = yieldMultiplierSuffix(p.yieldMultiplier);
169
+ let next;
170
+ if (p.nextRunAt <= 0) {
171
+ next = "not scheduled yet";
172
+ }
173
+ else {
174
+ const remain = secondsUntil(p.nextRunAt, now);
175
+ next = nextRunPhrase(remain, p.backoffSeconds > 0, "due now", true) + suffix;
176
+ }
177
+ const last = p.lastRunEndedAt !== undefined
178
+ ? `${formatTimestamp(p.lastRunEndedAt)} (${humanSeconds(secondsSince(p.lastRunEndedAt, now))} ago)`
179
+ : "never";
180
+ return [`- Next run: ${next}`, `- Last run: ${last}`, ...(p.quietHours ?? []).map(quietHoursLine)];
181
+ }
182
+ /** Render one loop's inspection payload as Markdown. `now` is injected (default Date.now())
183
+ * so the schedule lines stay a pure function of the payload — the test pins a fixed clock. */
184
+ export function renderRoleMarkdown(p, now = Date.now()) {
72
185
  const lines = [`# tumwater role: ${p.id}`, ""];
73
- lines.push(`- Loop: "${p.id}" (${p.title})${p.custom ? " — user-defined loop" : ""}`);
186
+ lines.push(`- ${p.custom ? "Custom role" : "Role"}: "${p.id}" (${p.title})`);
74
187
  const state = [p.enabled ? "enabled" : "disabled", p.paused ? "paused" : "not paused"].join(", ");
75
188
  lines.push(`- State: ${state}`);
76
- lines.push(`- Scheduling tier: ${p.tier} (${p.tier === 0 ? "work" : "maintenance/observer"})`);
189
+ lines.push(`- Priority: ${p.tier === 0 ? "work" : "maintenance or observer"}`);
77
190
  lines.push(`- Model: ${modelLine(p)} (${p.modelTier} tier)`);
191
+ // Context window: the resolved primary seam's declared window (the one `- Next run:`
192
+ // previews, not the fallback — a run on the model fallback may use a different window, the
193
+ // fallback's own documented state). `unknown` when it cannot be known (a non-pi seam, an
194
+ // unresolvable pair, a model with no declared window).
195
+ lines.push(`- Context window: ${p.contextWindow === null ? "unknown" : `${compactTokens(p.contextWindow)} tokens`}`);
196
+ // 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
198
+ // `maxConcurrentPerProvider` maps this seam's key — a cap-less default team has no
199
+ // resolvable key entry, so its role view renders no new line and stays byte-identical.
200
+ if (p.providerKey !== undefined && p.providerCap !== undefined) {
201
+ lines.push(`- Provider: ${p.providerKey} (cap ${p.providerCap})`);
202
+ }
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>`
206
+ // explains the team alert without running doctor. Unknown (null) renders `unknown` rather
207
+ // than guessing.
208
+ const images = p.imageInput === true
209
+ ? "yes"
210
+ : p.imageInput === false
211
+ ? p.imageReroute
212
+ ? `no — reruns on ${p.imageReroute}`
213
+ : "no"
214
+ : "unknown";
215
+ lines.push(`- Images: ${images}`);
78
216
  // Model failure fallback, part 2/2: name the off-model pair, when the episode began, and
79
217
  // the provider-class failure that tripped it, so `tumwater role <id>` explains the state.
80
218
  if (p.modelFallback) {
81
- const pair = modelPair(p.modelFallback.provider, p.modelFallback.model) || agentDefault(p);
219
+ const pair = fallbackPairLabel(p.modelFallback, agentDefault(p));
82
220
  lines.push(`- Model fallback: ${pair} — on fallback since ${formatTimestamp(p.modelFallback.since)} (primary failing: ${p.modelFallback.reason})`);
83
221
  }
84
222
  if (p.fallback) {
85
- const pair = modelPair(p.fallback.provider, p.fallback.model) || agentDefault(p);
86
- lines.push(`- Budget fallback: ${pair} (${p.fallbackFree ? "free" : "priced"})`);
223
+ const pair = fallbackPairLabel(p.fallback, agentDefault(p));
224
+ lines.push(`- Over-cap model: ${pair} (${p.fallbackFree ? "free" : "priced"})`);
87
225
  }
226
+ const running = chainStepLine(p);
227
+ if (running)
228
+ lines.push(running);
88
229
  // The claude seam's billing (plans/agent-backends.md §7/§8, PLANS.md part 6d/7): absent on
89
230
  // a pi seam, so its role view is byte-identical to before.
90
231
  if (p.billing) {
91
232
  const window = p.billing === "subscription" && p.billingWindow ? ` (${claudeWindowLabel(p.billingWindow)} window)` : "";
92
233
  lines.push(`- Billing: ${p.billing === "api" ? "API" : p.billing}${window}`);
93
234
  }
94
- lines.push(`- Min tick interval: ${p.minTickIntervalSeconds}s`);
95
- lines.push(`- Queued prompts: ${p.inboxCount}`);
235
+ lines.push(`- Min run interval: ${p.minRunIntervalSeconds}s`);
236
+ if (p.instances !== undefined)
237
+ lines.push(`- Instances: ${p.instances}`);
238
+ 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,
241
+ // 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`
243
+ // goldens are byte-identical.
244
+ if (p.usage) {
245
+ let spend = `- Spend: ${usd(p.usage.costUsd)} · ${compactTokens(p.usage.tokens)} tokens`;
246
+ if (p.usage.notionalCostUsd !== 0)
247
+ spend += ` · ${usd(p.usage.notionalCostUsd)} notional`;
248
+ lines.push(spend);
249
+ }
250
+ // 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.
252
+ if (p.dailyCap) {
253
+ const dc = p.dailyCap;
254
+ 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"})`;
255
+ if (dc.roleIds.length > 1)
256
+ cap += ` · shared by ${dc.roleIds.join(", ")}`;
257
+ lines.push(cap);
258
+ }
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
262
+ // injected `now`, so the lines stay a pure function of the payload.
263
+ 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.
267
+ if (p.recentOutcomes && p.recentOutcomes.length > 0) {
268
+ lines.push("", "## Recent runs", "");
269
+ for (const o of p.recentOutcomes)
270
+ lines.push(`- ${recentOutcomeLine(o)}`);
271
+ }
96
272
  lines.push("");
97
273
  lines.push(...fencedOrNone("Instructions override", p.instructions));
98
- lines.push(...fencedOrNone("Find text", p.find, "none — the director is driven by its queued prompts, not a find text"));
99
- lines.push(...inspectMarkdownLines(p));
274
+ lines.push(...fencedOrNone("Find text", p.find, "none — the director is driven by its inbox, not by instructions"));
275
+ lines.push(...inspectBodyLines(p));
100
276
  return lines.join("\n").trimEnd();
101
277
  }