@miraland-labs/conduit-bridge 0.9.5 → 0.9.7
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 +1 -1
- package/dist/cli.js +14 -4
- package/dist/driver.js +14 -4
- package/dist/execution.js +17 -10
- package/dist/ops.js +24 -1
- package/dist/preflight.js +116 -0
- package/ops/conduit-ops +2 -2
- package/ops/conduit-ops.cmd +2 -2
- package/ops/doctor.cmd +3 -0
- package/ops/doctor.sh +3 -0
- package/package.json +1 -1
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
|
-
**Current npm:** `0.9.
|
|
5
|
+
**Current npm:** `0.9.6` — **ops** helpers for macOS / Linux / Windows, LaunchAgent/systemd **PATH** for `~/.local/bin`, disconnect/disengage, multi-driver lanes, shared slots **1–4**, worktrees, optional `--ensure-checkout`.
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +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
22
|
const [command] = process.argv.slice(2);
|
|
22
23
|
/** Read our own package version so every runner start logs exactly which build is live. */
|
|
23
24
|
function bridgeVersion() {
|
|
@@ -367,7 +368,7 @@ async function initOps() {
|
|
|
367
368
|
async function opsCommand() {
|
|
368
369
|
const verb = process.argv[3];
|
|
369
370
|
if (!verb || !OPS_VERBS.includes(verb)) {
|
|
370
|
-
throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|online|offline|status|disconnect|uninstall>", "[driver…]")}`);
|
|
371
|
+
throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|online|offline|status|doctor|disconnect|uninstall>", "[driver…]")}`);
|
|
371
372
|
}
|
|
372
373
|
await runOps(verb, process.argv.slice(4));
|
|
373
374
|
}
|
|
@@ -490,11 +491,17 @@ async function runner() {
|
|
|
490
491
|
config.drivers = latest.drivers;
|
|
491
492
|
config.fuelSource = latest.fuelSource;
|
|
492
493
|
config.leaseCapacity = latest.leaseCapacity;
|
|
493
|
-
|
|
494
|
+
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);
|
|
494
497
|
await renewLeases(client, config);
|
|
495
498
|
if (workspace && (processDriver || onlineDriverIds(config).length)) {
|
|
496
499
|
progressed = await pumpExecutionSlots(client, config, workspace, brief, timeoutMs, {
|
|
497
|
-
heartbeat: async () =>
|
|
500
|
+
heartbeat: async () => {
|
|
501
|
+
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);
|
|
504
|
+
},
|
|
498
505
|
heartbeatIntervalMs: intervalMs,
|
|
499
506
|
}, running, { driver: processDriver, processOnlineIds });
|
|
500
507
|
}
|
|
@@ -510,7 +517,7 @@ async function runner() {
|
|
|
510
517
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
|
|
511
518
|
}
|
|
512
519
|
}
|
|
513
|
-
async function heartbeat(client, config, brief, processOnlineIds) {
|
|
520
|
+
async function heartbeat(client, config, brief, processOnlineIds, preflight) {
|
|
514
521
|
// Process-level `--agent` override must count as online even when saved lanes are offline.
|
|
515
522
|
const online = processOnlineIds?.length ? processOnlineIds : onlineDriverIds(config);
|
|
516
523
|
const status = heartbeatStatusForDrivers(online);
|
|
@@ -519,7 +526,10 @@ async function heartbeat(client, config, brief, processOnlineIds) {
|
|
|
519
526
|
capabilities: config.capabilities,
|
|
520
527
|
lease_capacity: config.leaseCapacity,
|
|
521
528
|
fuel_source: config.fuelSource === "local" ? "local" : "conduit",
|
|
529
|
+
bridge_version: bridgeVersion(),
|
|
530
|
+
bridge_protocol: BRIDGE_PROTOCOL_VERSION,
|
|
522
531
|
drivers: driversHeartbeatReport(config, config.activeAttempts, processOnlineIds),
|
|
532
|
+
...(preflight ? { preflight } : {}),
|
|
523
533
|
...(brief ? { workspace_brief: brief } : {}),
|
|
524
534
|
}) });
|
|
525
535
|
}
|
package/dist/driver.js
CHANGED
|
@@ -90,7 +90,9 @@ export function buildAssignmentPrompt(context) {
|
|
|
90
90
|
const changeScope = packageContext?.change_scope?.length ? packageContext.change_scope : spec.change_scope;
|
|
91
91
|
const evidence = packageContext?.required_evidence?.length ? packageContext.required_evidence : spec.required_evidence;
|
|
92
92
|
const rework = packageContext?.rework_feedback ?? context.reworkFeedback;
|
|
93
|
-
const
|
|
93
|
+
const labeledArtifact = (packageContext?.deliverable ?? spec.deliverable) === "artifact";
|
|
94
|
+
// Research is discovery in the delivery report — never a published content-pack artifact.
|
|
95
|
+
const artifact = labeledArtifact && role !== "research";
|
|
94
96
|
const lines = [
|
|
95
97
|
`You are completing one delegated Conduit assignment as the ${role} role. Work only inside the current workspace.`,
|
|
96
98
|
"",
|
|
@@ -451,10 +453,14 @@ export function cursorPermissionsForGrants(grants, verificationCommands = []) {
|
|
|
451
453
|
* block. Real authority boundaries are the `.cursor/cli.json` shell allow-list and the delivery
|
|
452
454
|
* contract (validateDeliveryReport rejects reported repository changes without repo_write, Invariant 9).
|
|
453
455
|
*/
|
|
456
|
+
export function cursorSupportsWorkspaceTrust(helpOutput) {
|
|
457
|
+
return /(?:^|\s)--trust(?:\s|$)/m.test(helpOutput);
|
|
458
|
+
}
|
|
454
459
|
export function cursorRunArgs(input) {
|
|
455
|
-
// Do not pass --trust: current Cursor Agent CLI rejects it ("unknown option '--trust'").
|
|
456
460
|
// Workspace authority is the operator-configured Bridge workspace; tool authority is .cursor/cli.json.
|
|
457
461
|
const args = ["-p", "--output-format", "json", "--workspace", input.workspace];
|
|
462
|
+
if (input.trustWorkspace)
|
|
463
|
+
args.push("--trust");
|
|
458
464
|
if (input.model)
|
|
459
465
|
args.push("--model", input.model);
|
|
460
466
|
if (input.resumeSessionId)
|
|
@@ -524,11 +530,15 @@ export const cursorDriver = {
|
|
|
524
530
|
// `agent status` exits 0 either way, so the text is the signal.
|
|
525
531
|
if (!process.env.CURSOR_API_KEY) {
|
|
526
532
|
const status = await execute(executable, ["status"], input.workspace, 15_000, undefined, "local");
|
|
527
|
-
if (status.code !== 0 ||
|
|
533
|
+
if (status.code !== 0 || !/logged in as/i.test(`${status.stdout}\n${status.stderr}`)) {
|
|
528
534
|
return { status: "failed", resultText: null, sessionId: null, error: "cursor has no login (set CURSOR_API_KEY or run `agent login`)" };
|
|
529
535
|
}
|
|
530
536
|
}
|
|
531
|
-
|
|
537
|
+
// Cursor has alternated between accepting and rejecting --trust across releases. Detect the
|
|
538
|
+
// local contract instead of making either version fail every attempt worktree.
|
|
539
|
+
const help = await execute(executable, ["--help"], input.workspace, 15_000, undefined, "local");
|
|
540
|
+
const trustWorkspace = help.code === 0 && cursorSupportsWorkspaceTrust(`${help.stdout}\n${help.stderr}`);
|
|
541
|
+
const configured = await withCursorPermissions(input.workspace, cursorPermissionsForGrants(input.grants, input.verificationCommands), () => execute(executable, cursorRunArgs({ ...input, trustWorkspace }), input.workspace, input.timeoutMs ?? 20 * 60_000, undefined, "local"));
|
|
532
542
|
const { code, stdout, stderr } = configured;
|
|
533
543
|
const parsed = parseCursorOutput(stdout);
|
|
534
544
|
if (code !== 0 || parsed.isError) {
|
package/dist/execution.js
CHANGED
|
@@ -21,6 +21,10 @@ function changesRequestedFeedback(summary) {
|
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
/** Failures that require an operator/configuration change must never burn the remaining attempts. */
|
|
25
|
+
export function retryableAgentFailure(message) {
|
|
26
|
+
return !/unknown (?:option|argument)|unrecognized (?:option|argument)|not logged in|no login|not authenticated|login required|\bENOENT\b|could not verify the installed CLI|requires local fuel|No Bridge-mapped|preflight/i.test(message);
|
|
27
|
+
}
|
|
24
28
|
const assignmentSchema = z.object({
|
|
25
29
|
id: z.string().uuid(),
|
|
26
30
|
attempt_id: z.string().uuid(),
|
|
@@ -405,8 +409,9 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
405
409
|
if (result.sessionId)
|
|
406
410
|
config.sessions = { ...config.sessions, [taskId]: result.sessionId };
|
|
407
411
|
if (result.status === "failed") {
|
|
408
|
-
|
|
409
|
-
|
|
412
|
+
const message = result.error ?? "Agent execution failed";
|
|
413
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(message), idempotency_key: `bridge:fail:${active.attemptId}` } });
|
|
414
|
+
console.error(`Assignment ${taskId} failed: ${redactSecrets(message)}`);
|
|
410
415
|
return;
|
|
411
416
|
}
|
|
412
417
|
let report;
|
|
@@ -441,7 +446,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
441
446
|
}
|
|
442
447
|
catch (repairError) {
|
|
443
448
|
const message = repairError instanceof Error ? repairError.message : "Delivery report repair failed";
|
|
444
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair failed: ${message}`, retryable:
|
|
449
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair failed: ${message}`, retryable: retryableAgentFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
445
450
|
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
446
451
|
return;
|
|
447
452
|
}
|
|
@@ -449,7 +454,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
449
454
|
config.sessions = { ...config.sessions, [taskId]: repaired.sessionId };
|
|
450
455
|
if (repaired.status === "failed") {
|
|
451
456
|
const message = repaired.error ?? "Delivery report repair failed";
|
|
452
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable:
|
|
457
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
453
458
|
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
454
459
|
return;
|
|
455
460
|
}
|
|
@@ -459,7 +464,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
459
464
|
}
|
|
460
465
|
catch (repairError) {
|
|
461
466
|
const message = repairError instanceof Error ? repairError.message : "Repaired delivery report was invalid";
|
|
462
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair exhausted: ${message}`, retryable:
|
|
467
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair exhausted: ${message}`, retryable: false, idempotency_key: `bridge:delivery-repair-invalid:${active.attemptId}` } });
|
|
463
468
|
console.error(`Assignment ${taskId} exhausted its report-only repair: ${redactSecrets(message)}`);
|
|
464
469
|
const replyTail = (repaired.resultText ?? "").slice(-8_000);
|
|
465
470
|
console.error(`Assignment ${taskId} repaired reply tail (${(repaired.resultText ?? "").length} chars total, redacted): ${redactSecrets(replyTail) || "<empty>"}`);
|
|
@@ -482,10 +487,9 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
482
487
|
}
|
|
483
488
|
catch (error) {
|
|
484
489
|
const message = error instanceof Error ? error.message : "Agent delivery report was invalid";
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable, idempotency_key: `bridge:invalid-delivery:${active.attemptId}` } });
|
|
490
|
+
// Contract/tooling/credential failures need a human fix. Automatic retry would repeat the same
|
|
491
|
+
// state immediately and spend another attempt before the operator can change anything.
|
|
492
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: false, idempotency_key: `bridge:invalid-delivery:${active.attemptId}` } });
|
|
489
493
|
console.error(`Assignment ${taskId} could not produce a valid Delivery: ${redactSecrets(message)}`);
|
|
490
494
|
const replyTail = reportText.slice(-8_000);
|
|
491
495
|
console.error(`Assignment ${taskId} agent reply tail (${reportText.length} chars total, redacted): ${redactSecrets(replyTail) || "<empty>"}`);
|
|
@@ -671,7 +675,10 @@ async function prepareDelivery(client, attemptId, taskId, report) {
|
|
|
671
675
|
}
|
|
672
676
|
export function validateDeliveryReport(report, spec, grants = []) {
|
|
673
677
|
validateChangeScope(report, spec.change_scope ?? []);
|
|
674
|
-
|
|
678
|
+
// Research findings live in the report. Do not demand a published pack URL for work_role=research
|
|
679
|
+
// even when a planner mis-labeled the package as deliverable=artifact.
|
|
680
|
+
const artifactDelivery = spec.deliverable === "artifact" && spec.work_role !== "research";
|
|
681
|
+
if (artifactDelivery) {
|
|
675
682
|
const published = report.evidence.some((item) => {
|
|
676
683
|
if (!["preview", "research", "documentation"].includes(item.kind))
|
|
677
684
|
return false;
|
package/dist/ops.js
CHANGED
|
@@ -6,8 +6,10 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { homedir, platform } from "node:os";
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
|
+
import { loadConfig } from "./config.js";
|
|
10
|
+
import { describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
9
11
|
export const OPS_VERBS = [
|
|
10
|
-
"connect", "install", "online", "offline", "status", "disconnect", "uninstall",
|
|
12
|
+
"connect", "install", "online", "offline", "status", "doctor", "disconnect", "uninstall",
|
|
11
13
|
];
|
|
12
14
|
const LOCAL_FUEL_DRIVERS = new Set(["cursor", "kiro", "antigravity"]);
|
|
13
15
|
export function defaultOpsEnvPath(home = homedir()) {
|
|
@@ -147,6 +149,25 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
147
149
|
runBridge(["drivers"]);
|
|
148
150
|
return;
|
|
149
151
|
}
|
|
152
|
+
if (verb === "doctor") {
|
|
153
|
+
requireOpsEnv(env);
|
|
154
|
+
if (!env.CONDUIT_WORKSPACE)
|
|
155
|
+
throw new Error(`Set CONDUIT_WORKSPACE in ${defaultOpsEnvPath()}`);
|
|
156
|
+
const config = await (deps.loadBridgeConfig ?? loadConfig)();
|
|
157
|
+
const report = await (deps.preflight ?? runBridgePreflight)({
|
|
158
|
+
config,
|
|
159
|
+
workspace: resolve(expandOpsValue(env.CONDUIT_WORKSPACE)),
|
|
160
|
+
expectedRepository: env.CONDUIT_REPO || undefined,
|
|
161
|
+
});
|
|
162
|
+
console.log(`Bridge preflight: ${report.ready ? "READY" : "BLOCKED"}`);
|
|
163
|
+
if (report.ready) {
|
|
164
|
+
console.log("Workspace, online lanes, local fuel, CLI availability, and authentication are ready.");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
for (const issue of report.issues)
|
|
168
|
+
console.log(`- ${describePreflightIssue(issue)}`);
|
|
169
|
+
throw new Error("Fix the preflight issues before retrying Conduit work");
|
|
170
|
+
}
|
|
150
171
|
// Lane toggles only need Bridge config + optional driver ids — not a full ops.env.
|
|
151
172
|
if (verb === "online" || verb === "offline") {
|
|
152
173
|
const drivers = resolveDrivers(env, argv);
|
|
@@ -177,6 +198,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
177
198
|
runBridge(["drivers", "fuel", id, "local"]);
|
|
178
199
|
}
|
|
179
200
|
runBridge(["drivers", "online", ...drivers]);
|
|
201
|
+
// Prove the exact local environment before installing a service that advertises availability.
|
|
202
|
+
runBridge(["ops", "doctor"]);
|
|
180
203
|
if (host === "win32") {
|
|
181
204
|
const runnerArgs = ["runner", "--workspace", workspace];
|
|
182
205
|
if (env.CONDUIT_REPO)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { buildWorkspaceBrief, normalizeRepositoryUrl } from "./brief.js";
|
|
4
|
+
import { hasAntigravityLogin, hasClaudeLogin, hasOpenAiLogin, hasOpenCodeLogin, resolveCodexExecutable, } from "./driver.js";
|
|
5
|
+
import { localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel } from "./drivers.js";
|
|
6
|
+
export const BRIDGE_PROTOCOL_VERSION = 1;
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
async function defaultCommandRunner(command, args, cwd) {
|
|
9
|
+
try {
|
|
10
|
+
const result = await execFileAsync(command, args, { cwd, timeout: 15_000, windowsHide: true });
|
|
11
|
+
return { code: 0, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const failure = error;
|
|
15
|
+
const code = typeof failure.code === "number" ? failure.code : 1;
|
|
16
|
+
return { code, stdout: typeof failure.stdout === "string" ? failure.stdout : "", stderr: typeof failure.stderr === "string" ? failure.stderr : "" };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const DRIVER_COMMAND = {
|
|
20
|
+
"claude-code": "claude",
|
|
21
|
+
codex: "codex",
|
|
22
|
+
cursor: "agent",
|
|
23
|
+
opencode: "opencode",
|
|
24
|
+
kiro: "kiro-cli",
|
|
25
|
+
antigravity: "agy",
|
|
26
|
+
};
|
|
27
|
+
function executableFor(driver) {
|
|
28
|
+
return driver === "codex" ? resolveCodexExecutable() : DRIVER_COMMAND[driver] ?? driver;
|
|
29
|
+
}
|
|
30
|
+
async function localAuthenticationReady(driver, workspace, run) {
|
|
31
|
+
if (driver === "claude-code")
|
|
32
|
+
return hasClaudeLogin();
|
|
33
|
+
if (driver === "codex")
|
|
34
|
+
return hasOpenAiLogin();
|
|
35
|
+
if (driver === "opencode")
|
|
36
|
+
return hasOpenCodeLogin();
|
|
37
|
+
if (driver === "antigravity")
|
|
38
|
+
return hasAntigravityLogin();
|
|
39
|
+
if (driver === "cursor") {
|
|
40
|
+
if (process.env.CURSOR_API_KEY)
|
|
41
|
+
return true;
|
|
42
|
+
const status = await run(executableFor(driver), ["status"], workspace);
|
|
43
|
+
return status.code === 0 && /logged in as/i.test(`${status.stdout}\n${status.stderr}`);
|
|
44
|
+
}
|
|
45
|
+
if (driver === "kiro") {
|
|
46
|
+
if (process.env.KIRO_API_KEY)
|
|
47
|
+
return true;
|
|
48
|
+
const status = await run(executableFor(driver), ["whoami"], workspace);
|
|
49
|
+
return status.code === 0 && !/not logged in|not authenticated|login required/i.test(`${status.stdout}\n${status.stderr}`);
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
export async function runBridgePreflight(input, deps = {}) {
|
|
54
|
+
const issues = [];
|
|
55
|
+
const online = input.processOnlineIds?.length ? input.processOnlineIds : onlineDriverIds(input.config);
|
|
56
|
+
const run = deps.runCommand ?? defaultCommandRunner;
|
|
57
|
+
const makeBrief = deps.buildBrief ?? buildWorkspaceBrief;
|
|
58
|
+
let brief = input.brief;
|
|
59
|
+
if (!online.length)
|
|
60
|
+
issues.push({ code: "no_online_driver" });
|
|
61
|
+
try {
|
|
62
|
+
brief ??= await makeBrief(input.workspace);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
issues.push({ code: "workspace_unavailable" });
|
|
66
|
+
}
|
|
67
|
+
if (brief && !brief.repository)
|
|
68
|
+
issues.push({ code: "workspace_repository_missing" });
|
|
69
|
+
if (brief?.repository && input.expectedRepository
|
|
70
|
+
&& normalizeRepositoryUrl(brief.repository) !== normalizeRepositoryUrl(input.expectedRepository)) {
|
|
71
|
+
issues.push({ code: "workspace_repository_mismatch" });
|
|
72
|
+
}
|
|
73
|
+
const driverIssues = await Promise.all(online.map(async (driver) => {
|
|
74
|
+
const fuel = resolveDriverFuel(input.config, driver);
|
|
75
|
+
if (localFuelOnlyDriver(driver) && fuel !== "local") {
|
|
76
|
+
return { code: "driver_fuel_mismatch", driver };
|
|
77
|
+
}
|
|
78
|
+
const version = await run(executableFor(driver), ["--version"], input.workspace);
|
|
79
|
+
if (version.code !== 0 || !`${version.stdout}${version.stderr}`.trim()) {
|
|
80
|
+
return { code: "driver_missing", driver };
|
|
81
|
+
}
|
|
82
|
+
if (fuel === "local" && !await localAuthenticationReady(driver, input.workspace, run)) {
|
|
83
|
+
return { code: "driver_not_authenticated", driver };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}));
|
|
87
|
+
issues.push(...driverIssues.filter((issue) => issue !== null));
|
|
88
|
+
return { ready: issues.length === 0, checked_at: new Date().toISOString(), issues };
|
|
89
|
+
}
|
|
90
|
+
let cached = null;
|
|
91
|
+
/** Keep auth probes off the 15-second heartbeat hot path while still expiring readiness promptly. */
|
|
92
|
+
export async function cachedBridgePreflight(input) {
|
|
93
|
+
const online = input.processOnlineIds?.length ? input.processOnlineIds : onlineDriverIds(input.config);
|
|
94
|
+
const key = JSON.stringify([input.workspace, input.expectedRepository ?? "", online, input.config.fuelSource, input.config.drivers]);
|
|
95
|
+
if (cached?.key === key && Date.now() - cached.at < 5 * 60_000)
|
|
96
|
+
return cached.report;
|
|
97
|
+
const report = await runBridgePreflight(input);
|
|
98
|
+
cached = { key, at: Date.now(), report };
|
|
99
|
+
return report;
|
|
100
|
+
}
|
|
101
|
+
export function describePreflightIssue(issue) {
|
|
102
|
+
const lane = issue.driver ? ` (${issue.driver})` : "";
|
|
103
|
+
if (issue.code === "no_online_driver")
|
|
104
|
+
return "No agent lane is online";
|
|
105
|
+
if (issue.code === "workspace_unavailable")
|
|
106
|
+
return "Workspace cannot be read";
|
|
107
|
+
if (issue.code === "workspace_repository_missing")
|
|
108
|
+
return "Workspace has no origin repository";
|
|
109
|
+
if (issue.code === "workspace_repository_mismatch")
|
|
110
|
+
return "Workspace origin does not match CONDUIT_REPO";
|
|
111
|
+
if (issue.code === "driver_missing")
|
|
112
|
+
return `Agent CLI is missing from PATH${lane}`;
|
|
113
|
+
if (issue.code === "driver_not_authenticated")
|
|
114
|
+
return `Agent CLI is not logged in${lane}`;
|
|
115
|
+
return `Agent lane requires local fuel${lane}`;
|
|
116
|
+
}
|
package/ops/conduit-ops
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Usage: ./conduit-ops <connect|install|online|offline|status|disconnect|uninstall> [args…]
|
|
2
|
+
# Usage: ./conduit-ops <connect|install|online|offline|status|doctor|disconnect|uninstall> [args…]
|
|
3
3
|
set -euo pipefail
|
|
4
4
|
if [[ $# -lt 1 ]]; then
|
|
5
|
-
echo "Usage: $0 <connect|install|online|offline|status|disconnect|uninstall> [args…]" >&2
|
|
5
|
+
echo "Usage: $0 <connect|install|online|offline|status|doctor|disconnect|uninstall> [args…]" >&2
|
|
6
6
|
exit 1
|
|
7
7
|
fi
|
|
8
8
|
verb=$1
|
package/ops/conduit-ops.cmd
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
@echo off
|
|
2
|
-
REM Usage: conduit-ops.cmd <connect|install|online|offline|status|disconnect|uninstall> [args…]
|
|
2
|
+
REM Usage: conduit-ops.cmd <connect|install|online|offline|status|doctor|disconnect|uninstall> [args…]
|
|
3
3
|
if "%~1"=="" (
|
|
4
|
-
echo Usage: %~nx0 ^<connect^|install^|online^|offline^|status^|disconnect^|uninstall^> [args…]
|
|
4
|
+
echo Usage: %~nx0 ^<connect^|install^|online^|offline^|status^|doctor^|disconnect^|uninstall^> [args…]
|
|
5
5
|
exit /b 1
|
|
6
6
|
)
|
|
7
7
|
npx --yes @miraland-labs/conduit-bridge@latest ops %*
|
package/ops/doctor.cmd
ADDED
package/ops/doctor.sh
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|