vibe-splain 3.4.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +26 -20
  2. package/dist/commands/hook.d.ts +9 -0
  3. package/dist/commands/hook.js +84 -0
  4. package/dist/commands/install.d.ts +5 -0
  5. package/dist/commands/install.js +58 -9
  6. package/dist/export/ExportOrchestrator.js +8 -6
  7. package/dist/export/renderers/AgentMarkdownRenderer.d.ts +1 -2
  8. package/dist/export/renderers/AgentMarkdownRenderer.js +1 -17
  9. package/dist/hook/preToolUse.d.ts +30 -0
  10. package/dist/hook/preToolUse.js +81 -0
  11. package/dist/hook.d.ts +1 -0
  12. package/dist/hook.js +337 -0
  13. package/dist/index.js +2336 -2825
  14. package/dist/mcp/server.js +1 -25
  15. package/dist/mcp/tools/hydration/get_evidence_slice.js +1 -3
  16. package/dist/mcp/tools/scan_project.d.ts +15 -0
  17. package/dist/mcp/tools/scan_project.js +16 -13
  18. package/dist/ui/index.html +3 -3
  19. package/package.json +2 -2
  20. package/dist/commands/bundle.d.ts +0 -4
  21. package/dist/commands/bundle.js +0 -68
  22. package/dist/commands/export.d.ts +0 -1
  23. package/dist/commands/export.js +0 -19
  24. package/dist/commands/gc.d.ts +0 -3
  25. package/dist/commands/gc.js +0 -59
  26. package/dist/commands/importBundle.d.ts +0 -4
  27. package/dist/commands/importBundle.js +0 -80
  28. package/dist/export/renderers/DeltaRenderer.d.ts +0 -6
  29. package/dist/export/renderers/DeltaRenderer.js +0 -22
  30. package/dist/mcp/tools/apply_patch.d.ts +0 -37
  31. package/dist/mcp/tools/apply_patch.js +0 -103
  32. package/dist/mcp/tools/get_call_chain.d.ts +0 -42
  33. package/dist/mcp/tools/get_call_chain.js +0 -50
  34. package/dist/mcp/tools/set_session_scope.d.ts +0 -19
  35. package/dist/mcp/tools/set_session_scope.js +0 -40
  36. package/dist/mcp/tools/submit_receipt.d.ts +0 -68
  37. package/dist/mcp/tools/submit_receipt.js +0 -94
  38. package/dist/mcp/tools/work_orders.d.ts +0 -79
  39. package/dist/mcp/tools/work_orders.js +0 -126
  40. package/dist/mcp/tools/yield_for_scope_expansion.d.ts +0 -29
  41. package/dist/mcp/tools/yield_for_scope_expansion.js +0 -59
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <strong>◈ VIBE-SPLAIN</strong>
2
+ <strong>◈ vibe-splain</strong>
3
3
  <br />
4
4
  <em>Map architectural DNA and behavioral call-chains in complex codebases.</em>
5
5
  </p>
@@ -14,13 +14,13 @@
14
14
 
15
15
  ---
16
16
 
17
- VIBE-SPLAIN is a high-fidelity **static analysis engine** and MCP server. It uses [Tree-Sitter](https://tree-sitter.github.io/tree-sitter/) to extract the structural and behavioral patterns of a codebase—identifying high-gravity components, mapping semantic actions, and tracing call-chains between entrypoints and side effects.
17
+ vibe-splain is a high-fidelity **static analysis engine** and MCP server. It uses [Tree-Sitter](https://tree-sitter.github.io/tree-sitter/) to extract the structural and behavioral patterns of a codebase—identifying high-gravity components, mapping semantic actions, and tracing call-chains between entrypoints and side effects.
18
18
 
19
- While VIBE-SPLAIN is built on a language-agnostic foundation, the current toolset is **highly optimized for TypeScript and JavaScript** (especially Next.js, Prisma, and tRPC environments).
19
+ While vibe-splain is built on a language-agnostic foundation, the current toolset is **highly optimized for TypeScript and JavaScript** (especially Next.js, Prisma, and tRPC environments).
20
20
 
21
21
  **Zero LLM calls. Zero API keys. Pure static analysis.**
22
22
 
23
- Your coding agent does all the thinking — VIBE-SPLAIN just gives it the right data.
23
+ Your coding agent does all the thinking — vibe-splain just gives it the right data.
24
24
 
25
25
  ## Install
26
26
 
@@ -28,17 +28,17 @@ Your coding agent does all the thinking — VIBE-SPLAIN just gives it the right
28
28
  npx vibe-splain install
29
29
  ```
30
30
 
31
- That's it. This patches your coding agent's MCP config so it can call VIBE-SPLAIN's tools. Restart your agent.
31
+ That's it. This patches your coding agent's MCP config so it can call vibe-splain's tools. Restart your agent.
32
32
 
33
33
  ### Running the Analysis
34
34
 
35
- You don't need to write a complex prompt. VIBE-SPLAIN provides a built-in MCP Prompt called `build_dossier` that automatically tells your agent exactly what to do.
35
+ You don't need to write a complex prompt. vibe-splain provides a built-in MCP Prompt called `build_dossier` that automatically tells your agent exactly what to do.
36
36
 
37
37
  **In Claude Code / Gemini CLI:**
38
38
  Type `/prompt build_dossier` and press enter.
39
39
 
40
40
  **In Cursor / Windsurf:**
41
- Open the MCP panel or agent chat, select the `build_dossier` prompt from the VIBE-SPLAIN server, and run it.
41
+ Open the MCP panel or agent chat, select the `build_dossier` prompt from the vibe-splain server, and run it.
42
42
 
43
43
  Your agent will loop through the high-gravity files, analyze each one, and build an **Architectural Dossier** — a structured set of **Decision Cards** explaining the technical rationale of the code.
44
44
 
@@ -71,22 +71,27 @@ Your agent will loop through the high-gravity files, analyze each one, and build
71
71
  ```
72
72
 
73
73
  ### Three Levels of Analysis
74
-
75
74
  1. **Level 0 — Semantic Classification**: Maps files to architectural "pillars" (Auth, Payments, Database, etc.) using import-path heuristics and library signatures.
75
+ 2. **Level 1 — Structural Gravity**: Computes `gravity` (0–100) using Tree-Sitter AST analysis — PageRank centrality, fan-in, cyclomatic complexity, public surface, and nesting depth. High-gravity files are the load-bearing hubs of the codebase. (An optional domain-adapter extension point exists for projects that want to add product-specific `behavioralLift`, but no adapters ship with the core — every scan is pure static analysis.)
76
+ 3. **Level 2 — Behavioral Traceability**: Tree-Sitter powered call-graph analysis. It maps function-level dependencies and identifies **Critical Functions** (entrypoints, semantic actions, or high-outbound callers).
77
+
78
+ ### Claude Code PreToolUse Hook
76
79
 
77
- 2. **Level 1 — Cognitive Complexity**: Tree-Sitter AST analysis computes a complexity score per file based on link density, nesting depth, and mutation counts. Files scoring ≥ 15 are identified as **High-Gravity**.
80
+ `vibe-splain` integrates directly with Claude Code's hook system to prevent AI agents from accidentally breaking your codebase:
78
81
 
79
- 3. **Level 2 Behavioral Traceability**: Tree-Sitter powered call-graph analysis. It maps function-level dependencies and identifies **Critical Functions** (entrypoints, semantic actions, or high-outbound callers) so your agent can trace the exact ripple effect of a code change.
82
+ - **Ultra-Fast Local Gating**: Installs a standalone entrypoint (`dist/hook.js`) that runs in **< 15ms** with zero network calls, avoiding heavy WASM/tree-sitter load overhead.
83
+ - **Hybrid Blast-Radius Logic**: Combines static gravity and substance-based direct dependents to protect important files (e.g. dynamically-loaded execution nodes or central utility modules) even if they lack standard entrypoints.
84
+ - **Developer Friction Suppression**: Automatically demotes generated, minified, or vendored files to a `low` blast-radius, ensuring the agent is never blocked when editing build targets or lockfiles.
85
+ - **Warn-Once session behavior**: If `.vibe-splainer/gate.json` is missing, the hook notifies you exactly once per session to run a project scan, then gracefully gets out of the way.
80
86
 
81
87
  ## MCP Tools
82
88
 
83
- VIBE-SPLAIN exposes **8 tools** over MCP stdio:
89
+ vibe-splain exposes these core dossier tools over MCP stdio:
84
90
 
85
91
  | Tool | Purpose |
86
92
  |------|---------|
87
93
  | `scan_project` | **Call first.** Scans the codebase, returns high-gravity files grouped by pillar. Starts file watcher. |
88
94
  | `get_file_context` | Returns full source + import graph neighbors for a specific file. |
89
- | `get_call_chain` | **New.** Traces function-level call chains (upstream/downstream) to map behavior paths. |
90
95
  | `write_decision_card` | Persists a Decision Card (narrative + evidence + optional Mermaid diagram). |
91
96
  | `get_strategic_overview` | Returns dossier state without evidence snippets (saves tokens). |
92
97
  | `inspect_pillar` | Returns all Decision Cards for a pillar with full evidence. |
@@ -98,8 +103,7 @@ VIBE-SPLAIN exposes **8 tools** over MCP stdio:
98
103
  ```
99
104
  1. scan_project → get high-gravity files
100
105
  2. For each file: get_file_context → read source + neighbors
101
- 3. Trace: use get_call_chain to see what calls what
102
- 4. Synthesize: "WHY does this code exist?"
106
+ 3. Synthesize: "WHY does this code exist?"
103
107
  5. write_decision_card → persist the narrative
104
108
  6. Share the file:// UI link with the user
105
109
  ```
@@ -107,10 +111,10 @@ VIBE-SPLAIN exposes **8 tools** over MCP stdio:
107
111
  ## How It Works
108
112
 
109
113
  ### Deep Analysis Pipeline
110
- Unlike simple regex scanners, VIBE-SPLAIN runs a deterministic **13-stage pipeline**—from AST inventory and alias resolution to semantic classification and function-level scoring—to ensure every Decision Card is grounded in actual code paths.
114
+ Unlike simple regex scanners, vibe-splain runs a deterministic **13-stage pipeline**—from AST inventory and alias resolution to semantic classification and function-level scoring—to ensure every Decision Card is grounded in actual code paths.
111
115
 
112
116
  ### Semantic Rulesets
113
- VIBE-SPLAIN uses specialized rulesets to understand framework-specific semantics. Current optimizations include:
117
+ vibe-splain uses specialized rulesets to understand framework-specific semantics. Current optimizations include:
114
118
  - **Next.js**: Server Actions, `cookies()`, `headers()`, and App Router conventions.
115
119
  - **Database**: Prisma model mutations and raw query patterns.
116
120
  - **API**: tRPC procedure calls (`mutate`/`query`) and standard `fetch`/`axios` patterns.
@@ -137,12 +141,14 @@ The UI features:
137
141
 
138
142
  ```
139
143
  packages/
140
- ├── brain/ # @vibe-splain/brain — analysis engine
144
+ ├── brain/ # @vibe-splain/brain — repo-agnostic static analysis core
141
145
  │ └── src/
142
- │ ├── scanner.ts # Tree-Sitter AST analysis (L0 + L1 + L2)
146
+ │ ├── pipeline/
147
+ │ │ └── adapters/ # Optional domain-adapter extension point (none bundled)
148
+ │ ├── scanner.ts # Tree-Sitter AST analysis (static analysis, graph building)
143
149
  │ ├── dossier.ts # Atomic persistence + UI regeneration
144
150
  │ ├── graph.ts # Import graph read/write
145
- │ └── watcher.ts # Chokidar file watcher
151
+ │ └── network/ # Deterministic PreToolUse gate (escalation + gate index)
146
152
  ├── cli/ # vibe-splain — MCP server + CLI
147
153
  │ └── src/
148
154
  │ ├── index.ts # #!/usr/bin/env node entry
@@ -161,7 +167,7 @@ packages/
161
167
 
162
168
  ### Key Design Decisions
163
169
 
164
- - **No LLM calls**: VIBE-SPLAIN is a pure static analysis tool. The coding agent provides all synthesis.
170
+ - **No LLM calls**: vibe-splain is a pure static analysis tool. The coding agent provides all synthesis.
165
171
  - **`async-mutex`**: All dossier writes are guarded by a mutex with atomic tmp+rename.
166
172
  - **`startOnLoad: false`**: Mermaid is initialized manually — never auto-scans the DOM.
167
173
  - **`base: './'`**: Vite builds with relative paths so the UI works from `file://` URLs.
@@ -0,0 +1,9 @@
1
+ export interface HookCommandResult {
2
+ /** JSON to print to stdout, or null to print nothing (defer / exit 0). */
3
+ stdout: string | null;
4
+ }
5
+ export declare function findProjectRoot(start: string | undefined): string | null;
6
+ /** Pure-ish core: raw stdin string in, hook decision (as a string or null) out. */
7
+ export declare function runHookCommand(rawStdin: string): Promise<HookCommandResult>;
8
+ /** CLI entrypoint: read all of stdin, run the gate, emit the result. */
9
+ export declare function hookPreToolUseCommand(): Promise<void>;
@@ -0,0 +1,84 @@
1
+ // `vibe-splain hook pretooluse` — the deterministic PreToolUse gate, wired to stdio.
2
+ //
3
+ // Claude Code pipes a JSON describing the tool call about to run onto stdin; we
4
+ // decide whether to escalate and, if so, write hook JSON to stdout. On a safe
5
+ // edit (or any non-edit tool, unscanned repo, etc.) we print nothing and exit 0,
6
+ // deferring to normal permission flow.
7
+ //
8
+ // NOTE on the console.log ban: that rule protects the MCP *server's* stdout.
9
+ // This is a separate, short-lived process whose stdout IS the hook protocol —
10
+ // writing JSON here is correct. Diagnostics still go to stderr.
11
+ import { existsSync, writeFileSync, readFileSync } from 'fs';
12
+ import { dirname, join } from 'path';
13
+ import { tmpdir } from 'os';
14
+ import { decidePreToolUse } from '../hook/preToolUse.js';
15
+ // Walk up from `start` looking for the directory that holds a `.vibe-splainer` scan.
16
+ // Returns null if none is found — an unscanned repo means the gate is inert.
17
+ export function findProjectRoot(start) {
18
+ let dir = start || process.cwd();
19
+ // Bound the walk so a stray cwd can't traverse the whole filesystem.
20
+ for (let i = 0; i < 64; i++) {
21
+ if (existsSync(join(dir, '.vibe-splainer', 'analysis.json')))
22
+ return dir;
23
+ const parent = dirname(dir);
24
+ if (parent === dir)
25
+ break;
26
+ dir = parent;
27
+ }
28
+ return null;
29
+ }
30
+ /** Pure-ish core: raw stdin string in, hook decision (as a string or null) out. */
31
+ export async function runHookCommand(rawStdin) {
32
+ let input;
33
+ try {
34
+ input = JSON.parse(rawStdin);
35
+ }
36
+ catch {
37
+ return { stdout: null }; // malformed input → never block the agent
38
+ }
39
+ const root = findProjectRoot(input.cwd);
40
+ const gatePath = root ? join(root, '.vibe-splainer', 'gate.json') : null;
41
+ const gateExists = gatePath ? existsSync(gatePath) : false;
42
+ let gateIndex = null;
43
+ if (gateExists) {
44
+ try {
45
+ gateIndex = JSON.parse(readFileSync(gatePath, 'utf8'));
46
+ }
47
+ catch { }
48
+ }
49
+ const sessionId = input.session_id || 'default';
50
+ const warnFile = join(tmpdir(), `vibe-splain-warn-${sessionId}`);
51
+ const warningShown = existsSync(warnFile);
52
+ const result = decidePreToolUse(input, gateIndex, { warningShown });
53
+ if (result.action === 'defer')
54
+ return { stdout: null };
55
+ if (!gateIndex && result.action === 'emit') {
56
+ try {
57
+ writeFileSync(warnFile, '1');
58
+ }
59
+ catch { }
60
+ }
61
+ return { stdout: JSON.stringify(result.output) };
62
+ }
63
+ /** CLI entrypoint: read all of stdin, run the gate, emit the result. */
64
+ export async function hookPreToolUseCommand() {
65
+ const raw = await readStdin();
66
+ const { stdout } = await runHookCommand(raw);
67
+ if (stdout)
68
+ process.stdout.write(stdout);
69
+ // No stdout on defer; exit code stays 0.
70
+ }
71
+ function readStdin() {
72
+ return new Promise((resolve) => {
73
+ let data = '';
74
+ if (process.stdin.isTTY) {
75
+ resolve('');
76
+ return;
77
+ }
78
+ process.stdin.setEncoding('utf8');
79
+ process.stdin.on('data', (chunk) => { data += chunk; });
80
+ process.stdin.on('end', () => resolve(data));
81
+ process.stdin.on('error', () => resolve(data));
82
+ });
83
+ }
84
+ //# sourceMappingURL=hook.js.map
@@ -1 +1,6 @@
1
+ /**
2
+ * Idempotently register the vibe-splain PreToolUse hook in a Claude Code settings object.
3
+ * Pure: takes a config, returns the same object mutated. Safe to run repeatedly.
4
+ */
5
+ export declare function addPreToolUseHook(config: Record<string, any>, hookPath?: string): Record<string, any>;
1
6
  export declare function installCommand(): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  import { readFile, writeFile } from 'fs/promises';
2
2
  import { existsSync } from 'fs';
3
- import { join } from 'path';
3
+ import { join, dirname } from 'path';
4
4
  import { homedir } from 'os';
5
+ import { fileURLToPath } from 'url';
5
6
  function expandPath(p) {
6
7
  if (p.startsWith('~')) {
7
8
  return join(homedir(), p.slice(1));
@@ -24,8 +25,48 @@ const MCP_ENTRY = {
24
25
  command: 'npx',
25
26
  args: ['-y', 'vibe-splain', 'serve'],
26
27
  };
28
+ // Claude Code PreToolUse hook: deterministically gate edits to high-blast-radius
29
+ // files. Hooks are a Claude Code CLI feature (settings.json) — not Desktop/Cursor.
30
+ const HOOK_MATCHER = 'Edit|Write|MultiEdit';
31
+ const DEFAULT_HOOK_COMMAND = 'npx -y vibe-splain hook pretooluse';
32
+ /**
33
+ * Idempotently register the vibe-splain PreToolUse hook in a Claude Code settings object.
34
+ * Pure: takes a config, returns the same object mutated. Safe to run repeatedly.
35
+ */
36
+ export function addPreToolUseHook(config, hookPath) {
37
+ if (!config.hooks)
38
+ config.hooks = {};
39
+ if (!Array.isArray(config.hooks.PreToolUse))
40
+ config.hooks.PreToolUse = [];
41
+ const hookCommand = hookPath ? `node "${hookPath}"` : DEFAULT_HOOK_COMMAND;
42
+ const already = config.hooks.PreToolUse.some((entry) => Array.isArray(entry?.hooks) &&
43
+ entry.hooks.some((h) => typeof h?.command === 'string' && (h.command.includes('vibe-splain hook pretooluse') || h.command.includes('hook.js'))));
44
+ if (already) {
45
+ // Update existing hook command to the resolved hook.js path
46
+ config.hooks.PreToolUse = config.hooks.PreToolUse.map((entry) => {
47
+ if (Array.isArray(entry?.hooks)) {
48
+ return {
49
+ ...entry,
50
+ hooks: entry.hooks.map((h) => {
51
+ if (typeof h?.command === 'string' && (h.command.includes('vibe-splain hook pretooluse') || h.command.includes('hook.js'))) {
52
+ return { ...h, command: hookCommand };
53
+ }
54
+ return h;
55
+ })
56
+ };
57
+ }
58
+ return entry;
59
+ });
60
+ return config;
61
+ }
62
+ config.hooks.PreToolUse.push({
63
+ matcher: HOOK_MATCHER,
64
+ hooks: [{ type: 'command', command: hookCommand }],
65
+ });
66
+ return config;
67
+ }
27
68
  export async function installCommand() {
28
- console.error('\n🔧 VIBE-SPLAIN Installer\n');
69
+ console.error('\n🔧 vibe-splain Installer\n');
29
70
  let patchedCount = 0;
30
71
  for (const agent of AGENT_CONFIGS) {
31
72
  const resolvedPath = expandPath(agent.path);
@@ -43,17 +84,25 @@ export async function installCommand() {
43
84
  console.error(` ⚠ Could not parse JSON, skipping`);
44
85
  continue;
45
86
  }
46
- // Check idempotency
47
- if (config.mcpServers?.['vibe-splain']) {
87
+ // Patch config (idempotent: only write when something actually changes,
88
+ // so existing users still pick up newly-added pieces like the hook).
89
+ const before = JSON.stringify(config);
90
+ if (!config.mcpServers)
91
+ config.mcpServers = {};
92
+ if (!config.mcpServers['vibe-splain'])
93
+ config.mcpServers['vibe-splain'] = MCP_ENTRY;
94
+ // The PreToolUse gate is a Claude Code CLI feature (settings.json hooks).
95
+ // Only register it there; Desktop/Gemini/Cursor don't run these hooks.
96
+ if (agent.name === 'Claude Code CLI') {
97
+ const __dirname = dirname(fileURLToPath(import.meta.url));
98
+ const hookPath = join(__dirname, '..', 'hook.js');
99
+ addPreToolUseHook(config, hookPath);
100
+ }
101
+ if (JSON.stringify(config) === before) {
48
102
  console.error(` ✓ Already configured, skipping`);
49
103
  patchedCount++;
50
104
  continue;
51
105
  }
52
- // Patch config
53
- if (!config.mcpServers) {
54
- config.mcpServers = {};
55
- }
56
- config.mcpServers['vibe-splain'] = MCP_ENTRY;
57
106
  await writeFile(resolvedPath, JSON.stringify(config, null, 2), 'utf8');
58
107
  console.error(` ✅ Patched successfully`);
59
108
  patchedCount++;
@@ -1,8 +1,7 @@
1
- import { readAnalysis, readActionBindings, RecommendationEngine } from '@vibe-splain/brain';
1
+ import { readAnalysis, RecommendationEngine, buildGateIndex } from '@vibe-splain/brain';
2
2
  import { ArtifactBundleWriter } from './ArtifactBundleWriter.js';
3
3
  import { JsonRenderer } from './renderers/JsonRenderer.js';
4
4
  import { HtmlRenderer } from './renderers/HtmlRenderer.js';
5
- import { DeltaRenderer } from './renderers/DeltaRenderer.js';
6
5
  import { AgentMarkdownRenderer } from './renderers/AgentMarkdownRenderer.js';
7
6
  import { ValidationRenderer } from './renderers/ValidationRenderer.js';
8
7
  import { RawAnalysisRenderer } from './renderers/RawAnalysisRenderer.js';
@@ -20,22 +19,25 @@ export class ExportOrchestrator {
20
19
  if (!finalStore) {
21
20
  throw new Error('Analysis store not found. Scan the project first.');
22
21
  }
23
- const bindings = await readActionBindings(this.projectRoot);
24
22
  // Aggressive Boilerplate Culling
25
23
  for (const p of dossier.pillars) {
26
24
  p.decisions = p.decisions.filter(c => !(c.severity === 1 && c.category === 'Convention'));
27
25
  p.cardCount = p.decisions.length;
28
26
  }
29
27
  const viewModel = this.buildViewModel(dossier, finalStore);
30
- // Always render JSON, Delta, Validation, and Raw Analysis.
31
28
  const artifacts = [];
32
29
  artifacts.push(...await new JsonRenderer().render(viewModel, finalStore));
33
- artifacts.push(...await new DeltaRenderer().render(viewModel, finalStore));
34
30
  artifacts.push(...await new ValidationRenderer().render(viewModel, finalStore));
35
31
  artifacts.push(...await new RawAnalysisRenderer().render(viewModel, finalStore));
36
32
  if (graph) {
37
33
  artifacts.push(...await new GraphRenderer(graph).render(viewModel, finalStore));
38
34
  }
35
+ const gateIndex = buildGateIndex(finalStore);
36
+ artifacts.push({
37
+ type: 'gate',
38
+ path: 'gate.json',
39
+ content: JSON.stringify(gateIndex, null, 2),
40
+ });
39
41
  // Determine additional formats
40
42
  const formats = ['html', 'markdown']; // default
41
43
  if (options.format && options.format !== 'json' && options.format !== 'delta') {
@@ -46,7 +48,7 @@ export class ExportOrchestrator {
46
48
  artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
47
49
  }
48
50
  if (formats.includes('markdown')) {
49
- artifacts.push(...await new AgentMarkdownRenderer(options.budget, bindings).render(viewModel, finalStore));
51
+ artifacts.push(...await new AgentMarkdownRenderer(options.budget).render(viewModel, finalStore));
50
52
  }
51
53
  const writer = new ArtifactBundleWriter(this.projectRoot);
52
54
  await writer.writeBundle(artifacts);
@@ -3,7 +3,6 @@ import type { Renderer } from './Renderer.js';
3
3
  import type { Artifact } from '../ArtifactBundleWriter.js';
4
4
  export declare class AgentMarkdownRenderer implements Renderer {
5
5
  private budget;
6
- private bindings;
7
- constructor(budget?: number, bindings?: any | null);
6
+ constructor(budget?: number);
8
7
  render(viewModel: DossierViewModel, store: AnalysisStore): Artifact[];
9
8
  }
@@ -1,9 +1,7 @@
1
1
  export class AgentMarkdownRenderer {
2
2
  budget;
3
- bindings;
4
- constructor(budget = 8000, bindings = null) {
3
+ constructor(budget = 8000) {
5
4
  this.budget = budget;
6
- this.bindings = bindings;
7
5
  }
8
6
  render(viewModel, store) {
9
7
  let md = `# Architectural Dossier: ${viewModel.projectRoot}\n\n`;
@@ -54,20 +52,6 @@ export class AgentMarkdownRenderer {
54
52
  md += `**Severity**: ${card.severity} | **Category**: ${card.category}\n`;
55
53
  md += `**Narrative**: ${card.narrative}\n`;
56
54
  }
57
- // Add Function-Level Action Bindings for Tier 1
58
- if (this.bindings && this.bindings.files[path]) {
59
- const fileBinding = this.bindings.files[path];
60
- const criticalFunctions = fileBinding.functions.filter((fn) => fn.semanticActions.length > 0 || fn.isEntrypoint);
61
- if (criticalFunctions.length > 0) {
62
- md += `\n**Critical Functions**:\n`;
63
- for (const fn of criticalFunctions) {
64
- md += `- \`${fn.displayName}\` (lines ${fn.startLine}-${fn.endLine})${fn.isEntrypoint ? ' [Entrypoint]' : ''}\n`;
65
- for (const action of fn.semanticActions) {
66
- md += ` - **${action.actionKind}**${action.targetModel ? ` on ${action.targetModel}` : ''}: \`${action.calleeText}\` (line ${action.sourceLine})\n`;
67
- }
68
- }
69
- }
70
- }
71
55
  if (recs.length > 0) {
72
56
  md += `\n**Safe Patch Strategies**:\n`;
73
57
  for (const r of recs) {
@@ -0,0 +1,30 @@
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;
@@ -0,0 +1,81 @@
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.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function findProjectRoot(start: string | undefined): string | null;