@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 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: bridgeVersion(),
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 result = await driver.run({
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
- await client.updateAttempt(taskId, { phase: "terminal_pending", terminal });
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
- const bootstrapResult = bootstrap(workspace);
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 locked JavaScript dependencies before a Conduit-managed checkout advertises readiness. */
13
+ /** Install dependencies with the repository's own committed package-manager identity. */
14
14
  export function bootstrapManagedWorkspace(workspace, run = defaultRunner) {
15
- if (!existsSync(join(workspace, "package.json")))
15
+ const packagePath = join(workspace, "package.json");
16
+ if (!existsSync(packagePath))
16
17
  return "not_applicable";
17
- if (!existsSync(join(workspace, "package-lock.json"))) {
18
- throw new Error("managed_workspace_bootstrap_package_lock_required");
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.23",
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": {