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
@@ -3,10 +3,10 @@ import { DIRECTOR_ROLE, OBSERVER_ROLES } from "../roles/roles.js";
3
3
  import { baseRoleOf } from "../roles/loop-ids.js";
4
4
  import { ERROR_BACKOFF, pushYieldOutcome, scheduleAtMinInterval, scheduleBackoff, } from "../scheduling/backoff.js";
5
5
  import { firstReason } from "../text/phrases.js";
6
- /** The state machine that applies a finished tick or landing to the loop's state: the
6
+ /** The state machine that applies a finished tick or merge to the loop's state: the
7
7
  * outcome application (applyTickOutcome — which outcomes count which streaks, when a
8
- * cut-off or quiet-killed session resumes and when it is abandoned), its landing sibling
9
- * (applyLandingOutcome), the error-streak thresholds, the resume limits, and the
8
+ * cut-off or quiet-killed session resumes and when it is abandoned), its merge sibling
9
+ * (applyMergeOutcome), the error-streak thresholds, the resume limits, and the
10
10
  * review-verdict record the next tick's prompt reads. Split out of tick-outcome.ts — which
11
11
  * keeps the TickResult/TickOutcome vocabulary the dashboards and call sites share — because
12
12
  * "what an outcome is" and "what applying it records" are different concerns that happen to
@@ -19,13 +19,13 @@ export function recordReview(s, verdict, reasons, head) {
19
19
  s.lastReview = { verdict, reasons, ...(head === undefined ? {} : { head }), at: Date.now() };
20
20
  }
21
21
  /** Drop a stale review rejection once the change it objected to is superseded (BUGS.md
22
- * 2026-10-07): the review-exempt and refusal-note landings record no new verdict, so a reject
22
+ * 2026-10-07): the review-exempt and refusal-note merges record no new verdict, so a reject
23
23
  * would otherwise ride every later tick prompt as if it were the role's last review outcome.
24
- * Called from the points a change lands (`applyLandingOutcome`'s changed branch and
25
- * `handleRefusal`'s note merge), never while a revision is still owed: a landing that leaves
24
+ * Called from the points a change merges (`applyMergeOutcome`'s changed branch and
25
+ * `handleRefusal`'s note merge), never while a revision is still owed: a merge that leaves
26
26
  * `state.revision` set — a per-role user-request tick did not apply the rejected change — did
27
27
  * not resolve it, and its next revision tick still needs the objections (loop-tick.ts's revision
28
- * note). A model-reviewed landing already overwrote `lastReview` with its approve; this only
28
+ * note). A model-reviewed merge already overwrote `lastReview` with its approve; this only
29
29
  * clears a standing `reject`. */
30
30
  export function clearSupersededRejection(s) {
31
31
  if (s.revision === undefined && s.lastReview?.verdict === "reject")
@@ -33,7 +33,7 @@ export function clearSupersededRejection(s) {
33
33
  }
34
34
  /** Consecutive failed ticks after which the loop raises a harness warning and its state
35
35
  * cell reads "failing" (BUGS.md 2026-09-15: every loop failing identically looked like a
36
- * quiet fleet). Small on purpose — at 3 the error ladder has already doubled twice and
36
+ * quiet team). Small on purpose — at 3 the error ladder has already doubled twice and
37
37
  * the failure is clearly not transient. */
38
38
  export const ERROR_STREAK_WARN = 3;
39
39
  /** Consecutive failed ticks after which the error-streak circuit breaker (src/gates/streak-gate.ts)
@@ -53,7 +53,7 @@ const CUT_OFF_RESUME_LIMIT = 3;
53
53
  * and takes a fresh tick on the idle ladder. A session the backend defers past the quiet
54
54
  * watchdog every attempt would otherwise re-send the same input immediately and forever,
55
55
  * burning an hour of slot time per retry with no output (BUGS.md 2026-09-18: 41 quiet kills
56
- * consumed 44% of the fleet over two days). Mirrors CUT_OFF_RESUME_LIMIT; the streak itself
56
+ * consumed 44% of the team over two days). Mirrors CUT_OFF_RESUME_LIMIT; the streak itself
57
57
  * keeps counting (LoopState.quietKillStreak) so one warning per episode can be raised. */
58
58
  export const QUIET_KILL_RESUME_LIMIT = 3;
59
59
  /** Record a finished tick on the loop's state and schedule its next run from the outcome.
@@ -61,30 +61,31 @@ export const QUIET_KILL_RESUME_LIMIT = 3;
61
61
  * promptly, which back off and on which ladder, how cut-off resumes are bounded — sits with
62
62
  * the other scheduling helpers here instead of inline in the tick lifecycle. Mutates `s` in place:
63
63
  * the caller's state object is authoritative across an in-flight tick (see resetCounters).
64
- * `cfg` is the role-resolved config (configForRole): its minTickIntervalSeconds carries any
64
+ * `cfg` is the role-resolved config (configForRole): its minRunIntervalSeconds carries any
65
65
  * per-role slow clock, and idleBackoff passes through it unchanged from the top level. */
66
66
  export function applyTickOutcome(s, cfg, role, outcome) {
67
67
  s.running = false;
68
68
  s.parkedSince = undefined;
69
+ s.parkedOn = undefined;
69
70
  // The cut-off streak counts EVERY consecutive tick truncated at the context ceiling, past the
70
71
  // resume limit too: the next fresh tick's prompt names how many attempts the window has eaten
71
72
  // (buildCutOffNote), so the count must not freeze at the limit. Any other outcome resets it.
72
73
  s.cutOffStreak = outcome.cutOff ? (s.cutOffStreak ?? 0) + 1 : 0;
73
74
  // The error streak counts consecutive failed ticks; any other result resets it
74
75
  // (BUGS.md 2026-09-15: 44 identical failures raised no alarm). A tick whose leftover
75
- // recovery re-queued a pin its last landing failed to land counts too, even though the tick
76
+ // recovery re-queued a pin its last merge failed counts too, even though the tick
76
77
  // itself ends `queued` (outcome.recoveryFailure): a dead reviewer backend would otherwise
77
78
  // reset the streak every tick and never raise the alarm (BUGS.md 2026-09-21).
78
79
  // tick-finalize.ts emits one warning when the streak crosses ERROR_STREAK_WARN — once per
79
80
  // episode, since the reset re-arms it — and the dashboards read "failing" from the same field.
80
81
  // A plain `queued` tick is not yet a verdict about anything: it committed and enqueued, and
81
- // its LANDING resolves the streak — applyLandingOutcome resets it when the change lands and
82
+ // its MERGE resolves the streak — applyMergeOutcome resets it when the change merges and
82
83
  // counts a review rejection into it (BUGS.md 2026-09-30: review-rejected authoring, the
83
- // digest's #1 loss cause, fell straight through — the tick's `queued` reset and the landing's
84
+ // digest's #1 loss cause, fell straight through — the tick's `queued` reset and the merge's
84
85
  // `rejected` increment met nowhere, so the streak read 0 through the whole episode and the
85
86
  // re-author ran at min interval, warning-free and pause-proof). Resetting here would pin a
86
87
  // rejection streak at 1 forever, since every authoring tick in a rejection episode ends
87
- // `queued` before its landing is decided.
88
+ // `queued` before its merge is decided.
88
89
  const failed = outcome.result === "error" || outcome.recoveryFailure !== undefined;
89
90
  if (failed)
90
91
  s.consecutiveErrors = (s.consecutiveErrors ?? 0) + 1;
@@ -94,7 +95,16 @@ export function applyTickOutcome(s, cfg, role, outcome) {
94
95
  // tick-finalize.ts warns once when it crosses QUIET_KILL_RESUME_LIMIT, and the branch below uses
95
96
  // it to bound how many times a starved session is resumed (BUGS.md 2026-09-18).
96
97
  s.quietKillStreak = outcome.result === "quiet_killed" ? (s.quietKillStreak ?? 0) + 1 : 0;
97
- // The yield ring records the result beside the streaks: a landing or a counted empty
98
+ // The no-change streak counts consecutive completed ticks that ended `no_change` (the
99
+ // resting state cell's ` · no change ×N` suffix — status-model.ts loopPhase). Any other
100
+ // completed result resets it, so a loop that merges between no-ops never shows a stale
101
+ // streak. Every completed result reaches this block (tick-finalize.ts calls
102
+ // applyTickOutcome for every outcome), so `changed`/`queued`/`error`/`aborted`/
103
+ // `quiet_killed`/`user_aborted` all reset it and only a `no_change` tick grows it.
104
+ // Observational only — never warns, unlike the error/quiet-kill streaks: a legitimately
105
+ // idle maintenance loop with an empty backlog can hold a long streak and that is expected.
106
+ s.noChangeStreak = outcome.result === "no_change" ? (s.noChangeStreak ?? 0) + 1 : 0;
107
+ // The yield ring records the result beside the streaks: a merge or a counted empty
98
108
  // stretches the role's min-tick gap (yieldMultiplier, read by isEligible), an error-class
99
109
  // result is skipped entirely — no evidence either way (yield-scaled clocks, PLANS.md).
100
110
  pushYieldOutcome(s, outcome.result);
@@ -105,18 +115,18 @@ export function applyTickOutcome(s, cfg, role, outcome) {
105
115
  // already committed.
106
116
  if (outcome.result !== "aborted")
107
117
  s.phase = undefined;
108
- s.lastTickEndedAt = Date.now();
118
+ s.lastRunEndedAt = Date.now();
109
119
  // `lastResult`/`lastSummary` are the last COMPLETED result, the pair the dashboards' "last
110
120
  // result" cell renders, and they must describe ONE tick: a completed outcome without a
111
121
  // summary CLEARS `lastSummary`, so a summary-less result never renders beside another
112
122
  // tick's work description (BUGS.md 2026-09-30: a no_change tick read "No change — Implement
113
- // the … plan" beside work that had landed, because the prior summary was retained).
123
+ // the … plan" beside work that had merged, because the prior summary was retained).
114
124
  // A `queued` tick is not a completed result: its change is committed but in flight —
115
- // the state column shows that (`landing <elapsed>`, the land-queue badge) — so the pair
125
+ // the state column shows that (`merge <elapsed>`, the merge-queue badge) — so the pair
116
126
  // keeps the prior outcome WITH its own summary while the change waits (BUGS.md 2026-09-23),
117
- // and the tick's summary is stashed under the pinned sha for applyLandingOutcome to pair with
118
- // the landing's result. Every other outcome is completed and clears the stash: a role with a
119
- // queued or in-flight landing never ticks (the orchestrator's interlock), so a stash that
127
+ // and the tick's summary is stashed under the pinned sha for applyMergeOutcome to pair with
128
+ // the merge's result. Every other outcome is completed and clears the stash: a role with a
129
+ // queued or in-flight merge never ticks (the orchestrator's interlock), so a stash that
120
130
  // survives to a later tick names a change that is no longer waiting.
121
131
  if (outcome.result === "queued") {
122
132
  s.queuedSummary =
@@ -129,12 +139,12 @@ export function applyTickOutcome(s, cfg, role, outcome) {
129
139
  }
130
140
  if (outcome.result === "changed" || outcome.result === "queued") {
131
141
  // "queued" schedules exactly like "changed" — the tick committed and enqueued — but the
132
- // commit count waits for the landing: `commits` keeps meaning "landed on main", and
133
- // applyLandingOutcome increments it when the change actually lands. The phase-clear above
142
+ // commit count waits for the merge: `commits` keeps meaning "merged to main", and
143
+ // applyMergeOutcome increments it when the change actually merges. The phase-clear above
134
144
  // (`result !== "aborted"`) covers both.
135
145
  if (outcome.result === "changed")
136
146
  s.commits += 1;
137
- // A landed change's patch-id approval has done its job: the same patch authored again
147
+ // A merged change's patch-id approval has done its job: the same patch authored again
138
148
  // later (after a revert, say) is a new change and gets its own review.
139
149
  if (outcome.result === "changed")
140
150
  s.lastApprovedPatchId = undefined;
@@ -146,7 +156,7 @@ export function applyTickOutcome(s, cfg, role, outcome) {
146
156
  else if (outcome.result === "rejected") {
147
157
  // The reviewer objected and the gate already reset the branch: the author should address
148
158
  // the recorded reasons on its next eligible tick, not sleep through them — schedule like
149
- // a change without counting a commit (nothing landed).
159
+ // a change without counting a commit (nothing merged).
150
160
  s.backoffSeconds = 0;
151
161
  scheduleAtMinInterval(s, cfg);
152
162
  }
@@ -205,7 +215,7 @@ export function applyTickOutcome(s, cfg, role, outcome) {
205
215
  // value, so an error streak capped at the error ceiling never sleeps LESS than the loop
206
216
  // already was sleeping, and the idle ladder picks up from there if the failures stop.
207
217
  // A subscription claude usage-window rejection is a transient hold, not a failure of the
208
- // session: the fleet hold keeps ticks from starting until `resetsAt` (plans/agent-backends.md
218
+ // session: the team hold keeps ticks from starting until `resetsAt` (plans/agent-backends.md
209
219
  // §8, part 6c), and the kept session and worktree edits resume when it lifts (part 6e).
210
220
  // The named cause is what the bridge prompt reads, so it does not tell the session the
211
221
  // harness merely restarted.
@@ -228,7 +238,7 @@ export function applyTickOutcome(s, cfg, role, outcome) {
228
238
  else if (OBSERVER_ROLES.has(baseRoleOf(role))) {
229
239
  // An observer's no_change is a success ("checked, all well"), not an idle verdict: it must
230
240
  // not climb the idle ladder, which would punish the role monotonically for the product
231
- // being healthy. With no ladder, minTickIntervalSeconds is the sole, honest cadence
241
+ // being healthy. With no ladder, minRunIntervalSeconds is the sole, honest cadence
232
242
  // (plans/observer-roles.md). The error/user_aborted arms above are untouched — a broken
233
243
  // toolchain and a deliberate operator stop still back off like any other role.
234
244
  s.backoffSeconds = 0;
@@ -243,7 +253,7 @@ export function applyTickOutcome(s, cfg, role, outcome) {
243
253
  // outcome abandons them (the next fresh tick resets the worktree to main), so the hand-back
244
254
  // is spent: clear the record, or it would make a later change's cap conflict discard the
245
255
  // pin instead of handing it back (PLANS.md "Robust conflict landing, part 2/2"). `changed`
246
- // clears too — the resolved change landed in-tick, so its life is over. This runs after the
256
+ // clears too — the resolved change merged in-tick, so its life is over. This runs after the
247
257
  // resume branches above have set `resumePending`.
248
258
  if (s.conflictHandback &&
249
259
  outcome.result !== "queued" &&
@@ -251,71 +261,88 @@ export function applyTickOutcome(s, cfg, role, outcome) {
251
261
  s.conflictHandback = undefined;
252
262
  }
253
263
  }
254
- /** Record a completed LANDING on the authoring loop's state (plans/merge-queue.md 3/5):
255
- * the orchestrator calls this on the state object the landing ran against — the runner's live
264
+ /** Record a completed MERGE on the authoring loop's state (plans/merge-queue.md 3/5):
265
+ * the orchestrator calls this on the state object the merge ran against — the runner's live
256
266
  * copy when one exists, a disk load otherwise — then saves it, the same in-place discipline
257
267
  * applyTickOutcome has (the caller's state object is authoritative; a role with a queued or
258
- * in-flight landing never ticks, so no other writer holds it meanwhile). Sets `lastResult`
259
- * to the lander's outcome, increments `commits` only on a success (the "landed on main"
260
- * counter applyTickOutcome's "queued" branch left to the landing), and clears `phase` — the
268
+ * in-flight merge never ticks, so no other writer holds it meanwhile). Sets `lastResult`
269
+ * to the merger's outcome, increments `commits` only on a success (the "merged to main"
270
+ * counter applyTickOutcome's "queued" branch left to the merge), and clears `phase` — the
261
271
  * gate persisted `phase = "review"` on this same state object during the run, exactly as it
262
- * did inside a tick, so the label must not linger past the landing (mirroring
263
- * applyTickOutcome's rule: every outcome except "aborted" clears it — an aborted landing
272
+ * did inside a tick, so the label must not linger past the merge (mirroring
273
+ * applyTickOutcome's rule: every outcome except "aborted" clears it — an aborted merge
264
274
  * keeps it, because the interruption hit mid-review and the next launch must recover and
265
- * re-review the pinned work). `lastSummary` becomes the summary of the change this landing
266
- * resolved — `change` is its land-queue entry — so the landing's result, success or failure,
275
+ * re-review the pinned work). `lastSummary` becomes the summary of the change this merge
276
+ * resolved — `change` is its merge-queue entry — so the merge's result, success or failure,
267
277
  * reads beside the work it names (BUGS.md 2026-09-23: the queued tick held its summary back
268
278
  * rather than pair it with the prior result). The tick's stashed `queuedSummary` wins when it
269
279
  * names this sha: it is the text the tick reported, high-friction annotation included. The
270
280
  * entry's own summary (the commit subject) covers a stash that is missing or names another
271
- * change — a crash between enqueue and the tick's state save, or a landing that finished before
281
+ * change — a crash between enqueue and the tick's state save, or a merge that finished before
272
282
  * its tick's outcome was applied. Either way the stash is consumed. The failure detail rides
273
- * in `lastReview` and the landed/land_failed events. */
274
- export function applyLandingOutcome(s, result, change) {
283
+ * in `lastReview` and the merge_done/merge_failed events. */
284
+ export function applyMergeOutcome(s, result, change) {
275
285
  s.lastResult = result;
276
286
  s.lastSummary = s.queuedSummary?.sha === change.sha ? s.queuedSummary.summary : change.summary;
277
287
  s.queuedSummary = undefined;
278
288
  if (result === "changed") {
279
289
  s.commits += 1;
290
+ // The last-merged timestamp: set only on a successful merge, so the rendered age means
291
+ // "time since the loop last actually merged to main" — a rejected/merge_conflict/aborted
292
+ // merge, like a no_change tick, leaves the prior value standing (PLANS.md "Each loop's
293
+ // last-landed age").
294
+ s.lastMergedAt = Date.now();
280
295
  s.lastApprovedPatchId = undefined; // see applyTickOutcome
281
- // A landing supersedes a stale rejection: a model-reviewed landing has already overwritten
282
- // lastReview with its approve, but a review-exempt (or review-disabled) landing records no
296
+ // A merge supersedes a stale rejection: a model-reviewed merge has already overwritten
297
+ // lastReview with its approve, but a review-exempt (or review-disabled) merge records no
283
298
  // verdict, so a stale reject would otherwise ride every later tick prompt (BUGS.md
284
299
  // 2026-10-07). Cleared only with no revision pending — see clearSupersededRejection.
285
300
  clearSupersededRejection(s);
286
- // A landed change resets the error streak exactly as an error-free tick does (BUGS.md
301
+ // A merged change resets the error streak exactly as an error-free tick does (BUGS.md
287
302
  // 2026-09-30): the queued tick that authored it preserved the streak — a rejection counts
288
- // into it below — so the landing, the episode's only clean verdict, ends it.
303
+ // into it below — so the merge, the episode's only clean verdict, ends it.
289
304
  s.consecutiveErrors = 0;
290
305
  }
291
306
  if (result === "rejected") {
292
- // A review rejection is a failure the streak must count (BUGS.md 2026-09-30): the
293
- // authoring tick ended `queued`, which no longer resets the streak (see applyTickOutcome),
294
- // so this landing outcome is the episode's accumulation point — the same field the warn,
295
- // the breaker (src/gates/streak-gate.ts), and the error-storm observation
296
- // (src/fleet/fleet-polls.ts) already read. No new machinery, no new constants: the warn
307
+ // A review rejection is a failure the streak must count ONLY when it is final (BUGS.md
308
+ // 2026-10-14): recordRejectedChange (src/merge/merge-core.ts) already decided finality
309
+ // by the time this runs — it sets `state.revision` when the change goes back for another
310
+ // round and clears it when the chain is exhausted (or never sets it for a role that cannot
311
+ // revise, the director). A rejection that leaves a revision to work on is not a verdict
312
+ // about the author yet: counting every round of ONE change made an exhausted chain's third
313
+ // round cross the warn bar and flash a red "failing tick after tick" alert that needed
314
+ // nothing from the operator and was gone before it could be read (0–30 s). Counting only
315
+ // final rejections makes a streak of 3 mean three changes ended rejected (or errored) —
316
+ // the non-converging author the 2026-09-30 fix meant to catch — while a chain's rounds
317
+ // leave the streak unchanged and keep the fast minimum-interval retry the queued tick
318
+ // scheduled. This merge outcome is the accumulation point (the authoring tick ends
319
+ // `queued`, which no longer resets the streak, see applyTickOutcome) — the same field the
320
+ // warn, the breaker (src/gates/streak-gate.ts), and the error-storm observation
321
+ // (src/team/team-polls.ts) already read. No new machinery, no new constants: the warn
297
322
  // bar and the breaker bar are ERROR_STREAK_WARN and ERROR_STREAK_BREAKER themselves.
298
- s.consecutiveErrors = (s.consecutiveErrors ?? 0) + 1;
299
- const lastReasons = s.lastReview?.verdict === "reject" ? s.lastReview.reasons : undefined;
300
- s.lastError = `review rejected: ${firstReason(lastReasons).slice(0, 200)}`;
301
- // Past the warn bar the re-author backs off on the error ladder instead of the minimum
302
- // interval the queued tick scheduled: the fast first retries stay — the rejected branch's
303
- // design intent, letting the author address the recorded reasons at once — but a
304
- // non-converging author slows down instead of re-authoring at full price all day. The
305
- // rung derives from the streak (not from backoffSeconds, which every queued tick zeroes
306
- // on its way out, pinning a state-derived ladder at its first rung), and never pulls
307
- // nextRunAt earlier than the already-scheduled minimum interval.
308
- const streak = s.consecutiveErrors;
309
- if (streak >= ERROR_STREAK_WARN) {
310
- const rung = Math.min(ERROR_BACKOFF.initialSeconds * 2 ** (streak - ERROR_STREAK_WARN), ERROR_BACKOFF.maxSeconds);
311
- s.backoffSeconds = rung;
312
- s.nextRunAt = Math.max(s.nextRunAt ?? 0, Date.now() + rung * 1000);
323
+ if (s.revision === undefined) {
324
+ s.consecutiveErrors = (s.consecutiveErrors ?? 0) + 1;
325
+ const lastReasons = s.lastReview?.verdict === "reject" ? s.lastReview.reasons : undefined;
326
+ s.lastError = `review rejected: ${firstReason(lastReasons).slice(0, 200)}`;
327
+ // Past the warn bar the re-author backs off on the error ladder instead of the minimum
328
+ // interval the queued tick scheduled: the fast first retries stay — the rejected branch's
329
+ // design intent, letting the author address the recorded reasons at once — but a
330
+ // non-converging author slows down instead of re-authoring at full price all day. The
331
+ // rung derives from the streak (not from backoffSeconds, which every queued tick zeroes
332
+ // on its way out, pinning a state-derived ladder at its first rung), and never pulls
333
+ // nextRunAt earlier than the already-scheduled minimum interval.
334
+ const streak = s.consecutiveErrors;
335
+ if (streak >= ERROR_STREAK_WARN) {
336
+ const rung = Math.min(ERROR_BACKOFF.initialSeconds * 2 ** (streak - ERROR_STREAK_WARN), ERROR_BACKOFF.maxSeconds);
337
+ s.backoffSeconds = rung;
338
+ s.nextRunAt = Math.max(s.nextRunAt ?? 0, Date.now() + rung * 1000);
339
+ }
313
340
  }
314
341
  }
315
- // A landing or a rejection is the end of the change's life; a merge_conflict keeps its pin.
342
+ // A merge or a rejection is the end of the change's life; a merge_conflict keeps its pin.
316
343
  // The per-change markers below all clear on this one outcome set, so name it once.
317
344
  const changeGone = result === "changed" || result === "rejected";
318
- // The conflict streak counts failed resolutions of one pinned sha; a landing or a rejection
345
+ // The conflict streak counts failed resolutions of one pinned sha; a merge or a rejection
319
346
  // ends that pin's life. Every other outcome kept the pin as it was, so its count stands.
320
347
  if (result === "merge_conflict") {
321
348
  const prior = s.mergeConflicts?.sha === change.sha ? s.mergeConflicts.count : 0;
@@ -324,13 +351,15 @@ export function applyLandingOutcome(s, result, change) {
324
351
  else if (changeGone) {
325
352
  s.mergeConflicts = undefined;
326
353
  }
327
- // Likewise the red-landing-check streak (keyed by patch-id): its change is gone either way.
328
- if (changeGone)
329
- s.landingCheckFailures = undefined;
330
- // A permanent-reviewer-config hold ends with its pin's life: landed, or rejected away.
354
+ // Likewise the red-merge-check streak (keyed by patch-id): its change is gone either way.
355
+ if (changeGone) {
356
+ s.mergeCheckFailures = undefined;
357
+ s.mergeFlakeExcused = undefined;
358
+ }
359
+ // A permanent-reviewer-config hold ends with its pin's life: merged, or rejected away.
331
360
  if (changeGone)
332
- s.landingReviewError = undefined;
333
- // A hand-back ends with its change's life too: once the resolved change lands or is rejected,
361
+ s.mergeReviewError = undefined;
362
+ // A hand-back ends with its change's life too: once the resolved change merges or is rejected,
334
363
  // a later change gets its own one-hand-back budget (PLANS.md "Robust conflict landing, part
335
364
  // 2/2"). A merge_conflict keeps it, so the resolved change's own cap discard fires.
336
365
  if (changeGone)
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  /** Collection half of `tumwater tick <role> <n>`: one completed tick's full event block,
2
3
  * distilled from the event log into a TickDetail payload. The collector-in-core convention
3
4
  * beside history-data.ts and src/report/report-data.ts — a pure read over root, so core
@@ -7,58 +8,58 @@
7
8
  * Every datum rides the events the harness already writes, so this is a distillation of the
8
9
  * existing record, not a new one. The rendering half — the summary header, formatEvent, the
9
10
  * CLI command — lives in tick-detail.ts. */
10
- import { HISTORY_SCAN_MAX_EVENTS } from "../history/history-data.js";
11
+ import { HISTORY_SCAN_MAX_EVENTS, bucketMergeEvents, resolveQueuedResult } from "../history/history-data.js";
11
12
  import { eventUsage, readEvents, tickSpanMs, tickStartMap } from "../events/event-read.js";
12
13
  import { outcomeText, usageText } from "../events/event-format.js";
13
14
  /** The event block of `role`'s tick `n`, or null when the scanned window holds no such tick.
14
- * The newest `tick_start` the log retains for `${role}#${n}` wins (a counter reset can make an
15
+ * The newest `run_start` the log retains for `${role}#${n}` wins (a counter reset can make an
15
16
  * old tick number recur; the operator asking by number means the most recent one), and the
16
- * block closes at the first `tick_end` for that tick after the start — or, when that end is
17
- * missing (in flight, or its tail lost to rotation), at the loop's next `tick_start`, so a
17
+ * block closes at the first `run_end` for that tick after the start — or, when that end is
18
+ * missing (in flight, or its tail lost to rotation), at the loop's next `run_start`, so a
18
19
  * rotation-cut block never leaks the following tick's events into this one. When even the
19
- * start is gone but a matching `tick_end` survives, the end-alone block still answers with
20
+ * start is gone but a matching `run_end` survives, the end-alone block still answers with
20
21
  * `startTs` null and a null duration (the unpaired rule); nothing at all in the scan is the
21
22
  * not-found case the caller renders as a not-found line. */
22
23
  export function readTickDetail(root, role, tick) {
23
24
  // The scan rides history-data.ts's shared one-ask ceiling (HISTORY_SCAN_MAX_EVENTS): one
24
- // tick's block is small, but it can sit arbitrarily far back in a long-lived fleet's log,
25
+ // tick's block is small, but it can sit arbitrarily far back in a long-lived team's log,
25
26
  // and readTailText reads bytes proportional to the limit's line count — so the cap is also
26
27
  // the largest byte read one command can cost, and every read-only view scans under the
27
28
  // same bound.
28
- const events = readEvents(root, HISTORY_SCAN_MAX_EVENTS).filter((e) => e.loop === role);
29
- // Newest tick_start for this loop+tick: the block's lower bound.
29
+ const events = readEvents(root, HISTORY_SCAN_MAX_EVENTS).filter((e) => e.source === role);
30
+ // Newest run_start for this loop+tick: the block's lower bound.
30
31
  let startIdx = -1;
31
32
  for (let i = events.length - 1; i >= 0; i--) {
32
33
  const e = events[i];
33
- if (e.type === "tick_start" && e.tick === tick) {
34
+ if (e.type === "run_start" && e.run === tick) {
34
35
  startIdx = i;
35
36
  break;
36
37
  }
37
38
  }
38
39
  if (startIdx < 0) {
39
- // The start is gone (rotation) or the tick never ran. A surviving tick_end for the same
40
+ // The start is gone (rotation) or the tick never ran. A surviving run_end for the same
40
41
  // number still answers — unpaired, undated at the front — else there is no such tick here.
41
42
  for (let i = events.length - 1; i >= 0; i--) {
42
43
  const e = events[i];
43
- if (e.type === "tick_end" && e.tick === tick)
44
+ if (e.type === "run_end" && e.run === tick)
44
45
  return detailFrom(role, tick, events, [e], e, null);
45
46
  }
46
47
  return null;
47
48
  }
48
49
  const start = events[startIdx];
49
- // The block's upper bound: the first tick_end for this tick after the start ends it; a
50
- // later tick_start (this tick's end lost to rotation) closes the block exclusive — the
50
+ // The block's upper bound: the first run_end for this tick after the start ends it; a
51
+ // later run_start (this tick's end lost to rotation) closes the block exclusive — the
51
52
  // events after it belong to the next tick, not this one. No end and no next start reads as
52
53
  // in flight: everything since the start belongs to the open tick.
53
54
  let end = null;
54
55
  let nextStart = null;
55
56
  for (let i = startIdx + 1; i < events.length; i++) {
56
57
  const e = events[i];
57
- if (e.type === "tick_end" && e.tick === tick) {
58
+ if (e.type === "run_end" && e.run === tick) {
58
59
  end = e;
59
60
  break;
60
61
  }
61
- if (e.type === "tick_start") {
62
+ if (e.type === "run_start") {
62
63
  nextStart = e;
63
64
  break;
64
65
  }
@@ -74,21 +75,34 @@ export function readTickDetail(root, role, tick) {
74
75
  }
75
76
  return detailFrom(role, tick, events, block, end, start.ts);
76
77
  }
77
- /** Fill the payload around a collected block: usage from the tick_end via eventUsage/usageText,
78
+ /** Fill the payload around a collected block: usage from the run_end via eventUsage/usageText,
78
79
  * duration via tickSpanMs against the loop's own start map (the shared pairing helper, so this
79
80
  * view and the history rows cannot drift on what a span is), events oldest-first. */
80
81
  function detailFrom(role, tick, scoped, block, end, startTs) {
81
82
  const usageNums = end === null ? { tokens: 0, costUsd: 0 } : eventUsage(end);
83
+ let result = end === null ? null : outcomeText(end);
84
+ let merge = null;
85
+ if (end !== null && result === "queued") {
86
+ // A fresh claim cursor suits one tick: the walk starts at the newest pin at or before this
87
+ // tick's end, which is its own pin, since later ticks' pins sit after it.
88
+ const { mergeQueuedByLoop, outcomeByLoop } = bucketMergeEvents(scoped);
89
+ const resolved = resolveQueuedResult(end, mergeQueuedByLoop, outcomeByLoop, new Map(), tickStartMap(scoped), null);
90
+ if (resolved !== null) {
91
+ result = resolved.result;
92
+ merge = resolved.outcome;
93
+ }
94
+ }
82
95
  return {
83
96
  role,
84
- tick,
97
+ run: tick,
85
98
  startTs,
86
99
  endTs: end === null ? null : end.ts,
87
100
  durationMs: end === null ? null : tickSpanMs(end, tickStartMap(scoped)),
88
- result: end === null ? null : outcomeText(end),
101
+ result,
89
102
  tokens: usageNums.tokens,
90
103
  costUsd: usageNums.costUsd,
91
104
  usage: end === null ? "" : usageText(end),
92
105
  events: block,
106
+ merge,
93
107
  };
94
108
  }
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  /** Rendering + command half of `tumwater tick <role> <n>`: one completed tick's full event
2
3
  * block printed as a short summary header followed by the tick's events through the shared
3
4
  * formatEvent. The payload itself is collected by tick-detail-data.ts (the collector-in-core
@@ -10,13 +11,17 @@ import { knownRoleIdsCached } from "../config/config.js";
10
11
  import { readTickRows } from "../history/history-data.js";
11
12
  import { readTickDetail } from "./tick-detail-data.js";
12
13
  import { formatEvent } from "../events/event-format.js";
14
+ import { eventTypeLabel } from "../events/event-labels.js";
15
+ import { roleLabel } from "../roles/role-labels.js";
13
16
  import { unknownRoleMessage } from "../roles/roles.js";
14
17
  import { shortSpanPhrase } from "../text/phrases.js";
15
18
  import { shortSha } from "../text/format.js";
19
+ import { gitTry } from "../git/git-run.js";
20
+ import { canonicalRoleId } from "../roles/legacy-role-ids.js";
16
21
  /** The tick command's synopsis, word for word what cli.ts's dispatcher gate and cmdTick's own
17
22
  * arity and unknown-role guards fail with — one string so the three sites cannot drift apart
18
23
  * when the command's shape changes. */
19
- export const TICK_USAGE = "tumwater tick <role> [<n>] [--last] [--json]";
24
+ export const TICK_USAGE = "tumwater history <role> [<n>] [--last] [--stat] [--json]";
20
25
  /** The shared not-found frame both wordings complete — what the scan looked for, the loop,
21
26
  * and that the scanned window (not the world) is what came up empty. Exactly two consumers,
22
27
  * the not-found wordings below (the numbered form's exported message and the --last form's
@@ -25,41 +30,76 @@ function tickNotFoundPhrase(role, what) {
25
30
  return `no ${what} for ${role} in the scanned window (the retained log may have rotated past it)`;
26
31
  }
27
32
  /** The not-found wording a missed tick lookup owes its surface — `tumwater tick <role> <n>`'s
28
- * stdout line and the GUI /api/tick endpoint's 404 JSON error, one template so the CLI's prose
33
+ * stdout line and the GUI /api/run endpoint's 404 JSON error, one template so the CLI's prose
29
34
  * and the browser's error card cannot drift when the phrasing moves. Names the shape a user
30
35
  * can act on: the tick number, the loop, and that the scanned window (not the world) is what
31
36
  * came up empty. */
32
37
  export function tickNotFoundMessage(role, tick) {
33
- return tickNotFoundPhrase(role, `tick #${tick}`);
38
+ return tickNotFoundPhrase(role, `run #${tick}`);
34
39
  }
35
40
  /** The not-found wording for `tumwater tick <role> --last`, sibling of tickNotFoundMessage —
36
41
  * the same surfaces but naming what the last-form scan actually asked for: the newest
37
- * completed tick, not a number the user supplied. Module-local: the GUI's /api/tick 404 only
42
+ * completed tick, not a number the user supplied. Module-local: the GUI's /api/run 404 only
38
43
  * ever serves the numbered form, so this wording has no second consumer. */
39
44
  function tickNotFoundLastMessage(role) {
40
- return tickNotFoundPhrase(role, "completed tick");
45
+ return tickNotFoundPhrase(role, "completed run");
46
+ }
47
+ /** The commit a tick pinned: the `commit` of the first `merge_queued`/`merge_done` event in its
48
+ * block carrying a non-empty one, else null. The one rule both the header's sha and --stat use. */
49
+ function tickCommitSha(d) {
50
+ const queued = d.events.find((e) => (e.type === "merge_queued" || e.type === "merge_done") && typeof e.commit === "string" && e.commit !== "");
51
+ return queued === undefined ? null : queued.commit;
52
+ }
53
+ /** The per-file `git show --stat` summary of a tick's pinned commit. The sha is the authored
54
+ * commit the tick pinned: a merge that rebased gives main a different sha with the same
55
+ * changes, so this describes what the tick changed. */
56
+ async function tickCommitStat(root, sha) {
57
+ if (sha === null)
58
+ return "(no commit: this run sent nothing to merge)";
59
+ const out = await gitTry(root, "show", "--stat", "--format=", sha);
60
+ if (out === null)
61
+ return `(commit ${shortSha(sha)} is not in this repository)`;
62
+ return out.trimEnd();
63
+ }
64
+ /** The pinned commit's stat text for a tick, shared by `tick --stat` and GET /api/run so both
65
+ * apply one rule for the sha and the "(no commit …)" / "(commit … is not in this repository)"
66
+ * texts. */
67
+ export function tickDetailStat(root, d) {
68
+ return tickCommitStat(root, tickCommitSha(d));
41
69
  }
42
70
  /** The human view of a TickDetail: one summary line — result (or the in-flight/unpaired
43
- * marker), duration, usage, and the pinned commit sha when the block's `land_queued`/`landed`
71
+ * marker), duration, usage, and the pinned commit sha when the block's `merge_queued`/`merge_done`
44
72
  * event carries one — then each event through formatEvent, the shared renderer `logs` and the
45
- * TUI use, so the trail reads exactly like the feed it came from. */
73
+ * TUI use, so the trail reads exactly like the feed it came from. A queued tick whose merge
74
+ * has finished shows that verdict in the header and ends with the merge's outcome line; a
75
+ * leftover-recovery run's verdict is the failed attempt it re-queued (resolveQueuedResult),
76
+ * logged before the run began, so that outcome line leads the trail instead — the trail stays
77
+ * in time order. */
46
78
  export function renderTickDetail(d) {
47
79
  const summary = [];
48
80
  if (d.endTs === null)
49
81
  summary.push(d.startTs === null ? "unpaired" : "in flight");
50
82
  else
51
- summary.push(d.result || "?");
83
+ summary.push(d.result ? eventTypeLabel(d.result) : "?");
52
84
  if (d.durationMs !== null)
53
85
  summary.push(shortSpanPhrase(d.durationMs));
54
86
  if (d.usage !== "")
55
87
  summary.push(d.usage);
56
- const queued = d.events.find((e) => (e.type === "land_queued" || e.type === "landed") && typeof e.commit === "string" && e.commit !== "");
57
- let header = `${d.role} tick #${d.tick} — ${summary.join(" · ")}`;
58
- if (queued !== undefined)
59
- header += ` · commit ${shortSha(queued.commit)}`;
60
- return [header, ...d.events.map(formatEvent)].join("\n");
88
+ const sha = tickCommitSha(d);
89
+ let header = `${roleLabel(d.role)} run #${d.run} — ${summary.join(" · ")}`;
90
+ if (sha !== null)
91
+ header += ` · commit ${shortSha(sha)}`;
92
+ const trail = d.events.map(formatEvent);
93
+ if (d.merge !== null) {
94
+ const first = d.events[0];
95
+ if (first !== undefined && d.merge.ts < first.ts)
96
+ trail.unshift(formatEvent(d.merge));
97
+ else
98
+ trail.push(formatEvent(d.merge));
99
+ }
100
+ return [header, ...trail].join("\n");
61
101
  }
62
- /** `tumwater tick <role> [<n>] [--last] [--json]`: print one completed tick's full event
102
+ /** `tumwater tick <role> [<n>] [--last] [--stat] [--json]`: print one completed tick's full event
63
103
  * trail. Read-only: stdout only, no state file created — a tick the scan cannot find prints a
64
104
  * not-found line and exits 0 (history's empty-output convention: an absent record is an
65
105
  * answer, not a failure), and under --json it prints the JSON document `null` instead — the
@@ -69,7 +109,7 @@ export function renderTickDetail(d) {
69
109
  * numbered form would — the summary header and event rendering are shared, only the number's
70
110
  * provenance differs (readTickRows' newest-first scan, not the user's argument). --last and a
71
111
  * numeric <n> are rivals: exactly one positional under --last, exactly two without. */
72
- export async function cmdTick(root, positionals, json, last = false) {
112
+ export async function cmdTick(root, positionals, json, last = false, stat = false) {
73
113
  // Positional arity by form: cli.ts has already peeled the flags off and gated them (only
74
114
  // --json and --last are admitted), so anything left over that does not fit the chosen form
75
115
  // is a mistake. --last takes exactly one positional (<role>); the numeric path keeps
@@ -77,7 +117,7 @@ export async function cmdTick(root, positionals, json, last = false) {
77
117
  const arity = last ? 1 : 2;
78
118
  if (positionals.length !== arity)
79
119
  fail(`usage: ${TICK_USAGE}`);
80
- const role = positionals[0];
120
+ const role = canonicalRoleId(positionals[0]);
81
121
  // The role is validated like every role-targeting command's --role (knownRoleIdsCached: the
82
122
  // built-ins plus user-defined loops, read through the never-throwing cached loader so a
83
123
  // transiently broken tumwater.json cannot take a read-only view down), with the shared
@@ -91,8 +131,8 @@ export async function cmdTick(root, positionals, json, last = false) {
91
131
  // number, read from readTickRows' newest-first scan for the role. An empty scan is not
92
132
  // found by construction, so the not-found path below takes it with the last-form wording.
93
133
  const tick = last
94
- ? (readTickRows(root, 1, role)[0]?.tick ?? null)
95
- : parseCountFlag("tick <n>", positionals[1]);
134
+ ? (readTickRows(root, 1, role)[0]?.run ?? null)
135
+ : parseCountFlag("history <n>", positionals[1]);
96
136
  const detail = tick === null ? null : readTickDetail(root, role, tick);
97
137
  if (tick === null) {
98
138
  // The --last scan came up empty: not found by construction, the last-form wording.
@@ -106,7 +146,7 @@ export async function cmdTick(root, positionals, json, last = false) {
106
146
  // Not found stays exit 0 in both renderings, but --json still owes a parseable document in
107
147
  // every exit-0 case (the report --json precedent, the invariant this command's own doc
108
148
  // comment claims): `null` is the JSON answer for a single-record lookup that missed — the
109
- // prose line would end a jq pipe mid-fleet-incident.
149
+ // prose line would end a jq pipe mid-team-incident.
110
150
  if (json)
111
151
  sayJson(null);
112
152
  else
@@ -115,8 +155,9 @@ export async function cmdTick(root, positionals, json, last = false) {
115
155
  }
116
156
  // --json swaps the renderer for the collector's own payload, exactly as history --json: a
117
157
  // JSON document in every exit-0 case, never prose.
158
+ const statText = stat ? await tickDetailStat(root, detail) : null;
118
159
  if (json)
119
- sayJson(detail);
160
+ sayJson(statText === null ? detail : { ...detail, stat: statText });
120
161
  else
121
- say(renderTickDetail(detail));
162
+ say(statText === null ? renderTickDetail(detail) : `${renderTickDetail(detail)}\n\n${statText}`);
122
163
  }