toolroll 0.9.93 → 0.9.95

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 (436) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/activity-line.d.ts +7 -4
  3. package/dist/activity-line.js +9 -7
  4. package/dist/agent-fence.d.ts +10 -0
  5. package/dist/agent-fence.js +6 -3
  6. package/dist/approval-authority.d.ts +7 -1
  7. package/dist/approval-authority.js +11 -2
  8. package/dist/approval-password.d.ts +31 -0
  9. package/dist/approval-password.js +26 -0
  10. package/dist/assignment-status.d.ts +1 -1
  11. package/dist/assignment-ui.js +3 -1
  12. package/dist/assignment.js +5 -0
  13. package/dist/attest.d.ts +3 -0
  14. package/dist/attest.js +10 -0
  15. package/dist/boot-identity.d.ts +2 -1
  16. package/dist/boot-identity.js +6 -4
  17. package/dist/bounded-spawn.d.ts +57 -0
  18. package/dist/bounded-spawn.js +220 -0
  19. package/dist/browser/terminal.js +42 -42
  20. package/dist/browser/workspace.css +1 -1
  21. package/dist/browser/workspace.js +32 -32
  22. package/dist/browser-crew.js +2 -0
  23. package/dist/browser-shell.d.ts +1 -1
  24. package/dist/browser-shell.js +2 -2
  25. package/dist/browser-workspace.d.ts +50 -2
  26. package/dist/browser-workspace.js +3 -3
  27. package/dist/build-review.js +2 -0
  28. package/dist/builder-restart.d.ts +21 -0
  29. package/dist/builder-restart.js +50 -0
  30. package/dist/builder.d.ts +11 -2
  31. package/dist/builder.js +234 -101
  32. package/dist/card-catalog.d.ts +209 -0
  33. package/dist/card-catalog.js +133 -0
  34. package/dist/card-cli.d.ts +43 -0
  35. package/dist/card-cli.js +44 -0
  36. package/dist/card-render.d.ts +26 -0
  37. package/dist/card-render.js +223 -0
  38. package/dist/card-sources.d.ts +68 -0
  39. package/dist/card-sources.js +234 -0
  40. package/dist/chat-acceptance.js +1 -1
  41. package/dist/chat-actions.d.ts +106 -9
  42. package/dist/chat-actions.js +473 -46
  43. package/dist/chat-approval.d.ts +13 -0
  44. package/dist/chat-approval.js +39 -9
  45. package/dist/chat-card-part.d.ts +11 -0
  46. package/dist/chat-card-part.js +18 -0
  47. package/dist/chat-card.d.ts +46 -0
  48. package/dist/chat-card.js +99 -0
  49. package/dist/chat-channel.d.ts +2 -0
  50. package/dist/chat-channel.js +37 -14
  51. package/dist/chat-controls.d.ts +4 -0
  52. package/dist/chat-controls.js +4 -0
  53. package/dist/chat-decide.d.ts +10 -16
  54. package/dist/chat-decide.js +79 -62
  55. package/dist/chat-delivery.js +13 -4
  56. package/dist/chat-flow.d.ts +0 -27
  57. package/dist/chat-flow.js +31 -2
  58. package/dist/chat-quiet.js +4 -4
  59. package/dist/chat-task-actions.d.ts +3 -2
  60. package/dist/chat-task-actions.js +19 -5
  61. package/dist/chat-terms.d.ts +14 -2
  62. package/dist/chat-terms.js +35 -0
  63. package/dist/chat-voice.d.ts +1 -1
  64. package/dist/chat-voice.js +2 -2
  65. package/dist/check-facts.d.ts +3 -1
  66. package/dist/check-facts.js +57 -11
  67. package/dist/check-failure.d.ts +24 -3
  68. package/dist/check-failure.js +89 -13
  69. package/dist/check-levels-ui.d.ts +4 -0
  70. package/dist/check-levels-ui.js +10 -1
  71. package/dist/check-load.d.ts +85 -0
  72. package/dist/check-load.js +147 -0
  73. package/dist/check-only-standing.d.ts +36 -0
  74. package/dist/check-only-standing.js +39 -0
  75. package/dist/check-only.d.ts +15 -0
  76. package/dist/check-only.js +99 -0
  77. package/dist/check-repair.d.ts +47 -0
  78. package/dist/check-repair.js +240 -0
  79. package/dist/check-report.d.ts +2 -0
  80. package/dist/check-report.js +1 -1
  81. package/dist/check-stamp.d.ts +46 -0
  82. package/dist/check-stamp.js +93 -0
  83. package/dist/checkout-cleanup.d.ts +36 -5
  84. package/dist/checkout-cleanup.js +166 -14
  85. package/dist/claim.d.ts +6 -0
  86. package/dist/claim.js +13 -2
  87. package/dist/cli-help.d.ts +4 -1
  88. package/dist/cli-help.js +49 -30
  89. package/dist/cli.js +15 -1
  90. package/dist/code-extract.d.ts +44 -0
  91. package/dist/code-extract.js +88 -0
  92. package/dist/code-map-acts.d.ts +24 -0
  93. package/dist/code-map-acts.js +53 -0
  94. package/dist/code-map-admission.d.ts +52 -0
  95. package/dist/code-map-admission.js +295 -0
  96. package/dist/code-map-build.d.ts +63 -0
  97. package/dist/code-map-build.js +415 -0
  98. package/dist/code-map-cli.d.ts +153 -0
  99. package/dist/code-map-cli.js +103 -0
  100. package/dist/code-map-main.d.ts +1 -0
  101. package/dist/code-map-main.js +48 -0
  102. package/dist/code-map-read.d.ts +106 -0
  103. package/dist/code-map-read.js +171 -0
  104. package/dist/code-map-report.d.ts +37 -0
  105. package/dist/code-map-report.js +95 -0
  106. package/dist/code-map-shape.d.ts +172 -0
  107. package/dist/code-map-shape.js +327 -0
  108. package/dist/code-map-store.d.ts +136 -0
  109. package/dist/code-map-store.js +233 -0
  110. package/dist/coding-provider.d.ts +3 -1
  111. package/dist/coding-provider.js +12 -7
  112. package/dist/coding-update.js +3 -2
  113. package/dist/coding-workspace.d.ts +1 -1
  114. package/dist/coding-workspace.js +8 -8
  115. package/dist/console-chrome.js +2 -2
  116. package/dist/containment.d.ts +5 -0
  117. package/dist/containment.js +14 -1
  118. package/dist/contracts/chat-actions.d.ts +92 -0
  119. package/dist/contracts/chat-actions.js +20 -0
  120. package/dist/contracts/chat-card.d.ts +91 -0
  121. package/dist/contracts/chat-card.js +54 -0
  122. package/dist/contracts/chat-content.d.ts +191 -0
  123. package/dist/contracts/chat-content.js +4 -0
  124. package/dist/contracts/cli.d.ts +52 -0
  125. package/dist/contracts/cli.js +4 -0
  126. package/dist/contracts/console-api.d.ts +12 -3
  127. package/dist/contracts/console-api.js +12 -3
  128. package/dist/contracts/flow-propose.d.ts +8 -1
  129. package/dist/contracts/flow-propose.js +3 -1
  130. package/dist/contracts/flow-send.d.ts +126 -0
  131. package/dist/contracts/flow-send.js +3 -0
  132. package/dist/contracts/gateway-tools.d.ts +17 -0
  133. package/dist/contracts/gateway-tools.js +3 -1
  134. package/dist/contracts/lead-tools.d.ts +234 -0
  135. package/dist/contracts/lead-tools.js +25 -1
  136. package/dist/contracts/project-knowledge.d.ts +38 -5
  137. package/dist/contracts/project-knowledge.js +11 -5
  138. package/dist/contracts/project-memory.d.ts +3 -1
  139. package/dist/contracts/project-memory.js +3 -2
  140. package/dist/contracts/storage.d.ts +14 -0
  141. package/dist/contracts/storage.js +3 -1
  142. package/dist/contracts/store-json.d.ts +18 -0
  143. package/dist/contracts/youiyoux-request.d.ts +1 -1
  144. package/dist/contracts/youiyoux-request.js +1 -1
  145. package/dist/desktop-access.js +6 -2
  146. package/dist/discord-chat.js +14 -0
  147. package/dist/dispatch.d.ts +1 -1
  148. package/dist/dispatch.js +25 -16
  149. package/dist/dispose.d.ts +19 -0
  150. package/dist/dispose.js +55 -9
  151. package/dist/easy-start.js +2 -3
  152. package/dist/eval-oracle.js +4 -1
  153. package/dist/evals.js +2 -1
  154. package/dist/evidence-pair.d.ts +53 -0
  155. package/dist/evidence-pair.js +128 -0
  156. package/dist/exhaustion.d.ts +17 -4
  157. package/dist/exhaustion.js +44 -4
  158. package/dist/export.js +1 -1
  159. package/dist/failing-first.d.ts +31 -0
  160. package/dist/failing-first.js +44 -0
  161. package/dist/fallback-schema.d.ts +8 -0
  162. package/dist/fallback-schema.js +40 -0
  163. package/dist/fallback.d.ts +138 -0
  164. package/dist/fallback.js +605 -0
  165. package/dist/fast-start.d.ts +136 -0
  166. package/dist/fast-start.js +431 -0
  167. package/dist/fast-terms.d.ts +9 -0
  168. package/dist/fast-terms.js +23 -0
  169. package/dist/fenced-check-coverage.d.ts +62 -0
  170. package/dist/fenced-check-coverage.js +54 -0
  171. package/dist/findings.d.ts +224 -0
  172. package/dist/findings.js +449 -0
  173. package/dist/first-run.js +1 -1
  174. package/dist/flaky-ci.d.ts +31 -0
  175. package/dist/flaky-ci.js +149 -0
  176. package/dist/flaky-cli.d.ts +60 -0
  177. package/dist/flaky-cli.js +60 -0
  178. package/dist/flaky.d.ts +75 -9
  179. package/dist/flaky.js +255 -0
  180. package/dist/fleet-schema.d.ts +17 -0
  181. package/dist/fleet-schema.js +135 -0
  182. package/dist/flow-browser.js +2 -2
  183. package/dist/flow-card-task.js +3 -0
  184. package/dist/flow-code.d.ts +2 -2
  185. package/dist/flow-code.js +9 -5
  186. package/dist/flow-result-card.d.ts +22 -0
  187. package/dist/flow-result-card.js +55 -0
  188. package/dist/flow-send.js +12 -9
  189. package/dist/flow-steps.js +1 -1
  190. package/dist/flow-triggers.d.ts +1 -1
  191. package/dist/flow-triggers.js +2 -2
  192. package/dist/flow-unattended.d.ts +26 -0
  193. package/dist/flow-unattended.js +12 -3
  194. package/dist/flows-ui.js +10 -2
  195. package/dist/follow-through-brief.d.ts +12 -0
  196. package/dist/follow-through-brief.js +31 -0
  197. package/dist/follow-through-cli.d.ts +23 -0
  198. package/dist/follow-through-cli.js +153 -0
  199. package/dist/git-incident.d.ts +16 -0
  200. package/dist/git-incident.js +69 -6
  201. package/dist/github-import.d.ts +62 -0
  202. package/dist/github-import.js +146 -0
  203. package/dist/guides.js +15 -10
  204. package/dist/handover-note.d.ts +33 -0
  205. package/dist/handover-note.js +106 -0
  206. package/dist/invoke.d.ts +25 -2
  207. package/dist/invoke.js +157 -58
  208. package/dist/job-object-helper.ps1 +10 -3
  209. package/dist/keymap.d.ts +41 -0
  210. package/dist/keymap.js +51 -0
  211. package/dist/knowledge-cli.js +1 -1
  212. package/dist/knowledge-ui.d.ts +21 -4
  213. package/dist/knowledge-ui.js +75 -7
  214. package/dist/laptop-background.d.ts +16 -0
  215. package/dist/laptop-background.js +108 -2
  216. package/dist/lead-cli.js +26 -24
  217. package/dist/lead-context.js +1 -1
  218. package/dist/lead-contract.d.ts +1 -1
  219. package/dist/lead-contract.js +3 -2
  220. package/dist/lead-doors.d.ts +4 -0
  221. package/dist/lead-doors.js +2 -2
  222. package/dist/lead-progress.js +5 -0
  223. package/dist/lead-status.d.ts +2 -0
  224. package/dist/lead-status.js +6 -2
  225. package/dist/lead-tools.js +162 -34
  226. package/dist/lead.js +2 -2
  227. package/dist/lesson-notes.d.ts +81 -0
  228. package/dist/lesson-notes.js +336 -0
  229. package/dist/limits-ui.js +1 -1
  230. package/dist/local-owner.d.ts +1 -1
  231. package/dist/local-owner.js +16 -0
  232. package/dist/machine-check.d.ts +16 -0
  233. package/dist/machine-check.js +26 -0
  234. package/dist/machine-identity.d.ts +75 -0
  235. package/dist/machine-identity.js +170 -0
  236. package/dist/mcp-core.js +11 -1
  237. package/dist/memory-cli.d.ts +300 -1
  238. package/dist/memory-cli.js +62 -9
  239. package/dist/memory-pass.d.ts +16 -0
  240. package/dist/memory-pass.js +67 -3
  241. package/dist/memory-tidy.d.ts +181 -0
  242. package/dist/memory-tidy.js +523 -0
  243. package/dist/memory-use.d.ts +35 -0
  244. package/dist/memory-use.js +109 -0
  245. package/dist/merge-checklist.d.ts +55 -0
  246. package/dist/merge-checklist.js +182 -0
  247. package/dist/modes.d.ts +6 -6
  248. package/dist/modes.js +3 -1
  249. package/dist/needs-you.d.ts +31 -9
  250. package/dist/needs-you.js +110 -41
  251. package/dist/nightly-github.d.ts +2 -0
  252. package/dist/nightly-github.js +4 -4
  253. package/dist/operate-remote-arguments.js +7 -1
  254. package/dist/operate-remote.js +3 -1
  255. package/dist/operate.d.ts +6 -1
  256. package/dist/operate.js +565 -64
  257. package/dist/phase-routing.js +2 -2
  258. package/dist/plain-schema.d.ts +23 -0
  259. package/dist/plain-schema.js +94 -0
  260. package/dist/plain-words.d.ts +20 -0
  261. package/dist/plain-words.js +66 -0
  262. package/dist/plan-login.d.ts +14 -0
  263. package/dist/plan-login.js +58 -0
  264. package/dist/planner.d.ts +1 -1
  265. package/dist/planner.js +4 -2
  266. package/dist/process-liveness.d.ts +2 -0
  267. package/dist/process-liveness.js +61 -17
  268. package/dist/project-delete-ui.d.ts +19 -2
  269. package/dist/project-delete-ui.js +29 -1
  270. package/dist/project-delete.js +14 -1
  271. package/dist/project-knowledge.d.ts +18 -11
  272. package/dist/project-knowledge.js +39 -9
  273. package/dist/project-learning.d.ts +2 -1
  274. package/dist/project-learning.js +15 -45
  275. package/dist/project-memory.d.ts +5 -0
  276. package/dist/project-memory.js +22 -2
  277. package/dist/project-tools.d.ts +4 -4
  278. package/dist/project-tools.js +23 -7
  279. package/dist/proof.js +30 -5
  280. package/dist/protected-storage-look.d.ts +1 -0
  281. package/dist/protected-storage-look.js +13 -0
  282. package/dist/protected-storage.d.ts +70 -39
  283. package/dist/protected-storage.js +196 -147
  284. package/dist/provider-auth.js +1 -1
  285. package/dist/provider-capture.d.ts +34 -0
  286. package/dist/provider-capture.js +53 -0
  287. package/dist/publish.js +9 -1
  288. package/dist/pull-request-flow.js +5 -0
  289. package/dist/push.d.ts +18 -2
  290. package/dist/push.js +40 -16
  291. package/dist/release-adapters.d.ts +9 -0
  292. package/dist/release-adapters.js +76 -0
  293. package/dist/release-auto.js +13 -13
  294. package/dist/release-card.d.ts +1 -2
  295. package/dist/release-card.js +11 -0
  296. package/dist/release-carry.d.ts +7 -2
  297. package/dist/release-carry.js +49 -10
  298. package/dist/release-cli.d.ts +4 -0
  299. package/dist/release-cli.js +29 -22
  300. package/dist/release-coverage.js +1 -1
  301. package/dist/release-next.d.ts +48 -0
  302. package/dist/release-next.js +161 -0
  303. package/dist/release.d.ts +20 -1
  304. package/dist/release.js +85 -9
  305. package/dist/remote-destination.d.ts +36 -0
  306. package/dist/remote-destination.js +217 -0
  307. package/dist/remote-main.d.ts +48 -0
  308. package/dist/remote-main.js +84 -5
  309. package/dist/repository-context.d.ts +3 -2
  310. package/dist/repository-context.js +112 -70
  311. package/dist/restart-certification.d.ts +1 -0
  312. package/dist/restart-certification.js +8 -6
  313. package/dist/result-acts.d.ts +27 -19
  314. package/dist/result-acts.js +36 -25
  315. package/dist/result-follow-ups.d.ts +2 -2
  316. package/dist/result-follow-ups.js +18 -4
  317. package/dist/result-review.d.ts +1 -1
  318. package/dist/route-change-check.d.ts +4 -0
  319. package/dist/route-change-check.js +13 -0
  320. package/dist/route-change.d.ts +22 -0
  321. package/dist/route-change.js +89 -0
  322. package/dist/route-hooks.js +3 -2
  323. package/dist/route-results.d.ts +6 -1
  324. package/dist/route-results.js +14 -5
  325. package/dist/run-closeout.d.ts +1 -1
  326. package/dist/run-closeout.js +7 -5
  327. package/dist/runner-process.js +3 -2
  328. package/dist/runner.js +2 -1
  329. package/dist/scope.d.ts +28 -4
  330. package/dist/scope.js +92 -28
  331. package/dist/scout.d.ts +1 -1
  332. package/dist/scout.js +2 -1
  333. package/dist/serve.d.ts +1 -1
  334. package/dist/serve.js +26 -18
  335. package/dist/server/chat.js +16 -14
  336. package/dist/server/chrome.d.ts +1 -1
  337. package/dist/server/chrome.js +26 -35
  338. package/dist/server/console.css +73 -0
  339. package/dist/server/guards.js +3 -0
  340. package/dist/server/home.d.ts +6 -0
  341. package/dist/server/home.js +21 -13
  342. package/dist/server/http.js +1 -1
  343. package/dist/server/kit.js +1 -1
  344. package/dist/server/merge-ready-view.d.ts +10 -0
  345. package/dist/server/merge-ready-view.js +30 -0
  346. package/dist/server/pages-assets.d.ts +1 -0
  347. package/dist/server/pages-assets.js +8 -5
  348. package/dist/server/pages.js +103 -82
  349. package/dist/server/render-chat.js +16 -3
  350. package/dist/server/render-code-map.d.ts +30 -0
  351. package/dist/server/render-code-map.js +137 -0
  352. package/dist/server/render-flaky.d.ts +20 -0
  353. package/dist/server/render-flaky.js +59 -0
  354. package/dist/server/render-follow-through.d.ts +11 -0
  355. package/dist/server/render-follow-through.js +68 -0
  356. package/dist/server/render-history.js +1 -1
  357. package/dist/server/render-new-task.d.ts +26 -1
  358. package/dist/server/render-new-task.js +30 -5
  359. package/dist/server/render-pages.d.ts +1 -175
  360. package/dist/server/render-pages.js +38 -313
  361. package/dist/server/render-projects.d.ts +2 -1
  362. package/dist/server/render-projects.js +9 -7
  363. package/dist/server/render-release.d.ts +2 -0
  364. package/dist/server/render-release.js +3 -3
  365. package/dist/server/render-results.d.ts +2 -0
  366. package/dist/server/render-results.js +68 -68
  367. package/dist/server/render-tasks.d.ts +16 -2
  368. package/dist/server/render-tasks.js +177 -141
  369. package/dist/server/retired-routes.d.ts +17 -0
  370. package/dist/server/retired-routes.js +53 -0
  371. package/dist/server/route-table.d.ts +3 -0
  372. package/dist/server/route-table.js +48 -12
  373. package/dist/server/settings.js +166 -34
  374. package/dist/server/task-views.js +66 -26
  375. package/dist/server/tasks.d.ts +2 -6
  376. package/dist/server/tasks.js +250 -321
  377. package/dist/server/welcome.js +1 -1
  378. package/dist/settings-groups-ui.d.ts +1 -0
  379. package/dist/settings-groups-ui.js +4 -2
  380. package/dist/settings-kit.js +3 -2
  381. package/dist/settings-proofs.js +3 -1
  382. package/dist/slack-chat.js +14 -1
  383. package/dist/steer-mailbox.d.ts +8 -1
  384. package/dist/steer-mailbox.js +164 -6
  385. package/dist/stop-proof.js +6 -4
  386. package/dist/storage-sweep-process.js +1 -1
  387. package/dist/storage-sweep.d.ts +5 -3
  388. package/dist/storage-sweep.js +30 -9
  389. package/dist/storage-ui.js +28 -17
  390. package/dist/storage.d.ts +27 -2
  391. package/dist/storage.js +39 -2
  392. package/dist/store.d.ts +89 -7
  393. package/dist/store.js +386 -114
  394. package/dist/surface.js +67 -6
  395. package/dist/sweep-quarantine.d.ts +1 -0
  396. package/dist/sweep-quarantine.js +6 -4
  397. package/dist/tamper-scan.d.ts +4 -0
  398. package/dist/tamper-scan.js +90 -0
  399. package/dist/task-aliases.d.ts +5 -0
  400. package/dist/task-aliases.js +9 -0
  401. package/dist/task-control.d.ts +2 -1
  402. package/dist/task-control.js +4 -1
  403. package/dist/task-outcome-cli.d.ts +2 -0
  404. package/dist/task-outcome-cli.js +17 -17
  405. package/dist/task-sizing.d.ts +5 -0
  406. package/dist/task-sizing.js +2 -0
  407. package/dist/task-source.d.ts +7 -0
  408. package/dist/task-source.js +14 -0
  409. package/dist/task-status.d.ts +9 -0
  410. package/dist/task-status.js +45 -49
  411. package/dist/teardown-git.d.ts +18 -0
  412. package/dist/teardown-git.js +105 -21
  413. package/dist/telegram-lead.js +23 -6
  414. package/dist/telegram-progress.js +5 -3
  415. package/dist/telegram-status.js +6 -4
  416. package/dist/telegram.js +12 -1
  417. package/dist/test-infra.d.ts +8 -0
  418. package/dist/test-infra.js +18 -0
  419. package/dist/tidy.js +1 -1
  420. package/dist/toolroll-update.js +19 -5
  421. package/dist/tui-contract.d.ts +24 -0
  422. package/dist/tui-source.js +67 -8
  423. package/dist/tui.js +51 -51
  424. package/dist/vanished-retry.d.ts +30 -0
  425. package/dist/vanished-retry.js +174 -0
  426. package/dist/work-index.d.ts +2 -0
  427. package/dist/work-index.js +95 -66
  428. package/dist/work-summary.d.ts +1 -1
  429. package/dist/work-summary.js +7 -2
  430. package/dist/workspace-ui.d.ts +7 -3
  431. package/dist/workspace-ui.js +38 -23
  432. package/dist/worktree.d.ts +12 -3
  433. package/dist/worktree.js +91 -14
  434. package/package.json +2 -1
  435. package/dist/server/render-next.d.ts +0 -63
  436. package/dist/server/render-next.js +0 -109
@@ -8,6 +8,7 @@
8
8
  import { type ReleaseAdapters } from "./release.js";
9
9
  import { type GateReleaseWorld } from "./release-gate.js";
10
10
  import { type RescueWorld } from "./deploy-rescue.js";
11
+ import { type RunLockSeams, type SettleWorld } from "./release-next.js";
11
12
  export declare const RELEASE_HELP: string;
12
13
  /** A runner lease id (claim.ts mints randomUUID()). */
13
14
  export declare const LEASE_SHAPED: RegExp;
@@ -23,6 +24,9 @@ export type ReleaseCliDeps = {
23
24
  gateWorld?: GateReleaseWorld;
24
25
  /** What --recover-deploy reads and runs (deploy-rescue.ts); the installation's own by default. */
25
26
  rescueWorld?: RescueWorld;
27
+ /** What a stopped deployment left, settled before the release goes on (release-next.ts). Tests that inject adapters skip it. */
28
+ settle?: SettleWorld;
29
+ runLock?: RunLockSeams;
26
30
  /** The plane's database for the authority commands (default: this installation's). */
27
31
  database?: string;
28
32
  now?: () => Date;
@@ -16,6 +16,7 @@ import { RELEASE_LIMITS, runRelease } from "./release.js";
16
16
  import { processes, releaseAdapters } from "./release-adapters.js";
17
17
  import { journals, releaseGate } from "./release-gate.js";
18
18
  import { rescueDeployment } from "./deploy-rescue.js";
19
+ import { settleDeployment, takeReleaseRun } from "./release-next.js";
19
20
  import { databasePath, elapsedSeconds, latestReleaseStepTask, openStore, releaseProgressFor, revokeRelease } from "./store.js";
20
21
  import { dur } from "./design/kit.js";
21
22
  import { carryRefusalWords, proveCarry, setAskEveryReleaseCommit, setUnsandboxedCarry } from "./release-carry.js";
@@ -34,7 +35,10 @@ export const RELEASE_HELP = `toolroll release <branch> — release Toolroll itse
34
35
  squash-merges, deletes the matching remote branch, verifies the merged tree and tags. Both macOS legs gate npm
35
36
  publication; Homebrew waits for its checks to register and all pass before merging.
36
37
 
37
- Run the same command to resume after a stop. Interrupted deployments recover their saved stage before retrying.
38
+ Run the same command to go on after any stop. It reads where the release got to and does the one thing that's next:
39
+ finishes a deployment forward, puts one back, lifts a pause left before a swap, waits for another run of the release
40
+ (naming its computer and process), or waits for your approval. It says what it did. A branch behind main is brought up
41
+ to date for you; that new commit needs its own approval, CI and check.
38
42
 
39
43
  A branch whose name is one of this command's own words (approve, status, nightly, carry…) is named with --branch <name>
40
44
  or after --: \`toolroll release --branch status\`.
@@ -47,19 +51,6 @@ toolroll release approve [<branch>] [--repo <path>] — approve the release wait
47
51
  toolroll release status [<task>] — the steps of a release (the newest one recorded, or the release task named): each
48
52
  step's state and how long it took, and where it stopped
49
53
 
50
- toolroll release --release-gate <id> [--yes] — lift the update pause a deployment left before its swap
51
-
52
- Shows why it is safe first: the pause is that deployment's, its journal stopped before the swap, and the
53
- database, service definition and running service are all still the previous build's. --yes lifts that one pause.
54
- Any missing or contradicting evidence refuses and leaves the pause in place.
55
-
56
- toolroll release --recover-deploy [--stage <dir>] [--yes] — put back a deployment that stopped half way
57
-
58
- Finds the one unfinished deployment (or the stage named) and shows what its recovery will do; --yes does it. When
59
- the old service isn't proved stopped, it boots the service out and stops what it left running, but only processes
60
- proved, by exact runtime path and arguments, to be that deployment's own; then it starts the previous service
61
- again, waits for it, and lifts the update pause. Anything it can't prove refuses. Safe to run again.
62
-
63
54
  toolroll release carry <release> --to <commit> [--request <id>] — carry a person's release approval to a later commit
64
55
 
65
56
  Only tests, docs and the changelog may change after the approval, within the limits, with an identical production
@@ -100,10 +91,6 @@ Options
100
91
  --new release a new commit on the branch, setting the earlier unfinished release aside
101
92
  --limit <step>=<min> change one step's time limit (${Object.entries(RELEASE_LIMITS).map(([step, minutes]) => `${step} ${minutes}`).join(", ")})
102
93
  --tap <owner/name> the Homebrew tap (default ap9000/homebrew-toolroll)
103
- --release-gate <id> check (and with --yes, lift) one update pause left before a deployment's swap
104
- --yes with --release-gate: lift the pause once it is proved safe; with --recover-deploy: recover
105
- --recover-deploy recover the unfinished deployment (see above)
106
- --stage <dir> with --recover-deploy: the deployment's staging folder, when more than one is unfinished
107
94
  --json answer with one machine envelope
108
95
 
109
96
  A runner's lease is returned with \`toolroll worker release <lease>\`.`;
@@ -270,8 +257,23 @@ export async function runReleaseCommand(args, write, deps = {}) {
270
257
  const adapters = deps.adapters === undefined
271
258
  ? releaseAdapters({ checkout, say, ...(tap === undefined ? {} : { tap }), ...(limits["deploy"] === undefined ? {} : { deployMinutes: limits["deploy"] }) })
272
259
  : { ...deps.adapters, say };
273
- const outcome = await runRelease({ branch, checkout, stateDir, limits, fresh: bare.has("--new"), noCarry: bare.has("--no-carry") }, adapters);
274
- return report(outcome, write, json, lines);
260
+ // One command: wait out another run of this release, settle what a stopped deployment left, then go on from the journal.
261
+ const taken = await takeReleaseRun(stateDir, branch, say, deps.runLock);
262
+ if (!taken.ok)
263
+ return refuse("running", `Another release of ${branch} is still running on ${taken.holder.host} (process ${taken.holder.pid}). Nothing was changed.`, 3);
264
+ try {
265
+ const world = deps.settle ?? (deps.adapters === undefined ? { rescue: installedRescueWorld(checkout), gate: installedGateWorld() } : null);
266
+ if (world !== null) {
267
+ const settled = await settleDeployment(world, say);
268
+ if (!settled.ok)
269
+ return refuse("deployment", settled.message, 3);
270
+ }
271
+ const outcome = await runRelease({ branch, checkout, stateDir, limits, fresh: bare.has("--new"), noCarry: bare.has("--no-carry") }, adapters);
272
+ return report(outcome, write, json, lines);
273
+ }
274
+ finally {
275
+ taken.release();
276
+ }
275
277
  }
276
278
  function report(outcome, write, json, lines) {
277
279
  const release = outcome.journal === null ? null : {
@@ -590,10 +592,15 @@ async function runStatusCommand(task, write, json, deps) {
590
592
  }
591
593
  const word = { running: "running", waiting: "waiting", done: "done", failed: "failed" };
592
594
  write(`Release ${progress.version} (${progress.tag})${progress.pr === null ? "" : ` · pull request #${progress.pr}`} · task ${taskId}`);
593
- for (const one of steps)
594
- write(` ${one.step.padEnd(9)} ${word[one.state].padEnd(8)} ${one.seconds === null ? "–" : dur(one.seconds)}${one.detail === null ? "" : ` ${one.detail}`}`);
595
+ for (const one of steps) {
596
+ // A failure the release has gone on from is history, as on the task page.
597
+ const passed = one.state === "failed" && progress.stopped === null;
598
+ write(` ${one.step.padEnd(9)} ${(passed ? "went on" : word[one.state]).padEnd(8)} ${one.seconds === null ? "–" : dur(one.seconds)}${one.detail === null ? "" : ` ${one.detail}`}`);
599
+ }
595
600
  if (progress.stopped !== null)
596
601
  write(`Stopped at ${progress.stopped.step}: ${progress.stopped.message}`);
602
+ if (progress.building !== null)
603
+ write(`Build #${progress.building.run} is running the check.`);
597
604
  return 0;
598
605
  }
599
606
  finally {
@@ -26,7 +26,7 @@ export const gitIsAncestor = (repo, ancestor, descendant) => {
26
26
  let answer;
27
27
  try {
28
28
  // Every git version through a frozen read view, bounded (eval-harness review 1, finding 7).
29
- const status = viewedGitSync(repo, ["merge-base", "--is-ancestor", ancestor, descendant], { timeoutMs: 5_000 }).status;
29
+ const status = viewedGitSync(repo, ["merge-base", "--is-ancestor", ancestor, descendant], { timeoutMs: 5_000, binding: { repo } }).status;
30
30
  if (status !== 0)
31
31
  throw Object.assign(new Error("git merge-base"), { status });
32
32
  answer = true;
@@ -0,0 +1,48 @@
1
+ import { type RescueWorld } from "./deploy-rescue.js";
2
+ import { type GateReleaseWorld } from "./release-gate.js";
3
+ import { type ReleaseJournal } from "./release.js";
4
+ import type { Store } from "./store.js";
5
+ /** How long a run waits for another before it says so and stops. */
6
+ export declare const RELEASE_WAIT_MS: number;
7
+ export type RunHolder = {
8
+ host: string;
9
+ pid: number;
10
+ };
11
+ export type RunLockSeams = {
12
+ host?: string;
13
+ pid?: number;
14
+ alive?: (pid: number) => boolean;
15
+ sleep?: (ms: number) => Promise<void>;
16
+ now?: () => number;
17
+ waitMs?: number;
18
+ pollMs?: number;
19
+ };
20
+ /** Take this branch's run lock, waiting for a live holder. Answers how to give it back, or who kept it. */
21
+ export declare function takeReleaseRun(stateDir: string, branch: string, say: (line: string) => void, seams?: RunLockSeams): Promise<{
22
+ ok: true;
23
+ release: () => void;
24
+ } | {
25
+ ok: false;
26
+ holder: RunHolder;
27
+ }>;
28
+ export type SettleWorld = {
29
+ rescue: RescueWorld;
30
+ gate: GateReleaseWorld;
31
+ };
32
+ export type DeployAction = "none" | "lift-pause" | "forward" | "roll-back" | "refuse";
33
+ /** The one action a deployment's journal calls for. */
34
+ export declare function deployActionOf(phase: unknown): DeployAction;
35
+ /** Settle what a stopped deployment left, saying what was done. A refusal stops the release before it does anything. */
36
+ export declare function settleDeployment(world: SettleWorld, say: (line: string) => void): Promise<{
37
+ ok: true;
38
+ } | {
39
+ ok: false;
40
+ message: string;
41
+ }>;
42
+ export declare const RELEASE_RESUMED_ACTION = "release resumed";
43
+ /**
44
+ * A person let a held release go on (release-auto.ts allowAutoPublish; the Release sheet's button or `publish-ok`): each
45
+ * sweep, its journal goes on from where it stopped, once per that person's act, under the same run lock. It was merged
46
+ * already, so only the tag, publication and Homebrew are left, and each re-proves itself.
47
+ */
48
+ export declare function resumeAllowedReleases(store: Store, stateDir: string, run: (journal: ReleaseJournal) => Promise<unknown>, now: Date): Promise<number>;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * What `toolroll release <branch>` does before its journal's next step (docs/plans/plain-toolroll.md P2.6): one command,
3
+ * so a person never chooses between --recover-deploy, --release-gate and rerunning.
4
+ *
5
+ * - **Another run**: a run lock per branch, by host and pid. A live one is waited for, named; two coordinators never act
6
+ * on one release at once. A lock whose process is proved gone on this host is taken over.
7
+ * - **A deployment left open**: the journal decides. Before the swap: its pause is lifted, once proved. Past the new
8
+ * service's start: forward, by the release. Between: rolled back through its own recovery. Each of those re-proves
9
+ * itself, so a crash at any phase ends the same on the next run.
10
+ */
11
+ import { closeSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeSync } from "node:fs";
12
+ import { hostname } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+ import { FORWARD_ONLY, rescueDeployment } from "./deploy-rescue.js";
15
+ import { BEFORE_SWAP, releaseGate } from "./release-gate.js";
16
+ import { journalFile, readJournal } from "./release.js";
17
+ import { autoPublishHeld, PUBLISH_ALLOWED_ACTION } from "./release-auto.js";
18
+ import { sameMachineHost } from "./machine-identity.js";
19
+ const lockFile = (stateDir, branch) => journalFile(stateDir, branch).replace(/\.json$/, ".running");
20
+ /** How long a run waits for another before it says so and stops. */
21
+ export const RELEASE_WAIT_MS = 30 * 60_000;
22
+ const processAlive = (pid) => {
23
+ try {
24
+ process.kill(pid, 0);
25
+ return true;
26
+ }
27
+ catch (error) {
28
+ return error.code === "EPERM";
29
+ }
30
+ };
31
+ function readHolder(file) {
32
+ try {
33
+ const read = JSON.parse(readFileSync(file, "utf8"));
34
+ return typeof read.host === "string" && Number.isSafeInteger(read.pid) ? { host: read.host, pid: read.pid } : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ /** Take this branch's run lock, waiting for a live holder. Answers how to give it back, or who kept it. */
41
+ export async function takeReleaseRun(stateDir, branch, say, seams = {}) {
42
+ const file = lockFile(stateDir, branch);
43
+ const host = seams.host ?? hostname(), pid = seams.pid ?? process.pid;
44
+ const alive = seams.alive ?? processAlive, sleep = seams.sleep ?? (ms => new Promise(done => setTimeout(done, ms)));
45
+ const now = seams.now ?? Date.now;
46
+ const deadline = now() + (seams.waitMs ?? RELEASE_WAIT_MS);
47
+ mkdirSync(dirname(file), { recursive: true });
48
+ let said = false;
49
+ for (;;) {
50
+ try {
51
+ const fd = openSync(file, "wx", 0o600);
52
+ try {
53
+ writeSync(fd, JSON.stringify({ host, pid }));
54
+ }
55
+ finally {
56
+ closeSync(fd);
57
+ }
58
+ return { ok: true, release: () => { const mine = readHolder(file); if (mine?.pid === pid && mine.host === host)
59
+ rmSync(file, { force: true }); } };
60
+ }
61
+ catch (error) {
62
+ if (error.code !== "EEXIST")
63
+ throw error;
64
+ }
65
+ const holder = readHolder(file);
66
+ // A lock with no readable holder, or a process proved gone on this computer, holds nothing. Another computer's can't
67
+ // be proved gone from here, so it is waited for.
68
+ if (holder === null || (sameMachineHost(holder.host, host) && !alive(holder.pid))) {
69
+ rmSync(file, { force: true });
70
+ continue;
71
+ }
72
+ if (!said) {
73
+ say(`Waiting: another release of ${branch} is running on ${holder.host} (process ${holder.pid}).`);
74
+ said = true;
75
+ }
76
+ if (now() >= deadline)
77
+ return { ok: false, holder };
78
+ await sleep(seams.pollMs ?? 5_000);
79
+ }
80
+ }
81
+ /** The one action a deployment's journal calls for. */
82
+ export function deployActionOf(phase) {
83
+ if (typeof phase !== "string")
84
+ return "refuse";
85
+ if (phase === "released" || phase === "deployed")
86
+ return "none";
87
+ if (BEFORE_SWAP.includes(phase))
88
+ return "lift-pause";
89
+ if (FORWARD_ONLY.includes(phase))
90
+ return "forward";
91
+ return "roll-back";
92
+ }
93
+ /** Settle what a stopped deployment left, saying what was done. A refusal stops the release before it does anything. */
94
+ export async function settleDeployment(world, say) {
95
+ const open = world.rescue.journals().filter(one => deployActionOf(one.journal?.["phase"]) !== "none");
96
+ if (open.length === 0)
97
+ return { ok: true };
98
+ if (open.length > 1)
99
+ return { ok: false, message: `${open.length} deployments are unfinished: ${open.map(one => one.stage).join(", ")}. Nothing was changed.` };
100
+ const { stage, journal } = open[0];
101
+ const action = deployActionOf(journal?.["phase"]);
102
+ if (action === "refuse")
103
+ return { ok: false, message: `The deployment journal in ${stage} can't be read. Nothing was changed.` };
104
+ if (action === "forward") {
105
+ say("Finishing the deployment that was under way.");
106
+ return { ok: true };
107
+ }
108
+ if (action === "lift-pause") {
109
+ const lifted = releaseGate(String(journal?.["id"] ?? ""), world.gate, true);
110
+ if (!lifted.ok)
111
+ return { ok: false, message: lifted.message };
112
+ say("Lifted the pause an unfinished deployment left before its swap.");
113
+ return { ok: true };
114
+ }
115
+ const back = await rescueDeployment(stage, true, world.rescue);
116
+ if (!back.ok)
117
+ return { ok: false, message: back.message };
118
+ say("Put back the deployment that stopped half way.");
119
+ return { ok: true };
120
+ }
121
+ // ---- a held release resumes itself -----------------------------------------------------------------------------------
122
+ export const RELEASE_RESUMED_ACTION = "release resumed";
123
+ /**
124
+ * A person let a held release go on (release-auto.ts allowAutoPublish; the Release sheet's button or `publish-ok`): each
125
+ * sweep, its journal goes on from where it stopped, once per that person's act, under the same run lock. It was merged
126
+ * already, so only the tag, publication and Homebrew are left, and each re-proves itself.
127
+ */
128
+ export async function resumeAllowedReleases(store, stateDir, run, now) {
129
+ let folder;
130
+ try {
131
+ folder = readdirSync(join(stateDir, "releases")).filter(name => name.endsWith(".json") && !name.includes(".superseded-"));
132
+ }
133
+ catch {
134
+ return 0;
135
+ }
136
+ let resumed = 0;
137
+ for (const name of folder) {
138
+ const journal = readJournal(join(stateDir, "releases", name));
139
+ if (journal === null || !["tag", "publish", "homebrew"].includes(journal.step) || autoPublishHeld(store, journal.task.id) !== null)
140
+ continue;
141
+ const allowed = store.handle.prepare("SELECT id FROM action_ledger WHERE task_id = ? AND action = ? ORDER BY id DESC LIMIT 1").get(journal.task.id, PUBLISH_ALLOWED_ACTION);
142
+ if (allowed === undefined)
143
+ continue;
144
+ // Once per act: a resume that stops again waits for a person, never for the next sweep.
145
+ const done = store.handle.prepare("SELECT 1 AS hit FROM action_ledger WHERE task_id = ? AND action = ? AND outcome = ?").get(journal.task.id, RELEASE_RESUMED_ACTION, String(allowed["id"]));
146
+ if (done !== undefined)
147
+ continue;
148
+ const taken = await takeReleaseRun(stateDir, journal.branch, () => { }, { waitMs: 0 });
149
+ if (!taken.ok)
150
+ continue;
151
+ try {
152
+ store.recordAction({ at: now.toISOString(), actor: "system", repo: store.lookupRef(journal.task.id)?.repo ?? null, taskId: journal.task.id, runId: null, action: RELEASE_RESUMED_ACTION, outcome: String(allowed["id"]), source: "work", detail: journal.tag });
153
+ await run(journal);
154
+ resumed++;
155
+ }
156
+ finally {
157
+ taken.release();
158
+ }
159
+ }
160
+ return resumed;
161
+ }
package/dist/release.d.ts CHANGED
@@ -77,7 +77,9 @@ export declare const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
77
77
  export declare const RELEASE_STEPS: readonly ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
78
78
  export type ReleaseStep = typeof RELEASE_STEPS[number];
79
79
  type WaitingStep = "gate" | "approval" | "check" | "quiet" | "deploy" | "ci" | "publish" | "homebrew";
80
- /** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
80
+ /** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). The
81
+ * check's covers the first attempt and the two same-commit reruns the plane may give it, each of which a busy machine
82
+ * may stretch to two hours (check-load.ts). */
81
83
  export declare const RELEASE_LIMITS: Readonly<Record<WaitingStep, number>>;
82
84
  /** One check attempt; `app` is the GitHub App that made it (github-actions for a workflow), when known; `id` its check
83
85
  * run id, `suite` its check suite (one workflow run: its re-runs stay in it) and `head` the commit GitHub filed it
@@ -332,6 +334,15 @@ export type ReleaseAuthorityAdapter = {
332
334
  why: string;
333
335
  warning: string | null;
334
336
  }>;
337
+ /** Whether a sandboxed Full check's skipped cases are covered (fenced-check-coverage.ts, H3). Run `run`'s stamped level
338
+ * decides: an unsandboxed check answers at once. Absent: a plane that can't say, as for every older one. */
339
+ fencedCoverage?(taskId: string, run: number, candidate: string): Promise<{
340
+ ok: true;
341
+ covered: number;
342
+ } | {
343
+ ok: false;
344
+ why: string;
345
+ }>;
335
346
  /** One-tap release approval (docs/plans/trusted-release-reports.md, slice A): what the plane tells a person and
336
347
  * keeps for the approval sheet. Best effort: a failure here never changes the release. Absent on an older plane. */
337
348
  ready?: ReleaseReadyAdapter;
@@ -536,6 +547,14 @@ export type ReleaseAdapters = {
536
547
  mainHead(repo: string): Promise<string>;
537
548
  /** Whether `sha` already holds `ancestor`, by GitHub's own comparison. */
538
549
  contains(repo: string, sha: string, ancestor: string): Promise<boolean>;
550
+ /** Bring a pull request's branch up to main, only while its head is `expectedHead` (GitHub's own compare-and-swap: a
551
+ * push that got there first refuses). Absent: a behind branch stops, for a person to update. */
552
+ updateBranch?(repo: string, pr: number, expectedHead: string): Promise<{
553
+ ok: true;
554
+ } | {
555
+ ok: false;
556
+ why: string;
557
+ }>;
539
558
  /** Whether GitHub refuses to merge a branch that is behind main ("Require branches to be up to date before
540
559
  * merging"); null when it can't be read. Read only. */
541
560
  upToDateRequired(repo: string): Promise<MainProtection | null>;
package/dist/release.js CHANGED
@@ -93,6 +93,8 @@ export const ADVISORY_PR_CHECKS = {
93
93
  "youiyoux · UI drift": ".github/workflows/youiyoux.yml",
94
94
  "journeys · app (scripted)": ".github/workflows/journeys.yml",
95
95
  "journeys · flows (scripted)": ".github/workflows/journeys.yml",
96
+ "upgrade path · scope": ".github/workflows/upgrade-path.yml",
97
+ "upgrade path": ".github/workflows/upgrade-path.yml",
96
98
  };
97
99
  /** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
98
100
  * the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
@@ -101,8 +103,10 @@ export const ADVISORY_PR_CHECKS = {
101
103
  export const RELEASE_TAG_CHECKS = ["release proof", "publish"];
102
104
  export const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
103
105
  export const RELEASE_STEPS = ["gate", "approval", "check", "complete", "quiet", "deploy", "link", "ci", "merge", "tree", "tag", "publish", "homebrew", "done"];
104
- /** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
105
- export const RELEASE_LIMITS = { gate: 60, approval: 24 * 60, check: 120, quiet: 60, deploy: 30, ci: 60, publish: 45, homebrew: 45 };
106
+ /** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). The
107
+ * check's covers the first attempt and the two same-commit reruns the plane may give it, each of which a busy machine
108
+ * may stretch to two hours (check-load.ts). */
109
+ export const RELEASE_LIMITS = { gate: 60, approval: 24 * 60, check: 360, quiet: 60, deploy: 30, ci: 60, publish: 45, homebrew: 45 };
106
110
  const POLL_MS = 15_000;
107
111
  /**
108
112
  * Failure classes replace a flat flake allowance (docs/plans/eval-harness.md; check-failure.ts says the classes): only a
@@ -129,9 +133,12 @@ class HomebrewFailed extends Error {
129
133
  /** The branch moved before the deploy reservation: carry the approval to the new head, or file it for a person. */
130
134
  class Restart extends Error {
131
135
  head;
132
- constructor(head) {
136
+ updated;
137
+ /** `updated`: Toolroll itself brought the branch up to main; the new commit is a new candidate and nothing carries. */
138
+ constructor(head, updated = false) {
133
139
  super(`restart at ${head}`);
134
140
  this.head = head;
141
+ this.updated = updated;
135
142
  }
136
143
  }
137
144
  /** Every commit that landed after the approval, in words: the summary and the merge message list them all. */
@@ -244,6 +251,8 @@ export async function runRelease(options, adapters) {
244
251
  let step = "preflight";
245
252
  let freshApproval = false;
246
253
  let supersededTask = null;
254
+ /** The commit whose check stopped the release being set aside. The stop tells a person to approve it again with --new. */
255
+ let stoppedCheck = null;
247
256
  const save = () => { journal.updatedAt = adapters.now().toISOString(); writeJournal(file, journal); };
248
257
  /** Tell the progress record about a step (v146). Never throws and never waits: the release goes on whatever it says. */
249
258
  const mark = (at, state, detail = null) => {
@@ -317,6 +326,8 @@ export async function runRelease(options, adapters) {
317
326
  say(`Kept the earlier release journal at ${aside}.`);
318
327
  freshApproval = journal.needsApproval === true;
319
328
  supersededTask = journal.task.id;
329
+ if (journal.stopped?.reason === "check-failed")
330
+ stoppedCheck = journal.candidate.sha;
320
331
  journal = null;
321
332
  }
322
333
  if (journal?.step === "done") {
@@ -331,8 +342,10 @@ export async function runRelease(options, adapters) {
331
342
  journal = await begin(options, adapters, adapters.now());
332
343
  // The earlier attempt needs a person's fresh approval (a CI failure after its deploy): this one files a task of its
333
344
  // own for the same commit, so the old approval can't carry it.
345
+ // A check that stopped this same commit is the same: its task keeps that failure, so reusing it would only stop again
346
+ // (a passing run on it, run 3333, can't erase it). A new task reruns the check and asks a person, never the plane.
334
347
  // Only an id that has never existed, numbered after the superseded one: never a task whose approval still stands.
335
- if (freshApproval) {
348
+ if (freshApproval || journal.candidate.sha === stoppedCheck) {
336
349
  const base = journal.task.id;
337
350
  const suffix = supersededTask === null ? null : supersededTask === base ? 1 : supersededTask.startsWith(`${base}-`) ? Number(supersededTask.slice(base.length + 1)) : null;
338
351
  let picked = null;
@@ -626,11 +639,41 @@ export async function runRelease(options, adapters) {
626
639
  /** identity(), on a plane that keeps release authority; a legacy plane keeps the old rules. */
627
640
  const watch = async (action) => { if (rel !== undefined)
628
641
  await identity(action, true); };
629
- const restart = async (head) => {
642
+ /**
643
+ * Behind main (plain-toolroll.md P2.6): update the branch under this release's lease and the head it was gated on.
644
+ * A concurrent push aborts it. The approval stops first, so nothing can ride it across the update; the updated head
645
+ * is a new candidate with a new approval, fresh CI and a fresh check, and there is no carry.
646
+ */
647
+ const updateFromMain = async () => {
648
+ const update = adapters.github.updateBranch;
649
+ if (update === undefined)
650
+ return null;
651
+ if (rel !== undefined && j.authority) {
652
+ if (epoch === null || !await rel.renew(j.authority.id, owner, epoch))
653
+ throw new Stop("lease", "Another release coordinator took over this release.");
654
+ }
655
+ const pr = await adapters.github.pullRequest(j.github, j.branch);
656
+ if (pr === null || pr.number !== j.pr || pr.state !== "open" || pr.head !== j.candidate.sha)
657
+ throw new Stop("drift", `${j.branch} moved while it was being brought up to main. Nothing was changed; rerun \`${resume}\`.`);
658
+ if (rel !== undefined && j.authority && !await rel.needsApproval(j.authority.id, "the branch was brought up to main, so its new commit needs a fresh approval", epoch)) {
659
+ throw new Stop("lease", "The release's approval couldn't be stopped before the update, so the branch wasn't updated.");
660
+ }
661
+ say(` ${j.branch} is behind main: bringing it up to date`);
662
+ const moved = await update(j.github, j.pr, j.candidate.sha);
663
+ if (!moved.ok)
664
+ throw new Stop("drift", `${j.branch} couldn't be brought up to main: ${moved.why}. Rerun \`${resume}\`.`);
665
+ return waitFor("ci", `${j.branch} to move past ${j.candidate.sha.slice(0, 7)}`, async () => {
666
+ const head = await adapters.repo.remoteHead(j.branch);
667
+ return head !== null && head !== j.candidate.sha ? head : null;
668
+ });
669
+ };
670
+ const restart = async (head, updated = false) => {
630
671
  // The step this commit was at ends here: the release goes on with the new commit (and, filed again, a new task).
631
672
  mark(j.step, "failed", `${j.branch} moved to ${head.slice(0, 7)}`);
632
673
  const tree = await adapters.repo.tree(head);
633
- if (rel !== undefined && j.authority && epoch !== null) {
674
+ if (updated)
675
+ say(` ${j.branch} is at ${head.slice(0, 7)}: a new commit, so it needs its own approval, CI and check`);
676
+ else if (rel !== undefined && j.authority && epoch !== null) {
634
677
  say(` ${j.branch} moved to ${head.slice(0, 7)}: checking whether the approval carries`);
635
678
  const carried = await rel.carry(j.authority.id, head, randomUUID(), epoch);
636
679
  if (carried.ok) {
@@ -808,7 +851,17 @@ export async function runRelease(options, adapters) {
808
851
  }
809
852
  // The tree CI tested (the pull request's head, not GitHub's merge commit) is the tree that merges only when the
810
853
  // branch already contains main's head: refused here, before anything is filed, and again right before merging.
811
- await containsMain(adapters, j, "Nothing was filed or sent for approval");
854
+ try {
855
+ await containsMain(adapters, j, "Nothing was filed or sent for approval");
856
+ }
857
+ catch (error) {
858
+ if (!(error instanceof Stop) || error.reason !== "base-moved")
859
+ throw error;
860
+ const head = await updateFromMain();
861
+ if (head === null)
862
+ throw error;
863
+ throw new Restart(head, true);
864
+ }
812
865
  if (j.intents.gate === undefined)
813
866
  await mainProtection(adapters, j, false, "Nothing was filed or sent for approval");
814
867
  const existing = await adapters.toolroll.gate(j.task.id);
@@ -963,6 +1016,15 @@ export async function runRelease(options, adapters) {
963
1016
  else if (standing.retried.length > 0)
964
1017
  say(` run ${gate.result.run} passed, but only on a retry: ${standing.retried.join(", ")}`);
965
1018
  }
1019
+ // A sandboxed Full check must show that what it skipped passed elsewhere (trusted-release-reports.md H3). The plane
1020
+ // answers at once for a check that ran unsandboxed, as Toolroll's own do: nothing there skips for this reason.
1021
+ if (rel?.fencedCoverage !== undefined) {
1022
+ const covered = await rel.fencedCoverage(j.task.id, gate.result.run, j.candidate.sha);
1023
+ if (!covered.ok)
1024
+ throw new Stop("check-failed", `Run ${gate.result.run} passed on ${j.candidate.sha.slice(0, 7)}, but ${covered.why}.`);
1025
+ if (covered.covered > 0)
1026
+ say(` ${covered.covered} cases skipped inside the sandbox passed in this commit's macOS CI`);
1027
+ }
966
1028
  return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree, head };
967
1029
  };
968
1030
  const result = await waitFor("check", `the release check of ${j.candidate.sha.slice(0, 7)}`, async () => {
@@ -1175,7 +1237,21 @@ export async function runRelease(options, adapters) {
1175
1237
  await identity("merge", false);
1176
1238
  await pinned();
1177
1239
  // A squash merge only reproduces the gated (and CI-tested) tree when main hasn't moved past the branch's base.
1178
- const main = await containsMain(adapters, j, "Nothing was merged");
1240
+ let main;
1241
+ try {
1242
+ main = await containsMain(adapters, j, "Nothing was merged");
1243
+ }
1244
+ catch (error) {
1245
+ if (!(error instanceof Stop) || error.reason !== "base-moved")
1246
+ throw error;
1247
+ // Past the deploy: the updated commit isn't what's running, so it starts over, with a person.
1248
+ const head = await updateFromMain();
1249
+ if (head === null)
1250
+ throw error;
1251
+ j.needsApproval = true;
1252
+ save();
1253
+ throw new Stop("approval", `${j.branch} was behind main, so it's now at ${head.slice(0, 7)}. Nothing was merged. The new commit needs a person's approval: run \`toolroll release ${j.branch} --repo ${j.checkout} --new\`.`);
1254
+ }
1179
1255
  // Every release merges only where GitHub itself refuses a branch behind main.
1180
1256
  await mainProtection(adapters, j, j.auto?.state === "approved", "Nothing was merged");
1181
1257
  // The checks again, at the moment of merging: a rerun that failed since must not merge.
@@ -1290,7 +1366,7 @@ export async function runRelease(options, adapters) {
1290
1366
  catch (error) {
1291
1367
  if (!(error instanceof Restart))
1292
1368
  throw error;
1293
- await restart(error.head);
1369
+ await restart(error.head, error.updated);
1294
1370
  }
1295
1371
  }
1296
1372
  const left = homebrewLeft;
@@ -0,0 +1,36 @@
1
+ export type Lookup = (host: string) => Promise<readonly string[]>;
2
+ export declare const lookupAll: Lookup;
3
+ export type Destination = {
4
+ scheme: "https" | "ssh";
5
+ host: string;
6
+ port: number;
7
+ literal: boolean;
8
+ };
9
+ export type Pin = {
10
+ /** `-c` settings for git, or nothing for ssh. */
11
+ gitArgs: string[];
12
+ /** Environment on top of remote-main.ts's ENV. */
13
+ env: Record<string, string>;
14
+ /** The proxy's host name when the read goes through the operator's proxy. */
15
+ proxy: string | null;
16
+ };
17
+ export type PinResult = {
18
+ ok: true;
19
+ pin: Pin;
20
+ } | {
21
+ ok: false;
22
+ why: string;
23
+ };
24
+ /** Whether an address is loopback, unspecified, link-local, or an IPv4-mapped or NAT64 form of these. */
25
+ export declare function addressRefused(address: string): boolean;
26
+ /** The host, effective port and scheme a recorded URL connects to; null when it can't be read. The caller has already
27
+ * passed remoteUrlProblem. Node's URL parser normalizes every spelling of a literal host. */
28
+ export declare function destinationOf(url: string): Destination | null;
29
+ /** Whether curl would skip the proxy for this host; "unknown" when an entry can't be decided. */
30
+ export declare function noProxyApplies(noProxy: string, host: string): boolean | "unknown";
31
+ /**
32
+ * Decide the route and pin it. `env` is what git will see (the names remote-main.ts's ENV_ALLOWLIST passes). ssh never
33
+ * uses a proxy. Through a proxy, the proxy's own address is checked and pinned; the operator's proxy then chooses the
34
+ * final destination, and the caller says so.
35
+ */
36
+ export declare function pinDestination(url: string, env: Readonly<Record<string, string | undefined>>, lookup?: Lookup): Promise<PinResult>;