@jigyasudham/veto 0.8.2 → 1.0.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.
Files changed (111) hide show
  1. package/README.md +209 -52
  2. package/dist/agents/executor.js +36 -3
  3. package/dist/cli.js +350 -51
  4. package/dist/context/reader.js +113 -0
  5. package/dist/council/index.js +3 -1
  6. package/dist/plugins/loader.js +49 -0
  7. package/dist/router/index.js +2 -2
  8. package/dist/router/learning-updater.js +45 -1
  9. package/dist/server.js +478 -14
  10. package/dist/watcher/index.js +77 -0
  11. package/dist/workflow/pipeline.js +64 -0
  12. package/package.json +12 -3
  13. package/.claude/settings.local.json +0 -9
  14. package/src/adapters/claude.ts +0 -70
  15. package/src/adapters/codex.ts +0 -71
  16. package/src/adapters/gemini.ts +0 -71
  17. package/src/adapters/index.ts +0 -217
  18. package/src/agents/development/api.ts +0 -120
  19. package/src/agents/development/backend.ts +0 -85
  20. package/src/agents/development/coder.ts +0 -213
  21. package/src/agents/development/database.ts +0 -83
  22. package/src/agents/development/debugger.ts +0 -238
  23. package/src/agents/development/devops.ts +0 -86
  24. package/src/agents/development/frontend.ts +0 -85
  25. package/src/agents/development/migration.ts +0 -144
  26. package/src/agents/development/performance.ts +0 -144
  27. package/src/agents/development/refactor.ts +0 -86
  28. package/src/agents/development/reviewer.ts +0 -268
  29. package/src/agents/development/tester.ts +0 -151
  30. package/src/agents/executor.ts +0 -158
  31. package/src/agents/memory/context-manager.ts +0 -171
  32. package/src/agents/memory/decision-logger.ts +0 -160
  33. package/src/agents/memory/knowledge-base.ts +0 -124
  34. package/src/agents/memory/pattern-learner.ts +0 -143
  35. package/src/agents/memory/project-mapper.ts +0 -118
  36. package/src/agents/quality/accessibility.ts +0 -99
  37. package/src/agents/quality/code-quality.ts +0 -115
  38. package/src/agents/quality/compatibility.ts +0 -58
  39. package/src/agents/quality/documentation.ts +0 -105
  40. package/src/agents/quality/error-handling.ts +0 -96
  41. package/src/agents/research/competitor-analyzer.ts +0 -45
  42. package/src/agents/research/cost-analyzer.ts +0 -54
  43. package/src/agents/research/estimator.ts +0 -60
  44. package/src/agents/research/ethics-bias.ts +0 -113
  45. package/src/agents/research/researcher.ts +0 -114
  46. package/src/agents/research/risk-assessor.ts +0 -63
  47. package/src/agents/research/tech-advisor.ts +0 -55
  48. package/src/agents/security/auth.ts +0 -287
  49. package/src/agents/security/dependency-audit.ts +0 -337
  50. package/src/agents/security/penetration.ts +0 -262
  51. package/src/agents/security/privacy.ts +0 -285
  52. package/src/agents/security/scanner.ts +0 -322
  53. package/src/agents/security/secrets.ts +0 -249
  54. package/src/agents/types.ts +0 -66
  55. package/src/agents/workflow/automation.ts +0 -59
  56. package/src/agents/workflow/file-manager.ts +0 -52
  57. package/src/agents/workflow/git-agent.ts +0 -55
  58. package/src/agents/workflow/reporter.ts +0 -51
  59. package/src/agents/workflow/search-agent.ts +0 -40
  60. package/src/agents/workflow/task-coordinator.ts +0 -41
  61. package/src/agents/workflow/task-planner.ts +0 -47
  62. package/src/cli.ts +0 -135
  63. package/src/council/decision-engine.ts +0 -171
  64. package/src/council/devil-advocate.ts +0 -116
  65. package/src/council/index.ts +0 -44
  66. package/src/council/lead-developer.ts +0 -118
  67. package/src/council/legal-compliance.ts +0 -152
  68. package/src/council/product-manager.ts +0 -102
  69. package/src/council/security.ts +0 -172
  70. package/src/council/system-architect.ts +0 -132
  71. package/src/council/types.ts +0 -33
  72. package/src/council/ux-designer.ts +0 -121
  73. package/src/memory/local.ts +0 -305
  74. package/src/memory/schema.ts +0 -174
  75. package/src/memory/sync.ts +0 -274
  76. package/src/router/complexity-scorer.ts +0 -96
  77. package/src/router/context-compressor.ts +0 -74
  78. package/src/router/index.ts +0 -60
  79. package/src/router/learning-updater.ts +0 -271
  80. package/src/router/model-selector.ts +0 -83
  81. package/src/router/rate-monitor.ts +0 -103
  82. package/src/server.ts +0 -1038
  83. package/src/skills/development/skill-api-design.ts +0 -329
  84. package/src/skills/development/skill-auth.ts +0 -271
  85. package/src/skills/development/skill-ci-cd.ts +0 -0
  86. package/src/skills/development/skill-crud.ts +0 -209
  87. package/src/skills/development/skill-db-schema.ts +0 -0
  88. package/src/skills/development/skill-docker.ts +0 -0
  89. package/src/skills/development/skill-env-setup.ts +0 -0
  90. package/src/skills/development/skill-scaffold.ts +0 -323
  91. package/src/skills/intelligence/skill-complexity-score.ts +0 -69
  92. package/src/skills/intelligence/skill-cost-track.ts +0 -39
  93. package/src/skills/intelligence/skill-learning-loop.ts +0 -69
  94. package/src/skills/intelligence/skill-pattern-detect.ts +0 -38
  95. package/src/skills/intelligence/skill-rate-watch.ts +0 -61
  96. package/src/skills/memory/skill-context-compress.ts +0 -98
  97. package/src/skills/memory/skill-cross-sync.ts +0 -104
  98. package/src/skills/memory/skill-decision-log.ts +0 -119
  99. package/src/skills/memory/skill-session-restore.ts +0 -59
  100. package/src/skills/memory/skill-session-save.ts +0 -94
  101. package/src/skills/quality/skill-accessibility.ts +0 -0
  102. package/src/skills/quality/skill-code-review.ts +0 -84
  103. package/src/skills/quality/skill-docs-gen.ts +0 -0
  104. package/src/skills/quality/skill-perf-audit.ts +0 -0
  105. package/src/skills/quality/skill-security-scan.ts +0 -91
  106. package/src/skills/quality/skill-test-suite.ts +0 -290
  107. package/src/skills/workflow/skill-deploy.ts +0 -0
  108. package/src/skills/workflow/skill-git-workflow.ts +0 -0
  109. package/src/skills/workflow/skill-rollback.ts +0 -0
  110. package/src/skills/workflow/skill-task-breakdown.ts +0 -0
  111. package/tsconfig.json +0 -20
@@ -0,0 +1,77 @@
1
+ import { watch } from 'node:fs';
2
+ import { extname, basename, resolve } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ const sessions = new Map();
5
+ const EXT_RULES = [
6
+ [(f) => basename(f) === 'package.json', 'dependency-audit', 'veto_agent_plan', 'package.json changed — check for new CVEs or unwanted deps'],
7
+ [(f) => /\.(env|env\..+)$/.test(basename(f)), 'secrets', 'veto_secrets_scan', '.env file changed — scan for exposed credentials'],
8
+ [(f, e) => ['.sql', '.prisma'].includes(e), 'database', 'veto_agent_plan', 'Database schema file changed'],
9
+ [(f, e) => /\.(test|spec)\.(ts|js|tsx|jsx)$/.test(f), 'tester', 'veto_agent_plan', 'Test file changed — review test coverage'],
10
+ [(f, e) => ['.ts', '.tsx', '.js', '.jsx'].includes(e), 'code-quality', 'veto_code_review', 'Source file saved — code quality check recommended'],
11
+ [(f, e) => ['.yaml', '.yml', '.toml'].includes(e), 'devops', 'veto_agent_plan', 'Config file changed'],
12
+ [(f, e) => e === '.json' && basename(f) !== 'package.json', 'coder', 'veto_agent_plan', 'JSON config changed'],
13
+ ];
14
+ function classify(file) {
15
+ const ext = extname(file).toLowerCase();
16
+ for (const [match, agent, tool, reason] of EXT_RULES) {
17
+ if (match(file, ext))
18
+ return { agent, tool, reason };
19
+ }
20
+ return { agent: 'coder', tool: 'veto_agent_plan', reason: 'File changed' };
21
+ }
22
+ export function startWatch(projectDir) {
23
+ const dir = resolve(projectDir);
24
+ const id = randomUUID().slice(0, 8);
25
+ const watcher = watch(dir, { recursive: true }, (eventType, filename) => {
26
+ if (!filename)
27
+ return;
28
+ // ignore node_modules, .git, dist
29
+ if (/node_modules|\.git|dist|\.veto/.test(filename))
30
+ return;
31
+ const session = sessions.get(id);
32
+ if (!session)
33
+ return;
34
+ const { agent, tool, reason } = classify(filename);
35
+ session.events.push({
36
+ event_type: eventType === 'rename' ? 'rename' : 'change',
37
+ file: filename,
38
+ ext: extname(filename).toLowerCase(),
39
+ triggered_at: new Date().toISOString(),
40
+ recommended_agent: agent,
41
+ suggested_tool: tool,
42
+ reason,
43
+ });
44
+ });
45
+ sessions.set(id, {
46
+ watcher,
47
+ project_dir: dir,
48
+ events: [],
49
+ started_at: new Date().toISOString(),
50
+ });
51
+ return id;
52
+ }
53
+ export function pollWatch(watchId) {
54
+ const session = sessions.get(watchId);
55
+ if (!session)
56
+ return { found: false, events: [] };
57
+ const events = [...session.events];
58
+ session.events = [];
59
+ return { found: true, events, project_dir: session.project_dir };
60
+ }
61
+ export function stopWatch(watchId) {
62
+ const session = sessions.get(watchId);
63
+ if (!session)
64
+ return false;
65
+ session.watcher.close();
66
+ sessions.delete(watchId);
67
+ return true;
68
+ }
69
+ export function listWatches() {
70
+ return Array.from(sessions.entries()).map(([id, s]) => ({
71
+ id,
72
+ project_dir: s.project_dir,
73
+ started_at: s.started_at,
74
+ pending_events: s.events.length,
75
+ }));
76
+ }
77
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,64 @@
1
+ import { executeOne } from '../agents/executor.js';
2
+ export async function runPipeline(steps, globalProjectDir) {
3
+ const results = [];
4
+ const start = Date.now();
5
+ let stoppedAt;
6
+ let stopReason;
7
+ for (const step of steps) {
8
+ const result = await executeOne({
9
+ id: step.id,
10
+ agent: step.agent,
11
+ task: step.task,
12
+ code: step.code,
13
+ context: step.context,
14
+ project_dir: step.project_dir ?? globalProjectDir,
15
+ });
16
+ const confidence = result.output.confidence;
17
+ const confidencePct = Math.round(confidence * 100);
18
+ if (result.error) {
19
+ results.push({
20
+ id: step.id, agent: step.agent, status: 'error',
21
+ confidence: 0, severity: 'critical',
22
+ recommendation: result.error,
23
+ gate: step.gate, duration_ms: result.duration_ms, error: result.error,
24
+ });
25
+ stoppedAt = step.id;
26
+ stopReason = `Step "${step.id}" errored: ${result.error}`;
27
+ break;
28
+ }
29
+ const gateFailed = step.gate !== undefined && confidencePct < step.gate;
30
+ results.push({
31
+ id: step.id, agent: step.agent,
32
+ status: gateFailed ? 'failed_gate' : 'passed',
33
+ confidence: confidencePct,
34
+ severity: result.output.severity,
35
+ recommendation: result.output.recommendation,
36
+ gate: step.gate,
37
+ duration_ms: result.duration_ms,
38
+ });
39
+ if (gateFailed) {
40
+ stoppedAt = step.id;
41
+ stopReason = `Step "${step.id}" confidence ${confidencePct}% below gate ${step.gate}%`;
42
+ // mark remaining steps as skipped
43
+ const remaining = steps.slice(steps.indexOf(step) + 1);
44
+ for (const s of remaining) {
45
+ results.push({ id: s.id, agent: s.agent, status: 'skipped', confidence: 0, severity: 'info', recommendation: 'Skipped — upstream gate failed', gate: s.gate, duration_ms: 0 });
46
+ }
47
+ break;
48
+ }
49
+ }
50
+ const passed = results.filter(r => r.status === 'passed').length;
51
+ const failed = results.filter(r => r.status === 'failed_gate' || r.status === 'error').length;
52
+ const verdict = failed === 0 ? 'passed' : passed > 0 ? 'partial' : 'failed';
53
+ return {
54
+ verdict,
55
+ steps_total: steps.length,
56
+ steps_passed: passed,
57
+ steps_failed: failed,
58
+ stopped_at: stoppedAt,
59
+ stop_reason: stopReason,
60
+ results,
61
+ total_duration_ms: Date.now() - start,
62
+ };
63
+ }
64
+ //# sourceMappingURL=pipeline.js.map
package/package.json CHANGED
@@ -1,14 +1,23 @@
1
1
  {
2
2
  "name": "@jigyasudham/veto",
3
- "version": "0.8.2",
4
- "description": "50 agents. 28 skills. 3 AIs. Self-learning. Zero extra cost.",
3
+ "version": "1.0.0",
4
+ "description": "50 agents. 34 tools. 3 AIs. Self-learning. Zero extra cost.",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "ai",
8
8
  "claude",
9
9
  "agents",
10
10
  "memory",
11
- "council"
11
+ "council",
12
+ "gemini",
13
+ "codex",
14
+ "cursor",
15
+ "windsurf",
16
+ "vscode",
17
+ "self-learning",
18
+ "router",
19
+ "workflow",
20
+ "code-review"
12
21
  ],
13
22
  "author": "Jigyasu Dham <jigyasudham18@gmail.com>",
14
23
  "license": "MIT",
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "PowerShell(Get-ChildItem D:\\\\Veto\\\\src -Recurse -File | Where-Object { $_.FullName -notmatch \"node_modules\" } | Select-Object FullName | ForEach-Object { $_.FullName.Replace\\(\"D:\\\\Veto\\\\\", \"\"\\) })",
5
- "PowerShell(cd D:\\\\Veto; npm run build 2>&1; Write-Host \"Exit: $LASTEXITCODE\")",
6
- "PowerShell(Get-ChildItem D:\\\\Veto\\\\docs -File | Select-Object FullName; Get-ChildItem D:\\\\Veto -File | Where-Object { $_.Extension -eq \".md\" } | Select-Object FullName)"
7
- ]
8
- }
9
- }
@@ -1,70 +0,0 @@
1
- // Claude Code adapter — connection config, rate signal detection, handoff format
2
-
3
- export const PLATFORM = 'claude' as const;
4
-
5
- export type AdapterSetup = {
6
- platform: string;
7
- mcp_config: object;
8
- setup_steps: string[];
9
- rate_limit_signals: string[];
10
- continue_command: string;
11
- notes: string[];
12
- };
13
-
14
- export function getSetup(vetoServerPath: string): AdapterSetup {
15
- return {
16
- platform: 'claude',
17
- mcp_config: {
18
- mcpServers: {
19
- veto: {
20
- command: 'node',
21
- args: [vetoServerPath],
22
- },
23
- },
24
- },
25
- setup_steps: [
26
- `1. Build veto: npm run build`,
27
- `2. Add to ~/.claude/mcp_servers.json:\n${JSON.stringify({ mcpServers: { veto: { command: 'node', args: [vetoServerPath] } } }, null, 2)}`,
28
- `3. Restart Claude Code`,
29
- `4. Verify: call veto_status — should return { "status": "running", "version": "0.7.0" }`,
30
- ],
31
- rate_limit_signals: [
32
- 'rate limit exceeded',
33
- 'too many requests',
34
- '429',
35
- 'overloaded',
36
- 'capacity',
37
- 'quota exceeded',
38
- ],
39
- continue_command: 'veto_continue',
40
- notes: [
41
- 'Claude Code connects to Veto via stdio MCP — the server runs as a child process',
42
- 'All veto_* tools are available natively in Claude Code once connected',
43
- 'Rate limits are tracked by Veto per day — call veto_rate_status to check headroom',
44
- 'Before hitting the limit: call veto_handoff to get a session ID and switch instructions',
45
- ],
46
- };
47
- }
48
-
49
- export function detectRateLimit(errorText: string): boolean {
50
- const signals = ['rate limit', 'too many requests', '429', 'overloaded', 'quota'];
51
- return signals.some(s => errorText.toLowerCase().includes(s));
52
- }
53
-
54
- export function formatHandoffMessage(sessionId: string, targetPlatform: 'gemini' | 'codex'): string {
55
- const target = targetPlatform === 'gemini' ? 'Gemini CLI' : 'Codex CLI';
56
- return [
57
- `Claude is approaching its rate limit. Switching to ${target}.`,
58
- ``,
59
- `Session saved. ID: ${sessionId}`,
60
- ``,
61
- `On ${target}, run:`,
62
- ` veto_continue`,
63
- ``,
64
- `Veto will restore full context automatically. Nothing needs to be re-explained.`,
65
- ].join('\n');
66
- }
67
-
68
- export function formatSwitchPrompt(sessionId: string): string {
69
- return `Rate limit approaching. Session saved (ID: ${sessionId}). Call veto_continue on the next platform to resume instantly.`;
70
- }
@@ -1,71 +0,0 @@
1
- // Codex CLI adapter — connection config, rate signal detection, handoff format
2
- // Codex CLI: https://github.com/openai/codex
3
-
4
- export const PLATFORM = 'codex' as const;
5
-
6
- export type AdapterSetup = {
7
- platform: string;
8
- mcp_config: object;
9
- setup_steps: string[];
10
- rate_limit_signals: string[];
11
- continue_command: string;
12
- notes: string[];
13
- };
14
-
15
- export function getSetup(vetoServerPath: string): AdapterSetup {
16
- return {
17
- platform: 'codex',
18
- mcp_config: {
19
- mcpServers: {
20
- veto: {
21
- command: 'node',
22
- args: [vetoServerPath],
23
- },
24
- },
25
- },
26
- setup_steps: [
27
- `1. Install Codex CLI: npm install -g @openai/codex`,
28
- `2. Set API key: export OPENAI_API_KEY=your-key`,
29
- `3. Add to ~/.codex/config.json:\n${JSON.stringify({ mcpServers: { veto: { command: 'node', args: [vetoServerPath] } } }, null, 2)}`,
30
- `4. Restart Codex CLI`,
31
- `5. Verify: call veto_status — should return { "status": "running", "version": "0.7.0" }`,
32
- ],
33
- rate_limit_signals: [
34
- 'rate limit reached',
35
- 'too many requests',
36
- '429',
37
- 'insufficient quota',
38
- 'rate_limit_exceeded',
39
- 'quota_exceeded',
40
- ],
41
- continue_command: 'veto_continue',
42
- notes: [
43
- 'Codex CLI connects to Veto via stdio MCP — same server instance as Claude and Gemini',
44
- 'Config path: ~/.codex/config.json (created automatically on first run)',
45
- 'All veto_* tools work identically on Codex as on Claude and Gemini',
46
- 'Codex is the Tier 1/2 overflow when both Claude and Gemini are rate-limited',
47
- 'Uses GPT-4o / o4-mini depending on the tier assigned by the Veto router',
48
- 'ChatGPT web app does NOT support MCP — Codex CLI is the only OpenAI option',
49
- ],
50
- };
51
- }
52
-
53
- export function detectRateLimit(errorText: string): boolean {
54
- const signals = ['rate limit', 'too many requests', '429', 'insufficient quota', 'rate_limit_exceeded'];
55
- return signals.some(s => errorText.toLowerCase().includes(s.toLowerCase()));
56
- }
57
-
58
- export function formatHandoffMessage(sessionId: string, fromPlatform: 'claude' | 'gemini'): string {
59
- const from = fromPlatform === 'claude' ? 'Claude' : 'Gemini';
60
- return [
61
- `${from} handed off to Codex. Session restored.`,
62
- ``,
63
- `Session ID: ${sessionId}`,
64
- ``,
65
- `Codex has full context. Continue the task as if nothing interrupted.`,
66
- ].join('\n');
67
- }
68
-
69
- export function formatContinueInstructions(): string {
70
- return 'Run: veto_continue\nVeto will restore the most recent session automatically.';
71
- }
@@ -1,71 +0,0 @@
1
- // Gemini CLI adapter — connection config, rate signal detection, handoff format
2
- // Gemini CLI MCP support: https://github.com/google-gemini/gemini-cli
3
-
4
- export const PLATFORM = 'gemini' as const;
5
-
6
- export type AdapterSetup = {
7
- platform: string;
8
- mcp_config: object;
9
- setup_steps: string[];
10
- rate_limit_signals: string[];
11
- continue_command: string;
12
- notes: string[];
13
- };
14
-
15
- export function getSetup(vetoServerPath: string): AdapterSetup {
16
- return {
17
- platform: 'gemini',
18
- mcp_config: {
19
- mcpServers: {
20
- veto: {
21
- command: 'node',
22
- args: [vetoServerPath],
23
- },
24
- },
25
- },
26
- setup_steps: [
27
- `1. Install Gemini CLI: npm install -g @google/gemini-cli`,
28
- `2. Authenticate: gemini auth`,
29
- `3. Add to ~/.gemini/settings.json:\n${JSON.stringify({ mcpServers: { veto: { command: 'node', args: [vetoServerPath] } } }, null, 2)}`,
30
- `4. Restart Gemini CLI`,
31
- `5. Verify: call veto_status — should return { "status": "running", "version": "0.7.0" }`,
32
- ],
33
- rate_limit_signals: [
34
- 'quota exceeded',
35
- 'resource exhausted',
36
- '429',
37
- 'rate limit',
38
- 'too many requests',
39
- 'RESOURCE_EXHAUSTED',
40
- ],
41
- continue_command: 'veto_continue',
42
- notes: [
43
- 'Gemini CLI connects to Veto via stdio MCP — same server instance as Claude',
44
- 'Gemini CLI uses the same ~/.gemini/settings.json for MCP server config',
45
- 'All veto_* tools work identically on Gemini as on Claude',
46
- 'Gemini Flash is the default Tier 1/2 overflow platform when Claude is rate-limited',
47
- 'Free tier: 1,500 requests/day (15 RPM) — Veto tracks this in rate_usage table',
48
- ],
49
- };
50
- }
51
-
52
- export function detectRateLimit(errorText: string): boolean {
53
- const signals = ['quota exceeded', 'resource exhausted', '429', 'rate limit', 'RESOURCE_EXHAUSTED'];
54
- return signals.some(s => errorText.toLowerCase().includes(s.toLowerCase()));
55
- }
56
-
57
- export function formatHandoffMessage(sessionId: string, fromPlatform: 'claude' | 'codex'): string {
58
- const from = fromPlatform === 'claude' ? 'Claude' : 'Codex';
59
- return [
60
- `${from} handed off to Gemini. Session restored.`,
61
- ``,
62
- `Session ID: ${sessionId}`,
63
- ``,
64
- `Gemini has full context. Continue the task as if nothing interrupted.`,
65
- `Call veto_session_restore { "session_id": "${sessionId}" } if context needs refreshing.`,
66
- ].join('\n');
67
- }
68
-
69
- export function formatContinueInstructions(): string {
70
- return 'Run: veto_continue\nVeto will restore the most recent session automatically.';
71
- }
@@ -1,217 +0,0 @@
1
- // Handoff engine — platform detection, session save+switch, continue restore
2
-
3
- import { getRateStatus, trackRequest } from '../router/rate-monitor.js';
4
- import { saveSession, listSessions, restoreSession } from '../memory/local.js';
5
- import type { Platform } from '../router/rate-monitor.js';
6
- import * as claude from './claude.js';
7
- import * as gemini from './gemini.js';
8
- import * as codex from './codex.js';
9
-
10
- export type HandoffResult = {
11
- session_id: string;
12
- saved_at: string;
13
- from_platform: Platform;
14
- to_platform: Platform;
15
- reason: string;
16
- rate_status: ReturnType<typeof getRateStatus>;
17
- instructions: string;
18
- one_liner: string;
19
- };
20
-
21
- export type ContinueResult = {
22
- found: boolean;
23
- session_id?: string;
24
- platform?: string;
25
- summary?: string;
26
- context?: unknown;
27
- task_state?: unknown;
28
- next_action?: string;
29
- project_dir?: string;
30
- token_count?: number;
31
- message: string;
32
- restored_at: string;
33
- };
34
-
35
- export type PlatformSetupResult = {
36
- platform: Platform;
37
- mcp_config: object;
38
- setup_steps: string[];
39
- rate_limit_signals: string[];
40
- continue_command: string;
41
- notes: string[];
42
- };
43
-
44
- // ─── Handoff ──────────────────────────────────────────────────────────────────
45
-
46
- export function handoff(options: {
47
- summary?: string;
48
- context?: string;
49
- task_state?: string;
50
- from_platform?: Platform;
51
- to_platform?: Platform;
52
- token_count?: number;
53
- project_dir?: string;
54
- }): HandoffResult {
55
- const rateStatus = getRateStatus();
56
- const from: Platform = options.from_platform ?? 'claude';
57
-
58
- // Pick the best available target platform
59
- const to: Platform = options.to_platform ?? selectTarget(from, rateStatus);
60
-
61
- const reason = rateStatus[from].status === 'critical'
62
- ? `${from} is at ${rateStatus[from].used_percent}% of daily limit`
63
- : rateStatus[from].status === 'warning'
64
- ? `${from} is at ${rateStatus[from].used_percent}% — switching proactively`
65
- : 'Manual platform switch requested';
66
-
67
- const { session_id, saved_at } = saveSession({
68
- platform: from,
69
- summary: options.summary ?? `Session handed off from ${from} to ${to}`,
70
- context: options.context,
71
- task_state: options.task_state,
72
- token_count: options.token_count ?? 0,
73
- project_dir: options.project_dir,
74
- });
75
-
76
- const instructions = buildInstructions(session_id, from, to, rateStatus);
77
- const one_liner = `Session saved (${session_id.slice(0, 8)}…). On ${to}: call veto_continue`;
78
-
79
- return {
80
- session_id,
81
- saved_at,
82
- from_platform: from,
83
- to_platform: to,
84
- reason,
85
- rate_status: rateStatus,
86
- instructions,
87
- one_liner,
88
- };
89
- }
90
-
91
- // ─── Continue ─────────────────────────────────────────────────────────────────
92
-
93
- export function continueSession(sessionId?: string): ContinueResult {
94
- const now = new Date().toISOString();
95
-
96
- if (sessionId) {
97
- const result = restoreSession(sessionId);
98
- if (!result.found || !result.session) {
99
- return { found: false, message: `No session found with ID: ${sessionId}`, restored_at: now };
100
- }
101
- return buildContinueResult(result.session, now);
102
- }
103
-
104
- // No ID given — find the most recent session
105
- const sessions = listSessions(1);
106
- if (sessions.length === 0) {
107
- return {
108
- found: false,
109
- message: 'No saved sessions found. Save a session first with veto_handoff or veto_session_save.',
110
- restored_at: now,
111
- };
112
- }
113
-
114
- const result = restoreSession(sessions[0].id);
115
- if (!result.found || !result.session) {
116
- return { found: false, message: 'Could not restore the most recent session.', restored_at: now };
117
- }
118
-
119
- return buildContinueResult(result.session, now);
120
- }
121
-
122
- function buildContinueResult(session: ReturnType<typeof listSessions>[0], now: string): ContinueResult {
123
- let context: unknown = null;
124
- let task_state: unknown = null;
125
- let next_action: string | undefined;
126
-
127
- try {
128
- context = session.context ? JSON.parse(session.context) : null;
129
- } catch { context = session.context; }
130
-
131
- try {
132
- let ts: unknown = session.task_state ? JSON.parse(session.task_state) : null;
133
- // saveSession double-stringifies when given a pre-serialised string — unwrap if needed
134
- if (typeof ts === 'string') { try { ts = JSON.parse(ts); } catch { /* keep as string */ } }
135
- task_state = ts;
136
- if (ts && typeof ts === 'object' && 'nextAction' in ts) {
137
- next_action = String((ts as Record<string, unknown>)['nextAction']);
138
- }
139
- } catch { task_state = session.task_state; }
140
-
141
- const message = [
142
- `Session restored from ${session.platform} (saved ${session.started_at.slice(0, 16)}).`,
143
- session.summary ? `\nSummary: ${session.summary}` : '',
144
- next_action ? `\nNext action: ${next_action}` : '',
145
- `\nContinue exactly where you left off. Nothing needs to be re-explained.`,
146
- ].filter(Boolean).join('');
147
-
148
- return {
149
- found: true,
150
- session_id: session.id,
151
- platform: session.platform,
152
- summary: session.summary ?? undefined,
153
- context,
154
- task_state,
155
- next_action,
156
- project_dir: session.project_dir ?? undefined,
157
- token_count: session.token_count,
158
- message,
159
- restored_at: now,
160
- };
161
- }
162
-
163
- // ─── Platform Setup ───────────────────────────────────────────────────────────
164
-
165
- export function getPlatformSetup(platform: Platform, vetoServerPath: string): PlatformSetupResult {
166
- if (platform === 'claude') return claude.getSetup(vetoServerPath) as PlatformSetupResult;
167
- if (platform === 'gemini') return gemini.getSetup(vetoServerPath) as PlatformSetupResult;
168
- return codex.getSetup(vetoServerPath) as PlatformSetupResult;
169
- }
170
-
171
- // ─── Helpers ──────────────────────────────────────────────────────────────────
172
-
173
- function selectTarget(from: Platform, rateStatus: ReturnType<typeof getRateStatus>): Platform {
174
- const order: Platform[] = from === 'claude'
175
- ? ['gemini', 'codex']
176
- : from === 'gemini'
177
- ? ['codex', 'claude']
178
- : ['claude', 'gemini'];
179
-
180
- for (const p of order) {
181
- if (rateStatus[p].status !== 'critical') return p;
182
- }
183
- // All critical — pick the one with most headroom remaining
184
- return order.reduce((best, p) =>
185
- rateStatus[p].used_percent < rateStatus[best].used_percent ? p : best
186
- , order[0]);
187
- }
188
-
189
- function buildInstructions(
190
- sessionId: string,
191
- from: Platform,
192
- to: Platform,
193
- rateStatus: ReturnType<typeof getRateStatus>
194
- ): string {
195
- const toStatus = rateStatus[to];
196
- const lines: string[] = [
197
- `── Veto Handoff ──────────────────────────────────────────`,
198
- `From: ${from.padEnd(8)} (${rateStatus[from].used_percent}% used)`,
199
- `To: ${to.padEnd(8)} (${toStatus.used_percent}% used)`,
200
- `Session: ${sessionId}`,
201
- ``,
202
- `On ${to}:`,
203
- ` 1. Open a new terminal with ${to} CLI`,
204
- ` 2. Veto is already running — same server, same memory`,
205
- ` 3. Call: veto_continue`,
206
- ` Veto restores full context automatically.`,
207
- ` Nothing needs to be re-explained.`,
208
- ``,
209
- `Or if you have the session ID:`,
210
- ` veto_continue { "session_id": "${sessionId}" }`,
211
- ``,
212
- `Rate resets at: ${rateStatus[from].resets_at.slice(0, 16)} UTC`,
213
- `─────────────────────────────────────────────────────────`,
214
- ];
215
-
216
- return lines.join('\n');
217
- }