@miraland-labs/conduit-bridge 0.11.2 → 0.11.4

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connects a computer to one organization, claims work, and drives a local agent.
4
4
 
5
- **Package version:** `0.11.0` — **ExecutionClass inversion:** the control plane stamps `execution_class` on the assignment; Bridge launches from that stamp (fail closed if missing), asserts the full class floor after ceilings, and projects permissions locally (“Bridge wins” on tools). Ordinary grants are derived server-side as classMax project ceiling; landing is package `lands`, not a class axiom. Workspace briefs discover verification commands for Node, Make, Cargo, Go, pytest, and Gradle. Control-plane `instruction` is **additive** evidence/class prose; Bridge always appends local RULES (shell allow-list, verbatim paste, criterion mapping, hard-denied). Prompt is data; permissions stay local — residual risk if the control plane is compromised (sharpest for `observe_network`). Artifact digests still normalize bare 64-hex to `sha256:<hex>`. Experimental Pi lane remains; `observe_network` is refused on Pi. Heartbeat protocol 2, worktrees, ops helpers, multi-driver lanes, and slots **1–4** unchanged. Protocol 1 heartbeats remain compatible for presence but cannot receive work.
5
+ **Package version:** `0.11.4` — finalize transport flakes stay **retryable**; stamped **ExecutionClass** is required (fail closed if missing). Heartbeat requires **protocol 2 + preflight** (protocol 1 rejected). Bring lanes online with `drivers online`, then `runner --workspace` or `install-service` (no `--agent` process override). Prefs commands (`fuel`, `drivers online|offline`) do not heartbeat a fake not-ready preflight over a live runner. In-flight claims prefer `runtime.json`; a one-time migrate still adopts legacy `config.json` claims when runtime is absent. Ordinary grants derive server-side as classMax project ceiling; landing is package `lands`.
6
6
 
7
7
  ## Prerequisites
8
8
 
package/dist/cli.js CHANGED
@@ -18,7 +18,7 @@ import { ensureCheckout } from "./checkout.js";
18
18
  import { pumpExecutionSlots, renewLeases } from "./execution.js";
19
19
  import { installRunnerService, uninstallRunnerService } from "./service.js";
20
20
  import { OPS_VERBS, runOps } from "./ops.js";
21
- import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight } from "./preflight.js";
21
+ import { BRIDGE_PROTOCOL_VERSION, cachedBridgePreflight, unavailableWorkspacePreflight } from "./preflight.js";
22
22
  const [command] = process.argv.slice(2);
23
23
  /** Read our own package version so every runner start logs exactly which build is live. */
24
24
  function bridgeVersion() {
@@ -35,7 +35,7 @@ const BRIDGE_NPX = "npx @miraland-labs/conduit-bridge";
35
35
  function bridgeUsage(...args) {
36
36
  return `${BRIDGE_NPX} ${args.join(" ")}`;
37
37
  }
38
- /** Single source for the --agent placeholder so CLI help never drifts from DRIVERS. */
38
+ /** Single source for the driver-id placeholder so CLI help never drifts from DRIVERS. */
39
39
  const AGENT_PLACEHOLDER = `<${Object.keys(DRIVERS).join("|")}>`;
40
40
  function parseFuelSource(value) {
41
41
  if (value === undefined)
@@ -231,7 +231,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
231
231
  const client = new ConduitClient(config);
232
232
  let heartbeatOk = true;
233
233
  try {
234
- await heartbeat(client, config, null);
234
+ await heartbeat(client, config, null, unavailableWorkspacePreflight({ config }));
235
235
  }
236
236
  catch {
237
237
  heartbeatOk = false;
@@ -254,7 +254,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
254
254
  console.log("Heartbeats report capabilities for diagnostics only; matching uses the Connect confirmation. Detected clients never receive grants automatically.");
255
255
  console.log(`MCP setup: {"mcpServers":{"conduit":{"command":"npx","args":["-y","@miraland-labs/conduit-bridge","mcp"]}}}`);
256
256
  console.log(`Bring a lane online: ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
257
- console.log(`Execute work: ${bridgeUsage("runner", "--workspace", "<repo>")} (or ${bridgeUsage("runner", "--agent", AGENT_PLACEHOLDER, "--workspace", "<repo>")})`);
257
+ console.log(`Execute work: ${bridgeUsage("runner", "--workspace", "<repo>")}`);
258
258
  console.log(`Keep on shift (macOS/Linux): ${bridgeUsage("install-service", "--workspace", "<repo>")}`);
259
259
  console.log(`Flip machine fuel later: ${bridgeUsage("fuel", "local|conduit")} (per-lane override: ${bridgeUsage("drivers", "fuel", "<id>", "local|conduit")})`);
260
260
  console.log("Windows: keep the runner terminal open — install-service is macOS/Linux only.");
@@ -281,24 +281,15 @@ function openUrl(url) {
281
281
  async function installService() {
282
282
  let config = await loadConfig();
283
283
  const { values } = parseArgs({ args: process.argv.slice(3), options: {
284
- agent: { type: "string" }, workspace: { type: "string" }, interval: { type: "string" }, "agent-timeout-minutes": { type: "string" },
284
+ workspace: { type: "string" }, interval: { type: "string" }, "agent-timeout-minutes": { type: "string" },
285
285
  "ensure-checkout": { type: "string" },
286
286
  } });
287
287
  if (!values.workspace) {
288
- throw new Error(`Usage: ${bridgeUsage("install-service", "--workspace", "<repository-path>", `[--agent ${AGENT_PLACEHOLDER}]`, "[--ensure-checkout <repository-url>]")} (workspace required; uses online driver lanes)`);
288
+ throw new Error(`Usage: ${bridgeUsage("install-service", "--workspace", "<repository-path>", "[--ensure-checkout <repository-url>]")} (workspace required; uses online driver lanes)`);
289
289
  }
290
290
  config = (await seedDriversFromDetection(config)).config;
291
- if (values.agent) {
292
- if (!DRIVERS[values.agent]) {
293
- throw new Error(`Unknown agent driver: ${values.agent}. Available: ${Object.keys(DRIVERS).join(", ")}`);
294
- }
295
- config = seedDriverLanes(config, [values.agent]).config;
296
- config = setDriversOnline(config, [values.agent], true);
297
- await saveConfigPrefs(config);
298
- console.log(`Brought ${values.agent} online for this computer (shared capacity ${config.leaseCapacity}).`);
299
- }
300
- else if (!onlineDriverIds(config).length) {
301
- throw new Error(`No online driver lanes. Run \`${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}\` first, or pass --agent to bring one online.`);
291
+ if (!onlineDriverIds(config).length) {
292
+ throw new Error(`No online driver lanes. Run \`${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}\` first.`);
302
293
  }
303
294
  const workspace = resolve(values.workspace);
304
295
  if (values["ensure-checkout"]) {
@@ -307,7 +298,6 @@ async function installService() {
307
298
  ? `Cloned ${values["ensure-checkout"]} into ${workspace}`
308
299
  : `Workspace already matches ${values["ensure-checkout"]}`);
309
300
  }
310
- // Supervisor service: no baked --agent; honors config drivers online set.
311
301
  const result = await installRunnerService({
312
302
  workspace,
313
303
  interval: values.interval,
@@ -380,9 +370,8 @@ async function fuelCommand() {
380
370
  const config = await loadConfig();
381
371
  config.fuelSource = mode;
382
372
  await saveConfigPrefs(config);
383
- const client = new ConduitClient(config);
384
- await heartbeat(client, config, null);
385
- console.log(`Machine fuel source set to ${mode === "local" ? "local subscription" : "Conduit pump"} and reported on heartbeat.`);
373
+ // Do not heartbeat here: a workspace-less preflight would overwrite a live runner's ready report.
374
+ console.log(`Machine fuel source set to ${mode === "local" ? "local subscription" : "Conduit pump"}. The runner reports it on the next heartbeat.`);
386
375
  }
387
376
  async function driversCommand() {
388
377
  const sub = process.argv[3];
@@ -416,8 +405,7 @@ async function driversCommand() {
416
405
  config = seedDriverLanes(config, ids).config;
417
406
  config = setDriversOnline(config, ids, sub === "online");
418
407
  await saveConfigPrefs(config);
419
- const client = new ConduitClient(config);
420
- await heartbeat(client, config, null).catch(() => undefined);
408
+ // Prefs only a not-ready preflight here would clear a live runner's dispatch readiness.
421
409
  console.log(`${sub === "online" ? "Online" : "Offline"}: ${ids.join(", ")}`);
422
410
  console.log(`Now online: ${onlineDriverIds(config).join(", ") || "(none)"}`);
423
411
  return;
@@ -438,7 +426,7 @@ async function driversCommand() {
438
426
  }
439
427
  async function runner() {
440
428
  const { values } = parseArgs({ args: process.argv.slice(3), options: {
441
- agent: { type: "string" }, workspace: { type: "string" }, interval: { type: "string" }, once: { type: "boolean" },
429
+ workspace: { type: "string" }, interval: { type: "string" }, once: { type: "boolean" },
442
430
  "agent-timeout-minutes": { type: "string" }, fuel: { type: "string" }, "ensure-checkout": { type: "string" },
443
431
  } });
444
432
  let config = await loadConfig();
@@ -450,16 +438,6 @@ async function runner() {
450
438
  config = (await seedDriversFromDetection(config)).config;
451
439
  await saveConfigPrefs(config);
452
440
  const client = new ConduitClient(config);
453
- let processDriver = null;
454
- let processOnlineIds = null;
455
- if (values.agent) {
456
- processDriver = DRIVERS[values.agent] ?? null;
457
- if (!processDriver)
458
- throw new Error(`Unknown agent driver: ${values.agent}. Available: ${Object.keys(DRIVERS).join(", ")}`);
459
- if (!values.workspace)
460
- throw new Error("--workspace <repository-path> is required with --agent");
461
- processOnlineIds = [values.agent];
462
- }
463
441
  if (values["ensure-checkout"] && !values.workspace) {
464
442
  throw new Error("--ensure-checkout requires --workspace <repository-path>");
465
443
  }
@@ -473,14 +451,14 @@ async function runner() {
473
451
  const brief = workspace ? await buildWorkspaceBrief(workspace) : null;
474
452
  const intervalMs = values.interval ? Math.max(5_000, Number(values.interval)) : 15_000;
475
453
  const timeoutMs = values["agent-timeout-minutes"] ? Number(values["agent-timeout-minutes"]) * 60_000 : undefined;
476
- const canExecute = Boolean(workspace) && (Boolean(processDriver) || onlineDriverIds(config).length > 0);
454
+ const canExecute = Boolean(workspace) && onlineDriverIds(config).length > 0;
477
455
  if (!workspace) {
478
456
  console.warn(`WARNING: heartbeat only — pass --workspace <repo>. Lanes: ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
479
457
  }
480
458
  else if (!canExecute) {
481
- console.warn(`WARNING: no online driver lanes — ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)} (or pass --agent)`);
459
+ console.warn(`WARNING: no online driver lanes — ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
482
460
  }
483
- const onlineLabel = (processOnlineIds?.join(", ") || onlineDriverIds(config).join(", ")) || "(none)";
461
+ const onlineLabel = onlineDriverIds(config).join(", ") || "(none)";
484
462
  console.log(`Conduit runner (bridge v${bridgeVersion()}) connected to ${config.baseUrl}${workspace ? ` — workspace ${workspace}` : " — heartbeat only"} (lanes: ${onlineLabel}; machine fuel: ${config.fuelSource === "local" ? "local" : "conduit"}; slots: ${config.leaseCapacity})`);
485
463
  const running = new Map();
486
464
  for (;;) {
@@ -492,18 +470,20 @@ async function runner() {
492
470
  config.fuelSource = latest.fuelSource;
493
471
  config.leaseCapacity = latest.leaseCapacity;
494
472
  const currentBrief = workspace ? await buildWorkspaceBrief(workspace).catch(() => brief) : null;
495
- const preflight = workspace ? await cachedBridgePreflight({ config, workspace, processOnlineIds, brief: currentBrief ?? undefined }) : undefined;
496
- await heartbeat(client, config, currentBrief, processOnlineIds, preflight);
473
+ const preflight = workspace
474
+ ? await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined })
475
+ : unavailableWorkspacePreflight({ config });
476
+ await heartbeat(client, config, currentBrief, preflight);
497
477
  await renewLeases(client, config);
498
- if (workspace && (processDriver || onlineDriverIds(config).length)) {
478
+ if (workspace && onlineDriverIds(config).length) {
499
479
  progressed = await pumpExecutionSlots(client, config, workspace, brief, timeoutMs, {
500
480
  heartbeat: async () => {
501
481
  const currentBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
502
- const preflight = await cachedBridgePreflight({ config, workspace, processOnlineIds, brief: currentBrief ?? undefined });
503
- await heartbeat(client, config, currentBrief, processOnlineIds, preflight);
482
+ const preflight = await cachedBridgePreflight({ config, workspace, brief: currentBrief ?? undefined });
483
+ await heartbeat(client, config, currentBrief, preflight);
504
484
  },
505
485
  heartbeatIntervalMs: intervalMs,
506
- }, running, { driver: processDriver, processOnlineIds });
486
+ }, running, {});
507
487
  }
508
488
  }
509
489
  catch (error) {
@@ -517,9 +497,8 @@ async function runner() {
517
497
  await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
518
498
  }
519
499
  }
520
- async function heartbeat(client, config, brief, processOnlineIds, preflight) {
521
- // Process-level `--agent` override must count as online even when saved lanes are offline.
522
- const online = processOnlineIds?.length ? processOnlineIds : onlineDriverIds(config);
500
+ async function heartbeat(client, config, brief, preflight) {
501
+ const online = onlineDriverIds(config);
523
502
  const status = heartbeatStatusForDrivers(online);
524
503
  await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
525
504
  status,
@@ -528,8 +507,8 @@ async function heartbeat(client, config, brief, processOnlineIds, preflight) {
528
507
  fuel_source: config.fuelSource === "local" ? "local" : "conduit",
529
508
  bridge_version: bridgeVersion(),
530
509
  bridge_protocol: BRIDGE_PROTOCOL_VERSION,
531
- drivers: driversHeartbeatReport(config, config.activeAttempts, processOnlineIds),
532
- ...(preflight ? { preflight } : {}),
510
+ drivers: driversHeartbeatReport(config, config.activeAttempts),
511
+ preflight,
533
512
  ...(brief ? { workspace_brief: brief } : {}),
534
513
  }) });
535
514
  }
package/dist/config.js CHANGED
@@ -23,6 +23,22 @@ const runtimePath = join(directory, "runtime.json");
23
23
  const lockPath = join(directory, "config.lock");
24
24
  const pendingPath = join(directory, "pending-connect.json");
25
25
  const installationPath = join(directory, "installation.json");
26
+ /**
27
+ * Prefer runtime.json; when absent, adopt legacy activeAttempts/sessions still on config.json
28
+ * so one Bridge upgrade does not strand in-flight claims.
29
+ */
30
+ export function adoptLegacyRuntimeClaims(input) {
31
+ if (input.runtime) {
32
+ return {
33
+ activeAttempts: input.runtime.activeAttempts ?? {},
34
+ sessions: input.runtime.sessions,
35
+ };
36
+ }
37
+ return {
38
+ activeAttempts: input.legacyAttempts,
39
+ sessions: input.legacySessions,
40
+ };
41
+ }
26
42
  export async function loadConfig() {
27
43
  const config = await loadConfigIfPresent();
28
44
  if (!config)
@@ -49,9 +65,13 @@ export async function loadConfigIfPresent() {
49
65
  }
50
66
  }
51
67
  const runtime = await loadRuntime();
52
- // Prefer dedicated runtime file; fall back to legacy fields still present in config.json.
53
- config.activeAttempts = runtime?.activeAttempts ?? config.activeAttempts ?? {};
54
- config.sessions = runtime?.sessions ?? config.sessions;
68
+ const adopted = adoptLegacyRuntimeClaims({
69
+ runtime,
70
+ legacyAttempts: config.activeAttempts && typeof config.activeAttempts === "object" ? config.activeAttempts : {},
71
+ legacySessions: config.sessions && typeof config.sessions === "object" ? config.sessions : undefined,
72
+ });
73
+ config.activeAttempts = adopted.activeAttempts;
74
+ config.sessions = adopted.sessions;
55
75
  for (const active of Object.values(config.activeAttempts))
56
76
  active.phase ??= "agent_running";
57
77
  if (config.fuelSource !== "local" && config.fuelSource !== "conduit")
@@ -165,10 +185,12 @@ export async function saveConfig(config, options = {}) {
165
185
  let legacySessions;
166
186
  try {
167
187
  const legacy = JSON.parse(await readFile(path, "utf8"));
168
- if (legacy.activeAttempts && typeof legacy.activeAttempts === "object")
188
+ if (legacy.activeAttempts && typeof legacy.activeAttempts === "object") {
169
189
  legacyAttempts = legacy.activeAttempts;
170
- if (legacy.sessions && typeof legacy.sessions === "object")
190
+ }
191
+ if (legacy.sessions && typeof legacy.sessions === "object") {
171
192
  legacySessions = legacy.sessions;
193
+ }
172
194
  }
173
195
  catch { /* no prior config */ }
174
196
  if (Object.keys(legacyAttempts).length || legacySessions) {
package/dist/driver.js CHANGED
@@ -3,8 +3,8 @@ import { existsSync } from "node:fs";
3
3
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { z } from "zod";
6
- import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectKiroTools, projectPi, requireStampedExecutionClass, resolveExecutionClass, } from "./execution-class.js";
7
- export { branchCreateCommands, deniedCommands, isBoundedVerificationCommand, prCreateCommands, requireStampedExecutionClass, resolveExecutionClass, } from "./execution-class.js";
6
+ import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
7
+ export { branchCreateCommands, deniedCommands, isBoundedVerificationCommand, prCreateCommands, requireStampedExecutionClass, } from "./execution-class.js";
8
8
  function deliveryLanguageRule(language) {
9
9
  if (language === "zh") {
10
10
  return "- Write every human-facing delivery packet string (outcome, changes, verification, assumptions, risks, limitations, criterion text) in Chinese (Simplified). Keep JSON keys and enum token values in English.";
@@ -424,10 +424,8 @@ export const codexDriver = {
424
424
  * publish shell under open class policy. Research-only networked assignments
425
425
  * deny Shell(*) and Write(**).
426
426
  */
427
- export function cursorPermissionsForGrants(grants, verificationCommands = [], capabilities = [], deliverable = "repository", workRole, executionClass) {
428
- const cls = executionClass
429
- ?? resolveExecutionClass({ grants, capabilities, deliverable, workRole });
430
- const { allow, deny } = projectCursor(cls, { grants, verificationCommands, capabilities });
427
+ export function cursorPermissionsForGrants(grants, verificationCommands, capabilities, executionClass) {
428
+ const { allow, deny } = projectCursor(executionClass, { grants, verificationCommands, capabilities });
431
429
  return { allow, deny };
432
430
  }
433
431
  /**
package/dist/drivers.js CHANGED
@@ -146,7 +146,7 @@ export function driversHeartbeatReport(config, activeAttempts, processOnlineIds)
146
146
  }
147
147
  /**
148
148
  * Pick an online driver for a new claim: least loaded among online (shared machine pool).
149
- * `processOnlineIds` overrides config when runner was started with --agent.
149
+ * `processOnlineIds` overrides config online lanes (tests / harness).
150
150
  */
151
151
  export function pickDriverForClaim(config, processOnlineIds) {
152
152
  const online = (processOnlineIds?.length ? processOnlineIds : onlineDriverIds(config))
@@ -27,31 +27,6 @@ export const EXECUTION_CLASSES = [
27
27
  "verify",
28
28
  "mutate_repo",
29
29
  ];
30
- /**
31
- * Migrator / test helper only. Launch must use the control-plane stamped class
32
- * via `requireStampedExecutionClass` — do not re-derive as source of truth.
33
- */
34
- export function resolveExecutionClass(input) {
35
- const grants = input.grants;
36
- const capabilities = input.capabilities ?? [];
37
- const deliverable = input.deliverable ?? "repository";
38
- const workRole = input.workRole ?? null;
39
- const hasWrite = grants.includes("repo_write");
40
- const hasLand = grants.includes("branch_create") || grants.includes("pr_create");
41
- const hasTest = grants.includes("test_run");
42
- const hasRead = grants.includes("repo_read");
43
- const external = capabilities.includes("external_network");
44
- if (deliverable === "artifact" && workRole !== "research" && hasWrite)
45
- return "publish_artifact";
46
- if (external && !hasWrite && !hasLand && !hasTest)
47
- return "observe_network";
48
- // verify before observe: repo_read+test_run is the usual verify shape and must keep shell.
49
- if (hasTest && !hasWrite && !hasLand)
50
- return "verify";
51
- if (!hasWrite && !hasLand && !hasTest && (hasRead || workRole === "research"))
52
- return "observe";
53
- return "mutate_repo";
54
- }
55
30
  const STAMPED_CLASSES = new Set(EXECUTION_CLASSES);
56
31
  /** Fail closed when the assignment has no stamped class (upgrade Bridge / redeploy CP). */
57
32
  export function requireStampedExecutionClass(stamped) {
@@ -89,9 +64,7 @@ export function assertClassFloor(executionClass, ordinaryGrants) {
89
64
  * with `external_network` was therefore told to fetch by a prompt no driver could honour. One
90
65
  * predicate, read by all of them, is what keeps that from recurring for the next class added.
91
66
  *
92
- * `observe_network` implies the capability by construction (`resolveExecutionClass` only returns it
93
- * when `external_network` is present); it stays in the test so a spec that loses its capability list
94
- * in transit still fails toward the class it was stamped with.
67
+ * `observe_network` implies the capability by construction.
95
68
  */
96
69
  export function allowsExternalFetch(executionClass, capabilities = []) {
97
70
  return executionClass === "observe_network" || capabilities.includes("external_network");
package/dist/execution.js CHANGED
@@ -22,6 +22,34 @@ function changesRequestedFeedback(summary) {
22
22
  return null;
23
23
  }
24
24
  }
25
+ /**
26
+ * A network fault reaching the code host. Mirrors FORGE_TRANSPORT_PATTERN in the control plane's
27
+ * failures.ts — Bridge cannot import from there, so test/failure-classification.test.ts pins the two
28
+ * together rather than trusting them to stay in step.
29
+ */
30
+ export const FORGE_TRANSPORT_PATTERN = /unable to access '?https?:\/\/|error in the http2 framing layer|could not resolve host|connection (?:reset|timed out|refused)|\bcurl\b.*\b(?:52|55|56|92)\b|remote end hung up unexpectedly|\brpc failed\b|tls handshake|network is unreachable|operation timed out/i;
31
+ export function forgeTransportFailure(message) {
32
+ return FORGE_TRANSPORT_PATTERN.test(message);
33
+ }
34
+ /** Environment faults that must Hold — mirror CP ENVIRONMENT_FAILURES message patterns. */
35
+ const FINALIZE_ENVIRONMENT_PATTERN = /base[_ ]not[_ ]ancestor|required base commit is not available|source[_ ]workspace[_ ]dirty|uncommitted changes|dirty workspace|workspace[_ ]head[_ ]changed|workspace[_ ]repository|workspace[_ ]unavailable|driver[_ ]not[_ ]authenticated|not logged in|no login|not authenticated|login required|no[_ ]online[_ ]driver|bridge[_ ]preflight|stale bridge|bridge version/i;
36
+ /** Delivery-report / grant / evidence contract defects (non-retryable rework). */
37
+ const FINALIZE_CONTRACT_PATTERN = /missing required evidence|without the repo_write grant|pull_request_url|Artifact delivery requires|outside the approved scope|head commit|test evidence must include|Met acceptance criteria|Agent report|Agent reported|Delivery report/i;
38
+ /**
39
+ * Classify throws from ensureDeliveryPullRequest + validateDeliveryReport.
40
+ * Unknown non-contract faults must not be laundered as execution_contract_failed.
41
+ */
42
+ export function classifyFinalizeFailure(message) {
43
+ if (forgeTransportFailure(message))
44
+ return { retryable: true, error: message };
45
+ if (FINALIZE_ENVIRONMENT_PATTERN.test(message))
46
+ return { retryable: false, error: message };
47
+ if (FINALIZE_CONTRACT_PATTERN.test(message))
48
+ return { retryable: false, error: message };
49
+ // Unknown finalize faults fail closed once — retrying burns the attempt budget (Invariant 22).
50
+ // Distinct from execution_contract_failed so owners see a Bridge fault, not a contract lie.
51
+ return { retryable: false, error: `Bridge finalize interrupted: ${message}` };
52
+ }
25
53
  /** Failures that require an operator/configuration change must never burn the remaining attempts. */
26
54
  export function retryableAgentFailure(message) {
27
55
  // `cannot enforce` is a driver projection refusing a class it structurally cannot bound (Pi and
@@ -188,7 +216,6 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
188
216
  let laneDriver = null;
189
217
  let driverId = null;
190
218
  if (fallbackDriver) {
191
- // Legacy `--agent` / tests: one fixed driver for every new claim.
192
219
  laneDriver = fallbackDriver;
193
220
  driverId = processOnlineIds?.[0]
194
221
  ?? Object.entries(DRIVERS).find(([, d]) => d === fallbackDriver)?.[0]
@@ -521,10 +548,18 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
521
548
  }
522
549
  catch (error) {
523
550
  const message = error instanceof Error ? error.message : "Agent delivery report was invalid";
524
- // Contract/tooling/credential failures need a human fix. Automatic retry would repeat the same
525
- // state immediately and spend another attempt before the operator can change anything.
526
- await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: false, idempotency_key: `bridge:invalid-delivery:${active.attemptId}` } });
527
- console.error(`Assignment ${taskId} could not produce a valid Delivery: ${redactSecrets(message)}`);
551
+ // classifyFinalizeFailure: contract/environment non-retryable; forge transport retryable;
552
+ // unknown faults stay retryable as "Bridge finalize interrupted" (not laundered as contract).
553
+ const classified = classifyFinalizeFailure(message);
554
+ await queueTerminal(client, taskId, {
555
+ action: "fail",
556
+ body: {
557
+ error: classified.error,
558
+ retryable: classified.retryable,
559
+ idempotency_key: `bridge:invalid-delivery:${active.attemptId}`,
560
+ },
561
+ });
562
+ console.error(`Assignment ${taskId} could not produce a valid Delivery: ${redactSecrets(classified.error)}`);
528
563
  const replyTail = reportText.slice(-8_000);
529
564
  console.error(`Assignment ${taskId} agent reply tail (${reportText.length} chars total, redacted): ${redactSecrets(replyTail) || "<empty>"}`);
530
565
  return;
package/dist/preflight.js CHANGED
@@ -110,6 +110,21 @@ export async function cachedBridgePreflight(input) {
110
110
  cached = { key, at: Date.now(), report };
111
111
  return report;
112
112
  }
113
+ /** Heartbeat-only / missing workspace still reports protocol 2 preflight (not ready). */
114
+ export function unavailableWorkspacePreflight(input) {
115
+ const online = input.processOnlineIds?.length ? input.processOnlineIds : onlineDriverIds(input.config);
116
+ const issues = [];
117
+ if (!online.length)
118
+ issues.push({ code: "no_online_driver" });
119
+ issues.push({ code: "workspace_unavailable" });
120
+ return {
121
+ ready: false,
122
+ checked_at: new Date().toISOString(),
123
+ workspace_clean: false,
124
+ drivers: online.map((id) => ({ id, version: null, ready: false })),
125
+ issues,
126
+ };
127
+ }
113
128
  export function describePreflightIssue(issue) {
114
129
  const lane = issue.driver ? ` (${issue.driver})` : "";
115
130
  if (issue.code === "no_online_driver")
package/dist/service.js CHANGED
@@ -22,8 +22,6 @@ export function runnerProgramArguments(options = {}) {
22
22
  if (!cli)
23
23
  throw new Error("Unable to resolve the conduit CLI path");
24
24
  const args = [process.execPath, cli, "runner"];
25
- if (options.agent)
26
- args.push("--agent", options.agent);
27
25
  if (options.workspace)
28
26
  args.push("--workspace", options.workspace);
29
27
  if (options.ensureCheckout)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
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": {