@agentbridge1/cli 0.0.5 → 0.0.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/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",
@@ -126,9 +128,26 @@ function deriveDomainKeyFromPath(path) {
126
128
  if (normalized.startsWith("db/") || normalized.startsWith("prisma/") || normalized.startsWith("supabase/")) {
127
129
  return "database";
128
130
  }
131
+ if (normalized.startsWith("backend/supabase/") ||
132
+ normalized.includes("/supabase/functions/") ||
133
+ normalized.includes("/supabase/migrations/")) {
134
+ return "database";
135
+ }
129
136
  if (/^supabase_.*\.sql$/i.test(normalized)) {
130
137
  return "database";
131
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
+ }
132
151
  return null;
133
152
  }
134
153
  function extractDomainPrefix(path) {
@@ -147,8 +166,18 @@ function extractDomainPrefix(path) {
147
166
  return "prisma/";
148
167
  if (normalized.startsWith("supabase/"))
149
168
  return "supabase/";
169
+ if (normalized.startsWith("backend/supabase/"))
170
+ return "backend/supabase/";
150
171
  if (/^supabase_.*\.sql$/i.test(normalized))
151
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
+ }
152
181
  return null;
153
182
  }
154
183
  function classifyDomainKey(path) {
@@ -218,6 +247,19 @@ function collectWorkspaceDomainSeeds() {
218
247
  seeds.add("database");
219
248
  }
220
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
+ }
221
263
  return Array.from(seeds);
222
264
  }
223
265
  function refineRecoveredDomains(inferredClusters, evidence, architectureDomains) {
@@ -366,14 +408,42 @@ function toCursorRule(domains) {
366
408
  "",
367
409
  ].join("\n");
368
410
  }
369
- function writeLocalRulesArtifacts(domains) {
411
+ function writeLocalRulesArtifacts(domains, opts = {}) {
370
412
  const md = toRulesMarkdown(domains);
371
413
  const cursorRule = toCursorRule(domains);
372
414
  const rootFile = (0, node_path_1.resolve)(process.cwd(), "AGENTBRIDGE.md");
373
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
+ }
374
430
  (0, node_fs_1.mkdirSync)(cursorRulesDir, { recursive: true });
375
- (0, node_fs_1.writeFileSync)(rootFile, `${md}\n`, "utf8");
376
- (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
+ }));
377
447
  }
378
448
  async function buildBootstrapPayloadFromEvidence(ctx, productSummary, architectureDomains, evidence) {
379
449
  const clusterResult = await (0, http_1.postJson)(ctx, `/v1/dev/projects/${encodeURIComponent(ctx.projectId)}/recovery/evidence`, evidence);
@@ -484,16 +554,12 @@ async function runInit(ctx) {
484
554
  rl.close();
485
555
  }
486
556
  }
487
- async function runBootstrapRecovery(ctx, opts = {}) {
557
+ async function scanRecoveryFromRepo(ctx, opts = {}) {
488
558
  assertInsideGitRepo();
489
- const runtimeCliPath = process.argv[1] ? (0, node_path_1.resolve)(process.argv[1]) : undefined;
490
- const hookCliPath = runtimeCliPath ?? (0, node_path_1.resolve)(__dirname, "index.js");
491
559
  const productSummary = opts.productSummary?.trim() || "Recovered existing repository baseline for supervised agent work.";
492
560
  const architectureDomains = opts.architectureDomains ?? [];
493
561
  const collectedEvidence = (0, git_evidence_1.collectGitEvidence)();
494
- const evidence = collectedEvidence.payload;
495
- const { inferredClusters, payload: bootstrapPayload } = await buildBootstrapPayloadFromEvidence(ctx, productSummary, architectureDomains, evidence);
496
- 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);
497
563
  const recoveredDomains = inferredClusters
498
564
  .filter(isValidAuthorityDomain)
499
565
  .map((cluster) => ({
@@ -502,25 +568,75 @@ async function runBootstrapRecovery(ctx, opts = {}) {
502
568
  ownerAgentId: `${cluster.suggested_name} Agent`,
503
569
  tier: cluster.protection_tier ?? (0, watch_core_1.inferTierFromDomainName)(cluster.suggested_name),
504
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
+ if (process.env.AGENTBRIDGE_DEBUG_HTTP === "1") {
601
+ const payloadDomains = Array.isArray(bootstrapPayload.domains)
602
+ ? bootstrapPayload.domains
603
+ : [];
604
+ const mappedPathCount = payloadDomains.reduce((count, domain) => {
605
+ const paths = domain.files && domain.files.length > 0 ? domain.files : domain.pathPatterns;
606
+ return count + (Array.isArray(paths) ? paths.length : 0);
607
+ }, 0);
608
+ process.stderr.write(`[agentbridge:recover] bootstrap POST project=${ctx.projectId} domains=${payloadDomains.length} mapped_paths=${mappedPathCount} product_summary_len=${String(bootstrapPayload.product_summary ?? "").length}\n`);
609
+ }
610
+ await (0, http_1.postJson)(ctx, `/v1/dev/projects/${encodeURIComponent(ctx.projectId)}/bootstrap`, bootstrapPayload);
611
+ const payloadDomains = Array.isArray(bootstrapPayload.domains)
612
+ ? bootstrapPayload.domains
613
+ : [];
614
+ const payloadDomainKeys = new Set(payloadDomains.map((d) => (d.name ?? "").trim().toLowerCase()).filter(Boolean));
615
+ const mergedLocalDomains = payloadDomainKeys.size > 0
616
+ ? recoveredDomains.filter((d) => payloadDomainKeys.has(d.domain.trim().toLowerCase()))
617
+ : recoveredDomains;
505
618
  (0, config_1.updateConfig)({
506
619
  apiBaseUrl: ctx.apiBaseUrl,
507
620
  projectId: ctx.projectId,
508
- domains: recoveredDomains,
621
+ domains: (0, watch_core_1.toDomainOwnershipInput)(mergedLocalDomains),
509
622
  cliPath: runtimeCliPath,
510
623
  });
511
- writeLocalRulesArtifacts(recoveredDomains.map((domain) => ({
624
+ writeLocalRulesArtifacts(mergedLocalDomains.map((domain) => ({
512
625
  domain: domain.domain,
513
626
  owner: domain.ownerAgentId,
514
627
  tier: domain.tier,
515
- })));
628
+ })), { force: opts.reconcilePlan?.mode === "force" });
516
629
  if (opts.installHook) {
517
630
  (0, precommit_1.installPrecommitHookWithPath)(hookCliPath);
518
631
  (0, config_1.updateConfig)({ precommitHookInstalled: true, cliPath: runtimeCliPath });
519
632
  }
520
- node_process_1.stdout.write([
521
- "Recovery baseline created.",
522
- `Domains mapped: ${recoveredDomains.length}.`,
523
- "Project context and local rules are now ready.",
524
- "",
525
- ].join("\n"));
633
+ if (!opts.reconcilePlan) {
634
+ node_process_1.stdout.write([
635
+ "Recovery baseline created.",
636
+ `Domains mapped: ${mergedLocalDomains.length}.`,
637
+ "Project context and local rules are now ready.",
638
+ "",
639
+ ].join("\n"));
640
+ }
641
+ return { recoveredDomains: mergedLocalDomains };
526
642
  }
@@ -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
+ }
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyAgentActivityMerge = applyAgentActivityMerge;
4
+ exports.mirrorMcpLifecycleEvent = mirrorMcpLifecycleEvent;
5
+ exports.buildMirrorPatchFromMcp = buildMirrorPatchFromMcp;
6
+ exports.mirrorMcpToolSuccess = mirrorMcpToolSuccess;
7
+ const session_1 = require("./session");
8
+ const session_state_1 = require("./session-state");
9
+ function uniquePaths(paths) {
10
+ return [...new Set(paths.map((p) => p.trim()).filter(Boolean))].sort();
11
+ }
12
+ function truncateSummary(text, max = 120) {
13
+ const trimmed = text.trim();
14
+ if (trimmed.length <= max)
15
+ return trimmed;
16
+ return `${trimmed.slice(0, max - 1)}…`;
17
+ }
18
+ function mergeClaimedPaths(existing, incoming) {
19
+ return uniquePaths([...existing, ...incoming]);
20
+ }
21
+ function effectiveClaimedPaths(state, patch) {
22
+ const base = state.claimedPaths ?? [];
23
+ const fromActivity = state.agentActivity?.claimedPaths ?? [];
24
+ const incoming = patch.claimedPaths ?? [];
25
+ return mergeClaimedPaths(mergeClaimedPaths(base, fromActivity), incoming);
26
+ }
27
+ function buildAgentActivity(state, patch, now) {
28
+ const claimedPaths = patch.claimedPaths?.length
29
+ ? mergeClaimedPaths(state.agentActivity?.claimedPaths ?? [], patch.claimedPaths)
30
+ : state.agentActivity?.claimedPaths;
31
+ const activity = {
32
+ source: "mcp",
33
+ updatedAt: now,
34
+ lastEvent: patch.event,
35
+ workSessionId: patch.workSessionId ?? state.agentActivity?.workSessionId ?? state.serverSessionId,
36
+ changeRequestId: patch.changeRequestId ?? state.agentActivity?.changeRequestId ?? state.changeRequestId,
37
+ intent: patch.intent ?? state.agentActivity?.intent ?? state.intent,
38
+ claimedPaths,
39
+ laneDomain: patch.laneDomain !== undefined ? patch.laneDomain : state.agentActivity?.laneDomain,
40
+ laneId: patch.laneId ?? state.agentActivity?.laneId,
41
+ laneType: patch.laneType ?? state.agentActivity?.laneType,
42
+ laneValue: patch.laneValue ?? state.agentActivity?.laneValue,
43
+ summary: patch.summary !== undefined ? truncateSummary(patch.summary) : state.agentActivity?.summary,
44
+ handoffId: patch.handoffId ?? state.agentActivity?.handoffId,
45
+ closedAt: patch.closeSession ? now : state.agentActivity?.closedAt,
46
+ };
47
+ return activity;
48
+ }
49
+ function createShellSession(patch, now) {
50
+ const baseline = (0, session_1.captureLocalSessionBaseline)();
51
+ const claimedPaths = uniquePaths(patch.claimedPaths ?? []);
52
+ return {
53
+ id: `local_${Date.now().toString(36)}`,
54
+ agentId: "mcp",
55
+ laneDomain: patch.laneDomain ?? null,
56
+ intent: patch.intent?.trim() || undefined,
57
+ changeRequestId: patch.changeRequestId,
58
+ claimedPaths,
59
+ status: "active",
60
+ changedFiles: [],
61
+ crossings: [],
62
+ approvals: [],
63
+ domains: [],
64
+ serverSessionId: patch.workSessionId,
65
+ createdAt: baseline.startedAt,
66
+ updatedAt: now,
67
+ startedAt: baseline.startedAt,
68
+ gitHead: baseline.gitHead,
69
+ dirtyFilesAtStart: baseline.dirtyFilesAtStart,
70
+ fingerprintsAtStart: baseline.fingerprintsAtStart,
71
+ };
72
+ }
73
+ /** Pure merge for tests — does not write disk. */
74
+ function applyAgentActivityMerge(existing, patch, now = new Date().toISOString()) {
75
+ const needsNew = !existing || existing.status === "closed" || (patch.event === "start_work_session" && existing.status !== "active");
76
+ let state;
77
+ if (needsNew && patch.event !== "close_work_session") {
78
+ state = createShellSession(patch, now);
79
+ }
80
+ else if (!existing) {
81
+ state = createShellSession(patch, now);
82
+ }
83
+ else {
84
+ state = { ...existing };
85
+ }
86
+ const agentActivity = buildAgentActivity(state, patch, now);
87
+ state.agentActivity = agentActivity;
88
+ state.updatedAt = now;
89
+ if (patch.workSessionId) {
90
+ state.serverSessionId = patch.workSessionId;
91
+ }
92
+ if (patch.changeRequestId) {
93
+ state.changeRequestId = patch.changeRequestId;
94
+ }
95
+ if (patch.intent?.trim()) {
96
+ state.intent = patch.intent.trim();
97
+ }
98
+ const mergedClaimed = effectiveClaimedPaths(state, patch);
99
+ if (mergedClaimed.length > 0) {
100
+ state.claimedPaths = mergedClaimed;
101
+ }
102
+ if (patch.laneDomain !== undefined) {
103
+ state.laneDomain = patch.laneDomain;
104
+ }
105
+ if (patch.closeSession) {
106
+ state.status = "closed";
107
+ state.closedAt = now;
108
+ }
109
+ else if (state.status === "closed" && patch.event === "start_work_session") {
110
+ state.status = "active";
111
+ state.closedAt = undefined;
112
+ }
113
+ return state;
114
+ }
115
+ /** Mirror MCP lifecycle success into `.agentbridge/session.json`. Never throws. */
116
+ function mirrorMcpLifecycleEvent(patch) {
117
+ try {
118
+ const existing = (0, session_state_1.readSessionState)();
119
+ const now = new Date().toISOString();
120
+ const next = applyAgentActivityMerge(existing, patch, now);
121
+ (0, session_state_1.writeSessionState)(next);
122
+ }
123
+ catch {
124
+ // Swallow — MCP tools must not fail on mirror errors.
125
+ }
126
+ }
127
+ function asStringArray(value) {
128
+ if (!Array.isArray(value))
129
+ return [];
130
+ return value.filter((item) => typeof item === "string");
131
+ }
132
+ function laneDomainFromArgs(args) {
133
+ const type = typeof args.type === "string" ? args.type : undefined;
134
+ const value = typeof args.value === "string" ? args.value : undefined;
135
+ if (type === "product_area" && value)
136
+ return value;
137
+ return null;
138
+ }
139
+ function buildMirrorPatchFromMcp(toolName, args, json) {
140
+ switch (toolName) {
141
+ case "start_work_session": {
142
+ const status = typeof json.status === "string" ? json.status : "";
143
+ if (status === "pending_boundary_approval" || status === "blocked")
144
+ return null;
145
+ const workSessionId = typeof json.id === "string" ? json.id : undefined;
146
+ const intent = typeof args.intent === "string" ? args.intent : undefined;
147
+ const changeRequestId = typeof args.change_request_id === "string" ? args.change_request_id : undefined;
148
+ const claimedPaths = asStringArray(args.claimed_paths);
149
+ return {
150
+ event: "start_work_session",
151
+ workSessionId,
152
+ changeRequestId,
153
+ intent,
154
+ claimedPaths,
155
+ summary: intent,
156
+ };
157
+ }
158
+ case "start_recovery_bootstrap_session": {
159
+ const workSessionId = typeof json.work_session_id === "string"
160
+ ? json.work_session_id
161
+ : typeof json.id === "string"
162
+ ? json.id
163
+ : undefined;
164
+ const intent = typeof args.intent === "string" ? args.intent : "recovery bootstrap";
165
+ const claimedPaths = asStringArray(args.claimed_paths);
166
+ return {
167
+ event: "start_work_session",
168
+ workSessionId,
169
+ intent,
170
+ claimedPaths,
171
+ summary: intent,
172
+ };
173
+ }
174
+ case "claim_lane": {
175
+ const workSessionId = typeof args.work_session_id === "string"
176
+ ? args.work_session_id
177
+ : typeof json.work_session_id === "string"
178
+ ? json.work_session_id
179
+ : undefined;
180
+ const changedFiles = asStringArray(args.changed_files);
181
+ const type = typeof args.type === "string" ? args.type : undefined;
182
+ const value = typeof args.value === "string" ? args.value : undefined;
183
+ const laneId = typeof json.id === "string" ? json.id : undefined;
184
+ return {
185
+ event: "claim_lane",
186
+ workSessionId,
187
+ claimedPaths: changedFiles,
188
+ laneDomain: laneDomainFromArgs(args),
189
+ laneId,
190
+ laneType: type,
191
+ laneValue: value,
192
+ summary: value ? `claimed ${type ?? "lane"}: ${value}` : "claimed lane",
193
+ };
194
+ }
195
+ case "release_lane":
196
+ return {
197
+ event: "release_lane",
198
+ workSessionId: typeof args.work_session_id === "string" ? args.work_session_id : undefined,
199
+ laneId: typeof args.lane_id === "string" ? args.lane_id : undefined,
200
+ summary: "released lane",
201
+ };
202
+ case "update_work_session_scope": {
203
+ const additional = asStringArray(args.additional_paths);
204
+ return {
205
+ event: "update_work_session_scope",
206
+ workSessionId: typeof args.work_session_id === "string" ? args.work_session_id : undefined,
207
+ claimedPaths: additional,
208
+ summary: additional.length > 0 ? `scope +${additional.length} path(s)` : "updated scope",
209
+ };
210
+ }
211
+ case "post_handoff": {
212
+ const handoffId = typeof json.id === "string" ? json.id : undefined;
213
+ const summary = typeof args.completed === "string"
214
+ ? args.completed
215
+ : typeof args.summary === "string"
216
+ ? args.summary
217
+ : typeof args.handoff_summary === "string"
218
+ ? args.handoff_summary
219
+ : "handoff posted";
220
+ return {
221
+ event: "post_handoff",
222
+ workSessionId: typeof args.work_session_id === "string" ? args.work_session_id : undefined,
223
+ handoffId,
224
+ summary,
225
+ };
226
+ }
227
+ case "close_work_session":
228
+ return {
229
+ event: "close_work_session",
230
+ workSessionId: typeof args.work_session_id === "string" ? args.work_session_id : undefined,
231
+ summary: typeof args.note === "string"
232
+ ? args.note
233
+ : typeof args.summary === "string"
234
+ ? args.summary
235
+ : "session closed",
236
+ closeSession: true,
237
+ };
238
+ default:
239
+ return null;
240
+ }
241
+ }
242
+ function mirrorMcpToolSuccess(toolName, args, json) {
243
+ const patch = buildMirrorPatchFromMcp(toolName, args, json);
244
+ if (!patch)
245
+ return;
246
+ mirrorMcpLifecycleEvent(patch);
247
+ }