@miraland-labs/conduit-bridge 0.16.101 → 0.16.103
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/brief.js +27 -4
- package/dist/ensure-test-evidence.js +19 -2
- package/dist/execution-class.js +1 -1
- package/dist/execution-facts.js +5 -1
- package/dist/execution.js +20 -4
- package/dist/failure-signal.js +2 -0
- package/dist/git-witness.js +14 -5
- package/package.json +1 -1
package/dist/brief.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
1
|
+
import { access, readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
@@ -8,18 +8,28 @@ const execFileAsync = promisify(execFile);
|
|
|
8
8
|
const MANIFESTS = [
|
|
9
9
|
"package.json", "wrangler.jsonc", "wrangler.toml", "tsconfig.json",
|
|
10
10
|
"Cargo.toml", "pyproject.toml", "pytest.ini", "setup.cfg", "go.mod",
|
|
11
|
-
"Makefile", "build.gradle", "build.gradle.kts", "gradlew",
|
|
11
|
+
"Makefile", "build.gradle", "build.gradle.kts", "gradlew", "pom.xml", "mvnw",
|
|
12
|
+
"DESCRIPTION", "renv.lock", "tests/testthat.R",
|
|
12
13
|
];
|
|
13
14
|
/** Prefer test before typecheck/lint so discovery order matches what pickVerificationCommand wants. */
|
|
14
15
|
const VERIFICATION_SCRIPTS = ["test", "verify", "typecheck", "lint", "build"];
|
|
15
16
|
export const MAKE_VERIFICATION_TARGETS = ["test", "check", "verify", "replay", "typecheck", "lint", "build"];
|
|
16
17
|
const EXCLUDED_DIRECTORIES = new Set(["node_modules", "dist", "build", "target", "coverage", ".venv", "venv"]);
|
|
18
|
+
function rDescriptionUsesTestthat(text) {
|
|
19
|
+
return [...text.matchAll(/^(?:Depends|Imports|Suggests):[^\n]*(?:\n[ \t]+[^\n]*)*/gmi)]
|
|
20
|
+
.some((field) => /\btestthat\b/i.test(field[0]));
|
|
21
|
+
}
|
|
17
22
|
export async function buildWorkspaceBrief(workspace) {
|
|
18
23
|
const entries = await readdir(workspace, { withFileTypes: true });
|
|
19
24
|
const modules = entries
|
|
20
25
|
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !EXCLUDED_DIRECTORIES.has(entry.name))
|
|
21
26
|
.map((entry) => entry.name).sort().slice(0, 30);
|
|
22
27
|
const files = new Set(entries.filter((entry) => entry.isFile()).map((entry) => entry.name));
|
|
28
|
+
try {
|
|
29
|
+
await access(join(workspace, "tests", "testthat.R"));
|
|
30
|
+
files.add("tests/testthat.R");
|
|
31
|
+
}
|
|
32
|
+
catch { /* optional nested R test entrypoint */ }
|
|
23
33
|
const manifests = MANIFESTS.filter((name) => files.has(name));
|
|
24
34
|
const declared = await readDeclaredVerification(workspace);
|
|
25
35
|
return {
|
|
@@ -258,8 +268,21 @@ export async function discoverVerificationCommands(workspace, files) {
|
|
|
258
268
|
}
|
|
259
269
|
}
|
|
260
270
|
if (files.has("build.gradle") || files.has("build.gradle.kts")) {
|
|
261
|
-
|
|
262
|
-
|
|
271
|
+
commands.push(files.has("gradlew") ? "./gradlew test" : "gradle test");
|
|
272
|
+
}
|
|
273
|
+
if (files.has("pom.xml")) {
|
|
274
|
+
commands.push(files.has("mvnw") ? "./mvnw test" : "mvn test");
|
|
275
|
+
}
|
|
276
|
+
if (files.has("DESCRIPTION")) {
|
|
277
|
+
try {
|
|
278
|
+
const description = await readFile(join(workspace, "DESCRIPTION"), "utf8");
|
|
279
|
+
if (rDescriptionUsesTestthat(description))
|
|
280
|
+
commands.push("Rscript -e testthat::test_local()");
|
|
281
|
+
}
|
|
282
|
+
catch { /* unreadable manifests do not broaden execution */ }
|
|
283
|
+
}
|
|
284
|
+
else if (files.has("tests/testthat.R")) {
|
|
285
|
+
commands.push("Rscript tests/testthat.R");
|
|
263
286
|
}
|
|
264
287
|
// The heartbeat schema caps `verification` at 10. A gate-rich checkout discovers more than that,
|
|
265
288
|
// and the whole heartbeat was then refused: the computer went dark with no reason on any card.
|
|
@@ -29,7 +29,7 @@ export function needsTestEvidence(spec, grants) {
|
|
|
29
29
|
* is not satisfied by a silent `npm run typecheck`.
|
|
30
30
|
*/
|
|
31
31
|
export function isPreferentialTestCommand(command) {
|
|
32
|
-
return /^(npm run test|pnpm test|yarn test|cargo test|go test(?: \.\/\.\.\.)?|make test|python3? -m (?:pytest|unittest)(?: [\w./=:-]+)*|pytest(?: [\w./=-]+)
|
|
32
|
+
return /^(npm run test|pnpm test|yarn test|cargo test|go test(?: \.\/\.\.\.)?|make test|python3? -m (?:pytest|unittest)(?: [\w./=:-]+)*|pytest(?: [\w./=-]+)?|(?:\.\/gradlew|gradle|\.\/mvnw|mvn) test|Rscript tests\/testthat\.R|Rscript -e testthat::test_local\(\))$/.test(command.trim());
|
|
33
33
|
}
|
|
34
34
|
export function pickVerificationCommand(commands) {
|
|
35
35
|
const bounded = commands
|
|
@@ -91,7 +91,7 @@ export function verificationCommandsMentioned(text, offered = []) {
|
|
|
91
91
|
const source = text.slice(0, 20_000);
|
|
92
92
|
const verbatim = [...new Set(offered.map((command) => command.trim()))]
|
|
93
93
|
.filter((command) => isBoundedVerificationCommand(command) && textNamesCommand(source, command));
|
|
94
|
-
const pattern = /(?:^|[\s("'`])((?:npm run [A-Za-z0-9][A-Za-z0-9._:-]*|pnpm [A-Za-z0-9][A-Za-z0-9._:-]*|yarn [A-Za-z0-9][A-Za-z0-9._:-]*|cargo (?:test|check)|go test(?: \.\/\.\.\.)?|make [A-Za-z0-9][A-Za-z0-9._-]*|python3? -m [A-Za-z0-9_][\w.]*(?: [\w./=:-]*[./=-][\w./=:-]*)*|pytest(?: [\w./=-]*[./=-][\w./=-]*)?|python3? [\w./-]+\.py(?: [\w./=:-]*[./=-][\w./=:-]*)
|
|
94
|
+
const pattern = /(?:^|[\s("'`])((?:npm run [A-Za-z0-9][A-Za-z0-9._:-]*|pnpm [A-Za-z0-9][A-Za-z0-9._:-]*|yarn [A-Za-z0-9][A-Za-z0-9._:-]*|cargo (?:test|check)|go test(?: \.\/\.\.\.)?|make [A-Za-z0-9][A-Za-z0-9._-]*|python3? -m [A-Za-z0-9_][\w.]*(?: [\w./=:-]*[./=-][\w./=:-]*)*|pytest(?: [\w./=-]*[./=-][\w./=-]*)?|python3? [\w./-]+\.py(?: [\w./=:-]*[./=-][\w./=:-]*)*|(?:\.\/gradlew|gradle|\.\/mvnw|mvn) test|Rscript tests\/testthat\.R|Rscript -e testthat::test_local\(\)))(?=$|[\s"'`),.;:])/g;
|
|
95
95
|
const mentions = [...new Set([...source.matchAll(pattern)].map((match) => match[1]))]
|
|
96
96
|
.filter((mention) => isBoundedVerificationCommand(mention)
|
|
97
97
|
&& !verbatim.some((command) => command === mention || command.startsWith(`${mention} `)));
|
|
@@ -423,6 +423,23 @@ export function detectGateRanNothing(command, stdout, stderr, code) {
|
|
|
423
423
|
if (/^go test\b/.test(trimmed) && code === 0 && /\[no test files\]/.test(combined) && !/^ok\s/m.test(combined)) {
|
|
424
424
|
return "gate ran no tests";
|
|
425
425
|
}
|
|
426
|
+
if (/^(?:\.\/mvnw|mvn) test$/.test(trimmed)
|
|
427
|
+
&& /\bNo tests to run\b/i.test(combined)
|
|
428
|
+
&& !/\bTests run:\s*[1-9]\d*/i.test(combined)) {
|
|
429
|
+
return "gate ran no tests";
|
|
430
|
+
}
|
|
431
|
+
if (/^(?:\.\/gradlew|gradle) test$/.test(trimmed)) {
|
|
432
|
+
const testTasks = combined.split(/\r?\n/)
|
|
433
|
+
.map((line) => line.trim().replace(/^> Task /, ""))
|
|
434
|
+
.filter((line) => /^:(?:[^:\s]+:)*test(?:\s|$)/.test(line));
|
|
435
|
+
if (testTasks.length > 0 && testTasks.every((line) => /\sNO-SOURCE\b/.test(line))) {
|
|
436
|
+
return "gate ran no tests";
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (/^Rscript (?:tests\/testthat\.R|-e testthat::test_local\(\))$/.test(trimmed)
|
|
440
|
+
&& /\bNo test files found\b/i.test(combined)) {
|
|
441
|
+
return "gate ran no tests";
|
|
442
|
+
}
|
|
426
443
|
const firstLine = combined.trim().split("\n")[0] ?? "";
|
|
427
444
|
if ((code === 1 || code === 2) && /^usage:/i.test(firstLine)) {
|
|
428
445
|
return "gate printed usage";
|
package/dist/execution-class.js
CHANGED
|
@@ -13,7 +13,7 @@ export function isBoundedVerificationCommand(command) {
|
|
|
13
13
|
// A `*` is a legal character in a Python argument (`-p test_*.py` is how a repository spells its
|
|
14
14
|
// own gate). Bridge starts a gate with execFile and no shell, so the star stays literal and no
|
|
15
15
|
// expansion can happen. Refusing it only dropped the declaration, with no reason given.
|
|
16
|
-
return /^(npm run [A-Za-z0-9][A-Za-z0-9._:-]*|pnpm [A-Za-z0-9][A-Za-z0-9._:-]*|yarn [A-Za-z0-9][A-Za-z0-9._:-]*|cargo (test|check)|go test(?: \.\/\.\.\.)?|make [A-Za-z0-9][A-Za-z0-9._-]*|python3? -m [A-Za-z0-9_][\w.]*(?: [\w./=:*-]+)*|pytest(?: [\w./=*-]+)?|python3? [\w./-]+\.py(?: [\w./=:-]+)
|
|
16
|
+
return /^(npm run [A-Za-z0-9][A-Za-z0-9._:-]*|pnpm [A-Za-z0-9][A-Za-z0-9._:-]*|yarn [A-Za-z0-9][A-Za-z0-9._:-]*|cargo (test|check)|go test(?: \.\/\.\.\.)?|make [A-Za-z0-9][A-Za-z0-9._-]*|python3? -m [A-Za-z0-9_][\w.]*(?: [\w./=:*-]+)*|pytest(?: [\w./=*-]+)?|python3? [\w./-]+\.py(?: [\w./=:-]+)*|(?:\.\/gradlew|gradle|\.\/mvnw|mvn) test|Rscript tests\/testthat\.R|Rscript -e testthat::test_local\(\))$/.test(command);
|
|
17
17
|
}
|
|
18
18
|
/** Shell commands each grant authorizes — mapped per driver so they cannot drift. */
|
|
19
19
|
export const branchCreateCommands = [
|
package/dist/execution-facts.js
CHANGED
|
@@ -15,6 +15,10 @@ export const BOOTSTRAP_RESULTS = ["not_applicable", "not_run", "installed", "fai
|
|
|
15
15
|
const LOCKFILE_NAMES = [
|
|
16
16
|
"package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml",
|
|
17
17
|
"Cargo.lock", "poetry.lock", "Pipfile.lock", "go.sum", "Gemfile.lock", "composer.lock",
|
|
18
|
+
"gradle.lockfile", "renv.lock",
|
|
19
|
+
];
|
|
20
|
+
const JAVASCRIPT_LOCKFILE_NAMES = [
|
|
21
|
+
"package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml",
|
|
18
22
|
];
|
|
19
23
|
/**
|
|
20
24
|
* The facts the control plane refuses to see change once it holds them, in the order it compares
|
|
@@ -114,7 +118,7 @@ export async function bootstrapResultForWorkspace(workspace) {
|
|
|
114
118
|
return "not_applicable";
|
|
115
119
|
}
|
|
116
120
|
try {
|
|
117
|
-
const lockfile = await Promise.any(
|
|
121
|
+
const lockfile = await Promise.any(JAVASCRIPT_LOCKFILE_NAMES.map(async (name) => {
|
|
118
122
|
await access(join(workspace, name));
|
|
119
123
|
return true;
|
|
120
124
|
}));
|
package/dist/execution.js
CHANGED
|
@@ -28,7 +28,7 @@ const execFileAsync = promisify(execFile);
|
|
|
28
28
|
import { captureVerificationFailure, detectGateRanNothing, ensureTestEvidence, needsTestEvidence, runBoundedVerificationCommand, verificationEvidenceCommands, VerificationGateRanNothingError } from "./ensure-test-evidence.js";
|
|
29
29
|
import { ensureChangeEvidence } from "./ensure-change-evidence.js";
|
|
30
30
|
import { ensureNormativeEvidence } from "./ensure-normative-evidence.js";
|
|
31
|
-
import { changedPathsSince, compareWorktreeState, worktreeStateFingerprint, worktreeWrittenPaths } from "./git-witness.js";
|
|
31
|
+
import { changedPathsSince, compareWorktreeState, diffStatSince, worktreeStateFingerprint, worktreeWrittenPaths } from "./git-witness.js";
|
|
32
32
|
import { assertNormativeRefsMaterialized, checkNormativeMarkers, materializeNormativeRefs, NORMATIVE_MARKER_PREFIX, NORMATIVE_REF_PREFIX, parseNormativeRefs, } from "./normative-refs.js";
|
|
33
33
|
import { checkIntegrationObligations, INTEGRATION_OBLIGATION_PREFIX, parseIntegrationObligations, } from "./integration-obligations.js";
|
|
34
34
|
/** Keep a Mac awake only while an assignment is active; display sleep remains allowed. */
|
|
@@ -1717,6 +1717,11 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
1717
1717
|
: null;
|
|
1718
1718
|
// The vendor named the model as the cause, so the model is what the retry must change. The
|
|
1719
1719
|
// model rides in the prefix: the control plane cannot read Bridge's selection any other way.
|
|
1720
|
+
const diffStat = verificationDetail && attemptWorkspace && startCommit
|
|
1721
|
+
? await diffStatSince(attemptWorkspace, startCommit)
|
|
1722
|
+
.then((stat) => stat?.slice(0, 8_000) ?? null)
|
|
1723
|
+
.catch(() => null)
|
|
1724
|
+
: null;
|
|
1720
1725
|
const abortPrefix = agentModelAbortPrefix(selection.model);
|
|
1721
1726
|
const modelAborted = agentModelAborted(`${agentMessage}\n${result.signal?.stderr_tail ?? ""}`);
|
|
1722
1727
|
// T52: the epoch's second abort is not a retryable model swap — the first one already bought
|
|
@@ -1724,7 +1729,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
1724
1729
|
// "Agent Looping Detected" runs on the cursor CLI default). Hold on the tier instead.
|
|
1725
1730
|
const bindingRequired = modelAborted && !verificationDetail && modelBindingRequired(selection);
|
|
1726
1731
|
const message = verificationDetail
|
|
1727
|
-
? `Verification failed after the agent run.\n${verificationDetail}`
|
|
1732
|
+
? `Verification failed after the agent run.\n${verificationDetail}${diffStat ? `\n\nChanged files:\n${diffStat}` : ""}`
|
|
1728
1733
|
: bindingRequired
|
|
1729
1734
|
? `${modelBindingRequiredPrefix(selection.tier, driver.name)}${agentMessage}`
|
|
1730
1735
|
: modelAborted ? `${abortPrefix}${agentMessage}` : agentMessage;
|
|
@@ -1744,7 +1749,7 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
1744
1749
|
? { phase: "agent", witness: "vendor", kind: "quota", vendor_message: agentMessage.slice(0, 2_000) }
|
|
1745
1750
|
: result.signal
|
|
1746
1751
|
? verificationDetail
|
|
1747
|
-
? { ...result.signal, phase: "verification", witness: "bridge", stderr_tail: undefined, stdout_tail: undefined }
|
|
1752
|
+
? { ...result.signal, phase: "verification", witness: "bridge", stderr_tail: undefined, stdout_tail: undefined, ...(diffStat ? { diff_stat: diffStat } : {}) }
|
|
1748
1753
|
: result.signal
|
|
1749
1754
|
: undefined;
|
|
1750
1755
|
const response = await queueTerminal(client, taskId, { action: "fail", body: {
|
|
@@ -2172,8 +2177,13 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
2172
2177
|
// exact tree and send the witness marker that arms Conductor diagnosis. Treating it as a
|
|
2173
2178
|
// delivery-envelope defect would stop before Invariant 23 ever ran.
|
|
2174
2179
|
const verificationFailed = /^Agent report: Verification failed \(/.test(message);
|
|
2180
|
+
const diffStat = verificationFailed && attemptWorkspace
|
|
2181
|
+
? await diffStatSince(attemptWorkspace, deliveryBaseCommit)
|
|
2182
|
+
.then((stat) => stat?.slice(0, 8_000) ?? null)
|
|
2183
|
+
.catch(() => null)
|
|
2184
|
+
: null;
|
|
2175
2185
|
const classified = verificationFailed
|
|
2176
|
-
? { retryable: true, error: `Verification failed after the agent run.\n${message}` }
|
|
2186
|
+
? { retryable: true, error: `Verification failed after the agent run.\n${message}${diffStat ? `\n\nChanged files:\n${diffStat}` : ""}` }
|
|
2177
2187
|
// classifyFinalizeFailure: forge transport → retryable; contract/environment/unknown → fail
|
|
2178
2188
|
// closed (unknown prefixed as "Bridge finalize interrupted", not laundered as contract).
|
|
2179
2189
|
: classifyFinalizeFailure(message);
|
|
@@ -2184,6 +2194,12 @@ async function runAttempt(client, config, driver, workspace, brief, taskId, watc
|
|
|
2184
2194
|
body: {
|
|
2185
2195
|
error: classified.error,
|
|
2186
2196
|
retryable: classified.retryable,
|
|
2197
|
+
...(verificationFailed ? { signal: {
|
|
2198
|
+
phase: "verification",
|
|
2199
|
+
witness: "bridge",
|
|
2200
|
+
kind: "verification_failed",
|
|
2201
|
+
...(diffStat ? { diff_stat: diffStat } : {}),
|
|
2202
|
+
} } : {}),
|
|
2187
2203
|
// A rework's own PR may already be open (ensureDeliveryPullRequest ran before this
|
|
2188
2204
|
// gate). Carry the URL so the control plane can close it instead of littering.
|
|
2189
2205
|
...(report.pull_request_url ? { pull_request_url: report.pull_request_url } : {}),
|
package/dist/failure-signal.js
CHANGED
|
@@ -24,6 +24,8 @@ export const failureSignalSchema = z.object({
|
|
|
24
24
|
paths: z.array(z.string().max(300)).max(20).optional(),
|
|
25
25
|
/** The base commit the dry run ran against, when a gate already fails there. */
|
|
26
26
|
base_commit: z.string().max(200).optional(),
|
|
27
|
+
/** Short git diff --stat of files modified in the attempt worktree. */
|
|
28
|
+
diff_stat: z.string().max(8_000).optional(),
|
|
27
29
|
exit_code: z.number().int().nullable().optional(),
|
|
28
30
|
/** The vendor envelope's own status, e.g. agy's "ERROR". */
|
|
29
31
|
vendor_status: z.string().max(200).optional(),
|
package/dist/git-witness.js
CHANGED
|
@@ -123,11 +123,20 @@ export function compareWorktreeState(before, after) {
|
|
|
123
123
|
/** Short stat summary for change evidence artifacts. */
|
|
124
124
|
export async function diffStatSince(worktree, startCommit) {
|
|
125
125
|
try {
|
|
126
|
-
const { stdout }
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
const [{ stdout }, { stdout: untracked }] = await Promise.all([
|
|
127
|
+
execFileAsync("git", ["-C", worktree, "diff", "--stat", startCommit], {
|
|
128
|
+
timeout: 30_000,
|
|
129
|
+
maxBuffer: 2_000_000,
|
|
130
|
+
}),
|
|
131
|
+
execFileAsync("git", ["-C", worktree, "ls-files", "--others", "--exclude-standard", "-z"], {
|
|
132
|
+
timeout: 30_000,
|
|
133
|
+
maxBuffer: 2_000_000,
|
|
134
|
+
encoding: "buffer",
|
|
135
|
+
}),
|
|
136
|
+
]);
|
|
137
|
+
const untrackedLines = untracked.toString("utf8").split("\0").filter(Boolean)
|
|
138
|
+
.map((path) => ` ${path} | untracked`);
|
|
139
|
+
const text = [stdout.trim(), ...untrackedLines].filter(Boolean).join("\n");
|
|
131
140
|
return text || null;
|
|
132
141
|
}
|
|
133
142
|
catch {
|
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.103",
|
|
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": {
|