@miraland-labs/conduit-bridge 0.14.7 → 0.14.9
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 +1 -1
- package/dist/execution.js +76 -6
- package/package.json +1 -1
package/dist/driver.js
CHANGED
|
@@ -15,7 +15,7 @@ function deliveryLanguageRule(language) {
|
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
17
|
export const evidenceKinds = ["change", "test", "preview", "research", "documentation"];
|
|
18
|
-
export const agentReportTemplate = '{"outcome": "one-paragraph summary", "changes": ["path — what changed"], "verification": ["command — result"], "acceptance_results": [{"criterion": "exact criterion text", "status": "met|not_met|unknown", "unverified_reason": "omit unless status is unknown", "unverified_detail": "one sentence naming what was missing; omit unless status is unknown"}], "evidence": [{"kind": "change|test|preview|research|documentation", "name": "concise evidence name", "uri": "external URL if one exists", "digest": "optional digest", "details": ["observable result"], "acceptance_criteria": ["exact criterion text supported by this evidence"]}], "assumptions": [], "risks": [], "limitations": [], "head_commit": "full sha of your final commit, omit if none"}';
|
|
18
|
+
export const agentReportTemplate = '{"outcome": "one-paragraph summary", "changes": ["path from the repository root — what changed"], "verification": ["command — result"], "acceptance_results": [{"criterion": "exact criterion text", "status": "met|not_met|unknown", "unverified_reason": "omit unless status is unknown", "unverified_detail": "one sentence naming what was missing; omit unless status is unknown"}], "evidence": [{"kind": "change|test|preview|research|documentation", "name": "concise evidence name", "uri": "external URL if one exists", "digest": "optional digest", "details": ["observable result"], "acceptance_criteria": ["exact criterion text supported by this evidence"]}], "assumptions": [], "risks": [], "limitations": [], "head_commit": "full sha of your final commit, omit if none"}';
|
|
19
19
|
/**
|
|
20
20
|
* Resolve an operator's ordered tier candidates against the live model list.
|
|
21
21
|
* Picks the first safe candidate the CLI currently offers; without a live list
|
package/dist/execution.js
CHANGED
|
@@ -7,8 +7,11 @@ import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, ext
|
|
|
7
7
|
import { assertClassFloor } from "./execution-class.js";
|
|
8
8
|
import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
9
9
|
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
10
12
|
import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
|
|
11
13
|
import { ensureDeliveryPullRequest } from "./ensure-pull-request.js";
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
12
15
|
import { captureVerificationFailure, ensureTestEvidence } from "./ensure-test-evidence.js";
|
|
13
16
|
/** Keep a Mac awake only while an assignment is active; display sleep remains allowed. */
|
|
14
17
|
export function startIdleSleepGuard(options = {}) {
|
|
@@ -969,7 +972,12 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
969
972
|
grants,
|
|
970
973
|
verificationCommands: attemptBrief?.verification ?? [],
|
|
971
974
|
});
|
|
972
|
-
|
|
975
|
+
// Ask git what changed before trusting what the report says changed.
|
|
976
|
+
const startCommit = executionContract.requested_base_commit && executionContract.claimed_head
|
|
977
|
+
? await resolveAttemptStartCommit(workspace, executionContract.requested_base_commit, executionContract.claimed_head)
|
|
978
|
+
: null;
|
|
979
|
+
const actualPaths = startCommit ? await changedPathsSince(attemptWorkspace, startCommit) : null;
|
|
980
|
+
validateDeliveryReport(report, spec, grants, actualPaths ?? undefined);
|
|
973
981
|
}
|
|
974
982
|
catch (error) {
|
|
975
983
|
const message = error instanceof Error ? error.message : "Agent delivery report was invalid";
|
|
@@ -1198,13 +1206,13 @@ async function prepareDelivery(client, attemptId, taskId, report) {
|
|
|
1198
1206
|
},
|
|
1199
1207
|
};
|
|
1200
1208
|
}
|
|
1201
|
-
export function validateDeliveryReport(report, spec, grants = []) {
|
|
1209
|
+
export function validateDeliveryReport(report, spec, grants = [], actualPaths) {
|
|
1202
1210
|
// Research findings live in the report. Do not demand a published pack URL for work_role=research
|
|
1203
1211
|
// even when a planner mis-labeled the package as deliverable=artifact.
|
|
1204
1212
|
const artifactDelivery = spec.deliverable === "artifact" && spec.work_role !== "research";
|
|
1205
1213
|
// Artifact packs are not git landings — do not demand head_commit when the planner put
|
|
1206
1214
|
// `.conduit/artifacts/**` in change_scope as a write hint (path-4 canary incident).
|
|
1207
|
-
validateChangeScope(report, spec.change_scope ?? [], { requireHeadCommit: !artifactDelivery });
|
|
1215
|
+
validateChangeScope(report, spec.change_scope ?? [], { requireHeadCommit: !artifactDelivery, actualPaths });
|
|
1208
1216
|
if (artifactDelivery) {
|
|
1209
1217
|
const published = report.evidence.some((item) => {
|
|
1210
1218
|
if (!["preview", "research", "documentation"].includes(item.kind))
|
|
@@ -1271,6 +1279,45 @@ function referenceKind(kind) {
|
|
|
1271
1279
|
return "document";
|
|
1272
1280
|
return "other";
|
|
1273
1281
|
}
|
|
1282
|
+
/**
|
|
1283
|
+
* What this attempt actually changed, according to git rather than to the agent's prose.
|
|
1284
|
+
*
|
|
1285
|
+
* The scope check used to compare approved paths against strings the agent wrote about itself, so a
|
|
1286
|
+
* run that edited an approved file and named it "tools.ts" failed the contract and spent an attempt
|
|
1287
|
+
* on a path format. Git knows the truth, and Bridge holds the worktree: this is both stricter (a
|
|
1288
|
+
* change cannot be omitted from the report to escape the check) and more forgiving (how the agent
|
|
1289
|
+
* phrases a path stops mattering).
|
|
1290
|
+
*
|
|
1291
|
+
* Null when git cannot answer — an unknown must not fail a delivery, so the caller falls back to the
|
|
1292
|
+
* report exactly as before.
|
|
1293
|
+
*/
|
|
1294
|
+
export async function changedPathsSince(worktree, startCommit) {
|
|
1295
|
+
const run = async (args) => {
|
|
1296
|
+
try {
|
|
1297
|
+
const { stdout } = await execFileAsync("git", ["-C", worktree, ...args], { timeout: 30_000, maxBuffer: 8_000_000 });
|
|
1298
|
+
return stdout;
|
|
1299
|
+
}
|
|
1300
|
+
catch {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
const committed = await run(["diff", "--name-only", `${startCommit}..HEAD`]);
|
|
1305
|
+
if (committed === null)
|
|
1306
|
+
return null;
|
|
1307
|
+
const paths = new Set(committed.split("\n").map((line) => line.trim()).filter(Boolean));
|
|
1308
|
+
// Work the agent left uncommitted still lands in the delivery for lands=false packages.
|
|
1309
|
+
const pending = await run(["status", "--porcelain"]);
|
|
1310
|
+
for (const line of (pending ?? "").split("\n")) {
|
|
1311
|
+
const entry = line.slice(3).trim();
|
|
1312
|
+
if (!entry)
|
|
1313
|
+
continue;
|
|
1314
|
+
// A rename reports "old -> new"; the new path is the one that exists.
|
|
1315
|
+
const path = entry.includes(" -> ") ? entry.slice(entry.lastIndexOf(" -> ") + 4).trim() : entry;
|
|
1316
|
+
if (path)
|
|
1317
|
+
paths.add(path.replace(/^"|"$/g, ""));
|
|
1318
|
+
}
|
|
1319
|
+
return [...paths];
|
|
1320
|
+
}
|
|
1274
1321
|
export function validateChangeScope(report, changeScope, options = {}) {
|
|
1275
1322
|
if (!changeScope.length)
|
|
1276
1323
|
return;
|
|
@@ -1278,13 +1325,36 @@ export function validateChangeScope(report, changeScope, options = {}) {
|
|
|
1278
1325
|
if (requireHeadCommit && !report.head_commit) {
|
|
1279
1326
|
throw new Error("Repository changes require a delivered head commit");
|
|
1280
1327
|
}
|
|
1281
|
-
|
|
1328
|
+
// Git's answer when we have it; the agent's description only when we do not.
|
|
1329
|
+
const paths = options.actualPaths
|
|
1330
|
+
?? report.changes.map((change) => change.split(/\s+[—-]\s+/, 1)[0].trim().replaceAll("\\", "/"));
|
|
1331
|
+
if (!paths.length)
|
|
1282
1332
|
return;
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1333
|
+
const outside = paths.filter((path) => {
|
|
1334
|
+
if (!path || path.startsWith("/") || path.split("/").includes(".."))
|
|
1335
|
+
return true;
|
|
1336
|
+
if (changeScope.some((scope) => pathMatchesScope(path, scope)))
|
|
1337
|
+
return false;
|
|
1338
|
+
return !namesOneScopedFile(path, changeScope);
|
|
1339
|
+
});
|
|
1285
1340
|
if (outside.length)
|
|
1286
1341
|
throw new Error(`Agent reported changes outside the approved scope: ${outside.join(", ")}`);
|
|
1287
1342
|
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Did the agent name an approved file the short way?
|
|
1345
|
+
*
|
|
1346
|
+
* A run that edited src/lib/tools.ts and reported "tools.ts" did the approved work and described it
|
|
1347
|
+
* loosely. Rejecting that spends the attempt, fails the contract, and puts a decision in front of
|
|
1348
|
+
* the owner over a path format — and the rerun reports it the same way. Accept the shorthand only
|
|
1349
|
+
* when it can mean exactly one approved file: with two candidates we genuinely cannot tell which was
|
|
1350
|
+
* touched, so it stays a violation.
|
|
1351
|
+
*/
|
|
1352
|
+
function namesOneScopedFile(path, changeScope) {
|
|
1353
|
+
const candidates = changeScope
|
|
1354
|
+
.map((scope) => scope.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""))
|
|
1355
|
+
.filter((scope) => !scope.includes("*") && scope.endsWith(`/${path}`));
|
|
1356
|
+
return candidates.length === 1;
|
|
1357
|
+
}
|
|
1288
1358
|
function pathMatchesScope(path, scope) {
|
|
1289
1359
|
const normalized = scope.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
1290
1360
|
if (normalized.endsWith("/**")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.9",
|
|
4
4
|
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|