@miraland-labs/conduit-bridge 0.16.5 → 0.16.7
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 +25 -1
- package/dist/ensure-change-evidence.js +55 -0
- package/dist/ensure-normative-evidence.js +22 -0
- package/dist/execution.js +84 -43
- package/dist/git-witness.js +52 -0
- package/dist/integration-obligations.js +69 -0
- package/dist/land-git-reality.js +1 -1
- package/dist/normative-refs.js +217 -0
- package/package.json +1 -1
package/dist/driver.js
CHANGED
|
@@ -99,6 +99,28 @@ export function buildAssignmentPrompt(context) {
|
|
|
99
99
|
}
|
|
100
100
|
if (changeScope?.length)
|
|
101
101
|
lines.push("", `CHANGE SCOPE — only modify paths under\n${changeScope.map((item) => `- ${item}`).join("\n")}`);
|
|
102
|
+
const integrationObligations = spec.integration_obligations ?? [];
|
|
103
|
+
if (integrationObligations.length) {
|
|
104
|
+
lines.push("", "INTEGRATION OBLIGATIONS — finalize fails unless these hold");
|
|
105
|
+
for (const obligation of integrationObligations) {
|
|
106
|
+
if (obligation.kind !== "symbol_referenced_from")
|
|
107
|
+
continue;
|
|
108
|
+
const globs = obligation.from_globs?.length ? obligation.from_globs.join(", ") : "runtime entrypoints";
|
|
109
|
+
lines.push(`- symbol "${obligation.symbol}" must be referenced from ${globs}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (context.normativeRefs?.length) {
|
|
113
|
+
lines.push("", "NORMATIVE REFERENCES — read-only contract files; your implementation must align. Do not edit anything under .conduit/normative/.");
|
|
114
|
+
for (const ref of context.normativeRefs) {
|
|
115
|
+
lines.push(`- ${ref.label} (${ref.repository} @ ${ref.revision.slice(0, 12)})`);
|
|
116
|
+
for (const path of ref.workspace_paths)
|
|
117
|
+
lines.push(` - ${path}`);
|
|
118
|
+
const specRef = context.spec.normative_refs?.find((entry) => entry.label === ref.label);
|
|
119
|
+
if (specRef?.markers?.length) {
|
|
120
|
+
lines.push(` - Required markers in changed files: ${specRef.markers.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
102
124
|
if (spec.repository?.base_commit)
|
|
103
125
|
lines.push("", `The workspace is expected to be at or after base commit ${spec.repository.base_commit}.`);
|
|
104
126
|
if (context.currentHead)
|
|
@@ -127,7 +149,9 @@ export function buildAssignmentPrompt(context) {
|
|
|
127
149
|
mustCommit,
|
|
128
150
|
mustOpenPr,
|
|
129
151
|
});
|
|
130
|
-
lines.push("", "RULES", "- Stay within the change scope and boundaries.", ...(
|
|
152
|
+
lines.push("", "RULES", "- Stay within the change scope and boundaries.", ...(context.normativeRefs?.length
|
|
153
|
+
? ["- When normative references are listed, treat them as the authoritative contract for any cross-repo API, signature, or protocol shape they describe."]
|
|
154
|
+
: []), ...(languageRule ? [languageRule] : []), ...classRules, "- Never merge, deploy, force-push, push to protected branches, or touch production.", `- Hard-denied commands (all drivers): ${deniedCommands.join("; ")}.`, "- Do not invent evidence. Report unknown when you could not verify a criterion.",
|
|
131
155
|
// The reviewer requests changes when "verification output is only summarized (not verbatim)"
|
|
132
156
|
// (REVIEWER_PROMPT). Only the bounded-shell rule said to report real output, so publish_artifact
|
|
133
157
|
// — the class the reviewer scrutinises hardest — summarized, and every criterion came back
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { changedPathsSince, diffStatSince } from "./git-witness.js";
|
|
2
|
+
export function needsChangeEvidence(_report, spec, grants) {
|
|
3
|
+
if (!grants.includes("repo_write"))
|
|
4
|
+
return false;
|
|
5
|
+
return (spec.required_evidence ?? []).includes("change");
|
|
6
|
+
}
|
|
7
|
+
/** Reuse criteria the agent mapped onto change-kind evidence. */
|
|
8
|
+
export function criteriaForChangeEvidence(report) {
|
|
9
|
+
const mapped = new Set();
|
|
10
|
+
for (const item of report.evidence) {
|
|
11
|
+
if (item.kind !== "change")
|
|
12
|
+
continue;
|
|
13
|
+
for (const criterion of item.acceptance_criteria ?? []) {
|
|
14
|
+
if (criterion)
|
|
15
|
+
mapped.add(criterion);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...mapped];
|
|
19
|
+
}
|
|
20
|
+
export async function ensureChangeEvidence(input) {
|
|
21
|
+
if (!needsChangeEvidence(input.report, input.spec, input.grants))
|
|
22
|
+
return input.report;
|
|
23
|
+
const paths = input.changedPaths
|
|
24
|
+
?? (input.baseCommit ? await changedPathsSince(input.workspace, input.baseCommit) : null);
|
|
25
|
+
if (paths === null)
|
|
26
|
+
return input.report;
|
|
27
|
+
if (!paths.length) {
|
|
28
|
+
return {
|
|
29
|
+
...input.report,
|
|
30
|
+
evidence: input.report.evidence.filter((item) => item.kind !== "change"),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const stat = input.baseCommit ? await diffStatSince(input.workspace, input.baseCommit) : null;
|
|
34
|
+
const baseLabel = input.baseCommit?.slice(0, 12) ?? "base";
|
|
35
|
+
const details = [
|
|
36
|
+
`git diff --name-only ${baseLabel}..HEAD`,
|
|
37
|
+
...paths,
|
|
38
|
+
...(stat ? ["", stat] : []),
|
|
39
|
+
];
|
|
40
|
+
const withoutAgentChange = input.report.evidence.filter((item) => item.kind !== "change");
|
|
41
|
+
const criteria = criteriaForChangeEvidence(input.report);
|
|
42
|
+
return {
|
|
43
|
+
...input.report,
|
|
44
|
+
evidence: [
|
|
45
|
+
...withoutAgentChange,
|
|
46
|
+
{
|
|
47
|
+
kind: "change",
|
|
48
|
+
name: "Repository diff (Bridge witness)",
|
|
49
|
+
details,
|
|
50
|
+
acceptance_criteria: criteria,
|
|
51
|
+
witness: "bridge",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function ensureNormativeEvidence(input) {
|
|
2
|
+
if (!input.materialized.length)
|
|
3
|
+
return input.report;
|
|
4
|
+
const without = input.report.evidence.filter((item) => item.name !== "Normative references (Bridge witness)");
|
|
5
|
+
const details = input.materialized.flatMap((entry) => [
|
|
6
|
+
`${entry.label} @ ${entry.repository} (${entry.revision.slice(0, 12)})`,
|
|
7
|
+
...entry.workspace_paths.map((path) => ` ${path}`),
|
|
8
|
+
]);
|
|
9
|
+
return {
|
|
10
|
+
...input.report,
|
|
11
|
+
evidence: [
|
|
12
|
+
...without,
|
|
13
|
+
{
|
|
14
|
+
kind: "documentation",
|
|
15
|
+
name: "Normative references (Bridge witness)",
|
|
16
|
+
details,
|
|
17
|
+
acceptance_criteria: [],
|
|
18
|
+
witness: "bridge",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/execution.js
CHANGED
|
@@ -16,6 +16,11 @@ import { AgentNoLandCommitError, agentNoLandCommitMessage, requiresLandCommit }
|
|
|
16
16
|
import { agentClaimsRepositoryWork, claimsVsGitMismatch, isLandTreeEmpty, reportClaimsRepositoryWork, } from "./land-git-reality.js";
|
|
17
17
|
const execFileAsync = promisify(execFile);
|
|
18
18
|
import { captureVerificationFailure, ensureTestEvidence } from "./ensure-test-evidence.js";
|
|
19
|
+
import { ensureChangeEvidence } from "./ensure-change-evidence.js";
|
|
20
|
+
import { ensureNormativeEvidence } from "./ensure-normative-evidence.js";
|
|
21
|
+
import { changedPathsSince } from "./git-witness.js";
|
|
22
|
+
import { assertNormativeRefsMaterialized, checkNormativeMarkers, materializeNormativeRefs, NORMATIVE_MARKER_PREFIX, NORMATIVE_REF_PREFIX, parseNormativeRefs, } from "./normative-refs.js";
|
|
23
|
+
import { checkIntegrationObligations, INTEGRATION_OBLIGATION_PREFIX, parseIntegrationObligations, } from "./integration-obligations.js";
|
|
19
24
|
/** Keep a Mac awake only while an assignment is active; display sleep remains allowed. */
|
|
20
25
|
export function startIdleSleepGuard(options = {}) {
|
|
21
26
|
if ((options.platform ?? process.platform) !== "darwin")
|
|
@@ -66,7 +71,7 @@ export function forgeTransportFailure(message) {
|
|
|
66
71
|
/** Environment faults that must Hold — mirror CP ENVIRONMENT_FAILURES message patterns. */
|
|
67
72
|
const FINALIZE_ENVIRONMENT_PATTERN = /base[_ ]not[_ ]ancestor|required base commit is not available|source[_ ]workspace[_ ]dirty|uncommitted changes|dirty workspace|workspace[_ ]head[_ ]changed|workspace[_ ]repository|workspace[_ ]unavailable|driver[_ ]not[_ ]authenticated|not logged in|no login|not authenticated|login required|no[_ ]online[_ ]driver|bridge[_ ]preflight|stale bridge|bridge version/i;
|
|
68
73
|
/** Delivery-report / grant / evidence contract defects (non-retryable rework). */
|
|
69
|
-
const FINALIZE_CONTRACT_PATTERN = /missing required evidence|without the repo_write grant|pull_request_url|Artifact delivery requires|outside the approved scope|head commit|test evidence must include|Met acceptance criteria|Agent report|Agent reported|Agent did not land repository changes|Delivery report/i;
|
|
74
|
+
const FINALIZE_CONTRACT_PATTERN = /missing required evidence|without the repo_write grant|pull_request_url|Artifact delivery requires|outside the approved scope|head commit|test evidence must include|Met acceptance criteria|Agent report|Agent reported|Agent did not land repository changes|Delivery report|Integration obligation not met|Normative reference unavailable|Normative marker not found/i;
|
|
70
75
|
/**
|
|
71
76
|
* Classify throws from ensureDeliveryPullRequest + validateDeliveryReport.
|
|
72
77
|
* Unknown non-contract faults must not be laundered as execution_contract_failed.
|
|
@@ -125,6 +130,18 @@ const taskSpecSchema = z.object({
|
|
|
125
130
|
instruction: z.string().optional(),
|
|
126
131
|
evidence_standard: z.unknown().optional(),
|
|
127
132
|
working_language: z.enum(["en", "zh"]).optional(),
|
|
133
|
+
integration_obligations: z.array(z.object({
|
|
134
|
+
kind: z.literal("symbol_referenced_from"),
|
|
135
|
+
symbol: z.string(),
|
|
136
|
+
from_globs: z.array(z.string()).optional(),
|
|
137
|
+
})).optional(),
|
|
138
|
+
normative_refs: z.array(z.object({
|
|
139
|
+
label: z.string(),
|
|
140
|
+
repository: z.string(),
|
|
141
|
+
paths: z.array(z.string()),
|
|
142
|
+
revision: z.string().optional(),
|
|
143
|
+
markers: z.array(z.string()).optional(),
|
|
144
|
+
})).optional(),
|
|
128
145
|
});
|
|
129
146
|
const executionContractSchema = z.object({
|
|
130
147
|
repository_fingerprint: z.string().nullable().optional().default(null),
|
|
@@ -612,6 +629,34 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
612
629
|
}
|
|
613
630
|
await client.updateAttempt(taskId, { worktreePath: attemptWorkspace });
|
|
614
631
|
}
|
|
632
|
+
let normativeMaterialized = [];
|
|
633
|
+
if (!diagnosis) {
|
|
634
|
+
const normativeRefs = parseNormativeRefs(spec.normative_refs);
|
|
635
|
+
if (normativeRefs.length) {
|
|
636
|
+
try {
|
|
637
|
+
normativeMaterialized = await materializeNormativeRefs({
|
|
638
|
+
sourceWorkspace: workspace,
|
|
639
|
+
attemptWorkspace,
|
|
640
|
+
refs: normativeRefs,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
645
|
+
const retryable = message.startsWith(NORMATIVE_REF_PREFIX)
|
|
646
|
+
&& /could not clone|could not checkout|timed out|Could not resolve|connection/i.test(message);
|
|
647
|
+
await queueTerminal(client, taskId, {
|
|
648
|
+
action: "fail",
|
|
649
|
+
body: {
|
|
650
|
+
error: message,
|
|
651
|
+
retryable,
|
|
652
|
+
idempotency_key: `bridge:normative-ref:${active.attemptId}`,
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
console.error(`Assignment ${taskId} normative reference materialization failed: ${redactSecrets(message)}`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
615
660
|
// Diagnosis reads manifests and verification commands from the failed tree itself. The source
|
|
616
661
|
// checkout may be clean but stale relative to edits that caused the failure.
|
|
617
662
|
const attemptBrief = diagnosis
|
|
@@ -707,7 +752,19 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
707
752
|
failureOutput: diagnosticFailure,
|
|
708
753
|
verificationCommands: attemptBrief?.verification ?? [],
|
|
709
754
|
})
|
|
710
|
-
: buildAssignmentPrompt({
|
|
755
|
+
: buildAssignmentPrompt({
|
|
756
|
+
taskId,
|
|
757
|
+
objective: task.objective,
|
|
758
|
+
spec,
|
|
759
|
+
grants,
|
|
760
|
+
workspace: attemptWorkspace,
|
|
761
|
+
currentHead: worktreeStart,
|
|
762
|
+
reworkFeedback,
|
|
763
|
+
workPackage,
|
|
764
|
+
verificationCommands: attemptBrief?.verification ?? [],
|
|
765
|
+
executionClass,
|
|
766
|
+
normativeRefs: normativeMaterialized,
|
|
767
|
+
});
|
|
711
768
|
const resuming = Boolean(options.forceResumeSessionId);
|
|
712
769
|
await client.attemptRequest(taskId, "progress", {
|
|
713
770
|
phase: diagnosis ? "inspecting" : "changing",
|
|
@@ -1151,6 +1208,10 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
1151
1208
|
title: task.objective,
|
|
1152
1209
|
repositoryFingerprint: executionContract.repository_fingerprint,
|
|
1153
1210
|
});
|
|
1211
|
+
// Ask git what changed before trusting what the report says changed.
|
|
1212
|
+
const actualPaths = /^[0-9a-f]{7,40}$/i.test(worktreeStart)
|
|
1213
|
+
? await changedPathsSince(attemptWorkspace, worktreeStart)
|
|
1214
|
+
: null;
|
|
1154
1215
|
// Mechanical test path: agent often forgets verbatim make test / npm test output.
|
|
1155
1216
|
report = await ensureTestEvidence({
|
|
1156
1217
|
workspace: attemptWorkspace,
|
|
@@ -1159,11 +1220,25 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
1159
1220
|
grants,
|
|
1160
1221
|
verificationCommands: attemptBrief?.verification ?? [],
|
|
1161
1222
|
});
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
?
|
|
1165
|
-
|
|
1166
|
-
|
|
1223
|
+
report = await ensureChangeEvidence({
|
|
1224
|
+
workspace: attemptWorkspace,
|
|
1225
|
+
baseCommit: /^[0-9a-f]{7,40}$/i.test(worktreeStart) ? worktreeStart : startCommit,
|
|
1226
|
+
report,
|
|
1227
|
+
spec,
|
|
1228
|
+
grants,
|
|
1229
|
+
changedPaths: actualPaths,
|
|
1230
|
+
});
|
|
1231
|
+
const integrationCheck = await checkIntegrationObligations(attemptWorkspace, parseIntegrationObligations(spec.integration_obligations));
|
|
1232
|
+
if (!integrationCheck.ok) {
|
|
1233
|
+
throw new Error(`${INTEGRATION_OBLIGATION_PREFIX}: ${integrationCheck.failures.join("; ")}`);
|
|
1234
|
+
}
|
|
1235
|
+
const normativeRefs = parseNormativeRefs(spec.normative_refs);
|
|
1236
|
+
const markerCheck = await checkNormativeMarkers(attemptWorkspace, normativeRefs, actualPaths);
|
|
1237
|
+
if (!markerCheck.ok) {
|
|
1238
|
+
throw new Error(`${NORMATIVE_MARKER_PREFIX}: ${markerCheck.failures.join("; ")}`);
|
|
1239
|
+
}
|
|
1240
|
+
assertNormativeRefsMaterialized(normativeRefs, normativeMaterialized);
|
|
1241
|
+
report = ensureNormativeEvidence({ report, spec, materialized: normativeMaterialized });
|
|
1167
1242
|
validateDeliveryReport(report, spec, grants, actualPaths ?? undefined);
|
|
1168
1243
|
}
|
|
1169
1244
|
catch (error) {
|
|
@@ -1582,43 +1657,9 @@ function referenceKind(kind) {
|
|
|
1582
1657
|
}
|
|
1583
1658
|
/**
|
|
1584
1659
|
* What this attempt actually changed, according to git rather than to the agent's prose.
|
|
1585
|
-
*
|
|
1586
|
-
* The scope check used to compare approved paths against strings the agent wrote about itself, so a
|
|
1587
|
-
* run that edited an approved file and named it "tools.ts" failed the contract and spent an attempt
|
|
1588
|
-
* on a path format. Git knows the truth, and Bridge holds the worktree: this is both stricter (a
|
|
1589
|
-
* change cannot be omitted from the report to escape the check) and more forgiving (how the agent
|
|
1590
|
-
* phrases a path stops mattering).
|
|
1591
|
-
*
|
|
1592
|
-
* Null when git cannot answer — an unknown must not fail a delivery, so the caller falls back to the
|
|
1593
|
-
* report exactly as before.
|
|
1660
|
+
* Implemented in git-witness.ts; re-exported for control-plane test pins and investigation.
|
|
1594
1661
|
*/
|
|
1595
|
-
export
|
|
1596
|
-
const run = async (args) => {
|
|
1597
|
-
try {
|
|
1598
|
-
const { stdout } = await execFileAsync("git", ["-C", worktree, ...args], { timeout: 30_000, maxBuffer: 8_000_000 });
|
|
1599
|
-
return stdout;
|
|
1600
|
-
}
|
|
1601
|
-
catch {
|
|
1602
|
-
return null;
|
|
1603
|
-
}
|
|
1604
|
-
};
|
|
1605
|
-
const committed = await run(["diff", "--name-only", `${startCommit}..HEAD`]);
|
|
1606
|
-
if (committed === null)
|
|
1607
|
-
return null;
|
|
1608
|
-
const paths = new Set(committed.split("\n").map((line) => line.trim()).filter(Boolean));
|
|
1609
|
-
// Work the agent left uncommitted still lands in the delivery for lands=false packages.
|
|
1610
|
-
const pending = await run(["status", "--porcelain"]);
|
|
1611
|
-
for (const line of (pending ?? "").split("\n")) {
|
|
1612
|
-
const entry = line.slice(3).trim();
|
|
1613
|
-
if (!entry)
|
|
1614
|
-
continue;
|
|
1615
|
-
// A rename reports "old -> new"; the new path is the one that exists.
|
|
1616
|
-
const path = entry.includes(" -> ") ? entry.slice(entry.lastIndexOf(" -> ") + 4).trim() : entry;
|
|
1617
|
-
if (path)
|
|
1618
|
-
paths.add(path.replace(/^"|"$/g, ""));
|
|
1619
|
-
}
|
|
1620
|
-
return [...paths];
|
|
1621
|
-
}
|
|
1662
|
+
export { changedPathsSince } from "./git-witness.js";
|
|
1622
1663
|
export function validateChangeScope(report, changeScope, options = {}) {
|
|
1623
1664
|
if (!changeScope.length)
|
|
1624
1665
|
return;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git facts Bridge witnesses during finalize — not agent prose.
|
|
3
|
+
*/
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
async function gitLines(worktree, args) {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout } = await execFileAsync("git", ["-C", worktree, ...args], {
|
|
10
|
+
timeout: 30_000,
|
|
11
|
+
maxBuffer: 8_000_000,
|
|
12
|
+
});
|
|
13
|
+
return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Paths changed on the attempt branch since `startCommit`, including uncommitted work.
|
|
21
|
+
* Null when git cannot answer — callers must not treat null as failure.
|
|
22
|
+
*/
|
|
23
|
+
export async function changedPathsSince(worktree, startCommit) {
|
|
24
|
+
const committed = await gitLines(worktree, ["diff", "--name-only", `${startCommit}..HEAD`]);
|
|
25
|
+
if (committed === null)
|
|
26
|
+
return null;
|
|
27
|
+
const paths = new Set(committed);
|
|
28
|
+
const pending = await gitLines(worktree, ["status", "--porcelain"]);
|
|
29
|
+
for (const line of pending ?? []) {
|
|
30
|
+
const entry = line.slice(3).trim();
|
|
31
|
+
if (!entry)
|
|
32
|
+
continue;
|
|
33
|
+
const path = entry.includes(" -> ") ? entry.slice(entry.lastIndexOf(" -> ") + 4).trim() : entry;
|
|
34
|
+
if (path)
|
|
35
|
+
paths.add(path.replace(/^"|"$/g, ""));
|
|
36
|
+
}
|
|
37
|
+
return [...paths];
|
|
38
|
+
}
|
|
39
|
+
/** Short stat summary for change evidence artifacts. */
|
|
40
|
+
export async function diffStatSince(worktree, startCommit) {
|
|
41
|
+
try {
|
|
42
|
+
const { stdout } = await execFileAsync("git", ["-C", worktree, "diff", "--stat", `${startCommit}..HEAD`], {
|
|
43
|
+
timeout: 30_000,
|
|
44
|
+
maxBuffer: 2_000_000,
|
|
45
|
+
});
|
|
46
|
+
const text = stdout.trim();
|
|
47
|
+
return text || null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration obligations — planner-authored wiring checks, enforced at Bridge finalize.
|
|
3
|
+
*
|
|
4
|
+
* Generic across stacks: a new implementation must be referenced from runtime entrypoints,
|
|
5
|
+
* not only defined in an isolated module.
|
|
6
|
+
*/
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
export const integrationObligationSchema = z.discriminatedUnion("kind", [
|
|
12
|
+
z.object({
|
|
13
|
+
kind: z.literal("symbol_referenced_from"),
|
|
14
|
+
symbol: z.string().trim().min(1).max(200),
|
|
15
|
+
from_globs: z.array(z.string().trim().min(1).max(200)).max(20).optional(),
|
|
16
|
+
}),
|
|
17
|
+
]);
|
|
18
|
+
/** Default runtime entrypoint globs when the planner omits from_globs. */
|
|
19
|
+
export const DEFAULT_ENTRYPOINT_GLOBS = [
|
|
20
|
+
"**/main.rs",
|
|
21
|
+
"**/lib.rs",
|
|
22
|
+
"**/mod.rs",
|
|
23
|
+
"**/index.ts",
|
|
24
|
+
"**/index.tsx",
|
|
25
|
+
"**/index.js",
|
|
26
|
+
"**/App.tsx",
|
|
27
|
+
"**/app/main.py",
|
|
28
|
+
"**/main.go",
|
|
29
|
+
];
|
|
30
|
+
export function parseIntegrationObligations(value) {
|
|
31
|
+
if (!Array.isArray(value))
|
|
32
|
+
return [];
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const entry of value) {
|
|
35
|
+
const parsed = integrationObligationSchema.safeParse(entry);
|
|
36
|
+
if (parsed.success)
|
|
37
|
+
out.push(parsed.data);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
async function symbolReferencedFrom(workspace, symbol, globs) {
|
|
42
|
+
const pathspecs = globs.map((glob) => `:(glob)${glob}`);
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execFileAsync("git", ["-C", workspace, "grep", "-l", "-F", "--untracked", symbol, "--", ...pathspecs], { timeout: 30_000, maxBuffer: 4_000_000 });
|
|
45
|
+
return stdout.trim().length > 0;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const code = error.code;
|
|
49
|
+
if (code === 1 || code === "1")
|
|
50
|
+
return false;
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function checkIntegrationObligations(workspace, obligations) {
|
|
55
|
+
if (!obligations.length)
|
|
56
|
+
return { ok: true };
|
|
57
|
+
const failures = [];
|
|
58
|
+
for (const obligation of obligations) {
|
|
59
|
+
if (obligation.kind === "symbol_referenced_from") {
|
|
60
|
+
const globs = obligation.from_globs?.length ? obligation.from_globs : [...DEFAULT_ENTRYPOINT_GLOBS];
|
|
61
|
+
const referenced = await symbolReferencedFrom(workspace, obligation.symbol, globs);
|
|
62
|
+
if (!referenced) {
|
|
63
|
+
failures.push(`symbol "${obligation.symbol}" is not referenced from any entrypoint (${globs.join(", ")})`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return failures.length ? { ok: false, failures } : { ok: true };
|
|
68
|
+
}
|
|
69
|
+
export const INTEGRATION_OBLIGATION_PREFIX = "Integration obligation not met";
|
package/dist/land-git-reality.js
CHANGED
|
@@ -84,7 +84,7 @@ export function agentClaimsRepositoryWork(text) {
|
|
|
84
84
|
}
|
|
85
85
|
if (NO_CHANGE_OUTCOME.test(text))
|
|
86
86
|
return false;
|
|
87
|
-
return /\b(rewrote|replaced|implemented|fetcher\.rs|head_commit|"changes"\s*:\s*\[)/i.test(text);
|
|
87
|
+
return /\b(rewrote|replaced|implemented|fetcher\.rs|head_commit|"changes"\s*:\s*\[\s*")/i.test(text);
|
|
88
88
|
}
|
|
89
89
|
/** Parsed Delivery claims repository work (stricter than free text). */
|
|
90
90
|
export function reportClaimsRepositoryWork(report) {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize planner-authored normative references into the attempt workspace.
|
|
3
|
+
*
|
|
4
|
+
* Cached under {sourceWorkspace}/.conduit/normative-cache/ so sibling-repo specs are
|
|
5
|
+
* fetched once per machine checkout, not once per attempt.
|
|
6
|
+
*/
|
|
7
|
+
import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import { constants } from "node:fs";
|
|
9
|
+
import { dirname, join, relative } from "node:path";
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { normalizeRepositoryUrl } from "./brief.js";
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
export const normativeRefSchema = z.object({
|
|
16
|
+
label: z.string().trim().min(1).max(120),
|
|
17
|
+
repository: z.string().trim().min(3).max(500),
|
|
18
|
+
paths: z.array(z.string().trim().min(1).max(500)).min(1).max(20),
|
|
19
|
+
revision: z.string().trim().min(1).max(120).optional(),
|
|
20
|
+
markers: z.array(z.string().trim().min(1).max(500)).max(20).optional(),
|
|
21
|
+
});
|
|
22
|
+
export const NORMATIVE_REF_PREFIX = "Normative reference unavailable";
|
|
23
|
+
export const NORMATIVE_MARKER_PREFIX = "Normative marker not found";
|
|
24
|
+
export function parseNormativeRefs(value) {
|
|
25
|
+
if (!Array.isArray(value))
|
|
26
|
+
return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const entry of value) {
|
|
29
|
+
const parsed = normativeRefSchema.safeParse(entry);
|
|
30
|
+
if (parsed.success)
|
|
31
|
+
out.push(parsed.data);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function fingerprint(repository) {
|
|
36
|
+
return normalizeRepositoryUrl(repository.includes("://") ? repository : `https://${repository}`);
|
|
37
|
+
}
|
|
38
|
+
function sanitizeLabel(label) {
|
|
39
|
+
const cleaned = label.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
40
|
+
return cleaned || "ref";
|
|
41
|
+
}
|
|
42
|
+
function validateNormativePath(path) {
|
|
43
|
+
const normalized = path.replaceAll("\\", "/");
|
|
44
|
+
if (!normalized || normalized.startsWith("/") || normalized.split("/").includes("..")) {
|
|
45
|
+
throw new Error(`${NORMATIVE_REF_PREFIX}: invalid path ${path}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function cacheDir(sourceWorkspace, repository) {
|
|
49
|
+
return join(sourceWorkspace, ".conduit", "normative-cache", fingerprint(repository).replaceAll("/", "__"));
|
|
50
|
+
}
|
|
51
|
+
const GIT_NO_PROMPT = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
52
|
+
function cloneUrl(repository) {
|
|
53
|
+
return `https://${fingerprint(repository)}.git`;
|
|
54
|
+
}
|
|
55
|
+
function assertPublicGitHost(repository) {
|
|
56
|
+
const host = fingerprint(repository).split("/")[0] ?? "";
|
|
57
|
+
if (!host || !host.includes(".") || host === "localhost" || host.endsWith(".localhost")
|
|
58
|
+
|| host.endsWith(".internal") || /^\d/.test(host) || host.includes(":")) {
|
|
59
|
+
throw new Error(`${NORMATIVE_REF_PREFIX}: repository host is not a public git host`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function pathExists(path) {
|
|
63
|
+
try {
|
|
64
|
+
await access(path, constants.F_OK);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function readPinnedRevision(repoDir) {
|
|
72
|
+
try {
|
|
73
|
+
const { stdout } = await execFileAsync("git", ["-C", repoDir, "rev-parse", "HEAD"], {
|
|
74
|
+
timeout: 30_000,
|
|
75
|
+
maxBuffer: 256_000,
|
|
76
|
+
});
|
|
77
|
+
return stdout.trim() || null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function gitNoPrompt(args, timeout, maxBuffer) {
|
|
84
|
+
await execFileAsync("git", args, { timeout, maxBuffer, env: GIT_NO_PROMPT });
|
|
85
|
+
}
|
|
86
|
+
async function ensureRepoCache(sourceWorkspace, ref) {
|
|
87
|
+
assertPublicGitHost(ref.repository);
|
|
88
|
+
const dir = cacheDir(sourceWorkspace, ref.repository);
|
|
89
|
+
const wantedRevision = ref.revision ?? "HEAD";
|
|
90
|
+
const marker = join(dir, ".conduit-normative-revision");
|
|
91
|
+
let cachedRevision = null;
|
|
92
|
+
if (await pathExists(marker)) {
|
|
93
|
+
cachedRevision = (await readFile(marker, "utf8")).trim() || null;
|
|
94
|
+
}
|
|
95
|
+
const repoReady = await pathExists(join(dir, ".git"));
|
|
96
|
+
if (!repoReady) {
|
|
97
|
+
await mkdir(dirname(dir), { recursive: true });
|
|
98
|
+
const cloneArgs = ["clone", "--depth", "1"];
|
|
99
|
+
if (ref.revision && !/^[0-9a-f]{40}$/i.test(ref.revision)) {
|
|
100
|
+
cloneArgs.push("--branch", ref.revision);
|
|
101
|
+
}
|
|
102
|
+
cloneArgs.push(cloneUrl(ref.repository), dir);
|
|
103
|
+
try {
|
|
104
|
+
await gitNoPrompt(cloneArgs, 180_000, 4_000_000);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
throw new Error(`${NORMATIVE_REF_PREFIX}: could not clone ${fingerprint(ref.repository)} (${message})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (ref.revision && cachedRevision !== wantedRevision) {
|
|
112
|
+
try {
|
|
113
|
+
await gitNoPrompt(["-C", dir, "fetch", "--depth", "1", "origin", ref.revision], 120_000, 4_000_000);
|
|
114
|
+
await gitNoPrompt(["-C", dir, "checkout", "--detach", "FETCH_HEAD"], 60_000, 1_000_000);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
throw new Error(`${NORMATIVE_REF_PREFIX}: could not checkout ${ref.revision} in ${fingerprint(ref.repository)} (${message})`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (!ref.revision) {
|
|
122
|
+
try {
|
|
123
|
+
await gitNoPrompt(["-C", dir, "fetch", "--depth", "1", "origin", "HEAD"], 120_000, 4_000_000);
|
|
124
|
+
await gitNoPrompt(["-C", dir, "checkout", "--detach", "FETCH_HEAD"], 60_000, 1_000_000);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Keep the last successful cache when origin is unreachable (offline tests, private host already rejected).
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const revision = (await readPinnedRevision(dir)) ?? wantedRevision;
|
|
131
|
+
await writeFile(marker, `${revision}\n`, "utf8");
|
|
132
|
+
return { dir, revision };
|
|
133
|
+
}
|
|
134
|
+
async function copyNormativePath(cacheRepo, attemptWorkspace, label, path) {
|
|
135
|
+
validateNormativePath(path);
|
|
136
|
+
const src = join(cacheRepo, path);
|
|
137
|
+
if (!(await pathExists(src))) {
|
|
138
|
+
throw new Error(`${NORMATIVE_REF_PREFIX}: ${path} not found in ${cacheRepo}`);
|
|
139
|
+
}
|
|
140
|
+
const dest = join(attemptWorkspace, ".conduit", "normative", sanitizeLabel(label), path);
|
|
141
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
142
|
+
await cp(src, dest, { recursive: true });
|
|
143
|
+
return relative(attemptWorkspace, dest).replaceAll("\\", "/");
|
|
144
|
+
}
|
|
145
|
+
export async function materializeNormativeRefs(input) {
|
|
146
|
+
if (!input.refs.length)
|
|
147
|
+
return [];
|
|
148
|
+
const materialized = [];
|
|
149
|
+
for (const ref of input.refs) {
|
|
150
|
+
const { dir, revision } = await ensureRepoCache(input.sourceWorkspace, ref);
|
|
151
|
+
const workspace_paths = [];
|
|
152
|
+
for (const path of ref.paths) {
|
|
153
|
+
workspace_paths.push(await copyNormativePath(dir, input.attemptWorkspace, ref.label, path));
|
|
154
|
+
}
|
|
155
|
+
materialized.push({
|
|
156
|
+
label: ref.label,
|
|
157
|
+
repository: fingerprint(ref.repository),
|
|
158
|
+
revision,
|
|
159
|
+
workspace_paths,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return materialized;
|
|
163
|
+
}
|
|
164
|
+
export function assertNormativeRefsMaterialized(refs, materialized) {
|
|
165
|
+
if (!refs.length)
|
|
166
|
+
return;
|
|
167
|
+
if (materialized.length !== refs.length) {
|
|
168
|
+
throw new Error(`${NORMATIVE_REF_PREFIX}: expected ${refs.length} materialized references, got ${materialized.length}`);
|
|
169
|
+
}
|
|
170
|
+
for (const entry of materialized) {
|
|
171
|
+
if (!entry.workspace_paths.length) {
|
|
172
|
+
throw new Error(`${NORMATIVE_REF_PREFIX}: ${entry.label} has no workspace paths`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function isConduitPath(path) {
|
|
177
|
+
const normalized = path.replaceAll("\\", "/");
|
|
178
|
+
return normalized === ".conduit" || normalized.startsWith(".conduit/");
|
|
179
|
+
}
|
|
180
|
+
async function markerPresent(workspace, marker, paths) {
|
|
181
|
+
const args = ["-C", workspace, "grep", "-l", "-F", "--untracked", marker, "--", ...paths];
|
|
182
|
+
try {
|
|
183
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
184
|
+
timeout: 30_000,
|
|
185
|
+
maxBuffer: 4_000_000,
|
|
186
|
+
});
|
|
187
|
+
return stdout.trim().length > 0;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const code = error.code;
|
|
191
|
+
if (code === 1 || code === "1")
|
|
192
|
+
return false;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export async function checkNormativeMarkers(workspace, refs, changedPaths) {
|
|
197
|
+
const scoped = refs.filter((ref) => ref.markers?.length);
|
|
198
|
+
if (!scoped.length)
|
|
199
|
+
return { ok: true };
|
|
200
|
+
if (changedPaths === null) {
|
|
201
|
+
return { ok: false, failures: ["changed files are unknown; required markers cannot be verified"] };
|
|
202
|
+
}
|
|
203
|
+
const searchPaths = changedPaths.filter((path) => path && !isConduitPath(path));
|
|
204
|
+
if (!searchPaths.length) {
|
|
205
|
+
return { ok: false, failures: ["no changed files to search for required markers"] };
|
|
206
|
+
}
|
|
207
|
+
const failures = [];
|
|
208
|
+
for (const ref of scoped) {
|
|
209
|
+
for (const marker of ref.markers ?? []) {
|
|
210
|
+
const found = await markerPresent(workspace, marker, searchPaths);
|
|
211
|
+
if (!found) {
|
|
212
|
+
failures.push(`marker "${marker}" from ${ref.label} not found in changed files`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return failures.length ? { ok: false, failures } : { ok: true };
|
|
217
|
+
}
|
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.7",
|
|
4
4
|
"description": "Conduit Bridge CLI — 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": {
|