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
@@ -1,17 +1,17 @@
1
1
  import { eventRole, eventUsage, tickSpanMs, tickStartMap } from "../events/event-read.js";
2
2
  import { errorTextOrPlaceholder, normalizeClusterKey, poolTimeoutKey, sortedRoles, truncateExample } from "./failure-cluster.js";
3
- import { resolveQueuedResult, bucketLandingEvents } from "../history/history-data.js";
3
+ import { resolveQueuedResult, bucketMergeEvents } from "../history/history-data.js";
4
4
  import { nonNegativeNumber, stringList } from "../files/json-object.js";
5
5
  import { getOrCreate, groupBy, rankByCount } from "../collections.js";
6
6
  /** How the Outcome table's results collapse for costing:
7
- * "landed" made progress, "no_change" spent a tick and landed nothing, and every remaining
8
- * result is "error-class" — it burned agent time without landing, whether the cause was a
7
+ * "merged" made progress, "no_change" spent a tick and merged nothing, and every remaining
8
+ * result is "error-class" — it burned agent time without merge, whether the cause was a
9
9
  * hard failure or the review gate. Typed as a full Record so a result added to
10
10
  * src/tick/tick-outcome.ts and forgotten here is a compile error, like RESULT_ORDER in
11
11
  * src/failure/failure-render.ts. */
12
- const OUTCOME_CLASS = {
13
- changed: "landed",
14
- queued: "landed",
12
+ export const OUTCOME_CLASS = {
13
+ changed: "merged",
14
+ queued: "merged",
15
15
  no_change: "no_change",
16
16
  refused: "no_change",
17
17
  skipped: "no_change",
@@ -28,29 +28,39 @@ const OUTCOME_CLASS = {
28
28
  /** A row's summed wall-clock span across its outcome classes — the sort key that puts the
29
29
  * burning role first, the way the usage report ranks its cost lines. */
30
30
  function sumMs(classes) {
31
- return classes.landed.ms + classes.no_change.ms + classes.error.ms;
31
+ return classes.merged.ms + classes.no_change.ms + classes.error.ms;
32
32
  }
33
33
  /** The loss-cause cut, like ERROR_TOP above: the five most expensive causes by time. */
34
34
  const LOSS_TOP = 5;
35
35
  /** The raw results whose loss attributes to a cluster — an error, an abort, a quiet kill, or
36
- * a red-main skip, each carrying its own message on the tick_end (main_red's is the
36
+ * a red-main skip, each carrying its own message on the run_end (main_red's is the
37
37
  * redMainMessage the error-clusters section already itemizes, BUGS.md 2026-10-07). A queued
38
- * tick's landing outcomes are owned by the joins below instead: review_error through its
38
+ * tick's merge outcomes are owned by the joins below instead: review_error through its
39
39
  * matching `review_failed` message, review-rejected through its review reasons, and
40
40
  * merge_conflict/merge_blocked by the result itself (BUGS.md 2026-10-10). (A raw
41
- * `user_aborted` tick_end is also error-class but is not in this set, so it stays out of the
41
+ * `user_aborted` run_end is also error-class but is not in this set, so it stays out of the
42
42
  * ranking.) */
43
43
  const CLUSTERED_RESULTS = new Set(["error", "aborted", "quiet_killed", "main_red"]);
44
- /** The newest event for `role` inside a landing attempt's pin→outcome window — the attempt no
44
+ /** The newest event for `role` inside a merge attempt's pin→outcome window — the attempt no
45
45
  * other can occupy. Shared by the review-rejected and review_error cause joins, so the two
46
46
  * agree on which event belongs to the attempt (BUGS.md 2026-10-10: the window, not the pin's
47
- * sha, is the match, since every vet rebases its pin before the gate). */
47
+ * sha, is the match, since every verification rebases its pin before the gate). */
48
48
  function newestAttemptEvent(eventsByLoop, role, attempt) {
49
49
  return (eventsByLoop.get(role) ?? [])
50
50
  .filter((e) => e.ts >= attempt.pin.ts && e.ts <= attempt.outcome.ts)
51
51
  .at(-1);
52
52
  }
53
- /** The time a `tick_end` attests: its own `durationMs` when it carries one (every event
53
+ /** The pooled cluster key and truncated example a failure-message source's span prices under:
54
+ * the shared placeholder when the source carries no message, the pooled render
55
+ * (poolTimeoutKey(normalizeClusterKey)) of the message's cluster — so a review timeout pools
56
+ * into the same `timed out after <dur>` cause the error-cluster section counts — and the
57
+ * truncated example the loss-rank cell shows. One copy of the placeholder/pool/truncate chain
58
+ * so the error-cluster and review-failure causes cannot drift on it (BUGS.md 2026-10-10). */
59
+ function pooledClusterCause(source) {
60
+ const text = errorTextOrPlaceholder(source);
61
+ return { key: poolTimeoutKey(normalizeClusterKey(text)), example: truncateExample(text) };
62
+ }
63
+ /** The time a `run_end` attests: its own `durationMs` when it carries one (every event
54
64
  * written since 2026-09-29 does), else the start→end pairing over `starts` — the same
55
65
  * fallback history-data.ts renders, so old events read their span the one way it can still be
56
66
  * known. 0 when neither source has the start (rotation cut it); the fold prices no time it
@@ -61,43 +71,46 @@ function tickDurationMs(ev, starts) {
61
71
  return own;
62
72
  return tickSpanMs(ev, starts) ?? 0;
63
73
  }
64
- /** Add one tick to a {ticks, ms, costUsd} accumulator — the single home of the fold shared
74
+ /** Add one tick to a {runs, ms, costUsd} accumulator — the single home of the fold shared
65
75
  * by the time-and-spend table's outcome cells (SpendCell) and the loss-cause drafts
66
76
  * (LossDraft), so the two tallies cannot drift apart in what they count or price. The
67
77
  * caller passes one event's already-computed durationMs and usage, so folding an event
68
78
  * into both a cell and a draft parses the usage and pairs the span exactly once. */
69
79
  function tally(target, durationMs, usage) {
70
- target.ticks++;
80
+ target.runs++;
71
81
  target.ms += durationMs;
72
82
  target.costUsd += usage.costUsd;
73
83
  }
74
- /** Fold the window's tick_ends into the time-and-spend table and the loss ranking. Pairing
84
+ /** Fold the window's run_ends into the time-and-spend table and the loss ranking. Pairing
75
85
  * runs over BOTH windows' events (the caller passes the whole read), so a tick that opened
76
- * in the preceding day and ended in the current one still gets its span. The landing-outcome
77
- * join runs over that same whole read too (BUGS.md 2026-09-30): a queued tick_end resolves
78
- * through its own land_queued pin to the later landed/land_failed outcome for the same
79
- * loop+sha — history-data.ts's exact join — so an authoring tick whose landing was rejected
80
- * prices into the error-class cell its contract names instead of the landed cell its
81
- * transient queued result suggested. A landing still in the pipeline, or an outcome event
82
- * matching no pin, keeps the queued→landed reading: the conservative fallback. */
86
+ * in the preceding day and ended in the current one still gets its span. The merge-outcome
87
+ * join runs over that same whole read too (BUGS.md 2026-09-30): a queued run_end resolves
88
+ * through its own merge_queued pin to the later merge_done/merge_failed outcome for the same
89
+ * loop+sha — history-data.ts's exact join — so an authoring tick whose merge was rejected
90
+ * prices into the error-class cell its contract names instead of the merged cell its
91
+ * transient queued result suggested. A leftover-recovery run resolves to the failed attempt it
92
+ * re-queued (the history row's rule), so its near-zero span counts under that failure — a
93
+ * review timeout's cluster, say — rather than adding a run to the change's final
94
+ * review-rejected cause once per re-queue. A merge still in the pipeline, or an outcome event
95
+ * matching no pin, keeps the queued→merged reading: the conservative fallback. */
83
96
  export function timeAndSpend(tickEvents, allEvents) {
84
97
  const starts = tickStartMap(allEvents);
85
- const emptyCell = () => ({ ticks: 0, ms: 0, costUsd: 0 });
98
+ const emptyCell = () => ({ runs: 0, ms: 0, costUsd: 0 });
86
99
  const byRole = new Map();
87
100
  const losses = new Map();
88
- // The landing evidence rides history-data.ts's bucketLandingEvents — the same buckets
101
+ // The merge evidence rides history-data.ts's bucketMergeEvents — the same buckets
89
102
  // history's rows read — so the digest's join cannot drift from the rows' join.
90
- const { landQueuedByLoop, outcomeByLoop } = bucketLandingEvents(allEvents);
91
- const rejectedByLoop = groupBy(allEvents.filter((e) => e.type === "review_rejected"), (e) => e.loop);
92
- // A review_error landing kept its pin but left no message on its own land_failed; the
103
+ const { mergeQueuedByLoop, outcomeByLoop } = bucketMergeEvents(allEvents);
104
+ const rejectedByLoop = groupBy(allEvents.filter((e) => e.type === "review_rejected"), (e) => e.source);
105
+ // A review_error merge kept its pin but left no message on its own merge_failed; the
93
106
  // matching review_failed event for the same loop+sha carries it, so the span prices under
94
107
  // the same cluster key the digest's review-failure section already counts.
95
- const reviewFailedByLoop = groupBy(allEvents.filter((e) => e.type === "review_failed"), (e) => e.loop);
108
+ const reviewFailedByLoop = groupBy(allEvents.filter((e) => e.type === "review_failed"), (e) => e.source);
96
109
  const deadlineKilledByLoop = groupBy(allEvents.filter((e) => e.type === "build_check" &&
97
110
  e.status === "failed" &&
98
- (e.scope === "landing" || e.scope === "batch") &&
99
- typeof e.timeoutMs === "number"), (e) => e.loop);
100
- // Queued tick_ends resolve newest-first so each claims the newest pin at or before its end
111
+ (e.scope === "merge" || e.scope === "batch") &&
112
+ typeof e.timeoutMs === "number"), (e) => e.source);
113
+ // Queued run_ends resolve newest-first so each claims the newest pin at or before its end
101
114
  // that no newer tick has claimed — resolveQueuedResult's exact claim rule, shared with the
102
115
  // history rows. tickEvents is oldest-first, hence the backward walk.
103
116
  const claimTop = new Map();
@@ -106,7 +119,7 @@ export function timeAndSpend(tickEvents, allEvents) {
106
119
  const ev = tickEvents[i];
107
120
  if (ev.result !== "queued")
108
121
  continue;
109
- const joined = resolveQueuedResult(ev, landQueuedByLoop, outcomeByLoop, claimTop, new Map(), null);
122
+ const joined = resolveQueuedResult(ev, mergeQueuedByLoop, outcomeByLoop, claimTop, new Map(), null);
110
123
  if (joined !== null)
111
124
  resolved.set(ev, joined);
112
125
  }
@@ -127,37 +140,46 @@ export function timeAndSpend(tickEvents, allEvents) {
127
140
  const usage = eventUsage(ev);
128
141
  const durationMs = tickDurationMs(ev, starts);
129
142
  const row = getOrCreate(byRole, role, () => ({
130
- landed: emptyCell(),
143
+ merged: emptyCell(),
131
144
  no_change: emptyCell(),
132
145
  error: emptyCell(),
133
146
  }));
134
147
  tally(row[cls], durationMs, usage);
135
148
  // Loss causes: a clustered failure's cluster owns its time, a no_change's role does, and
136
- // a landing-gate rejection prices its authoring span under a per-role cause of its own —
137
- // unrepresentable as an error cluster, since the land_failed event carries no message;
138
- // the example is the matching review_rejected's first reason, matched by landing attempt.
149
+ // a merge-gate rejection prices its authoring span under a per-role cause of its own —
150
+ // unrepresentable as an error cluster, since the merge_failed event carries no message;
151
+ // the example is the matching review_rejected's first reason, matched by merge attempt.
139
152
  let key = null;
140
153
  let kind = "error-cluster";
141
154
  let example = "";
142
- if (cls === "no_change") {
155
+ if (ev.result === "refused") {
156
+ // A refusal is a deliberate objection that blocks its entry, not quiet idleness — the
157
+ // reason live in the event + lastSummary (src/verdict/refusal.ts). Rank it as its own
158
+ // per-role cause so the digest surfaces the objection instead of pricing the tick
159
+ // exactly like a silently idle one; the branch sits BEFORE the no_change one because a
160
+ // refused tick is also no_change-class in the outcome table. The example rides the
161
+ // refusal's summary the way a review-rejected cause rides its reason.
162
+ key = `refused\u0000${role}`;
163
+ kind = "refused";
164
+ example = typeof ev.summary === "string" ? truncateExample(ev.summary) : "";
165
+ }
166
+ else if (cls === "no_change") {
143
167
  key = `no_change\u0000${role}`;
144
168
  kind = "no_change";
145
169
  }
146
170
  else if (CLUSTERED_RESULTS.has(String(ev.result))) {
147
- // A clustered result with no error text still owns its time: an abort's tick_end never
171
+ // A clustered result with no error text still owns its time: an abort's run_end never
148
172
  // carries one (planTickStart clears s.lastError, tick-finalize logs error: undefined),
149
173
  // and a malformed error tick can lose it too, so both take the same placeholder cause the
150
174
  // error-clusters section already itemizes rather than dropping the span itemized nowhere.
151
- const text = errorTextOrPlaceholder(ev.error);
152
- key = poolTimeoutKey(normalizeClusterKey(text));
153
- example = truncateExample(text);
175
+ ({ key, example } = pooledClusterCause(ev.error));
154
176
  }
155
177
  else if (ev.result === "queued" && resolvedOutcome?.result === "rejected") {
156
178
  key = `review-rejected\u0000${role}`;
157
179
  kind = "review-rejected";
158
- // Match the landing ATTEMPT, not the pin's sha: every vet rebases its pin onto main
180
+ // Match the merge ATTEMPT, not the pin's sha: every verification rebases its pin onto main
159
181
  // before the gate (syncPinToMain), so `review_rejected.head` is the rebased commit while
160
- // `land_queued` keeps the authored one — exact-sha equality names no reason for a rebased
182
+ // `merge_queued` keeps the authored one — exact-sha equality names no reason for a rebased
161
183
  // rejection (BUGS.md 2026-10-10). The rejection for this attempt is the one inside the
162
184
  // claimed pin and its outcome, the window no other attempt can occupy.
163
185
  const rej = newestAttemptEvent(rejectedByLoop, role, resolvedOutcome);
@@ -170,34 +192,32 @@ export function timeAndSpend(tickEvents, allEvents) {
170
192
  // message the review-failure section already clusters. Price the authoring span under
171
193
  // that cluster's key — a review timeout pools into the shared `timed out after <dur>`
172
194
  // cause via poolTimeoutKey — and fall back to the shared placeholder when no event is
173
- // visible, so the span is itemized rather than hidden. The match keys on the landing
174
- // ATTEMPT, not the pin's sha: every vet rebases its pin onto main before the gate
175
- // (syncPinToMain), so `review_failed.head` is the rebased commit while `land_queued`
176
- // keeps the authored one — exact-sha equality missed every rebased vet (BUGS.md
195
+ // visible, so the span is itemized rather than hidden. The match keys on the merge
196
+ // ATTEMPT, not the pin's sha: every verification rebases its pin onto main before the gate
197
+ // (syncPinToMain), so `review_failed.head` is the rebased commit while `merge_queued`
198
+ // keeps the authored one — exact-sha equality missed every rebased verification (BUGS.md
177
199
  // 2026-10-10). The review failure for this attempt is the one that falls inside the
178
200
  // claimed pin and its outcome, the window no other attempt can occupy.
179
201
  const failed = newestAttemptEvent(reviewFailedByLoop, role, resolvedOutcome);
180
- const text = errorTextOrPlaceholder(failed?.message);
181
- key = poolTimeoutKey(normalizeClusterKey(text));
182
- example = truncateExample(text);
202
+ ({ key, example } = pooledClusterCause(failed?.message));
183
203
  }
184
204
  else if (ev.result === "queued" &&
185
205
  (resolvedOutcome?.result === "merge_conflict" || resolvedOutcome?.result === "merge_blocked")) {
186
- // The landing failed to merge (a conflict, or a blocked fast-forward/rebase): this
187
- // authoring span burned. The `land_failed` event carries no message — writeLandingOutcome
206
+ // The merge failed to merge (a conflict, or a blocked fast-forward/rebase): this
207
+ // authoring span burned. The `merge_failed` event carries no message — writeMergeOutcome
188
208
  // logs only result/commit/durationMs/usage — so the cause is named by its own result and
189
- // takes the shared no-text placeholder, the same fallback a message-less aborted tick_end
209
+ // takes the shared no-text placeholder, the same fallback a message-less aborted run_end
190
210
  // uses, rather than dropping a span the error-class cell already priced (BUGS.md
191
211
  // 2026-10-10).
192
212
  key = `${resolvedOutcome.result}\u0000${role}`;
193
213
  kind = resolvedOutcome.result === "merge_conflict" ? "merge-conflict" : "merge-blocked";
194
214
  example = errorTextOrPlaceholder(undefined);
195
- // A merge-scope check the harness killed at its own deadline is the fleet's load, not a
215
+ // A merge-scope check the harness killed at its own deadline is the team's load, not a
196
216
  // verdict on the change: split it into its own cause so the ranking names it.
197
217
  const killed = newestAttemptEvent(deadlineKilledByLoop, role, resolvedOutcome);
198
218
  if (kind === "merge-blocked" && killed) {
199
219
  key = `merge_blocked_deadline\u0000${role}`;
200
- example = `${String(killed.scope)} build check killed at its ${Number(killed.timeoutMs) / 1000}s deadline (the harness's own timeout, not a test failure)`;
220
+ example = `${String(killed.scope)} build check killed at its ${Number(killed.timeoutMs) / 1000}s deadline (tumwater's own timeout, not a test failure)`;
201
221
  }
202
222
  }
203
223
  if (key === null)
@@ -209,7 +229,7 @@ export function timeAndSpend(tickEvents, allEvents) {
209
229
  roles: new Set(),
210
230
  lastSeen: ev.ts,
211
231
  example,
212
- ticks: 0,
232
+ runs: 0,
213
233
  ms: 0,
214
234
  costUsd: 0,
215
235
  };
@@ -230,11 +250,11 @@ export function timeAndSpend(tickEvents, allEvents) {
230
250
  kind: d.kind,
231
251
  roles: sortedRoles(d.roles),
232
252
  example: d.example,
233
- ticks: d.ticks,
253
+ runs: d.runs,
234
254
  ms: d.ms,
235
255
  costUsd: d.costUsd,
236
256
  }))
237
- .sort((a, b) => b.ms - a.ms || b.ticks - a.ticks || a.example.localeCompare(b.example));
257
+ .sort((a, b) => b.ms - a.ms || b.runs - a.runs || a.example.localeCompare(b.example));
238
258
  const lossCauses = rankedLosses.slice(0, LOSS_TOP);
239
259
  return { timeSpend, lossCauses, lossCausesHidden: Math.max(0, rankedLosses.length - LOSS_TOP) };
240
260
  }
@@ -2,17 +2,29 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { errCode } from "../errno.js";
4
4
  /** The directory-of-timestamped-files queue convention shared by the director's prompt inbox
5
- * (inbox.ts) and the durable land queue (landing-queue.ts): one file per entry, ordered by
5
+ * (inbox.ts) and the durable merge queue (merge-queue.ts): one file per entry, ordered by
6
6
  * filename across processes and listed by a directory read. Split out because both queues
7
7
  * would otherwise carry their own copy of the listing, naming, and ENOENT-tolerant removal
8
8
  * rules — exactly the parts that must not drift between the two. */
9
9
  /** Every entry file under `dir` whose name ends in `ext`, oldest first by filename sort. A
10
10
  * missing directory reads as an empty queue, like every reader in both queues. */
11
11
  export function listQueueFiles(dir, ext) {
12
- if (!fs.existsSync(dir))
12
+ let entries;
13
+ try {
14
+ entries = fs.readdirSync(dir);
15
+ }
16
+ catch (err) {
17
+ // The "no queue here" cases — a directory removed between the caller's existence check
18
+ // and this read (ENOENT), or a regular file squatting at the path (ENOTDIR) — read the
19
+ // same empty list a missing directory does, per the contract. Any other error (EACCES:
20
+ // a real but unreadable directory) is a genuine failure the caller must see, never a
21
+ // silently empty queue that would hide queued work.
22
+ const code = errCode(err);
23
+ if (code !== "ENOENT" && code !== "ENOTDIR")
24
+ throw err;
13
25
  return [];
14
- return fs
15
- .readdirSync(dir)
26
+ }
27
+ return entries
16
28
  .filter((f) => f.endsWith(ext))
17
29
  .sort()
18
30
  .map((f) => path.join(dir, f));
@@ -27,9 +27,9 @@ export function statOrNull(file) {
27
27
  * plus matching size and, when the caller tracks it, mtime. The one home of the freshness
28
28
  * check shared by files/stat-cache.ts's cachedByStat, events/events.ts's appended-shape memo
29
29
  * (`mtimeMs` omitted — it decides "our own append left this log clean" from identity and size
30
- * alone), report/report-data.ts's fold cache, gates/maintenance-quota.ts's window counter,
30
+ * alone), report/report-fold.ts's fold cache, gates/maintenance-quota.ts's window counter,
31
31
  * and failure/failure-data.ts's digest cache. Callers whose cached field is a byte offset
32
- * rather than a size (report-data, maintenance-quota) pass that offset as `size`, since a
32
+ * rather than a size (report-fold, maintenance-quota) pass that offset as `size`, since a
33
33
  * fully-folded log's offset equals its length. */
34
34
  export function sameStatIdentity(st, prev) {
35
35
  if (st.dev !== prev.dev || st.ino !== prev.ino || st.size !== prev.size)
@@ -130,6 +130,18 @@ export function unlinkAllMissingTolerant(files) {
130
130
  /** The PATH a binary lookup searches by default: the process PATH, or the empty string when
131
131
  * it is unset (findOnPath then finds no directories). The one home of the default so every
132
132
  * caller that exposes a `pathEnv` override falls back the same way. */
133
+ /** Expand a `~` at the start of a directory against the home dir, for an env var that points
134
+ * at a subdirectory of home — e.g. `$PI_CODING_AGENT_DIR` (pi-models.ts's piAgentDir) and
135
+ * `$CLAUDE_CONFIG_DIR` (claude-backend.ts's claudeConfigDir), which share this exact rule. The
136
+ * empty case is the caller's job (each returns its own default subpath); this helper only maps
137
+ * the bare `~` → home and `~/x` → home/x, otherwise the literal dir. */
138
+ export function expandHomeTilde(dir, homeDir) {
139
+ if (dir === "~")
140
+ return homeDir;
141
+ if (dir.startsWith("~/"))
142
+ return path.join(homeDir, dir.slice(2));
143
+ return dir;
144
+ }
133
145
  export function envPath() {
134
146
  return process.env.PATH ?? "";
135
147
  }
@@ -247,7 +259,7 @@ export function subdirectoryNames(dir, keep) {
247
259
  * name carries the pid because several processes can write at once (CLI, TUI, GUI, a loop's
248
260
  * re-queue); on failure the tmp is removed and the error rethrown, leaving the target untouched.
249
261
  * The harness's JSON writer (writeJsonAtomic in src/files/json-files.ts) and the config write-back
250
- * (writeBytesAtomic's caller, landing-git.ts's restoreConfigBytes) write through this, so the
262
+ * (writeBytesAtomic's caller, merge-git.ts's restoreConfigBytes) write through this, so the
251
263
  * pid-tmp and no-torn-file contract lives in exactly one place. */
252
264
  function writeAtomic(file, data) {
253
265
  ensureParentDir(file);
@@ -266,8 +278,8 @@ export function writeTextAtomic(file, text) {
266
278
  writeAtomic(file, text);
267
279
  }
268
280
  /** Write raw `data` bytes to `file` — the byte entry point to writeAtomic's tmp+rename, for
269
- * writers that must preserve the source bytes exactly (landing-git.ts's restoreConfigBytes
270
- * writes back the live tumwater.json bytes a landing's merge deleted). */
281
+ * writers that must preserve the source bytes exactly (merge-git.ts's restoreConfigBytes
282
+ * writes back the live tumwater.json bytes the merge deleted). */
271
283
  export function writeBytesAtomic(file, data) {
272
284
  writeAtomic(file, data);
273
285
  }
@@ -302,7 +314,7 @@ export function removeTree(dir) {
302
314
  * A subdirectory that cannot be listed — without read permission, or removed by a concurrent
303
315
  * pass between the parent's readdir and this one — is skipped, the same vanish-tolerant policy
304
316
  * the per-file stat/delete below applies. This cleanup runs inside the orchestrator's poll loop
305
- * (RetentionPruner.poll), so a readdir throw escaping here would end the whole fleet; one
317
+ * (RetentionPruner.poll), so a readdir throw escaping here would end the whole team; one
306
318
  * unreadable session directory must cost at most its own contents' retention pass.
307
319
  *
308
320
  * After a subdirectory is walked, an empty one whose own mtime is past the cutoff is removed:
@@ -16,7 +16,7 @@ export function isJsonObject(value) {
16
16
  * when it is an array and [] otherwise — the one home of the "an event/record field is the
17
17
  * list or it is nothing" read every formatter applies (event-format.ts's review_rejected
18
18
  * reasons, dep_install packages, config_changed keys; src/failure/failure-state-change.ts's
19
- * config_changed keys; time-spend.ts's review-rejection reasons; fleet-state.ts's paused-roles
19
+ * config_changed keys; time-spend.ts's review-rejection reasons; team-state.ts's paused-roles
20
20
  * list). A torn or hand-edited line
21
21
  * never throws and never renders a placeholder: the field reads as absent, and the caller's
22
22
  * empty-list branch fires. Foreign entries inside an array are dropped for the same reason —
@@ -38,7 +38,7 @@ export function stringList(value) {
38
38
  * autoRestartRecord (lastAt → null); build/build-info.ts's readBuildInfo (builtAt → 0);
39
39
  * failure/failure-state-change.ts's model_fallback_ended (durationMs → 0) and build_stale
40
40
  * renderer (aheadCommits → 0); gates/maintenance-quota.ts's foldLine and seed (ev.ts → null);
41
- * fleet/orchestrator-info.ts's isFiniteNumber (v → undefined); fleet/fleet-state.ts's
41
+ * team/orchestrator-info.ts's isFiniteNumber (v → undefined); team/team-state.ts's
42
42
  * standingMarker, pausedRoles, and standingRolesUntil (until → undefined);
43
43
  * operator/operator-requests.ts's consumeWakeRequest (notBeforeMs → undefined);
44
44
  * git/slots-state.ts's readSlotRecord (pinnedAt, lastReleasedAt → null); git/worktree-use.ts's
@@ -60,7 +60,7 @@ export function finiteNumber(value, fallback) {
60
60
  * previous context, output and its peak floor → 0), time-spend.ts's tickDurationMs (an event's
61
61
  * durationMs → null, the "no own duration" marker), pi-extension/context-budget.ts's
62
62
  * contextNote (usedTokens, usedWindow → null), pi-extension/context-shake.ts's percentOf
63
- * (tokens, contextWindow → null), landing/landing-queue.ts's isLandingEntry (tick, enqueuedAt,
63
+ * (tokens, contextWindow → null), merge/merge-queue.ts's isMergeEntry (tick, enqueuedAt,
64
64
  * revisionRound → null), git/slots-state.ts's isSlotLease (since, pid → null); and
65
65
  * agent/claude/claude-stream.ts's canonicalUsage (each wire usage
66
66
  * number → 0) and its result/retry/rate-limit reads (api_error_status, attempt,
@@ -92,3 +92,18 @@ export function parseJsonObject(text) {
92
92
  export function objectOrEmpty(value) {
93
93
  return isJsonObject(value) ? value : {};
94
94
  }
95
+ /** Assemble a stream translator's canonical usage block from the four already-validated non-
96
+ * negative counts and the USD cost — the one home of the shape both the claude and opencode
97
+ * translators (agent/claude/claude-stream.ts, agent/opencode/opencode-stream.ts) build, so the
98
+ * `totalTokens` sum and the `cost.total` envelope cannot drift between backends. The wire-
99
+ * specific field-name reads stay in each translator; this owns only the identical assembly. */
100
+ export function canonicalUsageBlock(input, cacheRead, cacheWrite, output, costTotal) {
101
+ return {
102
+ input,
103
+ cacheRead,
104
+ cacheWrite,
105
+ output,
106
+ totalTokens: input + cacheRead + cacheWrite + output,
107
+ cost: { total: costTotal },
108
+ };
109
+ }
@@ -17,7 +17,7 @@ import { piLogPath } from "../paths.js";
17
17
  * re-read next poll once its writer has written the newline instead of being parsed torn
18
18
  * or lost. The tail primitives here build on it (withTail, followFile); outside this
19
19
  * module it is called directly by src/ui/progress-data.ts, src/ui/transcript-tail.ts, and
20
- * the report fold in src/report/report-data.ts. */
20
+ * the report fold in src/report/report-fold.ts. */
21
21
  export function readCompleteLines(file, offset, size) {
22
22
  const len = size - offset;
23
23
  if (len <= 0)
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  /** New-project bootstrap (plans/work-ratio.md, "New-project bootstrap"; part 2/2): a fresh
2
3
  * project seeds `bootstrap.untilPlansDone` in tumwater.json (part 1/2), and this gate holds
3
4
  * every maintenance loop until that many plans have moved to PLANS.md's `## Done`. Only plan,
@@ -13,7 +14,7 @@
13
14
  * may let a later poll retry. Removing `bootstrap` from config (live
14
15
  * reload) also ends it, without a latch. Both the latch write and the completion event are
15
16
  * best-effort, like every sibling gate: the poll runs inside the orchestrator's catch-less
16
- * loop, so an unwritable repo must degrade (the hold still lifts) rather than end the fleet.
17
+ * loop, so an unwritable repo must degrade (the hold still lifts) rather than end the team.
17
18
  *
18
19
  * Shape, like every gate in the family (src/gates/gate-polls.ts): the read-only verdict (and
19
20
  * the pure hold predicate) is separated from the edge-triggered bookkeeping so both the
@@ -24,7 +25,7 @@ import fs from "node:fs";
24
25
  import { donePlans, openBugs } from "../backlog/backlog.js";
25
26
  import { bootstrapLatchPath } from "../paths.js";
26
27
  import { writeJsonAtomic } from "../files/json-files.js";
27
- import { logEventBestEffort } from "../events/events.js";
28
+ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
28
29
  import { errorMessage, stripTerminalControls } from "../text/text.js";
29
30
  import { BUGFIX_ROLE, DIRECTOR_ROLE } from "../roles/roles.js";
30
31
  import { baseRoleOf } from "../roles/loop-ids.js";
@@ -76,10 +77,10 @@ export function pollBootstrapGate(root, state, runners, config, now) {
76
77
  catch (err) {
77
78
  // A terminal boundary: the error can name a path carrying control characters. Sanitize
78
79
  // like cli-output's say()/fail(), so the fallback line cannot drive the operator's shell.
79
- process.stderr.write(`tumwater: harness: could not write bootstrap latch: ${stripTerminalControls(errorMessage(err))}\n`);
80
+ process.stderr.write(`tumwater: could not write bootstrap latch: ${stripTerminalControls(errorMessage(err))}\n`);
80
81
  }
81
82
  logEventBestEffort(root, {
82
- loop: "harness",
83
+ source: TUMWATER_SOURCE,
83
84
  type: "bootstrap_complete",
84
85
  plansDone: status.plansDone,
85
86
  untilPlansDone: status.untilPlansDone,