@miraland-labs/conduit-bridge 0.16.23 → 0.16.25
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 +32 -7
- package/dist/client.js +24 -0
- package/dist/execution-facts.js +127 -0
- package/dist/execution.js +140 -3
- package/dist/ops.js +4 -1
- package/dist/service.js +2 -0
- package/dist/workspace-bootstrap.js +56 -9
- 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,7 @@ 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);
|
|
565
580
|
},
|
|
566
581
|
beforeClaim: async (driverId) => {
|
|
567
582
|
// Never let the five-minute heartbeat cache span a CLI upgrade into a certified claim.
|
|
@@ -569,7 +584,7 @@ async function runner() {
|
|
|
569
584
|
// exact version frozen onto the claim response and event.
|
|
570
585
|
const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
|
|
571
586
|
const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined }, { force: true });
|
|
572
|
-
const hb = await heartbeat(client, config, workspace, currentBrief, preflight);
|
|
587
|
+
const hb = await heartbeat(client, config, workspace, currentBrief, preflight, undefined, bootstrapResult);
|
|
573
588
|
if (hb.staleBridge)
|
|
574
589
|
return false;
|
|
575
590
|
const lane = preflight.drivers.find((candidate) => candidate.id === driverId);
|
|
@@ -590,19 +605,29 @@ async function runner() {
|
|
|
590
605
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
|
|
591
606
|
}
|
|
592
607
|
}
|
|
593
|
-
async function heartbeat(client, config, workspacePath, brief, preflight, intentProof) {
|
|
608
|
+
async function heartbeat(client, config, workspacePath, brief, preflight, intentProof, bootstrapResult) {
|
|
594
609
|
const online = onlineDriverIds(config);
|
|
595
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
|
+
});
|
|
596
620
|
const response = await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
|
|
597
621
|
status,
|
|
598
622
|
capabilities: config.capabilities,
|
|
599
623
|
lease_capacity: config.leaseCapacity,
|
|
600
624
|
workspace_path: workspacePath,
|
|
601
625
|
fuel_source: config.fuelSource === "local" ? "local" : "conduit",
|
|
602
|
-
bridge_version:
|
|
626
|
+
bridge_version: version,
|
|
603
627
|
bridge_protocol: BRIDGE_PROTOCOL_VERSION,
|
|
604
628
|
drivers: driversHeartbeatReport(config, config.activeAttempts),
|
|
605
629
|
preflight,
|
|
630
|
+
execution_facts: executionFacts,
|
|
606
631
|
...(intentProof ? { on_shift_intent_proof: intentProof } : {}),
|
|
607
632
|
...(brief ? { workspace_brief: brief } : {}),
|
|
608
633
|
}) });
|
package/dist/client.js
CHANGED
|
@@ -80,6 +80,30 @@ export class ConduitClient {
|
|
|
80
80
|
const active = this.attempt(taskId);
|
|
81
81
|
return this.request(`/runner/v1/tasks/${taskId}/${action}`, { method: "POST", body: JSON.stringify({ ...body, attempt_id: active.attemptId, lease_token: active.leaseToken }) });
|
|
82
82
|
}
|
|
83
|
+
async listInstructions(taskId, after = "") {
|
|
84
|
+
const active = this.attempt(taskId);
|
|
85
|
+
const query = new URLSearchParams({ attempt_id: active.attemptId, ...(after ? { after } : {}) });
|
|
86
|
+
const data = await this.request(`/runner/v1/tasks/${taskId}/instructions?${query}`);
|
|
87
|
+
return Array.isArray(data.instructions)
|
|
88
|
+
? data.instructions.flatMap((row) => {
|
|
89
|
+
if (!row || typeof row !== "object")
|
|
90
|
+
return [];
|
|
91
|
+
const instruction = row;
|
|
92
|
+
return typeof instruction.id === "string"
|
|
93
|
+
&& typeof instruction.payload_json === "string"
|
|
94
|
+
&& typeof instruction.created_at === "string"
|
|
95
|
+
? [{ id: instruction.id, payload_json: instruction.payload_json, created_at: instruction.created_at }]
|
|
96
|
+
: [];
|
|
97
|
+
})
|
|
98
|
+
: [];
|
|
99
|
+
}
|
|
100
|
+
async acknowledgeInstruction(taskId, instructionId) {
|
|
101
|
+
const active = this.attempt(taskId);
|
|
102
|
+
await this.attemptRequest(taskId, `instructions/${instructionId}/ack`, {
|
|
103
|
+
instruction_id: instructionId,
|
|
104
|
+
idempotency_key: `bridge:instruction-ack:${active.attemptId}:${instructionId}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
83
107
|
async updateAttempt(taskId, patch) {
|
|
84
108
|
const active = this.attempt(taskId);
|
|
85
109
|
for (const key of Object.keys(patch)) {
|
|
@@ -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";
|
|
@@ -408,6 +411,79 @@ export async function renewLeases(client, config) {
|
|
|
408
411
|
}
|
|
409
412
|
}
|
|
410
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Poll while the headless CLI owns the worktree, then hand owner guidance to its next session turn.
|
|
416
|
+
* Driver stdin is intentionally closed after the initial prompt, so pretending a mid-process write
|
|
417
|
+
* is portable would silently lose guidance on most lanes. The first safe common checkpoint is the
|
|
418
|
+
* driver's completed turn, before Bridge validates or submits its delivery.
|
|
419
|
+
*/
|
|
420
|
+
function pollCheckpointInstructions(client, taskId, state, intervalMs = 15_000) {
|
|
421
|
+
let stopped = false;
|
|
422
|
+
let inFlight = null;
|
|
423
|
+
const pending = new Map();
|
|
424
|
+
const poll = async () => {
|
|
425
|
+
const rows = await client.listInstructions(taskId, state.cursor);
|
|
426
|
+
for (const row of rows) {
|
|
427
|
+
state.cursor = `${row.created_at}|${row.id}`;
|
|
428
|
+
if (state.seen.has(row.id))
|
|
429
|
+
continue;
|
|
430
|
+
state.seen.add(row.id);
|
|
431
|
+
const parsed = parseCheckpointInstruction(row);
|
|
432
|
+
if (parsed && state.acceptedCount < 10 && state.acceptedCharacters + parsed.message.length <= 40_000) {
|
|
433
|
+
pending.set(parsed.id, parsed);
|
|
434
|
+
state.acceptedCount += 1;
|
|
435
|
+
state.acceptedCharacters += parsed.message.length;
|
|
436
|
+
}
|
|
437
|
+
else if (parsed) {
|
|
438
|
+
console.error(`Instruction ${parsed.id} exceeded the checkpoint guidance bound and was not acknowledged.`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
const runPoll = () => {
|
|
443
|
+
if (stopped || inFlight)
|
|
444
|
+
return;
|
|
445
|
+
inFlight = poll()
|
|
446
|
+
.catch((error) => console.error(`Instruction poll failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`))
|
|
447
|
+
.finally(() => { inFlight = null; });
|
|
448
|
+
};
|
|
449
|
+
runPoll();
|
|
450
|
+
const timer = setInterval(runPoll, intervalMs);
|
|
451
|
+
timer.unref?.();
|
|
452
|
+
return {
|
|
453
|
+
stop: async () => {
|
|
454
|
+
stopped = true;
|
|
455
|
+
clearInterval(timer);
|
|
456
|
+
await inFlight;
|
|
457
|
+
await poll().catch((error) => console.error(`Final instruction poll failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
|
|
458
|
+
return [...pending.values()];
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function parseCheckpointInstruction(row) {
|
|
463
|
+
try {
|
|
464
|
+
const payload = JSON.parse(row.payload_json);
|
|
465
|
+
if (payload.changes_scope === true) {
|
|
466
|
+
console.error(`Refusing scope-changing instruction ${row.id}; revise the approved initiative contract instead.`);
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
const message = typeof payload.message === "string" ? payload.message.trim() : "";
|
|
470
|
+
return message ? { id: row.id, message: message.slice(0, 20_000), createdAt: row.created_at } : null;
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function checkpointInstructionPrompt(instructions) {
|
|
477
|
+
return [
|
|
478
|
+
"OWNER INSTRUCTIONS AT CHECKPOINT",
|
|
479
|
+
"These messages refine the current run only. They do not amend approved scope, boundaries, acceptance criteria, or grants.",
|
|
480
|
+
...instructions.map((instruction) => `- ${instruction.message}`),
|
|
481
|
+
"",
|
|
482
|
+
"Apply the guidance within the approved contract, re-check the resulting work, then return a complete final report.",
|
|
483
|
+
"Return only one fenced ```json object with exactly this shape:",
|
|
484
|
+
agentReportTemplate,
|
|
485
|
+
].join("\n");
|
|
486
|
+
}
|
|
411
487
|
function resolveAttemptDriver(config, active, fallback) {
|
|
412
488
|
if (active.driverId && DRIVERS[active.driverId])
|
|
413
489
|
return DRIVERS[active.driverId];
|
|
@@ -907,8 +983,20 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
907
983
|
normativeRefs: normativeMaterialized,
|
|
908
984
|
});
|
|
909
985
|
const resuming = Boolean(options.forceResumeSessionId);
|
|
986
|
+
const executionFacts = await buildSovereignExecutionFacts({
|
|
987
|
+
workspace: attemptWorkspace,
|
|
988
|
+
brief: attemptBrief,
|
|
989
|
+
workspaceClean: await workspaceIsClean(workspace),
|
|
990
|
+
bridgeVersion: bridgeVersion(),
|
|
991
|
+
bridgeProtocol: BRIDGE_PROTOCOL_VERSION,
|
|
992
|
+
executionClass,
|
|
993
|
+
claimedHead: executionContract.claimed_head ?? startCommit,
|
|
994
|
+
bootstrapResult: await bootstrapResultForWorkspace(workspace),
|
|
995
|
+
});
|
|
996
|
+
await client.updateAttempt(taskId, { executionFacts });
|
|
910
997
|
await client.attemptRequest(taskId, "progress", {
|
|
911
998
|
phase: diagnosis ? "inspecting" : "changing",
|
|
999
|
+
execution_facts: executionFacts,
|
|
912
1000
|
message: resuming
|
|
913
1001
|
? `Resuming ${driver.name} after Bridge restart with proven session and worktree.`
|
|
914
1002
|
: diagnosis
|
|
@@ -968,7 +1056,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
968
1056
|
// turn already "finished" makes the agent reply conversationally without the report block.
|
|
969
1057
|
const resumeSessionId = options.forceResumeSessionId
|
|
970
1058
|
?? (reworkFeedback && task.repair_mode !== "briefed" ? config.sessions?.[taskId] : undefined);
|
|
971
|
-
const
|
|
1059
|
+
const runInput = {
|
|
972
1060
|
prompt,
|
|
973
1061
|
workspace: attemptWorkspace,
|
|
974
1062
|
grants,
|
|
@@ -982,7 +1070,40 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
982
1070
|
model: selection.model,
|
|
983
1071
|
fuel,
|
|
984
1072
|
fuelSource,
|
|
985
|
-
}
|
|
1073
|
+
};
|
|
1074
|
+
let result;
|
|
1075
|
+
let currentRunInput = runInput;
|
|
1076
|
+
const instructionPollState = {
|
|
1077
|
+
cursor: "",
|
|
1078
|
+
seen: new Set(),
|
|
1079
|
+
acceptedCount: 0,
|
|
1080
|
+
acceptedCharacters: 0,
|
|
1081
|
+
};
|
|
1082
|
+
let instructionsInPrompt = [];
|
|
1083
|
+
for (let round = 0;; round += 1) {
|
|
1084
|
+
const instructionPoll = diagnosis ? null : pollCheckpointInstructions(client, taskId, instructionPollState);
|
|
1085
|
+
let polled = [];
|
|
1086
|
+
try {
|
|
1087
|
+
result = await driver.run(currentRunInput);
|
|
1088
|
+
}
|
|
1089
|
+
finally {
|
|
1090
|
+
polled = await instructionPoll?.stop() ?? polled;
|
|
1091
|
+
}
|
|
1092
|
+
if (instructionsInPrompt.length > 0) {
|
|
1093
|
+
await Promise.all(instructionsInPrompt.map((instruction) => client.acknowledgeInstruction(taskId, instruction.id).catch((error) => console.error(`Instruction acknowledgement failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`))));
|
|
1094
|
+
}
|
|
1095
|
+
// One initial turn plus at most two bounded checkpoint turns. Guidance arriving after that
|
|
1096
|
+
// remains unacknowledged rather than creating an unbounded owner-driven agent loop.
|
|
1097
|
+
if (diagnosis || result.status !== "completed" || polled.length === 0 || round >= 2)
|
|
1098
|
+
break;
|
|
1099
|
+
instructionsInPrompt = polled;
|
|
1100
|
+
const followup = checkpointInstructionPrompt(instructionsInPrompt);
|
|
1101
|
+
currentRunInput = {
|
|
1102
|
+
...runInput,
|
|
1103
|
+
prompt: result.sessionId ? followup : `${prompt}\n\n${followup}`,
|
|
1104
|
+
resumeSessionId: result.sessionId ?? undefined,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
986
1107
|
if (result.sessionId)
|
|
987
1108
|
config.sessions = { ...config.sessions, [taskId]: result.sessionId };
|
|
988
1109
|
await learnDriverFuel(config, driver.name, fuelSource, result);
|
|
@@ -1595,6 +1716,10 @@ async function submitFinishedDelivery(client, taskId) {
|
|
|
1595
1716
|
const active = client.attempt(taskId);
|
|
1596
1717
|
if (!active.delivery)
|
|
1597
1718
|
throw new Error("Finished agent run is missing its persisted Delivery data");
|
|
1719
|
+
const finalCommit = active.delivery.report.head_commit?.toLowerCase() ?? null;
|
|
1720
|
+
if (finalCommit && active.executionFacts) {
|
|
1721
|
+
await client.updateAttempt(taskId, { executionFacts: { ...active.executionFacts, final_commit: finalCommit } });
|
|
1722
|
+
}
|
|
1598
1723
|
const terminal = await prepareDelivery(client, active.attemptId, taskId, active.delivery.report);
|
|
1599
1724
|
await client.attemptRequest(taskId, "progress", { phase: "preparing_delivery", message: "Agent finished; submitting the Delivery.", idempotency_key: `bridge:progress:${active.attemptId}:delivery` });
|
|
1600
1725
|
return queueTerminal(client, taskId, terminal);
|
|
@@ -1870,7 +1995,19 @@ function retainDiagnosticWorktree(response) {
|
|
|
1870
1995
|
return response.retain_worktree === true || response.status === "invalid_lease";
|
|
1871
1996
|
}
|
|
1872
1997
|
async function queueTerminal(client, taskId, terminal) {
|
|
1873
|
-
|
|
1998
|
+
const active = client.attempt(taskId);
|
|
1999
|
+
let executionFacts = active.executionFacts;
|
|
2000
|
+
if (executionFacts && executionFacts.final_commit === null && active.worktreePath) {
|
|
2001
|
+
const finalCommit = await finalCommitForWorkspace(active.worktreePath);
|
|
2002
|
+
if (finalCommit) {
|
|
2003
|
+
executionFacts = { ...executionFacts, final_commit: finalCommit };
|
|
2004
|
+
await client.updateAttempt(taskId, { executionFacts });
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
const body = executionFacts && !terminal.body.execution_facts
|
|
2008
|
+
? { ...terminal.body, execution_facts: executionFacts }
|
|
2009
|
+
: terminal.body;
|
|
2010
|
+
await client.updateAttempt(taskId, { phase: "terminal_pending", terminal: { ...terminal, body } });
|
|
1874
2011
|
return flushTerminal(client, taskId);
|
|
1875
2012
|
}
|
|
1876
2013
|
export async function flushTerminal(client, taskId) {
|
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/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
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
function defaultRunner(command, args, workspace) {
|
|
5
5
|
const result = spawnSync(command, args, {
|
|
@@ -10,17 +10,64 @@ function defaultRunner(command, args, workspace) {
|
|
|
10
10
|
});
|
|
11
11
|
return { status: result.status, ...(result.error ? { error: result.error } : {}) };
|
|
12
12
|
}
|
|
13
|
-
/** Install
|
|
13
|
+
/** Install dependencies with the repository's own committed package-manager identity. */
|
|
14
14
|
export function bootstrapManagedWorkspace(workspace, run = defaultRunner) {
|
|
15
|
-
|
|
15
|
+
const packagePath = join(workspace, "package.json");
|
|
16
|
+
if (!existsSync(packagePath))
|
|
16
17
|
return "not_applicable";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
const result = run("npm", ["ci", "--include=optional", "--no-audit", "--no-fund"], workspace);
|
|
18
|
+
const install = lockedInstall(workspace, packagePath);
|
|
19
|
+
const result = run(install.command, install.args, workspace);
|
|
21
20
|
if (result.error)
|
|
22
|
-
throw new Error(`managed_workspace_bootstrap_failed:${result.error.message}`);
|
|
21
|
+
throw new Error(`managed_workspace_bootstrap_failed:${install.id}:${result.error.message}`);
|
|
23
22
|
if (result.status !== 0)
|
|
24
|
-
throw new Error(`managed_workspace_bootstrap_failed:exit_${result.status ?? "unknown"}`);
|
|
23
|
+
throw new Error(`managed_workspace_bootstrap_failed:${install.id}:exit_${result.status ?? "unknown"}`);
|
|
25
24
|
return "installed";
|
|
26
25
|
}
|
|
26
|
+
function lockedInstall(workspace, packagePath) {
|
|
27
|
+
const declared = declaredPackageManager(packagePath);
|
|
28
|
+
const managers = [
|
|
29
|
+
existsSync(join(workspace, "package-lock.json")) || existsSync(join(workspace, "npm-shrinkwrap.json")) ? "npm" : null,
|
|
30
|
+
existsSync(join(workspace, "pnpm-lock.yaml")) ? "pnpm" : null,
|
|
31
|
+
existsSync(join(workspace, "yarn.lock")) ? "yarn" : null,
|
|
32
|
+
existsSync(join(workspace, "bun.lock")) || existsSync(join(workspace, "bun.lockb")) ? "bun" : null,
|
|
33
|
+
].filter((manager) => manager !== null);
|
|
34
|
+
if (managers.length === 0)
|
|
35
|
+
throw new Error("managed_workspace_bootstrap_lockfile_required");
|
|
36
|
+
if (declared && !managers.includes(declared)) {
|
|
37
|
+
throw new Error(`managed_workspace_bootstrap_package_manager_lockfile_mismatch:${declared}:${managers.join(",")}`);
|
|
38
|
+
}
|
|
39
|
+
const selected = declared ?? (managers.length === 1 ? managers[0] : null);
|
|
40
|
+
if (!selected)
|
|
41
|
+
throw new Error(`managed_workspace_bootstrap_lockfile_ambiguous:${managers.join(",")}`);
|
|
42
|
+
if (selected === "npm")
|
|
43
|
+
return { id: selected, command: "npm", args: ["ci", "--include=optional", "--no-audit", "--no-fund"] };
|
|
44
|
+
if (selected === "pnpm")
|
|
45
|
+
return { id: selected, command: "pnpm", args: ["install", "--frozen-lockfile"] };
|
|
46
|
+
if (selected === "bun")
|
|
47
|
+
return { id: selected, command: "bun", args: ["install", "--frozen-lockfile"] };
|
|
48
|
+
const modernYarn = existsSync(join(workspace, ".yarnrc.yml")) || declaredPackageManagerMajor(packagePath, "yarn") >= 2;
|
|
49
|
+
return { id: selected, command: "yarn", args: ["install", modernYarn ? "--immutable" : "--frozen-lockfile"] };
|
|
50
|
+
}
|
|
51
|
+
function declaredPackageManager(packagePath) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
54
|
+
const match = typeof parsed.packageManager === "string" ? /^(npm|pnpm|yarn|bun)@/.exec(parsed.packageManager.trim()) : null;
|
|
55
|
+
const id = match?.[1];
|
|
56
|
+
return id === "npm" || id === "pnpm" || id === "yarn" || id === "bun" ? id : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function declaredPackageManagerMajor(packagePath, manager) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
65
|
+
const match = typeof parsed.packageManager === "string"
|
|
66
|
+
? new RegExp(`^${manager}@(\\d+)`).exec(parsed.packageManager.trim())
|
|
67
|
+
: null;
|
|
68
|
+
return match ? Number(match[1]) : 0;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
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.25",
|
|
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": {
|