@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 +1 -1
- package/dist/execution.js +52 -4
- package/dist/preflight.js +26 -8
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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": {
|