@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.3

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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -2
  3. package/bin/codeflow.mjs +8 -0
  4. package/bin/launcher.mjs +127 -0
  5. package/dist/remote-host.json +1 -0
  6. package/licenses/inter.txt +92 -0
  7. package/licenses/preact.txt +21 -0
  8. package/licenses/signals-core.txt +21 -0
  9. package/licenses/signals.txt +21 -0
  10. package/licenses/typescript-notices.txt +193 -0
  11. package/licenses/typescript.txt +55 -0
  12. package/licenses/uqr.txt +22 -0
  13. package/licenses/xterm-fit.txt +19 -0
  14. package/licenses/xterm.txt +21 -0
  15. package/package.json +34 -4
  16. package/public/assets/changes-62ZIEQSN.js +1 -0
  17. package/public/assets/chunk-2PZVOKID.js +1 -0
  18. package/public/assets/chunk-4D5IENEB.js +1 -0
  19. package/public/assets/chunk-5SARP5MU.js +1 -0
  20. package/public/assets/chunk-63YLZ3PR.js +1 -0
  21. package/public/assets/chunk-7FUHKF2M.js +1 -0
  22. package/public/assets/chunk-APQEVJ2K.js +1 -0
  23. package/public/assets/chunk-CGOZQMEZ.js +2 -0
  24. package/public/assets/chunk-J4VYS3KV.js +2 -0
  25. package/public/assets/chunk-L7TUCIYF.js +1 -0
  26. package/public/assets/chunk-SIHDWM2Y.js +154 -0
  27. package/public/assets/chunk-TEJVYIKC.js +1 -0
  28. package/public/assets/code-browser-VB2HGZSG.js +4 -0
  29. package/public/assets/filtered-tasks-ES5CP4KV.js +1 -0
  30. package/public/assets/interactive-browser-7F5ZXRUV.js +7 -0
  31. package/public/assets/join-project-6RTFHNCA.js +1 -0
  32. package/public/assets/main-65EW73SB.js +1 -0
  33. package/public/assets/project-invitations-view-GMPWGWYM.js +1 -0
  34. package/public/assets/settings-UB5Q4AQM.js +9 -0
  35. package/public/assets/styles-18792BB4.css +32 -0
  36. package/public/assets/team-server-wizard-2GLDWS6D.js +8 -0
  37. package/public/assets/terminal-IWAIM4DE.js +62 -0
  38. package/public/assets.json +1 -0
  39. package/public/bundle-meta.json +1 -0
  40. package/public/docs/.nojekyll +0 -0
  41. package/public/docs/accessibility.html +19 -0
  42. package/public/docs/acp-agents.html +117 -0
  43. package/public/docs/agent-containers.html +79 -0
  44. package/public/docs/api.html +1130 -0
  45. package/public/docs/app.js +342 -0
  46. package/public/docs/application-improvements.html +275 -0
  47. package/public/docs/architecture-explorer.html +19 -0
  48. package/public/docs/architecture.html +149 -0
  49. package/public/docs/auto-team.html +148 -0
  50. package/public/docs/automations.html +109 -0
  51. package/public/docs/checkpoint-review.html +37 -0
  52. package/public/docs/code-hosts.html +35 -0
  53. package/public/docs/code-intelligence.html +35 -0
  54. package/public/docs/configuration.html +346 -0
  55. package/public/docs/context-inspection.html +23 -0
  56. package/public/docs/coordinated-changes.html +28 -0
  57. package/public/docs/deferred-runs.html +25 -0
  58. package/public/docs/dependency-discovery.html +19 -0
  59. package/public/docs/documentation.html +84 -0
  60. package/public/docs/dynamic-teams-evaluation.html +174 -0
  61. package/public/docs/dynamic-teams-implementation-plan.html +882 -0
  62. package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
  63. package/public/docs/dynamic-teams-plan-review-0.html +204 -0
  64. package/public/docs/extensions.html +42 -0
  65. package/public/docs/external-sources.html +96 -0
  66. package/public/docs/filesystem-policy.html +26 -0
  67. package/public/docs/filtered-tasks.html +44 -0
  68. package/public/docs/font-license.txt +92 -0
  69. package/public/docs/getting-started.html +143 -0
  70. package/public/docs/github-review-threads.html +26 -0
  71. package/public/docs/github.html +157 -0
  72. package/public/docs/help.html +69 -0
  73. package/public/docs/index.html +88 -0
  74. package/public/docs/installation.html +80 -0
  75. package/public/docs/launch-profiles.html +48 -0
  76. package/public/docs/maintenance.html +60 -0
  77. package/public/docs/memory-claims.html +31 -0
  78. package/public/docs/memory-storage.html +58 -0
  79. package/public/docs/notifications.html +39 -0
  80. package/public/docs/patterns.html +195 -0
  81. package/public/docs/performance-results.html +210 -0
  82. package/public/docs/performance.html +166 -0
  83. package/public/docs/project-conventions.html +22 -0
  84. package/public/docs/project-first-navigation-plan.html +587 -0
  85. package/public/docs/project-teams-implementation-plan.html +772 -0
  86. package/public/docs/project-teams.html +129 -0
  87. package/public/docs/provider-compatibility.html +20 -0
  88. package/public/docs/releasing.html +65 -0
  89. package/public/docs/remote-workers.html +64 -0
  90. package/public/docs/review-inbox.html +26 -0
  91. package/public/docs/run-storage.html +23 -0
  92. package/public/docs/runs.html +141 -0
  93. package/public/docs/search.json +1 -0
  94. package/public/docs/security.html +133 -0
  95. package/public/docs/shared-projects.html +31 -0
  96. package/public/docs/ssh-projects.html +60 -0
  97. package/public/docs/style.css +768 -0
  98. package/public/docs/task-reruns.html +30 -0
  99. package/public/docs/team-collaboration.html +412 -0
  100. package/public/docs/verification.html +22 -0
  101. package/public/docs/vscode.html +53 -0
  102. package/public/docs/worker-permission-verification.html +131 -0
  103. package/public/index.html +17 -0
  104. package/scripts/check-node.mjs +7 -0
  105. package/scripts/codeflow.mjs +55 -0
  106. package/scripts/computer-use.swift +64 -0
  107. package/scripts/pty.py +273 -0
  108. package/src/acp-connections.js +110 -0
  109. package/src/adapters/acp-presets.js +37 -0
  110. package/src/adapters/acp-types.js +0 -0
  111. package/src/adapters/acp.js +328 -0
  112. package/src/adapters/agent-tools.js +28 -0
  113. package/src/adapters/agy-auth.js +67 -0
  114. package/src/adapters/agy-isolation.js +242 -0
  115. package/src/adapters/agy-tools.js +45 -0
  116. package/src/adapters/agy.js +326 -0
  117. package/src/adapters/claude-stream.js +58 -0
  118. package/src/adapters/claude.js +282 -0
  119. package/src/adapters/codex-app-server.js +555 -0
  120. package/src/adapters/codex.js +311 -0
  121. package/src/adapters/errors.js +57 -0
  122. package/src/adapters/index.js +91 -0
  123. package/src/adapters/managed-permissions.js +37 -0
  124. package/src/adapters/model-discovery.js +100 -0
  125. package/src/adapters/process.js +433 -0
  126. package/src/adapters/team-tools.js +103 -0
  127. package/src/adapters/types.js +0 -0
  128. package/src/agent-browser.js +688 -0
  129. package/src/agent-computer.js +88 -0
  130. package/src/agent-container.js +155 -0
  131. package/src/agent-team-definitions.js +131 -0
  132. package/src/agent-tool-client.mjs +79 -0
  133. package/src/agent-tools.js +296 -0
  134. package/src/approvals.js +134 -0
  135. package/src/architecture.js +456 -0
  136. package/src/assets.js +44 -0
  137. package/src/automation.js +901 -0
  138. package/src/browser-files.js +293 -0
  139. package/src/browser-ownership.js +150 -0
  140. package/src/certificate-renewal.js +75 -0
  141. package/src/chat-family.js +25 -0
  142. package/src/chat-handoff.js +87 -0
  143. package/src/checkpoint-review.js +72 -0
  144. package/src/checks.js +303 -0
  145. package/src/code-host-git.js +102 -0
  146. package/src/code-hosts-api.js +99 -0
  147. package/src/code-hosts.js +482 -0
  148. package/src/code-intelligence-worker.mjs +452 -0
  149. package/src/code-intelligence.js +141 -0
  150. package/src/collab/api.js +365 -0
  151. package/src/collab/checkpoints.js +198 -0
  152. package/src/collab/connections.js +139 -0
  153. package/src/collab/host-execution.js +347 -0
  154. package/src/collab/hub-client.js +210 -0
  155. package/src/collab/link-api.js +229 -0
  156. package/src/collab/link.js +595 -0
  157. package/src/collab/model.js +496 -0
  158. package/src/collab/runner.js +1014 -0
  159. package/src/collab/store.js +1452 -0
  160. package/src/collab/terminals.js +361 -0
  161. package/src/collaboration-reviews.js +116 -0
  162. package/src/config.js +48 -0
  163. package/src/context-budget.js +14 -0
  164. package/src/context-inspection.js +204 -0
  165. package/src/context.js +154 -0
  166. package/src/conventions.js +283 -0
  167. package/src/coordinated-api.js +247 -0
  168. package/src/deferred-launch.js +82 -0
  169. package/src/deferred-runs.js +515 -0
  170. package/src/dependency-health.js +35 -0
  171. package/src/diff.js +43 -0
  172. package/src/docs-help.js +316 -0
  173. package/src/doctor.js +232 -0
  174. package/src/documentation-changes.js +20 -0
  175. package/src/engine.js +5258 -0
  176. package/src/env.js +44 -0
  177. package/src/execution-budget.js +8 -0
  178. package/src/experience.js +50 -0
  179. package/src/extensions.js +144 -0
  180. package/src/filesystem-access.js +107 -0
  181. package/src/filesystem-policy.js +238 -0
  182. package/src/filtered-task-limits.js +93 -0
  183. package/src/filtered-tasks-api.js +53 -0
  184. package/src/filtered-tasks.js +576 -0
  185. package/src/frame-policy.js +66 -0
  186. package/src/fs.js +57 -0
  187. package/src/git.js +626 -0
  188. package/src/github-review-threads.js +181 -0
  189. package/src/github.js +538 -0
  190. package/src/http.js +69 -0
  191. package/src/improve-settings.js +38 -0
  192. package/src/improve.js +187 -0
  193. package/src/integrations.js +373 -0
  194. package/src/integrity.js +259 -0
  195. package/src/journey-api.js +77 -0
  196. package/src/journey-replay.js +65 -0
  197. package/src/journey-store.js +97 -0
  198. package/src/journeys.js +60 -0
  199. package/src/json.js +81 -0
  200. package/src/launch-profiles.js +292 -0
  201. package/src/learnings.js +80 -0
  202. package/src/local-app-url.js +19 -0
  203. package/src/log.js +39 -0
  204. package/src/maintenance.js +335 -0
  205. package/src/memory-analysis.js +182 -0
  206. package/src/memory-claims.js +153 -0
  207. package/src/memory-sqlite.js +236 -0
  208. package/src/memory.js +552 -0
  209. package/src/merge-validation.js +58 -0
  210. package/src/model-catalog.js +239 -0
  211. package/src/model-research.js +91 -0
  212. package/src/notes.js +176 -0
  213. package/src/notifications.js +413 -0
  214. package/src/patterns/auto-team-model.js +230 -0
  215. package/src/patterns/auto-team.js +816 -0
  216. package/src/patterns/council.js +199 -0
  217. package/src/patterns/debate.js +188 -0
  218. package/src/patterns/findings.js +141 -0
  219. package/src/patterns/index.js +21 -0
  220. package/src/patterns/loop.js +158 -0
  221. package/src/patterns/pipeline.js +125 -0
  222. package/src/patterns/prompts.js +181 -0
  223. package/src/patterns/race.js +147 -0
  224. package/src/patterns/schemas.js +40 -0
  225. package/src/patterns/solo.js +46 -0
  226. package/src/patterns/supervisor.js +235 -0
  227. package/src/patterns/task-draft.js +29 -0
  228. package/src/patterns/task-review.js +45 -0
  229. package/src/patterns/tdd.js +231 -0
  230. package/src/patterns/team-strategies.js +67 -0
  231. package/src/performance.js +43 -0
  232. package/src/persistence-status.js +35 -0
  233. package/src/phone-sessions.js +96 -0
  234. package/src/preview-overlay.js +392 -0
  235. package/src/preview.js +230 -0
  236. package/src/privacy.js +23 -0
  237. package/src/project-report.js +22 -0
  238. package/src/project-team-budget.js +22 -0
  239. package/src/project-team-model.js +225 -0
  240. package/src/project-team-policy.js +42 -0
  241. package/src/project-team-store.js +234 -0
  242. package/src/project-team-usage.js +24 -0
  243. package/src/project-teams-api.js +43 -0
  244. package/src/project-teams.js +644 -0
  245. package/src/projects.js +290 -0
  246. package/src/provider-eligibility.js +9 -0
  247. package/src/readiness.js +98 -0
  248. package/src/remote-tls.js +66 -0
  249. package/src/remote-workers/bundles.js +198 -0
  250. package/src/remote-workers/client.js +332 -0
  251. package/src/remote-workers/durability.js +37 -0
  252. package/src/remote-workers/protocol.js +68 -0
  253. package/src/remote-workers/stdio.js +138 -0
  254. package/src/remote-workers/worker.js +304 -0
  255. package/src/remote.js +204 -0
  256. package/src/repo-config.js +201 -0
  257. package/src/repo-coordination.js +725 -0
  258. package/src/repo-tree.js +106 -0
  259. package/src/repository-identity.js +13 -0
  260. package/src/retention-policy.js +466 -0
  261. package/src/review-inbox.js +38 -0
  262. package/src/routing.js +49 -0
  263. package/src/run-analysis.js +111 -0
  264. package/src/run-cold-store.js +231 -0
  265. package/src/run-environment.js +15 -0
  266. package/src/run-history.js +91 -0
  267. package/src/run-log.js +160 -0
  268. package/src/run-permissions.js +111 -0
  269. package/src/run-projection.js +77 -0
  270. package/src/run-recovery.js +53 -0
  271. package/src/run-status.js +56 -0
  272. package/src/serve.js +138 -0
  273. package/src/server.js +2871 -0
  274. package/src/shared-access.js +103 -0
  275. package/src/shared-accounts.js +381 -0
  276. package/src/shared-api.js +239 -0
  277. package/src/shell.js +346 -0
  278. package/src/ship.js +344 -0
  279. package/src/slots.js +74 -0
  280. package/src/snapshot.js +388 -0
  281. package/src/source-context.js +17 -0
  282. package/src/sse-writer.js +70 -0
  283. package/src/ssh-projects/bootstrap.mjs +95 -0
  284. package/src/ssh-projects/config.js +45 -0
  285. package/src/ssh-projects/gateway.js +143 -0
  286. package/src/ssh-projects/manager.js +320 -0
  287. package/src/ssh-projects/team-autostart.mjs +93 -0
  288. package/src/ssh-projects/team-remove.mjs +69 -0
  289. package/src/ssh-projects/team-status.mjs +39 -0
  290. package/src/ssh-projects/transport.js +133 -0
  291. package/src/stats.js +139 -0
  292. package/src/store.js +544 -0
  293. package/src/stream.js +91 -0
  294. package/src/sweeps.js +241 -0
  295. package/src/tailscale.js +101 -0
  296. package/src/task-draft.js +23 -0
  297. package/src/task-team.js +164 -0
  298. package/src/team-evaluation.js +243 -0
  299. package/src/team-mailbox.js +160 -0
  300. package/src/team-memory.js +84 -0
  301. package/src/team-rerun-graph.js +15 -0
  302. package/src/team-reruns.js +130 -0
  303. package/src/team-scheduler.js +121 -0
  304. package/src/team-steering.js +21 -0
  305. package/src/team-template-store.js +98 -0
  306. package/src/team-templates.js +117 -0
  307. package/src/team-tool-client.mjs +79 -0
  308. package/src/team-tools.js +235 -0
  309. package/src/teams.js +0 -0
  310. package/src/templates.js +62 -0
  311. package/src/transcript.js +134 -0
  312. package/src/verification-integrity.js +27 -0
  313. package/src/verification.js +183 -0
  314. package/src/watch.js +195 -0
  315. package/src/work-queue.js +47 -0
  316. package/src/workflow-control.js +83 -0
  317. package/src/workspace-api.js +207 -0
  318. package/src/workspace-changes.js +61 -0
  319. package/src/workspace.js +146 -0
@@ -0,0 +1,725 @@
1
+ import { constants } from "node:fs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdir, open, realpath, rename, rm } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { CODEFLOW_HOME } from "./config.js";
6
+ import { sanitizeDeferredRequest } from "./deferred-runs.js";
7
+ import { redactText } from "./privacy.js";
8
+ import { repositoryIdentity } from "./repository-identity.js";
9
+ class CoordinationConflictError extends Error {
10
+ constructor(current2) {
11
+ super("This coordinated change changed. Refresh before acting.");
12
+ this.current = current2;
13
+ }
14
+ status = 409;
15
+ }
16
+ class CoordinationRejectedError extends Error {
17
+ }
18
+ const COORDINATION_LIMITS = { nodes: 10, active: 10, retained: 20, attempts: 3, history: 100, fileBytes: 32 * 1024 * 1024, previewMs: 30 * 6e4 };
19
+ const queues = /* @__PURE__ */ new Map();
20
+ const states = ["pending", "waiting", "claimed", "running", "verified", "failed", "unknown", "blocked", "cancelled", "shipping", "shipping-unknown", "shipped"];
21
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
22
+ const copy = (value) => structuredClone(value);
23
+ const canonical = (value) => JSON.stringify(value, (_key, item) => object(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
24
+ const digest = (value) => createHash("sha256").update(canonical(value)).digest("hex");
25
+ const commitId = (value) => typeof value === "string" && /^[a-f0-9]{40,64}$/i.test(value);
26
+ const date = (value) => Number.isSafeInteger(value) && value >= 0 && value <= 864e13;
27
+ const id = (value) => typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/.test(value);
28
+ const text = (value, maximum, label) => {
29
+ if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\0")) throw new Error(`Invalid coordinated ${label}`);
30
+ return value.trim();
31
+ };
32
+ const diagnostic = (value) => redactText(String(value ?? "Unknown coordinated change error")).replace(/\0/g, "").slice(0, 1e3);
33
+ function onlyKeys(value, allowed, label) {
34
+ if (Object.keys(value).some((key) => !allowed.includes(key))) throw new Error(`Unsupported coordinated ${label} fields`);
35
+ }
36
+ function branch(value) {
37
+ const result2 = text(value, 240, "branch");
38
+ if (result2.startsWith("-") || /[\s~^:?*\[\\]/.test(result2) || result2.includes("..") || result2.includes("@{") || result2.startsWith("/") || result2.endsWith("/") || result2.endsWith(".") || result2.split("/").some((part) => !part || part.endsWith(".lock"))) throw new Error("Choose a valid local Git branch");
39
+ return result2;
40
+ }
41
+ function safeRun(value, project) {
42
+ const run = sanitizeDeferredRequest(value);
43
+ if (run.parentRunId) throw new Error("Coordinated tasks start from pinned repository commits; parent-run inheritance is unavailable");
44
+ run.project = project;
45
+ run.loop = { ...run.loop, copy: [], maxSteps: Math.min(50, Math.floor(run.loop.maxSteps ?? 20)), maxInvocations: Math.min(50, Math.floor(run.loop.maxInvocations ?? run.loop.maxSteps ?? 20)), maxMinutes: Math.min(60, run.loop.maxMinutes ?? 15), maxTokens: Math.min(2e6, run.loop.maxTokens ?? 5e5), maxCostUsd: Math.min(100, run.loop.maxCostUsd ?? 25) };
46
+ return run;
47
+ }
48
+ function checkGraph(nodes) {
49
+ const names = new Set(nodes.map((node) => node.id));
50
+ if (names.size !== nodes.length || nodes.some((node) => !id(node.id) || !Array.isArray(node.dependsOn) || node.dependsOn.length >= nodes.length || new Set(node.dependsOn).size !== node.dependsOn.length || node.dependsOn.some((dependency) => !names.has(dependency) || dependency === node.id))) throw new Error("Choose distinct task IDs and dependencies from this coordinated change");
51
+ const visiting = /* @__PURE__ */ new Set(), visited = /* @__PURE__ */ new Set();
52
+ const visit = (name) => {
53
+ if (visiting.has(name)) throw new Error("Coordinated task dependencies contain a cycle");
54
+ if (visited.has(name)) return;
55
+ visiting.add(name);
56
+ nodes.find((node) => node.id === name).dependsOn.forEach(visit);
57
+ visiting.delete(name);
58
+ visited.add(name);
59
+ };
60
+ nodes.forEach((node) => visit(node.id));
61
+ }
62
+ function result(value, nodeId) {
63
+ if (!object(value) || value.nodeId !== nodeId || !id(value.runId) || typeof value.repo !== "string" || !isAbsolute(value.repo) || !commitId(value.commit) || !date(value.verifiedAt)) throw new Error("Invalid coordinated verified result");
64
+ onlyKeys(value, ["nodeId", "runId", "repo", "branch", "commit", "verificationId", "verifiedAt", "status"], "verified result");
65
+ return { nodeId, runId: value.runId, repo: value.repo, branch: branch(value.branch), commit: value.commit, verificationId: text(value.verificationId, 240, "verification identity"), verifiedAt: value.verifiedAt };
66
+ }
67
+ function validation(value) {
68
+ if (!object(value) || !commitId(value.source) || !commitId(value.target) || !commitId(value.tree) || !date(value.verifiedAt)) throw new Error("Shipping requires exact verified source, target and tree commits");
69
+ onlyKeys(value, ["source", "target", "tree", "into", "verifiedAt", "verificationId"], "shipping validation");
70
+ return { source: value.source, target: value.target, tree: value.tree, into: branch(value.into), verifiedAt: value.verifiedAt, verificationId: text(value.verificationId, 240, "shipping verification identity") };
71
+ }
72
+ function refresh(job) {
73
+ const nodes = job.nodes;
74
+ job.status = job.cancelRequested ? "cancelled" : nodes.every((node) => node.status === "shipped") ? "shipped" : nodes.some((node) => ["shipped", "shipping", "shipping-unknown"].includes(node.status)) ? "partial-shipped" : nodes.every((node) => node.status === "verified") ? "ready" : nodes.some((node) => ["failed", "unknown", "blocked", "cancelled"].includes(node.status)) ? "blocked" : "running";
75
+ }
76
+ function invalidatePreview(job) {
77
+ if (!job.nodes.some((node) => ["shipping", "shipping-unknown"].includes(node.status))) delete job.shipping;
78
+ }
79
+ function history(job, now, detail, node, fields = {}) {
80
+ job.updatedAt = now;
81
+ job.history.push({ id: randomUUID(), at: now, detail: diagnostic(detail), ...node ? { nodeId: node.id } : {}, ...fields });
82
+ job.history = job.history.slice(-COORDINATION_LIMITS.history);
83
+ refresh(job);
84
+ }
85
+ function current(document, name, revision) {
86
+ const job = document.jobs.find((item) => item.id === name);
87
+ if (!job) throw new Error("Coordinated change not found");
88
+ if (!Number.isSafeInteger(revision) || job.revision !== revision) throw new CoordinationConflictError(copy(job));
89
+ return job;
90
+ }
91
+ function getNode(job, name) {
92
+ const node = job.nodes.find((item) => item.id === name);
93
+ if (!node) throw new Error("Coordinated task not found");
94
+ return node;
95
+ }
96
+ const sameResult = (left, right) => left.nodeId === right.nodeId && left.runId === right.runId && left.repo === right.repo && left.branch === right.branch && left.commit === right.commit && left.verificationId === right.verificationId;
97
+ class CoordinatedChanges {
98
+ constructor(options) {
99
+ this.options = options;
100
+ this.file = resolve(options.file ?? join(CODEFLOW_HOME, "coordinated-changes.json"));
101
+ this.now = options.now ?? Date.now;
102
+ }
103
+ file;
104
+ now;
105
+ loaded = false;
106
+ stopped = false;
107
+ timer;
108
+ ticking;
109
+ controllers = /* @__PURE__ */ new Map();
110
+ async load() {
111
+ if (this.loaded) return;
112
+ await this.change((document) => {
113
+ for (const job of document.jobs) for (const node of job.nodes) {
114
+ if (node.status === "claimed" || node.status === "shipping") {
115
+ node.status = node.status === "claimed" ? "unknown" : "shipping-unknown";
116
+ node.reason = "The application stopped after claiming an operation. Inspect its exact receipt before recovery.";
117
+ history(job, this.now(), node.reason, node);
118
+ }
119
+ }
120
+ });
121
+ this.loaded = true;
122
+ }
123
+ async list() {
124
+ await queues.get(this.file);
125
+ return copy((await readDocument(this.file)).jobs).sort((a, b) => b.createdAt - a.createdAt || a.id.localeCompare(b.id));
126
+ }
127
+ async create(raw) {
128
+ const project = text(raw.project, 240, "project"), name = text(raw.name, 120, "name");
129
+ if (!Array.isArray(raw.nodes) || raw.nodes.length < 2 || raw.nodes.length > COORDINATION_LIMITS.nodes) throw new Error("Coordinate 2\u201310 repositories");
130
+ const nodes = raw.nodes.map((node) => {
131
+ if (!object(node) || !Array.isArray(node.dependsOn) || Object.keys(node).some((key) => !["id", "run", "dependsOn"].includes(key))) throw new Error("Unsupported coordinated task fields");
132
+ return { id: node.id, dependsOn: [...node.dependsOn], run: safeRun(node.run, project) };
133
+ });
134
+ checkGraph(nodes);
135
+ const operationId = raw.operationId === void 0 ? void 0 : text(raw.operationId, 240, "operation identity"), operationHash = digest({ project, name, nodes });
136
+ const previous = operationId ? (await this.list()).find((job) => job.operationId === operationId) : void 0;
137
+ if (previous) {
138
+ if (previous.operationHash !== operationHash) throw new Error("This coordinated operation ID already names different content");
139
+ return previous;
140
+ }
141
+ const pins = await Promise.all(nodes.map(async (node) => {
142
+ const pinned = await this.options.pin(copy(node.run));
143
+ if (!commitId(pinned.commit) || !isAbsolute(pinned.repo) || await realpath(pinned.repo) !== pinned.repo) throw new Error("Pin a canonical authorized repository and exact commit");
144
+ return { ...pinned, repositoryId: await repositoryIdentity(pinned.repo) };
145
+ }));
146
+ if (new Set(pins.map((pin) => pin.repo)).size !== pins.length || new Set(pins.map((pin) => pin.repositoryId)).size !== pins.length) throw new Error("Each coordinated task must own a distinct repository, with independent Git branch storage");
147
+ return this.change((document) => {
148
+ const previous2 = operationId ? document.jobs.find((job2) => job2.operationId === operationId) : void 0;
149
+ if (previous2) {
150
+ if (previous2.operationHash !== operationHash) throw new Error("This coordinated operation ID already names different content");
151
+ return copy(previous2);
152
+ }
153
+ if (document.jobs.filter((job2) => !["cancelled", "shipped"].includes(job2.status)).length >= COORDINATION_LIMITS.active) throw new Error("Resolve or cancel older coordinated changes before creating another");
154
+ const now = this.now(), jobId = `c-${randomUUID()}`;
155
+ const job = { id: jobId, revision: 1, project, name, createdAt: now, updatedAt: now, status: "running", cancelRequested: false, nodes: nodes.map((node, index) => ({ jobId, id: node.id, run: { ...node.run, repo: pins[index].repo, baseRef: pins[index].commit }, dependsOn: [...node.dependsOn].sort(), baseCommit: pins[index].commit, repositoryId: pins[index].repositoryId, status: "pending", attempts: 0 })), history: [], ...operationId ? { operationId, operationHash } : {} };
156
+ history(job, now, "Explicitly created with pinned repository bases and bounded safe tasks");
157
+ document.jobs.push(job);
158
+ while (document.jobs.length > COORDINATION_LIMITS.retained) {
159
+ const index = document.jobs.findIndex((item) => item.id !== job.id && ["cancelled", "shipped"].includes(item.status));
160
+ if (index < 0) throw new Error("Coordinated history is full");
161
+ document.jobs.splice(index, 1);
162
+ }
163
+ return copy(job);
164
+ });
165
+ }
166
+ async cancel(name, revision) {
167
+ const job = await this.change((document) => {
168
+ const job2 = current(document, name, revision);
169
+ job2.cancelRequested = true;
170
+ for (const node of job2.nodes) {
171
+ node.cancelRequested = true;
172
+ if (["pending", "waiting", "blocked", "failed"].includes(node.status)) node.status = "cancelled";
173
+ }
174
+ history(job2, this.now(), "Cancellation requested for all unshipped tasks");
175
+ return copy(job2);
176
+ });
177
+ for (const [key, controller] of this.controllers) if (key.startsWith(job.id + ":")) controller.abort();
178
+ for (const node of job.nodes.filter((node2) => node2.runId && ["running", "verified", "unknown"].includes(node2.status))) {
179
+ try {
180
+ if (!this.options.cancelRun) throw new Error("Stop the known run separately; cancellation is not configured");
181
+ await this.options.cancelRun(node.runId);
182
+ await this.updateNode(job.id, node.id, node.claimId, (latest, item) => {
183
+ item.status = "cancelled";
184
+ history(latest, this.now(), "Sent cancellation to this exact owned run", item, { runId: node.runId });
185
+ });
186
+ } catch (error) {
187
+ await this.updateNode(job.id, node.id, node.claimId, (latest, item) => {
188
+ item.reason = diagnostic(error);
189
+ history(latest, this.now(), "Run cancellation could not be confirmed: " + item.reason, item, { runId: node.runId });
190
+ });
191
+ }
192
+ }
193
+ return (await this.list()).find((item) => item.id === name);
194
+ }
195
+ async retryNode(name, nodeId, revision, options = {}) {
196
+ return this.change((document) => {
197
+ const job = current(document, name, revision), node = getNode(job, nodeId);
198
+ if (!["failed", "unknown", "blocked", "cancelled"].includes(node.status)) throw new Error("Retry an explicitly failed, unknown, blocked or cancelled task");
199
+ if (node.status === "unknown" && options.acknowledgeUnknown !== true) throw new Error("Inspect existing runs and acknowledge possible duplicate work before retrying an unknown claim");
200
+ if (node.attempts >= COORDINATION_LIMITS.attempts) throw new Error("This task reached its three-attempt limit");
201
+ const closure = /* @__PURE__ */ new Set([nodeId]);
202
+ for (let changed = true; changed; ) {
203
+ changed = false;
204
+ for (const item of job.nodes) if (!closure.has(item.id) && item.dependsOn.some((dependency) => closure.has(dependency))) {
205
+ closure.add(item.id);
206
+ changed = true;
207
+ }
208
+ }
209
+ if (job.nodes.some((item) => ["shipping", "shipping-unknown"].includes(item.status))) throw new Error("Resolve every claimed or unknown shipping operation before retrying tasks");
210
+ if (job.nodes.some((item) => closure.has(item.id) && item.status === "unknown") && options.acknowledgeUnknown !== true) throw new Error("Inspect every unknown dependent launch and acknowledge possible duplicate work before invalidating it");
211
+ if (job.nodes.some((item) => closure.has(item.id) && item.attempts >= COORDINATION_LIMITS.attempts)) throw new Error("A dependent task reached its three-attempt limit");
212
+ if (job.nodes.some((item) => closure.has(item.id) && ["claimed", "running", "shipping", "shipping-unknown", "shipped"].includes(item.status))) throw new Error("Stop active dependent work and resolve shipping before invalidating its inputs");
213
+ job.cancelRequested = false;
214
+ delete job.shipping;
215
+ for (const item of job.nodes.filter((item2) => closure.has(item2.id))) {
216
+ history(job, this.now(), "Explicit retry invalidated this task's prior result and downstream evidence", item, { ...item.runId ? { runId: item.runId } : {}, ...item.claimId ? { claimId: item.claimId } : {} });
217
+ for (const key of ["claimId", "claimedAt", "runId", "dependencies", "result", "reason", "nextCheckAt", "cancelRequested"]) delete item[key];
218
+ item.status = "pending";
219
+ }
220
+ refresh(job);
221
+ return copy(job);
222
+ });
223
+ }
224
+ /** The callback checks exact persisted Run.coordination identity before an unknown claim is linked. */
225
+ async attach(name, nodeId, revision, runId) {
226
+ if (!id(runId)) throw new Error("Choose a valid run ID");
227
+ const snapshot = (await this.list()).find((job) => job.id === name);
228
+ if (!snapshot) throw new Error("Coordinated change not found");
229
+ if (snapshot.revision !== revision) throw new CoordinationConflictError(snapshot);
230
+ const node = getNode(snapshot, nodeId);
231
+ if (node.status !== "unknown" || !node.claimId) throw new Error("Only an unknown exact claim can be recovered");
232
+ const probe = { ...node, runId }, observed = await this.withOperation(`${name}:${nodeId}:attach`, (signal) => this.observe(probe, signal));
233
+ if (observed.status === "missing") throw new Error("This run does not belong to the exact owned coordinated launch claim");
234
+ if (snapshot.cancelRequested || node.cancelRequested) {
235
+ if (!this.options.cancelRun) throw new Error("The recovered run must be stopped before resolving requested cancellation");
236
+ await this.options.cancelRun(runId);
237
+ }
238
+ return this.change((document) => {
239
+ const job = current(document, name, revision), latest = getNode(job, nodeId);
240
+ if (latest.claimId !== node.claimId || latest.status !== "unknown") throw new Error("The reviewed launch claim changed");
241
+ latest.runId = runId;
242
+ latest.status = job.cancelRequested || latest.cancelRequested ? "cancelled" : "running";
243
+ history(job, this.now(), "Linked a run matching the exact persisted launch claim", latest, { runId, claimId: latest.claimId });
244
+ return copy(job);
245
+ });
246
+ }
247
+ async previewShipping(name, revision, targets) {
248
+ const job = (await this.list()).find((item) => item.id === name);
249
+ if (!job) throw new Error("Coordinated change not found");
250
+ if (job.revision !== revision) throw new CoordinationConflictError(job);
251
+ if (job.cancelRequested || job.nodes.some((node) => !["verified", "shipped"].includes(node.status))) throw new Error("Every task needs a current verified immutable result before shipping review");
252
+ if (!object(targets) || Object.keys(targets).length !== job.nodes.filter((node) => node.status !== "shipped").length || Object.keys(targets).some((nodeId) => !job.nodes.some((node) => node.id === nodeId && node.status !== "shipped"))) throw new Error("Choose a local target branch for each unshipped repository");
253
+ await this.assertResults(job);
254
+ const entries = [];
255
+ for (const node of job.nodes.filter((node2) => node2.status !== "shipped")) {
256
+ const into = branch(targets[node.id]);
257
+ const checked = validation(await this.withOperation(`${name}:${node.id}:preview`, (signal) => this.options.previewShipping(copy(node), into, signal)));
258
+ if (checked.source !== node.result.commit || checked.into !== into) throw new Error("Shipping validation did not verify the pinned result and selected target");
259
+ entries.push({ nodeId: node.id, validation: checked });
260
+ }
261
+ await this.assertResults(job);
262
+ return this.change((document) => {
263
+ const currentJob = current(document, name, revision), now = this.now();
264
+ currentJob.shipping = { id: randomUUID(), createdAt: now, expiresAt: now + COORDINATION_LIMITS.previewMs, entries };
265
+ history(currentJob, now, "Reviewed exact source/target/tree shipping matrix; each repository still needs an explicit action");
266
+ return copy(currentJob);
267
+ });
268
+ }
269
+ async shipNode(name, nodeId, revision, previewId) {
270
+ const job = (await this.list()).find((item) => item.id === name);
271
+ if (!job) throw new Error("Coordinated change not found");
272
+ if (job.revision !== revision) throw new CoordinationConflictError(job);
273
+ const node = getNode(job, nodeId), entry = job.shipping?.entries.find((entry2) => entry2.nodeId === nodeId);
274
+ if (job.cancelRequested || !entry || job.shipping?.id !== previewId || job.shipping.expiresAt <= this.now() || node.status !== "verified") throw new Error("Review a fresh shipping matrix and select an unshipped verified task");
275
+ await this.assertResults(job);
276
+ const checked = validation(await this.withOperation(`${name}:${nodeId}:preship`, (signal) => this.options.previewShipping(copy(node), entry.validation.into, signal)));
277
+ if (["source", "target", "tree", "into"].some((key) => checked[key] !== entry.validation[key])) throw new Error("The reviewed source, target or merge tree changed; no shipping was attempted");
278
+ const claimed = await this.change((document) => {
279
+ const currentJob = current(document, name, revision), latest = getNode(currentJob, nodeId);
280
+ if (currentJob.shipping?.id !== previewId || currentJob.shipping.expiresAt <= this.now() || latest.status !== "verified") throw new Error("The reviewed shipping operation changed or expired");
281
+ latest.status = "shipping";
282
+ latest.shippingClaimId = randomUUID();
283
+ history(currentJob, this.now(), "Claimed this explicit repository shipping operation durably", latest, { claimId: latest.shippingClaimId });
284
+ return copy(latest);
285
+ });
286
+ try {
287
+ const shipped = await this.withOperation(`${name}:${nodeId}:ship`, (signal) => this.options.ship(claimed, copy(entry.validation), signal), this.options.launchTimeoutMs ?? 12e4);
288
+ if (!commitId(shipped.commit)) throw new Error("Shipping returned an invalid commit receipt");
289
+ await this.recordShipping(name, nodeId, claimed.shippingClaimId, shipped.commit, shipped.detail);
290
+ } catch (error) {
291
+ await this.updateNode(name, nodeId, claimed.claimId, (currentJob, latest) => {
292
+ if (latest.shippingClaimId === claimed.shippingClaimId && latest.status === "shipping") {
293
+ latest.status = "shipping-unknown";
294
+ latest.reason = diagnostic(error);
295
+ history(currentJob, this.now(), "Shipping outcome is unknown; inspect the target before manual recovery: " + latest.reason, latest, { claimId: latest.shippingClaimId });
296
+ }
297
+ });
298
+ }
299
+ return (await this.list()).find((item) => item.id === name);
300
+ }
301
+ async recoverShipping(name, nodeId, revision, commit) {
302
+ const job = (await this.list()).find((item) => item.id === name);
303
+ if (!job) throw new Error("Coordinated change not found");
304
+ if (job.revision !== revision) throw new CoordinationConflictError(job);
305
+ const node = getNode(job, nodeId), entry = job.shipping?.entries.find((entry2) => entry2.nodeId === nodeId);
306
+ if (node.status !== "shipping-unknown" || !entry || !commitId(commit) || !this.options.confirmShipping) throw new Error("An unknown shipping operation needs a verifiable exact commit receipt");
307
+ if (!await this.withOperation(`${name}:${nodeId}:recover-ship`, (signal) => this.options.confirmShipping(copy(node), copy(entry.validation), commit, signal))) throw new Error("That commit does not prove the exact reviewed source/target/tree shipping operation");
308
+ return this.change((document) => {
309
+ const latest = current(document, name, revision), item = getNode(latest, nodeId);
310
+ if (item.status !== "shipping-unknown" || item.shippingClaimId !== node.shippingClaimId) throw new Error("Shipping recovery claim changed");
311
+ item.status = "shipped";
312
+ item.shippedCommit = commit;
313
+ history(latest, this.now(), "Recovered this exact shipping commit after manual review", item, { commit });
314
+ return copy(latest);
315
+ });
316
+ }
317
+ start(intervalMs = 5e3) {
318
+ if (!this.loaded) throw new Error("Load coordinated changes before dispatch");
319
+ if (this.timer) return;
320
+ if (!Number.isInteger(intervalMs) || intervalMs < 100 || intervalMs > 6e4) throw new Error("Invalid coordination interval");
321
+ this.stopped = false;
322
+ this.timer = setInterval(() => {
323
+ void this.tick().catch(() => void 0);
324
+ }, intervalMs);
325
+ this.timer.unref?.();
326
+ }
327
+ async stop() {
328
+ this.stopped = true;
329
+ if (this.timer) clearInterval(this.timer);
330
+ this.timer = void 0;
331
+ for (const controller of this.controllers.values()) controller.abort();
332
+ await this.ticking;
333
+ await queues.get(this.file);
334
+ }
335
+ tick() {
336
+ if (!this.loaded) return Promise.reject(new Error("Load coordinated changes before dispatch"));
337
+ if (this.stopped) return Promise.resolve();
338
+ if (this.ticking) return this.ticking;
339
+ const work = this.tickOnce().finally(() => {
340
+ if (this.ticking === work) this.ticking = void 0;
341
+ });
342
+ this.ticking = work;
343
+ return work;
344
+ }
345
+ async tickOnce() {
346
+ for (const jobSnapshot of (await this.list()).reverse()) {
347
+ if (jobSnapshot.cancelRequested || jobSnapshot.status === "shipped") continue;
348
+ for (const node of jobSnapshot.nodes.filter((node2) => ["running", "verified"].includes(node2.status))) {
349
+ let observed;
350
+ try {
351
+ observed = await this.withOperation(`${jobSnapshot.id}:${node.id}:observe`, (signal) => this.observe(copy(node), signal));
352
+ } catch {
353
+ continue;
354
+ }
355
+ await this.updateNode(jobSnapshot.id, node.id, node.claimId, (job2, latest) => {
356
+ if (job2.cancelRequested || !["running", "verified"].includes(latest.status)) return;
357
+ if (observed.status === "verified") {
358
+ const checked = result({ ...observed, nodeId: node.id }, node.id);
359
+ if (checked.runId !== latest.runId || checked.repo !== latest.run.repo) throw new Error("Verified result does not match its claimed run and repository");
360
+ if (latest.result && !sameResult(latest.result, checked)) {
361
+ latest.status = "blocked";
362
+ latest.reason = "The result branch or verification evidence changed after it was pinned";
363
+ invalidatePreview(job2);
364
+ history(job2, this.now(), latest.reason, latest);
365
+ } else if (latest.status !== "verified") {
366
+ latest.result = checked;
367
+ latest.status = "verified";
368
+ delete latest.reason;
369
+ history(job2, this.now(), "Pinned the current immutable result and genuine verification receipt", latest, { runId: latest.runId, commit: checked.commit });
370
+ }
371
+ } else if (observed.status !== "running" || latest.status === "verified") {
372
+ latest.status = observed.status === "cancelled" ? "cancelled" : observed.status === "failed" ? "failed" : "blocked";
373
+ latest.reason = diagnostic(observed.reason ?? "The result or verification evidence is unavailable or no longer current");
374
+ invalidatePreview(job2);
375
+ history(job2, this.now(), latest.reason, latest);
376
+ }
377
+ });
378
+ }
379
+ const job = (await this.list()).find((item) => item.id === jobSnapshot.id);
380
+ if (job.cancelRequested) continue;
381
+ for (const node of job.nodes.filter((item) => ["pending", "waiting"].includes(item.status))) {
382
+ if ((node.nextCheckAt ?? 0) > this.now()) continue;
383
+ try {
384
+ if (await repositoryIdentity(node.run.repo) !== node.repositoryId) throw new Error("The pinned repository metadata was replaced");
385
+ } catch (error) {
386
+ await this.updateNode(job.id, node.id, void 0, (latest, item) => {
387
+ if (!latest.cancelRequested && ["pending", "waiting"].includes(item.status)) {
388
+ item.status = "blocked";
389
+ item.reason = diagnostic(error);
390
+ history(latest, this.now(), item.reason, item);
391
+ }
392
+ });
393
+ continue;
394
+ }
395
+ const dependencies = node.dependsOn.map((name) => getNode(job, name));
396
+ if (dependencies.some((item) => !["verified", "shipped"].includes(item.status) || !item.result)) {
397
+ await this.wait(job.id, node.id, "Waiting for every dependency's current verified result");
398
+ continue;
399
+ }
400
+ try {
401
+ await this.assertResults(job, dependencies);
402
+ } catch (error) {
403
+ await this.wait(job.id, node.id, diagnostic(error));
404
+ continue;
405
+ }
406
+ const ready = this.options.ready ? await this.withOperation(`${job.id}:${node.id}:ready`, (signal) => this.options.ready(copy(node.run), signal)).catch((error) => ({ ready: false, reason: diagnostic(error), retryAt: this.now() + 6e4 })) : { ready: true };
407
+ if (!ready.ready) {
408
+ await this.wait(job.id, node.id, diagnostic(ready.reason ?? "Waiting for provider readiness"), ready.retryAt);
409
+ continue;
410
+ }
411
+ if (this.stopped) return;
412
+ const claimed = await this.change((document) => {
413
+ const currentJob = document.jobs.find((item2) => item2.id === job.id);
414
+ if (!currentJob || currentJob.cancelRequested) return;
415
+ const item = getNode(currentJob, node.id);
416
+ if (!["pending", "waiting"].includes(item.status) || item.attempts >= COORDINATION_LIMITS.attempts) return;
417
+ const inputs = item.dependsOn.map((name) => getNode(currentJob, name));
418
+ if (inputs.some((dependency, index) => !["verified", "shipped"].includes(dependency.status) || !dependency.result || !sameResult(dependency.result, dependencies[index].result))) return;
419
+ item.status = "claimed";
420
+ item.claimId = randomUUID();
421
+ item.claimedAt = this.now();
422
+ item.attempts++;
423
+ item.dependencies = inputs.map((dependency) => copy(dependency.result));
424
+ delete item.reason;
425
+ delete item.nextCheckAt;
426
+ history(currentJob, this.now(), "Durably claimed one bounded safe repository task", item, { claimId: item.claimId });
427
+ return copy(item);
428
+ });
429
+ if (!claimed) continue;
430
+ const controller = new AbortController(), key = `${job.id}:${node.id}:launch`;
431
+ this.controllers.set(key, controller);
432
+ const claim = { jobId: job.id, nodeId: node.id, claimId: claimed.claimId, attempt: claimed.attempts, dependencies: copy(claimed.dependencies) };
433
+ const launch = Promise.resolve().then(() => this.options.launch(copy(claimed.run), claim, controller.signal));
434
+ void launch.then((receipt) => this.recordReceipt(job.id, node.id, claimed.claimId, receipt.runId)).catch(() => void 0);
435
+ try {
436
+ const receipt = await bounded(launch, this.options.launchTimeoutMs ?? 6e4, controller);
437
+ await this.recordReceipt(job.id, node.id, claimed.claimId, receipt.runId);
438
+ } catch (error) {
439
+ await this.updateNode(job.id, node.id, claimed.claimId, (latest, item) => {
440
+ if (item.status === "claimed") {
441
+ item.status = error instanceof CoordinationRejectedError ? "failed" : "unknown";
442
+ item.reason = diagnostic(error);
443
+ history(latest, this.now(), item.reason, item, { claimId: item.claimId });
444
+ }
445
+ });
446
+ } finally {
447
+ this.controllers.delete(key);
448
+ }
449
+ return;
450
+ }
451
+ }
452
+ }
453
+ async assertResults(job, selected = job.nodes) {
454
+ const closure = new Set(selected.map((node) => node.id));
455
+ for (let changed = true; changed; ) {
456
+ changed = false;
457
+ for (const node of job.nodes.filter((node2) => closure.has(node2.id))) for (const dependency of node.dependsOn) if (!closure.has(dependency)) {
458
+ closure.add(dependency);
459
+ changed = true;
460
+ }
461
+ }
462
+ for (const node of job.nodes.filter((node2) => closure.has(node2.id))) {
463
+ if (!node.result || !["verified", "shipped"].includes(node.status)) throw new Error("A dependency or result is not currently verified");
464
+ const observation = await this.withOperation(`${job.id}:${node.id}:verify-pin`, (signal) => this.observe(copy(node), signal));
465
+ if (observation.status !== "verified" || !sameResult(node.result, result({ ...observation, nodeId: node.id }, node.id))) throw new Error("A dependency result or its genuine verification receipt changed; review it before continuing");
466
+ for (const input of node.dependencies ?? []) {
467
+ const parent = getNode(job, input.nodeId);
468
+ if (!parent.result || !sameResult(input, parent.result) || !["verified", "shipped"].includes(parent.status)) throw new Error("Downstream evidence refers to an invalidated dependency result");
469
+ }
470
+ }
471
+ }
472
+ async observe(node, signal) {
473
+ try {
474
+ if (await repositoryIdentity(node.run.repo) !== node.repositoryId) return { status: "missing", reason: "The pinned repository metadata was replaced" };
475
+ } catch (error) {
476
+ return { status: "missing", reason: diagnostic(error) };
477
+ }
478
+ signal.throwIfAborted();
479
+ return this.options.validateResult(node, signal);
480
+ }
481
+ async wait(jobId, nodeId, reason, retryAt) {
482
+ await this.updateNode(jobId, nodeId, void 0, (job, node) => {
483
+ if (job.cancelRequested || !["pending", "waiting"].includes(node.status)) return;
484
+ const next = date(retryAt) ? Math.max(this.now() + 100, Math.min(this.now() + 24 * 60 * 6e4, retryAt)) : this.now() + 1e3;
485
+ if (node.status === "waiting" && node.reason === reason) {
486
+ node.nextCheckAt = next;
487
+ return;
488
+ }
489
+ node.status = "waiting";
490
+ node.reason = reason;
491
+ node.nextCheckAt = next;
492
+ history(job, this.now(), reason, node);
493
+ });
494
+ }
495
+ async recordReceipt(jobId, nodeId, claimId, runId) {
496
+ if (!id(runId)) throw new Error("Invalid coordinated run receipt");
497
+ let cancel = false;
498
+ await this.change((document) => {
499
+ const job = document.jobs.find((item) => item.id === jobId);
500
+ if (!job) return;
501
+ const node = getNode(job, nodeId);
502
+ if (node.claimId !== claimId) {
503
+ if (!job.history.some((entry) => entry.claimId === claimId && entry.runId === runId)) history(job, this.now(), "Late receipt for an earlier explicitly retried claim; stopping duplicate work", node, { claimId, runId });
504
+ cancel = true;
505
+ return;
506
+ }
507
+ if (node.runId === runId && !["claimed", "unknown"].includes(node.status)) return;
508
+ if (!["claimed", "unknown"].includes(node.status)) throw new Error("The current launch no longer accepts a receipt");
509
+ node.runId = runId;
510
+ node.status = "running";
511
+ delete node.reason;
512
+ cancel = job.cancelRequested || node.cancelRequested === true;
513
+ history(job, this.now(), "Recorded the durable matching run receipt", node, { claimId, runId });
514
+ });
515
+ if (cancel) {
516
+ try {
517
+ if (!this.options.cancelRun) throw new Error("Run cancellation is not configured; stop this exact run separately");
518
+ await this.options.cancelRun(runId);
519
+ await this.updateNode(jobId, nodeId, claimId, (job, node) => {
520
+ if (node.runId === runId) {
521
+ node.status = "cancelled";
522
+ history(job, this.now(), "Cancelled the late matching run after requested cancellation", node, { claimId, runId });
523
+ }
524
+ });
525
+ } catch (error) {
526
+ await this.updateNode(jobId, nodeId, claimId, (job, node) => {
527
+ if (node.runId === runId) {
528
+ node.reason = diagnostic(error);
529
+ history(job, this.now(), "Late run cancellation could not be confirmed: " + node.reason, node, { claimId, runId });
530
+ }
531
+ });
532
+ }
533
+ }
534
+ }
535
+ async recordShipping(jobId, nodeId, claim, commit, detail) {
536
+ await this.change((document) => {
537
+ const job = document.jobs.find((item) => item.id === jobId);
538
+ if (!job) return;
539
+ const node = getNode(job, nodeId);
540
+ if (node.shippingClaimId !== claim || !["shipping", "shipping-unknown"].includes(node.status)) return;
541
+ node.status = "shipped";
542
+ node.shippedCommit = commit;
543
+ delete node.reason;
544
+ history(job, this.now(), detail ?? "Shipped this repository's exact reviewed merge; no push was performed", node, { claimId: claim, commit });
545
+ });
546
+ }
547
+ updateNode(jobId, nodeId, claimId, mutate) {
548
+ return this.change((document) => {
549
+ const job = document.jobs.find((item) => item.id === jobId);
550
+ if (!job) return;
551
+ const node = getNode(job, nodeId);
552
+ if (claimId && node.claimId !== claimId) return;
553
+ mutate(job, node);
554
+ });
555
+ }
556
+ async withOperation(key, operation, timeout = this.options.operationTimeoutMs ?? 12e4) {
557
+ const controller = new AbortController(), uniqueKey = `${key}:${randomUUID()}`;
558
+ this.controllers.set(uniqueKey, controller);
559
+ try {
560
+ return await bounded(Promise.resolve().then(() => operation(controller.signal)), timeout, controller);
561
+ } finally {
562
+ this.controllers.delete(uniqueKey);
563
+ }
564
+ }
565
+ change(mutate) {
566
+ const work = (queues.get(this.file) ?? Promise.resolve()).then(async () => {
567
+ const document = await readDocument(this.file), before = canonical(document), previous = JSON.parse(before), result2 = mutate(document);
568
+ for (const job of document.jobs) {
569
+ refresh(job);
570
+ const prior = previous.jobs.find((item) => item.id === job.id);
571
+ if (prior && canonical(prior) !== canonical(job)) job.revision = prior.revision + 1;
572
+ }
573
+ if (canonical(document) !== before) await writeDocument(this.file, document);
574
+ return object(result2) && Array.isArray(result2.nodes) && typeof result2.id === "string" ? copy(document.jobs.find((job) => job.id === result2.id)) : result2;
575
+ });
576
+ const tail = work.then(() => void 0, () => void 0);
577
+ queues.set(this.file, tail);
578
+ void tail.then(() => {
579
+ if (queues.get(this.file) === tail) queues.delete(this.file);
580
+ });
581
+ return work;
582
+ }
583
+ }
584
+ function bounded(promise, timeout, controller) {
585
+ return new Promise((resolve2, reject) => {
586
+ let finished = false;
587
+ const finish = (value, failure) => {
588
+ if (finished) return;
589
+ finished = true;
590
+ clearTimeout(timer);
591
+ controller.signal.removeEventListener("abort", abort);
592
+ failure ? reject(value) : resolve2(value);
593
+ };
594
+ const abort = () => finish(new Error("Coordinated operation was cancelled; inspect any claimed side effects"), true), timer = setTimeout(() => {
595
+ finish(new Error("Coordinated operation timed out; inspect any claimed side effects"), true);
596
+ controller.abort();
597
+ }, timeout);
598
+ controller.signal.addEventListener("abort", abort, { once: true });
599
+ if (controller.signal.aborted) abort();
600
+ promise.then((value) => finish(value, false), (error) => finish(error, true));
601
+ });
602
+ }
603
+ function validateCoordinationDocument(raw) {
604
+ if (!object(raw) || raw.version !== 1 || !Array.isArray(raw.jobs) || raw.jobs.length > COORDINATION_LIMITS.retained) throw new Error("Invalid coordinated change storage");
605
+ onlyKeys(raw, ["version", "jobs"], "storage");
606
+ const seen = /* @__PURE__ */ new Set();
607
+ for (const job of raw.jobs) {
608
+ if (!object(job) || !id(job.id) || seen.has(job.id) || !Number.isSafeInteger(job.revision) || job.revision < 1 || !date(job.createdAt) || !date(job.updatedAt) || typeof job.cancelRequested !== "boolean" || !["running", "blocked", "ready", "partial-shipped", "shipped", "cancelled"].includes(job.status) || !Array.isArray(job.nodes) || job.nodes.length < 2 || job.nodes.length > COORDINATION_LIMITS.nodes || !Array.isArray(job.history) || job.history.length > COORDINATION_LIMITS.history) throw new Error("Invalid coordinated change record");
609
+ onlyKeys(job, ["id", "revision", "project", "name", "createdAt", "updatedAt", "status", "cancelRequested", "nodes", "shipping", "operationId", "operationHash", "history"], "saved job");
610
+ seen.add(job.id);
611
+ text(job.project, 240, "saved project");
612
+ text(job.name, 120, "saved name");
613
+ checkGraph(job.nodes);
614
+ const repos = /* @__PURE__ */ new Set(), repositoryIds = /* @__PURE__ */ new Set();
615
+ for (const node of job.nodes) {
616
+ if (!object(node) || node.jobId !== job.id || !states.includes(node.status) || !Number.isInteger(node.attempts) || node.attempts < 0 || node.attempts > COORDINATION_LIMITS.attempts || !commitId(node.baseCommit) || !/^[a-f0-9]{64}$/.test(node.repositoryId) || repositoryIds.has(node.repositoryId)) throw new Error("Invalid coordinated task state or repository ownership");
617
+ onlyKeys(node, ["jobId", "id", "run", "dependsOn", "baseCommit", "repositoryId", "status", "attempts", "claimId", "claimedAt", "runId", "dependencies", "result", "reason", "nextCheckAt", "cancelRequested", "shippingClaimId", "shippedCommit"], "saved task");
618
+ const run = safeRun(node.run, job.project);
619
+ if (canonical(run) !== canonical(node.run) || run.baseRef !== node.baseCommit || repos.has(run.repo)) throw new Error("Saved coordinated task policy or pinned base changed");
620
+ repos.add(run.repo);
621
+ repositoryIds.add(node.repositoryId);
622
+ if (node.claimedAt !== void 0 && !date(node.claimedAt) || node.nextCheckAt !== void 0 && !date(node.nextCheckAt) || node.cancelRequested !== void 0 && typeof node.cancelRequested !== "boolean" || node.reason !== void 0 && (typeof node.reason !== "string" || node.reason.length > 1e3)) throw new Error("Invalid coordinated task dates or diagnostics");
623
+ if (node.claimId !== void 0) text(node.claimId, 240, "saved claim");
624
+ if (node.runId !== void 0 && !id(node.runId)) throw new Error("Invalid coordinated run identity");
625
+ if (node.attempts && !node.claimId && !["pending", "waiting"].includes(node.status)) throw new Error("Missing coordinated claim");
626
+ if (["claimed", "running", "verified", "unknown", "shipping", "shipping-unknown", "shipped"].includes(node.status) && !node.claimId) throw new Error("Missing coordinated launch claim");
627
+ if (["running", "verified", "shipping", "shipping-unknown", "shipped"].includes(node.status) && !node.runId) throw new Error("Missing coordinated run receipt");
628
+ if (node.result) {
629
+ const checked = result(node.result, node.id);
630
+ if (checked.repo !== run.repo || checked.runId !== node.runId || canonical(checked) !== canonical(node.result)) throw new Error("Mismatched coordinated result receipt");
631
+ }
632
+ if (["verified", "shipping", "shipping-unknown", "shipped"].includes(node.status) && !node.result) throw new Error("Missing verified coordinated result");
633
+ if (node.dependencies !== void 0 && (!Array.isArray(node.dependencies) || node.dependencies.length !== node.dependsOn.length || node.dependencies.some((entry) => !object(entry) || !node.dependsOn.includes(result(entry, entry.nodeId).nodeId) || canonical(result(entry, entry.nodeId)) !== canonical(entry) || entry.repo !== getNode(job, entry.nodeId).run.repo) || new Set(node.dependencies.map((entry) => entry.nodeId)).size !== node.dependsOn.length)) throw new Error("Invalid coordinated dependency receipts");
634
+ if (["claimed", "running", "verified", "unknown", "shipping", "shipping-unknown", "shipped"].includes(node.status) && !node.dependencies) throw new Error("Missing claimed dependency receipts");
635
+ if (node.shippingClaimId !== void 0) text(node.shippingClaimId, 240, "saved shipping claim");
636
+ if (["shipping", "shipping-unknown", "shipped"].includes(node.status) && !node.shippingClaimId) throw new Error("Missing shipping claim");
637
+ if (node.shippedCommit !== void 0 && !commitId(node.shippedCommit)) throw new Error("Invalid shipping commit");
638
+ if (node.status === "shipped" && !node.shippedCommit) throw new Error("Missing shipping commit receipt");
639
+ }
640
+ if (job.shipping) {
641
+ const preview = job.shipping;
642
+ if (!object(preview) || typeof preview.id !== "string" || !date(preview.createdAt) || preview.expiresAt !== preview.createdAt + COORDINATION_LIMITS.previewMs || !Array.isArray(preview.entries) || preview.entries.length > job.nodes.length || new Set(preview.entries.map((entry) => entry.nodeId)).size !== preview.entries.length) throw new Error("Invalid coordinated shipping matrix");
643
+ for (const entry of preview.entries) {
644
+ const node = getNode(job, entry.nodeId);
645
+ if (canonical(validation(entry.validation)) !== canonical(entry.validation) || entry.validation.source !== node.result?.commit) throw new Error("Mismatched reviewed shipping result");
646
+ }
647
+ }
648
+ for (const entry of job.history) if (!object(entry) || typeof entry.id !== "string" || !date(entry.at) || typeof entry.detail !== "string" || entry.detail.length > 1e3 || entry.nodeId !== void 0 && !job.nodes.some((node) => node.id === entry.nodeId)) throw new Error("Invalid coordinated operation history");
649
+ if (job.operationId !== void 0) {
650
+ text(job.operationId, 240, "saved operation identity");
651
+ if (!/^[a-f0-9]{64}$/.test(job.operationHash)) throw new Error("Invalid coordinated operation checksum");
652
+ }
653
+ const computed = copy(job);
654
+ refresh(computed);
655
+ if (computed.status !== job.status) throw new Error("Inconsistent coordinated job status");
656
+ }
657
+ return copy(raw);
658
+ }
659
+ function disableRestoredCoordinatedChanges(raw, now = Date.now()) {
660
+ const document = validateCoordinationDocument(raw);
661
+ let cancelled = 0;
662
+ for (const job of document.jobs) {
663
+ if (job.status === "shipped" || job.status === "cancelled") continue;
664
+ cancelled++;
665
+ job.cancelRequested = true;
666
+ for (const node of job.nodes) {
667
+ node.cancelRequested = true;
668
+ if (!["shipped", "shipping", "shipping-unknown", "unknown", "claimed", "running"].includes(node.status)) node.status = "cancelled";
669
+ if (node.status === "claimed") node.status = "unknown";
670
+ if (node.status === "shipping") node.status = "shipping-unknown";
671
+ node.reason = "Automatic execution disabled by encrypted backup restore; inspect existing receipts before explicit recovery";
672
+ }
673
+ job.revision++;
674
+ history(job, now, "Cancelled automatic dispatch after encrypted backup restore; receipt metadata preserved");
675
+ }
676
+ return { document, cancelled };
677
+ }
678
+ async function readDocument(file) {
679
+ let handle;
680
+ try {
681
+ handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
682
+ const info = await handle.stat();
683
+ if (!info.isFile() || info.size > COORDINATION_LIMITS.fileBytes) throw new Error("Coordinated storage must be a regular bounded file");
684
+ return validateCoordinationDocument(JSON.parse(await handle.readFile("utf8")));
685
+ } catch (error) {
686
+ if (error.code === "ENOENT") return { version: 1, jobs: [] };
687
+ throw error;
688
+ } finally {
689
+ await handle?.close();
690
+ }
691
+ }
692
+ async function writeDocument(file, document) {
693
+ const serialized = JSON.stringify(validateCoordinationDocument(document), null, 2);
694
+ if (Buffer.byteLength(serialized) > COORDINATION_LIMITS.fileBytes) throw new Error("Coordinated storage reached its byte limit");
695
+ await mkdir(dirname(file), { recursive: true });
696
+ const temp = `${file}.${randomUUID()}.tmp`;
697
+ try {
698
+ const handle = await open(temp, "wx", 384);
699
+ try {
700
+ await handle.writeFile(serialized);
701
+ await handle.sync();
702
+ } finally {
703
+ await handle.close();
704
+ }
705
+ await rename(temp, file);
706
+ if (process.platform !== "win32") {
707
+ const folder = await open(dirname(file), "r");
708
+ try {
709
+ await folder.sync();
710
+ } finally {
711
+ await folder.close();
712
+ }
713
+ }
714
+ } finally {
715
+ await rm(temp, { force: true });
716
+ }
717
+ }
718
+ export {
719
+ COORDINATION_LIMITS,
720
+ CoordinatedChanges,
721
+ CoordinationConflictError,
722
+ CoordinationRejectedError,
723
+ disableRestoredCoordinatedChanges,
724
+ validateCoordinationDocument
725
+ };