1 line
35 KiB
JSON
1 line
35 KiB
JSON
{"repo": "baolux0904-rgb/baolux", "files": {"src/lib/multi-model-review.js": {"169ccebffa659905f6877c8bef26c462e925b3ef": {"t": 1785746667.8525956, "n": 11312, "h": "21de0ab04277304b", "c": "/**\n * multi-model-review.js\n *\n * Multi-model plan review: sends plan text to 6 LLMs in parallel,\n * collects issues/suggestions/scores, builds consensus, and presents\n * an interactive approval flow in the CLI.\n */\n\nimport Anthropic from '@anthropic-ai/sdk';\nimport readline from 'readline';\nimport { logCost } from './cost-monitor.js';\n\nconst REVIEW_PROMPT = (planText) => `You are a senior software architect reviewing a project plan.\nAnalyze this plan and respond in EXACTLY this JSON format (no markdown, no code fences):\n{\"issues\":[\"issue 1\",\"issue 2\"],\"suggestions\":[\"suggestion 1\"],\"score\":7}\n\nRules:\n- issues: list concrete problems (gaps, risks, contradictions, missing details). Empty array if none.\n- suggestions: list actionable improvements. Empty array if none.\n- score: integer 1-10 (1=terrible, 10=flawless).\n\nPlan to review:\n${planText}`;\n\nconst TIMEOUT_MS = 8000;\nconst OLLAMA_TIMEOUT_MS = 20000;\n\n// ═══ INDIVIDUAL REVIEWERS ═══\n\nasync function reviewWithGemini(planText) {\n const key = process.env.GEMINI_API_KEY;\n if (!key) throw new Error('GEMINI_API_KEY not set');\n\n const res = await fetch(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${key}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [{ parts: [{ text: REVIEW_PROMPT(planText) }] }],\n generationConfig: { temperature: 0.3, maxOutputTokens: 1024 },\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n }\n );\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text;\n return parseReview(text, 'Gemini 2.5 Flash');\n}\n\nasync function reviewWithGroq(planText) {\n const key = process.env.GROQ_API_KEY;\n if (!key) throw new Error('GROQ_API_KEY not set');\n\n const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },\n body: JSON.stringify({\n model: 'llama-3.3-70b-versatile',\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n temperature: 0.3,\n max_tokens: 1024,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content;\n return parseReview(text, 'Groq Llama 3.3 70B');\n}\n\nasync function reviewWithOpenAI(planText) {\n const key = process.env.OPENAI_API_KEY;\n if (!key) throw new Error('OPENAI_API_KEY not set');\n\n const res = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },\n body: JSON.stringify({\n model: 'gpt-4o-mini',\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n temperature: 0.3,\n max_tokens: 1024,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content;\n return parseReview(text, 'GPT-4o mini');\n}\n\nasync function reviewWithDeepSeek(planText) {\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) throw new Error('DEEPSEEK_API_KEY not set');\n\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },\n body: JSON.stringify({\n model: 'deepseek-chat',\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n temperature: 0.3,\n max_tokens: 1024,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content;\n return parseReview(text, 'DeepSeek');\n}\n\nasync function reviewWithOllama(planText) {\n const base = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';\n const model = process.env.OLLAMA_MODEL || 'deepseek-r1:7b';\n\n const res = await fetch(`${base}/api/generate`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model,\n prompt: REVIEW_PROMPT(planText),\n stream: false,\n options: { temperature: 0.3, num_predict: 1024 },\n }),\n signal: AbortSignal.timeout(OLLAMA_TIMEOUT_MS),\n });\n const data = await res.json();\n return parseReview(data.response, `Ollama ${model}`);\n}\n\nasync function reviewWithClaude(planText) {\n const key = process.env.ANTHROPIC_API_KEY;\n if (!key) throw new Error('ANTHROPIC_API_KEY not set');\n\n const client = new Anthropic();\n const msg = await Promise.race([\n client.messages.create({\n model: 'claude-haiku-4-5-20251001',\n max_tokens: 1024,\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n }),\n new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), TIMEOUT_MS)),\n ]);\n\n logCost({\n service: 'anthropic',\n model: msg.model || 'claude-haiku-4-5-20251001',\n agent: 'multi-model-review',\n inputTokens: msg.usage?.input_tokens || 0,\n outputTokens: msg.usage?.output_tokens || 0,\n });\n\n const text = msg.content?.[0]?.text;\n return parseReview(text, 'Claude Haiku 4.5');\n}\n\n// ═══ PARSE ═══\n\nfunction parseReview(text, modelName) {\n if (!text) throw new Error(`${modelName}: empty response`);\n\n // Strip markdown fences if present\n const cleaned = text.replace(/```json\\s*/g, '').replace(/```\\s*/g, '').trim();\n\n // Find JSON object in response\n const match = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (!match) throw new Error(`${modelName}: no JSON found in response`);\n\n const parsed = JSON.parse(match[0]);\n return {\n model: modelName,\n issues: Array.isArray(parsed.issues) ? parsed.issues.map(String) : [],\n suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.map(String) : [],\n score: Math.min(10, Math.max(1, Math.round(Number(parsed.score) || 5))),\n };\n}\n\n// ═══ REVIEW ORCHESTRATION ═══\n\nconst ALL_REVIEWERS = [\n { name: 'Gemini 2.5 Flash', fn: reviewWithGemini },\n { name: 'Groq Llama 3.3 70B', fn: reviewWithGroq },\n { name: 'GPT-4o mini', fn: reviewWithOpenAI },\n { name: 'DeepSeek', fn: reviewWithDeepSeek },\n { name: 'Ollama local', fn: reviewWithOllama },\n { name: 'Claude Haiku 4.5', fn: reviewWithClaude },\n];\n\n/**\n * Send plan to all 6 models in parallel, collect results.\n * Returns { reviews, consensus } or null if < 2 models succeeded.\n */\nexport async function reviewPlan(planText) {\n console.log('\\n[REVIEW] Sending plan to 6 models...');\n\n const results = await Promise.allSettled(\n ALL_REVIEWERS.map(async ({ name, fn }) => {\n try {\n const review = await fn(planText);\n console.log(` [OK] ${name} — score: ${review.score}/10`);\n return review;\n } catch (err) {\n console.log(` [SKIP] ${name} — ${err.message}`);\n throw err;\n }\n })\n );\n\n const reviews = results\n .filter((r) => r.status === 'fulfilled')\n .map((r) => r.value);\n\n if (reviews.length < 2) {\n console.log(`\\n[REVIEW] Only ${reviews.length} model(s) responded — need at least 2. Skipping review.`);\n return null;\n }\n\n console.log(`\\n[REVIEW] ${reviews.length}/${ALL_REVIEWERS.length} models responded.`);\n\n // ── Build consensus ──\n const issueCount = new Map();\n for (const review of reviews) {\n for (const issue of review.issues) {\n const key = issue.toLowerCase().trim();\n issueCount.set(key, (issueCount.get(key) || { text: issue, count: 0 }));\n issueCount.get(key).count++;\n }\n }\n\n const confirmedIssues = [];\n const possibleIssues = [];\n for (const { text, count } of issueCount.values()) {\n if (count >= 2) {\n confirmedIssues.push(text);\n } else {\n possibleIssues.push(text);\n }\n }\n\n // Merge all suggestions (deduplicate by lowercase)\n const seenSuggestions = new Set();\n const suggestions = [];\n for (const review of reviews) {\n for (const s of review.suggestions) {\n const key = s.toLowerCase().trim();\n if (!seenSuggestions.has(key)) {\n seenSuggestions.add(key);\n suggestions.push(s);\n }\n }\n }\n\n const avgScore = reviews.reduce((sum, r) => sum + r.score, 0) / reviews.length;\n\n const consensus = {\n avgScore: Math.round(avgScore * 10) / 10,\n confirmedIssues,\n possibleIssues,\n suggestions,\n modelCount: reviews.length,\n };\n\n return { reviews, consensus };\n}\n\n// ═══ PRESENTATION & APPROVAL ═══\n\nfunction formatReport(reviews, consensus) {\n const lines = [];\n\n lines.push('');\n lines.push('╔══════════════════════════════════════╗');\n lines.push('║ MULTI-MODEL PLAN REVIEW ║');\n lines.push('╚══════════════════════════════════════╝');\n lines.push('');\n\n // Scores per model\n lines.push(` Overall Score: ${consensus.avgScore}/10 (${consensus.modelCount} models)`);\n lines.push('');\n for (const r of reviews) {\n const bar = '█'.repeat(r.score) + '░'.repeat(10 - r.score);\n lines.push(` ${r.model.padEnd(22)} ${bar} ${r.score}/10`);\n }\n\n // Confirmed issues\n if (consensus.confirmedIssues.length > 0) {\n lines.push('');\n lines.push(' ── CONFIRMED ISSUES (2+ models agree — must fix) ──');\n for (const issue of consensus.confirmedIssues) {\n lines.push(` ✗ ${issue}`);\n }\n }\n\n // Possible issues\n if (consensus.possibleIssues.length > 0) {\n lines.push('');\n lines.push(' ── POSSIBLE ISSUES (1 model flagged — consider) ──');\n for (const issue of consensus.possibleIssues) {\n lines.push(` ? ${issue}`);\n }\n }\n\n // Suggestions\n if (consensus.suggestions.length > 0) {\n lines.push('');\n lines.push(' ── SUGGESTIONS ──');\n for (const s of consensus.suggestions) {\n lines.push(` → ${s}`);\n }\n }\n\n if (consensus.confirmedIssues.length === 0 && consensus.possibleIssues.length === 0) {\n lines.push('');\n lines.push(' ✓ No issues found across models.');\n }\n\n lines.push('');\n return lines.join('\\n');\n}\n\nfunction askUser(question, externalRl = null) {\n if (externalRl) {\n return new Promise((resolve) => externalRl.question(question, resolve));\n }\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer); }));\n}\n\n/**\n * Review plan, present report, ask for approval.\n * Returns { approved: boolean, consensus } or { approved: true, consensus: null } if skipped.\n */\nexport async function presentAndApprove(planText, rl = null) {\n const result = await reviewPlan(planText);\n\n if (!result) {\n console.log('[REVIEW] Could not complete review — not enough models available.');\n const answer = await askUser(' Execute anyway? (y = yes / n = no): ', rl);\n return { approved: answer.trim().toLowerCase() === 'y', consensus: null };\n }\n\n const { reviews, consensus } = result;\n console.log(formatReport(reviews, consensus));\n\n const answer = await askUser(' Approve plan? (y = execute / n = revise / s = skip review): ', rl);\n const choice = answer.trim().toLowerCase();\n\n if (choice === 'y') {\n return { approved: true, consensus };\n } else if (choice === 's') {\n console.log('[REVIEW] Skipped — executing without approval.');\n return { approved: true, consensus };\n } else {\n return { approved: false, consensus };\n }\n}\n"}, "f73624adde48d810ee76d46d709d3c29462f4aff": {"t": 1785766707.1914933, "n": 11312, "h": "21de0ab04277304b", "c": "/**\n * multi-model-review.js\n *\n * Multi-model plan review: sends plan text to 6 LLMs in parallel,\n * collects issues/suggestions/scores, builds consensus, and presents\n * an interactive approval flow in the CLI.\n */\n\nimport Anthropic from '@anthropic-ai/sdk';\nimport readline from 'readline';\nimport { logCost } from './cost-monitor.js';\n\nconst REVIEW_PROMPT = (planText) => `You are a senior software architect reviewing a project plan.\nAnalyze this plan and respond in EXACTLY this JSON format (no markdown, no code fences):\n{\"issues\":[\"issue 1\",\"issue 2\"],\"suggestions\":[\"suggestion 1\"],\"score\":7}\n\nRules:\n- issues: list concrete problems (gaps, risks, contradictions, missing details). Empty array if none.\n- suggestions: list actionable improvements. Empty array if none.\n- score: integer 1-10 (1=terrible, 10=flawless).\n\nPlan to review:\n${planText}`;\n\nconst TIMEOUT_MS = 8000;\nconst OLLAMA_TIMEOUT_MS = 20000;\n\n// ═══ INDIVIDUAL REVIEWERS ═══\n\nasync function reviewWithGemini(planText) {\n const key = process.env.GEMINI_API_KEY;\n if (!key) throw new Error('GEMINI_API_KEY not set');\n\n const res = await fetch(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${key}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [{ parts: [{ text: REVIEW_PROMPT(planText) }] }],\n generationConfig: { temperature: 0.3, maxOutputTokens: 1024 },\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n }\n );\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text;\n return parseReview(text, 'Gemini 2.5 Flash');\n}\n\nasync function reviewWithGroq(planText) {\n const key = process.env.GROQ_API_KEY;\n if (!key) throw new Error('GROQ_API_KEY not set');\n\n const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },\n body: JSON.stringify({\n model: 'llama-3.3-70b-versatile',\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n temperature: 0.3,\n max_tokens: 1024,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content;\n return parseReview(text, 'Groq Llama 3.3 70B');\n}\n\nasync function reviewWithOpenAI(planText) {\n const key = process.env.OPENAI_API_KEY;\n if (!key) throw new Error('OPENAI_API_KEY not set');\n\n const res = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },\n body: JSON.stringify({\n model: 'gpt-4o-mini',\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n temperature: 0.3,\n max_tokens: 1024,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content;\n return parseReview(text, 'GPT-4o mini');\n}\n\nasync function reviewWithDeepSeek(planText) {\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) throw new Error('DEEPSEEK_API_KEY not set');\n\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },\n body: JSON.stringify({\n model: 'deepseek-chat',\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n temperature: 0.3,\n max_tokens: 1024,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content;\n return parseReview(text, 'DeepSeek');\n}\n\nasync function reviewWithOllama(planText) {\n const base = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';\n const model = process.env.OLLAMA_MODEL || 'deepseek-r1:7b';\n\n const res = await fetch(`${base}/api/generate`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model,\n prompt: REVIEW_PROMPT(planText),\n stream: false,\n options: { temperature: 0.3, num_predict: 1024 },\n }),\n signal: AbortSignal.timeout(OLLAMA_TIMEOUT_MS),\n });\n const data = await res.json();\n return parseReview(data.response, `Ollama ${model}`);\n}\n\nasync function reviewWithClaude(planText) {\n const key = process.env.ANTHROPIC_API_KEY;\n if (!key) throw new Error('ANTHROPIC_API_KEY not set');\n\n const client = new Anthropic();\n const msg = await Promise.race([\n client.messages.create({\n model: 'claude-haiku-4-5-20251001',\n max_tokens: 1024,\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n }),\n new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), TIMEOUT_MS)),\n ]);\n\n logCost({\n service: 'anthropic',\n model: msg.model || 'claude-haiku-4-5-20251001',\n agent: 'multi-model-review',\n inputTokens: msg.usage?.input_tokens || 0,\n outputTokens: msg.usage?.output_tokens || 0,\n });\n\n const text = msg.content?.[0]?.text;\n return parseReview(text, 'Claude Haiku 4.5');\n}\n\n// ═══ PARSE ═══\n\nfunction parseReview(text, modelName) {\n if (!text) throw new Error(`${modelName}: empty response`);\n\n // Strip markdown fences if present\n const cleaned = text.replace(/```json\\s*/g, '').replace(/```\\s*/g, '').trim();\n\n // Find JSON object in response\n const match = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (!match) throw new Error(`${modelName}: no JSON found in response`);\n\n const parsed = JSON.parse(match[0]);\n return {\n model: modelName,\n issues: Array.isArray(parsed.issues) ? parsed.issues.map(String) : [],\n suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.map(String) : [],\n score: Math.min(10, Math.max(1, Math.round(Number(parsed.score) || 5))),\n };\n}\n\n// ═══ REVIEW ORCHESTRATION ═══\n\nconst ALL_REVIEWERS = [\n { name: 'Gemini 2.5 Flash', fn: reviewWithGemini },\n { name: 'Groq Llama 3.3 70B', fn: reviewWithGroq },\n { name: 'GPT-4o mini', fn: reviewWithOpenAI },\n { name: 'DeepSeek', fn: reviewWithDeepSeek },\n { name: 'Ollama local', fn: reviewWithOllama },\n { name: 'Claude Haiku 4.5', fn: reviewWithClaude },\n];\n\n/**\n * Send plan to all 6 models in parallel, collect results.\n * Returns { reviews, consensus } or null if < 2 models succeeded.\n */\nexport async function reviewPlan(planText) {\n console.log('\\n[REVIEW] Sending plan to 6 models...');\n\n const results = await Promise.allSettled(\n ALL_REVIEWERS.map(async ({ name, fn }) => {\n try {\n const review = await fn(planText);\n console.log(` [OK] ${name} — score: ${review.score}/10`);\n return review;\n } catch (err) {\n console.log(` [SKIP] ${name} — ${err.message}`);\n throw err;\n }\n })\n );\n\n const reviews = results\n .filter((r) => r.status === 'fulfilled')\n .map((r) => r.value);\n\n if (reviews.length < 2) {\n console.log(`\\n[REVIEW] Only ${reviews.length} model(s) responded — need at least 2. Skipping review.`);\n return null;\n }\n\n console.log(`\\n[REVIEW] ${reviews.length}/${ALL_REVIEWERS.length} models responded.`);\n\n // ── Build consensus ──\n const issueCount = new Map();\n for (const review of reviews) {\n for (const issue of review.issues) {\n const key = issue.toLowerCase().trim();\n issueCount.set(key, (issueCount.get(key) || { text: issue, count: 0 }));\n issueCount.get(key).count++;\n }\n }\n\n const confirmedIssues = [];\n const possibleIssues = [];\n for (const { text, count } of issueCount.values()) {\n if (count >= 2) {\n confirmedIssues.push(text);\n } else {\n possibleIssues.push(text);\n }\n }\n\n // Merge all suggestions (deduplicate by lowercase)\n const seenSuggestions = new Set();\n const suggestions = [];\n for (const review of reviews) {\n for (const s of review.suggestions) {\n const key = s.toLowerCase().trim();\n if (!seenSuggestions.has(key)) {\n seenSuggestions.add(key);\n suggestions.push(s);\n }\n }\n }\n\n const avgScore = reviews.reduce((sum, r) => sum + r.score, 0) / reviews.length;\n\n const consensus = {\n avgScore: Math.round(avgScore * 10) / 10,\n confirmedIssues,\n possibleIssues,\n suggestions,\n modelCount: reviews.length,\n };\n\n return { reviews, consensus };\n}\n\n// ═══ PRESENTATION & APPROVAL ═══\n\nfunction formatReport(reviews, consensus) {\n const lines = [];\n\n lines.push('');\n lines.push('╔══════════════════════════════════════╗');\n lines.push('║ MULTI-MODEL PLAN REVIEW ║');\n lines.push('╚══════════════════════════════════════╝');\n lines.push('');\n\n // Scores per model\n lines.push(` Overall Score: ${consensus.avgScore}/10 (${consensus.modelCount} models)`);\n lines.push('');\n for (const r of reviews) {\n const bar = '█'.repeat(r.score) + '░'.repeat(10 - r.score);\n lines.push(` ${r.model.padEnd(22)} ${bar} ${r.score}/10`);\n }\n\n // Confirmed issues\n if (consensus.confirmedIssues.length > 0) {\n lines.push('');\n lines.push(' ── CONFIRMED ISSUES (2+ models agree — must fix) ──');\n for (const issue of consensus.confirmedIssues) {\n lines.push(` ✗ ${issue}`);\n }\n }\n\n // Possible issues\n if (consensus.possibleIssues.length > 0) {\n lines.push('');\n lines.push(' ── POSSIBLE ISSUES (1 model flagged — consider) ──');\n for (const issue of consensus.possibleIssues) {\n lines.push(` ? ${issue}`);\n }\n }\n\n // Suggestions\n if (consensus.suggestions.length > 0) {\n lines.push('');\n lines.push(' ── SUGGESTIONS ──');\n for (const s of consensus.suggestions) {\n lines.push(` → ${s}`);\n }\n }\n\n if (consensus.confirmedIssues.length === 0 && consensus.possibleIssues.length === 0) {\n lines.push('');\n lines.push(' ✓ No issues found across models.');\n }\n\n lines.push('');\n return lines.join('\\n');\n}\n\nfunction askUser(question, externalRl = null) {\n if (externalRl) {\n return new Promise((resolve) => externalRl.question(question, resolve));\n }\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer); }));\n}\n\n/**\n * Review plan, present report, ask for approval.\n * Returns { approved: boolean, consensus } or { approved: true, consensus: null } if skipped.\n */\nexport async function presentAndApprove(planText, rl = null) {\n const result = await reviewPlan(planText);\n\n if (!result) {\n console.log('[REVIEW] Could not complete review — not enough models available.');\n const answer = await askUser(' Execute anyway? (y = yes / n = no): ', rl);\n return { approved: answer.trim().toLowerCase() === 'y', consensus: null };\n }\n\n const { reviews, consensus } = result;\n console.log(formatReport(reviews, consensus));\n\n const answer = await askUser(' Approve plan? (y = execute / n = revise / s = skip review): ', rl);\n const choice = answer.trim().toLowerCase();\n\n if (choice === 'y') {\n return { approved: true, consensus };\n } else if (choice === 's') {\n console.log('[REVIEW] Skipped — executing without approval.');\n return { approved: true, consensus };\n } else {\n return { approved: false, consensus };\n }\n}\n"}, "22c48734c55083e6c56be067a3925364b9f507e4": {"t": 1785766708.788621, "n": 11039, "h": "9510538ee4e83f68", "c": "/**\n * multi-model-review.js\n *\n * Multi-model plan review: sends plan text to 6 LLMs in parallel,\n * collects issues/suggestions/scores, builds consensus, and presents\n * an interactive approval flow in the CLI.\n */\n\nimport Anthropic from '@anthropic-ai/sdk';\nimport readline from 'readline';\n\nconst REVIEW_PROMPT = (planText) => `You are a senior software architect reviewing a project plan.\nAnalyze this plan and respond in EXACTLY this JSON format (no markdown, no code fences):\n{\"issues\":[\"issue 1\",\"issue 2\"],\"suggestions\":[\"suggestion 1\"],\"score\":7}\n\nRules:\n- issues: list concrete problems (gaps, risks, contradictions, missing details). Empty array if none.\n- suggestions: list actionable improvements. Empty array if none.\n- score: integer 1-10 (1=terrible, 10=flawless).\n\nPlan to review:\n${planText}`;\n\nconst TIMEOUT_MS = 8000;\nconst OLLAMA_TIMEOUT_MS = 20000;\n\n// ═══ INDIVIDUAL REVIEWERS ═══\n\nasync function reviewWithGemini(planText) {\n const key = process.env.GEMINI_API_KEY;\n if (!key) throw new Error('GEMINI_API_KEY not set');\n\n const res = await fetch(\n `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${key}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [{ parts: [{ text: REVIEW_PROMPT(planText) }] }],\n generationConfig: { temperature: 0.3, maxOutputTokens: 1024 },\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n }\n );\n const data = await res.json();\n const text = data.candidates?.[0]?.content?.parts?.[0]?.text;\n return parseReview(text, 'Gemini 2.5 Flash');\n}\n\nasync function reviewWithGroq(planText) {\n const key = process.env.GROQ_API_KEY;\n if (!key) throw new Error('GROQ_API_KEY not set');\n\n const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },\n body: JSON.stringify({\n model: 'llama-3.3-70b-versatile',\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n temperature: 0.3,\n max_tokens: 1024,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content;\n return parseReview(text, 'Groq Llama 3.3 70B');\n}\n\nasync function reviewWithOpenAI(planText) {\n const key = process.env.OPENAI_API_KEY;\n if (!key) throw new Error('OPENAI_API_KEY not set');\n\n const res = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },\n body: JSON.stringify({\n model: 'gpt-4o-mini',\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n temperature: 0.3,\n max_tokens: 1024,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content;\n return parseReview(text, 'GPT-4o mini');\n}\n\nasync function reviewWithDeepSeek(planText) {\n const key = process.env.DEEPSEEK_API_KEY;\n if (!key) throw new Error('DEEPSEEK_API_KEY not set');\n\n const res = await fetch('https://api.deepseek.com/chat/completions', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },\n body: JSON.stringify({\n model: 'deepseek-chat',\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n temperature: 0.3,\n max_tokens: 1024,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n const data = await res.json();\n const text = data.choices?.[0]?.message?.content;\n return parseReview(text, 'DeepSeek');\n}\n\nasync function reviewWithOllama(planText) {\n const base = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';\n const model = process.env.OLLAMA_MODEL || 'deepseek-r1:7b';\n\n const res = await fetch(`${base}/api/generate`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n model,\n prompt: REVIEW_PROMPT(planText),\n stream: false,\n options: { temperature: 0.3, num_predict: 1024 },\n }),\n signal: AbortSignal.timeout(OLLAMA_TIMEOUT_MS),\n });\n const data = await res.json();\n return parseReview(data.response, `Ollama ${model}`);\n}\n\nasync function reviewWithClaude(planText) {\n const key = process.env.ANTHROPIC_API_KEY;\n if (!key) throw new Error('ANTHROPIC_API_KEY not set');\n\n const client = new Anthropic();\n const msg = await Promise.race([\n client.messages.create({\n model: 'claude-haiku-4-5-20251001',\n max_tokens: 1024,\n messages: [{ role: 'user', content: REVIEW_PROMPT(planText) }],\n }),\n new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), TIMEOUT_MS)),\n ]);\n const text = msg.content?.[0]?.text;\n return parseReview(text, 'Claude Haiku 4.5');\n}\n\n// ═══ PARSE ═══\n\nfunction parseReview(text, modelName) {\n if (!text) throw new Error(`${modelName}: empty response`);\n\n // Strip markdown fences if present\n const cleaned = text.replace(/```json\\s*/g, '').replace(/```\\s*/g, '').trim();\n\n // Find JSON object in response\n const match = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (!match) throw new Error(`${modelName}: no JSON found in response`);\n\n const parsed = JSON.parse(match[0]);\n return {\n model: modelName,\n issues: Array.isArray(parsed.issues) ? parsed.issues.map(String) : [],\n suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.map(String) : [],\n score: Math.min(10, Math.max(1, Math.round(Number(parsed.score) || 5))),\n };\n}\n\n// ═══ REVIEW ORCHESTRATION ═══\n\nconst ALL_REVIEWERS = [\n { name: 'Gemini 2.5 Flash', fn: reviewWithGemini },\n { name: 'Groq Llama 3.3 70B', fn: reviewWithGroq },\n { name: 'GPT-4o mini', fn: reviewWithOpenAI },\n { name: 'DeepSeek', fn: reviewWithDeepSeek },\n { name: 'Ollama local', fn: reviewWithOllama },\n { name: 'Claude Haiku 4.5', fn: reviewWithClaude },\n];\n\n/**\n * Send plan to all 6 models in parallel, collect results.\n * Returns { reviews, consensus } or null if < 2 models succeeded.\n */\nexport async function reviewPlan(planText) {\n console.log('\\n[REVIEW] Sending plan to 6 models...');\n\n const results = await Promise.allSettled(\n ALL_REVIEWERS.map(async ({ name, fn }) => {\n try {\n const review = await fn(planText);\n console.log(` [OK] ${name} — score: ${review.score}/10`);\n return review;\n } catch (err) {\n console.log(` [SKIP] ${name} — ${err.message}`);\n throw err;\n }\n })\n );\n\n const reviews = results\n .filter((r) => r.status === 'fulfilled')\n .map((r) => r.value);\n\n if (reviews.length < 2) {\n console.log(`\\n[REVIEW] Only ${reviews.length} model(s) responded — need at least 2. Skipping review.`);\n return null;\n }\n\n console.log(`\\n[REVIEW] ${reviews.length}/${ALL_REVIEWERS.length} models responded.`);\n\n // ── Build consensus ──\n const issueCount = new Map();\n for (const review of reviews) {\n for (const issue of review.issues) {\n const key = issue.toLowerCase().trim();\n issueCount.set(key, (issueCount.get(key) || { text: issue, count: 0 }));\n issueCount.get(key).count++;\n }\n }\n\n const confirmedIssues = [];\n const possibleIssues = [];\n for (const { text, count } of issueCount.values()) {\n if (count >= 2) {\n confirmedIssues.push(text);\n } else {\n possibleIssues.push(text);\n }\n }\n\n // Merge all suggestions (deduplicate by lowercase)\n const seenSuggestions = new Set();\n const suggestions = [];\n for (const review of reviews) {\n for (const s of review.suggestions) {\n const key = s.toLowerCase().trim();\n if (!seenSuggestions.has(key)) {\n seenSuggestions.add(key);\n suggestions.push(s);\n }\n }\n }\n\n const avgScore = reviews.reduce((sum, r) => sum + r.score, 0) / reviews.length;\n\n const consensus = {\n avgScore: Math.round(avgScore * 10) / 10,\n confirmedIssues,\n possibleIssues,\n suggestions,\n modelCount: reviews.length,\n };\n\n return { reviews, consensus };\n}\n\n// ═══ PRESENTATION & APPROVAL ═══\n\nfunction formatReport(reviews, consensus) {\n const lines = [];\n\n lines.push('');\n lines.push('╔══════════════════════════════════════╗');\n lines.push('║ MULTI-MODEL PLAN REVIEW ║');\n lines.push('╚══════════════════════════════════════╝');\n lines.push('');\n\n // Scores per model\n lines.push(` Overall Score: ${consensus.avgScore}/10 (${consensus.modelCount} models)`);\n lines.push('');\n for (const r of reviews) {\n const bar = '█'.repeat(r.score) + '░'.repeat(10 - r.score);\n lines.push(` ${r.model.padEnd(22)} ${bar} ${r.score}/10`);\n }\n\n // Confirmed issues\n if (consensus.confirmedIssues.length > 0) {\n lines.push('');\n lines.push(' ── CONFIRMED ISSUES (2+ models agree — must fix) ──');\n for (const issue of consensus.confirmedIssues) {\n lines.push(` ✗ ${issue}`);\n }\n }\n\n // Possible issues\n if (consensus.possibleIssues.length > 0) {\n lines.push('');\n lines.push(' ── POSSIBLE ISSUES (1 model flagged — consider) ──');\n for (const issue of consensus.possibleIssues) {\n lines.push(` ? ${issue}`);\n }\n }\n\n // Suggestions\n if (consensus.suggestions.length > 0) {\n lines.push('');\n lines.push(' ── SUGGESTIONS ──');\n for (const s of consensus.suggestions) {\n lines.push(` → ${s}`);\n }\n }\n\n if (consensus.confirmedIssues.length === 0 && consensus.possibleIssues.length === 0) {\n lines.push('');\n lines.push(' ✓ No issues found across models.');\n }\n\n lines.push('');\n return lines.join('\\n');\n}\n\nfunction askUser(question, externalRl = null) {\n if (externalRl) {\n return new Promise((resolve) => externalRl.question(question, resolve));\n }\n const rl = readline.createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer); }));\n}\n\n/**\n * Review plan, present report, ask for approval.\n * Returns { approved: boolean, consensus } or { approved: true, consensus: null } if skipped.\n */\nexport async function presentAndApprove(planText, rl = null) {\n const result = await reviewPlan(planText);\n\n if (!result) {\n console.log('[REVIEW] Could not complete review — not enough models available.');\n const answer = await askUser(' Execute anyway? (y = yes / n = no): ', rl);\n return { approved: answer.trim().toLowerCase() === 'y', consensus: null };\n }\n\n const { reviews, consensus } = result;\n console.log(formatReport(reviews, consensus));\n\n const answer = await askUser(' Approve plan? (y = execute / n = revise / s = skip review): ', rl);\n const choice = answer.trim().toLowerCase();\n\n if (choice === 'y') {\n return { approved: true, consensus };\n } else if (choice === 's') {\n console.log('[REVIEW] Skipped — executing without approval.');\n return { approved: true, consensus };\n } else {\n return { approved: false, consensus };\n }\n}\n"}}}} |