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.
- package/README.md +26 -20
- package/dist/commands/hook.d.ts +9 -0
- package/dist/commands/hook.js +84 -0
- package/dist/commands/install.d.ts +5 -0
- package/dist/commands/install.js +58 -9
- package/dist/export/ExportOrchestrator.js +8 -6
- package/dist/export/renderers/AgentMarkdownRenderer.d.ts +1 -2
- package/dist/export/renderers/AgentMarkdownRenderer.js +1 -17
- package/dist/hook/preToolUse.d.ts +30 -0
- package/dist/hook/preToolUse.js +81 -0
- package/dist/hook.d.ts +1 -0
- package/dist/hook.js +337 -0
- package/dist/index.js +2336 -2825
- package/dist/mcp/server.js +1 -25
- package/dist/mcp/tools/hydration/get_evidence_slice.js +1 -3
- package/dist/mcp/tools/scan_project.d.ts +15 -0
- package/dist/mcp/tools/scan_project.js +16 -13
- package/dist/ui/index.html +3 -3
- package/package.json +2 -2
- 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/importBundle.d.ts +0 -4
- package/dist/commands/importBundle.js +0 -80
- package/dist/export/renderers/DeltaRenderer.d.ts +0 -6
- package/dist/export/renderers/DeltaRenderer.js +0 -22
- package/dist/mcp/tools/apply_patch.d.ts +0 -37
- package/dist/mcp/tools/apply_patch.js +0 -103
- 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/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/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<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
|
-
|
|
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
|
|
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 —
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
80
|
+
`vibe-splain` integrates directly with Claude Code's hook system to prevent AI agents from accidentally breaking your codebase:
|
|
78
81
|
|
|
79
|
-
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
|
144
|
+
├── brain/ # @vibe-splain/brain — repo-agnostic static analysis core
|
|
141
145
|
│ └── src/
|
|
142
|
-
│ ├──
|
|
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
|
-
│ └──
|
|
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**:
|
|
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>;
|
package/dist/commands/install.js
CHANGED
|
@@ -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🔧
|
|
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
|
-
//
|
|
47
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|