@miraland-labs/conduit-bridge 0.7.0 → 0.8.0
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 +25 -13
- package/dist/attempt-worktree.js +13 -1
- package/dist/cli.js +113 -25
- package/dist/config.js +11 -0
- package/dist/driver.js +1 -1
- package/dist/drivers.js +172 -0
- package/dist/execution.js +54 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
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.8.0` — **multi-driver lanes** on one Connect computer (`drivers online|offline`), shared concurrent slots **1–4** (sum across online IDEs, not per IDE), per-attempt worktrees, optional `--ensure-checkout`, safe resume.
|
|
6
|
+
|
|
5
7
|
## Prerequisites
|
|
6
8
|
|
|
7
9
|
- Node.js 20+
|
|
8
|
-
- One supported agent
|
|
10
|
+
- One or more supported agent CLIs on PATH (common: install several on one Mac):
|
|
9
11
|
- **Claude Code** — `claude` (Conduit pump or Anthropic login)
|
|
10
12
|
- **Codex** — `codex` (Conduit pump or ChatGPT/OpenAI login). Since the July 2026 ChatGPT desktop merge, Bridge also finds the CLI bundled inside ChatGPT.app when `codex` is not on PATH (macOS)
|
|
11
13
|
- **OpenCode** — `opencode` (Conduit pump or local provider login)
|
|
12
|
-
- **Cursor Agent** — `agent` (
|
|
13
|
-
- **Kiro CLI** — `kiro-cli` (
|
|
14
|
-
- **Antigravity** — `agy` (
|
|
14
|
+
- **Cursor Agent** — `agent` (local fuel; set with `drivers fuel cursor local` or machine `fuel local`)
|
|
15
|
+
- **Kiro CLI** — `kiro-cli` (local fuel)
|
|
16
|
+
- **Antigravity** — `agy` (local fuel)
|
|
15
17
|
- macOS or Linux for `install-service` (Windows: keep a terminal runner open)
|
|
16
18
|
|
|
17
19
|
## Install / run
|
|
@@ -20,26 +22,36 @@ Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connect
|
|
|
20
22
|
npx @miraland-labs/conduit-bridge join --url <https://your-conduit> --organization <slug>
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
After approval:
|
|
25
|
+
After approval, seed detects installed CLIs as **offline lanes**. Bring one or more online (subscription failover / concurrent lanes):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @miraland-labs/conduit-bridge drivers # list lanes
|
|
29
|
+
npx @miraland-labs/conduit-bridge drivers online claude-code
|
|
30
|
+
npx @miraland-labs/conduit-bridge drivers online claude-code codex # both can run at once
|
|
31
|
+
npx @miraland-labs/conduit-bridge drivers offline claude-code # finish in-flight; no new claims
|
|
32
|
+
npx @miraland-labs/conduit-bridge runner --workspace /path/to/repo
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Legacy single-driver process override (does not change saved online set):
|
|
24
36
|
|
|
25
37
|
```bash
|
|
26
38
|
npx @miraland-labs/conduit-bridge runner --agent claude-code --workspace /path/to/repo
|
|
27
|
-
npx @miraland-labs/conduit-bridge runner --agent codex --workspace /path/to/repo
|
|
28
|
-
npx @miraland-labs/conduit-bridge runner --agent opencode --workspace /path/to/repo
|
|
29
|
-
npx @miraland-labs/conduit-bridge fuel local # required before cursor / kiro / antigravity
|
|
30
|
-
npx @miraland-labs/conduit-bridge runner --agent cursor --workspace /path/to/repo
|
|
31
|
-
npx @miraland-labs/conduit-bridge runner --agent kiro --workspace /path/to/repo
|
|
32
|
-
npx @miraland-labs/conduit-bridge runner --agent antigravity --workspace /path/to/repo
|
|
33
39
|
```
|
|
34
40
|
|
|
35
|
-
Persist (macOS/Linux):
|
|
41
|
+
Persist (macOS/Linux) — one LaunchAgent; honors the drivers online set:
|
|
36
42
|
|
|
37
43
|
```bash
|
|
38
|
-
npx @miraland-labs/conduit-bridge install-service --
|
|
44
|
+
npx @miraland-labs/conduit-bridge install-service --workspace /path/to/repo
|
|
45
|
+
# optional: bring a lane online at install time
|
|
46
|
+
npx @miraland-labs/conduit-bridge install-service --agent claude-code --workspace /path/to/repo
|
|
39
47
|
```
|
|
40
48
|
|
|
49
|
+
Reinstall after upgrading from Bridge versions before 0.8 if the old unit baked a single `--agent`.
|
|
50
|
+
|
|
41
51
|
For a Start-created GitHub repo that is not checked out yet, point `--workspace` at the intended path and add `--ensure-checkout <https://github.com/…>` so Bridge clones with the machine’s Git credentials before the first heartbeat (never the control-plane PAT).
|
|
42
52
|
|
|
53
|
+
**Capacity:** Connect-approved `lease_capacity` (1–4) is a **shared pool** for all online lanes on that computer. Claude alone may use all 4 slots; Claude + Codex share those 4 — never 4×N.
|
|
54
|
+
|
|
43
55
|
## Grant enforcement
|
|
44
56
|
|
|
45
57
|
Every driver enforces the assignment's granted actions with its CLI's native mechanism — never just the prompt:
|
package/dist/attempt-worktree.js
CHANGED
|
@@ -11,6 +11,14 @@ const execFileAsync = promisify(execFile);
|
|
|
11
11
|
export function attemptWorktreePath(sourceWorkspace, attemptId) {
|
|
12
12
|
return join(sourceWorkspace, ".conduit", "attempts", attemptId);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The branch each attempt works on. Named by attempt id, which is globally unique, so concurrent slots
|
|
16
|
+
* (F-07 multi-slot) never collide in the shared ref namespace and a retry never clashes with a prior
|
|
17
|
+
* attempt's branch. The agent commits on this branch — "the current branch" in the prompt is now true.
|
|
18
|
+
*/
|
|
19
|
+
export function attemptBranchName(attemptId) {
|
|
20
|
+
return `conduit/attempt/${attemptId}`;
|
|
21
|
+
}
|
|
14
22
|
export async function ensureConduitExclude(sourceWorkspace) {
|
|
15
23
|
const excludePath = join(sourceWorkspace, ".git", "info", "exclude");
|
|
16
24
|
await mkdir(join(sourceWorkspace, ".git", "info"), { recursive: true });
|
|
@@ -40,7 +48,11 @@ export async function createAttemptWorktree(input) {
|
|
|
40
48
|
await assertSourceWorkspaceClean(input.sourceWorkspace);
|
|
41
49
|
const path = attemptWorktreePath(input.sourceWorkspace, input.attemptId);
|
|
42
50
|
await mkdir(join(input.sourceWorkspace, ".conduit", "attempts"), { recursive: true });
|
|
43
|
-
await execFileAsync("git",
|
|
51
|
+
await execFileAsync("git",
|
|
52
|
+
// A named attempt branch, not --detach: it collision-proofs concurrent slots and makes the agent's
|
|
53
|
+
// "commit on the current branch" accurate. -b fails if the branch exists, which is the desired
|
|
54
|
+
// guard — createAttemptWorktree only ever runs for a fresh attempt id (resume reuses its worktree).
|
|
55
|
+
["-C", input.sourceWorkspace, "worktree", "add", "-b", attemptBranchName(input.attemptId), path, input.startCommit], { timeout: 60_000, maxBuffer: 2_000_000 });
|
|
44
56
|
return path;
|
|
45
57
|
}
|
|
46
58
|
export async function removeAttemptWorktree(sourceWorkspace, worktreePath) {
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, c
|
|
|
10
10
|
import { runMcp } from "./mcp.js";
|
|
11
11
|
import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from "./detect.js";
|
|
12
12
|
import { DRIVERS } from "./driver.js";
|
|
13
|
+
import { driversHeartbeatReport, driverLabel, heartbeatStatusForDrivers, isSupportedDriverId, listDriverLanes, onlineDriverIds, seedDriversFromDetection, seedDriverLanes, setDriverFuel, setDriversOnline, } from "./drivers.js";
|
|
13
14
|
import { buildWorkspaceBrief } from "./brief.js";
|
|
14
15
|
import { ensureCheckout } from "./checkout.js";
|
|
15
16
|
import { pumpExecutionSlots, renewLeases } from "./execution.js";
|
|
@@ -187,7 +188,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
187
188
|
const detected = await detectInstalledClients();
|
|
188
189
|
const resolvedFuel = fuelSource ?? suggestFuelSource(detected);
|
|
189
190
|
const fuelAutoLocal = fuelSource === undefined && resolvedFuel === "local";
|
|
190
|
-
|
|
191
|
+
let config = {
|
|
191
192
|
baseUrl,
|
|
192
193
|
organizationId: String(data.organization_id),
|
|
193
194
|
machineId: String(data.machine_id),
|
|
@@ -199,6 +200,7 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
199
200
|
fuelSource: resolvedFuel,
|
|
200
201
|
...(Object.keys(fuel).length ? { fuel } : {}),
|
|
201
202
|
};
|
|
203
|
+
config = (await seedDriversFromDetection(config)).config;
|
|
202
204
|
await saveConfig(config);
|
|
203
205
|
const client = new ConduitClient(config);
|
|
204
206
|
let heartbeatOk = true;
|
|
@@ -211,19 +213,24 @@ async function finishConnection(baseUrl, data, fuelSource) {
|
|
|
211
213
|
console.log(`Connected machine: ${config.machineId}`);
|
|
212
214
|
console.log(heartbeatOk ? "Runner credential and heartbeat: OK" : "Runner credential saved; heartbeat will retry when the runner starts");
|
|
213
215
|
console.log(`Detected local clients: ${detected.join(", ") || "none"}`);
|
|
216
|
+
const lanes = listDriverLanes(config);
|
|
217
|
+
if (lanes.length) {
|
|
218
|
+
console.log(`Driver lanes (all offline until you bring one online): ${lanes.map((lane) => lane.id).join(", ")}`);
|
|
219
|
+
}
|
|
214
220
|
console.log(`Confirmed capabilities (matching): ${config.capabilities.join(", ") || "none"}`);
|
|
215
221
|
console.log(`Allowed actions: ${config.grants.join(", ") || "none"}`);
|
|
216
222
|
console.log(`Fuel source: ${config.fuelSource === "local" ? "local subscription" : "Conduit pump"}`
|
|
217
223
|
+ (fuelAutoLocal ? " (auto: only Cursor/Kiro/Antigravity detected)" : ""));
|
|
218
224
|
const localOnly = localFuelOnlyClients(detected);
|
|
219
225
|
if (config.fuelSource === "conduit" && localOnly.length) {
|
|
220
|
-
console.log(`Note: ${localOnly.join(", ")}
|
|
226
|
+
console.log(`Note: ${localOnly.join(", ")} require local fuel per lane (set automatically for cursor/kiro/antigravity).`);
|
|
221
227
|
}
|
|
222
228
|
console.log("Heartbeats report capabilities for diagnostics only; matching uses the Connect confirmation. Detected clients never receive grants automatically.");
|
|
223
229
|
console.log(`MCP setup: {"mcpServers":{"conduit":{"command":"npx","args":["-y","@miraland-labs/conduit-bridge","mcp"]}}}`);
|
|
224
|
-
console.log(`
|
|
225
|
-
console.log(`
|
|
226
|
-
console.log(`
|
|
230
|
+
console.log(`Bring a lane online: ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
|
|
231
|
+
console.log(`Execute work: ${bridgeUsage("runner", "--workspace", "<repo>")} (or ${bridgeUsage("runner", "--agent", AGENT_PLACEHOLDER, "--workspace", "<repo>")})`);
|
|
232
|
+
console.log(`Keep on shift (macOS/Linux): ${bridgeUsage("install-service", "--workspace", "<repo>")}`);
|
|
233
|
+
console.log(`Flip machine fuel later: ${bridgeUsage("fuel", "local|conduit")} (per-lane override: ${bridgeUsage("drivers", "fuel", "<id>", "local|conduit")})`);
|
|
227
234
|
console.log("Windows: keep the runner terminal open — install-service is macOS/Linux only.");
|
|
228
235
|
if (Object.keys(fuel).length) {
|
|
229
236
|
console.log(`Fleet fueling: ${Object.keys(fuel).length} project key(s) configured for Conduit /v1`);
|
|
@@ -246,16 +253,26 @@ function openUrl(url) {
|
|
|
246
253
|
}
|
|
247
254
|
}
|
|
248
255
|
async function installService() {
|
|
249
|
-
await loadConfig();
|
|
256
|
+
let config = await loadConfig();
|
|
250
257
|
const { values } = parseArgs({ args: process.argv.slice(3), options: {
|
|
251
258
|
agent: { type: "string" }, workspace: { type: "string" }, interval: { type: "string" }, "agent-timeout-minutes": { type: "string" },
|
|
252
259
|
"ensure-checkout": { type: "string" },
|
|
253
260
|
} });
|
|
254
|
-
if (!values.
|
|
255
|
-
throw new Error(`Usage: ${bridgeUsage("install-service", "--
|
|
261
|
+
if (!values.workspace) {
|
|
262
|
+
throw new Error(`Usage: ${bridgeUsage("install-service", "--workspace", "<repository-path>", `[--agent ${AGENT_PLACEHOLDER}]`, "[--ensure-checkout <repository-url>]")} (workspace required; uses online driver lanes)`);
|
|
263
|
+
}
|
|
264
|
+
config = (await seedDriversFromDetection(config)).config;
|
|
265
|
+
if (values.agent) {
|
|
266
|
+
if (!DRIVERS[values.agent]) {
|
|
267
|
+
throw new Error(`Unknown agent driver: ${values.agent}. Available: ${Object.keys(DRIVERS).join(", ")}`);
|
|
268
|
+
}
|
|
269
|
+
config = seedDriverLanes(config, [values.agent]).config;
|
|
270
|
+
config = setDriversOnline(config, [values.agent], true);
|
|
271
|
+
await saveConfig(config);
|
|
272
|
+
console.log(`Brought ${values.agent} online for this computer (shared capacity ${config.leaseCapacity}).`);
|
|
256
273
|
}
|
|
257
|
-
if (!
|
|
258
|
-
throw new Error(`
|
|
274
|
+
else if (!onlineDriverIds(config).length) {
|
|
275
|
+
throw new Error(`No online driver lanes. Run \`${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}\` first, or pass --agent to bring one online.`);
|
|
259
276
|
}
|
|
260
277
|
const workspace = resolve(values.workspace);
|
|
261
278
|
if (values["ensure-checkout"]) {
|
|
@@ -264,14 +281,15 @@ async function installService() {
|
|
|
264
281
|
? `Cloned ${values["ensure-checkout"]} into ${workspace}`
|
|
265
282
|
: `Workspace already matches ${values["ensure-checkout"]}`);
|
|
266
283
|
}
|
|
284
|
+
// Supervisor service: no baked --agent; honors config drivers online set.
|
|
267
285
|
const result = await installRunnerService({
|
|
268
|
-
agent: values.agent,
|
|
269
286
|
workspace,
|
|
270
287
|
interval: values.interval,
|
|
271
288
|
agentTimeoutMinutes: values["agent-timeout-minutes"],
|
|
272
289
|
ensureCheckout: values["ensure-checkout"],
|
|
273
290
|
});
|
|
274
291
|
console.log(`Installed Conduit runner service (${result.platform}): ${result.path}`);
|
|
292
|
+
console.log(`Online lanes: ${onlineDriverIds(config).join(", ")}. Toggle with \`${bridgeUsage("drivers", "online|offline", "…")}\`.`);
|
|
275
293
|
console.log("The runner keeps executing after this terminal closes. Laptop sleep still pauses work.");
|
|
276
294
|
}
|
|
277
295
|
async function uninstallService() {
|
|
@@ -288,27 +306,83 @@ async function fuelCommand() {
|
|
|
288
306
|
await saveConfig(config);
|
|
289
307
|
const client = new ConduitClient(config);
|
|
290
308
|
await heartbeat(client, config, null);
|
|
291
|
-
console.log(`
|
|
309
|
+
console.log(`Machine fuel source set to ${mode === "local" ? "local subscription" : "Conduit pump"} and reported on heartbeat.`);
|
|
310
|
+
}
|
|
311
|
+
async function driversCommand() {
|
|
312
|
+
const sub = process.argv[3];
|
|
313
|
+
let config = await loadConfig();
|
|
314
|
+
config = (await seedDriversFromDetection(config)).config;
|
|
315
|
+
if (!sub || sub === "list") {
|
|
316
|
+
await saveConfig(config);
|
|
317
|
+
const lanes = listDriverLanes(config);
|
|
318
|
+
if (!lanes.length) {
|
|
319
|
+
console.log("No driver lanes registered. Install a supported agent CLI, then re-run this command.");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
console.log(`Computer ${config.machineId} — shared capacity ${config.leaseCapacity} (sum across online lanes, not per IDE)`);
|
|
323
|
+
for (const lane of lanes) {
|
|
324
|
+
console.log(` ${lane.id.padEnd(14)} ${lane.state.padEnd(8)} fuel=${lane.fuel} (${lane.label})`);
|
|
325
|
+
}
|
|
326
|
+
const online = onlineDriverIds(config);
|
|
327
|
+
console.log(online.length
|
|
328
|
+
? `Online: ${online.join(", ")}. Toggle: ${bridgeUsage("drivers", "online|offline", "<id…>")}`
|
|
329
|
+
: `All offline — no new claims. Bring one online: ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (sub === "online" || sub === "offline") {
|
|
333
|
+
const ids = process.argv.slice(4).map((id) => id.trim()).filter(Boolean);
|
|
334
|
+
if (!ids.length)
|
|
335
|
+
throw new Error(`Usage: ${bridgeUsage("drivers", sub, AGENT_PLACEHOLDER, "[…]")}`);
|
|
336
|
+
for (const id of ids) {
|
|
337
|
+
if (!isSupportedDriverId(id))
|
|
338
|
+
throw new Error(`Unknown driver: ${id}. Available: ${Object.keys(DRIVERS).join(", ")}`);
|
|
339
|
+
}
|
|
340
|
+
config = seedDriverLanes(config, ids).config;
|
|
341
|
+
config = setDriversOnline(config, ids, sub === "online");
|
|
342
|
+
await saveConfig(config);
|
|
343
|
+
const client = new ConduitClient(config);
|
|
344
|
+
await heartbeat(client, config, null).catch(() => undefined);
|
|
345
|
+
console.log(`${sub === "online" ? "Online" : "Offline"}: ${ids.join(", ")}`);
|
|
346
|
+
console.log(`Now online: ${onlineDriverIds(config).join(", ") || "(none)"}`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (sub === "fuel") {
|
|
350
|
+
const id = process.argv[4]?.trim();
|
|
351
|
+
const mode = process.argv[5];
|
|
352
|
+
if (!id || (mode !== "local" && mode !== "conduit")) {
|
|
353
|
+
throw new Error(`Usage: ${bridgeUsage("drivers", "fuel", "<driver-id>", "local|conduit")}`);
|
|
354
|
+
}
|
|
355
|
+
config = seedDriverLanes(config, [id]).config;
|
|
356
|
+
config = setDriverFuel(config, id, mode);
|
|
357
|
+
await saveConfig(config);
|
|
358
|
+
console.log(`${driverLabel(id)} lane fuel set to ${mode}`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
throw new Error(`Usage: ${bridgeUsage("drivers", "[list|online|offline|fuel]", "…")}`);
|
|
292
362
|
}
|
|
293
363
|
async function runner() {
|
|
294
364
|
const { values } = parseArgs({ args: process.argv.slice(3), options: {
|
|
295
365
|
agent: { type: "string" }, workspace: { type: "string" }, interval: { type: "string" }, once: { type: "boolean" },
|
|
296
366
|
"agent-timeout-minutes": { type: "string" }, fuel: { type: "string" }, "ensure-checkout": { type: "string" },
|
|
297
367
|
} });
|
|
298
|
-
|
|
368
|
+
let config = await loadConfig();
|
|
299
369
|
const fuelOverride = parseFuelSource(values.fuel);
|
|
300
370
|
if (fuelOverride) {
|
|
301
371
|
config.fuelSource = fuelOverride;
|
|
302
372
|
await saveConfig(config);
|
|
303
373
|
}
|
|
374
|
+
config = (await seedDriversFromDetection(config)).config;
|
|
375
|
+
await saveConfig(config);
|
|
304
376
|
const client = new ConduitClient(config);
|
|
305
|
-
let
|
|
377
|
+
let processDriver = null;
|
|
378
|
+
let processOnlineIds = null;
|
|
306
379
|
if (values.agent) {
|
|
307
|
-
|
|
308
|
-
if (!
|
|
380
|
+
processDriver = DRIVERS[values.agent] ?? null;
|
|
381
|
+
if (!processDriver)
|
|
309
382
|
throw new Error(`Unknown agent driver: ${values.agent}. Available: ${Object.keys(DRIVERS).join(", ")}`);
|
|
310
383
|
if (!values.workspace)
|
|
311
384
|
throw new Error("--workspace <repository-path> is required with --agent");
|
|
385
|
+
processOnlineIds = [values.agent];
|
|
312
386
|
}
|
|
313
387
|
if (values["ensure-checkout"] && !values.workspace) {
|
|
314
388
|
throw new Error("--ensure-checkout requires --workspace <repository-path>");
|
|
@@ -323,22 +397,31 @@ async function runner() {
|
|
|
323
397
|
const brief = workspace ? await buildWorkspaceBrief(workspace) : null;
|
|
324
398
|
const intervalMs = values.interval ? Math.max(5_000, Number(values.interval)) : 15_000;
|
|
325
399
|
const timeoutMs = values["agent-timeout-minutes"] ? Number(values["agent-timeout-minutes"]) * 60_000 : undefined;
|
|
326
|
-
const
|
|
327
|
-
if (!
|
|
328
|
-
console.warn(`WARNING: heartbeat only —
|
|
400
|
+
const canExecute = Boolean(workspace) && (Boolean(processDriver) || onlineDriverIds(config).length > 0);
|
|
401
|
+
if (!workspace) {
|
|
402
|
+
console.warn(`WARNING: heartbeat only — pass --workspace <repo>. Lanes: ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)}`);
|
|
403
|
+
}
|
|
404
|
+
else if (!canExecute) {
|
|
405
|
+
console.warn(`WARNING: no online driver lanes — ${bridgeUsage("drivers", "online", AGENT_PLACEHOLDER)} (or pass --agent)`);
|
|
329
406
|
}
|
|
330
|
-
|
|
407
|
+
const onlineLabel = (processOnlineIds?.join(", ") || onlineDriverIds(config).join(", ")) || "(none)";
|
|
408
|
+
console.log(`Conduit runner (bridge v${bridgeVersion()}) connected to ${config.baseUrl}${workspace ? ` — workspace ${workspace}` : " — heartbeat only"} (lanes: ${onlineLabel}; machine fuel: ${config.fuelSource === "local" ? "local" : "conduit"}; slots: ${config.leaseCapacity})`);
|
|
331
409
|
const running = new Map();
|
|
332
410
|
for (;;) {
|
|
333
411
|
let progressed = false;
|
|
334
412
|
try {
|
|
413
|
+
// Reload drivers online/offline each cycle so CLI toggles apply without restart.
|
|
414
|
+
const latest = await loadConfig();
|
|
415
|
+
config.drivers = latest.drivers;
|
|
416
|
+
config.fuelSource = latest.fuelSource;
|
|
417
|
+
config.leaseCapacity = latest.leaseCapacity;
|
|
335
418
|
await heartbeat(client, config, workspace ? await buildWorkspaceBrief(workspace).catch(() => brief) : null);
|
|
336
419
|
await renewLeases(client, config);
|
|
337
|
-
if (
|
|
338
|
-
progressed = await pumpExecutionSlots(client, config,
|
|
420
|
+
if (workspace && (processDriver || onlineDriverIds(config).length)) {
|
|
421
|
+
progressed = await pumpExecutionSlots(client, config, workspace, brief, timeoutMs, {
|
|
339
422
|
heartbeat: async () => heartbeat(client, config, await buildWorkspaceBrief(workspace).catch(() => brief)),
|
|
340
423
|
heartbeatIntervalMs: intervalMs,
|
|
341
|
-
}, running);
|
|
424
|
+
}, running, { driver: processDriver, processOnlineIds });
|
|
342
425
|
}
|
|
343
426
|
}
|
|
344
427
|
catch (error) {
|
|
@@ -353,11 +436,14 @@ async function runner() {
|
|
|
353
436
|
}
|
|
354
437
|
}
|
|
355
438
|
async function heartbeat(client, config, brief) {
|
|
439
|
+
const online = onlineDriverIds(config);
|
|
440
|
+
const status = heartbeatStatusForDrivers(online);
|
|
356
441
|
await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
|
|
357
|
-
status
|
|
442
|
+
status,
|
|
358
443
|
capabilities: config.capabilities,
|
|
359
444
|
lease_capacity: config.leaseCapacity,
|
|
360
445
|
fuel_source: config.fuelSource === "local" ? "local" : "conduit",
|
|
446
|
+
drivers: driversHeartbeatReport(config, config.activeAttempts),
|
|
361
447
|
...(brief ? { workspace_brief: brief } : {}),
|
|
362
448
|
}) });
|
|
363
449
|
}
|
|
@@ -368,6 +454,8 @@ try {
|
|
|
368
454
|
await join();
|
|
369
455
|
else if (command === "fuel")
|
|
370
456
|
await fuelCommand();
|
|
457
|
+
else if (command === "drivers")
|
|
458
|
+
await driversCommand();
|
|
371
459
|
else if (command === "mcp")
|
|
372
460
|
await runMcp();
|
|
373
461
|
else if (command === "runner")
|
|
@@ -377,7 +465,7 @@ try {
|
|
|
377
465
|
else if (command === "uninstall-service")
|
|
378
466
|
await uninstallService();
|
|
379
467
|
else
|
|
380
|
-
throw new Error(`Usage: ${BRIDGE_NPX} <join|connect|fuel|mcp|runner|install-service|uninstall-service>`);
|
|
468
|
+
throw new Error(`Usage: ${BRIDGE_NPX} <join|connect|fuel|drivers|mcp|runner|install-service|uninstall-service>`);
|
|
381
469
|
}
|
|
382
470
|
catch (error) {
|
|
383
471
|
console.error(error instanceof Error ? redactSecrets(error.message) : "Conduit command failed");
|
package/dist/config.js
CHANGED
|
@@ -50,6 +50,17 @@ export async function loadConfigIfPresent() {
|
|
|
50
50
|
config.fuelSource = "conduit";
|
|
51
51
|
// Clamp to what concurrent slot workers can run (1..BRIDGE_MAX_LEASE_CAPACITY).
|
|
52
52
|
config.leaseCapacity = clampLeaseCapacity(config.leaseCapacity ?? BRIDGE_LEASE_CAPACITY);
|
|
53
|
+
if (config.drivers && typeof config.drivers === "object") {
|
|
54
|
+
const cleaned = {};
|
|
55
|
+
for (const [id, lane] of Object.entries(config.drivers)) {
|
|
56
|
+
if (!lane || typeof lane !== "object")
|
|
57
|
+
continue;
|
|
58
|
+
const state = lane.state === "online" ? "online" : "offline";
|
|
59
|
+
const fuel = lane.fuel === "local" || lane.fuel === "conduit" ? lane.fuel : undefined;
|
|
60
|
+
cleaned[id] = fuel ? { state, fuel } : { state };
|
|
61
|
+
}
|
|
62
|
+
config.drivers = cleaned;
|
|
63
|
+
}
|
|
53
64
|
return config;
|
|
54
65
|
}
|
|
55
66
|
export async function saveConfig(config) {
|
package/dist/driver.js
CHANGED
|
@@ -130,7 +130,7 @@ export function buildAssignmentPrompt(context) {
|
|
|
130
130
|
`- These are the ONLY shell commands you may run: ${runnableCommands.join("; ")}. Anything else is rejected — do not try variations, wrappers, or \`echo\`. Run the relevant ones and report their real output.`,
|
|
131
131
|
]
|
|
132
132
|
: ["- You have no shell authority for this assignment. Do not attempt shell commands; verify by reading files and report what you could not verify as unknown."]), ...(mustCommit
|
|
133
|
-
? ["- Commit your repository changes
|
|
133
|
+
? ["- Your working branch is already checked out. Commit your repository changes on the current branch (git add + git commit) — do not create a new branch — and report the resulting sha as head_commit. A delivery that changed files without a commit is rejected. Local commits are required; pushing is a separate authority."]
|
|
134
134
|
: ["- You have no repository write authority for this assignment. Do not create, modify, or commit any file; a delivery reporting repository changes will be rejected. Record all findings, verification output, and conclusions in your final report instead."]), "- Never merge, deploy, push to protected branches, or touch production.", `- Hard-denied commands (all drivers): ${deniedCommands.join("; ")}.`, "- Do not invent evidence. Report unknown when you could not verify a criterion.",
|
|
135
135
|
// A met claim with nothing backing it is rejected server-side ("Met acceptance criteria require
|
|
136
136
|
// mapped evidence"). Observed live: six criteria marked met, evidence mapped to four, whole
|
package/dist/drivers.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local driver lanes on one Bridge computer: operator online/offline set + shared slot pool.
|
|
3
|
+
* Matching stays computer-scoped; Bridge assigns each new claim to a free online driver.
|
|
4
|
+
*/
|
|
5
|
+
import { detectInstalledClients } from "./detect.js";
|
|
6
|
+
import { DRIVERS, SUPPORTED_AGENTS } from "./driver.js";
|
|
7
|
+
/** Display labels from detectInstalledClients → Bridge driver ids. */
|
|
8
|
+
const LABEL_TO_DRIVER = {
|
|
9
|
+
"Claude Code": "claude-code",
|
|
10
|
+
"Codex CLI": "codex",
|
|
11
|
+
OpenCode: "opencode",
|
|
12
|
+
"Cursor Agent": "cursor",
|
|
13
|
+
Cursor: "cursor",
|
|
14
|
+
"Kiro CLI": "kiro",
|
|
15
|
+
Antigravity: "antigravity",
|
|
16
|
+
};
|
|
17
|
+
const LOCAL_FUEL_ONLY_DRIVERS = new Set(["cursor", "kiro", "antigravity"]);
|
|
18
|
+
export function isSupportedDriverId(id) {
|
|
19
|
+
return Object.prototype.hasOwnProperty.call(DRIVERS, id);
|
|
20
|
+
}
|
|
21
|
+
export function driverLabel(id) {
|
|
22
|
+
return SUPPORTED_AGENTS.find((agent) => agent.id === id)?.label ?? id;
|
|
23
|
+
}
|
|
24
|
+
export function localFuelOnlyDriver(id) {
|
|
25
|
+
return LOCAL_FUEL_ONLY_DRIVERS.has(id);
|
|
26
|
+
}
|
|
27
|
+
/** Map PATH probe labels to unique driver ids. */
|
|
28
|
+
export function driverIdsFromDetectedLabels(labels) {
|
|
29
|
+
const ids = new Set();
|
|
30
|
+
for (const label of labels) {
|
|
31
|
+
const id = LABEL_TO_DRIVER[label];
|
|
32
|
+
if (id)
|
|
33
|
+
ids.add(id);
|
|
34
|
+
}
|
|
35
|
+
return [...ids];
|
|
36
|
+
}
|
|
37
|
+
export function normalizeDrivers(raw) {
|
|
38
|
+
const out = {};
|
|
39
|
+
if (!raw || typeof raw !== "object")
|
|
40
|
+
return out;
|
|
41
|
+
for (const [id, lane] of Object.entries(raw)) {
|
|
42
|
+
if (!isSupportedDriverId(id) || !lane || typeof lane !== "object")
|
|
43
|
+
continue;
|
|
44
|
+
const state = lane.state === "online" ? "online" : "offline";
|
|
45
|
+
const fuel = lane.fuel === "local" || lane.fuel === "conduit" ? lane.fuel : undefined;
|
|
46
|
+
out[id] = fuel ? { state, fuel } : { state };
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Ensure detected (or listed) drivers exist in config. New lanes default offline (fail-closed).
|
|
52
|
+
* Does not flip existing online/offline state.
|
|
53
|
+
*/
|
|
54
|
+
export function seedDriverLanes(config, driverIds) {
|
|
55
|
+
const drivers = normalizeDrivers(config.drivers);
|
|
56
|
+
const added = [];
|
|
57
|
+
for (const id of driverIds) {
|
|
58
|
+
if (!isSupportedDriverId(id))
|
|
59
|
+
continue;
|
|
60
|
+
if (drivers[id])
|
|
61
|
+
continue;
|
|
62
|
+
drivers[id] = localFuelOnlyDriver(id) ? { state: "offline", fuel: "local" } : { state: "offline" };
|
|
63
|
+
added.push(id);
|
|
64
|
+
}
|
|
65
|
+
return { config: { ...config, drivers }, added };
|
|
66
|
+
}
|
|
67
|
+
export async function seedDriversFromDetection(config) {
|
|
68
|
+
const labels = await detectInstalledClients();
|
|
69
|
+
return seedDriverLanes(config, driverIdsFromDetectedLabels(labels));
|
|
70
|
+
}
|
|
71
|
+
export function listDriverLanes(config) {
|
|
72
|
+
const drivers = normalizeDrivers(config.drivers);
|
|
73
|
+
return SUPPORTED_AGENTS
|
|
74
|
+
.filter((agent) => drivers[agent.id])
|
|
75
|
+
.map((agent) => {
|
|
76
|
+
const lane = drivers[agent.id];
|
|
77
|
+
return {
|
|
78
|
+
id: agent.id,
|
|
79
|
+
label: agent.label,
|
|
80
|
+
state: lane.state,
|
|
81
|
+
fuel: resolveDriverFuel(config, agent.id),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function onlineDriverIds(config) {
|
|
86
|
+
const drivers = normalizeDrivers(config.drivers);
|
|
87
|
+
return SUPPORTED_AGENTS.map((agent) => agent.id).filter((id) => drivers[id]?.state === "online");
|
|
88
|
+
}
|
|
89
|
+
export function setDriversOnline(config, ids, online) {
|
|
90
|
+
const drivers = normalizeDrivers(config.drivers);
|
|
91
|
+
for (const id of ids) {
|
|
92
|
+
if (!isSupportedDriverId(id))
|
|
93
|
+
throw new Error(`Unknown driver: ${id}. Available: ${Object.keys(DRIVERS).join(", ")}`);
|
|
94
|
+
const prev = drivers[id] ?? (localFuelOnlyDriver(id) ? { state: "offline", fuel: "local" } : { state: "offline" });
|
|
95
|
+
drivers[id] = { ...prev, state: online ? "online" : "offline" };
|
|
96
|
+
}
|
|
97
|
+
return { ...config, drivers };
|
|
98
|
+
}
|
|
99
|
+
/** Fuel for a lane: per-driver override → machine default; local-only drivers always local. */
|
|
100
|
+
export function resolveDriverFuel(config, driverId) {
|
|
101
|
+
if (localFuelOnlyDriver(driverId))
|
|
102
|
+
return "local";
|
|
103
|
+
const lane = normalizeDrivers(config.drivers)[driverId];
|
|
104
|
+
if (lane?.fuel === "local" || lane?.fuel === "conduit")
|
|
105
|
+
return lane.fuel;
|
|
106
|
+
return config.fuelSource === "local" ? "local" : "conduit";
|
|
107
|
+
}
|
|
108
|
+
export function setDriverFuel(config, driverId, fuel) {
|
|
109
|
+
if (!isSupportedDriverId(driverId))
|
|
110
|
+
throw new Error(`Unknown driver: ${driverId}`);
|
|
111
|
+
if (localFuelOnlyDriver(driverId) && fuel === "conduit") {
|
|
112
|
+
throw new Error(`${driverLabel(driverId)} requires local fuel`);
|
|
113
|
+
}
|
|
114
|
+
const drivers = normalizeDrivers(config.drivers);
|
|
115
|
+
const prev = drivers[driverId] ?? { state: "offline" };
|
|
116
|
+
drivers[driverId] = { ...prev, fuel };
|
|
117
|
+
return { ...config, drivers };
|
|
118
|
+
}
|
|
119
|
+
/** Heartbeat status: standby when no online lanes (supervisor mode). */
|
|
120
|
+
export function heartbeatStatusForDrivers(onlineIds) {
|
|
121
|
+
return onlineIds.length > 0 ? "online" : "standby";
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Effective capacity advertised on heartbeat: full approved pool when any lane is online;
|
|
125
|
+
* 0 (via standby + no claims) when none are. Caller still sends lease_capacity = approved when online.
|
|
126
|
+
*/
|
|
127
|
+
export function advertiseLeaseCapacity(approved, onlineIds) {
|
|
128
|
+
if (onlineIds.length === 0)
|
|
129
|
+
return 1; // schema/heartbeat min; status standby prevents matching
|
|
130
|
+
return approved;
|
|
131
|
+
}
|
|
132
|
+
export function driversHeartbeatReport(config, activeAttempts) {
|
|
133
|
+
const busyByDriver = new Map();
|
|
134
|
+
for (const active of Object.values(activeAttempts)) {
|
|
135
|
+
if (!active.driverId)
|
|
136
|
+
continue;
|
|
137
|
+
busyByDriver.set(active.driverId, (busyByDriver.get(active.driverId) ?? 0) + 1);
|
|
138
|
+
}
|
|
139
|
+
return listDriverLanes(config).map((lane) => ({
|
|
140
|
+
id: lane.id,
|
|
141
|
+
state: lane.state,
|
|
142
|
+
busy: (busyByDriver.get(lane.id) ?? 0) > 0,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Pick an online driver for a new claim: least loaded among online (shared machine pool).
|
|
147
|
+
* `processOnlineIds` overrides config when runner was started with --agent.
|
|
148
|
+
*/
|
|
149
|
+
export function pickDriverForClaim(config, processOnlineIds) {
|
|
150
|
+
const online = (processOnlineIds?.length ? processOnlineIds : onlineDriverIds(config))
|
|
151
|
+
.filter((id) => isSupportedDriverId(id));
|
|
152
|
+
if (!online.length)
|
|
153
|
+
return null;
|
|
154
|
+
const load = new Map();
|
|
155
|
+
for (const id of online)
|
|
156
|
+
load.set(id, 0);
|
|
157
|
+
for (const active of Object.values(config.activeAttempts)) {
|
|
158
|
+
if (active.driverId && load.has(active.driverId)) {
|
|
159
|
+
load.set(active.driverId, (load.get(active.driverId) ?? 0) + 1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let best = online[0];
|
|
163
|
+
let bestLoad = load.get(best) ?? 0;
|
|
164
|
+
for (const id of online.slice(1)) {
|
|
165
|
+
const n = load.get(id) ?? 0;
|
|
166
|
+
if (n < bestLoad) {
|
|
167
|
+
best = id;
|
|
168
|
+
bestLoad = n;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return best;
|
|
172
|
+
}
|
package/dist/execution.js
CHANGED
|
@@ -2,7 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { ConduitRequestError } from "./client.js";
|
|
4
4
|
import { redactSecrets } from "./config.js";
|
|
5
|
-
import { agentReportTemplate, buildAssignmentPrompt, evidenceKinds, parseAgentReport, pickModelCandidate, tierForRisk } from "./driver.js";
|
|
5
|
+
import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, parseAgentReport, pickModelCandidate, tierForRisk } from "./driver.js";
|
|
6
|
+
import { pickDriverForClaim, resolveDriverFuel } from "./drivers.js";
|
|
6
7
|
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
7
8
|
import { buildWorkspaceBrief, isBaseCommitAncestor, normalizeRepositoryUrl } from "./brief.js";
|
|
8
9
|
const assignmentSchema = z.object({
|
|
@@ -66,17 +67,27 @@ export async function renewLeases(client, config) {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
function resolveAttemptDriver(config, active, fallback) {
|
|
71
|
+
if (active.driverId && DRIVERS[active.driverId])
|
|
72
|
+
return DRIVERS[active.driverId];
|
|
73
|
+
return fallback ?? null;
|
|
74
|
+
}
|
|
69
75
|
export async function recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision, taskId) {
|
|
70
76
|
const active = taskId ? config.activeAttempts[taskId] : Object.values(config.activeAttempts)[0];
|
|
71
77
|
if (!active)
|
|
72
78
|
return false;
|
|
79
|
+
const laneDriver = resolveAttemptDriver(config, active, driver);
|
|
80
|
+
if (!laneDriver) {
|
|
81
|
+
console.error(`No driver for attempt ${active.attemptId} (driverId=${active.driverId ?? "unset"})`);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
73
84
|
if (active.phase === "agent_running") {
|
|
74
85
|
const worktree = active.worktreePath ?? attemptWorktreePath(workspace, active.attemptId);
|
|
75
86
|
const sessionId = config.sessions?.[active.taskId]?.trim() || "";
|
|
76
87
|
// F-07 safe resume: only when session + worktree identity are both proven. Otherwise interrupt.
|
|
77
88
|
if (sessionId && await proveResumeWorktree(workspace, active.attemptId, worktree)) {
|
|
78
89
|
console.log(`Resuming interrupted attempt ${active.attemptId} with proven worktree and session.`);
|
|
79
|
-
await runClaimedAssignment(client, config,
|
|
90
|
+
await runClaimedAssignment(client, config, laneDriver, workspace, brief, active.taskId, timeoutMs, supervision, {
|
|
80
91
|
existingWorktree: worktree,
|
|
81
92
|
forceResumeSessionId: sessionId,
|
|
82
93
|
});
|
|
@@ -98,20 +109,32 @@ export async function recoverActiveAttempt(client, config, driver, workspace, br
|
|
|
98
109
|
await flushTerminal(client, active.taskId);
|
|
99
110
|
return true;
|
|
100
111
|
}
|
|
101
|
-
await runClaimedAssignment(client, config,
|
|
112
|
+
await runClaimedAssignment(client, config, laneDriver, workspace, brief, active.taskId, timeoutMs, supervision);
|
|
102
113
|
return true;
|
|
103
114
|
}
|
|
104
115
|
/**
|
|
105
|
-
* F-07 multi-slot: keep up to leaseCapacity concurrent agent runs
|
|
116
|
+
* F-07 multi-slot + multi-driver supervisor: keep up to leaseCapacity concurrent agent runs
|
|
117
|
+
* (each with its own worktree). Shared pool across online lanes — not 4 per IDE.
|
|
106
118
|
* `running` is owned by the runner loop and must outlive a single pump call.
|
|
107
119
|
*/
|
|
108
|
-
export async function pumpExecutionSlots(client, config,
|
|
120
|
+
export async function pumpExecutionSlots(client, config, workspace, brief, timeoutMs, supervision, running, options = {}) {
|
|
121
|
+
const fallbackDriver = options.driver ?? null;
|
|
122
|
+
const processOnlineIds = options.processOnlineIds
|
|
123
|
+
?? (fallbackDriver
|
|
124
|
+
? [Object.entries(DRIVERS).find(([, d]) => d === fallbackDriver)?.[0]].filter((id) => Boolean(id))
|
|
125
|
+
: null);
|
|
109
126
|
let progressed = running.size > 0;
|
|
110
127
|
for (const id of Object.keys(config.activeAttempts)) {
|
|
111
128
|
if (running.has(id))
|
|
112
129
|
continue;
|
|
130
|
+
const active = config.activeAttempts[id];
|
|
131
|
+
const laneDriver = resolveAttemptDriver(config, active, fallbackDriver);
|
|
132
|
+
if (!laneDriver) {
|
|
133
|
+
console.error(`Slot recovery skipped for ${id}: no driver lane`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
113
136
|
progressed = true;
|
|
114
|
-
const slot = recoverActiveAttempt(client, config,
|
|
137
|
+
const slot = recoverActiveAttempt(client, config, laneDriver, workspace, brief, timeoutMs, supervision, id)
|
|
115
138
|
.catch((error) => {
|
|
116
139
|
console.error(`Slot recovery failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
|
|
117
140
|
})
|
|
@@ -120,11 +143,28 @@ export async function pumpExecutionSlots(client, config, driver, workspace, brie
|
|
|
120
143
|
running.set(id, slot);
|
|
121
144
|
}
|
|
122
145
|
while (running.size < config.leaseCapacity) {
|
|
146
|
+
let laneDriver = null;
|
|
147
|
+
let driverId = null;
|
|
148
|
+
if (fallbackDriver) {
|
|
149
|
+
// Legacy `--agent` / tests: one fixed driver for every new claim.
|
|
150
|
+
laneDriver = fallbackDriver;
|
|
151
|
+
driverId = processOnlineIds?.[0]
|
|
152
|
+
?? Object.entries(DRIVERS).find(([, d]) => d === fallbackDriver)?.[0]
|
|
153
|
+
?? fallbackDriver.name;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
driverId = pickDriverForClaim(config, processOnlineIds);
|
|
157
|
+
laneDriver = driverId ? DRIVERS[driverId] ?? null : null;
|
|
158
|
+
}
|
|
159
|
+
if (!laneDriver || !driverId)
|
|
160
|
+
break;
|
|
123
161
|
const taskId = await claimNextAssignment(client, config, workspace, brief);
|
|
124
162
|
if (!taskId)
|
|
125
163
|
break;
|
|
164
|
+
await client.updateAttempt(taskId, { driverId });
|
|
126
165
|
progressed = true;
|
|
127
|
-
|
|
166
|
+
console.log(`Executing ${taskId} via ${laneDriver.name}`);
|
|
167
|
+
const slot = runClaimedAssignment(client, config, laneDriver, workspace, brief, taskId, timeoutMs, supervision)
|
|
128
168
|
.catch((error) => {
|
|
129
169
|
console.error(`Slot execution failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
|
|
130
170
|
})
|
|
@@ -241,7 +281,12 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
241
281
|
: `Starting ${driver.name} for this assignment${reworkFeedback ? " with review feedback" : ""}.`,
|
|
242
282
|
idempotency_key: resuming ? `bridge:progress:${active.attemptId}:resume` : `bridge:progress:${active.attemptId}:start`,
|
|
243
283
|
});
|
|
244
|
-
const
|
|
284
|
+
const driverId = active.driverId
|
|
285
|
+
?? Object.entries(DRIVERS).find(([, d]) => d === driver)?.[0]
|
|
286
|
+
?? "unknown";
|
|
287
|
+
if (!active.driverId && driverId !== "unknown")
|
|
288
|
+
await client.updateAttempt(taskId, { driverId });
|
|
289
|
+
const fuelSource = resolveDriverFuel(config, driverId);
|
|
245
290
|
let fuel;
|
|
246
291
|
if (fuelSource === "conduit") {
|
|
247
292
|
const gatewayKey = await client.ensureFuel(task.project_id);
|
|
@@ -261,7 +306,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
261
306
|
: `Started ${driver.name}${selection.model ? ` on ${selection.model}` : " on its CLI default"}.`,
|
|
262
307
|
idempotency_key: resuming ? `bridge:progress:${active.attemptId}:agent-resume` : `bridge:progress:${active.attemptId}:agent-start`,
|
|
263
308
|
});
|
|
264
|
-
await client.updateAttempt(taskId, { phase: "agent_running", fuelMode: fuelSource, worktreePath: attemptWorkspace });
|
|
309
|
+
await client.updateAttempt(taskId, { phase: "agent_running", fuelMode: fuelSource, worktreePath: attemptWorkspace, driverId });
|
|
265
310
|
const renewTimer = setInterval(() => { void renewLeases(client, config).catch((error) => console.error(`Lease renewal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`)); }, 60_000);
|
|
266
311
|
let heartbeatRunning = false;
|
|
267
312
|
const heartbeatTimer = supervision ? setInterval(() => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Conduit Bridge CLI — join, connect, and run Claude Code / Codex / Cursor / OpenCode / Kiro / Antigravity agents for a Conduit organization",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Conduit Bridge CLI — join, connect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"conduit": "dist/cli.js"
|