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,160 +0,0 @@
1
- /**
2
- * claude's `role_notes` surface (plans/agent-backends.md "What the pi extensions did, per
3
- * backend"): a dependency-free stdio MCP server shipped in dist. claude does not load pi's
4
- * bundled extensions, so the claude adapter passes this server via `--mcp-config` whenever the
5
- * run carries a notebook, and the model calls `mcp__tumwater__role_notes`. It is MCP rather
6
- * than "edit this file" because the notebook lives under `.tumwater/`, which the prompts
7
- * forbid touching, and because every future agent CLI speaks MCP.
8
- *
9
- * The write path is shared with the pi extension (src/agent/role-notes-write.ts), so the 4 KB
10
- * cap, the parameter schema, and the atomic write are the same on both backends. Only the
11
- * JSON-RPC framing lives here.
12
- *
13
- * Transport is the MCP stdio convention: one JSON-RPC 2.0 message per line on stdin/stdout,
14
- * and nothing else on stdout (logs would corrupt the stream). The server exits when stdin
15
- * closes — exactly the lifetime claude gives a configured MCP server.
16
- */
17
- import { pathToFileURL } from "node:url";
18
- import { errorMessage } from "../../text/text.js";
19
- import { ROLE_NOTES_DESCRIPTION, ROLE_NOTES_PARAMETERS, saveRoleNote } from "../role-notes-write.js";
20
- const SERVER_NAME = "tumwater";
21
- const SERVER_VERSION = "0.1.0";
22
- const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
23
- const ROLE_NOTES_TOOL = "role_notes";
24
- /** Handle one JSON-RPC line and return the response line to write, or null for a
25
- * notification (no `id`). `notesPath` is the server's own TUMWATER_NOTES_PATH. */
26
- function handleMcpLine(line, notesPath) {
27
- let msg;
28
- try {
29
- msg = JSON.parse(line);
30
- }
31
- catch {
32
- return errorResponse(null, -32700, "Parse error");
33
- }
34
- if (msg === null || typeof msg !== "object" || Array.isArray(msg)) {
35
- return errorResponse(null, -32600, "Invalid Request");
36
- }
37
- const method = msg.method;
38
- // A message with no method is a response to a request the server never sends; the CLIENT
39
- // does not send one, so ignoring it is correct. A notification (no id) is answered with
40
- // nothing in every case.
41
- if (typeof method !== "string")
42
- return null;
43
- const id = msg.id;
44
- const isNotification = id === undefined || id === null;
45
- const result = (value) => ({ jsonrpc: "2.0", id, result: value });
46
- const respond = (value) => (isNotification ? null : result(value));
47
- switch (method) {
48
- case "initialize": {
49
- const params = (msg.params ?? {});
50
- const requested = typeof params.protocolVersion === "string" ? params.protocolVersion : DEFAULT_PROTOCOL_VERSION;
51
- return respond({
52
- protocolVersion: requested,
53
- capabilities: { tools: {} },
54
- serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
55
- });
56
- }
57
- case "notifications/initialized":
58
- case "notifications/cancelled":
59
- return null;
60
- case "ping":
61
- return respond({});
62
- case "tools/list":
63
- return respond({
64
- tools: [
65
- {
66
- name: ROLE_NOTES_TOOL,
67
- description: ROLE_NOTES_DESCRIPTION,
68
- inputSchema: ROLE_NOTES_PARAMETERS,
69
- },
70
- ],
71
- });
72
- case "tools/call": {
73
- const params = (msg.params ?? {});
74
- if (params.name !== ROLE_NOTES_TOOL) {
75
- return respond(toolError(`unknown tool ${JSON.stringify(params.name)}; this server exposes ${ROLE_NOTES_TOOL}`));
76
- }
77
- if (notesPath === undefined) {
78
- return respond(toolError("no notebook path is configured for this run"));
79
- }
80
- const args = (params.arguments ?? {});
81
- try {
82
- return respond({ content: [{ type: "text", text: saveRoleNote(notesPath, args.text) }] });
83
- }
84
- catch (err) {
85
- // Per MCP, a tool's own failure (bad input, a write error) is a successful JSON-RPC
86
- // response carrying isError, so the model sees a tool error and can retry — while a
87
- // JSON-RPC error would read as a transport failure of the whole server.
88
- return respond(toolError(errorMessage(err)));
89
- }
90
- }
91
- default:
92
- return isNotification ? null : errorResponse(id, -32601, `Method not found: ${method}`);
93
- }
94
- }
95
- function toolError(text) {
96
- return { content: [{ type: "text", text }], isError: true };
97
- }
98
- function errorResponse(id, code, message) {
99
- return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
100
- }
101
- /** The largest buffered stdin line before the server treats the stream as malformed. This
102
- * server only ever receives small control frames (the tool it exposes caps its `text` at 4 KB),
103
- * so a message this size is not a legitimate request. Past it the buffered line is discarded
104
- * and resynchronized on the next newline (the same policy the agent runner's
105
- * MAX_NATIVE_LINE_CHARS applies to a byte stream with no newline), so a client that streams
106
- * bytes without ever terminating a line cannot grow the buffer until the process is OOM-killed. */
107
- export const MAX_MCP_LINE_CHARS = 1024 * 1024;
108
- /** Wire the handler to stdin/stdout and exit on EOF. */
109
- function runRoleNotesServer() {
110
- const notesPath = process.env.TUMWATER_NOTES_PATH;
111
- let buffer = "";
112
- // Set when a buffered line passed MAX_MCP_LINE_CHARS without a newline: the rest of that
113
- // line is then skipped through its newline so the stream resynchronizes on the next frame.
114
- let overflow = false;
115
- const flush = (line) => {
116
- if (line.trim() === "")
117
- return;
118
- const response = handleMcpLine(line, notesPath);
119
- if (response !== null)
120
- process.stdout.write(JSON.stringify(response) + "\n");
121
- };
122
- /** Answer an over-cap frame with the JSON-RPC parse error its unparseable line would have
123
- * produced, so the client sees a malformed request rather than a dead server. */
124
- const discardOversized = () => {
125
- buffer = "";
126
- overflow = true;
127
- const message = `Parse error: frame exceeded ${MAX_MCP_LINE_CHARS / (1024 * 1024)} MiB without a newline`;
128
- process.stdout.write(JSON.stringify(errorResponse(null, -32700, message)) + "\n");
129
- };
130
- process.stdin.setEncoding("utf8");
131
- process.stdin.on("data", (chunk) => {
132
- buffer += chunk;
133
- if (overflow) {
134
- const nl = buffer.indexOf("\n");
135
- if (nl < 0) {
136
- buffer = ""; // Still inside the discarded frame; keep the buffer bounded.
137
- return;
138
- }
139
- overflow = false;
140
- buffer = buffer.slice(nl + 1);
141
- }
142
- let nl;
143
- while ((nl = buffer.indexOf("\n")) >= 0) {
144
- flush(buffer.slice(0, nl));
145
- buffer = buffer.slice(nl + 1);
146
- }
147
- if (buffer.length > MAX_MCP_LINE_CHARS)
148
- discardOversized();
149
- });
150
- process.stdin.on("end", () => {
151
- // A discarded over-cap frame has no complete line to flush.
152
- if (!overflow && buffer !== "")
153
- flush(buffer);
154
- });
155
- }
156
- // Only run the transport when executed as the server; importing this module (the tests) must
157
- // not attach stdin handlers. import.meta.url is the compiled file claude spawns by path.
158
- if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
159
- runRoleNotesServer();
160
- }
@@ -1,116 +0,0 @@
1
- /** Deterministic first pass over a conflicted rebase for the backlog markdown files
2
- * (plans/parallel-work-instances.md, "Insert-only backlog conflicts", part 3/7).
3
- *
4
- * The move guidance pastes every finished entry as the first one under `## Done` or
5
- * `## Fixed`, so two landings off the same base insert different entries at the same
6
- * location. Git's diff3 conflict for that shape has an empty base section and both sides
7
- * non-empty; keeping both inserted sides adds no authored bytes, so no model run is
8
- * needed. Anything else — the same entry edited on both sides, a code file, a deleted
9
- * side — goes to the pi resolver untouched. */
10
- import fs from "node:fs";
11
- import path from "node:path";
12
- import { BACKLOG_FILES } from "../backlog/backlog-md.js";
13
- import { writeTextAtomic } from "../files/files.js";
14
- import { git } from "../git/git-run.js";
15
- /** Rewrite every insert-only conflict hunk in `text` and return the result, or null when any
16
- * hunk is not insert-only (or the markers are malformed) — the caller then leaves the file
17
- * for the model resolver. A hunk is insert-only when its base section is empty and both sides
18
- * carry lines; it is replaced by the change's (theirs) lines, a blank separator when neither
19
- * side already supplies one, then main's (ours) lines, so the later landing stays first. */
20
- export function resolveInsertOnlyText(text) {
21
- const lines = text.split("\n");
22
- const hunks = parseHunks(lines);
23
- if (hunks === null)
24
- return null;
25
- if (hunks.length === 0)
26
- return null;
27
- for (const hunk of hunks) {
28
- if (hunk.base.length !== 0 || hunk.ours.length === 0 || hunk.theirs.length === 0)
29
- return null;
30
- }
31
- // Replace from the end so earlier indices stay valid.
32
- for (const hunk of [...hunks].sort((a, b) => b.start - a.start)) {
33
- const separated = hunk.theirs[hunk.theirs.length - 1] === "" || hunk.ours[0] === "";
34
- const replacement = separated
35
- ? [...hunk.theirs, ...hunk.ours]
36
- : [...hunk.theirs, "", ...hunk.ours];
37
- lines.splice(hunk.start, hunk.end - hunk.start + 1, ...replacement);
38
- }
39
- return lines.join("\n");
40
- }
41
- /** Split `lines` into conflict hunks. Returns null when a marker sequence is malformed (an
42
- * unterminated hunk, or a missing `|||||||`/`=======`), so the caller can bail out. */
43
- function parseHunks(lines) {
44
- const hunks = [];
45
- let i = 0;
46
- while (i < lines.length) {
47
- if (!lines[i].startsWith("<<<<<<<")) {
48
- i++;
49
- continue;
50
- }
51
- const start = i;
52
- const ours = [];
53
- const base = [];
54
- const theirs = [];
55
- // Consume each side up to its marker (the `<<<<<<<` line itself is skipped by starting
56
- // at i + 1); a missing marker means the diff3 hunk is malformed and the parse aborts.
57
- const afterOurs = collectUntilMarker(lines, i + 1, "|||||||", ours);
58
- if (afterOurs === null)
59
- return null; // no base marker
60
- const afterBase = collectUntilMarker(lines, afterOurs, "=======", base);
61
- if (afterBase === null)
62
- return null; // no separator
63
- const afterTheirs = collectUntilMarker(lines, afterBase, ">>>>>>>", theirs);
64
- if (afterTheirs === null)
65
- return null; // no end marker
66
- hunks.push({ start, end: afterTheirs - 1, ours, base, theirs });
67
- i = afterTheirs;
68
- }
69
- return hunks;
70
- }
71
- /** Append lines from `start` into `into` until one starts with `marker` (the marker itself is
72
- * consumed, not kept), and return the index just past it. Returns null when no line from
73
- * `start` on carries the marker — the malformed-hunk signal parseHunks turns into a bail-out.
74
- * The one home of the drain-until-marker step shared by parseHunks's three sides, so the
75
- * marker-consumption rule cannot drift between `|||||||`, `=======`, and `>>>>>>>`. */
76
- function collectUntilMarker(lines, start, marker, into) {
77
- let i = start;
78
- while (i < lines.length && !lines[i].startsWith(marker))
79
- into.push(lines[i++]);
80
- return i >= lines.length ? null : i + 1;
81
- }
82
- /** Settle the insert-only conflicts among `files` in `wt` (plans/parallel-work-instances.md,
83
- * part 3/7): every conflicted repo-root PLANS.md, BUGS.md or QUESTIONS.md whose hunks are all
84
- * insert-only is rewritten without markers and staged. Returns the files still conflicted —
85
- * every file that was not fully settled, including non-backlog files and files with any other
86
- * hunk — so the caller runs the model resolver only over those. */
87
- export async function resolveBacklogInsertConflicts(wt, files) {
88
- const remaining = [];
89
- for (const file of files) {
90
- if (path.dirname(file) !== "." || !BACKLOG_FILES.has(file)) {
91
- remaining.push(file);
92
- continue;
93
- }
94
- const p = path.join(wt, file);
95
- let text;
96
- try {
97
- text = fs.readFileSync(p, "utf8");
98
- }
99
- catch {
100
- remaining.push(file); // deleted side or unreadable path — leave it to the resolver
101
- continue;
102
- }
103
- const resolved = resolveInsertOnlyText(text);
104
- if (resolved === null) {
105
- remaining.push(file);
106
- continue;
107
- }
108
- // Atomic (tmp + rename): a landing process killed mid-write (SIGKILL, the timeout
109
- // group-kill) must leave either the conflicted file or the fully resolved one, never a
110
- // truncated backlog — the CLI's backlog writers (backlog-write.ts, question-commands.ts)
111
- // already write these unbounded-history files through this helper.
112
- writeTextAtomic(p, resolved);
113
- await git(wt, "add", "--", file);
114
- }
115
- return remaining;
116
- }
@@ -1,204 +0,0 @@
1
- import { landVetted } from "./landing-batch.js";
2
- import { removeLandingChange, resolveWaitStage, setLandingChangeStatus } from "./landing-slot.js";
3
- import { dropLanding, queuedLandingFiles } from "./landing-queue.js";
4
- import { errorMessage } from "../text/text.js";
5
- import { warnEventBestEffort } from "../events/events.js";
6
- import { saveLoopState } from "../loop/loop-state.js";
7
- import { abortOnShutdown, discardPinnedRefs, guardLandingTask, settleLandingOutcome, } from "./landing-pipeline.js";
8
- import { drainVetting, LANDING_TIER } from "./landing-vetting.js";
9
- /** The tier the merge slot's conflict-resolution runs wait at: ahead of any vet parked for a
10
- * permit, because the merge is the one serial step every queued change waits on. (Nothing that
11
- * holds a permit ever waits on the merge: no vet takes the merge lock, and the resolver runs
12
- * outside it — landing-merge.ts's mergeToMain — so this cannot deadlock.) */
13
- const MERGE_TIER = LANDING_TIER - 1;
14
- /** One poll of the land queue (the scheduler's WHEN: after the gates, never while a restart or
15
- * a fleet hold is pending): start a vet for every queued change the pipeline does not already
16
- * hold (drainVetting, in landing-vetting.ts), and start the merge when the slot is free and
17
- * something is vetted (drainMerge). A queued landing is COMMITTED work awaiting completion, not
18
- * a new tick, so the budget and user-pause gates deliberately do not hold it (pausing it would
19
- * leave main behind while the interlock blocks that role's next tick forever). Starts tasks and
20
- * returns; never awaits them — authors keep ticking behind them, which is the entire point. */
21
- export async function drainLandings(ctx, p) {
22
- await drainVetting(ctx, p);
23
- drainMerge(ctx, p);
24
- }
25
- /** Settle every vetted change `tumwater abort --role` flagged (consumeAbortRequests over
26
- * abortableLandings): it has no task to stop, so it ends here exactly as an aborted landing
27
- * does — "aborted" written back, entry dropped, pin discarded (the deliberate stop throws the
28
- * committed work away). The scheduler runs it every poll right after consuming the requests,
29
- * held or not: a restart hold that skipped it would hand the flagged change, pin intact, to
30
- * the next generation to land.
31
- *
32
- * Unlike a vet or a merge, this settles in the poll body with nothing to guard it: a throw out
33
- * of the outcome write — an unwritable state file — would end the fleet's run. It must also
34
- * never leave the deliberately aborted change queued (an abandoned queue file would be
35
- * re-vetted and landed next poll, against the operator's intent) or keep its pin. So a failed
36
- * write is reported and the abort honored by hand: queue file and marker dropped, pin
37
- * discarded. */
38
- export async function settleAbortedVetted(root, p) {
39
- for (const [role, v] of [...p.vetted]) {
40
- if (!v.userAborted)
41
- continue;
42
- p.vetted.delete(role);
43
- try {
44
- await settleLandingOutcome(root, v.entry, v.author.state, "aborted", Date.now() - v.startedAt, v.usage, v.file);
45
- }
46
- catch (err) {
47
- warnEventBestEffort(root, "harness", `the aborted landing for ${role} could not be settled: ${errorMessage(err)}`);
48
- dropLanding(v.file);
49
- removeLandingChange(root, role);
50
- }
51
- await discardPinnedRefs(root, [role]);
52
- }
53
- }
54
- /** Start the merge when the slot is free and something is vetted: every vetted change, up to
55
- * landBatchMax, in queue order among the vetted — an unvetted entry ahead of them in the queue
56
- * (a slow review) does not hold them back. A vetted change whose queue entry is gone is
57
- * forgotten (nothing in-process drops one; defensive). */
58
- export function drainMerge(ctx, p) {
59
- if (p.merge !== null || p.vetted.size === 0)
60
- return;
61
- const queued = queuedLandingFiles(ctx.root);
62
- const files = new Set(queued.map((q) => q.file));
63
- for (const [role, v] of [...p.vetted]) {
64
- if (files.has(v.file))
65
- continue;
66
- p.vetted.delete(role);
67
- removeLandingChange(ctx.root, role);
68
- }
69
- const picks = queued.flatMap((q) => {
70
- const v = p.vetted.get(q.entry.role);
71
- return v?.file === q.file ? [v] : [];
72
- });
73
- picks.splice(ctx.liveConfig.landBatchMax);
74
- if (picks.length === 0)
75
- return;
76
- for (const v of picks)
77
- p.vetted.delete(v.entry.role);
78
- p.merge = startMerge(ctx, p, picks);
79
- }
80
- /** The merge task — main's one writer in the pipeline: land `picks` through landVetted on
81
- * main's current tip — one change through landApprovedChange, two or more as one stack with one
82
- * scope-`batch` check, one fast-forward, and 3d's largest-passing-prefix bisect — with no second
83
- * review. A vetted change whose main moved since its vet is re-checked on the tree that lands
84
- * (the in-lock re-check, or the stack's check), so an approval that outlived its base never
85
- * lands unverified. Each defined result is written back and its entry dropped; an unattempted
86
- * change (behind a bisect's attributed one) goes back to `vetted` for the next merge. A plumbing
87
- * throw keeps every entry queued, but un-vetted: each is vetted afresh from its pin next poll. A
88
- * user abort of any merged role stops the whole stack, and every change it had not landed ends
89
- * "aborted" with its pin discarded; a shutdown keeps the pins. The task takes no permit of its
90
- * own — its only model run is mergeToMain's conflict resolver, which takes a shared permit at
91
- * MERGE_TIER for the run's length, outside the merge lock. */
92
- function startMerge(ctx, p, picks) {
93
- const { root, mainBranch, signal, roleConfig, semaphore } = ctx;
94
- const merge = {
95
- promise: Promise.resolve(), // Replaced below; the placeholder satisfies the type.
96
- controller: new AbortController(),
97
- roles: picks.map((v) => v.entry.role),
98
- userAborted: false,
99
- };
100
- const detachShutdown = abortOnShutdown(signal, merge.controller);
101
- const wiring = new Map(picks.map((v) => [
102
- v.entry.role,
103
- {
104
- state: v.author.state,
105
- foldUsage: v.foldUsage,
106
- runGatePi: (opts) => v.author.runGatePi(opts),
107
- runPi: async (w, prompt, s, cfg) => {
108
- // A parked resolver names its wait (resolve-wait, then resolving once granted) the way
109
- // a parked check does (withCheckPermit): on 2026-10-12 one waited 2.5 min for a permit
110
- // with the merge queue behind it, reading as "merging". tryAcquire is acquire's own
111
- // fast path for this tier (MERGE_TIER ignores the maintenance reserve).
112
- if (!semaphore.tryAcquire()) {
113
- const hooks = resolveWaitStage(root, v.entry.role);
114
- hooks.waiting();
115
- await semaphore.acquire(MERGE_TIER);
116
- hooks.granted();
117
- }
118
- try {
119
- return await v.author.runLandingPi(w, prompt, s, cfg);
120
- }
121
- finally {
122
- semaphore.release();
123
- }
124
- },
125
- },
126
- ]));
127
- const task = (async () => {
128
- let results;
129
- let threw = false;
130
- // A result reaches settleLandingOutcome the moment landVetted finalizes it, not after the
131
- // whole batch; this set lets the end-of-batch loop skip what is already written back.
132
- const settled = new Set();
133
- const settleOne = async (s, result) => {
134
- const v = picks[s];
135
- settled.add(s);
136
- await settleLandingOutcome(root, v.entry, v.author.state, result, Date.now() - v.startedAt, v.usage, v.file);
137
- };
138
- try {
139
- results = await landVetted({
140
- root,
141
- mainBranch,
142
- config: roleConfig,
143
- signal: () => merge.controller.signal,
144
- onChangeStatus: (role, status) => setLandingChangeStatus(root, role, status),
145
- onResult: settleOne,
146
- runGatePi: (opts) => picks[0].author.runGatePi(opts),
147
- }, picks.map((v) => ({
148
- role: v.entry.role,
149
- sha: v.sha,
150
- tick: v.entry.tick,
151
- summary: v.entry.summary,
152
- body: v.entry.body,
153
- highFriction: v.entry.highFriction,
154
- ...(v.entry.revisionRound !== undefined ? { revisionRound: v.entry.revisionRound } : {}),
155
- ...(v.entry.priorReview !== undefined ? { priorReview: v.entry.priorReview } : {}),
156
- ...(v.verifiedHead !== undefined ? { verifiedHead: v.verifiedHead } : {}),
157
- })), (role) => wiring.get(role));
158
- }
159
- catch (err) {
160
- threw = true;
161
- results = picks.map(() => undefined);
162
- const head = picks[0].author;
163
- head.state.lastError = errorMessage(err);
164
- // The catch exists to keep every entry queued and un-vetted when landVetted throws. A
165
- // throw out of this write — an unwritable state file — must not skip the settle loop
166
- // and the finally that frees the merge slot, or the pipeline wedges with p.merge set
167
- // forever; the write is bookkeeping a later save retries, so warn and keep going.
168
- try {
169
- saveLoopState(root, head.state);
170
- }
171
- catch (writeErr) {
172
- warnEventBestEffort(root, "harness", `the merge error for ${head.state.role} could not be saved: ${errorMessage(writeErr)}`);
173
- }
174
- }
175
- try {
176
- for (let s = 0; s < picks.length; s++) {
177
- const v = picks[s];
178
- const { role } = v.entry;
179
- if (settled.has(s))
180
- continue; // already written back through onResult
181
- const result = results[s] ?? (merge.userAborted ? "aborted" : undefined);
182
- if (result === undefined && !threw) {
183
- p.vetted.set(role, v);
184
- setLandingChangeStatus(root, role, "vetted");
185
- continue;
186
- }
187
- if (result !== undefined) {
188
- await settleOne(s, result);
189
- }
190
- else {
191
- removeLandingChange(root, role);
192
- }
193
- }
194
- }
195
- finally {
196
- detachShutdown();
197
- if (merge.userAborted)
198
- await discardPinnedRefs(root, merge.roles);
199
- p.merge = null;
200
- }
201
- })();
202
- merge.promise = guardLandingTask(root, merge.roles.join(", "), task);
203
- return merge;
204
- }
@@ -1,96 +0,0 @@
1
- import { runOnAbort } from "../process/process.js";
2
- import { warnEventBestEffort } from "../events/events.js";
3
- import { errorMessage } from "../text/text.js";
4
- import { deleteRef } from "../git/git.js";
5
- import { removeLandingChange, writeLandingOutcome } from "./landing-slot.js";
6
- import { landingRefName, rejectedRefName } from "../paths.js";
7
- /** A fresh pipeline: nothing in flight anywhere — no vet running, no approved change waiting,
8
- * no merge holding main. The orchestrator builds one per fleet start (orchestrator.ts). */
9
- export function newLandingPipeline() {
10
- return { vetting: new Map(), vetted: new Map(), merge: null };
11
- }
12
- /** Every landing task in flight — each vet holding its permit, and the merge — for the shutdown
13
- * and restart waits. A parked vet is not one of them: it has started nothing, and a shutdown
14
- * (its controller) or a closed start gate (at its grant) settles it without a run. */
15
- export function landingTasks(p) {
16
- return [...[...p.vetting.values()].filter((v) => !v.parked), ...(p.merge ? [p.merge] : [])];
17
- }
18
- /** Everything `tumwater abort --role` can reach: every task in flight, plus each vetted change
19
- * waiting with no task of its own. A parked vet is out of reach, as a plainly queued entry
20
- * always was: its change has not started landing. */
21
- export function abortableLandings(p) {
22
- return [...landingTasks(p), ...p.vetted.values()];
23
- }
24
- /** Wire harness shutdown to one task's own controller — through the shared register/abort
25
- * race helper, so an already-fired signal aborts at once. Returns the detach function. The
26
- * fleet shutdown signal outlives every landing, so each task that settles while the signal is
27
- * still live must detach in its own finally; without that, the one long-lived signal
28
- * accumulates a listener (and the retained controller behind it) for every landing the fleet
29
- * ever runs, growing without bound until the shutdown it was waiting for. */
30
- export function abortOnShutdown(signal, controller) {
31
- return runOnAbort(signal, () => controller.abort());
32
- }
33
- /** Wrap a landing task's stored promise so it can never reject: the tasks are fire-and-forget
34
- * during normal operation (only the shutdown/restart drain awaits them), and an unexpected
35
- * throw out of their bookkeeping — an unwritable state file, event feed, or marker while
36
- * settling an outcome — would otherwise surface as an unhandled rejection and take the
37
- * orchestrator down. The throw is reported best-effort; the task's own finally has already
38
- * released its slot and controller, and the surviving queue entry and pin recover on the next
39
- * poll. */
40
- export function guardLandingTask(root, role, promise) {
41
- return promise.catch((err) => {
42
- warnEventBestEffort(root, "harness", `landing task for ${role} could not settle: ${errorMessage(err)}`);
43
- });
44
- }
45
- /** Take a permit at `tier` unless `signal` fires first: resolves to its release, or to null
46
- * when the signal won — a grant that arrives after that is handed straight back (a hop, never
47
- * a leak), so a parked vet that a shutdown settles holds nothing. */
48
- export async function acquireUnlessAborted(semaphore, tier, signal) {
49
- if (signal.aborted)
50
- return null;
51
- const granted = semaphore.acquire(tier).then(() => true);
52
- const stopped = new Promise((resolve) => signal.addEventListener("abort", () => resolve(false), { once: true }));
53
- if (await Promise.race([granted, stopped]))
54
- return () => semaphore.release();
55
- void granted.then(() => semaphore.release());
56
- return null;
57
- }
58
- /** The roles the pipeline holds right now — vetting (parked or not), vetted, or merging. */
59
- export function liveRoles(p) {
60
- return new Set([...p.vetting.keys(), ...p.vetted.keys(), ...(p.merge?.roles ?? [])]);
61
- }
62
- /** Discard the pinned landing refs of every role in a deliberately-aborted landing.
63
- * `tumwater abort --role` throws the committed work away, and the pin is what would otherwise
64
- * recover it, so the ref must go; a shutdown abort leaves `userAborted` unset and every ref
65
- * survives for recovery. Shared by every task's settling code so their discard semantics
66
- * cannot drift. A ref that is already gone is not an error. */
67
- export async function discardPinnedRefs(root, roles) {
68
- for (const role of roles) {
69
- try {
70
- await deleteRef(root, landingRefName(role));
71
- }
72
- catch {
73
- /* already gone */
74
- }
75
- }
76
- }
77
- /** Settle a landing queue entry that reached a terminal outcome — the one home of the
78
- * write-plus-drop pairing every settled landing goes through: the result lands in the role's
79
- * state and the landed/land_failed event (writeLandingOutcome), then the change's marker record
80
- * is dropped (removeLandingChange — landing-slot leaves the marker to the caller). Shared by
81
- * settleAbortedVetted, the vet's final-verdict branch, and the merge's per-change write-back, so
82
- * no settled entry can keep a stale marker or lose its outcome write. The pinned-ref discard
83
- * stays with the callers: only a user abort throws work away, and the merge discards its whole
84
- * stack at once rather than change by change. */
85
- export async function settleLandingOutcome(root, entry, state, result, durationMs, usage, file) {
86
- writeLandingOutcome(root, entry, state, result, durationMs, usage, file);
87
- // A revision that lands ends the rejected change's life: drop the rejected ref (plans/revise-
88
- // rejected.md). A rejection repoints or deletes it in landing-core.ts, so this is the only
89
- // ref cleanup a landed revision needs here. Guarded on the entry's revisionRound, not just a
90
- // `changed` result: a fresh change can land while a different rejection's revision is still
91
- // pending, and its landing must leave that pending revision's ref alone.
92
- if (result === "changed" && entry.revisionRound !== undefined) {
93
- await deleteRef(root, rejectedRefName(entry.role));
94
- }
95
- removeLandingChange(root, entry.role);
96
- }
@@ -1,31 +0,0 @@
1
- /** The renderer behind `tumwater queue` — the operator-facing text for the land queue view the
2
- * reader gathers (landing-queue.ts's readLandingQueue, beside it): one line per queued landing
3
- * in execution order, plus the count of queue files the reader skipped. Pure function of the
4
- * collected view and a `now` instant — no I/O, no clock reads — so the age it prints can be
5
- * pinned by a test. */
6
- import { humanSeconds, secondsSince } from "../text/datetime.js";
7
- import { shortSha } from "../text/format.js";
8
- import { plural } from "../text/phrases.js";
9
- /** Render the queue as the text `tumwater queue` prints: a `land queue (<depth>):` header then
10
- * one 1-based line per readable entry, oldest first, carrying the loop, tick number, short
11
- * pinned sha, age, and summary. An empty queue (no entries and nothing skipped) prints
12
- * `land queue empty`; a non-zero skip count appends one `(+N unreadable queue file(s) skipped)`
13
- * line so a torn or foreign file the reader dropped is visible rather than silent. */
14
- export function renderLandingQueue(view, now) {
15
- const depth = view.entries.length + view.unreadable;
16
- const lines = [];
17
- if (depth === 0) {
18
- lines.push("land queue empty");
19
- }
20
- else {
21
- lines.push(`land queue (${depth}):`);
22
- view.entries.forEach((e, i) => {
23
- const age = humanSeconds(secondsSince(e.enqueuedAt, now));
24
- lines.push(` ${i + 1}. ${e.role} tick #${e.tick} · ${shortSha(e.sha)} · ${age} ago · ${e.summary}`);
25
- });
26
- }
27
- if (view.unreadable > 0) {
28
- lines.push(` (+${plural(view.unreadable, "unreadable queue file")} skipped)`);
29
- }
30
- return lines.join("\n");
31
- }