@miraland-labs/conduit-bridge 0.16.142 → 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,7 +2,7 @@
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
7
  import { basename, join, resolve } from "node:path";
8
8
  import { execFile } from "node:child_process";
@@ -136,28 +136,3 @@ export async function proveResumeWorktree(sourceWorkspace, attemptId, worktreePa
136
136
  return false;
137
137
  }
138
138
  }
139
- /** Hold a crashed attempt tree for local diagnosis; never reused as agent cwd. */
140
- export async function quarantineAttemptWorktree(sourceWorkspace, worktreePath, attemptId) {
141
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
142
- const dest = join(sourceWorkspace, ".conduit", "quarantine", `${attemptId}-${stamp}`);
143
- await mkdir(join(sourceWorkspace, ".conduit", "quarantine"), { recursive: true });
144
- try {
145
- await execFileAsync("git", ["-C", sourceWorkspace, "worktree", "remove", "--force", worktreePath], { timeout: 60_000, maxBuffer: 2_000_000 });
146
- return null;
147
- }
148
- catch {
149
- try {
150
- await rename(worktreePath, dest);
151
- await execFileAsync("git", ["-C", sourceWorkspace, "worktree", "prune"], {
152
- timeout: 30_000,
153
- maxBuffer: 1_000_000,
154
- }).catch(() => undefined);
155
- await writeFile(join(dest, ".conduit-quarantine.json"), `${JSON.stringify({ attemptId, at: stamp })}\n`, "utf8");
156
- return dest;
157
- }
158
- catch {
159
- await removeAttemptWorktree(sourceWorkspace, worktreePath);
160
- return null;
161
- }
162
- }
163
- }
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;
@@ -916,21 +911,6 @@ export const codexDriver = {
916
911
  return { status: "completed", resultText, sessionId: parsed.sessionId, usage: parsed.usage };
917
912
  },
918
913
  };
919
- /**
920
- * Grants + capabilities → Cursor CLI permissions via ExecutionClass.
921
- * Current Cursor Agent schema accepts only `permissions.{allow,deny}` (no
922
- * `version` / `approvalMode`). Outside the allow list is denied without --force.
923
- *
924
- * `observe_network` / networked mutate / publish_artifact use `--force` (see
925
- * cursorRunArgs). Field evidence: allow-listing WebFetch alone still yields
926
- * "User Rejected" under headless `-p`; `--force` is required for fetch and for
927
- * publish shell under open class policy. Research-only networked assignments
928
- * deny Shell(*) and Write(**).
929
- */
930
- export function cursorPermissionsForGrants(grants, verificationCommands, capabilities, executionClass) {
931
- const { allow, deny } = projectCursor(executionClass, { grants, verificationCommands, capabilities });
932
- return { allow, deny };
933
- }
934
914
  /**
935
915
  * Cursor CLI arguments. Deliberately no `--mode plan` for read-only assignments: plan mode is not an
936
916
  * enforcement boundary (in headless `-p` mode the agent can self-escalate out of it and edit anyway)
@@ -1996,7 +1976,8 @@ const LOCAL_VENDOR_ENV = [
1996
1976
  "CURSOR_API_KEY", "KIRO_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY",
1997
1977
  "XAI_API_KEY",
1998
1978
  ];
1999
- 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) {
2000
1981
  // Proxy and TLS variables stay: fueled agents must reach Conduit's /v1 on
2001
1982
  // machines that only have network access through a proxy.
2002
1983
  const allowed = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "TERM", "NO_COLOR", "FORCE_COLOR",
@@ -2071,7 +2052,3 @@ export function hasGrokLogin(env = process.env) {
2071
2052
  // `~/.grok` exists after install; auth.json is the `grok login` / cached_token file.
2072
2053
  return Boolean(home && existsSync(join(home, ".grok", "auth.json")));
2073
2054
  }
2074
- /** Exported for tests — builds the stripped process env with optional Conduit fuel. */
2075
- export function agentProcessEnv(fuel, fuelSource = "conduit", extraEnv) {
2076
- return boundedEnvironment(fuel, fuelSource, extraEnv);
2077
- }
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 ?? ""),
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, DeliveryReportDefectError, 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"})`;
@@ -2888,15 +2880,28 @@ export function verificationScopeConflictDetail(paths, changeScope) {
2888
2880
  export async function verificationScopeDryRun(input) {
2889
2881
  const commands = verificationEvidenceCommands(input.verificationCommands, input.declaredVerificationCommands ?? [], input.acceptance ?? null);
2890
2882
  if (!commands.length)
2891
- return { ranNothing: null, redBase: null, outsideScope: [] };
2883
+ return { ranNothing: null, timedOut: null, redBase: null, outsideScope: [] };
2892
2884
  const run = input.runCommand ?? runBoundedVerificationCommand;
2893
2885
  let ranNothing = null;
2886
+ let timedOut = null;
2894
2887
  let redBase = null;
2895
2888
  for (const command of commands) {
2896
2889
  // A gate that cannot start says nothing about the scope; the next one still can.
2897
2890
  const result = await run(command, input.workspace).catch(() => null);
2898
2891
  if (!result)
2899
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
+ }
2900
2905
  // Ahead of the exit code: a gate that discovered no test exits 5, and a no-op recipe exits 0
2901
2906
  // with no output. Neither is a red base, so neither goes to the initiative author.
2902
2907
  const reason = detectGateRanNothing(command, result.stdout, result.stderr, result.code)
@@ -2921,7 +2926,7 @@ export async function verificationScopeDryRun(input) {
2921
2926
  const outsideScope = written === null
2922
2927
  ? []
2923
2928
  : written.filter((path) => !input.changeScope.some((scope) => pathMatchesScope(path, scope)));
2924
- return { ranNothing, redBase, outsideScope };
2929
+ return { ranNothing, timedOut, redBase, outsideScope };
2925
2930
  }
2926
2931
  /** Prompt and failure lines keep a short tail; the investigation brief caps each failure at 1,000. */
2927
2932
  const DELIVERY_VERIFICATION_GATE_TAIL_CHARS = 800;
@@ -399,6 +399,23 @@ export function verificationRedBaseSignal(input) {
399
399
  ...(input.stdout?.trim() ? { stdout_tail: bounded2k(input.stdout) } : {}),
400
400
  };
401
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
+ }
402
419
  /** A fact value as a person reads it on the card; an absent value says so in words. */
403
420
  function factValue(value) {
404
421
  return value === null ? "none" : value;
@@ -421,17 +438,19 @@ function quotedVendorText(signal) {
421
438
  * 2. model_unbound → the tier has no other model to retry into: Hold, naming the tier and driver.
422
439
  * 3. denied_tool / usage_error → the driver and vendor cannot run this assignment: Hold.
423
440
  * 4. a changed pinned execution fact → the computer moved under the run: Hold, then Retry re-pins.
424
- * 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
425
444
  * gate broken before it started: Hold, on the initiative author, naming the gate and the base.
426
- * 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
427
446
  * defect: Hold, on the initiative author, who opens a revision.
428
- * 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:
429
448
  * Hold, on the computer operator, naming the command and the file that declares it. Never a
430
449
  * repair attempt: the delivery pack may be entirely correct.
431
- * 8. verification run by Bridge, any other cause → the verify class: diagnose and brief a repair.
432
- * 9. contract, or a delivery_report phase → the delivery contract codes: rework.
433
- * 10. timeout → the turn reached its execution limit: stop.
434
- * 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
435
454
  * decides whether the work was looping or the cap is too tight. Never a silent retry.
436
455
  */
437
456
  export function classifyFailureSignal(signal) {
@@ -535,6 +554,21 @@ export function classifyFailureSignal(signal) {
535
554
  diagnostic_detail: `${fact.name}: pinned ${factValue(fact.pinned)}, found ${factValue(fact.observed)}`,
536
555
  };
537
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
+ }
538
572
  // Ahead of the scope and generic verification rules: the base commit itself does not pass the
539
573
  // gates, so no agent ran and no agent can repair a gate that was already broken before it
540
574
  // started. This is the earlier fact — checked in the dry run before the scope conflict — so it
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.142",
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": {