@miraland-labs/conduit-bridge 0.11.12 → 0.12.1
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 +1 -1
- package/dist/driver.js +57 -6
- package/dist/ensure-pull-request.js +7 -4
- package/dist/execution.js +69 -51
- package/dist/ops.js +63 -17
- package/ops/env.example +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Optional local shortcuts after `init-ops` (same verbs):
|
|
|
51
51
|
|
|
52
52
|
**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
53
|
|
|
54
|
-
`ops.env` keys: `CONDUIT_URL`, `CONDUIT_ORG` (optional), `CONDUIT_WORKSPACE`, `CONDUIT_REPO` (optional), `CONDUIT_DRIVERS` (default
|
|
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 `~`.
|
|
55
55
|
|
|
56
56
|
## Advanced CLI
|
|
57
57
|
|
package/dist/cli.js
CHANGED
|
@@ -359,7 +359,7 @@ async function initOps() {
|
|
|
359
359
|
async function opsCommand() {
|
|
360
360
|
const verb = process.argv[3];
|
|
361
361
|
if (!verb || !OPS_VERBS.includes(verb)) {
|
|
362
|
-
throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|doctor|disconnect|uninstall>", "[driver…]")}`);
|
|
362
|
+
throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|doctor|disconnect|uninstall>", "[driver…]")} (install also takes --workspace <path> [--repo <url>])`);
|
|
363
363
|
}
|
|
364
364
|
await runOps(verb, process.argv.slice(4));
|
|
365
365
|
}
|
package/dist/driver.js
CHANGED
|
@@ -139,17 +139,68 @@ export function buildAssignmentPrompt(context) {
|
|
|
139
139
|
"- EVERY criterion you mark \"met\" must appear in the acceptance_criteria list of at least one evidence entry. A met criterion with no evidence mapped to it fails the whole delivery — mark it unknown instead, or add the evidence that supports it.", "", "When the work is finished, end your reply with exactly one fenced ```json block:", agentReportTemplate);
|
|
140
140
|
return lines.join("\n");
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Extract report JSON text from an agent reply. Prefers the last closed ```json fence,
|
|
144
|
+
* then an unclosed ```json fence (truncated replies), then a balanced top-level object.
|
|
145
|
+
*/
|
|
146
|
+
export function extractAgentReportJsonText(text) {
|
|
147
|
+
const closed = [...text.matchAll(/```json\s*([\s\S]*?)```/gi)];
|
|
148
|
+
const fromFence = closed.at(-1)?.[1]?.trim();
|
|
149
|
+
if (fromFence)
|
|
150
|
+
return fromFence;
|
|
151
|
+
const unclosed = /```json\s*([\s\S]*)$/i.exec(text);
|
|
152
|
+
const fromUnclosed = unclosed?.[1]?.trim();
|
|
153
|
+
if (fromUnclosed)
|
|
154
|
+
return fromUnclosed;
|
|
155
|
+
const start = text.lastIndexOf("{");
|
|
156
|
+
if (start < 0)
|
|
157
|
+
throw new Error("Agent did not emit the required structured report");
|
|
158
|
+
return text.slice(start).trim();
|
|
159
|
+
}
|
|
160
|
+
/** Parse JSON, or the first balanced `{...}` object when the candidate is truncated prose. */
|
|
161
|
+
export function parseJsonObjectCandidate(candidate) {
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(candidate);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
const start = candidate.indexOf("{");
|
|
167
|
+
if (start < 0)
|
|
168
|
+
throw new Error("Agent emitted malformed report JSON");
|
|
169
|
+
let depth = 0;
|
|
170
|
+
let inString = false;
|
|
171
|
+
for (let i = start; i < candidate.length; i++) {
|
|
172
|
+
const ch = candidate[i];
|
|
173
|
+
if (inString) {
|
|
174
|
+
if (ch === "\\")
|
|
175
|
+
i++;
|
|
176
|
+
else if (ch === '"')
|
|
177
|
+
inString = false;
|
|
178
|
+
}
|
|
179
|
+
else if (ch === '"')
|
|
180
|
+
inString = true;
|
|
181
|
+
else if (ch === "{")
|
|
182
|
+
depth++;
|
|
183
|
+
else if (ch === "}" && --depth === 0) {
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(candidate.slice(start, i + 1));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw new Error("Agent emitted malformed report JSON");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw new Error("Agent emitted malformed report JSON");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
142
195
|
/** Parse the agent's final fenced JSON block into a bounded report. */
|
|
143
196
|
export function parseAgentReport(text, acceptance) {
|
|
144
|
-
const blocks = [...text.matchAll(/```json\s*([\s\S]*?)```/g)];
|
|
145
|
-
const last = blocks.at(-1)?.[1];
|
|
146
|
-
if (!last)
|
|
147
|
-
throw new Error("Agent did not emit the required structured report");
|
|
148
197
|
let raw;
|
|
149
198
|
try {
|
|
150
|
-
raw =
|
|
199
|
+
raw = parseJsonObjectCandidate(extractAgentReportJsonText(text));
|
|
151
200
|
}
|
|
152
|
-
catch {
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error instanceof Error && /structured report|malformed report JSON/.test(error.message))
|
|
203
|
+
throw error;
|
|
153
204
|
throw new Error("Agent emitted malformed report JSON");
|
|
154
205
|
}
|
|
155
206
|
const stringList = z.array(z.string().trim().min(1).max(4_000)).max(100);
|
|
@@ -151,17 +151,20 @@ export async function pushOriginHead(workspace, options = {}) {
|
|
|
151
151
|
throw new Error(lastMessage);
|
|
152
152
|
}
|
|
153
153
|
/**
|
|
154
|
-
* Bind
|
|
154
|
+
* Bind head_commit to the checkout HEAD, then open a PR when still needed.
|
|
155
|
+
* Workspace HEAD is authoritative for scoped repository deliveries: agents (especially
|
|
156
|
+
* Experimental local-fuel lanes) often invent or paste a wrong SHA. Prefer the worktree
|
|
157
|
+
* over failing a delivery that already committed correctly.
|
|
155
158
|
* Agent-supplied PR URLs skip creation but still require HEAD identity.
|
|
156
159
|
*/
|
|
157
160
|
export async function ensureDeliveryPullRequest(input) {
|
|
158
161
|
const readHead = input.readHeadCommit ?? workspaceHeadCommit;
|
|
159
162
|
const push = input.pushOriginHead ?? pushOriginHead;
|
|
160
163
|
let report = adoptPullRequestUrlFromEvidence(input.report, input.repositoryFingerprint);
|
|
161
|
-
if (
|
|
164
|
+
if ((input.spec.change_scope?.length ?? 0) > 0) {
|
|
162
165
|
const head = await readHead(input.workspace);
|
|
163
|
-
if (!commitsMatch(report.head_commit, head)) {
|
|
164
|
-
|
|
166
|
+
if (report.head_commit && !commitsMatch(report.head_commit, head)) {
|
|
167
|
+
console.error(`Agent head_commit ${report.head_commit} does not match workspace HEAD ${head}; using workspace HEAD`);
|
|
165
168
|
}
|
|
166
169
|
report = { ...report, head_commit: head };
|
|
167
170
|
}
|
package/dist/execution.js
CHANGED
|
@@ -479,59 +479,76 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
479
479
|
report = parseAgentReport(reportText, spec.acceptance ?? []);
|
|
480
480
|
}
|
|
481
481
|
catch (error) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
let
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
// capability is available while the agent repairs the response envelope.
|
|
497
|
-
grants: grants.filter((grant) => grant === "repo_read"),
|
|
498
|
-
capabilities: [],
|
|
499
|
-
verificationCommands: [],
|
|
500
|
-
// Envelope repair is read-only observe authority regardless of the original class.
|
|
501
|
-
executionClass: "observe",
|
|
502
|
-
resumeSessionId: result.sessionId ?? undefined,
|
|
503
|
-
timeoutMs,
|
|
504
|
-
model: selection.model,
|
|
505
|
-
fuel,
|
|
506
|
-
fuelSource,
|
|
482
|
+
// Pump fuel keeps one repair turn. Local-fuel Experimental lanes (Pi et al.) get a second
|
|
483
|
+
// read-only repair — truncated JSON fences are common there and re-implementing is wasteful.
|
|
484
|
+
const maxRepairs = fuelSource === "local" ? 2 : 1;
|
|
485
|
+
let parseError = error instanceof Error ? error.message : "Agent delivery report was invalid";
|
|
486
|
+
let previousReply = reportText;
|
|
487
|
+
let resumeSessionId = result.sessionId ?? undefined;
|
|
488
|
+
let repaired = null;
|
|
489
|
+
let parsed = null;
|
|
490
|
+
for (let repairTurn = 1; repairTurn <= maxRepairs; repairTurn++) {
|
|
491
|
+
const replyDigest = createHash("sha256").update(previousReply).digest("hex");
|
|
492
|
+
await client.attemptRequest(taskId, "progress", {
|
|
493
|
+
phase: "preparing_delivery",
|
|
494
|
+
message: `Delivery envelope invalid (${redactSecrets(parseError)}); starting report-only repair turn ${repairTurn}/${maxRepairs} without rerunning implementation. Original reply: ${previousReply.length} characters, sha256:${replyDigest}.`,
|
|
495
|
+
idempotency_key: `bridge:progress:${active.attemptId}:delivery-repair:${repairTurn}`,
|
|
507
496
|
});
|
|
497
|
+
try {
|
|
498
|
+
repaired = await driver.run({
|
|
499
|
+
prompt: buildDeliveryRepairPrompt(parseError, previousReply, spec.acceptance ?? []),
|
|
500
|
+
workspace: attemptWorkspace,
|
|
501
|
+
// Preserve only existing read authority. No write, test, branch, or push
|
|
502
|
+
// capability is available while the agent repairs the response envelope.
|
|
503
|
+
grants: grants.filter((grant) => grant === "repo_read"),
|
|
504
|
+
capabilities: [],
|
|
505
|
+
verificationCommands: [],
|
|
506
|
+
// Envelope repair is read-only observe authority regardless of the original class.
|
|
507
|
+
executionClass: "observe",
|
|
508
|
+
resumeSessionId,
|
|
509
|
+
timeoutMs,
|
|
510
|
+
model: selection.model,
|
|
511
|
+
fuel,
|
|
512
|
+
fuelSource,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch (repairError) {
|
|
516
|
+
const message = repairError instanceof Error ? repairError.message : "Delivery report repair failed";
|
|
517
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair failed: ${message}`, retryable: retryableAgentFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
518
|
+
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (repaired.sessionId) {
|
|
522
|
+
config.sessions = { ...config.sessions, [taskId]: repaired.sessionId };
|
|
523
|
+
resumeSessionId = repaired.sessionId;
|
|
524
|
+
}
|
|
525
|
+
if (repaired.status === "failed") {
|
|
526
|
+
const message = repaired.error ?? "Delivery report repair failed";
|
|
527
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
528
|
+
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
previousReply = repaired.resultText ?? "";
|
|
532
|
+
try {
|
|
533
|
+
parsed = parseAgentReport(previousReply, spec.acceptance ?? []);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
catch (repairError) {
|
|
537
|
+
parseError = repairError instanceof Error ? repairError.message : "Repaired delivery report was invalid";
|
|
538
|
+
if (repairTurn >= maxRepairs) {
|
|
539
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair exhausted: ${parseError}`, retryable: false, idempotency_key: `bridge:delivery-repair-invalid:${active.attemptId}` } });
|
|
540
|
+
console.error(`Assignment ${taskId} exhausted its report-only repair: ${redactSecrets(parseError)}`);
|
|
541
|
+
const replyTail = previousReply.slice(-8_000);
|
|
542
|
+
console.error(`Assignment ${taskId} repaired reply tail (${previousReply.length} chars total, redacted): ${redactSecrets(replyTail) || "<empty>"}`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
console.error(`Assignment ${taskId} report-only repair turn ${repairTurn} still invalid: ${redactSecrets(parseError)}; retrying`);
|
|
546
|
+
}
|
|
508
547
|
}
|
|
509
|
-
|
|
510
|
-
const message = repairError instanceof Error ? repairError.message : "Delivery report repair failed";
|
|
511
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair failed: ${message}`, retryable: retryableAgentFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
512
|
-
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
548
|
+
if (!parsed)
|
|
513
549
|
return;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
config.sessions = { ...config.sessions, [taskId]: repaired.sessionId };
|
|
517
|
-
if (repaired.status === "failed") {
|
|
518
|
-
const message = repaired.error ?? "Delivery report repair failed";
|
|
519
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
520
|
-
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
try {
|
|
524
|
-
reportText = repaired.resultText ?? "";
|
|
525
|
-
report = parseAgentReport(reportText, spec.acceptance ?? []);
|
|
526
|
-
}
|
|
527
|
-
catch (repairError) {
|
|
528
|
-
const message = repairError instanceof Error ? repairError.message : "Repaired delivery report was invalid";
|
|
529
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair exhausted: ${message}`, retryable: false, idempotency_key: `bridge:delivery-repair-invalid:${active.attemptId}` } });
|
|
530
|
-
console.error(`Assignment ${taskId} exhausted its report-only repair: ${redactSecrets(message)}`);
|
|
531
|
-
const replyTail = (repaired.resultText ?? "").slice(-8_000);
|
|
532
|
-
console.error(`Assignment ${taskId} repaired reply tail (${(repaired.resultText ?? "").length} chars total, redacted): ${redactSecrets(replyTail) || "<empty>"}`);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
550
|
+
reportText = previousReply;
|
|
551
|
+
report = parsed;
|
|
535
552
|
}
|
|
536
553
|
try {
|
|
537
554
|
// Mechanical land path: agent may forget gh; Bridge pushes and the control plane opens the PR.
|
|
@@ -604,9 +621,10 @@ function buildDeliveryRepairPrompt(parseError, previousReply, acceptance) {
|
|
|
604
621
|
return [
|
|
605
622
|
"The implementation turn is complete. Perform exactly one report-only repair.",
|
|
606
623
|
"Do not inspect or modify files. Do not run commands or tools. Do not redo implementation.",
|
|
607
|
-
"Return only one corrected fenced ```json Delivery object and no other prose.",
|
|
624
|
+
"Return only one corrected fenced ```json Delivery object and no other prose. Close the fence.",
|
|
608
625
|
"Preserve the previous reply's substantive outcome, changes, verification, acceptance statuses, evidence, assumptions, risks, and limitations.",
|
|
609
626
|
"Use only claims and evidence already present in the previous reply. Do not invent evidence, broaden scope, change reported work, or claim a criterion is met without existing supporting evidence.",
|
|
627
|
+
"Omit head_commit if uncertain — Bridge binds workspace HEAD. Keep pull_request_url only when the previous reply already had a real PR URL.",
|
|
610
628
|
"",
|
|
611
629
|
"REQUIRED DELIVERY SHAPE",
|
|
612
630
|
agentReportTemplate,
|
package/dist/ops.js
CHANGED
|
@@ -9,6 +9,8 @@ import { dirname, join, resolve } from "node:path";
|
|
|
9
9
|
import { parseArgs } from "node:util";
|
|
10
10
|
import { ConduitClient } from "./client.js";
|
|
11
11
|
import { loadConfig } from "./config.js";
|
|
12
|
+
import { detectInstalledClients } from "./detect.js";
|
|
13
|
+
import { driverIdsFromDetectedLabels } from "./drivers.js";
|
|
12
14
|
import { describePreflightIssue, runBridgePreflight } from "./preflight.js";
|
|
13
15
|
export const OPS_VERBS = [
|
|
14
16
|
"connect", "install", "switch", "online", "offline", "status", "doctor", "disconnect", "uninstall",
|
|
@@ -73,7 +75,7 @@ export function loadOpsEnv(home = homedir(), cwd = process.cwd()) {
|
|
|
73
75
|
CONDUIT_ORG: pick("CONDUIT_ORG", ""),
|
|
74
76
|
CONDUIT_WORKSPACE: pick("CONDUIT_WORKSPACE", ""),
|
|
75
77
|
CONDUIT_REPO: pick("CONDUIT_REPO", ""),
|
|
76
|
-
CONDUIT_DRIVERS: pick("CONDUIT_DRIVERS", "
|
|
78
|
+
CONDUIT_DRIVERS: pick("CONDUIT_DRIVERS", ""),
|
|
77
79
|
CONDUIT_ROLES: pick("CONDUIT_ROLES", "implement research review"),
|
|
78
80
|
loadedFrom,
|
|
79
81
|
};
|
|
@@ -120,12 +122,21 @@ export function writeOpsEnvFile(path, patch, existingText) {
|
|
|
120
122
|
}
|
|
121
123
|
writeFileSync(path, `${next.filter((line, index) => !(index === next.length - 1 && line === "")).join("\n").replace(/\n*$/, "\n")}`, { mode: 0o600 });
|
|
122
124
|
}
|
|
123
|
-
|
|
125
|
+
/** Explicit ids win; otherwise CONDUIT_DRIVERS; otherwise agents detected on PATH (fail when none). */
|
|
126
|
+
export async function resolveDrivers(env, argv, detect = detectInstalledClients) {
|
|
124
127
|
const fromArgs = argv.map((item) => item.trim()).filter(Boolean);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
if (fromArgs.length)
|
|
129
|
+
return fromArgs;
|
|
130
|
+
const fromEnv = splitOpsList(env.CONDUIT_DRIVERS);
|
|
131
|
+
if (fromEnv.length)
|
|
132
|
+
return fromEnv;
|
|
133
|
+
const detected = driverIdsFromDetectedLabels(await detect());
|
|
134
|
+
if (detected.length) {
|
|
135
|
+
console.log(`CONDUIT_DRIVERS not set — using detected agents: ${detected.join(", ")}`);
|
|
136
|
+
return detected;
|
|
137
|
+
}
|
|
138
|
+
throw new Error("No coding agent found on this computer. Install one (Claude Code, Codex, Cursor, OpenCode, Pi, Kiro, Antigravity), " +
|
|
139
|
+
"then retry — or set CONDUIT_DRIVERS / pass driver ids explicitly.");
|
|
129
140
|
}
|
|
130
141
|
/** Quote args for a copy-pasteable shell/cmd line (paths with spaces). */
|
|
131
142
|
export function shellQuoteArgs(args) {
|
|
@@ -171,7 +182,7 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
171
182
|
console.log(`URL: ${env.CONDUIT_URL || "(unset)"}`);
|
|
172
183
|
console.log(`Workspace: ${env.CONDUIT_WORKSPACE || "(unset)"}`);
|
|
173
184
|
console.log(`Repo: ${env.CONDUIT_REPO || "(none)"}`);
|
|
174
|
-
console.log(`Drivers: ${env.CONDUIT_DRIVERS}`);
|
|
185
|
+
console.log(`Drivers: ${env.CONDUIT_DRIVERS || "(auto-detect installed agents)"}`);
|
|
175
186
|
console.log(`Roles: ${env.CONDUIT_ROLES}`);
|
|
176
187
|
console.log("");
|
|
177
188
|
}
|
|
@@ -202,11 +213,45 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
202
213
|
}
|
|
203
214
|
// Lane toggles only need Bridge config + optional driver ids — not a full ops.env.
|
|
204
215
|
if (verb === "online" || verb === "offline") {
|
|
205
|
-
const drivers = resolveDrivers(env, argv);
|
|
216
|
+
const drivers = await resolveDrivers(env, argv);
|
|
206
217
|
runBridge(["drivers", verb, ...drivers]);
|
|
207
218
|
return;
|
|
208
219
|
}
|
|
209
|
-
|
|
220
|
+
// `ops install --workspace <path> [--repo <url>]` persists declared intent into ops.env so the
|
|
221
|
+
// Connect UI path works without hand-editing a config file first.
|
|
222
|
+
let installEnv = env;
|
|
223
|
+
let installArgv = argv;
|
|
224
|
+
if (verb === "install") {
|
|
225
|
+
const { values, positionals } = parseArgs({
|
|
226
|
+
args: argv,
|
|
227
|
+
options: {
|
|
228
|
+
workspace: { type: "string" },
|
|
229
|
+
repo: { type: "string" },
|
|
230
|
+
},
|
|
231
|
+
allowPositionals: true,
|
|
232
|
+
});
|
|
233
|
+
installArgv = positionals;
|
|
234
|
+
const declaredWorkspace = values.workspace?.trim();
|
|
235
|
+
const declaredRepo = values.repo?.trim();
|
|
236
|
+
if (declaredWorkspace || declaredRepo) {
|
|
237
|
+
const envPath = env.loadedFrom ?? defaultOpsEnvPath();
|
|
238
|
+
writeOpsEnvFile(envPath, {
|
|
239
|
+
...(declaredWorkspace ? { CONDUIT_WORKSPACE: declaredWorkspace } : {}),
|
|
240
|
+
...(declaredRepo ? { CONDUIT_REPO: declaredRepo } : {}),
|
|
241
|
+
});
|
|
242
|
+
console.log(`Saved ${[
|
|
243
|
+
declaredWorkspace ? `workspace ${declaredWorkspace}` : "",
|
|
244
|
+
declaredRepo ? `repo ${declaredRepo}` : "",
|
|
245
|
+
].filter(Boolean).join(" and ")} to ${envPath}`);
|
|
246
|
+
installEnv = {
|
|
247
|
+
...env,
|
|
248
|
+
...(declaredWorkspace ? { CONDUIT_WORKSPACE: declaredWorkspace } : {}),
|
|
249
|
+
...(declaredRepo ? { CONDUIT_REPO: declaredRepo } : {}),
|
|
250
|
+
loadedFrom: env.loadedFrom ?? envPath,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
requireOpsEnv(verb === "install" ? installEnv : env);
|
|
210
255
|
if (verb === "switch") {
|
|
211
256
|
const { values } = parseArgs({
|
|
212
257
|
args: argv,
|
|
@@ -278,10 +323,11 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
278
323
|
return;
|
|
279
324
|
}
|
|
280
325
|
if (verb === "install") {
|
|
281
|
-
if (!
|
|
282
|
-
throw new Error(`
|
|
283
|
-
|
|
284
|
-
const
|
|
326
|
+
if (!installEnv.CONDUIT_WORKSPACE) {
|
|
327
|
+
throw new Error(`No workspace declared. Rerun with --workspace /path/to/repo, or set CONDUIT_WORKSPACE in ${defaultOpsEnvPath()}`);
|
|
328
|
+
}
|
|
329
|
+
const workspace = resolve(expandOpsValue(installEnv.CONDUIT_WORKSPACE));
|
|
330
|
+
const drivers = await resolveDrivers(installEnv, installArgv);
|
|
285
331
|
for (const id of drivers) {
|
|
286
332
|
if (LOCAL_FUEL_DRIVERS.has(id))
|
|
287
333
|
runBridge(["drivers", "fuel", id, "local"]);
|
|
@@ -291,8 +337,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
291
337
|
runBridge(["ops", "doctor"]);
|
|
292
338
|
if (host === "win32") {
|
|
293
339
|
const runnerArgs = ["runner", "--workspace", workspace];
|
|
294
|
-
if (
|
|
295
|
-
runnerArgs.push("--ensure-checkout",
|
|
340
|
+
if (installEnv.CONDUIT_REPO)
|
|
341
|
+
runnerArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
|
|
296
342
|
console.log("Windows: background install-service is not available.");
|
|
297
343
|
console.log("Keep a terminal open and run:");
|
|
298
344
|
console.log(` npx @miraland-labs/conduit-bridge@latest ${shellQuoteArgs(runnerArgs)}`);
|
|
@@ -300,8 +346,8 @@ export async function runOps(verb, argv = [], deps = {}) {
|
|
|
300
346
|
return;
|
|
301
347
|
}
|
|
302
348
|
const installArgs = ["install-service", "--workspace", workspace];
|
|
303
|
-
if (
|
|
304
|
-
installArgs.push("--ensure-checkout",
|
|
349
|
+
if (installEnv.CONDUIT_REPO)
|
|
350
|
+
installArgs.push("--ensure-checkout", installEnv.CONDUIT_REPO);
|
|
305
351
|
console.log(`Installing runner for ${workspace} (drivers: ${drivers.join(", ")})`);
|
|
306
352
|
runBridge(installArgs);
|
|
307
353
|
console.log("Done. Check with: npx @miraland-labs/conduit-bridge@latest ops status");
|
package/ops/env.example
CHANGED
|
@@ -11,6 +11,8 @@ CONDUIT_URL=https://api.conduit.miraland.io
|
|
|
11
11
|
# CONDUIT_ORG=your-org-slug
|
|
12
12
|
CONDUIT_WORKSPACE=$HOME/path/to/your-repo
|
|
13
13
|
# CONDUIT_REPO=https://github.com/your-org/your-repo.git
|
|
14
|
-
CONDUIT_DRIVERS
|
|
14
|
+
# Leave CONDUIT_DRIVERS unset to auto-detect the coding agents installed on this computer.
|
|
15
|
+
# Pin explicitly only when you want a subset, e.g.:
|
|
16
|
+
# CONDUIT_DRIVERS=claude-code codex
|
|
15
17
|
# Product-standard roles — leave all three so Start/retry plans can match without reconnect.
|
|
16
18
|
CONDUIT_ROLES=implement research review
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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": {
|