@jigyasudham/veto 0.8.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +217 -54
  2. package/dist/adapters/index.js +4 -3
  3. package/dist/agents/executor.js +36 -3
  4. package/dist/cli.js +246 -7
  5. package/dist/context/reader.js +113 -0
  6. package/dist/council/index.js +3 -1
  7. package/dist/memory/local.js +18 -1
  8. package/dist/memory/schema.js +12 -10
  9. package/dist/plugins/loader.js +49 -0
  10. package/dist/router/index.js +2 -2
  11. package/dist/router/learning-updater.js +45 -1
  12. package/dist/server.js +507 -21
  13. package/dist/watcher/index.js +77 -0
  14. package/dist/workflow/pipeline.js +64 -0
  15. package/package.json +12 -3
  16. package/.claude/settings.local.json +0 -9
  17. package/src/adapters/claude.ts +0 -70
  18. package/src/adapters/codex.ts +0 -71
  19. package/src/adapters/gemini.ts +0 -71
  20. package/src/adapters/index.ts +0 -217
  21. package/src/agents/development/api.ts +0 -120
  22. package/src/agents/development/backend.ts +0 -85
  23. package/src/agents/development/coder.ts +0 -213
  24. package/src/agents/development/database.ts +0 -83
  25. package/src/agents/development/debugger.ts +0 -238
  26. package/src/agents/development/devops.ts +0 -86
  27. package/src/agents/development/frontend.ts +0 -85
  28. package/src/agents/development/migration.ts +0 -144
  29. package/src/agents/development/performance.ts +0 -144
  30. package/src/agents/development/refactor.ts +0 -86
  31. package/src/agents/development/reviewer.ts +0 -268
  32. package/src/agents/development/tester.ts +0 -151
  33. package/src/agents/executor.ts +0 -158
  34. package/src/agents/memory/context-manager.ts +0 -171
  35. package/src/agents/memory/decision-logger.ts +0 -160
  36. package/src/agents/memory/knowledge-base.ts +0 -124
  37. package/src/agents/memory/pattern-learner.ts +0 -143
  38. package/src/agents/memory/project-mapper.ts +0 -118
  39. package/src/agents/quality/accessibility.ts +0 -99
  40. package/src/agents/quality/code-quality.ts +0 -115
  41. package/src/agents/quality/compatibility.ts +0 -58
  42. package/src/agents/quality/documentation.ts +0 -105
  43. package/src/agents/quality/error-handling.ts +0 -96
  44. package/src/agents/research/competitor-analyzer.ts +0 -45
  45. package/src/agents/research/cost-analyzer.ts +0 -54
  46. package/src/agents/research/estimator.ts +0 -60
  47. package/src/agents/research/ethics-bias.ts +0 -113
  48. package/src/agents/research/researcher.ts +0 -114
  49. package/src/agents/research/risk-assessor.ts +0 -63
  50. package/src/agents/research/tech-advisor.ts +0 -55
  51. package/src/agents/security/auth.ts +0 -287
  52. package/src/agents/security/dependency-audit.ts +0 -337
  53. package/src/agents/security/penetration.ts +0 -262
  54. package/src/agents/security/privacy.ts +0 -285
  55. package/src/agents/security/scanner.ts +0 -322
  56. package/src/agents/security/secrets.ts +0 -249
  57. package/src/agents/types.ts +0 -66
  58. package/src/agents/workflow/automation.ts +0 -59
  59. package/src/agents/workflow/file-manager.ts +0 -52
  60. package/src/agents/workflow/git-agent.ts +0 -55
  61. package/src/agents/workflow/reporter.ts +0 -51
  62. package/src/agents/workflow/search-agent.ts +0 -40
  63. package/src/agents/workflow/task-coordinator.ts +0 -41
  64. package/src/agents/workflow/task-planner.ts +0 -47
  65. package/src/cli.ts +0 -204
  66. package/src/council/decision-engine.ts +0 -171
  67. package/src/council/devil-advocate.ts +0 -116
  68. package/src/council/index.ts +0 -44
  69. package/src/council/lead-developer.ts +0 -118
  70. package/src/council/legal-compliance.ts +0 -152
  71. package/src/council/product-manager.ts +0 -102
  72. package/src/council/security.ts +0 -172
  73. package/src/council/system-architect.ts +0 -132
  74. package/src/council/types.ts +0 -33
  75. package/src/council/ux-designer.ts +0 -121
  76. package/src/memory/local.ts +0 -305
  77. package/src/memory/schema.ts +0 -174
  78. package/src/memory/sync.ts +0 -274
  79. package/src/router/complexity-scorer.ts +0 -96
  80. package/src/router/context-compressor.ts +0 -74
  81. package/src/router/index.ts +0 -60
  82. package/src/router/learning-updater.ts +0 -271
  83. package/src/router/model-selector.ts +0 -83
  84. package/src/router/rate-monitor.ts +0 -103
  85. package/src/server.ts +0 -1038
  86. package/src/skills/development/skill-api-design.ts +0 -329
  87. package/src/skills/development/skill-auth.ts +0 -271
  88. package/src/skills/development/skill-ci-cd.ts +0 -0
  89. package/src/skills/development/skill-crud.ts +0 -209
  90. package/src/skills/development/skill-db-schema.ts +0 -0
  91. package/src/skills/development/skill-docker.ts +0 -0
  92. package/src/skills/development/skill-env-setup.ts +0 -0
  93. package/src/skills/development/skill-scaffold.ts +0 -323
  94. package/src/skills/intelligence/skill-complexity-score.ts +0 -69
  95. package/src/skills/intelligence/skill-cost-track.ts +0 -39
  96. package/src/skills/intelligence/skill-learning-loop.ts +0 -69
  97. package/src/skills/intelligence/skill-pattern-detect.ts +0 -38
  98. package/src/skills/intelligence/skill-rate-watch.ts +0 -61
  99. package/src/skills/memory/skill-context-compress.ts +0 -98
  100. package/src/skills/memory/skill-cross-sync.ts +0 -104
  101. package/src/skills/memory/skill-decision-log.ts +0 -119
  102. package/src/skills/memory/skill-session-restore.ts +0 -59
  103. package/src/skills/memory/skill-session-save.ts +0 -94
  104. package/src/skills/quality/skill-accessibility.ts +0 -0
  105. package/src/skills/quality/skill-code-review.ts +0 -84
  106. package/src/skills/quality/skill-docs-gen.ts +0 -0
  107. package/src/skills/quality/skill-perf-audit.ts +0 -0
  108. package/src/skills/quality/skill-security-scan.ts +0 -91
  109. package/src/skills/quality/skill-test-suite.ts +0 -290
  110. package/src/skills/workflow/skill-deploy.ts +0 -0
  111. package/src/skills/workflow/skill-git-workflow.ts +0 -0
  112. package/src/skills/workflow/skill-rollback.ts +0 -0
  113. package/src/skills/workflow/skill-task-breakdown.ts +0 -0
  114. 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.3",
4
- "description": "50 agents. 28 skills. 3 AIs. Self-learning. Zero extra cost.",
3
+ "version": "1.1.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
- }