@jigyasudham/veto 1.2.16 → 1.2.19
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 +31 -6
- package/dist/cli.js +173 -14
- package/dist/cli.js.map +1 -1
- package/dist/memory/local.d.ts +25 -0
- package/dist/memory/local.d.ts.map +1 -1
- package/dist/memory/local.js +73 -3
- package/dist/memory/local.js.map +1 -1
- package/dist/memory/schema.d.ts +2 -1
- package/dist/memory/schema.d.ts.map +1 -1
- package/dist/memory/schema.js +14 -0
- package/dist/memory/schema.js.map +1 -1
- package/dist/server.js +188 -14
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Veto MCP Server —
|
|
2
|
+
// Veto MCP Server — 46 tools, 21 phases complete, auto-learning router
|
|
3
3
|
// Suppress node:sqlite experimental warning — it would corrupt the MCP stdio protocol
|
|
4
4
|
process.removeAllListeners('warning');
|
|
5
5
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { buildContextString } from './context/reader.js';
|
|
9
|
-
import { saveSession, restoreSession, listSessions, closeSession, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, getContextStatus, fetchAndCacheDocs, saveTaskPlan, getUsageStatus, getAuditLog, getHealthStats, CONTEXT_WINDOWS, logUsage, getUsageLogs, } from './memory/local.js';
|
|
9
|
+
import { saveSession, restoreSession, listSessions, closeSession, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, getContextStatus, fetchAndCacheDocs, saveTaskPlan, getUsageStatus, getAuditLog, getHealthStats, CONTEXT_WINDOWS, logUsage, getUsageLogs, getDb, storeScanDiagnostics, clearScanDiagnostics, updateSession, } from './memory/local.js';
|
|
10
10
|
import { exportMemory, importMemory } from './memory/sync.js';
|
|
11
11
|
import { runDebate } from './council/index.js';
|
|
12
12
|
import { routeTask, getRateStatus, trackTokens, recordOutcome, getLearningStats, getLearnedThresholds, applyLearnedThresholds, getAgentPerformanceStats, getTaskTypeBreakdown, getCouncilInsights, getRecommendedAgent } from './router/index.js';
|
|
@@ -52,7 +52,7 @@ function maybeAutoSave(token_count, platform) {
|
|
|
52
52
|
if (elapsed < autoSave.cooldown_ms)
|
|
53
53
|
return { triggered: false };
|
|
54
54
|
}
|
|
55
|
-
const result = saveSession({ ...autoSave.cached, token_count, platform });
|
|
55
|
+
const result = saveSession({ ...autoSave.cached, token_count, platform, save_type: 'auto' });
|
|
56
56
|
autoSave.last_save_at = result.saved_at;
|
|
57
57
|
autoSave.last_session_id = result.session_id;
|
|
58
58
|
return { triggered: true, session_id: result.session_id, usage_pct };
|
|
@@ -90,6 +90,7 @@ const TOOL_ANNOTATIONS = {
|
|
|
90
90
|
veto_discover: { readOnlyHint: true },
|
|
91
91
|
veto_summarize: { readOnlyHint: true },
|
|
92
92
|
veto_explain: { readOnlyHint: true },
|
|
93
|
+
veto_benchmark: { readOnlyHint: false, destructiveHint: false },
|
|
93
94
|
// read-only + open world (external network)
|
|
94
95
|
veto_docs_fetch: { readOnlyHint: true, openWorldHint: true },
|
|
95
96
|
veto_pr_review: { readOnlyHint: true, openWorldHint: true },
|
|
@@ -150,7 +151,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
150
151
|
},
|
|
151
152
|
{
|
|
152
153
|
name: 'veto_session_save',
|
|
153
|
-
description: 'Saves the current session context to SQLite
|
|
154
|
+
description: 'Saves the current session context to SQLite. Pass session_id to update an existing session instead of creating a new one — use this when refreshing context mid-conversation rather than starting a new snapshot.',
|
|
154
155
|
inputSchema: {
|
|
155
156
|
type: 'object',
|
|
156
157
|
properties: {
|
|
@@ -184,6 +185,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
184
185
|
type: 'number',
|
|
185
186
|
description: 'Approximate tokens used this session. Veto uses this for context window monitoring.',
|
|
186
187
|
},
|
|
188
|
+
session_id: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
description: 'Optional. UUID of an existing session to update in-place. When provided, Veto updates that row instead of inserting a new one — prevents session inflation when refreshing mid-conversation.',
|
|
191
|
+
},
|
|
187
192
|
},
|
|
188
193
|
required: ['summary', 'context'],
|
|
189
194
|
},
|
|
@@ -322,12 +327,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
322
327
|
},
|
|
323
328
|
{
|
|
324
329
|
name: 'veto_code_review',
|
|
325
|
-
description: 'Runs the Code Reviewer agent on provided code. Returns scored findings (complexity, error handling, magic numbers, nesting, dead code) with severity and fixes.',
|
|
330
|
+
description: 'Runs the Code Reviewer agent on provided code. Returns scored findings (complexity, error handling, magic numbers, nesting, dead code) with severity and fixes. Pass file_path to surface findings as VS Code inline diagnostics (squiggles).',
|
|
326
331
|
inputSchema: {
|
|
327
332
|
type: 'object',
|
|
328
333
|
properties: {
|
|
329
334
|
code: { type: 'string', description: 'The code to review.' },
|
|
330
335
|
context: { type: 'string', description: 'Optional: file name, module description, or review focus.' },
|
|
336
|
+
file_path: { type: 'string', description: 'Optional: absolute path to the file being reviewed. When provided, findings are stored as VS Code inline diagnostics.' },
|
|
331
337
|
},
|
|
332
338
|
required: ['code'],
|
|
333
339
|
},
|
|
@@ -347,23 +353,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
347
353
|
},
|
|
348
354
|
{
|
|
349
355
|
name: 'veto_security_scan',
|
|
350
|
-
description: 'Runs the Security Scanner (OWASP Top 10) on provided code. Returns vulnerabilities with severity, CWE/OWASP category, and remediation steps.',
|
|
356
|
+
description: 'Runs the Security Scanner (OWASP Top 10) on provided code. Returns vulnerabilities with severity, CWE/OWASP category, and remediation steps. Pass file_path to surface findings as VS Code inline diagnostics.',
|
|
351
357
|
inputSchema: {
|
|
352
358
|
type: 'object',
|
|
353
359
|
properties: {
|
|
354
360
|
code: { type: 'string', description: 'The code to scan.' },
|
|
355
361
|
context: { type: 'string', description: 'Optional: language, framework, or specific concerns.' },
|
|
362
|
+
file_path: { type: 'string', description: 'Optional: absolute path to the file being scanned. When provided, findings are stored as VS Code inline diagnostics.' },
|
|
356
363
|
},
|
|
357
364
|
required: ['code'],
|
|
358
365
|
},
|
|
359
366
|
},
|
|
360
367
|
{
|
|
361
368
|
name: 'veto_secrets_scan',
|
|
362
|
-
description: 'Scans text or code for exposed credentials — API keys, tokens, passwords, connection strings, private keys. Returns findings with masked values and line numbers.',
|
|
369
|
+
description: 'Scans text or code for exposed credentials — API keys, tokens, passwords, connection strings, private keys. Returns findings with masked values and line numbers. Pass file_path to surface findings as VS Code inline diagnostics.',
|
|
363
370
|
inputSchema: {
|
|
364
371
|
type: 'object',
|
|
365
372
|
properties: {
|
|
366
373
|
text: { type: 'string', description: 'The text or code to scan for secrets.' },
|
|
374
|
+
file_path: { type: 'string', description: 'Optional: absolute path to the file being scanned. When provided, findings are stored as VS Code inline diagnostics.' },
|
|
367
375
|
},
|
|
368
376
|
required: ['text'],
|
|
369
377
|
},
|
|
@@ -825,6 +833,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
825
833
|
required: [],
|
|
826
834
|
},
|
|
827
835
|
},
|
|
836
|
+
{
|
|
837
|
+
name: 'veto_benchmark',
|
|
838
|
+
description: 'Compares two competing approaches by running a full council debate on each in parallel, then returns a structured winner analysis with verdict, confidence delta, warning counts, and council reasoning. Use when you have two valid options and want an unbiased council judgment before committing.',
|
|
839
|
+
inputSchema: {
|
|
840
|
+
type: 'object',
|
|
841
|
+
properties: {
|
|
842
|
+
task: { type: 'string', description: 'The decision context — what problem are both approaches solving?' },
|
|
843
|
+
approach_a: { type: 'string', description: 'First approach to evaluate. Be specific about tech choices, trade-offs, and constraints.' },
|
|
844
|
+
approach_b: { type: 'string', description: 'Second approach to evaluate. Same level of detail as approach_a.' },
|
|
845
|
+
context: { type: 'string', description: 'Optional: shared context for both debates (architecture notes, constraints, team size, etc.).' },
|
|
846
|
+
project_dir: { type: 'string', description: 'Optional: auto-inject package.json and git diff context.' },
|
|
847
|
+
},
|
|
848
|
+
required: ['task', 'approach_a', 'approach_b'],
|
|
849
|
+
},
|
|
850
|
+
},
|
|
828
851
|
];
|
|
829
852
|
return { tools: tools.map(t => ({ ...t, annotations: TOOL_ANNOTATIONS[t.name] ?? {} })) };
|
|
830
853
|
});
|
|
@@ -847,11 +870,31 @@ async function runTripleScan(diff, context) {
|
|
|
847
870
|
autoRecord('scan', 'secrets', (secretsResult.analysis?.findings?.length ?? 0) === 0 ? 100 : secretsResult.analysis?.score ?? Math.round(secretsResult.output.confidence * 100));
|
|
848
871
|
return { reviewResult, secResult, secretsResult, verdict };
|
|
849
872
|
}
|
|
873
|
+
function parseLineFromLocation(location) {
|
|
874
|
+
if (!location)
|
|
875
|
+
return 1;
|
|
876
|
+
const m = location.match(/(?:^|line\s+)(\d+)/i);
|
|
877
|
+
return m ? parseInt(m[1], 10) : 1;
|
|
878
|
+
}
|
|
879
|
+
function mapSeverityToVscode(severity) {
|
|
880
|
+
if (severity === 'critical' || severity === 'high')
|
|
881
|
+
return 'error';
|
|
882
|
+
if (severity === 'medium')
|
|
883
|
+
return 'warning';
|
|
884
|
+
return 'information';
|
|
885
|
+
}
|
|
850
886
|
// Auto-learning helper — records a learning_data row from any agent result.
|
|
851
887
|
// Keeps call sites to one line rather than repeating the tier/quality logic.
|
|
888
|
+
// After every 20 outcomes the thresholds are applied automatically.
|
|
852
889
|
function autoRecord(taskType, agent, quality, complexity = 50) {
|
|
853
890
|
const tier = quality >= 80 ? 1 : quality >= 40 ? 2 : 3;
|
|
854
891
|
recordOutcome(taskType.slice(0, 50), complexity, tier, agent, quality);
|
|
892
|
+
try {
|
|
893
|
+
const count = getDb().prepare('SELECT COUNT(*) as c FROM learning_data').get().c;
|
|
894
|
+
if (count >= 20 && count % 20 === 0)
|
|
895
|
+
applyLearnedThresholds();
|
|
896
|
+
}
|
|
897
|
+
catch { /* never interrupt the caller */ }
|
|
855
898
|
}
|
|
856
899
|
// Auto-store helper — writes a knowledge_base entry when a scan produces critical/blocking issues.
|
|
857
900
|
// This is what populates the Memory panel in the VS Code extension automatically.
|
|
@@ -938,7 +981,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
938
981
|
const saveSummary = String(args?.summary ?? '');
|
|
939
982
|
const saveContext = String(args?.context ?? '');
|
|
940
983
|
const saveTaskState = args?.task_state ? String(args.task_state) : undefined;
|
|
941
|
-
const
|
|
984
|
+
const existingId = args?.session_id ? String(args.session_id) : undefined;
|
|
985
|
+
const sessionInput = {
|
|
942
986
|
summary: saveSummary,
|
|
943
987
|
context: saveContext,
|
|
944
988
|
task_state: saveTaskState,
|
|
@@ -946,20 +990,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
946
990
|
connection_type: args?.connection_type ? String(args.connection_type) : 'subscription',
|
|
947
991
|
project_dir: sessionProjectDir,
|
|
948
992
|
token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
|
|
949
|
-
}
|
|
993
|
+
};
|
|
994
|
+
let result;
|
|
995
|
+
let wasUpdate = false;
|
|
996
|
+
if (existingId) {
|
|
997
|
+
const updated = updateSession(existingId, sessionInput);
|
|
998
|
+
if (updated) {
|
|
999
|
+
result = { session_id: updated.session_id, saved_at: updated.saved_at, usage_pct: 0, context_warning: false, continuation_prompt: null };
|
|
1000
|
+
wasUpdate = true;
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
result = saveSession(sessionInput);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
result = saveSession(sessionInput);
|
|
1008
|
+
}
|
|
950
1009
|
// Cache for auto-save: future veto_status calls with high token_count will re-save this context
|
|
951
1010
|
autoSave.cached = { summary: saveSummary, context: saveContext, task_state: saveTaskState, platform: savePlatform, project_dir: sessionProjectDir };
|
|
952
1011
|
autoSave.last_save_at = result.saved_at;
|
|
953
1012
|
autoSave.last_session_id = result.session_id;
|
|
954
1013
|
const responseObj = {
|
|
955
1014
|
success: true,
|
|
956
|
-
message:
|
|
957
|
-
?
|
|
958
|
-
:
|
|
1015
|
+
message: wasUpdate
|
|
1016
|
+
? `Session updated in-place. ID unchanged: ${result.session_id}`
|
|
1017
|
+
: result.context_warning
|
|
1018
|
+
? `⚠️ Context at ${result.usage_pct}% — consider handing off soon.`
|
|
1019
|
+
: 'Session saved. Use this ID to restore on any AI platform.',
|
|
959
1020
|
session_id: result.session_id,
|
|
960
1021
|
saved_at: result.saved_at,
|
|
961
|
-
|
|
962
|
-
context_warning: result.context_warning,
|
|
1022
|
+
updated: wasUpdate,
|
|
1023
|
+
...(wasUpdate ? {} : { usage_pct: result.usage_pct, context_warning: result.context_warning }),
|
|
963
1024
|
};
|
|
964
1025
|
if (result.continuation_prompt)
|
|
965
1026
|
responseObj.continuation_prompt = result.continuation_prompt;
|
|
@@ -1170,11 +1231,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1170
1231
|
if (!code) {
|
|
1171
1232
|
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'code is required.' }) }], isError: true };
|
|
1172
1233
|
}
|
|
1234
|
+
const reviewFilePath = args?.file_path ? String(args.file_path) : undefined;
|
|
1173
1235
|
const result = await executeOne({ id: 'review-1', agent: 'reviewer', task: 'review this code', code, context: args?.context ? String(args.context) : undefined });
|
|
1174
1236
|
if (result.error) {
|
|
1175
1237
|
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: result.error }) }], isError: true };
|
|
1176
1238
|
}
|
|
1177
1239
|
autoRecord('code review', 'reviewer', result.analysis?.score ?? Math.round(result.output.confidence * 100));
|
|
1240
|
+
if (reviewFilePath && result.analysis?.findings) {
|
|
1241
|
+
storeScanDiagnostics(reviewFilePath, result.analysis.findings.map((f) => ({
|
|
1242
|
+
line: parseLineFromLocation(f.location),
|
|
1243
|
+
message: f.description ?? '',
|
|
1244
|
+
severity: mapSeverityToVscode(f.severity),
|
|
1245
|
+
})), 'code-review');
|
|
1246
|
+
}
|
|
1247
|
+
else if (reviewFilePath) {
|
|
1248
|
+
clearScanDiagnostics(reviewFilePath);
|
|
1249
|
+
}
|
|
1178
1250
|
return { content: [{ type: 'text', text: JSON.stringify(result.analysis, null, 2) }] };
|
|
1179
1251
|
}
|
|
1180
1252
|
case 'veto_diff_review': {
|
|
@@ -1265,11 +1337,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1265
1337
|
if (!code) {
|
|
1266
1338
|
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'code is required.' }) }], isError: true };
|
|
1267
1339
|
}
|
|
1340
|
+
const secFilePath = args?.file_path ? String(args.file_path) : undefined;
|
|
1268
1341
|
const result = await executeOne({ id: 'scan-1', agent: 'security-scanner', task: 'scan this code for security issues', code, context: args?.context ? String(args.context) : undefined });
|
|
1269
1342
|
if (result.error) {
|
|
1270
1343
|
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: result.error }) }], isError: true };
|
|
1271
1344
|
}
|
|
1272
1345
|
autoRecord('security scan', 'security-scanner', result.analysis?.score ?? Math.round(result.output.confidence * 100));
|
|
1346
|
+
if (secFilePath && result.analysis?.findings) {
|
|
1347
|
+
storeScanDiagnostics(secFilePath, result.analysis.findings.map((f) => ({
|
|
1348
|
+
line: parseLineFromLocation(f.location),
|
|
1349
|
+
message: f.description ?? '',
|
|
1350
|
+
severity: mapSeverityToVscode(f.severity),
|
|
1351
|
+
})), 'security');
|
|
1352
|
+
}
|
|
1353
|
+
else if (secFilePath) {
|
|
1354
|
+
clearScanDiagnostics(secFilePath);
|
|
1355
|
+
}
|
|
1273
1356
|
return { content: [{ type: 'text', text: JSON.stringify(result.analysis, null, 2) }] };
|
|
1274
1357
|
}
|
|
1275
1358
|
case 'veto_secrets_scan': {
|
|
@@ -1277,11 +1360,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1277
1360
|
if (!text) {
|
|
1278
1361
|
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'text is required.' }) }], isError: true };
|
|
1279
1362
|
}
|
|
1363
|
+
const secretsFilePath = args?.file_path ? String(args.file_path) : undefined;
|
|
1280
1364
|
const result = await executeOne({ id: 'secrets-1', agent: 'secrets', task: 'scan for exposed credentials', code: text });
|
|
1281
1365
|
if (result.error) {
|
|
1282
1366
|
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: result.error }) }], isError: true };
|
|
1283
1367
|
}
|
|
1284
1368
|
autoRecord('secrets scan', 'secrets', (result.analysis?.findings?.length ?? 0) === 0 ? 100 : result.analysis?.score ?? Math.round(result.output.confidence * 100));
|
|
1369
|
+
if (secretsFilePath && result.analysis?.findings) {
|
|
1370
|
+
storeScanDiagnostics(secretsFilePath, result.analysis.findings.map((f) => ({
|
|
1371
|
+
line: parseLineFromLocation(f.location),
|
|
1372
|
+
message: f.description ?? '',
|
|
1373
|
+
severity: mapSeverityToVscode(f.severity),
|
|
1374
|
+
})), 'secrets');
|
|
1375
|
+
}
|
|
1376
|
+
else if (secretsFilePath) {
|
|
1377
|
+
clearScanDiagnostics(secretsFilePath);
|
|
1378
|
+
}
|
|
1285
1379
|
return { content: [{ type: 'text', text: JSON.stringify(result.analysis, null, 2) }] };
|
|
1286
1380
|
}
|
|
1287
1381
|
case 'veto_execute_parallel': {
|
|
@@ -2092,6 +2186,86 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2092
2186
|
agent_used: 'project-mapper', duration_ms: Date.now() - sumStart,
|
|
2093
2187
|
}, null, 2) }] };
|
|
2094
2188
|
}
|
|
2189
|
+
case 'veto_benchmark': {
|
|
2190
|
+
const task = String(args?.task ?? '');
|
|
2191
|
+
const approachA = String(args?.approach_a ?? '');
|
|
2192
|
+
const approachB = String(args?.approach_b ?? '');
|
|
2193
|
+
const ctx = args?.context ? String(args.context) : undefined;
|
|
2194
|
+
const projectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
2195
|
+
if (!task || !approachA || !approachB) {
|
|
2196
|
+
throw new Error('veto_benchmark requires task, approach_a, and approach_b');
|
|
2197
|
+
}
|
|
2198
|
+
const bmStart = Date.now();
|
|
2199
|
+
// Run both debates synchronously (council agents are all sync)
|
|
2200
|
+
const debateA = runDebate({ task: `${task}\n\nApproach A: ${approachA}`, context: ctx, project_dir: projectDir });
|
|
2201
|
+
const debateB = runDebate({ task: `${task}\n\nApproach B: ${approachB}`, context: ctx, project_dir: projectDir });
|
|
2202
|
+
// Score: GREEN=3, YELLOW=2, RED=1, DEADLOCK=0
|
|
2203
|
+
const verdictScore = { GREEN: 3, YELLOW: 2, RED: 1, DEADLOCK: 0 };
|
|
2204
|
+
const scoreA = verdictScore[debateA.final_verdict] ?? 0;
|
|
2205
|
+
const scoreB = verdictScore[debateB.final_verdict] ?? 0;
|
|
2206
|
+
const warnCountA = debateA.warnings.length;
|
|
2207
|
+
const warnCountB = debateB.warnings.length;
|
|
2208
|
+
const blockCountA = debateA.block_reasons.length;
|
|
2209
|
+
const blockCountB = debateB.block_reasons.length;
|
|
2210
|
+
let winner;
|
|
2211
|
+
let confidence;
|
|
2212
|
+
let reasoning;
|
|
2213
|
+
if (scoreA !== scoreB) {
|
|
2214
|
+
winner = scoreA > scoreB ? 'A' : 'B';
|
|
2215
|
+
const diff = Math.abs(scoreA - scoreB);
|
|
2216
|
+
confidence = diff >= 2 ? 'high' : 'medium';
|
|
2217
|
+
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'}.`;
|
|
2218
|
+
}
|
|
2219
|
+
else {
|
|
2220
|
+
// Same verdict — break tie on warnings, then block reasons
|
|
2221
|
+
if (warnCountA !== warnCountB) {
|
|
2222
|
+
winner = warnCountA < warnCountB ? 'A' : 'B';
|
|
2223
|
+
confidence = 'low';
|
|
2224
|
+
reasoning = `Both approaches received ${debateA.final_verdict}. Approach ${winner} had fewer warnings (${winner === 'A' ? warnCountA : warnCountB} vs ${winner === 'A' ? warnCountB : warnCountA}).`;
|
|
2225
|
+
}
|
|
2226
|
+
else if (blockCountA !== blockCountB) {
|
|
2227
|
+
winner = blockCountA < blockCountB ? 'A' : 'B';
|
|
2228
|
+
confidence = 'low';
|
|
2229
|
+
reasoning = `Both approaches received ${debateA.final_verdict} with equal warnings. Approach ${winner} had fewer blocking concerns.`;
|
|
2230
|
+
}
|
|
2231
|
+
else {
|
|
2232
|
+
winner = 'TIE';
|
|
2233
|
+
confidence = 'low';
|
|
2234
|
+
reasoning = `Both approaches received ${debateA.final_verdict} with equal warnings and blocks. Council cannot differentiate — consider a more specific framing.`;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
// Auto-record both debates
|
|
2238
|
+
const qMap = { GREEN: 90, YELLOW: 60, RED: 20, DEADLOCK: 50 };
|
|
2239
|
+
autoRecord('benchmark', 'council', qMap[debateA.final_verdict] ?? 50);
|
|
2240
|
+
autoRecord('benchmark', 'council', qMap[debateB.final_verdict] ?? 50);
|
|
2241
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
2242
|
+
winner,
|
|
2243
|
+
confidence,
|
|
2244
|
+
reasoning,
|
|
2245
|
+
recommendation: winner !== 'TIE'
|
|
2246
|
+
? `Use Approach ${winner}. ${winner === 'A' ? debateA.recommended : debateB.recommended}`
|
|
2247
|
+
: `No clear winner. ${debateA.recommended}`,
|
|
2248
|
+
approach_a: {
|
|
2249
|
+
label: 'A',
|
|
2250
|
+
description: approachA.slice(0, 120),
|
|
2251
|
+
verdict: debateA.final_verdict,
|
|
2252
|
+
warnings: warnCountA,
|
|
2253
|
+
block_reasons: blockCountA,
|
|
2254
|
+
recommended: debateA.recommended,
|
|
2255
|
+
votes: Object.fromEntries(Object.entries(debateA.votes).map(([k, v]) => [k, v.verdict])),
|
|
2256
|
+
},
|
|
2257
|
+
approach_b: {
|
|
2258
|
+
label: 'B',
|
|
2259
|
+
description: approachB.slice(0, 120),
|
|
2260
|
+
verdict: debateB.final_verdict,
|
|
2261
|
+
warnings: warnCountB,
|
|
2262
|
+
block_reasons: blockCountB,
|
|
2263
|
+
recommended: debateB.recommended,
|
|
2264
|
+
votes: Object.fromEntries(Object.entries(debateB.votes).map(([k, v]) => [k, v.verdict])),
|
|
2265
|
+
},
|
|
2266
|
+
duration_ms: Date.now() - bmStart,
|
|
2267
|
+
}, null, 2) }] };
|
|
2268
|
+
}
|
|
2095
2269
|
default:
|
|
2096
2270
|
throw new Error(`Unknown tool: ${name}`);
|
|
2097
2271
|
}
|