@miraland-labs/conduit-bridge 0.16.21 → 0.16.23

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
@@ -520,7 +520,7 @@ async function runner() {
520
520
  const applied = await maybeApplyOnShiftIntent({
521
521
  currentWorkspace: workspace,
522
522
  onShift: hb.on_shift,
523
- runInstall: async (nextWorkspace, repo) => {
523
+ runInstall: async (nextWorkspace, repo, managedRoot) => {
524
524
  let env;
525
525
  try {
526
526
  env = loadOpsEnv();
@@ -530,7 +530,14 @@ async function runner() {
530
530
  }
531
531
  await runOps("install", [], {
532
532
  env: env
533
- ? { ...env, CONDUIT_WORKSPACE: nextWorkspace, CONDUIT_REPO: repo }
533
+ ? {
534
+ ...env,
535
+ CONDUIT_WORKSPACE: nextWorkspace,
536
+ CONDUIT_REPO: repo,
537
+ CONDUIT_MANAGED_ROOT: managedRoot === undefined
538
+ ? env.CONDUIT_MANAGED_ROOT
539
+ : managedRoot ?? "",
540
+ }
534
541
  : undefined,
535
542
  });
536
543
  },
@@ -556,6 +563,18 @@ async function runner() {
556
563
  const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined });
557
564
  await heartbeat(client, config, workspace, currentBrief, preflight);
558
565
  },
566
+ beforeClaim: async (driverId) => {
567
+ // Never let the five-minute heartbeat cache span a CLI upgrade into a certified claim.
568
+ // Publish the fresh probe first; the Control Plane then remains the authority for the
569
+ // exact version frozen onto the claim response and event.
570
+ const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
571
+ const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined }, { force: true });
572
+ const hb = await heartbeat(client, config, workspace, currentBrief, preflight);
573
+ if (hb.staleBridge)
574
+ return false;
575
+ const lane = preflight.drivers.find((candidate) => candidate.id === driverId);
576
+ return preflight.ready && lane?.ready === true && typeof lane.version === "string" && lane.version.length > 0;
577
+ },
559
578
  heartbeatIntervalMs: intervalMs,
560
579
  }, running, {});
561
580
  }
package/dist/execution.js CHANGED
@@ -538,7 +538,9 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
538
538
  }
539
539
  if (!laneDriver || !driverId)
540
540
  break;
541
- const claimed = await claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds);
541
+ const claimed = await claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds, {
542
+ beforeClaim: supervision?.beforeClaim,
543
+ });
542
544
  if (!claimed)
543
545
  break;
544
546
  const taskId = claimed.taskId;
@@ -557,7 +559,7 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
557
559
  return progressed;
558
560
  }
559
561
  /** 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) {
562
+ export async function claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds, options = {}) {
561
563
  if (Object.keys(config.activeAttempts).length >= config.leaseCapacity)
562
564
  return null;
563
565
  const data = await client.request("/runner/v1/assignments");
@@ -607,6 +609,10 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
607
609
  console.error(`Assignment ${assignment.id} rejected before claim: ${rejection}`);
608
610
  return null;
609
611
  }
612
+ if (selectedDriverId && options.beforeClaim && !await options.beforeClaim(selectedDriverId)) {
613
+ console.warn(`Assignment ${assignment.id} held before claim: driver ${selectedDriverId} preflight is not ready`);
614
+ return null;
615
+ }
610
616
  console.log(`Claiming ${assignment.execution_kind} assignment ${assignment.id} (attempt ${assignment.attempt_id})`);
611
617
  const fuelMode = selectedDriverId ? resolveDriverFuel(config, selectedDriverId) : undefined;
612
618
  const fuelProvenance = selectedDriverId ? resolveDriverFuelProvenance(config, selectedDriverId) : undefined;
@@ -631,7 +637,9 @@ export async function executeNextAssignment(client, config, driver, workspace, b
631
637
  if (Object.keys(config.activeAttempts).length)
632
638
  return recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision);
633
639
  const preferredDriverId = Object.entries(DRIVERS).find(([, candidate]) => candidate === driver)?.[0] ?? null;
634
- const claimed = await claimNextAssignment(client, config, workspace, brief, preferredDriverId);
640
+ const claimed = await claimNextAssignment(client, config, workspace, brief, preferredDriverId, null, {
641
+ beforeClaim: supervision?.beforeClaim,
642
+ });
635
643
  if (!claimed)
636
644
  return false;
637
645
  const selectedDriver = claimedAssignmentDriver(driver, claimed.driverId);
@@ -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) {
@@ -47,6 +47,6 @@ export async function maybeApplyOnShiftIntent(input) {
47
47
  return false;
48
48
  }
49
49
  console.log(`Applying on-shift workspace intent: ${target}${repo ? ` (${repo})` : ""}`);
50
- await input.runInstall(target, repo);
50
+ await input.runInstall(target, repo, input.onShift?.managed_workspace_root);
51
51
  return true;
52
52
  }
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 8 reports the resolved lane fuel and credential provenance in heartbeat and claim, so
20
- // the control plane can freeze what will actually run before the agent spends tokens.
21
- export const BRIDGE_PROTOCOL_VERSION = 8;
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 = `${version.stdout}${version.stderr}`.trim().slice(0, 200) || null;
113
- if (version.code !== 0 || !`${version.stdout}${version.stderr}`.trim()) {
114
- return { issue: { code: "driver_missing", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly } };
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, ...(quota ? { quota } : {}) },
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.21",
3
+ "version": "0.16.23",
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": {