@miraland-labs/conduit-bridge 0.12.2 → 0.12.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.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`.
5
+ **Package version:** `0.12.3` — `ops connect` asks which org unless `--organization` is passed (does **not** silently reuse a leftover `CONDUIT_ORG` from `ops.env`); `ops install --workspace/--repo` persists declared intent without hand-editing the file; stamped **ExecutionClass** required; heartbeat **protocol 2 + preflight**. Prefer the Connect UI **join** command when switching organizations.
6
6
 
7
7
  ## Prerequisites
8
8
 
@@ -18,19 +18,27 @@ Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connect
18
18
 
19
19
  ## Easy path (all platforms)
20
20
 
21
- One config file + the same `ops` commands on macOS, Linux, and Windows:
21
+ **Preferred:** copy **join** from Conduit Computers (includes `--organization` for the org you are signed into), approve, then:
22
+
23
+ ```bash
24
+ npx @miraland-labs/conduit-bridge@latest ops install \
25
+ --workspace /path/to/repo \
26
+ --repo https://github.com/org/repo
27
+ npx @miraland-labs/conduit-bridge@latest ops status
28
+ ```
29
+
30
+ **Alternate** (optional `ops.env` for URL/workspace defaults):
22
31
 
23
32
  ```bash
24
- # 1) Config once
25
33
  mkdir -p ~/.config/conduit
26
34
  # Windows: mkdir %USERPROFILE%\.config\conduit
27
35
  npx @miraland-labs/conduit-bridge@latest init-ops
28
36
  cp ~/conduit/ops/env.example ~/.config/conduit/ops.env
29
- # edit ops.env (URL, WORKSPACE, optional REPO, DRIVERS, ROLES)
37
+ # edit: CONDUIT_URL; leave CONDUIT_ORG unset (or commented) so connect asks
30
38
 
31
- # 2) Connect approve in browser → install
32
- npx @miraland-labs/conduit-bridge@latest ops connect
33
- npx @miraland-labs/conduit-bridge@latest ops install
39
+ npx @miraland-labs/conduit-bridge@latest ops connect # asks "Join which org?"
40
+ # or: ops connect --organization your-org-slug
41
+ npx @miraland-labs/conduit-bridge@latest ops install --workspace /path/to/repo
34
42
  npx @miraland-labs/conduit-bridge@latest ops status
35
43
  ```
36
44
 
@@ -39,7 +47,7 @@ Later:
39
47
  ```bash
40
48
  npx @miraland-labs/conduit-bridge@latest ops online cursor
41
49
  npx @miraland-labs/conduit-bridge@latest ops offline cursor
42
- npx @miraland-labs/conduit-bridge@latest ops disconnect
50
+ npx @miraland-labs/conduit-bridge@latest ops disconnect # then join again to change organizations
43
51
  ```
44
52
 
45
53
  Optional local shortcuts after `init-ops` (same verbs):
@@ -51,7 +59,7 @@ Optional local shortcuts after `init-ops` (same verbs):
51
59
 
52
60
  **Windows note:** `ops install` brings lanes online and prints a `runner --workspace …` command to keep open (no LaunchAgent). macOS/Linux install a background service.
53
61
 
54
- `ops.env` keys: `CONDUIT_URL`, `CONDUIT_ORG` (optional), `CONDUIT_WORKSPACE`, `CONDUIT_REPO` (optional), `CONDUIT_DRIVERS` (default: auto-detect installed agents), `CONDUIT_ROLES` (default `implement research review`). Values expand `$HOME`, `%USERPROFILE%`, and `~`.
62
+ `ops.env` keys: `CONDUIT_URL`, `CONDUIT_ORG` (optional; leave unset for interactive org prompt on `ops connect`), `CONDUIT_WORKSPACE`, `CONDUIT_REPO` (optional), `CONDUIT_DRIVERS` (default: auto-detect installed agents), `CONDUIT_ROLES` (default `implement research review`). Values expand `$HOME`, `%USERPROFILE%`, and `~`. Prefer `ops install --workspace/--repo` over hand-editing workspace keys.
55
63
 
56
64
  ## Advanced CLI
57
65
 
@@ -30,6 +30,61 @@ export function pickVerificationCommand(commands) {
30
30
  function argvForBoundedCommand(command) {
31
31
  return command.trim().split(/\s+/);
32
32
  }
33
+ /**
34
+ * Why an agent failure needs its own verification run.
35
+ *
36
+ * When a driver run fails, `result.error` is `stderr || resultText` — and a coding agent runs its
37
+ * own verification *inside its tool loop*, so that output never reaches Bridge's stderr. Production
38
+ * proved the consequence: the failure reason reaching Conduit was a bare git SHA or a chunk of the
39
+ * agent's echoed source, and Conductor had nothing to diagnose from.
40
+ *
41
+ * Running the project's own bounded verification command in the attempt worktree recovers the real
42
+ * compiler/test output. Read-only by construction: the same bounded command list Bridge already
43
+ * trusts for test evidence, in the workspace the failed attempt left behind.
44
+ */
45
+ /**
46
+ * Bounded commands to try for failure diagnosis. Unlike `pickVerificationCommand` (used for
47
+ * delivery test evidence), do **not** prefer `npm run test` alone: a green test suite must not
48
+ * hide a failed typecheck/lint/build that the agent broke.
49
+ */
50
+ export function diagnosisVerificationCommands(commands) {
51
+ const bounded = commands
52
+ .map((command) => command.trim())
53
+ .filter((command) => command && isBoundedVerificationCommand(command));
54
+ const nonTest = bounded.filter((command) => !isPreferentialTestCommand(command));
55
+ const tests = bounded.filter((command) => isPreferentialTestCommand(command));
56
+ return [...nonTest, ...tests];
57
+ }
58
+ export async function captureVerificationFailure(input) {
59
+ const commands = diagnosisVerificationCommands(input.verificationCommands);
60
+ if (commands.length === 0)
61
+ return null;
62
+ const run = input.runCommand ?? defaultRunCommand;
63
+ // Try each bounded command until one fails. First non-zero wins (typecheck before test when both exist).
64
+ for (const command of commands) {
65
+ let result;
66
+ try {
67
+ result = await run(command, input.workspace);
68
+ }
69
+ catch {
70
+ // Diagnosis is best-effort: try the next command rather than failing the attempt again.
71
+ continue;
72
+ }
73
+ if (result.code === 0)
74
+ continue;
75
+ const lines = [
76
+ `$ ${command}`,
77
+ ...(result.stdout.trim() ? result.stdout.trim().split("\n") : []),
78
+ ...(result.stderr.trim() ? result.stderr.trim().split("\n") : []),
79
+ `exit ${result.code}`,
80
+ ].map((line) => line.slice(0, 4_000)).slice(0, 120);
81
+ const text = lines.join("\n").slice(0, 12_000);
82
+ if (text.trim())
83
+ return text;
84
+ }
85
+ // Every bounded command passed (or none could start): agent failed for another reason.
86
+ return null;
87
+ }
33
88
  async function defaultRunCommand(command, workspace) {
34
89
  const argv = argvForBoundedCommand(command);
35
90
  const bin = argv[0];
package/dist/execution.js CHANGED
@@ -8,7 +8,7 @@ import { pickDriverForClaim, resolveDriverFuel } from "./drivers.js";
8
8
  import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
9
9
  import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
10
10
  import { ensureDeliveryPullRequest } from "./ensure-pull-request.js";
11
- import { ensureTestEvidence } from "./ensure-test-evidence.js";
11
+ import { captureVerificationFailure, ensureTestEvidence } from "./ensure-test-evidence.js";
12
12
  /** Feedback text for changes_requested summaries (plain string or `{ feedback }`). */
13
13
  function changesRequestedFeedback(summary) {
14
14
  if (!summary)
@@ -468,8 +468,22 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
468
468
  if (result.sessionId)
469
469
  config.sessions = { ...config.sessions, [taskId]: result.sessionId };
470
470
  if (result.status === "failed") {
471
- const message = result.error ?? "Agent execution failed";
472
- await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(message), idempotency_key: `bridge:fail:${active.attemptId}` } });
471
+ const agentMessage = result.error ?? "Agent execution failed";
472
+ // The agent verifies inside its own tool loop, so its compiler/test output never reaches
473
+ // result.error — production saw a bare git SHA and echoed source arrive as the "failure
474
+ // reason", leaving Conductor nothing to diagnose. Re-run the project's bounded verification
475
+ // in the attempt worktree to recover the real errors. Best-effort: keep the agent's own
476
+ // message when there is no command, the command cannot run, or the tree actually verifies.
477
+ const verificationDetail = await captureVerificationFailure({
478
+ workspace: attemptWorkspace,
479
+ verificationCommands: liveBrief?.verification ?? [],
480
+ });
481
+ const message = verificationDetail
482
+ ? `Verification failed after the agent run.\n${verificationDetail}`
483
+ : agentMessage;
484
+ // Retryability stays keyed to what the agent reported; a recovered verification log describes
485
+ // the same run and must not silently reclassify an unretryable failure.
486
+ await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(agentMessage), idempotency_key: `bridge:fail:${active.attemptId}` } });
473
487
  console.error(`Assignment ${taskId} failed: ${redactSecrets(message)}`);
474
488
  return;
475
489
  }
package/dist/ops.js CHANGED
@@ -310,14 +310,43 @@ export async function runOps(verb, argv = [], deps = {}) {
310
310
  return;
311
311
  }
312
312
  if (verb === "connect") {
313
- if (!env.CONDUIT_URL)
314
- throw new Error(`Set CONDUIT_URL in ${defaultOpsEnvPath()}`);
315
- const args = ["join", "--url", env.CONDUIT_URL];
316
- if (env.CONDUIT_ORG)
317
- args.push("--organization", env.CONDUIT_ORG);
313
+ const { values } = parseArgs({
314
+ args: argv,
315
+ options: {
316
+ organization: { type: "string", short: "o" },
317
+ url: { type: "string" },
318
+ },
319
+ allowPositionals: true,
320
+ });
321
+ const url = (values.url?.trim() || env.CONDUIT_URL).trim();
322
+ if (!url)
323
+ throw new Error(`Set CONDUIT_URL in ${defaultOpsEnvPath()} or pass --url`);
324
+ // Never silently reuse CONDUIT_ORG from ops.env — a leftover public-org slug skipped
325
+ // "Join which org?" and enrolled the wrong workspace. Pass --organization explicitly, or
326
+ // omit it so join prompts interactively (and suggest the prior slug when present).
327
+ const organization = values.organization?.trim().toLowerCase() || "";
328
+ if (organization && !/^[a-z0-9][a-z0-9-]{0,62}$/.test(organization)) {
329
+ throw new Error("Organization must be its lowercase workspace slug");
330
+ }
331
+ const envPath = env.loadedFrom ?? defaultOpsEnvPath();
332
+ writeOpsEnvFile(envPath, {
333
+ CONDUIT_URL: url,
334
+ ...(organization ? { CONDUIT_ORG: organization } : {}),
335
+ });
336
+ const args = ["join", "--url", url];
337
+ if (organization)
338
+ args.push("--organization", organization);
318
339
  for (const role of splitOpsList(env.CONDUIT_ROLES))
319
340
  args.push("--capability", role);
320
- console.log(`Connecting to ${env.CONDUIT_URL} (roles: ${env.CONDUIT_ROLES})`);
341
+ console.log(`Connecting to ${url} (roles: ${env.CONDUIT_ROLES})`);
342
+ if (organization) {
343
+ console.log(`Organization: ${organization}`);
344
+ }
345
+ else {
346
+ console.log(env.CONDUIT_ORG
347
+ ? `Organization not set on the command — join will ask. (ops.env still has ${env.CONDUIT_ORG}; type the slug you want now.)`
348
+ : "Organization not set — join will ask which org to connect.");
349
+ }
321
350
  console.log("Approve this computer in Connect when the browser opens.");
322
351
  runBridge(args);
323
352
  return;
package/ops/env.example CHANGED
@@ -8,6 +8,9 @@
8
8
  #
9
9
  # Replace the placeholders below with YOUR org, local checkout, and (optional) git remote.
10
10
  CONDUIT_URL=https://api.conduit.miraland.io
11
+ # Leave CONDUIT_ORG unset so `ops connect` asks "Join which org?".
12
+ # Or pass once: ops connect --organization your-org-slug
13
+ # Prefer the Connect UI join command (includes the org you are signed into).
11
14
  # CONDUIT_ORG=your-org-slug
12
15
  CONDUIT_WORKSPACE=$HOME/path/to/your-repo
13
16
  # CONDUIT_REPO=https://github.com/your-org/your-repo.git
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.12.2",
3
+ "version": "0.12.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": {