vibe-splain 3.4.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -158
- package/dist/commands/install.d.ts +0 -5
- package/dist/commands/install.js +5 -55
- package/dist/commands/serve.js +1 -1
- package/dist/export/ArtifactBundleWriter.js +3 -3
- package/dist/export/ExportOrchestrator.d.ts +2 -20
- package/dist/export/ExportOrchestrator.js +5 -116
- package/dist/export/Watcher.js +5 -5
- package/dist/export/renderers/AgentMarkdownRenderer.d.ts +1 -1
- package/dist/export/renderers/GraphRenderer.d.ts +2 -2
- package/dist/export/renderers/HtmlRenderer.d.ts +1 -1
- package/dist/export/renderers/HtmlRenderer.js +1 -1
- package/dist/export/renderers/JsonRenderer.d.ts +1 -1
- package/dist/export/renderers/RawAnalysisRenderer.d.ts +1 -1
- package/dist/export/renderers/Renderer.d.ts +1 -1
- package/dist/export/renderers/ValidationRenderer.d.ts +1 -1
- package/dist/index.js +1425 -6511
- package/dist/mcp/server.js +6 -46
- package/dist/mcp/tools/get_file_context.js +1 -1
- package/dist/mcp/tools/get_project_map.js +1 -1
- package/dist/mcp/tools/get_strategic_overview.js +1 -1
- package/dist/mcp/tools/get_wild_discoveries.js +1 -1
- package/dist/mcp/tools/inspect_pillar.js +1 -1
- package/dist/mcp/tools/mark_stale.js +1 -1
- package/dist/mcp/tools/scan_project.d.ts +2 -3
- package/dist/mcp/tools/scan_project.js +12 -13
- package/dist/mcp/tools/set_project_brief.js +1 -1
- package/dist/mcp/tools/write_decision_card.d.ts +1 -1
- package/dist/mcp/tools/write_decision_card.js +2 -2
- package/dist/ui/index.html +1 -1
- package/package.json +9 -14
- package/dist/commands/bundle.d.ts +0 -4
- package/dist/commands/bundle.js +0 -68
- package/dist/commands/export.d.ts +0 -1
- package/dist/commands/export.js +0 -19
- package/dist/commands/gc.d.ts +0 -3
- package/dist/commands/gc.js +0 -59
- package/dist/commands/hook.d.ts +0 -9
- package/dist/commands/hook.js +0 -84
- package/dist/commands/importBundle.d.ts +0 -4
- package/dist/commands/importBundle.js +0 -80
- package/dist/commands/network.d.ts +0 -1
- package/dist/commands/network.js +0 -91
- package/dist/commands/scan.d.ts +0 -13
- package/dist/commands/scan.js +0 -97
- package/dist/export/renderers/DeltaRenderer.d.ts +0 -6
- package/dist/export/renderers/DeltaRenderer.js +0 -22
- package/dist/hook/preToolUse.d.ts +0 -30
- package/dist/hook/preToolUse.js +0 -81
- package/dist/hook-entry.d.ts +0 -2
- package/dist/hook-entry.js +0 -8
- package/dist/hook.d.ts +0 -1
- package/dist/hook.js +0 -337
- package/dist/mcp/BudgetGuard.d.ts +0 -13
- package/dist/mcp/BudgetGuard.js +0 -55
- package/dist/mcp/SessionScope.d.ts +0 -26
- package/dist/mcp/SessionScope.js +0 -56
- package/dist/mcp/tools/apply_patch.d.ts +0 -37
- package/dist/mcp/tools/apply_patch.js +0 -103
- package/dist/mcp/tools/explainSpecialistScope.d.ts +0 -15
- package/dist/mcp/tools/explainSpecialistScope.js +0 -30
- package/dist/mcp/tools/get_call_chain.d.ts +0 -42
- package/dist/mcp/tools/get_call_chain.js +0 -50
- package/dist/mcp/tools/get_file_skeleton.d.ts +0 -23
- package/dist/mcp/tools/get_file_skeleton.js +0 -124
- package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +0 -31
- package/dist/mcp/tools/hydration/get_evidence_slice.js +0 -57
- package/dist/mcp/tools/hydration/get_project_summary.d.ts +0 -23
- package/dist/mcp/tools/hydration/get_project_summary.js +0 -58
- package/dist/mcp/tools/hydration/get_start_here.d.ts +0 -23
- package/dist/mcp/tools/hydration/get_start_here.js +0 -52
- package/dist/mcp/tools/prepareTaskContext.d.ts +0 -38
- package/dist/mcp/tools/prepareTaskContext.js +0 -93
- package/dist/mcp/tools/read_file.d.ts +0 -31
- package/dist/mcp/tools/read_file.js +0 -90
- package/dist/mcp/tools/set_session_scope.d.ts +0 -19
- package/dist/mcp/tools/set_session_scope.js +0 -40
- package/dist/mcp/tools/submit_receipt.d.ts +0 -68
- package/dist/mcp/tools/submit_receipt.js +0 -94
- package/dist/mcp/tools/work_orders.d.ts +0 -79
- package/dist/mcp/tools/work_orders.js +0 -126
- package/dist/mcp/tools/yield_for_scope_expansion.d.ts +0 -29
- package/dist/mcp/tools/yield_for_scope_expansion.js +0 -59
- package/dist/store/BlobStore.d.ts +0 -22
- package/dist/store/BlobStore.js +0 -96
- package/dist/store/PointerStore.d.ts +0 -52
- package/dist/store/PointerStore.js +0 -154
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { EscalationContext } from '@vibe-splain/brain/dist/network/escalation.js';
|
|
2
|
-
import type { GateIndex } from '@vibe-splain/brain/dist/network/gateIndex.js';
|
|
3
|
-
export interface PreToolUseInput {
|
|
4
|
-
tool_name?: string;
|
|
5
|
-
tool_input?: {
|
|
6
|
-
file_path?: string;
|
|
7
|
-
[k: string]: unknown;
|
|
8
|
-
};
|
|
9
|
-
cwd?: string;
|
|
10
|
-
session_id?: string;
|
|
11
|
-
}
|
|
12
|
-
export interface HookOutput {
|
|
13
|
-
hookSpecificOutput: {
|
|
14
|
-
hookEventName: 'PreToolUse';
|
|
15
|
-
permissionDecision: 'allow' | 'ask';
|
|
16
|
-
permissionDecisionReason?: string;
|
|
17
|
-
additionalContext?: string;
|
|
18
|
-
systemMessage?: string;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
export type HookResult = {
|
|
22
|
-
action: 'defer';
|
|
23
|
-
} | {
|
|
24
|
-
action: 'emit';
|
|
25
|
-
output: HookOutput;
|
|
26
|
-
};
|
|
27
|
-
export declare function decidePreToolUse(input: PreToolUseInput, gateIndex: GateIndex | null, sessionState?: {
|
|
28
|
-
warningShown?: boolean;
|
|
29
|
-
}): HookResult;
|
|
30
|
-
export declare function formatEscalation(ctx: EscalationContext): string;
|
package/dist/hook/preToolUse.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// PreToolUse hook decision logic — the deterministic gate.
|
|
2
|
-
//
|
|
3
|
-
// Pure: given the agent's intended tool call and the scan artifact, decide
|
|
4
|
-
// whether to escalate and how. No stdin/stdout, no filesystem — the CLI command
|
|
5
|
-
// wraps this with I/O. This separation is what makes the gate testable against
|
|
6
|
-
// real scanned stores.
|
|
7
|
-
import { buildEscalationContext } from '@vibe-splain/brain/dist/network/escalation.js';
|
|
8
|
-
const DEFER = { action: 'defer' };
|
|
9
|
-
// Only file-editing tools can have a blast radius. Anything else defers.
|
|
10
|
-
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
11
|
-
export function decidePreToolUse(input, gateIndex, sessionState) {
|
|
12
|
-
if (!EDIT_TOOLS.has(input.tool_name ?? ''))
|
|
13
|
-
return DEFER;
|
|
14
|
-
if (!gateIndex) {
|
|
15
|
-
if (sessionState && !sessionState.warningShown) {
|
|
16
|
-
const warnMsg = "vibe-splain: .vibe-splainer/gate.json is missing. Please run 'vibe-splain scan' to generate the scan architecture and enable tool guarding.";
|
|
17
|
-
return {
|
|
18
|
-
action: 'emit',
|
|
19
|
-
output: {
|
|
20
|
-
hookSpecificOutput: {
|
|
21
|
-
hookEventName: 'PreToolUse',
|
|
22
|
-
permissionDecision: 'allow',
|
|
23
|
-
additionalContext: warnMsg,
|
|
24
|
-
systemMessage: warnMsg,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
return DEFER;
|
|
30
|
-
}
|
|
31
|
-
const filePath = input.tool_input?.file_path;
|
|
32
|
-
if (!filePath)
|
|
33
|
-
return DEFER;
|
|
34
|
-
const ctx = buildEscalationContext(filePath, gateIndex);
|
|
35
|
-
if (!ctx || ctx.blastRadius === 'low')
|
|
36
|
-
return DEFER;
|
|
37
|
-
const block = formatEscalation(ctx);
|
|
38
|
-
// high → stop-and-reconsider (human gate). medium → non-blocking awareness:
|
|
39
|
-
// the agent sees the context on its next request without being interrupted.
|
|
40
|
-
if (ctx.blastRadius === 'high') {
|
|
41
|
-
return {
|
|
42
|
-
action: 'emit',
|
|
43
|
-
output: {
|
|
44
|
-
hookSpecificOutput: {
|
|
45
|
-
hookEventName: 'PreToolUse',
|
|
46
|
-
permissionDecision: 'ask',
|
|
47
|
-
permissionDecisionReason: block,
|
|
48
|
-
additionalContext: block,
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
action: 'emit',
|
|
55
|
-
output: {
|
|
56
|
-
hookSpecificOutput: {
|
|
57
|
-
hookEventName: 'PreToolUse',
|
|
58
|
-
permissionDecision: 'allow',
|
|
59
|
-
additionalContext: block,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
export function formatEscalation(ctx) {
|
|
65
|
-
const lines = [];
|
|
66
|
-
lines.push(`vibe-splain: ${ctx.blastRadius} blast radius — ${ctx.targetFile} (gravity ${ctx.gravity}).`);
|
|
67
|
-
if (ctx.dependentCount > 0) {
|
|
68
|
-
const shown = ctx.dependents.slice(0, 8);
|
|
69
|
-
const more = ctx.dependentCount - shown.length;
|
|
70
|
-
lines.push(`${ctx.dependentCount} file(s) depend on it:`);
|
|
71
|
-
for (const d of shown)
|
|
72
|
-
lines.push(` - ${d}`);
|
|
73
|
-
if (more > 0)
|
|
74
|
-
lines.push(` … (+${more} more)`);
|
|
75
|
-
}
|
|
76
|
-
for (const w of ctx.riskWarnings)
|
|
77
|
-
lines.push(`[${w.level}] ${w.message}`);
|
|
78
|
-
lines.push(ctx.smallestSafeChange.summary);
|
|
79
|
-
return lines.join('\n');
|
|
80
|
-
}
|
|
81
|
-
//# sourceMappingURL=preToolUse.js.map
|
package/dist/hook-entry.d.ts
DELETED
package/dist/hook-entry.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Lean entrypoint for the PreToolUse hook. Bundled separately from the main CLI
|
|
3
|
-
// so it pulls in ONLY the gate-index reader + decision logic — no Tree-Sitter
|
|
4
|
-
// WASM, no MCP SDK, no chokidar. Keeps per-edit startup near the bare-Node floor
|
|
5
|
-
// (~40–60ms) instead of loading the full scanner bundle (~260ms+).
|
|
6
|
-
import { hookPreToolUseCommand } from './commands/hook.js';
|
|
7
|
-
hookPreToolUseCommand();
|
|
8
|
-
//# sourceMappingURL=hook-entry.js.map
|
package/dist/hook.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function findProjectRoot(start: string | undefined): string | null;
|
package/dist/hook.js
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// dist/hook.js
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
-
import { dirname, join } from "path";
|
|
5
|
-
import { tmpdir } from "os";
|
|
6
|
-
|
|
7
|
-
// ../brain/dist/network/gateIndex.js
|
|
8
|
-
function isDenoisedImporter(relPath) {
|
|
9
|
-
if (!relPath)
|
|
10
|
-
return false;
|
|
11
|
-
const norm = relPath.replace(/\\/g, "/");
|
|
12
|
-
const segs = norm.split("/");
|
|
13
|
-
const excludedDirs = /* @__PURE__ */ new Set([
|
|
14
|
-
"node_modules",
|
|
15
|
-
"vendor",
|
|
16
|
-
"vendored",
|
|
17
|
-
"site-packages",
|
|
18
|
-
"third_party",
|
|
19
|
-
"third-party",
|
|
20
|
-
".yarn",
|
|
21
|
-
"bower_components",
|
|
22
|
-
"venv",
|
|
23
|
-
".venv",
|
|
24
|
-
"env",
|
|
25
|
-
"virtualenv",
|
|
26
|
-
".git",
|
|
27
|
-
".vibe-splainer",
|
|
28
|
-
"dist",
|
|
29
|
-
"build",
|
|
30
|
-
"out",
|
|
31
|
-
"target",
|
|
32
|
-
".next",
|
|
33
|
-
".nuxt",
|
|
34
|
-
".docusaurus",
|
|
35
|
-
"coverage",
|
|
36
|
-
".nyc_output",
|
|
37
|
-
".cache"
|
|
38
|
-
]);
|
|
39
|
-
for (const s of segs) {
|
|
40
|
-
const sLower = s.toLowerCase();
|
|
41
|
-
if (excludedDirs.has(sLower)) {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
if (sLower.endsWith(".venv")) {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const fileName = segs[segs.length - 1];
|
|
49
|
-
const fileNameLower = fileName.toLowerCase();
|
|
50
|
-
if (/\.min\.[a-z]+$/.test(fileNameLower) || fileNameLower.includes(".min.")) {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
if (/\.generated\./.test(fileNameLower) || fileNameLower.includes("__generated__")) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
if (fileNameLower.endsWith(".lock")) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
if (norm.startsWith("virtual:") || norm.startsWith("__virtual:") || norm.startsWith("webpack:")) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ../brain/dist/network/escalation.js
|
|
66
|
-
var SECURITY_PATH = /\b(auth|credential|secret|token|webhook|payment|password|oauth|session)\b/i;
|
|
67
|
-
var SENSITIVE_EFFECTS = /* @__PURE__ */ new Set([
|
|
68
|
-
"database_write",
|
|
69
|
-
"server_action",
|
|
70
|
-
"trpc_mutation",
|
|
71
|
-
"email_send",
|
|
72
|
-
"external_api_call"
|
|
73
|
-
]);
|
|
74
|
-
var NOTABLE_RISKS = /* @__PURE__ */ new Set([
|
|
75
|
-
"state_machine",
|
|
76
|
-
"mutation_orchestration",
|
|
77
|
-
"side_effect_coupling",
|
|
78
|
-
"async_race_risk",
|
|
79
|
-
"registry_bottleneck",
|
|
80
|
-
"error_swallowing"
|
|
81
|
-
]);
|
|
82
|
-
function buildEscalationContext(targetFile, gateIndex) {
|
|
83
|
-
const file = lookupFile(targetFile, gateIndex);
|
|
84
|
-
if (!file)
|
|
85
|
-
return null;
|
|
86
|
-
const rel = file.relativePath;
|
|
87
|
-
const gravity = file.gravity;
|
|
88
|
-
const dependents = file.dependents;
|
|
89
|
-
const dependentsCount = dependents.length;
|
|
90
|
-
const sideEffects = file.sideEffects;
|
|
91
|
-
const riskTypes = file.riskTypes;
|
|
92
|
-
const severity = file.severity;
|
|
93
|
-
const gravity_tier = gravity > 70 ? "high" : gravity > 40 ? "medium" : "low";
|
|
94
|
-
const raw_substance_tier = file.hasBehavioralSubstance && dependentsCount >= 10 ? "high" : file.hasBehavioralSubstance && dependentsCount >= 4 ? "medium" : "low";
|
|
95
|
-
const tierOrder = { low: 1, medium: 2, high: 3 };
|
|
96
|
-
let blastRadius = tierOrder[gravity_tier] >= tierOrder[raw_substance_tier] ? gravity_tier : raw_substance_tier;
|
|
97
|
-
const isGeneratedOrVendored = !isDenoisedImporter(targetFile) || file.demoteReason !== null && file.demoteReason !== "no inbound references from application code";
|
|
98
|
-
if (isGeneratedOrVendored) {
|
|
99
|
-
blastRadius = "low";
|
|
100
|
-
}
|
|
101
|
-
const riskWarnings = buildRiskWarnings(rel, gravity, dependentsCount, severity, sideEffects, riskTypes);
|
|
102
|
-
return {
|
|
103
|
-
targetFile: rel,
|
|
104
|
-
gravity,
|
|
105
|
-
blastRadius,
|
|
106
|
-
dependents,
|
|
107
|
-
dependentCount: dependentsCount,
|
|
108
|
-
fanIn: file.fanIn,
|
|
109
|
-
fanOut: file.fanOut,
|
|
110
|
-
centrality: file.centrality,
|
|
111
|
-
severity,
|
|
112
|
-
sideEffects,
|
|
113
|
-
riskTypes,
|
|
114
|
-
riskWarnings,
|
|
115
|
-
smallestSafeChange: buildSafeChangePolicy(blastRadius)
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function lookupFile(targetFile, gateIndex) {
|
|
119
|
-
const files = gateIndex?.files;
|
|
120
|
-
if (!files)
|
|
121
|
-
return null;
|
|
122
|
-
const norm = targetFile.replace(/\\/g, "/");
|
|
123
|
-
if (files[norm])
|
|
124
|
-
return files[norm];
|
|
125
|
-
const suffixHits = [];
|
|
126
|
-
for (const [key, rec] of Object.entries(files)) {
|
|
127
|
-
if (norm === key || norm.endsWith("/" + key) || key.endsWith("/" + norm)) {
|
|
128
|
-
suffixHits.push(rec);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return suffixHits.length === 1 ? suffixHits[0] : null;
|
|
132
|
-
}
|
|
133
|
-
function buildRiskWarnings(rel, gravity, dependentCount, severity, sideEffects, riskTypes) {
|
|
134
|
-
const warnings = [];
|
|
135
|
-
if (gravity > 70) {
|
|
136
|
-
warnings.push({
|
|
137
|
-
id: `rw_${rel}_blast_radius`,
|
|
138
|
-
level: "critical",
|
|
139
|
-
message: `Central file \u2014 editing it has a large blast radius. ${dependentCount} file(s) depend on it.`,
|
|
140
|
-
reason: `Gravity ${gravity}/100; fan-in ${dependentCount}.`
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
if (severity >= 4) {
|
|
144
|
-
warnings.push({
|
|
145
|
-
id: `rw_${rel}_severity`,
|
|
146
|
-
level: "warning",
|
|
147
|
-
message: "High-severity smells already present here. Avoid making them worse.",
|
|
148
|
-
reason: `Canonical severity ${severity}/5.`
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
if (SECURITY_PATH.test(rel)) {
|
|
152
|
-
warnings.push({
|
|
153
|
-
id: `rw_${rel}_security_path`,
|
|
154
|
-
level: "critical",
|
|
155
|
-
message: "Security-sensitive file (auth/credential/webhook/payment). Do not alter auth or credential handling unless that is the explicit task.",
|
|
156
|
-
reason: `Path matches security-sensitive pattern.`
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
const sensitive = sideEffects.filter((e) => SENSITIVE_EFFECTS.has(e));
|
|
160
|
-
if (sensitive.length > 0) {
|
|
161
|
-
warnings.push({
|
|
162
|
-
id: `rw_${rel}_side_effects`,
|
|
163
|
-
level: "warning",
|
|
164
|
-
message: `This file performs side effects (${sensitive.join(", ")}). Preserve existing behavior; do not drop or reorder them.`,
|
|
165
|
-
reason: `Side-effect profile: ${sensitive.join(", ")}.`
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
const notable = riskTypes.filter((r) => NOTABLE_RISKS.has(r));
|
|
169
|
-
if (notable.length > 0) {
|
|
170
|
-
warnings.push({
|
|
171
|
-
id: `rw_${rel}_risk_types`,
|
|
172
|
-
level: "info",
|
|
173
|
-
message: `Structural risk patterns detected (${notable.join(", ")}). Trace the affected paths before editing.`,
|
|
174
|
-
reason: `Risk types: ${notable.join(", ")}.`
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
return warnings;
|
|
178
|
-
}
|
|
179
|
-
function buildSafeChangePolicy(blastRadius) {
|
|
180
|
-
const rules = [
|
|
181
|
-
"Locate the exact failure site before editing.",
|
|
182
|
-
"Make the smallest localized change that addresses the task.",
|
|
183
|
-
"Do not perform general cleanup or refactoring.",
|
|
184
|
-
"Do not modify unrelated files, credentials, or auth configuration."
|
|
185
|
-
];
|
|
186
|
-
if (blastRadius === "high") {
|
|
187
|
-
rules.push("This file is load-bearing: verify each dependent still type-checks against the changed surface.");
|
|
188
|
-
}
|
|
189
|
-
return {
|
|
190
|
-
summary: "Make the smallest localized change that addresses the requested task. Do not modify unrelated files, credentials, auth configuration, or neighboring modules unless the task explicitly requires it.",
|
|
191
|
-
rules,
|
|
192
|
-
forbiddenEditClasses: ["credentials", "auth_configuration", "global_refactoring"]
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// dist/hook/preToolUse.js
|
|
197
|
-
var DEFER = { action: "defer" };
|
|
198
|
-
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
199
|
-
function decidePreToolUse(input, gateIndex, sessionState) {
|
|
200
|
-
if (!EDIT_TOOLS.has(input.tool_name ?? ""))
|
|
201
|
-
return DEFER;
|
|
202
|
-
if (!gateIndex) {
|
|
203
|
-
if (sessionState && !sessionState.warningShown) {
|
|
204
|
-
const warnMsg = "vibe-splain: .vibe-splainer/gate.json is missing. Please run 'vibe-splain scan' to generate the scan architecture and enable tool guarding.";
|
|
205
|
-
return {
|
|
206
|
-
action: "emit",
|
|
207
|
-
output: {
|
|
208
|
-
hookSpecificOutput: {
|
|
209
|
-
hookEventName: "PreToolUse",
|
|
210
|
-
permissionDecision: "allow",
|
|
211
|
-
additionalContext: warnMsg,
|
|
212
|
-
systemMessage: warnMsg
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
return DEFER;
|
|
218
|
-
}
|
|
219
|
-
const filePath = input.tool_input?.file_path;
|
|
220
|
-
if (!filePath)
|
|
221
|
-
return DEFER;
|
|
222
|
-
const ctx = buildEscalationContext(filePath, gateIndex);
|
|
223
|
-
if (!ctx || ctx.blastRadius === "low")
|
|
224
|
-
return DEFER;
|
|
225
|
-
const block = formatEscalation(ctx);
|
|
226
|
-
if (ctx.blastRadius === "high") {
|
|
227
|
-
return {
|
|
228
|
-
action: "emit",
|
|
229
|
-
output: {
|
|
230
|
-
hookSpecificOutput: {
|
|
231
|
-
hookEventName: "PreToolUse",
|
|
232
|
-
permissionDecision: "ask",
|
|
233
|
-
permissionDecisionReason: block,
|
|
234
|
-
additionalContext: block
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
return {
|
|
240
|
-
action: "emit",
|
|
241
|
-
output: {
|
|
242
|
-
hookSpecificOutput: {
|
|
243
|
-
hookEventName: "PreToolUse",
|
|
244
|
-
permissionDecision: "allow",
|
|
245
|
-
additionalContext: block
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
function formatEscalation(ctx) {
|
|
251
|
-
const lines = [];
|
|
252
|
-
lines.push(`vibe-splain: ${ctx.blastRadius} blast radius \u2014 ${ctx.targetFile} (gravity ${ctx.gravity}).`);
|
|
253
|
-
if (ctx.dependentCount > 0) {
|
|
254
|
-
const shown = ctx.dependents.slice(0, 8);
|
|
255
|
-
const more = ctx.dependentCount - shown.length;
|
|
256
|
-
lines.push(`${ctx.dependentCount} file(s) depend on it:`);
|
|
257
|
-
for (const d of shown)
|
|
258
|
-
lines.push(` - ${d}`);
|
|
259
|
-
if (more > 0)
|
|
260
|
-
lines.push(` \u2026 (+${more} more)`);
|
|
261
|
-
}
|
|
262
|
-
for (const w of ctx.riskWarnings)
|
|
263
|
-
lines.push(`[${w.level}] ${w.message}`);
|
|
264
|
-
lines.push(ctx.smallestSafeChange.summary);
|
|
265
|
-
return lines.join("\n");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// dist/hook.js
|
|
269
|
-
function findProjectRoot(start) {
|
|
270
|
-
let dir = start || process.cwd();
|
|
271
|
-
for (let i = 0; i < 64; i++) {
|
|
272
|
-
if (existsSync(join(dir, ".vibe-splainer")))
|
|
273
|
-
return dir;
|
|
274
|
-
const parent = dirname(dir);
|
|
275
|
-
if (parent === dir)
|
|
276
|
-
break;
|
|
277
|
-
dir = parent;
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
function readStdin() {
|
|
282
|
-
return new Promise((resolve) => {
|
|
283
|
-
let data = "";
|
|
284
|
-
if (process.stdin.isTTY) {
|
|
285
|
-
resolve("");
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
process.stdin.setEncoding("utf8");
|
|
289
|
-
process.stdin.on("data", (chunk) => {
|
|
290
|
-
data += chunk;
|
|
291
|
-
});
|
|
292
|
-
process.stdin.on("end", () => resolve(data));
|
|
293
|
-
process.stdin.on("error", () => resolve(data));
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
async function main() {
|
|
297
|
-
const rawStdin = await readStdin();
|
|
298
|
-
let input;
|
|
299
|
-
try {
|
|
300
|
-
input = JSON.parse(rawStdin);
|
|
301
|
-
} catch {
|
|
302
|
-
process.exit(0);
|
|
303
|
-
}
|
|
304
|
-
const root = findProjectRoot(input.cwd);
|
|
305
|
-
const gatePath = root ? join(root, ".vibe-splainer", "gate.json") : null;
|
|
306
|
-
const gateExists = gatePath ? existsSync(gatePath) : false;
|
|
307
|
-
let result;
|
|
308
|
-
if (!gateExists) {
|
|
309
|
-
const sessionId = input.session_id || "default";
|
|
310
|
-
const warnFile = join(tmpdir(), `vibe-splain-warn-${sessionId}`);
|
|
311
|
-
const warningShown = existsSync(warnFile);
|
|
312
|
-
result = decidePreToolUse(input, null, { warningShown });
|
|
313
|
-
if (result.action === "emit") {
|
|
314
|
-
try {
|
|
315
|
-
writeFileSync(warnFile, "1");
|
|
316
|
-
} catch {
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
} else {
|
|
320
|
-
let gateIndex = null;
|
|
321
|
-
try {
|
|
322
|
-
gateIndex = JSON.parse(readFileSync(gatePath, "utf8"));
|
|
323
|
-
} catch {
|
|
324
|
-
}
|
|
325
|
-
result = decidePreToolUse(input, gateIndex);
|
|
326
|
-
}
|
|
327
|
-
if (result.action === "emit") {
|
|
328
|
-
process.stdout.write(JSON.stringify(result.output));
|
|
329
|
-
}
|
|
330
|
-
process.exit(0);
|
|
331
|
-
}
|
|
332
|
-
main().catch(() => {
|
|
333
|
-
process.exit(0);
|
|
334
|
-
});
|
|
335
|
-
export {
|
|
336
|
-
findProjectRoot
|
|
337
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export interface BudgetExceededResult {
|
|
2
|
-
pointerId: string;
|
|
3
|
-
contentHash: string;
|
|
4
|
-
sizeBytes: number;
|
|
5
|
-
summary: string;
|
|
6
|
-
hydrators: string[];
|
|
7
|
-
}
|
|
8
|
-
export declare function applyBudgetGuard(projectRoot: string, scanId: string, artifactName: string, output: unknown): Promise<unknown>;
|
|
9
|
-
/** Verify a pointer exists, is unexpired, and its blob hash matches */
|
|
10
|
-
export declare function hydratePointer(projectRoot: string, pointerId: string): Promise<{
|
|
11
|
-
content: Buffer;
|
|
12
|
-
row: import('../store/PointerStore.js').PointerRow;
|
|
13
|
-
}>;
|
package/dist/mcp/BudgetGuard.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { BlobStore } from '../store/BlobStore.js';
|
|
2
|
-
import { PointerStore } from '../store/PointerStore.js';
|
|
3
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
-
/** ~2000 tokens ≈ 8000 chars */
|
|
5
|
-
const BUDGET_CHARS = 8000;
|
|
6
|
-
export async function applyBudgetGuard(projectRoot, scanId, artifactName, output) {
|
|
7
|
-
const serialized = JSON.stringify(output, null, 2);
|
|
8
|
-
if (serialized.length <= BUDGET_CHARS)
|
|
9
|
-
return output;
|
|
10
|
-
const blobStore = new BlobStore(projectRoot);
|
|
11
|
-
const pointerStore = PointerStore.open(projectRoot);
|
|
12
|
-
const { contentHash, blobPath } = await blobStore.writeAtomic(serialized);
|
|
13
|
-
const pointerId = `ptr_${uuidv4().replace(/-/g, '').slice(0, 16)}`;
|
|
14
|
-
await pointerStore.insertPointer({
|
|
15
|
-
pointerId,
|
|
16
|
-
scanId,
|
|
17
|
-
artifactName,
|
|
18
|
-
contentHash,
|
|
19
|
-
blobPath,
|
|
20
|
-
schemaVersion: '1.0.0',
|
|
21
|
-
createdAt: Date.now(),
|
|
22
|
-
expiresAt: null,
|
|
23
|
-
});
|
|
24
|
-
const result = {
|
|
25
|
-
pointerId,
|
|
26
|
-
contentHash,
|
|
27
|
-
sizeBytes: serialized.length,
|
|
28
|
-
summary: `Output exceeded context budget (${serialized.length} chars). Written to artifact blob.`,
|
|
29
|
-
hydrators: ['get_evidence_slice', 'get_start_here', 'get_project_summary'],
|
|
30
|
-
};
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
33
|
-
/** Verify a pointer exists, is unexpired, and its blob hash matches */
|
|
34
|
-
export async function hydratePointer(projectRoot, pointerId) {
|
|
35
|
-
const pointerStore = PointerStore.open(projectRoot);
|
|
36
|
-
const row = pointerStore.getPointer(pointerId);
|
|
37
|
-
if (!row) {
|
|
38
|
-
throw new Error(`ArtifactNotFound: pointer ${pointerId} does not exist`);
|
|
39
|
-
}
|
|
40
|
-
if (row.expiresAt !== null && row.expiresAt < Date.now()) {
|
|
41
|
-
throw new Error(`ArtifactCollectedError: pointer ${pointerId} has expired`);
|
|
42
|
-
}
|
|
43
|
-
const SUPPORTED_VERSIONS = ['1.0.0', '2.0.0'];
|
|
44
|
-
if (!SUPPORTED_VERSIONS.includes(row.schemaVersion)) {
|
|
45
|
-
throw new Error(`UnsupportedSchema: pointer ${pointerId} has schema version ${row.schemaVersion}`);
|
|
46
|
-
}
|
|
47
|
-
const blobStore = new BlobStore(projectRoot);
|
|
48
|
-
const content = await blobStore.readBlob(row.blobPath);
|
|
49
|
-
const valid = await blobStore.verifyIntegrity(row.blobPath, row.contentHash);
|
|
50
|
-
if (!valid) {
|
|
51
|
-
throw new Error(`IntegrityError: blob for pointer ${pointerId} failed hash verification`);
|
|
52
|
-
}
|
|
53
|
-
return { content, row };
|
|
54
|
-
}
|
|
55
|
-
//# sourceMappingURL=BudgetGuard.js.map
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { WorkOrderRow, ProofDescriptor } from '../store/PointerStore.js';
|
|
2
|
-
export interface ScopePolicy {
|
|
3
|
-
workOrderId: string;
|
|
4
|
-
allowedFiles: string[];
|
|
5
|
-
allowedGlobs: string[];
|
|
6
|
-
deniedGlobs: string[];
|
|
7
|
-
requiredProof: ProofDescriptor[];
|
|
8
|
-
}
|
|
9
|
-
export declare class ScopeViolation extends Error {
|
|
10
|
-
readonly path: string;
|
|
11
|
-
readonly workOrderId: string;
|
|
12
|
-
constructor(path: string, workOrderId: string, reason: string);
|
|
13
|
-
}
|
|
14
|
-
export declare const SessionScope: {
|
|
15
|
-
set(policy: ScopePolicy): void;
|
|
16
|
-
clear(): void;
|
|
17
|
-
get(): ScopePolicy | null;
|
|
18
|
-
/**
|
|
19
|
-
* Enforce scope for a file path.
|
|
20
|
-
* Throws ScopeViolation if:
|
|
21
|
-
* - a scope is active AND the path is not allowed
|
|
22
|
-
* If no scope is active, all paths are permitted.
|
|
23
|
-
*/
|
|
24
|
-
enforce(filePath: string): void;
|
|
25
|
-
fromWorkOrderRow(row: WorkOrderRow): ScopePolicy;
|
|
26
|
-
};
|
package/dist/mcp/SessionScope.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { minimatch } from 'minimatch';
|
|
2
|
-
export class ScopeViolation extends Error {
|
|
3
|
-
path;
|
|
4
|
-
workOrderId;
|
|
5
|
-
constructor(path, workOrderId, reason) {
|
|
6
|
-
super(`ScopeViolation [${workOrderId}]: ${reason} — path: ${path}`);
|
|
7
|
-
this.path = path;
|
|
8
|
-
this.workOrderId = workOrderId;
|
|
9
|
-
this.name = 'ScopeViolation';
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
let activeScope = null;
|
|
13
|
-
export const SessionScope = {
|
|
14
|
-
set(policy) {
|
|
15
|
-
activeScope = policy;
|
|
16
|
-
},
|
|
17
|
-
clear() {
|
|
18
|
-
activeScope = null;
|
|
19
|
-
},
|
|
20
|
-
get() {
|
|
21
|
-
return activeScope;
|
|
22
|
-
},
|
|
23
|
-
/**
|
|
24
|
-
* Enforce scope for a file path.
|
|
25
|
-
* Throws ScopeViolation if:
|
|
26
|
-
* - a scope is active AND the path is not allowed
|
|
27
|
-
* If no scope is active, all paths are permitted.
|
|
28
|
-
*/
|
|
29
|
-
enforce(filePath) {
|
|
30
|
-
if (!activeScope)
|
|
31
|
-
return;
|
|
32
|
-
const { workOrderId, allowedFiles, allowedGlobs, deniedGlobs } = activeScope;
|
|
33
|
-
// Explicit file list match (exact suffix or relative path)
|
|
34
|
-
const inAllowedFiles = allowedFiles.some(f => filePath === f || filePath.endsWith('/' + f) || filePath.endsWith(f));
|
|
35
|
-
// Glob match
|
|
36
|
-
const inAllowedGlobs = allowedGlobs.some(g => minimatch(filePath, g, { matchBase: true }));
|
|
37
|
-
if (!inAllowedFiles && !inAllowedGlobs) {
|
|
38
|
-
throw new ScopeViolation(filePath, workOrderId, 'path not in allowedFiles or allowedGlobs');
|
|
39
|
-
}
|
|
40
|
-
// Deny globs have priority over allow
|
|
41
|
-
const isDenied = deniedGlobs.some(g => minimatch(filePath, g, { matchBase: true }));
|
|
42
|
-
if (isDenied) {
|
|
43
|
-
throw new ScopeViolation(filePath, workOrderId, 'path matches deniedGlobs');
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
fromWorkOrderRow(row) {
|
|
47
|
-
return {
|
|
48
|
-
workOrderId: row.workOrderId,
|
|
49
|
-
allowedFiles: JSON.parse(row.allowedFiles),
|
|
50
|
-
allowedGlobs: JSON.parse(row.allowedGlobs),
|
|
51
|
-
deniedGlobs: JSON.parse(row.deniedGlobs),
|
|
52
|
-
requiredProof: JSON.parse(row.requiredProof),
|
|
53
|
-
};
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
//# sourceMappingURL=SessionScope.js.map
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export declare class StalePatchError extends Error {
|
|
2
|
-
readonly filePath: string;
|
|
3
|
-
readonly expectedHash: string;
|
|
4
|
-
readonly actualHash: string;
|
|
5
|
-
constructor(filePath: string, expectedHash: string, actualHash: string);
|
|
6
|
-
}
|
|
7
|
-
export declare const applyPatchTool: {
|
|
8
|
-
name: string;
|
|
9
|
-
description: string;
|
|
10
|
-
inputSchema: {
|
|
11
|
-
type: "object";
|
|
12
|
-
properties: {
|
|
13
|
-
projectRoot: {
|
|
14
|
-
type: string;
|
|
15
|
-
description: string;
|
|
16
|
-
};
|
|
17
|
-
filePath: {
|
|
18
|
-
type: string;
|
|
19
|
-
description: string;
|
|
20
|
-
};
|
|
21
|
-
newContent: {
|
|
22
|
-
type: string;
|
|
23
|
-
description: string;
|
|
24
|
-
};
|
|
25
|
-
expectedPrePatchHash: {
|
|
26
|
-
type: string;
|
|
27
|
-
description: string;
|
|
28
|
-
};
|
|
29
|
-
scanId: {
|
|
30
|
-
type: string;
|
|
31
|
-
description: string;
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
required: string[];
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
export declare function handleApplyPatch(args: Record<string, unknown>): Promise<unknown>;
|