@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 +8 -3
- package/dist/brief.js +36 -1
- package/dist/cli.js +22 -5
- package/dist/client.js +24 -0
- package/dist/execution-class.js +9 -2
- package/dist/execution.js +113 -4
- package/dist/ops.js +8 -2
- package/dist/workspace-bootstrap.js +56 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connects a computer to one organization, claims work, and drives a local agent.
|
|
4
4
|
|
|
5
|
-
**Package version:** `0.16.
|
|
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.
|
|
82
|
-
|
|
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
|
|
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)) {
|
package/dist/execution-class.js
CHANGED
|
@@ -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
|
-
|
|
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")
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
13
|
+
/** Install dependencies with the repository's own committed package-manager identity. */
|
|
14
14
|
export function bootstrapManagedWorkspace(workspace, run = defaultRunner) {
|
|
15
|
-
|
|
15
|
+
const packagePath = join(workspace, "package.json");
|
|
16
|
+
if (!existsSync(packagePath))
|
|
16
17
|
return "not_applicable";
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
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": {
|