@miraland-labs/conduit-bridge 0.7.0 → 0.9.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/brief.js +35 -0
- package/dist/cli.js +121 -32
- package/dist/client.js +34 -10
- package/dist/config.js +189 -5
- package/dist/driver.js +36 -5
- package/dist/drivers.js +173 -0
- package/dist/ensure-pull-request.js +98 -0
- package/dist/execution.js +227 -48
- 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/brief.js
CHANGED
|
@@ -43,6 +43,41 @@ export async function isBaseCommitAncestor(workspace, baseCommit, headCommit) {
|
|
|
43
43
|
throw error;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
/** True when `sha` is a local commit object, optionally after fetching it from origin. */
|
|
47
|
+
export async function ensureCommitAvailable(workspace, sha) {
|
|
48
|
+
const present = async () => {
|
|
49
|
+
try {
|
|
50
|
+
await execFileAsync("git", ["-C", workspace, "cat-file", "-e", `${sha}^{commit}`], {
|
|
51
|
+
timeout: 10_000,
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
});
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
if (await present())
|
|
61
|
+
return true;
|
|
62
|
+
try {
|
|
63
|
+
await execFileAsync("git", ["-C", workspace, "fetch", "--no-tags", "--depth=1", "origin", sha], {
|
|
64
|
+
timeout: 120_000,
|
|
65
|
+
windowsHide: true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
try {
|
|
70
|
+
await execFileAsync("git", ["-C", workspace, "fetch", "--no-tags", "origin"], {
|
|
71
|
+
timeout: 120_000,
|
|
72
|
+
windowsHide: true,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return present();
|
|
80
|
+
}
|
|
46
81
|
async function gitRemoteUrl(workspace) {
|
|
47
82
|
try {
|
|
48
83
|
const { common } = await gitDirectories(workspace);
|
package/dist/cli.js
CHANGED
|
@@ -6,10 +6,11 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { ConduitClient } from "./client.js";
|
|
9
|
-
import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, savePendingConnection, suggestMachineName, } from "./config.js";
|
|
9
|
+
import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, saveConfigPrefs, savePendingConnection, suggestMachineName, } from "./config.js";
|
|
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
|
-
console.log(`Fuel source: ${config.fuelSource === "local" ? "local subscription" : "Conduit
|
|
222
|
+
console.log(`Fuel source: ${config.fuelSource === "local" ? "local subscription" : "Conduit /v1 (project owner BYOK on open orgs; org credentials on invite-only)"}`
|
|
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 saveConfigPrefs(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() {
|
|
@@ -285,30 +303,86 @@ async function fuelCommand() {
|
|
|
285
303
|
}
|
|
286
304
|
const config = await loadConfig();
|
|
287
305
|
config.fuelSource = mode;
|
|
288
|
-
await
|
|
306
|
+
await saveConfigPrefs(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 saveConfigPrefs(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 saveConfigPrefs(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 saveConfigPrefs(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
|
-
await
|
|
372
|
+
await saveConfigPrefs(config);
|
|
303
373
|
}
|
|
374
|
+
config = (await seedDriversFromDetection(config)).config;
|
|
375
|
+
await saveConfigPrefs(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 {
|
|
335
|
-
|
|
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;
|
|
418
|
+
await heartbeat(client, config, workspace ? await buildWorkspaceBrief(workspace).catch(() => brief) : null, processOnlineIds);
|
|
336
419
|
await renewLeases(client, config);
|
|
337
|
-
if (
|
|
338
|
-
progressed = await pumpExecutionSlots(client, config,
|
|
339
|
-
heartbeat: async () => heartbeat(client, config, await buildWorkspaceBrief(workspace).catch(() => brief)),
|
|
420
|
+
if (workspace && (processDriver || onlineDriverIds(config).length)) {
|
|
421
|
+
progressed = await pumpExecutionSlots(client, config, workspace, brief, timeoutMs, {
|
|
422
|
+
heartbeat: async () => heartbeat(client, config, await buildWorkspaceBrief(workspace).catch(() => brief), processOnlineIds),
|
|
340
423
|
heartbeatIntervalMs: intervalMs,
|
|
341
|
-
}, running);
|
|
424
|
+
}, running, { driver: processDriver, processOnlineIds });
|
|
342
425
|
}
|
|
343
426
|
}
|
|
344
427
|
catch (error) {
|
|
@@ -352,12 +435,16 @@ async function runner() {
|
|
|
352
435
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, intervalMs));
|
|
353
436
|
}
|
|
354
437
|
}
|
|
355
|
-
async function heartbeat(client, config, brief) {
|
|
438
|
+
async function heartbeat(client, config, brief, processOnlineIds) {
|
|
439
|
+
// Process-level `--agent` override must count as online even when saved lanes are offline.
|
|
440
|
+
const online = processOnlineIds?.length ? processOnlineIds : onlineDriverIds(config);
|
|
441
|
+
const status = heartbeatStatusForDrivers(online);
|
|
356
442
|
await client.request("/runner/v1/heartbeat", { method: "POST", body: JSON.stringify({
|
|
357
|
-
status
|
|
443
|
+
status,
|
|
358
444
|
capabilities: config.capabilities,
|
|
359
445
|
lease_capacity: config.leaseCapacity,
|
|
360
446
|
fuel_source: config.fuelSource === "local" ? "local" : "conduit",
|
|
447
|
+
drivers: driversHeartbeatReport(config, config.activeAttempts, processOnlineIds),
|
|
361
448
|
...(brief ? { workspace_brief: brief } : {}),
|
|
362
449
|
}) });
|
|
363
450
|
}
|
|
@@ -368,6 +455,8 @@ try {
|
|
|
368
455
|
await join();
|
|
369
456
|
else if (command === "fuel")
|
|
370
457
|
await fuelCommand();
|
|
458
|
+
else if (command === "drivers")
|
|
459
|
+
await driversCommand();
|
|
371
460
|
else if (command === "mcp")
|
|
372
461
|
await runMcp();
|
|
373
462
|
else if (command === "runner")
|
|
@@ -377,7 +466,7 @@ try {
|
|
|
377
466
|
else if (command === "uninstall-service")
|
|
378
467
|
await uninstallService();
|
|
379
468
|
else
|
|
380
|
-
throw new Error(`Usage: ${BRIDGE_NPX} <join|connect|fuel|mcp|runner|install-service|uninstall-service>`);
|
|
469
|
+
throw new Error(`Usage: ${BRIDGE_NPX} <join|connect|fuel|drivers|mcp|runner|install-service|uninstall-service>`);
|
|
381
470
|
}
|
|
382
471
|
catch (error) {
|
|
383
472
|
console.error(error instanceof Error ? redactSecrets(error.message) : "Conduit command failed");
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { removeRuntimeAttempt, saveConfigPrefs, saveRuntime } from "./config.js";
|
|
2
2
|
export class ConduitRequestError extends Error {
|
|
3
3
|
status;
|
|
4
4
|
code;
|
|
@@ -10,10 +10,24 @@ export class ConduitRequestError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
export class ConduitClient {
|
|
12
12
|
config;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
persistRuntime;
|
|
14
|
+
persistPrefs;
|
|
15
|
+
removeAttempt;
|
|
16
|
+
constructor(config, persist) {
|
|
15
17
|
this.config = config;
|
|
16
|
-
|
|
18
|
+
if (typeof persist === "function") {
|
|
19
|
+
this.persistRuntime = persist;
|
|
20
|
+
this.persistPrefs = persist;
|
|
21
|
+
this.removeAttempt = async (taskId) => {
|
|
22
|
+
delete this.config.activeAttempts[taskId];
|
|
23
|
+
await persist(this.config);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
this.persistRuntime = persist?.runtime ?? saveRuntime;
|
|
28
|
+
this.persistPrefs = persist?.prefs ?? saveConfigPrefs;
|
|
29
|
+
this.removeAttempt = persist?.removeAttempt ?? removeRuntimeAttempt;
|
|
30
|
+
}
|
|
17
31
|
}
|
|
18
32
|
async request(path, init = {}) {
|
|
19
33
|
const response = await fetch(`${this.config.baseUrl}${path}`, {
|
|
@@ -25,11 +39,18 @@ export class ConduitClient {
|
|
|
25
39
|
throw new ConduitRequestError(data.error?.message ?? `Conduit request failed (${response.status})`, response.status, data.error?.code);
|
|
26
40
|
return data;
|
|
27
41
|
}
|
|
28
|
-
async claim(taskId, attemptId) {
|
|
42
|
+
async claim(taskId, attemptId, extras) {
|
|
29
43
|
const data = await this.request(`/runner/v1/tasks/${taskId}/claim`, { method: "POST", body: JSON.stringify({ attempt_id: attemptId, idempotency_key: `bridge:claim:${attemptId}` }) });
|
|
30
|
-
const active = {
|
|
44
|
+
const active = {
|
|
45
|
+
taskId,
|
|
46
|
+
attemptId,
|
|
47
|
+
leaseToken: String(data.lease_token),
|
|
48
|
+
leaseExpiresAt: String(data.lease_expires_at),
|
|
49
|
+
phase: "claimed",
|
|
50
|
+
...(extras?.driverId ? { driverId: extras.driverId } : {}),
|
|
51
|
+
};
|
|
31
52
|
this.config.activeAttempts[taskId] = active;
|
|
32
|
-
await this.
|
|
53
|
+
await this.persistRuntime(this.config);
|
|
33
54
|
return { ...data, lease_token: "stored by Conduit Bridge" };
|
|
34
55
|
}
|
|
35
56
|
attempt(taskId) {
|
|
@@ -51,12 +72,15 @@ export class ConduitClient {
|
|
|
51
72
|
else
|
|
52
73
|
Object.assign(active, { [key]: value });
|
|
53
74
|
}
|
|
54
|
-
await this.
|
|
75
|
+
await this.persistRuntime(this.config);
|
|
55
76
|
return active;
|
|
56
77
|
}
|
|
57
78
|
async clearAttempt(taskId) {
|
|
79
|
+
const active = this.config.activeAttempts[taskId];
|
|
58
80
|
delete this.config.activeAttempts[taskId];
|
|
59
|
-
|
|
81
|
+
if (this.config.sessions)
|
|
82
|
+
delete this.config.sessions[taskId];
|
|
83
|
+
await this.removeAttempt(taskId, active?.attemptId);
|
|
60
84
|
}
|
|
61
85
|
/**
|
|
62
86
|
* Ensure a project-scoped gateway fuel key for agent /v1 calls.
|
|
@@ -74,7 +98,7 @@ export class ConduitClient {
|
|
|
74
98
|
gatewayKey = String(rotated.gateway_secret);
|
|
75
99
|
}
|
|
76
100
|
this.config.fuel = { ...this.config.fuel, [projectId]: { gatewayKey } };
|
|
77
|
-
await this.
|
|
101
|
+
await this.persistPrefs(this.config);
|
|
78
102
|
return gatewayKey;
|
|
79
103
|
}
|
|
80
104
|
}
|