@miraland-labs/conduit-bridge 0.13.0 → 0.14.3

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/driver.js CHANGED
@@ -152,7 +152,7 @@ export function extractAgentReportJsonText(text) {
152
152
  const fromUnclosed = unclosed?.[1]?.trim();
153
153
  if (fromUnclosed)
154
154
  return fromUnclosed;
155
- const start = text.lastIndexOf("{");
155
+ const start = text.indexOf("{");
156
156
  if (start < 0)
157
157
  throw new Error("Agent did not emit the required structured report");
158
158
  return text.slice(start).trim();
package/dist/execution.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { createHash } from "node:crypto";
2
3
  import { z } from "zod";
3
4
  import { ConduitRequestError } from "./client.js";
@@ -9,6 +10,30 @@ import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quaran
9
10
  import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
10
11
  import { ensureDeliveryPullRequest } from "./ensure-pull-request.js";
11
12
  import { captureVerificationFailure, ensureTestEvidence } from "./ensure-test-evidence.js";
13
+ /** Keep a Mac awake only while an assignment is active; display sleep remains allowed. */
14
+ export function startIdleSleepGuard(options = {}) {
15
+ if ((options.platform ?? process.platform) !== "darwin")
16
+ return () => undefined;
17
+ const launch = options.launch ?? ((command, args) => spawn(command, args, { stdio: "ignore" }));
18
+ let guard;
19
+ try {
20
+ // -w ties the assertion to the runner as a crash-safe ceiling; normal completion kills it sooner.
21
+ guard = launch("caffeinate", ["-i", "-w", String(options.pid ?? process.pid)]);
22
+ }
23
+ catch (error) {
24
+ console.error(`Idle-sleep guard unavailable: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
25
+ return () => undefined;
26
+ }
27
+ guard.once("error", (error) => console.error(`Idle-sleep guard unavailable: ${redactSecrets(error.message)}`));
28
+ guard.unref();
29
+ let released = false;
30
+ return () => {
31
+ if (released)
32
+ return;
33
+ released = true;
34
+ guard.kill("SIGTERM");
35
+ };
36
+ }
12
37
  /** Feedback text for changes_requested summaries (plain string or `{ feedback }`). */
13
38
  function changesRequestedFeedback(summary) {
14
39
  if (!summary)
@@ -438,13 +463,23 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
438
463
  });
439
464
  return { taskId: assignment.id, driverId: selectedDriverId };
440
465
  }
466
+ export function claimedAssignmentDriver(fallback, driverId) {
467
+ if (!driverId)
468
+ return fallback;
469
+ const selected = DRIVERS[driverId];
470
+ if (!selected)
471
+ throw new Error(`Claimed assignment selected unavailable driver ${driverId}`);
472
+ return selected;
473
+ }
441
474
  export async function executeNextAssignment(client, config, driver, workspace, brief, timeoutMs, supervision) {
442
475
  if (Object.keys(config.activeAttempts).length)
443
476
  return recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision);
444
- const claimed = await claimNextAssignment(client, config, workspace, brief);
477
+ const preferredDriverId = Object.entries(DRIVERS).find(([, candidate]) => candidate === driver)?.[0] ?? null;
478
+ const claimed = await claimNextAssignment(client, config, workspace, brief, preferredDriverId);
445
479
  if (!claimed)
446
480
  return false;
447
- await runClaimedAssignment(client, config, driver, workspace, brief, claimed.taskId, timeoutMs, supervision);
481
+ const selectedDriver = claimedAssignmentDriver(driver, claimed.driverId);
482
+ await runClaimedAssignment(client, config, selectedDriver, workspace, brief, claimed.taskId, timeoutMs, supervision);
448
483
  return true;
449
484
  }
450
485
  async function runClaimedAssignment(client, config, driver, workspace, brief, taskId, timeoutMs, supervision, options = {}) {
@@ -710,6 +745,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
710
745
  executionKind,
711
746
  sourceAttemptId: sourceAttemptId ?? undefined,
712
747
  });
748
+ const releaseIdleSleep = startIdleSleepGuard();
713
749
  const renewTimer = setInterval(() => { void renewLeases(client, config).catch((error) => console.error(`Lease renewal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`)); }, 60_000);
714
750
  let heartbeatRunning = false;
715
751
  const heartbeatTimer = supervision ? setInterval(() => {
@@ -948,6 +984,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
948
984
  console.log(`Assignment ${taskId} delivered for review and acceptance.`);
949
985
  }
950
986
  finally {
987
+ releaseIdleSleep();
951
988
  clearInterval(renewTimer);
952
989
  if (heartbeatTimer)
953
990
  clearInterval(heartbeatTimer);
@@ -1044,7 +1081,7 @@ export async function conduitAliases(fuel) {
1044
1081
  return null;
1045
1082
  }
1046
1083
  }
1047
- async function resolveAssignmentModel(driver, config, spec, taskId, workspace, fuel) {
1084
+ export async function resolveAssignmentModel(driver, config, spec, taskId, workspace, fuel) {
1048
1085
  const tier = tierForRisk(spec.risk_level);
1049
1086
  const configured = config.models?.[tier];
1050
1087
  const risk = spec.risk_level === "low" || spec.risk_level === "medium" || spec.risk_level === "high" ? spec.risk_level : "unset";
@@ -1067,7 +1104,18 @@ async function resolveAssignmentModel(driver, config, spec, taskId, workspace, f
1067
1104
  return { risk, tier };
1068
1105
  }
1069
1106
  const candidates = Array.isArray(configured) ? configured : [configured];
1070
- const { model, skipped } = pickModelCandidate(candidates, await list());
1107
+ const available = await list();
1108
+ // Model names are driver-specific, but the tier -> model map is machine-wide, so switching the
1109
+ // online lane silently invalidates it. Under local fuel the lane's own default is authenticated
1110
+ // and works; a configured name we cannot confirm it supports hard-fails ("No API key found for
1111
+ // <vendor>") — Pi resolved a Claude-era "opus" to amazon-bedrock and Held. When we cannot verify a
1112
+ // local model, prefer the authenticated default over a guess that breaks the run. Under Conduit
1113
+ // fuel the CLI default cannot resolve, so the mapped alias is still the right bet even unverified.
1114
+ if (!fuel && available === null) {
1115
+ console.error(`Assignment ${taskId} intelligence tier ${tier} (risk ${risk}): configured model not confirmed available on ${driver.name}; using its authenticated default. Set this lane's own default model to pin a specific one.`);
1116
+ return { risk, tier };
1117
+ }
1118
+ const { model, skipped } = pickModelCandidate(candidates, available);
1071
1119
  const skippedNote = skipped.length ? ` (skipped unavailable/unsafe: ${skipped.join(", ")})` : "";
1072
1120
  if (!model) {
1073
1121
  const none = `Assignment ${taskId} intelligence tier ${tier} (risk ${risk}): no configured candidate available — CLI default${skippedNote}`;
package/dist/preflight.js CHANGED
@@ -1,10 +1,22 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { promisify } from "node:util";
3
4
  import { buildWorkspaceBrief, normalizeRepositoryUrl } from "./brief.js";
4
5
  import { hasAntigravityLogin, hasClaudeLogin, hasOpenAiLogin, hasOpenCodeLogin, resolveCodexExecutable, } from "./driver.js";
5
- import { localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel } from "./drivers.js";
6
+ import { localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
6
7
  /** Protocol 3: read-only diagnosis can reuse a retained failed-attempt worktree. */
7
- export const BRIDGE_PROTOCOL_VERSION = 3;
8
+ // 4: each driver snapshot reports diagnosis_read_only, so the control plane can require a
9
+ // capable diagnostic lane *before* dispatching instead of learning it from a failed attempt.
10
+ /** Stable digest of the tier -> model mapping, so a model change moves the readiness fingerprint. */
11
+ function modelsFingerprint(config) {
12
+ const models = config.models ?? {};
13
+ const canonical = Object.keys(models).sort().map((tier) => {
14
+ const value = models[tier];
15
+ return `${tier}=${(Array.isArray(value) ? value : [value]).join("|")}`;
16
+ }).join(";");
17
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
18
+ }
19
+ export const BRIDGE_PROTOCOL_VERSION = 4;
8
20
  const execFileAsync = promisify(execFile);
9
21
  async function defaultCommandRunner(command, args, cwd) {
10
22
  try {
@@ -83,22 +95,27 @@ export async function runBridgePreflight(input, deps = {}) {
83
95
  issues.push({ code: status.code === 0 ? "workspace_dirty" : "workspace_unavailable" });
84
96
  }
85
97
  const driverChecks = await Promise.all(online.map(async (driver) => {
98
+ // Whether this lane can combine bounded test execution with enforced read-only repository
99
+ // access. Reported here so dispatch can require it; the control plane must not keep its own
100
+ // copy of driver-name policy.
101
+ const diagnosisReadOnly = supportsReadOnlyDiagnosis(driver);
86
102
  const fuel = resolveDriverFuel(input.config, driver);
87
103
  if (localFuelOnlyDriver(driver) && fuel !== "local") {
88
- return { issue: { code: "driver_fuel_mismatch", driver }, snapshot: { id: driver, version: null, ready: false } };
104
+ return { issue: { code: "driver_fuel_mismatch", driver }, snapshot: { id: driver, version: null, ready: false, diagnosis_read_only: diagnosisReadOnly } };
89
105
  }
90
106
  const version = await run(executableFor(driver), ["--version"], input.workspace);
91
107
  const versionText = `${version.stdout}${version.stderr}`.trim().slice(0, 200) || null;
92
108
  if (version.code !== 0 || !`${version.stdout}${version.stderr}`.trim()) {
93
- return { issue: { code: "driver_missing", driver }, snapshot: { id: driver, version: versionText, ready: false } };
109
+ return { issue: { code: "driver_missing", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly } };
94
110
  }
95
111
  if (fuel === "local" && !await localAuthenticationReady(driver, input.workspace, run)) {
96
- return { issue: { code: "driver_not_authenticated", driver }, snapshot: { id: driver, version: versionText, ready: false } };
112
+ return { issue: { code: "driver_not_authenticated", driver }, snapshot: { id: driver, version: versionText, ready: false, diagnosis_read_only: diagnosisReadOnly } };
97
113
  }
98
- return { issue: null, snapshot: { id: driver, version: versionText, ready: true } };
114
+ return { issue: null, snapshot: { id: driver, version: versionText, ready: true, diagnosis_read_only: diagnosisReadOnly } };
99
115
  }));
100
116
  issues.push(...driverChecks.map((check) => check.issue).filter((issue) => issue !== null));
101
- return { ready: issues.length === 0, checked_at: new Date().toISOString(), workspace_clean: workspaceClean, drivers: driverChecks.map((check) => check.snapshot), issues };
117
+ return { ready: issues.length === 0, checked_at: new Date().toISOString(), workspace_clean: workspaceClean,
118
+ drivers: driverChecks.map((check) => check.snapshot), models_fingerprint: modelsFingerprint(input.config), issues };
102
119
  }
103
120
  let cached = null;
104
121
  /** Keep auth probes off the 15-second heartbeat hot path while still expiring readiness promptly. */
@@ -122,7 +139,8 @@ export function unavailableWorkspacePreflight(input) {
122
139
  ready: false,
123
140
  checked_at: new Date().toISOString(),
124
141
  workspace_clean: false,
125
- drivers: online.map((id) => ({ id, version: null, ready: false })),
142
+ drivers: online.map((id) => ({ id, version: null, ready: false, diagnosis_read_only: supportsReadOnlyDiagnosis(id) })),
143
+ models_fingerprint: modelsFingerprint(input.config),
126
144
  issues,
127
145
  };
128
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.13.0",
3
+ "version": "0.14.3",
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": {