@miraland-labs/conduit-bridge 0.16.22 → 0.16.24
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/dist/cli.js +43 -6
- package/dist/execution-facts.js +127 -0
- package/dist/execution.js +43 -4
- package/dist/land-contract.js +0 -5
- package/dist/ops.js +4 -1
- package/dist/preflight.js +85 -12
- package/dist/service.js +2 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from
|
|
|
14
14
|
import { DRIVERS } from "./driver.js";
|
|
15
15
|
import { driversHeartbeatReport, driverLabel, heartbeatStatusForDrivers, isSupportedDriverId, listDriverLanes, onlineDriverIds, seedDriversFromDetection, seedDriverLanes, setDriverFuel, setDriversOnline, } from "./drivers.js";
|
|
16
16
|
import { buildWorkspaceBrief } from "./brief.js";
|
|
17
|
+
import { BOOTSTRAP_RESULTS, buildSovereignExecutionFacts } from "./execution-facts.js";
|
|
17
18
|
import { ensureCheckout } from "./checkout.js";
|
|
18
19
|
import { buildOnShiftIntentProof, maybeApplyOnShiftIntent } from "./on-shift-apply.js";
|
|
19
20
|
import { loadOpsEnv, OPS_VERBS, runOps } from "./ops.js";
|
|
@@ -37,6 +38,12 @@ function parseFuelSource(value) {
|
|
|
37
38
|
return value;
|
|
38
39
|
throw new Error("Fuel source must be conduit or local");
|
|
39
40
|
}
|
|
41
|
+
function parseBootstrapResult(value) {
|
|
42
|
+
const result = BOOTSTRAP_RESULTS.find((candidate) => candidate === value);
|
|
43
|
+
if (!result)
|
|
44
|
+
throw new Error(`Bootstrap result must be one of: ${BOOTSTRAP_RESULTS.join(", ")}`);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
40
47
|
async function connect() {
|
|
41
48
|
const { values } = parseArgs({ args: process.argv.slice(3), options: { url: { type: "string" }, code: { type: "string" }, fuel: { type: "string" } } });
|
|
42
49
|
if (!values.url || !values.code)
|
|
@@ -319,7 +326,7 @@ async function installService() {
|
|
|
319
326
|
let config = await loadConfig();
|
|
320
327
|
const { values } = parseArgs({ args: process.argv.slice(3), options: {
|
|
321
328
|
workspace: { type: "string" }, interval: { type: "string" }, "agent-timeout-minutes": { type: "string" },
|
|
322
|
-
"ensure-checkout": { type: "string" },
|
|
329
|
+
"ensure-checkout": { type: "string" }, "bootstrap-result": { type: "string" },
|
|
323
330
|
} });
|
|
324
331
|
if (!values.workspace) {
|
|
325
332
|
throw new Error(`Usage: ${bridgeUsage("install-service", "--workspace", "<repository-path>", "[--ensure-checkout <repository-url>]")} (workspace required; uses online driver lanes)`);
|
|
@@ -329,6 +336,9 @@ async function installService() {
|
|
|
329
336
|
throw new Error(`No online driver lanes. Run \`${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}\` first.`);
|
|
330
337
|
}
|
|
331
338
|
const workspace = resolve(values.workspace);
|
|
339
|
+
const bootstrapResult = values["bootstrap-result"]
|
|
340
|
+
? parseBootstrapResult(values["bootstrap-result"])
|
|
341
|
+
: undefined;
|
|
332
342
|
if (values["ensure-checkout"]) {
|
|
333
343
|
const result = await ensureCheckout(workspace, values["ensure-checkout"]);
|
|
334
344
|
console.log(result === "cloned"
|
|
@@ -340,6 +350,7 @@ async function installService() {
|
|
|
340
350
|
interval: values.interval,
|
|
341
351
|
agentTimeoutMinutes: values["agent-timeout-minutes"],
|
|
342
352
|
ensureCheckout: values["ensure-checkout"],
|
|
353
|
+
bootstrapResult,
|
|
343
354
|
});
|
|
344
355
|
console.log(`Installed Conduit Bridge v${bridgeVersion()} (protocol ${BRIDGE_PROTOCOL_VERSION}) runner service (${result.platform}): ${result.path}`);
|
|
345
356
|
console.log(`Online lanes: ${onlineDriverIds(config).join(", ")}. Toggle with \`${bridgeUsage("drivers", "online|offline", "…")}\`.`);
|
|
@@ -466,9 +477,13 @@ async function runner() {
|
|
|
466
477
|
const { values } = parseArgs({ args: process.argv.slice(3), options: {
|
|
467
478
|
workspace: { type: "string" }, interval: { type: "string" }, once: { type: "boolean" },
|
|
468
479
|
"agent-timeout-minutes": { type: "string" }, fuel: { type: "string" }, "ensure-checkout": { type: "string" },
|
|
480
|
+
"bootstrap-result": { type: "string" },
|
|
469
481
|
} });
|
|
470
482
|
let config = await loadConfig();
|
|
471
483
|
const fuelOverride = parseFuelSource(values.fuel);
|
|
484
|
+
const bootstrapResult = values["bootstrap-result"]
|
|
485
|
+
? parseBootstrapResult(values["bootstrap-result"])
|
|
486
|
+
: undefined;
|
|
472
487
|
if (fuelOverride) {
|
|
473
488
|
config.fuelSource = fuelOverride;
|
|
474
489
|
await saveConfigPrefs(config);
|
|
@@ -511,7 +526,7 @@ async function runner() {
|
|
|
511
526
|
const preflight = workspace
|
|
512
527
|
? await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined })
|
|
513
528
|
: unavailableWorkspacePreflight({ config });
|
|
514
|
-
const hb = await heartbeat(client, config, workspace, currentBrief, preflight);
|
|
529
|
+
const hb = await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
|
|
515
530
|
if (hb.staleBridge) {
|
|
516
531
|
console.error(`Bridge update required: running protocol ${BRIDGE_PROTOCOL_VERSION}; control plane requires protocol ${hb.requiredProtocol ?? "unknown"} (minimum Bridge ${hb.minimumVersion ?? "unknown"}). Reinstall the latest Bridge, then restart this runner.`);
|
|
517
532
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, Math.max(intervalMs, 60_000)));
|
|
@@ -548,7 +563,7 @@ async function runner() {
|
|
|
548
563
|
}
|
|
549
564
|
const intentProof = buildOnShiftIntentProof({ currentWorkspace: workspace, onShift: hb.on_shift });
|
|
550
565
|
if (intentProof) {
|
|
551
|
-
await heartbeat(client, config, workspace, currentBrief, preflight, intentProof);
|
|
566
|
+
await heartbeat(client, config, workspace, currentBrief, preflight, intentProof, bootstrapResult);
|
|
552
567
|
}
|
|
553
568
|
await renewLeases(client, config);
|
|
554
569
|
if (workspace && onlineDriverIds(config).length) {
|
|
@@ -561,7 +576,19 @@ async function runner() {
|
|
|
561
576
|
heartbeat: async () => {
|
|
562
577
|
const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
|
|
563
578
|
const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined });
|
|
564
|
-
await heartbeat(client, config, workspace, currentBrief, preflight);
|
|
579
|
+
await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
|
|
580
|
+
},
|
|
581
|
+
beforeClaim: async (driverId) => {
|
|
582
|
+
// Never let the five-minute heartbeat cache span a CLI upgrade into a certified claim.
|
|
583
|
+
// Publish the fresh probe first; the Control Plane then remains the authority for the
|
|
584
|
+
// exact version frozen onto the claim response and event.
|
|
585
|
+
const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
|
|
586
|
+
const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined }, { force: true });
|
|
587
|
+
const hb = await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
|
|
588
|
+
if (hb.staleBridge)
|
|
589
|
+
return false;
|
|
590
|
+
const lane = preflight.drivers.find((candidate) => candidate.id === driverId);
|
|
591
|
+
return preflight.ready && lane?.ready === true && typeof lane.version === "string" && lane.version.length > 0;
|
|
565
592
|
},
|
|
566
593
|
heartbeatIntervalMs: intervalMs,
|
|
567
594
|
}, running, {});
|
|
@@ -578,19 +605,29 @@ async function runner() {
|
|
|
578
605
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
|
|
579
606
|
}
|
|
580
607
|
}
|
|
581
|
-
async function heartbeat(client, config, workspacePath, brief, preflight, intentProof) {
|
|
608
|
+
async function heartbeat(client, config, workspacePath, brief, preflight, intentProof, bootstrapResult) {
|
|
582
609
|
const online = onlineDriverIds(config);
|
|
583
610
|
const status = heartbeatStatusForDrivers(online);
|
|
611
|
+
const version = bridgeVersion();
|
|
612
|
+
const executionFacts = await buildSovereignExecutionFacts({
|
|
613
|
+
workspace: workspacePath,
|
|
614
|
+
brief,
|
|
615
|
+
workspaceClean: preflight.workspace_clean,
|
|
616
|
+
bridgeVersion: version,
|
|
617
|
+
bridgeProtocol: BRIDGE_PROTOCOL_VERSION,
|
|
618
|
+
bootstrapResult,
|
|
619
|
+
});
|
|
584
620
|
const response = await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
|
|
585
621
|
status,
|
|
586
622
|
capabilities: config.capabilities,
|
|
587
623
|
lease_capacity: config.leaseCapacity,
|
|
588
624
|
workspace_path: workspacePath,
|
|
589
625
|
fuel_source: config.fuelSource === "local" ? "local" : "conduit",
|
|
590
|
-
bridge_version:
|
|
626
|
+
bridge_version: version,
|
|
591
627
|
bridge_protocol: BRIDGE_PROTOCOL_VERSION,
|
|
592
628
|
drivers: driversHeartbeatReport(config, config.activeAttempts),
|
|
593
629
|
preflight,
|
|
630
|
+
execution_facts: executionFacts,
|
|
594
631
|
...(intentProof ? { on_shift_intent_proof: intentProof } : {}),
|
|
595
632
|
...(brief ? { workspace_brief: brief } : {}),
|
|
596
633
|
}) });
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { normalizeRepositoryUrl } from "./brief.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
export const EXECUTION_CLASSES = ["observe", "observe_network", "publish_artifact", "verify", "mutate_repo"];
|
|
9
|
+
export const BOOTSTRAP_RESULTS = ["not_applicable", "not_run", "installed", "failed"];
|
|
10
|
+
/**
|
|
11
|
+
* Lockfiles are hashed, never uploaded. The set is intentionally explicit and root-scoped so a
|
|
12
|
+
* dependency change becomes a durable identity without moving repository contents onto Conduit.
|
|
13
|
+
*/
|
|
14
|
+
const LOCKFILE_NAMES = [
|
|
15
|
+
"package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml",
|
|
16
|
+
"Cargo.lock", "poetry.lock", "Pipfile.lock", "go.sum", "Gemfile.lock", "composer.lock",
|
|
17
|
+
];
|
|
18
|
+
/** Stable identity for the exact bounded verification command list used by Bridge. */
|
|
19
|
+
export function verificationCommandsDigest(commands) {
|
|
20
|
+
const normalized = [...new Set(commands.map((command) => command.trim()).filter(Boolean))];
|
|
21
|
+
if (!normalized.length)
|
|
22
|
+
return null;
|
|
23
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hash the root lockfiles in deterministic name order. Missing or unreadable lockfiles are simply
|
|
27
|
+
* absent from the identity; a workspace with no readable lockfile reports null rather than an
|
|
28
|
+
* invented digest.
|
|
29
|
+
*/
|
|
30
|
+
export async function lockfileDigest(workspace) {
|
|
31
|
+
const hash = createHash("sha256");
|
|
32
|
+
let found = false;
|
|
33
|
+
for (const name of LOCKFILE_NAMES) {
|
|
34
|
+
try {
|
|
35
|
+
const contents = await readFile(join(workspace, name));
|
|
36
|
+
hash.update(name);
|
|
37
|
+
hash.update("\0");
|
|
38
|
+
hash.update(contents);
|
|
39
|
+
hash.update("\0");
|
|
40
|
+
found = true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// A missing or unreadable optional lockfile contributes no claim.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return found ? hash.digest("hex") : null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Report whether a JavaScript workspace has dependencies installed. This is an observed state, not
|
|
50
|
+
* a promise that Bridge will run a package manager during execution. Managed installs already run
|
|
51
|
+
* `npm ci` before the service is advertised, so a present node_modules directory is the durable
|
|
52
|
+
* local evidence available on later heartbeats.
|
|
53
|
+
*/
|
|
54
|
+
export async function bootstrapResultForWorkspace(workspace) {
|
|
55
|
+
if (!workspace)
|
|
56
|
+
return "not_applicable";
|
|
57
|
+
try {
|
|
58
|
+
await access(join(workspace, "package.json"));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return "not_applicable";
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const lockfile = await Promise.any(LOCKFILE_NAMES.map(async (name) => {
|
|
65
|
+
await access(join(workspace, name));
|
|
66
|
+
return true;
|
|
67
|
+
}));
|
|
68
|
+
void lockfile;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return "not_run";
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const dependencies = await stat(join(workspace, "node_modules"));
|
|
75
|
+
return dependencies.isDirectory() ? "installed" : "not_run";
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return "not_run";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Read the source/attempt tree's Git cleanliness without returning its contents. */
|
|
82
|
+
export async function workspaceIsClean(workspace) {
|
|
83
|
+
try {
|
|
84
|
+
const result = await execFileAsync("git", ["-C", workspace, "status", "--porcelain", "--untracked-files=normal"], {
|
|
85
|
+
timeout: 15_000,
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
});
|
|
88
|
+
return result.stdout.trim().length === 0;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Read only the bounded HEAD identity retained when an attempt terminates. */
|
|
95
|
+
export async function finalCommitForWorkspace(workspace) {
|
|
96
|
+
try {
|
|
97
|
+
const result = await execFileAsync("git", ["-C", workspace, "rev-parse", "HEAD"], {
|
|
98
|
+
timeout: 15_000,
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
});
|
|
101
|
+
return commitOrNull(result.stdout.trim());
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function commitOrNull(value) {
|
|
108
|
+
return value && /^[0-9a-f]{40,64}$/i.test(value) ? value.toLowerCase() : null;
|
|
109
|
+
}
|
|
110
|
+
/** Build one self-contained heartbeat/attempt snapshot from existing Bridge observations. */
|
|
111
|
+
export async function buildSovereignExecutionFacts(input) {
|
|
112
|
+
const repository = input.brief?.repository?.trim();
|
|
113
|
+
return {
|
|
114
|
+
os: process.platform,
|
|
115
|
+
architecture: process.arch,
|
|
116
|
+
repository_fingerprint: repository ? normalizeRepositoryUrl(repository) : null,
|
|
117
|
+
claimed_head: commitOrNull(input.claimedHead ?? input.brief?.base_commit),
|
|
118
|
+
lockfile_digest: input.workspace ? await lockfileDigest(input.workspace) : null,
|
|
119
|
+
bootstrap_result: input.bootstrapResult ?? await bootstrapResultForWorkspace(input.workspace),
|
|
120
|
+
bridge_version: input.bridgeVersion,
|
|
121
|
+
bridge_protocol: input.bridgeProtocol,
|
|
122
|
+
execution_class: input.executionClass ?? null,
|
|
123
|
+
verification_commands_digest: verificationCommandsDigest(input.brief?.verification ?? []),
|
|
124
|
+
workspace_clean: input.workspaceClean,
|
|
125
|
+
final_commit: commitOrNull(input.finalCommit),
|
|
126
|
+
};
|
|
127
|
+
}
|
package/dist/execution.js
CHANGED
|
@@ -10,6 +10,9 @@ import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quaran
|
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
12
|
import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
|
|
13
|
+
import { buildSovereignExecutionFacts, bootstrapResultForWorkspace, finalCommitForWorkspace, workspaceIsClean } from "./execution-facts.js";
|
|
14
|
+
import { BRIDGE_PROTOCOL_VERSION } from "./preflight.js";
|
|
15
|
+
import { bridgeVersion } from "./version.js";
|
|
13
16
|
import { ensureDeliveryPullRequest } from "./ensure-pull-request.js";
|
|
14
17
|
import { ensureLandCommit } from "./ensure-land-commit.js";
|
|
15
18
|
import { AgentNoLandCommitError, agentNoLandCommitMessage, requiresLandCommit } from "./land-contract.js";
|
|
@@ -538,7 +541,9 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
|
|
|
538
541
|
}
|
|
539
542
|
if (!laneDriver || !driverId)
|
|
540
543
|
break;
|
|
541
|
-
const claimed = await claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds
|
|
544
|
+
const claimed = await claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds, {
|
|
545
|
+
beforeClaim: supervision?.beforeClaim,
|
|
546
|
+
});
|
|
542
547
|
if (!claimed)
|
|
543
548
|
break;
|
|
544
549
|
const taskId = claimed.taskId;
|
|
@@ -557,7 +562,7 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
|
|
|
557
562
|
return progressed;
|
|
558
563
|
}
|
|
559
564
|
/** Claim one assignment when under capacity; does not start the agent (multi-slot pump does). */
|
|
560
|
-
export async function claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds) {
|
|
565
|
+
export async function claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds, options = {}) {
|
|
561
566
|
if (Object.keys(config.activeAttempts).length >= config.leaseCapacity)
|
|
562
567
|
return null;
|
|
563
568
|
const data = await client.request("/runner/v1/assignments");
|
|
@@ -607,6 +612,10 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
|
|
|
607
612
|
console.error(`Assignment ${assignment.id} rejected before claim: ${rejection}`);
|
|
608
613
|
return null;
|
|
609
614
|
}
|
|
615
|
+
if (selectedDriverId && options.beforeClaim && !await options.beforeClaim(selectedDriverId)) {
|
|
616
|
+
console.warn(`Assignment ${assignment.id} held before claim: driver ${selectedDriverId} preflight is not ready`);
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
610
619
|
console.log(`Claiming ${assignment.execution_kind} assignment ${assignment.id} (attempt ${assignment.attempt_id})`);
|
|
611
620
|
const fuelMode = selectedDriverId ? resolveDriverFuel(config, selectedDriverId) : undefined;
|
|
612
621
|
const fuelProvenance = selectedDriverId ? resolveDriverFuelProvenance(config, selectedDriverId) : undefined;
|
|
@@ -631,7 +640,9 @@ export async function executeNextAssignment(client, config, driver, workspace, b
|
|
|
631
640
|
if (Object.keys(config.activeAttempts).length)
|
|
632
641
|
return recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision);
|
|
633
642
|
const preferredDriverId = Object.entries(DRIVERS).find(([, candidate]) => candidate === driver)?.[0] ?? null;
|
|
634
|
-
const claimed = await claimNextAssignment(client, config, workspace, brief, preferredDriverId
|
|
643
|
+
const claimed = await claimNextAssignment(client, config, workspace, brief, preferredDriverId, null, {
|
|
644
|
+
beforeClaim: supervision?.beforeClaim,
|
|
645
|
+
});
|
|
635
646
|
if (!claimed)
|
|
636
647
|
return false;
|
|
637
648
|
const selectedDriver = claimedAssignmentDriver(driver, claimed.driverId);
|
|
@@ -899,8 +910,20 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
899
910
|
normativeRefs: normativeMaterialized,
|
|
900
911
|
});
|
|
901
912
|
const resuming = Boolean(options.forceResumeSessionId);
|
|
913
|
+
const executionFacts = await buildSovereignExecutionFacts({
|
|
914
|
+
workspace: attemptWorkspace,
|
|
915
|
+
brief: attemptBrief,
|
|
916
|
+
workspaceClean: await workspaceIsClean(workspace),
|
|
917
|
+
bridgeVersion: bridgeVersion(),
|
|
918
|
+
bridgeProtocol: BRIDGE_PROTOCOL_VERSION,
|
|
919
|
+
executionClass,
|
|
920
|
+
claimedHead: executionContract.claimed_head ?? startCommit,
|
|
921
|
+
bootstrapResult: await bootstrapResultForWorkspace(workspace),
|
|
922
|
+
});
|
|
923
|
+
await client.updateAttempt(taskId, { executionFacts });
|
|
902
924
|
await client.attemptRequest(taskId, "progress", {
|
|
903
925
|
phase: diagnosis ? "inspecting" : "changing",
|
|
926
|
+
execution_facts: executionFacts,
|
|
904
927
|
message: resuming
|
|
905
928
|
? `Resuming ${driver.name} after Bridge restart with proven session and worktree.`
|
|
906
929
|
: diagnosis
|
|
@@ -1587,6 +1610,10 @@ async function submitFinishedDelivery(client, taskId) {
|
|
|
1587
1610
|
const active = client.attempt(taskId);
|
|
1588
1611
|
if (!active.delivery)
|
|
1589
1612
|
throw new Error("Finished agent run is missing its persisted Delivery data");
|
|
1613
|
+
const finalCommit = active.delivery.report.head_commit?.toLowerCase() ?? null;
|
|
1614
|
+
if (finalCommit && active.executionFacts) {
|
|
1615
|
+
await client.updateAttempt(taskId, { executionFacts: { ...active.executionFacts, final_commit: finalCommit } });
|
|
1616
|
+
}
|
|
1590
1617
|
const terminal = await prepareDelivery(client, active.attemptId, taskId, active.delivery.report);
|
|
1591
1618
|
await client.attemptRequest(taskId, "progress", { phase: "preparing_delivery", message: "Agent finished; submitting the Delivery.", idempotency_key: `bridge:progress:${active.attemptId}:delivery` });
|
|
1592
1619
|
return queueTerminal(client, taskId, terminal);
|
|
@@ -1862,7 +1889,19 @@ function retainDiagnosticWorktree(response) {
|
|
|
1862
1889
|
return response.retain_worktree === true || response.status === "invalid_lease";
|
|
1863
1890
|
}
|
|
1864
1891
|
async function queueTerminal(client, taskId, terminal) {
|
|
1865
|
-
|
|
1892
|
+
const active = client.attempt(taskId);
|
|
1893
|
+
let executionFacts = active.executionFacts;
|
|
1894
|
+
if (executionFacts && executionFacts.final_commit === null && active.worktreePath) {
|
|
1895
|
+
const finalCommit = await finalCommitForWorkspace(active.worktreePath);
|
|
1896
|
+
if (finalCommit) {
|
|
1897
|
+
executionFacts = { ...executionFacts, final_commit: finalCommit };
|
|
1898
|
+
await client.updateAttempt(taskId, { executionFacts });
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
const body = executionFacts && !terminal.body.execution_facts
|
|
1902
|
+
? { ...terminal.body, execution_facts: executionFacts }
|
|
1903
|
+
: terminal.body;
|
|
1904
|
+
await client.updateAttempt(taskId, { phase: "terminal_pending", terminal: { ...terminal, body } });
|
|
1866
1905
|
return flushTerminal(client, taskId);
|
|
1867
1906
|
}
|
|
1868
1907
|
export async function flushTerminal(client, taskId) {
|
package/dist/land-contract.js
CHANGED
|
@@ -19,11 +19,6 @@ export const AGENT_NO_LAND_COMMIT_PREFIX = "Agent did not land repository change
|
|
|
19
19
|
export function agentNoLandCommitMessage(baseCommit) {
|
|
20
20
|
return `${AGENT_NO_LAND_COMMIT_PREFIX}: the attempt branch has no commit after base ${baseCommit.slice(0, 12)}.`;
|
|
21
21
|
}
|
|
22
|
-
/** Legacy ensureDeliveryPullRequest text — kept recognizable for stored failures. */
|
|
23
|
-
export const LEGACY_NO_LAND_COMMIT_PATTERN = /no commit after base .*; this run produced no change to deliver/i;
|
|
24
|
-
export function isAgentNoLandCommitMessage(message) {
|
|
25
|
-
return message.startsWith(AGENT_NO_LAND_COMMIT_PREFIX) || LEGACY_NO_LAND_COMMIT_PATTERN.test(message);
|
|
26
|
-
}
|
|
27
22
|
export class AgentNoLandCommitError extends Error {
|
|
28
23
|
baseCommit;
|
|
29
24
|
constructor(baseCommit) {
|
package/dist/ops.js
CHANGED
|
@@ -570,6 +570,7 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
570
570
|
}
|
|
571
571
|
const workspace = resolve(expandOpsValue(installEnv.CONDUIT_WORKSPACE));
|
|
572
572
|
const drivers = await (deps.resolveInstallDrivers ?? resolveInstallDrivers)(installEnv, installArgv);
|
|
573
|
+
let bootstrapResult;
|
|
573
574
|
if (installEnv.CONDUIT_REPO) {
|
|
574
575
|
const checkout = deps.ensureCheckout ?? ensureCheckout;
|
|
575
576
|
const result = await checkout(workspace, installEnv.CONDUIT_REPO);
|
|
@@ -587,7 +588,7 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
587
588
|
&& !isAbsolute(managedRelative));
|
|
588
589
|
if (managedCheckout) {
|
|
589
590
|
const bootstrap = deps.bootstrapWorkspace ?? bootstrapManagedWorkspace;
|
|
590
|
-
|
|
591
|
+
bootstrapResult = bootstrap(workspace);
|
|
591
592
|
if (bootstrapResult === "installed")
|
|
592
593
|
console.log(`Installed locked project dependencies in ${workspace}`);
|
|
593
594
|
}
|
|
@@ -616,6 +617,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
616
617
|
const installArgs = ["install-service", "--workspace", workspace];
|
|
617
618
|
if (installEnv.CONDUIT_REPO)
|
|
618
619
|
installArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
|
|
620
|
+
if (bootstrapResult)
|
|
621
|
+
installArgs.push("--bootstrap-result", bootstrapResult);
|
|
619
622
|
console.log(`Bridge: v${bridgeVersion()} (protocol ${BRIDGE_PROTOCOL_VERSION})`);
|
|
620
623
|
console.log(`Installing runner for ${workspace} (drivers: ${drivers.join(", ")})`);
|
|
621
624
|
runBridge(installArgs);
|
package/dist/preflight.js
CHANGED
|
@@ -16,9 +16,31 @@ function modelsFingerprint(config) {
|
|
|
16
16
|
}).join(";");
|
|
17
17
|
return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
|
|
18
18
|
}
|
|
19
|
-
// Protocol
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// Protocol 9 adds the Bridge-observed driver interface cohort to every preflight lane.
|
|
20
|
+
export const BRIDGE_PROTOCOL_VERSION = 9;
|
|
21
|
+
/** Keep the version identity within the same bound accepted by the heartbeat schema. */
|
|
22
|
+
export const MAX_DRIVER_VERSION_LENGTH = 200;
|
|
23
|
+
/**
|
|
24
|
+
* Turn one successful CLI version probe into a stable, bounded identity.
|
|
25
|
+
*
|
|
26
|
+
* A probe is deliberately stricter than "some output": warnings, multiple lines, control
|
|
27
|
+
* sequences, and overlong output cannot be assigned to a particular CLI version. Returning null
|
|
28
|
+
* makes the lane unready, so a stale/truncated prefix can never be used for certification.
|
|
29
|
+
*/
|
|
30
|
+
export function parseDriverVersion(result) {
|
|
31
|
+
if (result.code !== 0)
|
|
32
|
+
return null;
|
|
33
|
+
const streams = [result.stdout, result.stderr]
|
|
34
|
+
.filter((value) => typeof value === "string")
|
|
35
|
+
.map((value) => value.trim())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
if (streams.length !== 1)
|
|
38
|
+
return null;
|
|
39
|
+
const version = streams[0];
|
|
40
|
+
if (version.length > MAX_DRIVER_VERSION_LENGTH || /[\r\n\u0000-\u001f\u007f]/u.test(version))
|
|
41
|
+
return null;
|
|
42
|
+
return version;
|
|
43
|
+
}
|
|
22
44
|
const execFileAsync = promisify(execFile);
|
|
23
45
|
async function defaultCommandRunner(command, args, cwd) {
|
|
24
46
|
try {
|
|
@@ -44,6 +66,55 @@ const DRIVER_COMMAND = {
|
|
|
44
66
|
function executableFor(driver) {
|
|
45
67
|
return driver === "codex" ? resolveCodexExecutable() : DRIVER_COMMAND[driver] ?? driver;
|
|
46
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Primary-driver interface surfaces used by the existing AgentDriver implementations. This is
|
|
71
|
+
* deliberately not a hash of all help prose: vendor copy edits must not manufacture a new cohort.
|
|
72
|
+
*/
|
|
73
|
+
const DRIVER_INTERFACE_CONTRACTS = {
|
|
74
|
+
"claude-code": {
|
|
75
|
+
adapter_revision: 1,
|
|
76
|
+
help_args: ["--help"],
|
|
77
|
+
required: ["outputformat", "allowedtools", "disallowedtools"],
|
|
78
|
+
observed: ["outputformat", "allowedtools", "disallowedtools", "permissionmode", "resume", "model"],
|
|
79
|
+
},
|
|
80
|
+
codex: {
|
|
81
|
+
adapter_revision: 1,
|
|
82
|
+
help_args: ["exec", "--help"],
|
|
83
|
+
required: ["json", "sandbox"],
|
|
84
|
+
observed: ["json", "sandbox", "model", "config"],
|
|
85
|
+
},
|
|
86
|
+
cursor: {
|
|
87
|
+
adapter_revision: 1,
|
|
88
|
+
help_args: ["--help"],
|
|
89
|
+
required: ["outputformat", "workspace"],
|
|
90
|
+
observed: ["outputformat", "workspace", "trust", "force", "resume", "model"],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
function normalizedHelpSurface(value) {
|
|
94
|
+
return value.toLowerCase().replace(/[-_\s]/g, "");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Fingerprint only what a bounded, non-spending interface probe can prove. It is not a claim that
|
|
98
|
+
* vendor semantics or model quality stayed identical; per-attempt gates and field outcomes remain
|
|
99
|
+
* authoritative for those facts.
|
|
100
|
+
*/
|
|
101
|
+
async function driverCompatibilityFingerprint(driver, workspace, run) {
|
|
102
|
+
const contract = DRIVER_INTERFACE_CONTRACTS[driver];
|
|
103
|
+
if (!contract)
|
|
104
|
+
return null;
|
|
105
|
+
const help = await run(executableFor(driver), contract.help_args, workspace);
|
|
106
|
+
if (help.code !== 0)
|
|
107
|
+
return null;
|
|
108
|
+
const surface = normalizedHelpSurface(`${help.stdout}\n${help.stderr}`);
|
|
109
|
+
if (!contract.required.every((signal) => surface.includes(signal)))
|
|
110
|
+
return null;
|
|
111
|
+
const features = contract.observed.map((signal) => [signal, surface.includes(signal)]);
|
|
112
|
+
return createHash("sha256").update(JSON.stringify({
|
|
113
|
+
driver,
|
|
114
|
+
adapter_revision: contract.adapter_revision,
|
|
115
|
+
features,
|
|
116
|
+
})).digest("hex");
|
|
117
|
+
}
|
|
47
118
|
async function localAuthenticationReady(driver, workspace, run) {
|
|
48
119
|
if (driver === "claude-code")
|
|
49
120
|
return hasClaudeLogin();
|
|
@@ -106,23 +177,25 @@ export async function runBridgePreflight(input, deps = {}) {
|
|
|
106
177
|
const diagnosisReadOnly = supportsReadOnlyDiagnosis(driver);
|
|
107
178
|
const fuel = resolveDriverFuel(input.config, driver);
|
|
108
179
|
if (localFuelOnlyDriver(driver) && fuel !== "local") {
|
|
109
|
-
return { issue: { code: "driver_fuel_mismatch", driver }, snapshot: { id: driver, version: null, ready: false, diagnosis_read_only: diagnosisReadOnly } };
|
|
180
|
+
return { issue: { code: "driver_fuel_mismatch", driver }, snapshot: { id: driver, version: null, ready: false, diagnosis_read_only: diagnosisReadOnly, compatibility_fingerprint: null } };
|
|
110
181
|
}
|
|
111
182
|
const version = await run(executableFor(driver), ["--version"], input.workspace);
|
|
112
|
-
const versionText =
|
|
113
|
-
if (
|
|
114
|
-
return { issue: { code: "driver_missing", driver }, snapshot: { id: driver, version:
|
|
183
|
+
const versionText = parseDriverVersion(version);
|
|
184
|
+
if (!versionText) {
|
|
185
|
+
return { issue: { code: "driver_missing", driver }, snapshot: { id: driver, version: null, ready: false, diagnosis_read_only: diagnosisReadOnly, compatibility_fingerprint: null } };
|
|
115
186
|
}
|
|
116
187
|
if (fuel === "local" && !await localAuthenticationReady(driver, input.workspace, run)) {
|
|
117
|
-
return { issue: { code: "driver_not_authenticated", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly } };
|
|
188
|
+
return { issue: { code: "driver_not_authenticated", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly, compatibility_fingerprint: null } };
|
|
118
189
|
}
|
|
119
190
|
// Reported, not probed: the window state was learned from the vendor's last refusal and read
|
|
120
191
|
// back through laneQuota, which retires a passed reset. Nothing is executed here, so a lane
|
|
121
192
|
// running dry costs the heartbeat nothing.
|
|
122
193
|
const quota = fuel === "local" ? laneQuota(input.config, driver) ?? undefined : undefined;
|
|
194
|
+
const compatibilityFingerprint = await driverCompatibilityFingerprint(driver, input.workspace, run);
|
|
123
195
|
return {
|
|
124
196
|
issue: null,
|
|
125
|
-
snapshot: { id: driver, version: versionText, ready: quota?.exhausted !== true, diagnosis_read_only: diagnosisReadOnly,
|
|
197
|
+
snapshot: { id: driver, version: versionText, ready: quota?.exhausted !== true, diagnosis_read_only: diagnosisReadOnly,
|
|
198
|
+
compatibility_fingerprint: compatibilityFingerprint, ...(quota ? { quota } : {}) },
|
|
126
199
|
};
|
|
127
200
|
}));
|
|
128
201
|
issues.push(...driverChecks.map((check) => check.issue).filter((issue) => issue !== null));
|
|
@@ -137,10 +210,10 @@ export async function runBridgePreflight(input, deps = {}) {
|
|
|
137
210
|
}
|
|
138
211
|
let cached = null;
|
|
139
212
|
/** Keep auth probes off the 15-second heartbeat hot path while still expiring readiness promptly. */
|
|
140
|
-
export async function cachedBridgePreflight(input) {
|
|
213
|
+
export async function cachedBridgePreflight(input, options = {}) {
|
|
141
214
|
const online = input.processOnlineIds?.length ? input.processOnlineIds : onlineDriverIds(input.config);
|
|
142
215
|
const key = JSON.stringify([input.workspace, input.expectedRepository ?? "", online, input.config.fuelSource, input.config.drivers]);
|
|
143
|
-
if (cached?.key === key && Date.now() - cached.at < 5 * 60_000)
|
|
216
|
+
if (!options.force && cached?.key === key && Date.now() - cached.at < 5 * 60_000)
|
|
144
217
|
return cached.report;
|
|
145
218
|
const report = await runBridgePreflight(input);
|
|
146
219
|
cached = { key, at: Date.now(), report };
|
|
@@ -157,7 +230,7 @@ export function unavailableWorkspacePreflight(input) {
|
|
|
157
230
|
ready: false,
|
|
158
231
|
checked_at: new Date().toISOString(),
|
|
159
232
|
workspace_clean: false,
|
|
160
|
-
drivers: online.map((id) => ({ id, version: null, ready: false, diagnosis_read_only: supportsReadOnlyDiagnosis(id) })),
|
|
233
|
+
drivers: online.map((id) => ({ id, version: null, ready: false, diagnosis_read_only: supportsReadOnlyDiagnosis(id), compatibility_fingerprint: null })),
|
|
161
234
|
models_fingerprint: modelsFingerprint(input.config),
|
|
162
235
|
issues,
|
|
163
236
|
};
|
package/dist/service.js
CHANGED
|
@@ -30,6 +30,8 @@ export function runnerProgramArguments(options = {}) {
|
|
|
30
30
|
args.push("--interval", options.interval);
|
|
31
31
|
if (options.agentTimeoutMinutes)
|
|
32
32
|
args.push("--agent-timeout-minutes", options.agentTimeoutMinutes);
|
|
33
|
+
if (options.bootstrapResult)
|
|
34
|
+
args.push("--bootstrap-result", options.bootstrapResult);
|
|
33
35
|
return args;
|
|
34
36
|
}
|
|
35
37
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.24",
|
|
4
4
|
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity / Grok Build agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|