@miraland-labs/conduit-bridge 0.16.24 → 0.16.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connects a computer to one organization, claims work, and drives a local agent.
4
4
 
5
- **Package version:** `0.16.21` — mutate_repo with `pr_create` opens a pull request even when `change_scope` is empty. `ops disconnect` now asks before it removes the runner service and clears credentials; pass `--yes` to skip the question. `ops.env` is enrollment/bootstrap defaults only: an explicit workspace does not inherit an old repository, and project switches do not rewrite global project state. `ops enroll` exchanges a single-use token, registers the machine, binds project affinity, provisions fuel keys, brings detected drivers online, and starts the runner in one command. On Linux, `ops install` refuses any effective systemd drop-in. A switch intent remains queued until the target runner heartbeat proves Bound. After every Bridge publish, operators must re-run `ops install` (LaunchAgent pins an absolute `cli.js`).
5
+ **Package version:** `0.16.26` — local-fuel provider refusals make the affected lane unavailable instead of repeatedly spending attempts; an operator who changes the lane's account or plan can invalidate that observation with `ops quota-clear <driver>`. Bridge discovers bounded repository verification commands, including Make targets `test`, `check`, `verify`, `replay`, `typecheck`, `lint`, and `build`. `ops disconnect` asks before it removes the runner service and clears credentials; pass `--yes` to skip the question. `ops.env` is enrollment/bootstrap defaults only: an explicit workspace does not inherit an old repository, and project switches do not rewrite global project state. `ops enroll` exchanges a single-use token, registers the machine, binds project affinity, provisions fuel keys, brings detected drivers online, and starts the runner in one command. On Linux, `ops install` refuses any effective systemd drop-in. A switch intent remains queued until the target runner heartbeat proves Bound. After every Bridge publish, operators must re-run `ops install` (LaunchAgent pins an absolute `cli.js`).
6
6
 
7
7
  ## Prerequisites
8
8
 
@@ -48,6 +48,7 @@ Later:
48
48
  ```bash
49
49
  npx @miraland-labs/conduit-bridge@latest ops online cursor
50
50
  npx @miraland-labs/conduit-bridge@latest ops offline cursor
51
+ npx @miraland-labs/conduit-bridge@latest ops quota-clear cursor # after changing its account or plan
51
52
  npx @miraland-labs/conduit-bridge@latest ops disconnect # then join again to change organizations
52
53
  ```
53
54
 
@@ -78,8 +79,12 @@ npx @miraland-labs/conduit-bridge disconnect --yes
78
79
  Before agent spend, Bridge checks that the workspace is readable, matches the expected repository,
79
80
  and is clean; each online driver must be installed, compatible with its configured fuel, and signed
80
81
  in when local fuel is used. The same bounded report is sent on heartbeat so Conduit can place an
81
- environment failure on Hold without consuming an execution attempt. Fix the named issue and let a
82
- fresh heartbeat land before choosing **Recheck** in Activity.
82
+ environment failure on Hold without consuming an execution attempt. A local provider refusal is
83
+ persisted and excludes only that lane; `online`/`offline` does not erase it. `ops doctor` proves CLI
84
+ installation and authentication, not remaining vendor allowance. After deliberately changing an
85
+ account or plan, run `ops quota-clear <driver>` to clear the old observation; this does not probe the
86
+ provider, and the next real claim records another refusal if the new allowance is still unavailable.
87
+ Fix the named issue and let a fresh heartbeat land before choosing **Recheck** in Activity.
83
88
 
84
89
  Conduit labels an exact driver/CLI version **Certified** only after two consecutive real canaries:
85
90
  repository delivery with PR/evidence and a successful rework cycle. All other lanes are
package/dist/brief.js CHANGED
@@ -2,6 +2,7 @@ import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
+ import { isBoundedVerificationCommand } from "./execution-class.js";
5
6
  const execFileAsync = promisify(execFile);
6
7
  const MANIFESTS = [
7
8
  "package.json", "wrangler.jsonc", "wrangler.toml", "tsconfig.json",
@@ -10,6 +11,7 @@ const MANIFESTS = [
10
11
  ];
11
12
  /** Prefer test before typecheck/lint so discovery order matches what pickVerificationCommand wants. */
12
13
  const VERIFICATION_SCRIPTS = ["test", "verify", "typecheck", "lint", "build"];
14
+ const MAKE_VERIFICATION_TARGETS = ["test", "check", "verify", "replay", "typecheck", "lint", "build"];
13
15
  const EXCLUDED_DIRECTORIES = new Set(["node_modules", "dist", "build", "target", "coverage", ".venv", "venv"]);
14
16
  export async function buildWorkspaceBrief(workspace) {
15
17
  const entries = await readdir(workspace, { withFileTypes: true });
@@ -197,8 +199,40 @@ async function gitDirectories(workspace) {
197
199
  }
198
200
  }
199
201
  /** Exported for tests — discovers bounded verification commands from workspace manifests. */
202
+ /**
203
+ * Commands a project declares for itself, in `.conduit/verification` — one per line, `#` comments
204
+ * ignored.
205
+ *
206
+ * Discovery below recognises the conventional names (`test`, `check`, `npm run verify`, …). A project
207
+ * whose gate is named anything else had no way to say so, and an acceptance criterion naming that
208
+ * gate was unrunnable and therefore unverifiable — which is how four agent attempts died on a target
209
+ * that was declared in the very Makefile Bridge had already read.
210
+ *
211
+ * The file is repository content and therefore UNTRUSTED. Every line must pass the same shape check
212
+ * every discovered command passes, so a declaration can name a gate but can never smuggle a command:
213
+ * one tool, one identifier, no shell metacharacters. That check is the boundary, not the name — a
214
+ * recipe body was always repository-authored.
215
+ */
216
+ const MAX_DECLARED_VERIFICATION = 8;
217
+ export function parseDeclaredVerification(text) {
218
+ const declared = [];
219
+ for (const raw of text.split("\n")) {
220
+ const line = raw.split("#")[0].trim();
221
+ if (!line || !isBoundedVerificationCommand(line))
222
+ continue;
223
+ if (!declared.includes(line))
224
+ declared.push(line);
225
+ if (declared.length >= MAX_DECLARED_VERIFICATION)
226
+ break;
227
+ }
228
+ return declared;
229
+ }
200
230
  export async function discoverVerificationCommands(workspace, files) {
201
231
  const commands = [];
232
+ try {
233
+ commands.push(...parseDeclaredVerification(await readFile(join(workspace, ".conduit", "verification"), "utf8")));
234
+ }
235
+ catch { /* no declaration, or unreadable: discovery below still applies */ }
202
236
  if (files.has("package.json")) {
203
237
  try {
204
238
  const manifest = JSON.parse(await readFile(join(workspace, "package.json"), "utf8"));
@@ -209,9 +243,10 @@ export async function discoverVerificationCommands(workspace, files) {
209
243
  if (files.has("Makefile")) {
210
244
  try {
211
245
  const makefile = await readFile(join(workspace, "Makefile"), "utf8");
212
- for (const target of ["test", "check"])
246
+ for (const target of MAKE_VERIFICATION_TARGETS) {
213
247
  if (new RegExp(`^${target}\\s*:`, "m").test(makefile))
214
248
  commands.push(`make ${target}`);
249
+ }
215
250
  }
216
251
  catch { /* unreadable Makefiles do not broaden execution */ }
217
252
  }
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
8
8
  import { stdin as input, stdout as output } from "node:process";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { ConduitClient, ConduitRequestError } from "./client.js";
11
- import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearLocalConnection, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, saveConfigPrefs, savePendingConnection, suggestMachineName, } from "./config.js";
11
+ import { BRIDGE_LEASE_CAPACITY, BRIDGE_MAX_LEASE_CAPACITY, clampLeaseCapacity, clearLocalConnection, clearPendingConnection, loadConfig, loadConfigIfPresent, loadOrCreateInstallationId, loadPendingConnection, redactSecrets, saveConfig, saveConfigPrefs, saveDriverQuota, savePendingConnection, suggestMachineName, } from "./config.js";
12
12
  import { runMcp } from "./mcp.js";
13
13
  import { detectInstalledClients, localFuelOnlyClients, suggestFuelSource } from "./detect.js";
14
14
  import { DRIVERS } from "./driver.js";
@@ -382,7 +382,7 @@ async function initOps() {
382
382
  console.log(`Config file (separate): ${envPath}`);
383
383
  console.log("");
384
384
  console.log("Works the same on macOS, Linux, and Windows via:");
385
- console.log(` ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|doctor|disconnect|uninstall>")}`);
385
+ console.log(` ${bridgeUsage("ops", "<connect|install|switch|online|offline|quota-clear|status|doctor|disconnect|uninstall>")}`);
386
386
  console.log("");
387
387
  console.log("1. Set enrollment/bootstrap defaults once:");
388
388
  console.log(` Create folder: ${configDir}`);
@@ -401,12 +401,12 @@ async function initOps() {
401
401
  console.log("3. Install runner:");
402
402
  console.log(` ${pathJoin(target, "install.sh")}`);
403
403
  }
404
- console.log("Later: status · online · offline · disconnect (same names .sh / .cmd)");
404
+ console.log("Later: status · online · offline · quota-clear · disconnect (same names .sh / .cmd)");
405
405
  }
406
406
  async function opsCommand() {
407
407
  const verb = process.argv[3];
408
408
  if (!verb || !OPS_VERBS.includes(verb)) {
409
- throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|switch|online|offline|status|doctor|disconnect|uninstall>", "[driver…]")} (install also takes --workspace <path> [--repo <url>])`);
409
+ throw new Error(`Usage: ${bridgeUsage("ops", "<connect|install|switch|online|offline|quota-clear|status|doctor|disconnect|uninstall>", "[driver…]")} (install also takes --workspace <path> [--repo <url>])`);
410
410
  }
411
411
  await runOps(verb, process.argv.slice(4));
412
412
  }
@@ -458,6 +458,23 @@ async function driversCommand() {
458
458
  console.log(`Now online: ${onlineDriverIds(config).join(", ") || "(none)"}`);
459
459
  return;
460
460
  }
461
+ if (sub === "quota") {
462
+ const action = process.argv[4]?.trim();
463
+ const id = process.argv[5]?.trim();
464
+ if (action !== "clear" || !id || process.argv[6]) {
465
+ throw new Error(`Usage: ${bridgeUsage("drivers", "quota", "clear", AGENT_PLACEHOLDER)}`);
466
+ }
467
+ if (!isSupportedDriverId(id))
468
+ throw new Error(`Unknown driver: ${id}. Available: ${Object.keys(DRIVERS).join(", ")}`);
469
+ if (!listDriverLanes(config).some((lane) => lane.id === id))
470
+ throw new Error(`Driver lane is not registered: ${id}`);
471
+ // A login or plan change happens outside Bridge, so only the operator can invalidate the last
472
+ // observed refusal. This intentionally does not toggle the lane or synthesize a successful probe:
473
+ // the next real claim is the proof, and will record another refusal if allowance is still spent.
474
+ await saveDriverQuota(id, undefined);
475
+ console.log(`Cleared recorded provider allowance refusal for ${id}. The next claim will recheck this lane.`);
476
+ return;
477
+ }
461
478
  if (sub === "fuel") {
462
479
  const id = process.argv[4]?.trim();
463
480
  const mode = process.argv[5];
@@ -470,7 +487,7 @@ async function driversCommand() {
470
487
  console.log(`${driverLabel(id)} lane fuel set to ${mode}`);
471
488
  return;
472
489
  }
473
- throw new Error(`Usage: ${bridgeUsage("drivers", "[list|online|offline|fuel]", "…")}`);
490
+ throw new Error(`Usage: ${bridgeUsage("drivers", "[list|online|offline|fuel|quota]", "…")}`);
474
491
  }
475
492
  async function runner() {
476
493
  applyRunnerToolPath();
package/dist/client.js CHANGED
@@ -80,6 +80,30 @@ export class ConduitClient {
80
80
  const active = this.attempt(taskId);
81
81
  return this.request(`/runner/v1/tasks/${taskId}/${action}`, { method: "POST", body: JSON.stringify({ ...body, attempt_id: active.attemptId, lease_token: active.leaseToken }) });
82
82
  }
83
+ async listInstructions(taskId, after = "") {
84
+ const active = this.attempt(taskId);
85
+ const query = new URLSearchParams({ attempt_id: active.attemptId, ...(after ? { after } : {}) });
86
+ const data = await this.request(`/runner/v1/tasks/${taskId}/instructions?${query}`);
87
+ return Array.isArray(data.instructions)
88
+ ? data.instructions.flatMap((row) => {
89
+ if (!row || typeof row !== "object")
90
+ return [];
91
+ const instruction = row;
92
+ return typeof instruction.id === "string"
93
+ && typeof instruction.payload_json === "string"
94
+ && typeof instruction.created_at === "string"
95
+ ? [{ id: instruction.id, payload_json: instruction.payload_json, created_at: instruction.created_at }]
96
+ : [];
97
+ })
98
+ : [];
99
+ }
100
+ async acknowledgeInstruction(taskId, instructionId) {
101
+ const active = this.attempt(taskId);
102
+ await this.attemptRequest(taskId, `instructions/${instructionId}/ack`, {
103
+ instruction_id: instructionId,
104
+ idempotency_key: `bridge:instruction-ack:${active.attemptId}:${instructionId}`,
105
+ });
106
+ }
83
107
  async updateAttempt(taskId, patch) {
84
108
  const active = this.attempt(taskId);
85
109
  for (const key of Object.keys(patch)) {
@@ -5,7 +5,12 @@
5
5
  /** Bounded verification commands a `test_run` grant may execute. */
6
6
  export function isBoundedVerificationCommand(command) {
7
7
  // Exact forms only — a trailing `.*` on pytest would admit `pytest ; curl … | sh`.
8
- return /^(npm run (verify|typecheck|lint|test|build)|pnpm (verify|typecheck|lint|test|build)|yarn (verify|typecheck|lint|test|build)|cargo (test|check)|go test(?: \.\/\.\.\.)?|make (test|check)|python -m pytest|pytest(?: [\w./=-]+)?|\.\/gradlew test)$/.test(command);
8
+ // A target or script NAME is not a trust boundary: `make check` already runs whatever recipe the
9
+ // repository authored, so restricting the name to check/test grants no safety — it only stops a
10
+ // project from naming its own gate, which is what forced four attempts to die on a command they
11
+ // could not run. The boundary that matters is the SHAPE: one tool, one identifier, no shell
12
+ // metacharacters, so nothing can be smuggled in through the name.
13
+ return /^(npm run [A-Za-z0-9][A-Za-z0-9._:-]*|pnpm [A-Za-z0-9][A-Za-z0-9._:-]*|yarn [A-Za-z0-9][A-Za-z0-9._:-]*|cargo (test|check)|go test(?: \.\/\.\.\.)?|make [A-Za-z0-9][A-Za-z0-9._-]*|python -m pytest|pytest(?: [\w./=-]+)?|\.\/gradlew test)$/.test(command);
9
14
  }
10
15
  /** Shell commands each grant authorizes — mapped per driver so they cannot drift. */
11
16
  export const branchCreateCommands = [
@@ -76,7 +81,9 @@ export function executionClassPromptRules(input) {
76
81
  const artifact = executionClass === "publish_artifact";
77
82
  const rules = [];
78
83
  const grantShell = [
79
- ...(grants.includes("test_run") ? verificationCommands.filter(isBoundedVerificationCommand) : []),
84
+ ...(grants.includes("test_run")
85
+ ? verificationCommands.filter((command) => isBoundedVerificationCommand(command) && !deniedCommands.includes(command))
86
+ : []),
80
87
  ...(grants.includes("branch_create") ? branchCreateCommands : []),
81
88
  ...(grants.includes("pr_create") ? prCreateCommands : []),
82
89
  ];
package/dist/execution.js CHANGED
@@ -79,7 +79,7 @@ export function forgeTransportFailure(message) {
79
79
  * costs an evening. Deliberately narrow — an unmatched refusal degrades to an ordinary agent
80
80
  * failure, which is exactly today's behaviour.
81
81
  */
82
- export const SUBSCRIPTION_EXHAUSTED_PATTERN = /\b(?:usage|rate)[ _-]?limit(?:ed|s)?\b|\bquota (?:exceeded|exhausted|reached)\b|\b(?:http|status|code)\W{0,3}429\b|\b429\b(?=\W{0,3}too many)|too many requests|out of (?:credits|usage)/i;
82
+ export const SUBSCRIPTION_EXHAUSTED_PATTERN = /\b(?:usage|rate|session)[ _-]?limit(?:ed|s)?\b|\b(?:daily|weekly|monthly) limit\b|\bquota (?:exceeded|exhausted|reached)\b|\b(?:http|status|code)\W{0,3}429\b|\b429\b(?=\W{0,3}too many)|too many requests|out of (?:credits|usage)/i;
83
83
  /**
84
84
  * Does this failure mean the plan is spent, rather than the prompt being wrong?
85
85
  *
@@ -411,6 +411,79 @@ export async function renewLeases(client, config) {
411
411
  }
412
412
  }
413
413
  }
414
+ /**
415
+ * Poll while the headless CLI owns the worktree, then hand owner guidance to its next session turn.
416
+ * Driver stdin is intentionally closed after the initial prompt, so pretending a mid-process write
417
+ * is portable would silently lose guidance on most lanes. The first safe common checkpoint is the
418
+ * driver's completed turn, before Bridge validates or submits its delivery.
419
+ */
420
+ function pollCheckpointInstructions(client, taskId, state, intervalMs = 15_000) {
421
+ let stopped = false;
422
+ let inFlight = null;
423
+ const pending = new Map();
424
+ const poll = async () => {
425
+ const rows = await client.listInstructions(taskId, state.cursor);
426
+ for (const row of rows) {
427
+ state.cursor = `${row.created_at}|${row.id}`;
428
+ if (state.seen.has(row.id))
429
+ continue;
430
+ state.seen.add(row.id);
431
+ const parsed = parseCheckpointInstruction(row);
432
+ if (parsed && state.acceptedCount < 10 && state.acceptedCharacters + parsed.message.length <= 40_000) {
433
+ pending.set(parsed.id, parsed);
434
+ state.acceptedCount += 1;
435
+ state.acceptedCharacters += parsed.message.length;
436
+ }
437
+ else if (parsed) {
438
+ console.error(`Instruction ${parsed.id} exceeded the checkpoint guidance bound and was not acknowledged.`);
439
+ }
440
+ }
441
+ };
442
+ const runPoll = () => {
443
+ if (stopped || inFlight)
444
+ return;
445
+ inFlight = poll()
446
+ .catch((error) => console.error(`Instruction poll failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`))
447
+ .finally(() => { inFlight = null; });
448
+ };
449
+ runPoll();
450
+ const timer = setInterval(runPoll, intervalMs);
451
+ timer.unref?.();
452
+ return {
453
+ stop: async () => {
454
+ stopped = true;
455
+ clearInterval(timer);
456
+ await inFlight;
457
+ await poll().catch((error) => console.error(`Final instruction poll failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
458
+ return [...pending.values()];
459
+ },
460
+ };
461
+ }
462
+ function parseCheckpointInstruction(row) {
463
+ try {
464
+ const payload = JSON.parse(row.payload_json);
465
+ if (payload.changes_scope === true) {
466
+ console.error(`Refusing scope-changing instruction ${row.id}; revise the approved initiative contract instead.`);
467
+ return null;
468
+ }
469
+ const message = typeof payload.message === "string" ? payload.message.trim() : "";
470
+ return message ? { id: row.id, message: message.slice(0, 20_000), createdAt: row.created_at } : null;
471
+ }
472
+ catch {
473
+ return null;
474
+ }
475
+ }
476
+ function checkpointInstructionPrompt(instructions) {
477
+ return [
478
+ "OWNER INSTRUCTIONS AT CHECKPOINT",
479
+ "These messages refine the current run only. They do not amend approved scope, boundaries, acceptance criteria, or grants.",
480
+ ...instructions.map((instruction) => `- ${instruction.message}`),
481
+ "",
482
+ "Apply the guidance within the approved contract, re-check the resulting work, then return a complete final report.",
483
+ "Return only one fenced ```json object with exactly this shape:",
484
+ agentReportTemplate,
485
+ ].join("\n");
486
+ }
414
487
  function resolveAttemptDriver(config, active, fallback) {
415
488
  if (active.driverId && DRIVERS[active.driverId])
416
489
  return DRIVERS[active.driverId];
@@ -983,7 +1056,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
983
1056
  // turn already "finished" makes the agent reply conversationally without the report block.
984
1057
  const resumeSessionId = options.forceResumeSessionId
985
1058
  ?? (reworkFeedback && task.repair_mode !== "briefed" ? config.sessions?.[taskId] : undefined);
986
- const result = await driver.run({
1059
+ const runInput = {
987
1060
  prompt,
988
1061
  workspace: attemptWorkspace,
989
1062
  grants,
@@ -997,7 +1070,40 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
997
1070
  model: selection.model,
998
1071
  fuel,
999
1072
  fuelSource,
1000
- });
1073
+ };
1074
+ let result;
1075
+ let currentRunInput = runInput;
1076
+ const instructionPollState = {
1077
+ cursor: "",
1078
+ seen: new Set(),
1079
+ acceptedCount: 0,
1080
+ acceptedCharacters: 0,
1081
+ };
1082
+ let instructionsInPrompt = [];
1083
+ for (let round = 0;; round += 1) {
1084
+ const instructionPoll = diagnosis ? null : pollCheckpointInstructions(client, taskId, instructionPollState);
1085
+ let polled = [];
1086
+ try {
1087
+ result = await driver.run(currentRunInput);
1088
+ }
1089
+ finally {
1090
+ polled = await instructionPoll?.stop() ?? polled;
1091
+ }
1092
+ if (instructionsInPrompt.length > 0) {
1093
+ await Promise.all(instructionsInPrompt.map((instruction) => client.acknowledgeInstruction(taskId, instruction.id).catch((error) => console.error(`Instruction acknowledgement failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`))));
1094
+ }
1095
+ // One initial turn plus at most two bounded checkpoint turns. Guidance arriving after that
1096
+ // remains unacknowledged rather than creating an unbounded owner-driven agent loop.
1097
+ if (diagnosis || result.status !== "completed" || polled.length === 0 || round >= 2)
1098
+ break;
1099
+ instructionsInPrompt = polled;
1100
+ const followup = checkpointInstructionPrompt(instructionsInPrompt);
1101
+ currentRunInput = {
1102
+ ...runInput,
1103
+ prompt: result.sessionId ? followup : `${prompt}\n\n${followup}`,
1104
+ resumeSessionId: result.sessionId ?? undefined,
1105
+ };
1106
+ }
1001
1107
  if (result.sessionId)
1002
1108
  config.sessions = { ...config.sessions, [taskId]: result.sessionId };
1003
1109
  await learnDriverFuel(config, driver.name, fuelSource, result);
@@ -1052,7 +1158,10 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
1052
1158
  // reason", leaving Conductor nothing to diagnose. Re-run the project's bounded verification
1053
1159
  // in the attempt worktree to recover the real errors. Best-effort: keep the agent's own
1054
1160
  // message when there is no command, the command cannot run, or the tree actually verifies.
1055
- const verificationDetail = grants.includes("test_run")
1161
+ // A vendor allowance refusal is already a precise environment diagnosis. Running repository
1162
+ // verification after it can surface an unrelated pre-existing failure and overwrite the lane
1163
+ // Hold with a false source-repair brief.
1164
+ const verificationDetail = grants.includes("test_run") && !subscriptionExhausted(agentMessage)
1056
1165
  ? await captureVerificationFailure({
1057
1166
  workspace: attemptWorkspace,
1058
1167
  verificationCommands: attemptBrief?.verification ?? [],
package/dist/ops.js CHANGED
@@ -19,7 +19,7 @@ import { applyRunnerToolPath, readActiveRunnerServiceWorkspace, readStoredRunner
19
19
  import { bridgeVersion } from "./version.js";
20
20
  import { bootstrapManagedWorkspace } from "./workspace-bootstrap.js";
21
21
  export const OPS_VERBS = [
22
- "connect", "enroll", "install", "switch", "online", "offline", "status", "doctor", "disconnect", "uninstall",
22
+ "connect", "enroll", "install", "switch", "online", "offline", "quota-clear", "status", "doctor", "disconnect", "uninstall",
23
23
  ];
24
24
  const LOCAL_FUEL_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity", "grok"]);
25
25
  /**
@@ -343,12 +343,18 @@ export async function runOps(verb, argv = [], deps = {}) {
343
343
  console.log(`- ${describePreflightIssue(issue)}`);
344
344
  throw new Error("Fix the preflight issues before retrying Conduit work");
345
345
  }
346
- // Lane toggles only need Bridge config + optional driver ids — not a full ops.env.
346
+ // Lane controls only need Bridge config + optional driver ids — not a full ops.env.
347
347
  if (verb === "online" || verb === "offline") {
348
348
  const drivers = await resolveDrivers(env, argv);
349
349
  runBridge(["drivers", verb, ...drivers]);
350
350
  return;
351
351
  }
352
+ if (verb === "quota-clear") {
353
+ if (argv.length !== 1)
354
+ throw new Error("Usage: ops quota-clear <driver>");
355
+ runBridge(["drivers", "quota", "clear", argv[0]]);
356
+ return;
357
+ }
352
358
  // Explicit install arguments are active-run inputs, not machine-wide defaults. Enrollment may
353
359
  // seed ops.env once; later project changes must not mutate it.
354
360
  let installEnv = env;
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  function defaultRunner(command, args, workspace) {
5
5
  const result = spawnSync(command, args, {
@@ -10,17 +10,64 @@ function defaultRunner(command, args, workspace) {
10
10
  });
11
11
  return { status: result.status, ...(result.error ? { error: result.error } : {}) };
12
12
  }
13
- /** Install locked JavaScript dependencies before a Conduit-managed checkout advertises readiness. */
13
+ /** Install dependencies with the repository's own committed package-manager identity. */
14
14
  export function bootstrapManagedWorkspace(workspace, run = defaultRunner) {
15
- if (!existsSync(join(workspace, "package.json")))
15
+ const packagePath = join(workspace, "package.json");
16
+ if (!existsSync(packagePath))
16
17
  return "not_applicable";
17
- if (!existsSync(join(workspace, "package-lock.json"))) {
18
- throw new Error("managed_workspace_bootstrap_package_lock_required");
19
- }
20
- const result = run("npm", ["ci", "--include=optional", "--no-audit", "--no-fund"], workspace);
18
+ const install = lockedInstall(workspace, packagePath);
19
+ const result = run(install.command, install.args, workspace);
21
20
  if (result.error)
22
- throw new Error(`managed_workspace_bootstrap_failed:${result.error.message}`);
21
+ throw new Error(`managed_workspace_bootstrap_failed:${install.id}:${result.error.message}`);
23
22
  if (result.status !== 0)
24
- throw new Error(`managed_workspace_bootstrap_failed:exit_${result.status ?? "unknown"}`);
23
+ throw new Error(`managed_workspace_bootstrap_failed:${install.id}:exit_${result.status ?? "unknown"}`);
25
24
  return "installed";
26
25
  }
26
+ function lockedInstall(workspace, packagePath) {
27
+ const declared = declaredPackageManager(packagePath);
28
+ const managers = [
29
+ existsSync(join(workspace, "package-lock.json")) || existsSync(join(workspace, "npm-shrinkwrap.json")) ? "npm" : null,
30
+ existsSync(join(workspace, "pnpm-lock.yaml")) ? "pnpm" : null,
31
+ existsSync(join(workspace, "yarn.lock")) ? "yarn" : null,
32
+ existsSync(join(workspace, "bun.lock")) || existsSync(join(workspace, "bun.lockb")) ? "bun" : null,
33
+ ].filter((manager) => manager !== null);
34
+ if (managers.length === 0)
35
+ throw new Error("managed_workspace_bootstrap_lockfile_required");
36
+ if (declared && !managers.includes(declared)) {
37
+ throw new Error(`managed_workspace_bootstrap_package_manager_lockfile_mismatch:${declared}:${managers.join(",")}`);
38
+ }
39
+ const selected = declared ?? (managers.length === 1 ? managers[0] : null);
40
+ if (!selected)
41
+ throw new Error(`managed_workspace_bootstrap_lockfile_ambiguous:${managers.join(",")}`);
42
+ if (selected === "npm")
43
+ return { id: selected, command: "npm", args: ["ci", "--include=optional", "--no-audit", "--no-fund"] };
44
+ if (selected === "pnpm")
45
+ return { id: selected, command: "pnpm", args: ["install", "--frozen-lockfile"] };
46
+ if (selected === "bun")
47
+ return { id: selected, command: "bun", args: ["install", "--frozen-lockfile"] };
48
+ const modernYarn = existsSync(join(workspace, ".yarnrc.yml")) || declaredPackageManagerMajor(packagePath, "yarn") >= 2;
49
+ return { id: selected, command: "yarn", args: ["install", modernYarn ? "--immutable" : "--frozen-lockfile"] };
50
+ }
51
+ function declaredPackageManager(packagePath) {
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
54
+ const match = typeof parsed.packageManager === "string" ? /^(npm|pnpm|yarn|bun)@/.exec(parsed.packageManager.trim()) : null;
55
+ const id = match?.[1];
56
+ return id === "npm" || id === "pnpm" || id === "yarn" || id === "bun" ? id : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function declaredPackageManagerMajor(packagePath, manager) {
63
+ try {
64
+ const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
65
+ const match = typeof parsed.packageManager === "string"
66
+ ? new RegExp(`^${manager}@(\\d+)`).exec(parsed.packageManager.trim())
67
+ : null;
68
+ return match ? Number(match[1]) : 0;
69
+ }
70
+ catch {
71
+ return 0;
72
+ }
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.24",
3
+ "version": "0.16.26",
4
4
  "description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity / Grok Build agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {