@miraland-labs/conduit-bridge 0.16.159 → 0.16.160
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/driver.js +18 -10
- package/dist/ensure-land-commit.js +2 -2
- package/dist/ensure-test-evidence.js +13 -17
- package/dist/execution.js +18 -24
- package/dist/git-witness.js +1 -1
- package/dist/git.js +2 -2
- package/dist/land-git-reality.js +1 -1
- package/package.json +1 -1
package/dist/driver.js
CHANGED
|
@@ -1884,14 +1884,14 @@ const MAX_AGENT_OUTPUT = 8 * 1024 * 1024;
|
|
|
1884
1884
|
*/
|
|
1885
1885
|
export const STDIO_DRAIN_MS = 2_000;
|
|
1886
1886
|
/** How long a stopped child may take to die before the run reports what it already has. */
|
|
1887
|
-
const CHILD_STOP_GRACE_MS = 20_000;
|
|
1887
|
+
export const CHILD_STOP_GRACE_MS = 20_000;
|
|
1888
1888
|
/**
|
|
1889
1889
|
* An agent runs as the leader of its own process group, so what it starts in the background (a dev
|
|
1890
1890
|
* server, a test watcher, a database) is stopped with it. Before, only the agent itself was
|
|
1891
1891
|
* stopped: its background processes outlived the run and held files and ports on the computer
|
|
1892
1892
|
* (acme-sim SV1b, 2026-10-10). Windows has no process groups; there only the agent is stopped.
|
|
1893
1893
|
*/
|
|
1894
|
-
const OWN_PROCESS_GROUP = process.platform !== "win32";
|
|
1894
|
+
export const OWN_PROCESS_GROUP = process.platform !== "win32";
|
|
1895
1895
|
/** Process groups of the agents running now, stopped when the runner itself is stopped. */
|
|
1896
1896
|
const runningGroups = new Set();
|
|
1897
1897
|
let shutdownHookInstalled = false;
|
|
@@ -1903,6 +1903,17 @@ export function signalGroup(pid, signal) {
|
|
|
1903
1903
|
// ESRCH: every process of the group is already gone.
|
|
1904
1904
|
}
|
|
1905
1905
|
}
|
|
1906
|
+
export function trackProcessGroup(pid) {
|
|
1907
|
+
installShutdownHook();
|
|
1908
|
+
runningGroups.add(pid);
|
|
1909
|
+
}
|
|
1910
|
+
export function untrackProcessGroup(pid) {
|
|
1911
|
+
runningGroups.delete(pid);
|
|
1912
|
+
}
|
|
1913
|
+
export function stopTrackedProcessGroups() {
|
|
1914
|
+
for (const pid of runningGroups)
|
|
1915
|
+
signalGroup(pid, "SIGKILL");
|
|
1916
|
+
}
|
|
1906
1917
|
/**
|
|
1907
1918
|
* systemd stops a whole cgroup, but launchd stops only the runner's own process group, which an
|
|
1908
1919
|
* agent no longer shares. Stop the agents' groups first, then let the signal end the runner as it
|
|
@@ -1914,8 +1925,7 @@ function installShutdownHook() {
|
|
|
1914
1925
|
shutdownHookInstalled = true;
|
|
1915
1926
|
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
1916
1927
|
process.once(signal, () => {
|
|
1917
|
-
|
|
1918
|
-
signalGroup(pid, "SIGKILL");
|
|
1928
|
+
stopTrackedProcessGroups();
|
|
1919
1929
|
process.kill(process.pid, signal);
|
|
1920
1930
|
});
|
|
1921
1931
|
}
|
|
@@ -1931,10 +1941,8 @@ env) {
|
|
|
1931
1941
|
detached: OWN_PROCESS_GROUP,
|
|
1932
1942
|
});
|
|
1933
1943
|
const group = OWN_PROCESS_GROUP ? child.pid : undefined;
|
|
1934
|
-
if (group !== undefined)
|
|
1935
|
-
|
|
1936
|
-
runningGroups.add(group);
|
|
1937
|
-
}
|
|
1944
|
+
if (group !== undefined)
|
|
1945
|
+
trackProcessGroup(group);
|
|
1938
1946
|
const stop = (signal) => {
|
|
1939
1947
|
if (group !== undefined)
|
|
1940
1948
|
signalGroup(group, signal);
|
|
@@ -1966,7 +1974,7 @@ env) {
|
|
|
1966
1974
|
child.stderr?.destroy();
|
|
1967
1975
|
// The agent has answered; whatever it left running in its group stops now.
|
|
1968
1976
|
if (group !== undefined) {
|
|
1969
|
-
|
|
1977
|
+
untrackProcessGroup(group);
|
|
1970
1978
|
signalGroup(group, "SIGTERM");
|
|
1971
1979
|
setTimeout(() => signalGroup(group, "SIGKILL"), 5_000).unref?.();
|
|
1972
1980
|
}
|
|
@@ -2012,7 +2020,7 @@ env) {
|
|
|
2012
2020
|
return;
|
|
2013
2021
|
settled = true;
|
|
2014
2022
|
if (group !== undefined)
|
|
2015
|
-
|
|
2023
|
+
untrackProcessGroup(group);
|
|
2016
2024
|
clearTimeout(timer);
|
|
2017
2025
|
signal?.removeEventListener("abort", stopChild);
|
|
2018
2026
|
reject(error);
|
|
@@ -37,7 +37,7 @@ export async function ensureLandCommit(input) {
|
|
|
37
37
|
return { kind: "skipped" };
|
|
38
38
|
const countAhead = input.countCommitsAhead ?? commitsAheadOfBase;
|
|
39
39
|
const readHead = input.readHeadCommit ?? readHeadCommit;
|
|
40
|
-
const listPending = input.listUncommittedPaths ?? ((workspace) => uncommittedPaths(workspace
|
|
40
|
+
const listPending = input.listUncommittedPaths ?? ((workspace) => uncommittedPaths(workspace));
|
|
41
41
|
const commitPaths = input.commitPaths ?? commitScopedPaths;
|
|
42
42
|
const ahead = await countAhead(input.workspace, base);
|
|
43
43
|
if (ahead !== null && ahead > 0) {
|
|
@@ -69,7 +69,7 @@ async function currentBranch(workspace) {
|
|
|
69
69
|
export async function checkpointWorkInProgress(input) {
|
|
70
70
|
if (!requiresLandCommit({ grants: input.grants, spec: input.spec }))
|
|
71
71
|
return null;
|
|
72
|
-
const pending = filterPathsInScope(await uncommittedPaths(input.workspace
|
|
72
|
+
const pending = filterPathsInScope(await uncommittedPaths(input.workspace), input.spec.change_scope ?? []);
|
|
73
73
|
if (pending.length) {
|
|
74
74
|
await commitScopedPaths(input.workspace, pending, "Conduit: work in progress at the package budget");
|
|
75
75
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* it never substitutes for it.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
-
import { boundedEnvironment, criterionKey, resolveCriterionReference, signalGroup, STDIO_DRAIN_MS } from "./driver.js";
|
|
7
|
+
import { boundedEnvironment, criterionKey, resolveCriterionReference, signalGroup, STDIO_DRAIN_MS, CHILD_STOP_GRACE_MS, OWN_PROCESS_GROUP, trackProcessGroup, untrackProcessGroup } from "./driver.js";
|
|
8
8
|
import { isBoundedVerificationCommand, isRunnableVerificationCommand } from "./execution-class.js";
|
|
9
9
|
const TEST_EVIDENCE_DETAILS_MIN = 32;
|
|
10
10
|
/** Cold `cargo test` in an attempt worktree routinely exceeds two minutes of compile. */
|
|
@@ -193,14 +193,10 @@ export async function captureVerificationFailure(input) {
|
|
|
193
193
|
const run = input.runCommand ?? runBoundedVerificationCommand;
|
|
194
194
|
// Try each bounded command until one fails. First non-zero wins (typecheck before test when both exist).
|
|
195
195
|
for (const command of commands) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
// Diagnosis is best-effort: try the next command rather than failing the attempt again.
|
|
196
|
+
// Diagnosis is best-effort: try the next command rather than failing the attempt again.
|
|
197
|
+
const result = await run(command, input.workspace).catch(() => null);
|
|
198
|
+
if (!result)
|
|
202
199
|
continue;
|
|
203
|
-
}
|
|
204
200
|
if (result.timedOut) {
|
|
205
201
|
return { command, detail: boundedTail(outputLines(result).join("\n"), DIAGNOSIS_DETAIL_MAX_CHARS),
|
|
206
202
|
timedOut: true, stdout: result.stdout, stderr: result.stderr };
|
|
@@ -429,12 +425,14 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
|
|
|
429
425
|
};
|
|
430
426
|
const output = () => [lines.text().replace(/\n$/, ""), partial.stdout.text(), partial.stderr.text()].filter(Boolean).join("\n");
|
|
431
427
|
return new Promise((resolve, reject) => {
|
|
432
|
-
const ownGroup = process.platform !== "win32";
|
|
433
428
|
const child = spawn(bin, argv.slice(1), { cwd: workspace, env: boundedEnvironment(),
|
|
434
|
-
stdio: ["ignore", "pipe", "pipe"], detached:
|
|
429
|
+
stdio: ["ignore", "pipe", "pipe"], detached: OWN_PROCESS_GROUP });
|
|
430
|
+
const group = OWN_PROCESS_GROUP ? child.pid : undefined;
|
|
431
|
+
if (group !== undefined)
|
|
432
|
+
trackProcessGroup(group);
|
|
435
433
|
const stop = (signal) => {
|
|
436
|
-
if (
|
|
437
|
-
signalGroup(
|
|
434
|
+
if (group !== undefined)
|
|
435
|
+
signalGroup(group, signal);
|
|
438
436
|
else
|
|
439
437
|
child.kill(signal);
|
|
440
438
|
};
|
|
@@ -442,10 +440,11 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
|
|
|
442
440
|
const stderr = boundedOutput(VERIFICATION_MAX_BUFFER_BYTES);
|
|
443
441
|
let timedOut = false;
|
|
444
442
|
let settled = false;
|
|
445
|
-
let exited = false;
|
|
446
443
|
let drainTimer = null;
|
|
447
444
|
let graceTimer = null;
|
|
448
445
|
const cleanup = () => {
|
|
446
|
+
if (group !== undefined)
|
|
447
|
+
untrackProcessGroup(group);
|
|
449
448
|
clearTimeout(timer);
|
|
450
449
|
if (drainTimer)
|
|
451
450
|
clearTimeout(drainTimer);
|
|
@@ -467,12 +466,10 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
|
|
|
467
466
|
: { stdout: stdout.text(), stderr: stderr.text(), output: output(), code: code ?? 1 });
|
|
468
467
|
};
|
|
469
468
|
const timer = setTimeout(() => {
|
|
470
|
-
if (exited)
|
|
471
|
-
return;
|
|
472
469
|
timedOut = true;
|
|
473
470
|
stop("SIGTERM");
|
|
474
471
|
setTimeout(() => stop("SIGKILL"), 10_000).unref?.();
|
|
475
|
-
graceTimer = setTimeout(() => settle(null),
|
|
472
|
+
graceTimer = setTimeout(() => settle(null), CHILD_STOP_GRACE_MS);
|
|
476
473
|
}, timeoutMs);
|
|
477
474
|
child.stdout?.setEncoding("utf8");
|
|
478
475
|
child.stderr?.setEncoding("utf8");
|
|
@@ -494,7 +491,6 @@ export async function runBoundedVerificationCommand(command, workspace, timeoutM
|
|
|
494
491
|
});
|
|
495
492
|
child.on("close", (code) => settle(code));
|
|
496
493
|
child.on("exit", (code) => {
|
|
497
|
-
exited = true;
|
|
498
494
|
clearTimeout(timer);
|
|
499
495
|
drainTimer = setTimeout(() => settle(code), STDIO_DRAIN_MS);
|
|
500
496
|
});
|
package/dist/execution.js
CHANGED
|
@@ -1090,6 +1090,17 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
1090
1090
|
// A diagnostic terminal defaults to retention. If its lease disappears, deleting the only copy
|
|
1091
1091
|
// of the failed code would make the durable retry mechanically impossible.
|
|
1092
1092
|
let retainAttemptWorktree = diagnosis;
|
|
1093
|
+
const reportVerificationGateTimeout = async (input) => {
|
|
1094
|
+
const response = await queueTerminal(client, taskId, { action: "fail", body: {
|
|
1095
|
+
error: input.detail,
|
|
1096
|
+
retryable: false,
|
|
1097
|
+
signal: verificationGateTimeoutSignal({ command: input.command, stderr: input.stderr, stdout: input.stdout }),
|
|
1098
|
+
...(input.pullRequestUrl ? { pull_request_url: input.pullRequestUrl } : {}),
|
|
1099
|
+
idempotency_key: `bridge:verification-gate-timeout:${active.attemptId}`,
|
|
1100
|
+
} });
|
|
1101
|
+
retainAttemptWorktree = response.retain_worktree === true;
|
|
1102
|
+
console.error(`Assignment ${taskId} verification gate did not finish: ${redactSecrets(input.detail)}`);
|
|
1103
|
+
};
|
|
1093
1104
|
if (diagnosis) {
|
|
1094
1105
|
if (!sourceAttemptId) {
|
|
1095
1106
|
await queueTerminal(client, taskId, {
|
|
@@ -1804,15 +1815,8 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
1804
1815
|
: null;
|
|
1805
1816
|
if (capturedVerification?.timedOut) {
|
|
1806
1817
|
retainAttemptWorktree = true;
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
retryable: false,
|
|
1810
|
-
signal: verificationGateTimeoutSignal({ command: capturedVerification.command,
|
|
1811
|
-
stderr: capturedVerification.stderr ?? "", stdout: capturedVerification.stdout ?? "" }),
|
|
1812
|
-
idempotency_key: `bridge:verification-gate-timeout:${active.attemptId}`,
|
|
1813
|
-
} });
|
|
1814
|
-
retainAttemptWorktree = response.retain_worktree === true;
|
|
1815
|
-
console.error(`Assignment ${taskId} verification gate did not finish: ${redactSecrets(capturedVerification.detail)}`);
|
|
1818
|
+
await reportVerificationGateTimeout({ command: capturedVerification.command, detail: capturedVerification.detail,
|
|
1819
|
+
stderr: capturedVerification.stderr ?? "", stdout: capturedVerification.stdout ?? "" });
|
|
1816
1820
|
return;
|
|
1817
1821
|
}
|
|
1818
1822
|
const verificationDetail = capturedVerification?.detail ?? null;
|
|
@@ -2274,18 +2278,8 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
2274
2278
|
// timeout signal the dry run emits, held on the computer operator — never a diagnosis or a
|
|
2275
2279
|
// repair brief. Task ce673ef1 (2026-10-10) ran two diagnoses on a CPU-starved `npm run test`.
|
|
2276
2280
|
if (error instanceof VerificationGateTimedOutError) {
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
body: {
|
|
2280
|
-
error: error.message,
|
|
2281
|
-
retryable: false,
|
|
2282
|
-
signal: verificationGateTimeoutSignal({ command: error.command, stderr: error.stderr, stdout: error.stdout }),
|
|
2283
|
-
...(report.pull_request_url ? { pull_request_url: report.pull_request_url } : {}),
|
|
2284
|
-
idempotency_key: `bridge:verification-gate-timeout:${active.attemptId}`,
|
|
2285
|
-
},
|
|
2286
|
-
});
|
|
2287
|
-
retainAttemptWorktree = response.retain_worktree === true;
|
|
2288
|
-
console.error(`Assignment ${taskId} verification gate did not finish: ${redactSecrets(error.message)}`);
|
|
2281
|
+
await reportVerificationGateTimeout({ command: error.command, detail: error.message,
|
|
2282
|
+
stderr: error.stderr, stdout: error.stdout, pullRequestUrl: report.pull_request_url });
|
|
2289
2283
|
return;
|
|
2290
2284
|
}
|
|
2291
2285
|
// A gate that ran no tests is a misconfigured gate, not a failed verification: the delivery
|
|
@@ -2924,10 +2918,10 @@ export function verificationScopeConflictDetail(paths, changeScope) {
|
|
|
2924
2918
|
return `Verification writes outside the approved scope: ${paths.join(", ")}\napproved change scope: ${changeScope.join(", ")}`;
|
|
2925
2919
|
}
|
|
2926
2920
|
/**
|
|
2927
|
-
* Run the project's verification commands once in the fresh attempt worktree, then answer
|
|
2921
|
+
* Run the project's verification commands once in the fresh attempt worktree, then answer four
|
|
2928
2922
|
* questions before the first model call: did a gate start but test nothing, did a gate already fail
|
|
2929
|
-
* on the untouched base commit,
|
|
2930
|
-
* does not contain?
|
|
2923
|
+
* on the untouched base commit, which paths did a gate write that the approved change scope
|
|
2924
|
+
* does not contain, and did a gate fail to finish?
|
|
2931
2925
|
*
|
|
2932
2926
|
* R5 (2026-09-03) and R10 (2026-09-05) failed the same way: the gates write files under the
|
|
2933
2927
|
* repository root, the change scope did not contain them, and `validateChangeScope` rejected the
|
package/dist/git-witness.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function changedPathsSince(worktree, startCommit) {
|
|
|
46
46
|
*/
|
|
47
47
|
export async function worktreeWrittenPaths(worktree) {
|
|
48
48
|
try {
|
|
49
|
-
return [...new Set(await uncommittedPaths(worktree
|
|
49
|
+
return [...new Set(await uncommittedPaths(worktree))];
|
|
50
50
|
}
|
|
51
51
|
catch {
|
|
52
52
|
return null;
|
package/dist/git.js
CHANGED
|
@@ -12,8 +12,8 @@ const execFileAsync = promisify(execFile);
|
|
|
12
12
|
* and escape (`中文.txt` as `"\344\270\255\346\226\207.txt"`) comes back verbatim and can be
|
|
13
13
|
* handed back to git (review 2026-10-03, finding 3).
|
|
14
14
|
*/
|
|
15
|
-
export async function uncommittedPaths(workspace
|
|
16
|
-
const { stdout } = await execFileAsync("git", ["-C", workspace, "status", "--porcelain=v1", "-z",
|
|
15
|
+
export async function uncommittedPaths(workspace) {
|
|
16
|
+
const { stdout } = await execFileAsync("git", ["-C", workspace, "status", "--porcelain=v1", "-z", "--untracked-files=all"], { timeout: 30_000, maxBuffer: 8_000_000 });
|
|
17
17
|
return porcelainPaths(stdout);
|
|
18
18
|
}
|
|
19
19
|
/** Porcelain v1 with `-z`: `XY path`, and a rename or copy adds its source as the next field. */
|
package/dist/land-git-reality.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function isLandTreeEmpty(input) {
|
|
|
18
18
|
const ahead = await countAhead(input.workspace, base);
|
|
19
19
|
if (ahead !== null && ahead > 0)
|
|
20
20
|
return false;
|
|
21
|
-
const pending = filterPathsInScope(await (input.listUncommittedPaths ?? ((workspace) => listUncommittedPaths(workspace
|
|
21
|
+
const pending = filterPathsInScope(await (input.listUncommittedPaths ?? ((workspace) => listUncommittedPaths(workspace)))(input.workspace), input.spec.change_scope ?? []);
|
|
22
22
|
return pending.length === 0;
|
|
23
23
|
}
|
|
24
24
|
function deliveryObjectClaimsWork(raw) {
|
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.160",
|
|
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": {
|