tumwater 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -31,30 +31,41 @@ export function runDetailStat(root, d) {
31
31
  export function runDetailPatch(root, d) {
32
32
  return runCommitShow(root, runCommitSha(d), "--patch");
33
33
  }
34
- /** The human view of a RunDetail: one summary line — result (or the in-flight/unpaired
35
- * marker), duration, usage, and the model the run started on when its `run_start` carried one
36
- * (after the usage, before the pinned-commit sha), plus the pinned commit sha when the block's
37
- * `merge_queued`/`merge_done` event carries one — then each event through formatEvent, the
38
- * shared renderer `logs` and the
39
- * TUI use, so the trail reads exactly like the feed it came from. A queued run whose merge
40
- * has finished shows that verdict in the header and ends with the merge's outcome line; a
41
- * leftover-recovery run's verdict is the failed attempt it re-queued (resolveQueuedResult),
42
- * logged before the run began, so that outcome line leads the trail instead — the trail stays
43
- * in time order. */
44
- export function renderRunDetail(d) {
45
- const summary = [];
34
+ /** The run's result cell, always present: the outcome label when the run finished, the
35
+ * in-flight/unpaired marker when it has not. Owns the first summary cell. */
36
+ function runResultCell(d) {
46
37
  if (d.endTs === null)
47
- summary.push(d.startTs === null ? "unpaired" : "in flight");
48
- else
49
- summary.push(d.result ? eventTypeLabel(d.result) : "?");
38
+ return d.startTs === null ? "unpaired" : "in flight";
39
+ return d.result ? eventTypeLabel(d.result) : "?";
40
+ }
41
+ /** The run's cut-off cell, the ceiling-truncation marker straight after the result cell. It
42
+ * only rides a completed end (cutOff is set from the run_end), so it can never follow in
43
+ * flight/unpaired. */
44
+ function runCutOffCell(d) {
50
45
  // The ceiling-truncation marker, straight after the result cell. It only rides a
51
46
  // completed end (cutOff is set from the run_end), so it can never follow in flight/unpaired.
52
- if (d.cutOff === true)
53
- summary.push("cut off at the context ceiling");
54
- if (d.durationMs !== null)
55
- summary.push(shortSpanPhrase(d.durationMs));
56
- if (d.usage !== "")
57
- summary.push(d.usage);
47
+ if (d.cutOff !== true)
48
+ return null;
49
+ return "cut off at the context ceiling";
50
+ }
51
+ /** The run's duration cell. */
52
+ function runDurationCell(d) {
53
+ if (d.durationMs === null)
54
+ return null;
55
+ return shortSpanPhrase(d.durationMs);
56
+ }
57
+ /** The run's usage cell. */
58
+ function runUsageCell(d) {
59
+ if (d.usage === "")
60
+ return null;
61
+ return d.usage;
62
+ }
63
+ /** The run's prompt cell: the prompt-token total and the share the backend served from cache,
64
+ * the input side a cost-only header hides. The `${n}% cached` wording and rounding are the
65
+ * usage report's `Prompt tokens by role` share; a run with prompt tokens but no cache reads
66
+ * shows no parenthetical, and a run that reported neither keeps today's header byte-identical
67
+ * (the omit-when-zero convention). */
68
+ function runPromptCell(d) {
58
69
  // The run's prompt-token total and the share the backend served from cache, the input
59
70
  // side a cost-only header hides. The `${n}% cached` wording and rounding are the usage
60
71
  // report's `Prompt tokens by role` share; a run with prompt tokens but no cache reads
@@ -62,37 +73,71 @@ export function renderRunDetail(d) {
62
73
  // byte-identical (the omit-when-zero convention).
63
74
  const prompt = d.promptTokens ?? 0;
64
75
  const cached = Math.min(d.cacheReadTokens ?? 0, prompt);
65
- if (prompt > 0)
66
- summary.push(cached > 0 ? `prompt ${compactTokens(prompt)} tok (${Math.round((100 * cached) / prompt)}% cached)` : `prompt ${compactTokens(prompt)} tok`);
76
+ if (prompt <= 0)
77
+ return null;
78
+ return cached > 0 ? `prompt ${compactTokens(prompt)} tok (${Math.round((100 * cached) / prompt)}% cached)` : `prompt ${compactTokens(prompt)} tok`;
79
+ }
80
+ /** The run's pre-edit prompt-token share, the `… before first edit` signal a cost-only header
81
+ * hides: how much of the prompt budget went out before the run made its first edit. Clamped
82
+ * to the total (run-detail-data.ts), written after the prompt cell and before the
83
+ * peak-context cell; a run that reported no pre-edit reading — and an in-flight run — keeps
84
+ * today's header byte-identical. */
85
+ function runPreEditCell(d) {
67
86
  // The run's pre-edit prompt-token share, the `… before first edit` signal a cost-only
68
87
  // header hides: how much of the prompt budget went out before the run made its first
69
88
  // edit. Clamped to the total (run-detail-data.ts), written after the prompt cell and
70
89
  // before the peak-context cell; a run that reported no pre-edit reading — and an
71
90
  // in-flight run — keeps today's header byte-identical.
91
+ const prompt = d.promptTokens ?? 0;
72
92
  const preEdit = Math.min(d.preEditPromptTokens ?? 0, prompt);
73
- if (preEdit > 0)
74
- summary.push(`pre-edit ${compactTokens(preEdit)} tok`);
93
+ if (preEdit <= 0)
94
+ return null;
95
+ return `pre-edit ${compactTokens(preEdit)} tok`;
96
+ }
97
+ /** The run's peak-context cell against the seam's declared window, the one header every surface
98
+ * renders. Omitted when the run reported no peak (byte-identical to today's header). */
99
+ function runPeakContextCell(d) {
75
100
  // The run's peak context against the seam's declared window, the one header every surface
76
101
  // renders. Omitted when the run reported no peak (byte-identical to today's header).
77
- if ((d.peakContextTokens ?? 0) > 0)
78
- summary.push(`peak ctx ${contextShareLabel(d.peakContextTokens ?? 0, d.contextWindow)}`);
79
- if (d.model !== null)
80
- summary.push(d.model);
102
+ if ((d.peakContextTokens ?? 0) <= 0)
103
+ return null;
104
+ return `peak ctx ${contextShareLabel(d.peakContextTokens ?? 0, d.contextWindow)}`;
105
+ }
106
+ /** The run's model cell, the model the run started on. */
107
+ function runModelCell(d) {
108
+ if (d.model === null)
109
+ return null;
110
+ return d.model;
111
+ }
112
+ /** The summary cells in today's push order — result, cut-off, duration, usage, prompt,
113
+ * pre-edit, peak-context, model — each present only when its guard omits nothing. */
114
+ function runSummaryCells(d) {
115
+ return [runResultCell(d), runCutOffCell(d), runDurationCell(d), runUsageCell(d), runPromptCell(d), runPreEditCell(d), runPeakContextCell(d), runModelCell(d)].filter((cell) => cell !== null);
116
+ }
117
+ /** The header's ` · `-separated suffix cells after the summary: the pinned commit, `merged
118
+ * as`, end-of-run landing latency, and the deliberate-revert verdict. */
119
+ function runHeaderSuffixCells(d) {
81
120
  const sha = runCommitSha(d);
82
- let header = `${roleLabel(d.role)} run #${d.run} — ${summary.join(" · ")}`;
121
+ const cells = [];
83
122
  if (sha !== null)
84
- header += ` · commit ${shortSha(sha)}`;
123
+ cells.push(`commit ${shortSha(sha)}`);
85
124
  if (d.mergedSha !== null && d.mergedSha !== sha)
86
- header += ` · merged as ${shortSha(d.mergedSha)}`;
125
+ cells.push(`merged as ${shortSha(d.mergedSha)}`);
87
126
  // The end-of-run → on-main landing latency, the per-change twin of the day report's `Merge
88
127
  // wait median`. shortSpanPhrase is the duration cell's renderer, so both spans read alike; a
89
128
  // run without the field (rejected/failed merge, in flight, landing rotated out, no-op
90
129
  // fast-forward) keeps today's header byte-identical.
91
130
  if (d.mergeAfterRunMs !== undefined)
92
- header += ` · merged ${shortSpanPhrase(d.mergeAfterRunMs)} after the run`;
131
+ cells.push(`merged ${shortSpanPhrase(d.mergeAfterRunMs)} after the run`);
93
132
  if (d.reverted !== null) {
94
- header += ` · reverted ${shortSha(d.reverted.sha)} (by operator)`; // vocabulary-ok: the deliberate-revert writer this header names
133
+ cells.push(`reverted ${shortSha(d.reverted.sha)} (by operator)`); // vocabulary-ok: the deliberate-revert writer this header names
95
134
  }
135
+ return cells;
136
+ }
137
+ /** The trail lines: the run's events through the shared formatEvent, with the merge outcome
138
+ * line placed before the first event when it predates it (a leftover-recovery run) and after
139
+ * the last otherwise, keeping the trail in time order. */
140
+ function runTrailLines(d) {
96
141
  const trail = d.events.map(formatEvent);
97
142
  if (d.merge !== null) {
98
143
  const first = d.events[0];
@@ -101,7 +146,19 @@ export function renderRunDetail(d) {
101
146
  else
102
147
  trail.push(formatEvent(d.merge));
103
148
  }
104
- return [header, ...trail].join("\n");
149
+ return trail;
150
+ }
151
+ /** The human view of a RunDetail: one summary line assembled from the per-cell and
152
+ * per-trail-step helpers — the result (or in-flight/unpaired marker), cut-off, duration,
153
+ * usage, prompt, pre-edit, peak-context and model cells, then the committed sha, `merged as`,
154
+ * merge-latency and reverted suffix cells — and each event through formatEvent. A queued run
155
+ * whose merge has finished shows that verdict in the header and ends with the merge's outcome
156
+ * line; a leftover-recovery run's verdict is the failed attempt it re-queued
157
+ * (resolveQueuedResult), logged before the run began, so that outcome line leads the trail
158
+ * instead — the trail stays in time order. */
159
+ export function renderRunDetail(d) {
160
+ const header = [`${roleLabel(d.role)} run #${d.run} — ${runSummaryCells(d).join(" · ")}`, ...runHeaderSuffixCells(d)].join(" · ");
161
+ return [header, ...runTrailLines(d)].join("\n");
105
162
  }
106
163
  /** The transcript block of `--transcript`: the rendered lines of the agent runs that started in the
107
164
  * run's window, or `found: []` plus the `note` the text view prints in their place. The
@@ -57,15 +57,11 @@ export function newestRunRow(root, role, classes, models) {
57
57
  function newestRunNumber(root, role, classes, models) {
58
58
  return newestRunRow(root, role, classes, models)?.run ?? null;
59
59
  }
60
- /** The invocation half of cmdRunDetail: rule out the arity and flag mistakes before any run is
61
- * resolved, so the shared unknown-role wording and the usage string are the whole validation
62
- * surface, found by this one name. Succeeds only for a well-formed call; returns the parsed
63
- * bare/role/classes/models for the resolver. The role is checked first, so a typo is named
64
- * whatever the count (knownRoleIdsCached: the built-ins plus user-defined roles, read through
65
- * the never-throwing cached loader so a transiently broken tumwater.json cannot take a
66
- * read-only view down). The arity, unknown-role and flag-dependency failures all `fail` with
67
- * the usage. */
68
- function parseRunDetailInvocation(root, positionals, { last, transcript, prompt, grep, result, model }) {
60
+ /** The positional and role half of parseRunDetailInvocation's validation: the bare/last arity,
61
+ * the canonical role id, the unknown-role guard (knownRoleIdsCached) and the three arity
62
+ * `fail`s — one name so the whole positional surface is found by this helper. Returns whether
63
+ * the call is bare (`--last` with no role) and the resolved role. */
64
+ function runDetailPositionals(root, positionals, last) {
69
65
  // Positional arity by form: cli.ts has already peeled the flags off and gated them (only
70
66
  // --json and --last are admitted), so anything left over that does not fit the chosen form
71
67
  // is a mistake. The role is checked first, so a typo is named whatever the count; then
@@ -91,6 +87,13 @@ function parseRunDetailInvocation(root, positionals, { last, transcript, prompt,
91
87
  }
92
88
  fail(`usage: ${RUN_DETAIL_USAGE}`);
93
89
  }
90
+ return { bare, role };
91
+ }
92
+ /** The flag-dependency half of parseRunDetailInvocation's validation: the four `fail` guards —
93
+ * `--prompt`/`--grep` need `--transcript`, `--result`/`--model` need `--last` — one name so the
94
+ * whole dependency surface is found by this helper. */
95
+ function checkRunDetailFlagDeps(flags) {
96
+ const { last, transcript, prompt, grep, result, model } = flags;
94
97
  if (prompt && !transcript)
95
98
  fail(`history --prompt needs --transcript (usage: ${RUN_DETAIL_USAGE})`);
96
99
  if (grep !== null && !transcript)
@@ -99,10 +102,67 @@ function parseRunDetailInvocation(root, positionals, { last, transcript, prompt,
99
102
  fail(`history --result needs --last on a one-run trail (usage: ${RUN_DETAIL_USAGE})`);
100
103
  if (model !== null && !last)
101
104
  fail(`history --model needs --last on a one-run trail (usage: ${RUN_DETAIL_USAGE})`);
105
+ }
106
+ /** The invocation half of cmdRunDetail: rule out the arity and flag mistakes before any run is
107
+ * resolved, so the shared unknown-role wording and the usage string are the whole validation
108
+ * surface, found by this one name. Succeeds only for a well-formed call; returns the parsed
109
+ * bare/role/classes/models for the resolver. Delegates the positional/role arity to
110
+ * runDetailPositionals and the flag-dependency guards to checkRunDetailFlagDeps, then parses
111
+ * the --result class and --model selector lists. */
112
+ function parseRunDetailInvocation(root, positionals, { last, transcript, prompt, grep, result, model }) {
113
+ const { bare, role } = runDetailPositionals(root, positionals, last);
114
+ checkRunDetailFlagDeps({ last, transcript, prompt, grep, result, model });
102
115
  const classes = result !== null ? parseHistoryResultList(result) : null;
103
116
  const models = model !== null ? parseHistoryModelList(model) : null;
104
117
  return { bare, role, classes, models };
105
118
  }
119
+ /** The not-found branch of cmdRunDetail for the --last form: the scan came up empty, not found
120
+ * by construction, the last-form wording — `sayJson(null)` under --json, else
121
+ * runNotFoundLastMessage. */
122
+ function runDetailLastMiss(parsed, role, json) {
123
+ // The --last scan came up empty: not found by construction, the last-form wording.
124
+ if (json)
125
+ sayJson(null);
126
+ else
127
+ say(runNotFoundLastMessage(parsed.bare ? null : role, parsed.classes, parsed.models));
128
+ }
129
+ /** The not-found branch of cmdRunDetail for the numbered form: not found stays exit 0 in both
130
+ * renderings, but --json still owes a parseable document in every exit-0 case (`null`), else
131
+ * the runNotFoundMessage prose line. */
132
+ function runDetailNumberMiss(role, runNumber, json) {
133
+ // Not found stays exit 0 in both renderings, but --json still owes a parseable document in
134
+ // every exit-0 case (the report --json precedent, the invariant this command's own doc
135
+ // comment claims): `null` is the JSON answer for a single-record lookup that missed — the
136
+ // prose line would end a jq pipe mid-team-incident.
137
+ if (json)
138
+ sayJson(null);
139
+ else
140
+ say(runNotFoundMessage(role, runNumber));
141
+ }
142
+ /** The render half of cmdRunDetail: build the --stat/--patch footer and the --transcript lines,
143
+ * then emit the --json payload or the human-rendered trail (renderRunDetail plus the footer and
144
+ * transcript text). */
145
+ async function emitRunDetail(root, role, runNumber, detail, json, opts) {
146
+ // --json swaps the renderer for the collector's own payload, exactly as history --json: a
147
+ // JSON document in every exit-0 case, never prose.
148
+ const footer = {};
149
+ if (opts.stat)
150
+ footer.stat = await runDetailStat(root, detail);
151
+ if (opts.patch)
152
+ footer.patch = await runDetailPatch(root, detail);
153
+ const transcriptLines = opts.transcript
154
+ ? runTranscriptLines(root, role, runNumber, detail, { includePrompts: opts.prompt, grep: opts.grep ?? undefined })
155
+ : null;
156
+ if (json) {
157
+ sayJson({ ...detail, ...footer, ...(transcriptLines === null ? {} : { transcript: transcriptLines.found }) });
158
+ }
159
+ else {
160
+ const transcriptText = transcriptLines === null
161
+ ? []
162
+ : [transcriptLines.found.length > 0 ? transcriptLines.found.join("\n") : transcriptLines.note];
163
+ say([renderRunDetail(detail), ...Object.values(footer), ...transcriptText].join("\n\n"));
164
+ }
165
+ }
106
166
  /** The resolution half of cmdRunDetail: pick the run to open and read its detail. Bare `--last`
107
167
  * takes the newest run of any role (its role from the row); `--last` with a role the newest run
108
168
  * by number; the numbered path the user's positive integer. An empty or unreadable scan is null
@@ -150,41 +210,12 @@ export async function cmdRunDetail(root, positionals, json, { last = false, stat
150
210
  const parsed = parseRunDetailInvocation(root, positionals, { last, transcript, prompt, grep, result, model });
151
211
  const { role, runNumber, detail } = resolveRunDetailTarget(root, parsed, positionals, last);
152
212
  if (runNumber === null) {
153
- // The --last scan came up empty: not found by construction, the last-form wording.
154
- if (json)
155
- sayJson(null);
156
- else
157
- say(runNotFoundLastMessage(parsed.bare ? null : role, parsed.classes, parsed.models));
213
+ runDetailLastMiss(parsed, role, json);
158
214
  return;
159
215
  }
160
216
  if (detail === null) {
161
- // Not found stays exit 0 in both renderings, but --json still owes a parseable document in
162
- // every exit-0 case (the report --json precedent, the invariant this command's own doc
163
- // comment claims): `null` is the JSON answer for a single-record lookup that missed — the
164
- // prose line would end a jq pipe mid-team-incident.
165
- if (json)
166
- sayJson(null);
167
- else
168
- say(runNotFoundMessage(role, runNumber));
217
+ runDetailNumberMiss(role, runNumber, json);
169
218
  return;
170
219
  }
171
- // --json swaps the renderer for the collector's own payload, exactly as history --json: a
172
- // JSON document in every exit-0 case, never prose.
173
- const footer = {};
174
- if (stat)
175
- footer.stat = await runDetailStat(root, detail);
176
- if (patch)
177
- footer.patch = await runDetailPatch(root, detail);
178
- const transcriptLines = transcript
179
- ? runTranscriptLines(root, role, runNumber, detail, { includePrompts: prompt, grep: grep ?? undefined })
180
- : null;
181
- if (json) {
182
- sayJson({ ...detail, ...footer, ...(transcriptLines === null ? {} : { transcript: transcriptLines.found }) });
183
- }
184
- else {
185
- const transcriptText = transcriptLines === null
186
- ? []
187
- : [transcriptLines.found.length > 0 ? transcriptLines.found.join("\n") : transcriptLines.note];
188
- say([renderRunDetail(detail), ...Object.values(footer), ...transcriptText].join("\n\n"));
189
- }
220
+ await emitRunDetail(root, role, runNumber, detail, json, { stat, patch, transcript, prompt, grep });
190
221
  }
@@ -23,7 +23,7 @@ import { readFeedbackFilings, renderFilingsBlock } from "./feedback-filings.js";
23
23
  import { roleNotesPath } from "../paths.js";
24
24
  import * as path from "node:path";
25
25
  import { pathTokens } from "../text/text.js";
26
- import { isDirectory, readTextOrNull, statOrNull } from "../files/files.js";
26
+ import { isDirectory, readRegularTextOrNull, readTextOrNull, statOrNull } from "../files/files.js";
27
27
  import { dayKey } from "../text/datetime.js";
28
28
  import { appendPromptNote, prependPromptNote } from "../text/format.js";
29
29
  import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
@@ -55,9 +55,11 @@ function missingEntryPaths(root, file, range) {
55
55
  * brief text reader apply to their hand-editable files. A UTF-8 character is never
56
56
  * fewer than one byte, so a note the tool accepted (≤ 4096 bytes) is always ≤ 4096 chars
57
57
  * and never trips this bound; only an over-cap file is cut, with truncateWithNote's visible
58
- * marker naming the loss. */
58
+ * marker naming the loss. A symlinked note file reads as absent (readRegularTextOrNull never
59
+ * follows a symlink out of the worktree, INV-G3/G4's lstat policy): a hostile note that is a
60
+ * symlink cannot pull an arbitrary host file's text into this role's prompt. */
59
61
  function readRoleNote(root, role) {
60
- const text = readTextOrNull(roleNotesPath(root, role));
62
+ const text = readRegularTextOrNull(roleNotesPath(root, role));
61
63
  return text === null ? undefined : truncateWithNote(text, ROLE_NOTES_MAX_BYTES, "role notes");
62
64
  }
63
65
  /** The bugfix run's tally of checks the merge gate excused as flaky in the last day, or
@@ -199,55 +201,40 @@ function collectRoleRequest(root, role, preview) {
199
201
  function collectSelfcheckDigest(root, base) {
200
202
  return base === "selfcheck" || base === "feedback" ? selfcheckDigest(root) : undefined;
201
203
  }
202
- /** The `qa` observer's flow-coverage ledger: rerendered from disk each run, degrading to no
203
- * block on a missing or unreadable ledger (plans/observer-roles.md 2/2). */
204
- function collectQaCoverageBlock(root, base) {
205
- // The `qa` observer's flow rotation needs a memory of what it last exercised; every run
206
- // is a fresh session, and a passing cheap check leaves nothing in the repo. The ledger is
207
- // runtime state, and a missing or unreadable one degrades to no block.
208
- let coverage;
209
- if (base === "qa") {
210
- try {
211
- coverage = renderCoverageBlock(readQaCoverage(root));
212
- }
213
- catch {
214
- coverage = undefined;
215
- }
204
+ /** One observer-ledger block: rendered from disk only for its owning role, degrading to no
205
+ * block on a missing or unreadable ledger (a throw) so the run prompt stays byte-unchanged in
206
+ * the common first-run case. The `qa` (flow coverage), `ci` (parity) and `feedback` (filings)
207
+ * ledgers are runtime state a fresh session must not depend on, and the three collect*Block
208
+ * functions each rendered their ledger through this same try/catch guard before the helper. */
209
+ function runtimeLedgerBlock(base, role, render) {
210
+ if (base !== role)
211
+ return undefined;
212
+ try {
213
+ return render();
214
+ }
215
+ catch {
216
+ return undefined;
216
217
  }
217
- return coverage;
218
218
  }
219
- /** The `ci` observer's parity block: what the last run covered (tip and lock hash) so a
220
- * fresh session can decide nothing-to-do without re-running the whole suite (part 3/3).
221
- * Runtime state, and a missing/unreadable record degrades to no block, exactly like qa's. */
219
+ /** The `qa` observer's flow-coverage ledger, rerendered from disk each run (the flow rotation
220
+ * needs a memory of what it last exercised — every run is a fresh session, and a passing cheap
221
+ * check leaves nothing in the repo), degrading to no block (plans/observer-roles.md 2/2). */
222
+ function collectQaCoverageBlock(root, base) {
223
+ return runtimeLedgerBlock(base, "qa", () => renderCoverageBlock(readQaCoverage(root)));
224
+ }
225
+ /** The `ci` observer's parity block: what the last run covered (tip and lock hash) so a fresh
226
+ * session can decide nothing-to-do without re-running the whole suite (part 3/3). Degrades to
227
+ * no block exactly like qa's. */
222
228
  function collectCiParityBlock(root, config, base) {
223
- let ciParity;
224
- if (base === "ci") {
225
- try {
226
- ciParity = parityBlockFor(root, config);
227
- }
228
- catch {
229
- ciParity = undefined;
230
- }
231
- }
232
- return ciParity;
229
+ return runtimeLedgerBlock(base, "ci", () => parityBlockFor(root, config));
233
230
  }
234
231
  /** The `feedback` observer's per-install filing ledger: what this install already filed as
235
232
  * GitHub issues on znation/tumwater, so the fresh session's charter dedupes against a
236
- * "previously filed" block instead of the network alone. Runtime state, and a missing or
237
- * unreadable ledger degrades to no block (plans/feedback-role.md 2/2). */
233
+ * "previously filed" block instead of the network alone. Degrades to no block
234
+ * (plans/feedback-role.md 2/2); the render returns undefined for an empty ledger, so the
235
+ * prompt is unchanged in the common first-run case. */
238
236
  function collectFeedbackFilingsBlock(root, base) {
239
- // The render returns undefined for an empty ledger, so the prompt is unchanged in the common
240
- // first-run case.
241
- let filings;
242
- if (base === "feedback") {
243
- try {
244
- filings = renderFilingsBlock(readFeedbackFilings(root));
245
- }
246
- catch {
247
- filings = undefined;
248
- }
249
- }
250
- return filings;
237
+ return runtimeLedgerBlock(base, "feedback", () => renderFilingsBlock(readFeedbackFilings(root)));
251
238
  }
252
239
  /** The clean role's deterministic backlog repair: stranded plan headings in the primary
253
240
  * checkout's PLANS.md (plans, part 3/4), rendered like the digest and coverage blocks. */
@@ -258,7 +245,7 @@ function collectBacklogStructureBlock(root, base) {
258
245
  /** Read the role's own notes plus their saved day and live backlog state (notes-backlog.ts),
259
246
  * returning all three in one place. */
260
247
  function readPromptNotes(root, role) {
261
- // The role's notes (PLANS.md "Role notebook"): its own earlier runs' note. A missing,
248
+ // The role's own notes (readRoleNote): its earlier runs' note. A missing,
262
249
  // empty, or unreadable file yields undefined, so the block is omitted; the write
263
250
  // instruction in buildRunPrompt is present either way. The director branch above never
264
251
  // reads this path, so it carries no notes.
@@ -312,15 +299,12 @@ function collectRoleDirectives(root, config, base, check) {
312
299
  : undefined;
313
300
  return { planTargetNote, flakyChecks, noCheck };
314
301
  }
315
- /** Append the shared claim/held/discard/cut-off tail — identical for a director and a role —
316
- * in its current order, and return the finished prompt. */
317
- function appendRunEpilogue(root, role, state, prompt, userPrompt, rejectedReview) {
302
+ function appendAssignmentNotes(prompt, root, role, claim, userPrompt) {
318
303
  // A work instance holding a claim is told which entry is its one task (plans/parallel-work-
319
304
  // instances.md "Claims", part 4/7) — the harness picked it, so the charter's choosing step
320
305
  // is replaced. A run that dequeued a user request runs that request instead and keeps the
321
306
  // claim for the next non-request run. The range is looked up by key in the current checkout,
322
307
  // so it survives a moved section boundary.
323
- const claim = role === DIRECTOR_ROLE ? undefined : state.claim;
324
308
  if (claim && userPrompt === null) {
325
309
  const range = eligibleEntries(root, role).find((e) => e.key === claim.key);
326
310
  prompt = appendPromptNote(prompt, buildAssignmentNote(claim, range, range ? missingEntryPaths(root, claim.file, range) : []));
@@ -328,20 +312,32 @@ function appendRunEpilogue(root, role, state, prompt, userPrompt, rejectedReview
328
312
  if (merged.length > 0)
329
313
  prompt = appendPromptNote(prompt, buildMergedPrerequisitesNote(merged));
330
314
  }
315
+ return prompt;
316
+ }
317
+ function appendWaitingPartsNote(prompt, root, claim, userPrompt) {
331
318
  if (claim && userPrompt === null && claim.file === "PLANS.md") {
332
319
  const waiting = waitingPartRanges(root, claim.title);
333
320
  if (waiting.length > 0)
334
321
  prompt = appendPromptNote(prompt, buildWaitingPartsNote(waiting));
335
322
  }
323
+ return prompt;
324
+ }
325
+ function appendHeldElsewhereNote(prompt, role, claim, userPrompt, state) {
336
326
  if (!claim && userPrompt === null && role !== DIRECTOR_ROLE && state.heldElsewhere?.length) {
337
327
  prompt = appendPromptNote(prompt, buildHeldEntriesNote(state.heldElsewhere));
338
328
  }
329
+ return prompt;
330
+ }
331
+ function prependRejectedReviewNote(prompt, role, rejectedReview) {
339
332
  // The director builds its prompt through buildDirectorPrompt, which does not take the rejected
340
333
  // note, so the standing rejection leads its prompt here instead (a director-authored change can
341
334
  // be rejected too). Kept uniform with the authoring roles: the reasons open the run.
342
335
  if (rejectedReview !== undefined && role === DIRECTOR_ROLE) {
343
336
  prompt = prependPromptNote(prompt, rejectedReview);
344
337
  }
338
+ return prompt;
339
+ }
340
+ function appendRunMemoryNotes(prompt, state) {
345
341
  // Likewise a change leftover recovery discarded as unmergeable: named until the role queues
346
342
  // its next change (the discarding run itself appends it after recovery — see runOnce).
347
343
  const discard = state.conflictDiscard;
@@ -355,6 +351,23 @@ function appendRunEpilogue(root, role, state, prompt, userPrompt, rejectedReview
355
351
  }
356
352
  return prompt;
357
353
  }
354
+ /** Append the shared claim/held/discard/cut-off tail — identical for a director and a role —
355
+ * in its current order, and return the finished prompt. It delegates each note group to a
356
+ * named module-private helper: appendAssignmentNotes (which folds in the merged-prerequisite
357
+ * follow-up), appendWaitingPartsNote, appendHeldElsewhereNote, prependRejectedReviewNote and
358
+ * appendRunMemoryNotes. */
359
+ function appendRunEpilogue(root, role, state, prompt, userPrompt, rejectedReview) {
360
+ // The shared cross-run tail every role prompt carries, ordinally: the claim assignment (with
361
+ // its merged-prerequisite and waiting-part follow-ups), the held-elsewhere note, the
362
+ // director's leading rejection, and the discard and cut-off run memory.
363
+ const claim = role === DIRECTOR_ROLE ? undefined : state.claim;
364
+ prompt = appendAssignmentNotes(prompt, root, role, claim, userPrompt);
365
+ prompt = appendWaitingPartsNote(prompt, root, claim, userPrompt);
366
+ prompt = appendHeldElsewhereNote(prompt, role, claim, userPrompt, state);
367
+ prompt = prependRejectedReviewNote(prompt, role, rejectedReview);
368
+ prompt = appendRunMemoryNotes(prompt, state);
369
+ return prompt;
370
+ }
358
371
  /** Assemble the prompt a role's next run operates on — the whole "what should this run see"
359
372
  * concern, split out of runner.ts so the runner owns only lifecycle and the prompt owns only
360
373
  * content. Returns null when the role has nothing to run this run (a director with an empty