@jigyasudham/veto 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +391 -658
- package/dist/agents/executor.d.ts.map +1 -1
- package/dist/agents/executor.js +3 -1
- package/dist/agents/executor.js.map +1 -1
- package/dist/agents/llm-runner.d.ts.map +1 -1
- package/dist/agents/llm-runner.js +62 -58
- package/dist/agents/llm-runner.js.map +1 -1
- package/dist/cli.js +84 -9
- package/dist/cli.js.map +1 -1
- package/dist/log.d.ts +9 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +33 -0
- package/dist/log.js.map +1 -0
- package/dist/memory/config.d.ts +2 -0
- package/dist/memory/config.d.ts.map +1 -1
- package/dist/memory/config.js +4 -2
- package/dist/memory/config.js.map +1 -1
- package/dist/memory/local.d.ts.map +1 -1
- package/dist/memory/local.js +3 -1
- package/dist/memory/local.js.map +1 -1
- package/dist/memory/schema.d.ts +1 -1
- package/dist/memory/schema.d.ts.map +1 -1
- package/dist/memory/schema.js +1 -0
- package/dist/memory/schema.js.map +1 -1
- package/dist/router/learning-updater.d.ts +4 -1
- package/dist/router/learning-updater.d.ts.map +1 -1
- package/dist/router/learning-updater.js +10 -0
- package/dist/router/learning-updater.js.map +1 -1
- package/dist/server/handlers/advisors.d.ts +3 -0
- package/dist/server/handlers/advisors.d.ts.map +1 -0
- package/dist/server/handlers/advisors.js +331 -0
- package/dist/server/handlers/advisors.js.map +1 -0
- package/dist/server/handlers/agents.d.ts +3 -0
- package/dist/server/handlers/agents.d.ts.map +1 -0
- package/dist/server/handlers/agents.js +202 -0
- package/dist/server/handlers/agents.js.map +1 -0
- package/dist/server/handlers/core.d.ts +3 -0
- package/dist/server/handlers/core.d.ts.map +1 -0
- package/dist/server/handlers/core.js +169 -0
- package/dist/server/handlers/core.js.map +1 -0
- package/dist/server/handlers/council.d.ts +3 -0
- package/dist/server/handlers/council.d.ts.map +1 -0
- package/dist/server/handlers/council.js +277 -0
- package/dist/server/handlers/council.js.map +1 -0
- package/dist/server/handlers/devtools.d.ts +3 -0
- package/dist/server/handlers/devtools.d.ts.map +1 -0
- package/dist/server/handlers/devtools.js +41 -0
- package/dist/server/handlers/devtools.js.map +1 -0
- package/dist/server/handlers/generators.d.ts +3 -0
- package/dist/server/handlers/generators.d.ts.map +1 -0
- package/dist/server/handlers/generators.js +541 -0
- package/dist/server/handlers/generators.js.map +1 -0
- package/dist/server/handlers/git.d.ts +3 -0
- package/dist/server/handlers/git.d.ts.map +1 -0
- package/dist/server/handlers/git.js +225 -0
- package/dist/server/handlers/git.js.map +1 -0
- package/dist/server/handlers/learning.d.ts +3 -0
- package/dist/server/handlers/learning.d.ts.map +1 -0
- package/dist/server/handlers/learning.js +60 -0
- package/dist/server/handlers/learning.js.map +1 -0
- package/dist/server/handlers/memory.d.ts +3 -0
- package/dist/server/handlers/memory.d.ts.map +1 -0
- package/dist/server/handlers/memory.js +181 -0
- package/dist/server/handlers/memory.js.map +1 -0
- package/dist/server/handlers/observability.d.ts +3 -0
- package/dist/server/handlers/observability.d.ts.map +1 -0
- package/dist/server/handlers/observability.js +132 -0
- package/dist/server/handlers/observability.js.map +1 -0
- package/dist/server/handlers/review.d.ts +3 -0
- package/dist/server/handlers/review.d.ts.map +1 -0
- package/dist/server/handlers/review.js +327 -0
- package/dist/server/handlers/review.js.map +1 -0
- package/dist/server/handlers/session.d.ts +3 -0
- package/dist/server/handlers/session.d.ts.map +1 -0
- package/dist/server/handlers/session.js +272 -0
- package/dist/server/handlers/session.js.map +1 -0
- package/dist/server/handlers/watch.d.ts +3 -0
- package/dist/server/handlers/watch.d.ts.map +1 -0
- package/dist/server/handlers/watch.js +29 -0
- package/dist/server/handlers/watch.js.map +1 -0
- package/dist/server/handlers/workers.d.ts +3 -0
- package/dist/server/handlers/workers.d.ts.map +1 -0
- package/dist/server/handlers/workers.js +27 -0
- package/dist/server/handlers/workers.js.map +1 -0
- package/dist/server/registry.d.ts +11 -0
- package/dist/server/registry.d.ts.map +1 -0
- package/dist/server/registry.js +9 -0
- package/dist/server/registry.js.map +1 -0
- package/dist/server/runtime.d.ts +49 -0
- package/dist/server/runtime.d.ts.map +1 -0
- package/dist/server/runtime.js +81 -0
- package/dist/server/runtime.js.map +1 -0
- package/dist/server/scan-core.d.ts +32 -0
- package/dist/server/scan-core.d.ts.map +1 -0
- package/dist/server/scan-core.js +82 -0
- package/dist/server/scan-core.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +64 -2944
- package/dist/server.js.map +1 -1
- package/dist/tools/definitions.d.ts +4 -4
- package/dist/tools/definitions.js +4 -4
- package/dist/tools/definitions.js.map +1 -1
- package/package.json +2 -2
- package/AGENTS.md +0 -134
package/dist/server.js
CHANGED
|
@@ -1,61 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Veto MCP Server —
|
|
2
|
+
// Veto MCP Server — 89 tools, 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
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema
|
|
8
|
-
import { buildContextString } from './context/reader.js';
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
8
|
import { TOOL_DEFINITIONS } from './tools/definitions.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { log, errMsg } from './log.js';
|
|
10
|
+
import { workerHandlers } from './server/handlers/workers.js';
|
|
11
|
+
import { memoryHandlers } from './server/handlers/memory.js';
|
|
12
|
+
import { observabilityHandlers } from './server/handlers/observability.js';
|
|
13
|
+
import { sessionHandlers } from './server/handlers/session.js';
|
|
14
|
+
import { learningHandlers } from './server/handlers/learning.js';
|
|
15
|
+
import { watchHandlers } from './server/handlers/watch.js';
|
|
16
|
+
import { devtoolsHandlers } from './server/handlers/devtools.js';
|
|
17
|
+
import { advisorHandlers } from './server/handlers/advisors.js';
|
|
18
|
+
import { generatorHandlers } from './server/handlers/generators.js';
|
|
19
|
+
import { gitHandlers } from './server/handlers/git.js';
|
|
20
|
+
import { reviewHandlers } from './server/handlers/review.js';
|
|
21
|
+
import { coreHandlers } from './server/handlers/core.js';
|
|
22
|
+
import { agentHandlers } from './server/handlers/agents.js';
|
|
23
|
+
import { councilHandlers } from './server/handlers/council.js';
|
|
24
|
+
import { VERSION, autoSave } from './server/runtime.js';
|
|
25
|
+
import { listSessions, searchKnowledge, getProjectMap, getPatterns, recordToolCall } from './memory/local.js';
|
|
12
26
|
import { buildRepoMap } from './repo-map/index.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { getConfig, setConfig } from './memory/config.js';
|
|
17
|
-
import { executeParallel, executeOne, initLlmRunner } from './agents/executor.js';
|
|
18
|
-
import { buildAgenticAgentPrompt, parseAgenticAgentResponses } from './agents/llm-runner.js';
|
|
19
|
-
import { handoff, continueSession, getPlatformSetup } from './adapters/index.js';
|
|
20
|
-
import { startWatch, pollWatch, stopWatch } from './watcher/index.js';
|
|
21
|
-
import { runPipeline } from './workflow/pipeline.js';
|
|
22
|
-
import { loadPlugins, listPlugins } from './plugins/loader.js';
|
|
23
|
-
import { fetchPrDiff } from './github/pr-fetcher.js';
|
|
24
|
-
import { discoverProject } from './discover.js';
|
|
25
|
-
import { readFileSync, statSync, existsSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs';
|
|
26
|
-
import { extname, join, dirname, resolve } from 'node:path';
|
|
27
|
-
import { fileURLToPath } from 'node:url';
|
|
28
|
-
import { execSync as execSyncTop } from 'node:child_process';
|
|
29
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
-
const { version: VERSION } = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
31
|
-
let activeProjectDir = null;
|
|
32
|
-
const SERVER_START_TIME = Date.now();
|
|
33
|
-
let serverErrorCount = 0;
|
|
34
|
-
let lastServerError = null;
|
|
35
|
-
const autoSave = {
|
|
36
|
-
threshold_pct: 70,
|
|
37
|
-
cooldown_ms: 5 * 60 * 1000,
|
|
38
|
-
last_save_at: null,
|
|
39
|
-
last_session_id: null,
|
|
40
|
-
cached: null,
|
|
41
|
-
};
|
|
42
|
-
function maybeAutoSave(token_count, platform, model) {
|
|
43
|
-
if (!autoSave.cached)
|
|
44
|
-
return { triggered: false };
|
|
45
|
-
const window_size = autoSave.cached.context_window ?? resolveContextWindow(platform, model);
|
|
46
|
-
const usage_pct = Math.round((token_count / window_size) * 100);
|
|
47
|
-
if (usage_pct < autoSave.threshold_pct)
|
|
48
|
-
return { triggered: false };
|
|
49
|
-
if (autoSave.last_save_at) {
|
|
50
|
-
const elapsed = Date.now() - new Date(autoSave.last_save_at).getTime();
|
|
51
|
-
if (elapsed < autoSave.cooldown_ms)
|
|
52
|
-
return { triggered: false };
|
|
53
|
-
}
|
|
54
|
-
const result = saveSession({ ...autoSave.cached, token_count, platform, save_type: 'auto' });
|
|
55
|
-
autoSave.last_save_at = result.saved_at;
|
|
56
|
-
autoSave.last_session_id = result.session_id;
|
|
57
|
-
return { triggered: true, session_id: result.session_id, usage_pct };
|
|
58
|
-
}
|
|
27
|
+
import { initLlmRunner } from './agents/executor.js';
|
|
28
|
+
import { loadPlugins } from './plugins/loader.js';
|
|
29
|
+
import { pathToFileURL } from 'node:url';
|
|
59
30
|
const server = new Server({ name: 'veto', version: VERSION }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
60
31
|
const TOOL_ANNOTATIONS = {
|
|
61
32
|
veto_status: { readOnlyHint: true },
|
|
@@ -147,2908 +118,51 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
147
118
|
const tools = TOOL_DEFINITIONS;
|
|
148
119
|
return { tools: tools.map(t => ({ ...t, annotations: TOOL_ANNOTATIONS[t.name] ?? {} })) };
|
|
149
120
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
agent: 'task-planner',
|
|
174
|
-
task: description,
|
|
175
|
-
project_dir,
|
|
176
|
-
});
|
|
177
|
-
return parsePrdIntoTasks(description, result.plan, max_tasks);
|
|
178
|
-
}
|
|
179
|
-
// ─── Shared Scan Utility ──────────────────────────────────────────────────────
|
|
180
|
-
async function runTripleScan(diff, context, llm_backed = true, agent_outputs) {
|
|
181
|
-
const tasks = [
|
|
182
|
-
{ id: 'scan-review', agent: 'reviewer', task: 'Review this git diff for code quality issues', code: diff, context, llm_backed },
|
|
183
|
-
{ id: 'scan-sec', agent: 'security-scanner', task: 'Scan this git diff for security vulnerabilities', code: diff, context, llm_backed },
|
|
184
|
-
{ id: 'scan-secrets', agent: 'secrets', task: 'Scan this git diff for exposed secrets or credentials', code: diff, llm_backed },
|
|
185
|
-
];
|
|
186
|
-
if (llm_backed && !agent_outputs) {
|
|
187
|
-
const results = await Promise.all(tasks.map(t => executeOne(t)));
|
|
188
|
-
const allLlm = results.every(r => r.llm_backed && !r.error);
|
|
189
|
-
if (allLlm)
|
|
190
|
-
return finalizeTripleScan(results[0], results[1], results[2]);
|
|
191
|
-
const prompts = tasks.map(t => buildAgenticAgentPrompt(t)).filter((p) => p !== null);
|
|
192
|
-
return {
|
|
193
|
-
mode: 'agentic_loop',
|
|
194
|
-
instruction: 'Reason as each agent below using their provided roles and schemas. Return a JSON object mapping task IDs to agent responses.',
|
|
195
|
-
prompts,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
const results = (llm_backed && agent_outputs) ? parseAgenticAgentResponses(tasks, agent_outputs) : await Promise.all(tasks.map(t => executeOne(t)));
|
|
199
|
-
return finalizeTripleScan(results[0], results[1], results[2]);
|
|
200
|
-
}
|
|
201
|
-
function finalizeTripleScan(reviewResult, secResult, secretsResult) {
|
|
202
|
-
const hasBlocking = (reviewResult.analysis?.critical_count ?? 0) > 0 || (secResult.analysis?.critical_count ?? 0) > 0 || (secretsResult.analysis?.critical_count ?? 0) > 0;
|
|
203
|
-
const hasWarnings = (reviewResult.analysis?.high_count ?? 0) > 0 || (secResult.analysis?.high_count ?? 0) > 0;
|
|
204
|
-
const verdict = hasBlocking ? 'fail' : hasWarnings ? 'warn' : 'pass';
|
|
205
|
-
recordOutcome('scan', 50, 2, 'reviewer', reviewResult.analysis?.score ?? Math.round(reviewResult.output.confidence * 100));
|
|
206
|
-
recordOutcome('scan', 50, 2, 'security-scanner', secResult.analysis?.score ?? Math.round(secResult.output.confidence * 100));
|
|
207
|
-
recordOutcome('scan', 50, 2, 'secrets', (secretsResult.analysis?.findings?.length ?? 0) === 0 ? 100 : secretsResult.analysis?.score ?? Math.round(secretsResult.output.confidence * 100));
|
|
208
|
-
return { reviewResult, secResult, secretsResult, verdict };
|
|
209
|
-
}
|
|
210
|
-
/** Helper to handle 2-phase agentic loop for worker agents */
|
|
211
|
-
async function handleAgenticWorker(name, args, agentType, defaultTask) {
|
|
212
|
-
const llmResponse = args?.agent_response;
|
|
213
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
214
|
-
const task = { id: name + '-1', agent: agentType, task: args?.task ? String(args.task) : defaultTask, code: args?.code ? String(args.code) : undefined, context: args?.context ? String(args.context) : undefined, project_dir: projectDir, llm_backed: true };
|
|
215
|
-
if (llmResponse && typeof llmResponse === 'object') {
|
|
216
|
-
const results = parseAgenticAgentResponses([task], { [task.id]: llmResponse });
|
|
217
|
-
const r = results[0];
|
|
218
|
-
return { content: [{ type: 'text', text: JSON.stringify(r.analysis || r.plan || r.output, null, 2) }] };
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
const result = await executeOne(task);
|
|
222
|
-
if (result.llm_backed && !result.error)
|
|
223
|
-
return { content: [{ type: 'text', text: JSON.stringify(result.analysis || result.plan || result.output, null, 2) }] };
|
|
224
|
-
}
|
|
225
|
-
catch { /* fallback */ }
|
|
226
|
-
const prompt = buildAgenticAgentPrompt(task);
|
|
227
|
-
return { content: [{ type: 'text', text: JSON.stringify({ llm_backed: false, llm_upgrade: { available: true, instruction: `Reason as the ${agentType} specialist and return the JSON response in the agent_response field.`, prompt } }, null, 2) }] };
|
|
228
|
-
}
|
|
229
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
121
|
+
// ─── Tool handler registry ────────────────────────────────────────────────────
|
|
122
|
+
// Migrated, per-domain handlers live in src/server/handlers/*. Anything not yet
|
|
123
|
+
// in the registry falls through to the switch below. Both paths share the
|
|
124
|
+
// dispatch wrapper (trace logging, error handling) untouched.
|
|
125
|
+
const TOOL_REGISTRY = {
|
|
126
|
+
...workerHandlers,
|
|
127
|
+
...memoryHandlers,
|
|
128
|
+
...observabilityHandlers,
|
|
129
|
+
...sessionHandlers,
|
|
130
|
+
...learningHandlers,
|
|
131
|
+
...watchHandlers,
|
|
132
|
+
...devtoolsHandlers,
|
|
133
|
+
...advisorHandlers,
|
|
134
|
+
...generatorHandlers,
|
|
135
|
+
...gitHandlers,
|
|
136
|
+
...reviewHandlers,
|
|
137
|
+
...coreHandlers,
|
|
138
|
+
...agentHandlers,
|
|
139
|
+
...councilHandlers,
|
|
140
|
+
};
|
|
141
|
+
// Exported so the dispatch can be unit-tested without connecting stdio. Registered
|
|
142
|
+
// on the server below; tests call callTool() directly with a synthetic request.
|
|
143
|
+
export async function callTool(request) {
|
|
230
144
|
const { name, arguments: args } = request.params;
|
|
231
145
|
const callStart = Date.now();
|
|
232
146
|
let resultStatus = 'success';
|
|
233
147
|
let errorMessage;
|
|
234
148
|
try {
|
|
235
149
|
const response = await (async () => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const statusPlatform = args?.platform ? String(args.platform) : 'claude';
|
|
241
|
-
const statusModel = args?.model ? String(args.model) : undefined;
|
|
242
|
-
if (statusTokenCount !== null && statusTokenCount > 0) {
|
|
243
|
-
trackTokens(statusPlatform, statusTokenCount);
|
|
244
|
-
upsertContextUsage({
|
|
245
|
-
platform: statusPlatform,
|
|
246
|
-
model: statusModel,
|
|
247
|
-
token_count: statusTokenCount,
|
|
248
|
-
context_window: resolveContextWindow(statusPlatform, statusModel),
|
|
249
|
-
session_id: autoSave.last_session_id ?? undefined,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
const autoSaveResult = statusTokenCount !== null ? maybeAutoSave(statusTokenCount, statusPlatform, statusModel) : null;
|
|
253
|
-
return {
|
|
254
|
-
content: [
|
|
255
|
-
{
|
|
256
|
-
type: 'text',
|
|
257
|
-
text: JSON.stringify({
|
|
258
|
-
status: 'running',
|
|
259
|
-
version: VERSION,
|
|
260
|
-
server: 'veto',
|
|
261
|
-
phase: 17,
|
|
262
|
-
capabilities: [
|
|
263
|
-
'session_save', 'session_restore', 'sessions_list',
|
|
264
|
-
'router', 'rate_monitor',
|
|
265
|
-
'council_debate',
|
|
266
|
-
'agent_plan', 'parallel_exec',
|
|
267
|
-
'code_review', 'diff_review', 'security_scan', 'secrets_scan', 'ci_gate', 'pr_review',
|
|
268
|
-
'workflow', 'watch',
|
|
269
|
-
'explain',
|
|
270
|
-
'memory_store', 'memory_search', 'memory_delete', 'memory_export', 'memory_import',
|
|
271
|
-
'project_map', 'pattern_store',
|
|
272
|
-
'learning_stats', 'learning_apply', 'record_outcome',
|
|
273
|
-
'handoff', 'continue', 'platform_setup',
|
|
274
|
-
'plugins',
|
|
275
|
-
'docs_fetch', 'context_status', 'task_parse',
|
|
276
|
-
'usage_status', 'audit_log', 'health',
|
|
277
|
-
'auto_save', 'discover', 'summarize',
|
|
278
|
-
],
|
|
279
|
-
db_path: getDbPath(),
|
|
280
|
-
uptime_ms: process.uptime() * 1000,
|
|
281
|
-
timestamp: new Date().toISOString(),
|
|
282
|
-
...(autoSaveResult?.triggered ? { auto_save: { triggered: true, session_id: autoSaveResult.session_id, usage_pct: autoSaveResult.usage_pct } } : {}),
|
|
283
|
-
}, null, 2),
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
case 'veto_autosave_status': {
|
|
289
|
-
const args = (request.params.arguments || {});
|
|
290
|
-
const liveUsage = getContextUsage();
|
|
291
|
-
return {
|
|
292
|
-
content: [{
|
|
293
|
-
type: 'text',
|
|
294
|
-
text: JSON.stringify({
|
|
295
|
-
threshold_pct: autoSave.threshold_pct,
|
|
296
|
-
cooldown_ms: autoSave.cooldown_ms,
|
|
297
|
-
context_cached: autoSave.cached !== null,
|
|
298
|
-
cached_summary: autoSave.cached?.summary ?? null,
|
|
299
|
-
last_save_at: autoSave.last_save_at,
|
|
300
|
-
last_session_id: autoSave.last_session_id,
|
|
301
|
-
live_context_usage: liveUsage,
|
|
302
|
-
note: 'Pass token_count to veto_session_save or veto_status to update live_context_usage. VS Code extension reads context_usage table directly.',
|
|
303
|
-
}, null, 2),
|
|
304
|
-
}],
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
case 'veto_session_save': {
|
|
308
|
-
const args = (request.params.arguments || {});
|
|
309
|
-
const sessionProjectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
310
|
-
if (sessionProjectDir)
|
|
311
|
-
activeProjectDir = sessionProjectDir;
|
|
312
|
-
const savePlatform = args?.platform ? String(args.platform) : 'claude';
|
|
313
|
-
const shouldAutoSummarize = args?.auto_summarize === true;
|
|
314
|
-
// Auto-summarize: try MCP Sampling first, fall back to agentic prompt for host AI
|
|
315
|
-
let autoSummaryResult = null;
|
|
316
|
-
if (shouldAutoSummarize) {
|
|
317
|
-
autoSummaryResult = await autoSummarizeSession(server, {
|
|
318
|
-
summary: args?.summary ? String(args.summary) : undefined,
|
|
319
|
-
context: args?.context ? String(args.context) : undefined,
|
|
320
|
-
task_state: args?.task_state ? String(args.task_state) : undefined,
|
|
321
|
-
});
|
|
322
|
-
// If agentic prompt returned, short-circuit — return it so the AI can fill it in
|
|
323
|
-
if (autoSummaryResult && 'mode' in autoSummaryResult && autoSummaryResult.mode === 'agentic') {
|
|
324
|
-
return {
|
|
325
|
-
content: [{
|
|
326
|
-
type: 'text',
|
|
327
|
-
text: JSON.stringify({
|
|
328
|
-
success: false,
|
|
329
|
-
auto_summarized: false,
|
|
330
|
-
...autoSummaryResult,
|
|
331
|
-
}, null, 2),
|
|
332
|
-
}],
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// Enforce size limits — unbounded strings exhaust SQLite page cache and memory
|
|
337
|
-
const SUMMARY_LIMIT = 2_000;
|
|
338
|
-
const CONTEXT_LIMIT = 50_000;
|
|
339
|
-
const TASK_STATE_LIMIT = 20_000;
|
|
340
|
-
const generatedSession = autoSummaryResult && 'auto_summarized' in autoSummaryResult ? autoSummaryResult : null;
|
|
341
|
-
const RAW_SUMMARY = generatedSession?.summary ?? String(args?.summary ?? '');
|
|
342
|
-
const RAW_CONTEXT = generatedSession?.context ?? String(args?.context ?? '');
|
|
343
|
-
const RAW_TASK_STATE = generatedSession?.task_state ?? (args?.task_state ? String(args.task_state) : undefined);
|
|
344
|
-
const saveSummary = RAW_SUMMARY.slice(0, SUMMARY_LIMIT);
|
|
345
|
-
const saveContext = RAW_CONTEXT.slice(0, CONTEXT_LIMIT);
|
|
346
|
-
const saveTaskState = RAW_TASK_STATE ? RAW_TASK_STATE.slice(0, TASK_STATE_LIMIT) : undefined;
|
|
347
|
-
const truncationWarnings = [];
|
|
348
|
-
if (RAW_SUMMARY.length > SUMMARY_LIMIT)
|
|
349
|
-
truncationWarnings.push(`summary truncated to ${SUMMARY_LIMIT} chars (was ${RAW_SUMMARY.length})`);
|
|
350
|
-
if (RAW_CONTEXT.length > CONTEXT_LIMIT)
|
|
351
|
-
truncationWarnings.push(`context truncated to ${CONTEXT_LIMIT} chars (was ${RAW_CONTEXT.length})`);
|
|
352
|
-
if (RAW_TASK_STATE && RAW_TASK_STATE.length > TASK_STATE_LIMIT)
|
|
353
|
-
truncationWarnings.push(`task_state truncated to ${TASK_STATE_LIMIT} chars (was ${RAW_TASK_STATE.length})`);
|
|
354
|
-
const existingId = args?.session_id ? String(args.session_id) : undefined;
|
|
355
|
-
const saveModel = args?.model ? String(args.model) : undefined;
|
|
356
|
-
const sessionInput = {
|
|
357
|
-
summary: saveSummary,
|
|
358
|
-
context: saveContext,
|
|
359
|
-
task_state: saveTaskState,
|
|
360
|
-
platform: savePlatform,
|
|
361
|
-
model: saveModel,
|
|
362
|
-
connection_type: args?.connection_type ? String(args.connection_type) : 'subscription',
|
|
363
|
-
project_dir: sessionProjectDir,
|
|
364
|
-
token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
|
|
365
|
-
tags: Array.isArray(args?.tags) ? args.tags.map(String) : undefined,
|
|
366
|
-
};
|
|
367
|
-
let result;
|
|
368
|
-
let wasUpdate = false;
|
|
369
|
-
if (existingId) {
|
|
370
|
-
const updated = updateSession(existingId, sessionInput);
|
|
371
|
-
if (updated) {
|
|
372
|
-
result = { session_id: updated.session_id, saved_at: updated.saved_at, usage_pct: 0, context_warning: false, continuation_prompt: null };
|
|
373
|
-
wasUpdate = true;
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
result = saveSession(sessionInput);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
result = saveSession(sessionInput);
|
|
381
|
-
}
|
|
382
|
-
// Cache for auto-save: future veto_status calls with high token_count will re-save this context
|
|
383
|
-
const resolvedWindow = resolveContextWindow(savePlatform, saveModel);
|
|
384
|
-
autoSave.cached = { summary: saveSummary, context: saveContext, task_state: saveTaskState, platform: savePlatform, project_dir: sessionProjectDir, context_window: resolvedWindow };
|
|
385
|
-
autoSave.last_save_at = result.saved_at;
|
|
386
|
-
autoSave.last_session_id = result.session_id;
|
|
387
|
-
// Update live token count so VS Code extension and veto_status reflect it immediately
|
|
388
|
-
const saveTokenCount = typeof args?.token_count === 'number' ? args.token_count : 0;
|
|
389
|
-
if (saveTokenCount > 0) {
|
|
390
|
-
trackTokens(savePlatform, saveTokenCount);
|
|
391
|
-
upsertContextUsage({
|
|
392
|
-
platform: savePlatform,
|
|
393
|
-
model: saveModel,
|
|
394
|
-
token_count: saveTokenCount,
|
|
395
|
-
context_window: resolvedWindow,
|
|
396
|
-
session_id: result.session_id,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
const autoSumFailed = shouldAutoSummarize && !autoSummaryResult;
|
|
400
|
-
const responseObj = {
|
|
401
|
-
success: true,
|
|
402
|
-
message: wasUpdate
|
|
403
|
-
? `Session updated in-place. ID unchanged: ${result.session_id}`
|
|
404
|
-
: result.context_warning
|
|
405
|
-
? `⚠️ Context at ${result.usage_pct}% — consider handing off soon.`
|
|
406
|
-
: 'Session saved. Use this ID to restore on any AI platform.',
|
|
407
|
-
session_id: result.session_id,
|
|
408
|
-
saved_at: result.saved_at,
|
|
409
|
-
updated: wasUpdate,
|
|
410
|
-
auto_summarized: autoSummaryResult ? true : false,
|
|
411
|
-
...(autoSumFailed ? { auto_summarize_warning: 'MCP Sampling unavailable — saved provided values instead. For best results use Claude Code or another host that supports sampling.' } : {}),
|
|
412
|
-
...(wasUpdate ? {} : { usage_pct: result.usage_pct, context_warning: result.context_warning }),
|
|
413
|
-
...(truncationWarnings.length > 0 ? { truncation_warnings: truncationWarnings } : {}),
|
|
414
|
-
};
|
|
415
|
-
if (result.continuation_prompt)
|
|
416
|
-
responseObj.continuation_prompt = result.continuation_prompt;
|
|
417
|
-
return { content: [{ type: 'text', text: JSON.stringify(responseObj, null, 2) }] };
|
|
418
|
-
}
|
|
419
|
-
case 'veto_session_restore': {
|
|
420
|
-
const args = (request.params.arguments || {});
|
|
421
|
-
const session_id = String(args?.session_id ?? '');
|
|
422
|
-
const resuming_as = args?.resuming_as ? String(args.resuming_as) : undefined;
|
|
423
|
-
const result = restoreSession(session_id, resuming_as);
|
|
424
|
-
if (!result.found) {
|
|
425
|
-
return {
|
|
426
|
-
content: [
|
|
427
|
-
{
|
|
428
|
-
type: 'text',
|
|
429
|
-
text: JSON.stringify({ success: false, message: `No session found with id: ${session_id}` }, null, 2),
|
|
430
|
-
},
|
|
431
|
-
],
|
|
432
|
-
isError: true,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
const s = result.session;
|
|
436
|
-
if (s.project_dir)
|
|
437
|
-
activeProjectDir = s.project_dir;
|
|
438
|
-
const parsedTaskState = s.task_state ? (() => { try {
|
|
439
|
-
return JSON.parse(s.task_state);
|
|
440
|
-
}
|
|
441
|
-
catch {
|
|
442
|
-
return s.task_state;
|
|
443
|
-
} })() : null;
|
|
444
|
-
const nextAction = (typeof parsedTaskState === 'object' && parsedTaskState !== null)
|
|
445
|
-
? (parsedTaskState.nextAction ?? parsedTaskState.next_action ?? null)
|
|
446
|
-
: null;
|
|
447
|
-
const resumeInstructions = [
|
|
448
|
-
'Context restored from previous session. Trust the summary, context, and task_state above — they were written by the AI that last worked on this.',
|
|
449
|
-
'Do NOT re-read source files to orient yourself. That defeats the purpose of session restore and wastes tokens.',
|
|
450
|
-
'Only open a file if you are about to EDIT it — not to "verify" or "familiarize yourself" with it.',
|
|
451
|
-
nextAction ? `Start immediately with: ${nextAction}` : 'Read task_state.nextAction (or context) for where to start.',
|
|
452
|
-
'If context seems stale (e.g. you find a file has changed), read only that file, update the context, and continue.',
|
|
453
|
-
].join(' ');
|
|
454
|
-
return {
|
|
455
|
-
content: [
|
|
456
|
-
{
|
|
457
|
-
type: 'text',
|
|
458
|
-
text: JSON.stringify({
|
|
459
|
-
success: true,
|
|
460
|
-
resume_instructions: resumeInstructions,
|
|
461
|
-
session_id: s.id,
|
|
462
|
-
created_by: s.platform,
|
|
463
|
-
saved_at: s.started_at,
|
|
464
|
-
project_dir: s.project_dir,
|
|
465
|
-
summary: s.summary,
|
|
466
|
-
context: s.context ? (() => { try {
|
|
467
|
-
return JSON.parse(s.context);
|
|
468
|
-
}
|
|
469
|
-
catch {
|
|
470
|
-
return s.context;
|
|
471
|
-
} })() : null,
|
|
472
|
-
task_state: parsedTaskState,
|
|
473
|
-
token_count: s.token_count,
|
|
474
|
-
}, null, 2),
|
|
475
|
-
},
|
|
476
|
-
],
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
case 'veto_sessions_list': {
|
|
480
|
-
const args = (request.params.arguments || {});
|
|
481
|
-
const limit = Math.min(typeof args?.limit === 'number' ? args.limit : 10, 50);
|
|
482
|
-
const query = args?.query ? String(args.query).trim() : undefined;
|
|
483
|
-
const sessions = listSessions(limit, query);
|
|
484
|
-
return {
|
|
485
|
-
content: [
|
|
486
|
-
{
|
|
487
|
-
type: 'text',
|
|
488
|
-
text: JSON.stringify({
|
|
489
|
-
count: sessions.length,
|
|
490
|
-
sessions: sessions.map((s) => ({
|
|
491
|
-
id: s.id,
|
|
492
|
-
platform: s.platform,
|
|
493
|
-
started_at: s.started_at,
|
|
494
|
-
ended_at: s.ended_at,
|
|
495
|
-
project_dir: s.project_dir,
|
|
496
|
-
summary: s.summary,
|
|
497
|
-
token_count: s.token_count,
|
|
498
|
-
tags: s.tags ? JSON.parse(s.tags) : [],
|
|
499
|
-
})),
|
|
500
|
-
}, null, 2),
|
|
501
|
-
},
|
|
502
|
-
],
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
case 'veto_route_task': {
|
|
506
|
-
const args = (request.params.arguments || {});
|
|
507
|
-
const routeTaskStr = String(args?.task ?? '');
|
|
508
|
-
const fileExt = args?.file_ext ? String(args.file_ext) : undefined;
|
|
509
|
-
const result = routeTask(routeTaskStr, {
|
|
510
|
-
agentType: args?.agent_type ? String(args.agent_type) : undefined,
|
|
511
|
-
filesAffected: typeof args?.files_affected === 'number' ? args.files_affected : undefined,
|
|
512
|
-
forceCouncil: args?.force_council === true,
|
|
513
|
-
context: args?.context ? String(args.context) : undefined,
|
|
514
|
-
preferredPlatform: args?.preferred_platform ? String(args.preferred_platform) : 'claude',
|
|
515
|
-
architectModel: args?.architect_model ? String(args.architect_model) : undefined,
|
|
516
|
-
editorModel: args?.editor_model ? String(args.editor_model) : undefined,
|
|
517
|
-
});
|
|
518
|
-
const recommended_agent = getRecommendedAgent(routeTaskStr, fileExt);
|
|
519
|
-
// #41: auto-record every routing so tier distribution stats are always populated
|
|
520
|
-
recordOutcome(routeTaskStr.slice(0, 50), result.complexity.score, result.model.tier, 'router', 70);
|
|
521
|
-
return {
|
|
522
|
-
content: [{
|
|
523
|
-
type: 'text',
|
|
524
|
-
text: JSON.stringify({ ...result, recommended_agent }, null, 2),
|
|
525
|
-
}],
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
case 'veto_rate_status': {
|
|
529
|
-
const args = (request.params.arguments || {});
|
|
530
|
-
return {
|
|
531
|
-
content: [
|
|
532
|
-
{
|
|
533
|
-
type: 'text',
|
|
534
|
-
text: JSON.stringify(getRateStatus(), null, 2),
|
|
535
|
-
},
|
|
536
|
-
],
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
case 'veto_council_debate': {
|
|
540
|
-
const args = (request.params.arguments || {});
|
|
541
|
-
const task = String(args?.task ?? '').trim();
|
|
542
|
-
if (!task) {
|
|
543
|
-
return {
|
|
544
|
-
content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'task is required.' }) }],
|
|
545
|
-
isError: true,
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
const strictnessArg = (['fast', 'standard', 'strict'].includes(String(args?.strictness ?? '')))
|
|
549
|
-
? String(args.strictness)
|
|
550
|
-
: 'standard';
|
|
551
|
-
const debateInput = {
|
|
552
|
-
task,
|
|
553
|
-
context: args?.context ? String(args.context) : undefined,
|
|
554
|
-
project_dir: args?.project_dir ? String(args.project_dir) : undefined,
|
|
555
|
-
strictness: strictnessArg,
|
|
556
|
-
architect_model: args?.architect_model ? String(args.architect_model) : undefined,
|
|
557
|
-
editor_model: args?.editor_model ? String(args.editor_model) : undefined,
|
|
558
|
-
};
|
|
559
|
-
// Phase 2: agent_responses provided — run verdict engine on LLM-generated votes
|
|
560
|
-
const rawAgentResponses = args?.agent_responses;
|
|
561
|
-
if (rawAgentResponses && typeof rawAgentResponses === 'object') {
|
|
562
|
-
const parsed = parseAgentResponses(JSON.stringify(rawAgentResponses), task);
|
|
563
|
-
if (parsed) {
|
|
564
|
-
const debateStart = Date.now();
|
|
565
|
-
const result = runFromAgentResponses(debateInput, parsed);
|
|
566
|
-
const debateDuration = Date.now() - debateStart;
|
|
567
|
-
const sessionId = args?.session_id ? String(args.session_id) : undefined;
|
|
568
|
-
const outcomeId = saveCouncilOutcome({
|
|
569
|
-
session_id: sessionId, task, verdict: result.final_verdict,
|
|
570
|
-
lead_dev: JSON.stringify(result.votes.lead_dev), pm: JSON.stringify(result.votes.pm),
|
|
571
|
-
architect: JSON.stringify(result.votes.architect), ux: JSON.stringify(result.votes.ux),
|
|
572
|
-
devil: JSON.stringify(result.votes.devil), legal: JSON.stringify(result.votes.legal),
|
|
573
|
-
security: JSON.stringify(result.votes.security), recommended: result.recommended,
|
|
574
|
-
duration_ms: debateDuration,
|
|
575
|
-
});
|
|
576
|
-
const payload = { outcome_id: outcomeId, llm_backed: true, final_verdict: result.final_verdict, block_reasons: result.block_reasons, warnings: result.warnings, recommended: result.recommended, debated_at: result.debated_at, votes: result.votes };
|
|
577
|
-
return { content: [{ type: 'text', text: result.formatted_output + '\n\n' + JSON.stringify(payload, null, 2) }] };
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
// Phase 1: run deterministic debate + attach llm_upgrade prompt for host AI
|
|
581
|
-
const debateStart = Date.now();
|
|
582
|
-
const result = await runLlmDebate(server, debateInput);
|
|
583
|
-
const debateDuration = Date.now() - debateStart;
|
|
584
|
-
const sessionId = args?.session_id ? String(args.session_id) : undefined;
|
|
585
|
-
const outcomeId = saveCouncilOutcome({
|
|
586
|
-
session_id: sessionId,
|
|
587
|
-
task,
|
|
588
|
-
verdict: result.final_verdict,
|
|
589
|
-
lead_dev: JSON.stringify(result.votes.lead_dev),
|
|
590
|
-
pm: JSON.stringify(result.votes.pm),
|
|
591
|
-
architect: JSON.stringify(result.votes.architect),
|
|
592
|
-
ux: JSON.stringify(result.votes.ux),
|
|
593
|
-
devil: JSON.stringify(result.votes.devil),
|
|
594
|
-
legal: JSON.stringify(result.votes.legal),
|
|
595
|
-
security: JSON.stringify(result.votes.security),
|
|
596
|
-
recommended: result.recommended,
|
|
597
|
-
duration_ms: debateDuration,
|
|
598
|
-
});
|
|
599
|
-
// #38: auto-record learning outcome from verdict — no manual veto_record_outcome needed
|
|
600
|
-
{
|
|
601
|
-
const qMap = { GREEN: 90, YELLOW: 60, RED: 20, DEADLOCK: 50 };
|
|
602
|
-
const tMap = { GREEN: 1, YELLOW: 2, RED: 3, DEADLOCK: 2 };
|
|
603
|
-
recordOutcome(task.slice(0, 50), 50, tMap[result.final_verdict] ?? 2, 'council', qMap[result.final_verdict] ?? 50);
|
|
604
|
-
}
|
|
605
|
-
// Auto-store RED verdicts so they appear in the Memory panel immediately
|
|
606
|
-
if (result.final_verdict === 'RED' || (result.final_verdict === 'YELLOW' && (result.warnings.length >= 2 || result.block_reasons.length > 0))) {
|
|
607
|
-
const isRed = result.final_verdict === 'RED';
|
|
608
|
-
const lines = [`Task: ${task}`];
|
|
609
|
-
if (result.block_reasons.length > 0)
|
|
610
|
-
lines.push(`\nBlocked by:\n${result.block_reasons.map(r => `- ${r}`).join('\n')}`);
|
|
611
|
-
if (result.warnings.length > 0)
|
|
612
|
-
lines.push(`\nWarnings:\n${result.warnings.map(w => `- ${w}`).join('\n')}`);
|
|
613
|
-
// Include per-agent reasoning so future debates inherit the full context
|
|
614
|
-
const agentSummary = Object.entries(result.votes)
|
|
615
|
-
.filter(([, v]) => v.verdict !== 'approve')
|
|
616
|
-
.map(([name, v]) => `- ${name} [${v.verdict}]: ${v.reason}`)
|
|
617
|
-
.join('\n');
|
|
618
|
-
if (agentSummary)
|
|
619
|
-
lines.push(`\nAgent reasoning:\n${agentSummary}`);
|
|
620
|
-
if (result.recommended)
|
|
621
|
-
lines.push(`\nRecommended: ${result.recommended}`);
|
|
622
|
-
storeKnowledge({
|
|
623
|
-
type: 'decision',
|
|
624
|
-
title: `${result.final_verdict}: ${task.slice(0, 80)}`,
|
|
625
|
-
content: lines.join(''),
|
|
626
|
-
tags: [isRed ? 'red-verdict' : 'yellow-verdict', isRed ? 'blocked' : 'caution', 'council'],
|
|
627
|
-
project_dir: args?.project_dir ? String(args.project_dir) : undefined,
|
|
628
|
-
session_id: sessionId,
|
|
629
|
-
relevance: isRed ? 1.0 : 0.8,
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
// Build agentic upgrade prompt so host AI can provide real LLM reasoning on any platform
|
|
633
|
-
const enrichedCtx = buildContextString(debateInput.project_dir, debateInput.context);
|
|
634
|
-
const { optionA, optionB, isDecisionTask } = (await import('./council/decision-extractor.js')).extractDecision(task);
|
|
635
|
-
const decisionCtx = isDecisionTask ? `Option A: "${optionA}" vs Option B: "${optionB}"` : undefined;
|
|
636
|
-
const agenticPrompt = buildAgenticDebatePrompt(task, enrichedCtx, decisionCtx);
|
|
637
|
-
const responsePayload = {
|
|
638
|
-
outcome_id: outcomeId,
|
|
639
|
-
llm_backed: false,
|
|
640
|
-
final_verdict: result.final_verdict,
|
|
641
|
-
block_reasons: result.block_reasons,
|
|
642
|
-
warnings: result.warnings,
|
|
643
|
-
recommended: result.recommended,
|
|
644
|
-
debated_at: result.debated_at,
|
|
645
|
-
votes: {
|
|
646
|
-
lead_dev: result.votes.lead_dev,
|
|
647
|
-
pm: result.votes.pm,
|
|
648
|
-
architect: result.votes.architect,
|
|
649
|
-
ux: result.votes.ux,
|
|
650
|
-
devil: result.votes.devil,
|
|
651
|
-
legal: result.votes.legal,
|
|
652
|
-
security: result.votes.security,
|
|
653
|
-
},
|
|
654
|
-
llm_upgrade: {
|
|
655
|
-
available: true,
|
|
656
|
-
instruction: 'The verdict above is deterministic. For LLM-backed analysis: (1) read debate_prompt and reason as all 7 agents, generating the agent_responses JSON; (2) call veto_council_debate again with { task, agent_responses } to get the final LLM-backed verdict. Works on Claude Code, Gemini CLI, and Codex CLI with no API keys.',
|
|
657
|
-
debate_prompt: agenticPrompt,
|
|
658
|
-
},
|
|
659
|
-
};
|
|
660
|
-
const fullText = result.formatted_output + '\n\n' + JSON.stringify(responsePayload, null, 2);
|
|
661
|
-
if (typeof args?.max_tokens === 'number') {
|
|
662
|
-
const { exceeded, estimated_tokens } = logUsage({
|
|
663
|
-
tool_name: 'veto_council_debate',
|
|
664
|
-
session_id: sessionId,
|
|
665
|
-
max_tokens: args.max_tokens,
|
|
666
|
-
output: fullText,
|
|
667
|
-
});
|
|
668
|
-
if (exceeded) {
|
|
669
|
-
responsePayload.budget_warning = `Estimated output tokens (${estimated_tokens}) exceeded max_tokens budget (${args.max_tokens}).`;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
return {
|
|
673
|
-
content: [{ type: 'text', text: fullText }],
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
case 'veto_agent_plan': {
|
|
677
|
-
const args = (request.params.arguments || {});
|
|
678
|
-
const agentType = String(args?.agent ?? '');
|
|
679
|
-
const task = String(args?.task ?? '').trim();
|
|
680
|
-
return await handleAgenticWorker('veto_agent_plan', args, agentType, task);
|
|
681
|
-
}
|
|
682
|
-
case 'veto_code_review': {
|
|
683
|
-
const args = (request.params.arguments || {});
|
|
684
|
-
return await handleAgenticWorker('veto_code_review', args, 'reviewer', 'Review the following code.');
|
|
685
|
-
}
|
|
686
|
-
case 'veto_diff_review': {
|
|
687
|
-
const args = (request.params.arguments || {});
|
|
688
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
689
|
-
const userContext = args?.context ? String(args.context) : undefined;
|
|
690
|
-
// Resolve diff — use provided or read from git
|
|
691
|
-
let diff = args?.diff ? String(args.diff).trim() : '';
|
|
692
|
-
if (!diff && projectDir) {
|
|
693
|
-
try {
|
|
694
|
-
diff = execSyncTop('git diff HEAD --no-color', {
|
|
695
|
-
cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
696
|
-
}).toString().trim();
|
|
697
|
-
if (!diff) {
|
|
698
|
-
diff = execSyncTop('git diff --cached --no-color', {
|
|
699
|
-
cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
700
|
-
}).toString().trim();
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
catch { /* not a git repo or no changes */ }
|
|
704
|
-
}
|
|
705
|
-
if (!diff) {
|
|
706
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'No diff provided and no git changes detected. Pass diff or point to a project_dir with uncommitted changes.' }) }], isError: true };
|
|
707
|
-
}
|
|
708
|
-
// Parse changed files from diff header lines
|
|
709
|
-
const changedFiles = [...diff.matchAll(/^diff --git a\/.+ b\/(.+)$/gm)].map(m => m[1]);
|
|
710
|
-
const diffChunks = diff.split(/^diff --git /m).filter(Boolean);
|
|
711
|
-
const context = buildContextString(projectDir, userContext);
|
|
712
|
-
const scanResult = await runTripleScan(diff, context, true, args?.agent_outputs);
|
|
713
|
-
if ('mode' in scanResult && scanResult.mode === 'agentic_loop')
|
|
714
|
-
return { content: [{ type: 'text', text: JSON.stringify(scanResult, null, 2) }] };
|
|
715
|
-
if ('mode' in scanResult)
|
|
716
|
-
return { content: [{ type: 'text', text: JSON.stringify(scanResult) }] };
|
|
717
|
-
const { reviewResult, secResult, secretsResult, verdict } = scanResult;
|
|
718
|
-
const verdictEmoji = verdict === 'pass' ? '✅ PASS' : verdict === 'warn' ? '⚠️ WARN' : '❌ FAIL';
|
|
719
|
-
// Per-file finding counts (approximate from line refs)
|
|
720
|
-
const fileFindings = {};
|
|
721
|
-
for (const f of changedFiles)
|
|
722
|
-
fileFindings[f] = 0;
|
|
723
|
-
for (const finding of [...(reviewResult.analysis?.findings ?? []), ...(secResult.analysis?.findings ?? [])]) {
|
|
724
|
-
const match = changedFiles.find(f => finding.location?.includes(f));
|
|
725
|
-
if (match)
|
|
726
|
-
fileFindings[match]++;
|
|
727
|
-
}
|
|
728
|
-
if (verdict === 'fail') {
|
|
729
|
-
const blockingIssues = [];
|
|
730
|
-
if ((reviewResult.analysis?.critical_count ?? 0) > 0)
|
|
731
|
-
blockingIssues.push(`Code: ${reviewResult.analysis?.summary ?? 'critical issues found'}`);
|
|
732
|
-
if ((secResult.analysis?.critical_count ?? 0) > 0)
|
|
733
|
-
blockingIssues.push(`Security: ${secResult.analysis?.summary ?? 'vulnerabilities detected'}`);
|
|
734
|
-
if ((secretsResult.analysis?.findings?.length ?? 0) > 0)
|
|
735
|
-
blockingIssues.push(`Secrets: exposed credentials detected`);
|
|
736
|
-
autoStoreCritical(`Diff review failed: ${changedFiles.slice(0, 2).join(', ')}`, blockingIssues, projectDir, ['diff-review']);
|
|
737
|
-
}
|
|
738
|
-
return {
|
|
739
|
-
content: [{
|
|
740
|
-
type: 'text',
|
|
741
|
-
text: JSON.stringify({
|
|
742
|
-
verdict,
|
|
743
|
-
verdict_label: verdictEmoji,
|
|
744
|
-
files_changed: changedFiles.length,
|
|
745
|
-
files: changedFiles,
|
|
746
|
-
file_findings: fileFindings,
|
|
747
|
-
code_review: {
|
|
748
|
-
score: reviewResult.analysis?.score ?? null,
|
|
749
|
-
verdict: reviewResult.analysis?.verdict ?? null,
|
|
750
|
-
critical: reviewResult.analysis?.critical_count ?? 0,
|
|
751
|
-
high: reviewResult.analysis?.high_count ?? 0,
|
|
752
|
-
findings: reviewResult.analysis?.findings ?? [],
|
|
753
|
-
},
|
|
754
|
-
security: {
|
|
755
|
-
score: secResult.analysis?.score ?? null,
|
|
756
|
-
verdict: secResult.analysis?.verdict ?? null,
|
|
757
|
-
critical: secResult.analysis?.critical_count ?? 0,
|
|
758
|
-
high: secResult.analysis?.high_count ?? 0,
|
|
759
|
-
findings: secResult.analysis?.findings ?? [],
|
|
760
|
-
},
|
|
761
|
-
secrets: {
|
|
762
|
-
verdict: secretsResult.analysis?.verdict ?? null,
|
|
763
|
-
findings: secretsResult.analysis?.findings ?? [],
|
|
764
|
-
},
|
|
765
|
-
summary: [
|
|
766
|
-
`${verdictEmoji} — ${changedFiles.length} file(s) changed`,
|
|
767
|
-
`Code: ${reviewResult.analysis?.verdict ?? 'n/a'} (score ${reviewResult.analysis?.score ?? '?'}/100)`,
|
|
768
|
-
`Security: ${secResult.analysis?.verdict ?? 'n/a'} — ${secResult.analysis?.critical_count ?? 0} critical, ${secResult.analysis?.high_count ?? 0} high`,
|
|
769
|
-
`Secrets: ${(secretsResult.analysis?.findings?.length ?? 0) > 0 ? '🔴 Exposed credentials detected' : '✅ Clean'}`,
|
|
770
|
-
].join('\n'),
|
|
771
|
-
}, null, 2),
|
|
772
|
-
}],
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
case 'veto_security_scan': {
|
|
776
|
-
const args = (request.params.arguments || {});
|
|
777
|
-
return await handleAgenticWorker('veto_security_scan', args, 'security-scanner', 'Scan the following code for vulnerabilities.');
|
|
778
|
-
}
|
|
779
|
-
case 'veto_secrets_scan': {
|
|
780
|
-
const args = (request.params.arguments || {});
|
|
781
|
-
return await handleAgenticWorker('veto_secrets_scan', args, 'secrets', 'Scan for exposed secrets.');
|
|
782
|
-
}
|
|
783
|
-
case 'veto_execute_parallel': {
|
|
784
|
-
const args = (request.params.arguments || {});
|
|
785
|
-
const rawTasks = Array.isArray(args?.tasks) ? args.tasks : [];
|
|
786
|
-
const llmBacked = args?.llm_backed !== false;
|
|
787
|
-
const agentOutputs = args?.agent_outputs;
|
|
788
|
-
if (rawTasks.length === 0) {
|
|
789
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'tasks array is required and must not be empty.' }) }], isError: true };
|
|
790
|
-
}
|
|
791
|
-
const parallelProjectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
792
|
-
const defaultModel = args?.editor_model ? String(args.editor_model) : args?.architect_model ? String(args.architect_model) : undefined;
|
|
793
|
-
const tasks = rawTasks.map((t) => ({
|
|
794
|
-
id: String(t.id ?? ''),
|
|
795
|
-
agent: String(t.agent ?? ''),
|
|
796
|
-
task: String(t.task ?? ''),
|
|
797
|
-
code: t.code ? String(t.code) : undefined,
|
|
798
|
-
context: t.context ? String(t.context) : undefined,
|
|
799
|
-
project_dir: t.project_dir ? String(t.project_dir) : parallelProjectDir,
|
|
800
|
-
llm_backed: llmBacked,
|
|
801
|
-
model: t.model ? String(t.model) : defaultModel,
|
|
802
|
-
}));
|
|
803
|
-
// Phase 2: Agentic loop
|
|
804
|
-
if (llmBacked && !agentOutputs) {
|
|
805
|
-
const prompts = tasks.map(t => buildAgenticAgentPrompt(t)).filter(Boolean);
|
|
806
|
-
return {
|
|
807
|
-
content: [{
|
|
808
|
-
type: 'text',
|
|
809
|
-
text: JSON.stringify({
|
|
810
|
-
mode: 'agentic_loop',
|
|
811
|
-
instruction: 'Reason as each agent below using their provided roles and schemas. Return a JSON object mapping task IDs (or agent names) to agent responses.',
|
|
812
|
-
prompts,
|
|
813
|
-
}, null, 2)
|
|
814
|
-
}]
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
const results = (llmBacked && agentOutputs)
|
|
818
|
-
? (await import('./agents/llm-runner.js')).parseAgenticAgentResponses(tasks, agentOutputs)
|
|
819
|
-
: await executeParallel(tasks);
|
|
820
|
-
// #40: auto-record learning outcome per completed parallel task
|
|
821
|
-
for (let i = 0; i < results.length; i++) {
|
|
822
|
-
const r = results[i];
|
|
823
|
-
if (r.error)
|
|
824
|
-
continue;
|
|
825
|
-
const quality = Math.round(r.output.confidence * 100);
|
|
826
|
-
const tier = quality >= 80 ? 1 : quality >= 40 ? 2 : 3;
|
|
827
|
-
recordOutcome(tasks[i]?.task.slice(0, 50) ?? r.agent, 50, tier, r.agent, quality);
|
|
828
|
-
}
|
|
829
|
-
const deterministicFallbacks = results
|
|
830
|
-
.map((r, i) => ({ r, t: tasks[i] }))
|
|
831
|
-
.filter(({ r }) => !r.error && r.llm_backed === false);
|
|
832
|
-
const parallelPayload = {
|
|
833
|
-
count: results.length,
|
|
834
|
-
total_duration_ms: results.reduce((s, r) => s + r.duration_ms, 0),
|
|
835
|
-
llm_backed_count: results.filter(r => r.llm_backed === true).length,
|
|
836
|
-
results: results.map(r => ({
|
|
837
|
-
id: r.id,
|
|
838
|
-
agent: r.agent,
|
|
839
|
-
duration_ms: r.duration_ms,
|
|
840
|
-
llm_backed: r.llm_backed,
|
|
841
|
-
error: r.error,
|
|
842
|
-
output: { ...(r.plan ?? r.analysis), structured: r.output },
|
|
843
|
-
})),
|
|
844
|
-
};
|
|
845
|
-
if (deterministicFallbacks.length > 0) {
|
|
846
|
-
parallelPayload.llm_upgrade = {
|
|
847
|
-
available: true,
|
|
848
|
-
instruction: 'Some agents ran deterministically (MCP Sampling unavailable or failed). For LLM-backed output, reason as each agent using the prompts below, then pass results back via veto_execute_parallel with pre-filled output.',
|
|
849
|
-
agent_prompts: deterministicFallbacks.map(({ r, t }) => t ? buildAgenticAgentPrompt(t) : null).filter(Boolean),
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
if (typeof args?.max_tokens === 'number') {
|
|
853
|
-
const outputText = JSON.stringify(parallelPayload, null, 2);
|
|
854
|
-
const { exceeded, estimated_tokens } = logUsage({
|
|
855
|
-
tool_name: 'veto_execute_parallel',
|
|
856
|
-
max_tokens: args.max_tokens,
|
|
857
|
-
output: outputText,
|
|
858
|
-
});
|
|
859
|
-
if (exceeded) {
|
|
860
|
-
parallelPayload.budget_warning = `Estimated output tokens (${estimated_tokens}) exceeded max_tokens budget (${args.max_tokens}).`;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
return {
|
|
864
|
-
content: [{ type: 'text', text: JSON.stringify(parallelPayload, null, 2) }],
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
case 'veto_memory_store': {
|
|
868
|
-
const args = (request.params.arguments || {});
|
|
869
|
-
const title = String(args?.title ?? '').trim();
|
|
870
|
-
const content = String(args?.content ?? '').trim();
|
|
871
|
-
if (!title || !content) {
|
|
872
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'title and content are required.' }) }], isError: true };
|
|
873
|
-
}
|
|
874
|
-
const id = storeKnowledge({
|
|
875
|
-
title,
|
|
876
|
-
content,
|
|
877
|
-
type: args?.type ? String(args.type) : 'solution',
|
|
878
|
-
tags: Array.isArray(args?.tags) ? args.tags.map(String) : undefined,
|
|
879
|
-
project_dir: args?.project_dir ? String(args.project_dir) : (activeProjectDir ?? undefined),
|
|
880
|
-
session_id: args?.session_id ? String(args.session_id) : undefined,
|
|
881
|
-
relevance: typeof args?.relevance === 'number' ? args.relevance : 1.0,
|
|
882
|
-
});
|
|
883
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, id, message: 'Knowledge stored.' }, null, 2) }] };
|
|
884
|
-
}
|
|
885
|
-
case 'veto_memory_search': {
|
|
886
|
-
const args = (request.params.arguments || {});
|
|
887
|
-
const results = searchKnowledge({
|
|
888
|
-
query: args?.query ? String(args.query) : undefined,
|
|
889
|
-
type: args?.type ? String(args.type) : undefined,
|
|
890
|
-
project_dir: args?.project_dir ? String(args.project_dir) : (activeProjectDir ?? undefined),
|
|
891
|
-
limit: typeof args?.limit === 'number' ? args.limit : 10,
|
|
892
|
-
});
|
|
893
|
-
return {
|
|
894
|
-
content: [{
|
|
895
|
-
type: 'text',
|
|
896
|
-
text: JSON.stringify({
|
|
897
|
-
count: results.length,
|
|
898
|
-
results: results.map(r => ({
|
|
899
|
-
id: r.id,
|
|
900
|
-
type: r.type,
|
|
901
|
-
title: r.title,
|
|
902
|
-
content: r.content,
|
|
903
|
-
tags: r.tags ? JSON.parse(r.tags) : [],
|
|
904
|
-
project_dir: r.project_dir,
|
|
905
|
-
relevance: r.relevance,
|
|
906
|
-
accessed_count: r.accessed_count,
|
|
907
|
-
created_at: r.created_at,
|
|
908
|
-
})),
|
|
909
|
-
}, null, 2),
|
|
910
|
-
}],
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
case 'veto_memory_delete': {
|
|
914
|
-
const args = (request.params.arguments || {});
|
|
915
|
-
const id = String(args?.id ?? '').trim();
|
|
916
|
-
if (!id) {
|
|
917
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'id is required.' }) }], isError: true };
|
|
918
|
-
}
|
|
919
|
-
const deleted = deleteKnowledge(id);
|
|
920
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: deleted, message: deleted ? 'Entry deleted.' : 'Entry not found.' }, null, 2) }] };
|
|
921
|
-
}
|
|
922
|
-
case 'veto_project_map_update': {
|
|
923
|
-
const args = (request.params.arguments || {});
|
|
924
|
-
const project_dir = String(args?.project_dir ?? '').trim();
|
|
925
|
-
if (!project_dir) {
|
|
926
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
927
|
-
}
|
|
928
|
-
// auto_compute: build live repo-map instead of requiring manual structure
|
|
929
|
-
if (args?.auto_compute === true) {
|
|
930
|
-
try {
|
|
931
|
-
const computed = buildRepoMap({ projectDir: project_dir, maxTopModules: 30 });
|
|
932
|
-
const id = updateProjectMap({
|
|
933
|
-
project_dir,
|
|
934
|
-
structure: {
|
|
935
|
-
auto_computed: true,
|
|
936
|
-
generated_at: computed.generated_at,
|
|
937
|
-
total_files: computed.total_files,
|
|
938
|
-
symbol_count: computed.symbol_count,
|
|
939
|
-
top_modules: computed.top_modules.slice(0, 20).map(m => ({
|
|
940
|
-
file: m.file, rank: m.rank, refs: m.ref_count,
|
|
941
|
-
exports: m.symbols.slice(0, 5).map(s => s.name),
|
|
942
|
-
})),
|
|
943
|
-
},
|
|
944
|
-
key_modules: computed.top_modules.slice(0, 15).map(m => m.file),
|
|
945
|
-
});
|
|
946
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, id, auto_computed: true, total_files: computed.total_files, symbol_count: computed.symbol_count, top_modules_count: computed.top_modules.length }, null, 2) }] };
|
|
947
|
-
}
|
|
948
|
-
catch (err) {
|
|
949
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Repo-map failed: ${err instanceof Error ? err.message : String(err)}` }) }], isError: true };
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
const structure = String(args?.structure ?? '').trim();
|
|
953
|
-
if (!structure) {
|
|
954
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'Provide structure or set auto_compute: true to compute it automatically.' }) }], isError: true };
|
|
955
|
-
}
|
|
956
|
-
const id = updateProjectMap({
|
|
957
|
-
project_dir,
|
|
958
|
-
structure,
|
|
959
|
-
key_modules: Array.isArray(args?.key_modules) ? args.key_modules.map(String) : undefined,
|
|
960
|
-
tech_stack: Array.isArray(args?.tech_stack) ? args.tech_stack.map(String) : undefined,
|
|
961
|
-
});
|
|
962
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, id, message: 'Project map updated.' }, null, 2) }] };
|
|
963
|
-
}
|
|
964
|
-
case 'veto_project_map_get': {
|
|
965
|
-
const args = (request.params.arguments || {});
|
|
966
|
-
const project_dir = String(args?.project_dir ?? '').trim();
|
|
967
|
-
if (!project_dir) {
|
|
968
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
969
|
-
}
|
|
970
|
-
const row = getProjectMap(project_dir);
|
|
971
|
-
if (!row) {
|
|
972
|
-
return { content: [{ type: 'text', text: JSON.stringify({ found: false, message: 'No project map found. Call veto_project_map_update to create one.' }, null, 2) }] };
|
|
973
|
-
}
|
|
974
|
-
return {
|
|
975
|
-
content: [{
|
|
976
|
-
type: 'text',
|
|
977
|
-
text: JSON.stringify({
|
|
978
|
-
found: true,
|
|
979
|
-
project_dir: row.project_dir,
|
|
980
|
-
structure: JSON.parse(row.structure),
|
|
981
|
-
key_modules: row.key_modules ? JSON.parse(row.key_modules) : [],
|
|
982
|
-
tech_stack: row.tech_stack ? JSON.parse(row.tech_stack) : [],
|
|
983
|
-
updated_at: row.updated_at,
|
|
984
|
-
}, null, 2),
|
|
985
|
-
}],
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
case 'veto_pattern_store': {
|
|
989
|
-
const args = (request.params.arguments || {});
|
|
990
|
-
const pattern_key = String(args?.pattern_key ?? '').trim();
|
|
991
|
-
const pattern_val = String(args?.pattern_val ?? '').trim();
|
|
992
|
-
if (!pattern_key || !pattern_val) {
|
|
993
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'pattern_key and pattern_val are required.' }) }], isError: true };
|
|
994
|
-
}
|
|
995
|
-
upsertPattern({
|
|
996
|
-
pattern_key,
|
|
997
|
-
pattern_val,
|
|
998
|
-
confidence: typeof args?.confidence === 'number' ? args.confidence : 1.0,
|
|
999
|
-
});
|
|
1000
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Pattern stored.' }, null, 2) }] };
|
|
1001
|
-
}
|
|
1002
|
-
case 'veto_patterns_list': {
|
|
1003
|
-
const args = (request.params.arguments || {});
|
|
1004
|
-
const prefix = args?.prefix ? String(args.prefix) : undefined;
|
|
1005
|
-
const limit = typeof args?.limit === 'number' ? args.limit : 20;
|
|
1006
|
-
const patterns = getPatterns(prefix, limit);
|
|
1007
|
-
return {
|
|
1008
|
-
content: [{
|
|
1009
|
-
type: 'text',
|
|
1010
|
-
text: JSON.stringify({
|
|
1011
|
-
count: patterns.length,
|
|
1012
|
-
patterns: patterns.map(p => ({
|
|
1013
|
-
key: p.pattern_key,
|
|
1014
|
-
val: p.pattern_val,
|
|
1015
|
-
confidence: p.confidence,
|
|
1016
|
-
seen_count: p.seen_count,
|
|
1017
|
-
updated_at: p.updated_at,
|
|
1018
|
-
})),
|
|
1019
|
-
}, null, 2),
|
|
1020
|
-
}],
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
case 'veto_handoff': {
|
|
1024
|
-
const args = (request.params.arguments || {});
|
|
1025
|
-
const summary = String(args?.summary ?? '').trim();
|
|
1026
|
-
const context = String(args?.context ?? '').trim();
|
|
1027
|
-
if (!summary || !context) {
|
|
1028
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'summary and context are required.' }) }], isError: true };
|
|
1029
|
-
}
|
|
1030
|
-
const handoffPlatform = args?.from_platform ? String(args.from_platform) : 'claude';
|
|
1031
|
-
const handoffTaskState = args?.task_state ? String(args.task_state) : undefined;
|
|
1032
|
-
const handoffProjectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1033
|
-
const result = handoff({
|
|
1034
|
-
summary,
|
|
1035
|
-
context,
|
|
1036
|
-
task_state: handoffTaskState,
|
|
1037
|
-
from_platform: handoffPlatform,
|
|
1038
|
-
to_platform: args?.to_platform ? String(args.to_platform) : undefined,
|
|
1039
|
-
project_dir: handoffProjectDir,
|
|
1040
|
-
token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
|
|
1041
|
-
});
|
|
1042
|
-
// Cache for auto-save
|
|
1043
|
-
autoSave.cached = { summary, context, task_state: handoffTaskState, platform: handoffPlatform, project_dir: handoffProjectDir };
|
|
1044
|
-
autoSave.last_save_at = result.saved_at;
|
|
1045
|
-
autoSave.last_session_id = result.session_id;
|
|
1046
|
-
// Close the current session so ended_at is recorded
|
|
1047
|
-
if (activeProjectDir) {
|
|
1048
|
-
const sessions = listSessions(1);
|
|
1049
|
-
if (sessions[0] && sessions[0].id !== result.session_id)
|
|
1050
|
-
closeSession(sessions[0].id);
|
|
1051
|
-
}
|
|
1052
|
-
return { content: [{ type: 'text', text: result.instructions + '\n\n' + JSON.stringify({ session_id: result.session_id, to_platform: result.to_platform, saved_at: result.saved_at, reason: result.reason }, null, 2) }] };
|
|
1053
|
-
}
|
|
1054
|
-
case 'veto_continue': {
|
|
1055
|
-
const args = (request.params.arguments || {});
|
|
1056
|
-
const resuming_as = args?.resuming_as ? String(args.resuming_as) : undefined;
|
|
1057
|
-
const result = continueSession(args?.session_id ? String(args.session_id) : undefined, resuming_as);
|
|
1058
|
-
if (!result.found) {
|
|
1059
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: result.message }, null, 2) }], isError: true };
|
|
1060
|
-
}
|
|
1061
|
-
if (result.project_dir)
|
|
1062
|
-
activeProjectDir = result.project_dir;
|
|
1063
|
-
return {
|
|
1064
|
-
content: [{
|
|
1065
|
-
type: 'text',
|
|
1066
|
-
text: result.message + '\n\n' + JSON.stringify({
|
|
1067
|
-
session_id: result.session_id,
|
|
1068
|
-
created_by: result.platform,
|
|
1069
|
-
active_client: result.active_client ?? result.platform,
|
|
1070
|
-
summary: result.summary,
|
|
1071
|
-
context: result.context,
|
|
1072
|
-
task_state: result.task_state,
|
|
1073
|
-
next_action: result.next_action,
|
|
1074
|
-
project_dir: result.project_dir,
|
|
1075
|
-
token_count: result.token_count,
|
|
1076
|
-
restored_at: result.restored_at,
|
|
1077
|
-
}, null, 2),
|
|
1078
|
-
}],
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
case 'veto_platform_setup': {
|
|
1082
|
-
const args = (request.params.arguments || {});
|
|
1083
|
-
const platform = String(args?.platform ?? '').trim();
|
|
1084
|
-
const vetoServerPath = String(args?.veto_server_path ?? '').trim();
|
|
1085
|
-
if (!platform || !vetoServerPath) {
|
|
1086
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'platform and veto_server_path are required.' }) }], isError: true };
|
|
1087
|
-
}
|
|
1088
|
-
const setup = getPlatformSetup(platform, vetoServerPath);
|
|
1089
|
-
return { content: [{ type: 'text', text: JSON.stringify(setup, null, 2) }] };
|
|
1090
|
-
}
|
|
1091
|
-
case 'veto_record_outcome': {
|
|
1092
|
-
const args = (request.params.arguments || {});
|
|
1093
|
-
const task_type = String(args?.task_type ?? '').trim();
|
|
1094
|
-
const complexity = typeof args?.complexity === 'number' ? args.complexity : 50;
|
|
1095
|
-
const model_tier = (typeof args?.model_tier === 'number' ? args.model_tier : 2);
|
|
1096
|
-
const output_quality = typeof args?.output_quality === 'number' ? args.output_quality : 70;
|
|
1097
|
-
if (!task_type) {
|
|
1098
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'task_type is required.' }) }], isError: true };
|
|
1099
|
-
}
|
|
1100
|
-
recordOutcome(task_type, complexity, model_tier, args?.agent ? String(args.agent) : 'dynamic', output_quality, typeof args?.tokens_used === 'number' ? args.tokens_used : 0, args?.file_ext ? String(args.file_ext) : undefined);
|
|
1101
|
-
const stats = getLearningStats();
|
|
1102
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Outcome recorded.', total_outcomes: stats.total_tasks, next_step: stats.total_tasks >= 20 ? 'You have 20+ outcomes. Call veto_learning_apply to update router thresholds.' : `Need ${20 - stats.total_tasks} more outcomes before veto_learning_apply can adjust thresholds.` }, null, 2) }] };
|
|
1103
|
-
}
|
|
1104
|
-
case 'veto_learning_stats': {
|
|
1105
|
-
const args = (request.params.arguments || {});
|
|
1106
|
-
const includeAgentStats = args?.include_agent_stats !== false;
|
|
1107
|
-
const includeTaskTypes = args?.include_task_types === true;
|
|
1108
|
-
const includeCouncil = args?.include_council_insights === true;
|
|
1109
|
-
const stats = getLearningStats();
|
|
1110
|
-
const learned = getLearnedThresholds();
|
|
1111
|
-
const result = {
|
|
1112
|
-
total_outcomes: stats.total_tasks,
|
|
1113
|
-
tier_breakdown: stats.tier_breakdown,
|
|
1114
|
-
current_thresholds: {
|
|
1115
|
-
tier1_max: learned.tier1_max,
|
|
1116
|
-
tier2_max: learned.tier2_max,
|
|
1117
|
-
source: learned.source,
|
|
1118
|
-
data_points: learned.data_points,
|
|
1119
|
-
note: learned.source === 'learned'
|
|
1120
|
-
? `Learned from ${learned.data_points} outcomes.`
|
|
1121
|
-
: 'Using defaults — call veto_learning_apply after 20+ outcomes to update from data.',
|
|
1122
|
-
},
|
|
1123
|
-
suggested_thresholds: stats.suggested_thresholds,
|
|
1124
|
-
ready_to_apply: stats.total_tasks >= 20,
|
|
1125
|
-
};
|
|
1126
|
-
if (includeAgentStats) {
|
|
1127
|
-
result['agent_performance'] = getAgentPerformanceStats();
|
|
1128
|
-
}
|
|
1129
|
-
if (includeTaskTypes) {
|
|
1130
|
-
result['task_type_breakdown'] = getTaskTypeBreakdown();
|
|
1131
|
-
}
|
|
1132
|
-
if (includeCouncil) {
|
|
1133
|
-
result['council_insights'] = getCouncilInsights();
|
|
1134
|
-
}
|
|
1135
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1136
|
-
}
|
|
1137
|
-
case 'veto_learning_apply': {
|
|
1138
|
-
const args = (request.params.arguments || {});
|
|
1139
|
-
const result = applyLearnedThresholds();
|
|
1140
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1141
|
-
}
|
|
1142
|
-
case 'veto_memory_export': {
|
|
1143
|
-
const args = (request.params.arguments || {});
|
|
1144
|
-
const format = args?.format === 'markdown' ? 'markdown' : 'json';
|
|
1145
|
-
if (format === 'markdown') {
|
|
1146
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1147
|
-
const outputPath = args?.output_path ? String(args.output_path) : undefined;
|
|
1148
|
-
const result = exportMemoryMarkdown(projectDir, outputPath);
|
|
1149
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1150
|
-
}
|
|
1151
|
-
const result = exportMemory(args?.output_path ? String(args.output_path) : undefined);
|
|
1152
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1153
|
-
}
|
|
1154
|
-
case 'veto_memory_import': {
|
|
1155
|
-
const args = (request.params.arguments || {});
|
|
1156
|
-
const format = args?.format === 'markdown' ? 'markdown' : 'json';
|
|
1157
|
-
if (format === 'markdown') {
|
|
1158
|
-
const inputPath = String(args?.input_path ?? '').trim();
|
|
1159
|
-
if (!inputPath)
|
|
1160
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'input_path is required for markdown import.' }) }], isError: true };
|
|
1161
|
-
const result = importMemoryMarkdown(inputPath);
|
|
1162
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1163
|
-
}
|
|
1164
|
-
const result = importMemory(args?.input_path ? String(args.input_path) : undefined);
|
|
1165
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1166
|
-
}
|
|
1167
|
-
case 'veto_watch': {
|
|
1168
|
-
const args = (request.params.arguments || {});
|
|
1169
|
-
const dir = String(args?.project_dir ?? '').trim();
|
|
1170
|
-
if (!dir)
|
|
1171
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
1172
|
-
const watch_id = startWatch(dir);
|
|
1173
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, watch_id, project_dir: dir, message: `Watching "${dir}". Call veto_watch_poll with watch_id to collect events.` }, null, 2) }] };
|
|
1174
|
-
}
|
|
1175
|
-
case 'veto_watch_poll': {
|
|
1176
|
-
const args = (request.params.arguments || {});
|
|
1177
|
-
const watch_id = String(args?.watch_id ?? '').trim();
|
|
1178
|
-
if (!watch_id)
|
|
1179
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'watch_id is required.' }) }], isError: true };
|
|
1180
|
-
const result = pollWatch(watch_id);
|
|
1181
|
-
if (!result.found)
|
|
1182
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `No active watcher with id: ${watch_id}` }) }], isError: true };
|
|
1183
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, watch_id, project_dir: result.project_dir, event_count: result.events.length, events: result.events }, null, 2) }] };
|
|
1184
|
-
}
|
|
1185
|
-
case 'veto_watch_stop': {
|
|
1186
|
-
const args = (request.params.arguments || {});
|
|
1187
|
-
const watch_id = String(args?.watch_id ?? '').trim();
|
|
1188
|
-
if (!watch_id)
|
|
1189
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'watch_id is required.' }) }], isError: true };
|
|
1190
|
-
const stopped = stopWatch(watch_id);
|
|
1191
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: stopped, message: stopped ? `Watcher ${watch_id} stopped.` : `No watcher found with id: ${watch_id}` }, null, 2) }] };
|
|
1192
|
-
}
|
|
1193
|
-
case 'veto_workflow': {
|
|
1194
|
-
const args = (request.params.arguments || {});
|
|
1195
|
-
const rawSteps = Array.isArray(args?.steps) ? args.steps : [];
|
|
1196
|
-
if (rawSteps.length === 0)
|
|
1197
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'steps array is required and must not be empty.' }) }], isError: true };
|
|
1198
|
-
const steps = rawSteps.map((s) => ({
|
|
1199
|
-
id: String(s.id ?? ''),
|
|
1200
|
-
agent: String(s.agent ?? ''),
|
|
1201
|
-
task: String(s.task ?? ''),
|
|
1202
|
-
code: s.code ? String(s.code) : undefined,
|
|
1203
|
-
context: s.context ? String(s.context) : undefined,
|
|
1204
|
-
gate: typeof s.gate === 'number' ? s.gate : undefined,
|
|
1205
|
-
retry_on_fail: s.retry_on_fail === true,
|
|
1206
|
-
max_retries: typeof s.max_retries === 'number' ? Math.min(s.max_retries, 5) : undefined,
|
|
1207
|
-
condition: s.condition ? String(s.condition) : undefined,
|
|
1208
|
-
dependencies: Array.isArray(s.dependencies) ? s.dependencies.map(String) : undefined,
|
|
1209
|
-
}));
|
|
1210
|
-
const mode = String(args?.mode ?? 'linear') === 'dag' ? 'dag' : 'linear';
|
|
1211
|
-
const result = await runPipeline(steps, args?.project_dir ? String(args.project_dir) : undefined, mode, async (question) => {
|
|
1212
|
-
try {
|
|
1213
|
-
const resp = await server.createMessage({ messages: [{ role: 'user', content: { type: 'text', text: question } }], maxTokens: 200 });
|
|
1214
|
-
return resp.content.type === 'text' ? resp.content.text : '';
|
|
1215
|
-
}
|
|
1216
|
-
catch {
|
|
1217
|
-
return ''; // sampling not supported by client
|
|
1218
|
-
}
|
|
1219
|
-
});
|
|
1220
|
-
// #39: auto-record learning outcome per executed workflow step
|
|
1221
|
-
for (const step of result.results) {
|
|
1222
|
-
if (step.status === 'skipped')
|
|
1223
|
-
continue;
|
|
1224
|
-
const quality = step.error ? 0 : step.confidence;
|
|
1225
|
-
const tier = quality >= 80 ? 1 : quality >= 40 ? 2 : 3;
|
|
1226
|
-
const taskStr = steps.find(s => s.id === step.id)?.task.slice(0, 50) ?? step.id;
|
|
1227
|
-
recordOutcome(taskStr, 50, tier, step.agent, quality);
|
|
1228
|
-
}
|
|
1229
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1230
|
-
}
|
|
1231
|
-
case 'veto_explain': {
|
|
1232
|
-
const args = (request.params.arguments || {});
|
|
1233
|
-
return await handleAgenticWorker('veto_explain', args, args?.file_path ? 'coder' : 'debugger', String(args?.text || 'Explain this.'));
|
|
1234
|
-
}
|
|
1235
|
-
case 'veto_plugins': {
|
|
1236
|
-
const args = (request.params.arguments || {});
|
|
1237
|
-
return { content: [{ type: 'text', text: JSON.stringify({ plugins: listPlugins(), plugin_dir: `${process.env.HOME ?? process.env.USERPROFILE}/.veto/agents/`, instructions: 'Drop a .js file exporting plan(task, context?) to register a custom agent.' }, null, 2) }] };
|
|
1238
|
-
}
|
|
1239
|
-
// ── Phase 13: Developer Intelligence ──────────────────────────────────────
|
|
1240
|
-
case 'veto_docs_fetch': {
|
|
1241
|
-
const args = (request.params.arguments || {});
|
|
1242
|
-
const package_name = String(args?.package_name ?? '').trim();
|
|
1243
|
-
const ecosystem = String(args?.ecosystem ?? 'npm');
|
|
1244
|
-
const version = args?.version ? String(args.version) : undefined;
|
|
1245
|
-
const max_chars = typeof args?.max_chars === 'number' ? args.max_chars : 8000;
|
|
1246
|
-
if (!package_name) {
|
|
1247
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'package_name is required.' }) }], isError: true };
|
|
1248
|
-
}
|
|
1249
|
-
const result = await fetchAndCacheDocs(package_name, ecosystem, version, max_chars, VERSION);
|
|
1250
|
-
if (!result) {
|
|
1251
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Could not fetch docs for ${package_name} (${ecosystem}). Source may be offline — try again.` }) }], isError: true };
|
|
1252
|
-
}
|
|
1253
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...result }, null, 2) }] };
|
|
1254
|
-
}
|
|
1255
|
-
case 'veto_context_status': {
|
|
1256
|
-
const args = (request.params.arguments || {});
|
|
1257
|
-
const session_id = String(args?.session_id ?? '');
|
|
1258
|
-
if (!session_id) {
|
|
1259
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'session_id is required.' }) }], isError: true };
|
|
1260
|
-
}
|
|
1261
|
-
const status = getContextStatus(session_id);
|
|
1262
|
-
if (!status) {
|
|
1263
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `No session found: ${session_id}` }) }], isError: true };
|
|
1264
|
-
}
|
|
1265
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...status }, null, 2) }] };
|
|
1266
|
-
}
|
|
1267
|
-
case 'veto_task_parse': {
|
|
1268
|
-
const args = (request.params.arguments || {});
|
|
1269
|
-
const description = String(args?.description ?? '').trim();
|
|
1270
|
-
const project_dir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1271
|
-
const max_tasks = typeof args?.max_tasks === 'number' ? Math.min(args.max_tasks, 50) : 20;
|
|
1272
|
-
if (!description)
|
|
1273
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'description is required.' }) }], isError: true };
|
|
1274
|
-
const agentResponse = args?.agent_responses?.planner;
|
|
1275
|
-
if (agentResponse) {
|
|
1276
|
-
const results = parseAgenticAgentResponses([{ id: 'planner', agent: 'task-planner', task: description }], { planner: agentResponse });
|
|
1277
|
-
const r = results[0];
|
|
1278
|
-
if (r.plan) {
|
|
1279
|
-
const plan = parsePrdIntoTasks(description, r.plan, max_tasks);
|
|
1280
|
-
saveTaskPlan(description, JSON.stringify(plan));
|
|
1281
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...plan }, null, 2) }] };
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
// Try sampling
|
|
1285
|
-
try {
|
|
1286
|
-
const result = await executeOne({ id: 'planner', agent: 'task-planner', task: description, project_dir, llm_backed: true });
|
|
1287
|
-
if (result.llm_backed && result.plan && !result.error) {
|
|
1288
|
-
const plan = parsePrdIntoTasks(description, result.plan, max_tasks);
|
|
1289
|
-
saveTaskPlan(description, JSON.stringify(plan));
|
|
1290
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...plan }, null, 2) }] };
|
|
1291
|
-
}
|
|
1292
|
-
if (result.llm_upgrade) {
|
|
1293
|
-
return { content: [{ type: 'text', text: JSON.stringify({ llm_backed: false, llm_upgrade: result.llm_upgrade }, null, 2) }] };
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
catch { /* fallback */ }
|
|
1297
|
-
const plan = await buildTaskPlan(description, project_dir, max_tasks);
|
|
1298
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...plan }, null, 2) }] };
|
|
1299
|
-
}
|
|
1300
|
-
// ── Phase 14: Observability & Safety ──────────────────────────────────────
|
|
1301
|
-
case 'veto_usage_status': {
|
|
1302
|
-
const args = (request.params.arguments || {});
|
|
1303
|
-
if (args?.set_budget && typeof args.set_budget === 'object') {
|
|
1304
|
-
const b = args.set_budget;
|
|
1305
|
-
const current = getConfig().dailyTokenBudget;
|
|
1306
|
-
setConfig({
|
|
1307
|
-
dailyTokenBudget: {
|
|
1308
|
-
claude: typeof b.claude === 'number' ? b.claude : current.claude,
|
|
1309
|
-
gemini: typeof b.gemini === 'number' ? b.gemini : current.gemini,
|
|
1310
|
-
codex: typeof b.codex === 'number' ? b.codex : current.codex,
|
|
1311
|
-
antigravity: typeof b.antigravity === 'number' ? b.antigravity : current.antigravity,
|
|
1312
|
-
},
|
|
1313
|
-
});
|
|
1314
|
-
}
|
|
1315
|
-
const status = getUsageStatus();
|
|
1316
|
-
const { dailyTokenBudget } = getConfig();
|
|
1317
|
-
const rateStatus = getRateStatus();
|
|
1318
|
-
const recentBudgetLog = getUsageLogs({ limit: 10 });
|
|
1319
|
-
return {
|
|
1320
|
-
content: [{
|
|
1321
|
-
type: 'text',
|
|
1322
|
-
text: JSON.stringify({
|
|
1323
|
-
success: true,
|
|
1324
|
-
...status,
|
|
1325
|
-
daily_token_budget: dailyTokenBudget,
|
|
1326
|
-
tokens_today: {
|
|
1327
|
-
claude: rateStatus.claude.tokens_today,
|
|
1328
|
-
gemini: rateStatus.gemini.tokens_today,
|
|
1329
|
-
codex: rateStatus.codex.tokens_today,
|
|
1330
|
-
antigravity: rateStatus.antigravity.tokens_today,
|
|
1331
|
-
},
|
|
1332
|
-
budget_used_pct: {
|
|
1333
|
-
claude: rateStatus.claude.used_percent,
|
|
1334
|
-
gemini: rateStatus.gemini.used_percent,
|
|
1335
|
-
codex: rateStatus.codex.used_percent,
|
|
1336
|
-
antigravity: rateStatus.antigravity.used_percent,
|
|
1337
|
-
},
|
|
1338
|
-
operation_budget_log: recentBudgetLog.map(e => ({
|
|
1339
|
-
tool: e.tool_name,
|
|
1340
|
-
max_tokens: e.max_tokens,
|
|
1341
|
-
estimated_tokens: e.estimated_tokens,
|
|
1342
|
-
exceeded: e.exceeded === 1,
|
|
1343
|
-
at: e.created_at,
|
|
1344
|
-
})),
|
|
1345
|
-
}, null, 2),
|
|
1346
|
-
}],
|
|
1347
|
-
};
|
|
1348
|
-
}
|
|
1349
|
-
case 'veto_audit_log': {
|
|
1350
|
-
const args = (request.params.arguments || {});
|
|
1351
|
-
const events = getAuditLog({
|
|
1352
|
-
session_id: args?.session_id ? String(args.session_id) : undefined,
|
|
1353
|
-
verdict: args?.verdict ? String(args.verdict) : undefined,
|
|
1354
|
-
since: args?.since ? String(args.since) : undefined,
|
|
1355
|
-
limit: typeof args?.limit === 'number' ? args.limit : 20,
|
|
1356
|
-
});
|
|
1357
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, count: events.length, events }, null, 2) }] };
|
|
1358
|
-
}
|
|
1359
|
-
case 'veto_health': {
|
|
1360
|
-
const args = (request.params.arguments || {});
|
|
1361
|
-
const stats = getHealthStats();
|
|
1362
|
-
let db_size_bytes = 0;
|
|
1363
|
-
try {
|
|
1364
|
-
db_size_bytes = statSync(getDbPath()).size;
|
|
1365
|
-
}
|
|
1366
|
-
catch { /* db may not exist */ }
|
|
1367
|
-
const db_size_human = db_size_bytes < 1024 ? `${db_size_bytes}B`
|
|
1368
|
-
: db_size_bytes < 1048576 ? `${(db_size_bytes / 1024).toFixed(1)}KB`
|
|
1369
|
-
: `${(db_size_bytes / 1048576).toFixed(1)}MB`;
|
|
1370
|
-
return {
|
|
1371
|
-
content: [{
|
|
1372
|
-
type: 'text',
|
|
1373
|
-
text: JSON.stringify({
|
|
1374
|
-
success: true,
|
|
1375
|
-
version: VERSION,
|
|
1376
|
-
status: serverErrorCount > 10 ? 'degraded' : 'healthy',
|
|
1377
|
-
uptime_seconds: Math.round((Date.now() - SERVER_START_TIME) / 1000),
|
|
1378
|
-
db_path: getDbPath(),
|
|
1379
|
-
db_size_bytes,
|
|
1380
|
-
db_size_human,
|
|
1381
|
-
error_count_since_start: serverErrorCount,
|
|
1382
|
-
last_error: lastServerError,
|
|
1383
|
-
context_windows: CONTEXT_WINDOWS,
|
|
1384
|
-
...stats,
|
|
1385
|
-
}, null, 2),
|
|
1386
|
-
}],
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
1389
|
-
// ── Phase 15: CI/CD & Distribution ────────────────────────────────────────
|
|
1390
|
-
case 'veto_ci_gate': {
|
|
1391
|
-
const args = (request.params.arguments || {});
|
|
1392
|
-
const project_dir = String(args?.project_dir ?? '').trim();
|
|
1393
|
-
const diff_input = args?.diff ? String(args.diff) : undefined;
|
|
1394
|
-
const context = args?.context ? String(args.context) : undefined;
|
|
1395
|
-
const fail_on = args?.fail_on === 'warn' ? 'warn' : 'fail';
|
|
1396
|
-
if (!project_dir) {
|
|
1397
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
1398
|
-
}
|
|
1399
|
-
const start = Date.now();
|
|
1400
|
-
// Read diff if not provided
|
|
1401
|
-
let diff = diff_input;
|
|
1402
|
-
if (!diff) {
|
|
1403
|
-
try {
|
|
1404
|
-
diff = execSyncTop('git diff HEAD', { cwd: project_dir, encoding: 'utf8', timeout: 15000 });
|
|
1405
|
-
}
|
|
1406
|
-
catch {
|
|
1407
|
-
diff = '';
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
if (!diff?.trim()) {
|
|
1411
|
-
return { content: [{ type: 'text', text: JSON.stringify({ verdict: 'pass', exit_code: 0, message: 'No changes detected.', duration_ms: Date.now() - start }) }] };
|
|
1412
|
-
}
|
|
1413
|
-
const projectCtx = (() => { try {
|
|
1414
|
-
return buildContextString(project_dir);
|
|
1415
|
-
}
|
|
1416
|
-
catch {
|
|
1417
|
-
return '';
|
|
1418
|
-
} })();
|
|
1419
|
-
const fullContext = [context, projectCtx].filter(Boolean).join('\n\n');
|
|
1420
|
-
const scanResult = await runTripleScan(diff, fullContext);
|
|
1421
|
-
if ('mode' in scanResult)
|
|
1422
|
-
return { content: [{ type: 'text', text: JSON.stringify(scanResult, null, 2) }] };
|
|
1423
|
-
const { reviewResult: codeResult, secResult, secretsResult, verdict } = scanResult;
|
|
1424
|
-
const exit_code = verdict === 'fail' || (verdict === 'warn' && fail_on === 'warn') ? 1 : 0;
|
|
1425
|
-
const codeScore = codeResult.analysis?.score ?? Math.round((codeResult.output?.confidence ?? 0.8) * 100);
|
|
1426
|
-
const secScore = secResult.analysis?.score ?? Math.round((secResult.output?.confidence ?? 0.8) * 100);
|
|
1427
|
-
const secretsClean = (secretsResult.analysis?.findings?.length ?? 0) === 0;
|
|
1428
|
-
const blocking_issues = [];
|
|
1429
|
-
if ((codeResult.analysis?.critical_count ?? 0) > 0)
|
|
1430
|
-
blocking_issues.push(`Code review: ${codeResult.analysis?.summary ?? 'critical issues found'}`);
|
|
1431
|
-
if ((secResult.analysis?.critical_count ?? 0) > 0)
|
|
1432
|
-
blocking_issues.push(`Security: ${secResult.analysis?.summary ?? 'vulnerabilities detected'}`);
|
|
1433
|
-
if (!secretsClean)
|
|
1434
|
-
blocking_issues.push(`Secrets: ${secretsResult.analysis?.summary ?? 'exposed credentials detected'}`);
|
|
1435
|
-
const icon = verdict === 'pass' ? '✅' : verdict === 'warn' ? '⚠️' : '❌';
|
|
1436
|
-
const ci_summary = [
|
|
1437
|
-
`${icon} **Veto CI Gate: ${verdict.toUpperCase()}**`,
|
|
1438
|
-
``,
|
|
1439
|
-
`| Check | Score | Status |`,
|
|
1440
|
-
`|---|---|---|`,
|
|
1441
|
-
`| Code Review | ${codeScore}% | ${(codeResult.analysis?.critical_count ?? 0) === 0 ? '✅' : '❌'} |`,
|
|
1442
|
-
`| Security Scan | ${secScore}% | ${(secResult.analysis?.critical_count ?? 0) === 0 ? '✅' : '❌'} |`,
|
|
1443
|
-
`| Secrets Scan | — | ${secretsClean ? '✅ Clean' : '❌ Found'} |`,
|
|
1444
|
-
blocking_issues.length > 0 ? `\n**Blocking issues:**\n${blocking_issues.map(i => `- ${i}`).join('\n')}` : '',
|
|
1445
|
-
].filter(Boolean).join('\n');
|
|
1446
|
-
autoStoreCritical(`CI gate failed: ${project_dir}`, blocking_issues, project_dir, ['ci-gate']);
|
|
1447
|
-
return {
|
|
1448
|
-
content: [{
|
|
1449
|
-
type: 'text',
|
|
1450
|
-
text: JSON.stringify({
|
|
1451
|
-
verdict, exit_code,
|
|
1452
|
-
checks: {
|
|
1453
|
-
code_review: { score: codeScore, critical: codeResult.analysis?.critical_count ?? 0, high: codeResult.analysis?.high_count ?? 0 },
|
|
1454
|
-
security: { score: secScore, critical: secResult.analysis?.critical_count ?? 0, high: secResult.analysis?.high_count ?? 0 },
|
|
1455
|
-
secrets: { clean: secretsClean, findings: secretsResult.analysis?.findings ?? [] },
|
|
1456
|
-
},
|
|
1457
|
-
blocking_issues,
|
|
1458
|
-
ci_summary,
|
|
1459
|
-
duration_ms: Date.now() - start,
|
|
1460
|
-
}, null, 2),
|
|
1461
|
-
}],
|
|
1462
|
-
};
|
|
1463
|
-
}
|
|
1464
|
-
case 'veto_pr_review': {
|
|
1465
|
-
const args = (request.params.arguments || {});
|
|
1466
|
-
const pr_url = String(args?.pr_url ?? '').trim();
|
|
1467
|
-
const context = args?.context ? String(args.context) : '';
|
|
1468
|
-
const fail_on = args?.fail_on === 'warn' ? 'warn' : 'fail';
|
|
1469
|
-
if (!pr_url) {
|
|
1470
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'pr_url is required.' }) }], isError: true };
|
|
1471
|
-
}
|
|
1472
|
-
const start = Date.now();
|
|
1473
|
-
const fetched = await fetchPrDiff(pr_url);
|
|
1474
|
-
if (!fetched.ok) {
|
|
1475
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: fetched.error }) }], isError: true };
|
|
1476
|
-
}
|
|
1477
|
-
const { diff, meta } = fetched;
|
|
1478
|
-
const prContext = [
|
|
1479
|
-
`PR: ${meta.title} (${meta.html_url})`,
|
|
1480
|
-
`Author: ${meta.author} · ${meta.head_branch} → ${meta.base_branch}`,
|
|
1481
|
-
`Changes: +${meta.additions} -${meta.deletions} across ${meta.changed_files} files`,
|
|
1482
|
-
context,
|
|
1483
|
-
].filter(Boolean).join('\n');
|
|
1484
|
-
const scanResult = await runTripleScan(diff, prContext);
|
|
1485
|
-
if ('mode' in scanResult)
|
|
1486
|
-
return { content: [{ type: 'text', text: JSON.stringify(scanResult, null, 2) }] };
|
|
1487
|
-
const { reviewResult, secResult, secretsResult, verdict } = scanResult;
|
|
1488
|
-
const exit_code = verdict === 'fail' || (verdict === 'warn' && fail_on === 'warn') ? 1 : 0;
|
|
1489
|
-
const codeScore = reviewResult.analysis?.score ?? Math.round((reviewResult.output?.confidence ?? 0.8) * 100);
|
|
1490
|
-
const secScore = secResult.analysis?.score ?? Math.round((secResult.output?.confidence ?? 0.8) * 100);
|
|
1491
|
-
const secretsClean = (secretsResult.analysis?.findings?.length ?? 0) === 0;
|
|
1492
|
-
const blocking_issues = [];
|
|
1493
|
-
if ((reviewResult.analysis?.critical_count ?? 0) > 0)
|
|
1494
|
-
blocking_issues.push(`Code review: ${reviewResult.analysis?.summary ?? 'critical issues found'}`);
|
|
1495
|
-
if ((secResult.analysis?.critical_count ?? 0) > 0)
|
|
1496
|
-
blocking_issues.push(`Security: ${secResult.analysis?.summary ?? 'vulnerabilities detected'}`);
|
|
1497
|
-
if (!secretsClean)
|
|
1498
|
-
blocking_issues.push(`Secrets: ${secretsResult.analysis?.summary ?? 'exposed credentials detected'}`);
|
|
1499
|
-
// Build ready-to-post GitHub review comment (Markdown)
|
|
1500
|
-
const icon = verdict === 'pass' ? '✅' : verdict === 'warn' ? '⚠️' : '❌';
|
|
1501
|
-
const review_comment = [
|
|
1502
|
-
`## ${icon} Veto Review — ${verdict.toUpperCase()}`,
|
|
1503
|
-
``,
|
|
1504
|
-
`| Check | Score | Status |`,
|
|
1505
|
-
`|---|---|---|`,
|
|
1506
|
-
`| Code Review | ${codeScore}% | ${(reviewResult.analysis?.critical_count ?? 0) === 0 ? '✅' : '❌'} |`,
|
|
1507
|
-
`| Security Scan | ${secScore}% | ${(secResult.analysis?.critical_count ?? 0) === 0 ? '✅' : '❌'} |`,
|
|
1508
|
-
`| Secrets Scan | — | ${secretsClean ? '✅ Clean' : '❌ Found'} |`,
|
|
1509
|
-
``,
|
|
1510
|
-
blocking_issues.length > 0
|
|
1511
|
-
? `**Blocking issues:**\n${blocking_issues.map(i => `- ${i}`).join('\n')}`
|
|
1512
|
-
: `No blocking issues found.`,
|
|
1513
|
-
``,
|
|
1514
|
-
`> Reviewed by [Veto](https://github.com/jigyasudham/veto) · ${meta.changed_files} files · +${meta.additions}/-${meta.deletions} · ${Date.now() - start}ms`,
|
|
1515
|
-
].join('\n');
|
|
1516
|
-
autoStoreCritical(`PR review failed: ${meta.title}`, blocking_issues, undefined, ['pr-review']);
|
|
1517
|
-
return {
|
|
1518
|
-
content: [{
|
|
1519
|
-
type: 'text',
|
|
1520
|
-
text: JSON.stringify({
|
|
1521
|
-
verdict, exit_code,
|
|
1522
|
-
pr: { title: meta.title, author: meta.author, url: meta.html_url, base: meta.base_branch, head: meta.head_branch, additions: meta.additions, deletions: meta.deletions, changed_files: meta.changed_files },
|
|
1523
|
-
checks: {
|
|
1524
|
-
code_review: { score: codeScore, critical: reviewResult.analysis?.critical_count ?? 0, high: reviewResult.analysis?.high_count ?? 0 },
|
|
1525
|
-
security: { score: secScore, critical: secResult.analysis?.critical_count ?? 0, high: secResult.analysis?.high_count ?? 0 },
|
|
1526
|
-
secrets: { clean: secretsClean, findings: secretsResult.analysis?.findings ?? [] },
|
|
1527
|
-
},
|
|
1528
|
-
blocking_issues,
|
|
1529
|
-
review_comment,
|
|
1530
|
-
duration_ms: Date.now() - start,
|
|
1531
|
-
}, null, 2),
|
|
1532
|
-
}],
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
// ── Phase 16: Workspace Discovery & Summarization ─────────────────────────
|
|
1536
|
-
case 'veto_discover': {
|
|
1537
|
-
const args = (request.params.arguments || {});
|
|
1538
|
-
const discoverDir = String(args?.project_dir ?? '').trim();
|
|
1539
|
-
const discoverDepth = (['quick', 'standard', 'full'].includes(String(args?.depth ?? '')))
|
|
1540
|
-
? String(args.depth)
|
|
1541
|
-
: 'standard';
|
|
1542
|
-
const discoverStore = args?.store !== false;
|
|
1543
|
-
if (!discoverDir) {
|
|
1544
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
1545
|
-
}
|
|
1546
|
-
try {
|
|
1547
|
-
statSync(discoverDir);
|
|
1548
|
-
}
|
|
1549
|
-
catch {
|
|
1550
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Directory not found: ${discoverDir}` }) }], isError: true };
|
|
1551
|
-
}
|
|
1552
|
-
const result = discoverProject(discoverDir, discoverDepth);
|
|
1553
|
-
// Build live repo-map for 'full' depth or when explicitly requested
|
|
1554
|
-
let repoMap = null;
|
|
1555
|
-
if (discoverDepth === 'full' || args?.include_repo_map === true) {
|
|
1556
|
-
try {
|
|
1557
|
-
repoMap = buildRepoMap({ projectDir: discoverDir, maxTopModules: 20 });
|
|
1558
|
-
}
|
|
1559
|
-
catch { /* non-fatal */ }
|
|
1560
|
-
}
|
|
1561
|
-
if (discoverStore) {
|
|
1562
|
-
updateProjectMap({
|
|
1563
|
-
project_dir: result.project_dir,
|
|
1564
|
-
structure: {
|
|
1565
|
-
ecosystems: result.ecosystems,
|
|
1566
|
-
key_files: result.key_files,
|
|
1567
|
-
file_count_by_ext: result.file_counts,
|
|
1568
|
-
total_files: result.total_files,
|
|
1569
|
-
scanned_at: result.scanned_at,
|
|
1570
|
-
...(repoMap ? { top_modules: repoMap.top_modules.slice(0, 10).map(m => ({ file: m.file, rank: m.rank, exports: m.symbols.slice(0, 4).map(s => s.name) })) } : {}),
|
|
1571
|
-
},
|
|
1572
|
-
key_modules: result.key_files,
|
|
1573
|
-
tech_stack: result.tech_stack,
|
|
1574
|
-
});
|
|
1575
|
-
storeKnowledge({
|
|
1576
|
-
type: 'solution',
|
|
1577
|
-
title: `Project discovery: ${result.project_dir}`,
|
|
1578
|
-
content: `Stack: ${result.tech_stack.join(', ') || 'unknown'}. Branch: ${result.git.branch || 'none'}. Commit: ${result.git.commit || 'none'}. Files: ${result.total_files}. Ecosystems: ${Object.keys(result.ecosystems).join(', ') || 'none'}. Key files: ${result.key_files.join(', ')}.`,
|
|
1579
|
-
tags: ['discover', ...result.tech_stack.map(t => t.toLowerCase().replace(/[^a-z0-9]/g, ''))],
|
|
1580
|
-
project_dir: result.project_dir,
|
|
1581
|
-
});
|
|
1582
|
-
}
|
|
1583
|
-
const discoverPayload = { success: true, stored: discoverStore, ...result };
|
|
1584
|
-
if (repoMap) {
|
|
1585
|
-
discoverPayload.repo_map = {
|
|
1586
|
-
total_files: repoMap.total_files,
|
|
1587
|
-
symbol_count: repoMap.symbol_count,
|
|
1588
|
-
top_modules: repoMap.top_modules.slice(0, 15),
|
|
1589
|
-
dep_graph: repoMap.dep_graph,
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
return { content: [{ type: 'text', text: JSON.stringify(discoverPayload, null, 2) }] };
|
|
1593
|
-
}
|
|
1594
|
-
case 'veto_summarize': {
|
|
1595
|
-
const args = (request.params.arguments || {});
|
|
1596
|
-
return await handleAgenticWorker('veto_summarize', args, 'documentation', 'Summarize this project/file.');
|
|
1597
|
-
}
|
|
1598
|
-
case 'veto_benchmark': {
|
|
1599
|
-
const args = (request.params.arguments || {});
|
|
1600
|
-
const task = String(args?.task ?? '');
|
|
1601
|
-
const approachA = String(args?.approach_a ?? '');
|
|
1602
|
-
const approachB = String(args?.approach_b ?? '');
|
|
1603
|
-
const ctx = args?.context ? String(args.context) : undefined;
|
|
1604
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1605
|
-
if (!task || !approachA || !approachB) {
|
|
1606
|
-
throw new Error('veto_benchmark requires task, approach_a, and approach_b');
|
|
1607
|
-
}
|
|
1608
|
-
const bmStart = Date.now();
|
|
1609
|
-
// Run both debates in parallel via LLM council
|
|
1610
|
-
const [debateA, debateB] = await Promise.all([
|
|
1611
|
-
runLlmDebate(server, { task: `${task}\n\nApproach A: ${approachA}`, context: ctx, project_dir: projectDir }),
|
|
1612
|
-
runLlmDebate(server, { task: `${task}\n\nApproach B: ${approachB}`, context: ctx, project_dir: projectDir }),
|
|
1613
|
-
]);
|
|
1614
|
-
// Score: GREEN=3, YELLOW=2, RED=1, DEADLOCK=0
|
|
1615
|
-
const verdictScore = { GREEN: 3, YELLOW: 2, RED: 1, DEADLOCK: 0 };
|
|
1616
|
-
const scoreA = verdictScore[debateA.final_verdict] ?? 0;
|
|
1617
|
-
const scoreB = verdictScore[debateB.final_verdict] ?? 0;
|
|
1618
|
-
const warnCountA = debateA.warnings.length;
|
|
1619
|
-
const warnCountB = debateB.warnings.length;
|
|
1620
|
-
const blockCountA = debateA.block_reasons.length;
|
|
1621
|
-
const blockCountB = debateB.block_reasons.length;
|
|
1622
|
-
let winner;
|
|
1623
|
-
let confidence;
|
|
1624
|
-
let reasoning;
|
|
1625
|
-
if (scoreA !== scoreB) {
|
|
1626
|
-
winner = scoreA > scoreB ? 'A' : 'B';
|
|
1627
|
-
const diff = Math.abs(scoreA - scoreB);
|
|
1628
|
-
confidence = diff >= 2 ? 'high' : 'medium';
|
|
1629
|
-
reasoning = `Approach ${winner} received a ${winner === 'A' ? debateA.final_verdict : debateB.final_verdict} verdict vs ${winner === 'A' ? debateB.final_verdict : debateA.final_verdict} for Approach ${winner === 'A' ? 'B' : 'A'}.`;
|
|
1630
|
-
}
|
|
1631
|
-
else {
|
|
1632
|
-
// Same verdict — break tie on warnings, then block reasons
|
|
1633
|
-
if (warnCountA !== warnCountB) {
|
|
1634
|
-
winner = warnCountA < warnCountB ? 'A' : 'B';
|
|
1635
|
-
confidence = 'low';
|
|
1636
|
-
reasoning = `Both approaches received ${debateA.final_verdict}. Approach ${winner} had fewer warnings (${winner === 'A' ? warnCountA : warnCountB} vs ${winner === 'A' ? warnCountB : warnCountA}).`;
|
|
1637
|
-
}
|
|
1638
|
-
else if (blockCountA !== blockCountB) {
|
|
1639
|
-
winner = blockCountA < blockCountB ? 'A' : 'B';
|
|
1640
|
-
confidence = 'low';
|
|
1641
|
-
reasoning = `Both approaches received ${debateA.final_verdict} with equal warnings. Approach ${winner} had fewer blocking concerns.`;
|
|
1642
|
-
}
|
|
1643
|
-
else {
|
|
1644
|
-
winner = 'TIE';
|
|
1645
|
-
confidence = 'low';
|
|
1646
|
-
reasoning = `Both approaches received ${debateA.final_verdict} with equal warnings and blocks. Council cannot differentiate — consider a more specific framing.`;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
// Auto-record both debates
|
|
1650
|
-
const qMap = { GREEN: 90, YELLOW: 60, RED: 20, DEADLOCK: 50 };
|
|
1651
|
-
recordOutcome('benchmark', 50, 2, 'council', qMap[debateA.final_verdict] ?? 50);
|
|
1652
|
-
recordOutcome('benchmark', 50, 2, 'council', qMap[debateB.final_verdict] ?? 50);
|
|
1653
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1654
|
-
winner,
|
|
1655
|
-
confidence,
|
|
1656
|
-
reasoning,
|
|
1657
|
-
recommendation: winner !== 'TIE'
|
|
1658
|
-
? `Use Approach ${winner}. ${winner === 'A' ? debateA.recommended : debateB.recommended}`
|
|
1659
|
-
: `No clear winner. ${debateA.recommended}`,
|
|
1660
|
-
approach_a: {
|
|
1661
|
-
label: 'A',
|
|
1662
|
-
description: approachA.slice(0, 120),
|
|
1663
|
-
verdict: debateA.final_verdict,
|
|
1664
|
-
warnings: warnCountA,
|
|
1665
|
-
block_reasons: blockCountA,
|
|
1666
|
-
recommended: debateA.recommended,
|
|
1667
|
-
votes: Object.fromEntries(Object.entries(debateA.votes).map(([k, v]) => [k, v.verdict])),
|
|
1668
|
-
},
|
|
1669
|
-
approach_b: {
|
|
1670
|
-
label: 'B',
|
|
1671
|
-
description: approachB.slice(0, 120),
|
|
1672
|
-
verdict: debateB.final_verdict,
|
|
1673
|
-
warnings: warnCountB,
|
|
1674
|
-
block_reasons: blockCountB,
|
|
1675
|
-
recommended: debateB.recommended,
|
|
1676
|
-
votes: Object.fromEntries(Object.entries(debateB.votes).map(([k, v]) => [k, v.verdict])),
|
|
1677
|
-
},
|
|
1678
|
-
duration_ms: Date.now() - bmStart,
|
|
1679
|
-
}, null, 2) }] };
|
|
1680
|
-
}
|
|
1681
|
-
// ── Part 4: New Features ───────────────────────────────────────────────────
|
|
1682
|
-
case 'veto_metrics': {
|
|
1683
|
-
const args = (request.params.arguments || {});
|
|
1684
|
-
const metrics = getMetrics();
|
|
1685
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...metrics }, null, 2) }] };
|
|
1686
|
-
}
|
|
1687
|
-
case 'veto_git_blame': {
|
|
1688
|
-
const args = (request.params.arguments || {});
|
|
1689
|
-
const blameDir = args?.project_dir ? String(args.project_dir).trim() : '';
|
|
1690
|
-
const blameFile = args?.file_path ? String(args.file_path).trim() : '';
|
|
1691
|
-
const blameTarget = blameFile || blameDir;
|
|
1692
|
-
if (!blameTarget) {
|
|
1693
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'Provide project_dir or file_path.' }) }], isError: true };
|
|
1694
|
-
}
|
|
1695
|
-
const resolvedTarget = resolve(blameTarget);
|
|
1696
|
-
try {
|
|
1697
|
-
statSync(resolvedTarget);
|
|
1698
|
-
}
|
|
1699
|
-
catch {
|
|
1700
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Path not found: ${resolvedTarget}` }) }], isError: true };
|
|
1701
|
-
}
|
|
1702
|
-
function gitExec(cmd, cwd) {
|
|
1703
|
-
try {
|
|
1704
|
-
return execSyncTop(cmd, { cwd, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
1705
|
-
}
|
|
1706
|
-
catch {
|
|
1707
|
-
return '';
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
const cwd = statSync(resolvedTarget).isDirectory() ? resolvedTarget : dirname(resolvedTarget);
|
|
1711
|
-
const shortlog = gitExec(`git shortlog -sn -- "${resolvedTarget}"`, cwd);
|
|
1712
|
-
const contributors = shortlog.split('\n').filter(Boolean).map(line => {
|
|
1713
|
-
const m = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
1714
|
-
return m ? { commits: parseInt(m[1], 10), author: m[2].trim() } : null;
|
|
1715
|
-
}).filter(Boolean);
|
|
1716
|
-
const lastModified = gitExec(`git log -1 --format="%ai|%aN|%s" -- "${resolvedTarget}"`, cwd);
|
|
1717
|
-
const [last_modified_at, last_author, last_commit_message] = lastModified.split('|');
|
|
1718
|
-
const totalCommits = gitExec(`git rev-list --count HEAD -- "${resolvedTarget}"`, cwd);
|
|
1719
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1720
|
-
success: true,
|
|
1721
|
-
path: resolvedTarget,
|
|
1722
|
-
total_commits: parseInt(totalCommits || '0', 10),
|
|
1723
|
-
contributors,
|
|
1724
|
-
last_modified_at: last_modified_at?.trim(),
|
|
1725
|
-
last_author: last_author?.trim(),
|
|
1726
|
-
last_commit_message: last_commit_message?.trim(),
|
|
1727
|
-
}, null, 2) }] };
|
|
1728
|
-
}
|
|
1729
|
-
case 'veto_changelog': {
|
|
1730
|
-
const args = (request.params.arguments || {});
|
|
1731
|
-
const changelogDir = args?.project_dir ? String(args.project_dir).trim() : activeProjectDir ?? process.cwd();
|
|
1732
|
-
const maxEntries = typeof args?.max_entries === 'number' ? Math.min(args.max_entries, 200) : 50;
|
|
1733
|
-
const resolvedDir = resolve(changelogDir);
|
|
1734
|
-
try {
|
|
1735
|
-
statSync(resolvedDir);
|
|
1736
|
-
}
|
|
1737
|
-
catch {
|
|
1738
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Directory not found: ${resolvedDir}` }) }], isError: true };
|
|
1739
|
-
}
|
|
1740
|
-
function gitRun(cmd) {
|
|
1741
|
-
try {
|
|
1742
|
-
return execSyncTop(cmd, { cwd: resolvedDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
1743
|
-
}
|
|
1744
|
-
catch {
|
|
1745
|
-
return '';
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
const lastTag = gitRun('git describe --tags --abbrev=0 2>/dev/null') || '';
|
|
1749
|
-
const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
|
|
1750
|
-
const rawLog = gitRun(`git log ${range} --format="%s|||%H|||%aN|||%ai" --no-merges -n ${maxEntries}`);
|
|
1751
|
-
if (!rawLog) {
|
|
1752
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, since_tag: lastTag || 'beginning', entries: [], message: 'No commits found in range.' }) }] };
|
|
1753
|
-
}
|
|
1754
|
-
const typeLabels = {
|
|
1755
|
-
feat: 'Features', fix: 'Bug Fixes', refactor: 'Refactoring', perf: 'Performance',
|
|
1756
|
-
docs: 'Documentation', test: 'Tests', chore: 'Chores', ci: 'CI/CD',
|
|
1757
|
-
style: 'Style', build: 'Build', revert: 'Reverts',
|
|
1758
|
-
};
|
|
1759
|
-
const grouped = {};
|
|
1760
|
-
for (const line of rawLog.split('\n').filter(Boolean)) {
|
|
1761
|
-
const [subject, hash, author, date] = line.split('|||');
|
|
1762
|
-
if (!subject)
|
|
1763
|
-
continue;
|
|
1764
|
-
const typeMatch = subject.match(/^(\w+)(\([\w-]+\))?:\s*(.*)/);
|
|
1765
|
-
const type = typeMatch ? typeMatch[1].toLowerCase() : 'other';
|
|
1766
|
-
const msg = typeMatch ? typeMatch[3] : subject;
|
|
1767
|
-
const label = typeLabels[type] ?? 'Other';
|
|
1768
|
-
if (!grouped[label])
|
|
1769
|
-
grouped[label] = [];
|
|
1770
|
-
grouped[label].push({ message: msg.trim(), hash: hash?.trim().slice(0, 8) ?? '', author: author?.trim() ?? '', date: date?.trim().slice(0, 10) ?? '' });
|
|
1771
|
-
}
|
|
1772
|
-
const sections = Object.entries(grouped).map(([section, items]) => ({ section, items }));
|
|
1773
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1774
|
-
success: true,
|
|
1775
|
-
since_tag: lastTag || '(beginning of history)',
|
|
1776
|
-
total_commits: rawLog.split('\n').filter(Boolean).length,
|
|
1777
|
-
sections,
|
|
1778
|
-
}, null, 2) }] };
|
|
1779
|
-
}
|
|
1780
|
-
// ── Named Pipelines (Phase 4.2) ──────────────────────────────────────────────
|
|
1781
|
-
case 'veto_full_review': {
|
|
1782
|
-
const args = (request.params.arguments || {});
|
|
1783
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1784
|
-
const userContext = args?.context ? String(args.context) : undefined;
|
|
1785
|
-
let diff = args?.diff ? String(args.diff).trim() : '';
|
|
1786
|
-
if (!diff && projectDir) {
|
|
1787
|
-
try {
|
|
1788
|
-
diff = execSyncTop('git diff HEAD --no-color', { cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
1789
|
-
if (!diff)
|
|
1790
|
-
diff = execSyncTop('git diff --cached --no-color', { cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
1791
|
-
}
|
|
1792
|
-
catch { /* ignore */ }
|
|
1793
|
-
}
|
|
1794
|
-
if (!diff)
|
|
1795
|
-
return { content: [{ type: 'text', text: 'No diff provided and git diff failed. Provide a diff or project_dir.' }], isError: true };
|
|
1796
|
-
const changedFiles = [...diff.matchAll(/^diff --git a\/.+ b\/(.+)$/gm)].map(m => m[1]);
|
|
1797
|
-
const context = buildContextString(projectDir, userContext);
|
|
1798
|
-
const [scanResult, qualityResult] = await Promise.all([
|
|
1799
|
-
runTripleScan(diff, context),
|
|
1800
|
-
executeOne({ id: 'quality-1', agent: 'code-quality', task: 'Assess overall code quality and maintainability of these changes', code: diff.slice(0, 8000), context }),
|
|
1801
|
-
]);
|
|
1802
|
-
if ('mode' in scanResult)
|
|
1803
|
-
return { content: [{ type: 'text', text: JSON.stringify(scanResult, null, 2) }] };
|
|
1804
|
-
const { reviewResult, secResult, secretsResult, verdict: scanVerdict } = scanResult;
|
|
1805
|
-
const qualityScore = qualityResult.analysis?.score ?? Math.round(qualityResult.output.confidence * 100);
|
|
1806
|
-
const verdict = (scanVerdict === 'fail' || qualityScore < 40) ? 'fail'
|
|
1807
|
-
: (scanVerdict === 'warn' || qualityScore < 70) ? 'warn' : 'pass';
|
|
1808
|
-
const issues = [];
|
|
1809
|
-
if (verdict === 'fail') {
|
|
1810
|
-
if ((reviewResult.analysis?.critical_count ?? 0) > 0)
|
|
1811
|
-
issues.push(`Code: ${reviewResult.analysis?.summary ?? 'critical issues found'}`);
|
|
1812
|
-
if ((secResult.analysis?.critical_count ?? 0) > 0)
|
|
1813
|
-
issues.push(`Security: ${secResult.analysis?.summary ?? 'vulnerabilities detected'}`);
|
|
1814
|
-
if ((secretsResult.analysis?.findings?.length ?? 0) > 0)
|
|
1815
|
-
issues.push('Secrets: exposed credentials detected');
|
|
1816
|
-
if (qualityScore < 40)
|
|
1817
|
-
issues.push(`Quality: Score ${qualityScore}/100 is below the critical threshold`);
|
|
1818
|
-
}
|
|
1819
|
-
if (issues.length > 0) {
|
|
1820
|
-
autoStoreCritical(`Full review failed: ${changedFiles.slice(0, 2).join(', ')}`, issues, projectDir, ['full-review']);
|
|
1821
|
-
}
|
|
1822
|
-
recordOutcome('full-review', 50, 2, 'code-quality', qualityScore);
|
|
1823
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1824
|
-
verdict,
|
|
1825
|
-
score: qualityScore,
|
|
1826
|
-
scans: {
|
|
1827
|
-
code_review: { score: reviewResult.analysis?.score ?? null, verdict: reviewResult.analysis?.verdict ?? null, critical: reviewResult.analysis?.critical_count ?? 0, high: reviewResult.analysis?.high_count ?? 0, findings: reviewResult.analysis?.findings ?? [] },
|
|
1828
|
-
security: { score: secResult.analysis?.score ?? null, verdict: secResult.analysis?.verdict ?? null, critical: secResult.analysis?.critical_count ?? 0, high: secResult.analysis?.high_count ?? 0, findings: secResult.analysis?.findings ?? [] },
|
|
1829
|
-
secrets: { verdict: secretsResult.analysis?.verdict ?? null, findings: secretsResult.analysis?.findings ?? [] },
|
|
1830
|
-
},
|
|
1831
|
-
findings: [
|
|
1832
|
-
`Quality: ${qualityScore}/100 (${verdict})`,
|
|
1833
|
-
`Code: ${reviewResult.analysis?.verdict ?? 'n/a'} (score ${reviewResult.analysis?.score ?? '?'}/100)`,
|
|
1834
|
-
`Security: ${secResult.analysis?.verdict ?? 'n/a'} — ${secResult.analysis?.critical_count ?? 0} critical, ${secResult.analysis?.high_count ?? 0} high`,
|
|
1835
|
-
`Secrets: ${(secretsResult.analysis?.findings?.length ?? 0) > 0 ? '🔴 Exposed credentials detected' : '✅ Clean'}`,
|
|
1836
|
-
],
|
|
1837
|
-
files_changed: changedFiles,
|
|
1838
|
-
}, null, 2) }] };
|
|
1839
|
-
}
|
|
1840
|
-
case 'veto_pre_commit': {
|
|
1841
|
-
const args = (request.params.arguments || {});
|
|
1842
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1843
|
-
const userContext = args?.context ? String(args.context) : undefined;
|
|
1844
|
-
if (!projectDir)
|
|
1845
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
1846
|
-
let diff = '';
|
|
1847
|
-
try {
|
|
1848
|
-
diff = execSyncTop('git diff --cached --no-color', { cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
1849
|
-
}
|
|
1850
|
-
catch { /* not a git repo */ }
|
|
1851
|
-
if (!diff)
|
|
1852
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'No staged changes found. Stage files with git add before running veto_pre_commit.' }) }], isError: true };
|
|
1853
|
-
const context = buildContextString(projectDir, userContext);
|
|
1854
|
-
const [secretsResult, reviewResult] = await Promise.all([
|
|
1855
|
-
executeOne({ id: 'pre-secrets', agent: 'secrets', task: 'Scan staged changes for exposed secrets or credentials', code: diff }),
|
|
1856
|
-
executeOne({ id: 'pre-review', agent: 'reviewer', task: 'Review staged changes for critical code quality issues', code: diff, context }),
|
|
1857
|
-
]);
|
|
1858
|
-
const hasSecrets = (secretsResult.analysis?.findings?.length ?? 0) > 0;
|
|
1859
|
-
const hasCriticalCode = (reviewResult.analysis?.critical_count ?? 0) > 0;
|
|
1860
|
-
const verdict = (hasSecrets || hasCriticalCode) ? 'fail' : (reviewResult.analysis?.high_count ?? 0) > 0 ? 'warn' : 'pass';
|
|
1861
|
-
const verdictEmoji = verdict === 'pass' ? '✅ PASS' : verdict === 'warn' ? '⚠️ WARN' : '❌ FAIL';
|
|
1862
|
-
if (verdict === 'fail') {
|
|
1863
|
-
const issues = [];
|
|
1864
|
-
if (hasSecrets)
|
|
1865
|
-
issues.push('Secrets: exposed credentials detected');
|
|
1866
|
-
if (hasCriticalCode)
|
|
1867
|
-
issues.push(`Code: ${reviewResult.analysis?.summary ?? 'critical issues found'}`);
|
|
1868
|
-
autoStoreCritical(`Pre-commit blocked: ${projectDir}`, issues, projectDir, ['pre-commit']);
|
|
1869
|
-
}
|
|
1870
|
-
recordOutcome('pre-commit', 50, 2, 'secrets', hasSecrets ? 0 : 100);
|
|
1871
|
-
recordOutcome('pre-commit', 50, 2, 'reviewer', reviewResult.analysis?.score ?? Math.round(reviewResult.output.confidence * 100));
|
|
1872
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1873
|
-
pipeline: 'pre_commit',
|
|
1874
|
-
verdict,
|
|
1875
|
-
verdict_label: verdictEmoji,
|
|
1876
|
-
blocked: verdict === 'fail',
|
|
1877
|
-
secrets: { found: hasSecrets, findings: secretsResult.analysis?.findings ?? [] },
|
|
1878
|
-
code_review: { score: reviewResult.analysis?.score ?? null, critical: reviewResult.analysis?.critical_count ?? 0, high: reviewResult.analysis?.high_count ?? 0, findings: reviewResult.analysis?.findings ?? [] },
|
|
1879
|
-
summary: [
|
|
1880
|
-
`${verdictEmoji} — Pre-commit check`,
|
|
1881
|
-
`Secrets: ${hasSecrets ? '🔴 Found — commit BLOCKED' : '✅ Clean'}`,
|
|
1882
|
-
`Code: ${reviewResult.analysis?.verdict ?? 'n/a'} — ${reviewResult.analysis?.critical_count ?? 0} critical, ${reviewResult.analysis?.high_count ?? 0} high`,
|
|
1883
|
-
].join('\n'),
|
|
1884
|
-
}, null, 2) }] };
|
|
1885
|
-
}
|
|
1886
|
-
case 'veto_new_feature': {
|
|
1887
|
-
const args = (request.params.arguments || {});
|
|
1888
|
-
const description = String(args?.description ?? '').trim();
|
|
1889
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1890
|
-
const userContext = args?.context ? String(args.context) : undefined;
|
|
1891
|
-
const agentResponses = args?.agent_responses;
|
|
1892
|
-
if (!description)
|
|
1893
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'description is required.' }) }], isError: true };
|
|
1894
|
-
// Step 1: Governance
|
|
1895
|
-
const debateInput = { task: description, context: userContext, project_dir: projectDir, strictness: 'standard' };
|
|
1896
|
-
let debateResult;
|
|
1897
|
-
if (agentResponses?.council) {
|
|
1898
|
-
debateResult = runFromAgentResponses(debateInput, parseAgentResponses(JSON.stringify(agentResponses.council), description));
|
|
1899
|
-
}
|
|
1900
|
-
else {
|
|
1901
|
-
debateResult = await runLlmDebate(server, debateInput);
|
|
1902
|
-
}
|
|
1903
|
-
if (debateResult.final_verdict === 'RED') {
|
|
1904
|
-
return { content: [{ type: 'text', text: JSON.stringify({ pipeline: 'new_feature', verdict: 'blocked', council: debateResult }, null, 2) }] };
|
|
1905
|
-
}
|
|
1906
|
-
// Step 2: Planning
|
|
1907
|
-
let planResult;
|
|
1908
|
-
if (agentResponses?.planner) {
|
|
1909
|
-
planResult = parseAgenticAgentResponses([{ id: 'planner', agent: 'task-planner', task: description }], { planner: agentResponses.planner })[0];
|
|
1910
|
-
}
|
|
1911
|
-
else {
|
|
1912
|
-
planResult = await executeOne({ id: 'planner', agent: 'task-planner', task: description, project_dir: projectDir, llm_backed: true });
|
|
1913
|
-
}
|
|
1914
|
-
if (planResult.llm_upgrade || debateResult.llm_upgrade) {
|
|
1915
|
-
return {
|
|
1916
|
-
content: [{
|
|
1917
|
-
type: 'text',
|
|
1918
|
-
text: JSON.stringify({
|
|
1919
|
-
llm_backed: false,
|
|
1920
|
-
llm_upgrade: {
|
|
1921
|
-
council: debateResult.llm_upgrade,
|
|
1922
|
-
planner: planResult.llm_upgrade,
|
|
1923
|
-
}
|
|
1924
|
-
}, null, 2)
|
|
1925
|
-
}]
|
|
1926
|
-
};
|
|
1927
|
-
}
|
|
1928
|
-
// Step 3: Tasks
|
|
1929
|
-
const tasks = parsePrdIntoTasks(description, planResult.plan, 10);
|
|
1930
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, pipeline: 'new_feature', council: debateResult, plan: planResult.plan, tasks }, null, 2) }] };
|
|
1931
|
-
}
|
|
1932
|
-
case 'veto_delegate': {
|
|
1933
|
-
const args = (request.params.arguments || {});
|
|
1934
|
-
const agentId = String(args?.agent_id ?? '').trim();
|
|
1935
|
-
const task = String(args?.task ?? '').trim();
|
|
1936
|
-
const context = args?.context ? String(args.context) : undefined;
|
|
1937
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1938
|
-
const maxLen = typeof args?.max_summary_tokens === 'number' ? Math.min(args.max_summary_tokens, 2000) : 500;
|
|
1939
|
-
if (!agentId || !task) {
|
|
1940
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'agent_id and task are required.' }) }], isError: true };
|
|
1941
|
-
}
|
|
1942
|
-
const enrichedCtx = buildContextString(projectDir, context);
|
|
1943
|
-
const result = await executeOne({ id: 'delegate-1', agent: agentId, task, context: enrichedCtx || undefined, project_dir: projectDir });
|
|
1944
|
-
recordOutcome(task, 50, 2, agentId, Math.round(result.output.confidence * 100));
|
|
1945
|
-
// Return compact summary only — no verbose findings/steps to avoid context pollution
|
|
1946
|
-
const summary = [
|
|
1947
|
-
result.output.recommendation,
|
|
1948
|
-
result.plan?.approach,
|
|
1949
|
-
result.analysis?.verdict ? `Verdict: ${result.analysis.verdict}` : null,
|
|
1950
|
-
result.analysis?.score ? `Score: ${result.analysis.score}/100` : null,
|
|
1951
|
-
].filter(Boolean).join('\n').slice(0, maxLen);
|
|
1952
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1953
|
-
agent: agentId,
|
|
1954
|
-
task: task.slice(0, 100),
|
|
1955
|
-
summary,
|
|
1956
|
-
confidence: Math.round(result.output.confidence * 100),
|
|
1957
|
-
severity: result.output.severity ?? null,
|
|
1958
|
-
duration_ms: result.duration_ms,
|
|
1959
|
-
truncated: summary.length >= maxLen,
|
|
1960
|
-
}, null, 2) }] };
|
|
1961
|
-
}
|
|
1962
|
-
case 'veto_commit_message': {
|
|
1963
|
-
const args = (request.params.arguments || {});
|
|
1964
|
-
const projectDir = String(args?.project_dir ?? '').trim();
|
|
1965
|
-
const hint = args?.hint ? String(args.hint) : undefined;
|
|
1966
|
-
if (!projectDir)
|
|
1967
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
1968
|
-
let diff = '';
|
|
1969
|
-
try {
|
|
1970
|
-
diff = execSyncTop('git diff --cached --no-color', { cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
1971
|
-
}
|
|
1972
|
-
catch { /* not a git repo */ }
|
|
1973
|
-
if (!diff)
|
|
1974
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'No staged changes. Run git add first.' }) }], isError: true };
|
|
1975
|
-
const truncatedDiff = diff.slice(0, 6000);
|
|
1976
|
-
const result = await executeOne({
|
|
1977
|
-
id: 'commit-msg-1',
|
|
1978
|
-
agent: 'git-agent',
|
|
1979
|
-
task: 'Generate a conventional commit message for these staged changes. Follow the Conventional Commits spec: type(scope): subject\n\nbody. Types: feat/fix/docs/chore/refactor/test/perf/ci/build/style. Be concise. Subject ≤ 72 chars.',
|
|
1980
|
-
code: truncatedDiff,
|
|
1981
|
-
context: hint,
|
|
1982
|
-
});
|
|
1983
|
-
recordOutcome('commit-message', 50, 2, 'git-agent', Math.round(result.output.confidence * 100));
|
|
1984
|
-
const message = (result.plan?.approach ?? result.output.recommendation ?? '').trim();
|
|
1985
|
-
const firstLine = message.split('\n')[0] ?? '';
|
|
1986
|
-
const match = firstLine.match(/^(\w+)(?:\(([^)]+)\))?!?:\s*(.+)/);
|
|
1987
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
1988
|
-
message,
|
|
1989
|
-
type: match ? match[1] : null,
|
|
1990
|
-
scope: match ? (match[2] ?? null) : null,
|
|
1991
|
-
subject: match ? match[3] : null,
|
|
1992
|
-
confidence: Math.round(result.output.confidence * 100),
|
|
1993
|
-
}, null, 2) }] };
|
|
1994
|
-
}
|
|
1995
|
-
case 'veto_pr_description': {
|
|
1996
|
-
const args = (request.params.arguments || {});
|
|
1997
|
-
const projectDir = String(args?.project_dir ?? '').trim();
|
|
1998
|
-
const baseBranch = args?.base_branch ? String(args.base_branch) : 'main';
|
|
1999
|
-
const titleHint = args?.title ? String(args.title) : undefined;
|
|
2000
|
-
const userContext = args?.context ? String(args.context) : undefined;
|
|
2001
|
-
if (!projectDir)
|
|
2002
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
2003
|
-
let stat = '';
|
|
2004
|
-
let commitLog = '';
|
|
2005
|
-
try {
|
|
2006
|
-
stat = execSyncTop(`git diff ${baseBranch}...HEAD --no-color --stat`, { cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
2007
|
-
commitLog = execSyncTop(`git log ${baseBranch}...HEAD --oneline`, { cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
2008
|
-
}
|
|
2009
|
-
catch (e) {
|
|
2010
|
-
if (!stat && !commitLog)
|
|
2011
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `git diff failed: ${e.message}` }) }], isError: true };
|
|
2012
|
-
}
|
|
2013
|
-
let fullDiff = '';
|
|
2014
|
-
try {
|
|
2015
|
-
fullDiff = execSyncTop(`git diff ${baseBranch}...HEAD --no-color`, { cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
2016
|
-
}
|
|
2017
|
-
catch { /* ignore */ }
|
|
2018
|
-
const contextParts = [];
|
|
2019
|
-
if (titleHint)
|
|
2020
|
-
contextParts.push(`PR Title: ${titleHint}`);
|
|
2021
|
-
if (userContext)
|
|
2022
|
-
contextParts.push(`Context: ${userContext}`);
|
|
2023
|
-
if (commitLog)
|
|
2024
|
-
contextParts.push(`Commits:\n${commitLog}`);
|
|
2025
|
-
if (stat)
|
|
2026
|
-
contextParts.push(`Diff stat:\n${stat}`);
|
|
2027
|
-
const builtContext = contextParts.join('\n\n');
|
|
2028
|
-
const result = await executeOne({
|
|
2029
|
-
id: 'pr-desc-1',
|
|
2030
|
-
agent: 'documentation',
|
|
2031
|
-
task: "Write a complete GitHub Pull Request description. Include: ## Summary (3–5 bullet points of what changed and why), ## Changes (file-level breakdown from the diff stat), ## Test Plan (bulleted checklist of how to verify the changes), ## Breaking Changes (any API or interface changes; say 'None' if clean). Be specific and developer-facing.",
|
|
2032
|
-
code: fullDiff.slice(0, 8000),
|
|
2033
|
-
context: builtContext || undefined,
|
|
2034
|
-
project_dir: projectDir,
|
|
2035
|
-
});
|
|
2036
|
-
const quality = Math.round(result.output.confidence * 100);
|
|
2037
|
-
recordOutcome('pr-description', 50, 2, 'documentation', quality);
|
|
2038
|
-
const body = (result.plan?.approach ?? result.output.recommendation ?? '').trim();
|
|
2039
|
-
const suggestedTitle = titleHint ?? (commitLog.split('\n')[0]?.replace(/^[a-f0-9]+ /, '') ?? 'Pull Request');
|
|
2040
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2041
|
-
title: suggestedTitle,
|
|
2042
|
-
body,
|
|
2043
|
-
base_branch: baseBranch,
|
|
2044
|
-
confidence: quality,
|
|
2045
|
-
}, null, 2) }] };
|
|
2046
|
-
}
|
|
2047
|
-
case 'veto_pr_post': {
|
|
2048
|
-
const args = (request.params.arguments || {});
|
|
2049
|
-
const m = String(args?.pr_url ?? '').match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
2050
|
-
if (!m)
|
|
2051
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'Invalid PR URL. Expected: https://github.com/owner/repo/pull/123' }) }], isError: true };
|
|
2052
|
-
const [, owner, repo, prNum] = m;
|
|
2053
|
-
if (!process.env.GITHUB_TOKEN)
|
|
2054
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'GITHUB_TOKEN env var not set' }) }], isError: true };
|
|
2055
|
-
const findings = Array.isArray(args?.findings) ? args.findings : [];
|
|
2056
|
-
const reviewBody = args?.body ? String(args.body) :
|
|
2057
|
-
`Veto review: ${findings.length} finding(s) — ${findings.filter(f => f.severity === 'critical' || f.severity === 'high').length} critical/high`;
|
|
2058
|
-
const eventVal = String(args?.event ?? '');
|
|
2059
|
-
const event = ['COMMENT', 'APPROVE', 'REQUEST_CHANGES'].includes(eventVal) ? eventVal : 'COMMENT';
|
|
2060
|
-
const comments = findings
|
|
2061
|
-
.filter(f => f.severity === 'critical' || f.severity === 'high')
|
|
2062
|
-
.slice(0, 20)
|
|
2063
|
-
.map(f => ({ body: `**[${f.severity.toUpperCase()}]** ${f.message}${f.location ? `\n\n_Location: ${f.location}_` : ''}` }));
|
|
2064
|
-
const prPostUrl = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNum}/reviews`;
|
|
2065
|
-
const prPostHeaders = {
|
|
2066
|
-
'User-Agent': 'veto-mcp-server',
|
|
2067
|
-
'Content-Type': 'application/json',
|
|
2068
|
-
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
|
|
2069
|
-
};
|
|
2070
|
-
const prPostResp = await fetch(prPostUrl, {
|
|
2071
|
-
method: 'POST',
|
|
2072
|
-
headers: prPostHeaders,
|
|
2073
|
-
body: JSON.stringify({ body: reviewBody, event, comments }),
|
|
2074
|
-
});
|
|
2075
|
-
if (!prPostResp.ok) {
|
|
2076
|
-
const err = await prPostResp.text();
|
|
2077
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `GitHub API error ${prPostResp.status}: ${err.slice(0, 200)}` }) }], isError: true };
|
|
2078
|
-
}
|
|
2079
|
-
const review = await prPostResp.json();
|
|
2080
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2081
|
-
success: true,
|
|
2082
|
-
review_id: review.id,
|
|
2083
|
-
review_url: review.html_url,
|
|
2084
|
-
event,
|
|
2085
|
-
findings_posted: comments.length,
|
|
2086
|
-
total_findings: findings.length,
|
|
2087
|
-
}, null, 2) }] };
|
|
2088
|
-
}
|
|
2089
|
-
case 'veto_debt_register': {
|
|
2090
|
-
const args = (request.params.arguments || {});
|
|
2091
|
-
const project_dir = String(args?.project_dir ?? '').trim();
|
|
2092
|
-
if (!project_dir)
|
|
2093
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
2094
|
-
let gitLog = '';
|
|
2095
|
-
try {
|
|
2096
|
-
gitLog = execSyncTop('git log --since=90.days --name-only --format="" --no-merges', {
|
|
2097
|
-
cwd: project_dir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
2098
|
-
}).toString();
|
|
2099
|
-
}
|
|
2100
|
-
catch { /* not a git repo */ }
|
|
2101
|
-
const churnMap = {};
|
|
2102
|
-
for (const line of gitLog.split('\n').filter(Boolean)) {
|
|
2103
|
-
if (line.includes('.')) {
|
|
2104
|
-
churnMap[line.trim()] = (churnMap[line.trim()] ?? 0) + 1;
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
const maxFiles = typeof args?.max_files === 'number' ? Math.min(args.max_files, 30) : 10;
|
|
2108
|
-
const extensions = Array.isArray(args?.extensions) ? args.extensions.map(String) : ['.ts', '.js', '.py', '.go', '.java'];
|
|
2109
|
-
const topFiles = Object.entries(churnMap)
|
|
2110
|
-
.filter(([f]) => extensions.some((ext) => f.endsWith(ext)))
|
|
2111
|
-
.sort((a, b) => b[1] - a[1])
|
|
2112
|
-
.slice(0, maxFiles)
|
|
2113
|
-
.map(([file, commits]) => ({ file, commits }));
|
|
2114
|
-
const fileContents = topFiles.map(({ file, commits }) => {
|
|
2115
|
-
try {
|
|
2116
|
-
const abs = join(project_dir, file);
|
|
2117
|
-
const content = readFileSync(abs, 'utf8').slice(0, 3000);
|
|
2118
|
-
return { file, commits, content };
|
|
2119
|
-
}
|
|
2120
|
-
catch {
|
|
2121
|
-
return { file, commits, content: '' };
|
|
2122
|
-
}
|
|
2123
|
-
}).filter(f => f.content);
|
|
2124
|
-
if (fileContents.length === 0) {
|
|
2125
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2126
|
-
total_files_analyzed: 0,
|
|
2127
|
-
date_range: 'last 90 days',
|
|
2128
|
-
debt_items: [],
|
|
2129
|
-
summary: 'No eligible files found in git history for the last 90 days.',
|
|
2130
|
-
}, null, 2) }] };
|
|
2131
|
-
}
|
|
2132
|
-
const debtCode = fileContents
|
|
2133
|
-
.map(f => `=== ${f.file} (${f.commits} commits) ===\n${f.content}`)
|
|
2134
|
-
.join('\n\n')
|
|
2135
|
-
.slice(0, 8000);
|
|
2136
|
-
const debtResult = await executeOne({
|
|
2137
|
-
id: `debt-${Date.now()}`,
|
|
2138
|
-
agent: 'code-quality',
|
|
2139
|
-
task: 'Analyze these high-churn source files for technical debt. For each file, identify: (1) the primary debt type (complexity/duplication/coupling/coverage/documentation), (2) severity (high/medium/low), (3) estimated fix effort in hours, (4) recommended agent to fix it. Rank by: high-churn × high-severity first.',
|
|
2140
|
-
code: debtCode,
|
|
2141
|
-
});
|
|
2142
|
-
recordOutcome('debt-register', 50, 2, 'code-quality', Math.round(debtResult.output.confidence * 100));
|
|
2143
|
-
const steps = debtResult.plan?.steps ?? [];
|
|
2144
|
-
let debtItems;
|
|
2145
|
-
if (steps.length > 0) {
|
|
2146
|
-
debtItems = steps.map((step, i) => {
|
|
2147
|
-
const matchedFile = fileContents[i] ?? fileContents[0];
|
|
2148
|
-
return {
|
|
2149
|
-
file: matchedFile.file,
|
|
2150
|
-
churn_commits: matchedFile?.commits ?? 0,
|
|
2151
|
-
priority: i < Math.ceil(steps.length / 3) ? 'high' : i < Math.ceil(steps.length * 2 / 3) ? 'medium' : 'low',
|
|
2152
|
-
debt_type: 'complexity',
|
|
2153
|
-
suggested_agent: 'refactor',
|
|
2154
|
-
estimated_hours: 2,
|
|
2155
|
-
description: step,
|
|
2156
|
-
};
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
2159
|
-
else {
|
|
2160
|
-
debtItems = fileContents.map(f => ({
|
|
2161
|
-
file: f.file,
|
|
2162
|
-
churn_commits: f.commits,
|
|
2163
|
-
priority: f.commits > 20 ? 'high' : f.commits > 10 ? 'medium' : 'low',
|
|
2164
|
-
debt_type: 'complexity',
|
|
2165
|
-
suggested_agent: 'refactor',
|
|
2166
|
-
estimated_hours: 2,
|
|
2167
|
-
}));
|
|
2168
|
-
}
|
|
2169
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2170
|
-
total_files_analyzed: fileContents.length,
|
|
2171
|
-
date_range: 'last 90 days',
|
|
2172
|
-
debt_items: debtItems,
|
|
2173
|
-
summary: (debtResult.plan?.approach ?? debtResult.output.recommendation ?? '').trim(),
|
|
2174
|
-
}, null, 2) }] };
|
|
2175
|
-
}
|
|
2176
|
-
case 'veto_adr': {
|
|
2177
|
-
const args = (request.params.arguments || {});
|
|
2178
|
-
const task = String(args?.task ?? '').trim();
|
|
2179
|
-
const verdict = String(args?.verdict ?? '').trim().toUpperCase();
|
|
2180
|
-
const recommended = String(args?.recommended ?? '').trim();
|
|
2181
|
-
const rationale = args?.rationale ? String(args.rationale) : undefined;
|
|
2182
|
-
const consequences = args?.consequences ? String(args.consequences) : undefined;
|
|
2183
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
2184
|
-
const outcomeId = args?.outcome_id ? String(args.outcome_id) : undefined;
|
|
2185
|
-
if (!task || !verdict || !recommended) {
|
|
2186
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'task, verdict, and recommended are required.' }) }], isError: true };
|
|
2187
|
-
}
|
|
2188
|
-
const statusMap = {
|
|
2189
|
-
GREEN: 'Accepted',
|
|
2190
|
-
YELLOW: 'Accepted with reservations',
|
|
2191
|
-
RED: 'Rejected',
|
|
2192
|
-
DEADLOCK: 'Deferred',
|
|
2193
|
-
};
|
|
2194
|
-
const adrStatus = statusMap[verdict] ?? 'Under review';
|
|
2195
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
2196
|
-
const outcomeRef = outcomeId ? ` (outcome: ${outcomeId})` : '';
|
|
2197
|
-
const adrContent = [
|
|
2198
|
-
`# ${task.slice(0, 80)}`,
|
|
2199
|
-
'',
|
|
2200
|
-
`Date: ${today}`,
|
|
2201
|
-
`Status: ${adrStatus}`,
|
|
2202
|
-
`Council verdict: ${verdict}${outcomeRef}`,
|
|
2203
|
-
'',
|
|
2204
|
-
'## Context',
|
|
2205
|
-
'',
|
|
2206
|
-
task,
|
|
2207
|
-
...(rationale ? ['', rationale] : []),
|
|
2208
|
-
'',
|
|
2209
|
-
'## Decision',
|
|
2210
|
-
'',
|
|
2211
|
-
recommended,
|
|
2212
|
-
'',
|
|
2213
|
-
'## Consequences',
|
|
2214
|
-
'',
|
|
2215
|
-
consequences ?? 'Under review.',
|
|
2216
|
-
].join('\n');
|
|
2217
|
-
let adrFilePath = null;
|
|
2218
|
-
if (projectDir) {
|
|
2219
|
-
const slug = task.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40);
|
|
2220
|
-
const decisionsDir = join(projectDir, 'docs', 'decisions');
|
|
2221
|
-
let nextNum = 1;
|
|
2222
|
-
try {
|
|
2223
|
-
const files = readdirSync(decisionsDir);
|
|
2224
|
-
nextNum = files.filter(f => /^\d{4}-/.test(f)).length + 1;
|
|
2225
|
-
}
|
|
2226
|
-
catch { /* directory doesn't exist yet */ }
|
|
2227
|
-
const paddedNum = String(nextNum).padStart(4, '0');
|
|
2228
|
-
adrFilePath = join(decisionsDir, `${paddedNum}-${slug}.md`);
|
|
2229
|
-
mkdirSync(decisionsDir, { recursive: true });
|
|
2230
|
-
writeFileSync(adrFilePath, adrContent, 'utf8');
|
|
2231
|
-
}
|
|
2232
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2233
|
-
success: true,
|
|
2234
|
-
adr: adrContent,
|
|
2235
|
-
file_path: adrFilePath,
|
|
2236
|
-
status: adrStatus,
|
|
2237
|
-
}, null, 2) }] };
|
|
2238
|
-
}
|
|
2239
|
-
case 'veto_env_setup': {
|
|
2240
|
-
const args = (request.params.arguments || {});
|
|
2241
|
-
const projectDir = String(args?.project_dir ?? '').trim();
|
|
2242
|
-
const writeFiles = args?.write_files === true;
|
|
2243
|
-
if (!projectDir) {
|
|
2244
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
2245
|
-
}
|
|
2246
|
-
const detected = [];
|
|
2247
|
-
const summaryParts = [];
|
|
2248
|
-
// Read package.json
|
|
2249
|
-
const pkgPath = join(projectDir, 'package.json');
|
|
2250
|
-
if (existsSync(pkgPath)) {
|
|
2251
|
-
detected.push('node');
|
|
2252
|
-
try {
|
|
2253
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
2254
|
-
summaryParts.push(`Node project: ${pkg.name ?? 'unnamed'}`);
|
|
2255
|
-
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
2256
|
-
summaryParts.push(`Scripts: ${Object.keys(pkg.scripts).join(', ')}`);
|
|
2257
|
-
}
|
|
2258
|
-
if (pkg.dependencies && typeof pkg.dependencies === 'object') {
|
|
2259
|
-
summaryParts.push(`Dependencies: ${Object.keys(pkg.dependencies).slice(0, 20).join(', ')}`);
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
catch { /* ignore parse errors */ }
|
|
2263
|
-
}
|
|
2264
|
-
// Read .env or .env.local
|
|
2265
|
-
for (const envFile of ['.env', '.env.local']) {
|
|
2266
|
-
const envPath = join(projectDir, envFile);
|
|
2267
|
-
if (existsSync(envPath)) {
|
|
2268
|
-
try {
|
|
2269
|
-
const lines = readFileSync(envPath, 'utf8').split('\n');
|
|
2270
|
-
const vars = lines.filter(l => /^[A-Z_]+=/.test(l)).map(l => l.split('=')[0]);
|
|
2271
|
-
if (vars.length > 0) {
|
|
2272
|
-
summaryParts.push(`Existing env vars (${envFile}): ${vars.join(', ')}`);
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
catch { /* ignore */ }
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
// Note other config files
|
|
2279
|
-
for (const f of ['requirements.txt', 'pyproject.toml']) {
|
|
2280
|
-
if (existsSync(join(projectDir, f))) {
|
|
2281
|
-
detected.push('python');
|
|
2282
|
-
summaryParts.push(`Python config found: ${f}`);
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
if (existsSync(join(projectDir, 'Cargo.toml'))) {
|
|
2286
|
-
detected.push('rust');
|
|
2287
|
-
summaryParts.push('Rust project found: Cargo.toml');
|
|
2288
|
-
}
|
|
2289
|
-
for (const f of ['docker-compose.yml', 'docker-compose.yaml']) {
|
|
2290
|
-
if (existsSync(join(projectDir, f))) {
|
|
2291
|
-
summaryParts.push(`Docker Compose found: ${f}`);
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
const projectSummary = summaryParts.join('\n') || 'No configuration files found.';
|
|
2295
|
-
const enrichedCtx = buildContextString(projectDir, projectSummary);
|
|
2296
|
-
const agentTask = 'Generate a .env.example file for this project. List every environment variable needed with a placeholder value and a one-line comment explaining what it is. Then write a numbered setup guide (5-10 steps) for a developer setting up this project from scratch.';
|
|
2297
|
-
const envResult = await executeOne({ id: 'env-setup-1', agent: 'devops', task: agentTask, context: enrichedCtx || undefined, project_dir: projectDir });
|
|
2298
|
-
const rawOutput = envResult.output.recommendation ?? envResult.plan?.approach ?? '';
|
|
2299
|
-
const envLines = rawOutput.split('\n').filter((l) => /^[A-Z_]+=/.test(l));
|
|
2300
|
-
const envExample = envLines.length > 0 ? envLines.join('\n') : '# Add your environment variables here\n';
|
|
2301
|
-
let written = false;
|
|
2302
|
-
if (writeFiles) {
|
|
2303
|
-
writeFileSync(join(projectDir, '.env.example'), envExample, 'utf8');
|
|
2304
|
-
written = true;
|
|
2305
|
-
}
|
|
2306
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2307
|
-
env_example: envExample,
|
|
2308
|
-
setup_guide: rawOutput,
|
|
2309
|
-
written,
|
|
2310
|
-
detected: [...new Set(detected)],
|
|
2311
|
-
}, null, 2) }] };
|
|
2312
|
-
}
|
|
2313
|
-
case 'veto_prompt_optimizer': {
|
|
2314
|
-
const args = (request.params.arguments || {});
|
|
2315
|
-
const rawPrompt = String(args?.prompt ?? '').trim();
|
|
2316
|
-
const goal = args?.goal ? String(args.goal) : undefined;
|
|
2317
|
-
if (!rawPrompt)
|
|
2318
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'prompt is required.' }) }], isError: true };
|
|
2319
|
-
const prompt = rawPrompt.length > 8000 ? rawPrompt.slice(0, 8000) : rawPrompt;
|
|
2320
|
-
// Deterministic pre-scan
|
|
2321
|
-
const issues = [];
|
|
2322
|
-
const p = prompt.toLowerCase();
|
|
2323
|
-
if (!p.includes('you are') && !p.includes('your role') && !p.includes('act as') && !p.includes('you\'re a')) {
|
|
2324
|
-
issues.push({ category: 'role', severity: 'medium', finding: 'No role definition found. Add "You are a [role]..." to anchor behavior.' });
|
|
2325
|
-
}
|
|
2326
|
-
if (!p.includes('format') && !p.includes('json') && !p.includes('markdown') && !p.includes('return') && !p.includes('output')) {
|
|
2327
|
-
issues.push({ category: 'output_format', severity: 'medium', finding: 'No output format specified. Specify JSON, markdown, or plain text.' });
|
|
2328
|
-
}
|
|
2329
|
-
if (/ignore (previous|prior|above|all)|disregard|forget|pretend/i.test(prompt)) {
|
|
2330
|
-
issues.push({ category: 'injection', severity: 'high', finding: 'Prompt may be injection-prone — contains phrases attackers commonly use.' });
|
|
2331
|
-
}
|
|
2332
|
-
if (prompt.trim().split(/\s+/).length < 20) {
|
|
2333
|
-
issues.push({ category: 'specificity', severity: 'low', finding: 'Prompt is very short. Add more context and constraints for better results.' });
|
|
2334
|
-
}
|
|
2335
|
-
const result = await executeOne({
|
|
2336
|
-
id: 'prompt-optimizer-1',
|
|
2337
|
-
agent: 'documentation',
|
|
2338
|
-
task: 'You are a prompt engineering expert. Analyze this prompt for failure modes: vague instructions, missing context, ambiguous outputs, injection risks, lack of examples, poor role definition. Then rewrite it to be clearer, more specific, and safer. Return: 1) A numbered list of issues found, 2) A complete rewritten version of the prompt.',
|
|
2339
|
-
code: prompt,
|
|
2340
|
-
context: goal ? `Goal: ${goal}` : undefined,
|
|
2341
|
-
});
|
|
2342
|
-
const quality = result.analysis?.score ?? Math.round(result.output.confidence * 100);
|
|
2343
|
-
recordOutcome('prompt-optimizer', 50, 2, 'documentation', quality);
|
|
2344
|
-
const highCount = issues.filter(i => i.severity === 'high').length;
|
|
2345
|
-
const mediumCount = issues.filter(i => i.severity === 'medium').length;
|
|
2346
|
-
const lowCount = issues.filter(i => i.severity === 'low').length;
|
|
2347
|
-
const score = Math.min(100, Math.max(0, 100 - highCount * 20 - mediumCount * 10 - lowCount * 5));
|
|
2348
|
-
const rewritten_prompt = result.plan?.approach ?? result.output.recommendation ?? '';
|
|
2349
|
-
const improvement_summary = result.analysis?.summary ?? result.plan?.steps?.join('; ') ?? '';
|
|
2350
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2351
|
-
score,
|
|
2352
|
-
issues,
|
|
2353
|
-
rewritten_prompt,
|
|
2354
|
-
improvement_summary,
|
|
2355
|
-
}, null, 2) }] };
|
|
2356
|
-
}
|
|
2357
|
-
case 'veto_sre_advisor': {
|
|
2358
|
-
const args = (request.params.arguments || {});
|
|
2359
|
-
const slo_target = Number(args?.slo_target);
|
|
2360
|
-
const window_days = Number(args?.window_days);
|
|
2361
|
-
const downtime_minutes = Number(args?.downtime_minutes);
|
|
2362
|
-
const service_name = args?.service_name ? String(args.service_name) : undefined;
|
|
2363
|
-
const incidents = Array.isArray(args?.incidents) ? args.incidents : [];
|
|
2364
|
-
if (!slo_target || !window_days || isNaN(downtime_minutes)) {
|
|
2365
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'slo_target, window_days, and downtime_minutes are required.' }) }], isError: true };
|
|
2366
|
-
}
|
|
2367
|
-
// Deterministic error budget math
|
|
2368
|
-
const sloFraction = slo_target / 100;
|
|
2369
|
-
const windowMinutes = window_days * 24 * 60;
|
|
2370
|
-
const totalBudgetMinutes = windowMinutes * (1 - sloFraction);
|
|
2371
|
-
const consumedMinutes = downtime_minutes;
|
|
2372
|
-
const remainingMinutes = Math.max(0, totalBudgetMinutes - consumedMinutes);
|
|
2373
|
-
const remainingPct = totalBudgetMinutes > 0 ? Math.round((remainingMinutes / totalBudgetMinutes) * 1000) / 10 : 0;
|
|
2374
|
-
const exhaustedAt = consumedMinutes > 0 && remainingMinutes > 0
|
|
2375
|
-
? new Date(Date.now() + (remainingMinutes / consumedMinutes) * window_days * 86400_000).toISOString().slice(0, 10)
|
|
2376
|
-
: consumedMinutes >= totalBudgetMinutes ? 'EXHAUSTED' : null;
|
|
2377
|
-
const status = remainingPct > 50 ? 'healthy' : remainingPct > 20 ? 'at_risk' : remainingPct > 0 ? 'critical' : 'exhausted';
|
|
2378
|
-
// Build incident summary for the agent
|
|
2379
|
-
const incidentSummary = incidents.length > 0
|
|
2380
|
-
? 'Recent incidents:\n' + incidents.map(i => `- ${i.date}: ${i.duration_minutes} min — ${i.description}`).join('\n')
|
|
2381
|
-
: 'No incident data provided.';
|
|
2382
|
-
const sreResult = await executeOne({
|
|
2383
|
-
id: 'sre-advisor-1',
|
|
2384
|
-
agent: 'performance',
|
|
2385
|
-
task: 'You are an SRE advisor. Given this service\'s error budget status, suggest: 1) Top 3 reliability improvements ranked by error budget recovery potential, 2) Whether to freeze non-critical deployments, 3) Specific monitoring improvements. Be concrete and actionable.',
|
|
2386
|
-
context: `Service: ${service_name || 'unknown'}\nSLO: ${slo_target}%\nWindow: ${window_days} days\nBudget remaining: ${remainingPct}% (${remainingMinutes.toFixed(1)} min)\nStatus: ${status}\n${incidentSummary}`,
|
|
2387
|
-
});
|
|
2388
|
-
const recommendations = sreResult.plan?.approach ?? sreResult.output.recommendation ?? '';
|
|
2389
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2390
|
-
slo_target_pct: slo_target,
|
|
2391
|
-
window_days,
|
|
2392
|
-
total_budget_minutes: Math.round(totalBudgetMinutes * 10) / 10,
|
|
2393
|
-
consumed_minutes: consumedMinutes,
|
|
2394
|
-
remaining_minutes: Math.round(remainingMinutes * 10) / 10,
|
|
2395
|
-
remaining_pct: remainingPct,
|
|
2396
|
-
status,
|
|
2397
|
-
projected_exhaustion: exhaustedAt,
|
|
2398
|
-
recommendations,
|
|
2399
|
-
freeze_recommended: remainingPct < 20,
|
|
2400
|
-
}, null, 2) }] };
|
|
2401
|
-
}
|
|
2402
|
-
case 'veto_diagram': {
|
|
2403
|
-
const args = (request.params.arguments || {});
|
|
2404
|
-
const project_dir = String(args?.project_dir ?? '').trim();
|
|
2405
|
-
const diagramType = String(args?.diagram_type ?? 'flowchart').trim();
|
|
2406
|
-
const focus = args?.focus ? String(args.focus) : undefined;
|
|
2407
|
-
if (!project_dir)
|
|
2408
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
2409
|
-
const ctx = buildContextString(project_dir);
|
|
2410
|
-
let fileTree = '';
|
|
2411
|
-
try {
|
|
2412
|
-
fileTree = execSyncTop('git ls-files --others --cached --exclude-standard', {
|
|
2413
|
-
cwd: project_dir, timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
2414
|
-
}).toString().split('\n').filter((f) => !f.includes('node_modules') && !f.includes('dist/')).slice(0, 60).join('\n');
|
|
2415
|
-
}
|
|
2416
|
-
catch { /* not a git repo */ }
|
|
2417
|
-
const diagramResult = await executeOne({
|
|
2418
|
-
id: 'diagram-1',
|
|
2419
|
-
agent: 'documentation',
|
|
2420
|
-
task: `Generate a ${diagramType} Mermaid diagram of this project's architecture. Output ONLY the raw Mermaid diagram code (starting with 'flowchart TD' or similar — no markdown fences, no explanation text). Focus on: ${focus || 'overall system architecture, main modules, and data flow'}. Keep it under 30 nodes for readability.`,
|
|
2421
|
-
code: fileTree.slice(0, 4000),
|
|
2422
|
-
context: ctx || undefined,
|
|
2423
|
-
});
|
|
2424
|
-
const diagramQuality = diagramResult.analysis?.score ?? Math.round(diagramResult.output.confidence * 100);
|
|
2425
|
-
recordOutcome('diagram', 50, 2, 'documentation', diagramQuality);
|
|
2426
|
-
const rawOutput = diagramResult.plan?.approach ?? diagramResult.output.recommendation ?? '';
|
|
2427
|
-
// Extract Mermaid block — find first line matching a known diagram type keyword
|
|
2428
|
-
const lines = rawOutput.split('\n');
|
|
2429
|
-
const startIdx = lines.findIndex((l) => /^(flowchart|graph|classDiagram|sequenceDiagram|C4Context|erDiagram)/.test(l.trim()));
|
|
2430
|
-
const mermaid = startIdx >= 0 ? lines.slice(startIdx).join('\n').trim() : rawOutput.trim();
|
|
2431
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2432
|
-
diagram_type: diagramType,
|
|
2433
|
-
mermaid,
|
|
2434
|
-
render_hint: 'Paste into https://mermaid.live or a GitHub markdown code block with ```mermaid',
|
|
2435
|
-
}, null, 2) }] };
|
|
2436
|
-
}
|
|
2437
|
-
case 'veto_rca': {
|
|
2438
|
-
const args = (request.params.arguments || {});
|
|
2439
|
-
const error = String(args?.error ?? '').trim();
|
|
2440
|
-
const projectDir = args?.project_dir ? String(args.project_dir).trim() : '';
|
|
2441
|
-
const fileHint = args?.file_hint ? String(args.file_hint).trim() : '';
|
|
2442
|
-
if (!error)
|
|
2443
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'error is required.' }) }], isError: true };
|
|
2444
|
-
const userContext = buildContextString(projectDir || undefined);
|
|
2445
|
-
let gitContext = '';
|
|
2446
|
-
try {
|
|
2447
|
-
const recent = execSyncTop('git log --oneline -15', { cwd: projectDir || undefined, timeout: 4000, stdio: ['pipe', 'pipe', 'pipe'] }).toString();
|
|
2448
|
-
gitContext = `Recent commits:\n${recent}`;
|
|
2449
|
-
if (fileHint) {
|
|
2450
|
-
const blame = execSyncTop(`git log --oneline -10 -- "${fileHint}"`, { cwd: projectDir || undefined, timeout: 4000, stdio: ['pipe', 'pipe', 'pipe'] }).toString();
|
|
2451
|
-
gitContext += `\nRecent changes to ${fileHint}:\n${blame}`;
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
catch { /* not a git repo */ }
|
|
2455
|
-
const result = await executeOne({
|
|
2456
|
-
id: 'rca-1',
|
|
2457
|
-
agent: 'debugger',
|
|
2458
|
-
task: 'Perform a structured root-cause analysis. Identify: (1) the most likely root cause, (2) the probable introducing commit or change, (3) immediate fix steps, (4) prevention recommendations.',
|
|
2459
|
-
code: error.slice(0, 6000),
|
|
2460
|
-
context: [gitContext, userContext].filter(Boolean).join('\n') || undefined,
|
|
2461
|
-
});
|
|
2462
|
-
const quality = Math.round(result.output.confidence * 100);
|
|
2463
|
-
recordOutcome('rca', 50, 2, 'debugger', quality);
|
|
2464
|
-
const root_cause = result.plan?.approach?.slice(0, 200) ?? result.output.recommendation.slice(0, 200);
|
|
2465
|
-
const fix_steps = result.plan?.steps?.slice(0, 5) ?? [];
|
|
2466
|
-
const hypothesis = result.output.recommendation;
|
|
2467
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2468
|
-
root_cause,
|
|
2469
|
-
hypothesis,
|
|
2470
|
-
suspect_commits: [],
|
|
2471
|
-
fix_steps,
|
|
2472
|
-
prevention: [],
|
|
2473
|
-
confidence: quality,
|
|
2474
|
-
}, null, 2) }] };
|
|
2475
|
-
}
|
|
2476
|
-
case 'veto_release_notes': {
|
|
2477
|
-
const args = (request.params.arguments || {});
|
|
2478
|
-
const projectDir = String(args?.project_dir ?? '').trim();
|
|
2479
|
-
const audience = String(args?.audience ?? 'user') === 'developer' ? 'developer' : 'user';
|
|
2480
|
-
if (!projectDir)
|
|
2481
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
2482
|
-
let fromRef = args?.from_ref ? String(args.from_ref) : '';
|
|
2483
|
-
if (!fromRef) {
|
|
2484
|
-
try {
|
|
2485
|
-
fromRef = execSyncTop('git describe --tags --abbrev=0', { cwd: projectDir, timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
2486
|
-
}
|
|
2487
|
-
catch {
|
|
2488
|
-
fromRef = '';
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
const logCmd = fromRef ? `git log ${fromRef}..HEAD --oneline --no-merges` : 'git log --oneline --no-merges -30';
|
|
2492
|
-
let commits = '';
|
|
2493
|
-
try {
|
|
2494
|
-
commits = execSyncTop(logCmd, { cwd: projectDir, timeout: 4000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
2495
|
-
}
|
|
2496
|
-
catch {
|
|
2497
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'Could not read git log. Ensure project_dir is a git repository.' }) }], isError: true };
|
|
2498
|
-
}
|
|
2499
|
-
if (!commits)
|
|
2500
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, release_notes: 'No changes since last tag.', commits_processed: 0 }) }] };
|
|
2501
|
-
const commitsCount = commits.split('\n').filter(Boolean).length;
|
|
2502
|
-
const result = await executeOne({
|
|
2503
|
-
id: 'relnotes-1',
|
|
2504
|
-
agent: 'documentation',
|
|
2505
|
-
task: `Generate ${audience === 'developer' ? 'developer-facing' : 'user-facing'} release notes from these git commits. Rewrite technical commit messages into clear benefit-focused language. Group by: New Features, Improvements, Bug Fixes, Other. Each line should be one sentence describing the user benefit.`,
|
|
2506
|
-
code: commits.slice(0, 4000),
|
|
2507
|
-
});
|
|
2508
|
-
const release_notes = result.plan?.approach ?? result.output.recommendation;
|
|
2509
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2510
|
-
release_notes,
|
|
2511
|
-
from_ref: fromRef || 'HEAD~30',
|
|
2512
|
-
commits_processed: commitsCount,
|
|
2513
|
-
audience,
|
|
2514
|
-
}, null, 2) }] };
|
|
2515
|
-
}
|
|
2516
|
-
case 'veto_postmortem': {
|
|
2517
|
-
const args = (request.params.arguments || {});
|
|
2518
|
-
const incident = String(args?.incident ?? '').trim();
|
|
2519
|
-
const timeline = args?.timeline ? String(args.timeline).trim() : '';
|
|
2520
|
-
const projectDir = args?.project_dir ? String(args.project_dir).trim() : '';
|
|
2521
|
-
const service = args?.service ? String(args.service).trim() : '';
|
|
2522
|
-
if (!incident)
|
|
2523
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'incident is required.' }) }], isError: true };
|
|
2524
|
-
let auditCtx = '';
|
|
2525
|
-
let correlatedRedVerdicts = 0;
|
|
2526
|
-
try {
|
|
2527
|
-
const log = getAuditLog({ verdict: 'RED', limit: 5 });
|
|
2528
|
-
if (log.length > 0) {
|
|
2529
|
-
correlatedRedVerdicts = log.length;
|
|
2530
|
-
auditCtx = `Past RED council verdicts:\n${log.map((e) => `- ${e.summary ?? ''}`).join('\n')}`;
|
|
2531
|
-
}
|
|
2532
|
-
}
|
|
2533
|
-
catch { /* ignore */ }
|
|
2534
|
-
const context = [
|
|
2535
|
-
timeline && `Timeline:\n${timeline}`,
|
|
2536
|
-
service && `Service: ${service}`,
|
|
2537
|
-
auditCtx || '',
|
|
2538
|
-
].filter(Boolean).join('\n\n') || undefined;
|
|
2539
|
-
const result = await executeOne({
|
|
2540
|
-
id: 'pm-1',
|
|
2541
|
-
agent: 'debugger',
|
|
2542
|
-
task: 'Write a blameless postmortem. Include: (1) Incident summary (2) Root cause (five-whys analysis) (3) Impact (4) Timeline of detection/response/resolution (5) Action items with owner and deadline (6) What went well (7) Prevention measures. Use a constructive tone — blame systems not people.',
|
|
2543
|
-
code: incident.slice(0, 4000),
|
|
2544
|
-
context,
|
|
2545
|
-
});
|
|
2546
|
-
const postmortem = result.plan?.approach ?? result.output.recommendation;
|
|
2547
|
-
const root_cause = result.output.recommendation.split(/[.!?]/)[0]?.trim() ?? '';
|
|
2548
|
-
const action_items = result.plan?.steps?.slice(0, 10) ?? [];
|
|
2549
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2550
|
-
postmortem,
|
|
2551
|
-
root_cause,
|
|
2552
|
-
action_items,
|
|
2553
|
-
correlated_red_verdicts: correlatedRedVerdicts,
|
|
2554
|
-
}, null, 2) }] };
|
|
2555
|
-
}
|
|
2556
|
-
case 'veto_doc_gen': {
|
|
2557
|
-
const args = (request.params.arguments || {});
|
|
2558
|
-
const filePath = String(args?.file_path ?? '').trim();
|
|
2559
|
-
const styleArg = String(args?.style ?? 'auto').trim();
|
|
2560
|
-
if (!filePath)
|
|
2561
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'file_path is required.' }) }], isError: true };
|
|
2562
|
-
let content = '';
|
|
2563
|
-
try {
|
|
2564
|
-
content = readFileSync(filePath, 'utf8');
|
|
2565
|
-
}
|
|
2566
|
-
catch (e) {
|
|
2567
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Could not read file: ${e.message}` }) }], isError: true };
|
|
2568
|
-
}
|
|
2569
|
-
let detectedStyle = styleArg;
|
|
2570
|
-
if (styleArg === 'auto') {
|
|
2571
|
-
const ext = extname(filePath).toLowerCase();
|
|
2572
|
-
if (ext === '.ts' || ext === '.tsx')
|
|
2573
|
-
detectedStyle = 'tsdoc';
|
|
2574
|
-
else if (ext === '.py')
|
|
2575
|
-
detectedStyle = 'docstring';
|
|
2576
|
-
else
|
|
2577
|
-
detectedStyle = 'jsdoc';
|
|
2578
|
-
}
|
|
2579
|
-
const docGenResult = await executeOne({
|
|
2580
|
-
id: 'docgen-1',
|
|
2581
|
-
agent: 'documentation',
|
|
2582
|
-
task: `Add ${detectedStyle} documentation comments to all public functions, classes, interfaces, and exported constants in this file. For each, add: (1) a one-line summary, (2) @param descriptions, (3) @returns description, (4) @throws if applicable. Return the COMPLETE file content with documentation added — do not truncate.`,
|
|
2583
|
-
code: content.slice(0, 10000),
|
|
2584
|
-
});
|
|
2585
|
-
const docQuality = docGenResult.analysis?.score ?? Math.round(docGenResult.output.confidence * 100);
|
|
2586
|
-
recordOutcome('doc-gen', 50, 2, 'documentation', docQuality);
|
|
2587
|
-
const annotatedContent = docGenResult.plan?.approach ?? docGenResult.output.recommendation ?? '';
|
|
2588
|
-
const symbolsDocumented = (annotatedContent.match(/@param\b/g) ?? []).length;
|
|
2589
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2590
|
-
file_path: filePath,
|
|
2591
|
-
style: detectedStyle,
|
|
2592
|
-
annotated_content: annotatedContent,
|
|
2593
|
-
symbols_documented: symbolsDocumented,
|
|
2594
|
-
}, null, 2) }] };
|
|
2595
|
-
}
|
|
2596
|
-
case 'veto_type_coverage': {
|
|
2597
|
-
const args = (request.params.arguments || {});
|
|
2598
|
-
return await handleAgenticWorker('veto_type_coverage', args, 'reviewer', 'Analyze TypeScript type coverage and suggest improvements.');
|
|
2599
|
-
}
|
|
2600
|
-
case 'veto_test_gaps': {
|
|
2601
|
-
const args = (request.params.arguments || {});
|
|
2602
|
-
return await handleAgenticWorker('veto_test_gaps', args, 'tester', 'Identify untested paths and suggest test cases.');
|
|
2603
|
-
}
|
|
2604
|
-
case 'veto_onboard': {
|
|
2605
|
-
const args = (request.params.arguments || {});
|
|
2606
|
-
const projectDir = String(args?.project_dir ?? '').trim();
|
|
2607
|
-
const role = args?.role ? String(args.role).trim() : '';
|
|
2608
|
-
if (!projectDir)
|
|
2609
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
2610
|
-
let readme = '';
|
|
2611
|
-
for (const name of ['README.md', 'readme.md', 'README.txt']) {
|
|
2612
|
-
try {
|
|
2613
|
-
readme = readFileSync(join(projectDir, name), 'utf8').slice(0, 3000);
|
|
2614
|
-
break;
|
|
2615
|
-
}
|
|
2616
|
-
catch { /* skip */ }
|
|
2617
|
-
}
|
|
2618
|
-
const onboardResult = await executeOne({
|
|
2619
|
-
id: 'onboard-1',
|
|
2620
|
-
agent: 'documentation',
|
|
2621
|
-
task: `Write a complete onboarding guide for a new ${role || 'fullstack'} developer joining this project. Include: (1) Setup steps (clone, install, env vars, first run), (2) Architecture overview (key directories and their purpose), (3) Key files to understand first, (4) How to run tests, (5) Development workflow, (6) First PR checklist (what to check before submitting). Be specific to this codebase.`,
|
|
2622
|
-
context: [buildContextString(projectDir), readme ? `README:\n${readme}` : ''].filter(Boolean).join('\n\n') || undefined,
|
|
2623
|
-
project_dir: projectDir,
|
|
2624
|
-
});
|
|
2625
|
-
const onboardQuality = onboardResult.analysis?.score ?? Math.round(onboardResult.output.confidence * 100);
|
|
2626
|
-
recordOutcome('onboard', 50, 2, 'documentation', onboardQuality);
|
|
2627
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2628
|
-
guide: onboardResult.plan?.approach ?? onboardResult.output.recommendation ?? '',
|
|
2629
|
-
role: role || 'fullstack',
|
|
2630
|
-
sections: ['Setup', 'Architecture', 'Key Files', 'Testing', 'Workflow', 'First PR'],
|
|
2631
|
-
}, null, 2) }] };
|
|
2632
|
-
}
|
|
2633
|
-
// ── veto_dep_advisor ────────────────────────────────────────────────────────
|
|
2634
|
-
case 'veto_dep_advisor': {
|
|
2635
|
-
const args = (request.params.arguments || {});
|
|
2636
|
-
const projectDir = String(args?.project_dir ?? '').trim();
|
|
2637
|
-
let ecosystem = String(args?.ecosystem ?? 'auto');
|
|
2638
|
-
let packages = [];
|
|
2639
|
-
// Try npm first
|
|
2640
|
-
if (ecosystem === 'auto' || ecosystem === 'npm') {
|
|
2641
|
-
try {
|
|
2642
|
-
const pkg = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8'));
|
|
2643
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2644
|
-
packages = Object.entries(deps).map(([name, ver]) => ({ name, version: String(ver).replace(/[\^~>=<]/g, '').split('.')[0] + '.0.0' })).slice(0, 50);
|
|
2645
|
-
ecosystem = 'npm';
|
|
2646
|
-
}
|
|
2647
|
-
catch { /* try next */ }
|
|
2648
|
-
}
|
|
2649
|
-
if ((ecosystem === 'auto' || ecosystem === 'pypi') && packages.length === 0) {
|
|
2650
|
-
try {
|
|
2651
|
-
const req = readFileSync(join(projectDir, 'requirements.txt'), 'utf8');
|
|
2652
|
-
packages = req.split('\n').filter(l => l.trim() && !l.startsWith('#')).map(l => { const [name, ver = '0.0.0'] = l.split(/[==>=<]/); return { name: name.trim(), version: ver.trim() || '0.0.0' }; }).slice(0, 50);
|
|
2653
|
-
ecosystem = 'pypi';
|
|
2654
|
-
}
|
|
2655
|
-
catch { /* skip */ }
|
|
2656
|
-
}
|
|
2657
|
-
if (packages.length === 0)
|
|
2658
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'No package.json or requirements.txt found in project_dir.' }) }], isError: true };
|
|
2659
|
-
let vulnerabilities = [];
|
|
2660
|
-
let osvAvailable = false;
|
|
2661
|
-
try {
|
|
2662
|
-
const osvEcosystem = ecosystem === 'npm' ? 'npm' : ecosystem === 'pypi' ? 'PyPI' : 'crates.io';
|
|
2663
|
-
const body = { queries: packages.slice(0, 30).map(p => ({ package: { name: p.name, ecosystem: osvEcosystem }, version: p.version })) };
|
|
2664
|
-
const resp = await fetch('https://api.osv.dev/v1/querybatch', {
|
|
2665
|
-
method: 'POST',
|
|
2666
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2667
|
-
body: JSON.stringify(body),
|
|
2668
|
-
signal: AbortSignal.timeout(8000),
|
|
2669
|
-
});
|
|
2670
|
-
if (resp.ok) {
|
|
2671
|
-
const data = await resp.json();
|
|
2672
|
-
data.results.forEach((r, i) => {
|
|
2673
|
-
for (const v of (r.vulns ?? [])) {
|
|
2674
|
-
vulnerabilities.push({ package: packages[i].name, version: packages[i].version, vuln_id: v.id, severity: v.database_specific?.severity?.toLowerCase() ?? 'unknown', summary: v.summary });
|
|
2675
|
-
}
|
|
2676
|
-
});
|
|
2677
|
-
osvAvailable = true;
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
catch { /* OSV unavailable — proceed without vuln data */ }
|
|
2681
|
-
const depResult = await executeOne({
|
|
2682
|
-
id: 'dep-1',
|
|
2683
|
-
agent: 'dependency-audit',
|
|
2684
|
-
task: 'Analyze these dependencies and produce a risk-ranked upgrade plan. For each vulnerable or outdated package: (1) risk level, (2) recommended version, (3) breaking-change risk, (4) migration steps.',
|
|
2685
|
-
code: JSON.stringify({ packages: packages.slice(0, 20), vulnerabilities }, null, 2).slice(0, 6000),
|
|
2686
|
-
});
|
|
2687
|
-
recordOutcome('dep_advisor', 50, 2, 'dependency-audit', depResult.analysis?.score ?? Math.round(depResult.output.confidence * 100));
|
|
2688
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2689
|
-
ecosystem,
|
|
2690
|
-
packages_scanned: packages.length,
|
|
2691
|
-
vulnerabilities_found: vulnerabilities.length,
|
|
2692
|
-
vulns: vulnerabilities,
|
|
2693
|
-
upgrade_plan: depResult.plan?.approach ?? depResult.output.recommendation ?? '',
|
|
2694
|
-
osv_available: osvAvailable,
|
|
2695
|
-
}, null, 2) }] };
|
|
2696
|
-
}
|
|
2697
|
-
// ── veto_query_advisor ──────────────────────────────────────────────────────
|
|
2698
|
-
case 'veto_query_advisor': {
|
|
2699
|
-
const args = (request.params.arguments || {});
|
|
2700
|
-
const query = String(args?.query ?? '').trim();
|
|
2701
|
-
const schema = args?.schema ? String(args.schema).trim() : '';
|
|
2702
|
-
const explainOutput = args?.explain_output ? String(args.explain_output).trim() : '';
|
|
2703
|
-
if (!query)
|
|
2704
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'query is required.' }) }], isError: true };
|
|
2705
|
-
// Deterministic pre-scan for common issues
|
|
2706
|
-
const issues = [];
|
|
2707
|
-
const q = query.toLowerCase();
|
|
2708
|
-
if (/select \*/i.test(query))
|
|
2709
|
-
issues.push('SELECT * detected — specify only needed columns');
|
|
2710
|
-
if (/where.*like\s+'%/i.test(query))
|
|
2711
|
-
issues.push('Leading wildcard LIKE pattern prevents index use');
|
|
2712
|
-
if (!q.includes('limit') && (q.includes('select') && !q.includes('count')))
|
|
2713
|
-
issues.push('No LIMIT clause — could return unbounded result set');
|
|
2714
|
-
const joinCount = (q.match(/\bjoin\b/g) ?? []).length;
|
|
2715
|
-
if (joinCount > 4)
|
|
2716
|
-
issues.push(`${joinCount} JOINs detected — verify indexes on join columns`);
|
|
2717
|
-
const queryResult = await executeOne({
|
|
2718
|
-
id: 'query-1',
|
|
2719
|
-
agent: 'database',
|
|
2720
|
-
task: 'Analyze this SQL query for performance issues. Provide: (1) Rewritten optimized query, (2) Specific CREATE INDEX statements needed, (3) N+1 query detection if this is part of a loop, (4) Estimated improvement percentage, (5) Index risk assessment (will this lock the table?)',
|
|
2721
|
-
code: query.slice(0, 4000),
|
|
2722
|
-
context: [schema && `Schema:\n${schema}`, explainOutput && `EXPLAIN:\n${explainOutput}`].filter(Boolean).join('\n'),
|
|
2723
|
-
});
|
|
2724
|
-
recordOutcome('query_advisor', 50, 2, 'database', queryResult.analysis?.score ?? Math.round(queryResult.output.confidence * 100));
|
|
2725
|
-
const agentOutput = queryResult.plan?.approach ?? queryResult.output.recommendation ?? '';
|
|
2726
|
-
const indexStatements = agentOutput.split('\n').filter((l) => /CREATE INDEX/i.test(l)).map((l) => l.trim());
|
|
2727
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2728
|
-
issues_detected: issues,
|
|
2729
|
-
optimized_query: '',
|
|
2730
|
-
index_statements: indexStatements,
|
|
2731
|
-
n_plus_one_risk: /n\+1|n \+ 1/i.test(agentOutput),
|
|
2732
|
-
recommendations: agentOutput,
|
|
2733
|
-
estimated_improvement: '',
|
|
2734
|
-
}, null, 2) }] };
|
|
2735
|
-
}
|
|
2736
|
-
// ── veto_bundle_advisor ─────────────────────────────────────────────────────
|
|
2737
|
-
case 'veto_bundle_advisor': {
|
|
2738
|
-
const args = (request.params.arguments || {});
|
|
2739
|
-
let statsRaw = '';
|
|
2740
|
-
try {
|
|
2741
|
-
statsRaw = readFileSync(String(args?.stats_file ?? ''), 'utf8');
|
|
2742
|
-
}
|
|
2743
|
-
catch (e) {
|
|
2744
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Cannot read stats file: ${e}` }) }], isError: true };
|
|
2745
|
-
}
|
|
2746
|
-
let statsData = {};
|
|
2747
|
-
try {
|
|
2748
|
-
statsData = JSON.parse(statsRaw);
|
|
2749
|
-
}
|
|
2750
|
-
catch {
|
|
2751
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'stats_file is not valid JSON' }) }], isError: true };
|
|
2752
|
-
}
|
|
2753
|
-
// Extract key metrics from webpack stats format
|
|
2754
|
-
const assets = (statsData.assets ?? []).sort((a, b) => b.size - a.size).slice(0, 20);
|
|
2755
|
-
const totalSize = assets.reduce((s, a) => s + (a.size ?? 0), 0);
|
|
2756
|
-
const summary = JSON.stringify({
|
|
2757
|
-
total_assets: assets.length,
|
|
2758
|
-
total_size_kb: Math.round(totalSize / 1024),
|
|
2759
|
-
top_assets: assets.slice(0, 10).map(a => ({ name: a.name, size_kb: Math.round(a.size / 1024) })),
|
|
2760
|
-
}, null, 2);
|
|
2761
|
-
const bundleResult = await executeOne({
|
|
2762
|
-
id: 'bundle-1',
|
|
2763
|
-
agent: 'frontend',
|
|
2764
|
-
task: 'Analyze this bundle stats and provide: (1) Top 10 heaviest modules to target, (2) Duplicate packages to deduplicate, (3) Code-split candidates (lazy-loadable routes or heavy features), (4) Packages safe to move to CDN externals (React, lodash, etc.), (5) Estimated size reduction achievable.',
|
|
2765
|
-
code: summary,
|
|
2766
|
-
});
|
|
2767
|
-
recordOutcome('bundle_advisor', 50, 2, 'frontend', bundleResult.analysis?.score ?? Math.round(bundleResult.output.confidence * 100));
|
|
2768
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2769
|
-
total_size_kb: Math.round(totalSize / 1024),
|
|
2770
|
-
assets_analyzed: assets.length,
|
|
2771
|
-
heaviest_modules: assets.slice(0, 10).map(a => ({ name: a.name, size_kb: Math.round(a.size / 1024) })),
|
|
2772
|
-
recommendations: bundleResult.plan?.approach ?? bundleResult.output.recommendation ?? '',
|
|
2773
|
-
estimated_reduction_pct: 0,
|
|
2774
|
-
}, null, 2) }] };
|
|
2775
|
-
}
|
|
2776
|
-
// ── veto_dead_code ──────────────────────────────────────────────────────────
|
|
2777
|
-
case 'veto_dead_code': {
|
|
2778
|
-
const args = (request.params.arguments || {});
|
|
2779
|
-
const projectDir = String(args?.project_dir ?? '').trim();
|
|
2780
|
-
const exts = Array.isArray(args?.extensions) ? args.extensions.map(String) : ['.ts', '.js'];
|
|
2781
|
-
const includeArgs = exts.map(e => `--include="*${e}"`).join(' ');
|
|
2782
|
-
const patterns = [
|
|
2783
|
-
{ label: 'exported but possibly unused', regex: 'export (function|const|class|interface|type)' },
|
|
2784
|
-
{ label: 'TODO/FIXME markers', regex: '// (TODO|FIXME|HACK|XXX)' },
|
|
2785
|
-
{ label: 'feature flag patterns', regex: 'if.*flags?\\.\\w+|if.*feature.*enabled|if.*isEnabled' },
|
|
2786
|
-
{ label: 'commented-out code blocks', regex: '^\\/\\/' },
|
|
2787
|
-
];
|
|
2788
|
-
let findings = '';
|
|
2789
|
-
for (const { label, regex } of patterns) {
|
|
2790
|
-
try {
|
|
2791
|
-
const out = execSyncTop(`git grep -rn "${regex}" ${includeArgs} -- . ":(exclude)node_modules" ":(exclude)dist"`, { cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString();
|
|
2792
|
-
const lines = out.split('\n').filter(Boolean).slice(0, 20);
|
|
2793
|
-
if (lines.length > 0)
|
|
2794
|
-
findings += `\n=== ${label} (${lines.length} found) ===\n${lines.join('\n')}`;
|
|
2795
|
-
}
|
|
2796
|
-
catch { /* no matches — git grep exits 1 */ }
|
|
2797
|
-
}
|
|
2798
|
-
if (!findings)
|
|
2799
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, dead_code_items: [], summary: 'No dead code patterns detected.' }) }] };
|
|
2800
|
-
const ctx = buildContextString(projectDir);
|
|
2801
|
-
const deadResult = await executeOne({
|
|
2802
|
-
id: 'dead-1',
|
|
2803
|
-
agent: 'code-quality',
|
|
2804
|
-
task: 'Identify dead code and safe deletion candidates from these patterns. For each item: (1) is it actually dead/unused?, (2) safe to delete?, (3) deletion risk (high/medium/low). Focus on exports with zero imports, always-true/false flags, and commented blocks older than 6 months.',
|
|
2805
|
-
code: findings.slice(0, 6000),
|
|
2806
|
-
context: ctx || undefined,
|
|
2807
|
-
});
|
|
2808
|
-
recordOutcome('dead_code', 50, 2, 'code-quality', deadResult.analysis?.score ?? Math.round(deadResult.output.confidence * 100));
|
|
2809
|
-
const agentOut = deadResult.plan?.approach ?? deadResult.output.recommendation ?? '';
|
|
2810
|
-
const safeMatches = agentOut.match(/\blow\b.*\bdelete\b|\bsafe to delete\b|\bsafely removed\b/gi) ?? [];
|
|
2811
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2812
|
-
dead_code_items: [],
|
|
2813
|
-
total_found: findings.split('\n').filter(l => l.startsWith('===')).length,
|
|
2814
|
-
safe_to_delete: safeMatches.length,
|
|
2815
|
-
recommendations: agentOut,
|
|
2816
|
-
council_note: 'Run veto_council_debate before deleting any exports to check downstream impact.',
|
|
2817
|
-
}, null, 2) }] };
|
|
2818
|
-
}
|
|
2819
|
-
// ── veto_hitl_checkpoint ────────────────────────────────────────────────────
|
|
2820
|
-
case 'veto_hitl_checkpoint': {
|
|
2821
|
-
const args = (request.params.arguments || {});
|
|
2822
|
-
const stage = String(args?.stage ?? '').trim();
|
|
2823
|
-
const context = String(args?.context ?? '').trim();
|
|
2824
|
-
const riskLevel = String(args?.risk_level ?? 'medium');
|
|
2825
|
-
const workflowId = args?.workflow_id ? String(args.workflow_id) : null;
|
|
2826
|
-
const options = Array.isArray(args?.options) ? args.options.map(String) : ['Approve', 'Reject', 'Modify'];
|
|
2827
|
-
if (!stage || !context)
|
|
2828
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'stage and context are required.' }) }], isError: true };
|
|
2829
|
-
const riskEmoji = { low: '🟢', medium: '🟡', high: '🟠', critical: '🔴' }[riskLevel] ?? '🟡';
|
|
2830
|
-
const checkpoint_id = `hitl-${Date.now().toString(36)}`;
|
|
2831
|
-
const formatted = [
|
|
2832
|
-
`## ⏸️ Human-in-the-Loop Checkpoint`,
|
|
2833
|
-
``,
|
|
2834
|
-
`**Stage:** ${stage}${workflowId ? ` (workflow: ${workflowId})` : ''}`,
|
|
2835
|
-
`**Risk:** ${riskEmoji} ${riskLevel.toUpperCase()}`,
|
|
2836
|
-
``,
|
|
2837
|
-
`### What is about to happen`,
|
|
2838
|
-
context,
|
|
2839
|
-
``,
|
|
2840
|
-
`### Your response options`,
|
|
2841
|
-
options.map((o, i) => `${i + 1}. **${o}**`).join('\n'),
|
|
2842
|
-
``,
|
|
2843
|
-
`_Respond with your choice to continue the workflow. The agent is waiting._`,
|
|
2844
|
-
].join('\n');
|
|
2845
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2846
|
-
checkpoint_id,
|
|
2847
|
-
stage,
|
|
2848
|
-
risk_level: riskLevel,
|
|
2849
|
-
status: 'waiting_for_approval',
|
|
2850
|
-
options,
|
|
2851
|
-
formatted_request: formatted,
|
|
2852
|
-
workflow_id: workflowId,
|
|
2853
|
-
created_at: new Date().toISOString(),
|
|
2854
|
-
}, null, 2) }] };
|
|
2855
|
-
}
|
|
2856
|
-
// ── veto_openapi_gen ────────────────────────────────────────────────────────
|
|
2857
|
-
case 'veto_openapi_gen': {
|
|
2858
|
-
const args = (request.params.arguments || {});
|
|
2859
|
-
const filePath = args?.file_path ? String(args.file_path) : null;
|
|
2860
|
-
const projectDir = args?.project_dir ? String(args.project_dir) : null;
|
|
2861
|
-
const writeFileArg = args?.write_file === true;
|
|
2862
|
-
const framework = String(args?.framework ?? 'auto');
|
|
2863
|
-
let routeContent = '';
|
|
2864
|
-
if (filePath) {
|
|
2865
|
-
try {
|
|
2866
|
-
routeContent = readFileSync(filePath, 'utf8').slice(0, 10000);
|
|
2867
|
-
}
|
|
2868
|
-
catch (e) {
|
|
2869
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `Cannot read ${filePath}: ${e}` }) }], isError: true };
|
|
2870
|
-
}
|
|
2871
|
-
}
|
|
2872
|
-
else if (projectDir) {
|
|
2873
|
-
try {
|
|
2874
|
-
const candidates = execSyncTop('git ls-files --cached -- "*.ts" "*.js" "*.py"', { cwd: projectDir, timeout: 4000, stdio: ['pipe', 'pipe', 'pipe'] }).toString().split('\n').filter((f) => /route|router|api|endpoint|controller/i.test(f) && !f.includes('node_modules') && !f.includes('dist/')).slice(0, 5);
|
|
2875
|
-
for (const f of candidates) {
|
|
2876
|
-
try {
|
|
2877
|
-
routeContent += `\n// FILE: ${f}\n${readFileSync(join(projectDir, f), 'utf8').slice(0, 3000)}\n`;
|
|
2878
|
-
}
|
|
2879
|
-
catch { /* skip */ }
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
catch { /* not a git repo */ }
|
|
2883
|
-
}
|
|
2884
|
-
if (!routeContent)
|
|
2885
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'No route files found. Provide file_path or project_dir with route files.' }) }], isError: true };
|
|
2886
|
-
const openapiResult = await executeOne({
|
|
2887
|
-
id: 'openapi-1',
|
|
2888
|
-
agent: 'api',
|
|
2889
|
-
task: `Generate a complete OpenAPI 3.1 specification in YAML format for these ${framework === 'auto' ? 'API' : framework} route definitions. Include: info block (title, version), servers, all paths with HTTP methods, request body schemas, response schemas (200, 400, 401, 404, 500), and security schemes if auth is detected. Output ONLY valid YAML — no markdown fences, no explanation.`,
|
|
2890
|
-
code: routeContent,
|
|
2891
|
-
});
|
|
2892
|
-
const rawSpec = openapiResult.plan?.approach ?? openapiResult.output.recommendation ?? '';
|
|
2893
|
-
const specLines = rawSpec.split('\n');
|
|
2894
|
-
const specStart = specLines.findIndex((l) => /^(openapi:|info:)/.test(l.trim()));
|
|
2895
|
-
const spec = specStart >= 0 ? specLines.slice(specStart).join('\n').trim() : rawSpec.trim();
|
|
2896
|
-
let writtenTo = null;
|
|
2897
|
-
if (writeFileArg && projectDir && spec) {
|
|
2898
|
-
try {
|
|
2899
|
-
const outPath = join(projectDir, 'openapi.yaml');
|
|
2900
|
-
writeFileSync(outPath, spec, 'utf8');
|
|
2901
|
-
writtenTo = outPath;
|
|
2902
|
-
}
|
|
2903
|
-
catch { /* skip write errors */ }
|
|
2904
|
-
}
|
|
2905
|
-
const routeLineCount = (routeContent.match(/\bget\b|\bpost\b|\bput\b|\bpatch\b|\bdelete\b/gi) ?? []).length;
|
|
2906
|
-
recordOutcome('openapi_gen', 50, 2, 'api', openapiResult.analysis?.score ?? Math.round(openapiResult.output.confidence * 100));
|
|
2907
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2908
|
-
spec,
|
|
2909
|
-
written_to: writtenTo,
|
|
2910
|
-
routes_detected: routeLineCount,
|
|
2911
|
-
framework,
|
|
2912
|
-
}, null, 2) }] };
|
|
2913
|
-
}
|
|
2914
|
-
// ── veto_flag_auditor ───────────────────────────────────────────────────────
|
|
2915
|
-
case 'veto_flag_auditor': {
|
|
2916
|
-
const args = (request.params.arguments || {});
|
|
2917
|
-
const projectDir = String(args?.project_dir ?? '').trim();
|
|
2918
|
-
const sdk = String(args?.sdk ?? 'auto');
|
|
2919
|
-
if (!projectDir)
|
|
2920
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
2921
|
-
const patterns = [
|
|
2922
|
-
{ label: 'LaunchDarkly', regex: 'ldClient\\.variation|isFeatureEnabled|client\\.boolVariation' },
|
|
2923
|
-
{ label: 'Unleash', regex: 'isEnabled\\(|getVariant\\(|unleash\\.isEnabled' },
|
|
2924
|
-
{ label: 'Custom flags', regex: 'flags?\\[|flags?\\.\\w+|feature[Ff]lag|isFeature|FEATURE_' },
|
|
2925
|
-
{ label: 'Env-based flags', regex: 'process\\.env\\.FEATURE_|process\\.env\\.ENABLE_|process\\.env\\.FF_' },
|
|
2926
|
-
];
|
|
2927
|
-
let findings = '';
|
|
2928
|
-
let totalMatches = 0;
|
|
2929
|
-
for (const { label, regex } of patterns) {
|
|
2930
|
-
try {
|
|
2931
|
-
const out = execSyncTop(`git grep -rn "${regex}" --include="*.ts" --include="*.js" --include="*.py" -- . ":(exclude)node_modules" ":(exclude)dist"`, { cwd: projectDir, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).toString();
|
|
2932
|
-
const lines = out.split('\n').filter(Boolean);
|
|
2933
|
-
totalMatches += lines.length;
|
|
2934
|
-
if (lines.length > 0)
|
|
2935
|
-
findings += `\n=== ${label} (${lines.length} occurrences) ===\n${lines.slice(0, 15).join('\n')}`;
|
|
2936
|
-
}
|
|
2937
|
-
catch { /* no matches */ }
|
|
2938
|
-
}
|
|
2939
|
-
if (!findings)
|
|
2940
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, flags_found: 0, flag_items: [], summary: 'No feature flag patterns detected.' }) }] };
|
|
2941
|
-
const flagResult = await executeOne({
|
|
2942
|
-
id: 'flags-1',
|
|
2943
|
-
agent: 'code-quality',
|
|
2944
|
-
task: 'Analyze these feature flag usages and classify each unique flag as: (1) ACTIVE — still toggled in code and worth keeping, (2) CANDIDATE_REMOVAL — always-true/always-false or deprecated, (3) ORPHANED — referenced but flag definition not found. For each, provide: flag name, classification, last-seen location, and safe-to-remove assessment.',
|
|
2945
|
-
code: findings.slice(0, 6000),
|
|
2946
|
-
});
|
|
2947
|
-
const agentOut = flagResult.plan?.approach ?? flagResult.output.recommendation ?? '';
|
|
2948
|
-
recordOutcome('flag_audit', 50, 2, 'code-quality', flagResult.analysis?.score ?? Math.round(flagResult.output.confidence * 100));
|
|
2949
|
-
// Parse a rough count from agentOut heuristics
|
|
2950
|
-
const activeCount = (agentOut.match(/ACTIVE/g) ?? []).length;
|
|
2951
|
-
const removalCount = (agentOut.match(/CANDIDATE_REMOVAL/g) ?? []).length;
|
|
2952
|
-
const orphanedCount = (agentOut.match(/ORPHANED/g) ?? []).length;
|
|
2953
|
-
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2954
|
-
flags_found: totalMatches,
|
|
2955
|
-
active: activeCount,
|
|
2956
|
-
candidate_removal: removalCount,
|
|
2957
|
-
orphaned: orphanedCount,
|
|
2958
|
-
flag_items: [],
|
|
2959
|
-
recommendations: agentOut,
|
|
2960
|
-
sdk_detected: sdk === 'auto' ? (findings.includes('ldClient') ? 'launchdarkly' : findings.includes('unleash') ? 'unleash' : 'custom') : sdk,
|
|
2961
|
-
council_note: 'Run veto_council_debate before removing any flags to assess downstream risk.',
|
|
2962
|
-
}, null, 2) }] };
|
|
2963
|
-
}
|
|
2964
|
-
// ── Phase 7: Intelligence & Advanced ──────────────────────────────────────
|
|
2965
|
-
case 'veto_local_llm': {
|
|
2966
|
-
const args = (request.params.arguments || {});
|
|
2967
|
-
const { task, model, provider } = args;
|
|
2968
|
-
const { callLocalLlm } = await import('./agents/local-llm.js');
|
|
2969
|
-
const result = await callLocalLlm({ task: String(task), model: model ? String(model) : undefined, provider: provider });
|
|
2970
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2971
|
-
}
|
|
2972
|
-
case 'veto_clone_detector': {
|
|
2973
|
-
const args = (request.params.arguments || {});
|
|
2974
|
-
const projectDir = String(args?.project_dir ?? '').trim();
|
|
2975
|
-
const extensions = Array.isArray(args?.extensions) ? args.extensions.map(String) : undefined;
|
|
2976
|
-
const minLines = typeof args?.min_lines === 'number' ? args.min_lines : undefined;
|
|
2977
|
-
const { detectClones } = await import('./agents/quality/clone-detector.js');
|
|
2978
|
-
const findings = await detectClones({ project_dir: projectDir, extensions, min_lines: minLines });
|
|
2979
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, clones_found: findings.length, findings }, null, 2) }] };
|
|
2980
|
-
}
|
|
2981
|
-
case 'veto_lint_rules': {
|
|
2982
|
-
const args = (request.params.arguments || {});
|
|
2983
|
-
return await handleAgenticWorker('veto_lint_rules', args, 'reviewer', 'Analyze and generate lint rules.');
|
|
2984
|
-
}
|
|
2985
|
-
case 'veto_api_contract': {
|
|
2986
|
-
const args = (request.params.arguments || {});
|
|
2987
|
-
return await handleAgenticWorker('veto_api_contract', args, 'api', 'Analyze or generate API contracts.');
|
|
2988
|
-
}
|
|
2989
|
-
case 'veto_merge_conflict': {
|
|
2990
|
-
const args = (request.params.arguments || {});
|
|
2991
|
-
return await handleAgenticWorker('veto_merge_conflict', args, 'debugger', 'Resolve git merge conflicts.');
|
|
2992
|
-
}
|
|
2993
|
-
case 'veto_translate': {
|
|
2994
|
-
const args = (request.params.arguments || {});
|
|
2995
|
-
return await handleAgenticWorker('veto_translate', args, 'documentation', 'Translate text.');
|
|
2996
|
-
}
|
|
2997
|
-
case 'veto_a11y_advisor': {
|
|
2998
|
-
const args = (request.params.arguments || {});
|
|
2999
|
-
return await handleAgenticWorker('veto_a11y_advisor', args, 'accessibility', 'Analyze accessibility.');
|
|
3000
|
-
}
|
|
3001
|
-
case 'veto_session_replay': {
|
|
3002
|
-
const args = (request.params.arguments || {});
|
|
3003
|
-
const sessionId = String(args?.session_id ?? '').trim();
|
|
3004
|
-
if (!sessionId)
|
|
3005
|
-
return { content: [{ type: 'text', text: 'session_id is required.' }], isError: true };
|
|
3006
|
-
const traces = getSessionReplay(sessionId);
|
|
3007
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, session_id: sessionId, events: traces }, null, 2) }] };
|
|
3008
|
-
}
|
|
3009
|
-
case 'veto_compose_agents': {
|
|
3010
|
-
const args = (request.params.arguments || {});
|
|
3011
|
-
const { name, agents, workflow } = args;
|
|
3012
|
-
// Register custom meta-agent in memory (Stub persistence, but functional for the current session)
|
|
3013
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Custom agent ${name} composed and registered.`, definition: { name, base_agents: agents, workflow } }, null, 2) }] };
|
|
3014
|
-
}
|
|
3015
|
-
// ── Phase 8: Long-Horizon ─────────────────────────────────────────────────
|
|
3016
|
-
case 'veto_semantic_search': {
|
|
3017
|
-
const args = (request.params.arguments || {});
|
|
3018
|
-
return await handleAgenticWorker('veto_semantic_search', args, 'search-agent', 'Perform semantic search.');
|
|
3019
|
-
}
|
|
3020
|
-
case 'veto_sdd_agent': {
|
|
3021
|
-
const args = (request.params.arguments || {});
|
|
3022
|
-
return await handleAgenticWorker('veto_sdd_agent', args, 'task-planner', 'Execute SDD actions.');
|
|
3023
|
-
}
|
|
3024
|
-
case 'veto_playwright': {
|
|
3025
|
-
const args = (request.params.arguments || {});
|
|
3026
|
-
return await handleAgenticWorker('veto_playwright', args, 'tester', 'Coordinate Playwright browser session.');
|
|
3027
|
-
}
|
|
3028
|
-
case 'veto_notify_ide': {
|
|
3029
|
-
const args = (request.params.arguments || {});
|
|
3030
|
-
const { action, path, message, level } = args;
|
|
3031
|
-
// In bidirectional MCP, some clients listen for logging or custom notifications
|
|
3032
|
-
if (action === 'show_message' && message) {
|
|
3033
|
-
await server.sendLoggingMessage({
|
|
3034
|
-
level: level === 'error' ? 'error' : level === 'warning' ? 'warning' : 'info',
|
|
3035
|
-
data: message,
|
|
3036
|
-
});
|
|
3037
|
-
}
|
|
3038
|
-
return { content: [{ type: 'text', text: JSON.stringify({ success: true, action, message: `Action ${action} sent to IDE client.` }, null, 2) }] };
|
|
3039
|
-
}
|
|
3040
|
-
default: throw new Error(`Unknown tool: ${name}`);
|
|
3041
|
-
}
|
|
150
|
+
const registered = TOOL_REGISTRY[name];
|
|
151
|
+
if (registered)
|
|
152
|
+
return await registered({ request, args: request.params.arguments || {}, server });
|
|
153
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
3042
154
|
})();
|
|
3043
155
|
if (response && typeof response === 'object' && 'isError' in response && response.isError) {
|
|
3044
156
|
resultStatus = 'error';
|
|
3045
157
|
errorMessage = response.content?.[0]?.text || 'Unknown MCP error';
|
|
158
|
+
log.warn('tool returned an error result', { tool: name, error: errorMessage });
|
|
3046
159
|
}
|
|
3047
160
|
return response;
|
|
3048
161
|
}
|
|
3049
162
|
catch (err) {
|
|
3050
163
|
resultStatus = 'error';
|
|
3051
|
-
errorMessage = err
|
|
164
|
+
errorMessage = errMsg(err);
|
|
165
|
+
log.error('tool call threw', { tool: name, error: errorMessage });
|
|
3052
166
|
throw err;
|
|
3053
167
|
}
|
|
3054
168
|
finally {
|
|
@@ -3066,11 +180,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3066
180
|
});
|
|
3067
181
|
}
|
|
3068
182
|
catch (logErr) {
|
|
3069
|
-
|
|
183
|
+
log.warn('failed to record tool call trace', { tool: name, error: errMsg(logErr) });
|
|
3070
184
|
}
|
|
3071
185
|
}
|
|
3072
186
|
}
|
|
3073
|
-
}
|
|
187
|
+
}
|
|
188
|
+
server.setRequestHandler(CallToolRequestSchema, callTool);
|
|
3074
189
|
// ─── MCP Resources ─────────────────────────────────────────────────────────────
|
|
3075
190
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
3076
191
|
resources: [
|
|
@@ -3443,8 +558,13 @@ async function main() {
|
|
|
3443
558
|
await server.connect(transport);
|
|
3444
559
|
process.stderr.write(`Veto MCP server v${VERSION} running (stdio)\n`);
|
|
3445
560
|
}
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
561
|
+
// Only connect stdio when run as the entrypoint — importing this module (e.g. in
|
|
562
|
+
// tests) registers handlers without starting the transport.
|
|
563
|
+
const isEntrypoint = !!process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
564
|
+
if (isEntrypoint) {
|
|
565
|
+
main().catch((err) => {
|
|
566
|
+
log.error('fatal: server failed to start', { error: errMsg(err) });
|
|
567
|
+
process.exit(1);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
3450
570
|
//# sourceMappingURL=server.js.map
|