@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 +17 -9
- package/dist/ensure-test-evidence.js +55 -0
- package/dist/execution.js +17 -3
- package/dist/ops.js +35 -6
- package/ops/env.example +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
|
-
**Package version:** `0.
|
|
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
|
-
|
|
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
|
|
37
|
+
# edit: CONDUIT_URL; leave CONDUIT_ORG unset (or commented) so connect asks
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
472
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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 ${
|
|
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.
|
|
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": {
|