@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 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.", ...(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.",
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({ taskId, objective: task.objective, spec, grants, workspace: attemptWorkspace, currentHead: worktreeStart, reworkFeedback, workPackage, verificationCommands: attemptBrief?.verification ?? [], executionClass });
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
- // Ask git what changed before trusting what the report says changed.
1163
- const startCommit = executionContract.requested_base_commit && executionContract.claimed_head
1164
- ? await resolveAttemptStartCommit(workspace, executionContract.requested_base_commit, executionContract.claimed_head)
1165
- : null;
1166
- const actualPaths = startCommit ? await changedPathsSince(attemptWorkspace, startCommit) : null;
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 async function changedPathsSince(worktree, startCommit) {
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";
@@ -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.5",
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": {