toolroll 0.9.92 → 0.9.94

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 (341) hide show
  1. package/CHANGELOG.md +87 -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 +4 -0
  5. package/dist/agent-fence.js +4 -1
  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 -3
  12. package/dist/assignment.js +16 -3
  13. package/dist/boot-identity.d.ts +2 -1
  14. package/dist/boot-identity.js +6 -4
  15. package/dist/bounded-spawn.d.ts +57 -0
  16. package/dist/bounded-spawn.js +220 -0
  17. package/dist/browser/terminal.js +42 -42
  18. package/dist/browser/workspace.css +2 -2
  19. package/dist/browser/workspace.js +26 -26
  20. package/dist/browser-crew.js +2 -2
  21. package/dist/browser-shell.d.ts +1 -1
  22. package/dist/browser-shell.js +2 -2
  23. package/dist/browser-workspace.d.ts +48 -7
  24. package/dist/browser-workspace.js +3 -3
  25. package/dist/builder-restart.d.ts +21 -0
  26. package/dist/builder-restart.js +50 -0
  27. package/dist/builder.d.ts +4 -0
  28. package/dist/builder.js +73 -25
  29. package/dist/chat-acceptance.js +1 -1
  30. package/dist/chat-actions.d.ts +12 -2
  31. package/dist/chat-actions.js +25 -2
  32. package/dist/chat-channel.js +11 -0
  33. package/dist/chat-decide.d.ts +7 -2
  34. package/dist/chat-decide.js +37 -9
  35. package/dist/chat-quiet.js +3 -3
  36. package/dist/chat-voice.d.ts +1 -1
  37. package/dist/chat-voice.js +2 -2
  38. package/dist/check-facts.d.ts +126 -0
  39. package/dist/check-facts.js +318 -0
  40. package/dist/check-failure.d.ts +22 -3
  41. package/dist/check-failure.js +57 -13
  42. package/dist/check-levels-ui.d.ts +4 -0
  43. package/dist/check-levels-ui.js +10 -1
  44. package/dist/check-load.d.ts +85 -0
  45. package/dist/check-load.js +147 -0
  46. package/dist/check-only-standing.d.ts +36 -0
  47. package/dist/check-only-standing.js +39 -0
  48. package/dist/check-only.d.ts +15 -0
  49. package/dist/check-only.js +99 -0
  50. package/dist/check-report.d.ts +7 -0
  51. package/dist/check-report.js +4 -3
  52. package/dist/check-stamp.d.ts +46 -0
  53. package/dist/check-stamp.js +93 -0
  54. package/dist/checkout-cleanup.d.ts +36 -5
  55. package/dist/checkout-cleanup.js +166 -14
  56. package/dist/cli-help.d.ts +4 -1
  57. package/dist/cli-help.js +47 -30
  58. package/dist/cli.js +16 -1
  59. package/dist/code-extract.d.ts +44 -0
  60. package/dist/code-extract.js +88 -0
  61. package/dist/code-map-acts.d.ts +24 -0
  62. package/dist/code-map-acts.js +53 -0
  63. package/dist/code-map-admission.d.ts +52 -0
  64. package/dist/code-map-admission.js +295 -0
  65. package/dist/code-map-build.d.ts +63 -0
  66. package/dist/code-map-build.js +415 -0
  67. package/dist/code-map-cli.d.ts +153 -0
  68. package/dist/code-map-cli.js +103 -0
  69. package/dist/code-map-main.d.ts +1 -0
  70. package/dist/code-map-main.js +48 -0
  71. package/dist/code-map-read.d.ts +106 -0
  72. package/dist/code-map-read.js +171 -0
  73. package/dist/code-map-report.d.ts +37 -0
  74. package/dist/code-map-report.js +95 -0
  75. package/dist/code-map-shape.d.ts +172 -0
  76. package/dist/code-map-shape.js +327 -0
  77. package/dist/code-map-store.d.ts +136 -0
  78. package/dist/code-map-store.js +233 -0
  79. package/dist/coding-provider.d.ts +2 -0
  80. package/dist/coding-provider.js +3 -2
  81. package/dist/coding-update.js +3 -2
  82. package/dist/coding-workspace.js +4 -4
  83. package/dist/console-chrome.js +2 -2
  84. package/dist/containment.d.ts +5 -0
  85. package/dist/containment.js +14 -1
  86. package/dist/contracts/chat-actions.d.ts +9 -0
  87. package/dist/contracts/chat-actions.js +2 -0
  88. package/dist/contracts/cli.d.ts +52 -0
  89. package/dist/contracts/cli.js +4 -0
  90. package/dist/contracts/console-api.d.ts +12 -5
  91. package/dist/contracts/console-api.js +12 -5
  92. package/dist/contracts/gateway-tools.d.ts +17 -0
  93. package/dist/contracts/gateway-tools.js +3 -1
  94. package/dist/contracts/lead-tools.d.ts +61 -0
  95. package/dist/contracts/lead-tools.js +10 -1
  96. package/dist/contracts/project-knowledge.d.ts +38 -5
  97. package/dist/contracts/project-knowledge.js +11 -5
  98. package/dist/contracts/project-memory.d.ts +3 -1
  99. package/dist/contracts/project-memory.js +3 -2
  100. package/dist/contracts/release-check.d.ts +25 -1
  101. package/dist/contracts/release-check.js +19 -0
  102. package/dist/contracts/storage.d.ts +14 -0
  103. package/dist/contracts/storage.js +3 -1
  104. package/dist/contracts/store-json.d.ts +2 -0
  105. package/dist/contracts/youiyoux-request.d.ts +1 -1
  106. package/dist/contracts/youiyoux-request.js +1 -1
  107. package/dist/desktop-access.js +6 -2
  108. package/dist/desktop-update.d.ts +2 -2
  109. package/dist/desktop-update.js +13 -9
  110. package/dist/dispatch.js +18 -15
  111. package/dist/dispose.js +8 -2
  112. package/dist/easy-start.js +2 -3
  113. package/dist/eval-hooks.d.ts +55 -0
  114. package/dist/eval-hooks.js +227 -4
  115. package/dist/evals.js +2 -1
  116. package/dist/fast-start.d.ts +136 -0
  117. package/dist/fast-start.js +431 -0
  118. package/dist/fast-terms.d.ts +9 -0
  119. package/dist/fast-terms.js +23 -0
  120. package/dist/fenced-check-coverage.d.ts +62 -0
  121. package/dist/fenced-check-coverage.js +54 -0
  122. package/dist/first-run.js +1 -1
  123. package/dist/flaky-ci.d.ts +31 -0
  124. package/dist/flaky-ci.js +149 -0
  125. package/dist/flaky-cli.d.ts +60 -0
  126. package/dist/flaky-cli.js +60 -0
  127. package/dist/flaky.d.ts +83 -0
  128. package/dist/flaky.js +264 -0
  129. package/dist/flow-card-task.js +3 -0
  130. package/dist/flow-triggers.d.ts +1 -1
  131. package/dist/flow-triggers.js +1 -1
  132. package/dist/github-import.d.ts +62 -0
  133. package/dist/github-import.js +146 -0
  134. package/dist/guides.js +15 -10
  135. package/dist/invoke.d.ts +5 -1
  136. package/dist/invoke.js +49 -14
  137. package/dist/job-object-helper.ps1 +10 -3
  138. package/dist/keymap.d.ts +41 -0
  139. package/dist/keymap.js +51 -0
  140. package/dist/knowledge-cli.js +1 -1
  141. package/dist/knowledge-ui.d.ts +21 -4
  142. package/dist/knowledge-ui.js +75 -7
  143. package/dist/laptop-background.d.ts +16 -0
  144. package/dist/laptop-background.js +108 -2
  145. package/dist/lead-context.js +1 -1
  146. package/dist/lead-contract.d.ts +1 -1
  147. package/dist/lead-contract.js +2 -2
  148. package/dist/lead-progress.js +3 -0
  149. package/dist/lead-status.d.ts +2 -0
  150. package/dist/lead-status.js +6 -2
  151. package/dist/lead-tools.js +51 -2
  152. package/dist/lead.js +1 -1
  153. package/dist/lesson-notes.d.ts +81 -0
  154. package/dist/lesson-notes.js +336 -0
  155. package/dist/limits-ui.js +1 -1
  156. package/dist/local-owner.d.ts +1 -1
  157. package/dist/local-owner.js +11 -0
  158. package/dist/machine-check.d.ts +16 -0
  159. package/dist/machine-check.js +26 -0
  160. package/dist/machine-identity.d.ts +75 -0
  161. package/dist/machine-identity.js +170 -0
  162. package/dist/mcp-core.js +11 -1
  163. package/dist/memory-cli.d.ts +300 -1
  164. package/dist/memory-cli.js +62 -9
  165. package/dist/memory-pass.d.ts +16 -0
  166. package/dist/memory-pass.js +67 -3
  167. package/dist/memory-tidy.d.ts +181 -0
  168. package/dist/memory-tidy.js +523 -0
  169. package/dist/memory-use.d.ts +35 -0
  170. package/dist/memory-use.js +109 -0
  171. package/dist/merge-checklist.d.ts +55 -0
  172. package/dist/merge-checklist.js +182 -0
  173. package/dist/needs-you.d.ts +32 -13
  174. package/dist/needs-you.js +108 -49
  175. package/dist/nightly-github.d.ts +2 -0
  176. package/dist/nightly-github.js +4 -4
  177. package/dist/operate-remote-arguments.js +6 -1
  178. package/dist/operate-remote.js +3 -1
  179. package/dist/operate.d.ts +6 -1
  180. package/dist/operate.js +540 -70
  181. package/dist/phase-routing.js +2 -2
  182. package/dist/plain-schema.d.ts +23 -0
  183. package/dist/plain-schema.js +94 -0
  184. package/dist/plain-words.d.ts +20 -0
  185. package/dist/plain-words.js +66 -0
  186. package/dist/planner.js +2 -1
  187. package/dist/process-liveness.d.ts +2 -0
  188. package/dist/process-liveness.js +61 -17
  189. package/dist/project-delete-ui.d.ts +7 -2
  190. package/dist/project-delete-ui.js +12 -1
  191. package/dist/project-delete.js +13 -2
  192. package/dist/project-knowledge.d.ts +18 -11
  193. package/dist/project-knowledge.js +39 -9
  194. package/dist/project-learning.d.ts +2 -1
  195. package/dist/project-learning.js +15 -45
  196. package/dist/project-memory.d.ts +5 -0
  197. package/dist/project-memory.js +22 -2
  198. package/dist/proof.js +30 -5
  199. package/dist/provider-auth.js +1 -1
  200. package/dist/publish.js +9 -1
  201. package/dist/pull-request-flow.js +5 -0
  202. package/dist/push.d.ts +26 -3
  203. package/dist/push.js +55 -15
  204. package/dist/release-adapters.d.ts +9 -0
  205. package/dist/release-adapters.js +87 -1
  206. package/dist/release-approval.d.ts +191 -0
  207. package/dist/release-approval.js +382 -0
  208. package/dist/release-auto.js +26 -17
  209. package/dist/release-card.d.ts +74 -6
  210. package/dist/release-card.js +179 -23
  211. package/dist/release-carry.d.ts +7 -2
  212. package/dist/release-carry.js +39 -9
  213. package/dist/release-cli.d.ts +4 -0
  214. package/dist/release-cli.js +38 -23
  215. package/dist/release-coverage.js +1 -1
  216. package/dist/release-next.d.ts +48 -0
  217. package/dist/release-next.js +161 -0
  218. package/dist/release.d.ts +50 -1
  219. package/dist/release.js +118 -11
  220. package/dist/remote-destination.d.ts +36 -0
  221. package/dist/remote-destination.js +217 -0
  222. package/dist/remote-main.d.ts +48 -0
  223. package/dist/remote-main.js +84 -5
  224. package/dist/repository-context.d.ts +3 -2
  225. package/dist/repository-context.js +112 -70
  226. package/dist/restart-certification.d.ts +1 -0
  227. package/dist/restart-certification.js +8 -6
  228. package/dist/result-acts.d.ts +29 -21
  229. package/dist/result-acts.js +36 -25
  230. package/dist/result-follow-ups.d.ts +2 -2
  231. package/dist/result-follow-ups.js +18 -4
  232. package/dist/result-review.d.ts +1 -1
  233. package/dist/route-change-check.d.ts +4 -0
  234. package/dist/route-change-check.js +13 -0
  235. package/dist/route-change.d.ts +22 -0
  236. package/dist/route-change.js +89 -0
  237. package/dist/route-hooks.js +3 -2
  238. package/dist/route-results.d.ts +6 -1
  239. package/dist/route-results.js +14 -5
  240. package/dist/run-closeout.d.ts +1 -1
  241. package/dist/run-closeout.js +7 -5
  242. package/dist/runner-process.js +3 -2
  243. package/dist/runner.js +2 -1
  244. package/dist/scope.d.ts +6 -1
  245. package/dist/scope.js +22 -9
  246. package/dist/serve.d.ts +1 -1
  247. package/dist/serve.js +24 -18
  248. package/dist/server/chat.js +14 -12
  249. package/dist/server/chrome.d.ts +1 -1
  250. package/dist/server/chrome.js +26 -35
  251. package/dist/server/console.css +23 -0
  252. package/dist/server/guards.js +4 -0
  253. package/dist/server/home.d.ts +6 -0
  254. package/dist/server/home.js +21 -13
  255. package/dist/server/http.js +1 -1
  256. package/dist/server/kit.js +1 -1
  257. package/dist/server/merge-ready-view.d.ts +10 -0
  258. package/dist/server/merge-ready-view.js +30 -0
  259. package/dist/server/pages-assets.d.ts +1 -0
  260. package/dist/server/pages-assets.js +8 -5
  261. package/dist/server/pages.js +103 -82
  262. package/dist/server/render-chat.js +2 -2
  263. package/dist/server/render-code-map.d.ts +30 -0
  264. package/dist/server/render-code-map.js +137 -0
  265. package/dist/server/render-flaky.d.ts +20 -0
  266. package/dist/server/render-flaky.js +59 -0
  267. package/dist/server/render-history.js +1 -1
  268. package/dist/server/render-new-task.d.ts +26 -1
  269. package/dist/server/render-new-task.js +30 -5
  270. package/dist/server/render-pages.d.ts +1 -175
  271. package/dist/server/render-pages.js +38 -313
  272. package/dist/server/render-projects.d.ts +2 -1
  273. package/dist/server/render-projects.js +9 -7
  274. package/dist/server/render-release.d.ts +19 -0
  275. package/dist/server/render-release.js +84 -0
  276. package/dist/server/render-results.d.ts +2 -2
  277. package/dist/server/render-results.js +71 -73
  278. package/dist/server/render-tasks.d.ts +13 -2
  279. package/dist/server/render-tasks.js +156 -139
  280. package/dist/server/retired-routes.d.ts +17 -0
  281. package/dist/server/retired-routes.js +53 -0
  282. package/dist/server/route-table.d.ts +4 -1
  283. package/dist/server/route-table.js +52 -13
  284. package/dist/server/settings.js +129 -22
  285. package/dist/server/task-views.js +61 -26
  286. package/dist/server/tasks.d.ts +17 -6
  287. package/dist/server/tasks.js +273 -338
  288. package/dist/server/welcome.js +1 -1
  289. package/dist/settings-groups-ui.js +1 -1
  290. package/dist/setup-guide.d.ts +2 -2
  291. package/dist/stop-proof.d.ts +67 -0
  292. package/dist/stop-proof.js +295 -0
  293. package/dist/storage-sweep-process.js +1 -1
  294. package/dist/storage-sweep.d.ts +5 -3
  295. package/dist/storage-sweep.js +30 -9
  296. package/dist/storage-ui.js +28 -17
  297. package/dist/storage.d.ts +27 -2
  298. package/dist/storage.js +39 -2
  299. package/dist/store.d.ts +148 -34
  300. package/dist/store.js +626 -177
  301. package/dist/surface.js +46 -5
  302. package/dist/sweep-quarantine.d.ts +1 -0
  303. package/dist/sweep-quarantine.js +6 -4
  304. package/dist/tamper-scan.d.ts +4 -0
  305. package/dist/tamper-scan.js +90 -0
  306. package/dist/task-aliases.d.ts +5 -0
  307. package/dist/task-aliases.js +9 -0
  308. package/dist/task-control.js +4 -1
  309. package/dist/task-outcome-cli.d.ts +2 -0
  310. package/dist/task-outcome-cli.js +36 -17
  311. package/dist/task-sizing.d.ts +5 -0
  312. package/dist/task-sizing.js +2 -0
  313. package/dist/task-source.d.ts +7 -0
  314. package/dist/task-source.js +14 -0
  315. package/dist/task-status.d.ts +9 -0
  316. package/dist/task-status.js +40 -49
  317. package/dist/teardown-git.d.ts +16 -0
  318. package/dist/teardown-git.js +63 -5
  319. package/dist/telegram-progress.js +5 -3
  320. package/dist/telegram-status.js +6 -4
  321. package/dist/test-infra.d.ts +8 -0
  322. package/dist/test-infra.js +18 -0
  323. package/dist/tidy.js +1 -1
  324. package/dist/toolroll-update-ui.js +2 -2
  325. package/dist/toolroll-update.js +18 -5
  326. package/dist/tui-contract.d.ts +24 -0
  327. package/dist/tui-source.js +67 -8
  328. package/dist/tui.js +51 -51
  329. package/dist/vanished-retry.d.ts +30 -0
  330. package/dist/vanished-retry.js +174 -0
  331. package/dist/work-index.d.ts +2 -0
  332. package/dist/work-index.js +82 -70
  333. package/dist/work-summary.d.ts +2 -2
  334. package/dist/work-summary.js +2 -0
  335. package/dist/workspace-ui.d.ts +4 -2
  336. package/dist/workspace-ui.js +28 -22
  337. package/dist/worktree.d.ts +12 -3
  338. package/dist/worktree.js +68 -13
  339. package/package.json +1 -1
  340. package/dist/server/render-next.d.ts +0 -63
  341. package/dist/server/render-next.js +0 -109
@@ -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,43 @@ 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
+ }>;
346
+ /** One-tap release approval (docs/plans/trusted-release-reports.md, slice A): what the plane tells a person and
347
+ * keeps for the approval sheet. Best effort: a failure here never changes the release. Absent on an older plane. */
348
+ ready?: ReleaseReadyAdapter;
349
+ };
350
+ /** What the coordinator tells the plane for a person's release (release-approval.ts). Ids and its own readings only. */
351
+ export type ReleaseReadyAdapter = {
352
+ /** The approval step was entered (a fresh filing or a resumed one): one notification per task and scope digest. */
353
+ asked(taskId: string): Promise<void>;
354
+ /** The pull request's required checks on the candidate, as this coordinator just read them. */
355
+ ci(input: {
356
+ taskId: string;
357
+ candidate: string;
358
+ state: "passed" | "running" | "failed";
359
+ failed: readonly string[];
360
+ runs: readonly CheckRun[];
361
+ }): Promise<void>;
362
+ /** A person's release stopped: one attention notification per task, step and reason. */
363
+ stopped(input: {
364
+ taskId: string;
365
+ step: string;
366
+ reason: string;
367
+ message: string;
368
+ }): Promise<void>;
369
+ /** Main's merge commit was proved to hold the approved tree: the plane checks it again and keeps the release's record. */
370
+ verified(input: {
371
+ taskId: string;
372
+ merge: string;
373
+ }): Promise<void>;
335
374
  };
336
375
  /** The plane's automatic release approval, as the coordinator asks it. Every answer is the plane's own: the coordinator
337
376
  * hands it ids, never evidence (the plane reads GitHub CI itself for `approve`). */
@@ -451,11 +490,13 @@ export type ReleaseAdapters = {
451
490
  }): Promise<{
452
491
  scopeDigest: string;
453
492
  }>;
493
+ /** `reason` "stopping": Toolroll is still proving the check's build stopped (stop-proof.ts); it clears on its own. */
454
494
  complete(taskId: string, receipt: string): Promise<{
455
495
  ok: true;
456
496
  } | {
457
497
  ok: false;
458
498
  message: string;
499
+ reason?: string;
459
500
  }>;
460
501
  busy(): Promise<{
461
502
  running: number;
@@ -506,6 +547,14 @@ export type ReleaseAdapters = {
506
547
  mainHead(repo: string): Promise<string>;
507
548
  /** Whether `sha` already holds `ancestor`, by GitHub's own comparison. */
508
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
+ }>;
509
558
  /** Whether GitHub refuses to merge a branch that is behind main ("Require branches to be up to date before
510
559
  * merging"); null when it can't be read. Read only. */
511
560
  upToDateRequired(repo: string): Promise<MainProtection | null>;
package/dist/release.js CHANGED
@@ -101,8 +101,10 @@ export const ADVISORY_PR_CHECKS = {
101
101
  export const RELEASE_TAG_CHECKS = ["release proof", "publish"];
102
102
  export const RELEASE_TAG_WORKFLOW = ".github/workflows/publish.yml";
103
103
  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 };
104
+ /** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). The
105
+ * check's covers the first attempt and the two same-commit reruns the plane may give it, each of which a busy machine
106
+ * may stretch to two hours (check-load.ts). */
107
+ export const RELEASE_LIMITS = { gate: 60, approval: 24 * 60, check: 360, quiet: 60, deploy: 30, ci: 60, publish: 45, homebrew: 45 };
106
108
  const POLL_MS = 15_000;
107
109
  /**
108
110
  * Failure classes replace a flat flake allowance (docs/plans/eval-harness.md; check-failure.ts says the classes): only a
@@ -129,9 +131,12 @@ class HomebrewFailed extends Error {
129
131
  /** The branch moved before the deploy reservation: carry the approval to the new head, or file it for a person. */
130
132
  class Restart extends Error {
131
133
  head;
132
- constructor(head) {
134
+ updated;
135
+ /** `updated`: Toolroll itself brought the branch up to main; the new commit is a new candidate and nothing carries. */
136
+ constructor(head, updated = false) {
133
137
  super(`restart at ${head}`);
134
138
  this.head = head;
139
+ this.updated = updated;
135
140
  }
136
141
  }
137
142
  /** Every commit that landed after the approval, in words: the summary and the merge message list them all. */
@@ -453,6 +458,20 @@ export async function runRelease(options, adapters) {
453
458
  };
454
459
  // ---- one approval per release (release-once.md) -------------------------------------------------------------
455
460
  const rel = adapters.release;
461
+ /** One-tap release approval: tell the plane, never stopping or changing the release whatever it answers. */
462
+ const tell = async (act) => { if (rel?.ready === undefined)
463
+ return; try {
464
+ await act(rel.ready);
465
+ }
466
+ catch { /* best effort */ } };
467
+ /** The last pull request checks this run read on the candidate (the gate reads them before the task exists). */
468
+ let lastCi = null;
469
+ const noteCi = async (runs, verdict) => {
470
+ lastCi = { candidate: j.candidate.sha, state: verdict.state === "pending" ? "running" : verdict.state, failed: verdict.failed ?? [], runs: runs.filter(run => REQUIRED_PR_CHECKS.includes(run.name)) };
471
+ const seen = lastCi;
472
+ await tell(ready => ready.ci({ taskId: j.task.id, ...seen }));
473
+ };
474
+ const currentCi = () => lastCi;
456
475
  let epoch = null;
457
476
  // This invocation's own lease owner (review round 1): two runs of the same journal never share an epoch.
458
477
  const owner = `${j.id}:${randomUUID()}`;
@@ -612,11 +631,41 @@ export async function runRelease(options, adapters) {
612
631
  /** identity(), on a plane that keeps release authority; a legacy plane keeps the old rules. */
613
632
  const watch = async (action) => { if (rel !== undefined)
614
633
  await identity(action, true); };
615
- const restart = async (head) => {
634
+ /**
635
+ * Behind main (plain-toolroll.md P2.6): update the branch under this release's lease and the head it was gated on.
636
+ * A concurrent push aborts it. The approval stops first, so nothing can ride it across the update; the updated head
637
+ * is a new candidate with a new approval, fresh CI and a fresh check, and there is no carry.
638
+ */
639
+ const updateFromMain = async () => {
640
+ const update = adapters.github.updateBranch;
641
+ if (update === undefined)
642
+ return null;
643
+ if (rel !== undefined && j.authority) {
644
+ if (epoch === null || !await rel.renew(j.authority.id, owner, epoch))
645
+ throw new Stop("lease", "Another release coordinator took over this release.");
646
+ }
647
+ const pr = await adapters.github.pullRequest(j.github, j.branch);
648
+ if (pr === null || pr.number !== j.pr || pr.state !== "open" || pr.head !== j.candidate.sha)
649
+ throw new Stop("drift", `${j.branch} moved while it was being brought up to main. Nothing was changed; rerun \`${resume}\`.`);
650
+ 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)) {
651
+ throw new Stop("lease", "The release's approval couldn't be stopped before the update, so the branch wasn't updated.");
652
+ }
653
+ say(` ${j.branch} is behind main: bringing it up to date`);
654
+ const moved = await update(j.github, j.pr, j.candidate.sha);
655
+ if (!moved.ok)
656
+ throw new Stop("drift", `${j.branch} couldn't be brought up to main: ${moved.why}. Rerun \`${resume}\`.`);
657
+ return waitFor("ci", `${j.branch} to move past ${j.candidate.sha.slice(0, 7)}`, async () => {
658
+ const head = await adapters.repo.remoteHead(j.branch);
659
+ return head !== null && head !== j.candidate.sha ? head : null;
660
+ });
661
+ };
662
+ const restart = async (head, updated = false) => {
616
663
  // The step this commit was at ends here: the release goes on with the new commit (and, filed again, a new task).
617
664
  mark(j.step, "failed", `${j.branch} moved to ${head.slice(0, 7)}`);
618
665
  const tree = await adapters.repo.tree(head);
619
- if (rel !== undefined && j.authority && epoch !== null) {
666
+ if (updated)
667
+ say(` ${j.branch} is at ${head.slice(0, 7)}: a new commit, so it needs its own approval, CI and check`);
668
+ else if (rel !== undefined && j.authority && epoch !== null) {
620
669
  say(` ${j.branch} moved to ${head.slice(0, 7)}: checking whether the approval carries`);
621
670
  const carried = await rel.carry(j.authority.id, head, randomUUID(), epoch);
622
671
  if (carried.ok) {
@@ -693,6 +742,7 @@ export async function runRelease(options, adapters) {
693
742
  throw error;
694
743
  }
695
744
  const verdict = prChecksVerdict(runs, j.candidate.sha, REQUIRED_PR_CHECKS, { workflow: REQUIRED_PR_CHECKS_WORKFLOW });
745
+ await noteCi(runs, verdict);
696
746
  if (verdict.state === "failed") {
697
747
  throw new Stop("ci-failed", `GitHub's checks failed on ${j.candidate.sha.slice(0, 7)}: ${verdict.failed.join(", ")}. ${nothing}. ${j.approval === null ? "" : `The approval covers ${j.candidate.sha.slice(0, 7)} only. `}Rerun the failed job on GitHub and run \`${resume}\` again, or fix the branch and release the new commit with \`toolroll release ${releaseBranchArg(j.branch)} --repo ${j.checkout} --new\`.`);
698
748
  }
@@ -793,7 +843,17 @@ export async function runRelease(options, adapters) {
793
843
  }
794
844
  // The tree CI tested (the pull request's head, not GitHub's merge commit) is the tree that merges only when the
795
845
  // branch already contains main's head: refused here, before anything is filed, and again right before merging.
796
- await containsMain(adapters, j, "Nothing was filed or sent for approval");
846
+ try {
847
+ await containsMain(adapters, j, "Nothing was filed or sent for approval");
848
+ }
849
+ catch (error) {
850
+ if (!(error instanceof Stop) || error.reason !== "base-moved")
851
+ throw error;
852
+ const head = await updateFromMain();
853
+ if (head === null)
854
+ throw error;
855
+ throw new Restart(head, true);
856
+ }
797
857
  if (j.intents.gate === undefined)
798
858
  await mainProtection(adapters, j, false, "Nothing was filed or sent for approval");
799
859
  const existing = await adapters.toolroll.gate(j.task.id);
@@ -810,7 +870,7 @@ export async function runRelease(options, adapters) {
810
870
  // The gate only stops on a check that already failed: the task is filed and a person asked while CI runs, and
811
871
  // the release check runs beside it (pipeline-speed.md, D2 and "Decision: a person's release asks during
812
872
  // CI"). The automatic approval waits for CI (the check step); nothing deploys before it passes (the deploy).
813
- const settled = await earlyChecks(adapters, j, (what, probe) => waitFor("gate", what, probe), { pendingIsEnough: true });
873
+ const settled = await earlyChecks(adapters, j, (what, probe) => waitFor("gate", what, probe), { pendingIsEnough: true, seen: (runs, verdict) => noteCi(runs, verdict) });
814
874
  // Recorded before the task exists, so its approval card says CI still runs from the first moment.
815
875
  if (!settled)
816
876
  await rel?.auto?.ciRunning?.(j.task.id, j.candidate.sha);
@@ -830,6 +890,10 @@ export async function runRelease(options, adapters) {
830
890
  ...(rel === undefined ? {} : { release: { version: j.packageVersion, pr: j.pr, branch: j.branch, noCarry: options.noCarry === true } }),
831
891
  });
832
892
  j.task.scopeDigest = filed.scopeDigest;
893
+ // The checks the gate read before the task existed, kept for its approval sheet now that it does.
894
+ const seen = currentCi();
895
+ if (seen !== null)
896
+ await tell(ready => ready.ci({ taskId: j.task.id, ...seen }));
833
897
  }
834
898
  advance("approval");
835
899
  break;
@@ -862,7 +926,11 @@ export async function runRelease(options, adapters) {
862
926
  // A check that already failed asks nobody (as at the gate).
863
927
  if (j.ciAhead === true)
864
928
  await ciWhileRunning("Nothing was deployed or merged", true);
929
+ // One push, on every entry to this step (a fresh filing or a resumed one): the plane files it once per task and
930
+ // scope digest, so a crash between filing and here is healed by the next run.
931
+ await tell(ready => ready.asked(j.task.id));
865
932
  say(` The release scope needs your approval: \`toolroll task approve ${j.task.id} --digest ${j.task.scopeDigest}\`, or open the task in the console.`);
933
+ say(` Or approve it from here: \`toolroll release approve ${releaseBranchArg(j.branch)}\`.`);
866
934
  if (j.ciAhead === true)
867
935
  say(` ${CI_RUNNING_WORDS}`);
868
936
  const approved = await waitFor("approval", `a person to approve task ${j.task.id}`, async () => {
@@ -940,6 +1008,15 @@ export async function runRelease(options, adapters) {
940
1008
  else if (standing.retried.length > 0)
941
1009
  say(` run ${gate.result.run} passed, but only on a retry: ${standing.retried.join(", ")}`);
942
1010
  }
1011
+ // A sandboxed Full check must show that what it skipped passed elsewhere (trusted-release-reports.md H3). The plane
1012
+ // answers at once for a check that ran unsandboxed, as Toolroll's own do: nothing there skips for this reason.
1013
+ if (rel?.fencedCoverage !== undefined) {
1014
+ const covered = await rel.fencedCoverage(j.task.id, gate.result.run, j.candidate.sha);
1015
+ if (!covered.ok)
1016
+ throw new Stop("check-failed", `Run ${gate.result.run} passed on ${j.candidate.sha.slice(0, 7)}, but ${covered.why}.`);
1017
+ if (covered.covered > 0)
1018
+ say(` ${covered.covered} cases skipped inside the sandbox passed in this commit's macOS CI`);
1019
+ }
943
1020
  return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree, head };
944
1021
  };
945
1022
  const result = await waitFor("check", `the release check of ${j.candidate.sha.slice(0, 7)}`, async () => {
@@ -1007,7 +1084,12 @@ export async function runRelease(options, adapters) {
1007
1084
  await checked(gate, "complete");
1008
1085
  if (gate.completion?.digest !== check.receipt) {
1009
1086
  intend("complete");
1010
- const done = await adapters.toolroll.complete(j.task.id, check.receipt);
1087
+ // Toolroll proves the check's build stopped by itself (stop-proof.ts) a short while after it finished: the
1088
+ // release waits for that, never for a person to confirm it.
1089
+ const done = await waitFor("quiet", "Toolroll to make sure the release check stopped", async () => {
1090
+ const tried = await adapters.toolroll.complete(j.task.id, check.receipt);
1091
+ return !tried.ok && tried.reason === "stopping" ? null : tried;
1092
+ });
1011
1093
  if (!done.ok)
1012
1094
  throw new Stop("complete", `Couldn't mark task ${j.task.id} complete: ${done.message} Mark that exact result complete in the console, then rerun.`);
1013
1095
  }
@@ -1147,7 +1229,21 @@ export async function runRelease(options, adapters) {
1147
1229
  await identity("merge", false);
1148
1230
  await pinned();
1149
1231
  // A squash merge only reproduces the gated (and CI-tested) tree when main hasn't moved past the branch's base.
1150
- const main = await containsMain(adapters, j, "Nothing was merged");
1232
+ let main;
1233
+ try {
1234
+ main = await containsMain(adapters, j, "Nothing was merged");
1235
+ }
1236
+ catch (error) {
1237
+ if (!(error instanceof Stop) || error.reason !== "base-moved")
1238
+ throw error;
1239
+ // Past the deploy: the updated commit isn't what's running, so it starts over, with a person.
1240
+ const head = await updateFromMain();
1241
+ if (head === null)
1242
+ throw error;
1243
+ j.needsApproval = true;
1244
+ save();
1245
+ 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\`.`);
1246
+ }
1151
1247
  // Every release merges only where GitHub itself refuses a branch behind main.
1152
1248
  await mainProtection(adapters, j, j.auto?.state === "approved", "Nothing was merged");
1153
1249
  // The checks again, at the moment of merging: a rerun that failed since must not merge.
@@ -1202,6 +1298,8 @@ export async function runRelease(options, adapters) {
1202
1298
  merge.tree = tree;
1203
1299
  if (tree !== j.candidate.tree)
1204
1300
  throw new Stop("tree-mismatch", `Main's merge commit ${merge.sha.slice(0, 7)} has tree ${tree.slice(0, 12)}, not the gated ${j.candidate.tree.slice(0, 12)}: main changed what was checked. Nothing was tagged; the deployed build stays as it is.`);
1301
+ // The release's verified record (trusted-release-reports.md A1): the next release's change list starts here.
1302
+ await tell(ready => ready.verified({ taskId: j.task.id, merge: merge.sha }));
1205
1303
  advance("tag");
1206
1304
  break;
1207
1305
  }
@@ -1260,7 +1358,7 @@ export async function runRelease(options, adapters) {
1260
1358
  catch (error) {
1261
1359
  if (!(error instanceof Restart))
1262
1360
  throw error;
1263
- await restart(error.head);
1361
+ await restart(error.head, error.updated);
1264
1362
  }
1265
1363
  }
1266
1364
  const left = homebrewLeft;
@@ -1288,6 +1386,14 @@ export async function runRelease(options, adapters) {
1288
1386
  }
1289
1387
  catch { /* the stop is still reported */ }
1290
1388
  mark(journal.step, "failed", stop.message);
1389
+ // A person's release that stops tells them (trusted-release-reports.md A5); the automatic road keeps its own notices.
1390
+ const ready = adapters.release?.ready;
1391
+ if (ready !== undefined && journal.auto?.state !== "checking" && journal.auto?.state !== "approved") {
1392
+ try {
1393
+ await ready.stopped({ taskId: journal.task.id, step: journal.step, reason: stop.reason, message: stop.message });
1394
+ }
1395
+ catch { /* best effort */ }
1396
+ }
1291
1397
  }
1292
1398
  return { ok: false, reason: stop.reason, message: stop.message, step: journal?.step ?? step, journal, file, resume };
1293
1399
  }
@@ -1428,6 +1534,7 @@ async function earlyChecks(adapters, j, wait, options = {}) {
1428
1534
  return null;
1429
1535
  }
1430
1536
  const verdict = prChecksVerdict(runs.filter(run => required.includes(run.name)), j.candidate.sha, required, { workflow: REQUIRED_PR_CHECKS_WORKFLOW });
1537
+ await options.seen?.(runs, verdict);
1431
1538
  if (verdict.state === "failed") {
1432
1539
  throw new Stop("ci-failed", `Pull request checks already failed on ${sha}: ${verdict.failed.join(", ")}. ${nothing}. Fix the branch, then release the new commit with \`toolroll release ${j.branch} --repo ${j.checkout} --new\` (or rerun the failed job on GitHub and run this command again).`);
1433
1540
  }
@@ -1616,6 +1723,6 @@ export function workflowIs(path, file) {
1616
1723
  return /^[A-Za-z0-9._+/-]+$/.test(ref) && !/\.ya?ml$/i.test(ref);
1617
1724
  }
1618
1725
  /** The words `toolroll release` reads as its own (release-cli.ts), never as a branch: such a branch is named with --branch. */
1619
- const RELEASE_WORDS = new Set(["status", "nightly", "carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"]);
1726
+ const RELEASE_WORDS = new Set(["status", "nightly", "carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok", "approve"]);
1620
1727
  /** How a person names this branch to `toolroll release` again. */
1621
1728
  export function releaseBranchArg(branch) { return RELEASE_WORDS.has(branch) ? `--branch ${branch}` : branch; }
@@ -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>;