tumwater 0.1.2 → 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 (361) hide show
  1. package/README.md +74 -108
  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-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -17,27 +17,28 @@ const CLAIM_RELEASING = new Set([
17
17
  * happens AFTER the pi run, extracted from LoopRunner.tick (src/loop/loop.ts) so the runner keeps
18
18
  * only tick lifecycle (worktree, sessions, abort) and this module owns the per-result
19
19
  * bookkeeping policy. Order matters here and is pinned by behavior, not by a test seam: the
20
- * outcome's folds land on state before applyTickOutcome schedules the next run, the main-head
21
- * read happens while `running` is still true (a poll landing in that window must not double-wake
22
- * the loop), and the warnings read the streaks exactly once per episode, after the schedule. */
20
+ * outcome's folds are applied to state before applyTickOutcome schedules the next run, the
21
+ * main-head read happens while `running` is still true (a poll that runs in that window must not
22
+ * double-wake the loop), and the warnings read the streaks exactly once per episode, after the
23
+ * schedule. */
23
24
  export async function finalizeTick(deps) {
24
25
  const { root, role, mainBranch, config, state: s, usage, tick } = deps;
25
26
  let { outcome } = deps;
26
- // A re-queued leftover's landing failure is not the tick's own result, so it rides the
27
+ // A re-queued leftover's merge failure is not the tick's own result, so it rides the
27
28
  // outcome separately: applyTickOutcome feeds it into the error streak, and the warning
28
29
  // below names it — even though runTick cleared `lastError` so it never latched onto
29
- // `tick_end` (BUGS.md 2026-09-21).
30
+ // `run_end` (BUGS.md 2026-09-21).
30
31
  if (deps.recoveryFailure !== undefined)
31
32
  outcome.recoveryFailure = deps.recoveryFailure;
32
33
  // A main_red tick's cause is written for that result (src/baseline/main-red.ts), not a leftover
33
- // landing failure: fold it onto the state here so `tick_end` logs it like every other
34
+ // merge failure: fold it onto the state here so `run_end` logs it like every other
34
35
  // failure's and the digest can itemize what the Outcome table's main_red cells mean
35
36
  // (BUGS.md 2026-09-28). runTick cleared `lastError` at its start, so nothing foreign
36
37
  // can precede this.
37
38
  if (outcome.error !== undefined)
38
39
  s.lastError = outcome.error;
39
40
  // Read main's current head while this tick is still reserved (running=true): the
40
- // applyTickOutcome below clears running, and a poll landing between that clear and a later
41
+ // applyTickOutcome below clears running, and a poll that runs between that clear and a later
41
42
  // head update would see a stale lastMainHead and wake the loop again on the very move
42
43
  // that triggered this tick — a duplicate wake event plus an extra tick for one world change.
43
44
  // branchHead reads the ref files first (microsecond-scale, like the orchestrator's per-poll
@@ -57,22 +58,22 @@ export async function finalizeTick(deps) {
57
58
  !hasOutstandingWork(s)) {
58
59
  const title = s.claim.title;
59
60
  s.claim = undefined;
60
- logEventBestEffort(root, { loop: role, type: "claim", action: "released", title, reason: outcome.result });
61
+ logEventBestEffort(root, { source: role, type: "claim", action: "released", title, reason: outcome.result });
61
62
  }
62
63
  // A wake consumed while this tick ran stamped the shared state in place, but the outcome
63
- // schedule above overwrites it (lastTickEndedAt past wokenAt, nextRunAt a fresh gap or
64
+ // schedule above overwrites it (lastRunEndedAt past wokenAt, nextRunAt a fresh gap or
64
65
  // backoff out), so a plain `tumwater wake --role` with an empty queue would silently wait
65
66
  // out the whole interval (BUGS.md 2026-09-25). Re-apply it here — exactly like a wake
66
67
  // arriving one poll after the tick ended.
67
68
  restoreMidTickWake(s);
68
69
  saveLoopState(root, s);
69
70
  // One warning per error episode (BUGS.md 2026-09-15: every loop failing identically
70
- // looked like a quiet fleet). applyTickOutcome increments the streak on error-class
71
- // outcomes, preserves it across a plain `queued` tick (the tick's landing resolves it),
71
+ // looked like a quiet team). applyTickOutcome increments the streak on error-class
72
+ // outcomes, preserves it across a plain `queued` tick (the tick's merge resolves it),
72
73
  // and resets it on any other completed result — so warn only when THIS outcome incremented
73
74
  // the streak up to the threshold: a preserved streak already sitting at the bar must not
74
- // re-warn on the queued tick's end. A rejection's crossing warns from the landing side
75
- // instead (writeLandingOutcome, src/landing/landing-slot.ts), which is where rejections resolve.
75
+ // re-warn on the queued tick's end. A rejection's crossing warns from the merge side
76
+ // instead (writeMergeOutcome, src/merge/merge-slot.ts), which is where rejections resolve.
76
77
  if ((outcome.result === "error" || outcome.recoveryFailure !== undefined) &&
77
78
  (s.consecutiveErrors ?? 0) === ERROR_STREAK_WARN) {
78
79
  warnEventBestEffort(root, role, consecutiveFailuresWarning(s.consecutiveErrors ?? 0, s.lastError ?? outcome.recoveryFailure ?? "unknown error"));
@@ -82,21 +83,21 @@ export async function finalizeTick(deps) {
82
83
  // streak on each kill and resets it on any other result, so the crossing fires once;
83
84
  // the give-up (fresh session + backoff) follows on the next kill.
84
85
  if ((s.quietKillStreak ?? 0) === QUIET_KILL_RESUME_LIMIT) {
85
- warnEventBestEffort(root, role, `${s.quietKillStreak} consecutive quiet kills (no progress): ${s.lastError ?? "unknown hang"}`);
86
+ warnEventBestEffort(root, role, `${s.quietKillStreak} consecutive stalls (no progress): ${s.lastError ?? "unknown hang"}`);
86
87
  }
87
88
  // Per-tick usage: the event feed is where operators see
88
- // spend — this tick's tokens and cost ride on tick_end so a budget pause or a money-burning
89
+ // spend — this tick's tokens and cost ride on run_end so a budget pause or a money-burning
89
90
  // no_change/error/rejected/refused tick shows what it spent without diffing status-table
90
91
  // snapshots. Both fields are per-tick windows (reset at tick start, folded in over every
91
92
  // pi run of the tick); they ride on HarnessEvent's index signature like other payloads and
92
93
  // are omitted when zero so skipped ticks render byte-identical to a pre-feature line.
93
- // The tick's wall-clock span rides on tick_end too: the
94
+ // The tick's wall-clock span rides on run_end too: the
94
95
  // failure digest prices a tick by its own durationMs first (src/failure/failure-data.ts
95
96
  // tickDurationMs), so the field is ALWAYS present — a tick that ends within the same
96
97
  // Date.now() millisecond it started (an instant early-abort error tick) or crosses a
97
98
  // backward clock step still attests its 0 ms, where the old omit-when-zero convention
98
- // dropped the field and broke the "every tick_end carries a span" contract (BUGS.md
99
- // 2026-09-29). Unlike the usage fields, tick_end's renderer never prints the span, so an
99
+ // dropped the field and broke the "every run_end carries a span" contract (BUGS.md
100
+ // 2026-09-29). Unlike the usage fields, run_end's renderer never prints the span, so an
100
101
  // always-present 0 changes no rendered line.
101
102
  const tickDurationMs = Math.max(0, Date.now() - deps.tickStartedAt);
102
103
  // The account state (plans/agent-backends.md part 6a): fold this tick's billing answer and
@@ -111,22 +112,28 @@ export async function finalizeTick(deps) {
111
112
  // The next reporting run rewrites it; nothing this tick depends on is lost.
112
113
  }
113
114
  }
115
+ // The per-provider split of this tick's usage (plans: per-provider spend in the day
116
+ // report): a Record<provider, {costUsd, notionalCostUsd}> for the report's `Cost by
117
+ // provider` line. Omitted when empty like the prompt-token fields, so a tick whose runs
118
+ // carried no provider renders byte-identically to today.
119
+ const providerCost = usage.serializeProviderCost();
114
120
  logEventBestEffort(root, {
115
- loop: role,
116
- type: "tick_end",
117
- tick,
121
+ source: role,
122
+ type: "run_end",
123
+ run: tick,
118
124
  result: outcome.result,
119
125
  summary: outcome.summary,
120
126
  error: s.lastError,
121
127
  durationMs: tickDurationMs,
122
128
  ...usageFragment(s.generatedTokens, usage.costUsd, usage.notionalCostUsd),
129
+ ...(Object.keys(providerCost).length > 0 ? { providerCost } : {}),
123
130
  // The billing answer the tick's runs reported, so the report can tell notional spend from
124
131
  // real spend without re-deriving it from the file (plans/agent-backends.md part 6a).
125
132
  ...(usage.billing !== undefined ? { billing: usage.billing } : {}),
126
133
  // Per-tick prompt-token telemetry: how many prompt
127
134
  // tokens the tick sent, how many were cache reads, and how many went out before its first
128
135
  // edit. Omitted when zero like usageFragment's fields, so a tick that sent no prompt — and
129
- // every tick_end written by an older build — renders byte-identically.
136
+ // every run_end written by an older build — renders byte-identically.
130
137
  ...(usage.promptTokens > 0 ? { promptTokens: usage.promptTokens } : {}),
131
138
  ...(usage.cacheReadTokens > 0 ? { cacheReadTokens: usage.cacheReadTokens } : {}),
132
139
  ...(usage.preEditPromptTokens > 0 ? { preEditPromptTokens: usage.preEditPromptTokens } : {}),
@@ -1,4 +1,5 @@
1
- /** What a finished tick or landing is (the TickResult/TickOutcome vocabulary), shared by the
1
+ // vocabulary: plain
2
+ /** What a finished tick or merge is (the TickResult/TickOutcome vocabulary), shared by the
2
3
  * runner that produces it, the dashboards that render it, and the state machine that applies
3
4
  * it. The application policy — which outcomes count which streaks, when a session resumes,
4
5
  * how the next run is scheduled — lives next door in tick-apply.ts; the clock arithmetic it
@@ -5,26 +5,45 @@ import { configForRole } from "../config/config-views.js";
5
5
  import { NOTHING_TO_DO } from "../verdict/reply-contract.js";
6
6
  import { dequeuePromptEntry, dequeueRolePromptEntry, peekPrompt, peekRolePrompt } from "../inbox/inbox.js";
7
7
  import { stripNotBeforeMarker } from "../inbox/prompt-not-before.js";
8
- import { readBrief } from "../brief.js";
8
+ import { readBrief } from "../brief/brief.js";
9
9
  import { buildDirectorPrompt, buildTickPrompt } from "../prompt/prompt.js";
10
10
  import { readPrinciples } from "../prompt/principles.js";
11
11
  import { buildCutOffNote } from "../prompt/prompt-followup.js";
12
- import { buildAssignmentNote, buildConflictDiscardNote, buildHeldEntriesNote, buildRejectedReviewNote } from "../prompt/gate-prompts.js";
13
- import { eligibleEntries } from "../backlog/backlog-eligibility.js";
12
+ import { buildAssignmentNote, buildConflictDiscardNote, buildHeldEntriesNote, buildMergedPrerequisitesNote, buildRejectedReviewNote, buildWaitingPartsNote } from "../prompt/gate-prompts.js";
13
+ import { eligibleEntries, mergedPrerequisiteRanges, waitingPartRanges } from "../backlog/backlog-eligibility.js";
14
14
  import { detectBuildCheck } from "../build/build-check-detect.js";
15
+ import { flakeCounts, KNOWN_FLAKE_WINDOW_MS } from "../review/known-flakes.js";
16
+ import { readEventsSince } from "../events/event-window.js";
15
17
  import { telemetryDigest } from "./telemetry-digest.js";
16
18
  import { readQaCoverage, renderCoverageBlock } from "./qa-coverage.js";
17
19
  import { roleNotesPath } from "../paths.js";
18
- import { readTextOrNull } from "../files/files.js";
19
- import { ROLE_NOTES_MAX_BYTES } from "../pi-extension/role-notes.js";
20
+ import * as path from "node:path";
21
+ import { pathTokens } from "./stage-check.js";
22
+ import { readTextOrNull, statOrNull } from "../files/files.js";
23
+ import { dayKey } from "../text/datetime.js";
24
+ import { appendPromptNote } from "../text/format.js";
25
+ import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
20
26
  import { truncateWithNote } from "../text/text.js";
21
27
  import { renderBacklogIndexBlock, renderBacklogStructureBlock } from "../backlog/backlog-structure.js";
28
+ /** The repo paths the entry at `file` lines `range` names that are absent from the checkout at
29
+ * `root`: tokens whose first segment is a directory there but which do not exist themselves —
30
+ * the self-check's top-level-directory rule, read from the working tree. */
31
+ function missingEntryPaths(root, file, range) {
32
+ const text = readTextOrNull(path.join(root, file));
33
+ if (text === null)
34
+ return [];
35
+ const entry = text.split("\n").slice(range.start - 1, range.end).join("\n");
36
+ return pathTokens(entry).filter((p) => {
37
+ const top = p.slice(0, p.indexOf("/"));
38
+ return statOrNull(path.join(root, top))?.isDirectory() === true && statOrNull(path.join(root, p)) === null;
39
+ });
40
+ }
22
41
  /** Read a role's notebook through files.ts's readTextOrNull — the one read-and-swallow
23
42
  * step — or undefined when it is missing or unreadable (an empty or whitespace-only note is
24
43
  * left for buildTickPrompt to omit). The degradation is deliberate: a torn or absent
25
44
  * runtime-state file must never fail a tick.
26
45
  *
27
- * The note is bounded on read as well as on write: the role_notes tool rejects text over
46
+ * The note is bounded on read as well as on write: the write path rejects text over
28
47
  * ROLE_NOTES_MAX_BYTES, but the file lives in hand-editable runtime state, so an oversized
29
48
  * note (an old build, a manual edit) would otherwise ride into every one of this role's
30
49
  * prefill-heavy tick prompts uncapped. The same defensive backstop readPrinciples and the
@@ -34,7 +53,30 @@ import { renderBacklogIndexBlock, renderBacklogStructureBlock } from "../backlog
34
53
  * marker naming the loss. */
35
54
  function readRoleNote(root, role) {
36
55
  const text = readTextOrNull(roleNotesPath(root, role));
37
- return text === null ? undefined : truncateWithNote(text, ROLE_NOTES_MAX_BYTES, "role notebook");
56
+ return text === null ? undefined : truncateWithNote(text, ROLE_NOTES_MAX_BYTES, "role notes");
57
+ }
58
+ /** The bugfix run's tally of checks the merge gate excused as flaky in the last day, or
59
+ * undefined when none (or the log is unreadable — a missing tally never fails a run). */
60
+ function flakyChecksNote(root) {
61
+ let counts;
62
+ try {
63
+ counts = flakeCounts(readEventsSince(root, KNOWN_FLAKE_WINDOW_MS).events);
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ if (counts.length === 0)
69
+ return undefined;
70
+ const shown = counts
71
+ .slice(0, 5)
72
+ .map(([key, n]) => `"${key}" ×${n}`)
73
+ .join("; ");
74
+ const more = counts.length > 5 ? ` (and ${counts.length - 5} more)` : "";
75
+ return (`Flaky checks: in the last 24h the merge gate excused these check failures as flaky (each ` +
76
+ `failed, then passed on its one re-run), most frequent first: ${shown}${more}. An excused ` +
77
+ `flake blocks no merge, so it stays unfixed until it has a BUGS.md entry. When an open bug ` +
78
+ `names one of these, that bug is still firing. When \`## Open\` holds no entries, a check ` +
79
+ `excused 2 or more times is a confirmed latent bug: take it as this run's latent-bug candidate.`);
38
80
  }
39
81
  /** Assemble the prompt a loop's next tick runs on — the whole "what should this tick see"
40
82
  * concern, split out of loop.ts so the runner owns only lifecycle and the prompt owns only
@@ -43,7 +85,7 @@ function readRoleNote(root, role) {
43
85
  * queued by `tumwater prompt --role <id>` — back so the runner can record it as pending and
44
86
  * re-queue it if the tick leaves it unfulfilled; `userPromptFile` is the queue file it came
45
87
  * from, so that bookkeeping can clean the prompt's attachments by that file's stem. */
46
- export function assembleTickPrompt({ root, config, role, state, preview = false, agent }) {
88
+ export function assembleTickPrompt({ root, config, role, state, preview = false, agent, compact = false }) {
47
89
  // One read of the brief yields both the prompt it carries and the owning file's name
48
90
  // (brief.ts's readBrief): readInitialPrompt + briefFile were two full reads of the same
49
91
  // file on this per-tick path.
@@ -51,7 +93,7 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
51
93
  const initialPrompt = briefState?.prompt ?? "";
52
94
  // The brief's owning file (TUMWATER.md first, README.md as the compatibility path —
53
95
  // plans/portability.md §7a/7), named in both prompt builders' rules instead of a hardcoded
54
- // README.md. "README.md" is the fallback for a repo with no marked file yet — the fleet
96
+ // README.md. "README.md" is the fallback for a repo with no marked file yet — the team
55
97
  // runs blind on prompts either way, so the name in the rules should still point somewhere.
56
98
  const brief = briefState?.file ?? "README.md";
57
99
  // The project's design principles ride along in every prompt — tick and director alike — so
@@ -79,24 +121,26 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
79
121
  return null;
80
122
  userPrompt = stripNotBeforeMarker(dequeued);
81
123
  userPromptFile = entry?.file ?? null;
82
- prompt = buildDirectorPrompt(userPrompt, initialPrompt, principles, check, brief, undefined, backlogIndex, agent ?? configForRole(config, DIRECTOR_ROLE).agent);
124
+ // The compact prompt leaves the brief block out: the director reads the brief file in full,
125
+ // and that file carries the same text.
126
+ prompt = buildDirectorPrompt(userPrompt, compact ? "" : initialPrompt, principles, check, brief, undefined, backlogIndex, agent ?? configForRole(config, DIRECTOR_ROLE).agent, compact, config.roles[DIRECTOR_ROLE]?.instructions);
83
127
  }
84
128
  else {
85
129
  // The loop id keys its inbox and notebook; every lookup ABOUT the role — catalog charter,
86
- // instructions, the qa/telemetry/clean blocks — resolves through the base role, so
130
+ // instructions, the qa/selfcheck/clean blocks — resolves through the base role, so
87
131
  // `feature-2` runs the feature charter and `roles.feature.instructions` while keeping its
88
132
  // own queue and notes (plans/parallel-work-instances.md).
89
133
  const base = baseRoleOf(role);
90
134
  // Catalog first, then user-defined loops (plans/user-defined-loops.md): a custom's task
91
135
  // is its entire find-something-to-do text and the title identifies it in the prompt.
92
- const custom = config.customLoops.find((c) => c.name === role);
136
+ const custom = config.customRoles.find((c) => c.name === role);
93
137
  const resolved = roleById(role) ?? (custom ? customRole(custom.name, custom.task) : undefined);
94
138
  if (!resolved) {
95
139
  // The shared unknownRoleMessage (parseRoleFlag, the operator commands) names the valid
96
140
  // ids; this defensive path — a runner asked to tick a role no catalog entry or
97
- // customLoops task can answer — says the same, so if it ever fires it reads as the
141
+ // customRoles task can answer — says the same, so if it ever fires it reads as the
98
142
  // harness bug it is instead of a bare dead end.
99
- const validIds = [...allRoleIds(), ...config.customLoops.map((c) => c.name)];
143
+ const validIds = [...allRoleIds(), ...config.customRoles.map((c) => c.name)];
100
144
  throw new Error(unknownRoleMessage(role, validIds));
101
145
  }
102
146
  // A queued per-role prompt is dequeued here, before the prompt is built, so its text rides
@@ -109,9 +153,9 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
109
153
  const entry = preview ? null : dequeueRolePromptEntry(root, role);
110
154
  const dequeued = preview ? peekRolePrompt(root, role) : entry?.text ?? null;
111
155
  const request = dequeued === null ? null : stripNotBeforeMarker(dequeued);
112
- // The telemetry role's evidence is the harness's own event log, one level outside this
156
+ // The selfcheck role's evidence is the harness's own event log, one level outside this
113
157
  // worktree, so its evidence module renders it (telemetryDigest) and the tick injects it.
114
- const digest = base === "telemetry" ? telemetryDigest(root) : undefined;
158
+ const digest = base === "selfcheck" ? telemetryDigest(root) : undefined;
115
159
  // The `qa` observer's flow rotation needs a memory of what it last exercised; every tick
116
160
  // is a fresh session, and a passing cheap check leaves nothing in the repo. The ledger is
117
161
  // runtime state, and a missing or unreadable one degrades to no block
@@ -134,31 +178,47 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
134
178
  // instruction in buildTickPrompt is present either way. The director branch above never
135
179
  // reads this path, so it carries no notebook.
136
180
  const notes = readRoleNote(root, role);
181
+ // The note file's mtime is when the notes were last saved (writes replace the file), so the
182
+ // run can weigh how stale they are.
183
+ const savedStat = notes === undefined ? null : statOrNull(roleNotesPath(root, role));
184
+ const notesSavedDay = savedStat ? dayKey(savedStat.mtimeMs) : undefined;
137
185
  // The plan charter's stop threshold is derived from config (plans/parallel-work-
138
186
  // instances.md "…keeping the plan loop ahead", part 5c/7). The catalog text stays a
139
187
  // config-free description of the rule; this note supplies the concrete eligible-plan count
140
188
  // so the plan loop stops only once every configured feature instance has a plan waiting.
141
189
  const planTargetNote = base === "plan"
142
- ? `This tick's stop target: when PLANS.md \`## Planned\` already holds ` +
143
- `${planBacklogTarget(config)} or more eligible plans, end with ${NOTHING_TO_DO}.`
190
+ ? `This run's stop target: when PLANS.md \`## Planned\` already holds ` +
191
+ `${planBacklogTarget(config)} or more eligible plans, end with ${NOTHING_TO_DO}. ` +
192
+ `It holds ${eligibleEntries(root, "feature").length} now (tumwater's count of entries ` +
193
+ `feature could take, the same count that decides whether a plan run is deferred).`
144
194
  : undefined;
195
+ const flakyChecks = base === "bugfix" ? flakyChecksNote(root) : undefined;
196
+ // A run holding a claim with no queued request is assigned its one entry (the assignment
197
+ // note below). In the compact prompt such a run reads that entry first, so the backlog index
198
+ // — what an unassigned run chooses its task from — stays out. The brief block stays out of
199
+ // every compact prompt: an unassigned run reads the brief file in full, which carries the
200
+ // same text, and an assigned one reads it only when its entry needs it.
201
+ const variant = compact ? { assigned: state.claim !== undefined && request === null } : undefined;
145
202
  prompt = buildTickPrompt({
146
203
  role: resolved,
147
- initialPrompt,
204
+ initialPrompt: compact ? "" : initialPrompt,
148
205
  principles,
149
206
  digest,
150
207
  coverage,
151
208
  backlogStructure,
152
- backlogIndex,
209
+ backlogIndex: variant?.assigned ? undefined : backlogIndex,
153
210
  notes,
211
+ notesSavedDay,
154
212
  planTargetNote,
213
+ flakyChecks,
155
214
  extraInstructions: config.roles[base]?.instructions,
156
215
  check,
157
216
  briefFile: brief,
158
217
  agent: agent ?? configForRole(config, role).agent,
218
+ compact: variant,
159
219
  // A per-role prompt queued by `tumwater prompt --role <id>` rides as an explicit user
160
220
  // request block; its text is also the tick's userPrompt,
161
- // so the runner's pending-prompt machinery (re-queue on unfulfilled, clear on landing)
221
+ // so the runner's pending-prompt machinery (re-queue on unfulfilled, clear on merge)
162
222
  // treats it exactly like the director's dequeued request.
163
223
  userRequest: request ?? undefined,
164
224
  });
@@ -175,27 +235,35 @@ export function assembleTickPrompt({ root, config, role, state, preview = false,
175
235
  const claim = role === DIRECTOR_ROLE ? undefined : state.claim;
176
236
  if (claim && userPrompt === null) {
177
237
  const range = eligibleEntries(root, role).find((e) => e.key === claim.key);
178
- prompt += `\n\n${buildAssignmentNote(claim, range)}`;
238
+ prompt = appendPromptNote(prompt, buildAssignmentNote(claim, range, range ? missingEntryPaths(root, claim.file, range) : []));
239
+ const merged = claim.file === "PLANS.md" ? mergedPrerequisiteRanges(root, claim.title) : [];
240
+ if (merged.length > 0)
241
+ prompt = appendPromptNote(prompt, buildMergedPrerequisitesNote(merged, compact));
242
+ }
243
+ if (claim && userPrompt === null && claim.file === "PLANS.md") {
244
+ const waiting = waitingPartRanges(root, claim.title);
245
+ if (waiting.length > 0)
246
+ prompt = appendPromptNote(prompt, buildWaitingPartsNote(waiting, compact));
179
247
  }
180
248
  if (!claim && userPrompt === null && role !== DIRECTOR_ROLE && state.heldElsewhere?.length) {
181
- prompt += `\n\n${buildHeldEntriesNote(state.heldElsewhere)}`;
249
+ prompt = appendPromptNote(prompt, buildHeldEntriesNote(state.heldElsewhere));
182
250
  }
183
251
  // A change rejected in review is the only cross-tick memory of what was built and why it
184
252
  // failed — every tick starts a fresh session, so the full reasons ride along on the next
185
253
  // prompt until the role's next reviewed change replaces them.
186
254
  if (state.lastReview?.verdict === "reject" && !state.revision) {
187
- prompt += `\n\n${buildRejectedReviewNote(state.lastReview, role)}`;
255
+ prompt = appendPromptNote(prompt, buildRejectedReviewNote(state.lastReview, role));
188
256
  }
189
257
  // Likewise a change leftover recovery discarded as unmergeable: named until the role queues
190
258
  // its next change (the discarding tick itself appends it after recovery — see runTick).
191
259
  const discard = state.conflictDiscard;
192
260
  if (discard)
193
- prompt += `\n\n${buildConflictDiscardNote(discard.summary, discard.attempts)}`;
261
+ prompt = appendPromptNote(prompt, buildConflictDiscardNote(discard.summary, discard.attempts));
194
262
  // A fresh tick after the previous run(s) were cut off at the context ceiling (the loop
195
263
  // stopped resuming, or never resumed — the director re-runs its prompt fresh): the only
196
264
  // memory that the last attempt was too big for the window is this note.
197
265
  if ((state.cutOffStreak ?? 0) > 0) {
198
- prompt += `\n\n${buildCutOffNote(state.cutOffStreak ?? 0)}`;
266
+ prompt = appendPromptNote(prompt, buildCutOffNote(state.cutOffStreak ?? 0));
199
267
  }
200
268
  return { prompt, userPrompt, userPromptFile };
201
269
  }
@@ -1,6 +1,6 @@
1
1
  import { agentSessionResumable } from "../agent/agent-backend.js";
2
2
  import { latestSession } from "../agent/session-ledger.js";
3
- import { RETRIABLE_LANDING_RESULTS } from "../landing/landing-core.js";
3
+ import { RETRIABLE_MERGE_RESULTS } from "../merge/merge-core.js";
4
4
  import { sessionDir } from "../paths.js";
5
5
  import { buildHandoffPrompt, buildResumePrompt } from "../prompt/prompt-followup.js";
6
6
  /** Decide how a tick starts, consuming the resume flags as a side effect (the flag is
@@ -10,12 +10,12 @@ import { buildHandoffPrompt, buildResumePrompt } from "../prompt/prompt-followup
10
10
  * inbox); a role loop's assembly never returns null. */
11
11
  export function planTickStart(opts) {
12
12
  const s = opts.state;
13
- // The last landing's failure, read before the clear below: a landing that failed
14
- // non-terminally kept its pin (the lander's own vocabulary — review_error, merge_conflict,
13
+ // The last merge's failure, read before the clear below: a merge that failed
14
+ // non-terminally kept its pin (the merger's own vocabulary — review_error, merge_conflict,
15
15
  // merge_blocked — with the detail it wrote into `lastError`), and when this tick's recovery
16
16
  // re-queues that pin, the failure must still feed the error streak (finishRecoveryTick).
17
- const priorLandingFailure = s.lastResult !== undefined && RETRIABLE_LANDING_RESULTS.has(s.lastResult)
18
- ? (s.lastError ?? `landing failed: ${s.lastResult}`)
17
+ const priorMergeFailure = s.lastResult !== undefined && RETRIABLE_MERGE_RESULTS.has(s.lastResult)
18
+ ? (s.lastError ?? `merge failed: ${s.lastResult}`)
19
19
  : undefined;
20
20
  s.lastError = undefined;
21
21
  // A tick interrupted by a harness shutdown left its pi session and its worktree's
@@ -42,7 +42,7 @@ export function planTickStart(opts) {
42
42
  // claude resumes an id (safe across a newer gate line). A backend with no `resumable` hook
43
43
  // falls back to the backend-neutral session-file check.
44
44
  const resumable = resumePending && !resumeMissing
45
- ? agentSessionResumable(dir, { kind: "author", agent })
45
+ ? agentSessionResumable(dir, { kind: "author", agent }, opts.claudeConfigDir)
46
46
  : false;
47
47
  const resumableSession = resumable;
48
48
  // A newest line our backend cannot resume is not ours to continue: another kind's or
@@ -53,7 +53,7 @@ export function planTickStart(opts) {
53
53
  // session over the kept worktree, carrying the assembled tick prompt plus the handoff
54
54
  // bridge (plans/agent-backends.md "Sessions"). The failed `resumeMissing` run arms
55
55
  // `resumePending` and records `resumeMissing` from the verdict (its error turn carries no
56
- // assistant content), so the very next tick lands here with the worktree intact. A dir with
56
+ // assistant content), so the very next tick ends up here with the worktree intact. A dir with
57
57
  // no ledger line at all is not a handoff — there is no recorded interrupted session to carry
58
58
  // forward.
59
59
  // Captured before the flag is consumed below; a stale cause must not leak into a later resume.
@@ -62,7 +62,12 @@ export function planTickStart(opts) {
62
62
  // survive, but the next run is a FRESH session — the fallback seam may be a different backend,
63
63
  // and claude keeps no flags across `--resume` (plans/agent-backends.md §7, part 7a/7).
64
64
  const budgetCapped = pendingResumeCause === "budget-capped";
65
- const handoff = (latest !== null && !resumable) || budgetCapped;
65
+ // An image-blind run with an image-capable peer tier ends `aborted` with this cause
66
+ // (plans/image-input.md part 4/5): the next tick runs a FRESH session on the TARGET seam
67
+ // over the kept worktree — never `--continue` — because the old session carries pi's
68
+ // "image omitted" note and the target may be a different backend.
69
+ const imageReroute = pendingResumeCause === "image-reroute";
70
+ const handoff = (latest !== null && !resumable) || budgetCapped || imageReroute;
66
71
  s.resumePending = false;
67
72
  s.resumeCause = undefined;
68
73
  s.resumeMissing = undefined;
@@ -74,7 +79,7 @@ export function planTickStart(opts) {
74
79
  const resumeBlocked = s.phase === "review";
75
80
  // A budget-capped handback never resumes the session whose seam the cap moved off of, even
76
81
  // when its transcript is still resumable: it hands off instead.
77
- const resuming = resumableSession && !resumeBlocked && !budgetCapped;
82
+ const resuming = resumableSession && !resumeBlocked && !budgetCapped && !imageReroute;
78
83
  const handingOff = handoff && !resumeBlocked;
79
84
  // Reclaim the prompt the interrupted tick re-queued for its resume (src/inbox/pending-prompt.ts):
80
85
  // the resumed session still owns that request in its context, so this tick's bookkeeping
@@ -91,19 +96,30 @@ export function planTickStart(opts) {
91
96
  const resumeCause = handingOff
92
97
  ? budgetCapped
93
98
  ? "budget-capped"
94
- : "handoff"
99
+ : imageReroute
100
+ ? "image-reroute"
101
+ : "handoff"
95
102
  : pendingResumeCause ?? ((s.cutOffStreak ?? 0) > 0 ? "cut-off" : "restart");
96
103
  let prompt;
97
104
  if (resuming) {
98
105
  prompt = buildResumePrompt(opts.role, resumeCause, agent);
99
106
  }
100
107
  else {
101
- const assembled = opts.tickPrompt(agent);
108
+ const assembled = opts.tickPrompt(agent, opts.compact);
109
+ // The image-reroute handoff note names the model the blind run was on and the image it
110
+ // read; those facts ride the one-shot marker, which is consumed here. A handoff's fresh
111
+ // session also needs the to-be-run model named for its own side of the note, but the
112
+ // bridge already carries the facts that matter.
113
+ const rerouteFacts = imageReroute && s.imageReroute
114
+ ? { from: s.imageReroute.from ?? "the prior model", path: s.imageReroute.path ?? "an image" }
115
+ : undefined;
116
+ if (imageReroute)
117
+ s.imageReroute = undefined;
102
118
  prompt =
103
119
  assembled === null
104
120
  ? null
105
121
  : handingOff
106
- ? buildHandoffPrompt(opts.role, assembled, resumeCause)
122
+ ? buildHandoffPrompt(opts.role, assembled, resumeCause, rerouteFacts)
107
123
  : assembled;
108
124
  }
109
125
  if (prompt === null)
@@ -111,5 +127,5 @@ export function planTickStart(opts) {
111
127
  // The raw user prompt a director tick is executing (null for role loops), so an
112
128
  // unfulfilled outcome below can re-queue it. Captured before the field is cleared.
113
129
  const userPrompt = opts.pending.get();
114
- return { priorLandingFailure, resuming, handoff: handingOff, resumeCause, prompt, userPrompt };
130
+ return { priorMergeFailure, resuming, handoff: handingOff, resumeCause, prompt, userPrompt };
115
131
  }