@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 +1 -1
- package/dist/attempt-worktree.js +1 -26
- package/dist/config.js +0 -2
- package/dist/driver.js +2 -25
- package/dist/drivers.js +0 -9
- package/dist/ensure-demonstration-evidence.js +1 -1
- package/dist/ensure-test-evidence.js +13 -2
- package/dist/execution.js +59 -54
- package/dist/failure-signal.js +41 -7
- package/dist/ops.js +0 -8
- package/dist/service.js +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connects a computer to one organization, claims work, and drives a local agent.
|
|
4
4
|
|
|
5
|
-
**Package version:** `0.16.125` — the workspace brief reports `declared_demonstration` so the planner knows whether the repository can show itself (
|
|
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
|
|
package/dist/attempt-worktree.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
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,
|
|
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
|
-
|
|
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(
|
|
637
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
1346
|
-
// base; only a green base still runs the scope check, which refuses
|
|
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;
|
package/dist/failure-signal.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
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.
|
|
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": {
|