vibe-splain 3.1.0 → 3.2.1

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 (40) 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/ExportOrchestrator.d.ts +19 -1
  8. package/dist/export/ExportOrchestrator.js +87 -1
  9. package/dist/index.js +1498 -36
  10. package/dist/mcp/BudgetGuard.d.ts +13 -0
  11. package/dist/mcp/BudgetGuard.js +55 -0
  12. package/dist/mcp/SessionScope.d.ts +26 -0
  13. package/dist/mcp/SessionScope.js +56 -0
  14. package/dist/mcp/server.js +38 -0
  15. package/dist/mcp/tools/apply_patch.d.ts +37 -0
  16. package/dist/mcp/tools/apply_patch.js +103 -0
  17. package/dist/mcp/tools/get_file_skeleton.d.ts +23 -0
  18. package/dist/mcp/tools/get_file_skeleton.js +124 -0
  19. package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +31 -0
  20. package/dist/mcp/tools/hydration/get_evidence_slice.js +59 -0
  21. package/dist/mcp/tools/hydration/get_project_summary.d.ts +23 -0
  22. package/dist/mcp/tools/hydration/get_project_summary.js +58 -0
  23. package/dist/mcp/tools/hydration/get_start_here.d.ts +23 -0
  24. package/dist/mcp/tools/hydration/get_start_here.js +52 -0
  25. package/dist/mcp/tools/read_file.d.ts +31 -0
  26. package/dist/mcp/tools/read_file.js +90 -0
  27. package/dist/mcp/tools/scan_project.js +5 -2
  28. package/dist/mcp/tools/set_session_scope.d.ts +19 -0
  29. package/dist/mcp/tools/set_session_scope.js +40 -0
  30. package/dist/mcp/tools/submit_receipt.d.ts +68 -0
  31. package/dist/mcp/tools/submit_receipt.js +94 -0
  32. package/dist/mcp/tools/work_orders.d.ts +79 -0
  33. package/dist/mcp/tools/work_orders.js +126 -0
  34. package/dist/mcp/tools/yield_for_scope_expansion.d.ts +29 -0
  35. package/dist/mcp/tools/yield_for_scope_expansion.js +59 -0
  36. package/dist/store/BlobStore.d.ts +22 -0
  37. package/dist/store/BlobStore.js +96 -0
  38. package/dist/store/PointerStore.d.ts +52 -0
  39. package/dist/store/PointerStore.js +138 -0
  40. 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;
@@ -3210,10 +3210,93 @@ var RecommendationEngine = class {
3210
3210
  }
3211
3211
  };
3212
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
+
3213
3296
  // dist/export/ArtifactBundleWriter.js
3214
3297
  import { join as join10 } from "path";
3215
3298
  import { writeFile as writeFile7, mkdir as mkdir6, rm, rename } from "fs/promises";
3216
- import { createHash } from "crypto";
3299
+ import { createHash as createHash2 } from "crypto";
3217
3300
  var ArtifactBundleWriter = class {
3218
3301
  projectRoot;
3219
3302
  constructor(projectRoot) {
@@ -3226,9 +3309,9 @@ var ArtifactBundleWriter = class {
3226
3309
  try {
3227
3310
  await rm(stagingDir, { recursive: true, force: true });
3228
3311
  await rm(oldDir, { recursive: true, force: true });
3229
- const { existsSync: existsSync5 } = await import("fs");
3312
+ const { existsSync: existsSync9 } = await import("fs");
3230
3313
  const { cp } = await import("fs/promises");
3231
- if (existsSync5(outputDir)) {
3314
+ if (existsSync9(outputDir)) {
3232
3315
  await cp(outputDir, stagingDir, { recursive: true });
3233
3316
  } else {
3234
3317
  await mkdir6(stagingDir, { recursive: true });
@@ -3243,7 +3326,7 @@ var ArtifactBundleWriter = class {
3243
3326
  manifestArtifacts.push({
3244
3327
  type: artifact.type,
3245
3328
  path: artifact.path,
3246
- checksum: "sha256:" + createHash("sha256").update(buffer).digest("hex"),
3329
+ checksum: "sha256:" + createHash2("sha256").update(buffer).digest("hex"),
3247
3330
  sizeBytes: buffer.length
3248
3331
  });
3249
3332
  }
@@ -3255,7 +3338,7 @@ var ArtifactBundleWriter = class {
3255
3338
  };
3256
3339
  await writeFile7(join10(stagingDir, "artifact_manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3257
3340
  let swapped = false;
3258
- if (existsSync5(outputDir)) {
3341
+ if (existsSync9(outputDir)) {
3259
3342
  await rename(outputDir, oldDir);
3260
3343
  swapped = true;
3261
3344
  }
@@ -3293,7 +3376,7 @@ var JsonRenderer = class {
3293
3376
  // dist/export/renderers/HtmlRenderer.js
3294
3377
  import { join as join11, dirname as dirname3, relative as relative3 } from "path";
3295
3378
  import { fileURLToPath as fileURLToPath2 } from "url";
3296
- import { existsSync as existsSync4, readFileSync, readdirSync, statSync } from "fs";
3379
+ import { existsSync as existsSync5, readFileSync, readdirSync, statSync } from "fs";
3297
3380
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
3298
3381
  function getAllFiles(dirPath, arrayOfFiles = []) {
3299
3382
  const files = readdirSync(dirPath);
@@ -3323,8 +3406,8 @@ var HtmlRenderer = class {
3323
3406
  ];
3324
3407
  let templateDir = "";
3325
3408
  for (const p of candidatePaths) {
3326
- if (existsSync4(p) && existsSync4(join11(p, "index.html"))) {
3327
- if (!existsSync4(join11(p, "vite.config.ts")) || p.endsWith("dist")) {
3409
+ if (existsSync5(p) && existsSync5(join11(p, "index.html"))) {
3410
+ if (!existsSync5(join11(p, "vite.config.ts")) || p.endsWith("dist")) {
3328
3411
  templateDir = p;
3329
3412
  break;
3330
3413
  }
@@ -3332,7 +3415,7 @@ var HtmlRenderer = class {
3332
3415
  }
3333
3416
  if (!templateDir) {
3334
3417
  for (const p of candidatePaths) {
3335
- if (existsSync4(join11(p, "index.html"))) {
3418
+ if (existsSync5(join11(p, "index.html"))) {
3336
3419
  templateDir = p;
3337
3420
  break;
3338
3421
  }
@@ -3567,13 +3650,234 @@ var GraphRenderer = class {
3567
3650
  }
3568
3651
  };
3569
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
+
3570
3873
  // dist/export/ExportOrchestrator.js
3874
+ import { v4 as uuidv4 } from "uuid";
3571
3875
  var ExportOrchestrator = class {
3572
3876
  projectRoot;
3573
3877
  constructor(projectRoot) {
3574
3878
  this.projectRoot = projectRoot;
3575
3879
  }
3576
- async writeBundle(dossier, options = {}, store, graph) {
3880
+ async writeBundle(dossier, options = {}, store, graph, scanId) {
3577
3881
  const finalStore = store || await readAnalysis(this.projectRoot);
3578
3882
  if (!finalStore) {
3579
3883
  throw new Error("Analysis store not found. Scan the project first.");
@@ -3605,6 +3909,83 @@ var ExportOrchestrator = class {
3605
3909
  }
3606
3910
  const writer = new ArtifactBundleWriter(this.projectRoot);
3607
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 };
3608
3989
  }
3609
3990
  buildViewModel(dossier, store) {
3610
3991
  const recommendations = {};
@@ -3631,9 +4012,9 @@ var ExportOrchestrator = class {
3631
4012
 
3632
4013
  // dist/export/Watcher.js
3633
4014
  import chokidar from "chokidar";
3634
- import { createHash as createHash2 } from "crypto";
3635
- import { readFile as readFile9 } from "fs/promises";
3636
- import { join as join12 } from "path";
4015
+ import { createHash as createHash4 } from "crypto";
4016
+ import { readFile as readFile10 } from "fs/promises";
4017
+ import { join as join14 } from "path";
3637
4018
  var activeWatchers = /* @__PURE__ */ new Map();
3638
4019
  async function startWatcher(projectRoot, watchedPaths) {
3639
4020
  const existing = activeWatchers.get(projectRoot);
@@ -3651,14 +4032,14 @@ async function startWatcher(projectRoot, watchedPaths) {
3651
4032
  const dossier = await readDossier(projectRoot);
3652
4033
  if (!dossier)
3653
4034
  return;
3654
- const content = await readFile9(filepath, "utf8");
3655
- const newHash = createHash2("sha256").update(content).digest("hex");
4035
+ const content = await readFile10(filepath, "utf8");
4036
+ const newHash = createHash4("sha256").update(content).digest("hex");
3656
4037
  let mutated = false;
3657
4038
  for (const pillar of dossier.pillars) {
3658
4039
  for (const card of pillar.decisions) {
3659
4040
  if (!card.primaryFile)
3660
4041
  continue;
3661
- const absMatch = filepath === join12(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
4042
+ const absMatch = filepath === join14(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
3662
4043
  if (absMatch && card.lastScannedHash !== newHash) {
3663
4044
  card.status = "stale";
3664
4045
  const rel = card.primaryFile;
@@ -3726,12 +4107,13 @@ async function handleScanProject(args, options = {}) {
3726
4107
  dossier.pillars.push({ name: def.name, cardCount: 0, decisions: [] });
3727
4108
  }
3728
4109
  }
4110
+ const scanId = `scan_${Date.now()}`;
3729
4111
  const orchestrator = new ExportOrchestrator(projectRoot);
3730
- await orchestrator.writeBundle(dossier, {
4112
+ const { manifestPointer } = await orchestrator.writeBundle(dossier, {
3731
4113
  format: options.format,
3732
4114
  budget: options.budget ? parseInt(options.budget, 10) : void 0,
3733
4115
  scope: options.scope
3734
- }, result.store, result.graph);
4116
+ }, result.store, result.graph, scanId);
3735
4117
  await startWatcher(projectRoot, result.files.map((f) => f.path));
3736
4118
  console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
3737
4119
  const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
@@ -3742,6 +4124,8 @@ async function handleScanProject(args, options = {}) {
3742
4124
  return {
3743
4125
  ok: true,
3744
4126
  message: statusMsg,
4127
+ scanId,
4128
+ manifestPointer,
3745
4129
  validation: {
3746
4130
  passed: validation.passed,
3747
4131
  errors: validation.errors,
@@ -3855,8 +4239,8 @@ async function handleSetProjectBrief(args, options = {}) {
3855
4239
  }
3856
4240
 
3857
4241
  // dist/mcp/tools/get_file_context.js
3858
- import { readFile as readFile10 } from "fs/promises";
3859
- 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";
3860
4244
  var getFileContextTool = {
3861
4245
  name: "get_file_context",
3862
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.",
@@ -3876,7 +4260,7 @@ async function handleGetFileContext(args) {
3876
4260
  const full = args.full === true;
3877
4261
  if (!projectRoot || !filePath)
3878
4262
  throw new Error("projectRoot and filePath are required");
3879
- const fullPath = isAbsolute(filePath) ? filePath : join13(projectRoot, filePath);
4263
+ const fullPath = isAbsolute(filePath) ? filePath : join15(projectRoot, filePath);
3880
4264
  const relPath = relative4(projectRoot, fullPath);
3881
4265
  const evidence = await getFileAnalysis(fullPath);
3882
4266
  if (!evidence) {
@@ -3901,16 +4285,16 @@ async function handleGetFileContext(args) {
3901
4285
  smellSpans: evidence.smellSpans
3902
4286
  };
3903
4287
  if (full) {
3904
- result.source = await readFile10(fullPath, "utf8");
4288
+ result.source = await readFile11(fullPath, "utf8");
3905
4289
  }
3906
4290
  return result;
3907
4291
  }
3908
4292
 
3909
4293
  // dist/mcp/tools/write_decision_card.js
3910
- import { v4 as uuidv4 } from "uuid";
3911
- import { createHash as createHash3 } from "crypto";
3912
- import { readFile as readFile11 } from "fs/promises";
3913
- 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";
3914
4298
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
3915
4299
  function normalizeSnippet(s) {
3916
4300
  let out = (s ?? "").replace(/\r\n/g, "\n");
@@ -4007,12 +4391,12 @@ async function handleWriteDecisionCard(args, options = {}) {
4007
4391
  const heat = persisted ? Math.round(persisted.heat) : void 0;
4008
4392
  let primaryContent = "";
4009
4393
  try {
4010
- primaryContent = await readFile11(join14(projectRoot, primaryFile), "utf8");
4394
+ primaryContent = await readFile12(join16(projectRoot, primaryFile), "utf8");
4011
4395
  } catch {
4012
4396
  }
4013
- const hash = createHash3("sha256").update(primaryContent).digest("hex");
4397
+ const hash = createHash5("sha256").update(primaryContent).digest("hex");
4014
4398
  const card = {
4015
- id: uuidv4(),
4399
+ id: uuidv42(),
4016
4400
  pillar,
4017
4401
  title,
4018
4402
  thesis,
@@ -4286,6 +4670,856 @@ async function handleGetCallChain(args) {
4286
4670
  }
4287
4671
  }
4288
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
+
4289
5523
  // dist/mcp/server.js
4290
5524
  var ALL_TOOLS = [
4291
5525
  scanProjectTool,
@@ -4297,7 +5531,21 @@ var ALL_TOOLS = [
4297
5531
  getStrategicOverviewTool,
4298
5532
  inspectPillarTool,
4299
5533
  getWildDiscoveriesTool,
4300
- 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
4301
5549
  ];
4302
5550
  var TOOL_HANDLERS = {
4303
5551
  scan_project: handleScanProject,
@@ -4309,7 +5557,21 @@ var TOOL_HANDLERS = {
4309
5557
  get_strategic_overview: handleGetStrategicOverview,
4310
5558
  inspect_pillar: handleInspectPillar,
4311
5559
  get_wild_discoveries: handleGetWildDiscoveries,
4312
- 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
4313
5575
  };
4314
5576
  async function startMCPServer(options = {}) {
4315
5577
  await initParser2();
@@ -4453,10 +5715,210 @@ async function exportCommand(projectRoot, options) {
4453
5715
  console.error("[vibe-splain] Export complete.");
4454
5716
  }
4455
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
+
4456
5900
  // dist/index.js
4457
5901
  var program = new Command();
4458
- 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");
4459
5903
  program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
4460
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));
4461
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
+ });
4462
5924
  program.parse();