@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.
- package/LICENSE +21 -0
- package/README.md +122 -2
- package/bin/codeflow.mjs +8 -0
- package/bin/launcher.mjs +127 -0
- package/dist/remote-host.json +1 -0
- package/licenses/inter.txt +92 -0
- package/licenses/preact.txt +21 -0
- package/licenses/signals-core.txt +21 -0
- package/licenses/signals.txt +21 -0
- package/licenses/typescript-notices.txt +193 -0
- package/licenses/typescript.txt +55 -0
- package/licenses/uqr.txt +22 -0
- package/licenses/xterm-fit.txt +19 -0
- package/licenses/xterm.txt +21 -0
- package/package.json +34 -4
- package/public/assets/browser-7XI52A6V.js +8 -0
- package/public/assets/changes-EYFPASEI.js +1 -0
- package/public/assets/chunk-7H3ZLTKA.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-LLMQG23J.js +163 -0
- package/public/assets/main-KIRM2OEK.js +1 -0
- package/public/assets/settings-QJZVW22Y.js +9 -0
- package/public/assets/styles-615EBB20.css +32 -0
- package/public/assets/terminal-A2FI2NAX.js +62 -0
- package/public/assets.json +1 -0
- package/public/bundle-meta.json +1 -0
- package/public/docs/.nojekyll +0 -0
- package/public/docs/accessibility.html +19 -0
- package/public/docs/acp-agents.html +117 -0
- package/public/docs/agent-containers.html +79 -0
- package/public/docs/api.html +1130 -0
- package/public/docs/app.js +342 -0
- package/public/docs/application-improvements.html +275 -0
- package/public/docs/architecture-explorer.html +19 -0
- package/public/docs/architecture.html +149 -0
- package/public/docs/auto-team.html +142 -0
- package/public/docs/automations.html +109 -0
- package/public/docs/checkpoint-review.html +37 -0
- package/public/docs/code-hosts.html +35 -0
- package/public/docs/code-intelligence.html +35 -0
- package/public/docs/configuration.html +346 -0
- package/public/docs/context-inspection.html +23 -0
- package/public/docs/coordinated-changes.html +28 -0
- package/public/docs/deferred-runs.html +25 -0
- package/public/docs/dependency-discovery.html +19 -0
- package/public/docs/documentation.html +84 -0
- package/public/docs/dynamic-teams-evaluation.html +174 -0
- package/public/docs/dynamic-teams-implementation-plan.html +882 -0
- package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
- package/public/docs/dynamic-teams-plan-review-0.html +204 -0
- package/public/docs/extensions.html +42 -0
- package/public/docs/external-sources.html +96 -0
- package/public/docs/filesystem-policy.html +26 -0
- package/public/docs/filtered-tasks.html +44 -0
- package/public/docs/font-license.txt +92 -0
- package/public/docs/getting-started.html +118 -0
- package/public/docs/github-review-threads.html +26 -0
- package/public/docs/github.html +157 -0
- package/public/docs/help.html +69 -0
- package/public/docs/index.html +77 -0
- package/public/docs/installation.html +74 -0
- package/public/docs/launch-profiles.html +48 -0
- package/public/docs/maintenance.html +60 -0
- package/public/docs/memory-claims.html +31 -0
- package/public/docs/memory-storage.html +58 -0
- package/public/docs/notifications.html +39 -0
- package/public/docs/patterns.html +195 -0
- package/public/docs/performance-results.html +210 -0
- package/public/docs/performance.html +166 -0
- package/public/docs/project-conventions.html +22 -0
- package/public/docs/project-first-navigation-plan.html +587 -0
- package/public/docs/project-teams-implementation-plan.html +772 -0
- package/public/docs/project-teams.html +99 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +61 -0
- package/public/docs/remote-workers.html +64 -0
- package/public/docs/review-inbox.html +26 -0
- package/public/docs/run-storage.html +23 -0
- package/public/docs/runs.html +141 -0
- package/public/docs/search.json +1 -0
- package/public/docs/security.html +133 -0
- package/public/docs/shared-projects.html +31 -0
- package/public/docs/ssh-projects.html +60 -0
- package/public/docs/style.css +768 -0
- package/public/docs/task-reruns.html +30 -0
- package/public/docs/team-collaboration.html +379 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +122 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/pty.py +273 -0
- package/src/acp-connections.js +110 -0
- package/src/adapters/acp-presets.js +37 -0
- package/src/adapters/acp-types.js +0 -0
- package/src/adapters/acp.js +328 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +223 -0
- package/src/adapters/agy.js +297 -0
- package/src/adapters/claude.js +220 -0
- package/src/adapters/codex-app-server.js +518 -0
- package/src/adapters/codex.js +299 -0
- package/src/adapters/errors.js +57 -0
- package/src/adapters/index.js +91 -0
- package/src/adapters/managed-permissions.js +37 -0
- package/src/adapters/model-discovery.js +100 -0
- package/src/adapters/process.js +418 -0
- package/src/adapters/team-tools.js +102 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-container.js +155 -0
- package/src/approvals.js +121 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-handoff.js +55 -0
- package/src/checkpoint-review.js +72 -0
- package/src/checks.js +303 -0
- package/src/code-host-git.js +102 -0
- package/src/code-hosts-api.js +99 -0
- package/src/code-hosts.js +482 -0
- package/src/code-intelligence-worker.mjs +452 -0
- package/src/code-intelligence.js +141 -0
- package/src/collab/api.js +365 -0
- package/src/collab/checkpoints.js +198 -0
- package/src/collab/connections.js +139 -0
- package/src/collab/host-execution.js +347 -0
- package/src/collab/hub-client.js +210 -0
- package/src/collab/link-api.js +229 -0
- package/src/collab/link.js +595 -0
- package/src/collab/model.js +496 -0
- package/src/collab/runner.js +1014 -0
- package/src/collab/store.js +1452 -0
- package/src/collab/terminals.js +361 -0
- package/src/collaboration-reviews.js +116 -0
- package/src/config.js +48 -0
- package/src/context-budget.js +14 -0
- package/src/context-inspection.js +204 -0
- package/src/context.js +154 -0
- package/src/conventions.js +283 -0
- package/src/coordinated-api.js +247 -0
- package/src/deferred-launch.js +82 -0
- package/src/deferred-runs.js +515 -0
- package/src/dependency-health.js +35 -0
- package/src/diff.js +43 -0
- package/src/docs-help.js +316 -0
- package/src/doctor.js +232 -0
- package/src/engine.js +4366 -0
- package/src/env.js +44 -0
- package/src/experience.js +50 -0
- package/src/extensions.js +144 -0
- package/src/filesystem-access.js +107 -0
- package/src/filesystem-policy.js +238 -0
- package/src/filtered-task-limits.js +93 -0
- package/src/filtered-tasks-api.js +53 -0
- package/src/filtered-tasks.js +576 -0
- package/src/fs.js +57 -0
- package/src/git.js +626 -0
- package/src/github-review-threads.js +181 -0
- package/src/github.js +538 -0
- package/src/http.js +69 -0
- package/src/improve-settings.js +38 -0
- package/src/improve.js +187 -0
- package/src/integrations.js +373 -0
- package/src/integrity.js +259 -0
- package/src/journey-api.js +77 -0
- package/src/journey-replay.js +65 -0
- package/src/journey-store.js +97 -0
- package/src/journeys.js +60 -0
- package/src/json.js +81 -0
- package/src/launch-profiles.js +292 -0
- package/src/learnings.js +80 -0
- package/src/log.js +39 -0
- package/src/maintenance.js +335 -0
- package/src/memory-analysis.js +182 -0
- package/src/memory-claims.js +153 -0
- package/src/memory-sqlite.js +236 -0
- package/src/memory.js +552 -0
- package/src/merge-validation.js +58 -0
- package/src/model-catalog.js +238 -0
- package/src/model-research.js +91 -0
- package/src/notes.js +176 -0
- package/src/notifications.js +413 -0
- package/src/patterns/auto-team-model.js +212 -0
- package/src/patterns/auto-team.js +659 -0
- package/src/patterns/council.js +199 -0
- package/src/patterns/debate.js +188 -0
- package/src/patterns/findings.js +141 -0
- package/src/patterns/index.js +19 -0
- package/src/patterns/loop.js +158 -0
- package/src/patterns/pipeline.js +125 -0
- package/src/patterns/prompts.js +181 -0
- package/src/patterns/race.js +147 -0
- package/src/patterns/schemas.js +40 -0
- package/src/patterns/solo.js +49 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/tdd.js +231 -0
- package/src/patterns/team-strategies.js +67 -0
- package/src/performance.js +43 -0
- package/src/persistence-status.js +35 -0
- package/src/phone-sessions.js +96 -0
- package/src/preview-overlay.js +383 -0
- package/src/preview.js +219 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-model.js +178 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +213 -0
- package/src/project-teams-api.js +36 -0
- package/src/project-teams.js +551 -0
- package/src/projects.js +286 -0
- package/src/provider-eligibility.js +9 -0
- package/src/readiness.js +98 -0
- package/src/remote-tls.js +66 -0
- package/src/remote-workers/bundles.js +198 -0
- package/src/remote-workers/client.js +332 -0
- package/src/remote-workers/durability.js +37 -0
- package/src/remote-workers/protocol.js +68 -0
- package/src/remote-workers/stdio.js +138 -0
- package/src/remote-workers/worker.js +304 -0
- package/src/remote.js +204 -0
- package/src/repo-config.js +201 -0
- package/src/repo-coordination.js +732 -0
- package/src/repo-tree.js +106 -0
- package/src/retention-policy.js +466 -0
- package/src/review-inbox.js +38 -0
- package/src/routing.js +49 -0
- package/src/run-analysis.js +111 -0
- package/src/run-cold-store.js +231 -0
- package/src/run-environment.js +15 -0
- package/src/run-history.js +91 -0
- package/src/run-log.js +160 -0
- package/src/run-permissions.js +110 -0
- package/src/run-projection.js +75 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +54 -0
- package/src/serve.js +138 -0
- package/src/server.js +2738 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +301 -0
- package/src/shared-api.js +239 -0
- package/src/shell.js +346 -0
- package/src/ship.js +344 -0
- package/src/slots.js +74 -0
- package/src/snapshot.js +388 -0
- package/src/source-context.js +17 -0
- package/src/sse-writer.js +70 -0
- package/src/ssh-projects/bootstrap.mjs +95 -0
- package/src/ssh-projects/config.js +45 -0
- package/src/ssh-projects/gateway.js +143 -0
- package/src/ssh-projects/manager.js +320 -0
- package/src/ssh-projects/team-autostart.mjs +93 -0
- package/src/ssh-projects/team-remove.mjs +69 -0
- package/src/ssh-projects/team-status.mjs +39 -0
- package/src/ssh-projects/transport.js +116 -0
- package/src/stats.js +139 -0
- package/src/store.js +544 -0
- package/src/stream.js +91 -0
- package/src/sweeps.js +241 -0
- package/src/tailscale.js +101 -0
- package/src/task-draft.js +23 -0
- package/src/team-evaluation.js +242 -0
- package/src/team-mailbox.js +160 -0
- package/src/team-memory.js +84 -0
- package/src/team-rerun-graph.js +15 -0
- package/src/team-reruns.js +130 -0
- package/src/team-scheduler.js +121 -0
- package/src/team-template-store.js +98 -0
- package/src/team-templates.js +117 -0
- package/src/team-tool-client.mjs +79 -0
- package/src/team-tools.js +176 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { open, realpath } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { HttpError, readJson, send } from "./http.js";
|
|
6
|
+
import { RUNS_DIR, flushWrites } from "./store.js";
|
|
7
|
+
import { StrictRunLogReader } from "./run-log.js";
|
|
8
|
+
import { git, repoRoot, resolveCommit } from "./git.js";
|
|
9
|
+
import { readRepoConfig } from "./repo-config.js";
|
|
10
|
+
import { validateVerificationProfile } from "./verification.js";
|
|
11
|
+
import { deferredProviderReadiness, sanitizeDeferredRequest } from "./deferred-runs.js";
|
|
12
|
+
import { CoordinatedChanges, CoordinationConflictError, CoordinationRejectedError } from "./repo-coordination.js";
|
|
13
|
+
const identity = (run) => run.coordination;
|
|
14
|
+
const sameIdentity = (left, right) => !!left && !!right && left.jobId === right.jobId && left.nodeId === right.nodeId && left.claimId === right.claimId && left.attempt === right.attempt;
|
|
15
|
+
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
16
|
+
const validRunId = (value) => typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}$/.test(value);
|
|
17
|
+
const safePolicy = (run) => run.trust === "safe" && !run.loop.copy?.length && !run.approvals.tools && !run.approvals.plan && !run.parentRunId && run.trigger?.via === "coordination" && !run.lockdownOnHost && (!run.lockdown || run.loop.container?.network === false);
|
|
18
|
+
async function persistCoordinatedReceipt(run, directory = RUNS_DIR, shipped) {
|
|
19
|
+
if (!validRunId(run.id) || !identity(run)) throw new Error("Invalid coordinated launch receipt");
|
|
20
|
+
await flushWrites();
|
|
21
|
+
const handle = await open(join(directory, `${run.id}.jsonl`), constants.O_RDWR | (constants.O_NOFOLLOW ?? 0));
|
|
22
|
+
try {
|
|
23
|
+
const info = await handle.stat();
|
|
24
|
+
if (!info.isFile() || !info.size) throw new Error("The coordinated run's initial record was not saved");
|
|
25
|
+
const buffer = Buffer.alloc(Math.min(info.size, 4 * 1024 * 1024)), { bytesRead } = await handle.read(buffer, 0, buffer.length, 0), end = buffer.subarray(0, bytesRead).indexOf(10);
|
|
26
|
+
if (end < 0) throw new Error("The coordinated initial record is incomplete or too large");
|
|
27
|
+
const first = JSON.parse(buffer.subarray(0, end).toString("utf8")), stored = first.type === "run" ? first.run : void 0;
|
|
28
|
+
if (!stored || stored.id !== run.id || stored.repo !== run.repo || stored.project !== run.project || stored.baseRef !== run.baseRef || !sameIdentity(stored.coordination, run.coordination) || !safePolicy(stored)) throw new Error("The saved coordinated identity, project, base or safe execution policy does not match");
|
|
29
|
+
if (shipped) {
|
|
30
|
+
const length = Math.min(info.size, 4 * 1024 * 1024), tail = Buffer.alloc(length), read = await handle.read(tail, 0, length, info.size - length), source = tail.subarray(0, read.bytesRead).toString("utf8");
|
|
31
|
+
if (!source.endsWith("\n")) throw new Error("The coordinated shipping record is incomplete");
|
|
32
|
+
const lines = source.split("\n");
|
|
33
|
+
if (info.size > length) lines.shift();
|
|
34
|
+
let reader = new StrictRunLogReader(run.id), latest;
|
|
35
|
+
for (const line of lines.filter(Boolean)) {
|
|
36
|
+
let event;
|
|
37
|
+
try {
|
|
38
|
+
event = reader.decode(JSON.parse(line));
|
|
39
|
+
} catch {
|
|
40
|
+
latest = void 0;
|
|
41
|
+
reader = new StrictRunLogReader(run.id);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (event?.type === "run") latest = event.run;
|
|
45
|
+
}
|
|
46
|
+
if (!latest || !sameIdentity(latest.coordination, run.coordination) || !latest.merges.some((merge) => merge.branch === shipped.branch && merge.into === shipped.into && merge.commit === shipped.commit)) throw new Error("The exact shipping receipt was not saved; inspect its local target before recovery");
|
|
47
|
+
}
|
|
48
|
+
await handle.sync();
|
|
49
|
+
} finally {
|
|
50
|
+
await handle.close();
|
|
51
|
+
}
|
|
52
|
+
if (process.platform !== "win32") {
|
|
53
|
+
const folder = await open(directory, "r");
|
|
54
|
+
try {
|
|
55
|
+
await folder.sync();
|
|
56
|
+
} finally {
|
|
57
|
+
await folder.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function createCoordinatedApi(options) {
|
|
62
|
+
const { manager, projects } = options, persist = options.persist ?? persistCoordinatedReceipt;
|
|
63
|
+
const authorize = (projectId, repo) => {
|
|
64
|
+
const project = typeof projectId === "string" ? projects.get(projectId) : void 0;
|
|
65
|
+
if (!project || !project.repos.includes(repo)) throw new HttpError(409, "This project or its explicit repository membership changed. Review the coordinated request before continuing.");
|
|
66
|
+
return project;
|
|
67
|
+
};
|
|
68
|
+
const canonicalRepo = async (request) => {
|
|
69
|
+
const repo = await realpath(await repoRoot(request.repo));
|
|
70
|
+
authorize(request.project, repo);
|
|
71
|
+
return repo;
|
|
72
|
+
};
|
|
73
|
+
const owned = (node) => {
|
|
74
|
+
authorize(node.run.project, node.run.repo);
|
|
75
|
+
const run = node.runId ? manager.get(node.runId) : void 0;
|
|
76
|
+
const expected = { jobId: node.jobId, nodeId: node.id, claimId: node.claimId, attempt: node.attempts };
|
|
77
|
+
return run && run.repo === node.run.repo && run.project === node.run.project && run.baseRef === node.baseCommit && sameIdentity(identity(run), expected) && safePolicy(run) ? run : void 0;
|
|
78
|
+
};
|
|
79
|
+
const preparedRequest = async (raw, project) => {
|
|
80
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new HttpError(400, "Choose a run configuration for each repository task");
|
|
81
|
+
const request = sanitizeDeferredRequest({ ...raw, project });
|
|
82
|
+
if (request.parentRunId) throw new HttpError(400, "Coordinated tasks cannot inherit parent runs");
|
|
83
|
+
if (request.source?.kind === "pr") throw new HttpError(400, "Coordinated changes pin local starting commits. Fetch and review the PR branch locally, then select that base explicitly.");
|
|
84
|
+
const repo = await canonicalRepo(request), configured = await readRepoConfig(repo);
|
|
85
|
+
if (configured.error) throw new HttpError(400, configured.error);
|
|
86
|
+
const check = request.loop.check ?? configured.config.check, setup = request.loop.setup ?? configured.config.setup;
|
|
87
|
+
const verification = request.loop.verification ? validateVerificationProfile(request.loop.verification) : check ? validateVerificationProfile({ name: "Coordinated task checks", checks: [{ id: "primary-check", kind: "custom", command: check, required: true, timeoutMs: (request.loop.checkTimeoutMin ?? 5) * 6e4 }] }) : void 0;
|
|
88
|
+
if (!verification || !verification.checks.some((gate) => gate.required)) throw new HttpError(400, "Each coordinated task needs at least one required verification command or profile");
|
|
89
|
+
const candidate = sanitizeDeferredRequest({ ...request, repo, loop: { ...request.loop, check: check ?? verification.checks.find((gate) => gate.required).command, ...setup ? { setup } : {}, verification, copy: [] } });
|
|
90
|
+
const built = await options.build(candidate);
|
|
91
|
+
authorize(project, repo);
|
|
92
|
+
return sanitizeDeferredRequest({ ...candidate, roles: built.roles, options: built.options, agentSettings: built.agentSettings, roleSettings: built.roleSettings, loop: { ...candidate.loop, ...built.loop, copy: [] } });
|
|
93
|
+
};
|
|
94
|
+
const validateResult = async (node, signal) => {
|
|
95
|
+
signal.throwIfAborted();
|
|
96
|
+
let run;
|
|
97
|
+
try {
|
|
98
|
+
run = owned(node);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return { status: "missing", reason: error.message };
|
|
101
|
+
}
|
|
102
|
+
if (!run) return { status: "missing", reason: "The run does not match this exact owned coordination claim, project and pinned repository base" };
|
|
103
|
+
if (run.status === "running" || run.followUps.some((follow) => follow.status === "pending")) return { status: "running" };
|
|
104
|
+
if (run.status !== "done") return { status: run.status === "cancelled" ? "cancelled" : "failed", reason: run.error ?? `The owned child is ${run.status}; it has no accepted verified result` };
|
|
105
|
+
const report = run.verification, branch = node.result?.branch ?? report?.branch;
|
|
106
|
+
if (!branch || !run.worktrees.some((tree) => tree.branch === branch) || !run.artifacts.some((artifact) => artifact.branch === branch) || !report || report.branch !== branch || report.state !== "verified" || !report.head || run.verdict === "failing") return { status: "failed", reason: "The owned result has no current published branch with successful engine verification" };
|
|
107
|
+
const configured = run.loop.verification;
|
|
108
|
+
if (!configured?.checks.some((gate) => gate.required) || digest(configured) !== digest(node.run.loop.verification) || configured.checks.some((gate) => gate.required && !report.checks.some((evidence) => evidence.check.id === gate.id && evidence.check.command === gate.command && evidence.diagnosis === "passed" && evidence.attempts.length && evidence.attempts.every((attempt) => attempt.ok && !attempt.timedOut && attempt.exitCode === 0))) || report.skipped.some((gate) => gate.check.required) || report.requirements?.some((requirement) => !requirement.covered)) return { status: "failed", reason: "Required pinned engine verification evidence is missing, changed, skipped, stale or unsuccessful" };
|
|
109
|
+
if (run.pattern === "auto-team" && (!run.team?.verification?.approved || !run.team.verification.reviewed || run.team.verification.head !== report.head)) return { status: "failed", reason: "The coordinated team result lacks its current independent accepted review" };
|
|
110
|
+
const coverage = await manager.reviewCoverage(run.id, branch);
|
|
111
|
+
signal.throwIfAborted();
|
|
112
|
+
authorize(node.run.project, node.run.repo);
|
|
113
|
+
if (coverage.head !== report.head || coverage.stale || await resolveCommit(run.repo, `refs/heads/${branch}`) !== report.head || run.verification !== report || run.status !== "done" || run.followUps.some((follow) => follow.status === "pending")) return { status: "failed", reason: "The result HEAD, verification or review changed during inspection" };
|
|
114
|
+
return { status: "verified", runId: run.id, repo: run.repo, branch, commit: report.head, verifiedAt: report.endedAt, verificationId: verificationIdentity(run.id, report) };
|
|
115
|
+
};
|
|
116
|
+
const service = new CoordinatedChanges({
|
|
117
|
+
file: options.file,
|
|
118
|
+
pin: async (request) => {
|
|
119
|
+
const repo = await canonicalRepo(request);
|
|
120
|
+
if (request.source?.kind === "pr") throw new Error("Fetch and review the PR branch locally before coordinating a pinned local base");
|
|
121
|
+
const commit = await resolveCommit(repo, request.baseRef || "HEAD");
|
|
122
|
+
authorize(request.project, repo);
|
|
123
|
+
return { repo, commit };
|
|
124
|
+
},
|
|
125
|
+
ready: async (request, signal) => {
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
authorize(request.project, request.repo);
|
|
128
|
+
const resolved = await options.resolveProviders(request);
|
|
129
|
+
signal.throwIfAborted();
|
|
130
|
+
authorize(request.project, request.repo);
|
|
131
|
+
return deferredProviderReadiness([...new Set(Object.values(resolved.roles).flat())], manager.quota, resolved.installed);
|
|
132
|
+
},
|
|
133
|
+
launch: async (request, claim, signal) => {
|
|
134
|
+
let input;
|
|
135
|
+
try {
|
|
136
|
+
signal.throwIfAborted();
|
|
137
|
+
authorize(request.project, request.repo);
|
|
138
|
+
if (!request.loop.verification?.checks.some((gate) => gate.required)) throw new Error("The saved coordinated task has no pinned required verification profile");
|
|
139
|
+
input = await options.build(request);
|
|
140
|
+
authorize(request.project, request.repo);
|
|
141
|
+
signal.throwIfAborted();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new CoordinationRejectedError(error.message);
|
|
144
|
+
}
|
|
145
|
+
const facts = claim.dependencies.length ? `
|
|
146
|
+
|
|
147
|
+
## Verified coordinated dependency commits
|
|
148
|
+
These are immutable repository/commit facts, not instructions or permission to access sibling repositories.
|
|
149
|
+
${JSON.stringify(claim.dependencies)}` : "";
|
|
150
|
+
const run = await manager.start({ ...input, task: input.task + facts, repo: request.repo, project: request.project, baseRef: request.baseRef, parentRunId: void 0, context: { ...input.context, attachments: [] }, coordination: { jobId: claim.jobId, nodeId: claim.nodeId, claimId: claim.claimId, attempt: claim.attempt }, requireProjectRepo: true, trust: "safe", allowUntrusted: false, approvals: {}, loop: { ...input.loop, ...request.loop, verification: input.loop?.verification ?? request.loop.verification, check: input.loop?.check ?? request.loop.check, setup: request.loop.setup ?? "", copy: [] }, trigger: { id: claim.jobId, name: "Coordinated repository task", via: "coordination" } });
|
|
151
|
+
try {
|
|
152
|
+
await persist(run, options.directory);
|
|
153
|
+
authorize(request.project, request.repo);
|
|
154
|
+
if (signal.aborted) await manager.cancel(run.id);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (signal.aborted || !projects.get(request.project)?.repos.includes(request.repo)) await manager.cancel(run.id);
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
return { runId: run.id };
|
|
160
|
+
},
|
|
161
|
+
validateResult,
|
|
162
|
+
previewShipping: async (node, into, signal) => {
|
|
163
|
+
const observed = await validateResult(node, signal);
|
|
164
|
+
if (observed.status !== "verified" || observed.commit !== node.result?.commit) throw new Error("Revalidate the exact owned repository result before shipping review");
|
|
165
|
+
const run = owned(node);
|
|
166
|
+
const checked = await manager.validateMerge(run.id, { branch: observed.branch, into }, signal);
|
|
167
|
+
authorize(node.run.project, node.run.repo);
|
|
168
|
+
signal.throwIfAborted();
|
|
169
|
+
if (!checked.tree || checked.source !== observed.commit || checked.report?.state !== "verified" || checked.conflicts.length) throw new Error("The exact local merge candidate did not pass its configured verification");
|
|
170
|
+
return { source: checked.source, target: checked.target, tree: checked.tree, into, verifiedAt: checked.at, verificationId: digest({ source: checked.source, target: checked.target, tree: checked.tree, report: checked.report }) };
|
|
171
|
+
},
|
|
172
|
+
ship: async (node, checked, signal) => {
|
|
173
|
+
const run = owned(node);
|
|
174
|
+
if (!run || !node.result) throw new Error("The exact owned coordinated run is unavailable");
|
|
175
|
+
signal.throwIfAborted();
|
|
176
|
+
return manager.withIdleRepository(run.repo, async () => {
|
|
177
|
+
authorize(node.run.project, node.run.repo);
|
|
178
|
+
signal.throwIfAborted();
|
|
179
|
+
const actual = run.mergeValidation;
|
|
180
|
+
if (!actual || actual.report?.state !== "verified" || actual.branch !== node.result.branch || actual.into !== checked.into || actual.source !== checked.source || actual.target !== checked.target || actual.tree !== checked.tree) throw new Error("The engine's verified source/target/tree no longer matches the reviewed shipping matrix");
|
|
181
|
+
const outcome = await manager.merge(run.id, { branch: node.result.branch, into: checked.into, requireValidation: true, archiveAfter: false });
|
|
182
|
+
if (!outcome.ok) throw new Error("The explicit repository merge did not finish successfully");
|
|
183
|
+
await persist(run, options.directory, { branch: node.result.branch, into: checked.into, commit: outcome.commit });
|
|
184
|
+
return { commit: outcome.commit, detail: "Merged this exact verified local candidate; no remote push was performed" };
|
|
185
|
+
}, { kind: "coordinated local merge", owner: node.jobId });
|
|
186
|
+
},
|
|
187
|
+
confirmShipping: async (node, checked, commit, signal) => {
|
|
188
|
+
const run = owned(node);
|
|
189
|
+
if (!run) return false;
|
|
190
|
+
const responses = await Promise.all([git(["show", "-s", "--format=%P", commit], run.repo, { signal, timeoutMs: 5e3 }), git(["rev-parse", "--verify", `${commit}^{tree}`], run.repo, { signal, timeoutMs: 5e3 }), git(["rev-parse", "--verify", `refs/heads/${checked.into}^{commit}`], run.repo, { signal, timeoutMs: 5e3 })]);
|
|
191
|
+
authorize(node.run.project, node.run.repo);
|
|
192
|
+
signal.throwIfAborted();
|
|
193
|
+
return responses.every((response) => !response.code) && responses[0].stdout.trim() === `${checked.target} ${checked.source}` && responses[1].stdout.trim() === checked.tree && responses[2].stdout.trim() === commit;
|
|
194
|
+
},
|
|
195
|
+
cancelRun: async (runId) => {
|
|
196
|
+
await manager.cancel(runId);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const create = async (body) => {
|
|
200
|
+
if (Object.keys(body).some((key) => !["project", "name", "nodes", "operationId"].includes(key)) || typeof body.project !== "string" || !Array.isArray(body.nodes) || body.nodes.length < 2 || body.nodes.length > 10) throw new HttpError(400, "Choose an explicit project and 2\u201310 configured repository tasks");
|
|
201
|
+
const nodes = [];
|
|
202
|
+
for (const node of body.nodes) {
|
|
203
|
+
if (!node || typeof node !== "object" || Array.isArray(node) || Object.keys(node).some((key) => !["id", "run", "dependsOn"].includes(key))) throw new HttpError(400, "Unsupported coordinated task fields");
|
|
204
|
+
nodes.push({ id: node.id, dependsOn: node.dependsOn, run: await preparedRequest(node.run, body.project) });
|
|
205
|
+
}
|
|
206
|
+
return service.create({ project: body.project, name: body.name, nodes, operationId: body.operationId });
|
|
207
|
+
};
|
|
208
|
+
const handle = async (req, res, parts) => {
|
|
209
|
+
try {
|
|
210
|
+
if (parts[1] !== "coordinated-changes") throw new HttpError(404, "Unknown coordinated change route");
|
|
211
|
+
const jobId = parts[2];
|
|
212
|
+
if (!jobId && req.method === "GET") return send(res, 200, await service.list());
|
|
213
|
+
if (!jobId && req.method === "POST") return send(res, 201, await create(await readJson(req)));
|
|
214
|
+
if (!jobId || req.method !== "POST") throw new HttpError(404, "Unknown coordinated change operation");
|
|
215
|
+
const body = await readJson(req);
|
|
216
|
+
if (parts.length === 4 && parts[3] === "cancel") return send(res, 200, await service.cancel(jobId, body.expectedRevision));
|
|
217
|
+
if (parts.length === 4 && parts[3] === "shipping-preview") return send(res, 200, await service.previewShipping(jobId, body.expectedRevision, body.targets));
|
|
218
|
+
if (parts.length === 6 && parts[3] === "nodes" && parts[4]) {
|
|
219
|
+
const nodeId = parts[4];
|
|
220
|
+
if (parts[5] === "retry") return send(res, 200, await service.retryNode(jobId, nodeId, body.expectedRevision, { acknowledgeUnknown: body.acknowledgeUnknown === true }));
|
|
221
|
+
if (parts[5] === "attach") return send(res, 200, await service.attach(jobId, nodeId, body.expectedRevision, body.runId));
|
|
222
|
+
if (parts[5] === "ship") return send(res, 200, await service.shipNode(jobId, nodeId, body.expectedRevision, body.previewId));
|
|
223
|
+
if (parts[5] === "recover-shipping") return send(res, 200, await service.recoverShipping(jobId, nodeId, body.expectedRevision, body.commit));
|
|
224
|
+
}
|
|
225
|
+
throw new HttpError(404, "Unknown coordinated change operation");
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw error instanceof HttpError ? error : new HttpError(error instanceof CoordinationConflictError ? 409 : 400, error.message);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const recover = async () => {
|
|
231
|
+
for (const snapshot of await service.list()) for (const node of snapshot.nodes.filter((node2) => node2.status === "unknown" && node2.claimId)) {
|
|
232
|
+
const matches = manager.list().filter((run) => run.repo === node.run.repo && run.project === snapshot.project && run.baseRef === node.baseCommit && sameIdentity(run.coordination, { jobId: snapshot.id, nodeId: node.id, claimId: node.claimId, attempt: node.attempts }));
|
|
233
|
+
if (matches.length !== 1) continue;
|
|
234
|
+
await persist(matches[0], options.directory);
|
|
235
|
+
const latest = (await service.list()).find((job) => job.id === snapshot.id);
|
|
236
|
+
await service.attach(latest.id, node.id, latest.revision, matches[0].id);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
return { service, create, handle, recover };
|
|
240
|
+
}
|
|
241
|
+
function verificationIdentity(runId, report) {
|
|
242
|
+
return digest({ runId, head: report.head, branch: report.branch, state: report.state, startedAt: report.startedAt, endedAt: report.endedAt, checks: report.checks.map((evidence) => ({ check: evidence.check, diagnosis: evidence.diagnosis, attempts: evidence.attempts.map((attempt) => ({ ok: attempt.ok, exitCode: attempt.exitCode, timedOut: attempt.timedOut, durationMs: attempt.durationMs, tests: attempt.tests })) })), skipped: report.skipped, requirements: report.requirements });
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
createCoordinatedApi,
|
|
246
|
+
persistCoordinatedReceipt
|
|
247
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { DeferredRunRejectedError, DeferredRuns, deferredProviderReadiness } from "./deferred-runs.js";
|
|
5
|
+
import { RUNS_DIR, flushWrites } from "./store.js";
|
|
6
|
+
async function persistDeferredReceipt(run, directory = RUNS_DIR) {
|
|
7
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,199}$/.test(run.id) || !run.deferred) throw new Error("Invalid deferred launch receipt");
|
|
8
|
+
await flushWrites();
|
|
9
|
+
const handle = await open(join(directory, `${run.id}.jsonl`), constants.O_RDWR | (constants.O_NOFOLLOW ?? 0));
|
|
10
|
+
try {
|
|
11
|
+
const info = await handle.stat();
|
|
12
|
+
if (!info.isFile() || !info.size) throw new Error("The deferred run's initial record was not saved");
|
|
13
|
+
const buffer = Buffer.alloc(Math.min(info.size, 4 * 1024 * 1024));
|
|
14
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
15
|
+
const end = buffer.subarray(0, bytesRead).indexOf(10);
|
|
16
|
+
if (end < 0) throw new Error("The deferred run's initial record is incomplete or too large");
|
|
17
|
+
const event = JSON.parse(buffer.subarray(0, end).toString("utf8"));
|
|
18
|
+
const stored = event.type === "run" ? event.run : void 0;
|
|
19
|
+
if (stored?.id !== run.id || stored.repo !== run.repo || stored.deferred?.jobId !== run.deferred.jobId || stored.deferred?.claimId !== run.deferred.claimId || stored.deferred?.attempt !== run.deferred.attempt) throw new Error("The deferred run's saved launch identity does not match");
|
|
20
|
+
await handle.sync();
|
|
21
|
+
} finally {
|
|
22
|
+
await handle.close();
|
|
23
|
+
}
|
|
24
|
+
if (process.platform !== "win32") {
|
|
25
|
+
const folder = await open(directory, "r");
|
|
26
|
+
try {
|
|
27
|
+
await folder.sync();
|
|
28
|
+
} finally {
|
|
29
|
+
await folder.close();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function createDeferredRuns(host, options) {
|
|
34
|
+
return new DeferredRuns({
|
|
35
|
+
file: options.file,
|
|
36
|
+
ready: async (request, providers, signal) => {
|
|
37
|
+
signal.throwIfAborted();
|
|
38
|
+
const resolved = await options.resolveProviders(request);
|
|
39
|
+
signal.throwIfAborted();
|
|
40
|
+
const needed = [.../* @__PURE__ */ new Set([...providers, ...Object.values(resolved.roles).flat()])];
|
|
41
|
+
return deferredProviderReadiness(needed, request.loop.remoteWorker ? host.quotaForWorker?.(request.loop.remoteWorker.workerId) ?? {} : host.quota, resolved.installed);
|
|
42
|
+
},
|
|
43
|
+
launch: async (request, claim, signal) => {
|
|
44
|
+
let input;
|
|
45
|
+
try {
|
|
46
|
+
signal.throwIfAborted();
|
|
47
|
+
if (request.parentRunId && !host.get(request.parentRunId)) throw new Error("The parent run no longer exists; review and create another deferred request");
|
|
48
|
+
input = await options.build(request);
|
|
49
|
+
signal.throwIfAborted();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new DeferredRunRejectedError(error.message);
|
|
52
|
+
}
|
|
53
|
+
const run = await host.start({ ...input, deferred: claim, context: { ...input.context, attachments: [] }, trust: "safe", allowUntrusted: false, approvals: {}, loop: { ...input.loop, copy: [] }, trigger: { id: claim.jobId, name: "Deferred run", via: "deferred" } });
|
|
54
|
+
try {
|
|
55
|
+
await (options.persist ?? persistDeferredReceipt)(run);
|
|
56
|
+
if (signal.aborted) await host.cancel(run.id);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (signal.aborted) await host.cancel(run.id);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
return { runId: run.id };
|
|
62
|
+
},
|
|
63
|
+
cancelRun: async (id) => {
|
|
64
|
+
await host.cancel(id);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function recoverDeferredRuns(queue, host, persist = persistDeferredReceipt) {
|
|
69
|
+
for (const job of await queue.list()) {
|
|
70
|
+
if (job.status !== "unknown" || !job.claimId) continue;
|
|
71
|
+
const matches = host.list().filter((run2) => run2.deferred?.jobId === job.id && run2.deferred.claimId === job.claimId && run2.deferred.attempt === job.attempts && run2.repo === job.run.repo && (!job.run.project || run2.project === job.run.project));
|
|
72
|
+
if (matches.length !== 1) continue;
|
|
73
|
+
const run = matches[0];
|
|
74
|
+
await persist(run);
|
|
75
|
+
await queue.attach(job.id, job.revision, run.id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
createDeferredRuns,
|
|
80
|
+
persistDeferredReceipt,
|
|
81
|
+
recoverDeferredRuns
|
|
82
|
+
};
|