@jigyasudham/veto 1.2.19 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
- // Veto MCP Server — 46 tools, 21 phases complete, auto-learning router
2
+ // Veto MCP Server — 49 tools, 23 phases complete, LLM council + auto-learning router
3
3
  // Suppress node:sqlite experimental warning — it would corrupt the MCP stdio protocol
4
4
  process.removeAllListeners('warning');
5
5
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { buildContextString } from './context/reader.js';
9
- import { saveSession, restoreSession, listSessions, closeSession, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, getContextStatus, fetchAndCacheDocs, saveTaskPlan, getUsageStatus, getAuditLog, getHealthStats, CONTEXT_WINDOWS, logUsage, getUsageLogs, getDb, storeScanDiagnostics, clearScanDiagnostics, updateSession, } from './memory/local.js';
9
+ import { TOOL_DEFINITIONS } from './tools/definitions.js';
10
+ import { saveSession, restoreSession, listSessions, closeSession, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, getContextStatus, fetchAndCacheDocs, saveTaskPlan, getTaskPlan, getUsageStatus, getAuditLog, getHealthStats, CONTEXT_WINDOWS, logUsage, getUsageLogs, getDb, storeScanDiagnostics, clearScanDiagnostics, updateSession, resolveContextWindow, getMetrics, } from './memory/local.js';
10
11
  import { exportMemory, importMemory } from './memory/sync.js';
11
- import { runDebate } from './council/index.js';
12
+ import { runLlmDebate } from './council/llm-council.js';
12
13
  import { routeTask, getRateStatus, trackTokens, recordOutcome, getLearningStats, getLearnedThresholds, applyLearnedThresholds, getAgentPerformanceStats, getTaskTypeBreakdown, getCouncilInsights, getRecommendedAgent } from './router/index.js';
13
14
  import { getConfig, setConfig } from './memory/config.js';
14
15
  import { executeParallel, executeOne } from './agents/executor.js';
@@ -19,7 +20,7 @@ import { loadPlugins, listPlugins } from './plugins/loader.js';
19
20
  import { fetchPrDiff } from './github/pr-fetcher.js';
20
21
  import { discoverProject } from './discover.js';
21
22
  import { readFileSync, statSync } from 'node:fs';
22
- import { extname, basename, join, dirname } from 'node:path';
23
+ import { extname, basename, join, dirname, resolve } from 'node:path';
23
24
  import { createHash } from 'node:crypto';
24
25
  import { fileURLToPath } from 'node:url';
25
26
  import { execSync as execSyncTop } from 'node:child_process';
@@ -40,10 +41,10 @@ const autoSave = {
40
41
  last_session_id: null,
41
42
  cached: null,
42
43
  };
43
- function maybeAutoSave(token_count, platform) {
44
+ function maybeAutoSave(token_count, platform, model) {
44
45
  if (!autoSave.cached)
45
46
  return { triggered: false };
46
- const window_size = CONTEXT_WINDOWS[platform] ?? 200_000;
47
+ const window_size = autoSave.cached.context_window ?? resolveContextWindow(platform, model);
47
48
  const usage_pct = Math.round((token_count / window_size) * 100);
48
49
  if (usage_pct < autoSave.threshold_pct)
49
50
  return { triggered: false };
@@ -120,735 +121,7 @@ const TOOL_ANNOTATIONS = {
120
121
  };
121
122
  // ─── Tool Definitions ─────────────────────────────────────────────────────────
122
123
  server.setRequestHandler(ListToolsRequestSchema, async () => {
123
- const tools = [
124
- {
125
- name: 'veto_status',
126
- description: 'Returns Veto server status, version, and database info. Pass token_count to trigger auto-save if context usage crosses 70%.',
127
- inputSchema: {
128
- type: 'object',
129
- properties: {
130
- token_count: {
131
- type: 'number',
132
- description: 'Current session token count. If provided and context usage ≥ 70%, Veto auto-saves the last known session context in the background.',
133
- },
134
- platform: {
135
- type: 'string',
136
- description: 'AI platform (claude, gemini, codex). Used to select the correct context window for threshold calculation. Defaults to "claude".',
137
- enum: ['claude', 'gemini', 'codex'],
138
- },
139
- },
140
- required: [],
141
- },
142
- },
143
- {
144
- name: 'veto_autosave_status',
145
- description: 'Returns the current auto-save state: whether a context is cached, the threshold, the last auto-save time, and the session ID.',
146
- inputSchema: {
147
- type: 'object',
148
- properties: {},
149
- required: [],
150
- },
151
- },
152
- {
153
- name: 'veto_session_save',
154
- description: 'Saves the current session context to SQLite. Pass session_id to update an existing session instead of creating a new one — use this when refreshing context mid-conversation rather than starting a new snapshot.',
155
- inputSchema: {
156
- type: 'object',
157
- properties: {
158
- summary: {
159
- type: 'string',
160
- description: 'A brief summary of what was accomplished this session.',
161
- },
162
- context: {
163
- type: 'string',
164
- description: 'Key context to restore (decisions, current task, file list, etc.).',
165
- },
166
- task_state: {
167
- type: 'string',
168
- description: 'Current task state — what is done and what is next.',
169
- },
170
- platform: {
171
- type: 'string',
172
- description: 'AI platform used (claude, gemini, codex). Defaults to "claude".',
173
- enum: ['claude', 'gemini', 'codex'],
174
- },
175
- project_dir: {
176
- type: 'string',
177
- description: 'Absolute path to the current project directory.',
178
- },
179
- connection_type: {
180
- type: 'string',
181
- description: 'How you are connected to this AI — "subscription" (Claude Pro, Gemini Advanced) or "api" (API key). Used for usage tracking.',
182
- enum: ['subscription', 'api'],
183
- },
184
- token_count: {
185
- type: 'number',
186
- description: 'Approximate tokens used this session. Veto uses this for context window monitoring.',
187
- },
188
- session_id: {
189
- type: 'string',
190
- description: 'Optional. UUID of an existing session to update in-place. When provided, Veto updates that row instead of inserting a new one — prevents session inflation when refreshing mid-conversation.',
191
- },
192
- },
193
- required: ['summary', 'context'],
194
- },
195
- },
196
- {
197
- name: 'veto_session_restore',
198
- description: 'Restores a previously saved session by ID. Use veto_sessions_list to find IDs.',
199
- inputSchema: {
200
- type: 'object',
201
- properties: {
202
- session_id: {
203
- type: 'string',
204
- description: 'UUID of the session to restore.',
205
- },
206
- resuming_as: {
207
- type: 'string',
208
- description: 'The AI client resuming this session (e.g. "claude", "gemini", "codex"). Recorded as active_client.',
209
- enum: ['claude', 'gemini', 'codex'],
210
- },
211
- },
212
- required: ['session_id'],
213
- },
214
- },
215
- {
216
- name: 'veto_sessions_list',
217
- description: 'Lists the most recent saved sessions (up to 10).',
218
- inputSchema: {
219
- type: 'object',
220
- properties: {
221
- limit: {
222
- type: 'number',
223
- description: 'Number of sessions to return (default 10, max 50).',
224
- },
225
- },
226
- required: [],
227
- },
228
- },
229
- {
230
- name: 'veto_route_task',
231
- description: 'Scores a task for complexity (0-100) and returns the optimal tier, model recommendation, and rate status. Use before any substantial task to let the router decide which model to use.',
232
- inputSchema: {
233
- type: 'object',
234
- properties: {
235
- task: {
236
- type: 'string',
237
- description: 'The task description to score and route.',
238
- },
239
- agent_type: {
240
- type: 'string',
241
- description: 'Optional agent type — some agents are tier-locked regardless of score.',
242
- enum: [
243
- 'lead-developer', 'system-architect', 'security-scanner',
244
- 'devil-advocate', 'decision-engine', 'risk-assessor',
245
- 'coder', 'tester', 'reviewer', 'database', 'documentation',
246
- 'file-manager', 'git-agent', 'search-agent', 'secrets', 'reporter',
247
- 'dynamic',
248
- ],
249
- },
250
- files_affected: {
251
- type: 'number',
252
- description: 'Number of files the task will touch (influences complexity score).',
253
- },
254
- force_council: {
255
- type: 'boolean',
256
- description: 'Set true to force a Tier 3 / council-required routing.',
257
- },
258
- context: {
259
- type: 'string',
260
- description: 'Current context text — router will return a compression plan.',
261
- },
262
- preferred_platform: {
263
- type: 'string',
264
- description: 'Preferred AI platform. Router may override if rate-limited.',
265
- enum: ['claude', 'gemini', 'codex'],
266
- },
267
- },
268
- required: ['task'],
269
- },
270
- },
271
- {
272
- name: 'veto_rate_status',
273
- description: 'Returns current request counts and rate limit status for all AI platforms tracked by Veto.',
274
- inputSchema: {
275
- type: 'object',
276
- properties: {},
277
- required: [],
278
- },
279
- },
280
- {
281
- name: 'veto_council_debate',
282
- description: 'Runs the Veto Council — 5 specialist agents (Lead Dev, PM, Architect, UX, Devil\'s Advocate) debate your task in parallel and return a GREEN / YELLOW / RED / DEADLOCK verdict before any code is written. Call this before architecture decisions, security-sensitive work, database migrations, or any task the router scores above 71.',
283
- inputSchema: {
284
- type: 'object',
285
- properties: {
286
- task: {
287
- type: 'string',
288
- description: 'The task or decision to debate. Be specific — include approach, tech stack, and constraints.',
289
- },
290
- context: {
291
- type: 'string',
292
- description: 'Optional: additional context such as codebase state, prior decisions, or constraints.',
293
- },
294
- project_dir: {
295
- type: 'string',
296
- 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.',
297
- },
298
- session_id: {
299
- type: 'string',
300
- description: 'Optional: session ID to associate this council outcome with an active session.',
301
- },
302
- max_tokens: {
303
- type: 'number',
304
- description: 'Optional: token budget for this operation. Veto estimates output tokens and warns in the response if the estimate exceeds this limit. Logged to usage_log for tracking.',
305
- },
306
- },
307
- required: ['task'],
308
- },
309
- },
310
- {
311
- name: 'veto_agent_plan',
312
- description: 'Gets a domain-expert execution plan from a specific worker agent. Returns approach, ordered steps, checklist, patterns, and pitfalls for the task.',
313
- inputSchema: {
314
- type: 'object',
315
- properties: {
316
- agent: {
317
- type: 'string',
318
- description: 'The worker agent to consult.',
319
- enum: ['coder', 'reviewer', 'tester', 'debugger', 'refactor', 'database', 'api', 'frontend', 'backend', 'devops', 'performance', 'migration', 'security-scanner', 'auth', 'privacy', 'secrets', 'dependency-audit', 'penetration', 'context-manager', 'decision-logger', 'project-mapper', 'pattern-learner', 'knowledge-base', 'researcher', 'tech-advisor', 'cost-analyzer', 'competitor-analyzer', 'risk-assessor', 'estimator', 'ethics-bias', 'code-quality', 'documentation', 'accessibility', 'compatibility', 'error-handling', 'task-planner', 'task-coordinator', 'file-manager', 'git-agent', 'search-agent', 'reporter', 'automation'],
320
- },
321
- task: { type: 'string', description: 'The task for the agent to plan.' },
322
- context: { type: 'string', description: 'Optional additional context.' },
323
- 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.' },
324
- },
325
- required: ['agent', 'task'],
326
- },
327
- },
328
- {
329
- name: 'veto_code_review',
330
- description: 'Runs the Code Reviewer agent on provided code. Returns scored findings (complexity, error handling, magic numbers, nesting, dead code) with severity and fixes. Pass file_path to surface findings as VS Code inline diagnostics (squiggles).',
331
- inputSchema: {
332
- type: 'object',
333
- properties: {
334
- code: { type: 'string', description: 'The code to review.' },
335
- context: { type: 'string', description: 'Optional: file name, module description, or review focus.' },
336
- file_path: { type: 'string', description: 'Optional: absolute path to the file being reviewed. When provided, findings are stored as VS Code inline diagnostics.' },
337
- },
338
- required: ['code'],
339
- },
340
- },
341
- {
342
- name: 'veto_diff_review',
343
- 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.',
344
- inputSchema: {
345
- type: 'object',
346
- properties: {
347
- diff: { type: 'string', description: 'The git diff to review. If omitted, Veto runs git diff HEAD in project_dir.' },
348
- project_dir: { type: 'string', description: 'Absolute project path. Used to auto-read git diff if diff is not provided, and to inject codebase context.' },
349
- context: { type: 'string', description: 'Optional: PR description, ticket number, or focus area.' },
350
- },
351
- required: [],
352
- },
353
- },
354
- {
355
- name: 'veto_security_scan',
356
- description: 'Runs the Security Scanner (OWASP Top 10) on provided code. Returns vulnerabilities with severity, CWE/OWASP category, and remediation steps. Pass file_path to surface findings as VS Code inline diagnostics.',
357
- inputSchema: {
358
- type: 'object',
359
- properties: {
360
- code: { type: 'string', description: 'The code to scan.' },
361
- context: { type: 'string', description: 'Optional: language, framework, or specific concerns.' },
362
- file_path: { type: 'string', description: 'Optional: absolute path to the file being scanned. When provided, findings are stored as VS Code inline diagnostics.' },
363
- },
364
- required: ['code'],
365
- },
366
- },
367
- {
368
- name: 'veto_secrets_scan',
369
- description: 'Scans text or code for exposed credentials — API keys, tokens, passwords, connection strings, private keys. Returns findings with masked values and line numbers. Pass file_path to surface findings as VS Code inline diagnostics.',
370
- inputSchema: {
371
- type: 'object',
372
- properties: {
373
- text: { type: 'string', description: 'The text or code to scan for secrets.' },
374
- file_path: { type: 'string', description: 'Optional: absolute path to the file being scanned. When provided, findings are stored as VS Code inline diagnostics.' },
375
- },
376
- required: ['text'],
377
- },
378
- },
379
- {
380
- name: 'veto_execute_parallel',
381
- description: 'Runs multiple worker agents simultaneously via Promise.all. Use to get domain expert input from several agents in one round-trip — e.g. coder + tester + security-scanner all planning the same feature together.',
382
- inputSchema: {
383
- type: 'object',
384
- properties: {
385
- tasks: {
386
- type: 'array',
387
- description: 'List of agent tasks to run in parallel.',
388
- items: {
389
- type: 'object',
390
- properties: {
391
- id: { type: 'string', description: 'Unique ID for this task (use any string).' },
392
- agent: { type: 'string', description: 'Worker agent type.' },
393
- task: { type: 'string', description: 'Task description for this agent.' },
394
- code: { type: 'string', description: 'Optional code to analyze (triggers analyze() instead of plan()).' },
395
- context: { type: 'string', description: 'Optional additional context.' },
396
- project_dir: { type: 'string', description: 'Optional: per-task project dir override.' },
397
- },
398
- required: ['id', 'agent', 'task'],
399
- },
400
- },
401
- project_dir: { type: 'string', description: 'Optional: project directory applied to all tasks (per-task project_dir overrides this). Auto-injects codebase context.' },
402
- max_tokens: {
403
- type: 'number',
404
- description: 'Optional: token budget for this parallel execution. Veto estimates combined output tokens and warns if the estimate exceeds this limit. Logged to usage_log.',
405
- },
406
- },
407
- required: ['tasks'],
408
- },
409
- },
410
- {
411
- name: 'veto_memory_store',
412
- description: 'Stores a knowledge entry (solution, pattern, error, reference, or decision) in the local knowledge base for retrieval across sessions. Search before storing to avoid duplicates.',
413
- inputSchema: {
414
- type: 'object',
415
- properties: {
416
- title: { type: 'string', description: 'Precise, searchable title. Bad: "Fixed bug". Good: "Fix: Node sqlite fails on Windows without --experimental-sqlite".' },
417
- content: { type: 'string', description: 'Self-contained content: problem → root cause → solution. Future agents must understand it without original context.' },
418
- type: {
419
- type: 'string',
420
- description: 'Entry type.',
421
- enum: ['solution', 'pattern', 'context', 'error', 'reference', 'decision'],
422
- },
423
- tags: { type: 'array', items: { type: 'string' }, description: 'Search tags (3–5 recommended). Examples: ["typescript", "auth", "jwt"].' },
424
- project_dir: { type: 'string', description: 'Absolute project path. Include for project-specific knowledge; omit for general programming knowledge.' },
425
- session_id: { type: 'string', description: 'Optional: associate this knowledge entry with an active session.' },
426
- relevance: { type: 'number', description: 'Initial relevance score 0.0–1.0 (default 1.0).' },
427
- },
428
- required: ['title', 'content'],
429
- },
430
- },
431
- {
432
- name: 'veto_memory_search',
433
- description: 'Searches the local knowledge base for entries matching a query. Call at the start of every task to find prior solutions before solving from scratch.',
434
- inputSchema: {
435
- type: 'object',
436
- properties: {
437
- query: { type: 'string', description: 'Search terms (full-text search on title and content).' },
438
- type: {
439
- type: 'string',
440
- description: 'Filter by entry type.',
441
- enum: ['solution', 'pattern', 'context', 'error', 'reference', 'decision'],
442
- },
443
- project_dir: { type: 'string', description: 'Filter to a specific project directory.' },
444
- limit: { type: 'number', description: 'Max results to return (default 10, max 50).' },
445
- },
446
- required: [],
447
- },
448
- },
449
- {
450
- name: 'veto_memory_delete',
451
- description: 'Deletes a knowledge entry by ID. Use to remove stale or duplicate entries found via veto_memory_search.',
452
- inputSchema: {
453
- type: 'object',
454
- properties: {
455
- id: { type: 'string', description: 'The knowledge entry ID (from veto_memory_search results).' },
456
- },
457
- required: ['id'],
458
- },
459
- },
460
- {
461
- name: 'veto_project_map_update',
462
- description: 'Updates the project structure map for a directory. Call after creating, deleting, or moving files. The map enables fast codebase navigation without filesystem scans.',
463
- inputSchema: {
464
- type: 'object',
465
- properties: {
466
- project_dir: { type: 'string', description: 'Absolute path to the project root.' },
467
- structure: { type: 'string', description: 'JSON string representing the directory tree. Example: {"src/":{"agents/":["coder.ts","reviewer.ts"],"router/":["index.ts"]}}' },
468
- key_modules: {
469
- type: 'array',
470
- items: { type: 'string' },
471
- description: 'The 10–20 most important files with their roles. Example: ["src/server.ts (MCP entry point)", "src/router/index.ts (task router)"].',
472
- },
473
- tech_stack: {
474
- type: 'array',
475
- items: { type: 'string' },
476
- description: 'Frameworks and key libraries. Example: ["TypeScript", "Node.js 22", "Express", "SQLite"].',
477
- },
478
- },
479
- required: ['project_dir', 'structure'],
480
- },
481
- },
482
- {
483
- name: 'veto_project_map_get',
484
- description: 'Returns the stored project structure map for a directory. Use to navigate the codebase without scanning the filesystem.',
485
- inputSchema: {
486
- type: 'object',
487
- properties: {
488
- project_dir: { type: 'string', description: 'Absolute path to the project root.' },
489
- },
490
- required: ['project_dir'],
491
- },
492
- },
493
- {
494
- name: 'veto_pattern_store',
495
- description: 'Stores or updates a coding pattern observed in the codebase. Patterns are keyed by category.pattern-name and confidence increases with repeated observation.',
496
- inputSchema: {
497
- type: 'object',
498
- properties: {
499
- pattern_key: { type: 'string', description: 'Pattern identifier in category.pattern-name format. Example: "code.async-pattern" or "naming.variable-case".' },
500
- pattern_val: { type: 'string', description: 'The observed pattern value. Example: "async/await with try/catch, no raw Promise chains".' },
501
- confidence: { type: 'number', description: 'Confidence score 0.0–1.0 (default 1.0). Increases automatically on repeated observation.' },
502
- },
503
- required: ['pattern_key', 'pattern_val'],
504
- },
505
- },
506
- {
507
- name: 'veto_patterns_list',
508
- description: 'Returns stored coding patterns. Filter by prefix to get patterns in a specific category (e.g. prefix="naming." for all naming conventions).',
509
- inputSchema: {
510
- type: 'object',
511
- properties: {
512
- prefix: { type: 'string', description: 'Optional prefix filter. Example: "code." or "naming." or "testing.".' },
513
- limit: { type: 'number', description: 'Max patterns to return (default 20).' },
514
- },
515
- required: [],
516
- },
517
- },
518
- {
519
- name: 'veto_memory_export',
520
- description: 'Exports all local memory (sessions, knowledge, patterns, decisions, project maps) to a portable JSON file. Copy the file to another machine and run veto_memory_import there to resume work. No external services required.',
521
- inputSchema: {
522
- type: 'object',
523
- properties: {
524
- output_path: {
525
- type: 'string',
526
- description: 'Where to write the export file. Defaults to ~/.veto/veto-export.json. Use a path on shared storage (Dropbox, OneDrive, USB) to make transfer easy.',
527
- },
528
- },
529
- required: [],
530
- },
531
- },
532
- {
533
- name: 'veto_memory_import',
534
- description: 'Imports memory from a JSON file exported by veto_memory_export on another machine. Merges into local SQLite using INSERT OR IGNORE — existing local rows are never overwritten. Call veto_sessions_list after import to confirm sessions arrived.',
535
- inputSchema: {
536
- type: 'object',
537
- properties: {
538
- input_path: {
539
- type: 'string',
540
- description: 'Path to the export JSON file. Defaults to ~/.veto/veto-export.json.',
541
- },
542
- },
543
- required: [],
544
- },
545
- },
546
- {
547
- name: 'veto_record_outcome',
548
- description: 'Records a task outcome (quality score) to feed the self-learning router. Call after completing any task. After 20+ outcomes, call veto_learning_apply to update tier thresholds.',
549
- inputSchema: {
550
- type: 'object',
551
- properties: {
552
- task_type: { type: 'string', description: 'Short consistent label for the task category (e.g. "write-unit-tests", "fix-auth-bug"). Use the same label for similar tasks to enable pattern detection.' },
553
- complexity: { type: 'number', description: 'The complexity score from veto_route_task (0–100).' },
554
- model_tier: { type: 'number', description: 'The tier that was actually used (1, 2, or 3).', enum: [1, 2, 3] },
555
- 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.' },
556
- agent: { type: 'string', description: 'The worker agent type used (optional but useful for agent performance tracking).' },
557
- tokens_used: { type: 'number', description: 'Approximate tokens used (optional).' },
558
- 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.' },
559
- },
560
- required: ['task_type', 'complexity', 'model_tier', 'output_quality'],
561
- },
562
- },
563
- {
564
- name: 'veto_learning_stats',
565
- description: 'Returns the self-learning router dashboard: tier distribution, per-agent quality stats, suggested threshold adjustments, and council insights. Use to understand how the router is performing and where to improve.',
566
- inputSchema: {
567
- type: 'object',
568
- properties: {
569
- include_agent_stats: { type: 'boolean', description: 'Include per-agent quality breakdown (default true).' },
570
- include_task_types: { type: 'boolean', description: 'Include per-task-type breakdown (default false, verbose).' },
571
- include_council_insights: { type: 'boolean', description: 'Include council decision → debugging correlation (default false).' },
572
- },
573
- required: [],
574
- },
575
- },
576
- {
577
- name: 'veto_learning_apply',
578
- description: 'Applies learned tier thresholds to the router based on recorded task outcomes. Requires at least 20 recorded outcomes. The router immediately uses the new thresholds on the next veto_route_task call.',
579
- inputSchema: {
580
- type: 'object',
581
- properties: {},
582
- required: [],
583
- },
584
- },
585
- {
586
- name: 'veto_handoff',
587
- description: 'Saves the current session and returns step-by-step instructions to continue on another AI platform (Gemini or Codex). Call this when Claude is approaching its rate limit. The receiving platform calls veto_continue to restore full context instantly.',
588
- inputSchema: {
589
- type: 'object',
590
- properties: {
591
- summary: { type: 'string', description: 'What was accomplished this session — one or two sentences.' },
592
- context: { type: 'string', description: 'Key context the next platform needs: active decisions, file paths, constraints.' },
593
- task_state: { type: 'string', description: 'Current task state — what is done, what is in progress, what is next.' },
594
- from_platform: { type: 'string', enum: ['claude', 'gemini', 'codex'], description: 'Platform handing off (default: claude).' },
595
- to_platform: { type: 'string', enum: ['gemini', 'codex', 'claude'], description: 'Target platform. If omitted, Veto picks the platform with the most headroom.' },
596
- project_dir: { type: 'string', description: 'Absolute path to the current project directory.' },
597
- token_count: { type: 'number', description: 'Approximate tokens used this session.' },
598
- },
599
- required: ['summary', 'context'],
600
- },
601
- },
602
- {
603
- name: 'veto_continue',
604
- description: 'Restores the most recent session on any platform. Call this immediately after switching platforms — Veto returns the full context, summary, and next action. Nothing needs to be re-explained.',
605
- inputSchema: {
606
- type: 'object',
607
- properties: {
608
- session_id: { type: 'string', description: 'Optional. Session ID from veto_handoff. If omitted, the most recent saved session is restored.' },
609
- 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'] },
610
- },
611
- required: [],
612
- },
613
- },
614
- {
615
- name: 'veto_platform_setup',
616
- description: 'Returns the exact MCP config and setup steps to connect a specific AI platform to this Veto server.',
617
- inputSchema: {
618
- type: 'object',
619
- properties: {
620
- platform: { type: 'string', enum: ['claude', 'gemini', 'codex'], description: 'The platform to get setup instructions for.' },
621
- veto_server_path: { type: 'string', description: 'Absolute path to the built veto server (dist/server.js).' },
622
- },
623
- required: ['platform', 'veto_server_path'],
624
- },
625
- },
626
- {
627
- name: 'veto_watch',
628
- 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.',
629
- inputSchema: {
630
- type: 'object',
631
- properties: {
632
- project_dir: { type: 'string', description: 'Absolute path to the project directory to watch.' },
633
- },
634
- required: ['project_dir'],
635
- },
636
- },
637
- {
638
- name: 'veto_watch_poll',
639
- 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.',
640
- inputSchema: {
641
- type: 'object',
642
- properties: {
643
- watch_id: { type: 'string', description: 'The watch_id returned by veto_watch.' },
644
- },
645
- required: ['watch_id'],
646
- },
647
- },
648
- {
649
- name: 'veto_watch_stop',
650
- description: 'Stops an active file watcher.',
651
- inputSchema: {
652
- type: 'object',
653
- properties: {
654
- watch_id: { type: 'string', description: 'The watch_id returned by veto_watch.' },
655
- },
656
- required: ['watch_id'],
657
- },
658
- },
659
- {
660
- name: 'veto_workflow',
661
- 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).',
662
- inputSchema: {
663
- type: 'object',
664
- properties: {
665
- steps: {
666
- type: 'array',
667
- description: 'Ordered pipeline steps.',
668
- items: {
669
- type: 'object',
670
- properties: {
671
- id: { type: 'string', description: 'Step identifier.' },
672
- agent: { type: 'string', description: 'Worker agent type.' },
673
- task: { type: 'string', description: 'Task description for this step.' },
674
- code: { type: 'string', description: 'Optional code to analyze.' },
675
- context: { type: 'string', description: 'Optional context.' },
676
- gate: { type: 'number', description: 'Optional minimum confidence % (0–100) required to proceed to the next step.' },
677
- },
678
- required: ['id', 'agent', 'task'],
679
- },
680
- },
681
- project_dir: { type: 'string', description: 'Optional project directory — auto-injects codebase context into all steps.' },
682
- },
683
- required: ['steps'],
684
- },
685
- },
686
- {
687
- name: 'veto_explain',
688
- 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.',
689
- inputSchema: {
690
- type: 'object',
691
- properties: {
692
- file_path: { type: 'string', description: 'Absolute path to the file to explain.' },
693
- depth: { type: 'string', enum: ['overview', 'detailed', 'line-by-line'], description: 'Explanation depth. Default: overview.' },
694
- context: { type: 'string', description: 'Optional additional context about what you want explained.' },
695
- },
696
- required: ['file_path'],
697
- },
698
- },
699
- {
700
- name: 'veto_plugins',
701
- 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.',
702
- inputSchema: {
703
- type: 'object',
704
- properties: {},
705
- required: [],
706
- },
707
- },
708
- // ── Phase 13: Developer Intelligence ──────────────────────────────────────
709
- {
710
- name: 'veto_docs_fetch',
711
- description: 'Fetches current, version-accurate documentation for any npm, PyPI, or crates.io package and returns it for injection into agent context. Eliminates hallucinated APIs. Results are cached for 24 hours.',
712
- inputSchema: {
713
- type: 'object',
714
- properties: {
715
- package_name: { type: 'string', description: 'Package name (e.g. "react", "requests", "serde").' },
716
- ecosystem: { type: 'string', enum: ['npm', 'pypi', 'crates'], description: 'Package ecosystem.' },
717
- version: { type: 'string', description: 'Specific version. Defaults to latest.' },
718
- max_chars: { type: 'number', description: 'Max characters to return (default 8000). Higher = more complete docs, more tokens.' },
719
- },
720
- required: ['package_name', 'ecosystem'],
721
- },
722
- },
723
- {
724
- name: 'veto_context_status',
725
- description: 'Returns the context window usage for a saved session — tokens used, % of platform limit consumed, and whether to compress or hand off before the window fills.',
726
- inputSchema: {
727
- type: 'object',
728
- properties: {
729
- session_id: { type: 'string', description: 'Session ID to check.' },
730
- },
731
- required: ['session_id'],
732
- },
733
- },
734
- {
735
- name: 'veto_task_parse',
736
- description: 'Parses a plain-English project description or PRD into a structured task DAG with dependencies, complexity scores, priorities, and suggested agent assignments. Feeds directly into veto_workflow.',
737
- inputSchema: {
738
- type: 'object',
739
- properties: {
740
- description: { type: 'string', description: 'Project description, PRD, or feature brief to parse into tasks.' },
741
- project_dir: { type: 'string', description: 'Optional project directory for codebase context injection.' },
742
- max_tasks: { type: 'number', description: 'Maximum number of tasks to generate (default 20).' },
743
- },
744
- required: ['description'],
745
- },
746
- },
747
- // ── Phase 14: Observability & Safety ──────────────────────────────────────
748
- {
749
- name: 'veto_usage_status',
750
- description: 'Live AI usage dashboard. Shows tokens consumed today, requests per platform, subscription vs API usage split, 7-day history, and warnings when approaching limits.',
751
- inputSchema: {
752
- type: 'object',
753
- properties: {},
754
- required: [],
755
- },
756
- },
757
- {
758
- name: 'veto_audit_log',
759
- description: 'Queryable log of every council verdict, decision, and session event. Filter by session, agent, verdict, or date. Essential for tracing what happened and why.',
760
- inputSchema: {
761
- type: 'object',
762
- properties: {
763
- session_id: { type: 'string', description: 'Filter to a specific session.' },
764
- verdict: { type: 'string', description: 'Filter by council verdict (GREEN, YELLOW, RED).' },
765
- since: { type: 'string', description: 'ISO date — only return events after this time.' },
766
- limit: { type: 'number', description: 'Max events to return (default 20, max 100).' },
767
- },
768
- required: [],
769
- },
770
- },
771
- {
772
- name: 'veto_health',
773
- description: 'Returns a live health snapshot of the Veto server — DB size, session/memory/pattern counts, uptime, error count, and average council latency.',
774
- inputSchema: {
775
- type: 'object',
776
- properties: {},
777
- required: [],
778
- },
779
- },
780
- // ── Phase 15: CI/CD & Distribution ────────────────────────────────────────
781
- {
782
- name: 'veto_ci_gate',
783
- description: 'CI/CD pipeline gate. Runs code review + security scan + secrets scan on a git diff and returns a structured pass/warn/fail verdict with exit code. Ready for GitHub Actions and GitLab CI.',
784
- inputSchema: {
785
- type: 'object',
786
- properties: {
787
- project_dir: { type: 'string', description: 'Absolute project path. Veto reads git diff HEAD automatically.' },
788
- diff: { type: 'string', description: 'Optional: pass a diff string directly instead of reading from project_dir.' },
789
- context: { type: 'string', description: 'Optional: PR description or ticket number for context.' },
790
- fail_on: { type: 'string', enum: ['warn', 'fail'], description: 'Whether WARN counts as a failure (exit code 1). Default: "fail" — only FAIL exits non-zero.' },
791
- },
792
- required: ['project_dir'],
793
- },
794
- },
795
- {
796
- name: 'veto_pr_review',
797
- description: 'Fetches a GitHub PR diff and runs the full Veto triple-scan (code review + security + secrets). Returns a structured verdict and ready-to-post GitHub review comments. Set GITHUB_TOKEN env var for private repos.',
798
- inputSchema: {
799
- type: 'object',
800
- properties: {
801
- pr_url: { type: 'string', description: 'Full GitHub PR URL. e.g. https://github.com/owner/repo/pull/123' },
802
- context: { type: 'string', description: 'Optional: PR description or ticket number for extra context.' },
803
- fail_on: { type: 'string', enum: ['warn', 'fail'], description: 'Whether WARN counts as a failure. Default: "fail".' },
804
- },
805
- required: ['pr_url'],
806
- },
807
- },
808
- // ── Phase 16: Workspace Discovery & Summarization ─────────────────────────
809
- {
810
- name: 'veto_discover',
811
- description: 'Scans a project directory and builds a rich context map: git state, tech stack, file structure, dependencies, and key config files. Stores the result in Veto memory so agents always have accurate project context. Call this once per project or after major structural changes.',
812
- inputSchema: {
813
- type: 'object',
814
- properties: {
815
- project_dir: { type: 'string', description: 'Absolute path to the project directory to scan.' },
816
- depth: { type: 'string', enum: ['quick', 'standard', 'full'], description: 'Scan depth. quick: git + package metadata only. standard: + file tree up to 3 levels (default). full: + contents of key config files.' },
817
- store: { type: 'boolean', description: 'Whether to store the discovery in Veto memory as a project map. Default: true.' },
818
- },
819
- required: ['project_dir'],
820
- },
821
- },
822
- {
823
- name: 'veto_summarize',
824
- description: 'Generates a concise expert briefing of a project, directory, or file. Use at the start of a session to orient yourself on unfamiliar code. Returns bullet-point summary, key components, tech stack, and entry points. Faster and higher-level than veto_explain.',
825
- inputSchema: {
826
- type: 'object',
827
- properties: {
828
- project_dir: { type: 'string', description: 'Absolute path to a project directory to summarize.' },
829
- file_path: { type: 'string', description: 'Absolute path to a single file to summarize. If both project_dir and file_path are given, file_path takes precedence.' },
830
- focus: { type: 'string', description: 'Optional focus area: e.g. "security", "APIs", "data flow", "architecture". Narrows the summary.' },
831
- format: { type: 'string', enum: ['brief', 'detailed'], description: 'brief: 4–6 bullet points (default). detailed: paragraph-level prose.' },
832
- },
833
- required: [],
834
- },
835
- },
836
- {
837
- name: 'veto_benchmark',
838
- description: 'Compares two competing approaches by running a full council debate on each in parallel, then returns a structured winner analysis with verdict, confidence delta, warning counts, and council reasoning. Use when you have two valid options and want an unbiased council judgment before committing.',
839
- inputSchema: {
840
- type: 'object',
841
- properties: {
842
- task: { type: 'string', description: 'The decision context — what problem are both approaches solving?' },
843
- approach_a: { type: 'string', description: 'First approach to evaluate. Be specific about tech choices, trade-offs, and constraints.' },
844
- approach_b: { type: 'string', description: 'Second approach to evaluate. Same level of detail as approach_a.' },
845
- context: { type: 'string', description: 'Optional: shared context for both debates (architecture notes, constraints, team size, etc.).' },
846
- project_dir: { type: 'string', description: 'Optional: auto-inject package.json and git diff context.' },
847
- },
848
- required: ['task', 'approach_a', 'approach_b'],
849
- },
850
- },
851
- ];
124
+ const tools = TOOL_DEFINITIONS;
852
125
  return { tools: tools.map(t => ({ ...t, annotations: TOOL_ANNOTATIONS[t.name] ?? {} })) };
853
126
  });
854
127
  // ─── Shared Scan Utility ──────────────────────────────────────────────────────
@@ -918,10 +191,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
918
191
  case 'veto_status': {
919
192
  const statusTokenCount = typeof args?.token_count === 'number' ? args.token_count : null;
920
193
  const statusPlatform = args?.platform ? String(args.platform) : 'claude';
194
+ const statusModel = args?.model ? String(args.model) : undefined;
921
195
  if (statusTokenCount !== null && statusTokenCount > 0) {
922
196
  trackTokens(statusPlatform, statusTokenCount);
923
197
  }
924
- const autoSaveResult = statusTokenCount !== null ? maybeAutoSave(statusTokenCount, statusPlatform) : null;
198
+ const autoSaveResult = statusTokenCount !== null ? maybeAutoSave(statusTokenCount, statusPlatform, statusModel) : null;
925
199
  return {
926
200
  content: [
927
201
  {
@@ -990,6 +264,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
990
264
  connection_type: args?.connection_type ? String(args.connection_type) : 'subscription',
991
265
  project_dir: sessionProjectDir,
992
266
  token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
267
+ tags: Array.isArray(args?.tags) ? args.tags.map(String) : undefined,
993
268
  };
994
269
  let result;
995
270
  let wasUpdate = false;
@@ -1007,7 +282,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1007
282
  result = saveSession(sessionInput);
1008
283
  }
1009
284
  // Cache for auto-save: future veto_status calls with high token_count will re-save this context
1010
- autoSave.cached = { summary: saveSummary, context: saveContext, task_state: saveTaskState, platform: savePlatform, project_dir: sessionProjectDir };
285
+ const saveModel = args?.model ? String(args.model) : undefined;
286
+ const resolvedWindow = saveModel ? resolveContextWindow(savePlatform, saveModel) : undefined;
287
+ autoSave.cached = { summary: saveSummary, context: saveContext, task_state: saveTaskState, platform: savePlatform, project_dir: sessionProjectDir, context_window: resolvedWindow };
1011
288
  autoSave.last_save_at = result.saved_at;
1012
289
  autoSave.last_session_id = result.session_id;
1013
290
  const responseObj = {
@@ -1068,7 +345,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1068
345
  }
1069
346
  case 'veto_sessions_list': {
1070
347
  const limit = Math.min(typeof args?.limit === 'number' ? args.limit : 10, 50);
1071
- const sessions = listSessions(limit);
348
+ const query = args?.query ? String(args.query).trim() : undefined;
349
+ const sessions = listSessions(limit, query);
1072
350
  return {
1073
351
  content: [
1074
352
  {
@@ -1083,6 +361,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1083
361
  project_dir: s.project_dir,
1084
362
  summary: s.summary,
1085
363
  token_count: s.token_count,
364
+ tags: s.tags ? JSON.parse(s.tags) : [],
1086
365
  })),
1087
366
  }, null, 2),
1088
367
  },
@@ -1127,11 +406,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1127
406
  isError: true,
1128
407
  };
1129
408
  }
409
+ const strictnessArg = (['fast', 'standard', 'strict'].includes(String(args?.strictness ?? '')))
410
+ ? String(args.strictness)
411
+ : 'standard';
1130
412
  const debateStart = Date.now();
1131
- const result = runDebate({
413
+ const result = await runLlmDebate(server, {
1132
414
  task,
1133
415
  context: args?.context ? String(args.context) : undefined,
1134
416
  project_dir: args?.project_dir ? String(args.project_dir) : undefined,
417
+ strictness: strictnessArg,
1135
418
  });
1136
419
  const debateDuration = Date.now() - debateStart;
1137
420
  const sessionId = args?.session_id ? String(args.session_id) : undefined;
@@ -1156,22 +439,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1156
439
  recordOutcome(task.slice(0, 50), 50, tMap[result.final_verdict] ?? 2, 'council', qMap[result.final_verdict] ?? 50);
1157
440
  }
1158
441
  // Auto-store RED verdicts so they appear in the Memory panel immediately
1159
- if (result.final_verdict === 'RED') {
442
+ if (result.final_verdict === 'RED' || (result.final_verdict === 'YELLOW' && (result.warnings.length >= 2 || result.block_reasons.length > 0))) {
443
+ const isRed = result.final_verdict === 'RED';
1160
444
  const lines = [`Task: ${task}`];
1161
445
  if (result.block_reasons.length > 0)
1162
446
  lines.push(`\nBlocked by:\n${result.block_reasons.map(r => `- ${r}`).join('\n')}`);
1163
447
  if (result.warnings.length > 0)
1164
448
  lines.push(`\nWarnings:\n${result.warnings.map(w => `- ${w}`).join('\n')}`);
449
+ // Include per-agent reasoning so future debates inherit the full context
450
+ const agentSummary = Object.entries(result.votes)
451
+ .filter(([, v]) => v.verdict !== 'approve')
452
+ .map(([name, v]) => `- ${name} [${v.verdict}]: ${v.reason}`)
453
+ .join('\n');
454
+ if (agentSummary)
455
+ lines.push(`\nAgent reasoning:\n${agentSummary}`);
1165
456
  if (result.recommended)
1166
457
  lines.push(`\nRecommended: ${result.recommended}`);
1167
458
  storeKnowledge({
1168
459
  type: 'decision',
1169
- title: `RED: ${task.slice(0, 80)}`,
460
+ title: `${result.final_verdict}: ${task.slice(0, 80)}`,
1170
461
  content: lines.join(''),
1171
- tags: ['red-verdict', 'blocked', 'council'],
462
+ tags: [isRed ? 'red-verdict' : 'yellow-verdict', isRed ? 'blocked' : 'caution', 'council'],
1172
463
  project_dir: args?.project_dir ? String(args.project_dir) : undefined,
1173
464
  session_id: sessionId,
1174
- relevance: 1.0,
465
+ relevance: isRed ? 1.0 : 0.8,
1175
466
  });
1176
467
  }
1177
468
  const responsePayload = {
@@ -1182,13 +473,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1182
473
  recommended: result.recommended,
1183
474
  debated_at: result.debated_at,
1184
475
  votes: {
1185
- lead_dev: result.votes.lead_dev.verdict,
1186
- pm: result.votes.pm.verdict,
1187
- architect: result.votes.architect.verdict,
1188
- ux: result.votes.ux.verdict,
1189
- devil: result.votes.devil.verdict,
1190
- legal: result.votes.legal.verdict,
1191
- security: result.votes.security.verdict,
476
+ lead_dev: result.votes.lead_dev,
477
+ pm: result.votes.pm,
478
+ architect: result.votes.architect,
479
+ ux: result.votes.ux,
480
+ devil: result.votes.devil,
481
+ legal: result.votes.legal,
482
+ security: result.votes.security,
1192
483
  },
1193
484
  };
1194
485
  const fullText = result.formatted_output + '\n\n' + JSON.stringify(responsePayload, null, 2);
@@ -1719,42 +1010,53 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1719
1010
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1720
1011
  }
1721
1012
  case 'veto_explain': {
1722
- const filePath = String(args?.file_path ?? '').trim();
1723
- if (!filePath)
1724
- return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'file_path is required.' }) }], isError: true };
1725
- let fileContent;
1726
- try {
1727
- fileContent = readFileSync(filePath, 'utf8');
1013
+ const filePath = args?.file_path ? String(args.file_path).trim() : '';
1014
+ const rawText = args?.text ? String(args.text).trim() : '';
1015
+ const depth = String(args?.depth ?? 'overview');
1016
+ const userContext = args?.context ? String(args.context) : undefined;
1017
+ if (!filePath && !rawText) {
1018
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'Provide file_path or text.' }) }], isError: true };
1728
1019
  }
1729
- catch {
1730
- return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Cannot read file: ${filePath}` }) }], isError: true };
1020
+ let fileContent;
1021
+ let agent;
1022
+ let taskLabel;
1023
+ if (rawText) {
1024
+ fileContent = rawText;
1025
+ // Auto-detect agent from content: error messages → debugger, stack traces → debugger, code → coder
1026
+ const looksLikeError = /error|exception|traceback|stack trace|at \w+\.|TypeError|SyntaxError|ENOENT/i.test(rawText);
1027
+ agent = looksLikeError ? 'debugger' : 'coder';
1028
+ taskLabel = looksLikeError ? 'raw error/stack trace' : 'raw text input';
1731
1029
  }
1732
- const ext = extname(filePath).toLowerCase();
1733
- const name_ = basename(filePath).toLowerCase();
1734
- const depth = String(args?.depth ?? 'overview');
1735
- // auto-detect best agent
1736
- let agent = 'coder';
1737
- if (['.tsx', '.jsx', '.vue', '.svelte'].includes(ext))
1738
- agent = 'frontend';
1739
- else if (['.sql', '.prisma'].includes(ext) || name_.includes('schema'))
1740
- agent = 'database';
1741
- else if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(name_))
1742
- agent = 'tester';
1743
- else if (['.yaml', '.yml', '.toml', '.dockerfile'].includes(ext) || name_ === 'dockerfile')
1744
- agent = 'devops';
1745
- else if (name_.includes('auth') || name_.includes('login') || name_.includes('jwt') || name_.includes('token'))
1746
- agent = 'auth';
1747
- else if (name_.includes('security') || name_.includes('crypt'))
1748
- agent = 'security-scanner';
1749
- else if (['.ts', '.js', '.mjs'].includes(ext))
1030
+ else {
1031
+ try {
1032
+ fileContent = readFileSync(filePath, 'utf8');
1033
+ }
1034
+ catch {
1035
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Cannot read file: ${filePath}` }) }], isError: true };
1036
+ }
1037
+ const ext = extname(filePath).toLowerCase();
1038
+ const name_ = basename(filePath).toLowerCase();
1750
1039
  agent = 'coder';
1751
- const userContext = args?.context ? String(args.context) : undefined;
1752
- const task = `Explain this ${ext} file at ${depth} depth. File: ${basename(filePath)}${userContext ? `. Focus: ${userContext}` : ''}`;
1040
+ if (['.tsx', '.jsx', '.vue', '.svelte'].includes(ext))
1041
+ agent = 'frontend';
1042
+ else if (['.sql', '.prisma'].includes(ext) || name_.includes('schema'))
1043
+ agent = 'database';
1044
+ else if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(name_))
1045
+ agent = 'tester';
1046
+ else if (['.yaml', '.yml', '.toml', '.dockerfile'].includes(ext) || name_ === 'dockerfile')
1047
+ agent = 'devops';
1048
+ else if (name_.includes('auth') || name_.includes('login') || name_.includes('jwt') || name_.includes('token'))
1049
+ agent = 'auth';
1050
+ else if (name_.includes('security') || name_.includes('crypt'))
1051
+ agent = 'security-scanner';
1052
+ taskLabel = `${ext} file: ${basename(filePath)}`;
1053
+ }
1054
+ const task = `Explain this ${taskLabel} at ${depth} depth.${userContext ? ` Focus: ${userContext}` : ''}`;
1753
1055
  const result = await executeOne({ id: 'explain-1', agent, task, code: fileContent, project_dir: undefined });
1754
- autoRecord(`explain ${basename(filePath)}`, agent, Math.round(result.output.confidence * 100));
1056
+ autoRecord(`explain ${taskLabel}`, agent, Math.round(result.output.confidence * 100));
1755
1057
  return {
1756
1058
  content: [{ type: 'text', text: JSON.stringify({
1757
- file: filePath, agent_used: agent, depth,
1059
+ source: filePath || 'raw text', agent_used: agent, depth,
1758
1060
  explanation: result.plan ?? result.analysis,
1759
1061
  output: result.output,
1760
1062
  }, null, 2) }],
@@ -1796,6 +1098,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1796
1098
  if (!description) {
1797
1099
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'description is required.' }) }], isError: true };
1798
1100
  }
1101
+ const hash = createHash('sha256').update(description).digest('hex').slice(0, 16);
1102
+ const cached = getTaskPlan(hash);
1103
+ if (cached) {
1104
+ const plan = JSON.parse(cached.plan_json);
1105
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, plan_id: cached.id, cached: true, ...plan }, null, 2) }] };
1106
+ }
1799
1107
  const ctx = project_dir ? buildContextString(project_dir) : '';
1800
1108
  const planResult = await executeOne({ id: 'task-parse-1', agent: 'task-planner', task: `Parse this project description into a structured task breakdown with dependencies and complexity scores (max ${max_tasks} tasks):\n\n${description}`, context: ctx || undefined, project_dir });
1801
1109
  // Build structured task DAG from planner output
@@ -1858,7 +1166,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1858
1166
  duration_estimate: planResult.plan?.duration_estimate ?? 'unknown',
1859
1167
  };
1860
1168
  autoRecord(description, 'task-planner', Math.round(planResult.output.confidence * 100));
1861
- const hash = createHash('sha256').update(description).digest('hex').slice(0, 16);
1862
1169
  const plan_id = saveTaskPlan(JSON.stringify(plan), hash, project_dir);
1863
1170
  return { content: [{ type: 'text', text: JSON.stringify({ success: true, plan_id, ...plan }, null, 2) }] };
1864
1171
  }
@@ -2196,9 +1503,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2196
1503
  throw new Error('veto_benchmark requires task, approach_a, and approach_b');
2197
1504
  }
2198
1505
  const bmStart = Date.now();
2199
- // Run both debates synchronously (council agents are all sync)
2200
- const debateA = runDebate({ task: `${task}\n\nApproach A: ${approachA}`, context: ctx, project_dir: projectDir });
2201
- const debateB = runDebate({ task: `${task}\n\nApproach B: ${approachB}`, context: ctx, project_dir: projectDir });
1506
+ // Run both debates in parallel via LLM council
1507
+ const [debateA, debateB] = await Promise.all([
1508
+ runLlmDebate(server, { task: `${task}\n\nApproach A: ${approachA}`, context: ctx, project_dir: projectDir }),
1509
+ runLlmDebate(server, { task: `${task}\n\nApproach B: ${approachB}`, context: ctx, project_dir: projectDir }),
1510
+ ]);
2202
1511
  // Score: GREEN=3, YELLOW=2, RED=1, DEADLOCK=0
2203
1512
  const verdictScore = { GREEN: 3, YELLOW: 2, RED: 1, DEADLOCK: 0 };
2204
1513
  const scoreA = verdictScore[debateA.final_verdict] ?? 0;
@@ -2266,6 +1575,102 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2266
1575
  duration_ms: Date.now() - bmStart,
2267
1576
  }, null, 2) }] };
2268
1577
  }
1578
+ // ── Part 4: New Features ───────────────────────────────────────────────────
1579
+ case 'veto_metrics': {
1580
+ const metrics = getMetrics();
1581
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...metrics }, null, 2) }] };
1582
+ }
1583
+ case 'veto_git_blame': {
1584
+ const blameDir = args?.project_dir ? String(args.project_dir).trim() : '';
1585
+ const blameFile = args?.file_path ? String(args.file_path).trim() : '';
1586
+ const blameTarget = blameFile || blameDir;
1587
+ if (!blameTarget) {
1588
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'Provide project_dir or file_path.' }) }], isError: true };
1589
+ }
1590
+ const resolvedTarget = resolve(blameTarget);
1591
+ try {
1592
+ statSync(resolvedTarget);
1593
+ }
1594
+ catch {
1595
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Path not found: ${resolvedTarget}` }) }], isError: true };
1596
+ }
1597
+ function gitExec(cmd, cwd) {
1598
+ try {
1599
+ return execSyncTop(cmd, { cwd, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
1600
+ }
1601
+ catch {
1602
+ return '';
1603
+ }
1604
+ }
1605
+ const cwd = statSync(resolvedTarget).isDirectory() ? resolvedTarget : dirname(resolvedTarget);
1606
+ const shortlog = gitExec(`git shortlog -sn -- "${resolvedTarget}"`, cwd);
1607
+ const contributors = shortlog.split('\n').filter(Boolean).map(line => {
1608
+ const m = line.match(/^\s*(\d+)\s+(.+)$/);
1609
+ return m ? { commits: parseInt(m[1], 10), author: m[2].trim() } : null;
1610
+ }).filter(Boolean);
1611
+ const lastModified = gitExec(`git log -1 --format="%ai|%aN|%s" -- "${resolvedTarget}"`, cwd);
1612
+ const [last_modified_at, last_author, last_commit_message] = lastModified.split('|');
1613
+ const totalCommits = gitExec(`git rev-list --count HEAD -- "${resolvedTarget}"`, cwd);
1614
+ return { content: [{ type: 'text', text: JSON.stringify({
1615
+ success: true,
1616
+ path: resolvedTarget,
1617
+ total_commits: parseInt(totalCommits || '0', 10),
1618
+ contributors,
1619
+ last_modified_at: last_modified_at?.trim(),
1620
+ last_author: last_author?.trim(),
1621
+ last_commit_message: last_commit_message?.trim(),
1622
+ }, null, 2) }] };
1623
+ }
1624
+ case 'veto_changelog': {
1625
+ const changelogDir = args?.project_dir ? String(args.project_dir).trim() : activeProjectDir ?? process.cwd();
1626
+ const maxEntries = typeof args?.max_entries === 'number' ? Math.min(args.max_entries, 200) : 50;
1627
+ const resolvedDir = resolve(changelogDir);
1628
+ try {
1629
+ statSync(resolvedDir);
1630
+ }
1631
+ catch {
1632
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Directory not found: ${resolvedDir}` }) }], isError: true };
1633
+ }
1634
+ function gitRun(cmd) {
1635
+ try {
1636
+ return execSyncTop(cmd, { cwd: resolvedDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
1637
+ }
1638
+ catch {
1639
+ return '';
1640
+ }
1641
+ }
1642
+ const lastTag = gitRun('git describe --tags --abbrev=0 2>/dev/null') || '';
1643
+ const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
1644
+ const rawLog = gitRun(`git log ${range} --format="%s|||%H|||%aN|||%ai" --no-merges -n ${maxEntries}`);
1645
+ if (!rawLog) {
1646
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, since_tag: lastTag || 'beginning', entries: [], message: 'No commits found in range.' }) }] };
1647
+ }
1648
+ const typeLabels = {
1649
+ feat: 'Features', fix: 'Bug Fixes', refactor: 'Refactoring', perf: 'Performance',
1650
+ docs: 'Documentation', test: 'Tests', chore: 'Chores', ci: 'CI/CD',
1651
+ style: 'Style', build: 'Build', revert: 'Reverts',
1652
+ };
1653
+ const grouped = {};
1654
+ for (const line of rawLog.split('\n').filter(Boolean)) {
1655
+ const [subject, hash, author, date] = line.split('|||');
1656
+ if (!subject)
1657
+ continue;
1658
+ const typeMatch = subject.match(/^(\w+)(\([\w-]+\))?:\s*(.*)/);
1659
+ const type = typeMatch ? typeMatch[1].toLowerCase() : 'other';
1660
+ const msg = typeMatch ? typeMatch[3] : subject;
1661
+ const label = typeLabels[type] ?? 'Other';
1662
+ if (!grouped[label])
1663
+ grouped[label] = [];
1664
+ grouped[label].push({ message: msg.trim(), hash: hash?.trim().slice(0, 8) ?? '', author: author?.trim() ?? '', date: date?.trim().slice(0, 10) ?? '' });
1665
+ }
1666
+ const sections = Object.entries(grouped).map(([section, items]) => ({ section, items }));
1667
+ return { content: [{ type: 'text', text: JSON.stringify({
1668
+ success: true,
1669
+ since_tag: lastTag || '(beginning of history)',
1670
+ total_commits: rawLog.split('\n').filter(Boolean).length,
1671
+ sections,
1672
+ }, null, 2) }] };
1673
+ }
2269
1674
  default:
2270
1675
  throw new Error(`Unknown tool: ${name}`);
2271
1676
  }