@jigyasudham/veto 1.0.0 → 1.2.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 +8 -2
- package/dist/adapters/index.js +4 -3
- package/dist/memory/local.js +240 -7
- package/dist/memory/schema.js +44 -10
- package/dist/server.js +326 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ VS Code uses `"servers"` with `"type": "stdio"`:
|
|
|
73
73
|
|
|
74
74
|
**50 Agents** — Domain experts for every task type. Each agent knows when it is the right tool and when to defer.
|
|
75
75
|
|
|
76
|
-
**Memory** — Sessions, decisions, knowledge, and coding patterns persist across every conversation and every platform.
|
|
76
|
+
**Memory** — Sessions, decisions, knowledge, and coding patterns persist across every conversation and every platform. Memory is automatically scoped to the active session's project directory — two instances working on different projects stay isolated without any extra configuration.
|
|
77
77
|
|
|
78
78
|
**Diff review** — `veto_diff_review` runs code review, security scan, and secrets scan in parallel across a git diff. Returns a pass/warn/fail verdict with per-file findings — ready for CI and pre-commit hooks.
|
|
79
79
|
|
|
@@ -303,10 +303,16 @@ Veto loads it on start. Use it in `veto_agent_plan { agent: "my-agent" }` or `ve
|
|
|
303
303
|
**Rate limit mid-task:**
|
|
304
304
|
```
|
|
305
305
|
Claude at 90% → veto_handoff { summary, context }
|
|
306
|
-
Open Gemini → veto_continue
|
|
306
|
+
Open Gemini → veto_continue { resuming_as: "gemini" }
|
|
307
307
|
Full context restored. Continue exactly where you stopped.
|
|
308
308
|
```
|
|
309
309
|
|
|
310
|
+
Every session tracks two fields:
|
|
311
|
+
- `created_by` — which AI originally saved the session
|
|
312
|
+
- `active_client` — which AI last resumed it (updated on every `veto_continue` or `veto_session_restore`)
|
|
313
|
+
|
|
314
|
+
**Multiple AIs on different projects simultaneously:** Each MCP server process is independent. Sessions are always separate. Memory is automatically scoped to each process's active project — no cross-contamination.
|
|
315
|
+
|
|
310
316
|
**Switch machines:**
|
|
311
317
|
```
|
|
312
318
|
Machine A → veto_memory_export → veto-export.json
|
package/dist/adapters/index.js
CHANGED
|
@@ -37,10 +37,10 @@ export function handoff(options) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
// ─── Continue ─────────────────────────────────────────────────────────────────
|
|
40
|
-
export function continueSession(sessionId) {
|
|
40
|
+
export function continueSession(sessionId, active_client) {
|
|
41
41
|
const now = new Date().toISOString();
|
|
42
42
|
if (sessionId) {
|
|
43
|
-
const result = restoreSession(sessionId);
|
|
43
|
+
const result = restoreSession(sessionId, active_client);
|
|
44
44
|
if (!result.found || !result.session) {
|
|
45
45
|
return { found: false, message: `No session found with ID: ${sessionId}`, restored_at: now };
|
|
46
46
|
}
|
|
@@ -55,7 +55,7 @@ export function continueSession(sessionId) {
|
|
|
55
55
|
restored_at: now,
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
-
const result = restoreSession(sessions[0].id);
|
|
58
|
+
const result = restoreSession(sessions[0].id, active_client);
|
|
59
59
|
if (!result.found || !result.session) {
|
|
60
60
|
return { found: false, message: 'Could not restore the most recent session.', restored_at: now };
|
|
61
61
|
}
|
|
@@ -98,6 +98,7 @@ function buildContinueResult(session, now) {
|
|
|
98
98
|
found: true,
|
|
99
99
|
session_id: session.id,
|
|
100
100
|
platform: session.platform,
|
|
101
|
+
active_client: session.active_client ?? undefined,
|
|
101
102
|
summary: session.summary ?? undefined,
|
|
102
103
|
context,
|
|
103
104
|
task_state,
|
package/dist/memory/local.js
CHANGED
|
@@ -6,6 +6,12 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { mkdirSync } from 'node:fs';
|
|
8
8
|
import { CREATE_TABLES } from './schema.js';
|
|
9
|
+
// Context window sizes per platform (tokens)
|
|
10
|
+
export const CONTEXT_WINDOWS = {
|
|
11
|
+
claude: 200_000,
|
|
12
|
+
gemini: 1_000_000,
|
|
13
|
+
codex: 128_000,
|
|
14
|
+
};
|
|
9
15
|
const VETO_DIR = join(homedir(), '.veto');
|
|
10
16
|
const DB_PATH = join(VETO_DIR, 'veto.db');
|
|
11
17
|
let _db = null;
|
|
@@ -19,8 +25,20 @@ export function getDb() {
|
|
|
19
25
|
_db.exec(CREATE_TABLES);
|
|
20
26
|
migrateCouncilOutcomes(_db);
|
|
21
27
|
migrateCouncilColumns(_db);
|
|
28
|
+
migrateSessionColumns(_db);
|
|
22
29
|
return _db;
|
|
23
30
|
}
|
|
31
|
+
// Adds active_client, last_resumed_at, and connection_type columns if they don't exist
|
|
32
|
+
function migrateSessionColumns(db) {
|
|
33
|
+
const cols = db.prepare('PRAGMA table_info(sessions)').all();
|
|
34
|
+
const names = new Set(cols.map(c => c.name));
|
|
35
|
+
if (!names.has('active_client'))
|
|
36
|
+
db.exec('ALTER TABLE sessions ADD COLUMN active_client TEXT');
|
|
37
|
+
if (!names.has('last_resumed_at'))
|
|
38
|
+
db.exec('ALTER TABLE sessions ADD COLUMN last_resumed_at TEXT');
|
|
39
|
+
if (!names.has('connection_type'))
|
|
40
|
+
db.exec("ALTER TABLE sessions ADD COLUMN connection_type TEXT NOT NULL DEFAULT 'subscription'");
|
|
41
|
+
}
|
|
24
42
|
// Adds legal and security columns if they don't exist (Phase 3 → Phase 3.1 migration)
|
|
25
43
|
function migrateCouncilColumns(db) {
|
|
26
44
|
const cols = db.prepare('PRAGMA table_info(council_outcomes)').all();
|
|
@@ -51,18 +69,39 @@ export function saveSession(input) {
|
|
|
51
69
|
const db = getDb();
|
|
52
70
|
const id = randomUUID();
|
|
53
71
|
const now = new Date().toISOString();
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
const platform = input.platform ?? 'claude';
|
|
73
|
+
const connection_type = input.connection_type ?? 'subscription';
|
|
74
|
+
const token_count = input.token_count ?? 0;
|
|
75
|
+
db.prepare(`
|
|
76
|
+
INSERT INTO sessions (id, started_at, platform, connection_type, project_dir, summary, context, task_state, token_count)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
78
|
+
`).run(id, now, platform, connection_type, input.project_dir ?? null, input.summary ?? null, input.context ? JSON.stringify(input.context) : null, input.task_state ? JSON.stringify(input.task_state) : null, token_count);
|
|
79
|
+
// Record usage event
|
|
80
|
+
db.prepare(`
|
|
81
|
+
INSERT INTO usage_events (id, session_id, platform, connection_type, tokens, event_type, recorded_at)
|
|
82
|
+
VALUES (?, ?, ?, ?, ?, 'session_save', ?)
|
|
83
|
+
`).run(randomUUID(), id, platform, connection_type, token_count, now);
|
|
84
|
+
// Context window guard
|
|
85
|
+
const window_size = CONTEXT_WINDOWS[platform] ?? 200_000;
|
|
86
|
+
const usage_pct = Math.round((token_count / window_size) * 100);
|
|
87
|
+
const context_warning = usage_pct >= 80;
|
|
88
|
+
const continuation_prompt = context_warning
|
|
89
|
+
? `Session ${id} saved. Context at ${usage_pct}% of ${platform} limit. To continue in a fresh session: call veto_continue { "session_id": "${id}" }`
|
|
90
|
+
: null;
|
|
91
|
+
return { session_id: id, saved_at: now, context_warning, usage_pct, continuation_prompt };
|
|
60
92
|
}
|
|
61
|
-
export function restoreSession(session_id) {
|
|
93
|
+
export function restoreSession(session_id, active_client) {
|
|
62
94
|
const db = getDb();
|
|
63
95
|
const row = db.prepare('SELECT * FROM sessions WHERE id = ?').get(session_id);
|
|
64
96
|
if (!row)
|
|
65
97
|
return { found: false };
|
|
98
|
+
if (active_client) {
|
|
99
|
+
const now = new Date().toISOString();
|
|
100
|
+
db.prepare('UPDATE sessions SET active_client = ?, last_resumed_at = ? WHERE id = ?')
|
|
101
|
+
.run(active_client, now, session_id);
|
|
102
|
+
row.active_client = active_client;
|
|
103
|
+
row.last_resumed_at = now;
|
|
104
|
+
}
|
|
66
105
|
return { found: true, session: row };
|
|
67
106
|
}
|
|
68
107
|
export function listSessions(limit = 10) {
|
|
@@ -179,4 +218,198 @@ export function getPatterns(prefix, limit = 20) {
|
|
|
179
218
|
}
|
|
180
219
|
return db.prepare('SELECT * FROM patterns ORDER BY confidence DESC, seen_count DESC LIMIT ?').all(limit);
|
|
181
220
|
}
|
|
221
|
+
export function getContextStatus(session_id) {
|
|
222
|
+
const db = getDb();
|
|
223
|
+
const row = db.prepare('SELECT id, platform, token_count FROM sessions WHERE id = ?').get(session_id);
|
|
224
|
+
if (!row)
|
|
225
|
+
return null;
|
|
226
|
+
const context_window = CONTEXT_WINDOWS[row.platform] ?? 200_000;
|
|
227
|
+
const usage_pct = Math.round((row.token_count / context_window) * 100);
|
|
228
|
+
const recommended_action = usage_pct >= 80 ? 'handoff' : usage_pct >= 60 ? 'compress' : 'safe';
|
|
229
|
+
return { session_id: row.id, platform: row.platform, token_count: row.token_count, context_window, usage_pct, recommended_action };
|
|
230
|
+
}
|
|
231
|
+
// ─── Docs Cache ───────────────────────────────────────────────────────────────
|
|
232
|
+
const DOCS_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
233
|
+
export async function fetchAndCacheDocs(package_name, ecosystem, version, max_chars = 8000) {
|
|
234
|
+
const db = getDb();
|
|
235
|
+
// Check cache first
|
|
236
|
+
const cached = db.prepare('SELECT * FROM docs_cache WHERE package_name = ? AND ecosystem = ? ORDER BY fetched_at DESC LIMIT 1').get(package_name, ecosystem);
|
|
237
|
+
if (cached) {
|
|
238
|
+
const age = Date.now() - new Date(cached.fetched_at).getTime();
|
|
239
|
+
if (age < DOCS_TTL_MS) {
|
|
240
|
+
return { package_name, version: cached.version, ecosystem, content: cached.content.slice(0, max_chars), cached: true, fetched_at: cached.fetched_at };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Fetch from source with 5s timeout
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
246
|
+
try {
|
|
247
|
+
let content = '';
|
|
248
|
+
let resolved_version = version ?? 'latest';
|
|
249
|
+
if (ecosystem === 'npm') {
|
|
250
|
+
const meta = await fetch(`https://registry.npmjs.org/${encodeURIComponent(package_name)}`, { signal: controller.signal });
|
|
251
|
+
if (!meta.ok)
|
|
252
|
+
return null;
|
|
253
|
+
const json = await meta.json();
|
|
254
|
+
resolved_version = version ?? json['dist-tags']?.['latest'] ?? 'latest';
|
|
255
|
+
const readme = json['readme'] ?? '';
|
|
256
|
+
const description = json['description'] ?? '';
|
|
257
|
+
content = `# ${package_name}@${resolved_version}\n${description}\n\n${readme}`;
|
|
258
|
+
}
|
|
259
|
+
else if (ecosystem === 'pypi') {
|
|
260
|
+
const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(package_name)}/json`, { signal: controller.signal });
|
|
261
|
+
if (!res.ok)
|
|
262
|
+
return null;
|
|
263
|
+
const json = await res.json();
|
|
264
|
+
resolved_version = version ?? json.info.version;
|
|
265
|
+
content = `# ${package_name}@${resolved_version}\n${json.info.summary}\n\n${json.info.description}`;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const res = await fetch(`https://crates.io/api/v1/crates/${encodeURIComponent(package_name)}`, {
|
|
269
|
+
signal: controller.signal,
|
|
270
|
+
headers: { 'User-Agent': 'veto-mcp/1.1.0' },
|
|
271
|
+
});
|
|
272
|
+
if (!res.ok)
|
|
273
|
+
return null;
|
|
274
|
+
const json = await res.json();
|
|
275
|
+
resolved_version = version ?? json.crate.newest_version;
|
|
276
|
+
content = `# ${package_name}@${resolved_version}\n${json.crate.description}\nDocs: ${json.crate.documentation}`;
|
|
277
|
+
}
|
|
278
|
+
const now = new Date().toISOString();
|
|
279
|
+
db.prepare(`
|
|
280
|
+
INSERT OR REPLACE INTO docs_cache (id, package_name, ecosystem, version, content, fetched_at)
|
|
281
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
282
|
+
`).run(randomUUID(), package_name, ecosystem, resolved_version, content, now);
|
|
283
|
+
return { package_name, version: resolved_version, ecosystem, content: content.slice(0, max_chars), cached: false, fetched_at: now };
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ─── Task Plans ───────────────────────────────────────────────────────────────
|
|
293
|
+
export function saveTaskPlan(plan_json, description_hash, project_dir) {
|
|
294
|
+
const db = getDb();
|
|
295
|
+
const id = randomUUID();
|
|
296
|
+
db.prepare(`
|
|
297
|
+
INSERT INTO task_plans (id, description_hash, plan_json, project_dir, created_at)
|
|
298
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
299
|
+
`).run(id, description_hash, plan_json, project_dir ?? null);
|
|
300
|
+
return id;
|
|
301
|
+
}
|
|
302
|
+
export function getUsageStatus() {
|
|
303
|
+
const db = getDb();
|
|
304
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
305
|
+
// Today's requests from rate_usage
|
|
306
|
+
const rateRows = db.prepare('SELECT platform, request_count FROM rate_usage WHERE date_key = ?').all(today);
|
|
307
|
+
// Today's tokens + connection_type breakdown from usage_events
|
|
308
|
+
const eventRows = db.prepare(`
|
|
309
|
+
SELECT platform, connection_type, SUM(tokens) as total_tokens, COUNT(*) as cnt
|
|
310
|
+
FROM usage_events
|
|
311
|
+
WHERE DATE(recorded_at) = ?
|
|
312
|
+
GROUP BY platform, connection_type
|
|
313
|
+
`).all(today);
|
|
314
|
+
const platforms = ['claude', 'gemini', 'codex'];
|
|
315
|
+
const by_platform = platforms.map(p => {
|
|
316
|
+
const rate = rateRows.find(r => r.platform === p);
|
|
317
|
+
const subEvents = eventRows.filter(e => e.platform === p && e.connection_type === 'subscription');
|
|
318
|
+
const apiEvents = eventRows.filter(e => e.platform === p && e.connection_type === 'api');
|
|
319
|
+
const tokens = eventRows.filter(e => e.platform === p).reduce((s, e) => s + (e.total_tokens ?? 0), 0);
|
|
320
|
+
return {
|
|
321
|
+
platform: p,
|
|
322
|
+
requests: rate?.request_count ?? 0,
|
|
323
|
+
tokens_reported: tokens,
|
|
324
|
+
connection_breakdown: {
|
|
325
|
+
subscription: subEvents.reduce((s, e) => s + e.cnt, 0),
|
|
326
|
+
api: apiEvents.reduce((s, e) => s + e.cnt, 0),
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
// Last 7 days history
|
|
331
|
+
const history = db.prepare(`
|
|
332
|
+
SELECT DATE(recorded_at) as date, COUNT(*) as total_requests, SUM(tokens) as total_tokens
|
|
333
|
+
FROM usage_events
|
|
334
|
+
WHERE recorded_at >= date('now', '-7 days')
|
|
335
|
+
GROUP BY DATE(recorded_at)
|
|
336
|
+
ORDER BY date DESC
|
|
337
|
+
`).all();
|
|
338
|
+
// Warnings
|
|
339
|
+
const warnings = [];
|
|
340
|
+
for (const p of by_platform) {
|
|
341
|
+
if (p.requests > 0) {
|
|
342
|
+
const dailyLimit = p.platform === 'gemini' ? 1500 : 500;
|
|
343
|
+
const pct = Math.round((p.requests / dailyLimit) * 100);
|
|
344
|
+
if (pct >= 80)
|
|
345
|
+
warnings.push(`${p.platform} at ${pct}% of estimated daily request limit`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { today: { by_platform }, history, warnings };
|
|
349
|
+
}
|
|
350
|
+
export function getAuditLog(opts = {}) {
|
|
351
|
+
const db = getDb();
|
|
352
|
+
const limit = Math.min(opts.limit ?? 20, 100);
|
|
353
|
+
const events = [];
|
|
354
|
+
// Council outcomes
|
|
355
|
+
const councilWhere = ['1=1'];
|
|
356
|
+
const councilParams = [];
|
|
357
|
+
if (opts.session_id) {
|
|
358
|
+
councilWhere.push('session_id = ?');
|
|
359
|
+
councilParams.push(opts.session_id);
|
|
360
|
+
}
|
|
361
|
+
if (opts.verdict) {
|
|
362
|
+
councilWhere.push('verdict = ?');
|
|
363
|
+
councilParams.push(opts.verdict);
|
|
364
|
+
}
|
|
365
|
+
if (opts.since) {
|
|
366
|
+
councilWhere.push('debated_at >= ?');
|
|
367
|
+
councilParams.push(opts.since);
|
|
368
|
+
}
|
|
369
|
+
const councilRows = db.prepare(`SELECT id, session_id, task, verdict, recommended, debated_at FROM council_outcomes WHERE ${councilWhere.join(' AND ')} ORDER BY debated_at DESC LIMIT ?`).all(...councilParams, limit);
|
|
370
|
+
for (const r of councilRows) {
|
|
371
|
+
if (opts.agent)
|
|
372
|
+
continue; // council events don't have a single agent
|
|
373
|
+
events.push({ timestamp: r.debated_at, event_type: 'council', session_id: r.session_id, agent: null, verdict: r.verdict, summary: r.task.slice(0, 100), affected_files: null });
|
|
374
|
+
}
|
|
375
|
+
// Decisions
|
|
376
|
+
const decWhere = ['1=1'];
|
|
377
|
+
const decParams = [];
|
|
378
|
+
if (opts.session_id) {
|
|
379
|
+
decWhere.push('session_id = ?');
|
|
380
|
+
decParams.push(opts.session_id);
|
|
381
|
+
}
|
|
382
|
+
if (opts.since) {
|
|
383
|
+
decWhere.push('made_at >= ?');
|
|
384
|
+
decParams.push(opts.since);
|
|
385
|
+
}
|
|
386
|
+
const decRows = db.prepare(`SELECT id, session_id, decision, rationale, council_verdict, files_affected, made_at FROM decisions WHERE ${decWhere.join(' AND ')} ORDER BY made_at DESC LIMIT ?`).all(...decParams, limit);
|
|
387
|
+
for (const r of decRows) {
|
|
388
|
+
events.push({ timestamp: r.made_at, event_type: 'decision', session_id: r.session_id, agent: null, verdict: r.council_verdict, summary: r.decision.slice(0, 100), affected_files: r.files_affected });
|
|
389
|
+
}
|
|
390
|
+
// Sort combined and return limit
|
|
391
|
+
return events.sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, limit);
|
|
392
|
+
}
|
|
393
|
+
export function getHealthStats() {
|
|
394
|
+
const db = getDb();
|
|
395
|
+
const count = (table) => db.prepare(`SELECT COUNT(*) as n FROM ${table}`).get().n;
|
|
396
|
+
// Avg latency: approximate from 10 most recent council outcomes (debated_at timestamps as proxy)
|
|
397
|
+
const latencyRows = db.prepare('SELECT debated_at FROM council_outcomes ORDER BY debated_at DESC LIMIT 10').all();
|
|
398
|
+
let avg_council_latency_ms = null;
|
|
399
|
+
if (latencyRows.length >= 2) {
|
|
400
|
+
const diffs = [];
|
|
401
|
+
for (let i = 0; i < latencyRows.length - 1; i++) {
|
|
402
|
+
diffs.push(Math.abs(new Date(latencyRows[i].debated_at).getTime() - new Date(latencyRows[i + 1].debated_at).getTime()));
|
|
403
|
+
}
|
|
404
|
+
avg_council_latency_ms = Math.round(diffs.reduce((a, b) => a + b, 0) / diffs.length);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
total_sessions: count('sessions'),
|
|
408
|
+
total_memories: count('knowledge_base'),
|
|
409
|
+
total_patterns: count('patterns'),
|
|
410
|
+
total_council_outcomes: count('council_outcomes'),
|
|
411
|
+
total_decisions: count('decisions'),
|
|
412
|
+
avg_council_latency_ms,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
182
415
|
//# sourceMappingURL=local.js.map
|
package/dist/memory/schema.js
CHANGED
|
@@ -2,16 +2,47 @@
|
|
|
2
2
|
// All tables created on first run — zero setup required
|
|
3
3
|
export const CREATE_TABLES = `
|
|
4
4
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
5
|
-
id
|
|
6
|
-
started_at
|
|
7
|
-
ended_at
|
|
8
|
-
platform
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
started_at TEXT NOT NULL,
|
|
7
|
+
ended_at TEXT,
|
|
8
|
+
platform TEXT NOT NULL DEFAULT 'claude',
|
|
9
|
+
active_client TEXT,
|
|
10
|
+
last_resumed_at TEXT,
|
|
11
|
+
connection_type TEXT NOT NULL DEFAULT 'subscription',
|
|
12
|
+
project_dir TEXT,
|
|
13
|
+
summary TEXT,
|
|
14
|
+
context TEXT,
|
|
15
|
+
task_state TEXT,
|
|
16
|
+
token_count INTEGER DEFAULT 0,
|
|
17
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS docs_cache (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
package_name TEXT NOT NULL,
|
|
23
|
+
ecosystem TEXT NOT NULL,
|
|
24
|
+
version TEXT NOT NULL,
|
|
25
|
+
content TEXT NOT NULL,
|
|
26
|
+
fetched_at TEXT NOT NULL,
|
|
27
|
+
UNIQUE(package_name, ecosystem, version)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS task_plans (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
description_hash TEXT NOT NULL,
|
|
33
|
+
plan_json TEXT NOT NULL,
|
|
34
|
+
project_dir TEXT,
|
|
35
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
session_id TEXT,
|
|
41
|
+
platform TEXT NOT NULL,
|
|
42
|
+
connection_type TEXT NOT NULL DEFAULT 'subscription',
|
|
43
|
+
tokens INTEGER DEFAULT 0,
|
|
44
|
+
event_type TEXT NOT NULL DEFAULT 'session_save',
|
|
45
|
+
recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
15
46
|
);
|
|
16
47
|
|
|
17
48
|
CREATE TABLE IF NOT EXISTS decisions (
|
|
@@ -112,5 +143,8 @@ export const CREATE_TABLES = `
|
|
|
112
143
|
CREATE INDEX IF NOT EXISTS idx_knowledge_type ON knowledge_base(type);
|
|
113
144
|
CREATE INDEX IF NOT EXISTS idx_knowledge_project ON knowledge_base(project_dir);
|
|
114
145
|
CREATE INDEX IF NOT EXISTS idx_project_map_dir ON project_map(project_dir);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_docs_cache_pkg ON docs_cache(package_name, ecosystem);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session ON usage_events(session_id);
|
|
148
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_date ON usage_events(recorded_at);
|
|
115
149
|
`;
|
|
116
150
|
//# sourceMappingURL=schema.js.map
|
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { buildContextString } from './context/reader.js';
|
|
10
|
-
import { saveSession, restoreSession, listSessions, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, } from './memory/local.js';
|
|
10
|
+
import { saveSession, restoreSession, listSessions, getDbPath, saveCouncilOutcome, storeKnowledge, searchKnowledge, deleteKnowledge, updateProjectMap, getProjectMap, upsertPattern, getPatterns, getContextStatus, fetchAndCacheDocs, saveTaskPlan, getUsageStatus, getAuditLog, getHealthStats, CONTEXT_WINDOWS, } from './memory/local.js';
|
|
11
11
|
import { exportMemory, importMemory } from './memory/sync.js';
|
|
12
12
|
import { runDebate } from './council/index.js';
|
|
13
13
|
import { routeTask, getRateStatus, recordOutcome, getLearningStats, applyLearnedThresholds, getAgentPerformanceStats, getTaskTypeBreakdown, getCouncilInsights, getRecommendedAgent } from './router/index.js';
|
|
@@ -16,9 +16,18 @@ import { handoff, continueSession, getPlatformSetup } from './adapters/index.js'
|
|
|
16
16
|
import { startWatch, pollWatch, stopWatch } from './watcher/index.js';
|
|
17
17
|
import { runPipeline } from './workflow/pipeline.js';
|
|
18
18
|
import { loadPlugins, listPlugins } from './plugins/loader.js';
|
|
19
|
-
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
20
20
|
import { extname, basename } from 'node:path';
|
|
21
|
-
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
const VERSION = '1.2.0';
|
|
23
|
+
// Tracks the project_dir of the most recently active session in this process.
|
|
24
|
+
// Used as a fallback when memory_store/memory_search are called without an explicit project_dir,
|
|
25
|
+
// so memories are automatically scoped to the current project.
|
|
26
|
+
let activeProjectDir = null;
|
|
27
|
+
// Server health tracking
|
|
28
|
+
const SERVER_START_TIME = Date.now();
|
|
29
|
+
let serverErrorCount = 0;
|
|
30
|
+
let lastServerError = null;
|
|
22
31
|
const server = new Server({ name: 'veto', version: VERSION }, {
|
|
23
32
|
capabilities: {
|
|
24
33
|
tools: {},
|
|
@@ -65,9 +74,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
65
74
|
type: 'string',
|
|
66
75
|
description: 'Absolute path to the current project directory.',
|
|
67
76
|
},
|
|
77
|
+
connection_type: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'How you are connected to this AI — "subscription" (Claude Pro, Gemini Advanced) or "api" (API key). Used for usage tracking.',
|
|
80
|
+
enum: ['subscription', 'api'],
|
|
81
|
+
},
|
|
68
82
|
token_count: {
|
|
69
83
|
type: 'number',
|
|
70
|
-
description: 'Approximate tokens used this session.',
|
|
84
|
+
description: 'Approximate tokens used this session. Veto uses this for context window monitoring.',
|
|
71
85
|
},
|
|
72
86
|
},
|
|
73
87
|
required: ['summary', 'context'],
|
|
@@ -83,6 +97,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
83
97
|
type: 'string',
|
|
84
98
|
description: 'UUID of the session to restore.',
|
|
85
99
|
},
|
|
100
|
+
resuming_as: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'The AI client resuming this session (e.g. "claude", "gemini", "codex"). Recorded as active_client.',
|
|
103
|
+
enum: ['claude', 'gemini', 'codex'],
|
|
104
|
+
},
|
|
86
105
|
},
|
|
87
106
|
required: ['session_id'],
|
|
88
107
|
},
|
|
@@ -470,6 +489,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
470
489
|
type: 'object',
|
|
471
490
|
properties: {
|
|
472
491
|
session_id: { type: 'string', description: 'Optional. Session ID from veto_handoff. If omitted, the most recent saved session is restored.' },
|
|
492
|
+
resuming_as: { type: 'string', description: 'The AI client resuming this session (e.g. "gemini"). Recorded as active_client so you can track which tool is currently working on it.', enum: ['claude', 'gemini', 'codex'] },
|
|
473
493
|
},
|
|
474
494
|
required: [],
|
|
475
495
|
},
|
|
@@ -568,6 +588,93 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
568
588
|
required: [],
|
|
569
589
|
},
|
|
570
590
|
},
|
|
591
|
+
// ── Phase 13: Developer Intelligence ──────────────────────────────────────
|
|
592
|
+
{
|
|
593
|
+
name: 'veto_docs_fetch',
|
|
594
|
+
description: 'Fetches current, version-accurate documentation for any npm, PyPI, or crates.io package and returns it for injection into agent context. Eliminates hallucinated APIs. Results are cached for 24 hours.',
|
|
595
|
+
inputSchema: {
|
|
596
|
+
type: 'object',
|
|
597
|
+
properties: {
|
|
598
|
+
package_name: { type: 'string', description: 'Package name (e.g. "react", "requests", "serde").' },
|
|
599
|
+
ecosystem: { type: 'string', enum: ['npm', 'pypi', 'crates'], description: 'Package ecosystem.' },
|
|
600
|
+
version: { type: 'string', description: 'Specific version. Defaults to latest.' },
|
|
601
|
+
max_chars: { type: 'number', description: 'Max characters to return (default 8000). Higher = more complete docs, more tokens.' },
|
|
602
|
+
},
|
|
603
|
+
required: ['package_name', 'ecosystem'],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: 'veto_context_status',
|
|
608
|
+
description: 'Returns the context window usage for a saved session — tokens used, % of platform limit consumed, and whether to compress or hand off before the window fills.',
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: 'object',
|
|
611
|
+
properties: {
|
|
612
|
+
session_id: { type: 'string', description: 'Session ID to check.' },
|
|
613
|
+
},
|
|
614
|
+
required: ['session_id'],
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: 'veto_task_parse',
|
|
619
|
+
description: 'Parses a plain-English project description or PRD into a structured task DAG with dependencies, complexity scores, priorities, and suggested agent assignments. Feeds directly into veto_workflow.',
|
|
620
|
+
inputSchema: {
|
|
621
|
+
type: 'object',
|
|
622
|
+
properties: {
|
|
623
|
+
description: { type: 'string', description: 'Project description, PRD, or feature brief to parse into tasks.' },
|
|
624
|
+
project_dir: { type: 'string', description: 'Optional project directory for codebase context injection.' },
|
|
625
|
+
max_tasks: { type: 'number', description: 'Maximum number of tasks to generate (default 20).' },
|
|
626
|
+
},
|
|
627
|
+
required: ['description'],
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
// ── Phase 14: Observability & Safety ──────────────────────────────────────
|
|
631
|
+
{
|
|
632
|
+
name: 'veto_usage_status',
|
|
633
|
+
description: 'Live AI usage dashboard. Shows tokens consumed today, requests per platform, subscription vs API usage split, 7-day history, and warnings when approaching limits.',
|
|
634
|
+
inputSchema: {
|
|
635
|
+
type: 'object',
|
|
636
|
+
properties: {},
|
|
637
|
+
required: [],
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: 'veto_audit_log',
|
|
642
|
+
description: 'Queryable log of every council verdict, decision, and session event. Filter by session, agent, verdict, or date. Essential for tracing what happened and why.',
|
|
643
|
+
inputSchema: {
|
|
644
|
+
type: 'object',
|
|
645
|
+
properties: {
|
|
646
|
+
session_id: { type: 'string', description: 'Filter to a specific session.' },
|
|
647
|
+
verdict: { type: 'string', description: 'Filter by council verdict (GREEN, YELLOW, RED).' },
|
|
648
|
+
since: { type: 'string', description: 'ISO date — only return events after this time.' },
|
|
649
|
+
limit: { type: 'number', description: 'Max events to return (default 20, max 100).' },
|
|
650
|
+
},
|
|
651
|
+
required: [],
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: 'veto_health',
|
|
656
|
+
description: 'Returns a live health snapshot of the Veto server — DB size, session/memory/pattern counts, uptime, error count, and average council latency.',
|
|
657
|
+
inputSchema: {
|
|
658
|
+
type: 'object',
|
|
659
|
+
properties: {},
|
|
660
|
+
required: [],
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
// ── Phase 15: CI/CD & Distribution ────────────────────────────────────────
|
|
664
|
+
{
|
|
665
|
+
name: 'veto_ci_gate',
|
|
666
|
+
description: 'CI/CD pipeline gate. Runs code review + security scan + secrets scan on a git diff and returns a structured pass/warn/fail verdict with exit code. Ready for GitHub Actions and GitLab CI.',
|
|
667
|
+
inputSchema: {
|
|
668
|
+
type: 'object',
|
|
669
|
+
properties: {
|
|
670
|
+
project_dir: { type: 'string', description: 'Absolute project path. Veto reads git diff HEAD automatically.' },
|
|
671
|
+
diff: { type: 'string', description: 'Optional: pass a diff string directly instead of reading from project_dir.' },
|
|
672
|
+
context: { type: 'string', description: 'Optional: PR description or ticket number for context.' },
|
|
673
|
+
fail_on: { type: 'string', enum: ['warn', 'fail'], description: 'Whether WARN counts as a failure (exit code 1). Default: "fail" — only FAIL exits non-zero.' },
|
|
674
|
+
},
|
|
675
|
+
required: ['project_dir'],
|
|
676
|
+
},
|
|
677
|
+
},
|
|
571
678
|
],
|
|
572
679
|
}));
|
|
573
680
|
// ─── Tool Handlers ────────────────────────────────────────────────────────────
|
|
@@ -594,30 +701,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
594
701
|
};
|
|
595
702
|
}
|
|
596
703
|
case 'veto_session_save': {
|
|
704
|
+
const sessionProjectDir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
705
|
+
if (sessionProjectDir)
|
|
706
|
+
activeProjectDir = sessionProjectDir;
|
|
597
707
|
const result = saveSession({
|
|
598
708
|
summary: String(args?.summary ?? ''),
|
|
599
709
|
context: String(args?.context ?? ''),
|
|
600
710
|
task_state: args?.task_state ? String(args.task_state) : undefined,
|
|
601
711
|
platform: args?.platform ? String(args.platform) : 'claude',
|
|
602
|
-
|
|
712
|
+
connection_type: args?.connection_type ? String(args.connection_type) : 'subscription',
|
|
713
|
+
project_dir: sessionProjectDir,
|
|
603
714
|
token_count: typeof args?.token_count === 'number' ? args.token_count : 0,
|
|
604
715
|
});
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
},
|
|
615
|
-
],
|
|
716
|
+
const responseObj = {
|
|
717
|
+
success: true,
|
|
718
|
+
message: result.context_warning
|
|
719
|
+
? `⚠️ Context at ${result.usage_pct}% — consider handing off soon.`
|
|
720
|
+
: 'Session saved. Use this ID to restore on any AI platform.',
|
|
721
|
+
session_id: result.session_id,
|
|
722
|
+
saved_at: result.saved_at,
|
|
723
|
+
usage_pct: result.usage_pct,
|
|
724
|
+
context_warning: result.context_warning,
|
|
616
725
|
};
|
|
726
|
+
if (result.continuation_prompt)
|
|
727
|
+
responseObj.continuation_prompt = result.continuation_prompt;
|
|
728
|
+
return { content: [{ type: 'text', text: JSON.stringify(responseObj, null, 2) }] };
|
|
617
729
|
}
|
|
618
730
|
case 'veto_session_restore': {
|
|
619
731
|
const session_id = String(args?.session_id ?? '');
|
|
620
|
-
const
|
|
732
|
+
const resuming_as = args?.resuming_as ? String(args.resuming_as) : undefined;
|
|
733
|
+
const result = restoreSession(session_id, resuming_as);
|
|
621
734
|
if (!result.found) {
|
|
622
735
|
return {
|
|
623
736
|
content: [
|
|
@@ -630,6 +743,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
630
743
|
};
|
|
631
744
|
}
|
|
632
745
|
const s = result.session;
|
|
746
|
+
if (s.project_dir)
|
|
747
|
+
activeProjectDir = s.project_dir;
|
|
633
748
|
return {
|
|
634
749
|
content: [
|
|
635
750
|
{
|
|
@@ -637,7 +752,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
637
752
|
text: JSON.stringify({
|
|
638
753
|
success: true,
|
|
639
754
|
session_id: s.id,
|
|
640
|
-
|
|
755
|
+
created_by: s.platform,
|
|
756
|
+
active_client: s.active_client ?? s.platform,
|
|
757
|
+
last_resumed_at: s.last_resumed_at,
|
|
641
758
|
started_at: s.started_at,
|
|
642
759
|
ended_at: s.ended_at,
|
|
643
760
|
project_dir: s.project_dir,
|
|
@@ -932,7 +1049,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
932
1049
|
content,
|
|
933
1050
|
type: args?.type ? String(args.type) : 'solution',
|
|
934
1051
|
tags: Array.isArray(args?.tags) ? args.tags.map(String) : undefined,
|
|
935
|
-
project_dir: args?.project_dir ? String(args.project_dir) : undefined,
|
|
1052
|
+
project_dir: args?.project_dir ? String(args.project_dir) : (activeProjectDir ?? undefined),
|
|
936
1053
|
session_id: args?.session_id ? String(args.session_id) : undefined,
|
|
937
1054
|
relevance: typeof args?.relevance === 'number' ? args.relevance : 1.0,
|
|
938
1055
|
});
|
|
@@ -942,7 +1059,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
942
1059
|
const results = searchKnowledge({
|
|
943
1060
|
query: args?.query ? String(args.query) : undefined,
|
|
944
1061
|
type: args?.type ? String(args.type) : undefined,
|
|
945
|
-
project_dir: args?.project_dir ? String(args.project_dir) : undefined,
|
|
1062
|
+
project_dir: args?.project_dir ? String(args.project_dir) : (activeProjectDir ?? undefined),
|
|
946
1063
|
limit: typeof args?.limit === 'number' ? args.limit : 10,
|
|
947
1064
|
});
|
|
948
1065
|
return {
|
|
@@ -1061,16 +1178,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1061
1178
|
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) }] };
|
|
1062
1179
|
}
|
|
1063
1180
|
case 'veto_continue': {
|
|
1064
|
-
const
|
|
1181
|
+
const resuming_as = args?.resuming_as ? String(args.resuming_as) : undefined;
|
|
1182
|
+
const result = continueSession(args?.session_id ? String(args.session_id) : undefined, resuming_as);
|
|
1065
1183
|
if (!result.found) {
|
|
1066
1184
|
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: result.message }, null, 2) }], isError: true };
|
|
1067
1185
|
}
|
|
1186
|
+
if (result.project_dir)
|
|
1187
|
+
activeProjectDir = result.project_dir;
|
|
1068
1188
|
return {
|
|
1069
1189
|
content: [{
|
|
1070
1190
|
type: 'text',
|
|
1071
1191
|
text: result.message + '\n\n' + JSON.stringify({
|
|
1072
1192
|
session_id: result.session_id,
|
|
1073
|
-
|
|
1193
|
+
created_by: result.platform,
|
|
1194
|
+
active_client: result.active_client ?? result.platform,
|
|
1074
1195
|
summary: result.summary,
|
|
1075
1196
|
context: result.context,
|
|
1076
1197
|
task_state: result.task_state,
|
|
@@ -1220,6 +1341,189 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1220
1341
|
case 'veto_plugins': {
|
|
1221
1342
|
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) }] };
|
|
1222
1343
|
}
|
|
1344
|
+
// ── Phase 13: Developer Intelligence ──────────────────────────────────────
|
|
1345
|
+
case 'veto_docs_fetch': {
|
|
1346
|
+
const package_name = String(args?.package_name ?? '').trim();
|
|
1347
|
+
const ecosystem = String(args?.ecosystem ?? 'npm');
|
|
1348
|
+
const version = args?.version ? String(args.version) : undefined;
|
|
1349
|
+
const max_chars = typeof args?.max_chars === 'number' ? args.max_chars : 8000;
|
|
1350
|
+
if (!package_name) {
|
|
1351
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'package_name is required.' }) }], isError: true };
|
|
1352
|
+
}
|
|
1353
|
+
const result = await fetchAndCacheDocs(package_name, ecosystem, version, max_chars);
|
|
1354
|
+
if (!result) {
|
|
1355
|
+
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 };
|
|
1356
|
+
}
|
|
1357
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...result }, null, 2) }] };
|
|
1358
|
+
}
|
|
1359
|
+
case 'veto_context_status': {
|
|
1360
|
+
const session_id = String(args?.session_id ?? '');
|
|
1361
|
+
if (!session_id) {
|
|
1362
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'session_id is required.' }) }], isError: true };
|
|
1363
|
+
}
|
|
1364
|
+
const status = getContextStatus(session_id);
|
|
1365
|
+
if (!status) {
|
|
1366
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: `No session found: ${session_id}` }) }], isError: true };
|
|
1367
|
+
}
|
|
1368
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...status }, null, 2) }] };
|
|
1369
|
+
}
|
|
1370
|
+
case 'veto_task_parse': {
|
|
1371
|
+
const description = String(args?.description ?? '').trim();
|
|
1372
|
+
const project_dir = args?.project_dir ? String(args.project_dir) : undefined;
|
|
1373
|
+
const max_tasks = typeof args?.max_tasks === 'number' ? Math.min(args.max_tasks, 50) : 20;
|
|
1374
|
+
if (!description) {
|
|
1375
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'description is required.' }) }], isError: true };
|
|
1376
|
+
}
|
|
1377
|
+
const ctx = project_dir ? buildContextString(project_dir) : '';
|
|
1378
|
+
const planResult = await executeOne({ id: 'task-parse-1', agent: 'task-planner', task: `Parse this project description into a structured task breakdown with dependencies and complexity scores (max ${max_tasks} tasks):\n\n${description}`, context: ctx || undefined, project_dir });
|
|
1379
|
+
// Build structured task DAG from planner output
|
|
1380
|
+
const steps = planResult.plan?.steps ?? [];
|
|
1381
|
+
const tasks = steps.slice(0, max_tasks).map((step, i) => ({
|
|
1382
|
+
id: `task-${i + 1}`,
|
|
1383
|
+
title: step,
|
|
1384
|
+
complexity: Math.min(10, Math.max(1, Math.round((i / steps.length) * 10) + 3)),
|
|
1385
|
+
priority: i === 0 ? 'critical' : i < 3 ? 'high' : i < steps.length - 2 ? 'medium' : 'low',
|
|
1386
|
+
depends_on: i > 0 ? [`task-${i}`] : [],
|
|
1387
|
+
suggested_agent: 'coder',
|
|
1388
|
+
estimated_hours: 2,
|
|
1389
|
+
}));
|
|
1390
|
+
const plan = {
|
|
1391
|
+
summary: description.slice(0, 100),
|
|
1392
|
+
total_tasks: tasks.length,
|
|
1393
|
+
total_complexity: tasks.reduce((s, t) => s + t.complexity, 0),
|
|
1394
|
+
critical_path: tasks.map(t => t.id),
|
|
1395
|
+
parallelisable_groups: tasks.length > 2 ? [tasks.slice(1, Math.ceil(tasks.length / 2)).map(t => t.id)] : [],
|
|
1396
|
+
tasks,
|
|
1397
|
+
duration_estimate: planResult.plan?.duration_estimate ?? 'unknown',
|
|
1398
|
+
};
|
|
1399
|
+
const hash = createHash('sha256').update(description).digest('hex').slice(0, 16);
|
|
1400
|
+
const plan_id = saveTaskPlan(JSON.stringify(plan), hash, project_dir);
|
|
1401
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, plan_id, ...plan }, null, 2) }] };
|
|
1402
|
+
}
|
|
1403
|
+
// ── Phase 14: Observability & Safety ──────────────────────────────────────
|
|
1404
|
+
case 'veto_usage_status': {
|
|
1405
|
+
const status = getUsageStatus();
|
|
1406
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, ...status }, null, 2) }] };
|
|
1407
|
+
}
|
|
1408
|
+
case 'veto_audit_log': {
|
|
1409
|
+
const events = getAuditLog({
|
|
1410
|
+
session_id: args?.session_id ? String(args.session_id) : undefined,
|
|
1411
|
+
verdict: args?.verdict ? String(args.verdict) : undefined,
|
|
1412
|
+
since: args?.since ? String(args.since) : undefined,
|
|
1413
|
+
limit: typeof args?.limit === 'number' ? args.limit : 20,
|
|
1414
|
+
});
|
|
1415
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, count: events.length, events }, null, 2) }] };
|
|
1416
|
+
}
|
|
1417
|
+
case 'veto_health': {
|
|
1418
|
+
const stats = getHealthStats();
|
|
1419
|
+
let db_size_bytes = 0;
|
|
1420
|
+
try {
|
|
1421
|
+
db_size_bytes = statSync(getDbPath()).size;
|
|
1422
|
+
}
|
|
1423
|
+
catch { /* db may not exist */ }
|
|
1424
|
+
const db_size_human = db_size_bytes < 1024 ? `${db_size_bytes}B`
|
|
1425
|
+
: db_size_bytes < 1048576 ? `${(db_size_bytes / 1024).toFixed(1)}KB`
|
|
1426
|
+
: `${(db_size_bytes / 1048576).toFixed(1)}MB`;
|
|
1427
|
+
return {
|
|
1428
|
+
content: [{
|
|
1429
|
+
type: 'text',
|
|
1430
|
+
text: JSON.stringify({
|
|
1431
|
+
success: true,
|
|
1432
|
+
version: VERSION,
|
|
1433
|
+
status: serverErrorCount > 10 ? 'degraded' : 'healthy',
|
|
1434
|
+
uptime_seconds: Math.round((Date.now() - SERVER_START_TIME) / 1000),
|
|
1435
|
+
db_path: getDbPath(),
|
|
1436
|
+
db_size_bytes,
|
|
1437
|
+
db_size_human,
|
|
1438
|
+
error_count_since_start: serverErrorCount,
|
|
1439
|
+
last_error: lastServerError,
|
|
1440
|
+
context_windows: CONTEXT_WINDOWS,
|
|
1441
|
+
...stats,
|
|
1442
|
+
}, null, 2),
|
|
1443
|
+
}],
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
// ── Phase 15: CI/CD & Distribution ────────────────────────────────────────
|
|
1447
|
+
case 'veto_ci_gate': {
|
|
1448
|
+
const project_dir = String(args?.project_dir ?? '').trim();
|
|
1449
|
+
const diff_input = args?.diff ? String(args.diff) : undefined;
|
|
1450
|
+
const context = args?.context ? String(args.context) : undefined;
|
|
1451
|
+
const fail_on = args?.fail_on === 'warn' ? 'warn' : 'fail';
|
|
1452
|
+
if (!project_dir) {
|
|
1453
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: false, message: 'project_dir is required.' }) }], isError: true };
|
|
1454
|
+
}
|
|
1455
|
+
const start = Date.now();
|
|
1456
|
+
// Read diff if not provided
|
|
1457
|
+
let diff = diff_input;
|
|
1458
|
+
if (!diff) {
|
|
1459
|
+
const { execSync } = await import('node:child_process');
|
|
1460
|
+
try {
|
|
1461
|
+
diff = execSync('git diff HEAD', { cwd: project_dir, encoding: 'utf8', timeout: 15000 });
|
|
1462
|
+
}
|
|
1463
|
+
catch {
|
|
1464
|
+
diff = '';
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (!diff?.trim()) {
|
|
1468
|
+
return { content: [{ type: 'text', text: JSON.stringify({ verdict: 'pass', exit_code: 0, message: 'No changes detected.', duration_ms: Date.now() - start }) }] };
|
|
1469
|
+
}
|
|
1470
|
+
// Run all three checks in parallel
|
|
1471
|
+
const projectCtx = (() => { try {
|
|
1472
|
+
return buildContextString(project_dir);
|
|
1473
|
+
}
|
|
1474
|
+
catch {
|
|
1475
|
+
return '';
|
|
1476
|
+
} })();
|
|
1477
|
+
const fullContext = [context, projectCtx].filter(Boolean).join('\n\n');
|
|
1478
|
+
const [codeResult, secResult, secretsResult] = await Promise.all([
|
|
1479
|
+
executeOne({ id: 'ci-code', agent: 'reviewer', task: `CI code review of this diff:\n\n${diff}`, context: fullContext }),
|
|
1480
|
+
executeOne({ id: 'ci-sec', agent: 'security-scanner', task: `CI security scan of this diff:\n\n${diff}`, context: fullContext }),
|
|
1481
|
+
executeOne({ id: 'ci-secrets', agent: 'secrets', task: `CI secrets scan — check for exposed credentials in this diff:\n\n${diff}`, context: fullContext }),
|
|
1482
|
+
]);
|
|
1483
|
+
const codeConf = codeResult.output?.confidence ?? 0.8;
|
|
1484
|
+
const secConf = secResult.output?.confidence ?? 0.8;
|
|
1485
|
+
const secretsConf = secretsResult.output?.confidence ?? 1.0;
|
|
1486
|
+
// Determine verdict
|
|
1487
|
+
const hasCritical = codeConf < 0.4 || secConf < 0.4 || secretsConf < 0.5;
|
|
1488
|
+
const hasWarn = codeConf < 0.7 || secConf < 0.6;
|
|
1489
|
+
const rawVerdict = hasCritical ? 'fail' : hasWarn ? 'warn' : 'pass';
|
|
1490
|
+
const verdict = rawVerdict;
|
|
1491
|
+
const exit_code = rawVerdict === 'fail' || (rawVerdict === 'warn' && fail_on === 'warn') ? 1 : 0;
|
|
1492
|
+
const blocking_issues = [];
|
|
1493
|
+
if (codeConf < 0.7)
|
|
1494
|
+
blocking_issues.push(`Code review: ${codeResult.output?.recommendation ?? 'issues found'}`);
|
|
1495
|
+
if (secConf < 0.6)
|
|
1496
|
+
blocking_issues.push(`Security: ${secResult.output?.recommendation ?? 'vulnerabilities detected'}`);
|
|
1497
|
+
if (secretsConf < 0.5)
|
|
1498
|
+
blocking_issues.push(`Secrets: ${secretsResult.output?.recommendation ?? 'potential secrets exposed'}`);
|
|
1499
|
+
const icon = verdict === 'pass' ? '✅' : verdict === 'warn' ? '⚠️' : '❌';
|
|
1500
|
+
const ci_summary = [
|
|
1501
|
+
`${icon} **Veto CI Gate: ${verdict.toUpperCase()}**`,
|
|
1502
|
+
``,
|
|
1503
|
+
`| Check | Score | Status |`,
|
|
1504
|
+
`|---|---|---|`,
|
|
1505
|
+
`| Code Review | ${Math.round(codeConf * 100)}% | ${codeConf >= 0.7 ? '✅' : '❌'} |`,
|
|
1506
|
+
`| Security Scan | ${Math.round(secConf * 100)}% | ${secConf >= 0.6 ? '✅' : '❌'} |`,
|
|
1507
|
+
`| Secrets Scan | ${Math.round(secretsConf * 100)}% | ${secretsConf >= 0.5 ? '✅' : '❌'} |`,
|
|
1508
|
+
blocking_issues.length > 0 ? `\n**Blocking issues:**\n${blocking_issues.map(i => `- ${i}`).join('\n')}` : '',
|
|
1509
|
+
].filter(Boolean).join('\n');
|
|
1510
|
+
return {
|
|
1511
|
+
content: [{
|
|
1512
|
+
type: 'text',
|
|
1513
|
+
text: JSON.stringify({
|
|
1514
|
+
verdict, exit_code,
|
|
1515
|
+
checks: {
|
|
1516
|
+
code_review: { score: Math.round(codeConf * 100) },
|
|
1517
|
+
security: { score: Math.round(secConf * 100) },
|
|
1518
|
+
secrets: { score: Math.round(secretsConf * 100) },
|
|
1519
|
+
},
|
|
1520
|
+
blocking_issues,
|
|
1521
|
+
ci_summary,
|
|
1522
|
+
duration_ms: Date.now() - start,
|
|
1523
|
+
}, null, 2),
|
|
1524
|
+
}],
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1223
1527
|
default:
|
|
1224
1528
|
throw new Error(`Unknown tool: ${name}`);
|
|
1225
1529
|
}
|