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.
- package/dist/commands/bundle.d.ts +4 -0
- package/dist/commands/bundle.js +68 -0
- package/dist/commands/gc.d.ts +3 -0
- package/dist/commands/gc.js +59 -0
- package/dist/commands/importBundle.d.ts +4 -0
- package/dist/commands/importBundle.js +80 -0
- package/dist/export/ExportOrchestrator.d.ts +19 -1
- package/dist/export/ExportOrchestrator.js +87 -1
- package/dist/index.js +1498 -36
- package/dist/mcp/BudgetGuard.d.ts +13 -0
- package/dist/mcp/BudgetGuard.js +55 -0
- package/dist/mcp/SessionScope.d.ts +26 -0
- package/dist/mcp/SessionScope.js +56 -0
- package/dist/mcp/server.js +38 -0
- package/dist/mcp/tools/apply_patch.d.ts +37 -0
- package/dist/mcp/tools/apply_patch.js +103 -0
- package/dist/mcp/tools/get_file_skeleton.d.ts +23 -0
- package/dist/mcp/tools/get_file_skeleton.js +124 -0
- package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +31 -0
- package/dist/mcp/tools/hydration/get_evidence_slice.js +59 -0
- package/dist/mcp/tools/hydration/get_project_summary.d.ts +23 -0
- package/dist/mcp/tools/hydration/get_project_summary.js +58 -0
- package/dist/mcp/tools/hydration/get_start_here.d.ts +23 -0
- package/dist/mcp/tools/hydration/get_start_here.js +52 -0
- package/dist/mcp/tools/read_file.d.ts +31 -0
- package/dist/mcp/tools/read_file.js +90 -0
- package/dist/mcp/tools/scan_project.js +5 -2
- package/dist/mcp/tools/set_session_scope.d.ts +19 -0
- package/dist/mcp/tools/set_session_scope.js +40 -0
- package/dist/mcp/tools/submit_receipt.d.ts +68 -0
- package/dist/mcp/tools/submit_receipt.js +94 -0
- package/dist/mcp/tools/work_orders.d.ts +79 -0
- package/dist/mcp/tools/work_orders.js +126 -0
- package/dist/mcp/tools/yield_for_scope_expansion.d.ts +29 -0
- package/dist/mcp/tools/yield_for_scope_expansion.js +59 -0
- package/dist/store/BlobStore.d.ts +22 -0
- package/dist/store/BlobStore.js +96 -0
- package/dist/store/PointerStore.d.ts +52 -0
- package/dist/store/PointerStore.js +138 -0
- 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:
|
|
1204
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
1205
1205
|
let entries = [];
|
|
1206
1206
|
try {
|
|
1207
|
-
entries = await
|
|
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:
|
|
1287
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
1288
1288
|
let entries = [];
|
|
1289
1289
|
try {
|
|
1290
|
-
const dirents = await
|
|
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:
|
|
3312
|
+
const { existsSync: existsSync9 } = await import("fs");
|
|
3230
3313
|
const { cp } = await import("fs/promises");
|
|
3231
|
-
if (
|
|
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:" +
|
|
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 (
|
|
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
|
|
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 (
|
|
3327
|
-
if (!
|
|
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 (
|
|
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
|
|
3635
|
-
import { readFile as
|
|
3636
|
-
import { join as
|
|
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
|
|
3655
|
-
const newHash =
|
|
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 ===
|
|
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
|
|
3859
|
-
import { join as
|
|
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 :
|
|
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
|
|
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
|
|
3911
|
-
import { createHash as
|
|
3912
|
-
import { readFile as
|
|
3913
|
-
import { join as
|
|
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
|
|
4394
|
+
primaryContent = await readFile12(join16(projectRoot, primaryFile), "utf8");
|
|
4011
4395
|
} catch {
|
|
4012
4396
|
}
|
|
4013
|
-
const hash =
|
|
4397
|
+
const hash = createHash5("sha256").update(primaryContent).digest("hex");
|
|
4014
4398
|
const card = {
|
|
4015
|
-
id:
|
|
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.
|
|
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();
|