@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 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` — **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`.
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
- await heartbeat(client, config, workspace ? await buildWorkspaceBrief(workspace).catch(() => brief) : null, processOnlineIds);
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 () => heartbeat(client, config, await buildWorkspaceBrief(workspace).catch(() => brief), processOnlineIds),
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 artifact = (packageContext?.deliverable ?? spec.deliverable) === "artifact";
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 || /not logged in/i.test(status.stdout)) {
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
- const configured = await withCursorPermissions(input.workspace, cursorPermissionsForGrants(input.grants, input.verificationCommands), () => execute(executable, cursorRunArgs(input), input.workspace, input.timeoutMs ?? 20 * 60_000, undefined, "local"));
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
- await queueTerminal(client, taskId, { action: "fail", body: { error: result.error ?? "Agent execution failed", idempotency_key: `bridge:fail:${active.attemptId}` } });
409
- console.error(`Assignment ${taskId} failed: ${redactSecrets(result.error ?? "unknown")}`);
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: true, idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
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: true, idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
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: true, idempotency_key: `bridge:delivery-repair-invalid:${active.attemptId}` } });
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
- // Missing PR tooling / credential is retryable once the environment is fixed; other contract
486
- // failures (scope, evidence mapping) stay non-retryable.
487
- const retryable = /pull_request|merge_request|forge|GitHub|GitLab|gh |push|credential/i.test(message);
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
- if (spec.deliverable === "artifact") {
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
@@ -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
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ npx --yes @miraland-labs/conduit-bridge@latest ops doctor %*
3
+ if errorlevel 1 exit /b %errorlevel%
package/ops/doctor.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ exec npx --yes @miraland-labs/conduit-bridge@latest ops doctor "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.9.5",
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": {