@jigyasudham/veto 1.2.18 → 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, } 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,731 +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 for later restoration across AI platforms.',
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
- },
189
- required: ['summary', 'context'],
190
- },
191
- },
192
- {
193
- name: 'veto_session_restore',
194
- description: 'Restores a previously saved session by ID. Use veto_sessions_list to find IDs.',
195
- inputSchema: {
196
- type: 'object',
197
- properties: {
198
- session_id: {
199
- type: 'string',
200
- description: 'UUID of the session to restore.',
201
- },
202
- resuming_as: {
203
- type: 'string',
204
- description: 'The AI client resuming this session (e.g. "claude", "gemini", "codex"). Recorded as active_client.',
205
- enum: ['claude', 'gemini', 'codex'],
206
- },
207
- },
208
- required: ['session_id'],
209
- },
210
- },
211
- {
212
- name: 'veto_sessions_list',
213
- description: 'Lists the most recent saved sessions (up to 10).',
214
- inputSchema: {
215
- type: 'object',
216
- properties: {
217
- limit: {
218
- type: 'number',
219
- description: 'Number of sessions to return (default 10, max 50).',
220
- },
221
- },
222
- required: [],
223
- },
224
- },
225
- {
226
- name: 'veto_route_task',
227
- 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.',
228
- inputSchema: {
229
- type: 'object',
230
- properties: {
231
- task: {
232
- type: 'string',
233
- description: 'The task description to score and route.',
234
- },
235
- agent_type: {
236
- type: 'string',
237
- description: 'Optional agent type — some agents are tier-locked regardless of score.',
238
- enum: [
239
- 'lead-developer', 'system-architect', 'security-scanner',
240
- 'devil-advocate', 'decision-engine', 'risk-assessor',
241
- 'coder', 'tester', 'reviewer', 'database', 'documentation',
242
- 'file-manager', 'git-agent', 'search-agent', 'secrets', 'reporter',
243
- 'dynamic',
244
- ],
245
- },
246
- files_affected: {
247
- type: 'number',
248
- description: 'Number of files the task will touch (influences complexity score).',
249
- },
250
- force_council: {
251
- type: 'boolean',
252
- description: 'Set true to force a Tier 3 / council-required routing.',
253
- },
254
- context: {
255
- type: 'string',
256
- description: 'Current context text — router will return a compression plan.',
257
- },
258
- preferred_platform: {
259
- type: 'string',
260
- description: 'Preferred AI platform. Router may override if rate-limited.',
261
- enum: ['claude', 'gemini', 'codex'],
262
- },
263
- },
264
- required: ['task'],
265
- },
266
- },
267
- {
268
- name: 'veto_rate_status',
269
- description: 'Returns current request counts and rate limit status for all AI platforms tracked by Veto.',
270
- inputSchema: {
271
- type: 'object',
272
- properties: {},
273
- required: [],
274
- },
275
- },
276
- {
277
- name: 'veto_council_debate',
278
- 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.',
279
- inputSchema: {
280
- type: 'object',
281
- properties: {
282
- task: {
283
- type: 'string',
284
- description: 'The task or decision to debate. Be specific — include approach, tech stack, and constraints.',
285
- },
286
- context: {
287
- type: 'string',
288
- description: 'Optional: additional context such as codebase state, prior decisions, or constraints.',
289
- },
290
- project_dir: {
291
- type: 'string',
292
- 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.',
293
- },
294
- session_id: {
295
- type: 'string',
296
- description: 'Optional: session ID to associate this council outcome with an active session.',
297
- },
298
- max_tokens: {
299
- type: 'number',
300
- 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.',
301
- },
302
- },
303
- required: ['task'],
304
- },
305
- },
306
- {
307
- name: 'veto_agent_plan',
308
- description: 'Gets a domain-expert execution plan from a specific worker agent. Returns approach, ordered steps, checklist, patterns, and pitfalls for the task.',
309
- inputSchema: {
310
- type: 'object',
311
- properties: {
312
- agent: {
313
- type: 'string',
314
- description: 'The worker agent to consult.',
315
- 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'],
316
- },
317
- task: { type: 'string', description: 'The task for the agent to plan.' },
318
- context: { type: 'string', description: 'Optional additional context.' },
319
- 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.' },
320
- },
321
- required: ['agent', 'task'],
322
- },
323
- },
324
- {
325
- name: 'veto_code_review',
326
- 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).',
327
- inputSchema: {
328
- type: 'object',
329
- properties: {
330
- code: { type: 'string', description: 'The code to review.' },
331
- context: { type: 'string', description: 'Optional: file name, module description, or review focus.' },
332
- file_path: { type: 'string', description: 'Optional: absolute path to the file being reviewed. When provided, findings are stored as VS Code inline diagnostics.' },
333
- },
334
- required: ['code'],
335
- },
336
- },
337
- {
338
- name: 'veto_diff_review',
339
- 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.',
340
- inputSchema: {
341
- type: 'object',
342
- properties: {
343
- diff: { type: 'string', description: 'The git diff to review. If omitted, Veto runs git diff HEAD in project_dir.' },
344
- project_dir: { type: 'string', description: 'Absolute project path. Used to auto-read git diff if diff is not provided, and to inject codebase context.' },
345
- context: { type: 'string', description: 'Optional: PR description, ticket number, or focus area.' },
346
- },
347
- required: [],
348
- },
349
- },
350
- {
351
- name: 'veto_security_scan',
352
- 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.',
353
- inputSchema: {
354
- type: 'object',
355
- properties: {
356
- code: { type: 'string', description: 'The code to scan.' },
357
- context: { type: 'string', description: 'Optional: language, framework, or specific concerns.' },
358
- file_path: { type: 'string', description: 'Optional: absolute path to the file being scanned. When provided, findings are stored as VS Code inline diagnostics.' },
359
- },
360
- required: ['code'],
361
- },
362
- },
363
- {
364
- name: 'veto_secrets_scan',
365
- 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.',
366
- inputSchema: {
367
- type: 'object',
368
- properties: {
369
- text: { type: 'string', description: 'The text or code to scan for secrets.' },
370
- file_path: { type: 'string', description: 'Optional: absolute path to the file being scanned. When provided, findings are stored as VS Code inline diagnostics.' },
371
- },
372
- required: ['text'],
373
- },
374
- },
375
- {
376
- name: 'veto_execute_parallel',
377
- 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.',
378
- inputSchema: {
379
- type: 'object',
380
- properties: {
381
- tasks: {
382
- type: 'array',
383
- description: 'List of agent tasks to run in parallel.',
384
- items: {
385
- type: 'object',
386
- properties: {
387
- id: { type: 'string', description: 'Unique ID for this task (use any string).' },
388
- agent: { type: 'string', description: 'Worker agent type.' },
389
- task: { type: 'string', description: 'Task description for this agent.' },
390
- code: { type: 'string', description: 'Optional code to analyze (triggers analyze() instead of plan()).' },
391
- context: { type: 'string', description: 'Optional additional context.' },
392
- project_dir: { type: 'string', description: 'Optional: per-task project dir override.' },
393
- },
394
- required: ['id', 'agent', 'task'],
395
- },
396
- },
397
- project_dir: { type: 'string', description: 'Optional: project directory applied to all tasks (per-task project_dir overrides this). Auto-injects codebase context.' },
398
- max_tokens: {
399
- type: 'number',
400
- 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.',
401
- },
402
- },
403
- required: ['tasks'],
404
- },
405
- },
406
- {
407
- name: 'veto_memory_store',
408
- 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.',
409
- inputSchema: {
410
- type: 'object',
411
- properties: {
412
- title: { type: 'string', description: 'Precise, searchable title. Bad: "Fixed bug". Good: "Fix: Node sqlite fails on Windows without --experimental-sqlite".' },
413
- content: { type: 'string', description: 'Self-contained content: problem → root cause → solution. Future agents must understand it without original context.' },
414
- type: {
415
- type: 'string',
416
- description: 'Entry type.',
417
- enum: ['solution', 'pattern', 'context', 'error', 'reference', 'decision'],
418
- },
419
- tags: { type: 'array', items: { type: 'string' }, description: 'Search tags (3–5 recommended). Examples: ["typescript", "auth", "jwt"].' },
420
- project_dir: { type: 'string', description: 'Absolute project path. Include for project-specific knowledge; omit for general programming knowledge.' },
421
- session_id: { type: 'string', description: 'Optional: associate this knowledge entry with an active session.' },
422
- relevance: { type: 'number', description: 'Initial relevance score 0.0–1.0 (default 1.0).' },
423
- },
424
- required: ['title', 'content'],
425
- },
426
- },
427
- {
428
- name: 'veto_memory_search',
429
- 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.',
430
- inputSchema: {
431
- type: 'object',
432
- properties: {
433
- query: { type: 'string', description: 'Search terms (full-text search on title and content).' },
434
- type: {
435
- type: 'string',
436
- description: 'Filter by entry type.',
437
- enum: ['solution', 'pattern', 'context', 'error', 'reference', 'decision'],
438
- },
439
- project_dir: { type: 'string', description: 'Filter to a specific project directory.' },
440
- limit: { type: 'number', description: 'Max results to return (default 10, max 50).' },
441
- },
442
- required: [],
443
- },
444
- },
445
- {
446
- name: 'veto_memory_delete',
447
- description: 'Deletes a knowledge entry by ID. Use to remove stale or duplicate entries found via veto_memory_search.',
448
- inputSchema: {
449
- type: 'object',
450
- properties: {
451
- id: { type: 'string', description: 'The knowledge entry ID (from veto_memory_search results).' },
452
- },
453
- required: ['id'],
454
- },
455
- },
456
- {
457
- name: 'veto_project_map_update',
458
- 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.',
459
- inputSchema: {
460
- type: 'object',
461
- properties: {
462
- project_dir: { type: 'string', description: 'Absolute path to the project root.' },
463
- structure: { type: 'string', description: 'JSON string representing the directory tree. Example: {"src/":{"agents/":["coder.ts","reviewer.ts"],"router/":["index.ts"]}}' },
464
- key_modules: {
465
- type: 'array',
466
- items: { type: 'string' },
467
- description: 'The 10–20 most important files with their roles. Example: ["src/server.ts (MCP entry point)", "src/router/index.ts (task router)"].',
468
- },
469
- tech_stack: {
470
- type: 'array',
471
- items: { type: 'string' },
472
- description: 'Frameworks and key libraries. Example: ["TypeScript", "Node.js 22", "Express", "SQLite"].',
473
- },
474
- },
475
- required: ['project_dir', 'structure'],
476
- },
477
- },
478
- {
479
- name: 'veto_project_map_get',
480
- description: 'Returns the stored project structure map for a directory. Use to navigate the codebase without scanning the filesystem.',
481
- inputSchema: {
482
- type: 'object',
483
- properties: {
484
- project_dir: { type: 'string', description: 'Absolute path to the project root.' },
485
- },
486
- required: ['project_dir'],
487
- },
488
- },
489
- {
490
- name: 'veto_pattern_store',
491
- description: 'Stores or updates a coding pattern observed in the codebase. Patterns are keyed by category.pattern-name and confidence increases with repeated observation.',
492
- inputSchema: {
493
- type: 'object',
494
- properties: {
495
- pattern_key: { type: 'string', description: 'Pattern identifier in category.pattern-name format. Example: "code.async-pattern" or "naming.variable-case".' },
496
- pattern_val: { type: 'string', description: 'The observed pattern value. Example: "async/await with try/catch, no raw Promise chains".' },
497
- confidence: { type: 'number', description: 'Confidence score 0.0–1.0 (default 1.0). Increases automatically on repeated observation.' },
498
- },
499
- required: ['pattern_key', 'pattern_val'],
500
- },
501
- },
502
- {
503
- name: 'veto_patterns_list',
504
- description: 'Returns stored coding patterns. Filter by prefix to get patterns in a specific category (e.g. prefix="naming." for all naming conventions).',
505
- inputSchema: {
506
- type: 'object',
507
- properties: {
508
- prefix: { type: 'string', description: 'Optional prefix filter. Example: "code." or "naming." or "testing.".' },
509
- limit: { type: 'number', description: 'Max patterns to return (default 20).' },
510
- },
511
- required: [],
512
- },
513
- },
514
- {
515
- name: 'veto_memory_export',
516
- 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.',
517
- inputSchema: {
518
- type: 'object',
519
- properties: {
520
- output_path: {
521
- type: 'string',
522
- 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.',
523
- },
524
- },
525
- required: [],
526
- },
527
- },
528
- {
529
- name: 'veto_memory_import',
530
- 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.',
531
- inputSchema: {
532
- type: 'object',
533
- properties: {
534
- input_path: {
535
- type: 'string',
536
- description: 'Path to the export JSON file. Defaults to ~/.veto/veto-export.json.',
537
- },
538
- },
539
- required: [],
540
- },
541
- },
542
- {
543
- name: 'veto_record_outcome',
544
- 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.',
545
- inputSchema: {
546
- type: 'object',
547
- properties: {
548
- 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.' },
549
- complexity: { type: 'number', description: 'The complexity score from veto_route_task (0–100).' },
550
- model_tier: { type: 'number', description: 'The tier that was actually used (1, 2, or 3).', enum: [1, 2, 3] },
551
- 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.' },
552
- agent: { type: 'string', description: 'The worker agent type used (optional but useful for agent performance tracking).' },
553
- tokens_used: { type: 'number', description: 'Approximate tokens used (optional).' },
554
- 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.' },
555
- },
556
- required: ['task_type', 'complexity', 'model_tier', 'output_quality'],
557
- },
558
- },
559
- {
560
- name: 'veto_learning_stats',
561
- 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.',
562
- inputSchema: {
563
- type: 'object',
564
- properties: {
565
- include_agent_stats: { type: 'boolean', description: 'Include per-agent quality breakdown (default true).' },
566
- include_task_types: { type: 'boolean', description: 'Include per-task-type breakdown (default false, verbose).' },
567
- include_council_insights: { type: 'boolean', description: 'Include council decision → debugging correlation (default false).' },
568
- },
569
- required: [],
570
- },
571
- },
572
- {
573
- name: 'veto_learning_apply',
574
- 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.',
575
- inputSchema: {
576
- type: 'object',
577
- properties: {},
578
- required: [],
579
- },
580
- },
581
- {
582
- name: 'veto_handoff',
583
- 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.',
584
- inputSchema: {
585
- type: 'object',
586
- properties: {
587
- summary: { type: 'string', description: 'What was accomplished this session — one or two sentences.' },
588
- context: { type: 'string', description: 'Key context the next platform needs: active decisions, file paths, constraints.' },
589
- task_state: { type: 'string', description: 'Current task state — what is done, what is in progress, what is next.' },
590
- from_platform: { type: 'string', enum: ['claude', 'gemini', 'codex'], description: 'Platform handing off (default: claude).' },
591
- to_platform: { type: 'string', enum: ['gemini', 'codex', 'claude'], description: 'Target platform. If omitted, Veto picks the platform with the most headroom.' },
592
- project_dir: { type: 'string', description: 'Absolute path to the current project directory.' },
593
- token_count: { type: 'number', description: 'Approximate tokens used this session.' },
594
- },
595
- required: ['summary', 'context'],
596
- },
597
- },
598
- {
599
- name: 'veto_continue',
600
- 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.',
601
- inputSchema: {
602
- type: 'object',
603
- properties: {
604
- session_id: { type: 'string', description: 'Optional. Session ID from veto_handoff. If omitted, the most recent saved session is restored.' },
605
- 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'] },
606
- },
607
- required: [],
608
- },
609
- },
610
- {
611
- name: 'veto_platform_setup',
612
- description: 'Returns the exact MCP config and setup steps to connect a specific AI platform to this Veto server.',
613
- inputSchema: {
614
- type: 'object',
615
- properties: {
616
- platform: { type: 'string', enum: ['claude', 'gemini', 'codex'], description: 'The platform to get setup instructions for.' },
617
- veto_server_path: { type: 'string', description: 'Absolute path to the built veto server (dist/server.js).' },
618
- },
619
- required: ['platform', 'veto_server_path'],
620
- },
621
- },
622
- {
623
- name: 'veto_watch',
624
- 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.',
625
- inputSchema: {
626
- type: 'object',
627
- properties: {
628
- project_dir: { type: 'string', description: 'Absolute path to the project directory to watch.' },
629
- },
630
- required: ['project_dir'],
631
- },
632
- },
633
- {
634
- name: 'veto_watch_poll',
635
- 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.',
636
- inputSchema: {
637
- type: 'object',
638
- properties: {
639
- watch_id: { type: 'string', description: 'The watch_id returned by veto_watch.' },
640
- },
641
- required: ['watch_id'],
642
- },
643
- },
644
- {
645
- name: 'veto_watch_stop',
646
- description: 'Stops an active file watcher.',
647
- inputSchema: {
648
- type: 'object',
649
- properties: {
650
- watch_id: { type: 'string', description: 'The watch_id returned by veto_watch.' },
651
- },
652
- required: ['watch_id'],
653
- },
654
- },
655
- {
656
- name: 'veto_workflow',
657
- 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).',
658
- inputSchema: {
659
- type: 'object',
660
- properties: {
661
- steps: {
662
- type: 'array',
663
- description: 'Ordered pipeline steps.',
664
- items: {
665
- type: 'object',
666
- properties: {
667
- id: { type: 'string', description: 'Step identifier.' },
668
- agent: { type: 'string', description: 'Worker agent type.' },
669
- task: { type: 'string', description: 'Task description for this step.' },
670
- code: { type: 'string', description: 'Optional code to analyze.' },
671
- context: { type: 'string', description: 'Optional context.' },
672
- gate: { type: 'number', description: 'Optional minimum confidence % (0–100) required to proceed to the next step.' },
673
- },
674
- required: ['id', 'agent', 'task'],
675
- },
676
- },
677
- project_dir: { type: 'string', description: 'Optional project directory — auto-injects codebase context into all steps.' },
678
- },
679
- required: ['steps'],
680
- },
681
- },
682
- {
683
- name: 'veto_explain',
684
- 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.',
685
- inputSchema: {
686
- type: 'object',
687
- properties: {
688
- file_path: { type: 'string', description: 'Absolute path to the file to explain.' },
689
- depth: { type: 'string', enum: ['overview', 'detailed', 'line-by-line'], description: 'Explanation depth. Default: overview.' },
690
- context: { type: 'string', description: 'Optional additional context about what you want explained.' },
691
- },
692
- required: ['file_path'],
693
- },
694
- },
695
- {
696
- name: 'veto_plugins',
697
- 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.',
698
- inputSchema: {
699
- type: 'object',
700
- properties: {},
701
- required: [],
702
- },
703
- },
704
- // ── Phase 13: Developer Intelligence ──────────────────────────────────────
705
- {
706
- name: 'veto_docs_fetch',
707
- 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.',
708
- inputSchema: {
709
- type: 'object',
710
- properties: {
711
- package_name: { type: 'string', description: 'Package name (e.g. "react", "requests", "serde").' },
712
- ecosystem: { type: 'string', enum: ['npm', 'pypi', 'crates'], description: 'Package ecosystem.' },
713
- version: { type: 'string', description: 'Specific version. Defaults to latest.' },
714
- max_chars: { type: 'number', description: 'Max characters to return (default 8000). Higher = more complete docs, more tokens.' },
715
- },
716
- required: ['package_name', 'ecosystem'],
717
- },
718
- },
719
- {
720
- name: 'veto_context_status',
721
- 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.',
722
- inputSchema: {
723
- type: 'object',
724
- properties: {
725
- session_id: { type: 'string', description: 'Session ID to check.' },
726
- },
727
- required: ['session_id'],
728
- },
729
- },
730
- {
731
- name: 'veto_task_parse',
732
- 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.',
733
- inputSchema: {
734
- type: 'object',
735
- properties: {
736
- description: { type: 'string', description: 'Project description, PRD, or feature brief to parse into tasks.' },
737
- project_dir: { type: 'string', description: 'Optional project directory for codebase context injection.' },
738
- max_tasks: { type: 'number', description: 'Maximum number of tasks to generate (default 20).' },
739
- },
740
- required: ['description'],
741
- },
742
- },
743
- // ── Phase 14: Observability & Safety ──────────────────────────────────────
744
- {
745
- name: 'veto_usage_status',
746
- 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.',
747
- inputSchema: {
748
- type: 'object',
749
- properties: {},
750
- required: [],
751
- },
752
- },
753
- {
754
- name: 'veto_audit_log',
755
- 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.',
756
- inputSchema: {
757
- type: 'object',
758
- properties: {
759
- session_id: { type: 'string', description: 'Filter to a specific session.' },
760
- verdict: { type: 'string', description: 'Filter by council verdict (GREEN, YELLOW, RED).' },
761
- since: { type: 'string', description: 'ISO date — only return events after this time.' },
762
- limit: { type: 'number', description: 'Max events to return (default 20, max 100).' },
763
- },
764
- required: [],
765
- },
766
- },
767
- {
768
- name: 'veto_health',
769
- description: 'Returns a live health snapshot of the Veto server — DB size, session/memory/pattern counts, uptime, error count, and average council latency.',
770
- inputSchema: {
771
- type: 'object',
772
- properties: {},
773
- required: [],
774
- },
775
- },
776
- // ── Phase 15: CI/CD & Distribution ────────────────────────────────────────
777
- {
778
- name: 'veto_ci_gate',
779
- 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.',
780
- inputSchema: {
781
- type: 'object',
782
- properties: {
783
- project_dir: { type: 'string', description: 'Absolute project path. Veto reads git diff HEAD automatically.' },
784
- diff: { type: 'string', description: 'Optional: pass a diff string directly instead of reading from project_dir.' },
785
- context: { type: 'string', description: 'Optional: PR description or ticket number for context.' },
786
- fail_on: { type: 'string', enum: ['warn', 'fail'], description: 'Whether WARN counts as a failure (exit code 1). Default: "fail" — only FAIL exits non-zero.' },
787
- },
788
- required: ['project_dir'],
789
- },
790
- },
791
- {
792
- name: 'veto_pr_review',
793
- 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.',
794
- inputSchema: {
795
- type: 'object',
796
- properties: {
797
- pr_url: { type: 'string', description: 'Full GitHub PR URL. e.g. https://github.com/owner/repo/pull/123' },
798
- context: { type: 'string', description: 'Optional: PR description or ticket number for extra context.' },
799
- fail_on: { type: 'string', enum: ['warn', 'fail'], description: 'Whether WARN counts as a failure. Default: "fail".' },
800
- },
801
- required: ['pr_url'],
802
- },
803
- },
804
- // ── Phase 16: Workspace Discovery & Summarization ─────────────────────────
805
- {
806
- name: 'veto_discover',
807
- 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.',
808
- inputSchema: {
809
- type: 'object',
810
- properties: {
811
- project_dir: { type: 'string', description: 'Absolute path to the project directory to scan.' },
812
- 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.' },
813
- store: { type: 'boolean', description: 'Whether to store the discovery in Veto memory as a project map. Default: true.' },
814
- },
815
- required: ['project_dir'],
816
- },
817
- },
818
- {
819
- name: 'veto_summarize',
820
- 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.',
821
- inputSchema: {
822
- type: 'object',
823
- properties: {
824
- project_dir: { type: 'string', description: 'Absolute path to a project directory to summarize.' },
825
- 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.' },
826
- focus: { type: 'string', description: 'Optional focus area: e.g. "security", "APIs", "data flow", "architecture". Narrows the summary.' },
827
- format: { type: 'string', enum: ['brief', 'detailed'], description: 'brief: 4–6 bullet points (default). detailed: paragraph-level prose.' },
828
- },
829
- required: [],
830
- },
831
- },
832
- {
833
- name: 'veto_benchmark',
834
- 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.',
835
- inputSchema: {
836
- type: 'object',
837
- properties: {
838
- task: { type: 'string', description: 'The decision context — what problem are both approaches solving?' },
839
- approach_a: { type: 'string', description: 'First approach to evaluate. Be specific about tech choices, trade-offs, and constraints.' },
840
- approach_b: { type: 'string', description: 'Second approach to evaluate. Same level of detail as approach_a.' },
841
- context: { type: 'string', description: 'Optional: shared context for both debates (architecture notes, constraints, team size, etc.).' },
842
- project_dir: { type: 'string', description: 'Optional: auto-inject package.json and git diff context.' },
843
- },
844
- required: ['task', 'approach_a', 'approach_b'],
845
- },
846
- },
847
- ];
124
+ const tools = TOOL_DEFINITIONS;
848
125
  return { tools: tools.map(t => ({ ...t, annotations: TOOL_ANNOTATIONS[t.name] ?? {} })) };
849
126
  });
850
127
  // ─── Shared Scan Utility ──────────────────────────────────────────────────────
@@ -914,10 +191,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
914
191
  case 'veto_status': {
915
192
  const statusTokenCount = typeof args?.token_count === 'number' ? args.token_count : null;
916
193
  const statusPlatform = args?.platform ? String(args.platform) : 'claude';
194
+ const statusModel = args?.model ? String(args.model) : undefined;
917
195
  if (statusTokenCount !== null && statusTokenCount > 0) {
918
196
  trackTokens(statusPlatform, statusTokenCount);
919
197
  }
920
- const autoSaveResult = statusTokenCount !== null ? maybeAutoSave(statusTokenCount, statusPlatform) : null;
198
+ const autoSaveResult = statusTokenCount !== null ? maybeAutoSave(statusTokenCount, statusPlatform, statusModel) : null;
921
199
  return {
922
200
  content: [
923
201
  {
@@ -977,7 +255,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
977
255
  const saveSummary = String(args?.summary ?? '');
978
256
  const saveContext = String(args?.context ?? '');
979
257
  const saveTaskState = args?.task_state ? String(args.task_state) : undefined;
980
- const result = saveSession({
258
+ const existingId = args?.session_id ? String(args.session_id) : undefined;
259
+ const sessionInput = {
981
260
  summary: saveSummary,
982
261
  context: saveContext,
983
262
  task_state: saveTaskState,
@@ -985,20 +264,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
985
264
  connection_type: args?.connection_type ? String(args.connection_type) : 'subscription',
986
265
  project_dir: sessionProjectDir,
987
266
  token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
988
- });
267
+ tags: Array.isArray(args?.tags) ? args.tags.map(String) : undefined,
268
+ };
269
+ let result;
270
+ let wasUpdate = false;
271
+ if (existingId) {
272
+ const updated = updateSession(existingId, sessionInput);
273
+ if (updated) {
274
+ result = { session_id: updated.session_id, saved_at: updated.saved_at, usage_pct: 0, context_warning: false, continuation_prompt: null };
275
+ wasUpdate = true;
276
+ }
277
+ else {
278
+ result = saveSession(sessionInput);
279
+ }
280
+ }
281
+ else {
282
+ result = saveSession(sessionInput);
283
+ }
989
284
  // Cache for auto-save: future veto_status calls with high token_count will re-save this context
990
- 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 };
991
288
  autoSave.last_save_at = result.saved_at;
992
289
  autoSave.last_session_id = result.session_id;
993
290
  const responseObj = {
994
291
  success: true,
995
- message: result.context_warning
996
- ? `⚠️ Context at ${result.usage_pct}% — consider handing off soon.`
997
- : 'Session saved. Use this ID to restore on any AI platform.',
292
+ message: wasUpdate
293
+ ? `Session updated in-place. ID unchanged: ${result.session_id}`
294
+ : result.context_warning
295
+ ? `⚠️ Context at ${result.usage_pct}% — consider handing off soon.`
296
+ : 'Session saved. Use this ID to restore on any AI platform.',
998
297
  session_id: result.session_id,
999
298
  saved_at: result.saved_at,
1000
- usage_pct: result.usage_pct,
1001
- context_warning: result.context_warning,
299
+ updated: wasUpdate,
300
+ ...(wasUpdate ? {} : { usage_pct: result.usage_pct, context_warning: result.context_warning }),
1002
301
  };
1003
302
  if (result.continuation_prompt)
1004
303
  responseObj.continuation_prompt = result.continuation_prompt;
@@ -1046,7 +345,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1046
345
  }
1047
346
  case 'veto_sessions_list': {
1048
347
  const limit = Math.min(typeof args?.limit === 'number' ? args.limit : 10, 50);
1049
- const sessions = listSessions(limit);
348
+ const query = args?.query ? String(args.query).trim() : undefined;
349
+ const sessions = listSessions(limit, query);
1050
350
  return {
1051
351
  content: [
1052
352
  {
@@ -1061,6 +361,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1061
361
  project_dir: s.project_dir,
1062
362
  summary: s.summary,
1063
363
  token_count: s.token_count,
364
+ tags: s.tags ? JSON.parse(s.tags) : [],
1064
365
  })),
1065
366
  }, null, 2),
1066
367
  },
@@ -1105,11 +406,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1105
406
  isError: true,
1106
407
  };
1107
408
  }
409
+ const strictnessArg = (['fast', 'standard', 'strict'].includes(String(args?.strictness ?? '')))
410
+ ? String(args.strictness)
411
+ : 'standard';
1108
412
  const debateStart = Date.now();
1109
- const result = runDebate({
413
+ const result = await runLlmDebate(server, {
1110
414
  task,
1111
415
  context: args?.context ? String(args.context) : undefined,
1112
416
  project_dir: args?.project_dir ? String(args.project_dir) : undefined,
417
+ strictness: strictnessArg,
1113
418
  });
1114
419
  const debateDuration = Date.now() - debateStart;
1115
420
  const sessionId = args?.session_id ? String(args.session_id) : undefined;
@@ -1134,22 +439,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1134
439
  recordOutcome(task.slice(0, 50), 50, tMap[result.final_verdict] ?? 2, 'council', qMap[result.final_verdict] ?? 50);
1135
440
  }
1136
441
  // Auto-store RED verdicts so they appear in the Memory panel immediately
1137
- 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';
1138
444
  const lines = [`Task: ${task}`];
1139
445
  if (result.block_reasons.length > 0)
1140
446
  lines.push(`\nBlocked by:\n${result.block_reasons.map(r => `- ${r}`).join('\n')}`);
1141
447
  if (result.warnings.length > 0)
1142
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}`);
1143
456
  if (result.recommended)
1144
457
  lines.push(`\nRecommended: ${result.recommended}`);
1145
458
  storeKnowledge({
1146
459
  type: 'decision',
1147
- title: `RED: ${task.slice(0, 80)}`,
460
+ title: `${result.final_verdict}: ${task.slice(0, 80)}`,
1148
461
  content: lines.join(''),
1149
- tags: ['red-verdict', 'blocked', 'council'],
462
+ tags: [isRed ? 'red-verdict' : 'yellow-verdict', isRed ? 'blocked' : 'caution', 'council'],
1150
463
  project_dir: args?.project_dir ? String(args.project_dir) : undefined,
1151
464
  session_id: sessionId,
1152
- relevance: 1.0,
465
+ relevance: isRed ? 1.0 : 0.8,
1153
466
  });
1154
467
  }
1155
468
  const responsePayload = {
@@ -1160,13 +473,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1160
473
  recommended: result.recommended,
1161
474
  debated_at: result.debated_at,
1162
475
  votes: {
1163
- lead_dev: result.votes.lead_dev.verdict,
1164
- pm: result.votes.pm.verdict,
1165
- architect: result.votes.architect.verdict,
1166
- ux: result.votes.ux.verdict,
1167
- devil: result.votes.devil.verdict,
1168
- legal: result.votes.legal.verdict,
1169
- 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,
1170
483
  },
1171
484
  };
1172
485
  const fullText = result.formatted_output + '\n\n' + JSON.stringify(responsePayload, null, 2);
@@ -1697,42 +1010,53 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1697
1010
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1698
1011
  }
1699
1012
  case 'veto_explain': {
1700
- const filePath = String(args?.file_path ?? '').trim();
1701
- if (!filePath)
1702
- return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'file_path is required.' }) }], isError: true };
1703
- let fileContent;
1704
- try {
1705
- 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 };
1706
1019
  }
1707
- catch {
1708
- 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';
1709
1029
  }
1710
- const ext = extname(filePath).toLowerCase();
1711
- const name_ = basename(filePath).toLowerCase();
1712
- const depth = String(args?.depth ?? 'overview');
1713
- // auto-detect best agent
1714
- let agent = 'coder';
1715
- if (['.tsx', '.jsx', '.vue', '.svelte'].includes(ext))
1716
- agent = 'frontend';
1717
- else if (['.sql', '.prisma'].includes(ext) || name_.includes('schema'))
1718
- agent = 'database';
1719
- else if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(name_))
1720
- agent = 'tester';
1721
- else if (['.yaml', '.yml', '.toml', '.dockerfile'].includes(ext) || name_ === 'dockerfile')
1722
- agent = 'devops';
1723
- else if (name_.includes('auth') || name_.includes('login') || name_.includes('jwt') || name_.includes('token'))
1724
- agent = 'auth';
1725
- else if (name_.includes('security') || name_.includes('crypt'))
1726
- agent = 'security-scanner';
1727
- 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();
1728
1039
  agent = 'coder';
1729
- const userContext = args?.context ? String(args.context) : undefined;
1730
- 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}` : ''}`;
1731
1055
  const result = await executeOne({ id: 'explain-1', agent, task, code: fileContent, project_dir: undefined });
1732
- autoRecord(`explain ${basename(filePath)}`, agent, Math.round(result.output.confidence * 100));
1056
+ autoRecord(`explain ${taskLabel}`, agent, Math.round(result.output.confidence * 100));
1733
1057
  return {
1734
1058
  content: [{ type: 'text', text: JSON.stringify({
1735
- file: filePath, agent_used: agent, depth,
1059
+ source: filePath || 'raw text', agent_used: agent, depth,
1736
1060
  explanation: result.plan ?? result.analysis,
1737
1061
  output: result.output,
1738
1062
  }, null, 2) }],
@@ -1774,6 +1098,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1774
1098
  if (!description) {
1775
1099
  return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'description is required.' }) }], isError: true };
1776
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
+ }
1777
1107
  const ctx = project_dir ? buildContextString(project_dir) : '';
1778
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 });
1779
1109
  // Build structured task DAG from planner output
@@ -1836,7 +1166,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1836
1166
  duration_estimate: planResult.plan?.duration_estimate ?? 'unknown',
1837
1167
  };
1838
1168
  autoRecord(description, 'task-planner', Math.round(planResult.output.confidence * 100));
1839
- const hash = createHash('sha256').update(description).digest('hex').slice(0, 16);
1840
1169
  const plan_id = saveTaskPlan(JSON.stringify(plan), hash, project_dir);
1841
1170
  return { content: [{ type: 'text', text: JSON.stringify({ success: true, plan_id, ...plan }, null, 2) }] };
1842
1171
  }
@@ -2174,9 +1503,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2174
1503
  throw new Error('veto_benchmark requires task, approach_a, and approach_b');
2175
1504
  }
2176
1505
  const bmStart = Date.now();
2177
- // Run both debates synchronously (council agents are all sync)
2178
- const debateA = runDebate({ task: `${task}\n\nApproach A: ${approachA}`, context: ctx, project_dir: projectDir });
2179
- 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
+ ]);
2180
1511
  // Score: GREEN=3, YELLOW=2, RED=1, DEADLOCK=0
2181
1512
  const verdictScore = { GREEN: 3, YELLOW: 2, RED: 1, DEADLOCK: 0 };
2182
1513
  const scoreA = verdictScore[debateA.final_verdict] ?? 0;
@@ -2244,6 +1575,102 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2244
1575
  duration_ms: Date.now() - bmStart,
2245
1576
  }, null, 2) }] };
2246
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
+ }
2247
1674
  default:
2248
1675
  throw new Error(`Unknown tool: ${name}`);
2249
1676
  }