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

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 (284) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -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/browser-7XI52A6V.js +8 -0
  17. package/public/assets/changes-EYFPASEI.js +1 -0
  18. package/public/assets/chunk-7H3ZLTKA.js +2 -0
  19. package/public/assets/chunk-L7TUCIYF.js +1 -0
  20. package/public/assets/chunk-LLMQG23J.js +163 -0
  21. package/public/assets/main-KIRM2OEK.js +1 -0
  22. package/public/assets/settings-QJZVW22Y.js +9 -0
  23. package/public/assets/styles-615EBB20.css +32 -0
  24. package/public/assets/terminal-A2FI2NAX.js +62 -0
  25. package/public/assets.json +1 -0
  26. package/public/bundle-meta.json +1 -0
  27. package/public/docs/.nojekyll +0 -0
  28. package/public/docs/accessibility.html +19 -0
  29. package/public/docs/acp-agents.html +117 -0
  30. package/public/docs/agent-containers.html +79 -0
  31. package/public/docs/api.html +1130 -0
  32. package/public/docs/app.js +342 -0
  33. package/public/docs/application-improvements.html +275 -0
  34. package/public/docs/architecture-explorer.html +19 -0
  35. package/public/docs/architecture.html +149 -0
  36. package/public/docs/auto-team.html +142 -0
  37. package/public/docs/automations.html +109 -0
  38. package/public/docs/checkpoint-review.html +37 -0
  39. package/public/docs/code-hosts.html +35 -0
  40. package/public/docs/code-intelligence.html +35 -0
  41. package/public/docs/configuration.html +346 -0
  42. package/public/docs/context-inspection.html +23 -0
  43. package/public/docs/coordinated-changes.html +28 -0
  44. package/public/docs/deferred-runs.html +25 -0
  45. package/public/docs/dependency-discovery.html +19 -0
  46. package/public/docs/documentation.html +84 -0
  47. package/public/docs/dynamic-teams-evaluation.html +174 -0
  48. package/public/docs/dynamic-teams-implementation-plan.html +882 -0
  49. package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
  50. package/public/docs/dynamic-teams-plan-review-0.html +204 -0
  51. package/public/docs/extensions.html +42 -0
  52. package/public/docs/external-sources.html +96 -0
  53. package/public/docs/filesystem-policy.html +26 -0
  54. package/public/docs/filtered-tasks.html +44 -0
  55. package/public/docs/font-license.txt +92 -0
  56. package/public/docs/getting-started.html +118 -0
  57. package/public/docs/github-review-threads.html +26 -0
  58. package/public/docs/github.html +157 -0
  59. package/public/docs/help.html +69 -0
  60. package/public/docs/index.html +77 -0
  61. package/public/docs/installation.html +74 -0
  62. package/public/docs/launch-profiles.html +48 -0
  63. package/public/docs/maintenance.html +60 -0
  64. package/public/docs/memory-claims.html +31 -0
  65. package/public/docs/memory-storage.html +58 -0
  66. package/public/docs/notifications.html +39 -0
  67. package/public/docs/patterns.html +195 -0
  68. package/public/docs/performance-results.html +210 -0
  69. package/public/docs/performance.html +166 -0
  70. package/public/docs/project-conventions.html +22 -0
  71. package/public/docs/project-first-navigation-plan.html +587 -0
  72. package/public/docs/project-teams-implementation-plan.html +772 -0
  73. package/public/docs/project-teams.html +99 -0
  74. package/public/docs/provider-compatibility.html +20 -0
  75. package/public/docs/releasing.html +61 -0
  76. package/public/docs/remote-workers.html +64 -0
  77. package/public/docs/review-inbox.html +26 -0
  78. package/public/docs/run-storage.html +23 -0
  79. package/public/docs/runs.html +141 -0
  80. package/public/docs/search.json +1 -0
  81. package/public/docs/security.html +133 -0
  82. package/public/docs/shared-projects.html +31 -0
  83. package/public/docs/ssh-projects.html +60 -0
  84. package/public/docs/style.css +768 -0
  85. package/public/docs/task-reruns.html +30 -0
  86. package/public/docs/team-collaboration.html +379 -0
  87. package/public/docs/verification.html +22 -0
  88. package/public/docs/vscode.html +53 -0
  89. package/public/docs/worker-permission-verification.html +122 -0
  90. package/public/index.html +17 -0
  91. package/scripts/check-node.mjs +7 -0
  92. package/scripts/codeflow.mjs +55 -0
  93. package/scripts/pty.py +273 -0
  94. package/src/acp-connections.js +110 -0
  95. package/src/adapters/acp-presets.js +37 -0
  96. package/src/adapters/acp-types.js +0 -0
  97. package/src/adapters/acp.js +328 -0
  98. package/src/adapters/agy-auth.js +67 -0
  99. package/src/adapters/agy-isolation.js +223 -0
  100. package/src/adapters/agy.js +297 -0
  101. package/src/adapters/claude.js +220 -0
  102. package/src/adapters/codex-app-server.js +518 -0
  103. package/src/adapters/codex.js +299 -0
  104. package/src/adapters/errors.js +57 -0
  105. package/src/adapters/index.js +91 -0
  106. package/src/adapters/managed-permissions.js +37 -0
  107. package/src/adapters/model-discovery.js +100 -0
  108. package/src/adapters/process.js +418 -0
  109. package/src/adapters/team-tools.js +102 -0
  110. package/src/adapters/types.js +0 -0
  111. package/src/agent-container.js +155 -0
  112. package/src/approvals.js +121 -0
  113. package/src/architecture.js +456 -0
  114. package/src/assets.js +44 -0
  115. package/src/automation.js +901 -0
  116. package/src/certificate-renewal.js +75 -0
  117. package/src/chat-handoff.js +55 -0
  118. package/src/checkpoint-review.js +72 -0
  119. package/src/checks.js +303 -0
  120. package/src/code-host-git.js +102 -0
  121. package/src/code-hosts-api.js +99 -0
  122. package/src/code-hosts.js +482 -0
  123. package/src/code-intelligence-worker.mjs +452 -0
  124. package/src/code-intelligence.js +141 -0
  125. package/src/collab/api.js +365 -0
  126. package/src/collab/checkpoints.js +198 -0
  127. package/src/collab/connections.js +139 -0
  128. package/src/collab/host-execution.js +347 -0
  129. package/src/collab/hub-client.js +210 -0
  130. package/src/collab/link-api.js +229 -0
  131. package/src/collab/link.js +595 -0
  132. package/src/collab/model.js +496 -0
  133. package/src/collab/runner.js +1014 -0
  134. package/src/collab/store.js +1452 -0
  135. package/src/collab/terminals.js +361 -0
  136. package/src/collaboration-reviews.js +116 -0
  137. package/src/config.js +48 -0
  138. package/src/context-budget.js +14 -0
  139. package/src/context-inspection.js +204 -0
  140. package/src/context.js +154 -0
  141. package/src/conventions.js +283 -0
  142. package/src/coordinated-api.js +247 -0
  143. package/src/deferred-launch.js +82 -0
  144. package/src/deferred-runs.js +515 -0
  145. package/src/dependency-health.js +35 -0
  146. package/src/diff.js +43 -0
  147. package/src/docs-help.js +316 -0
  148. package/src/doctor.js +232 -0
  149. package/src/engine.js +4366 -0
  150. package/src/env.js +44 -0
  151. package/src/experience.js +50 -0
  152. package/src/extensions.js +144 -0
  153. package/src/filesystem-access.js +107 -0
  154. package/src/filesystem-policy.js +238 -0
  155. package/src/filtered-task-limits.js +93 -0
  156. package/src/filtered-tasks-api.js +53 -0
  157. package/src/filtered-tasks.js +576 -0
  158. package/src/fs.js +57 -0
  159. package/src/git.js +626 -0
  160. package/src/github-review-threads.js +181 -0
  161. package/src/github.js +538 -0
  162. package/src/http.js +69 -0
  163. package/src/improve-settings.js +38 -0
  164. package/src/improve.js +187 -0
  165. package/src/integrations.js +373 -0
  166. package/src/integrity.js +259 -0
  167. package/src/journey-api.js +77 -0
  168. package/src/journey-replay.js +65 -0
  169. package/src/journey-store.js +97 -0
  170. package/src/journeys.js +60 -0
  171. package/src/json.js +81 -0
  172. package/src/launch-profiles.js +292 -0
  173. package/src/learnings.js +80 -0
  174. package/src/log.js +39 -0
  175. package/src/maintenance.js +335 -0
  176. package/src/memory-analysis.js +182 -0
  177. package/src/memory-claims.js +153 -0
  178. package/src/memory-sqlite.js +236 -0
  179. package/src/memory.js +552 -0
  180. package/src/merge-validation.js +58 -0
  181. package/src/model-catalog.js +238 -0
  182. package/src/model-research.js +91 -0
  183. package/src/notes.js +176 -0
  184. package/src/notifications.js +413 -0
  185. package/src/patterns/auto-team-model.js +212 -0
  186. package/src/patterns/auto-team.js +659 -0
  187. package/src/patterns/council.js +199 -0
  188. package/src/patterns/debate.js +188 -0
  189. package/src/patterns/findings.js +141 -0
  190. package/src/patterns/index.js +19 -0
  191. package/src/patterns/loop.js +158 -0
  192. package/src/patterns/pipeline.js +125 -0
  193. package/src/patterns/prompts.js +181 -0
  194. package/src/patterns/race.js +147 -0
  195. package/src/patterns/schemas.js +40 -0
  196. package/src/patterns/solo.js +49 -0
  197. package/src/patterns/supervisor.js +235 -0
  198. package/src/patterns/task-draft.js +29 -0
  199. package/src/patterns/tdd.js +231 -0
  200. package/src/patterns/team-strategies.js +67 -0
  201. package/src/performance.js +43 -0
  202. package/src/persistence-status.js +35 -0
  203. package/src/phone-sessions.js +96 -0
  204. package/src/preview-overlay.js +383 -0
  205. package/src/preview.js +219 -0
  206. package/src/privacy.js +23 -0
  207. package/src/project-report.js +22 -0
  208. package/src/project-team-model.js +178 -0
  209. package/src/project-team-policy.js +42 -0
  210. package/src/project-team-store.js +213 -0
  211. package/src/project-teams-api.js +36 -0
  212. package/src/project-teams.js +551 -0
  213. package/src/projects.js +286 -0
  214. package/src/provider-eligibility.js +9 -0
  215. package/src/readiness.js +98 -0
  216. package/src/remote-tls.js +66 -0
  217. package/src/remote-workers/bundles.js +198 -0
  218. package/src/remote-workers/client.js +332 -0
  219. package/src/remote-workers/durability.js +37 -0
  220. package/src/remote-workers/protocol.js +68 -0
  221. package/src/remote-workers/stdio.js +138 -0
  222. package/src/remote-workers/worker.js +304 -0
  223. package/src/remote.js +204 -0
  224. package/src/repo-config.js +201 -0
  225. package/src/repo-coordination.js +732 -0
  226. package/src/repo-tree.js +106 -0
  227. package/src/retention-policy.js +466 -0
  228. package/src/review-inbox.js +38 -0
  229. package/src/routing.js +49 -0
  230. package/src/run-analysis.js +111 -0
  231. package/src/run-cold-store.js +231 -0
  232. package/src/run-environment.js +15 -0
  233. package/src/run-history.js +91 -0
  234. package/src/run-log.js +160 -0
  235. package/src/run-permissions.js +110 -0
  236. package/src/run-projection.js +75 -0
  237. package/src/run-recovery.js +53 -0
  238. package/src/run-status.js +54 -0
  239. package/src/serve.js +138 -0
  240. package/src/server.js +2738 -0
  241. package/src/shared-access.js +103 -0
  242. package/src/shared-accounts.js +301 -0
  243. package/src/shared-api.js +239 -0
  244. package/src/shell.js +346 -0
  245. package/src/ship.js +344 -0
  246. package/src/slots.js +74 -0
  247. package/src/snapshot.js +388 -0
  248. package/src/source-context.js +17 -0
  249. package/src/sse-writer.js +70 -0
  250. package/src/ssh-projects/bootstrap.mjs +95 -0
  251. package/src/ssh-projects/config.js +45 -0
  252. package/src/ssh-projects/gateway.js +143 -0
  253. package/src/ssh-projects/manager.js +320 -0
  254. package/src/ssh-projects/team-autostart.mjs +93 -0
  255. package/src/ssh-projects/team-remove.mjs +69 -0
  256. package/src/ssh-projects/team-status.mjs +39 -0
  257. package/src/ssh-projects/transport.js +116 -0
  258. package/src/stats.js +139 -0
  259. package/src/store.js +544 -0
  260. package/src/stream.js +91 -0
  261. package/src/sweeps.js +241 -0
  262. package/src/tailscale.js +101 -0
  263. package/src/task-draft.js +23 -0
  264. package/src/team-evaluation.js +242 -0
  265. package/src/team-mailbox.js +160 -0
  266. package/src/team-memory.js +84 -0
  267. package/src/team-rerun-graph.js +15 -0
  268. package/src/team-reruns.js +130 -0
  269. package/src/team-scheduler.js +121 -0
  270. package/src/team-template-store.js +98 -0
  271. package/src/team-templates.js +117 -0
  272. package/src/team-tool-client.mjs +79 -0
  273. package/src/team-tools.js +176 -0
  274. package/src/teams.js +0 -0
  275. package/src/templates.js +62 -0
  276. package/src/transcript.js +134 -0
  277. package/src/verification-integrity.js +27 -0
  278. package/src/verification.js +183 -0
  279. package/src/watch.js +195 -0
  280. package/src/work-queue.js +47 -0
  281. package/src/workflow-control.js +83 -0
  282. package/src/workspace-api.js +207 -0
  283. package/src/workspace-changes.js +61 -0
  284. package/src/workspace.js +146 -0
@@ -0,0 +1,99 @@
1
+ import { constants } from "node:fs";
2
+ import { open } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { CodeHosts, CodeHostError } from "./code-hosts.js";
5
+ import { HttpError, readJson, send } from "./http.js";
6
+ import { RUNS_DIR, flushWrites } from "./store.js";
7
+ import { validateVerificationProfile } from "./verification.js";
8
+ import { validateAgentContainer } from "./agent-container.js";
9
+ async function persistCodeHostRun(run, directory = RUNS_DIR) {
10
+ await flushWrites();
11
+ const handle = await open(join(directory, `${run.id}.jsonl`), constants.O_RDWR | (constants.O_NOFOLLOW ?? 0));
12
+ try {
13
+ const info = await handle.stat();
14
+ if (!info.isFile() || !info.size) throw new Error("Missing code review run receipt");
15
+ const buffer = Buffer.alloc(Math.min(info.size, 4 * 1024 * 1024)), read = await handle.read(buffer, 0, buffer.length, 0), end = buffer.subarray(0, read.bytesRead).indexOf(10);
16
+ if (end < 0) throw new Error("Incomplete code review run receipt");
17
+ const first = JSON.parse(buffer.subarray(0, end).toString("utf8")), stored = first.type === "run" ? first.run : void 0;
18
+ if (!stored || stored.id !== run.id || stored.repo !== run.repo || stored.project !== run.project || stored.baseRef !== run.externalSource?.code?.head || JSON.stringify(stored.externalSource?.code) !== JSON.stringify(run.externalSource.code) || stored.externalSource?.integration !== run.externalSource.integration || stored.externalSource?.key !== run.externalSource.key || stored.trust !== "safe" || stored.loop.copy?.length || !stored.lockdown || stored.lockdownOnHost || stored.loop.container?.network !== false) throw new Error("The initial code review identity and isolated source policy were not saved");
19
+ await handle.sync();
20
+ } finally {
21
+ await handle.close();
22
+ }
23
+ if (process.platform !== "win32") {
24
+ const folder = await open(directory, "r");
25
+ try {
26
+ await folder.sync();
27
+ } finally {
28
+ await folder.close();
29
+ }
30
+ }
31
+ }
32
+ function createCodeHostsApi(options) {
33
+ const service = options.service ?? new CodeHosts(options), persist = options.persist ?? persistCodeHostRun;
34
+ const text = (value, label, max = 250) => {
35
+ if (typeof value !== "string" || !value || value.length > max) throw new HttpError(400, `Choose a valid ${label}`);
36
+ return value;
37
+ };
38
+ const handle = async (req, res, parts) => {
39
+ if (parts[0] !== "code-hosts") return false;
40
+ try {
41
+ if (req.method === "GET" && parts.length === 1) {
42
+ send(res, 200, { integrations: options.integrations.list().filter((item) => item.codeHost), history: service.history() });
43
+ return true;
44
+ }
45
+ if (req.method !== "POST") throw new HttpError(404, "Unknown code host operation");
46
+ const body = await readJson(req, 256e3);
47
+ let value;
48
+ if (parts.length === 4 && parts[1] === "history" && parts[3] === "acknowledge") {
49
+ if (body.acknowledge !== true) throw new HttpError(400, "Explicitly acknowledge inspecting this exact unknown operation");
50
+ send(res, 200, await service.acknowledge(text(parts[2], "unknown operation", 40)));
51
+ return true;
52
+ }
53
+ if (parts.length === 2 && parts[1] === "preview") value = await service.preview({ integration: text(body.integration, "code host", 64), key: text(body.key, "PR number", 12), repo: text(body.repo, "local repository", 4096), ...body.project ? { project: text(body.project, "project", 120) } : {} });
54
+ else if (parts.length === 2 && parts[1] === "create-preview") value = await service.createPreview({ integration: text(body.integration, "code host", 64), runId: text(body.runId, "owned run", 120), branch: text(body.branch, "owned result branch"), targetBranch: text(body.targetBranch, "target branch"), title: text(body.title, "PR title", 200), body: typeof body.body === "string" ? body.body : "" });
55
+ else {
56
+ const id = text(parts[1], "server preview", 40), action = parts[2];
57
+ if (parts.length !== 3) throw new HttpError(404, "Unknown code host operation");
58
+ if (action === "fetch") value = await service.fetch(id);
59
+ else if (action === "create") value = await service.create(id);
60
+ else if (action === "review") value = await service.review(id, body);
61
+ else if (action === "push-preview") value = await service.pushPreview(id, text(body.runId, "owned run", 120), text(body.branch, "owned result branch"));
62
+ else if (action === "push") value = await service.push(id, text(body.planId, "push preview", 40));
63
+ else if (action === "merge-preview") value = await service.mergePreview(id, text(body.runId, "owned run", 120), text(body.branch, "owned result branch"));
64
+ else if (action === "merge") value = await service.merge(id, text(body.planId, "merge preview", 40));
65
+ else if (action === "run") {
66
+ const raw = body.run;
67
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || raw.parentRunId || raw.source || raw.sourceReference || raw.externalSource || raw.context?.attachments?.length || raw.attachments?.length) throw new HttpError(400, "Code review runs use this server-fetched source and do not inherit parent runs or uploads");
68
+ if (!raw.loop?.agentContainer) throw new HttpError(409, "Fetched PR code requires an explicitly prepared agent container image and optional in-container authentication volume. Native Safe mode is not filesystem isolation.");
69
+ const agentContainer = validateAgentContainer(raw.loop.agentContainer), prepared = await service.prepareRun(id);
70
+ const check = typeof raw.loop?.check === "string" ? raw.loop.check.trim() : "";
71
+ const verification = raw.loop?.verification ? validateVerificationProfile(raw.loop.verification) : check ? validateVerificationProfile({ name: "Code host review", checks: [{ id: "code-host-check", kind: "custom", command: check, required: true, timeoutMs: 12e4 }] }) : void 0;
72
+ if (!verification?.checks.some((gate) => gate.required)) throw new HttpError(400, "Code review runs need at least one required verification command");
73
+ const built = await options.build({ ...raw, repo: prepared.preview.repo, project: prepared.preview.project, baseRef: prepared.preview.head, trust: "safe", allowUntrusted: false, approvals: { tools: false, plan: false }, loop: { ...raw.loop, agentContainer, verification, copy: [], container: { ...raw.loop?.container ?? { image: "node:22" }, network: false } } });
74
+ value = await service.action(id, "run", async () => {
75
+ const current = await service.prepareRun(id);
76
+ if (current.preview.repo !== built.repo || current.preview.head !== prepared.preview.head || current.preview.target !== prepared.preview.target) throw new CodeHostError("The fetched source changed during run validation");
77
+ const run = await options.manager.start({ ...built, parentRunId: void 0, source: void 0, externalSource: current.source, baseRef: current.preview.head, trust: "safe", allowUntrusted: false, requireProjectRepo: !!current.preview.project, approvals: { tools: false, plan: false }, context: { ...built.context, attachments: [] }, loop: { ...built.loop, copy: [], container: { ...built.loop?.container ?? { image: "node:22" }, network: false } } });
78
+ await service.recordRun(id, run.id);
79
+ await persist(run);
80
+ return { id: run.id };
81
+ }, (result) => ({ runId: result.id }));
82
+ } else throw new HttpError(404, "Unknown code host operation");
83
+ }
84
+ send(res, 200, value);
85
+ return true;
86
+ } catch (error) {
87
+ if (error instanceof CodeHostError) {
88
+ send(res, error.status, { error: error.message, ambiguous: error.ambiguous });
89
+ return true;
90
+ }
91
+ throw error;
92
+ }
93
+ };
94
+ return { service, handle };
95
+ }
96
+ export {
97
+ createCodeHostsApi,
98
+ persistCodeHostRun
99
+ };
@@ -0,0 +1,482 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { mkdir, open, rename, rm, realpath } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { CODEFLOW_HOME } from "./config.js";
6
+ import { CodeHostGit, codeHostRemote } from "./code-host-git.js";
7
+ import { git, repoRoot, resolveCommit } from "./git.js";
8
+ import { validateMergeTarget } from "./merge-validation.js";
9
+ import { redactExport } from "./privacy.js";
10
+ class CodeHostError extends Error {
11
+ constructor(message, status = 409, ambiguous = false) {
12
+ super(message);
13
+ this.status = status;
14
+ this.ambiguous = ambiguous;
15
+ }
16
+ }
17
+ const SHA = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
18
+ const ID = /^[a-f0-9]{40}$/;
19
+ const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
20
+ const text = (value, max = 2e4) => typeof value === "string" ? value.slice(0, max) : "";
21
+ const commit = (value) => {
22
+ if (typeof value !== "string" || !SHA.test(value)) throw new CodeHostError("The provider returned an invalid exact commit identity", 502);
23
+ return value;
24
+ };
25
+ const number = (value) => {
26
+ if (typeof value !== "string" || !/^[1-9]\d{0,11}$/.test(value)) throw new CodeHostError("Choose a positive pull request number", 400);
27
+ return value;
28
+ };
29
+ const safeId = (value) => {
30
+ const id = String(value);
31
+ if (!/^[A-Za-z0-9_-]{1,150}$/.test(id)) throw new CodeHostError("The provider returned an invalid discussion identity", 502);
32
+ return id;
33
+ };
34
+ const branch = (value) => {
35
+ if (typeof value !== "string" || value.length > 250 || !value || /[\x00-\x20~^:?*\[\\]/.test(value) || value.startsWith("-") || value.includes("..") || value.includes("@{") || value.endsWith(".") || value.endsWith("/") || value.split("/").some((part) => !part || part.startsWith(".") || part.endsWith(".lock"))) throw new CodeHostError("The provider returned an unsafe branch", 502);
36
+ return value;
37
+ };
38
+ const jsonArray = (value, name) => {
39
+ if (!Array.isArray(value) || value.length > 100) throw new CodeHostError(`The provider returned an invalid or oversized ${name}`, 502);
40
+ return value;
41
+ };
42
+ const apiPath = (session) => session.config.provider === "gitlab" ? `projects/${encodeURIComponent(session.config.project)}` : `repositories/${session.config.project.split("/").map(encodeURIComponent).join("/")}`;
43
+ function validateCodeHostDocument(value) {
44
+ if (!value || value.version !== 1 || !Array.isArray(value.operations) || value.operations.length > 200 || Object.keys(value).some((key) => !["version", "operations"].includes(key)) || value.operations.some((entry) => !entry || Object.keys(entry).some((key) => !["id", "previewId", "integration", "key", "kind", "status", "at", "commit", "runId", "sourceBranch", "target", "detail"].includes(key)) || !ID.test(entry.id) || !ID.test(entry.previewId) || !["claimed", "confirmed", "unknown", "inspected"].includes(entry.status) || typeof entry.integration !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(entry.integration) || typeof entry.key !== "string" || !/^(?:[1-9]\d{0,11}|new)$/.test(entry.key) || !["review", "fetch", "push", "merge", "run", "create"].includes(entry.kind) || !Number.isFinite(entry.at) || typeof entry.detail !== "string" || entry.detail.length > 500 || entry.commit !== void 0 && !SHA.test(entry.commit) || entry.target !== void 0 && !SHA.test(entry.target) || entry.sourceBranch !== void 0 && (typeof entry.sourceBranch !== "string" || !/^codeflow\/pr\/[a-f0-9]{20}$/.test(entry.sourceBranch)) || entry.runId !== void 0 && (typeof entry.runId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$/.test(entry.runId)))) throw new Error("Invalid code host action history");
45
+ return structuredClone(value);
46
+ }
47
+ function disableRestoredCodeHostActions(value) {
48
+ const document = validateCodeHostDocument(value);
49
+ for (const entry of document.operations) if (entry.status === "claimed") {
50
+ entry.status = "unknown";
51
+ entry.detail = "Restored action has an unknown outcome. Inspect exact provider and run state; no automatic replay is permitted.";
52
+ }
53
+ return document;
54
+ }
55
+ class CodeHosts {
56
+ constructor(options) {
57
+ this.options = options;
58
+ this.file = options.file ?? join(CODEFLOW_HOME, "code-host-operations.json");
59
+ this.transport = options.git ?? new CodeHostGit();
60
+ this.now = options.now ?? Date.now;
61
+ }
62
+ previews = /* @__PURE__ */ new Map();
63
+ busy = /* @__PURE__ */ new Set();
64
+ operations = [];
65
+ writes = Promise.resolve();
66
+ file;
67
+ transport;
68
+ now;
69
+ async load() {
70
+ const handle = await open(this.file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)).catch((error) => {
71
+ if (error.code === "ENOENT") return void 0;
72
+ throw error;
73
+ });
74
+ if (!handle) return;
75
+ try {
76
+ const info = await handle.stat();
77
+ if (!info.isFile() || info.size > 512e3) throw new Error("Invalid code host action history");
78
+ this.operations = disableRestoredCodeHostActions(JSON.parse(await handle.readFile("utf8"))).operations;
79
+ } finally {
80
+ await handle.close();
81
+ }
82
+ }
83
+ history() {
84
+ return structuredClone(this.operations).reverse();
85
+ }
86
+ async acknowledge(id) {
87
+ if (!ID.test(id)) throw new CodeHostError("Choose an unknown operation from the saved history", 400);
88
+ await this.ledger(() => {
89
+ const entry = this.operations.find((entry2) => entry2.id === id);
90
+ if (!entry || entry.status !== "unknown") throw new CodeHostError("This operation is not currently unknown");
91
+ entry.status = "inspected";
92
+ entry.detail = "Owner acknowledged inspecting this exact outcome. This records manual inspection, not provider confirmation, and never replays the action.";
93
+ });
94
+ return { inspected: true, detail: "Recorded manual inspection; the remote action was not replayed." };
95
+ }
96
+ async save() {
97
+ await mkdir(dirname(this.file), { recursive: true });
98
+ const tmp = `${this.file}.${randomBytes(10).toString("hex")}.tmp`;
99
+ try {
100
+ const handle = await open(tmp, "wx", 384);
101
+ try {
102
+ await handle.writeFile(JSON.stringify({ version: 1, operations: this.operations }));
103
+ await handle.sync();
104
+ } finally {
105
+ await handle.close();
106
+ }
107
+ await rename(tmp, this.file);
108
+ if (process.platform !== "win32") {
109
+ const directory = await open(dirname(this.file), "r");
110
+ try {
111
+ await directory.sync();
112
+ } finally {
113
+ await directory.close();
114
+ }
115
+ }
116
+ } finally {
117
+ await rm(tmp, { force: true });
118
+ }
119
+ }
120
+ async ledger(action) {
121
+ const pending = this.writes.catch(() => void 0).then(async () => {
122
+ action();
123
+ await this.save();
124
+ });
125
+ this.writes = pending;
126
+ await pending;
127
+ }
128
+ authorize(value) {
129
+ if (value.project) {
130
+ const project = this.options.projects.get(value.project);
131
+ if (!project?.repos.includes(value.repo)) throw new CodeHostError("Project membership changed. Load a fresh preview.");
132
+ }
133
+ }
134
+ get(id) {
135
+ const saved = this.previews.get(id);
136
+ if (!saved || saved.value.expiresAt <= this.now()) {
137
+ this.previews.delete(id);
138
+ throw new CodeHostError("Code host preview expired or is unknown. Load a fresh preview.");
139
+ }
140
+ this.authorize(saved.value);
141
+ return saved;
142
+ }
143
+ async action(id, kind, work, receipt) {
144
+ const saved = this.get(id), key = `${saved.value.integration}:${saved.value.key}`;
145
+ if (this.busy.has(key)) throw new CodeHostError("Another action is in progress for this pull request");
146
+ this.busy.add(key);
147
+ const creation = kind === "create" ? saved.raw.creation : void 0, relatedRun = kind === "push" ? saved.value.push?.runId : kind === "merge" ? saved.value.merge?.runId : creation?.runId;
148
+ const entry = { id: randomBytes(20).toString("hex"), previewId: id, integration: saved.value.integration, key: saved.value.key, kind, status: "claimed", at: this.now(), ...relatedRun ? { runId: relatedRun } : {}, ...creation ? { sourceBranch: creation.sourceBranch, target: creation.target, commit: creation.head } : {}, detail: "Explicit action claimed; no automatic replay" };
149
+ try {
150
+ await this.ledger(() => {
151
+ if (this.operations.length >= 200) {
152
+ const disposable = this.operations.findIndex((value) => ["confirmed", "inspected"].includes(value.status));
153
+ if (disposable < 0) throw new CodeHostError("The action history is full of unresolved outcomes. Inspect and acknowledge an unknown operation before starting another.");
154
+ this.operations.splice(disposable, 1);
155
+ }
156
+ this.operations.push(entry);
157
+ });
158
+ try {
159
+ const result = await (["push", "merge", "create", "review"].includes(kind) ? this.options.manager.withIdleRepository(saved.value.repo, () => work(saved), { kind: `code-host-${kind}` }) : work(saved));
160
+ await this.ledger(() => {
161
+ entry.status = "confirmed";
162
+ entry.detail = "Provider or local action confirmed; refresh before another mutation";
163
+ const proof = receipt?.(result);
164
+ if (typeof proof === "string") entry.commit = proof;
165
+ else if (proof) entry.runId = proof.runId;
166
+ });
167
+ return result;
168
+ } catch (error) {
169
+ await this.ledger(() => {
170
+ entry.status = "unknown";
171
+ entry.detail = "Action was not confirmed. Inspect exact local and provider state before retrying; never automatically replayed.";
172
+ }).catch(() => void 0);
173
+ throw new CodeHostError(`${error.message} The action receipt is unknown; inspect state before retrying.`, error instanceof CodeHostError ? error.status : 502, true);
174
+ }
175
+ } finally {
176
+ this.busy.delete(key);
177
+ }
178
+ }
179
+ async recordRun(id, runId) {
180
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$/.test(runId)) throw new Error("Invalid code host run receipt");
181
+ const entry = [...this.operations].reverse().find((value) => value.previewId === id && value.kind === "run" && value.status === "claimed");
182
+ if (!entry) throw new Error("The code host launch claim is unavailable");
183
+ await this.ledger(() => {
184
+ entry.runId = runId;
185
+ });
186
+ }
187
+ async read(session, key, signal) {
188
+ const path = apiPath(session), gl = session.config.provider === "gitlab", root = `${path}/${gl ? "merge_requests" : "pullrequests"}/${number(key)}`;
189
+ const { data: raw } = await session.request(root, { signal });
190
+ let sourcePath, targetPath, target;
191
+ if (gl) {
192
+ if (String(raw?.iid) !== key) throw new CodeHostError("GitLab returned a different merge request", 502);
193
+ const { data: project } = await session.request(path, { signal });
194
+ if (String(raw.target_project_id) !== String(project.id)) throw new CodeHostError("The merge request belongs to another configured project", 502);
195
+ targetPath = text(project.path_with_namespace, 200);
196
+ sourcePath = targetPath;
197
+ target = commit((await session.request(`${path}/repository/branches/${encodeURIComponent(branch(raw.target_branch))}`, { signal })).data?.commit?.id);
198
+ } else {
199
+ if (String(raw?.id) !== key || raw.destination?.repository?.full_name !== session.config.project) throw new CodeHostError("Bitbucket returned a different repository or pull request", 502);
200
+ sourcePath = text(raw.source?.repository?.full_name, 200);
201
+ targetPath = text(raw.destination?.repository?.full_name, 200);
202
+ target = commit(raw.destination?.commit?.hash);
203
+ }
204
+ const head = commit(gl ? raw.sha : raw.source?.commit?.hash), sourceBranch = branch(gl ? raw.source_branch : raw.source?.branch?.name), targetBranch = branch(gl ? raw.target_branch : raw.destination?.branch?.name);
205
+ const refs = { head, target, sourceUrl: codeHostRemote(session, sourcePath), targetUrl: codeHostRemote(session, targetPath), sourceRef: gl ? `refs/merge-requests/${key}/head` : `refs/heads/${sourceBranch}`, targetRef: `refs/heads/${targetBranch}` };
206
+ return { raw, root, path, refs, sourceBranch, targetBranch, state: text(raw.state, 30), title: redactExport(text(raw.title, 500)), body: redactExport(text(gl ? raw.description : raw.description)), url: (() => {
207
+ try {
208
+ const url = new URL(gl ? raw.web_url : raw.links?.html?.href);
209
+ return url.protocol === "https:" && !url.username && !url.password ? url.href : void 0;
210
+ } catch {
211
+ return void 0;
212
+ }
213
+ })() };
214
+ }
215
+ async policy(session, info, signal) {
216
+ const reasons = [], evidence = [], { raw, root, path } = info;
217
+ try {
218
+ if (session.config.provider === "gitlab") {
219
+ const responses = await Promise.all([session.request(path, { signal }), session.request(`${root}/approvals`, { signal }), session.request(`${root}/approval_state`, { signal }), session.request(`${path}/external_status_checks`, { signal }), session.request(`${root}/status_checks`, { signal })]);
220
+ const [project, approvals, rules, services, checks] = responses.map((result) => result.data);
221
+ evidence.push(project, approvals, rules, services, checks);
222
+ if (raw.state !== "opened" || raw.draft !== false || raw.has_conflicts !== false || raw.detailed_merge_status !== "mergeable") reasons.push("GitLab does not report a current open, non-draft, conflict-free mergeable request");
223
+ if (approvals?.approved !== true || !Array.isArray(rules?.rules) || rules.rules.some((rule) => rule.approved !== true)) reasons.push("Required GitLab approvals are missing or unknown");
224
+ if (typeof project.only_allow_merge_if_pipeline_succeeds !== "boolean" || typeof project.only_allow_merge_if_all_discussions_are_resolved !== "boolean") reasons.push("GitLab merge policy could not be fully identified");
225
+ if (project.only_allow_merge_if_pipeline_succeeds && (raw.head_pipeline?.status !== "success" || raw.head_pipeline?.sha !== info.refs.head)) reasons.push("The required GitLab exact-head pipeline is not successful");
226
+ if (project.only_allow_merge_if_all_discussions_are_resolved && raw.blocking_discussions_resolved !== true) reasons.push("GitLab blocking discussions are unresolved");
227
+ if (project.merge_trains_enabled || project.merge_pipelines_enabled || project.merge_method !== "merge" || !["never", "default_off"].includes(project.squash_option)) reasons.push("Native trains, merged pipelines or enforced squash/rebase policies require a provider-native workflow");
228
+ if (!Array.isArray(services) || !Array.isArray(checks) || services.some((service) => !checks.some((check) => check.id === service.id && check.status === "passed"))) reasons.push("External GitLab status checks are missing or not passed");
229
+ let restrictions;
230
+ try {
231
+ restrictions = (await session.request(`${path}/protected_branches/${encodeURIComponent(info.targetBranch)}`, { signal })).data;
232
+ if (!Array.isArray(restrictions?.push_access_levels) || !Array.isArray(restrictions?.merge_access_levels)) reasons.push("Protected branch permissions could not be identified");
233
+ } catch (error) {
234
+ if (error.status !== 404) throw error;
235
+ restrictions = { protected: false };
236
+ }
237
+ evidence.push(restrictions);
238
+ } else {
239
+ const checks = (await session.request(`${root}/mergeability/checks?pagelen=100`, { signal })).data, restrictions = (await session.request(`${path}/branch-restrictions?pagelen=100`, { signal })).data;
240
+ evidence.push(checks, restrictions);
241
+ if (raw.state !== "OPEN" || raw.draft === true) reasons.push("The Bitbucket pull request is not open and ready");
242
+ if (!Array.isArray(checks?.values) || checks.next || checks.values.length > 100 || !Array.isArray(restrictions?.values) || restrictions.next || restrictions.values.length > 100) reasons.push("Bitbucket policy results are incomplete or unknown");
243
+ else {
244
+ const allowed = /* @__PURE__ */ new Set(["pullrequest_state_check", "current_user_permission_check", "git_mergeability_check", "standard_merge_check", "custom_pre_merge_check"]);
245
+ if (!["pullrequest_state_check", "current_user_permission_check", "git_mergeability_check"].every((type) => checks.values.some((check) => check.type === type && check.status === "PASSED"))) reasons.push("Bitbucket state, permission or Git mergeability checks are missing");
246
+ if (checks.values.some((check) => !allowed.has(check.type) || !["PASSED", "FAILED"].includes(check.status) || check.blocking === true || check.required !== false && check.status !== "PASSED")) reasons.push("Required, unknown, native queue or on-merge checks cannot be satisfied by pinned Git merge");
247
+ const kinds = /* @__PURE__ */ new Set(["push", "force", "delete", "restrict_merges", "reset_pullrequest_approvals_on_change", "require_approvals_to_merge", "require_default_reviewer_approvals_to_merge", "require_tasks_to_be_completed", "require_passing_builds_to_merge", "require_no_changes_requested", "require_commits_behind", "require_all_dependencies_merged", "enforce_merge_checks"]);
248
+ if (restrictions.values.some((rule) => !kinds.has(rule.kind) || !["glob", "branching_model"].includes(rule.branch_match_kind))) reasons.push("Bitbucket branch policy contains unsupported requirements");
249
+ if (restrictions.values.some((rule) => rule.kind === "require_all_dependencies_merged")) reasons.push("Dependent PR merge requirements are not fully reported by mergeability checks");
250
+ }
251
+ }
252
+ } catch {
253
+ reasons.push("Provider approval, check or branch policy is unavailable; merging is blocked until it can be verified");
254
+ }
255
+ return { allowed: reasons.length === 0, reasons, fingerprint: digest({ head: info.refs.head, target: info.refs.target, raw: { state: raw.state, detailed_merge_status: raw.detailed_merge_status, head_pipeline: raw.head_pipeline, blocking_discussions_resolved: raw.blocking_discussions_resolved }, evidence }) };
256
+ }
257
+ async discussions(session, info, signal) {
258
+ const gl = session.config.provider === "gitlab", response = await session.request(`${info.root}/${gl ? "discussions?per_page=100" : "comments?pagelen=100"}`, { signal }), values = jsonArray(gl ? response.data : response.data?.values, "review discussions"), discussions = [];
259
+ for (const raw of values) {
260
+ const notes = gl ? jsonArray(raw.notes, "discussion comments") : [raw];
261
+ if (!notes.length) continue;
262
+ const first = notes[0], id = safeId(raw.id), resolved = gl ? first.resolved === true : raw.resolution !== void 0 && raw.resolution !== null;
263
+ const value = { id, resolved, canResolve: gl ? first.resolvable === true : raw.inline !== void 0, path: text(gl ? first.position?.new_path ?? first.position?.old_path : raw.inline?.path, 2e3) || void 0, line: gl ? first.position?.new_line ?? first.position?.old_line : raw.inline?.to ?? raw.inline?.from, comments: notes.map((note) => ({ id: safeId(note.id), body: redactExport(text(gl ? note.body : note.content?.raw, 8e3)), author: text(gl ? note.author?.username : note.user?.display_name, 100) })) };
264
+ discussions.push({ ...value, revision: digest(raw) });
265
+ }
266
+ return { discussions, truncated: gl ? !!response.headers.get("x-next-page") : !!response.data?.next };
267
+ }
268
+ async preview(input, signal) {
269
+ const repo = await realpath(await repoRoot(input.repo));
270
+ this.authorize({ ...input, repo });
271
+ const session = this.options.integrations.codeHost(input.integration), info = await this.read(session, number(input.key), signal), discussion = await this.discussions(session, info, signal), policy = await this.policy(session, info, signal);
272
+ const value = { id: randomBytes(20).toString("hex"), integration: input.integration, provider: session.config.provider, key: input.key, repo, ...input.project ? { project: input.project } : {}, title: info.title, body: info.body, url: info.url, state: info.state, head: info.refs.head, target: info.refs.target, sourceBranch: info.sourceBranch, targetBranch: info.targetBranch, expiresAt: this.now() + 15 * 6e4, trusted: false, discussions: discussion.discussions, discussionTruncated: discussion.truncated, policy };
273
+ for (const [id, saved] of this.previews) if (saved.value.expiresAt <= this.now()) this.previews.delete(id);
274
+ while (this.previews.size >= 50) this.previews.delete(this.previews.keys().next().value);
275
+ this.previews.set(value.id, { value, session, refs: info.refs, raw: info.raw });
276
+ return structuredClone(value);
277
+ }
278
+ async current(saved, signal, requirePolicy = false) {
279
+ this.get(saved.value.id);
280
+ const info = await this.read(saved.session, saved.value.key, signal);
281
+ if (info.refs.head !== saved.value.head || info.refs.target !== saved.value.target || JSON.stringify(info.refs) !== JSON.stringify(saved.refs) || !["opened", "OPEN"].includes(info.state)) throw new CodeHostError("The source, target or PR state changed. Load a fresh preview.");
282
+ if (requirePolicy) {
283
+ const policy = await this.policy(saved.session, info, signal);
284
+ if (!policy.allowed || policy.fingerprint !== saved.value.policy.fingerprint) throw new CodeHostError(`Provider policies changed or block merging: ${policy.reasons.join("; ") || "refresh and review current policy"}`);
285
+ }
286
+ this.authorize(saved.value);
287
+ return info;
288
+ }
289
+ async fetch(id, signal) {
290
+ return this.action(id, "fetch", async (saved) => this.options.manager.withIdleRepository(saved.value.repo, async () => {
291
+ await this.current(saved, signal);
292
+ saved.value.fetched = await this.transport.fetch(saved.value.repo, saved.session, saved.refs, signal, () => this.authorize(saved.value));
293
+ this.authorize(saved.value);
294
+ saved.value.base = saved.value.fetched.base;
295
+ return structuredClone(saved.value);
296
+ }, { kind: "code-host-fetch" }));
297
+ }
298
+ source(id) {
299
+ const { value } = this.get(id), fetched = value.fetched;
300
+ if (!fetched) throw new CodeHostError("Fetch this server preview before starting a code review run");
301
+ return { integration: value.integration, provider: value.provider, kind: "pr", key: value.key, title: value.title, body: value.body, url: value.url, state: value.state, sourceBranch: value.sourceBranch, targetBranch: value.targetBranch, sha: value.head, id: `code-${value.id}`, importedAt: this.now(), expiresAt: value.expiresAt, trusted: false, untrusted: true, code: { previewId: value.id, head: value.head, target: value.target, base: fetched.base } };
302
+ }
303
+ async prepareRun(id) {
304
+ const saved = this.get(id);
305
+ await this.current(saved);
306
+ if (!saved.value.fetched) await this.fetch(id);
307
+ return { preview: structuredClone(saved.value), source: this.source(id) };
308
+ }
309
+ async verified(value, runId, selected) {
310
+ const run = this.options.manager.get(runId);
311
+ this.authorize(value);
312
+ if (!run || run.repo !== value.repo || value.project !== void 0 && run.project !== value.project || run.status !== "done" || run.followUps.some((follow) => follow.status === "pending") || !run.worktrees.some((tree) => tree.branch === selected) || !run.artifacts.some((artifact) => artifact.branch === selected)) throw new CodeHostError("Choose a completed owned published result branch in this repository and project");
313
+ const head = await resolveCommit(run.repo, `refs/heads/${selected}`), report = run.verification, profile = run.loop.verification;
314
+ if (!report || report.branch !== selected || report.head !== head || report.state !== "verified" || run.verdict === "failing" || !profile?.checks.some((check) => check.required) || profile.checks.some((check) => check.required && !report.checks.some((evidence) => evidence.check.id === check.id && evidence.check.command === check.command && evidence.diagnosis === "passed" && evidence.attempts.length > 0 && evidence.attempts.every((attempt) => attempt.ok && attempt.exitCode === 0 && !attempt.timedOut))) || report.skipped.some((check) => check.check.required) || report.requirements?.some((requirement) => !requirement.covered)) throw new CodeHostError("The exact owned result has no successful current required verification evidence");
315
+ const coverage = await this.options.manager.reviewCoverage(run.id, selected);
316
+ if (coverage.head !== head || coverage.stale || run.pattern === "auto-team" && (!run.team?.verification?.approved || !run.team.verification.reviewed || run.team.verification.head !== head)) throw new CodeHostError("Independent review evidence is missing or stale");
317
+ this.authorize(value);
318
+ return { run, head, profile };
319
+ }
320
+ async owned(saved, runId, selected, exactHead) {
321
+ const owned = await this.verified(saved.value, runId, selected);
322
+ if (owned.run.externalSource?.integration !== saved.value.integration || owned.run.externalSource.key !== saved.value.key || !owned.run.externalSource.code || exactHead && owned.head !== saved.value.head || (await git(["merge-base", "--is-ancestor", saved.value.head, owned.head], owned.run.repo)).code) throw new CodeHostError("The owned result does not match this fetched PR source or contains an unreviewed source update");
323
+ return owned;
324
+ }
325
+ async repositoryTarget(session, targetBranch, signal) {
326
+ const path = apiPath(session), gl = session.config.provider === "gitlab", repository = (await session.request(path, { signal })).data;
327
+ if (gl ? !Number.isSafeInteger(repository?.id) || typeof repository.path_with_namespace !== "string" : repository?.full_name !== session.config.project) throw new CodeHostError("The configured repository identity could not be verified", 502);
328
+ const target = commit((await session.request(`${path}/${gl ? "repository/branches" : "refs/branches"}/${encodeURIComponent(branch(targetBranch))}`, { signal })).data?.[gl ? "commit" : "target"]?.[gl ? "id" : "hash"]);
329
+ return { url: codeHostRemote(session, gl ? repository.path_with_namespace : repository.full_name), target, path };
330
+ }
331
+ async createPreview(input, signal) {
332
+ const run = this.options.manager.get(input.runId);
333
+ if (!run) throw new CodeHostError("Choose an owned completed run", 400);
334
+ if (typeof input.title !== "string" || !input.title.trim() || input.title.length > 200 || typeof input.body !== "string" || input.body.length > 2e4) throw new CodeHostError("Write a PR title (1\u2013200 characters) and body (up to 20,000 characters)", 400);
335
+ const owned = await this.verified({ repo: run.repo, project: run.project }, input.runId, input.branch), session = this.options.integrations.codeHost(input.integration), targetBranch = branch(input.targetBranch), target = await this.repositoryTarget(session, targetBranch, signal);
336
+ await this.options.manager.withIdleRepository(run.repo, async () => {
337
+ await this.transport.fetch(run.repo, session, { head: target.target, target: target.target, sourceUrl: target.url, targetUrl: target.url, sourceRef: `refs/heads/${targetBranch}`, targetRef: `refs/heads/${targetBranch}` }, signal, () => this.authorize({ repo: run.repo, project: run.project }));
338
+ if ((await git(["merge-base", "--is-ancestor", target.target, owned.head], run.repo)).code || owned.head === target.target) throw new CodeHostError("The verified result must contain new commits descending from the exact current target. Update and verify the result before creating a PR.");
339
+ }, { kind: "code-host-create-preview" });
340
+ const creation = { id: randomBytes(20).toString("hex"), integration: input.integration, provider: session.config.provider, repo: run.repo, project: run.project, runId: run.id, branch: input.branch, head: owned.head, target: target.target, sourceBranch: `codeflow/pr/${randomBytes(10).toString("hex")}`, targetBranch, title: input.title.trim(), body: input.body, expiresAt: this.now() + 15 * 6e4 };
341
+ const value = { ...creation, key: "new", state: "creation-preview", trusted: false, discussions: [], discussionTruncated: false, policy: { allowed: false, reasons: ["New PR creation does not grant merge permission"], fingerprint: "" } };
342
+ for (const [id, old] of this.previews) if (old.value.expiresAt <= this.now()) this.previews.delete(id);
343
+ while (this.previews.size >= 50) this.previews.delete(this.previews.keys().next().value);
344
+ this.previews.set(creation.id, { value, session, refs: { head: creation.head, target: creation.target, sourceUrl: target.url, targetUrl: target.url, sourceRef: `refs/heads/${creation.sourceBranch}`, targetRef: `refs/heads/${targetBranch}` }, raw: { creation } });
345
+ return structuredClone(creation);
346
+ }
347
+ async create(id, signal) {
348
+ return this.action(id, "create", async (saved) => {
349
+ const creation = saved.raw.creation;
350
+ if (!creation) throw new CodeHostError("Select a server-issued new PR creation preview");
351
+ const owned = await this.verified(saved.value, creation.runId, creation.branch), target = await this.repositoryTarget(saved.session, creation.targetBranch, signal);
352
+ if (owned.head !== creation.head || target.target !== creation.target || target.url !== saved.refs.targetUrl) throw new CodeHostError("The owned result or target changed after PR preview");
353
+ this.previews.delete(id);
354
+ await this.transport.push(creation.repo, saved.session, { url: target.url, ref: saved.refs.sourceRef, expected: "", commit: creation.head, beforeEffect: () => this.authorize(saved.value) }, signal);
355
+ const description = `${creation.body}
356
+
357
+ Reviewed source commit: ${creation.head}
358
+ Observed target commit: ${creation.target}
359
+ Code-flow operation: ${id}`;
360
+ this.authorize(saved.value);
361
+ const gl = creation.provider === "gitlab", response = (await saved.session.request(`${target.path}/${gl ? "merge_requests" : "pullrequests"}`, { method: "POST", body: gl ? { source_branch: creation.sourceBranch, target_branch: creation.targetBranch, title: creation.title, description, remove_source_branch: false } : { title: creation.title, description, source: { branch: { name: creation.sourceBranch } }, destination: { branch: { name: creation.targetBranch } }, close_source_branch: false }, signal })).data;
362
+ const key = String(gl ? response?.iid : response?.id);
363
+ number(key);
364
+ const actual = await this.read(saved.session, key, signal);
365
+ if (actual.refs.head !== creation.head || actual.refs.target !== creation.target || actual.sourceBranch !== creation.sourceBranch || actual.targetBranch !== creation.targetBranch) throw new CodeHostError("The PR was created, but its source or target no longer matches the preview. Inspect the provider before another action.", 409, true);
366
+ return { key, url: actual.url, commit: creation.head, sourceBranch: creation.sourceBranch, detail: "Provider confirmed the new PR and exact observed source and target. Creating a PR does not grant merge permission.", refreshRequired: true };
367
+ }, (result) => result.commit);
368
+ }
369
+ async pushPreview(id, runId, selected) {
370
+ const saved = this.get(id);
371
+ await this.current(saved);
372
+ const result = await this.owned(saved, runId, selected, false);
373
+ saved.value.push = { id: randomBytes(20).toString("hex"), runId, branch: selected, commit: result.head, expected: saved.value.head };
374
+ return structuredClone(saved.value.push);
375
+ }
376
+ async push(id, planId, signal) {
377
+ return this.action(id, "push", async (saved) => {
378
+ const plan = saved.value.push;
379
+ if (!plan || plan.id !== planId) throw new CodeHostError("Review a current pinned push preview first");
380
+ delete saved.value.push;
381
+ await this.current(saved, signal);
382
+ const owned = await this.owned(saved, plan.runId, plan.branch, false);
383
+ if (owned.head !== plan.commit) throw new CodeHostError("The owned result changed after push preview");
384
+ if (saved.session.config.provider === "gitlab" && saved.raw.source_project_id !== saved.raw.target_project_id) throw new CodeHostError("Fork pushes require an explicitly configured source repository integration");
385
+ const url = saved.session.config.provider === "gitlab" ? saved.refs.targetUrl : saved.refs.sourceUrl;
386
+ const result = await this.transport.push(saved.value.repo, saved.session, { url, ref: `refs/heads/${saved.value.sourceBranch}`, expected: plan.expected, commit: plan.commit, beforeEffect: () => this.authorize(saved.value) }, signal);
387
+ return { ...result, refreshRequired: true };
388
+ }, (result) => result.commit);
389
+ }
390
+ async mergePreview(id, runId, selected, signal) {
391
+ const saved = this.get(id);
392
+ await this.current(saved, signal, true);
393
+ if (!saved.value.fetched) await this.fetch(id, signal);
394
+ const owned = await this.owned(saved, runId, selected, true);
395
+ return this.options.manager.withIdleRepository(saved.value.repo, async () => {
396
+ const validation = await validateMergeTarget(saved.value.repo, { branch: selected, into: saved.value.fetched.into, profile: owned.profile, setup: owned.run.loop.setup, container: { ...owned.run.loop.container ?? { image: "node:22" }, network: false }, signal });
397
+ if (validation.source !== saved.value.head || validation.target !== saved.value.target || !validation.tree || validation.conflicts.length || validation.report?.state !== "verified") throw new CodeHostError("The exact source and target merge candidate did not pass required isolated verification");
398
+ this.authorize(saved.value);
399
+ const commit2 = await this.transport.mergeCommit(saved.value.repo, { source: validation.source, target: validation.target, tree: validation.tree }, signal);
400
+ saved.value.merge = { id: randomBytes(20).toString("hex"), runId, branch: selected, source: validation.source, target: validation.target, tree: validation.tree, commit: commit2, verifiedAt: this.now() };
401
+ return structuredClone(saved.value.merge);
402
+ }, { kind: "code-host-merge-validation" });
403
+ }
404
+ async merge(id, planId, signal) {
405
+ return this.action(id, "merge", async (saved) => {
406
+ const plan = saved.value.merge;
407
+ if (!plan || plan.id !== planId) throw new CodeHostError("Review a current pinned merge preview first");
408
+ delete saved.value.merge;
409
+ await this.current(saved, signal, true);
410
+ const owned = await this.owned(saved, plan.runId, plan.branch, true);
411
+ if (owned.head !== plan.source) throw new CodeHostError("The validated merge identity changed");
412
+ const tree = await git(["show", "-s", "--format=%T %P", plan.commit], saved.value.repo);
413
+ if (tree.code || tree.stdout.trim() !== `${plan.tree} ${plan.target} ${plan.source}`) throw new CodeHostError("The validated merge tree or parent identities changed");
414
+ const result = await this.transport.push(saved.value.repo, saved.session, { url: saved.refs.targetUrl, ref: saved.refs.targetRef, expected: plan.target, commit: plan.commit, beforeEffect: () => this.authorize(saved.value) }, signal);
415
+ let state;
416
+ try {
417
+ state = (await this.read(saved.session, saved.value.key, signal)).state;
418
+ } catch {
419
+ }
420
+ return { ...result, providerState: state, detail: "Pinned Git merge pushed with an exact target lease. Provider PR closure is separate and may remain open. No native merge queue was used.", refreshRequired: true };
421
+ }, (result) => result.commit);
422
+ }
423
+ async review(id, input, signal) {
424
+ return this.action(id, "review", async (saved) => {
425
+ const info = await this.current(saved, signal), gl = saved.session.config.provider === "gitlab";
426
+ const body = text(input.body, 2e4).trim();
427
+ let path, method = "POST", payload;
428
+ if (["comment", "reply"].includes(input.action) && (!body || input.body.length > 2e4)) throw new CodeHostError("Write a review comment of 1\u201320,000 characters", 400);
429
+ const discussion = input.discussionId ? saved.value.discussions.find((value) => value.id === input.discussionId && value.revision === input.revision) : void 0;
430
+ if (["reply", "resolve", "reopen"].includes(input.action)) {
431
+ if (!discussion || saved.value.discussionTruncated) throw new CodeHostError("Select a complete current discussion from this server preview");
432
+ const current = await this.discussions(saved.session, info, signal);
433
+ if (current.truncated || current.discussions.find((value) => value.id === discussion.id)?.revision !== discussion.revision) throw new CodeHostError("The discussion changed. Refresh before replying or resolving.");
434
+ }
435
+ if (input.action === "approve" || input.action === "unapprove") {
436
+ if (!gl) throw new CodeHostError("Bitbucket does not offer a commit-bound approval precondition. Use a commit-scoped review comment or the provider's native review UI.", 400);
437
+ path = `${info.root}/${input.action}`;
438
+ payload = input.action === "approve" ? { sha: saved.value.head } : {};
439
+ } else if (input.action === "resolve" || input.action === "reopen") {
440
+ if (!discussion?.canResolve) throw new CodeHostError("This discussion cannot be resolved with the available provider capability");
441
+ path = gl ? `${info.root}/discussions/${discussion.id}` : `${info.root}/comments/${discussion.id}/resolve`;
442
+ method = gl ? "PUT" : input.action === "resolve" ? "POST" : "DELETE";
443
+ payload = gl ? { resolved: input.action === "resolve" } : void 0;
444
+ } else if (input.action === "reply") {
445
+ path = gl ? `${info.root}/discussions/${discussion.id}/notes` : `${info.root}/comments`;
446
+ payload = gl ? { body } : { content: { raw: body }, parent: { id: Number(discussion.id) } };
447
+ } else if (input.action === "comment") {
448
+ path = `${info.root}/${gl ? "discussions" : "comments"}`;
449
+ const scopedBody = `[Reviewed source commit ${saved.value.head}; target ${saved.value.target}]
450
+
451
+ ${body}`;
452
+ payload = gl ? { body: scopedBody } : { content: { raw: scopedBody } };
453
+ if (input.path !== void 0 || input.line !== void 0) {
454
+ if (!saved.value.fetched || typeof input.path !== "string" || !Number.isSafeInteger(input.line) || input.line < 1 || input.line > 1e7 || input.path.startsWith("/") || input.path.split("/").some((part) => part === "..") || /[\x00-\x1f]/.test(input.path)) throw new CodeHostError("Choose a fetched changed path and a valid new-file line", 400);
455
+ const changed2 = await git(["diff", "--name-only", "-z", saved.value.fetched.base, saved.value.head, "--"], saved.value.repo);
456
+ if (!changed2.stdout.split("\0").includes(input.path)) throw new CodeHostError("Inline review path is absent from the pinned diff", 400);
457
+ if (gl) {
458
+ if (info.raw.diff_refs?.head_sha !== saved.value.head || info.raw.diff_refs?.base_sha !== saved.value.fetched.base || !SHA.test(info.raw.diff_refs?.start_sha)) throw new CodeHostError("GitLab inline diff identities are not current");
459
+ payload.position = { position_type: "text", base_sha: info.raw.diff_refs.base_sha, head_sha: saved.value.head, start_sha: info.raw.diff_refs.start_sha, new_path: input.path, old_path: input.path, new_line: input.line };
460
+ } else payload.inline = { path: input.path, to: input.line };
461
+ }
462
+ } else throw new CodeHostError("Choose an explicit supported review action", 400);
463
+ this.authorize(saved.value);
464
+ this.previews.delete(id);
465
+ await saved.session.request(path, { method, body: payload, signal });
466
+ let changed = false;
467
+ try {
468
+ const current = await this.read(saved.session, saved.value.key, signal);
469
+ changed = current.refs.head !== saved.value.head || current.refs.target !== saved.value.target;
470
+ } catch {
471
+ changed = true;
472
+ }
473
+ return { applied: true, refreshRequired: true, detail: changed ? "Provider confirmed the request, but the PR head or target changed or could not be rechecked. Comments label the observed commit; Bitbucket inline anchors are not an atomic commit precondition. Refresh before further review." : "Provider confirmed the request. Comments label the observed commit; Bitbucket inline anchors are not an atomic commit precondition. Refresh to inspect current discussion state." };
474
+ });
475
+ }
476
+ }
477
+ export {
478
+ CodeHostError,
479
+ CodeHosts,
480
+ disableRestoredCodeHostActions,
481
+ validateCodeHostDocument
482
+ };