@miraland-labs/conduit-bridge 0.14.4 → 0.14.6

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
@@ -4,7 +4,7 @@ import { resolve, dirname, join as pathJoin } from "node:path";
4
4
  import { hostname, homedir, platform, userInfo } from "node:os";
5
5
  import { spawn } from "node:child_process";
6
6
  import { createInterface } from "node:readline/promises";
7
- import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
7
+ import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
8
8
  import { stdin as input, stdout as output } from "node:process";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { ConduitClient, ConduitRequestError } from "./client.js";
@@ -20,17 +20,8 @@ import { loadOpsEnv, OPS_VERBS, runOps } from "./ops.js";
20
20
  import { pumpExecutionSlots, renewLeases } from "./execution.js";
21
21
  import { installRunnerService, uninstallRunnerService } from "./service.js";
22
22
  import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, unavailableWorkspacePreflight } from "./preflight.js";
23
+ import { bridgeVersion } from "./version.js";
23
24
  const [command] = process.argv.slice(2);
24
- /** Read our own package version so every runner start logs exactly which build is live. */
25
- function bridgeVersion() {
26
- try {
27
- const packagePath = pathJoin(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
28
- return JSON.parse(readFileSync(packagePath, "utf8")).version ?? "unknown";
29
- }
30
- catch {
31
- return "unknown";
32
- }
33
- }
34
25
  /** Install-free form shown in help/output so clean laptops never need `conduit` on PATH. */
35
26
  const BRIDGE_NPX = "npx @miraland-labs/conduit-bridge";
36
27
  function bridgeUsage(...args) {
@@ -305,7 +296,7 @@ async function installService() {
305
296
  agentTimeoutMinutes: values["agent-timeout-minutes"],
306
297
  ensureCheckout: values["ensure-checkout"],
307
298
  });
308
- console.log(`Installed Conduit runner service (${result.platform}): ${result.path}`);
299
+ console.log(`Installed Conduit Bridge v${bridgeVersion()} (protocol ${BRIDGE_PROTOCOL_VERSION}) runner service (${result.platform}): ${result.path}`);
309
300
  console.log(`Online lanes: ${onlineDriverIds(config).join(", ")}. Toggle with \`${bridgeUsage("drivers", "online|offline", "…")}\`.`);
310
301
  console.log("The runner keeps executing after this terminal closes. Laptop sleep still pauses work.");
311
302
  }
@@ -475,6 +466,11 @@ async function runner() {
475
466
  ? await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined })
476
467
  : unavailableWorkspacePreflight({ config });
477
468
  const hb = await heartbeat(client, config, currentBrief, preflight);
469
+ if (hb.staleBridge) {
470
+ 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.`);
471
+ await new Promise((resolveSleep) => setTimeout(resolveSleep, Math.max(intervalMs, 60_000)));
472
+ continue;
473
+ }
478
474
  const applied = await maybeApplyOnShiftIntent({
479
475
  client,
480
476
  currentWorkspace: workspace,
@@ -535,10 +531,18 @@ async function heartbeat(client, config, brief, preflight) {
535
531
  preflight,
536
532
  ...(brief ? { workspace_brief: brief } : {}),
537
533
  }) });
534
+ const compatibility = response.bridge_compatibility && typeof response.bridge_compatibility === "object"
535
+ ? response.bridge_compatibility
536
+ : null;
538
537
  const onShift = response.on_shift && typeof response.on_shift === "object"
539
538
  ? response.on_shift
540
539
  : null;
541
- return { on_shift: onShift };
540
+ return {
541
+ on_shift: onShift,
542
+ staleBridge: response.stale_bridge === true,
543
+ ...(typeof compatibility?.minimum_version === "string" ? { minimumVersion: compatibility.minimum_version } : {}),
544
+ ...(typeof compatibility?.required_protocol === "number" ? { requiredProtocol: compatibility.required_protocol } : {}),
545
+ };
542
546
  }
543
547
  async function disconnect() {
544
548
  const { values } = parseArgs({ args: process.argv.slice(3), options: { yes: { type: "boolean", short: "y" } } });
package/dist/driver.js CHANGED
@@ -227,7 +227,18 @@ export function parseAgentReport(text, acceptance) {
227
227
  if (new Set(reported).size !== reported.length || acceptance.some((criterion) => !reported.includes(criterion))) {
228
228
  throw new Error("Agent report must include every acceptance criterion exactly once");
229
229
  }
230
- return { ...parsed.data, acceptance_results: parsed.data.acceptance_results.map((item) => ({ ...item, evidence_artifact_ids: [] })) };
230
+ // Missing evidence mappings cannot support a "met" claim. Preserve the evidence itself, but
231
+ // downgrade only the unsupported result to unknown so the existing quality loop can assess the
232
+ // completed work instead of throwing the whole implementation away.
233
+ const mappedCriteria = new Set(parsed.data.evidence.flatMap((item) => item.acceptance_criteria));
234
+ return {
235
+ ...parsed.data,
236
+ acceptance_results: parsed.data.acceptance_results.map((item) => ({
237
+ ...item,
238
+ status: item.status === "met" && !mappedCriteria.has(item.criterion) ? "unknown" : item.status,
239
+ evidence_artifact_ids: [],
240
+ })),
241
+ };
231
242
  }
232
243
  /**
233
244
  * Artifact publication receipts require `sha256:<64 hex>`. Agents often paste bare `shasum` output;
@@ -266,7 +277,11 @@ function sanitizeReportShape(raw) {
266
277
  const evidence = item;
267
278
  if ("details" in evidence)
268
279
  evidence.details = cleanList(evidence.details);
269
- if ("acceptance_criteria" in evidence)
280
+ // Experimental lanes have repeatedly omitted this list on otherwise-useful test evidence.
281
+ // An empty mapping asserts nothing; later logic downgrades any now-unsupported "met" result.
282
+ if (!("acceptance_criteria" in evidence) || evidence.acceptance_criteria == null)
283
+ evidence.acceptance_criteria = [];
284
+ else
270
285
  evidence.acceptance_criteria = cleanList(evidence.acceptance_criteria);
271
286
  // Agents emit null/"" for optional fields they have no value for — treat as absent.
272
287
  if ("digest" in evidence && (typeof evidence.digest !== "string" || evidence.digest.trim() === ""))
package/dist/execution.js CHANGED
@@ -912,7 +912,29 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
912
912
  catch (repairError) {
913
913
  parseError = repairError instanceof Error ? repairError.message : "Repaired delivery report was invalid";
914
914
  if (repairTurn >= maxRepairs) {
915
- await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair exhausted: ${parseError}`, retryable: false, idempotency_key: `bridge:delivery-repair-invalid:${active.attemptId}` } });
915
+ const detail = `Delivery report repair exhausted: ${parseError}`;
916
+ // Implementation finished; only its response envelope is invalid. Preserve the exact
917
+ // tree and identify this narrow condition so the control plane can prepare repair
918
+ // guidance instead of discarding the work and asking the owner to debug JSON.
919
+ retainAttemptWorktree = true;
920
+ const response = await queueTerminal(client, taskId, {
921
+ action: "fail",
922
+ body: {
923
+ error: detail,
924
+ retryable: false,
925
+ failure: {
926
+ code: "delivery_report_invalid",
927
+ class: "contract",
928
+ disposition: "rework",
929
+ responsible_party: "conduit",
930
+ message: "Conduit could not prepare a valid delivery report from the completed agent run.",
931
+ next_action: "Conductor will prepare bounded recovery guidance. You do not need to edit technical constraints.",
932
+ diagnostic_detail: detail,
933
+ },
934
+ idempotency_key: `bridge:delivery-repair-invalid:${active.attemptId}`,
935
+ },
936
+ });
937
+ retainAttemptWorktree = retainDiagnosticWorktree(response);
916
938
  console.error(`Assignment ${taskId} exhausted its report-only repair: ${redactSecrets(parseError)}`);
917
939
  const replyTail = previousReply.slice(-8_000);
918
940
  console.error(`Assignment ${taskId} repaired reply tail (${previousReply.length} chars total, redacted): ${redactSecrets(replyTail) || "<empty>"}`);
package/dist/ops.js CHANGED
@@ -11,11 +11,27 @@ import { ConduitClient } from "./client.js";
11
11
  import { loadConfig } from "./config.js";
12
12
  import { detectInstalledClients } from "./detect.js";
13
13
  import { driverIdsFromDetectedLabels } from "./drivers.js";
14
- import { describePreflightIssue, runBridgePreflight } from "./preflight.js";
14
+ import { BRIDGE_PROTOCOL_VERSION, describePreflightIssue, runBridgePreflight } from "./preflight.js";
15
+ import { bridgeVersion } from "./version.js";
15
16
  export const OPS_VERBS = [
16
17
  "connect", "install", "switch", "online", "offline", "status", "doctor", "disconnect", "uninstall",
17
18
  ];
18
19
  const LOCAL_FUEL_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity"]);
20
+ function bridgeVersionAtLeast(value, minimum) {
21
+ const parse = (input) => {
22
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(input);
23
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
24
+ };
25
+ const actual = parse(value);
26
+ const required = parse(minimum);
27
+ if (!actual || !required)
28
+ return false;
29
+ for (let index = 0; index < 3; index += 1) {
30
+ if (actual[index] !== required[index])
31
+ return actual[index] > required[index];
32
+ }
33
+ return true;
34
+ }
19
35
  export function defaultOpsEnvPath(home = homedir()) {
20
36
  return join(home, ".config", "conduit", "ops.env");
21
37
  }
@@ -177,6 +193,27 @@ export async function runOps(verb, argv = [], deps = {}) {
177
193
  return;
178
194
  }
179
195
  if (verb === "status") {
196
+ const packageVersion = bridgeVersion();
197
+ console.log(`Bridge: v${packageVersion} (protocol ${BRIDGE_PROTOCOL_VERSION})`);
198
+ try {
199
+ const config = await (deps.loadBridgeConfig ?? loadConfig)();
200
+ const controller = new AbortController();
201
+ const timeout = setTimeout(() => controller.abort(), 5_000);
202
+ let compatibility;
203
+ try {
204
+ compatibility = await new ConduitClient(config).request("/runner/v1/compatibility", { signal: controller.signal });
205
+ }
206
+ finally {
207
+ clearTimeout(timeout);
208
+ }
209
+ const minimumVersion = typeof compatibility.minimum_version === "string" ? compatibility.minimum_version : "unknown";
210
+ const requiredProtocol = typeof compatibility.required_protocol === "number" ? compatibility.required_protocol : "unknown";
211
+ const compatible = minimumVersion === "unknown" || (bridgeVersionAtLeast(packageVersion, minimumVersion) && (requiredProtocol === "unknown" || BRIDGE_PROTOCOL_VERSION >= requiredProtocol));
212
+ console.log(`Control plane: requires Bridge ${minimumVersion} (protocol ${requiredProtocol})${compatible ? "" : " — UPDATE REQUIRED"}`);
213
+ }
214
+ catch {
215
+ console.log("Control plane: compatibility check unavailable (showing local Bridge only)");
216
+ }
180
217
  if (env.loadedFrom) {
181
218
  console.log(`Env: ${env.loadedFrom}`);
182
219
  console.log(`URL: ${env.CONDUIT_URL || "(unset)"}`);
@@ -368,6 +405,7 @@ export async function runOps(verb, argv = [], deps = {}) {
368
405
  const runnerArgs = ["runner", "--workspace", workspace];
369
406
  if (installEnv.CONDUIT_REPO)
370
407
  runnerArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
408
+ console.log(`Bridge: v${bridgeVersion()} (protocol ${BRIDGE_PROTOCOL_VERSION})`);
371
409
  console.log("Windows: background install-service is not available.");
372
410
  console.log("Keep a terminal open and run:");
373
411
  console.log(` npx @miraland-labs/conduit-bridge@latest ${shellQuoteArgs(runnerArgs)}`);
@@ -377,6 +415,7 @@ export async function runOps(verb, argv = [], deps = {}) {
377
415
  const installArgs = ["install-service", "--workspace", workspace];
378
416
  if (installEnv.CONDUIT_REPO)
379
417
  installArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
418
+ console.log(`Bridge: v${bridgeVersion()} (protocol ${BRIDGE_PROTOCOL_VERSION})`);
380
419
  console.log(`Installing runner for ${workspace} (drivers: ${drivers.join(", ")})`);
381
420
  runBridge(installArgs);
382
421
  console.log("Done. Check with: npx @miraland-labs/conduit-bridge@latest ops status");
package/dist/preflight.js CHANGED
@@ -16,7 +16,9 @@ function modelsFingerprint(config) {
16
16
  }).join(";");
17
17
  return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
18
18
  }
19
- export const BRIDGE_PROTOCOL_VERSION = 4;
19
+ // Protocol 5 makes the lease-token claim contract explicit. Older Bridges may report a green
20
+ // preflight but cannot claim against the current control plane, so they must be admitted as stale.
21
+ export const BRIDGE_PROTOCOL_VERSION = 5;
20
22
  const execFileAsync = promisify(execFile);
21
23
  async function defaultCommandRunner(command, args, cwd) {
22
24
  try {
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** Read the version of the Bridge package that is actually running. */
5
+ export function bridgeVersion() {
6
+ try {
7
+ const packagePath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
8
+ return JSON.parse(readFileSync(packagePath, "utf8")).version ?? "unknown";
9
+ }
10
+ catch {
11
+ return "unknown";
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
4
4
  "description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {