@agentbridge1/cli 0.0.4 → 0.0.6

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/init.js CHANGED
@@ -5,6 +5,7 @@ exports.refineRecoveredDomains = refineRecoveredDomains;
5
5
  exports.writeLocalRulesArtifacts = writeLocalRulesArtifacts;
6
6
  exports.buildBootstrapPayloadFromEvidence = buildBootstrapPayloadFromEvidence;
7
7
  exports.runInit = runInit;
8
+ exports.scanRecoveryFromRepo = scanRecoveryFromRepo;
8
9
  exports.runBootstrapRecovery = runBootstrapRecovery;
9
10
  const promises_1 = require("node:readline/promises");
10
11
  const node_child_process_1 = require("node:child_process");
@@ -16,6 +17,7 @@ const git_evidence_1 = require("./git-evidence");
16
17
  const config_1 = require("./config");
17
18
  const watch_core_1 = require("./watch-core");
18
19
  const precommit_1 = require("./precommit");
20
+ const recovery_reconcile_1 = require("./recovery-reconcile");
19
21
  const WORKFLOW_SUMMARY = "Recover domains from repository evidence, map ownership boundaries, enforce lane discipline, and require authority requests before cross-domain edits.";
20
22
  const KNOWN_DOMAIN_LABELS = {
21
23
  communications: "Communications",
@@ -104,25 +106,83 @@ function assertInsideGitRepo() {
104
106
  function normalizeToken(value) {
105
107
  return value.trim().toLowerCase().replace(/[_\s]+/g, "-");
106
108
  }
107
- function classifyDomainKey(path) {
108
- const normalized = path.replace(/\\/g, "/");
109
- const srcMatch = normalized.match(/^src\/([^/]+)\//);
110
- if (srcMatch) {
111
- const folder = normalizeToken(srcMatch[1] ?? "");
109
+ function normalizePath(path) {
110
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
111
+ }
112
+ function deriveDomainKeyFromPath(path) {
113
+ const normalized = normalizePath(path);
114
+ const packageSourcesMatch = normalized.match(/^([^/]+)\/Sources\/([^/]+)\//);
115
+ if (packageSourcesMatch) {
116
+ const folder = normalizeToken(packageSourcesMatch[2] ?? "");
117
+ if (folder === "supabase")
118
+ return "database";
119
+ return folder || null;
120
+ }
121
+ const srcLikeMatch = normalized.match(/^(src|Sources|modules|Modules|feature|features)\/([^/]+)\//);
122
+ if (srcLikeMatch) {
123
+ const folder = normalizeToken(srcLikeMatch[2] ?? "");
112
124
  if (folder === "supabase")
113
125
  return "database";
114
- if (folder)
115
- return folder;
116
- return null;
126
+ return folder || null;
117
127
  }
118
128
  if (normalized.startsWith("db/") || normalized.startsWith("prisma/") || normalized.startsWith("supabase/")) {
119
129
  return "database";
120
130
  }
131
+ if (normalized.startsWith("backend/supabase/") ||
132
+ normalized.includes("/supabase/functions/") ||
133
+ normalized.includes("/supabase/migrations/")) {
134
+ return "database";
135
+ }
121
136
  if (/^supabase_.*\.sql$/i.test(normalized)) {
122
137
  return "database";
123
138
  }
139
+ const xcodeAppMatch = normalized.match(/^([^/]+)\/\1\//);
140
+ if (xcodeAppMatch) {
141
+ const appName = normalizeToken(xcodeAppMatch[1] ?? "");
142
+ if (appName)
143
+ return appName;
144
+ }
145
+ const topLevelMatch = normalized.match(/^([^/]+)\//);
146
+ if (topLevelMatch) {
147
+ const top = normalizeToken(topLevelMatch[1] ?? "");
148
+ if (top === "backend")
149
+ return "backend";
150
+ }
151
+ return null;
152
+ }
153
+ function extractDomainPrefix(path) {
154
+ const normalized = normalizePath(path);
155
+ const packageSourcesMatch = normalized.match(/^([^/]+)\/Sources\/([^/]+)\//);
156
+ if (packageSourcesMatch) {
157
+ return `${packageSourcesMatch[1]}/Sources/${packageSourcesMatch[2]}/`;
158
+ }
159
+ const srcLikeMatch = normalized.match(/^(src|Sources|modules|Modules|feature|features)\/([^/]+)\//);
160
+ if (srcLikeMatch) {
161
+ return `${srcLikeMatch[1]}/${srcLikeMatch[2]}/`;
162
+ }
163
+ if (normalized.startsWith("db/"))
164
+ return "db/";
165
+ if (normalized.startsWith("prisma/"))
166
+ return "prisma/";
167
+ if (normalized.startsWith("supabase/"))
168
+ return "supabase/";
169
+ if (normalized.startsWith("backend/supabase/"))
170
+ return "backend/supabase/";
171
+ if (/^supabase_.*\.sql$/i.test(normalized))
172
+ return "supabase_*.sql";
173
+ const xcodeAppMatch = normalized.match(/^([^/]+)\/\1\//);
174
+ if (xcodeAppMatch) {
175
+ return `${xcodeAppMatch[1]}/${xcodeAppMatch[1]}/`;
176
+ }
177
+ const topLevelMatch = normalized.match(/^([^/]+)\//);
178
+ if (topLevelMatch?.[1] === "backend") {
179
+ return "backend/";
180
+ }
124
181
  return null;
125
182
  }
183
+ function classifyDomainKey(path) {
184
+ return deriveDomainKeyFromPath(path);
185
+ }
126
186
  function tierForDomainKey(domainKey, files) {
127
187
  if (domainKey === "sessions") {
128
188
  const highRisk = files.some((file) => /(token|auth|state|orchestrator|repository|session)/i.test(file));
@@ -148,9 +208,11 @@ function confidenceForDomain(fileCount) {
148
208
  }
149
209
  function collectWorkspaceDomainSeeds() {
150
210
  const seeds = new Set();
151
- const srcPath = (0, node_path_1.resolve)(process.cwd(), "src");
152
- if ((0, node_fs_1.existsSync)(srcPath)) {
153
- for (const entry of (0, node_fs_1.readdirSync)(srcPath, { withFileTypes: true })) {
211
+ for (const root of ["src", "Sources", "modules", "Modules", "feature", "features"]) {
212
+ const rootPath = (0, node_path_1.resolve)(process.cwd(), root);
213
+ if (!(0, node_fs_1.existsSync)(rootPath))
214
+ continue;
215
+ for (const entry of (0, node_fs_1.readdirSync)(rootPath, { withFileTypes: true })) {
154
216
  if (!entry.isDirectory())
155
217
  continue;
156
218
  const folder = normalizeToken(entry.name);
@@ -162,19 +224,53 @@ function collectWorkspaceDomainSeeds() {
162
224
  }
163
225
  }
164
226
  }
227
+ for (const topLevelEntry of (0, node_fs_1.readdirSync)(process.cwd(), { withFileTypes: true })) {
228
+ if (!topLevelEntry.isDirectory())
229
+ continue;
230
+ const packageSourcesPath = (0, node_path_1.resolve)(process.cwd(), topLevelEntry.name, "Sources");
231
+ if (!(0, node_fs_1.existsSync)(packageSourcesPath))
232
+ continue;
233
+ for (const sourceEntry of (0, node_fs_1.readdirSync)(packageSourcesPath, { withFileTypes: true })) {
234
+ if (!sourceEntry.isDirectory())
235
+ continue;
236
+ const folder = normalizeToken(sourceEntry.name);
237
+ if (folder === "supabase") {
238
+ seeds.add("database");
239
+ }
240
+ else if (folder) {
241
+ seeds.add(folder);
242
+ }
243
+ }
244
+ }
165
245
  for (const dbFolder of ["db", "prisma", "supabase"]) {
166
246
  if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), dbFolder))) {
167
247
  seeds.add("database");
168
248
  }
169
249
  }
250
+ if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), "backend", "supabase"))) {
251
+ seeds.add("database");
252
+ }
253
+ for (const topLevelEntry of (0, node_fs_1.readdirSync)(process.cwd(), { withFileTypes: true })) {
254
+ if (!topLevelEntry.isDirectory())
255
+ continue;
256
+ const nestedSameName = (0, node_path_1.resolve)(process.cwd(), topLevelEntry.name, topLevelEntry.name);
257
+ if ((0, node_fs_1.existsSync)(nestedSameName)) {
258
+ const folder = normalizeToken(topLevelEntry.name);
259
+ if (folder)
260
+ seeds.add(folder);
261
+ }
262
+ }
170
263
  return Array.from(seeds);
171
264
  }
172
265
  function refineRecoveredDomains(inferredClusters, evidence, architectureDomains) {
173
266
  const domainFiles = new Map();
267
+ const domainPrefixes = new Map();
174
268
  const workspaceSeeds = collectWorkspaceDomainSeeds();
175
269
  for (const seed of workspaceSeeds) {
176
270
  if (!domainFiles.has(seed))
177
271
  domainFiles.set(seed, new Set());
272
+ if (!domainPrefixes.has(seed))
273
+ domainPrefixes.set(seed, new Set());
178
274
  }
179
275
  for (const file of evidence.files) {
180
276
  const key = classifyDomainKey(file);
@@ -183,6 +279,12 @@ function refineRecoveredDomains(inferredClusters, evidence, architectureDomains)
183
279
  if (!domainFiles.has(key))
184
280
  domainFiles.set(key, new Set());
185
281
  domainFiles.get(key)?.add(file);
282
+ const prefix = extractDomainPrefix(file);
283
+ if (prefix) {
284
+ if (!domainPrefixes.has(key))
285
+ domainPrefixes.set(key, new Set());
286
+ domainPrefixes.get(key)?.add(prefix);
287
+ }
186
288
  }
187
289
  for (const cluster of inferredClusters) {
188
290
  const clusterPaths = [...cluster.files, ...cluster.path_prefixes];
@@ -207,6 +309,12 @@ function refineRecoveredDomains(inferredClusters, evidence, architectureDomains)
207
309
  for (const file of cluster.files) {
208
310
  if (classifyDomainKey(file) === chosenKey) {
209
311
  domainFiles.get(chosenKey)?.add(file);
312
+ const prefix = extractDomainPrefix(file);
313
+ if (prefix) {
314
+ if (!domainPrefixes.has(chosenKey))
315
+ domainPrefixes.set(chosenKey, new Set());
316
+ domainPrefixes.get(chosenKey)?.add(prefix);
317
+ }
210
318
  }
211
319
  }
212
320
  }
@@ -237,12 +345,14 @@ function refineRecoveredDomains(inferredClusters, evidence, architectureDomains)
237
345
  return Array.from(domainFiles.entries())
238
346
  .map(([key, filesSet]) => {
239
347
  const files = Array.from(filesSet).sort((a, b) => a.localeCompare(b));
348
+ const inferredPrefixes = Array.from(domainPrefixes.get(key) ?? []);
240
349
  const pathPatterns = DOMAIN_PREFIX_BY_KEY[key] ??
350
+ (inferredPrefixes.length > 0 ? inferredPrefixes : undefined) ??
241
351
  (key === "database"
242
352
  ? ["db/", "prisma/", "supabase/", "supabase_*.sql"]
243
353
  : key === "shared"
244
354
  ? ["src/shared/"]
245
- : [`src/${key}/`]);
355
+ : [`${key}/`]);
246
356
  const primaryPrefix = pathPatterns[0] ? pathPatterns[0].replace(/\/$/, "") : key;
247
357
  const protectionTier = tierForDomainKey(key, files);
248
358
  return {
@@ -298,14 +408,42 @@ function toCursorRule(domains) {
298
408
  "",
299
409
  ].join("\n");
300
410
  }
301
- function writeLocalRulesArtifacts(domains) {
411
+ function writeLocalRulesArtifacts(domains, opts = {}) {
302
412
  const md = toRulesMarkdown(domains);
303
413
  const cursorRule = toCursorRule(domains);
304
414
  const rootFile = (0, node_path_1.resolve)(process.cwd(), "AGENTBRIDGE.md");
305
415
  const cursorRulesDir = (0, node_path_1.resolve)(process.cwd(), ".cursor", "rules");
416
+ const cursorFile = (0, node_path_1.resolve)(cursorRulesDir, "agentbridge.mdc");
417
+ const mdTarget = `${md}\n`;
418
+ const cursorTarget = `${cursorRule}\n`;
419
+ if ((0, node_fs_1.existsSync)(rootFile) && (0, node_fs_1.existsSync)(cursorFile) && !opts.force) {
420
+ const existingMd = (0, node_fs_1.readFileSync)(rootFile, "utf8");
421
+ const existingCursor = (0, node_fs_1.readFileSync)(cursorFile, "utf8");
422
+ if (existingMd === mdTarget && existingCursor === cursorTarget) {
423
+ return { wrote: false, skipped: true, reason: "already up to date" };
424
+ }
425
+ if (!existingMd.startsWith("# AgentBridge Local Rules") ||
426
+ !existingCursor.includes("Recovered domains:")) {
427
+ return { wrote: false, skipped: true, reason: "local edits preserved" };
428
+ }
429
+ }
306
430
  (0, node_fs_1.mkdirSync)(cursorRulesDir, { recursive: true });
307
- (0, node_fs_1.writeFileSync)(rootFile, `${md}\n`, "utf8");
308
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cursorRulesDir, "agentbridge.mdc"), `${cursorRule}\n`, "utf8");
431
+ (0, node_fs_1.writeFileSync)(rootFile, mdTarget, "utf8");
432
+ (0, node_fs_1.writeFileSync)(cursorFile, cursorTarget, "utf8");
433
+ return { wrote: true, skipped: false };
434
+ }
435
+ function refsToBootstrapDomains(refs) {
436
+ return refs.map((ref) => ({
437
+ name: ref.name,
438
+ pathPatterns: ref.pathPatterns.length > 0 ? ref.pathPatterns : [`${ref.name.toLowerCase()}/`],
439
+ files: [],
440
+ evidenceSource: "git_history",
441
+ confidence: 0.72,
442
+ protectionTier: (0, watch_core_1.inferTierFromDomainName)(ref.name),
443
+ knownTraps: [],
444
+ relatedDomains: [],
445
+ openRisks: [],
446
+ }));
309
447
  }
310
448
  async function buildBootstrapPayloadFromEvidence(ctx, productSummary, architectureDomains, evidence) {
311
449
  const clusterResult = await (0, http_1.postJson)(ctx, `/v1/dev/projects/${encodeURIComponent(ctx.projectId)}/recovery/evidence`, evidence);
@@ -416,16 +554,12 @@ async function runInit(ctx) {
416
554
  rl.close();
417
555
  }
418
556
  }
419
- async function runBootstrapRecovery(ctx, opts = {}) {
557
+ async function scanRecoveryFromRepo(ctx, opts = {}) {
420
558
  assertInsideGitRepo();
421
- const runtimeCliPath = process.argv[1] ? (0, node_path_1.resolve)(process.argv[1]) : undefined;
422
- const hookCliPath = runtimeCliPath ?? (0, node_path_1.resolve)(__dirname, "index.js");
423
559
  const productSummary = opts.productSummary?.trim() || "Recovered existing repository baseline for supervised agent work.";
424
560
  const architectureDomains = opts.architectureDomains ?? [];
425
561
  const collectedEvidence = (0, git_evidence_1.collectGitEvidence)();
426
- const evidence = collectedEvidence.payload;
427
- const { inferredClusters, payload: bootstrapPayload } = await buildBootstrapPayloadFromEvidence(ctx, productSummary, architectureDomains, evidence);
428
- await (0, http_1.postJson)(ctx, `/v1/dev/projects/${encodeURIComponent(ctx.projectId)}/bootstrap`, bootstrapPayload);
562
+ const { inferredClusters, payload: bootstrapPayload } = await buildBootstrapPayloadFromEvidence(ctx, productSummary, architectureDomains, collectedEvidence.payload);
429
563
  const recoveredDomains = inferredClusters
430
564
  .filter(isValidAuthorityDomain)
431
565
  .map((cluster) => ({
@@ -434,25 +568,65 @@ async function runBootstrapRecovery(ctx, opts = {}) {
434
568
  ownerAgentId: `${cluster.suggested_name} Agent`,
435
569
  tier: cluster.protection_tier ?? (0, watch_core_1.inferTierFromDomainName)(cluster.suggested_name),
436
570
  }));
571
+ const candidateRefs = recoveredDomains.map((domain) => ({
572
+ name: domain.domain,
573
+ pathPatterns: domain.pathPatterns,
574
+ }));
575
+ return { inferredClusters, bootstrapPayload, recoveredDomains, candidateRefs };
576
+ }
577
+ async function runBootstrapRecovery(ctx, opts = {}) {
578
+ assertInsideGitRepo();
579
+ const runtimeCliPath = process.argv[1] ? (0, node_path_1.resolve)(process.argv[1]) : undefined;
580
+ const hookCliPath = runtimeCliPath ?? (0, node_path_1.resolve)(__dirname, "index.js");
581
+ const scan = opts.scan ??
582
+ (await scanRecoveryFromRepo(ctx, {
583
+ productSummary: opts.productSummary,
584
+ architectureDomains: opts.architectureDomains,
585
+ }));
586
+ const { inferredClusters, bootstrapPayload, recoveredDomains } = scan;
587
+ if (opts.reconcilePlan) {
588
+ const candidateBootstrap = toBootstrapDomains(scan.candidateRefs.map((ref) => ({
589
+ name: ref.name,
590
+ files: [],
591
+ pathPatterns: ref.pathPatterns,
592
+ confidence: 0.72,
593
+ protectionTier: (0, watch_core_1.inferTierFromDomainName)(ref.name),
594
+ knownTraps: [],
595
+ rationale: "candidate from repository scan",
596
+ })));
597
+ const existingBootstrap = refsToBootstrapDomains(opts.reconcilePlan.existingActive);
598
+ bootstrapPayload.domains = (0, recovery_reconcile_1.mergeBootstrapDomainEntries)(opts.reconcilePlan, candidateBootstrap, existingBootstrap);
599
+ }
600
+ await (0, http_1.postJson)(ctx, `/v1/dev/projects/${encodeURIComponent(ctx.projectId)}/bootstrap`, bootstrapPayload);
601
+ const payloadDomains = Array.isArray(bootstrapPayload.domains)
602
+ ? bootstrapPayload.domains
603
+ : [];
604
+ const payloadDomainKeys = new Set(payloadDomains.map((d) => (d.name ?? "").trim().toLowerCase()).filter(Boolean));
605
+ const mergedLocalDomains = payloadDomainKeys.size > 0
606
+ ? recoveredDomains.filter((d) => payloadDomainKeys.has(d.domain.trim().toLowerCase()))
607
+ : recoveredDomains;
437
608
  (0, config_1.updateConfig)({
438
609
  apiBaseUrl: ctx.apiBaseUrl,
439
610
  projectId: ctx.projectId,
440
- domains: recoveredDomains,
611
+ domains: (0, watch_core_1.toDomainOwnershipInput)(mergedLocalDomains),
441
612
  cliPath: runtimeCliPath,
442
613
  });
443
- writeLocalRulesArtifacts(recoveredDomains.map((domain) => ({
614
+ writeLocalRulesArtifacts(mergedLocalDomains.map((domain) => ({
444
615
  domain: domain.domain,
445
616
  owner: domain.ownerAgentId,
446
617
  tier: domain.tier,
447
- })));
618
+ })), { force: opts.reconcilePlan?.mode === "force" });
448
619
  if (opts.installHook) {
449
620
  (0, precommit_1.installPrecommitHookWithPath)(hookCliPath);
450
621
  (0, config_1.updateConfig)({ precommitHookInstalled: true, cliPath: runtimeCliPath });
451
622
  }
452
- node_process_1.stdout.write([
453
- "Recovery baseline created.",
454
- `Domains mapped: ${recoveredDomains.length}.`,
455
- "Project context and local rules are now ready.",
456
- "",
457
- ].join("\n"));
623
+ if (!opts.reconcilePlan) {
624
+ node_process_1.stdout.write([
625
+ "Recovery baseline created.",
626
+ `Domains mapped: ${mergedLocalDomains.length}.`,
627
+ "Project context and local rules are now ready.",
628
+ "",
629
+ ].join("\n"));
630
+ }
631
+ return { recoveredDomains: mergedLocalDomains };
458
632
  }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeLocalMemory = writeLocalMemory;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const MEMORY_DIR = (0, node_path_1.resolve)(process.cwd(), ".agentbridge", "memory");
7
+ function summarizeProof(state) {
8
+ const run = state.lastLocalVerificationRun;
9
+ if (!run) {
10
+ return state.changedFiles.length > 0 ? { status: "missing" } : { status: "none" };
11
+ }
12
+ if (run.exitCode !== 0) {
13
+ return { status: "failed", command: run.command, finishedAt: run.finishedAt };
14
+ }
15
+ return { status: "ok", command: run.command, finishedAt: run.finishedAt };
16
+ }
17
+ function writeLocalMemory(state, options) {
18
+ const closedAt = state.closedAt ?? new Date().toISOString();
19
+ const record = {
20
+ sessionId: state.id,
21
+ intent: state.intent?.trim() || "Untitled task",
22
+ mode: state.mode ?? "local_supervision",
23
+ changedFiles: [...state.changedFiles].sort(),
24
+ proofSummary: summarizeProof(state),
25
+ openedAt: state.startedAt ?? state.createdAt,
26
+ closedAt,
27
+ cloudSync: options.cloudSync,
28
+ };
29
+ (0, node_fs_1.mkdirSync)(MEMORY_DIR, { recursive: true });
30
+ const filePath = (0, node_path_1.resolve)(MEMORY_DIR, `${state.id}.json`);
31
+ (0, node_fs_1.writeFileSync)(filePath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
32
+ return filePath;
33
+ }
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isProofNoiseFile = isProofNoiseFile;
4
+ exports.normalizeProofMatchingFileSet = normalizeProofMatchingFileSet;
5
+ exports.fingerprintSetKeyForPaths = fingerprintSetKeyForPaths;
6
+ exports.evaluateLocalProof = evaluateLocalProof;
7
+ exports.buildLocalProofBlockingIssue = buildLocalProofBlockingIssue;
8
+ exports.isLocalVerificationRun = isLocalVerificationRun;
9
+ const file_fingerprints_1 = require("./file-fingerprints");
10
+ function normalizePath(path) {
11
+ return path.trim().replaceAll("\\", "/");
12
+ }
13
+ function isProofNoiseFile(file) {
14
+ const normalized = normalizePath(file).toLowerCase();
15
+ return (normalized === "agentbridge.md" ||
16
+ normalized === ".cursor" ||
17
+ normalized.startsWith(".cursor/"));
18
+ }
19
+ function normalizeProofMatchingFileSet(files) {
20
+ return [...new Set(files.map(normalizePath).filter((file) => file.length > 0 && !isProofNoiseFile(file)))].sort();
21
+ }
22
+ function fingerprintValue(fingerprint) {
23
+ if (!fingerprint.exists)
24
+ return "missing";
25
+ if (fingerprint.sha256)
26
+ return `sha:${fingerprint.sha256}`;
27
+ return `meta:${fingerprint.fileType ?? "unknown"}:${fingerprint.size ?? -1}:${Math.trunc(fingerprint.mtimeMs ?? -1)}`;
28
+ }
29
+ function fingerprintSetKeyForPaths(paths, fingerprints) {
30
+ const normalizedPaths = normalizeProofMatchingFileSet(paths);
31
+ if (normalizedPaths.length === 0)
32
+ return "";
33
+ const map = new Map(fingerprints.map((fingerprint) => [normalizePath(fingerprint.path), fingerprint]));
34
+ const entries = [];
35
+ for (const path of normalizedPaths) {
36
+ const fingerprint = map.get(path);
37
+ if (!fingerprint)
38
+ return null;
39
+ entries.push(`${path}=>${fingerprintValue(fingerprint)}`);
40
+ }
41
+ return entries.join("\n");
42
+ }
43
+ function evaluateLocalProof(changedFiles, lastRun) {
44
+ const normalizedChanged = normalizeProofMatchingFileSet(changedFiles);
45
+ if (normalizedChanged.length === 0) {
46
+ return { decision: "ok", changedFiles: [], staleFiles: [] };
47
+ }
48
+ if (!lastRun) {
49
+ return { decision: "needs_proof", changedFiles: normalizedChanged, staleFiles: [] };
50
+ }
51
+ if (lastRun.status !== "passed") {
52
+ return { decision: "failed", changedFiles: normalizedChanged, staleFiles: normalizedChanged };
53
+ }
54
+ const currentKey = fingerprintSetKeyForPaths(normalizedChanged, (0, file_fingerprints_1.computeFileFingerprints)(normalizedChanged));
55
+ const proofKey = fingerprintSetKeyForPaths(lastRun.proofScopeFiles, lastRun.proofScopeFingerprints);
56
+ if (currentKey === null || proofKey === null || currentKey !== proofKey) {
57
+ const proofScopeSet = new Set(normalizeProofMatchingFileSet(lastRun.proofScopeFiles));
58
+ const staleFiles = normalizedChanged.filter((file) => {
59
+ if (!proofScopeSet.has(file))
60
+ return true;
61
+ const currentFp = (0, file_fingerprints_1.computeFileFingerprints)([file])[0];
62
+ const proofFp = lastRun.proofScopeFingerprints.find((fp) => normalizePath(fp.path) === file);
63
+ if (!proofFp)
64
+ return true;
65
+ return fingerprintValue(currentFp) !== fingerprintValue(proofFp);
66
+ });
67
+ return {
68
+ decision: "stale_evidence",
69
+ changedFiles: normalizedChanged,
70
+ staleFiles: staleFiles.length > 0 ? staleFiles : normalizedChanged,
71
+ };
72
+ }
73
+ return { decision: "ok", changedFiles: normalizedChanged, staleFiles: [] };
74
+ }
75
+ function buildLocalProofBlockingIssue(evaluation) {
76
+ const summarizePromptFiles = (files) => {
77
+ const unique = [...new Set(files)];
78
+ if (unique.length === 0)
79
+ return "current files";
80
+ const visible = unique.slice(0, 3);
81
+ if (unique.length <= 3)
82
+ return visible.join(", ");
83
+ return `${visible.join(", ")} (+${unique.length - visible.length} more)`;
84
+ };
85
+ if (evaluation.decision === "ok")
86
+ return null;
87
+ if (evaluation.decision === "stale_evidence") {
88
+ const staleFiles = [...new Set(evaluation.staleFiles)];
89
+ return {
90
+ errorCode: "PROOF_STALE_AFTER_CHANGE",
91
+ whatHappened: "Verification proof is stale because files changed after the last passing verify.",
92
+ whyItMatters: "Proof must match the final edited files before you can trust the agent is done.",
93
+ files: staleFiles,
94
+ suggestedPrompt: [
95
+ "Your proof is stale because files changed after verification.",
96
+ "Rerun verification after your final edit: agentbridge verify -- <test command>",
97
+ `Files: ${summarizePromptFiles(staleFiles)}`,
98
+ ].join("\n"),
99
+ nextAction: "Run: agentbridge verify -- <your test command>",
100
+ };
101
+ }
102
+ if (evaluation.decision === "failed") {
103
+ return {
104
+ errorCode: "VERIFICATION_FAILED",
105
+ whatHappened: "The last verification command failed.",
106
+ whyItMatters: "Failed verification cannot count as proof.",
107
+ files: evaluation.changedFiles,
108
+ suggestedPrompt: [
109
+ "The last verification command failed.",
110
+ "Fix the failure and rerun verification with a passing result: agentbridge verify -- <test command>",
111
+ ].join("\n"),
112
+ nextAction: "Run: agentbridge verify -- <your test command>",
113
+ };
114
+ }
115
+ const files = [...new Set(evaluation.changedFiles)];
116
+ return {
117
+ errorCode: "PROOF_MISSING",
118
+ whatHappened: "Coding changes were detected with no passing verification proof.",
119
+ whyItMatters: "Changed files need a recorded verify command before they are safe to trust.",
120
+ files,
121
+ suggestedPrompt: [
122
+ `AgentBridge found ${files.length} changed files without proof.`,
123
+ "Run verification for the changed files: agentbridge verify -- <test command>",
124
+ `Files: ${summarizePromptFiles(files)}`,
125
+ ].join("\n"),
126
+ nextAction: "Run: agentbridge verify -- <your test command>",
127
+ };
128
+ }
129
+ function isLocalVerificationRun(value) {
130
+ if (typeof value !== "object" || value === null)
131
+ return false;
132
+ const row = value;
133
+ if (typeof row.command !== "string" || row.command.length === 0)
134
+ return false;
135
+ if (typeof row.startedAt !== "string" || row.startedAt.length === 0)
136
+ return false;
137
+ if (typeof row.finishedAt !== "string" || row.finishedAt.length === 0)
138
+ return false;
139
+ if (typeof row.exitCode !== "number" || !Number.isFinite(row.exitCode))
140
+ return false;
141
+ if (typeof row.stdoutExcerpt !== "string")
142
+ return false;
143
+ if (typeof row.stderrExcerpt !== "string")
144
+ return false;
145
+ if (typeof row.gitHead !== "string")
146
+ return false;
147
+ if (!Array.isArray(row.repoDirtySnapshot) || !row.repoDirtySnapshot.every((entry) => typeof entry === "string")) {
148
+ return false;
149
+ }
150
+ if (!Array.isArray(row.proofScopeFiles) || !row.proofScopeFiles.every((entry) => typeof entry === "string")) {
151
+ return false;
152
+ }
153
+ if (!Array.isArray(row.proofScopeFingerprints))
154
+ return false;
155
+ if (row.status !== "passed" && row.status !== "failed")
156
+ return false;
157
+ return true;
158
+ }