vibe-splain 3.4.1 → 3.4.2
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 +25 -19
- 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/commands/network.d.ts +1 -0
- package/dist/commands/network.js +91 -0
- package/dist/commands/scan.d.ts +13 -0
- package/dist/commands/scan.js +97 -0
- 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-entry.d.ts +2 -0
- package/dist/hook-entry.js +8 -0
- package/dist/hook.d.ts +1 -0
- package/dist/hook.js +337 -0
- package/dist/index.js +2635 -1384
- package/dist/mcp/server.js +9 -10
- package/dist/mcp/tools/explainSpecialistScope.d.ts +15 -0
- package/dist/mcp/tools/explainSpecialistScope.js +30 -0
- package/dist/mcp/tools/hydration/get_evidence_slice.js +1 -3
- package/dist/mcp/tools/prepareTaskContext.d.ts +38 -0
- package/dist/mcp/tools/prepareTaskContext.js +93 -0
- 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 +1 -1
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 and Behavioral Gravity**: Core computes `staticGravity` using Tree-Sitter AST analysis based on link density, nesting depth, and mutation counts. **Bundled Adapters** (like Cal.com and n8n) interpret product semantics to add `behavioralLift` for critical dynamic execution paths. Final `gravity` is the combination of static evidence and adapter interpretation.
|
|
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 **8 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,9 +141,11 @@ The UI features:
|
|
|
137
141
|
|
|
138
142
|
```
|
|
139
143
|
packages/
|
|
140
|
-
├── brain/ # @vibe-splain/brain — analysis
|
|
144
|
+
├── brain/ # @vibe-splain/brain — repo-agnostic analysis core + bundled adapters
|
|
141
145
|
│ └── src/
|
|
142
|
-
│ ├──
|
|
146
|
+
│ ├── pipeline/
|
|
147
|
+
│ │ └── adapters/ # Bundled product adapters (Cal.com, n8n)
|
|
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
|
│ └── watcher.ts # Chokidar file watcher
|
|
@@ -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++;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function networkPlanCommand(task: string, options: any): Promise<void>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { join, resolve } from 'path';
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { buildExpertNetworkPacket } from '@vibe-splain/brain';
|
|
4
|
+
export async function networkPlanCommand(task, options) {
|
|
5
|
+
const projectRoot = options.root ? resolve(options.root) : process.cwd();
|
|
6
|
+
const maxFiles = options.maxFiles ? parseInt(options.maxFiles, 10) : 8;
|
|
7
|
+
// Try to load scan artifact
|
|
8
|
+
let scanArtifact = null;
|
|
9
|
+
const artifactPath = options.scanArtifact
|
|
10
|
+
? resolve(options.scanArtifact)
|
|
11
|
+
: join(projectRoot, '.vibe-splainer', 'analysis.json');
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(artifactPath, 'utf8');
|
|
14
|
+
scanArtifact = JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
if (options.scanArtifact) {
|
|
18
|
+
console.error(`[vibe-splain] Error reading scan artifact at ${artifactPath}:`, err.message);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Build the packet
|
|
22
|
+
const packet = buildExpertNetworkPacket({
|
|
23
|
+
task,
|
|
24
|
+
projectRoot,
|
|
25
|
+
scanArtifact,
|
|
26
|
+
errorTrace: options.errorTrace,
|
|
27
|
+
intendedBehavior: options.intendedBehavior,
|
|
28
|
+
maxFiles
|
|
29
|
+
});
|
|
30
|
+
// Save artifacts
|
|
31
|
+
const networkDir = join(projectRoot, '.vibe-splainer', 'network');
|
|
32
|
+
const packetsDir = join(networkDir, 'packets');
|
|
33
|
+
try {
|
|
34
|
+
await mkdir(packetsDir, { recursive: true });
|
|
35
|
+
// Save latest
|
|
36
|
+
const latestPath = join(networkDir, 'latest_packet.json');
|
|
37
|
+
await writeFile(latestPath, JSON.stringify(packet, null, 2), 'utf8');
|
|
38
|
+
// Save timestamped
|
|
39
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
40
|
+
const timestampedPath = join(packetsDir, `${timestamp}.json`);
|
|
41
|
+
await writeFile(timestampedPath, JSON.stringify(packet, null, 2), 'utf8');
|
|
42
|
+
if (options.json) {
|
|
43
|
+
process.stdout.write(JSON.stringify(packet, null, 2) + '\n');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
process.stdout.write('────────────────────────────────────────────────────────\n');
|
|
47
|
+
process.stdout.write(' Vibe-Splain Specialist Network — Task Plan\n');
|
|
48
|
+
process.stdout.write('────────────────────────────────────────────────────────\n');
|
|
49
|
+
process.stdout.write(`Task: "${packet.task}"\n`);
|
|
50
|
+
process.stdout.write(`Project Root: ${packet.projectRoot}\n`);
|
|
51
|
+
process.stdout.write(`Primary Specialist: ${packet.route.primarySpecialist}\n`);
|
|
52
|
+
process.stdout.write(`Panel: ${packet.route.panel.join(', ')}\n`);
|
|
53
|
+
process.stdout.write(`Confidence: ${packet.route.confidence}\n`);
|
|
54
|
+
process.stdout.write(`Reason: ${packet.route.reason}\n`);
|
|
55
|
+
process.stdout.write('\nSelected Files:\n');
|
|
56
|
+
if (packet.selectedFiles.length > 0) {
|
|
57
|
+
packet.selectedFiles.forEach(f => {
|
|
58
|
+
process.stdout.write(` • ${f.path} [blastRadius: ${f.blastRadius}]\n`);
|
|
59
|
+
process.stdout.write(` Reason: ${f.reason}\n`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
process.stdout.write(' (No files selected)\n');
|
|
64
|
+
}
|
|
65
|
+
process.stdout.write('\nRisk Warnings:\n');
|
|
66
|
+
if (packet.riskWarnings.length > 0) {
|
|
67
|
+
packet.riskWarnings.forEach(w => {
|
|
68
|
+
const levelLabel = w.level === 'critical' ? '🔴 CRITICAL' : (w.level === 'warning' ? '🟡 WARNING' : 'ℹ️ INFO');
|
|
69
|
+
process.stdout.write(` • [${levelLabel}] ${w.message}\n`);
|
|
70
|
+
process.stdout.write(` Reason: ${w.reason}\n`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
process.stdout.write(' (No risks identified)\n');
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write('\nSmallest Safe Change Policy:\n');
|
|
77
|
+
process.stdout.write(` ${packet.smallestSafeChangePolicy.summary}\n`);
|
|
78
|
+
process.stdout.write('\nSaved Artifacts:\n');
|
|
79
|
+
process.stdout.write(` • Latest: ${latestPath}\n`);
|
|
80
|
+
process.stdout.write(` • Packet: ${timestampedPath}\n`);
|
|
81
|
+
process.stdout.write('────────────────────────────────────────────────────────\n');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error('[vibe-splain] Error saving network artifacts:', err.message);
|
|
86
|
+
if (options.json) {
|
|
87
|
+
process.stdout.write(JSON.stringify(packet, null, 2) + '\n');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=network.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface ScanOptions {
|
|
2
|
+
root?: string;
|
|
3
|
+
out?: string;
|
|
4
|
+
json?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Headless one-shot scan. Reuses the exact MCP scan pipeline (performScan),
|
|
8
|
+
* relocates the artifact bundle to --out, and prints a summary. The gravity
|
|
9
|
+
* formula is selected by the ROZE_GRAVITY_V2 env var, never a CLI flag — v2 is
|
|
10
|
+
* still an experimental candidate (see CONTEXT.md / Phase 0).
|
|
11
|
+
*/
|
|
12
|
+
export declare function scanCommand(options?: ScanOptions): Promise<void>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { resolve, join, dirname } from 'path';
|
|
2
|
+
import { rm, rename, mkdir, writeFile } from 'fs/promises';
|
|
3
|
+
import { performScan } from '../mcp/tools/scan_project.js';
|
|
4
|
+
/**
|
|
5
|
+
* Headless one-shot scan. Reuses the exact MCP scan pipeline (performScan),
|
|
6
|
+
* relocates the artifact bundle to --out, and prints a summary. The gravity
|
|
7
|
+
* formula is selected by the ROZE_GRAVITY_V2 env var, never a CLI flag — v2 is
|
|
8
|
+
* still an experimental candidate (see CONTEXT.md / Phase 0).
|
|
9
|
+
*/
|
|
10
|
+
export async function scanCommand(options = {}) {
|
|
11
|
+
const root = resolve(options.root || '.');
|
|
12
|
+
const gravityMode = process.env.ROZE_GRAVITY_V2 === '1' ? 'v2_candidate' : 'v1_default';
|
|
13
|
+
console.error(`[roze] Scanning ${root} (gravity mode: ${gravityMode})...`);
|
|
14
|
+
const { result, scanId } = await performScan(root);
|
|
15
|
+
// The pipeline always writes to <root>/.roze. Relocate to --out if different
|
|
16
|
+
// so v1 and v2 runs can coexist for comparison.
|
|
17
|
+
const defaultDir = join(root, '.roze');
|
|
18
|
+
const outDir = resolve(options.out || defaultDir);
|
|
19
|
+
if (outDir !== defaultDir) {
|
|
20
|
+
await rm(outDir, { recursive: true, force: true });
|
|
21
|
+
await mkdir(dirname(outDir), { recursive: true });
|
|
22
|
+
await rename(defaultDir, outDir);
|
|
23
|
+
}
|
|
24
|
+
// Persist the gravity mode so v1/v2 scans are self-identifying.
|
|
25
|
+
const meta = {
|
|
26
|
+
gravityMode,
|
|
27
|
+
scannedAt: new Date().toISOString(),
|
|
28
|
+
root,
|
|
29
|
+
scanId,
|
|
30
|
+
totalFilesScanned: result.totalFilesScanned,
|
|
31
|
+
realSourceCount: result.realSourceCount,
|
|
32
|
+
wildCandidateCount: result.wildCandidates.length,
|
|
33
|
+
};
|
|
34
|
+
await writeFile(join(outDir, 'scan_meta.json'), JSON.stringify(meta, null, 2));
|
|
35
|
+
// Build gravity/heat lookups from the scored files for display.
|
|
36
|
+
const gravityByPath = new Map(result.files.map(f => [f.relativePath, f.gravity]));
|
|
37
|
+
const heatByPath = new Map(result.files.map(f => [f.relativePath, f.heat]));
|
|
38
|
+
const topGravity = result.map.topGravity.slice(0, 12).map(p => ({
|
|
39
|
+
path: p,
|
|
40
|
+
gravity: Math.round(gravityByPath.get(p) ?? 0),
|
|
41
|
+
}));
|
|
42
|
+
const topHeat = result.map.topHeat.slice(0, 12).map(p => ({
|
|
43
|
+
path: p,
|
|
44
|
+
heat: Math.round(heatByPath.get(p) ?? 0),
|
|
45
|
+
}));
|
|
46
|
+
const wildDiscovery = result.wildCandidates.slice(0, 12).map(f => ({
|
|
47
|
+
path: f.relativePath,
|
|
48
|
+
heat: Math.round(f.heat),
|
|
49
|
+
gravity: Math.round(f.gravity),
|
|
50
|
+
topSmells: f.smells.filter(s => s.severity >= 3).slice(0, 3).map(s => s.note),
|
|
51
|
+
}));
|
|
52
|
+
const specialistMap = result.map.pillars.map(p => ({
|
|
53
|
+
name: p.name,
|
|
54
|
+
fileCount: p.memberFiles.length,
|
|
55
|
+
}));
|
|
56
|
+
const summary = {
|
|
57
|
+
gravityMode,
|
|
58
|
+
root,
|
|
59
|
+
outDir,
|
|
60
|
+
dossier: join(outDir, 'dossier.json'),
|
|
61
|
+
analysis: join(outDir, 'analysis.json'),
|
|
62
|
+
stack: result.map.stack,
|
|
63
|
+
totalFilesScanned: result.totalFilesScanned,
|
|
64
|
+
realSourceCount: result.realSourceCount,
|
|
65
|
+
topGravity,
|
|
66
|
+
topHeat,
|
|
67
|
+
wildDiscovery,
|
|
68
|
+
specialistMap,
|
|
69
|
+
};
|
|
70
|
+
if (options.json) {
|
|
71
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const lines = [];
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push(`roze scan — gravity mode: ${gravityMode}`);
|
|
77
|
+
lines.push(` root: ${root}`);
|
|
78
|
+
lines.push(` stack: ${result.map.stack.join(', ') || '(none detected)'}`);
|
|
79
|
+
lines.push(` files: ${result.totalFilesScanned} scanned, ${result.realSourceCount} real-source`);
|
|
80
|
+
lines.push('');
|
|
81
|
+
lines.push('Top Gravity (Start Here):');
|
|
82
|
+
topGravity.forEach((f, i) => lines.push(` ${String(i + 1).padStart(2)}. [${String(f.gravity).padStart(3)}] ${f.path}`));
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('Top Heat (Wild Discovery):');
|
|
85
|
+
topHeat.forEach((f, i) => lines.push(` ${String(i + 1).padStart(2)}. [${String(f.heat).padStart(3)}] ${f.path}`));
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push('Specialist Map (pillars):');
|
|
88
|
+
specialistMap.forEach(p => lines.push(` - ${p.name} (${p.fileCount} files)`));
|
|
89
|
+
lines.push('');
|
|
90
|
+
lines.push(`Artifacts written to: ${outDir}`);
|
|
91
|
+
lines.push(` dossier: ${join(outDir, 'dossier.json')}`);
|
|
92
|
+
lines.push(` analysis: ${join(outDir, 'analysis.json')}`);
|
|
93
|
+
lines.push(` meta: ${join(outDir, 'scan_meta.json')} (gravityMode: ${gravityMode})`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
process.stdout.write(lines.join('\n'));
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -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;
|