milens 0.6.2 → 0.6.4

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 (123) hide show
  1. package/.agents/skills/adapters/SKILL.md +31 -0
  2. package/.agents/skills/analyzer/SKILL.md +55 -0
  3. package/.agents/skills/apps/SKILL.md +42 -0
  4. package/.agents/skills/docs/SKILL.md +46 -0
  5. package/.agents/skills/milens/SKILL.md +168 -0
  6. package/.agents/skills/milens-code-review/SKILL.md +186 -0
  7. package/.agents/skills/milens-eval/SKILL.md +221 -0
  8. package/.agents/skills/milens-plan/SKILL.md +227 -0
  9. package/.agents/skills/milens-refactor-clean/SKILL.md +209 -0
  10. package/.agents/skills/milens-security-review/SKILL.md +224 -0
  11. package/.agents/skills/milens-tdd/SKILL.md +156 -0
  12. package/.agents/skills/parser/SKILL.md +60 -0
  13. package/.agents/skills/root/SKILL.md +64 -0
  14. package/.agents/skills/scripts/SKILL.md +27 -0
  15. package/.agents/skills/security/SKILL.md +44 -0
  16. package/.agents/skills/server/SKILL.md +46 -0
  17. package/.agents/skills/store/SKILL.md +53 -0
  18. package/.agents/skills/test/SKILL.md +73 -0
  19. package/LICENSE +75 -75
  20. package/README.md +524 -305
  21. package/adapters/README.md +107 -0
  22. package/adapters/claude-code/.claude/mcp.json +9 -0
  23. package/adapters/claude-code/CLAUDE.md +58 -0
  24. package/adapters/codex/.codex/codex.md +52 -0
  25. package/adapters/copilot/.github/copilot-instructions.md +62 -0
  26. package/adapters/cursor/.cursorrules +9 -0
  27. package/adapters/gemini/.gemini/context.md +58 -0
  28. package/adapters/opencode/.opencode/config.json +9 -0
  29. package/adapters/opencode/AGENTS.md +58 -0
  30. package/adapters/zed/.zed/settings.json +8 -0
  31. package/dist/agents-md.d.ts +3 -0
  32. package/dist/agents-md.d.ts.map +1 -0
  33. package/dist/agents-md.js +112 -0
  34. package/dist/agents-md.js.map +1 -0
  35. package/dist/analyzer/engine.d.ts +1 -0
  36. package/dist/analyzer/engine.d.ts.map +1 -1
  37. package/dist/analyzer/engine.js +27 -8
  38. package/dist/analyzer/engine.js.map +1 -1
  39. package/dist/analyzer/review.d.ts +23 -0
  40. package/dist/analyzer/review.d.ts.map +1 -0
  41. package/dist/analyzer/review.js +143 -0
  42. package/dist/analyzer/review.js.map +1 -0
  43. package/dist/analyzer/testplan.d.ts +59 -0
  44. package/dist/analyzer/testplan.d.ts.map +1 -0
  45. package/dist/analyzer/testplan.js +218 -0
  46. package/dist/analyzer/testplan.js.map +1 -0
  47. package/dist/cli.js +1192 -401
  48. package/dist/cli.js.map +1 -1
  49. package/dist/metrics.d.ts +51 -0
  50. package/dist/metrics.d.ts.map +1 -0
  51. package/dist/metrics.js +64 -0
  52. package/dist/metrics.js.map +1 -0
  53. package/dist/parser/extract.d.ts +1 -0
  54. package/dist/parser/extract.d.ts.map +1 -1
  55. package/dist/parser/extract.js +8 -0
  56. package/dist/parser/extract.js.map +1 -1
  57. package/dist/parser/lang-go.d.ts.map +1 -1
  58. package/dist/parser/lang-go.js +75 -39
  59. package/dist/parser/lang-go.js.map +1 -1
  60. package/dist/parser/lang-java.d.ts.map +1 -1
  61. package/dist/parser/lang-java.js +30 -29
  62. package/dist/parser/lang-java.js.map +1 -1
  63. package/dist/parser/lang-js.js +105 -105
  64. package/dist/parser/lang-php.js +38 -38
  65. package/dist/parser/lang-py.d.ts.map +1 -1
  66. package/dist/parser/lang-py.js +53 -31
  67. package/dist/parser/lang-py.js.map +1 -1
  68. package/dist/parser/lang-ruby.d.ts.map +1 -1
  69. package/dist/parser/lang-ruby.js +15 -14
  70. package/dist/parser/lang-ruby.js.map +1 -1
  71. package/dist/parser/lang-rust.js +30 -30
  72. package/dist/parser/lang-ts.js +191 -191
  73. package/dist/security/deps.d.ts +38 -0
  74. package/dist/security/deps.d.ts.map +1 -0
  75. package/dist/security/deps.js +685 -0
  76. package/dist/security/deps.js.map +1 -0
  77. package/dist/security/rules.d.ts +42 -0
  78. package/dist/security/rules.d.ts.map +1 -0
  79. package/dist/security/rules.js +940 -0
  80. package/dist/security/rules.js.map +1 -0
  81. package/dist/server/hooks.d.ts +26 -0
  82. package/dist/server/hooks.d.ts.map +1 -0
  83. package/dist/server/hooks.js +253 -0
  84. package/dist/server/hooks.js.map +1 -0
  85. package/dist/server/mcp-prompts.d.ts +277 -0
  86. package/dist/server/mcp-prompts.d.ts.map +1 -0
  87. package/dist/server/mcp-prompts.js +627 -0
  88. package/dist/server/mcp-prompts.js.map +1 -0
  89. package/dist/server/mcp.d.ts.map +1 -1
  90. package/dist/server/mcp.js +520 -36
  91. package/dist/server/mcp.js.map +1 -1
  92. package/dist/server/test-plan.d.ts +20 -0
  93. package/dist/server/test-plan.d.ts.map +1 -0
  94. package/dist/server/test-plan.js +100 -0
  95. package/dist/server/test-plan.js.map +1 -0
  96. package/dist/skills.js +152 -120
  97. package/dist/skills.js.map +1 -1
  98. package/dist/store/annotations.d.ts +41 -0
  99. package/dist/store/annotations.d.ts.map +1 -0
  100. package/dist/store/annotations.js +192 -0
  101. package/dist/store/annotations.js.map +1 -0
  102. package/dist/store/confidence.d.ts +18 -0
  103. package/dist/store/confidence.d.ts.map +1 -0
  104. package/dist/store/confidence.js +82 -0
  105. package/dist/store/confidence.js.map +1 -0
  106. package/dist/store/db.d.ts +68 -1
  107. package/dist/store/db.d.ts.map +1 -1
  108. package/dist/store/db.js +349 -139
  109. package/dist/store/db.js.map +1 -1
  110. package/dist/store/schema.sql +128 -83
  111. package/dist/store/vectors.d.ts +65 -0
  112. package/dist/store/vectors.d.ts.map +1 -0
  113. package/dist/store/vectors.js +212 -0
  114. package/dist/store/vectors.js.map +1 -0
  115. package/dist/types.d.ts +101 -0
  116. package/dist/types.d.ts.map +1 -1
  117. package/dist/utils.d.ts +3 -0
  118. package/dist/utils.d.ts.map +1 -0
  119. package/dist/utils.js +9 -0
  120. package/dist/utils.js.map +1 -0
  121. package/docs/README.md +24 -0
  122. package/docs/diagram2.svg +1 -1
  123. package/package.json +80 -65
@@ -14,6 +14,10 @@ import { RepoRegistry } from '../store/registry.js';
14
14
  import { getParser, loadLanguage } from '../parser/loader.js';
15
15
  import { ALL_LANGS } from '../parser/languages.js';
16
16
  import { fileURLToPath } from 'node:url';
17
+ import { generateTestPlan } from './test-plan.js';
18
+ import { AnnotationStore } from '../store/annotations.js';
19
+ import { registerAllPrompts } from './mcp-prompts.js';
20
+ import { loadRules } from '../security/rules.js';
17
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
22
  const PKG_VERSION = process.env.MILENS_VERSION ?? JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8')).version;
19
23
  // ── Lazy DB connection with idle eviction ──
@@ -310,42 +314,42 @@ function loadGrepIgnoreRules(rootPath) {
310
314
  return ig;
311
315
  }
312
316
  // ── Server instructions (sent to client via MCP protocol on initialize) ──
313
- const MILENS_INSTRUCTIONS = `milens — code intelligence engine. Indexes codebases into symbol graphs.
314
-
315
- ## Tool selection
316
- - \`query\` — find symbol definitions (code identifiers only)
317
- - \`grep\` — text search ALL files. Use \`scope\` param: all (default), code (source only), imports, definitions
318
- - \`context\` — 360° view: incoming + outgoing for a symbol
319
- - \`impact\` — blast radius: what breaks if symbol changes
320
- - \`overview\` — combined context + impact + grep in one call (preferred for editing workflows)
321
- - \`edit_check\` — pre-edit safety: callers + export status + re-export chains + test coverage + ⚠ warnings (fastest for edits)
322
- - \`trace\` — execution flow: call chains from entrypoints to a symbol (or downstream from it)
323
- - \`routes\` — detect framework routes/endpoints (Express, FastAPI, NestJS, Flask, Go, PHP, Rails)
324
- - \`smart_context\` — intent-aware context: understand/edit/debug/test (returns only what matters for intent)
325
- - \`domains\` — show domain clusters: groups of files forming logical modules based on dependency graph
326
- - \`repos\` — list all indexed repositories with summary stats (multi-repo support)
327
- - \`detect_changes\` — git diff → affected symbols
328
- - \`explain_relationship\` — shortest path between two symbols
329
- - \`find_dead_code\` — unused exports
330
- - \`get_file_symbols\` — all symbols in a file
331
- - \`get_type_hierarchy\` — inheritance tree
332
-
333
- ## Rules
334
- - Before editing a symbol: run \`edit_check\` or \`smart_context\` with intent=edit
335
- - For debugging: run \`smart_context\` with intent=debug or \`trace\` to=symbol
336
- - For writing tests: run \`smart_context\` with intent=test — shows deps to mock + callers to cover
337
- - \`impact\` only tracks code deps — always pair with \`grep\` for templates/configs
338
- - Use \`query\` for camelCase/PascalCase identifiers, \`grep\` for display text or multi-word strings
339
- - impact depth: 1=WILL BREAK, 2=LIKELY AFFECTED, 3=MAY NEED TESTING
340
- - ⚠ markers indicate unresolved INTERNAL references — external package imports/calls are tracked separately
341
- - ✓ test coverage shown on edit_check — symbols with no test coverage get a warning
342
- - ⏳ staleness: files not re-analyzed in 24h are flagged — consider re-running \`milens analyze\`
343
-
344
- ## Resources (MCP Resources protocol)
345
- - \`milens://overview\` — index overview (stats, domains, coverage, staleness)
346
- - \`milens://symbol/{name}\` — symbol context by name
347
- - \`milens://file/{path}\` — all symbols in a file
348
- - \`milens://domain/{name}\` — domain cluster details
317
+ const MILENS_INSTRUCTIONS = `milens — code intelligence engine. Indexes codebases into symbol graphs.
318
+
319
+ ## Tool selection
320
+ - \`query\` — find symbol definitions (code identifiers only)
321
+ - \`grep\` — text search ALL files. Use \`scope\` param: all (default), code (source only), imports, definitions
322
+ - \`context\` — 360° view: incoming + outgoing for a symbol
323
+ - \`impact\` — blast radius: what breaks if symbol changes
324
+ - \`overview\` — combined context + impact + grep in one call (preferred for editing workflows)
325
+ - \`edit_check\` — pre-edit safety: callers + export status + re-export chains + test coverage + ⚠ warnings (fastest for edits)
326
+ - \`trace\` — execution flow: call chains from entrypoints to a symbol (or downstream from it)
327
+ - \`routes\` — detect framework routes/endpoints (Express, FastAPI, NestJS, Flask, Go, PHP, Rails)
328
+ - \`smart_context\` — intent-aware context: understand/edit/debug/test (returns only what matters for intent)
329
+ - \`domains\` — show domain clusters: groups of files forming logical modules based on dependency graph
330
+ - \`repos\` — list all indexed repositories with summary stats (multi-repo support)
331
+ - \`detect_changes\` — git diff → affected symbols
332
+ - \`explain_relationship\` — shortest path between two symbols
333
+ - \`find_dead_code\` — unused exports
334
+ - \`get_file_symbols\` — all symbols in a file
335
+ - \`get_type_hierarchy\` — inheritance tree
336
+
337
+ ## Rules
338
+ - Before editing a symbol: run \`edit_check\` or \`smart_context\` with intent=edit
339
+ - For debugging: run \`smart_context\` with intent=debug or \`trace\` to=symbol
340
+ - For writing tests: run \`smart_context\` with intent=test — shows deps to mock + callers to cover
341
+ - \`impact\` only tracks code deps — always pair with \`grep\` for templates/configs
342
+ - Use \`query\` for camelCase/PascalCase identifiers, \`grep\` for display text or multi-word strings
343
+ - impact depth: 1=WILL BREAK, 2=LIKELY AFFECTED, 3=MAY NEED TESTING
344
+ - ⚠ markers indicate unresolved INTERNAL references — external package imports/calls are tracked separately
345
+ - ✓ test coverage shown on edit_check — symbols with no test coverage get a warning
346
+ - ⏳ staleness: files not re-analyzed in 24h are flagged — consider re-running \`milens analyze\`
347
+
348
+ ## Resources (MCP Resources protocol)
349
+ - \`milens://overview\` — index overview (stats, domains, coverage, staleness)
350
+ - \`milens://symbol/{name}\` — symbol context by name
351
+ - \`milens://file/{path}\` — all symbols in a file
352
+ - \`milens://domain/{name}\` — domain cluster details
349
353
  `;
350
354
  // ── Server setup ──
351
355
  export function createMcpServer(rootPath) {
@@ -410,6 +414,29 @@ export function createMcpServer(rootPath) {
410
414
  }
411
415
  return origTool(...args);
412
416
  });
417
+ // ── Selective tool profiles (W4) ──
418
+ const profile = process.env.MILENS_PROFILE || undefined;
419
+ if (profile && profile !== 'full') {
420
+ const minimal = new Set(['query', 'grep', 'context', 'impact', 'status', 'codebase_summary', 'edit_check', 'detect_changes', 'get_file_symbols', 'overview']);
421
+ const standard = new Set([...minimal, 'domains', 'repos', 'explain_relationship', 'find_dead_code', 'get_type_hierarchy', 'trace', 'routes', 'smart_context', 'review_pr', 'review_symbol', 'test_coverage_gaps', 'test_plan', 'test_impact', 'session_start', 'recall']);
422
+ const allowed = profile === 'minimal' ? minimal : standard;
423
+ // Wrap server.tool again to gate by profile
424
+ const profileWrappedTool = server.tool.bind(server);
425
+ server.tool = ((...args) => {
426
+ const toolName = args[0];
427
+ if (!allowed.has(toolName)) {
428
+ // Return a no-op tool that explains it's disabled
429
+ const origLength = args.length;
430
+ const handler = args[origLength - 1];
431
+ if (typeof handler === 'function') {
432
+ args[origLength - 1] = async () => ({
433
+ content: [{ type: 'text', text: `Tool "${toolName}" disabled by profile "${profile}". Use --profile full to enable.` }],
434
+ });
435
+ }
436
+ }
437
+ return profileWrappedTool(...args);
438
+ });
439
+ }
413
440
  // ── Tool: query ──
414
441
  server.tool('query', 'Search indexed symbol definitions by name/kind. For text in templates/configs/docs, use `grep`.', {
415
442
  query: z.string().describe('Symbol name, kind, or keyword to search'),
@@ -1308,6 +1335,284 @@ export function createMcpServer(rootPath) {
1308
1335
  return { content: [{ type: 'text', text: `Query error: ${err.message}` }] };
1309
1336
  }
1310
1337
  });
1338
+ // ═══ codebase_summary ═══
1339
+ server.tool('codebase_summary', 'Compact ~500 token codebase overview: domains, top hubs, test coverage, annotations count. Use at the start of every session.', { repo: z.string().optional() }, async ({ repo }) => {
1340
+ const { db } = getDb(repo);
1341
+ const summary = db.getCodebaseSummary();
1342
+ const lines = [
1343
+ 'Milens Codebase Summary:',
1344
+ ` Symbols: ${summary.symbols} | Links: ${summary.links} | Files: ${summary.files}`,
1345
+ ];
1346
+ if (summary.exportedSymbols > 0) {
1347
+ lines.push(` Test coverage: ${summary.coveragePct}% (${summary.testedSymbols}/${summary.exportedSymbols} exported symbols tested)`);
1348
+ }
1349
+ if (summary.domains.length > 0) {
1350
+ const domainStr = summary.domains.map(d => `${d.domain}(${d.symbols}s)`).join(', ');
1351
+ lines.push(` Domains: ${domainStr}`);
1352
+ }
1353
+ if (summary.topHubs.length > 0) {
1354
+ const hubStr = summary.topHubs.map(h => `${h.name}(${h.kind},heat:${h.heat})`).join(', ');
1355
+ lines.push(` Top hubs: ${hubStr}`);
1356
+ }
1357
+ try {
1358
+ const annCount = db.getAnnotationCount();
1359
+ lines.push(` Total annotations: ${annCount}`);
1360
+ }
1361
+ catch {
1362
+ lines.push(' Total annotations: 0');
1363
+ }
1364
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1365
+ });
1366
+ // ═══ review_pr ═══
1367
+ server.tool('review_pr', 'PR risk assessment: git diff -> affected symbols with risk scores (LOW/MEDIUM/HIGH/CRITICAL).', { ref: z.string().optional().default('HEAD'), repo: z.string().optional() }, async ({ ref, repo }) => {
1368
+ const { db, root } = getDb(repo);
1369
+ let changedFiles = [];
1370
+ try {
1371
+ const { execSync } = await import('node:child_process');
1372
+ const diff = execSync(`git diff --name-only ${ref}`, { cwd: root, encoding: 'utf-8' }).trim();
1373
+ changedFiles = diff ? diff.split('\n').filter(Boolean) : [];
1374
+ }
1375
+ catch { }
1376
+ if (changedFiles.length === 0) {
1377
+ return { content: [{ type: 'text', text: 'No changed files detected.' }] };
1378
+ }
1379
+ const allAffected = [];
1380
+ for (const file of changedFiles) {
1381
+ const syms = db.getSymbolsByFile(file);
1382
+ if (syms.length === 0)
1383
+ continue;
1384
+ for (const sym of syms) {
1385
+ const incoming = db.getIncomingLinks(sym.id).filter(l => l.type !== 'contains');
1386
+ const depsCount = incoming.length;
1387
+ const heat = sym.heat ?? 0;
1388
+ const hasTest = db.getSymbolTestCoverage(sym.id);
1389
+ const score = Math.round((heat / 100) * 40 + Math.min(depsCount / 10, 1) * 35 + (hasTest ? 0 : 25));
1390
+ let level = 'LOW';
1391
+ if (score > 75)
1392
+ level = 'CRITICAL';
1393
+ else if (score > 50)
1394
+ level = 'HIGH';
1395
+ else if (score > 25)
1396
+ level = 'MEDIUM';
1397
+ allAffected.push({ symbol: sym.name, kind: sym.kind, file: sym.filePath, heat, dependents: depsCount, hasTest, riskScore: score, riskLevel: level });
1398
+ }
1399
+ }
1400
+ allAffected.sort((a, b) => b.riskScore - a.riskScore);
1401
+ const summary = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
1402
+ for (const a of allAffected)
1403
+ summary[a.riskLevel]++;
1404
+ const lines = [`PR Risk Assessment (vs ${ref}):\n`];
1405
+ lines.push(`${changedFiles.length} changed files, ${allAffected.length} affected symbols\n`);
1406
+ for (const a of allAffected.slice(0, 30)) {
1407
+ lines.push(` ${a.symbol} [${a.kind}] ${a.file} — heat:${a.heat} deps:${a.dependents} test:${a.hasTest ? 'yes' : 'no'} → ${a.riskLevel}(${a.riskScore})`);
1408
+ }
1409
+ lines.push(`\nSummary: CRITICAL=${summary.CRITICAL} HIGH=${summary.HIGH} MEDIUM=${summary.MEDIUM} LOW=${summary.LOW}`);
1410
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1411
+ });
1412
+ // ═══ review_symbol ═══
1413
+ server.tool('review_symbol', 'Deep-dive single symbol risk: role, heat, dependents, test status, risk level.', { name: z.string(), repo: z.string().optional() }, async ({ name, repo }) => {
1414
+ const { db, root } = getDb(repo);
1415
+ const syms = db.findSymbolByName(name);
1416
+ if (syms.length === 0)
1417
+ return { content: [{ type: 'text', text: `"${name}" not found.` }] };
1418
+ const lines = [];
1419
+ for (const sym of syms) {
1420
+ const incoming = db.getIncomingLinks(sym.id).filter(l => l.type !== 'contains');
1421
+ const outgoing = db.getOutgoingLinks(sym.id).filter(l => l.type !== 'contains');
1422
+ const depsCount = incoming.length;
1423
+ const depsTop = incoming.slice(0, 5).map(l => { const s = db.findSymbolById(l.fromId); return s?.name ?? l.fromId; });
1424
+ const outCount = outgoing.length;
1425
+ const outTop = outgoing.slice(0, 5).map(l => { const s = db.findSymbolById(l.toId); return s?.name ?? l.toId; });
1426
+ const hasTest = sym.exported ? db.getSymbolTestCoverage(sym.id) : false;
1427
+ const heat = sym.heat ?? 0;
1428
+ const score = Math.round((heat / 100) * 40 + Math.min(depsCount / 10, 1) * 35 + (hasTest ? 0 : 25));
1429
+ let risk = 'LOW';
1430
+ if (score > 75)
1431
+ risk = 'CRITICAL';
1432
+ else if (score > 50)
1433
+ risk = 'HIGH';
1434
+ else if (score > 25)
1435
+ risk = 'MEDIUM';
1436
+ lines.push(`${sym.name} [${sym.kind}] ${sym.filePath}:${sym.startLine}`);
1437
+ lines.push(` role: ${sym.role ?? 'unknown'} | heat: ${heat} | exported: ${sym.exported}`);
1438
+ lines.push(` dependents: ${depsCount} ${depsTop.length ? '(' + depsTop.join(', ') + ')' : ''}`);
1439
+ lines.push(` dependencies: ${outCount} ${outTop.length ? '(' + outTop.join(', ') + ')' : ''}`);
1440
+ lines.push(` test coverage: ${hasTest ? 'yes' : 'no'}`);
1441
+ lines.push(` risk: ${risk} (score: ${score})`);
1442
+ if (risk === 'CRITICAL')
1443
+ lines.push(` recommendation: High risk — has ${depsCount} dependents with no test coverage. Write tests before modifying.`);
1444
+ else if (risk === 'HIGH')
1445
+ lines.push(` recommendation: Review dependents carefully before modifying.`);
1446
+ lines.push('');
1447
+ }
1448
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1449
+ });
1450
+ // ═══ test_coverage_gaps ═══
1451
+ server.tool('test_coverage_gaps', 'Untested exported symbols sorted by risk. Prioritize writing tests for these.', { limit: z.number().optional().default(20), repo: z.string().optional() }, async ({ limit, repo }) => {
1452
+ const { db } = getDb(repo);
1453
+ const coverage = db.getTestCoverage();
1454
+ const gaps = db.getTestCoverageGaps(limit);
1455
+ const lines = [`Test Coverage: ${coverage.testedSymbols}/${coverage.exportedProductionSymbols} (${coverage.exportedProductionSymbols > 0 ? Math.round(coverage.testedSymbols / coverage.exportedProductionSymbols * 100) : 0}%) from ${coverage.testFiles} test files\n`];
1456
+ if (gaps.length === 0) {
1457
+ lines.push('All exported symbols have test coverage!');
1458
+ }
1459
+ else {
1460
+ lines.push(`Top ${gaps.length} untested symbols:\n`);
1461
+ for (const g of gaps) {
1462
+ const incoming = db.getIncomingLinks(g.id).filter(l => l.type !== 'contains');
1463
+ const risk = (g.heat ?? 0) > 80 ? 'CRITICAL' : (g.heat ?? 0) > 50 ? 'HIGH' : (g.heat ?? 0) > 30 ? 'MEDIUM' : 'LOW';
1464
+ lines.push(` ${g.name} [${g.kind}] ${g.filePath}:${g.startLine} — heat:${g.heat ?? 0} deps:${incoming.length} risk:${risk}`);
1465
+ }
1466
+ }
1467
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1468
+ });
1469
+ // ═══ test_impact ═══
1470
+ server.tool('test_impact', 'Map changed code -> which test files to run. Use after making changes.', { ref: z.string().optional().default('HEAD'), repo: z.string().optional() }, async ({ ref, repo }) => {
1471
+ const { db, root } = getDb(repo);
1472
+ let changedFiles = [];
1473
+ try {
1474
+ const { execSync } = await import('node:child_process');
1475
+ const diff = execSync(`git diff --name-only ${ref}`, { cwd: root, encoding: 'utf-8' }).trim();
1476
+ changedFiles = diff ? diff.split('\n').filter(Boolean) : [];
1477
+ }
1478
+ catch { }
1479
+ if (changedFiles.length === 0)
1480
+ return { content: [{ type: 'text', text: 'No changed files.' }] };
1481
+ const changedIds = [];
1482
+ const changedNames = [];
1483
+ for (const file of changedFiles) {
1484
+ for (const sym of db.getSymbolsByFile(file)) {
1485
+ changedIds.push(sym.id);
1486
+ changedNames.push(sym.name);
1487
+ }
1488
+ }
1489
+ if (changedIds.length === 0)
1490
+ return { content: [{ type: 'text', text: 'No symbols in changed files.' }] };
1491
+ const impact = db.getTestImpact(changedIds);
1492
+ const lines = [`Changed symbols (${changedNames.length}): ${changedNames.join(', ')}`];
1493
+ lines.push(`\nAffected test files (${impact.testFiles.length}):`);
1494
+ for (const f of impact.testFiles)
1495
+ lines.push(` ${f}`);
1496
+ if (impact.testFiles.length > 0) {
1497
+ lines.push(`\nSuggested command: npx vitest run ${impact.testFiles.join(' ')}`);
1498
+ }
1499
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1500
+ });
1501
+ // ═══ test_plan ═══
1502
+ server.tool('test_plan', 'Generate a test strategy for a symbol: mock plan + >=3 test scenarios.', { name: z.string(), repo: z.string().optional() }, async ({ name, repo }) => {
1503
+ const { db } = getDb(repo);
1504
+ const plan = generateTestPlan(db, name);
1505
+ if (!plan)
1506
+ return { content: [{ type: 'text', text: `"${name}" not found.` }] };
1507
+ return { content: [{ type: 'text', text: plan.planText }] };
1508
+ });
1509
+ // ═══ annotate ═══
1510
+ server.tool('annotate', 'Record a note about a symbol for future sessions. Use after discovering bugs, patterns, or important caveats.', {
1511
+ symbol: z.string(),
1512
+ key: z.enum(['note', 'bug', 'security', 'architecture', 'workflow', 'test', 'dependency', 'refactor']),
1513
+ value: z.string(),
1514
+ agent: z.string().optional(),
1515
+ session_id: z.string().optional(),
1516
+ confidence: z.number().optional().default(0.5),
1517
+ }, async ({ symbol, key, value, agent, session_id, confidence }) => {
1518
+ const { db } = getDb();
1519
+ const store = new AnnotationStore(db.connection);
1520
+ const ann = store.annotate(symbol, key, value, { agent, sessionId: session_id });
1521
+ return { content: [{ type: 'text', text: `Annotation saved: ${ann.id}\n symbol: ${ann.symbol}\n key: ${ann.key}\n confidence: ${ann.confidence}` }] };
1522
+ });
1523
+ // ═══ recall ═══
1524
+ server.tool('recall', 'Retrieve annotations saved in previous sessions. Filter by symbol, key, or agent.', {
1525
+ symbol: z.string().optional(), key: z.enum(['note', 'bug', 'security', 'architecture', 'workflow', 'test', 'dependency', 'refactor']).optional(),
1526
+ agent: z.string().optional(), limit: z.number().optional().default(50),
1527
+ }, async ({ symbol, key, agent, limit }) => {
1528
+ const { db } = getDb();
1529
+ const store = new AnnotationStore(db.connection);
1530
+ const results = store.recall({ symbol, key, agent, limit });
1531
+ if (results.length === 0)
1532
+ return { content: [{ type: 'text', text: 'No annotations found.' }] };
1533
+ const lines = [`${results.length} annotation(s):\n`];
1534
+ for (const a of results) {
1535
+ lines.push(`[${a.key}] ${a.symbol} — ${a.value.slice(0, 120)}`);
1536
+ lines.push(` confidence: ${a.confidence.toFixed(1)} | agent: ${a.agent ?? '?'} | ${a.updatedAt}\n`);
1537
+ }
1538
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1539
+ });
1540
+ // ═══ session_start ═══
1541
+ server.tool('session_start', 'Start a new session. Returns a session ID to use with annotate, session_end, and handoff.', { agent: z.string().describe('Agent name (e.g. vibe-coder, reviewer)') }, async ({ agent }) => {
1542
+ const { db } = getDb();
1543
+ const store = new AnnotationStore(db.connection);
1544
+ const sessionId = store.sessionStart(agent);
1545
+ return { content: [{ type: 'text', text: `Session started: ${sessionId}\nAgent: ${agent}\nUse this ID with annotate() and session_end().` }] };
1546
+ });
1547
+ // ═══ session_context ═══
1548
+ server.tool('session_context', 'Get metadata about a session: annotations, tool calls, duration.', { session_id: z.string() }, async ({ session_id }) => {
1549
+ const { db } = getDb();
1550
+ const store = new AnnotationStore(db.connection);
1551
+ const ctx = store.sessionContext(session_id);
1552
+ if (!ctx.session)
1553
+ return { content: [{ type: 'text', text: `Session "${session_id}" not found.` }] };
1554
+ const s = ctx.session;
1555
+ const lines = [
1556
+ `Session: ${s.id}`,
1557
+ `Agent: ${s.agent} | Status: ${s.status}`,
1558
+ `Started: ${s.startedAt} | Ended: ${s.endedAt ?? 'in progress'}`,
1559
+ `Tool calls: ${s.toolCallsCount} | Annotations: ${s.annotationsCount}`,
1560
+ ];
1561
+ if (s.context)
1562
+ lines.push(`Context: ${s.context}`);
1563
+ if (ctx.annotations.length > 0) {
1564
+ lines.push(`\nAnnotations (${ctx.annotations.length}):`);
1565
+ for (const a of ctx.annotations) {
1566
+ lines.push(` [${a.key}] ${a.symbol}: ${a.value.slice(0, 80)}`);
1567
+ }
1568
+ }
1569
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1570
+ });
1571
+ // ═══ session_end ═══
1572
+ server.tool('session_end', 'End a session and record its stats. Use at the end of every session.', { session_id: z.string(), status: z.enum(['completed', 'failed']).optional().default('completed') }, async ({ session_id, status }) => {
1573
+ const { db } = getDb();
1574
+ const store = new AnnotationStore(db.connection);
1575
+ const summary = store.sessionEnd(session_id, status);
1576
+ return { content: [{ type: 'text', text: `Session ended: ${session_id}\nStatus: ${status}\nAnnotations: ${summary.annotationCount}` }] };
1577
+ });
1578
+ // ═══ handoff ═══
1579
+ server.tool('handoff', 'Transfer context from one agent session to another. Ends the source session and creates a new one for the target agent.', {
1580
+ from_session: z.string(), to_agent: z.string(),
1581
+ context: z.string().describe('Summary of what was done, key decisions, and caveats for the next agent'),
1582
+ }, async ({ from_session, to_agent, context }) => {
1583
+ const { db } = getDb();
1584
+ const store = new AnnotationStore(db.connection);
1585
+ const result = store.handoff(from_session, to_agent, context);
1586
+ return { content: [{ type: 'text', text: `Handoff complete.\nNew session: ${result.newSessionId}\nAgent: ${to_agent}\nAnnotations copied: ${result.annotationsCopied}` }] };
1587
+ });
1588
+ // ═══ semantic_search ═══
1589
+ server.tool('semantic_search', 'Search symbols by semantic meaning (falls back to FTS5 keyword search when embeddings unavailable).', { query: z.string(), limit: z.number().optional().default(10), repo: z.string().optional() }, async ({ query, limit, repo }) => {
1590
+ const { db } = getDb(repo);
1591
+ if (db.searchSymbols(query, limit).length > 0) {
1592
+ const results = db.searchSymbols(query, limit);
1593
+ const lines = [`Semantic search (FTS5 fallback — embeddings not available):\n`];
1594
+ for (const s of results) {
1595
+ lines.push(`${s.name} [${s.kind}] ${s.filePath}:${s.startLine}${s.exported ? ' (exported)' : ''}`);
1596
+ }
1597
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1598
+ }
1599
+ return { content: [{ type: 'text', text: `No results for "${query}". Embeddings not available. Run \`milens analyze --embeddings\` for semantic search.` }] };
1600
+ });
1601
+ // ═══ find_similar ═══
1602
+ server.tool('find_similar', 'Find symbols topologically similar to a given symbol (shared callers/callees). Useful for finding patterns to copy or refactor together.', { name: z.string(), limit: z.number().optional().default(10), repo: z.string().optional() }, async ({ name, limit, repo }) => {
1603
+ const { db } = getDb(repo);
1604
+ const syms = db.findSymbolByName(name);
1605
+ if (syms.length === 0)
1606
+ return { content: [{ type: 'text', text: `"${name}" not found.` }] };
1607
+ const results = db.findTopologicallySimilar(syms[0].id, limit);
1608
+ if (results.length === 0)
1609
+ return { content: [{ type: 'text', text: `No similar symbols found for "${name}".` }] };
1610
+ const lines = [`Symbols similar to "${name}":\n`];
1611
+ for (const r of results) {
1612
+ lines.push(` ${r.symbol.name} [${r.symbol.kind}] ${r.symbol.filePath}:${r.symbol.startLine} — similarity: ${r.similarity.toFixed(2)}`);
1613
+ }
1614
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
1615
+ });
1311
1616
  // ══════════════════════════════════════════════
1312
1617
  // ── MCP Resources ──
1313
1618
  // ══════════════════════════════════════════════
@@ -1462,6 +1767,185 @@ export function createMcpServer(rootPath) {
1462
1767
  },
1463
1768
  }],
1464
1769
  }));
1770
+ // ── Prompt: vibe-code-planner ──
1771
+ server.prompt('vibe-code-planner', 'ECC-style Planner Agent workflow: analyze codebase, create implementation plan with blast radius awareness', { feature: z.string().describe('Feature or task name to plan') }, ({ feature }) => ({
1772
+ messages: [{
1773
+ role: 'user',
1774
+ content: {
1775
+ type: 'text',
1776
+ text: `I am the Planner Agent. I need to create an implementation plan for "${feature}".\n\n` +
1777
+ `Follow this ECC Planner workflow:\n\n` +
1778
+ `PHASE 1 — CODEBASE INTELLIGENCE:\n` +
1779
+ `1. Run \`codebase_summary()\` to understand the project structure\n` +
1780
+ `2. Run \`domains()\` to see module clusters\n` +
1781
+ `3. Run \`routes()\` to find relevant API endpoints\n\n` +
1782
+ `PHASE 2 — TARGET ANALYSIS:\n` +
1783
+ `4. Run \`smart_context({name: "keySymbol", intent: "edit"})\` for each affected symbol\n` +
1784
+ `5. Run \`edit_check({name: "keySymbol"})\` for safety\n` +
1785
+ `6. Run \`trace({to: "keySymbol"})\` to understand execution flow\n\n` +
1786
+ `PHASE 3 — IMPACT PREDICTION:\n` +
1787
+ `7. Run \`impact({target: "keySymbol", depth: 3})\` to see blast radius\n` +
1788
+ `8. Run \`explain_relationship({from: "A", to: "B"})\` for distant dependencies\n\n` +
1789
+ `PHASE 4 — TEST STRATEGY:\n` +
1790
+ `9. Run \`test_plan({name: "keySymbol"})\` for mock strategy\n` +
1791
+ `10. Run \`test_coverage_gaps()\` to check existing coverage\n\n` +
1792
+ `PHASE 5 — FINAL PLAN:\n` +
1793
+ `Output a plan.md with: Overview, Architecture Changes, Implementation Steps (file+action+why+deps+risk), Testing Strategy, Risks & Mitigations, Success Criteria.\n\n` +
1794
+ `Use the ECC plan format with specific file paths, dependencies, and risk levels (LOW/MEDIUM/HIGH).`,
1795
+ },
1796
+ }],
1797
+ }));
1798
+ // ── Prompt: vibe-code-reviewer ──
1799
+ server.prompt('vibe-code-reviewer', 'ECC-style Reviewer Agent workflow: PR risk assessment, dead code detection, security scan', { session_id: z.string().optional().describe('Optional session ID for annotation context') }, ({ session_id }) => ({
1800
+ messages: [{
1801
+ role: 'user',
1802
+ content: {
1803
+ type: 'text',
1804
+ text: `I am the Reviewer Agent. Review the current changes thoroughly.${session_id ? ` Session: ${session_id}` : ''}\n\n` +
1805
+ `Follow this ECC Reviewer workflow:\n\n` +
1806
+ `1. Run \`review_pr()\` to get risk scores for all changed symbols\n` +
1807
+ `2. For each CRITICAL/HIGH symbol:\n` +
1808
+ ` a. Run \`review_symbol({name})\` for deep dive\n` +
1809
+ ` b. Run \`context({name})\` to see relationships\n` +
1810
+ ` c. Run \`grep({pattern: "symbolName"})\` for text references\n` +
1811
+ `3. Run \`find_dead_code()\` to detect orphaned symbols\n` +
1812
+ `4. Run \`grep({pattern: "password|secret|api_key|token", scope: "code"})\` for secrets\n` +
1813
+ `5. Run \`grep({pattern: "TODO|FIXME|HACK|console\\\\.log", scope: "code"})\` for tech debt\n` +
1814
+ `6. Run \`detect_changes()\` to verify expected files only\n` +
1815
+ `7. Create a review report: symbols OK to merge vs symbols needing fixes\n` +
1816
+ `8. Run \`annotate({symbol, key: "bug"|"security", value})\` for any critical findings`,
1817
+ },
1818
+ }],
1819
+ }));
1820
+ // ── Prompt: closed-loop-session ──
1821
+ server.prompt('closed-loop-session', 'Complete 6-phase closed-loop session: Analyze → Plan → Code → Verify → Learn → Improve', { task: z.string().describe('Task description'), agent: z.string().optional().default('vibe-coder') }, ({ task, agent }) => ({
1822
+ messages: [{
1823
+ role: 'user',
1824
+ content: {
1825
+ type: 'text',
1826
+ text: `Run a complete closed-loop development session for: "${task}"\n\n` +
1827
+ `PHASE 1 — ANALYZE (bootstrap):\n` +
1828
+ ` session_start({agent: "${agent}"}) → codebase_summary() → domains() → recall()\n\n` +
1829
+ `PHASE 2 — PLAN:\n` +
1830
+ ` smart_context({intent: "edit"}) → edit_check() → impact({depth: 3}) → test_plan()\n\n` +
1831
+ `PHASE 3 — CODE:\n` +
1832
+ ` Implement changes with guard: edit_check() before each edit, impact() mid-edit, context() for reference\n\n` +
1833
+ `PHASE 4 — VERIFY:\n` +
1834
+ ` detect_changes() → test_impact() → review_pr() → test_coverage_gaps() → grep(secrets)\n\n` +
1835
+ `PHASE 5 — LEARN:\n` +
1836
+ ` annotate() key observations → session_context() → handoff() if needed\n\n` +
1837
+ `PHASE 6 — IMPROVE:\n` +
1838
+ ` milens evolve (if patterns ready) → milens metrics (check health)\n\n` +
1839
+ `At the end: session_end({session_id}) to record stats.`,
1840
+ },
1841
+ }],
1842
+ }));
1843
+ // ── Register MCP Prompts (W1) ──
1844
+ registerAllPrompts(server);
1845
+ // ── Tool: security_scan (S2) ──
1846
+ server.tool('security_scan', 'Scan codebase for security vulnerabilities using 50+ built-in rules. Replaces multiple manual grep() calls. Categories: secrets, injection, unicode, dangerous, config, data-leak, crypto, auth, file-access.', {
1847
+ scope: z.enum(['all', 'secrets', 'injection', 'unicode', 'dangerous', 'config', 'data-leak', 'crypto', 'auth', 'file-access']).optional().default('all').describe('Scan scope'),
1848
+ repo: z.string().optional().describe('Repository root path'),
1849
+ severity: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).optional().describe('Minimum severity filter'),
1850
+ limit: z.number().optional().default(50).describe('Max findings'),
1851
+ }, async ({ scope, repo, severity, limit }) => {
1852
+ const { db, root } = getDb(repo);
1853
+ const rules = loadRules();
1854
+ // Filter rules by scope and severity
1855
+ const filtered = rules.filter(r => {
1856
+ if (scope !== 'all' && r.category !== scope)
1857
+ return false;
1858
+ if (severity) {
1859
+ const sevOrder = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
1860
+ if ((sevOrder[r.severity] || 0) < (sevOrder[severity] || 0))
1861
+ return false;
1862
+ }
1863
+ return r.enabled;
1864
+ });
1865
+ // Get all source files from the DB
1866
+ const symbols = db.getAllSymbols();
1867
+ const fileSet = new Set();
1868
+ for (const s of symbols) {
1869
+ if (s.filePath && !s.filePath.includes('node_modules') && !s.filePath.includes('.git')) {
1870
+ fileSet.add(s.filePath);
1871
+ }
1872
+ }
1873
+ const files = [...fileSet].slice(0, 1000); // cap at 1000 files
1874
+ const { readFileSync: rfs, existsSync: es } = await import('node:fs');
1875
+ const { resolve: resolvePath } = await import('node:path');
1876
+ const findings = [];
1877
+ const byCategory = {};
1878
+ const bySeverity = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
1879
+ for (const file of files) {
1880
+ const fullPath = resolvePath(root, file);
1881
+ if (!es(fullPath))
1882
+ continue;
1883
+ // Skip files that don't match rule fileGlobs (simple check)
1884
+ const applicableRules = filtered.filter(r => {
1885
+ if (!r.fileGlob)
1886
+ return true;
1887
+ // Simple glob: just check extension
1888
+ const ext = r.fileGlob.replace('**/*.', '').replace('**/*', '');
1889
+ return file.endsWith(ext) || r.fileGlob === '**/*';
1890
+ });
1891
+ if (applicableRules.length === 0)
1892
+ continue;
1893
+ try {
1894
+ const content = rfs(fullPath, 'utf-8');
1895
+ const lines = content.split('\n');
1896
+ for (const rule of applicableRules) {
1897
+ for (const pattern of rule.patterns) {
1898
+ let match;
1899
+ // Reset regex lastIndex for global patterns
1900
+ pattern.lastIndex = 0;
1901
+ while ((match = pattern.exec(content)) !== null) {
1902
+ const lineNum = content.substring(0, match.index).split('\n').length;
1903
+ const ctxStart = Math.max(0, lineNum - 3);
1904
+ const ctxEnd = Math.min(lines.length, lineNum + 2);
1905
+ const context = lines.slice(ctxStart, ctxEnd).join('\n');
1906
+ findings.push({
1907
+ ruleId: rule.id,
1908
+ category: rule.category,
1909
+ severity: rule.severity,
1910
+ owasp: rule.owasp,
1911
+ file,
1912
+ line: lineNum,
1913
+ match: match[0].length > 100 ? match[0].slice(0, 97) + '...' : match[0],
1914
+ context,
1915
+ fix: rule.fix,
1916
+ });
1917
+ byCategory[rule.category] = (byCategory[rule.category] || 0) + 1;
1918
+ bySeverity[rule.severity] = (bySeverity[rule.severity] || 0) + 1;
1919
+ }
1920
+ }
1921
+ }
1922
+ }
1923
+ catch {
1924
+ // Skip unreadable files
1925
+ }
1926
+ }
1927
+ // Calculate security score (100 - deductions)
1928
+ const deduction = findings.filter((f) => f.severity === 'CRITICAL').length * 5 +
1929
+ findings.filter((f) => f.severity === 'HIGH').length * 2 +
1930
+ findings.filter((f) => f.severity === 'MEDIUM').length * 0.5;
1931
+ const score = Math.max(0, Math.round(100 - deduction));
1932
+ const limited = findings.slice(0, limit);
1933
+ return {
1934
+ content: [{
1935
+ type: 'text',
1936
+ text: JSON.stringify({
1937
+ summary: {
1938
+ totalScanned: files.length,
1939
+ findings: findings.length,
1940
+ byCategory,
1941
+ bySeverity,
1942
+ score,
1943
+ },
1944
+ findings: limited,
1945
+ }, null, 2),
1946
+ }],
1947
+ };
1948
+ });
1465
1949
  return server;
1466
1950
  }
1467
1951
  // ── Transport: stdio ──