@jigyasudham/veto 0.8.3 → 1.1.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 (114) hide show
  1. package/README.md +217 -54
  2. package/dist/adapters/index.js +4 -3
  3. package/dist/agents/executor.js +36 -3
  4. package/dist/cli.js +246 -7
  5. package/dist/context/reader.js +113 -0
  6. package/dist/council/index.js +3 -1
  7. package/dist/memory/local.js +18 -1
  8. package/dist/memory/schema.js +12 -10
  9. package/dist/plugins/loader.js +49 -0
  10. package/dist/router/index.js +2 -2
  11. package/dist/router/learning-updater.js +45 -1
  12. package/dist/server.js +507 -21
  13. package/dist/watcher/index.js +77 -0
  14. package/dist/workflow/pipeline.js +64 -0
  15. package/package.json +12 -3
  16. package/.claude/settings.local.json +0 -9
  17. package/src/adapters/claude.ts +0 -70
  18. package/src/adapters/codex.ts +0 -71
  19. package/src/adapters/gemini.ts +0 -71
  20. package/src/adapters/index.ts +0 -217
  21. package/src/agents/development/api.ts +0 -120
  22. package/src/agents/development/backend.ts +0 -85
  23. package/src/agents/development/coder.ts +0 -213
  24. package/src/agents/development/database.ts +0 -83
  25. package/src/agents/development/debugger.ts +0 -238
  26. package/src/agents/development/devops.ts +0 -86
  27. package/src/agents/development/frontend.ts +0 -85
  28. package/src/agents/development/migration.ts +0 -144
  29. package/src/agents/development/performance.ts +0 -144
  30. package/src/agents/development/refactor.ts +0 -86
  31. package/src/agents/development/reviewer.ts +0 -268
  32. package/src/agents/development/tester.ts +0 -151
  33. package/src/agents/executor.ts +0 -158
  34. package/src/agents/memory/context-manager.ts +0 -171
  35. package/src/agents/memory/decision-logger.ts +0 -160
  36. package/src/agents/memory/knowledge-base.ts +0 -124
  37. package/src/agents/memory/pattern-learner.ts +0 -143
  38. package/src/agents/memory/project-mapper.ts +0 -118
  39. package/src/agents/quality/accessibility.ts +0 -99
  40. package/src/agents/quality/code-quality.ts +0 -115
  41. package/src/agents/quality/compatibility.ts +0 -58
  42. package/src/agents/quality/documentation.ts +0 -105
  43. package/src/agents/quality/error-handling.ts +0 -96
  44. package/src/agents/research/competitor-analyzer.ts +0 -45
  45. package/src/agents/research/cost-analyzer.ts +0 -54
  46. package/src/agents/research/estimator.ts +0 -60
  47. package/src/agents/research/ethics-bias.ts +0 -113
  48. package/src/agents/research/researcher.ts +0 -114
  49. package/src/agents/research/risk-assessor.ts +0 -63
  50. package/src/agents/research/tech-advisor.ts +0 -55
  51. package/src/agents/security/auth.ts +0 -287
  52. package/src/agents/security/dependency-audit.ts +0 -337
  53. package/src/agents/security/penetration.ts +0 -262
  54. package/src/agents/security/privacy.ts +0 -285
  55. package/src/agents/security/scanner.ts +0 -322
  56. package/src/agents/security/secrets.ts +0 -249
  57. package/src/agents/types.ts +0 -66
  58. package/src/agents/workflow/automation.ts +0 -59
  59. package/src/agents/workflow/file-manager.ts +0 -52
  60. package/src/agents/workflow/git-agent.ts +0 -55
  61. package/src/agents/workflow/reporter.ts +0 -51
  62. package/src/agents/workflow/search-agent.ts +0 -40
  63. package/src/agents/workflow/task-coordinator.ts +0 -41
  64. package/src/agents/workflow/task-planner.ts +0 -47
  65. package/src/cli.ts +0 -204
  66. package/src/council/decision-engine.ts +0 -171
  67. package/src/council/devil-advocate.ts +0 -116
  68. package/src/council/index.ts +0 -44
  69. package/src/council/lead-developer.ts +0 -118
  70. package/src/council/legal-compliance.ts +0 -152
  71. package/src/council/product-manager.ts +0 -102
  72. package/src/council/security.ts +0 -172
  73. package/src/council/system-architect.ts +0 -132
  74. package/src/council/types.ts +0 -33
  75. package/src/council/ux-designer.ts +0 -121
  76. package/src/memory/local.ts +0 -305
  77. package/src/memory/schema.ts +0 -174
  78. package/src/memory/sync.ts +0 -274
  79. package/src/router/complexity-scorer.ts +0 -96
  80. package/src/router/context-compressor.ts +0 -74
  81. package/src/router/index.ts +0 -60
  82. package/src/router/learning-updater.ts +0 -271
  83. package/src/router/model-selector.ts +0 -83
  84. package/src/router/rate-monitor.ts +0 -103
  85. package/src/server.ts +0 -1038
  86. package/src/skills/development/skill-api-design.ts +0 -329
  87. package/src/skills/development/skill-auth.ts +0 -271
  88. package/src/skills/development/skill-ci-cd.ts +0 -0
  89. package/src/skills/development/skill-crud.ts +0 -209
  90. package/src/skills/development/skill-db-schema.ts +0 -0
  91. package/src/skills/development/skill-docker.ts +0 -0
  92. package/src/skills/development/skill-env-setup.ts +0 -0
  93. package/src/skills/development/skill-scaffold.ts +0 -323
  94. package/src/skills/intelligence/skill-complexity-score.ts +0 -69
  95. package/src/skills/intelligence/skill-cost-track.ts +0 -39
  96. package/src/skills/intelligence/skill-learning-loop.ts +0 -69
  97. package/src/skills/intelligence/skill-pattern-detect.ts +0 -38
  98. package/src/skills/intelligence/skill-rate-watch.ts +0 -61
  99. package/src/skills/memory/skill-context-compress.ts +0 -98
  100. package/src/skills/memory/skill-cross-sync.ts +0 -104
  101. package/src/skills/memory/skill-decision-log.ts +0 -119
  102. package/src/skills/memory/skill-session-restore.ts +0 -59
  103. package/src/skills/memory/skill-session-save.ts +0 -94
  104. package/src/skills/quality/skill-accessibility.ts +0 -0
  105. package/src/skills/quality/skill-code-review.ts +0 -84
  106. package/src/skills/quality/skill-docs-gen.ts +0 -0
  107. package/src/skills/quality/skill-perf-audit.ts +0 -0
  108. package/src/skills/quality/skill-security-scan.ts +0 -91
  109. package/src/skills/quality/skill-test-suite.ts +0 -290
  110. package/src/skills/workflow/skill-deploy.ts +0 -0
  111. package/src/skills/workflow/skill-git-workflow.ts +0 -0
  112. package/src/skills/workflow/skill-rollback.ts +0 -0
  113. package/src/skills/workflow/skill-task-breakdown.ts +0 -0
  114. package/tsconfig.json +0 -20
package/dist/server.js CHANGED
@@ -5,17 +5,29 @@
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.1.0';
22
+ // Tracks the project_dir of the most recently active session in this process.
23
+ // Used as a fallback when memory_store/memory_search are called without an explicit project_dir,
24
+ // so memories are automatically scoped to the current project.
25
+ let activeProjectDir = null;
16
26
  const server = new Server({ name: 'veto', version: VERSION }, {
17
27
  capabilities: {
18
28
  tools: {},
29
+ resources: {},
30
+ prompts: {},
19
31
  },
20
32
  });
21
33
  // ─── Tool Definitions ─────────────────────────────────────────────────────────
@@ -75,6 +87,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
75
87
  type: 'string',
76
88
  description: 'UUID of the session to restore.',
77
89
  },
90
+ resuming_as: {
91
+ type: 'string',
92
+ description: 'The AI client resuming this session (e.g. "claude", "gemini", "codex"). Recorded as active_client.',
93
+ enum: ['claude', 'gemini', 'codex'],
94
+ },
78
95
  },
79
96
  required: ['session_id'],
80
97
  },
@@ -158,6 +175,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
158
175
  type: 'string',
159
176
  description: 'Optional: additional context such as codebase state, prior decisions, or constraints.',
160
177
  },
178
+ project_dir: {
179
+ type: 'string',
180
+ 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.',
181
+ },
161
182
  session_id: {
162
183
  type: 'string',
163
184
  description: 'Optional: session ID to associate this council outcome with an active session.',
@@ -179,6 +200,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
179
200
  },
180
201
  task: { type: 'string', description: 'The task for the agent to plan.' },
181
202
  context: { type: 'string', description: 'Optional additional context.' },
203
+ 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
204
  },
183
205
  required: ['agent', 'task'],
184
206
  },
@@ -195,6 +217,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
195
217
  required: ['code'],
196
218
  },
197
219
  },
220
+ {
221
+ name: 'veto_diff_review',
222
+ 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.',
223
+ inputSchema: {
224
+ type: 'object',
225
+ properties: {
226
+ diff: { type: 'string', description: 'The git diff to review. If omitted, Veto runs git diff HEAD in project_dir.' },
227
+ project_dir: { type: 'string', description: 'Absolute project path. Used to auto-read git diff if diff is not provided, and to inject codebase context.' },
228
+ context: { type: 'string', description: 'Optional: PR description, ticket number, or focus area.' },
229
+ },
230
+ required: [],
231
+ },
232
+ },
198
233
  {
199
234
  name: 'veto_security_scan',
200
235
  description: 'Runs the Security Scanner (OWASP Top 10) on provided code. Returns vulnerabilities with severity, CWE/OWASP category, and remediation steps.',
@@ -235,10 +270,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
235
270
  task: { type: 'string', description: 'Task description for this agent.' },
236
271
  code: { type: 'string', description: 'Optional code to analyze (triggers analyze() instead of plan()).' },
237
272
  context: { type: 'string', description: 'Optional additional context.' },
273
+ project_dir: { type: 'string', description: 'Optional: per-task project dir override.' },
238
274
  },
239
275
  required: ['id', 'agent', 'task'],
240
276
  },
241
277
  },
278
+ project_dir: { type: 'string', description: 'Optional: project directory applied to all tasks (per-task project_dir overrides this). Auto-injects codebase context.' },
242
279
  },
243
280
  required: ['tasks'],
244
281
  },
@@ -391,6 +428,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
391
428
  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
429
  agent: { type: 'string', description: 'The worker agent type used (optional but useful for agent performance tracking).' },
393
430
  tokens_used: { type: 'number', description: 'Approximate tokens used (optional).' },
431
+ 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
432
  },
395
433
  required: ['task_type', 'complexity', 'model_tier', 'output_quality'],
396
434
  },
@@ -441,6 +479,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
441
479
  type: 'object',
442
480
  properties: {
443
481
  session_id: { type: 'string', description: 'Optional. Session ID from veto_handoff. If omitted, the most recent saved session is restored.' },
482
+ resuming_as: { type: 'string', description: 'The AI client resuming this session (e.g. "gemini"). Recorded as active_client so you can track which tool is currently working on it.', enum: ['claude', 'gemini', 'codex'] },
444
483
  },
445
484
  required: [],
446
485
  },
@@ -457,6 +496,88 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
457
496
  required: ['platform', 'veto_server_path'],
458
497
  },
459
498
  },
499
+ {
500
+ name: 'veto_watch',
501
+ 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.',
502
+ inputSchema: {
503
+ type: 'object',
504
+ properties: {
505
+ project_dir: { type: 'string', description: 'Absolute path to the project directory to watch.' },
506
+ },
507
+ required: ['project_dir'],
508
+ },
509
+ },
510
+ {
511
+ name: 'veto_watch_poll',
512
+ 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.',
513
+ inputSchema: {
514
+ type: 'object',
515
+ properties: {
516
+ watch_id: { type: 'string', description: 'The watch_id returned by veto_watch.' },
517
+ },
518
+ required: ['watch_id'],
519
+ },
520
+ },
521
+ {
522
+ name: 'veto_watch_stop',
523
+ description: 'Stops an active file watcher.',
524
+ inputSchema: {
525
+ type: 'object',
526
+ properties: {
527
+ watch_id: { type: 'string', description: 'The watch_id returned by veto_watch.' },
528
+ },
529
+ required: ['watch_id'],
530
+ },
531
+ },
532
+ {
533
+ name: 'veto_workflow',
534
+ 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).',
535
+ inputSchema: {
536
+ type: 'object',
537
+ properties: {
538
+ steps: {
539
+ type: 'array',
540
+ description: 'Ordered pipeline steps.',
541
+ items: {
542
+ type: 'object',
543
+ properties: {
544
+ id: { type: 'string', description: 'Step identifier.' },
545
+ agent: { type: 'string', description: 'Worker agent type.' },
546
+ task: { type: 'string', description: 'Task description for this step.' },
547
+ code: { type: 'string', description: 'Optional code to analyze.' },
548
+ context: { type: 'string', description: 'Optional context.' },
549
+ gate: { type: 'number', description: 'Optional minimum confidence % (0–100) required to proceed to the next step.' },
550
+ },
551
+ required: ['id', 'agent', 'task'],
552
+ },
553
+ },
554
+ project_dir: { type: 'string', description: 'Optional project directory — auto-injects codebase context into all steps.' },
555
+ },
556
+ required: ['steps'],
557
+ },
558
+ },
559
+ {
560
+ name: 'veto_explain',
561
+ 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.',
562
+ inputSchema: {
563
+ type: 'object',
564
+ properties: {
565
+ file_path: { type: 'string', description: 'Absolute path to the file to explain.' },
566
+ depth: { type: 'string', enum: ['overview', 'detailed', 'line-by-line'], description: 'Explanation depth. Default: overview.' },
567
+ context: { type: 'string', description: 'Optional additional context about what you want explained.' },
568
+ },
569
+ required: ['file_path'],
570
+ },
571
+ },
572
+ {
573
+ name: 'veto_plugins',
574
+ 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.',
575
+ inputSchema: {
576
+ type: 'object',
577
+ properties: {},
578
+ required: [],
579
+ },
580
+ },
460
581
  ],
461
582
  }));
462
583
  // ─── Tool Handlers ────────────────────────────────────────────────────────────
@@ -483,12 +604,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
483
604
  };
484
605
  }
485
606
  case 'veto_session_save': {
607
+ const sessionProjectDir = args?.project_dir ? String(args.project_dir) : undefined;
608
+ if (sessionProjectDir)
609
+ activeProjectDir = sessionProjectDir;
486
610
  const result = saveSession({
487
611
  summary: String(args?.summary ?? ''),
488
612
  context: String(args?.context ?? ''),
489
613
  task_state: args?.task_state ? String(args.task_state) : undefined,
490
614
  platform: args?.platform ? String(args.platform) : 'claude',
491
- project_dir: args?.project_dir ? String(args.project_dir) : undefined,
615
+ project_dir: sessionProjectDir,
492
616
  token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
493
617
  });
494
618
  return {
@@ -506,7 +630,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
506
630
  }
507
631
  case 'veto_session_restore': {
508
632
  const session_id = String(args?.session_id ?? '');
509
- const result = restoreSession(session_id);
633
+ const resuming_as = args?.resuming_as ? String(args.resuming_as) : undefined;
634
+ const result = restoreSession(session_id, resuming_as);
510
635
  if (!result.found) {
511
636
  return {
512
637
  content: [
@@ -519,6 +644,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
519
644
  };
520
645
  }
521
646
  const s = result.session;
647
+ if (s.project_dir)
648
+ activeProjectDir = s.project_dir;
522
649
  return {
523
650
  content: [
524
651
  {
@@ -526,7 +653,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
526
653
  text: JSON.stringify({
527
654
  success: true,
528
655
  session_id: s.id,
529
- platform: s.platform,
656
+ created_by: s.platform,
657
+ active_client: s.active_client ?? s.platform,
658
+ last_resumed_at: s.last_resumed_at,
530
659
  started_at: s.started_at,
531
660
  ended_at: s.ended_at,
532
661
  project_dir: s.project_dir,
@@ -563,20 +692,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
563
692
  };
564
693
  }
565
694
  case 'veto_route_task': {
566
- const result = routeTask(String(args?.task ?? ''), {
695
+ const routeTaskStr = String(args?.task ?? '');
696
+ const fileExt = args?.file_ext ? String(args.file_ext) : undefined;
697
+ const result = routeTask(routeTaskStr, {
567
698
  agentType: args?.agent_type ? String(args.agent_type) : undefined,
568
699
  filesAffected: typeof args?.files_affected === 'number' ? args.files_affected : undefined,
569
700
  forceCouncil: args?.force_council === true,
570
701
  context: args?.context ? String(args.context) : undefined,
571
702
  preferredPlatform: args?.preferred_platform ? String(args.preferred_platform) : 'claude',
572
703
  });
704
+ const recommended_agent = getRecommendedAgent(routeTaskStr, fileExt);
573
705
  return {
574
- content: [
575
- {
706
+ content: [{
576
707
  type: 'text',
577
- text: JSON.stringify(result, null, 2),
578
- },
579
- ],
708
+ text: JSON.stringify({ ...result, recommended_agent }, null, 2),
709
+ }],
580
710
  };
581
711
  }
582
712
  case 'veto_rate_status': {
@@ -600,6 +730,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
600
730
  const result = await runDebate({
601
731
  task,
602
732
  context: args?.context ? String(args.context) : undefined,
733
+ project_dir: args?.project_dir ? String(args.project_dir) : undefined,
603
734
  });
604
735
  const outcomeId = saveCouncilOutcome({
605
736
  session_id: args?.session_id ? String(args.session_id) : undefined,
@@ -645,11 +776,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
645
776
  if (!task) {
646
777
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'task is required.' }) }], isError: true };
647
778
  }
648
- const result = await executeOne({ id: 'plan-1', agent: agentType, task, context: args?.context ? String(args.context) : undefined });
779
+ const result = await executeOne({
780
+ id: 'plan-1',
781
+ agent: agentType,
782
+ task,
783
+ context: args?.context ? String(args.context) : undefined,
784
+ project_dir: args?.project_dir ? String(args.project_dir) : undefined,
785
+ });
649
786
  if (result.error) {
650
787
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: result.error }) }], isError: true };
651
788
  }
652
- return { content: [{ type: 'text', text: JSON.stringify(result.plan ?? result.analysis, null, 2) }] };
789
+ return { content: [{ type: 'text', text: JSON.stringify({ ...(result.plan ?? result.analysis), output: result.output }, null, 2) }] };
653
790
  }
654
791
  case 'veto_code_review': {
655
792
  const code = String(args?.code ?? '').trim();
@@ -662,6 +799,92 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
662
799
  }
663
800
  return { content: [{ type: 'text', text: JSON.stringify(result.analysis, null, 2) }] };
664
801
  }
802
+ case 'veto_diff_review': {
803
+ const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
804
+ const userContext = args?.context ? String(args.context) : undefined;
805
+ // Resolve diff — use provided or read from git
806
+ let diff = args?.diff ? String(args.diff).trim() : '';
807
+ if (!diff && projectDir) {
808
+ const { execSync: execSyncDiff } = await import('node:child_process');
809
+ try {
810
+ diff = execSyncDiff('git diff HEAD --no-color', {
811
+ cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
812
+ }).toString().trim();
813
+ if (!diff) {
814
+ diff = execSyncDiff('git diff --cached --no-color', {
815
+ cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
816
+ }).toString().trim();
817
+ }
818
+ }
819
+ catch { /* not a git repo or no changes */ }
820
+ }
821
+ if (!diff) {
822
+ 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 };
823
+ }
824
+ // Parse changed files from diff header lines
825
+ const changedFiles = [...diff.matchAll(/^diff --git a\/.+ b\/(.+)$/gm)].map(m => m[1]);
826
+ const diffChunks = diff.split(/^diff --git /m).filter(Boolean);
827
+ // Run code-review, security-scan, secrets-scan in parallel across the full diff
828
+ const context = buildContextString(projectDir, userContext);
829
+ const [reviewResult, secResult, secretsResult] = await Promise.all([
830
+ executeOne({ id: 'diff-review', agent: 'reviewer', task: 'Review this git diff for code quality issues', code: diff, context }),
831
+ executeOne({ id: 'diff-sec', agent: 'security-scanner', task: 'Scan this git diff for security vulnerabilities', code: diff, context }),
832
+ executeOne({ id: 'diff-secrets', agent: 'secrets', task: 'Scan this git diff for exposed secrets or credentials', code: diff }),
833
+ ]);
834
+ // Derive overall verdict
835
+ const hasBlocking = (reviewResult.analysis?.critical_count ?? 0) > 0
836
+ || (secResult.analysis?.critical_count ?? 0) > 0
837
+ || (secretsResult.analysis?.critical_count ?? 0) > 0;
838
+ const hasWarnings = (reviewResult.analysis?.high_count ?? 0) > 0
839
+ || (secResult.analysis?.high_count ?? 0) > 0;
840
+ const verdict = hasBlocking ? 'fail' : hasWarnings ? 'warn' : 'pass';
841
+ const verdictEmoji = verdict === 'pass' ? '✅ PASS' : verdict === 'warn' ? '⚠️ WARN' : '❌ FAIL';
842
+ // Per-file finding counts (approximate from line refs)
843
+ const fileFindings = {};
844
+ for (const f of changedFiles)
845
+ fileFindings[f] = 0;
846
+ for (const finding of [...(reviewResult.analysis?.findings ?? []), ...(secResult.analysis?.findings ?? [])]) {
847
+ const match = changedFiles.find(f => finding.location?.includes(f));
848
+ if (match)
849
+ fileFindings[match]++;
850
+ }
851
+ return {
852
+ content: [{
853
+ type: 'text',
854
+ text: JSON.stringify({
855
+ verdict,
856
+ verdict_label: verdictEmoji,
857
+ files_changed: changedFiles.length,
858
+ files: changedFiles,
859
+ file_findings: fileFindings,
860
+ code_review: {
861
+ score: reviewResult.analysis?.score ?? null,
862
+ verdict: reviewResult.analysis?.verdict ?? null,
863
+ critical: reviewResult.analysis?.critical_count ?? 0,
864
+ high: reviewResult.analysis?.high_count ?? 0,
865
+ findings: reviewResult.analysis?.findings ?? [],
866
+ },
867
+ security: {
868
+ score: secResult.analysis?.score ?? null,
869
+ verdict: secResult.analysis?.verdict ?? null,
870
+ critical: secResult.analysis?.critical_count ?? 0,
871
+ high: secResult.analysis?.high_count ?? 0,
872
+ findings: secResult.analysis?.findings ?? [],
873
+ },
874
+ secrets: {
875
+ verdict: secretsResult.analysis?.verdict ?? null,
876
+ findings: secretsResult.analysis?.findings ?? [],
877
+ },
878
+ summary: [
879
+ `${verdictEmoji} — ${changedFiles.length} file(s) changed`,
880
+ `Code: ${reviewResult.analysis?.verdict ?? 'n/a'} (score ${reviewResult.analysis?.score ?? '?'}/100)`,
881
+ `Security: ${secResult.analysis?.verdict ?? 'n/a'} — ${secResult.analysis?.critical_count ?? 0} critical, ${secResult.analysis?.high_count ?? 0} high`,
882
+ `Secrets: ${(secretsResult.analysis?.findings?.length ?? 0) > 0 ? '🔴 Exposed credentials detected' : '✅ Clean'}`,
883
+ ].join('\n'),
884
+ }, null, 2),
885
+ }],
886
+ };
887
+ }
665
888
  case 'veto_security_scan': {
666
889
  const code = String(args?.code ?? '').trim();
667
890
  if (!code) {
@@ -689,12 +912,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
689
912
  if (rawTasks.length === 0) {
690
913
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'tasks array is required and must not be empty.' }) }], isError: true };
691
914
  }
915
+ const parallelProjectDir = args?.project_dir ? String(args.project_dir) : undefined;
692
916
  const tasks = rawTasks.map((t) => ({
693
917
  id: String(t.id ?? ''),
694
918
  agent: String(t.agent ?? ''),
695
919
  task: String(t.task ?? ''),
696
920
  code: t.code ? String(t.code) : undefined,
697
921
  context: t.context ? String(t.context) : undefined,
922
+ project_dir: t.project_dir ? String(t.project_dir) : parallelProjectDir,
698
923
  }));
699
924
  const results = await executeParallel(tasks);
700
925
  return {
@@ -708,7 +933,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
708
933
  agent: r.agent,
709
934
  duration_ms: r.duration_ms,
710
935
  error: r.error,
711
- output: r.plan ?? r.analysis,
936
+ output: { ...(r.plan ?? r.analysis), structured: r.output },
712
937
  })),
713
938
  }, null, 2),
714
939
  }],
@@ -725,7 +950,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
725
950
  content,
726
951
  type: args?.type ? String(args.type) : 'solution',
727
952
  tags: Array.isArray(args?.tags) ? args.tags.map(String) : undefined,
728
- project_dir: args?.project_dir ? String(args.project_dir) : undefined,
953
+ project_dir: args?.project_dir ? String(args.project_dir) : (activeProjectDir ?? undefined),
729
954
  session_id: args?.session_id ? String(args.session_id) : undefined,
730
955
  relevance: typeof args?.relevance === 'number' ? args.relevance : 1.0,
731
956
  });
@@ -735,7 +960,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
735
960
  const results = searchKnowledge({
736
961
  query: args?.query ? String(args.query) : undefined,
737
962
  type: args?.type ? String(args.type) : undefined,
738
- project_dir: args?.project_dir ? String(args.project_dir) : undefined,
963
+ project_dir: args?.project_dir ? String(args.project_dir) : (activeProjectDir ?? undefined),
739
964
  limit: typeof args?.limit === 'number' ? args.limit : 10,
740
965
  });
741
966
  return {
@@ -854,16 +1079,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
854
1079
  return { content: [{ type: 'text', text: result.instructions + '\n\n' + JSON.stringify({ session_id: result.session_id, to_platform: result.to_platform, saved_at: result.saved_at, reason: result.reason }, null, 2) }] };
855
1080
  }
856
1081
  case 'veto_continue': {
857
- const result = continueSession(args?.session_id ? String(args.session_id) : undefined);
1082
+ const resuming_as = args?.resuming_as ? String(args.resuming_as) : undefined;
1083
+ const result = continueSession(args?.session_id ? String(args.session_id) : undefined, resuming_as);
858
1084
  if (!result.found) {
859
1085
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: result.message }, null, 2) }], isError: true };
860
1086
  }
1087
+ if (result.project_dir)
1088
+ activeProjectDir = result.project_dir;
861
1089
  return {
862
1090
  content: [{
863
1091
  type: 'text',
864
1092
  text: result.message + '\n\n' + JSON.stringify({
865
1093
  session_id: result.session_id,
866
- platform: result.platform,
1094
+ created_by: result.platform,
1095
+ active_client: result.active_client ?? result.platform,
867
1096
  summary: result.summary,
868
1097
  context: result.context,
869
1098
  task_state: result.task_state,
@@ -892,7 +1121,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
892
1121
  if (!task_type) {
893
1122
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'task_type is required.' }) }], isError: true };
894
1123
  }
895
- recordOutcome(task_type, complexity, model_tier, args?.agent ? String(args.agent) : 'dynamic', output_quality, typeof args?.tokens_used === 'number' ? args.tokens_used : 0);
1124
+ 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
1125
  const stats = getLearningStats();
897
1126
  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
1127
  }
@@ -931,15 +1160,272 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
931
1160
  const result = importMemory(args?.input_path ? String(args.input_path) : undefined);
932
1161
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
933
1162
  }
1163
+ case 'veto_watch': {
1164
+ const dir = String(args?.project_dir ?? '').trim();
1165
+ if (!dir)
1166
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
1167
+ const watch_id = startWatch(dir);
1168
+ 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) }] };
1169
+ }
1170
+ case 'veto_watch_poll': {
1171
+ const watch_id = String(args?.watch_id ?? '').trim();
1172
+ if (!watch_id)
1173
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'watch_id is required.' }) }], isError: true };
1174
+ const result = pollWatch(watch_id);
1175
+ if (!result.found)
1176
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `No active watcher with id: ${watch_id}` }) }], isError: true };
1177
+ 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) }] };
1178
+ }
1179
+ case 'veto_watch_stop': {
1180
+ const watch_id = String(args?.watch_id ?? '').trim();
1181
+ if (!watch_id)
1182
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'watch_id is required.' }) }], isError: true };
1183
+ const stopped = stopWatch(watch_id);
1184
+ return { content: [{ type: 'text', text: JSON.stringify({ success: stopped, message: stopped ? `Watcher ${watch_id} stopped.` : `No watcher found with id: ${watch_id}` }, null, 2) }] };
1185
+ }
1186
+ case 'veto_workflow': {
1187
+ const rawSteps = Array.isArray(args?.steps) ? args.steps : [];
1188
+ if (rawSteps.length === 0)
1189
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'steps array is required and must not be empty.' }) }], isError: true };
1190
+ const steps = rawSteps.map((s) => ({
1191
+ id: String(s.id ?? ''),
1192
+ agent: String(s.agent ?? ''),
1193
+ task: String(s.task ?? ''),
1194
+ code: s.code ? String(s.code) : undefined,
1195
+ context: s.context ? String(s.context) : undefined,
1196
+ gate: typeof s.gate === 'number' ? s.gate : undefined,
1197
+ }));
1198
+ const result = await runPipeline(steps, args?.project_dir ? String(args.project_dir) : undefined);
1199
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1200
+ }
1201
+ case 'veto_explain': {
1202
+ const filePath = String(args?.file_path ?? '').trim();
1203
+ if (!filePath)
1204
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'file_path is required.' }) }], isError: true };
1205
+ let fileContent;
1206
+ try {
1207
+ fileContent = readFileSync(filePath, 'utf8');
1208
+ }
1209
+ catch {
1210
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Cannot read file: ${filePath}` }) }], isError: true };
1211
+ }
1212
+ const ext = extname(filePath).toLowerCase();
1213
+ const name_ = basename(filePath).toLowerCase();
1214
+ const depth = String(args?.depth ?? 'overview');
1215
+ // auto-detect best agent
1216
+ let agent = 'coder';
1217
+ if (['.tsx', '.jsx', '.vue', '.svelte'].includes(ext))
1218
+ agent = 'frontend';
1219
+ else if (['.sql', '.prisma'].includes(ext) || name_.includes('schema'))
1220
+ agent = 'database';
1221
+ else if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(name_))
1222
+ agent = 'tester';
1223
+ else if (['.yaml', '.yml', '.toml', '.dockerfile'].includes(ext) || name_ === 'dockerfile')
1224
+ agent = 'devops';
1225
+ else if (name_.includes('auth') || name_.includes('login') || name_.includes('jwt') || name_.includes('token'))
1226
+ agent = 'auth';
1227
+ else if (name_.includes('security') || name_.includes('crypt'))
1228
+ agent = 'security-scanner';
1229
+ else if (['.ts', '.js', '.mjs'].includes(ext))
1230
+ agent = 'coder';
1231
+ const userContext = args?.context ? String(args.context) : undefined;
1232
+ const task = `Explain this ${ext} file at ${depth} depth. File: ${basename(filePath)}${userContext ? `. Focus: ${userContext}` : ''}`;
1233
+ const result = await executeOne({ id: 'explain-1', agent, task, code: fileContent, project_dir: undefined });
1234
+ return {
1235
+ content: [{ type: 'text', text: JSON.stringify({
1236
+ file: filePath, agent_used: agent, depth,
1237
+ explanation: result.plan ?? result.analysis,
1238
+ output: result.output,
1239
+ }, null, 2) }],
1240
+ };
1241
+ }
1242
+ case 'veto_plugins': {
1243
+ 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) }] };
1244
+ }
934
1245
  default:
935
1246
  throw new Error(`Unknown tool: ${name}`);
936
1247
  }
937
1248
  });
1249
+ // ─── MCP Resources ─────────────────────────────────────────────────────────────
1250
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
1251
+ resources: [
1252
+ {
1253
+ uri: 'veto://sessions',
1254
+ name: 'Saved Sessions',
1255
+ description: 'List of all saved Veto sessions across AI platforms.',
1256
+ mimeType: 'application/json',
1257
+ },
1258
+ {
1259
+ uri: 'veto://project-map',
1260
+ name: 'Project Map',
1261
+ description: 'Stored project structure maps. Append ?dir=<absolute_path> to filter by project.',
1262
+ mimeType: 'application/json',
1263
+ },
1264
+ {
1265
+ uri: 'veto://memory',
1266
+ name: 'Knowledge Base',
1267
+ description: 'All stored knowledge entries. Append ?q=<query> to search.',
1268
+ mimeType: 'application/json',
1269
+ },
1270
+ {
1271
+ uri: 'veto://patterns',
1272
+ name: 'Learned Patterns',
1273
+ description: 'Coding patterns Veto has learned from your sessions.',
1274
+ mimeType: 'application/json',
1275
+ },
1276
+ ],
1277
+ }));
1278
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
1279
+ const uri = request.params.uri;
1280
+ const url = new URL(uri);
1281
+ if (url.host === 'sessions') {
1282
+ const sessions = listSessions(50);
1283
+ return {
1284
+ contents: [{
1285
+ uri,
1286
+ mimeType: 'application/json',
1287
+ text: JSON.stringify(sessions.map(s => ({
1288
+ id: s.id, platform: s.platform, summary: s.summary,
1289
+ project_dir: s.project_dir, started_at: s.started_at,
1290
+ })), null, 2),
1291
+ }],
1292
+ };
1293
+ }
1294
+ if (url.host === 'project-map') {
1295
+ const dir = url.searchParams.get('dir') ?? '';
1296
+ if (dir) {
1297
+ const row = getProjectMap(dir);
1298
+ return {
1299
+ contents: [{
1300
+ uri,
1301
+ mimeType: 'application/json',
1302
+ text: JSON.stringify(row ?? { found: false }, null, 2),
1303
+ }],
1304
+ };
1305
+ }
1306
+ return { contents: [{ uri, mimeType: 'application/json', text: '{"message":"Append ?dir=<absolute_path> to get a specific project map."}' }] };
1307
+ }
1308
+ if (url.host === 'memory') {
1309
+ const q = url.searchParams.get('q') ?? undefined;
1310
+ const results = searchKnowledge({ query: q, limit: 20 });
1311
+ return {
1312
+ contents: [{
1313
+ uri,
1314
+ mimeType: 'application/json',
1315
+ text: JSON.stringify(results.map(r => ({
1316
+ id: r.id, type: r.type, title: r.title,
1317
+ content: r.content, tags: r.tags ? JSON.parse(r.tags) : [],
1318
+ })), null, 2),
1319
+ }],
1320
+ };
1321
+ }
1322
+ if (url.host === 'patterns') {
1323
+ const patterns = getPatterns(undefined, 50);
1324
+ return {
1325
+ contents: [{
1326
+ uri,
1327
+ mimeType: 'application/json',
1328
+ text: JSON.stringify(patterns.map(p => ({
1329
+ key: p.pattern_key, val: p.pattern_val,
1330
+ confidence: p.confidence, seen_count: p.seen_count,
1331
+ })), null, 2),
1332
+ }],
1333
+ };
1334
+ }
1335
+ throw new Error(`Unknown resource: ${uri}`);
1336
+ });
1337
+ // ─── MCP Prompts ───────────────────────────────────────────────────────────────
1338
+ const PROMPTS = [
1339
+ {
1340
+ name: 'code-review',
1341
+ description: 'Full code review prompt — paste code, get scored findings with severity and fixes.',
1342
+ arguments: [
1343
+ { name: 'code', description: 'The code to review.', required: true },
1344
+ { name: 'focus', description: 'Optional focus area (e.g. security, performance, style).', required: false },
1345
+ ],
1346
+ },
1347
+ {
1348
+ name: 'security-audit',
1349
+ description: 'OWASP Top 10 security audit — scans code for vulnerabilities with CWE references.',
1350
+ arguments: [
1351
+ { name: 'code', description: 'The code to audit.', required: true },
1352
+ { name: 'language', description: 'Language or framework (e.g. TypeScript, Express).', required: false },
1353
+ ],
1354
+ },
1355
+ {
1356
+ name: 'deploy-checklist',
1357
+ description: 'Pre-deploy checklist debate — council reviews your deployment plan.',
1358
+ arguments: [
1359
+ { name: 'plan', description: 'Your deployment plan or change description.', required: true },
1360
+ { name: 'environment', description: 'Target environment (prod, staging, etc.).', required: false },
1361
+ ],
1362
+ },
1363
+ {
1364
+ name: 'explain-file',
1365
+ description: 'Expert explanation of a file — routes to the best-fit agent based on file type.',
1366
+ arguments: [
1367
+ { name: 'file_path', description: 'Absolute path to the file to explain.', required: true },
1368
+ { name: 'depth', description: 'Explanation depth: overview | detailed | line-by-line.', required: false },
1369
+ ],
1370
+ },
1371
+ ];
1372
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: PROMPTS }));
1373
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1374
+ const { name, arguments: pArgs } = request.params;
1375
+ if (name === 'code-review') {
1376
+ const code = pArgs?.code ?? '<paste code here>';
1377
+ const focus = pArgs?.focus ? ` Focus on: ${pArgs.focus}.` : '';
1378
+ return {
1379
+ description: PROMPTS[0].description,
1380
+ messages: [{
1381
+ role: 'user',
1382
+ content: { type: 'text', text: `Use veto_code_review to review this code.${focus}\n\n\`\`\`\n${code}\n\`\`\`` },
1383
+ }],
1384
+ };
1385
+ }
1386
+ if (name === 'security-audit') {
1387
+ const code = pArgs?.code ?? '<paste code here>';
1388
+ const lang = pArgs?.language ? ` Language/framework: ${pArgs.language}.` : '';
1389
+ return {
1390
+ description: PROMPTS[1].description,
1391
+ messages: [{
1392
+ role: 'user',
1393
+ content: { type: 'text', text: `Use veto_security_scan to audit this code for OWASP Top 10 vulnerabilities.${lang}\n\n\`\`\`\n${code}\n\`\`\`` },
1394
+ }],
1395
+ };
1396
+ }
1397
+ if (name === 'deploy-checklist') {
1398
+ const plan = pArgs?.plan ?? '<describe your deployment plan>';
1399
+ const env = pArgs?.environment ? ` Target environment: ${pArgs.environment}.` : '';
1400
+ return {
1401
+ description: PROMPTS[2].description,
1402
+ messages: [{
1403
+ role: 'user',
1404
+ content: { type: 'text', text: `Use veto_council_debate to review this deployment plan before we ship.${env}\n\nPlan: ${plan}` },
1405
+ }],
1406
+ };
1407
+ }
1408
+ if (name === 'explain-file') {
1409
+ const filePath = pArgs?.file_path ?? '<absolute file path>';
1410
+ const depth = pArgs?.depth ?? 'overview';
1411
+ return {
1412
+ description: PROMPTS[3].description,
1413
+ messages: [{
1414
+ role: 'user',
1415
+ 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.` },
1416
+ }],
1417
+ };
1418
+ }
1419
+ throw new Error(`Unknown prompt: ${name}`);
1420
+ });
938
1421
  // ─── Start ─────────────────────────────────────────────────────────────────────
939
1422
  async function main() {
1423
+ const loadedPlugins = await loadPlugins();
1424
+ if (loadedPlugins.length > 0) {
1425
+ process.stderr.write(`[veto] Loaded ${loadedPlugins.length} plugin(s): ${loadedPlugins.join(', ')}\n`);
1426
+ }
940
1427
  const transport = new StdioServerTransport();
941
1428
  await server.connect(transport);
942
- // stderr so it doesn't pollute MCP stdio
943
1429
  process.stderr.write(`Veto MCP server v${VERSION} running (stdio)\n`);
944
1430
  }
945
1431
  main().catch((err) => {