@jigyasudham/veto 0.8.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +209 -52
  2. package/dist/agents/executor.js +36 -3
  3. package/dist/cli.js +350 -51
  4. package/dist/context/reader.js +113 -0
  5. package/dist/council/index.js +3 -1
  6. package/dist/plugins/loader.js +49 -0
  7. package/dist/router/index.js +2 -2
  8. package/dist/router/learning-updater.js +45 -1
  9. package/dist/server.js +478 -14
  10. package/dist/watcher/index.js +77 -0
  11. package/dist/workflow/pipeline.js +64 -0
  12. package/package.json +12 -3
  13. package/.claude/settings.local.json +0 -9
  14. package/src/adapters/claude.ts +0 -70
  15. package/src/adapters/codex.ts +0 -71
  16. package/src/adapters/gemini.ts +0 -71
  17. package/src/adapters/index.ts +0 -217
  18. package/src/agents/development/api.ts +0 -120
  19. package/src/agents/development/backend.ts +0 -85
  20. package/src/agents/development/coder.ts +0 -213
  21. package/src/agents/development/database.ts +0 -83
  22. package/src/agents/development/debugger.ts +0 -238
  23. package/src/agents/development/devops.ts +0 -86
  24. package/src/agents/development/frontend.ts +0 -85
  25. package/src/agents/development/migration.ts +0 -144
  26. package/src/agents/development/performance.ts +0 -144
  27. package/src/agents/development/refactor.ts +0 -86
  28. package/src/agents/development/reviewer.ts +0 -268
  29. package/src/agents/development/tester.ts +0 -151
  30. package/src/agents/executor.ts +0 -158
  31. package/src/agents/memory/context-manager.ts +0 -171
  32. package/src/agents/memory/decision-logger.ts +0 -160
  33. package/src/agents/memory/knowledge-base.ts +0 -124
  34. package/src/agents/memory/pattern-learner.ts +0 -143
  35. package/src/agents/memory/project-mapper.ts +0 -118
  36. package/src/agents/quality/accessibility.ts +0 -99
  37. package/src/agents/quality/code-quality.ts +0 -115
  38. package/src/agents/quality/compatibility.ts +0 -58
  39. package/src/agents/quality/documentation.ts +0 -105
  40. package/src/agents/quality/error-handling.ts +0 -96
  41. package/src/agents/research/competitor-analyzer.ts +0 -45
  42. package/src/agents/research/cost-analyzer.ts +0 -54
  43. package/src/agents/research/estimator.ts +0 -60
  44. package/src/agents/research/ethics-bias.ts +0 -113
  45. package/src/agents/research/researcher.ts +0 -114
  46. package/src/agents/research/risk-assessor.ts +0 -63
  47. package/src/agents/research/tech-advisor.ts +0 -55
  48. package/src/agents/security/auth.ts +0 -287
  49. package/src/agents/security/dependency-audit.ts +0 -337
  50. package/src/agents/security/penetration.ts +0 -262
  51. package/src/agents/security/privacy.ts +0 -285
  52. package/src/agents/security/scanner.ts +0 -322
  53. package/src/agents/security/secrets.ts +0 -249
  54. package/src/agents/types.ts +0 -66
  55. package/src/agents/workflow/automation.ts +0 -59
  56. package/src/agents/workflow/file-manager.ts +0 -52
  57. package/src/agents/workflow/git-agent.ts +0 -55
  58. package/src/agents/workflow/reporter.ts +0 -51
  59. package/src/agents/workflow/search-agent.ts +0 -40
  60. package/src/agents/workflow/task-coordinator.ts +0 -41
  61. package/src/agents/workflow/task-planner.ts +0 -47
  62. package/src/cli.ts +0 -135
  63. package/src/council/decision-engine.ts +0 -171
  64. package/src/council/devil-advocate.ts +0 -116
  65. package/src/council/index.ts +0 -44
  66. package/src/council/lead-developer.ts +0 -118
  67. package/src/council/legal-compliance.ts +0 -152
  68. package/src/council/product-manager.ts +0 -102
  69. package/src/council/security.ts +0 -172
  70. package/src/council/system-architect.ts +0 -132
  71. package/src/council/types.ts +0 -33
  72. package/src/council/ux-designer.ts +0 -121
  73. package/src/memory/local.ts +0 -305
  74. package/src/memory/schema.ts +0 -174
  75. package/src/memory/sync.ts +0 -274
  76. package/src/router/complexity-scorer.ts +0 -96
  77. package/src/router/context-compressor.ts +0 -74
  78. package/src/router/index.ts +0 -60
  79. package/src/router/learning-updater.ts +0 -271
  80. package/src/router/model-selector.ts +0 -83
  81. package/src/router/rate-monitor.ts +0 -103
  82. package/src/server.ts +0 -1038
  83. package/src/skills/development/skill-api-design.ts +0 -329
  84. package/src/skills/development/skill-auth.ts +0 -271
  85. package/src/skills/development/skill-ci-cd.ts +0 -0
  86. package/src/skills/development/skill-crud.ts +0 -209
  87. package/src/skills/development/skill-db-schema.ts +0 -0
  88. package/src/skills/development/skill-docker.ts +0 -0
  89. package/src/skills/development/skill-env-setup.ts +0 -0
  90. package/src/skills/development/skill-scaffold.ts +0 -323
  91. package/src/skills/intelligence/skill-complexity-score.ts +0 -69
  92. package/src/skills/intelligence/skill-cost-track.ts +0 -39
  93. package/src/skills/intelligence/skill-learning-loop.ts +0 -69
  94. package/src/skills/intelligence/skill-pattern-detect.ts +0 -38
  95. package/src/skills/intelligence/skill-rate-watch.ts +0 -61
  96. package/src/skills/memory/skill-context-compress.ts +0 -98
  97. package/src/skills/memory/skill-cross-sync.ts +0 -104
  98. package/src/skills/memory/skill-decision-log.ts +0 -119
  99. package/src/skills/memory/skill-session-restore.ts +0 -59
  100. package/src/skills/memory/skill-session-save.ts +0 -94
  101. package/src/skills/quality/skill-accessibility.ts +0 -0
  102. package/src/skills/quality/skill-code-review.ts +0 -84
  103. package/src/skills/quality/skill-docs-gen.ts +0 -0
  104. package/src/skills/quality/skill-perf-audit.ts +0 -0
  105. package/src/skills/quality/skill-security-scan.ts +0 -91
  106. package/src/skills/quality/skill-test-suite.ts +0 -290
  107. package/src/skills/workflow/skill-deploy.ts +0 -0
  108. package/src/skills/workflow/skill-git-workflow.ts +0 -0
  109. package/src/skills/workflow/skill-rollback.ts +0 -0
  110. package/src/skills/workflow/skill-task-breakdown.ts +0 -0
  111. package/tsconfig.json +0 -20
package/dist/server.js CHANGED
@@ -5,17 +5,25 @@
5
5
  process.removeAllListeners('warning');
6
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
9
+ import { buildContextString } from './context/reader.js';
9
10
  import { saveSession, restoreSession, listSessions, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, } from './memory/local.js';
10
11
  import { exportMemory, importMemory } from './memory/sync.js';
11
12
  import { runDebate } from './council/index.js';
12
- import { routeTask, getRateStatus, recordOutcome, getLearningStats, applyLearnedThresholds, getAgentPerformanceStats, getTaskTypeBreakdown, getCouncilInsights } from './router/index.js';
13
+ import { routeTask, getRateStatus, recordOutcome, getLearningStats, applyLearnedThresholds, getAgentPerformanceStats, getTaskTypeBreakdown, getCouncilInsights, getRecommendedAgent } from './router/index.js';
13
14
  import { executeParallel, executeOne } from './agents/executor.js';
14
15
  import { handoff, continueSession, getPlatformSetup } from './adapters/index.js';
15
- const VERSION = '0.8.0';
16
+ import { startWatch, pollWatch, stopWatch } from './watcher/index.js';
17
+ import { runPipeline } from './workflow/pipeline.js';
18
+ import { loadPlugins, listPlugins } from './plugins/loader.js';
19
+ import { readFileSync } from 'node:fs';
20
+ import { extname, basename } from 'node:path';
21
+ const VERSION = '1.0.0';
16
22
  const server = new Server({ name: 'veto', version: VERSION }, {
17
23
  capabilities: {
18
24
  tools: {},
25
+ resources: {},
26
+ prompts: {},
19
27
  },
20
28
  });
21
29
  // ─── Tool Definitions ─────────────────────────────────────────────────────────
@@ -158,6 +166,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
158
166
  type: 'string',
159
167
  description: 'Optional: additional context such as codebase state, prior decisions, or constraints.',
160
168
  },
169
+ project_dir: {
170
+ type: 'string',
171
+ description: 'Optional: absolute path to the project directory. Veto will auto-read package.json, git diff, and stack info to give the council real project context.',
172
+ },
161
173
  session_id: {
162
174
  type: 'string',
163
175
  description: 'Optional: session ID to associate this council outcome with an active session.',
@@ -179,6 +191,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
179
191
  },
180
192
  task: { type: 'string', description: 'The task for the agent to plan.' },
181
193
  context: { type: 'string', description: 'Optional additional context.' },
194
+ project_dir: { type: 'string', description: 'Optional: absolute path to the project directory. Auto-injects package.json, git diff, and stack info into the agent context.' },
182
195
  },
183
196
  required: ['agent', 'task'],
184
197
  },
@@ -195,6 +208,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
195
208
  required: ['code'],
196
209
  },
197
210
  },
211
+ {
212
+ name: 'veto_diff_review',
213
+ description: 'Reviews a git diff — runs code review, security scan, and secrets scan in parallel across all changed files. Returns a structured verdict (pass/warn/fail), per-file findings, and a CI-ready summary. Pass diff directly or let Veto read it from project_dir automatically.',
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ diff: { type: 'string', description: 'The git diff to review. If omitted, Veto runs git diff HEAD in project_dir.' },
218
+ project_dir: { type: 'string', description: 'Absolute project path. Used to auto-read git diff if diff is not provided, and to inject codebase context.' },
219
+ context: { type: 'string', description: 'Optional: PR description, ticket number, or focus area.' },
220
+ },
221
+ required: [],
222
+ },
223
+ },
198
224
  {
199
225
  name: 'veto_security_scan',
200
226
  description: 'Runs the Security Scanner (OWASP Top 10) on provided code. Returns vulnerabilities with severity, CWE/OWASP category, and remediation steps.',
@@ -235,10 +261,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
235
261
  task: { type: 'string', description: 'Task description for this agent.' },
236
262
  code: { type: 'string', description: 'Optional code to analyze (triggers analyze() instead of plan()).' },
237
263
  context: { type: 'string', description: 'Optional additional context.' },
264
+ project_dir: { type: 'string', description: 'Optional: per-task project dir override.' },
238
265
  },
239
266
  required: ['id', 'agent', 'task'],
240
267
  },
241
268
  },
269
+ project_dir: { type: 'string', description: 'Optional: project directory applied to all tasks (per-task project_dir overrides this). Auto-injects codebase context.' },
242
270
  },
243
271
  required: ['tasks'],
244
272
  },
@@ -391,6 +419,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
391
419
  output_quality: { type: 'number', description: 'Output quality score 0–100. 90–100=excellent, 70–89=good, 50–69=acceptable, 30–49=poor, 0–29=failed.' },
392
420
  agent: { type: 'string', description: 'The worker agent type used (optional but useful for agent performance tracking).' },
393
421
  tokens_used: { type: 'number', description: 'Approximate tokens used (optional).' },
422
+ file_ext: { type: 'string', description: 'File extension of the primary file worked on (e.g. ".ts", ".sql", ".tsx"). Enables predictive agent routing — next time you work on the same extension, veto_route_task will recommend the best agent.' },
394
423
  },
395
424
  required: ['task_type', 'complexity', 'model_tier', 'output_quality'],
396
425
  },
@@ -457,6 +486,88 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
457
486
  required: ['platform', 'veto_server_path'],
458
487
  },
459
488
  },
489
+ {
490
+ name: 'veto_watch',
491
+ description: 'Starts a file watcher on a project directory. Returns a watch_id. Call veto_watch_poll to collect file-change events with recommended agents. Call veto_watch_stop when done.',
492
+ inputSchema: {
493
+ type: 'object',
494
+ properties: {
495
+ project_dir: { type: 'string', description: 'Absolute path to the project directory to watch.' },
496
+ },
497
+ required: ['project_dir'],
498
+ },
499
+ },
500
+ {
501
+ name: 'veto_watch_poll',
502
+ description: 'Polls for file-change events from an active watcher. Returns accumulated events since last poll (events are cleared on read). Each event includes the file, recommended agent, and suggested veto tool to call.',
503
+ inputSchema: {
504
+ type: 'object',
505
+ properties: {
506
+ watch_id: { type: 'string', description: 'The watch_id returned by veto_watch.' },
507
+ },
508
+ required: ['watch_id'],
509
+ },
510
+ },
511
+ {
512
+ name: 'veto_watch_stop',
513
+ description: 'Stops an active file watcher.',
514
+ inputSchema: {
515
+ type: 'object',
516
+ properties: {
517
+ watch_id: { type: 'string', description: 'The watch_id returned by veto_watch.' },
518
+ },
519
+ required: ['watch_id'],
520
+ },
521
+ },
522
+ {
523
+ name: 'veto_workflow',
524
+ description: 'Runs a sequential agent pipeline with optional pass/fail gates between steps. Each step runs a worker agent; if a gate score is set and the step confidence falls below it, the pipeline stops. Returns per-step results plus an overall verdict (passed/partial/failed).',
525
+ inputSchema: {
526
+ type: 'object',
527
+ properties: {
528
+ steps: {
529
+ type: 'array',
530
+ description: 'Ordered pipeline steps.',
531
+ items: {
532
+ type: 'object',
533
+ properties: {
534
+ id: { type: 'string', description: 'Step identifier.' },
535
+ agent: { type: 'string', description: 'Worker agent type.' },
536
+ task: { type: 'string', description: 'Task description for this step.' },
537
+ code: { type: 'string', description: 'Optional code to analyze.' },
538
+ context: { type: 'string', description: 'Optional context.' },
539
+ gate: { type: 'number', description: 'Optional minimum confidence % (0–100) required to proceed to the next step.' },
540
+ },
541
+ required: ['id', 'agent', 'task'],
542
+ },
543
+ },
544
+ project_dir: { type: 'string', description: 'Optional project directory — auto-injects codebase context into all steps.' },
545
+ },
546
+ required: ['steps'],
547
+ },
548
+ },
549
+ {
550
+ name: 'veto_explain',
551
+ description: 'Reads a file and returns an expert explanation from the most appropriate agent (auto-detected from file extension and name). Pass depth to control detail level.',
552
+ inputSchema: {
553
+ type: 'object',
554
+ properties: {
555
+ file_path: { type: 'string', description: 'Absolute path to the file to explain.' },
556
+ depth: { type: 'string', enum: ['overview', 'detailed', 'line-by-line'], description: 'Explanation depth. Default: overview.' },
557
+ context: { type: 'string', description: 'Optional additional context about what you want explained.' },
558
+ },
559
+ required: ['file_path'],
560
+ },
561
+ },
562
+ {
563
+ name: 'veto_plugins',
564
+ description: 'Lists all custom agents loaded from ~/.veto/agents/. Drop a .js file there that exports plan(task, context?) to register a new agent available in veto_agent_plan and veto_execute_parallel.',
565
+ inputSchema: {
566
+ type: 'object',
567
+ properties: {},
568
+ required: [],
569
+ },
570
+ },
460
571
  ],
461
572
  }));
462
573
  // ─── Tool Handlers ────────────────────────────────────────────────────────────
@@ -563,20 +674,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
563
674
  };
564
675
  }
565
676
  case 'veto_route_task': {
566
- const result = routeTask(String(args?.task ?? ''), {
677
+ const routeTaskStr = String(args?.task ?? '');
678
+ const fileExt = args?.file_ext ? String(args.file_ext) : undefined;
679
+ const result = routeTask(routeTaskStr, {
567
680
  agentType: args?.agent_type ? String(args.agent_type) : undefined,
568
681
  filesAffected: typeof args?.files_affected === 'number' ? args.files_affected : undefined,
569
682
  forceCouncil: args?.force_council === true,
570
683
  context: args?.context ? String(args.context) : undefined,
571
684
  preferredPlatform: args?.preferred_platform ? String(args.preferred_platform) : 'claude',
572
685
  });
686
+ const recommended_agent = getRecommendedAgent(routeTaskStr, fileExt);
573
687
  return {
574
- content: [
575
- {
688
+ content: [{
576
689
  type: 'text',
577
- text: JSON.stringify(result, null, 2),
578
- },
579
- ],
690
+ text: JSON.stringify({ ...result, recommended_agent }, null, 2),
691
+ }],
580
692
  };
581
693
  }
582
694
  case 'veto_rate_status': {
@@ -600,6 +712,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
600
712
  const result = await runDebate({
601
713
  task,
602
714
  context: args?.context ? String(args.context) : undefined,
715
+ project_dir: args?.project_dir ? String(args.project_dir) : undefined,
603
716
  });
604
717
  const outcomeId = saveCouncilOutcome({
605
718
  session_id: args?.session_id ? String(args.session_id) : undefined,
@@ -645,11 +758,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
645
758
  if (!task) {
646
759
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'task is required.' }) }], isError: true };
647
760
  }
648
- const result = await executeOne({ id: 'plan-1', agent: agentType, task, context: args?.context ? String(args.context) : undefined });
761
+ const result = await executeOne({
762
+ id: 'plan-1',
763
+ agent: agentType,
764
+ task,
765
+ context: args?.context ? String(args.context) : undefined,
766
+ project_dir: args?.project_dir ? String(args.project_dir) : undefined,
767
+ });
649
768
  if (result.error) {
650
769
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: result.error }) }], isError: true };
651
770
  }
652
- return { content: [{ type: 'text', text: JSON.stringify(result.plan ?? result.analysis, null, 2) }] };
771
+ return { content: [{ type: 'text', text: JSON.stringify({ ...(result.plan ?? result.analysis), output: result.output }, null, 2) }] };
653
772
  }
654
773
  case 'veto_code_review': {
655
774
  const code = String(args?.code ?? '').trim();
@@ -662,6 +781,92 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
662
781
  }
663
782
  return { content: [{ type: 'text', text: JSON.stringify(result.analysis, null, 2) }] };
664
783
  }
784
+ case 'veto_diff_review': {
785
+ const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
786
+ const userContext = args?.context ? String(args.context) : undefined;
787
+ // Resolve diff — use provided or read from git
788
+ let diff = args?.diff ? String(args.diff).trim() : '';
789
+ if (!diff && projectDir) {
790
+ const { execSync: execSyncDiff } = await import('node:child_process');
791
+ try {
792
+ diff = execSyncDiff('git diff HEAD --no-color', {
793
+ cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
794
+ }).toString().trim();
795
+ if (!diff) {
796
+ diff = execSyncDiff('git diff --cached --no-color', {
797
+ cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
798
+ }).toString().trim();
799
+ }
800
+ }
801
+ catch { /* not a git repo or no changes */ }
802
+ }
803
+ if (!diff) {
804
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'No diff provided and no git changes detected. Pass diff or point to a project_dir with uncommitted changes.' }) }], isError: true };
805
+ }
806
+ // Parse changed files from diff header lines
807
+ const changedFiles = [...diff.matchAll(/^diff --git a\/.+ b\/(.+)$/gm)].map(m => m[1]);
808
+ const diffChunks = diff.split(/^diff --git /m).filter(Boolean);
809
+ // Run code-review, security-scan, secrets-scan in parallel across the full diff
810
+ const context = buildContextString(projectDir, userContext);
811
+ const [reviewResult, secResult, secretsResult] = await Promise.all([
812
+ executeOne({ id: 'diff-review', agent: 'reviewer', task: 'Review this git diff for code quality issues', code: diff, context }),
813
+ executeOne({ id: 'diff-sec', agent: 'security-scanner', task: 'Scan this git diff for security vulnerabilities', code: diff, context }),
814
+ executeOne({ id: 'diff-secrets', agent: 'secrets', task: 'Scan this git diff for exposed secrets or credentials', code: diff }),
815
+ ]);
816
+ // Derive overall verdict
817
+ const hasBlocking = (reviewResult.analysis?.critical_count ?? 0) > 0
818
+ || (secResult.analysis?.critical_count ?? 0) > 0
819
+ || (secretsResult.analysis?.critical_count ?? 0) > 0;
820
+ const hasWarnings = (reviewResult.analysis?.high_count ?? 0) > 0
821
+ || (secResult.analysis?.high_count ?? 0) > 0;
822
+ const verdict = hasBlocking ? 'fail' : hasWarnings ? 'warn' : 'pass';
823
+ const verdictEmoji = verdict === 'pass' ? '✅ PASS' : verdict === 'warn' ? '⚠️ WARN' : '❌ FAIL';
824
+ // Per-file finding counts (approximate from line refs)
825
+ const fileFindings = {};
826
+ for (const f of changedFiles)
827
+ fileFindings[f] = 0;
828
+ for (const finding of [...(reviewResult.analysis?.findings ?? []), ...(secResult.analysis?.findings ?? [])]) {
829
+ const match = changedFiles.find(f => finding.location?.includes(f));
830
+ if (match)
831
+ fileFindings[match]++;
832
+ }
833
+ return {
834
+ content: [{
835
+ type: 'text',
836
+ text: JSON.stringify({
837
+ verdict,
838
+ verdict_label: verdictEmoji,
839
+ files_changed: changedFiles.length,
840
+ files: changedFiles,
841
+ file_findings: fileFindings,
842
+ code_review: {
843
+ score: reviewResult.analysis?.score ?? null,
844
+ verdict: reviewResult.analysis?.verdict ?? null,
845
+ critical: reviewResult.analysis?.critical_count ?? 0,
846
+ high: reviewResult.analysis?.high_count ?? 0,
847
+ findings: reviewResult.analysis?.findings ?? [],
848
+ },
849
+ security: {
850
+ score: secResult.analysis?.score ?? null,
851
+ verdict: secResult.analysis?.verdict ?? null,
852
+ critical: secResult.analysis?.critical_count ?? 0,
853
+ high: secResult.analysis?.high_count ?? 0,
854
+ findings: secResult.analysis?.findings ?? [],
855
+ },
856
+ secrets: {
857
+ verdict: secretsResult.analysis?.verdict ?? null,
858
+ findings: secretsResult.analysis?.findings ?? [],
859
+ },
860
+ summary: [
861
+ `${verdictEmoji} — ${changedFiles.length} file(s) changed`,
862
+ `Code: ${reviewResult.analysis?.verdict ?? 'n/a'} (score ${reviewResult.analysis?.score ?? '?'}/100)`,
863
+ `Security: ${secResult.analysis?.verdict ?? 'n/a'} — ${secResult.analysis?.critical_count ?? 0} critical, ${secResult.analysis?.high_count ?? 0} high`,
864
+ `Secrets: ${(secretsResult.analysis?.findings?.length ?? 0) > 0 ? '🔴 Exposed credentials detected' : '✅ Clean'}`,
865
+ ].join('\n'),
866
+ }, null, 2),
867
+ }],
868
+ };
869
+ }
665
870
  case 'veto_security_scan': {
666
871
  const code = String(args?.code ?? '').trim();
667
872
  if (!code) {
@@ -689,12 +894,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
689
894
  if (rawTasks.length === 0) {
690
895
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'tasks array is required and must not be empty.' }) }], isError: true };
691
896
  }
897
+ const parallelProjectDir = args?.project_dir ? String(args.project_dir) : undefined;
692
898
  const tasks = rawTasks.map((t) => ({
693
899
  id: String(t.id ?? ''),
694
900
  agent: String(t.agent ?? ''),
695
901
  task: String(t.task ?? ''),
696
902
  code: t.code ? String(t.code) : undefined,
697
903
  context: t.context ? String(t.context) : undefined,
904
+ project_dir: t.project_dir ? String(t.project_dir) : parallelProjectDir,
698
905
  }));
699
906
  const results = await executeParallel(tasks);
700
907
  return {
@@ -708,7 +915,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
708
915
  agent: r.agent,
709
916
  duration_ms: r.duration_ms,
710
917
  error: r.error,
711
- output: r.plan ?? r.analysis,
918
+ output: { ...(r.plan ?? r.analysis), structured: r.output },
712
919
  })),
713
920
  }, null, 2),
714
921
  }],
@@ -892,7 +1099,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
892
1099
  if (!task_type) {
893
1100
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'task_type is required.' }) }], isError: true };
894
1101
  }
895
- recordOutcome(task_type, complexity, model_tier, args?.agent ? String(args.agent) : 'dynamic', output_quality, typeof args?.tokens_used === 'number' ? args.tokens_used : 0);
1102
+ recordOutcome(task_type, complexity, model_tier, args?.agent ? String(args.agent) : 'dynamic', output_quality, typeof args?.tokens_used === 'number' ? args.tokens_used : 0, args?.file_ext ? String(args.file_ext) : undefined);
896
1103
  const stats = getLearningStats();
897
1104
  return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Outcome recorded.', total_outcomes: stats.total_tasks, next_step: stats.total_tasks >= 20 ? 'You have 20+ outcomes. Call veto_learning_apply to update router thresholds.' : `Need ${20 - stats.total_tasks} more outcomes before veto_learning_apply can adjust thresholds.` }, null, 2) }] };
898
1105
  }
@@ -931,15 +1138,272 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
931
1138
  const result = importMemory(args?.input_path ? String(args.input_path) : undefined);
932
1139
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
933
1140
  }
1141
+ case 'veto_watch': {
1142
+ const dir = String(args?.project_dir ?? '').trim();
1143
+ if (!dir)
1144
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
1145
+ const watch_id = startWatch(dir);
1146
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, watch_id, project_dir: dir, message: `Watching "${dir}". Call veto_watch_poll with watch_id to collect events.` }, null, 2) }] };
1147
+ }
1148
+ case 'veto_watch_poll': {
1149
+ const watch_id = String(args?.watch_id ?? '').trim();
1150
+ if (!watch_id)
1151
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'watch_id is required.' }) }], isError: true };
1152
+ const result = pollWatch(watch_id);
1153
+ if (!result.found)
1154
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `No active watcher with id: ${watch_id}` }) }], isError: true };
1155
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, watch_id, project_dir: result.project_dir, event_count: result.events.length, events: result.events }, null, 2) }] };
1156
+ }
1157
+ case 'veto_watch_stop': {
1158
+ const watch_id = String(args?.watch_id ?? '').trim();
1159
+ if (!watch_id)
1160
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'watch_id is required.' }) }], isError: true };
1161
+ const stopped = stopWatch(watch_id);
1162
+ return { content: [{ type: 'text', text: JSON.stringify({ success: stopped, message: stopped ? `Watcher ${watch_id} stopped.` : `No watcher found with id: ${watch_id}` }, null, 2) }] };
1163
+ }
1164
+ case 'veto_workflow': {
1165
+ const rawSteps = Array.isArray(args?.steps) ? args.steps : [];
1166
+ if (rawSteps.length === 0)
1167
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'steps array is required and must not be empty.' }) }], isError: true };
1168
+ const steps = rawSteps.map((s) => ({
1169
+ id: String(s.id ?? ''),
1170
+ agent: String(s.agent ?? ''),
1171
+ task: String(s.task ?? ''),
1172
+ code: s.code ? String(s.code) : undefined,
1173
+ context: s.context ? String(s.context) : undefined,
1174
+ gate: typeof s.gate === 'number' ? s.gate : undefined,
1175
+ }));
1176
+ const result = await runPipeline(steps, args?.project_dir ? String(args.project_dir) : undefined);
1177
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1178
+ }
1179
+ case 'veto_explain': {
1180
+ const filePath = String(args?.file_path ?? '').trim();
1181
+ if (!filePath)
1182
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'file_path is required.' }) }], isError: true };
1183
+ let fileContent;
1184
+ try {
1185
+ fileContent = readFileSync(filePath, 'utf8');
1186
+ }
1187
+ catch {
1188
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Cannot read file: ${filePath}` }) }], isError: true };
1189
+ }
1190
+ const ext = extname(filePath).toLowerCase();
1191
+ const name_ = basename(filePath).toLowerCase();
1192
+ const depth = String(args?.depth ?? 'overview');
1193
+ // auto-detect best agent
1194
+ let agent = 'coder';
1195
+ if (['.tsx', '.jsx', '.vue', '.svelte'].includes(ext))
1196
+ agent = 'frontend';
1197
+ else if (['.sql', '.prisma'].includes(ext) || name_.includes('schema'))
1198
+ agent = 'database';
1199
+ else if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(name_))
1200
+ agent = 'tester';
1201
+ else if (['.yaml', '.yml', '.toml', '.dockerfile'].includes(ext) || name_ === 'dockerfile')
1202
+ agent = 'devops';
1203
+ else if (name_.includes('auth') || name_.includes('login') || name_.includes('jwt') || name_.includes('token'))
1204
+ agent = 'auth';
1205
+ else if (name_.includes('security') || name_.includes('crypt'))
1206
+ agent = 'security-scanner';
1207
+ else if (['.ts', '.js', '.mjs'].includes(ext))
1208
+ agent = 'coder';
1209
+ const userContext = args?.context ? String(args.context) : undefined;
1210
+ const task = `Explain this ${ext} file at ${depth} depth. File: ${basename(filePath)}${userContext ? `. Focus: ${userContext}` : ''}`;
1211
+ const result = await executeOne({ id: 'explain-1', agent, task, code: fileContent, project_dir: undefined });
1212
+ return {
1213
+ content: [{ type: 'text', text: JSON.stringify({
1214
+ file: filePath, agent_used: agent, depth,
1215
+ explanation: result.plan ?? result.analysis,
1216
+ output: result.output,
1217
+ }, null, 2) }],
1218
+ };
1219
+ }
1220
+ case 'veto_plugins': {
1221
+ return { content: [{ type: 'text', text: JSON.stringify({ plugins: listPlugins(), plugin_dir: `${process.env.HOME ?? process.env.USERPROFILE}/.veto/agents/`, instructions: 'Drop a .js file exporting plan(task, context?) to register a custom agent.' }, null, 2) }] };
1222
+ }
934
1223
  default:
935
1224
  throw new Error(`Unknown tool: ${name}`);
936
1225
  }
937
1226
  });
1227
+ // ─── MCP Resources ─────────────────────────────────────────────────────────────
1228
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
1229
+ resources: [
1230
+ {
1231
+ uri: 'veto://sessions',
1232
+ name: 'Saved Sessions',
1233
+ description: 'List of all saved Veto sessions across AI platforms.',
1234
+ mimeType: 'application/json',
1235
+ },
1236
+ {
1237
+ uri: 'veto://project-map',
1238
+ name: 'Project Map',
1239
+ description: 'Stored project structure maps. Append ?dir=<absolute_path> to filter by project.',
1240
+ mimeType: 'application/json',
1241
+ },
1242
+ {
1243
+ uri: 'veto://memory',
1244
+ name: 'Knowledge Base',
1245
+ description: 'All stored knowledge entries. Append ?q=<query> to search.',
1246
+ mimeType: 'application/json',
1247
+ },
1248
+ {
1249
+ uri: 'veto://patterns',
1250
+ name: 'Learned Patterns',
1251
+ description: 'Coding patterns Veto has learned from your sessions.',
1252
+ mimeType: 'application/json',
1253
+ },
1254
+ ],
1255
+ }));
1256
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1257
+ const uri = request.params.uri;
1258
+ const url = new URL(uri);
1259
+ if (url.host === 'sessions') {
1260
+ const sessions = listSessions(50);
1261
+ return {
1262
+ contents: [{
1263
+ uri,
1264
+ mimeType: 'application/json',
1265
+ text: JSON.stringify(sessions.map(s => ({
1266
+ id: s.id, platform: s.platform, summary: s.summary,
1267
+ project_dir: s.project_dir, started_at: s.started_at,
1268
+ })), null, 2),
1269
+ }],
1270
+ };
1271
+ }
1272
+ if (url.host === 'project-map') {
1273
+ const dir = url.searchParams.get('dir') ?? '';
1274
+ if (dir) {
1275
+ const row = getProjectMap(dir);
1276
+ return {
1277
+ contents: [{
1278
+ uri,
1279
+ mimeType: 'application/json',
1280
+ text: JSON.stringify(row ?? { found: false }, null, 2),
1281
+ }],
1282
+ };
1283
+ }
1284
+ return { contents: [{ uri, mimeType: 'application/json', text: '{"message":"Append ?dir=<absolute_path> to get a specific project map."}' }] };
1285
+ }
1286
+ if (url.host === 'memory') {
1287
+ const q = url.searchParams.get('q') ?? undefined;
1288
+ const results = searchKnowledge({ query: q, limit: 20 });
1289
+ return {
1290
+ contents: [{
1291
+ uri,
1292
+ mimeType: 'application/json',
1293
+ text: JSON.stringify(results.map(r => ({
1294
+ id: r.id, type: r.type, title: r.title,
1295
+ content: r.content, tags: r.tags ? JSON.parse(r.tags) : [],
1296
+ })), null, 2),
1297
+ }],
1298
+ };
1299
+ }
1300
+ if (url.host === 'patterns') {
1301
+ const patterns = getPatterns(undefined, 50);
1302
+ return {
1303
+ contents: [{
1304
+ uri,
1305
+ mimeType: 'application/json',
1306
+ text: JSON.stringify(patterns.map(p => ({
1307
+ key: p.pattern_key, val: p.pattern_val,
1308
+ confidence: p.confidence, seen_count: p.seen_count,
1309
+ })), null, 2),
1310
+ }],
1311
+ };
1312
+ }
1313
+ throw new Error(`Unknown resource: ${uri}`);
1314
+ });
1315
+ // ─── MCP Prompts ───────────────────────────────────────────────────────────────
1316
+ const PROMPTS = [
1317
+ {
1318
+ name: 'code-review',
1319
+ description: 'Full code review prompt — paste code, get scored findings with severity and fixes.',
1320
+ arguments: [
1321
+ { name: 'code', description: 'The code to review.', required: true },
1322
+ { name: 'focus', description: 'Optional focus area (e.g. security, performance, style).', required: false },
1323
+ ],
1324
+ },
1325
+ {
1326
+ name: 'security-audit',
1327
+ description: 'OWASP Top 10 security audit — scans code for vulnerabilities with CWE references.',
1328
+ arguments: [
1329
+ { name: 'code', description: 'The code to audit.', required: true },
1330
+ { name: 'language', description: 'Language or framework (e.g. TypeScript, Express).', required: false },
1331
+ ],
1332
+ },
1333
+ {
1334
+ name: 'deploy-checklist',
1335
+ description: 'Pre-deploy checklist debate — council reviews your deployment plan.',
1336
+ arguments: [
1337
+ { name: 'plan', description: 'Your deployment plan or change description.', required: true },
1338
+ { name: 'environment', description: 'Target environment (prod, staging, etc.).', required: false },
1339
+ ],
1340
+ },
1341
+ {
1342
+ name: 'explain-file',
1343
+ description: 'Expert explanation of a file — routes to the best-fit agent based on file type.',
1344
+ arguments: [
1345
+ { name: 'file_path', description: 'Absolute path to the file to explain.', required: true },
1346
+ { name: 'depth', description: 'Explanation depth: overview | detailed | line-by-line.', required: false },
1347
+ ],
1348
+ },
1349
+ ];
1350
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
1351
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1352
+ const { name, arguments: pArgs } = request.params;
1353
+ if (name === 'code-review') {
1354
+ const code = pArgs?.code ?? '<paste code here>';
1355
+ const focus = pArgs?.focus ? ` Focus on: ${pArgs.focus}.` : '';
1356
+ return {
1357
+ description: PROMPTS[0].description,
1358
+ messages: [{
1359
+ role: 'user',
1360
+ content: { type: 'text', text: `Use veto_code_review to review this code.${focus}\n\n\`\`\`\n${code}\n\`\`\`` },
1361
+ }],
1362
+ };
1363
+ }
1364
+ if (name === 'security-audit') {
1365
+ const code = pArgs?.code ?? '<paste code here>';
1366
+ const lang = pArgs?.language ? ` Language/framework: ${pArgs.language}.` : '';
1367
+ return {
1368
+ description: PROMPTS[1].description,
1369
+ messages: [{
1370
+ role: 'user',
1371
+ content: { type: 'text', text: `Use veto_security_scan to audit this code for OWASP Top 10 vulnerabilities.${lang}\n\n\`\`\`\n${code}\n\`\`\`` },
1372
+ }],
1373
+ };
1374
+ }
1375
+ if (name === 'deploy-checklist') {
1376
+ const plan = pArgs?.plan ?? '<describe your deployment plan>';
1377
+ const env = pArgs?.environment ? ` Target environment: ${pArgs.environment}.` : '';
1378
+ return {
1379
+ description: PROMPTS[2].description,
1380
+ messages: [{
1381
+ role: 'user',
1382
+ content: { type: 'text', text: `Use veto_council_debate to review this deployment plan before we ship.${env}\n\nPlan: ${plan}` },
1383
+ }],
1384
+ };
1385
+ }
1386
+ if (name === 'explain-file') {
1387
+ const filePath = pArgs?.file_path ?? '<absolute file path>';
1388
+ const depth = pArgs?.depth ?? 'overview';
1389
+ return {
1390
+ description: PROMPTS[3].description,
1391
+ messages: [{
1392
+ role: 'user',
1393
+ content: { type: 'text', text: `Read the file at "${filePath}" and use veto_agent_plan with the most appropriate agent (frontend for .tsx/.vue, database for .sql, backend for services, coder for general) to give a ${depth}-level explanation of what it does, how it works, and any concerns.` },
1394
+ }],
1395
+ };
1396
+ }
1397
+ throw new Error(`Unknown prompt: ${name}`);
1398
+ });
938
1399
  // ─── Start ─────────────────────────────────────────────────────────────────────
939
1400
  async function main() {
1401
+ const loadedPlugins = await loadPlugins();
1402
+ if (loadedPlugins.length > 0) {
1403
+ process.stderr.write(`[veto] Loaded ${loadedPlugins.length} plugin(s): ${loadedPlugins.join(', ')}\n`);
1404
+ }
940
1405
  const transport = new StdioServerTransport();
941
1406
  await server.connect(transport);
942
- // stderr so it doesn't pollute MCP stdio
943
1407
  process.stderr.write(`Veto MCP server v${VERSION} running (stdio)\n`);
944
1408
  }
945
1409
  main().catch((err) => {