@miraland-labs/conduit-bridge 0.16.207 → 0.16.210

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/dist/driver.js CHANGED
@@ -244,6 +244,12 @@ export function buildAssignmentPrompt(context) {
244
244
  if (packageContext?.decisions?.length) {
245
245
  lines.push("", "PROJECT DECISIONS (explicit human decisions — treat as binding context)", ...packageContext.decisions.map((item) => `- Q: ${item.question}\n A: ${item.decision}`));
246
246
  }
247
+ // The earlier work comes before the brief: the brief names a fix to that work.
248
+ if (context.resumeHead) {
249
+ lines.push("", context.repairMode === "briefed"
250
+ ? `${RESUME_PROMPT_HEADING} — an earlier run of this same package failed its verification before delivery.`
251
+ : `${RESUME_PROMPT_HEADING} — an earlier run of this same package ran out of time before it finished.`, `Its work is already on this branch at commit ${context.resumeHead}. Continue from that work: read what is there, keep what is correct, and finish the package. Do not start over.`);
252
+ }
247
253
  if (rework) {
248
254
  // T55 row 9: a briefed repair never delivered, so "an independent review returned this
249
255
  // delivery" is false for it — the brief's own first line already says the opposite. Head it by
@@ -253,9 +259,6 @@ export function buildAssignmentPrompt(context) {
253
259
  : "REWORK FEEDBACK — an independent review returned this delivery; address every point";
254
260
  lines.push("", `${heading}\n${rework}`);
255
261
  }
256
- if (context.resumeHead) {
257
- lines.push("", `${RESUME_PROMPT_HEADING} — an earlier run of this same package ran out of time before it finished.`, `Its work is already on this branch at commit ${context.resumeHead}. Continue from that work: read what is there, keep what is correct, and finish the package. Do not start over.`);
258
- }
259
262
  const mustCommit = context.grants.includes("repo_write") && context.grants.includes("branch_create");
260
263
  const mustOpenPr = mustCommit && context.grants.includes("pr_create");
261
264
  const executionClass = context.executionClass
@@ -728,7 +731,7 @@ export function claudeCodeFuelEnv(fuelSource, model) {
728
731
  return undefined;
729
732
  return { ANTHROPIC_MODEL: model, ANTHROPIC_SMALL_FAST_MODEL: model, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" };
730
733
  }
731
- export function codexSandboxFacts(projection) {
734
+ export function codexSandboxFacts(projection, writableRoots = []) {
732
735
  return [
733
736
  "SANDBOX FACTS",
734
737
  `Sandbox mode: ${projection.sandbox}.`,
@@ -736,7 +739,9 @@ export function codexSandboxFacts(projection) {
736
739
  ? "Network is on."
737
740
  : "Network is off, including loopback listen.",
738
741
  projection.sandbox === "workspace-write"
739
- ? "Writes are allowed only in the workspace and TMPDIR. HOME and the rest of the filesystem are read-only."
742
+ ? writableRoots.length
743
+ ? `Writes are allowed only in the workspace, TMPDIR and ${writableRoots.join(", ")}. HOME and the rest of the filesystem are read-only.`
744
+ : "Writes are allowed only in the workspace and TMPDIR. HOME and the rest of the filesystem are read-only."
740
745
  : "No writes are allowed. The filesystem is read-only.",
741
746
  "Child processes inherit these limits.",
742
747
  "A registered gate that fails only with EPERM, EROFS, or a refused listen does not test the change.",
@@ -765,11 +770,22 @@ export function codexExecArgs(input, sandbox) {
765
770
  if (sandbox === "workspace-write" && networkAccess) {
766
771
  args.push("-c", "sandbox_workspace_write.network_access=true");
767
772
  }
773
+ if (sandbox === "workspace-write" && input.writableRoots?.length) {
774
+ // `exec resume` takes no --add-dir, so both forms set the roots as a config override.
775
+ args.push("-c", `sandbox_workspace_write.writable_roots=${JSON.stringify(input.writableRoots)}`);
776
+ }
768
777
  if (input.resumeSessionId)
769
778
  args.push(input.resumeSessionId);
770
779
  args.push("-");
771
780
  return args;
772
781
  }
782
+ /** The machine's Cargo cache and build directory that exist, for a Codex agent to write. */
783
+ export function codexCargoRoots(env) {
784
+ return ["CARGO_HOME", "CARGO_TARGET_DIR"].flatMap((name) => {
785
+ const path = env[name];
786
+ return path && existsSync(path) ? [path] : [];
787
+ });
788
+ }
773
789
  /**
774
790
  * Since the July 2026 desktop merge, macOS machines often carry the codex CLI
775
791
  * only inside the ChatGPT app bundle rather than on PATH.
@@ -866,8 +882,12 @@ export const codexDriver = {
866
882
  capabilities: input.capabilities,
867
883
  diagnosis: input.workRole === "diagnose",
868
884
  });
885
+ // The agent shares the gate's Cargo cache and build directory (boundedEnvironment). Codex
886
+ // workspace-write made them read-only, so no cargo command the agent started could run
887
+ // (acme-sim FH17a, 2026-11-04: EROFS on .cargo-lock); the gate sandbox already writes them.
888
+ const writableRoots = projected.sandbox === "workspace-write" ? codexCargoRoots(process.env) : [];
869
889
  // Prompt via stdin avoids ARG_MAX limits on large assignment contracts.
870
- const { code, stdout, stderr } = await execute(driverExecutable(codexDriver.name, input.executable), codexExecArgs({ ...input, networkAccess: projected.networkAccess }, projected.sandbox), input.workspace, agentTurnTimeoutMs(input), fuelSource === "conduit" ? input.fuel : undefined, fuelSource, `${input.prompt}\n\n${codexSandboxFacts(projected)}`, input.signal);
890
+ const { code, stdout, stderr } = await execute(driverExecutable(codexDriver.name, input.executable), codexExecArgs({ ...input, networkAccess: projected.networkAccess, writableRoots }, projected.sandbox), input.workspace, agentTurnTimeoutMs(input), fuelSource === "conduit" ? input.fuel : undefined, fuelSource, `${input.prompt}\n\n${codexSandboxFacts(projected, writableRoots)}`, input.signal);
871
891
  const parsed = parseCodexJsonl(stdout);
872
892
  const resultText = parsed.resultText ?? (stdout || null);
873
893
  if (code !== 0) {
@@ -63,16 +63,17 @@ async function currentBranch(workspace) {
63
63
  return stdout.trim();
64
64
  }
65
65
  /**
66
- * A run that spent Conduit's package budget keeps its work for the retry: commit the scoped files the
67
- * agent left uncommitted, push the attempt branch where the grants allow a push, and name the head.
68
- * Null when the attempt has nothing after base — the retry then starts fresh, as before.
66
+ * A run that spent Conduit's package budget or failed its gate keeps its work for the next attempt:
67
+ * commit the scoped files the agent left uncommitted, push the attempt branch where the grants allow
68
+ * a push, and name the head. Null when the attempt has nothing after base — the next attempt then
69
+ * starts fresh.
69
70
  */
70
71
  export async function checkpointWorkInProgress(input) {
71
72
  if (!requiresLandCommit({ grants: input.grants, spec: input.spec }))
72
73
  return null;
73
74
  const pending = filterPathsInScope(await uncommittedPaths(input.workspace), input.spec.change_scope ?? []);
74
75
  if (pending.length) {
75
- await commitScopedPaths(input.workspace, pending, "Conduit: work in progress at the package budget");
76
+ await commitScopedPaths(input.workspace, pending, "Conduit: work in progress for the next attempt");
76
77
  }
77
78
  const ahead = await commitsAheadOfBase(input.workspace, input.baseCommit);
78
79
  if (!ahead)
@@ -572,6 +572,11 @@ async function missingNpmLockReason(directory) {
572
572
  return "pnpm-lock.yaml is not an npm install";
573
573
  return "no package-lock.json or npm-shrinkwrap.json";
574
574
  }
575
+ /** Lock-pinned crates and npm packages, fetched outside the sandbox before a gate runs inside it. */
576
+ export async function prepareVerificationDependencies(workspace) {
577
+ await fetchLockedDependencies(workspace);
578
+ await installLockedPackages(workspace);
579
+ }
575
580
  export async function installLockedPackages(workspace, options = {}) {
576
581
  const installed = [];
577
582
  const skipped = [];
@@ -612,7 +617,19 @@ export async function installLockedPackages(workspace, options = {}) {
612
617
  * `output` is stdout and stderr together, in the order the lines arrived: a failure detail reads its
613
618
  * tail from it.
614
619
  */
615
- export async function runBoundedVerificationCommand(command, workspace, timeoutMs = VERIFICATION_TIMEOUT_MS) {
620
+ export function runBoundedVerificationCommand(command, workspace, timeoutMs = VERIFICATION_TIMEOUT_MS) {
621
+ return runVerificationCommand(command, workspace, timeoutMs, true);
622
+ }
623
+ /**
624
+ * The same run without the dependency preparation, for a caller that is itself inside the sandbox
625
+ * (the delivered probe instrument). It has no network, so its own install would fail and leave a
626
+ * partial `node_modules` that hides the complete one. Bridge prepares that workspace before it
627
+ * starts the caller (CR31).
628
+ */
629
+ export function runPreparedVerificationCommand(command, workspace, timeoutMs = VERIFICATION_TIMEOUT_MS) {
630
+ return runVerificationCommand(command, workspace, timeoutMs, false);
631
+ }
632
+ async function runVerificationCommand(command, workspace, timeoutMs, prepare) {
616
633
  const argv = argvForBoundedCommand(command);
617
634
  const bin = argv[0];
618
635
  if (!bin)
@@ -670,8 +687,8 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
670
687
  partial[stream].append(parts.at(-1));
671
688
  };
672
689
  const output = () => [lines.text().replace(/\n$/, ""), partial.stdout.text(), partial.stderr.text()].filter(Boolean).join("\n");
673
- await fetchLockedDependencies(workspace);
674
- await installLockedPackages(workspace);
690
+ if (prepare)
691
+ await prepareVerificationDependencies(workspace);
675
692
  const tempRoot = untrustedCodeTempRoot(workspace);
676
693
  await mkdir(tempRoot, { recursive: true });
677
694
  const scratchHome = await mkdtemp(join(tempRoot, "gate-home-"));
package/dist/execution.js CHANGED
@@ -1734,16 +1734,17 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1734
1734
  clearInterval(heartbeatTimer);
1735
1735
  };
1736
1736
  attemptDisposers.set(taskId, disposeTimers);
1737
- // A run that spent Conduit's package budget keeps its work: the control plane retries it once, and
1738
- // the retry resumes from the head reported here instead of starting over at the base. The tree
1739
- // stays on this computer, where that head is always reachable.
1737
+ // A run that spent Conduit's package budget or failed its gate keeps its work: the next attempt
1738
+ // resumes from the head reported here instead of starting over at the base. The tree stays on this
1739
+ // computer, where that head is always reachable.
1740
+ const checkpointForNextAttempt = () => diagnosis ? Promise.resolve(null) : checkpointWorkInProgress({
1741
+ workspace: attemptWorkspace, spec, grants, baseCommit: deliveryBaseCommit,
1742
+ }).catch((error) => {
1743
+ console.error(`Work in progress was not committed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
1744
+ return null;
1745
+ });
1740
1746
  const reportTurnTimeout = async (message) => {
1741
- const resume = diagnosis || machineBoundTimeout(agentTimeout) ? null : await checkpointWorkInProgress({
1742
- workspace: attemptWorkspace, spec, grants, baseCommit: deliveryBaseCommit,
1743
- }).catch((error) => {
1744
- console.error(`Work in progress was not committed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`);
1745
- return null;
1746
- });
1747
+ const resume = machineBoundTimeout(agentTimeout) ? null : await checkpointForNextAttempt();
1747
1748
  retainAttemptWorktree = true;
1748
1749
  const response = await queueTerminal(client, taskId, {
1749
1750
  action: "fail",
@@ -1829,9 +1830,12 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1829
1830
  ? { ...result.signal, phase: "verification", witness: "bridge", kind: "verification_failed", command: capturedVerification.command, stderr_tail: undefined, stdout_tail: undefined, ...(diffStat ? { diff_stat: diffStat } : {}) }
1830
1831
  : result.signal
1831
1832
  : undefined;
1833
+ // A repair brief describes a fix to this work, so the briefed attempt must start from it.
1834
+ const resume = verificationDetail ? await checkpointForNextAttempt() : null;
1832
1835
  retainAttemptWorktree = true;
1833
1836
  const response = await queueTerminal(client, taskId, { action: "fail", body: {
1834
1837
  error: message,
1838
+ ...(resume ? { resume } : {}),
1835
1839
  retryable: bindingRequired ? false : retryableAgentFailure(agentMessage),
1836
1840
  // Bridge's own readings of the vendor's words name the cause, in the order the control plane
1837
1841
  // always applied: a computer fact (sign-in, incompatible or missing CLI), then a model abort,
@@ -2490,12 +2494,15 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2490
2494
  // classifyFinalizeFailure: forge transport → retryable; contract/environment/unknown → fail
2491
2495
  // closed (unknown prefixed as "Bridge finalize interrupted", not laundered as contract).
2492
2496
  : classifyFinalizeFailure(message);
2497
+ // A repair brief describes a fix to this work, so the briefed attempt must start from it.
2498
+ const resume = verificationFailed ? await checkpointForNextAttempt() : null;
2493
2499
  retainAttemptWorktree = true;
2494
2500
  const response = await queueTerminal(client, taskId, {
2495
2501
  action: "fail",
2496
2502
  body: {
2497
2503
  error: classified.error,
2498
2504
  retryable: classified.retryable,
2505
+ ...(resume ? { resume } : {}),
2499
2506
  ...(!verificationFailed && classified.failure ? { failure: classified.failure } : {}),
2500
2507
  ...(verificationFailed ? { signal: {
2501
2508
  phase: "verification",
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { redactSecrets, redactSecretsDeep } from "./secret-redaction.js";
9
9
  import { z } from "zod";
10
- import { boundedTail, runBoundedVerificationCommand, VERIFICATION_TIMEOUT_MS } from "./ensure-test-evidence.js";
10
+ import { boundedTail, prepareVerificationDependencies, runBoundedVerificationCommand, VERIFICATION_TIMEOUT_MS } from "./ensure-test-evidence.js";
11
11
  import { markAttemptActive } from "./preflight.js";
12
12
  import { createAttemptWorktree, removeAttemptWorktree } from "./attempt-worktree.js";
13
13
  import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl } from "./brief.js";
@@ -690,6 +690,10 @@ async function execDeliveredProbeRun(argv, stdin, workspace, timeoutMs) {
690
690
  }
691
691
  }
692
692
  export async function runDeliveredProbeInstrument(input) {
693
+ // The delivered instrument runs inside the sandbox and cannot install; its registered commands
694
+ // read what Bridge installs here, outside it (CR31).
695
+ if (input.probes.some((probe) => probe.test?.trim()))
696
+ await (input.prepare ?? prepareVerificationDependencies)(input.workspace);
693
697
  const entry = join(input.workspace, PROBE_RUN_ENTRY);
694
698
  const timeoutMs = deliveredProbeTimeoutMs(input.probes);
695
699
  const stdin = JSON.stringify({ probes: input.probes });
package/dist/probe-run.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /** Delivered probe instrument entry. `{ "probes": [...] }` on stdin, `{ witnesses }` on stdout. */
2
2
  import { z } from "zod";
3
+ import { runPreparedVerificationCommand } from "./ensure-test-evidence.js";
3
4
  import { runDeliveryVerificationProbes } from "./investigation.js";
4
5
  const probeRunInputSchema = z.object({
5
6
  probes: z.array(z.object({
@@ -19,7 +20,12 @@ async function readStdin() {
19
20
  }
20
21
  try {
21
22
  const parsed = probeRunInputSchema.parse(JSON.parse(await readStdin()));
22
- const witnesses = await runDeliveryVerificationProbes({ workspace: process.cwd(), probes: parsed.probes });
23
+ // This process is inside the sandbox; Bridge prepared the workspace's dependencies before it started.
24
+ const witnesses = await runDeliveryVerificationProbes({
25
+ workspace: process.cwd(),
26
+ probes: parsed.probes,
27
+ runCommand: (command, workspace) => runPreparedVerificationCommand(command, workspace),
28
+ });
23
29
  process.stdout.write(`${JSON.stringify({ witnesses })}\n`);
24
30
  }
25
31
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.207",
3
+ "version": "0.16.210",
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": {