@miraland-labs/conduit-bridge 0.16.141 → 0.16.143

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.125` — the workspace brief reports `declared_demonstration` so the planner knows whether the repository can show itself (KERNEL.md §51). Earlier (0.16.124) — when a project declares `.conduit/demonstrate`, Bridge runs that bounded command at the delivered head and attaches a stamped `preview` row (KERNEL.md §47). Earlier (0.16.95) — Conduit-fuel lanes bind organization aliases (T144), the Anthropic base URL is the origin (T148), the investigation lane resolves its model (T154); a hosted container image lives under `hosted/` (docs/HOSTED_RUNNER.md). Earlier (0.16.27) — work-package execution budgets reach every agent turn, are bounded by the machine ceiling, and expose their effective source for operations. 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`).
5
+ **Package version:** `0.16.125` — the workspace brief reports `declared_demonstration` so the planner knows whether the repository can show itself (docs/historical/KERNEL_HISTORY_2026-09.md §51). Earlier (0.16.124) — when a project declares `.conduit/demonstrate`, Bridge runs that bounded command at the delivered head and attaches a stamped `preview` row (docs/historical/KERNEL_HISTORY_2026-09.md §47). Earlier (0.16.95) — Conduit-fuel lanes bind organization aliases (T144), the Anthropic base URL is the origin (T148), the investigation lane resolves its model (T154); a hosted container image lives under `hosted/` (docs/HOSTED_RUNNER.md). Earlier (0.16.27) — work-package execution budgets reach every agent turn, are bounded by the machine ceiling, and expose their effective source for operations. 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
 
@@ -2,9 +2,9 @@
2
2
  * F-07 slice 1: one Git worktree per attempt. The install-service source checkout is never the
3
3
  * agent cwd; retries must not inherit dirty files from a failed run.
4
4
  */
5
- import { access, appendFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
5
+ import { access, appendFile, mkdir, readFile, rm } from "node:fs/promises";
6
6
  import { constants } from "node:fs";
7
- import { join, resolve } from "node:path";
7
+ import { basename, join, resolve } from "node:path";
8
8
  import { execFile } from "node:child_process";
9
9
  import { promisify } from "node:util";
10
10
  import { headCommitOrNull } from "./git.js";
@@ -106,6 +106,16 @@ export async function removeAttemptWorktree(sourceWorkspace, worktreePath) {
106
106
  maxBuffer: 1_000_000,
107
107
  }).catch(() => undefined);
108
108
  }
109
+ // The attempt branch goes with its worktree: left behind, every attempt added one local branch to
110
+ // the source checkout for ever. Only the branch Bridge created for this path, and only the local
111
+ // ref: a delivered branch was pushed before the release and lives on in the forge.
112
+ const attemptId = basename(worktreePath);
113
+ if (resolve(worktreePath) === resolve(attemptWorktreePath(sourceWorkspace, attemptId))) {
114
+ await execFileAsync("git", ["-C", sourceWorkspace, "branch", "-D", attemptBranchName(attemptId)], {
115
+ timeout: 30_000,
116
+ maxBuffer: 1_000_000,
117
+ }).catch(() => undefined);
118
+ }
109
119
  return true;
110
120
  }
111
121
  /**
@@ -126,28 +136,3 @@ export async function proveResumeWorktree(sourceWorkspace, attemptId, worktreePa
126
136
  return false;
127
137
  }
128
138
  }
129
- /** Hold a crashed attempt tree for local diagnosis; never reused as agent cwd. */
130
- export async function quarantineAttemptWorktree(sourceWorkspace, worktreePath, attemptId) {
131
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
132
- const dest = join(sourceWorkspace, ".conduit", "quarantine", `${attemptId}-${stamp}`);
133
- await mkdir(join(sourceWorkspace, ".conduit", "quarantine"), { recursive: true });
134
- try {
135
- await execFileAsync("git", ["-C", sourceWorkspace, "worktree", "remove", "--force", worktreePath], { timeout: 60_000, maxBuffer: 2_000_000 });
136
- return null;
137
- }
138
- catch {
139
- try {
140
- await rename(worktreePath, dest);
141
- await execFileAsync("git", ["-C", sourceWorkspace, "worktree", "prune"], {
142
- timeout: 30_000,
143
- maxBuffer: 1_000_000,
144
- }).catch(() => undefined);
145
- await writeFile(join(dest, ".conduit-quarantine.json"), `${JSON.stringify({ attemptId, at: stamp })}\n`, "utf8");
146
- return dest;
147
- }
148
- catch {
149
- await removeAttemptWorktree(sourceWorkspace, worktreePath);
150
- return null;
151
- }
152
- }
153
- }
package/dist/config.js CHANGED
@@ -58,8 +58,6 @@ export async function loadConfigIfPresent() {
58
58
  // Claims live only in runtime.json; a fresh install has none.
59
59
  config.activeAttempts = runtime?.activeAttempts ?? {};
60
60
  config.sessions = runtime?.sessions;
61
- for (const active of Object.values(config.activeAttempts))
62
- active.phase ??= "agent_running";
63
61
  if (config.fuelSource !== "local" && config.fuelSource !== "conduit")
64
62
  config.fuelSource = "conduit";
65
63
  // Clamp to what concurrent slot workers can run (1..BRIDGE_MAX_LEASE_CAPACITY).
package/dist/driver.js CHANGED
@@ -152,11 +152,6 @@ export function tierForRisk(risk) {
152
152
  export function isSafeModelName(value) {
153
153
  return /^[A-Za-z0-9][A-Za-z0-9._/-]{0,99}$/.test(value);
154
154
  }
155
- /** Grants → Claude Code tool allow-list (mutate_repo mapping). Privileged grants are never mapped. */
156
- export function claudeToolsForGrants(grants, verificationCommands = [], capabilities = []) {
157
- return projectClaude("mutate_repo", { grants, verificationCommands, capabilities }).allowedTools;
158
- }
159
- export const claudeDeniedTools = deniedCommands.map((command) => `Bash(${command}:*)`);
160
155
  function resolveRunClass(input) {
161
156
  if (input.executionClass)
162
157
  return input.executionClass;
@@ -739,7 +734,9 @@ export const claudeCodeDriver = {
739
734
  };
740
735
  }
741
736
  const maxTurns = resolveMaxTurns(input.maxTurns);
742
- const args = ["-p", input.prompt, "--output-format", "json", "--max-turns", String(maxTurns)];
737
+ // The prompt goes on stdin (`claude -p` reads it there when no prompt argument is given): Linux
738
+ // caps one argument at 128 KiB (MAX_ARG_STRLEN) and an assignment contract exceeds that.
739
+ const args = ["-p", "--output-format", "json", "--max-turns", String(maxTurns)];
743
740
  args.push(...modelArgs(claudeCodeDriver, input.model));
744
741
  if (input.resumeSessionId)
745
742
  args.push("--resume", input.resumeSessionId);
@@ -747,7 +744,7 @@ export const claudeCodeDriver = {
747
744
  args.push("--disallowedTools", projected.disallowedTools.join(","));
748
745
  if (projected.acceptEdits)
749
746
  args.push("--permission-mode", "acceptEdits");
750
- const { code, stdout, stderr } = await execute(driverExecutable(claudeCodeDriver.name, input.executable), args, input.workspace, agentTurnTimeoutMs(input), fuelSource === "conduit" ? input.fuel : undefined, fuelSource, undefined, input.signal, claudeCodeFuelEnv(fuelSource, input.model));
747
+ const { code, stdout, stderr } = await execute(driverExecutable(claudeCodeDriver.name, input.executable), args, input.workspace, agentTurnTimeoutMs(input), fuelSource === "conduit" ? input.fuel : undefined, fuelSource, input.prompt, input.signal, claudeCodeFuelEnv(fuelSource, input.model));
751
748
  let message = null;
752
749
  try {
753
750
  message = JSON.parse(stdout);
@@ -914,21 +911,6 @@ export const codexDriver = {
914
911
  return { status: "completed", resultText, sessionId: parsed.sessionId, usage: parsed.usage };
915
912
  },
916
913
  };
917
- /**
918
- * Grants + capabilities → Cursor CLI permissions via ExecutionClass.
919
- * Current Cursor Agent schema accepts only `permissions.{allow,deny}` (no
920
- * `version` / `approvalMode`). Outside the allow list is denied without --force.
921
- *
922
- * `observe_network` / networked mutate / publish_artifact use `--force` (see
923
- * cursorRunArgs). Field evidence: allow-listing WebFetch alone still yields
924
- * "User Rejected" under headless `-p`; `--force` is required for fetch and for
925
- * publish shell under open class policy. Research-only networked assignments
926
- * deny Shell(*) and Write(**).
927
- */
928
- export function cursorPermissionsForGrants(grants, verificationCommands, capabilities, executionClass) {
929
- const { allow, deny } = projectCursor(executionClass, { grants, verificationCommands, capabilities });
930
- return { allow, deny };
931
- }
932
914
  /**
933
915
  * Cursor CLI arguments. Deliberately no `--mode plan` for read-only assignments: plan mode is not an
934
916
  * enforcement boundary (in headless `-p` mode the agent can self-escalate out of it and edit anyway)
@@ -1229,9 +1211,9 @@ export const openCodeDriver = {
1229
1211
  if (!agent) {
1230
1212
  return { status: "failed", resultText: null, sessionId: null, error: "No Bridge-mapped OpenCode agent for active grants; refusing to start agent" };
1231
1213
  }
1232
- const args = openCodeRunArgs(input, agent);
1233
- args.push(input.prompt);
1234
- const { code, stdout, stderr } = await withOpenCodePermissions(input.workspace, () => execute(driverExecutable(openCodeDriver.name, input.executable), args, input.workspace, agentTurnTimeoutMs(input), fuelSource === "conduit" ? input.fuel : undefined, fuelSource, undefined, input.signal));
1214
+ // The prompt goes on stdin (`opencode run` reads a piped stdin as the message), as it does for
1215
+ // codex: Linux caps one argument at 128 KiB (MAX_ARG_STRLEN).
1216
+ const { code, stdout, stderr } = await withOpenCodePermissions(input.workspace, () => execute(driverExecutable(openCodeDriver.name, input.executable), openCodeRunArgs(input, agent), input.workspace, agentTurnTimeoutMs(input), fuelSource === "conduit" ? input.fuel : undefined, fuelSource, input.prompt, input.signal));
1235
1217
  const parsed = parseOpenCodeOutput(stdout);
1236
1218
  if (code !== 0 || parsed.isError) {
1237
1219
  return { status: "failed", resultText: parsed.resultText, sessionId: parsed.sessionId, signal: agentExitSignal({ exit_code: code, stderr, stdout, model: input.model }), error: boundedTail(stderr || parsed.resultText || `opencode exited with code ${code}`, 20_000) };
@@ -1313,9 +1295,9 @@ export const kiroDriver = {
1313
1295
  return { status: "failed", resultText: null, sessionId: null, error: "kiro has no login (set KIRO_API_KEY or run `kiro-cli login`)" };
1314
1296
  }
1315
1297
  }
1316
- const args = kiroChatArgs(input, trusted);
1317
- args.push(input.prompt);
1318
- const { code, stdout, stderr } = await execute(executable, args, input.workspace, agentTurnTimeoutMs(input), undefined, "local", undefined, input.signal);
1298
+ // The prompt goes on stdin (`kiro-cli chat` takes a piped stdin as its input), as it does for
1299
+ // codex: Linux caps one argument at 128 KiB (MAX_ARG_STRLEN).
1300
+ const { code, stdout, stderr } = await execute(executable, kiroChatArgs(input, trusted), input.workspace, agentTurnTimeoutMs(input), undefined, "local", input.prompt, input.signal);
1319
1301
  if (code !== 0) {
1320
1302
  return { status: "failed", resultText: stdout || null, sessionId: null, signal: agentExitSignal({ exit_code: code, stderr, stdout, model: input.model }), error: boundedTail(stderr || stdout || `kiro-cli exited with code ${code}`, 20_000) };
1321
1303
  }
@@ -1638,7 +1620,8 @@ export const antigravityDriver = {
1638
1620
  export function piRunArgs(input) {
1639
1621
  const args = ["--mode", "json", "-p", "--no-session", "--tools", input.tools];
1640
1622
  args.push(...modelArgs(piDriver, input.model));
1641
- args.push(input.prompt);
1623
+ // The prompt goes on stdin (pi reads a piped stdin as its initial message), as it does for codex:
1624
+ // Linux caps one argument at 128 KiB (MAX_ARG_STRLEN).
1642
1625
  return args;
1643
1626
  }
1644
1627
  /**
@@ -1703,7 +1686,7 @@ export const piDriver = {
1703
1686
  if (version.code !== 0 || !(version.stdout || version.stderr).trim()) {
1704
1687
  return { status: "failed", resultText: null, sessionId: null, error: "pi preflight could not verify the installed CLI version" };
1705
1688
  }
1706
- const { code, stdout, stderr } = await execute(executable, piRunArgs({ prompt: input.prompt, tools: projected.tools, model: input.model }), input.workspace, agentTurnTimeoutMs(input), undefined, "local", undefined, input.signal);
1689
+ const { code, stdout, stderr } = await execute(executable, piRunArgs({ tools: projected.tools, model: input.model }), input.workspace, agentTurnTimeoutMs(input), undefined, "local", input.prompt, input.signal);
1707
1690
  const parsed = parsePiJsonl(stdout);
1708
1691
  if (code !== 0) {
1709
1692
  return {
@@ -1894,12 +1877,14 @@ const MAX_AGENT_OUTPUT = 8 * 1024 * 1024;
1894
1877
  export const STDIO_DRAIN_MS = 2_000;
1895
1878
  /** How long a stopped child may take to die before the run reports what it already has. */
1896
1879
  const CHILD_STOP_GRACE_MS = 20_000;
1897
- export function execute(executable, args, cwd, timeoutMs, fuel, fuelSource = "conduit", stdinText, signal, extraEnv) {
1880
+ export function execute(executable, args, cwd, timeoutMs, fuel, fuelSource = "conduit", stdinText, signal, extraEnv,
1881
+ /** The whole environment, for a run that must see less than an agent does (a probe instrument). */
1882
+ env) {
1898
1883
  return new Promise((resolve, reject) => {
1899
1884
  const child = spawn(executable, args, {
1900
1885
  cwd,
1901
1886
  stdio: [stdinText !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
1902
- env: boundedEnvironment(fuel, fuelSource, extraEnv),
1887
+ env: env ?? boundedEnvironment(fuel, fuelSource, extraEnv),
1903
1888
  });
1904
1889
  let stdout = "";
1905
1890
  let stderr = "";
@@ -1991,7 +1976,8 @@ const LOCAL_VENDOR_ENV = [
1991
1976
  "CURSOR_API_KEY", "KIRO_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY",
1992
1977
  "XAI_API_KEY",
1993
1978
  ];
1994
- function boundedEnvironment(fuel, fuelSource = "conduit", extraEnv) {
1979
+ /** The stripped process environment an agent CLI runs with, carrying Conduit fuel when given. */
1980
+ export function boundedEnvironment(fuel, fuelSource = "conduit", extraEnv) {
1995
1981
  // Proxy and TLS variables stay: fueled agents must reach Conduit's /v1 on
1996
1982
  // machines that only have network access through a proxy.
1997
1983
  const allowed = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "TERM", "NO_COLOR", "FORCE_COLOR",
@@ -2066,7 +2052,3 @@ export function hasGrokLogin(env = process.env) {
2066
2052
  // `~/.grok` exists after install; auth.json is the `grok login` / cached_token file.
2067
2053
  return Boolean(home && existsSync(join(home, ".grok", "auth.json")));
2068
2054
  }
2069
- /** Exported for tests — builds the stripped process env with optional Conduit fuel. */
2070
- export function agentProcessEnv(fuel, fuelSource = "conduit", extraEnv) {
2071
- return boundedEnvironment(fuel, fuelSource, extraEnv);
2072
- }
package/dist/drivers.js CHANGED
@@ -400,15 +400,6 @@ export function setDriverFuel(config, driverId, fuel) {
400
400
  export function heartbeatStatusForDrivers(onlineIds) {
401
401
  return onlineIds.length > 0 ? "online" : "standby";
402
402
  }
403
- /**
404
- * Effective capacity advertised on heartbeat: full approved pool when any lane is online;
405
- * 0 (via standby + no claims) when none are. Caller still sends lease_capacity = approved when online.
406
- */
407
- export function advertiseLeaseCapacity(approved, onlineIds) {
408
- if (onlineIds.length === 0)
409
- return 1; // schema/heartbeat min; status standby prevents matching
410
- return approved;
411
- }
412
403
  export function driversHeartbeatReport(config, activeAttempts, processOnlineIds, env = process.env) {
413
404
  const processOnline = new Set(processOnlineIds ?? []);
414
405
  const busyByDriver = new Map();
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * When the project declares how it shows itself, Bridge runs that command at the delivered head and
3
- * attaches what it printed. The agent never authors this entry (KERNEL.md §47, "See it").
3
+ * attaches what it printed. The agent never authors this entry (docs/historical/KERNEL_HISTORY_2026-09.md §47, "See it").
4
4
  *
5
5
  * `.conduit/demonstrate` is one bounded command, `#` comments allowed, absent by default. It runs
6
6
  * with the same shape check, runner and bound as a verification command, and it is never a gate:
@@ -319,8 +319,11 @@ export function boundedFailureDetail(command, stdoutLines, stderrLines, code, ma
319
319
  /**
320
320
  * The bounded runner every Bridge-run verification uses: the declared command, in the given
321
321
  * workspace, with the verification timeout and buffer. A non-zero exit is a result, not a throw.
322
+ *
323
+ * A gate the timeout stopped did not exit: `timedOut` says so, and stderr ends with the limit it
324
+ * reached. Read as exit 1, a gate that hung on the untouched base was a red base on the author.
322
325
  */
323
- export async function runBoundedVerificationCommand(command, workspace) {
326
+ export async function runBoundedVerificationCommand(command, workspace, timeoutMs = VERIFICATION_TIMEOUT_MS) {
324
327
  const argv = argvForBoundedCommand(command);
325
328
  const bin = argv[0];
326
329
  if (!bin)
@@ -328,7 +331,7 @@ export async function runBoundedVerificationCommand(command, workspace) {
328
331
  try {
329
332
  const { stdout, stderr } = await execFileAsync(bin, argv.slice(1), {
330
333
  cwd: workspace,
331
- timeout: VERIFICATION_TIMEOUT_MS,
334
+ timeout: timeoutMs,
332
335
  maxBuffer: VERIFICATION_MAX_BUFFER_BYTES,
333
336
  env: process.env,
334
337
  });
@@ -343,6 +346,14 @@ export async function runBoundedVerificationCommand(command, workspace) {
343
346
  // message reaches the `enoent` rule that Holds it on the computer operator.
344
347
  if (typeof err.code === "string")
345
348
  throw error;
349
+ if (err.killed === true) {
350
+ return {
351
+ stdout: String(err.stdout ?? ""),
352
+ stderr: `${String(err.stderr ?? "")}\nBridge stopped the gate: it did not exit within ${Math.round(timeoutMs / 1_000)} seconds.`.trimStart(),
353
+ code: 1,
354
+ timedOut: true,
355
+ };
356
+ }
346
357
  const stderr = String(err.stderr || err.message || "verification failed");
347
358
  return {
348
359
  stdout: String(err.stdout ?? ""),
@@ -456,6 +467,18 @@ export function detectGateRanNothing(command, stdout, stderr, code) {
456
467
  }
457
468
  return null;
458
469
  }
470
+ /**
471
+ * A delivery report that falls short of the delivery contract while the work itself may be
472
+ * complete: a required evidence kind is missing, a met criterion cites no evidence, test evidence is
473
+ * a summary, or the pull request or published artifact is missing. The type, not the sentence, is
474
+ * what tells the control plane this is a report defect for repair.
475
+ */
476
+ export class DeliveryReportDefectError extends Error {
477
+ constructor(message) {
478
+ super(message);
479
+ this.name = "DeliveryReportDefectError";
480
+ }
481
+ }
459
482
  /**
460
483
  * A declared gate that ran and failed. Carries the command as a field so the verification signal
461
484
  * can name the gate without reading it back out of this error's text.
@@ -501,7 +524,7 @@ export async function ensureTestEvidence(input) {
501
524
  ...named.commands,
502
525
  ])];
503
526
  if (commands.length === 0) {
504
- throw new Error("Agent report is missing required evidence: test (no bounded verification command available for Bridge to run)");
527
+ throw new DeliveryReportDefectError("Agent report is missing required evidence: test (no bounded verification command available for Bridge to run)");
505
528
  }
506
529
  const run = input.runCommand ?? runBoundedVerificationCommand;
507
530
  const witnessed = [];
package/dist/execution.js CHANGED
@@ -11,12 +11,12 @@ import { saveDriverOutcome, saveDriverQuota, tierCandidates } from "./config.js"
11
11
  import { DRIVERS, acceptanceCriteriaLines, acceptanceReportSkeleton, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, isSafeModelName, driverExecutable, requireStampedExecutionClass, tierForRisk } from "./driver.js";
12
12
  import { assertClassFloor } from "./execution-class.js";
13
13
  import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, resolveDriverFuelProvenance, supportsReadOnlyDiagnosis, clearDriverOutcome, recordDriverTimeout } from "./drivers.js";
14
- import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
14
+ import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
15
15
  import { execFile } from "node:child_process";
16
16
  import { promisify } from "node:util";
17
17
  import { buildWorkspaceBrief, ensureCommitAvailable, fetchOrigin, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
18
18
  import { buildSovereignExecutionFacts, bootstrapResultForWorkspace, changedExecutionFact, resumeExecutionFacts, workspaceIsClean } from "./execution-facts.js";
19
- import { agentOutputCause, boundedDetail, bridgeCause, contractScopeConflictFailure, executionFactChangedSignal, verificationRedBaseSignal, verificationScopeConflictSignal } from "./failure-signal.js";
19
+ import { agentOutputCause, boundedDetail, bridgeCause, contractScopeConflictFailure, executionFactChangedSignal, verificationGateTimeoutSignal, verificationRedBaseSignal, verificationScopeConflictSignal } from "./failure-signal.js";
20
20
  import { forgeTransportFailure } from "./transport-fault.js";
21
21
  import { managedWorkspacePath, switchManagedWorkspace } from "./on-shift-apply.js";
22
22
  import { formatResetMinute, parseQuotaResetAt } from "./quota-reset.js";
@@ -28,7 +28,7 @@ import { ensureLandCommit } from "./ensure-land-commit.js";
28
28
  import { AGENT_NO_LAND_COMMIT_PREFIX, AgentNoLandCommitError, agentNoLandCommitMessage, requiresLandCommit } from "./land-contract.js";
29
29
  import { agentClaimsRepositoryWork, claimsVsGitMismatch, isLandTreeEmpty, reportClaimsRepositoryWork, } from "./land-git-reality.js";
30
30
  const execFileAsync = promisify(execFile);
31
- import { boundedTail, captureVerificationFailure, detectGateRanNothing, ensureTestEvidence, needsTestEvidence, runBoundedVerificationCommand, verificationEvidenceCommands, VerificationFailedError, VerificationGateRanNothingError } from "./ensure-test-evidence.js";
31
+ import { boundedTail, captureVerificationFailure, DeliveryReportDefectError, detectGateRanNothing, ensureTestEvidence, needsTestEvidence, runBoundedVerificationCommand, verificationEvidenceCommands, VerificationFailedError, VerificationGateRanNothingError, VERIFICATION_TIMEOUT_MS } from "./ensure-test-evidence.js";
32
32
  import { ensureChangeEvidence } from "./ensure-change-evidence.js";
33
33
  import { ensureDemonstrationEvidence } from "./ensure-demonstration-evidence.js";
34
34
  import { ensureNormativeEvidence } from "./ensure-normative-evidence.js";
@@ -534,7 +534,17 @@ export function buildDiagnosticPrompt(input) {
534
534
  * control plane has ended.
535
535
  */
536
536
  const slotAborts = new Map();
537
- export async function renewLeases(client, config) {
537
+ /**
538
+ * The renewal pass in flight. Each running slot and the runner loop all ask for a pass; one pass
539
+ * covers every attempt, so a caller that arrives while one runs waits for it instead of renewing
540
+ * the same leases and dropping the same attempts a second time.
541
+ */
542
+ let renewalPass = null;
543
+ export function renewLeases(client, config) {
544
+ renewalPass ??= renewEachLease(client, config).finally(() => { renewalPass = null; });
545
+ return renewalPass;
546
+ }
547
+ async function renewEachLease(client, config) {
538
548
  for (const active of Object.values(config.activeAttempts)) {
539
549
  if (Date.parse(active.leaseExpiresAt) - Date.now() < 120_000) {
540
550
  try {
@@ -633,14 +643,8 @@ function checkpointInstructionPrompt(instructions) {
633
643
  agentReportTemplate,
634
644
  ].join("\n");
635
645
  }
636
- function resolveAttemptDriver(config, active, fallback) {
637
- if (active.driverId && DRIVERS[active.driverId])
638
- return DRIVERS[active.driverId];
639
- if (fallback)
640
- return fallback;
641
- // Crash between claim and driverId persist: pick any free online lane so recovery is not stranded.
642
- const picked = pickDriverForClaim(config);
643
- return picked ? DRIVERS[picked] ?? null : null;
646
+ function resolveAttemptDriver(active, fallback) {
647
+ return (active.driverId ? DRIVERS[active.driverId] : undefined) ?? fallback ?? null;
644
648
  }
645
649
  export async function recoverActiveAttempt(client, config, driver, sourceWorkspace, brief, timeoutMs, supervision, taskId) {
646
650
  const active = taskId ? config.activeAttempts[taskId] : Object.values(config.activeAttempts)[0];
@@ -660,25 +664,15 @@ export async function recoverActiveAttempt(client, config, driver, sourceWorkspa
660
664
  await removeAttemptWorktree(workspace, worktree).catch(() => undefined);
661
665
  }
662
666
  else if (!diagnosis && response.retain_worktree !== true && worktree) {
663
- if (active.terminal?.action === "fail") {
664
- await quarantineAttemptWorktree(workspace, worktree, active.attemptId).catch(() => removeAttemptWorktree(workspace, worktree).catch(() => undefined));
665
- }
666
- else {
667
- await removeAttemptWorktree(workspace, worktree).catch(() => undefined);
668
- }
667
+ await removeAttemptWorktree(workspace, worktree).catch(() => undefined);
669
668
  }
670
669
  return true;
671
670
  }
672
- const laneDriver = resolveAttemptDriver(config, active, driver);
671
+ const laneDriver = resolveAttemptDriver(active, driver);
673
672
  if (!laneDriver) {
674
673
  console.error(`No driver for attempt ${active.attemptId} (driverId=${active.driverId ?? "unset"})`);
675
674
  return false;
676
675
  }
677
- if (!active.driverId) {
678
- const assigned = Object.entries(DRIVERS).find(([, d]) => d === laneDriver)?.[0];
679
- if (assigned)
680
- await client.updateAttempt(active.taskId, { driverId: assigned });
681
- }
682
676
  if (active.phase === "agent_running") {
683
677
  const diagnosis = active.executionKind === "diagnosis";
684
678
  const worktreeOwner = diagnosis ? active.sourceAttemptId : active.attemptId;
@@ -706,9 +700,9 @@ export async function recoverActiveAttempt(client, config, driver, sourceWorkspa
706
700
  await removeAttemptWorktree(workspace, worktree).catch(() => undefined);
707
701
  }
708
702
  else if (!diagnosis && response.retain_worktree !== true && worktree) {
709
- // Do not quarantine before the terminal response: Conduit may need this exact tree for a
703
+ // Do not remove before the terminal response: Conduit may need this exact tree for a
710
704
  // pinned diagnosis even when Bridge restarted before it could preserve the agent session.
711
- await quarantineAttemptWorktree(workspace, worktree, active.attemptId).catch(() => removeAttemptWorktree(workspace, worktree).catch(() => undefined));
705
+ await removeAttemptWorktree(workspace, worktree).catch(() => undefined);
712
706
  }
713
707
  return true;
714
708
  }
@@ -752,7 +746,7 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
752
746
  const active = config.activeAttempts[id];
753
747
  const laneDriver = active.phase === "terminal_pending"
754
748
  ? fallbackDriver
755
- : resolveAttemptDriver(config, active, fallbackDriver);
749
+ : resolveAttemptDriver(active, fallbackDriver);
756
750
  if (!laneDriver && active.phase !== "terminal_pending") {
757
751
  console.error(`Slot recovery skipped for ${id}: no driver lane`);
758
752
  continue;
@@ -799,7 +793,7 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
799
793
  break;
800
794
  const taskId = claimed.taskId;
801
795
  driverId = claimed.driverId ?? driverId;
802
- laneDriver = driverId ? DRIVERS[driverId] ?? laneDriver : laneDriver;
796
+ laneDriver = fallbackDriver ?? DRIVERS[driverId] ?? laneDriver;
803
797
  progressed = true;
804
798
  console.log(`Executing ${taskId} via ${laneDriver.name}`);
805
799
  // T86: the claim answers with the checkout it proved, which is this assignment's repository.
@@ -945,27 +939,6 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
945
939
  });
946
940
  return { taskId: assignment.id, driverId: selectedDriverId, workspace: claimWorkspace };
947
941
  }
948
- export function claimedAssignmentDriver(fallback, driverId) {
949
- if (!driverId)
950
- return fallback;
951
- const selected = DRIVERS[driverId];
952
- if (!selected)
953
- throw new Error(`Claimed assignment selected unavailable driver ${driverId}`);
954
- return selected;
955
- }
956
- export async function executeNextAssignment(client, config, driver, workspace, brief, timeoutMs, supervision) {
957
- if (Object.keys(config.activeAttempts).length)
958
- return recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision);
959
- const preferredDriverId = Object.entries(DRIVERS).find(([, candidate]) => candidate === driver)?.[0] ?? null;
960
- const claimed = await claimNextAssignment(client, config, workspace, brief, preferredDriverId, null, {
961
- beforeClaim: supervision?.beforeClaim,
962
- });
963
- if (!claimed)
964
- return false;
965
- const selectedDriver = claimedAssignmentDriver(driver, claimed.driverId);
966
- await runClaimedAssignment(client, config, selectedDriver, claimed.workspace, brief, claimed.taskId, timeoutMs, supervision);
967
- return true;
968
- }
969
942
  /**
970
943
  * T90: attempts whose stall report already ended them, by attempt id.
971
944
  *
@@ -1342,9 +1315,9 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1342
1315
  return;
1343
1316
  }
1344
1317
  // T22/T24/T28: the gates run once here, before the first model call. A gate that started but
1345
- // tested nothing is the earliest fact and refuses first; then a gate already red on the untouched
1346
- // base; only a green base still runs the scope check, which refuses when a gate wrote outside the
1347
- // approved change scope.
1318
+ // tested nothing is the earliest fact and refuses first; then a gate that never exited; then a gate
1319
+ // already red on the untouched base; only a green base still runs the scope check, which refuses
1320
+ // when a gate wrote outside the approved change scope.
1348
1321
  if (dryRunsVerification({
1349
1322
  executionKind,
1350
1323
  executionClass,
@@ -1388,6 +1361,25 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
1388
1361
  console.error(`Assignment ${taskId} refused before the agent started: ${redactSecrets(detail)}`);
1389
1362
  return;
1390
1363
  }
1364
+ if (dryRun.timedOut) {
1365
+ const { command, stderrTail, stdoutTail } = dryRun.timedOut;
1366
+ const detail = `Verification gate did not finish on the base commit ${worktreeStart}: ${command} (stopped after ${Math.round(VERIFICATION_TIMEOUT_MS / 60_000)} minutes)`;
1367
+ const response = await queueTerminal(client, taskId, {
1368
+ action: "fail",
1369
+ body: {
1370
+ error: detail,
1371
+ retryable: false,
1372
+ signal: verificationGateTimeoutSignal({ command, baseCommit: worktreeStart, stderr: stderrTail, stdout: stdoutTail }),
1373
+ idempotency_key: `bridge:verification-gate-timeout:${active.attemptId}`,
1374
+ },
1375
+ });
1376
+ // No agent ran and the tree stands at the base commit, so it holds no diagnostic value.
1377
+ if (response.retain_worktree !== true) {
1378
+ await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
1379
+ }
1380
+ console.error(`Assignment ${taskId} refused before the agent started: ${redactSecrets(detail)}`);
1381
+ return;
1382
+ }
1391
1383
  if (dryRun.redBase) {
1392
1384
  const { command, exitCode, stderrTail, stdoutTail } = dryRun.redBase;
1393
1385
  const detail = `Verification fails on the base commit ${worktreeStart}: ${command} (exit ${exitCode ?? "unknown"})`;
@@ -2264,11 +2256,16 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2264
2256
  .then((stat) => stat?.slice(0, 8_000) ?? null)
2265
2257
  .catch(() => null)
2266
2258
  : null;
2259
+ // A report that fell short of the delivery contract is named by its type and travels as the
2260
+ // delivery_report signal, so repair routing never reads this message (K3, debt 18 item 3c).
2261
+ const reportDefect = error instanceof DeliveryReportDefectError;
2267
2262
  const classified = verificationFailed
2268
2263
  ? { retryable: true, error: `Verification failed after the agent run.\n${message}${diffStat ? `\n\nChanged files:\n${diffStat}` : ""}` }
2269
- // classifyFinalizeFailure: forge transport → retryable; contract/environment/unknown → fail
2270
- // closed (unknown prefixed as "Bridge finalize interrupted", not laundered as contract).
2271
- : classifyFinalizeFailure(message);
2264
+ : reportDefect
2265
+ ? { retryable: false, error: message }
2266
+ // classifyFinalizeFailure: forge transport → retryable; contract/environment/unknown → fail
2267
+ // closed (unknown prefixed as "Bridge finalize interrupted", not laundered as contract).
2268
+ : classifyFinalizeFailure(message);
2272
2269
  if (verificationFailed)
2273
2270
  retainAttemptWorktree = true;
2274
2271
  const response = await queueTerminal(client, taskId, {
@@ -2284,6 +2281,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
2284
2281
  command: error.command,
2285
2282
  ...(diffStat ? { diff_stat: diffStat } : {}),
2286
2283
  } } : {}),
2284
+ ...(reportDefect ? { signal: { phase: "delivery_report", witness: "bridge", kind: "contract" } } : {}),
2287
2285
  // A rework's own PR may already be open (ensureDeliveryPullRequest ran before this
2288
2286
  // gate). Carry the URL so the control plane can close it instead of littering.
2289
2287
  ...(report.pull_request_url ? { pull_request_url: report.pull_request_url } : {}),
@@ -2759,7 +2757,7 @@ export function validateDeliveryReport(report, spec, grants = [], actualPaths, b
2759
2757
  }
2760
2758
  });
2761
2759
  if (!published) {
2762
- throw new Error("Artifact delivery requires a published HTTP(S) URL and sha256 digest");
2760
+ throw new DeliveryReportDefectError("Artifact delivery requires a published HTTP(S) URL and sha256 digest");
2763
2761
  }
2764
2762
  }
2765
2763
  // Invariant 9: grants bound what an attempt may do. Cursor/plan-mode are soft hints the headless
@@ -2773,7 +2771,7 @@ export function validateDeliveryReport(report, spec, grants = [], actualPaths, b
2773
2771
  if (grants.includes("pr_create")
2774
2772
  && report.head_commit
2775
2773
  && !report.pull_request_url) {
2776
- throw new Error("Repository changes require a pull_request_url when pr_create is granted");
2774
+ throw new DeliveryReportDefectError("Repository changes require a pull_request_url when pr_create is granted");
2777
2775
  }
2778
2776
  const requiredEvidence = spec.required_evidence ?? [];
2779
2777
  const supplied = new Set(report.evidence.map((item) => item.kind));
@@ -2782,20 +2780,20 @@ export function validateDeliveryReport(report, spec, grants = [], actualPaths, b
2782
2780
  // Name the base Bridge diffed against: an empty path list is a fact about that base, not proof
2783
2781
  // that the agent did no work. Without the base the repair brief can only guess the symptom.
2784
2782
  const since = missing.includes("change") && baseCommit ? ` (no paths changed since ${baseCommit})` : "";
2785
- throw new Error(`Agent report is missing required evidence: ${missing.join(", ")}${since}`);
2783
+ throw new DeliveryReportDefectError(`Agent report is missing required evidence: ${missing.join(", ")}${since}`);
2786
2784
  }
2787
2785
  const unsupported = report.acceptance_results
2788
2786
  .filter((result) => result.status === "met" && !report.evidence.some((item) => item.acceptance_criteria.includes(result.criterion)))
2789
2787
  .map((result) => result.criterion);
2790
2788
  if (unsupported.length)
2791
- throw new Error(`Met acceptance criteria require mapped evidence: ${unsupported.join("; ")}`);
2789
+ throw new DeliveryReportDefectError(`Met acceptance criteria require mapped evidence: ${unsupported.join("; ")}`);
2792
2790
  // Plan-time normalizeRequiredEvidence puts "test" on verify packages; reject summary-only details.
2793
2791
  if (requiredEvidence.includes("test")) {
2794
2792
  const thin = report.evidence
2795
2793
  .filter((item) => item.kind === "test")
2796
2794
  .filter((item) => item.details.join("\n").trim().length < TEST_EVIDENCE_DETAILS_MIN);
2797
2795
  if (thin.length) {
2798
- throw new Error("test evidence must include verbatim command output in details (not a summary)");
2796
+ throw new DeliveryReportDefectError("test evidence must include verbatim command output in details (not a summary)");
2799
2797
  }
2800
2798
  }
2801
2799
  }
@@ -2882,15 +2880,28 @@ export function verificationScopeConflictDetail(paths, changeScope) {
2882
2880
  export async function verificationScopeDryRun(input) {
2883
2881
  const commands = verificationEvidenceCommands(input.verificationCommands, input.declaredVerificationCommands ?? [], input.acceptance ?? null);
2884
2882
  if (!commands.length)
2885
- return { ranNothing: null, redBase: null, outsideScope: [] };
2883
+ return { ranNothing: null, timedOut: null, redBase: null, outsideScope: [] };
2886
2884
  const run = input.runCommand ?? runBoundedVerificationCommand;
2887
2885
  let ranNothing = null;
2886
+ let timedOut = null;
2888
2887
  let redBase = null;
2889
2888
  for (const command of commands) {
2890
2889
  // A gate that cannot start says nothing about the scope; the next one still can.
2891
2890
  const result = await run(command, input.workspace).catch(() => null);
2892
2891
  if (!result)
2893
2892
  continue;
2893
+ // A gate that never exited says nothing about the base commit: it is not a red base, and it
2894
+ // does not go to the initiative author.
2895
+ if (result.timedOut) {
2896
+ if (!timedOut) {
2897
+ timedOut = {
2898
+ command,
2899
+ ...(result.stderr.trim() ? { stderrTail: result.stderr } : {}),
2900
+ ...(result.stdout.trim() ? { stdoutTail: result.stdout } : {}),
2901
+ };
2902
+ }
2903
+ continue;
2904
+ }
2894
2905
  // Ahead of the exit code: a gate that discovered no test exits 5, and a no-op recipe exits 0
2895
2906
  // with no output. Neither is a red base, so neither goes to the initiative author.
2896
2907
  const reason = detectGateRanNothing(command, result.stdout, result.stderr, result.code)
@@ -2915,7 +2926,7 @@ export async function verificationScopeDryRun(input) {
2915
2926
  const outsideScope = written === null
2916
2927
  ? []
2917
2928
  : written.filter((path) => !input.changeScope.some((scope) => pathMatchesScope(path, scope)));
2918
- return { ranNothing, redBase, outsideScope };
2929
+ return { ranNothing, timedOut, redBase, outsideScope };
2919
2930
  }
2920
2931
  /** Prompt and failure lines keep a short tail; the investigation brief caps each failure at 1,000. */
2921
2932
  const DELIVERY_VERIFICATION_GATE_TAIL_CHARS = 800;
@@ -228,9 +228,10 @@ export function contractScopeConflictFailure(detail) {
228
228
  function resetClock(resetsAt) {
229
229
  return `${resetsAt.slice(0, 16).replace("T", " ")} UTC`;
230
230
  }
231
- /** Signal tails are bounded: one runaway log cannot fill the wire or the card. */
231
+ /** Signal tails are bounded: one runaway log cannot fill the wire or the card. A tail keeps the
232
+ * end of the output, because the decisive error is printed last. */
232
233
  function bounded2k(text) {
233
- return text.length > 2_000 ? `${text.slice(0, 1_999)}…` : text;
234
+ return text.length > 2_000 ? `…${text.slice(-1_999)}` : text;
234
235
  }
235
236
  /**
236
237
  * Vendor text that names the model as the reason the CLI refused the run. The model name is the one
@@ -398,14 +399,33 @@ export function verificationRedBaseSignal(input) {
398
399
  ...(input.stdout?.trim() ? { stdout_tail: bounded2k(input.stdout) } : {}),
399
400
  };
400
401
  }
402
+ /**
403
+ * The signal Bridge emits when the verification timeout stops a gate on the base commit, before the
404
+ * agent starts. The gate never exited, so it says nothing about the base: read as exit 1, a gate
405
+ * that hung (waiting for input or the network, or on a slow computer) was a red base on the author.
406
+ */
407
+ export function verificationGateTimeoutSignal(input) {
408
+ return {
409
+ phase: "verification",
410
+ witness: "bridge",
411
+ kind: "timeout",
412
+ command: input.command,
413
+ base_commit: input.baseCommit,
414
+ exit_code: null,
415
+ ...(input.stderr?.trim() ? { stderr_tail: bounded2k(input.stderr) } : {}),
416
+ ...(input.stdout?.trim() ? { stdout_tail: bounded2k(input.stdout) } : {}),
417
+ };
418
+ }
401
419
  /** A fact value as a person reads it on the card; an absent value says so in words. */
402
420
  function factValue(value) {
403
421
  return value === null ? "none" : value;
404
422
  }
405
- /** The vendor's own reason, kept inside the 2,000-char message bound. */
423
+ /** The vendor's own reason, kept inside the 2,000-char message bound; a stderr tail keeps its end. */
406
424
  function quotedVendorText(signal) {
407
- const text = signal.vendor_message ?? signal.stderr_tail ?? "";
408
- return text.length > 400 ? `${text.slice(0, 399)}…` : text;
425
+ if (signal.vendor_message)
426
+ return signal.vendor_message.length > 400 ? `${signal.vendor_message.slice(0, 399)}…` : signal.vendor_message;
427
+ const tail = signal.stderr_tail ?? "";
428
+ return tail.length > 400 ? `…${tail.slice(-399)}` : tail;
409
429
  }
410
430
  /**
411
431
  * Classify one typed signal. Null when the signal carries no typed verdict (`kind: "unknown"`):
@@ -418,17 +438,19 @@ function quotedVendorText(signal) {
418
438
  * 2. model_unbound → the tier has no other model to retry into: Hold, naming the tier and driver.
419
439
  * 3. denied_tool / usage_error → the driver and vendor cannot run this assignment: Hold.
420
440
  * 4. a changed pinned execution fact → the computer moved under the run: Hold, then Retry re-pins.
421
- * 5. a gate that already fails on the untouched base commit → no agent ran and none can repair a
441
+ * 5. a gate the verification timeout stopped on the base commit → it never exited, so it says
442
+ * nothing about the base: Hold, on the computer operator, naming the gate.
443
+ * 6. a gate that already fails on the untouched base commit → no agent ran and none can repair a
422
444
  * gate broken before it started: Hold, on the initiative author, naming the gate and the base.
423
- * 6. verification whose gates wrote outside the approved change scope → the work package is the
445
+ * 7. verification whose gates wrote outside the approved change scope → the work package is the
424
446
  * defect: Hold, on the initiative author, who opens a revision.
425
- * 7. verification run by Bridge whose gate ran nothing → the gate is misconfigured, not failed:
447
+ * 8. verification run by Bridge whose gate ran nothing → the gate is misconfigured, not failed:
426
448
  * Hold, on the computer operator, naming the command and the file that declares it. Never a
427
449
  * repair attempt: the delivery pack may be entirely correct.
428
- * 8. verification run by Bridge, any other cause → the verify class: diagnose and brief a repair.
429
- * 9. contract, or a delivery_report phase → the delivery contract codes: rework.
430
- * 10. timeout → the turn reached its execution limit: stop.
431
- * 11. turn_cap → the run reached the computer's per-attempt turn cap: Hold, on the operator, who
450
+ * 9. verification run by Bridge, any other cause → the verify class: diagnose and brief a repair.
451
+ * 10. contract, or a delivery_report phase → the delivery contract codes: rework.
452
+ * 11. timeout → the turn reached its execution limit: stop.
453
+ * 12. turn_cap → the run reached the computer's per-attempt turn cap: Hold, on the operator, who
432
454
  * decides whether the work was looping or the cap is too tight. Never a silent retry.
433
455
  */
434
456
  export function classifyFailureSignal(signal) {
@@ -532,6 +554,21 @@ export function classifyFailureSignal(signal) {
532
554
  diagnostic_detail: `${fact.name}: pinned ${factValue(fact.pinned)}, found ${factValue(fact.observed)}`,
533
555
  };
534
556
  }
557
+ // Ahead of the red-base and generic verification rules: the gate never exited, so neither the
558
+ // base commit nor a change is known to be at fault. The generic rule read it as a failed test.
559
+ if (signal.phase === "verification" && signal.kind === "timeout") {
560
+ const command = signal.command ?? "the declared gate";
561
+ const base = signal.base_commit ?? "the base commit";
562
+ return {
563
+ code: "verification_gate_timeout",
564
+ class: "environment",
565
+ disposition: "hold",
566
+ responsible_party: "computer_operator",
567
+ message: `The verification gate \`${command}\` did not finish on the base commit ${base} within Bridge's 10-minute limit, so Bridge stopped it before any change was made.`,
568
+ next_action: `Run \`${command}\` on this computer and find where it hangs — a test that waits for input or the network, or a computer too slow for the gate — then fix it and Recheck. Conduit does not retry or repair a gate that does not finish.`,
569
+ diagnostic_detail: [`$ ${command}`, "stopped after 10 minutes", signal.stderr_tail, signal.stdout_tail].filter(Boolean).join("\n"),
570
+ };
571
+ }
535
572
  // Ahead of the scope and generic verification rules: the base commit itself does not pass the
536
573
  // gates, so no agent ran and no agent can repair a gate that was already broken before it
537
574
  // started. This is the earlier fact — checked in the dry run before the scope conflict — so it
@@ -614,7 +651,7 @@ export function classifyFailureSignal(signal) {
614
651
  disposition: "rework",
615
652
  responsible_party: "conduit",
616
653
  message: "The delivery report did not satisfy the delivery contract.",
617
- next_action: "Bridge's report-only repair turns ran; review the report defect and rework it.",
654
+ next_action: "Conductor will prepare bounded recovery guidance. You do not need to edit technical constraints.",
618
655
  diagnostic_detail: signal.vendor_message ?? signal.stderr_tail,
619
656
  }
620
657
  : {
@@ -12,11 +12,11 @@ import { createAttemptWorktree, removeAttemptWorktree } from "./attempt-worktree
12
12
  import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl } from "./brief.js";
13
13
  import { headCommitOrNull } from "./git.js";
14
14
  import { learnDriverFuel, resolveAssignmentModel, changedPathsSince, runDeliveryVerificationGates, discardWorktreeWrites } from "./execution.js";
15
- import { DRIVERS, extractAgentReportJsonText, parseJsonObjectCandidate } from "./driver.js";
15
+ import { DRIVERS, execute, extractAgentReportJsonText, parseJsonObjectCandidate } from "./driver.js";
16
16
  import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
17
17
  import { diffStatSince } from "./git-witness.js";
18
18
  import { switchManagedWorkspace } from "./on-shift-apply.js";
19
- import { execFile, spawn } from "node:child_process";
19
+ import { execFile } from "node:child_process";
20
20
  import { rm, writeFile } from "node:fs/promises";
21
21
  import { createRequire } from "node:module";
22
22
  import { join } from "node:path";
@@ -634,34 +634,17 @@ function toStepWitness(witness) {
634
634
  instrument: witness.instrument === "delivered" ? "delivered" : "running",
635
635
  };
636
636
  }
637
+ /**
638
+ * The delivered instrument runs through the agent runs' own process helper (T90): bounded output,
639
+ * an answer on the child's exit even when a process it left behind holds the pipes, and a stop
640
+ * that escalates to SIGKILL. Its environment stays the probe's own, PATH and HOME only.
641
+ */
637
642
  async function execDeliveredProbeRun(argv, stdin, workspace, timeoutMs) {
638
643
  const [file, ...args] = argv;
639
644
  if (!file)
640
645
  throw new Error("delivered probe command is empty");
641
- return await new Promise((resolve, reject) => {
642
- const child = spawn(file, args, {
643
- cwd: workspace,
644
- env: probeEnv(),
645
- stdio: ["pipe", "pipe", "pipe"],
646
- });
647
- let stdout = "";
648
- let stderr = "";
649
- const timer = setTimeout(() => {
650
- child.kill("SIGKILL");
651
- }, timeoutMs);
652
- child.stdout?.on("data", (chunk) => { stdout += String(chunk); });
653
- child.stderr?.on("data", (chunk) => { stderr += String(chunk); });
654
- child.on("error", (error) => {
655
- clearTimeout(timer);
656
- reject(error);
657
- });
658
- child.on("close", (code) => {
659
- clearTimeout(timer);
660
- resolve({ code: code ?? -1, stdout, stderr });
661
- });
662
- child.stdin?.on("error", () => undefined);
663
- child.stdin?.end(stdin);
664
- });
646
+ const result = await execute(file, args, workspace, timeoutMs, undefined, "conduit", stdin, undefined, undefined, probeEnv());
647
+ return { code: result.code ?? -1, stdout: result.stdout, stderr: result.stderr };
665
648
  }
666
649
  export async function runDeliveredProbeInstrument(input) {
667
650
  const entry = join(input.workspace, PROBE_RUN_ENTRY);
@@ -724,10 +707,6 @@ export const INVESTIGATION_RENEW_INTERVAL_MS = 2 * 60_000;
724
707
  export function investigationRunId(investigationId) {
725
708
  return `${investigationId}-${crypto.randomUUID().slice(0, 8)}`;
726
709
  }
727
- async function deleteRunBranch(workspace, runId) {
728
- await execFileAsync("git", ["-C", workspace, "branch", "-D", `conduit/attempt/${runId}`], { timeout: 30_000, maxBuffer: 1_000_000 })
729
- .catch(() => undefined);
730
- }
731
710
  export async function executeNextInvestigation(client, config, workspace, brief, timeoutMs, options = {}) {
732
711
  const data = await client.request("/runner/v1/investigations");
733
712
  const assignments = z.array(investigationAssignmentSchema).parse(data.investigations ?? []);
@@ -959,7 +938,6 @@ export async function executeNextInvestigation(client, config, workspace, brief,
959
938
  if (attemptWorkspace) {
960
939
  await removeAttemptWorktree(workspace, attemptWorkspace)
961
940
  .catch((error) => console.error(`Investigation worktree release failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
962
- await deleteRunBranch(workspace, runId);
963
941
  }
964
942
  }
965
943
  return true;
package/dist/ops.js CHANGED
@@ -191,14 +191,6 @@ export function resolveInstallDrivers(env, argv, detect = detectInstalledClients
191
191
  return [preferred];
192
192
  });
193
193
  }
194
- /** Quote args for a copy-pasteable shell/cmd line (paths with spaces). */
195
- export function shellQuoteArgs(args) {
196
- return args.map((arg) => {
197
- if (!/[^\w@%+=:,./\\-]/i.test(arg))
198
- return arg;
199
- return `"${arg.replaceAll('"', '\\"')}"`;
200
- }).join(" ");
201
- }
202
194
  /** Ask the operator to approve a step that cannot be undone. Non-interactive shells must pass --yes. */
203
195
  async function confirmOps(question) {
204
196
  if (!input.isTTY || !output.isTTY) {
package/dist/service.js CHANGED
@@ -262,7 +262,6 @@ export function runnerServiceWorkspaceWarnings(declaredWorkspace, home = homedir
262
262
  }
263
263
  return warnings;
264
264
  }
265
- export const WINDOWS_TASK_NAME = "ConduitBridgeRunner";
266
265
  /** Quote one argv token for a Windows Task Scheduler /TR command line. */
267
266
  export function quoteWindowsTaskArg(value) {
268
267
  if (!/[\s&<>|^()"]/.test(value))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.141",
3
+ "version": "0.16.143",
4
4
  "description": "Conduit Bridge CLI \u2014 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": {