vibe-splain 3.0.0 → 3.2.0

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.
Files changed (46) hide show
  1. package/dist/commands/bundle.d.ts +4 -0
  2. package/dist/commands/bundle.js +68 -0
  3. package/dist/commands/gc.d.ts +3 -0
  4. package/dist/commands/gc.js +59 -0
  5. package/dist/commands/importBundle.d.ts +4 -0
  6. package/dist/commands/importBundle.js +80 -0
  7. package/dist/export/ArtifactBundleWriter.js +24 -6
  8. package/dist/export/ExportOrchestrator.d.ts +19 -1
  9. package/dist/export/ExportOrchestrator.js +90 -3
  10. package/dist/export/Watcher.d.ts +1 -1
  11. package/dist/export/Watcher.js +9 -1
  12. package/dist/export/renderers/AgentMarkdownRenderer.d.ts +2 -1
  13. package/dist/export/renderers/AgentMarkdownRenderer.js +17 -1
  14. package/dist/export/renderers/HtmlRenderer.js +29 -6
  15. package/dist/index.js +1671 -129
  16. package/dist/mcp/BudgetGuard.d.ts +13 -0
  17. package/dist/mcp/BudgetGuard.js +55 -0
  18. package/dist/mcp/SessionScope.d.ts +26 -0
  19. package/dist/mcp/SessionScope.js +56 -0
  20. package/dist/mcp/server.js +38 -0
  21. package/dist/mcp/tools/apply_patch.d.ts +37 -0
  22. package/dist/mcp/tools/apply_patch.js +103 -0
  23. package/dist/mcp/tools/get_file_skeleton.d.ts +23 -0
  24. package/dist/mcp/tools/get_file_skeleton.js +124 -0
  25. package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +31 -0
  26. package/dist/mcp/tools/hydration/get_evidence_slice.js +59 -0
  27. package/dist/mcp/tools/hydration/get_project_summary.d.ts +23 -0
  28. package/dist/mcp/tools/hydration/get_project_summary.js +58 -0
  29. package/dist/mcp/tools/hydration/get_start_here.d.ts +23 -0
  30. package/dist/mcp/tools/hydration/get_start_here.js +52 -0
  31. package/dist/mcp/tools/read_file.d.ts +31 -0
  32. package/dist/mcp/tools/read_file.js +90 -0
  33. package/dist/mcp/tools/scan_project.js +6 -3
  34. package/dist/mcp/tools/set_session_scope.d.ts +19 -0
  35. package/dist/mcp/tools/set_session_scope.js +40 -0
  36. package/dist/mcp/tools/submit_receipt.d.ts +68 -0
  37. package/dist/mcp/tools/submit_receipt.js +94 -0
  38. package/dist/mcp/tools/work_orders.d.ts +79 -0
  39. package/dist/mcp/tools/work_orders.js +126 -0
  40. package/dist/mcp/tools/yield_for_scope_expansion.d.ts +29 -0
  41. package/dist/mcp/tools/yield_for_scope_expansion.js +59 -0
  42. package/dist/store/BlobStore.d.ts +22 -0
  43. package/dist/store/BlobStore.js +96 -0
  44. package/dist/store/PointerStore.d.ts +52 -0
  45. package/dist/store/PointerStore.js +138 -0
  46. package/package.json +8 -1
package/dist/index.js CHANGED
@@ -1201,10 +1201,10 @@ async function discoverAllTsConfigs(dir, projectRoot, maxDepth = 4) {
1201
1201
  const result = {};
1202
1202
  if (maxDepth < 0)
1203
1203
  return result;
1204
- const { readdir: readdir2 } = await import("fs/promises");
1204
+ const { readdir: readdir3 } = await import("fs/promises");
1205
1205
  let entries = [];
1206
1206
  try {
1207
- entries = await readdir2(dir, { withFileTypes: true });
1207
+ entries = await readdir3(dir, { withFileTypes: true });
1208
1208
  } catch {
1209
1209
  return result;
1210
1210
  }
@@ -1284,10 +1284,10 @@ async function discoverWorkspacePackages(projectRoot) {
1284
1284
  const absPrefix = join3(projectRoot, prefix);
1285
1285
  if (!existsSync3(absPrefix))
1286
1286
  continue;
1287
- const { readdir: readdir2 } = await import("fs/promises");
1287
+ const { readdir: readdir3 } = await import("fs/promises");
1288
1288
  let entries = [];
1289
1289
  try {
1290
- const dirents = await readdir2(absPrefix, { withFileTypes: true });
1290
+ const dirents = await readdir3(absPrefix, { withFileTypes: true });
1291
1291
  entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
1292
1292
  } catch {
1293
1293
  continue;
@@ -2276,7 +2276,8 @@ async function runActionBinding(projectRoot, inv, res) {
2276
2276
  const startLine = node.startPosition.row + 1;
2277
2277
  const startCol = node.startPosition.column;
2278
2278
  const endLine = node.endPosition.row + 1;
2279
- const isDuplicate = functions.some((f) => f.startLine === startLine && f.endLine === endLine);
2279
+ const endCol = node.endPosition.column;
2280
+ const isDuplicate = functions.some((f) => f.startLine === startLine && f.startCol === startCol && f.endLine === endLine && f.functionKind === node.type);
2280
2281
  if (isDuplicate)
2281
2282
  continue;
2282
2283
  functionsExtracted++;
@@ -2538,13 +2539,30 @@ async function runActionBinding(projectRoot, inv, res) {
2538
2539
  for (const fileRec of Object.values(artifact.files)) {
2539
2540
  for (const fnRec of fileRec.functions) {
2540
2541
  for (const callRec of fnRec.calls) {
2541
- if (callRec.resolutionKind === "named_import_match" && callRec.resolvedFilePath) {
2542
- const targetFile = artifact.files[callRec.resolvedFilePath];
2543
- if (targetFile) {
2544
- const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeRoot);
2545
- if (targetFn) {
2546
- callRec.resolvedTargetFunctionId = targetFn.functionId;
2547
- }
2542
+ if (callRec.resolvedTargetFunctionId)
2543
+ continue;
2544
+ if (!callRec.resolvedFilePath)
2545
+ continue;
2546
+ const targetFile = artifact.files[callRec.resolvedFilePath];
2547
+ if (!targetFile)
2548
+ continue;
2549
+ if (callRec.resolutionKind === "named_import_match") {
2550
+ const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeRoot && f.isExported);
2551
+ if (targetFn) {
2552
+ callRec.resolvedTargetFunctionId = targetFn.functionId;
2553
+ callRec.confidence = "high";
2554
+ }
2555
+ } else if (callRec.resolutionKind === "namespace_import_property" && callRec.calleeProperty) {
2556
+ const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeProperty && f.isExported);
2557
+ if (targetFn) {
2558
+ callRec.resolvedTargetFunctionId = targetFn.functionId;
2559
+ callRec.confidence = "high";
2560
+ }
2561
+ } else if (callRec.resolutionKind === "namespace_import_property" && !callRec.calleeProperty) {
2562
+ const defaultFn = targetFile.functions.find((f) => f.displayName === "default");
2563
+ if (defaultFn) {
2564
+ callRec.resolvedTargetFunctionId = defaultFn.functionId;
2565
+ callRec.confidence = "high";
2548
2566
  }
2549
2567
  }
2550
2568
  }
@@ -2591,14 +2609,15 @@ async function traverseCallChain(projectRoot, args) {
2591
2609
  const chain = [];
2592
2610
  const unresolvedEdges = [];
2593
2611
  const visited = /* @__PURE__ */ new Set();
2594
- const queue = seedFunctionIds.map((id) => ({ functionId: id, depth: 0 }));
2612
+ const queue = seedFunctionIds.map((id) => ({ functionId: id, callerFunctionId: null, depth: 0 }));
2595
2613
  let targetReached = false;
2596
2614
  let truncatedAtDepth = false;
2597
2615
  while (queue.length > 0) {
2598
- const { functionId, depth } = queue.shift();
2599
- if (visited.has(functionId))
2616
+ const { functionId, callerFunctionId, depth, callsite } = queue.shift();
2617
+ const visitKey = `${callerFunctionId}->${functionId}`;
2618
+ if (visited.has(visitKey))
2600
2619
  continue;
2601
- visited.add(functionId);
2620
+ visited.add(visitKey);
2602
2621
  const indexEntry = artifact.functionIndex[functionId];
2603
2622
  if (!indexEntry)
2604
2623
  continue;
@@ -2610,63 +2629,42 @@ async function traverseCallChain(projectRoot, args) {
2610
2629
  const fnRec = fileRec.functions.find((f) => f.functionId === functionId);
2611
2630
  if (!fnRec)
2612
2631
  continue;
2613
- for (const call of fnRec.calls) {
2614
- if (call.resolutionKind === "semantic_action_only")
2615
- continue;
2616
- if (call.resolvedTargetFunctionId) {
2617
- if (depth < maxDepth) {
2618
- queue.push({ functionId: call.resolvedTargetFunctionId, depth: depth + 1 });
2619
- let isTarget = false;
2620
- if (targetFunctionName && call.calleeRoot === targetFunctionName)
2621
- isTarget = true;
2622
- if (isTarget)
2623
- targetReached = true;
2624
- chain.push({
2625
- functionId: call.resolvedTargetFunctionId,
2626
- displayName: call.calleeRoot,
2627
- filePath: call.resolvedFilePath || "unknown",
2628
- startLine: call.sourceLine,
2629
- edgeKind: "call_edge",
2630
- confidence: call.confidence,
2631
- evidenceText: call.evidenceText,
2632
- isTarget,
2633
- depth
2634
- });
2635
- } else {
2636
- unresolvedEdges.push({
2637
- fromFunctionId: functionId,
2638
- calleeText: call.calleeText,
2639
- sourceLine: call.sourceLine,
2640
- reason: "depth limit reached"
2641
- });
2642
- truncatedAtDepth = true;
2643
- }
2644
- } else {
2645
- unresolvedEdges.push({
2646
- fromFunctionId: functionId,
2647
- calleeText: call.calleeText,
2648
- sourceLine: call.sourceLine,
2649
- reason: call.resolutionKind
2650
- });
2651
- }
2652
- }
2632
+ let isTarget = false;
2633
+ if (targetFunctionName && fnRec.displayName === targetFunctionName)
2634
+ isTarget = true;
2635
+ if (isTarget)
2636
+ targetReached = true;
2637
+ chain.push({
2638
+ functionId,
2639
+ callerFunctionId,
2640
+ displayName: fnRec.displayName,
2641
+ filePath: fnRec.filePath,
2642
+ startLine: fnRec.startLine,
2643
+ edgeKind: "call_edge",
2644
+ confidence: "high",
2645
+ evidenceText: fnRec.evidenceText,
2646
+ isTarget,
2647
+ depth,
2648
+ callsite
2649
+ });
2653
2650
  for (const action of fnRec.semanticActions) {
2654
- let isTarget = false;
2651
+ let isActionTarget = false;
2655
2652
  if (targetActionKind && action.actionKind === targetActionKind) {
2656
- isTarget = true;
2653
+ isActionTarget = true;
2657
2654
  if (targetModel && action.targetModel !== targetModel)
2658
- isTarget = false;
2655
+ isActionTarget = false;
2659
2656
  if (targetOperation && action.targetOperation !== targetOperation)
2660
- isTarget = false;
2657
+ isActionTarget = false;
2661
2658
  } else if (targetModel && action.targetModel === targetModel) {
2662
- isTarget = true;
2659
+ isActionTarget = true;
2663
2660
  if (targetOperation && action.targetOperation !== targetOperation)
2664
- isTarget = false;
2661
+ isActionTarget = false;
2665
2662
  }
2666
- if (isTarget)
2663
+ if (isActionTarget)
2667
2664
  targetReached = true;
2668
2665
  chain.push({
2669
- functionId: action.sourceFunctionId,
2666
+ functionId: action.actionId,
2667
+ callerFunctionId: functionId,
2670
2668
  displayName: action.calleeText,
2671
2669
  filePath: fileRec.filePath,
2672
2670
  startLine: action.sourceLine,
@@ -2676,10 +2674,35 @@ async function traverseCallChain(projectRoot, args) {
2676
2674
  targetOperation: action.targetOperation || void 0,
2677
2675
  confidence: action.confidence,
2678
2676
  evidenceText: action.evidenceText,
2679
- isTarget,
2680
- depth
2677
+ isTarget: isActionTarget,
2678
+ depth: depth + 1
2681
2679
  });
2682
2680
  }
2681
+ if (depth < maxDepth) {
2682
+ for (const call of fnRec.calls) {
2683
+ if (call.resolvedTargetFunctionId) {
2684
+ queue.push({
2685
+ functionId: call.resolvedTargetFunctionId,
2686
+ callerFunctionId: functionId,
2687
+ depth: depth + 1,
2688
+ callsite: {
2689
+ file: fileRec.filePath,
2690
+ line: call.sourceLine,
2691
+ text: call.calleeText
2692
+ }
2693
+ });
2694
+ } else {
2695
+ unresolvedEdges.push({
2696
+ fromFunctionId: functionId,
2697
+ calleeText: call.calleeText,
2698
+ sourceLine: call.sourceLine,
2699
+ reason: call.resolutionKind
2700
+ });
2701
+ }
2702
+ }
2703
+ } else if (fnRec.calls.length > 0) {
2704
+ truncatedAtDepth = true;
2705
+ }
2683
2706
  }
2684
2707
  return {
2685
2708
  targetReached,
@@ -2895,33 +2918,13 @@ async function buildValidationReport(store, deltaTargets, projectRoot, cr) {
2895
2918
  passCount++;
2896
2919
  }
2897
2920
  const PAYMENT_PROVIDER_PATH_TERMS = ["stripe", "paypal", "btcpay", "btcpayserver", "alby", "hitpay", "payment"];
2898
- const PAYMENT_CONTENT_TERMS = [
2899
- "constructEvent",
2900
- "checkoutSession",
2901
- "paymentIntent",
2902
- "stripe-signature",
2903
- "webhook-signature",
2904
- "payment_mutation",
2905
- "paymentStatus",
2906
- "invoicePaid",
2907
- "chargeSucceeded"
2908
- ];
2909
2921
  for (const [rel, pf] of Object.entries(store.files)) {
2910
2922
  if (!pf.isRealSource)
2911
2923
  continue;
2912
- const pathLower = rel.toLowerCase();
2913
- if (!pathLower.includes("webhook"))
2914
- continue;
2915
- const primaryTrigger = PAYMENT_PROVIDER_PATH_TERMS.some((t) => pathLower.includes(t));
2916
- let secondaryTrigger = false;
2917
- if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
2918
- try {
2919
- const src = await readFile5(join6(projectRoot, rel), "utf8");
2920
- secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
2921
- } catch {
2922
- }
2923
- }
2924
- if (!primaryTrigger && !secondaryTrigger)
2924
+ const hasIntent = pf.writeIntents.includes("handle_payment_webhook");
2925
+ const hasEffects = pf.sideEffectProfile.includes("webhook_ingress") || pf.sideEffectProfile.includes("payment_mutation");
2926
+ const pathMentionsPayment = PAYMENT_PROVIDER_PATH_TERMS.some((t) => rel.toLowerCase().includes(t));
2927
+ if (!hasIntent && !(hasEffects && pathMentionsPayment))
2925
2928
  continue;
2926
2929
  const webhookChecks = [
2927
2930
  [
@@ -3087,6 +3090,15 @@ async function readAnalysis(projectRoot) {
3087
3090
  return null;
3088
3091
  }
3089
3092
  }
3093
+ async function readActionBindings(projectRoot) {
3094
+ const p = join8(projectRoot, ".vibe-splainer", "action_bindings.json");
3095
+ try {
3096
+ const raw = await readFile7(p, "utf8");
3097
+ return JSON.parse(raw);
3098
+ } catch {
3099
+ return null;
3100
+ }
3101
+ }
3090
3102
 
3091
3103
  // ../brain/dist/dossier.js
3092
3104
  import { join as join9 } from "path";
@@ -3198,10 +3210,93 @@ var RecommendationEngine = class {
3198
3210
  }
3199
3211
  };
3200
3212
 
3213
+ // ../brain/dist/ProofValidator.js
3214
+ import { createHash } from "crypto";
3215
+ import { readFile as readFile9 } from "fs/promises";
3216
+ import { existsSync as existsSync4 } from "fs";
3217
+ var ProofValidator = class {
3218
+ /**
3219
+ * Full validation of a WorkerReceipt against a Work Order's required proofs.
3220
+ * Checks all 8 required conditions from the spec.
3221
+ *
3222
+ * @param receipt - The WorkerReceipt to validate
3223
+ * @param requiredProof - Proof descriptors from the Work Order
3224
+ * @param isAllowedFile - Predicate built by the CLI layer (handles globs/allowedFiles)
3225
+ * @param blobDir - Path to .vibe-splainer/blobs/
3226
+ */
3227
+ static async validate(receipt, requiredProof, isAllowedFile, blobDir) {
3228
+ const errors = [];
3229
+ const warnings = [];
3230
+ for (const req of requiredProof) {
3231
+ const found = receipt.proofPointers.find((p) => p.schemaName === req.schemaName || p.pointer.includes(req.proofId));
3232
+ if (!found) {
3233
+ errors.push(`MissingProof: required proof "${req.proofId}" (schema: ${req.schemaName}) not in receipt`);
3234
+ }
3235
+ }
3236
+ for (const proof of receipt.proofPointers) {
3237
+ const blobPath = resolveBlobPath(blobDir, proof.contentHash);
3238
+ if (!existsSync4(blobPath)) {
3239
+ errors.push(`UnresolvablePointer: blob not found for pointer ${proof.pointer} (hash: ${proof.contentHash})`);
3240
+ continue;
3241
+ }
3242
+ const actualHash = await hashBlob(blobPath);
3243
+ if (actualHash !== proof.contentHash) {
3244
+ errors.push(`HashMismatch: proof ${proof.pointer} expected ${proof.contentHash}, got ${actualHash}`);
3245
+ continue;
3246
+ }
3247
+ const reqDescriptor = requiredProof.find((r) => r.schemaName === proof.schemaName);
3248
+ if (!reqDescriptor) {
3249
+ warnings.push(`UnknownSchema: proof ${proof.pointer} has schema "${proof.schemaName}" not listed in requiredProof`);
3250
+ }
3251
+ if (proof.schemaName.startsWith("test_report")) {
3252
+ try {
3253
+ const blobContent = await readFile9(blobPath, "utf8");
3254
+ const report = JSON.parse(blobContent);
3255
+ if (report.status !== "pass" && report.passed !== true && report.success !== true) {
3256
+ errors.push(`TestFailed: proof ${proof.pointer} (schema: ${proof.schemaName}) reports status "${report.status ?? "unknown"}"`);
3257
+ }
3258
+ } catch {
3259
+ errors.push(`UnreadableProof: cannot parse proof blob for ${proof.pointer}`);
3260
+ }
3261
+ }
3262
+ }
3263
+ for (const changed of receipt.changedFiles) {
3264
+ if (!isAllowedFile(changed.path)) {
3265
+ errors.push(`ScopeViolation: patch touched out-of-scope file "${changed.path}"`);
3266
+ }
3267
+ }
3268
+ for (const changed of receipt.changedFiles) {
3269
+ if (!changed.prePatchHash.startsWith("sha256:") && changed.prePatchHash !== "sha256:new") {
3270
+ errors.push(`InvalidHash: prePatchHash for ${changed.path} is not sha256 format`);
3271
+ }
3272
+ if (!changed.postPatchHash.startsWith("sha256:")) {
3273
+ errors.push(`InvalidHash: postPatchHash for ${changed.path} is not sha256 format`);
3274
+ }
3275
+ const matchingPatch = receipt.proofPointers.find((p) => p.schemaName === "patch_hash" && p.contentHash === changed.postPatchHash);
3276
+ if (!matchingPatch) {
3277
+ warnings.push(`NoMatchingPatchProof: no patch_hash proof matches postPatchHash for ${changed.path}`);
3278
+ }
3279
+ }
3280
+ return {
3281
+ valid: errors.length === 0,
3282
+ errors,
3283
+ warnings
3284
+ };
3285
+ }
3286
+ };
3287
+ function resolveBlobPath(blobDir, contentHash) {
3288
+ const hex = contentHash.replace("sha256:", "");
3289
+ return `${blobDir}/sha256_${hex}`;
3290
+ }
3291
+ async function hashBlob(blobPath) {
3292
+ const buf = await readFile9(blobPath);
3293
+ return `sha256:${createHash("sha256").update(buf).digest("hex")}`;
3294
+ }
3295
+
3201
3296
  // dist/export/ArtifactBundleWriter.js
3202
3297
  import { join as join10 } from "path";
3203
3298
  import { writeFile as writeFile7, mkdir as mkdir6, rm, rename } from "fs/promises";
3204
- import { createHash } from "crypto";
3299
+ import { createHash as createHash2 } from "crypto";
3205
3300
  var ArtifactBundleWriter = class {
3206
3301
  projectRoot;
3207
3302
  constructor(projectRoot) {
@@ -3210,11 +3305,13 @@ var ArtifactBundleWriter = class {
3210
3305
  async writeBundle(artifacts) {
3211
3306
  const outputDir = join10(this.projectRoot, ".vibe-splainer");
3212
3307
  const stagingDir = join10(this.projectRoot, ".vibe-splainer.tmp");
3308
+ const oldDir = join10(this.projectRoot, ".vibe-splainer.old");
3213
3309
  try {
3214
3310
  await rm(stagingDir, { recursive: true, force: true });
3215
- const { existsSync: existsSync5 } = await import("fs");
3311
+ await rm(oldDir, { recursive: true, force: true });
3312
+ const { existsSync: existsSync9 } = await import("fs");
3216
3313
  const { cp } = await import("fs/promises");
3217
- if (existsSync5(outputDir)) {
3314
+ if (existsSync9(outputDir)) {
3218
3315
  await cp(outputDir, stagingDir, { recursive: true });
3219
3316
  } else {
3220
3317
  await mkdir6(stagingDir, { recursive: true });
@@ -3229,7 +3326,7 @@ var ArtifactBundleWriter = class {
3229
3326
  manifestArtifacts.push({
3230
3327
  type: artifact.type,
3231
3328
  path: artifact.path,
3232
- checksum: "sha256:" + createHash("sha256").update(buffer).digest("hex"),
3329
+ checksum: "sha256:" + createHash2("sha256").update(buffer).digest("hex"),
3233
3330
  sizeBytes: buffer.length
3234
3331
  });
3235
3332
  }
@@ -3240,8 +3337,22 @@ var ArtifactBundleWriter = class {
3240
3337
  artifacts: manifestArtifacts
3241
3338
  };
3242
3339
  await writeFile7(join10(stagingDir, "artifact_manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3243
- await rm(outputDir, { recursive: true, force: true });
3244
- await rename(stagingDir, outputDir);
3340
+ let swapped = false;
3341
+ if (existsSync9(outputDir)) {
3342
+ await rename(outputDir, oldDir);
3343
+ swapped = true;
3344
+ }
3345
+ try {
3346
+ await rename(stagingDir, outputDir);
3347
+ } catch (err) {
3348
+ if (swapped) {
3349
+ await rename(oldDir, outputDir);
3350
+ }
3351
+ throw err;
3352
+ }
3353
+ if (swapped) {
3354
+ await rm(oldDir, { recursive: true, force: true });
3355
+ }
3245
3356
  } catch (err) {
3246
3357
  await rm(stagingDir, { recursive: true, force: true });
3247
3358
  throw err;
@@ -3265,7 +3376,7 @@ var JsonRenderer = class {
3265
3376
  // dist/export/renderers/HtmlRenderer.js
3266
3377
  import { join as join11, dirname as dirname3, relative as relative3 } from "path";
3267
3378
  import { fileURLToPath as fileURLToPath2 } from "url";
3268
- import { existsSync as existsSync4, readFileSync, readdirSync, statSync } from "fs";
3379
+ import { existsSync as existsSync5, readFileSync, readdirSync, statSync } from "fs";
3269
3380
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
3270
3381
  function getAllFiles(dirPath, arrayOfFiles = []) {
3271
3382
  const files = readdirSync(dirPath);
@@ -3281,12 +3392,37 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
3281
3392
  }
3282
3393
  var HtmlRenderer = class {
3283
3394
  render(viewModel, _store) {
3284
- let templateDir = join11(__dirname2, "..", "..", "ui");
3285
- if (!existsSync4(templateDir)) {
3286
- templateDir = join11(__dirname2, "..", "..", "..", "..", "dist", "ui");
3395
+ const candidatePaths = [
3396
+ join11(__dirname2, "ui"),
3397
+ // bundled: dist/index.js -> dist/ui
3398
+ join11(__dirname2, "..", "..", "ui"),
3399
+ // unbundled: dist/export/renderers -> dist/ui
3400
+ join11(__dirname2, "..", "ui"),
3401
+ // alt bundle
3402
+ join11(__dirname2, "..", "..", "..", "ui", "dist"),
3403
+ // dev: packages/cli/src/export/renderers -> packages/ui/dist
3404
+ join11(__dirname2, "..", "..", "packages", "ui", "dist")
3405
+ // repo root -> packages/ui/dist
3406
+ ];
3407
+ let templateDir = "";
3408
+ for (const p of candidatePaths) {
3409
+ if (existsSync5(p) && existsSync5(join11(p, "index.html"))) {
3410
+ if (!existsSync5(join11(p, "vite.config.ts")) || p.endsWith("dist")) {
3411
+ templateDir = p;
3412
+ break;
3413
+ }
3414
+ }
3287
3415
  }
3288
- if (!existsSync4(templateDir)) {
3289
- console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
3416
+ if (!templateDir) {
3417
+ for (const p of candidatePaths) {
3418
+ if (existsSync5(join11(p, "index.html"))) {
3419
+ templateDir = p;
3420
+ break;
3421
+ }
3422
+ }
3423
+ }
3424
+ if (!templateDir) {
3425
+ console.error("[vibe-splain] UI template not found. Checked:", candidatePaths);
3290
3426
  return [];
3291
3427
  }
3292
3428
  const artifacts = [];
@@ -3337,8 +3473,10 @@ var DeltaRenderer = class {
3337
3473
  // dist/export/renderers/AgentMarkdownRenderer.js
3338
3474
  var AgentMarkdownRenderer = class {
3339
3475
  budget;
3340
- constructor(budget = 8e3) {
3476
+ bindings;
3477
+ constructor(budget = 8e3, bindings = null) {
3341
3478
  this.budget = budget;
3479
+ this.bindings = bindings;
3342
3480
  }
3343
3481
  render(viewModel, store) {
3344
3482
  let md = `# Architectural Dossier: ${viewModel.projectRoot}
@@ -3401,6 +3539,23 @@ ${viewModel.map.brief}
3401
3539
  md += `**Narrative**: ${card.narrative}
3402
3540
  `;
3403
3541
  }
3542
+ if (this.bindings && this.bindings.files[path]) {
3543
+ const fileBinding = this.bindings.files[path];
3544
+ const criticalFunctions = fileBinding.functions.filter((fn) => fn.semanticActions.length > 0 || fn.isEntrypoint);
3545
+ if (criticalFunctions.length > 0) {
3546
+ md += `
3547
+ **Critical Functions**:
3548
+ `;
3549
+ for (const fn of criticalFunctions) {
3550
+ md += `- \`${fn.displayName}\` (lines ${fn.startLine}-${fn.endLine})${fn.isEntrypoint ? " [Entrypoint]" : ""}
3551
+ `;
3552
+ for (const action of fn.semanticActions) {
3553
+ md += ` - **${action.actionKind}**${action.targetModel ? ` on ${action.targetModel}` : ""}: \`${action.calleeText}\` (line ${action.sourceLine})
3554
+ `;
3555
+ }
3556
+ }
3557
+ }
3558
+ }
3404
3559
  if (recs.length > 0) {
3405
3560
  md += `
3406
3561
  **Safe Patch Strategies**:
@@ -3495,17 +3650,239 @@ var GraphRenderer = class {
3495
3650
  }
3496
3651
  };
3497
3652
 
3653
+ // dist/store/BlobStore.js
3654
+ import { createHash as createHash3 } from "crypto";
3655
+ import { mkdir as mkdir7, writeFile as writeFile8, open, rename as rename2, stat } from "fs/promises";
3656
+ import { existsSync as existsSync6 } from "fs";
3657
+ import { join as join12 } from "path";
3658
+ var BlobStore = class {
3659
+ blobsDir;
3660
+ tmpDir;
3661
+ constructor(projectRoot) {
3662
+ this.blobsDir = join12(projectRoot, ".vibe-splainer", "blobs");
3663
+ this.tmpDir = join12(projectRoot, ".vibe-splainer", "tmp");
3664
+ }
3665
+ async ensureDirs() {
3666
+ await mkdir7(this.blobsDir, { recursive: true });
3667
+ await mkdir7(this.tmpDir, { recursive: true });
3668
+ }
3669
+ async writeAtomic(payload) {
3670
+ await this.ensureDirs();
3671
+ const buf = typeof payload === "string" ? Buffer.from(payload, "utf8") : payload;
3672
+ const hex = createHash3("sha256").update(buf).digest("hex");
3673
+ const contentHash = `sha256:${hex}`;
3674
+ const blobPath = join12(this.blobsDir, `sha256_${hex}`);
3675
+ if (existsSync6(blobPath)) {
3676
+ return { contentHash, blobPath };
3677
+ }
3678
+ const tmpPath = join12(this.tmpDir, `tmp_${hex}_${Date.now()}`);
3679
+ await writeFile8(tmpPath, buf);
3680
+ const fh = await open(tmpPath, "r");
3681
+ try {
3682
+ await fh.datasync();
3683
+ } finally {
3684
+ await fh.close();
3685
+ }
3686
+ await rename2(tmpPath, blobPath);
3687
+ return { contentHash, blobPath };
3688
+ }
3689
+ async readBlob(blobPath) {
3690
+ const { readFile: readFile16 } = await import("fs/promises");
3691
+ return readFile16(blobPath);
3692
+ }
3693
+ async blobExists(contentHash) {
3694
+ const hex = contentHash.replace("sha256:", "");
3695
+ const blobPath = join12(this.blobsDir, `sha256_${hex}`);
3696
+ return existsSync6(blobPath);
3697
+ }
3698
+ blobPathForHash(contentHash) {
3699
+ const hex = contentHash.replace("sha256:", "");
3700
+ return join12(this.blobsDir, `sha256_${hex}`);
3701
+ }
3702
+ async verifyIntegrity(blobPath, expectedHash) {
3703
+ try {
3704
+ const { readFile: readFile16 } = await import("fs/promises");
3705
+ const buf = await readFile16(blobPath);
3706
+ const hex = createHash3("sha256").update(buf).digest("hex");
3707
+ return `sha256:${hex}` === expectedHash;
3708
+ } catch {
3709
+ return false;
3710
+ }
3711
+ }
3712
+ /** List all blob paths for GC reference counting */
3713
+ async listBlobPaths() {
3714
+ try {
3715
+ const { readdir: readdir3 } = await import("fs/promises");
3716
+ const files = await readdir3(this.blobsDir);
3717
+ return files.filter((f) => f.startsWith("sha256_")).map((f) => join12(this.blobsDir, f));
3718
+ } catch {
3719
+ return [];
3720
+ }
3721
+ }
3722
+ async getBlobSize(blobPath) {
3723
+ try {
3724
+ const info = await stat(blobPath);
3725
+ return info.size;
3726
+ } catch {
3727
+ return 0;
3728
+ }
3729
+ }
3730
+ };
3731
+ async function hashFile(filePath) {
3732
+ const { readFile: readFile16 } = await import("fs/promises");
3733
+ const buf = await readFile16(filePath);
3734
+ return `sha256:${createHash3("sha256").update(buf).digest("hex")}`;
3735
+ }
3736
+
3737
+ // dist/store/PointerStore.js
3738
+ import Database from "better-sqlite3";
3739
+ import { join as join13 } from "path";
3740
+ import { mkdirSync } from "fs";
3741
+ import { Mutex } from "async-mutex";
3742
+ var instance = null;
3743
+ var PointerStore = class _PointerStore {
3744
+ db;
3745
+ writeMutex = new Mutex();
3746
+ constructor(projectRoot) {
3747
+ const dir = join13(projectRoot, ".vibe-splainer");
3748
+ mkdirSync(dir, { recursive: true });
3749
+ this.db = new Database(join13(dir, "pointer_store.db"));
3750
+ this.db.pragma("busy_timeout = 5000");
3751
+ this.db.pragma("journal_mode = WAL");
3752
+ this.db.pragma("foreign_keys = ON");
3753
+ this._migrate();
3754
+ }
3755
+ static open(projectRoot) {
3756
+ if (!instance)
3757
+ instance = new _PointerStore(projectRoot);
3758
+ return instance;
3759
+ }
3760
+ static reset() {
3761
+ instance = null;
3762
+ }
3763
+ _migrate() {
3764
+ this.db.exec(`
3765
+ CREATE TABLE IF NOT EXISTS pointers (
3766
+ pointerId TEXT PRIMARY KEY,
3767
+ scanId TEXT NOT NULL,
3768
+ artifactName TEXT NOT NULL,
3769
+ contentHash TEXT NOT NULL,
3770
+ blobPath TEXT NOT NULL,
3771
+ schemaVersion TEXT NOT NULL DEFAULT '1.0.0',
3772
+ createdAt INTEGER NOT NULL,
3773
+ expiresAt INTEGER
3774
+ );
3775
+ CREATE INDEX IF NOT EXISTS idx_pointers_scan ON pointers(scanId);
3776
+ CREATE INDEX IF NOT EXISTS idx_pointers_hash ON pointers(contentHash);
3777
+
3778
+ CREATE TABLE IF NOT EXISTS work_orders (
3779
+ workOrderId TEXT PRIMARY KEY,
3780
+ intent TEXT NOT NULL,
3781
+ allowedFiles TEXT NOT NULL DEFAULT '[]',
3782
+ allowedGlobs TEXT NOT NULL DEFAULT '[]',
3783
+ deniedGlobs TEXT NOT NULL DEFAULT '[]',
3784
+ requiredProof TEXT NOT NULL DEFAULT '[]',
3785
+ status TEXT NOT NULL DEFAULT 'pending',
3786
+ createdAt INTEGER NOT NULL
3787
+ );
3788
+
3789
+ CREATE TABLE IF NOT EXISTS receipts (
3790
+ receiptId TEXT PRIMARY KEY,
3791
+ workOrderId TEXT NOT NULL REFERENCES work_orders(workOrderId),
3792
+ status TEXT NOT NULL,
3793
+ proofPointers TEXT NOT NULL DEFAULT '[]',
3794
+ changedFiles TEXT NOT NULL DEFAULT '[]',
3795
+ summary TEXT NOT NULL DEFAULT '',
3796
+ createdAt INTEGER NOT NULL,
3797
+ FOREIGN KEY (workOrderId) REFERENCES work_orders(workOrderId)
3798
+ );
3799
+ `);
3800
+ }
3801
+ async insertPointer(row) {
3802
+ await this.writeMutex.runExclusive(() => {
3803
+ this.db.prepare(`
3804
+ INSERT OR REPLACE INTO pointers
3805
+ (pointerId, scanId, artifactName, contentHash, blobPath, schemaVersion, createdAt, expiresAt)
3806
+ VALUES
3807
+ (@pointerId, @scanId, @artifactName, @contentHash, @blobPath, @schemaVersion, @createdAt, @expiresAt)
3808
+ `).run(row);
3809
+ });
3810
+ }
3811
+ getPointer(pointerId) {
3812
+ return this.db.prepare("SELECT * FROM pointers WHERE pointerId = ?").get(pointerId) ?? null;
3813
+ }
3814
+ listPointersByScan(scanId) {
3815
+ return this.db.prepare("SELECT * FROM pointers WHERE scanId = ?").all(scanId);
3816
+ }
3817
+ async insertWorkOrder(row) {
3818
+ await this.writeMutex.runExclusive(() => {
3819
+ this.db.prepare(`
3820
+ INSERT OR REPLACE INTO work_orders
3821
+ (workOrderId, intent, allowedFiles, allowedGlobs, deniedGlobs, requiredProof, status, createdAt)
3822
+ VALUES
3823
+ (@workOrderId, @intent, @allowedFiles, @allowedGlobs, @deniedGlobs, @requiredProof, @status, @createdAt)
3824
+ `).run(row);
3825
+ });
3826
+ }
3827
+ getWorkOrder(workOrderId) {
3828
+ return this.db.prepare("SELECT * FROM work_orders WHERE workOrderId = ?").get(workOrderId) ?? null;
3829
+ }
3830
+ async updateWorkOrderStatus(workOrderId, status) {
3831
+ await this.writeMutex.runExclusive(() => {
3832
+ this.db.prepare("UPDATE work_orders SET status = ? WHERE workOrderId = ?").run(status, workOrderId);
3833
+ });
3834
+ }
3835
+ async insertReceipt(receipt) {
3836
+ await this.writeMutex.runExclusive(() => {
3837
+ this.db.prepare(`
3838
+ INSERT OR REPLACE INTO receipts
3839
+ (receiptId, workOrderId, status, proofPointers, changedFiles, summary, createdAt)
3840
+ VALUES
3841
+ (@receiptId, @workOrderId, @status, @proofPointers, @changedFiles, @summary, @createdAt)
3842
+ `).run({
3843
+ ...receipt,
3844
+ proofPointers: JSON.stringify(receipt.proofPointers),
3845
+ changedFiles: JSON.stringify(receipt.changedFiles),
3846
+ createdAt: Date.now()
3847
+ });
3848
+ });
3849
+ }
3850
+ /** GC: delete pointers older than cutoffMs and not pinned, return deleted count */
3851
+ async gcScanPointers(keepScanIds) {
3852
+ return await this.writeMutex.runExclusive(() => {
3853
+ const placeholders = keepScanIds.map(() => "?").join(",");
3854
+ const whereClause = keepScanIds.length > 0 ? `WHERE scanId NOT IN (${placeholders})` : "";
3855
+ const result = this.db.prepare(`DELETE FROM pointers ${whereClause}`).run(...keepScanIds);
3856
+ return result.changes;
3857
+ });
3858
+ }
3859
+ listAllScanIds() {
3860
+ const rows = this.db.prepare("SELECT DISTINCT scanId FROM pointers").all();
3861
+ return rows.map((r) => r.scanId);
3862
+ }
3863
+ countPointers() {
3864
+ const row = this.db.prepare("SELECT COUNT(*) as cnt FROM pointers").get();
3865
+ return row.cnt;
3866
+ }
3867
+ close() {
3868
+ this.db.close();
3869
+ instance = null;
3870
+ }
3871
+ };
3872
+
3498
3873
  // dist/export/ExportOrchestrator.js
3874
+ import { v4 as uuidv4 } from "uuid";
3499
3875
  var ExportOrchestrator = class {
3500
3876
  projectRoot;
3501
3877
  constructor(projectRoot) {
3502
3878
  this.projectRoot = projectRoot;
3503
3879
  }
3504
- async writeBundle(dossier, options = {}, store, graph) {
3880
+ async writeBundle(dossier, options = {}, store, graph, scanId) {
3505
3881
  const finalStore = store || await readAnalysis(this.projectRoot);
3506
3882
  if (!finalStore) {
3507
3883
  throw new Error("Analysis store not found. Scan the project first.");
3508
3884
  }
3885
+ const bindings = await readActionBindings(this.projectRoot);
3509
3886
  for (const p of dossier.pillars) {
3510
3887
  p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
3511
3888
  p.cardCount = p.decisions.length;
@@ -3528,10 +3905,87 @@ var ExportOrchestrator = class {
3528
3905
  artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
3529
3906
  }
3530
3907
  if (formats.includes("markdown")) {
3531
- artifacts.push(...await new AgentMarkdownRenderer(options.budget).render(viewModel, finalStore));
3908
+ artifacts.push(...await new AgentMarkdownRenderer(options.budget, bindings).render(viewModel, finalStore));
3532
3909
  }
3533
3910
  const writer = new ArtifactBundleWriter(this.projectRoot);
3534
3911
  await writer.writeBundle(artifacts);
3912
+ const effectiveScanId = scanId ?? `scan_${Date.now()}`;
3913
+ const blobStore = new BlobStore(this.projectRoot);
3914
+ const pointerStore = PointerStore.open(this.projectRoot);
3915
+ const now = Date.now();
3916
+ const manifestEntries = [];
3917
+ for (const artifact of artifacts) {
3918
+ const content = typeof artifact.content === "string" ? Buffer.from(artifact.content, "utf8") : artifact.content;
3919
+ const { contentHash, blobPath } = await blobStore.writeAtomic(content);
3920
+ const pointerId = `ptr_${uuidv4().replace(/-/g, "").slice(0, 16)}`;
3921
+ await pointerStore.insertPointer({
3922
+ pointerId,
3923
+ scanId: effectiveScanId,
3924
+ artifactName: artifact.type,
3925
+ contentHash,
3926
+ blobPath,
3927
+ schemaVersion: "1.0.0",
3928
+ createdAt: now,
3929
+ expiresAt: null
3930
+ });
3931
+ const entry = {
3932
+ name: artifact.type,
3933
+ pointer: pointerId,
3934
+ contentHash,
3935
+ sizeBytes: content.length
3936
+ };
3937
+ if (artifact.type === "analysis") {
3938
+ entry.hydrators = ["get_project_summary", "get_start_here"];
3939
+ const analysisIndex = {
3940
+ schemaVersion: "1.0.0",
3941
+ scanId: effectiveScanId,
3942
+ startHere: dossier.map.topGravity.slice(0, 12),
3943
+ topHeat: dossier.map.topHeat.slice(0, 12),
3944
+ pillarSummary: dossier.map.pillars.map((p) => ({
3945
+ name: p.name,
3946
+ fileCount: p.memberFiles?.length ?? 0
3947
+ })),
3948
+ totalFiles: Object.keys(finalStore.files).length,
3949
+ realSourceFiles: Object.values(finalStore.files).filter((f) => f.isRealSource).length
3950
+ };
3951
+ const indexContent = Buffer.from(JSON.stringify(analysisIndex, null, 2), "utf8");
3952
+ const indexWrite = await blobStore.writeAtomic(indexContent);
3953
+ const indexPointerId = `ptr_${uuidv4().replace(/-/g, "").slice(0, 16)}`;
3954
+ await pointerStore.insertPointer({
3955
+ pointerId: indexPointerId,
3956
+ scanId: effectiveScanId,
3957
+ artifactName: "analysis.index",
3958
+ contentHash: indexWrite.contentHash,
3959
+ blobPath: indexWrite.blobPath,
3960
+ schemaVersion: "1.0.0",
3961
+ createdAt: now,
3962
+ expiresAt: null
3963
+ });
3964
+ entry.indexes = { startHere: indexPointerId };
3965
+ }
3966
+ manifestEntries.push(entry);
3967
+ }
3968
+ const manifest = {
3969
+ schemaVersion: "2.0.0",
3970
+ scanId: effectiveScanId,
3971
+ generatedAt: new Date(now).toISOString(),
3972
+ projectRoot: this.projectRoot,
3973
+ artifacts: manifestEntries
3974
+ };
3975
+ const manifestContent = Buffer.from(JSON.stringify(manifest, null, 2), "utf8");
3976
+ const manifestWrite = await blobStore.writeAtomic(manifestContent);
3977
+ const manifestPointerId = `ptr_manifest_${effectiveScanId}`;
3978
+ await pointerStore.insertPointer({
3979
+ pointerId: manifestPointerId,
3980
+ scanId: effectiveScanId,
3981
+ artifactName: "artifact_manifest",
3982
+ contentHash: manifestWrite.contentHash,
3983
+ blobPath: manifestWrite.blobPath,
3984
+ schemaVersion: "2.0.0",
3985
+ createdAt: now,
3986
+ expiresAt: null
3987
+ });
3988
+ return { scanId: effectiveScanId, manifestPointer: manifestPointerId };
3535
3989
  }
3536
3990
  buildViewModel(dossier, store) {
3537
3991
  const recommendations = {};
@@ -3558,10 +4012,16 @@ var ExportOrchestrator = class {
3558
4012
 
3559
4013
  // dist/export/Watcher.js
3560
4014
  import chokidar from "chokidar";
3561
- import { createHash as createHash2 } from "crypto";
3562
- import { readFile as readFile9 } from "fs/promises";
3563
- import { join as join12 } from "path";
3564
- function startWatcher(projectRoot, watchedPaths) {
4015
+ import { createHash as createHash4 } from "crypto";
4016
+ import { readFile as readFile10 } from "fs/promises";
4017
+ import { join as join14 } from "path";
4018
+ var activeWatchers = /* @__PURE__ */ new Map();
4019
+ async function startWatcher(projectRoot, watchedPaths) {
4020
+ const existing = activeWatchers.get(projectRoot);
4021
+ if (existing) {
4022
+ await existing.close();
4023
+ activeWatchers.delete(projectRoot);
4024
+ }
3565
4025
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
3566
4026
  ignoreInitial: true,
3567
4027
  ignored: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.vibe-splainer/**"],
@@ -3572,14 +4032,14 @@ function startWatcher(projectRoot, watchedPaths) {
3572
4032
  const dossier = await readDossier(projectRoot);
3573
4033
  if (!dossier)
3574
4034
  return;
3575
- const content = await readFile9(filepath, "utf8");
3576
- const newHash = createHash2("sha256").update(content).digest("hex");
4035
+ const content = await readFile10(filepath, "utf8");
4036
+ const newHash = createHash4("sha256").update(content).digest("hex");
3577
4037
  let mutated = false;
3578
4038
  for (const pillar of dossier.pillars) {
3579
4039
  for (const card of pillar.decisions) {
3580
4040
  if (!card.primaryFile)
3581
4041
  continue;
3582
- const absMatch = filepath === join12(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
4042
+ const absMatch = filepath === join14(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
3583
4043
  if (absMatch && card.lastScannedHash !== newHash) {
3584
4044
  card.status = "stale";
3585
4045
  const rel = card.primaryFile;
@@ -3598,6 +4058,7 @@ function startWatcher(projectRoot, watchedPaths) {
3598
4058
  console.error("[vibe-splain] Watcher error:", err);
3599
4059
  }
3600
4060
  });
4061
+ activeWatchers.set(projectRoot, watcher);
3601
4062
  console.error("[vibe-splain] File watcher started");
3602
4063
  }
3603
4064
 
@@ -3646,13 +4107,14 @@ async function handleScanProject(args, options = {}) {
3646
4107
  dossier.pillars.push({ name: def.name, cardCount: 0, decisions: [] });
3647
4108
  }
3648
4109
  }
4110
+ const scanId = `scan_${Date.now()}`;
3649
4111
  const orchestrator = new ExportOrchestrator(projectRoot);
3650
- await orchestrator.writeBundle(dossier, {
4112
+ const { manifestPointer } = await orchestrator.writeBundle(dossier, {
3651
4113
  format: options.format,
3652
4114
  budget: options.budget ? parseInt(options.budget, 10) : void 0,
3653
4115
  scope: options.scope
3654
- }, result.store, result.graph);
3655
- startWatcher(projectRoot, result.files.map((f) => f.path));
4116
+ }, result.store, result.graph, scanId);
4117
+ await startWatcher(projectRoot, result.files.map((f) => f.path));
3656
4118
  console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
3657
4119
  const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
3658
4120
  let statusMsg = "Scan complete.";
@@ -3662,6 +4124,8 @@ async function handleScanProject(args, options = {}) {
3662
4124
  return {
3663
4125
  ok: true,
3664
4126
  message: statusMsg,
4127
+ scanId,
4128
+ manifestPointer,
3665
4129
  validation: {
3666
4130
  passed: validation.passed,
3667
4131
  errors: validation.errors,
@@ -3775,8 +4239,8 @@ async function handleSetProjectBrief(args, options = {}) {
3775
4239
  }
3776
4240
 
3777
4241
  // dist/mcp/tools/get_file_context.js
3778
- import { readFile as readFile10 } from "fs/promises";
3779
- import { join as join13, relative as relative4, isAbsolute } from "path";
4242
+ import { readFile as readFile11 } from "fs/promises";
4243
+ import { join as join15, relative as relative4, isAbsolute } from "path";
3780
4244
  var getFileContextTool = {
3781
4245
  name: "get_file_context",
3782
4246
  description: "Returns PRE-EXTRACTED evidence for a file so you do not have to read the whole thing and paraphrase its header comment. Returns: gravity/heat scores + signals, importedBy (named fan-in \u2014 use this for blastRadius), hotSpans (the gnarliest function bodies, comment-stripped, each with a reason), smellSpans (located tech debt with \xB13 lines of context), and signature (the exported API surface). Base your evidence on hotSpans/smellSpans \u2014 NEVER on header comments. Pass { full: true } only if you truly need the raw source.",
@@ -3796,7 +4260,7 @@ async function handleGetFileContext(args) {
3796
4260
  const full = args.full === true;
3797
4261
  if (!projectRoot || !filePath)
3798
4262
  throw new Error("projectRoot and filePath are required");
3799
- const fullPath = isAbsolute(filePath) ? filePath : join13(projectRoot, filePath);
4263
+ const fullPath = isAbsolute(filePath) ? filePath : join15(projectRoot, filePath);
3800
4264
  const relPath = relative4(projectRoot, fullPath);
3801
4265
  const evidence = await getFileAnalysis(fullPath);
3802
4266
  if (!evidence) {
@@ -3821,16 +4285,16 @@ async function handleGetFileContext(args) {
3821
4285
  smellSpans: evidence.smellSpans
3822
4286
  };
3823
4287
  if (full) {
3824
- result.source = await readFile10(fullPath, "utf8");
4288
+ result.source = await readFile11(fullPath, "utf8");
3825
4289
  }
3826
4290
  return result;
3827
4291
  }
3828
4292
 
3829
4293
  // dist/mcp/tools/write_decision_card.js
3830
- import { v4 as uuidv4 } from "uuid";
3831
- import { createHash as createHash3 } from "crypto";
3832
- import { readFile as readFile11 } from "fs/promises";
3833
- import { join as join14 } from "path";
4294
+ import { v4 as uuidv42 } from "uuid";
4295
+ import { createHash as createHash5 } from "crypto";
4296
+ import { readFile as readFile12 } from "fs/promises";
4297
+ import { join as join16 } from "path";
3834
4298
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
3835
4299
  function normalizeSnippet(s) {
3836
4300
  let out = (s ?? "").replace(/\r\n/g, "\n");
@@ -3927,12 +4391,12 @@ async function handleWriteDecisionCard(args, options = {}) {
3927
4391
  const heat = persisted ? Math.round(persisted.heat) : void 0;
3928
4392
  let primaryContent = "";
3929
4393
  try {
3930
- primaryContent = await readFile11(join14(projectRoot, primaryFile), "utf8");
4394
+ primaryContent = await readFile12(join16(projectRoot, primaryFile), "utf8");
3931
4395
  } catch {
3932
4396
  }
3933
- const hash = createHash3("sha256").update(primaryContent).digest("hex");
4397
+ const hash = createHash5("sha256").update(primaryContent).digest("hex");
3934
4398
  const card = {
3935
- id: uuidv4(),
4399
+ id: uuidv42(),
3936
4400
  pillar,
3937
4401
  title,
3938
4402
  thesis,
@@ -4206,6 +4670,856 @@ async function handleGetCallChain(args) {
4206
4670
  }
4207
4671
  }
4208
4672
 
4673
+ // dist/mcp/tools/get_file_skeleton.js
4674
+ import { readFile as readFile13 } from "fs/promises";
4675
+ import { join as join17 } from "path";
4676
+
4677
+ // dist/mcp/SessionScope.js
4678
+ import { minimatch } from "minimatch";
4679
+ var ScopeViolation = class extends Error {
4680
+ path;
4681
+ workOrderId;
4682
+ constructor(path, workOrderId, reason) {
4683
+ super(`ScopeViolation [${workOrderId}]: ${reason} \u2014 path: ${path}`);
4684
+ this.path = path;
4685
+ this.workOrderId = workOrderId;
4686
+ this.name = "ScopeViolation";
4687
+ }
4688
+ };
4689
+ var activeScope = null;
4690
+ var SessionScope = {
4691
+ set(policy) {
4692
+ activeScope = policy;
4693
+ },
4694
+ clear() {
4695
+ activeScope = null;
4696
+ },
4697
+ get() {
4698
+ return activeScope;
4699
+ },
4700
+ /**
4701
+ * Enforce scope for a file path.
4702
+ * Throws ScopeViolation if:
4703
+ * - a scope is active AND the path is not allowed
4704
+ * If no scope is active, all paths are permitted.
4705
+ */
4706
+ enforce(filePath) {
4707
+ if (!activeScope)
4708
+ return;
4709
+ const { workOrderId, allowedFiles, allowedGlobs, deniedGlobs } = activeScope;
4710
+ const inAllowedFiles = allowedFiles.some((f) => filePath === f || filePath.endsWith("/" + f) || filePath.endsWith(f));
4711
+ const inAllowedGlobs = allowedGlobs.some((g) => minimatch(filePath, g, { matchBase: true }));
4712
+ if (!inAllowedFiles && !inAllowedGlobs) {
4713
+ throw new ScopeViolation(filePath, workOrderId, "path not in allowedFiles or allowedGlobs");
4714
+ }
4715
+ const isDenied = deniedGlobs.some((g) => minimatch(filePath, g, { matchBase: true }));
4716
+ if (isDenied) {
4717
+ throw new ScopeViolation(filePath, workOrderId, "path matches deniedGlobs");
4718
+ }
4719
+ },
4720
+ fromWorkOrderRow(row) {
4721
+ return {
4722
+ workOrderId: row.workOrderId,
4723
+ allowedFiles: JSON.parse(row.allowedFiles),
4724
+ allowedGlobs: JSON.parse(row.allowedGlobs),
4725
+ deniedGlobs: JSON.parse(row.deniedGlobs),
4726
+ requiredProof: JSON.parse(row.requiredProof)
4727
+ };
4728
+ }
4729
+ };
4730
+
4731
+ // dist/mcp/BudgetGuard.js
4732
+ import { v4 as uuidv43 } from "uuid";
4733
+ var BUDGET_CHARS = 8e3;
4734
+ async function applyBudgetGuard(projectRoot, scanId, artifactName, output) {
4735
+ const serialized = JSON.stringify(output, null, 2);
4736
+ if (serialized.length <= BUDGET_CHARS)
4737
+ return output;
4738
+ const blobStore = new BlobStore(projectRoot);
4739
+ const pointerStore = PointerStore.open(projectRoot);
4740
+ const { contentHash, blobPath } = await blobStore.writeAtomic(serialized);
4741
+ const pointerId = `ptr_${uuidv43().replace(/-/g, "").slice(0, 16)}`;
4742
+ await pointerStore.insertPointer({
4743
+ pointerId,
4744
+ scanId,
4745
+ artifactName,
4746
+ contentHash,
4747
+ blobPath,
4748
+ schemaVersion: "1.0.0",
4749
+ createdAt: Date.now(),
4750
+ expiresAt: null
4751
+ });
4752
+ const result = {
4753
+ pointerId,
4754
+ contentHash,
4755
+ sizeBytes: serialized.length,
4756
+ summary: `Output exceeded context budget (${serialized.length} chars). Written to artifact blob.`,
4757
+ hydrators: ["get_evidence_slice", "get_start_here", "get_project_summary"]
4758
+ };
4759
+ return result;
4760
+ }
4761
+ async function hydratePointer(projectRoot, pointerId) {
4762
+ const pointerStore = PointerStore.open(projectRoot);
4763
+ const row = pointerStore.getPointer(pointerId);
4764
+ if (!row) {
4765
+ throw new Error(`ArtifactNotFound: pointer ${pointerId} does not exist`);
4766
+ }
4767
+ if (row.expiresAt !== null && row.expiresAt < Date.now()) {
4768
+ throw new Error(`ArtifactCollectedError: pointer ${pointerId} has expired`);
4769
+ }
4770
+ const SUPPORTED_VERSIONS = ["1.0.0", "2.0.0"];
4771
+ if (!SUPPORTED_VERSIONS.includes(row.schemaVersion)) {
4772
+ throw new Error(`UnsupportedSchema: pointer ${pointerId} has schema version ${row.schemaVersion}`);
4773
+ }
4774
+ const blobStore = new BlobStore(projectRoot);
4775
+ const content = await blobStore.readBlob(row.blobPath);
4776
+ const valid = await blobStore.verifyIntegrity(row.blobPath, row.contentHash);
4777
+ if (!valid) {
4778
+ throw new Error(`IntegrityError: blob for pointer ${pointerId} failed hash verification`);
4779
+ }
4780
+ return { content, row };
4781
+ }
4782
+
4783
+ // dist/mcp/tools/get_file_skeleton.js
4784
+ import { v4 as uuidv44 } from "uuid";
4785
+ var getFileSkeletonTool = {
4786
+ name: "get_file_skeleton",
4787
+ description: "Returns a content-addressed skeleton view of a source file (function signatures, class names, exported symbols). Enforces active workOrder scope. Results are content-addressed \u2014 repeated calls on unchanged files return cached pointers.",
4788
+ inputSchema: {
4789
+ type: "object",
4790
+ properties: {
4791
+ projectRoot: { type: "string", description: "Absolute project root" },
4792
+ filePath: { type: "string", description: "Path relative to projectRoot" },
4793
+ scanId: { type: "string", description: "Current scan ID for pointer registration" }
4794
+ },
4795
+ required: ["projectRoot", "filePath", "scanId"]
4796
+ }
4797
+ };
4798
+ async function handleGetFileSkeleton(args) {
4799
+ const projectRoot = args.projectRoot;
4800
+ const filePath = args.filePath;
4801
+ const scanId = args.scanId;
4802
+ if (!projectRoot || !filePath || !scanId) {
4803
+ throw new Error("projectRoot, filePath, and scanId are required");
4804
+ }
4805
+ try {
4806
+ SessionScope.enforce(filePath);
4807
+ } catch (e) {
4808
+ if (e instanceof ScopeViolation)
4809
+ throw e;
4810
+ throw e;
4811
+ }
4812
+ const absolutePath = filePath.startsWith("/") ? filePath : join17(projectRoot, filePath);
4813
+ let currentHash;
4814
+ try {
4815
+ currentHash = await hashFile(absolutePath);
4816
+ } catch {
4817
+ throw new Error(`FileNotFound: cannot read ${filePath}`);
4818
+ }
4819
+ const parserVersion = "1.0.0";
4820
+ const cacheKey = `skeleton:${currentHash}:${parserVersion}`;
4821
+ const pointerStore = PointerStore.open(projectRoot);
4822
+ const existingPointers = pointerStore.listPointersByScan(scanId);
4823
+ const cached = existingPointers.find((p) => p.artifactName === "file_skeleton" && p.contentHash === cacheKey);
4824
+ if (cached) {
4825
+ return {
4826
+ pointerId: cached.pointerId,
4827
+ contentHash: currentHash,
4828
+ cached: true,
4829
+ filePath
4830
+ };
4831
+ }
4832
+ const source = await readFile13(absolutePath, "utf8");
4833
+ const skeleton = extractSkeleton(source, filePath);
4834
+ const skeletonPayload = {
4835
+ filePath,
4836
+ sourceHash: currentHash,
4837
+ parserVersion,
4838
+ skeleton
4839
+ };
4840
+ const blobStore = new BlobStore(projectRoot);
4841
+ const serialized = JSON.stringify(skeletonPayload, null, 2);
4842
+ const { contentHash: skeletonHash, blobPath } = await blobStore.writeAtomic(serialized);
4843
+ const pointerId = `ptr_skel_${uuidv44().replace(/-/g, "").slice(0, 12)}`;
4844
+ await pointerStore.insertPointer({
4845
+ pointerId,
4846
+ scanId,
4847
+ artifactName: "file_skeleton",
4848
+ contentHash: cacheKey,
4849
+ // cache key encodes source hash + parser version
4850
+ blobPath,
4851
+ schemaVersion: "1.0.0",
4852
+ createdAt: Date.now(),
4853
+ expiresAt: null
4854
+ });
4855
+ const result = {
4856
+ filePath,
4857
+ sourceHash: currentHash,
4858
+ pointerId,
4859
+ skeleton
4860
+ };
4861
+ return await applyBudgetGuard(projectRoot, scanId, "file_skeleton", result);
4862
+ }
4863
+ function extractSkeleton(source, filePath) {
4864
+ const lines = source.split("\n");
4865
+ const skeleton = [];
4866
+ const ext = filePath.split(".").pop() ?? "";
4867
+ const isTS = ["ts", "tsx"].includes(ext);
4868
+ const isJS = ["js", "jsx", "mjs", "cjs"].includes(ext);
4869
+ if (isTS || isJS) {
4870
+ for (let i = 0; i < lines.length; i++) {
4871
+ const line = lines[i].trim();
4872
+ if (/^(export\s+)?(async\s+)?function\b/.test(line) || /^(export\s+)?(abstract\s+)?class\b/.test(line) || /^(export\s+)?interface\b/.test(line) || /^(export\s+)?type\s+\w+/.test(line) || /^(export\s+)?enum\b/.test(line) || /^(export\s+)?const\s+\w+\s*[:=(]/.test(line) || /^(export\s+)?let\s+\w+\s*[:=(]/.test(line) || /^(export\s+)?(default\s+)/.test(line) || /^\s*(public|private|protected|static|readonly|abstract)\s+/.test(line) || /^import\b/.test(line)) {
4873
+ skeleton.push(`L${i + 1}: ${lines[i]}`);
4874
+ }
4875
+ }
4876
+ } else {
4877
+ return lines.slice(0, 80).map((l, i) => `L${i + 1}: ${l}`);
4878
+ }
4879
+ return skeleton;
4880
+ }
4881
+
4882
+ // dist/mcp/tools/read_file.js
4883
+ import { readFile as readFile14 } from "fs/promises";
4884
+ import { join as join18 } from "path";
4885
+ import { v4 as uuidv45 } from "uuid";
4886
+ var readFileTool = {
4887
+ name: "read_file",
4888
+ description: "Reads a file within the active workOrder scope. Enforces allowedFiles/allowedGlobs/deniedGlobs. Records content hash. Output is budgeted.",
4889
+ inputSchema: {
4890
+ type: "object",
4891
+ properties: {
4892
+ projectRoot: { type: "string", description: "Absolute project root" },
4893
+ filePath: { type: "string", description: "Path relative to projectRoot" },
4894
+ scanId: { type: "string", description: "Current scan ID for pointer registration" },
4895
+ startLine: { type: "number", description: "Optional: 1-based start line to return a slice" },
4896
+ endLine: { type: "number", description: "Optional: 1-based end line (inclusive). Capped at startLine+500." }
4897
+ },
4898
+ required: ["projectRoot", "filePath", "scanId"]
4899
+ }
4900
+ };
4901
+ async function handleReadFile(args) {
4902
+ const projectRoot = args.projectRoot;
4903
+ const filePath = args.filePath;
4904
+ const scanId = args.scanId;
4905
+ const startLine = args.startLine !== void 0 ? Number(args.startLine) : void 0;
4906
+ const endLine = args.endLine !== void 0 ? Math.min(Number(args.endLine), (startLine ?? 1) + 500) : void 0;
4907
+ if (!projectRoot || !filePath || !scanId) {
4908
+ throw new Error("projectRoot, filePath, and scanId are required");
4909
+ }
4910
+ try {
4911
+ SessionScope.enforce(filePath);
4912
+ } catch (e) {
4913
+ if (e instanceof ScopeViolation)
4914
+ throw e;
4915
+ throw e;
4916
+ }
4917
+ const absolutePath = filePath.startsWith("/") ? filePath : join18(projectRoot, filePath);
4918
+ let content;
4919
+ try {
4920
+ content = await readFile14(absolutePath, "utf8");
4921
+ } catch {
4922
+ throw new Error(`FileNotFound: cannot read ${filePath}`);
4923
+ }
4924
+ const contentHash = await hashFile(absolutePath);
4925
+ let output = content;
4926
+ let sliceInfo;
4927
+ if (startLine !== void 0) {
4928
+ const lines = content.split("\n");
4929
+ const end = endLine ?? lines.length;
4930
+ output = lines.slice(startLine - 1, end).join("\n");
4931
+ sliceInfo = { startLine, endLine: end, totalLines: lines.length };
4932
+ }
4933
+ const blobStore = new BlobStore(projectRoot);
4934
+ const pointerStore = PointerStore.open(projectRoot);
4935
+ const { blobPath } = await blobStore.writeAtomic(content);
4936
+ const pointerId = `ptr_file_${uuidv45().replace(/-/g, "").slice(0, 12)}`;
4937
+ await pointerStore.insertPointer({
4938
+ pointerId,
4939
+ scanId,
4940
+ artifactName: "file_read",
4941
+ contentHash,
4942
+ blobPath,
4943
+ schemaVersion: "1.0.0",
4944
+ createdAt: Date.now(),
4945
+ expiresAt: null
4946
+ });
4947
+ const result = {
4948
+ filePath,
4949
+ contentHash,
4950
+ pointerId,
4951
+ content: output
4952
+ };
4953
+ if (sliceInfo)
4954
+ result.slice = sliceInfo;
4955
+ return await applyBudgetGuard(projectRoot, scanId, "file_read", result);
4956
+ }
4957
+
4958
+ // dist/mcp/tools/apply_patch.js
4959
+ import { writeFile as writeFile9, rename as rename3, mkdir as mkdir8 } from "fs/promises";
4960
+ import { join as join19, dirname as dirname4 } from "path";
4961
+ import { v4 as uuidv46 } from "uuid";
4962
+ var StalePatchError = class extends Error {
4963
+ filePath;
4964
+ expectedHash;
4965
+ actualHash;
4966
+ constructor(filePath, expectedHash, actualHash) {
4967
+ super(`StalePatchError: ${filePath} hash mismatch \u2014 expected ${expectedHash}, got ${actualHash}. File was modified since the expectedPrePatchHash was computed. Re-read the file and regenerate the patch.`);
4968
+ this.filePath = filePath;
4969
+ this.expectedHash = expectedHash;
4970
+ this.actualHash = actualHash;
4971
+ this.name = "StalePatchError";
4972
+ }
4973
+ };
4974
+ var applyPatchTool = {
4975
+ name: "apply_patch",
4976
+ description: "Applies a text patch to a file within the active workOrder scope. Requires expectedPrePatchHash to prevent stale-patch corruption. Records pre- and post-patch hashes.",
4977
+ inputSchema: {
4978
+ type: "object",
4979
+ properties: {
4980
+ projectRoot: { type: "string", description: "Absolute project root" },
4981
+ filePath: { type: "string", description: "Path relative to projectRoot" },
4982
+ newContent: { type: "string", description: "Full new content of the file after the patch" },
4983
+ expectedPrePatchHash: {
4984
+ type: "string",
4985
+ description: "sha256:<hex> hash of the file BEFORE patching. Obtain via hashFile or the sourceHash from get_file_skeleton."
4986
+ },
4987
+ scanId: { type: "string", description: "Current scan ID for pointer registration" }
4988
+ },
4989
+ required: ["projectRoot", "filePath", "newContent", "expectedPrePatchHash", "scanId"]
4990
+ }
4991
+ };
4992
+ async function handleApplyPatch(args) {
4993
+ const projectRoot = args.projectRoot;
4994
+ const filePath = args.filePath;
4995
+ const newContent = args.newContent;
4996
+ const expectedPrePatchHash = args.expectedPrePatchHash;
4997
+ const scanId = args.scanId;
4998
+ if (!projectRoot || !filePath || !newContent || !expectedPrePatchHash || !scanId) {
4999
+ throw new Error("projectRoot, filePath, newContent, expectedPrePatchHash, and scanId are all required");
5000
+ }
5001
+ try {
5002
+ SessionScope.enforce(filePath);
5003
+ } catch (e) {
5004
+ if (e instanceof ScopeViolation)
5005
+ throw e;
5006
+ throw e;
5007
+ }
5008
+ const absolutePath = filePath.startsWith("/") ? filePath : join19(projectRoot, filePath);
5009
+ let actualPreHash;
5010
+ try {
5011
+ actualPreHash = await hashFile(absolutePath);
5012
+ } catch {
5013
+ actualPreHash = "sha256:new";
5014
+ }
5015
+ if (actualPreHash !== expectedPrePatchHash) {
5016
+ throw new StalePatchError(filePath, expectedPrePatchHash, actualPreHash);
5017
+ }
5018
+ const dir = dirname4(absolutePath);
5019
+ await mkdir8(dir, { recursive: true });
5020
+ const tmpPath = absolutePath + `.tmp_${Date.now()}`;
5021
+ await writeFile9(tmpPath, newContent, "utf8");
5022
+ await rename3(tmpPath, absolutePath);
5023
+ const postPatchHash = await hashFile(absolutePath);
5024
+ const blobStore = new BlobStore(projectRoot);
5025
+ const pointerStore = PointerStore.open(projectRoot);
5026
+ const { blobPath } = await blobStore.writeAtomic(newContent);
5027
+ const pointerId = `ptr_patch_${uuidv46().replace(/-/g, "").slice(0, 12)}`;
5028
+ await pointerStore.insertPointer({
5029
+ pointerId,
5030
+ scanId,
5031
+ artifactName: "patch_record",
5032
+ contentHash: postPatchHash,
5033
+ blobPath,
5034
+ schemaVersion: "1.0.0",
5035
+ createdAt: Date.now(),
5036
+ expiresAt: null
5037
+ });
5038
+ const result = {
5039
+ ok: true,
5040
+ filePath,
5041
+ prePatchHash: actualPreHash,
5042
+ postPatchHash,
5043
+ pointerId,
5044
+ message: `Patch applied to ${filePath}`
5045
+ };
5046
+ return await applyBudgetGuard(projectRoot, scanId, "patch_record", result);
5047
+ }
5048
+
5049
+ // dist/mcp/tools/work_orders.js
5050
+ import { v4 as uuidv47 } from "uuid";
5051
+ var createWorkOrderTool = {
5052
+ name: "create_work_order",
5053
+ description: "Creates a new Work Order defining intent, allowed file scope, and required verifiable proof. Returns the workOrderId and a manifestPointer for use with spawn_worker.",
5054
+ inputSchema: {
5055
+ type: "object",
5056
+ properties: {
5057
+ projectRoot: { type: "string", description: "Absolute project root" },
5058
+ intent: { type: "string", description: "Plain-language description of what the worker should do" },
5059
+ allowedFiles: {
5060
+ type: "array",
5061
+ items: { type: "string" },
5062
+ description: "Explicit file paths (relative to projectRoot) the worker may read/write"
5063
+ },
5064
+ allowedGlobs: {
5065
+ type: "array",
5066
+ items: { type: "string" },
5067
+ description: "Glob patterns for allowed files"
5068
+ },
5069
+ deniedGlobs: {
5070
+ type: "array",
5071
+ items: { type: "string" },
5072
+ description: "Glob patterns that override allowedFiles/allowedGlobs"
5073
+ },
5074
+ requiredProof: {
5075
+ type: "array",
5076
+ items: {
5077
+ type: "object",
5078
+ properties: {
5079
+ proofId: { type: "string" },
5080
+ schemaName: { type: "string", description: "e.g. test_report.v1, patch_hash" },
5081
+ description: { type: "string" }
5082
+ },
5083
+ required: ["proofId", "schemaName", "description"]
5084
+ },
5085
+ description: "Machine-verifiable evidence the worker must provide"
5086
+ }
5087
+ },
5088
+ required: ["projectRoot", "intent", "allowedFiles"]
5089
+ }
5090
+ };
5091
+ async function handleCreateWorkOrder(args) {
5092
+ const projectRoot = args.projectRoot;
5093
+ const intent = args.intent;
5094
+ const allowedFiles = args.allowedFiles ?? [];
5095
+ const allowedGlobs = args.allowedGlobs ?? [];
5096
+ const deniedGlobs = args.deniedGlobs ?? [];
5097
+ const requiredProof = args.requiredProof ?? [];
5098
+ if (!projectRoot || !intent) {
5099
+ throw new Error("projectRoot and intent are required");
5100
+ }
5101
+ const workOrderId = `wo_${uuidv47().replace(/-/g, "").slice(0, 16)}`;
5102
+ const pointerStore = PointerStore.open(projectRoot);
5103
+ await pointerStore.insertWorkOrder({
5104
+ workOrderId,
5105
+ intent,
5106
+ allowedFiles: JSON.stringify(allowedFiles),
5107
+ allowedGlobs: JSON.stringify(allowedGlobs),
5108
+ deniedGlobs: JSON.stringify(deniedGlobs),
5109
+ requiredProof: JSON.stringify(requiredProof),
5110
+ status: "pending",
5111
+ createdAt: Date.now()
5112
+ });
5113
+ return {
5114
+ ok: true,
5115
+ workOrderId,
5116
+ intent,
5117
+ allowedFiles,
5118
+ allowedGlobs,
5119
+ deniedGlobs,
5120
+ requiredProof,
5121
+ nextStep: `Call spawn_worker with workOrderId "${workOrderId}" to generate a DelegationRequest`
5122
+ };
5123
+ }
5124
+ var spawnWorkerTool = {
5125
+ name: "spawn_worker",
5126
+ description: "Generates a DelegationRequest from a Work Order. The Client Orchestrator uses this object to spawn an isolated Worker session. The MCP server does NOT spawn any subprocess.",
5127
+ inputSchema: {
5128
+ type: "object",
5129
+ properties: {
5130
+ projectRoot: { type: "string", description: "Absolute project root" },
5131
+ workOrderId: { type: "string", description: "ID returned by create_work_order" }
5132
+ },
5133
+ required: ["projectRoot", "workOrderId"]
5134
+ }
5135
+ };
5136
+ async function handleSpawnWorker(args) {
5137
+ const projectRoot = args.projectRoot;
5138
+ const workOrderId = args.workOrderId;
5139
+ if (!projectRoot || !workOrderId) {
5140
+ throw new Error("projectRoot and workOrderId are required");
5141
+ }
5142
+ const pointerStore = PointerStore.open(projectRoot);
5143
+ const row = pointerStore.getWorkOrder(workOrderId);
5144
+ if (!row) {
5145
+ throw new Error(`WorkOrderNotFound: ${workOrderId}`);
5146
+ }
5147
+ if (row.status === "completed" || row.status === "failed" || row.status === "active") {
5148
+ throw new Error(`WorkOrderClosed: ${workOrderId} is already ${row.status}`);
5149
+ }
5150
+ await pointerStore.updateWorkOrderStatus(workOrderId, "active");
5151
+ const delegationRequest = {
5152
+ schemaVersion: "1.0.0",
5153
+ workOrderId: row.workOrderId,
5154
+ intent: row.intent,
5155
+ sessionScope: {
5156
+ allowedFiles: JSON.parse(row.allowedFiles),
5157
+ allowedGlobs: JSON.parse(row.allowedGlobs),
5158
+ deniedGlobs: JSON.parse(row.deniedGlobs)
5159
+ },
5160
+ requiredProof: JSON.parse(row.requiredProof),
5161
+ instructions: [
5162
+ `1. Call set_session_scope with workOrderId "${workOrderId}" before any file operations.`,
5163
+ "2. Only read/write files within the sessionScope.",
5164
+ "3. If you need a file outside scope, call yield_for_scope_expansion \u2014 do NOT proceed.",
5165
+ "4. On completion, call submit_receipt with proof for every requiredProof entry."
5166
+ ]
5167
+ };
5168
+ return {
5169
+ ok: true,
5170
+ delegationRequest,
5171
+ note: "The Client Orchestrator must spawn the Worker session using this DelegationRequest. The MCP server does not spawn subprocesses."
5172
+ };
5173
+ }
5174
+
5175
+ // dist/mcp/tools/submit_receipt.js
5176
+ import { join as join20 } from "path";
5177
+ import { minimatch as minimatch2 } from "minimatch";
5178
+ import { v4 as uuidv48 } from "uuid";
5179
+ var submitReceiptTool = {
5180
+ name: "submit_receipt",
5181
+ description: "Worker submits a WorkerReceipt for a completed Work Order. The ProofValidator checks all 8 proof conditions. Returns accept/reject with detailed errors.",
5182
+ inputSchema: {
5183
+ type: "object",
5184
+ properties: {
5185
+ projectRoot: { type: "string", description: "Absolute project root" },
5186
+ receipt: {
5187
+ type: "object",
5188
+ description: "WorkerReceipt object",
5189
+ properties: {
5190
+ workOrderId: { type: "string" },
5191
+ status: { type: "string", enum: ["completed", "failed", "blocked"] },
5192
+ proofPointers: {
5193
+ type: "array",
5194
+ items: {
5195
+ type: "object",
5196
+ properties: {
5197
+ pointer: { type: "string" },
5198
+ schemaName: { type: "string" },
5199
+ contentHash: { type: "string" }
5200
+ },
5201
+ required: ["pointer", "schemaName", "contentHash"]
5202
+ }
5203
+ },
5204
+ changedFiles: {
5205
+ type: "array",
5206
+ items: {
5207
+ type: "object",
5208
+ properties: {
5209
+ path: { type: "string" },
5210
+ prePatchHash: { type: "string" },
5211
+ postPatchHash: { type: "string" }
5212
+ },
5213
+ required: ["path", "prePatchHash", "postPatchHash"]
5214
+ }
5215
+ },
5216
+ summary: { type: "string" }
5217
+ },
5218
+ required: ["workOrderId", "status", "proofPointers", "changedFiles", "summary"]
5219
+ }
5220
+ },
5221
+ required: ["projectRoot", "receipt"]
5222
+ }
5223
+ };
5224
+ async function handleSubmitReceipt(args) {
5225
+ const projectRoot = args.projectRoot;
5226
+ const receipt = args.receipt;
5227
+ if (!projectRoot || !receipt) {
5228
+ throw new Error("projectRoot and receipt are required");
5229
+ }
5230
+ const pointerStore = PointerStore.open(projectRoot);
5231
+ const workOrder = pointerStore.getWorkOrder(receipt.workOrderId);
5232
+ if (!workOrder) {
5233
+ throw new Error(`WorkOrderNotFound: ${receipt.workOrderId}`);
5234
+ }
5235
+ if (workOrder.status !== "active") {
5236
+ throw new Error(`WorkOrderNotActive: ${receipt.workOrderId} is "${workOrder.status}", expected "active"`);
5237
+ }
5238
+ const allowedFiles = JSON.parse(workOrder.allowedFiles);
5239
+ const allowedGlobs = JSON.parse(workOrder.allowedGlobs);
5240
+ const requiredProof = JSON.parse(workOrder.requiredProof);
5241
+ const blobDir = join20(projectRoot, ".vibe-splainer", "blobs");
5242
+ const isAllowedFile = (filePath) => {
5243
+ const inExplicit = allowedFiles.some((f) => filePath === f || filePath.endsWith("/" + f) || filePath.endsWith(f));
5244
+ const inGlobs = allowedGlobs.some((g) => minimatch2(filePath, g, { matchBase: true }));
5245
+ return inExplicit || inGlobs;
5246
+ };
5247
+ const validation = await ProofValidator.validate(receipt, requiredProof, isAllowedFile, blobDir);
5248
+ const receiptId = `rcpt_${uuidv48().replace(/-/g, "").slice(0, 16)}`;
5249
+ const finalStatus = validation.valid ? receipt.status : "failed";
5250
+ await pointerStore.insertReceipt({
5251
+ receiptId,
5252
+ workOrderId: receipt.workOrderId,
5253
+ status: finalStatus,
5254
+ proofPointers: receipt.proofPointers,
5255
+ changedFiles: receipt.changedFiles,
5256
+ summary: receipt.summary
5257
+ });
5258
+ await pointerStore.updateWorkOrderStatus(receipt.workOrderId, validation.valid ? receipt.status === "completed" ? "completed" : "failed" : "failed");
5259
+ return {
5260
+ receiptId,
5261
+ accepted: validation.valid,
5262
+ workOrderId: receipt.workOrderId,
5263
+ validation,
5264
+ finalStatus
5265
+ };
5266
+ }
5267
+
5268
+ // dist/mcp/tools/set_session_scope.js
5269
+ var setSessionScopeTool = {
5270
+ name: "set_session_scope",
5271
+ description: "Sets the active session scope from a Work Order. All subsequent file tools (read_file, get_file_skeleton, apply_patch) will enforce this scope until overwritten or the server restarts.",
5272
+ inputSchema: {
5273
+ type: "object",
5274
+ properties: {
5275
+ projectRoot: { type: "string", description: "Absolute project root" },
5276
+ workOrderId: { type: "string", description: "Work Order ID to load scope from" }
5277
+ },
5278
+ required: ["projectRoot", "workOrderId"]
5279
+ }
5280
+ };
5281
+ async function handleSetSessionScope(args) {
5282
+ const projectRoot = args.projectRoot;
5283
+ const workOrderId = args.workOrderId;
5284
+ if (!projectRoot || !workOrderId) {
5285
+ throw new Error("projectRoot and workOrderId are required");
5286
+ }
5287
+ const pointerStore = PointerStore.open(projectRoot);
5288
+ const row = pointerStore.getWorkOrder(workOrderId);
5289
+ if (!row) {
5290
+ throw new Error(`WorkOrderNotFound: ${workOrderId}`);
5291
+ }
5292
+ const policy = SessionScope.fromWorkOrderRow(row);
5293
+ SessionScope.set(policy);
5294
+ return {
5295
+ ok: true,
5296
+ workOrderId,
5297
+ scope: {
5298
+ allowedFiles: policy.allowedFiles,
5299
+ allowedGlobs: policy.allowedGlobs,
5300
+ deniedGlobs: policy.deniedGlobs,
5301
+ requiredProofCount: policy.requiredProof.length
5302
+ },
5303
+ message: `Session scope set from work order ${workOrderId}. All file tools will enforce this scope.`
5304
+ };
5305
+ }
5306
+
5307
+ // dist/mcp/tools/yield_for_scope_expansion.js
5308
+ var yieldForScopeExpansionTool = {
5309
+ name: "yield_for_scope_expansion",
5310
+ description: "Worker signals that it needs to access files outside its current scope. Immediately terminates the active scope and returns a Blocked receipt. The Manager must evaluate the evidence and decide whether to spawn a new Worker with expanded scope.",
5311
+ inputSchema: {
5312
+ type: "object",
5313
+ properties: {
5314
+ requestedPaths: {
5315
+ type: "array",
5316
+ items: { type: "string" },
5317
+ description: "Paths the worker needs but cannot access under current scope"
5318
+ },
5319
+ reason: {
5320
+ type: "string",
5321
+ description: "Why these paths are needed \u2014 root cause found in out-of-scope file"
5322
+ },
5323
+ evidencePointers: {
5324
+ type: "array",
5325
+ items: { type: "string" },
5326
+ description: "Pointer IDs for artifacts that justify the expansion request"
5327
+ }
5328
+ },
5329
+ required: ["requestedPaths", "reason"]
5330
+ }
5331
+ };
5332
+ async function handleYieldForScopeExpansion(args) {
5333
+ const requestedPaths = args.requestedPaths ?? [];
5334
+ const reason = args.reason;
5335
+ const evidencePointers = args.evidencePointers ?? [];
5336
+ if (!requestedPaths.length || !reason) {
5337
+ throw new Error("requestedPaths and reason are required");
5338
+ }
5339
+ const currentScope = SessionScope.get();
5340
+ const workOrderId = currentScope?.workOrderId ?? "unknown";
5341
+ SessionScope.clear();
5342
+ return {
5343
+ status: "blocked",
5344
+ workOrderId,
5345
+ requestedPaths,
5346
+ reason,
5347
+ evidencePointers,
5348
+ receipt: {
5349
+ workOrderId,
5350
+ status: "blocked",
5351
+ proofPointers: [],
5352
+ changedFiles: [],
5353
+ summary: `Worker blocked: scope expansion required. Reason: ${reason}`
5354
+ },
5355
+ managerInstructions: [
5356
+ "Worker has been terminated. Active session scope has been cleared.",
5357
+ "Evaluate the evidence pointers and reason.",
5358
+ "If expansion is warranted, create a new Work Order with the expanded allowedFiles and spawn a new Worker.",
5359
+ "If expansion is NOT warranted, the task is failed \u2014 do not retry with same scope."
5360
+ ]
5361
+ };
5362
+ }
5363
+
5364
+ // dist/mcp/tools/hydration/get_start_here.js
5365
+ var getStartHereTool = {
5366
+ name: "get_start_here",
5367
+ description: "Hydrates the start-here index for a scan manifest pointer. Returns the top 5 highest-gravity files. Pointer must be valid and unexpired.",
5368
+ inputSchema: {
5369
+ type: "object",
5370
+ properties: {
5371
+ projectRoot: { type: "string", description: "Absolute project root" },
5372
+ manifestPointer: { type: "string", description: "Pointer ID for the scan manifest or analysis.index artifact" },
5373
+ scanId: { type: "string", description: "Current scan ID for budget pointer registration" }
5374
+ },
5375
+ required: ["projectRoot", "manifestPointer", "scanId"]
5376
+ }
5377
+ };
5378
+ async function handleGetStartHere(args) {
5379
+ const projectRoot = args.projectRoot;
5380
+ const manifestPointer = args.manifestPointer;
5381
+ const scanId = args.scanId;
5382
+ if (!projectRoot || !manifestPointer || !scanId) {
5383
+ throw new Error("projectRoot, manifestPointer, and scanId are required");
5384
+ }
5385
+ const { content, row } = await hydratePointer(projectRoot, manifestPointer);
5386
+ const payload = JSON.parse(content.toString("utf8"));
5387
+ if (row.artifactName === "artifact_manifest") {
5388
+ const manifest = payload;
5389
+ const analysisEntry = manifest.artifacts.find((a) => a.name === "analysis" || a.name === "analysis.index");
5390
+ if (!analysisEntry?.indexes?.startHere) {
5391
+ throw new Error("Manifest has no analysis.index entry \u2014 rescan to regenerate");
5392
+ }
5393
+ const { content: indexContent } = await hydratePointer(projectRoot, analysisEntry.indexes.startHere);
5394
+ const index = JSON.parse(indexContent.toString("utf8"));
5395
+ const result = {
5396
+ startHere: index.startHere.slice(0, 5),
5397
+ schemaVersion: index.schemaVersion,
5398
+ scanId: index.scanId
5399
+ };
5400
+ return await applyBudgetGuard(projectRoot, scanId, "get_start_here_result", result);
5401
+ }
5402
+ if (row.artifactName === "analysis.index") {
5403
+ const result = {
5404
+ startHere: payload.startHere.slice(0, 5),
5405
+ schemaVersion: payload.schemaVersion,
5406
+ scanId: payload.scanId
5407
+ };
5408
+ return await applyBudgetGuard(projectRoot, scanId, "get_start_here_result", result);
5409
+ }
5410
+ throw new Error(`Unsupported artifact type for get_start_here: ${row.artifactName}`);
5411
+ }
5412
+
5413
+ // dist/mcp/tools/hydration/get_project_summary.js
5414
+ var getProjectSummaryTool = {
5415
+ name: "get_project_summary",
5416
+ description: "Returns high-level project metrics from a scan manifest pointer: file counts, pillar summary, stack. Token-safe. Pointer must be valid and unexpired.",
5417
+ inputSchema: {
5418
+ type: "object",
5419
+ properties: {
5420
+ projectRoot: { type: "string", description: "Absolute project root" },
5421
+ manifestPointer: { type: "string", description: "Pointer ID for the scan manifest" },
5422
+ scanId: { type: "string", description: "Current scan ID for budget pointer registration" }
5423
+ },
5424
+ required: ["projectRoot", "manifestPointer", "scanId"]
5425
+ }
5426
+ };
5427
+ async function handleGetProjectSummary(args) {
5428
+ const projectRoot = args.projectRoot;
5429
+ const manifestPointer = args.manifestPointer;
5430
+ const scanId = args.scanId;
5431
+ if (!projectRoot || !manifestPointer || !scanId) {
5432
+ throw new Error("projectRoot, manifestPointer, and scanId are required");
5433
+ }
5434
+ const { content, row } = await hydratePointer(projectRoot, manifestPointer);
5435
+ const payload = JSON.parse(content.toString("utf8"));
5436
+ if (row.artifactName === "artifact_manifest") {
5437
+ const manifest = payload;
5438
+ const analysisEntry = manifest.artifacts.find((a) => a.name === "analysis" && a.indexes?.startHere);
5439
+ let indexData = {};
5440
+ if (analysisEntry?.indexes?.startHere) {
5441
+ const { content: ic } = await hydratePointer(projectRoot, analysisEntry.indexes.startHere);
5442
+ indexData = JSON.parse(ic.toString("utf8"));
5443
+ }
5444
+ const result = {
5445
+ scanId: manifest.scanId,
5446
+ generatedAt: manifest.generatedAt,
5447
+ artifactCount: manifest.artifacts.length,
5448
+ totalArtifactBytes: manifest.artifacts.reduce((s, a) => s + (a.sizeBytes ?? 0), 0),
5449
+ startHere: indexData.startHere,
5450
+ topHeat: indexData.topHeat,
5451
+ pillarSummary: indexData.pillarSummary,
5452
+ totalFiles: indexData.totalFiles,
5453
+ realSourceFiles: indexData.realSourceFiles
5454
+ };
5455
+ return await applyBudgetGuard(projectRoot, scanId, "get_project_summary_result", result);
5456
+ }
5457
+ if (row.artifactName === "analysis.index") {
5458
+ const result = {
5459
+ scanId: payload.scanId,
5460
+ startHere: payload.startHere,
5461
+ topHeat: payload.topHeat,
5462
+ pillarSummary: payload.pillarSummary,
5463
+ totalFiles: payload.totalFiles,
5464
+ realSourceFiles: payload.realSourceFiles
5465
+ };
5466
+ return await applyBudgetGuard(projectRoot, scanId, "get_project_summary_result", result);
5467
+ }
5468
+ throw new Error(`Unsupported artifact type for get_project_summary: ${row.artifactName}`);
5469
+ }
5470
+
5471
+ // dist/mcp/tools/hydration/get_evidence_slice.js
5472
+ var getEvidenceSliceTool = {
5473
+ name: "get_evidence_slice",
5474
+ description: "Raw fallback: returns a line-range slice from a blob artifact. Pointer must be valid and unexpired. Output is budgeted.",
5475
+ inputSchema: {
5476
+ type: "object",
5477
+ properties: {
5478
+ projectRoot: { type: "string", description: "Absolute project root" },
5479
+ pointerId: { type: "string", description: "Pointer ID for the target artifact" },
5480
+ startLine: { type: "number", description: "Inclusive start line (1-based)" },
5481
+ endLine: { type: "number", description: "Inclusive end line (1-based). Capped at startLine+200." },
5482
+ scanId: { type: "string", description: "Current scan ID for budget pointer registration" }
5483
+ },
5484
+ required: ["projectRoot", "pointerId", "startLine", "endLine", "scanId"]
5485
+ }
5486
+ };
5487
+ async function handleGetEvidenceSlice(args) {
5488
+ const projectRoot = args.projectRoot;
5489
+ const pointerId = args.pointerId;
5490
+ const startLine = Number(args.startLine);
5491
+ const endLine = Math.min(Number(args.endLine), startLine + 200);
5492
+ const scanId = args.scanId;
5493
+ if (!projectRoot || !pointerId || !scanId) {
5494
+ throw new Error("projectRoot, pointerId, startLine, endLine, and scanId are required");
5495
+ }
5496
+ const { content, row } = await hydratePointer(projectRoot, pointerId);
5497
+ const rawText = content.toString("utf8");
5498
+ const scope = SessionScope.get();
5499
+ if (scope && (row.artifactName === "file_read" || row.artifactName === "file_skeleton")) {
5500
+ try {
5501
+ const parsed = JSON.parse(rawText);
5502
+ if (parsed.filePath) {
5503
+ SessionScope.enforce(parsed.filePath);
5504
+ }
5505
+ } catch (e) {
5506
+ if (e.name === "ScopeViolation")
5507
+ throw e;
5508
+ }
5509
+ }
5510
+ const lines = rawText.split("\n");
5511
+ const sliced = lines.slice(startLine - 1, endLine);
5512
+ const result = {
5513
+ pointerId,
5514
+ artifactName: row.artifactName,
5515
+ startLine,
5516
+ endLine,
5517
+ totalLines: lines.length,
5518
+ slice: sliced
5519
+ };
5520
+ return await applyBudgetGuard(projectRoot, scanId, "evidence_slice", result);
5521
+ }
5522
+
4209
5523
  // dist/mcp/server.js
4210
5524
  var ALL_TOOLS = [
4211
5525
  scanProjectTool,
@@ -4217,7 +5531,21 @@ var ALL_TOOLS = [
4217
5531
  getStrategicOverviewTool,
4218
5532
  inspectPillarTool,
4219
5533
  getWildDiscoveriesTool,
4220
- markStaleTool
5534
+ markStaleTool,
5535
+ // Phase 2: Skeletons + Hydration
5536
+ getFileSkeletonTool,
5537
+ readFileTool,
5538
+ getStartHereTool,
5539
+ getProjectSummaryTool,
5540
+ getEvidenceSliceTool,
5541
+ // Phase 3: Delegation & Proof
5542
+ createWorkOrderTool,
5543
+ spawnWorkerTool,
5544
+ applyPatchTool,
5545
+ submitReceiptTool,
5546
+ // Phase 4: Scope & Escalation
5547
+ setSessionScopeTool,
5548
+ yieldForScopeExpansionTool
4221
5549
  ];
4222
5550
  var TOOL_HANDLERS = {
4223
5551
  scan_project: handleScanProject,
@@ -4229,7 +5557,21 @@ var TOOL_HANDLERS = {
4229
5557
  get_strategic_overview: handleGetStrategicOverview,
4230
5558
  inspect_pillar: handleInspectPillar,
4231
5559
  get_wild_discoveries: handleGetWildDiscoveries,
4232
- mark_stale: handleMarkStale
5560
+ mark_stale: handleMarkStale,
5561
+ // Phase 2
5562
+ get_file_skeleton: handleGetFileSkeleton,
5563
+ read_file: handleReadFile,
5564
+ get_start_here: handleGetStartHere,
5565
+ get_project_summary: handleGetProjectSummary,
5566
+ get_evidence_slice: handleGetEvidenceSlice,
5567
+ // Phase 3
5568
+ create_work_order: handleCreateWorkOrder,
5569
+ spawn_worker: handleSpawnWorker,
5570
+ apply_patch: handleApplyPatch,
5571
+ submit_receipt: handleSubmitReceipt,
5572
+ // Phase 4
5573
+ set_session_scope: handleSetSessionScope,
5574
+ yield_for_scope_expansion: handleYieldForScopeExpansion
4233
5575
  };
4234
5576
  async function startMCPServer(options = {}) {
4235
5577
  await initParser2();
@@ -4373,10 +5715,210 @@ async function exportCommand(projectRoot, options) {
4373
5715
  console.error("[vibe-splain] Export complete.");
4374
5716
  }
4375
5717
 
5718
+ // dist/commands/gc.js
5719
+ import { join as join21 } from "path";
5720
+ import { rm as rm2, readdir as readdir2 } from "fs/promises";
5721
+ var DEFAULT_KEEP_SCANS = 3;
5722
+ async function gcCommand(projectRoot, opts = {}) {
5723
+ const root = projectRoot ?? process.cwd();
5724
+ const keepScans = opts.keepScans ?? DEFAULT_KEEP_SCANS;
5725
+ console.error(`[vibe-splain gc] Running GC on ${root} (keeping last ${keepScans} scans)`);
5726
+ const pointerStore = PointerStore.open(root);
5727
+ const blobStore = new BlobStore(root);
5728
+ const allScanIds = pointerStore.listAllScanIds();
5729
+ console.error(`[vibe-splain gc] Found ${allScanIds.length} scans`);
5730
+ const sorted = [...allScanIds].sort().reverse();
5731
+ const keepIds = sorted.slice(0, keepScans);
5732
+ const deleteIds = sorted.slice(keepScans);
5733
+ if (deleteIds.length === 0) {
5734
+ console.error("[vibe-splain gc] Nothing to collect");
5735
+ return;
5736
+ }
5737
+ const keptPointers = keepIds.flatMap((id) => pointerStore.listPointersByScan(id));
5738
+ const referencedBlobs = new Set(keptPointers.map((p) => p.blobPath));
5739
+ const deleted = await pointerStore.gcScanPointers(keepIds);
5740
+ console.error(`[vibe-splain gc] Deleted ${deleted} pointer rows`);
5741
+ const allBlobs = await blobStore.listBlobPaths();
5742
+ let blobsDeleted = 0;
5743
+ for (const blobPath of allBlobs) {
5744
+ if (!referencedBlobs.has(blobPath)) {
5745
+ try {
5746
+ await rm2(blobPath);
5747
+ blobsDeleted++;
5748
+ } catch {
5749
+ }
5750
+ }
5751
+ }
5752
+ console.error(`[vibe-splain gc] Deleted ${blobsDeleted} unreferenced blobs`);
5753
+ const tmpDir = join21(root, ".vibe-splainer", "tmp");
5754
+ try {
5755
+ const tmpFiles = await readdir2(tmpDir);
5756
+ for (const f of tmpFiles) {
5757
+ await rm2(join21(tmpDir, f), { force: true });
5758
+ }
5759
+ console.error(`[vibe-splain gc] Cleaned ${tmpFiles.length} tmp files`);
5760
+ } catch {
5761
+ }
5762
+ console.error("[vibe-splain gc] Done");
5763
+ }
5764
+
5765
+ // dist/commands/bundle.js
5766
+ import { join as join22 } from "path";
5767
+ import { writeFile as writeFile10, mkdir as mkdir9, copyFile, rm as rm3 } from "fs/promises";
5768
+ import { existsSync as existsSync7 } from "fs";
5769
+ import * as tar from "tar";
5770
+ async function bundleCommand(scanId, opts = {}) {
5771
+ const root = opts.projectRoot ?? process.cwd();
5772
+ const outputPath = opts.output ?? join22(root, `vibe-bundle-${scanId}.tar.gz`);
5773
+ console.error(`[vibe-splain bundle] Bundling scan ${scanId} from ${root}`);
5774
+ const pointerStore = PointerStore.open(root);
5775
+ const blobStore = new BlobStore(root);
5776
+ const pointers = pointerStore.listPointersByScan(scanId);
5777
+ if (pointers.length === 0) {
5778
+ throw new Error(`No pointers found for scanId "${scanId}"`);
5779
+ }
5780
+ const stagingDir = join22(root, ".vibe-splainer", "tmp", `bundle-stage-${scanId}`);
5781
+ const blobsStageDir = join22(stagingDir, "blobs");
5782
+ await mkdir9(blobsStageDir, { recursive: true });
5783
+ try {
5784
+ const bundleManifest = {
5785
+ schemaVersion: "1.0.0",
5786
+ scanId,
5787
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
5788
+ projectRoot: root,
5789
+ pointers: pointers.map((p) => ({
5790
+ pointerId: p.pointerId,
5791
+ scanId: p.scanId,
5792
+ artifactName: p.artifactName,
5793
+ contentHash: p.contentHash,
5794
+ blobFile: `blobs/${p.contentHash.replace("sha256:", "sha256_")}`,
5795
+ schemaVersion: p.schemaVersion,
5796
+ createdAt: p.createdAt,
5797
+ expiresAt: p.expiresAt
5798
+ }))
5799
+ };
5800
+ await writeFile10(join22(stagingDir, "bundle-manifest.json"), JSON.stringify(bundleManifest, null, 2), "utf8");
5801
+ const seen = /* @__PURE__ */ new Set();
5802
+ for (const p of pointers) {
5803
+ const hex = p.contentHash.replace("sha256:", "");
5804
+ if (seen.has(hex))
5805
+ continue;
5806
+ seen.add(hex);
5807
+ const srcPath = p.blobPath;
5808
+ if (!existsSync7(srcPath)) {
5809
+ console.error(`[vibe-splain bundle] Warning: blob missing for ${p.pointerId}: ${srcPath}`);
5810
+ continue;
5811
+ }
5812
+ await copyFile(srcPath, join22(blobsStageDir, `sha256_${hex}`));
5813
+ }
5814
+ await tar.create({
5815
+ gzip: true,
5816
+ file: outputPath,
5817
+ cwd: stagingDir,
5818
+ portable: true
5819
+ }, ["."]);
5820
+ console.error(`[vibe-splain bundle] Bundle written: ${outputPath}`);
5821
+ console.error(`[vibe-splain bundle] ${pointers.length} pointers, ${seen.size} blobs`);
5822
+ } finally {
5823
+ await rm3(stagingDir, { recursive: true, force: true });
5824
+ }
5825
+ }
5826
+
5827
+ // dist/commands/importBundle.js
5828
+ import { join as join23 } from "path";
5829
+ import { readFile as readFile15, mkdir as mkdir10, rm as rm4 } from "fs/promises";
5830
+ import { existsSync as existsSync8 } from "fs";
5831
+ import * as tar2 from "tar";
5832
+ import { createHash as createHash6 } from "crypto";
5833
+ async function importBundleCommand(tarballPath, opts = {}) {
5834
+ const root = opts.projectRoot ?? process.cwd();
5835
+ const namespace = opts.namespace ?? `imported_${Date.now()}`;
5836
+ console.error(`[vibe-splain import] Importing ${tarballPath} into ${root} (namespace: ${namespace})`);
5837
+ if (!existsSync8(tarballPath)) {
5838
+ throw new Error(`Tarball not found: ${tarballPath}`);
5839
+ }
5840
+ const extractDir = join23(root, ".vibe-splainer", "tmp", `import-${namespace}`);
5841
+ await mkdir10(extractDir, { recursive: true });
5842
+ try {
5843
+ await tar2.extract({
5844
+ file: tarballPath,
5845
+ cwd: extractDir
5846
+ });
5847
+ const manifestPath = join23(extractDir, "bundle-manifest.json");
5848
+ if (!existsSync8(manifestPath)) {
5849
+ throw new Error("Invalid bundle: missing bundle-manifest.json");
5850
+ }
5851
+ const manifestRaw = await readFile15(manifestPath, "utf8");
5852
+ const manifest = JSON.parse(manifestRaw);
5853
+ if (manifest.schemaVersion !== "1.0.0") {
5854
+ throw new Error(`Unsupported bundle schema version: ${manifest.schemaVersion}`);
5855
+ }
5856
+ const blobStore = new BlobStore(root);
5857
+ const pointerStore = PointerStore.open(root);
5858
+ await blobStore.ensureDirs();
5859
+ let imported = 0;
5860
+ let hashErrors = 0;
5861
+ for (const entry of manifest.pointers) {
5862
+ const blobSrcPath = join23(extractDir, entry.blobFile);
5863
+ if (!existsSync8(blobSrcPath)) {
5864
+ console.error(`[vibe-splain import] Missing blob for pointer ${entry.pointerId}: ${entry.blobFile}`);
5865
+ hashErrors++;
5866
+ continue;
5867
+ }
5868
+ const content = await readFile15(blobSrcPath);
5869
+ const actualHash = `sha256:${createHash6("sha256").update(content).digest("hex")}`;
5870
+ if (actualHash !== entry.contentHash) {
5871
+ console.error(`[vibe-splain import] Hash mismatch for ${entry.pointerId}: expected ${entry.contentHash}, got ${actualHash}`);
5872
+ hashErrors++;
5873
+ continue;
5874
+ }
5875
+ const { blobPath } = await blobStore.writeAtomic(content);
5876
+ const namespacedPointerId = `${namespace}::${entry.pointerId}`;
5877
+ const namespacedScanId = `${namespace}::${entry.scanId}`;
5878
+ await pointerStore.insertPointer({
5879
+ pointerId: namespacedPointerId,
5880
+ scanId: namespacedScanId,
5881
+ artifactName: entry.artifactName,
5882
+ contentHash: entry.contentHash,
5883
+ blobPath,
5884
+ schemaVersion: entry.schemaVersion,
5885
+ createdAt: entry.createdAt,
5886
+ expiresAt: entry.expiresAt
5887
+ });
5888
+ imported++;
5889
+ }
5890
+ if (hashErrors > 0) {
5891
+ console.error(`[vibe-splain import] Warning: ${hashErrors} blobs failed hash verification and were skipped`);
5892
+ }
5893
+ console.error(`[vibe-splain import] Imported ${imported}/${manifest.pointers.length} pointers under namespace "${namespace}"`);
5894
+ console.error(`[vibe-splain import] Original scanId: ${manifest.scanId} \u2192 namespaced as: ${namespace}::${manifest.scanId}`);
5895
+ } finally {
5896
+ await rm4(extractDir, { recursive: true, force: true });
5897
+ }
5898
+ }
5899
+
4376
5900
  // dist/index.js
4377
5901
  var program = new Command();
4378
- program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("3.0.0");
5902
+ program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("3.2.0");
4379
5903
  program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
4380
5904
  program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").option("--format <format>", "Export format (html, markdown, etc.)").option("--budget <budget>", "Token budget for markdown").option("--scope <scope>", "Scope for export").action((options) => serveCommand(options));
4381
5905
  program.command("export [projectRoot]").description("Manually trigger bundle generation").option("--format <format>", "Export format (html, markdown, etc.)").option("--budget <budget>", "Token budget for markdown").option("--scope <scope>", "Scope for export").action(exportCommand);
5906
+ program.command("gc [projectRoot]").description("Garbage-collect old scan artifacts, keeping the last N scans").option("--keep-scans <n>", "Number of scans to keep (default: 3)", "3").action((projectRoot, options) => {
5907
+ gcCommand(projectRoot, { keepScans: parseInt(options.keepScans, 10) }).catch((err) => {
5908
+ console.error("[vibe-splain gc] Error:", err.message);
5909
+ process.exit(1);
5910
+ });
5911
+ });
5912
+ program.command("bundle <scanId>").description("Bundle a scan into a portable vibe-bundle.tar.gz").option("--output <path>", "Output tarball path").option("--project-root <path>", "Project root (default: cwd)").action((scanId, options) => {
5913
+ bundleCommand(scanId, { output: options.output, projectRoot: options.projectRoot }).catch((err) => {
5914
+ console.error("[vibe-splain bundle] Error:", err.message);
5915
+ process.exit(1);
5916
+ });
5917
+ });
5918
+ program.command("import <tarball>").description("Import a vibe-bundle.tar.gz into the local pointer store").option("--namespace <ns>", "Bundle namespace alias (default: imported_<timestamp>)").option("--project-root <path>", "Project root (default: cwd)").action((tarball, options) => {
5919
+ importBundleCommand(tarball, { namespace: options.namespace, projectRoot: options.projectRoot }).catch((err) => {
5920
+ console.error("[vibe-splain import] Error:", err.message);
5921
+ process.exit(1);
5922
+ });
5923
+ });
4382
5924
  program.parse();