wiggum-cli 0.3.2 → 0.4.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 (79) hide show
  1. package/README.md +6 -4
  2. package/dist/ai/agents/codebase-analyst.d.ts +3 -0
  3. package/dist/ai/agents/codebase-analyst.d.ts.map +1 -1
  4. package/dist/ai/agents/codebase-analyst.js +3 -0
  5. package/dist/ai/agents/codebase-analyst.js.map +1 -1
  6. package/dist/ai/agents/context-enricher.d.ts +11 -0
  7. package/dist/ai/agents/context-enricher.d.ts.map +1 -0
  8. package/dist/ai/agents/context-enricher.js +163 -0
  9. package/dist/ai/agents/context-enricher.js.map +1 -0
  10. package/dist/ai/agents/evaluator-optimizer.d.ts +13 -0
  11. package/dist/ai/agents/evaluator-optimizer.d.ts.map +1 -0
  12. package/dist/ai/agents/evaluator-optimizer.js +231 -0
  13. package/dist/ai/agents/evaluator-optimizer.js.map +1 -0
  14. package/dist/ai/agents/index.d.ts +21 -3
  15. package/dist/ai/agents/index.d.ts.map +1 -1
  16. package/dist/ai/agents/index.js +151 -82
  17. package/dist/ai/agents/index.js.map +1 -1
  18. package/dist/ai/agents/mcp-detector.d.ts +26 -0
  19. package/dist/ai/agents/mcp-detector.d.ts.map +1 -0
  20. package/dist/ai/agents/mcp-detector.js +186 -0
  21. package/dist/ai/agents/mcp-detector.js.map +1 -0
  22. package/dist/ai/agents/orchestrator.d.ts +3 -0
  23. package/dist/ai/agents/orchestrator.d.ts.map +1 -1
  24. package/dist/ai/agents/orchestrator.js +3 -0
  25. package/dist/ai/agents/orchestrator.js.map +1 -1
  26. package/dist/ai/agents/planning-orchestrator.d.ts +12 -0
  27. package/dist/ai/agents/planning-orchestrator.d.ts.map +1 -0
  28. package/dist/ai/agents/planning-orchestrator.js +133 -0
  29. package/dist/ai/agents/planning-orchestrator.js.map +1 -0
  30. package/dist/ai/agents/stack-researcher.d.ts +3 -0
  31. package/dist/ai/agents/stack-researcher.d.ts.map +1 -1
  32. package/dist/ai/agents/stack-researcher.js +3 -0
  33. package/dist/ai/agents/stack-researcher.js.map +1 -1
  34. package/dist/ai/agents/stack-utils.d.ts +11 -0
  35. package/dist/ai/agents/stack-utils.d.ts.map +1 -0
  36. package/dist/ai/agents/stack-utils.js +27 -0
  37. package/dist/ai/agents/stack-utils.js.map +1 -0
  38. package/dist/ai/agents/synthesis-agent.d.ts +11 -0
  39. package/dist/ai/agents/synthesis-agent.d.ts.map +1 -0
  40. package/dist/ai/agents/synthesis-agent.js +202 -0
  41. package/dist/ai/agents/synthesis-agent.js.map +1 -0
  42. package/dist/ai/agents/tech-researcher.d.ts +16 -0
  43. package/dist/ai/agents/tech-researcher.d.ts.map +1 -0
  44. package/dist/ai/agents/tech-researcher.js +208 -0
  45. package/dist/ai/agents/tech-researcher.js.map +1 -0
  46. package/dist/ai/agents/types.d.ts +121 -0
  47. package/dist/ai/agents/types.d.ts.map +1 -1
  48. package/dist/ai/agents/types.js +6 -0
  49. package/dist/ai/agents/types.js.map +1 -1
  50. package/dist/ai/index.d.ts +1 -1
  51. package/dist/ai/index.d.ts.map +1 -1
  52. package/dist/ai/index.js +14 -2
  53. package/dist/ai/index.js.map +1 -1
  54. package/dist/commands/init.d.ts.map +1 -1
  55. package/dist/commands/init.js +9 -2
  56. package/dist/commands/init.js.map +1 -1
  57. package/dist/utils/tracing.d.ts +5 -0
  58. package/dist/utils/tracing.d.ts.map +1 -1
  59. package/dist/utils/tracing.js +40 -1
  60. package/dist/utils/tracing.js.map +1 -1
  61. package/package.json +5 -2
  62. package/src/ai/agents/codebase-analyst.ts +3 -0
  63. package/src/ai/agents/context-enricher.ts +189 -0
  64. package/src/ai/agents/evaluator-optimizer.ts +277 -0
  65. package/src/ai/agents/index.ts +197 -104
  66. package/src/ai/agents/mcp-detector.test.ts +290 -0
  67. package/src/ai/agents/mcp-detector.ts +210 -0
  68. package/src/ai/agents/orchestrator.ts +3 -0
  69. package/src/ai/agents/planning-orchestrator.ts +140 -0
  70. package/src/ai/agents/stack-researcher.ts +3 -0
  71. package/src/ai/agents/stack-utils.ts +34 -0
  72. package/src/ai/agents/synthesis-agent.ts +240 -0
  73. package/src/ai/agents/tech-researcher.ts +262 -0
  74. package/src/ai/agents/types.ts +153 -0
  75. package/src/ai/index.ts +26 -5
  76. package/src/commands/init.ts +10 -2
  77. package/src/utils/tracing.ts +44 -1
  78. package/tsconfig.json +1 -1
  79. package/vitest.config.ts +7 -0
@@ -0,0 +1,210 @@
1
+ /**
2
+ * MCP Detector (Phase 3 helper)
3
+ * Detects ralph-essential MCP servers based on the detected stack
4
+ *
5
+ * This is a pure function (no LLM) - uses rule-based detection for efficiency
6
+ */
7
+
8
+ import type { DetectedStack } from '../../scanner/types.js';
9
+ import type { RalphMcpServers } from './types.js';
10
+
11
+ /**
12
+ * Database name to MCP server mapping
13
+ */
14
+ const DATABASE_MCP_MAP: Record<string, string> = {
15
+ supabase: 'supabase',
16
+ convex: 'convex',
17
+ postgres: 'postgres',
18
+ postgresql: 'postgres',
19
+ sqlite: 'sqlite',
20
+ firebase: 'firebase',
21
+ firestore: 'firebase',
22
+ mongodb: 'mongodb',
23
+ mysql: 'mysql',
24
+ redis: 'redis',
25
+ planetscale: 'planetscale',
26
+ neon: 'postgres', // Neon is PostgreSQL-compatible
27
+ turso: 'sqlite', // Turso is SQLite-compatible
28
+ };
29
+
30
+ /**
31
+ * Framework-specific MCP recommendations
32
+ */
33
+ const FRAMEWORK_MCP_MAP: Record<string, string[]> = {
34
+ 'next.js': ['vercel'],
35
+ 'nextjs': ['vercel'],
36
+ 'vercel': ['vercel'],
37
+ 'remix': [],
38
+ 'astro': [],
39
+ 'nuxt': [],
40
+ 'sveltekit': [],
41
+ };
42
+
43
+ /**
44
+ * Service-specific MCP recommendations
45
+ */
46
+ const SERVICE_MCP_MAP: Record<string, string> = {
47
+ stripe: 'stripe',
48
+ clerk: 'clerk',
49
+ auth0: 'auth0',
50
+ github: 'github',
51
+ gitlab: 'gitlab',
52
+ aws: 'aws',
53
+ gcp: 'gcp',
54
+ azure: 'azure',
55
+ docker: 'docker',
56
+ kubernetes: 'kubernetes',
57
+ k8s: 'kubernetes',
58
+ posthog: 'posthog',
59
+ sentry: 'sentry',
60
+ resend: 'resend',
61
+ sendgrid: 'sendgrid',
62
+ twilio: 'twilio',
63
+ };
64
+
65
+ /**
66
+ * Detect ralph-essential MCP servers from the stack
67
+ *
68
+ * Ralph loop essentials:
69
+ * - Playwright: Always recommended for E2E testing
70
+ * - Database MCP: If database is detected
71
+ * - Additional MCPs based on services and deployment
72
+ */
73
+ export function detectRalphMcpServers(stack: DetectedStack): RalphMcpServers {
74
+ const result: RalphMcpServers = {
75
+ e2eTesting: 'playwright', // Always recommend Playwright for ralph loop
76
+ additional: [],
77
+ };
78
+
79
+ // Detect database MCP
80
+ if (stack.database) {
81
+ const dbName = stack.database.name.toLowerCase();
82
+
83
+ // Check direct mapping
84
+ for (const [key, mcp] of Object.entries(DATABASE_MCP_MAP)) {
85
+ if (dbName.includes(key)) {
86
+ result.database = mcp;
87
+ break;
88
+ }
89
+ }
90
+ }
91
+
92
+ // Check ORM for database hints
93
+ if (!result.database && stack.orm) {
94
+ const ormName = stack.orm.name.toLowerCase();
95
+ if (ormName.includes('prisma') || ormName.includes('drizzle')) {
96
+ // These ORMs often use PostgreSQL by default
97
+ // But we don't set a default - let it be detected from actual DB config
98
+ }
99
+ }
100
+
101
+ // Detect framework-specific MCPs
102
+ if (stack.framework) {
103
+ const frameworkName = stack.framework.name.toLowerCase();
104
+ for (const [key, mcps] of Object.entries(FRAMEWORK_MCP_MAP)) {
105
+ if (frameworkName.includes(key)) {
106
+ result.additional.push(...mcps);
107
+ break;
108
+ }
109
+ }
110
+ }
111
+
112
+ // Detect deployment MCPs
113
+ if (stack.deployment) {
114
+ for (const deploy of stack.deployment) {
115
+ const deployName = deploy.name.toLowerCase();
116
+ if (deployName.includes('docker')) {
117
+ addIfNotExists(result.additional, 'docker');
118
+ }
119
+ if (deployName.includes('vercel')) {
120
+ addIfNotExists(result.additional, 'vercel');
121
+ }
122
+ if (deployName.includes('railway')) {
123
+ addIfNotExists(result.additional, 'railway');
124
+ }
125
+ }
126
+ }
127
+
128
+ // Detect auth provider MCPs
129
+ if (stack.auth) {
130
+ const authName = stack.auth.name.toLowerCase();
131
+ for (const [key, mcp] of Object.entries(SERVICE_MCP_MAP)) {
132
+ if (authName.includes(key)) {
133
+ addIfNotExists(result.additional, mcp);
134
+ break;
135
+ }
136
+ }
137
+ }
138
+
139
+ // Detect analytics MCPs
140
+ if (stack.analytics) {
141
+ for (const analytics of stack.analytics) {
142
+ const analyticsName = analytics.name.toLowerCase();
143
+ for (const [key, mcp] of Object.entries(SERVICE_MCP_MAP)) {
144
+ if (analyticsName.includes(key)) {
145
+ addIfNotExists(result.additional, mcp);
146
+ break;
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ // Detect payment MCPs
153
+ if (stack.payments) {
154
+ const paymentName = stack.payments.name.toLowerCase();
155
+ for (const [key, mcp] of Object.entries(SERVICE_MCP_MAP)) {
156
+ if (paymentName.includes(key)) {
157
+ addIfNotExists(result.additional, mcp);
158
+ break;
159
+ }
160
+ }
161
+ }
162
+
163
+ // Add any MCP recommendations from scanner
164
+ if (stack.mcp?.recommended) {
165
+ for (const rec of stack.mcp.recommended) {
166
+ const normalizedRec = rec.toLowerCase();
167
+ // Skip if it's the database or playwright (already handled)
168
+ if (normalizedRec !== result.database && normalizedRec !== 'playwright') {
169
+ addIfNotExists(result.additional, rec);
170
+ }
171
+ }
172
+ }
173
+
174
+ return result;
175
+ }
176
+
177
+ /**
178
+ * Convert RalphMcpServers to the legacy McpRecommendations format
179
+ * for backward compatibility with existing code
180
+ */
181
+ export function convertToLegacyMcpRecommendations(ralphMcp: RalphMcpServers): {
182
+ essential: string[];
183
+ recommended: string[];
184
+ } {
185
+ const essential: string[] = ['filesystem', 'git'];
186
+
187
+ // Add E2E testing as essential for ralph
188
+ if (ralphMcp.e2eTesting) {
189
+ essential.push(ralphMcp.e2eTesting);
190
+ }
191
+
192
+ // Add database as essential if detected
193
+ if (ralphMcp.database) {
194
+ essential.push(ralphMcp.database);
195
+ }
196
+
197
+ return {
198
+ essential,
199
+ recommended: ralphMcp.additional,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Helper to add item to array if not already present
205
+ */
206
+ function addIfNotExists(arr: string[], item: string): void {
207
+ if (!arr.includes(item)) {
208
+ arr.push(item);
209
+ }
210
+ }
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Orchestrator Agent
3
3
  * Coordinates the multi-agent analysis and merges results
4
+ *
5
+ * @deprecated Use runPlanningOrchestrator + runSynthesisAgent instead.
6
+ * This agent is kept for backward compatibility.
4
7
  */
5
8
 
6
9
  import type { LanguageModel } from 'ai';
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Planning Orchestrator Agent (Phase 1)
3
+ * Creates an analysis plan that guides the parallel workers
4
+ */
5
+
6
+ import { type LanguageModel } from 'ai';
7
+ import { z } from 'zod';
8
+ import type { ScanResult } from '../../scanner/types.js';
9
+ import type { AnalysisPlan } from './types.js';
10
+ import { isReasoningModel } from '../providers.js';
11
+ import { logger } from '../../utils/logger.js';
12
+ import { getTracedAI } from '../../utils/tracing.js';
13
+
14
+ /**
15
+ * Schema for the analysis plan output
16
+ */
17
+ const analysisPlanSchema = z.object({
18
+ areasToExplore: z.array(z.string()).describe('Key directories and files to explore'),
19
+ technologiesToResearch: z.array(z.string()).describe('Technologies to research in depth'),
20
+ questionsToAnswer: z.array(z.string()).describe('Specific questions that need answers'),
21
+ estimatedComplexity: z.enum(['low', 'medium', 'high']).describe('Estimated project complexity'),
22
+ });
23
+
24
+ /**
25
+ * System prompt for the Planning Orchestrator
26
+ */
27
+ const PLANNING_ORCHESTRATOR_SYSTEM_PROMPT = `You are a senior software architect analyzing a codebase to create an analysis plan.
28
+
29
+ Based on the scan result, create a focused analysis plan that identifies:
30
+ 1. Key areas to explore (directories, config files, entry points)
31
+ 2. Technologies that need in-depth research (frameworks, libraries, tools)
32
+ 3. Specific questions that need answers for implementation guidance
33
+
34
+ ## Guidelines
35
+ - Focus on areas that would benefit from deeper exploration
36
+ - Identify technologies where best practices would be valuable
37
+ - Ask questions that would help an AI developer implement features correctly
38
+ - Keep lists focused (3-7 items each)
39
+ - Consider the project type when prioritizing areas
40
+
41
+ ## Example Output
42
+ {
43
+ "areasToExplore": ["src/", "config/", "lib/auth/"],
44
+ "technologiesToResearch": ["Next.js 14", "Prisma", "NextAuth"],
45
+ "questionsToAnswer": ["What is the authentication strategy?", "How is state managed?", "What testing patterns are used?"],
46
+ "estimatedComplexity": "medium"
47
+ }`;
48
+
49
+ /**
50
+ * Run the Planning Orchestrator to create an analysis plan
51
+ */
52
+ export async function runPlanningOrchestrator(
53
+ model: LanguageModel,
54
+ modelId: string,
55
+ scanResult: ScanResult,
56
+ verbose: boolean = false
57
+ ): Promise<AnalysisPlan> {
58
+ // Build technology summary from scan result
59
+ const technologies: string[] = [];
60
+ const stack = scanResult.stack;
61
+
62
+ if (stack.framework) technologies.push(stack.framework.name);
63
+ if (stack.database) technologies.push(stack.database.name);
64
+ if (stack.orm) technologies.push(stack.orm.name);
65
+ if (stack.testing?.unit) technologies.push(stack.testing.unit.name);
66
+ if (stack.testing?.e2e) technologies.push(stack.testing.e2e.name);
67
+ if (stack.stateManagement) technologies.push(stack.stateManagement.name);
68
+ if (stack.auth) technologies.push(stack.auth.name);
69
+ if (stack.styling) technologies.push(stack.styling.name);
70
+ if (stack.mcp?.isProject) technologies.push('MCP Server');
71
+
72
+ const prompt = `Analyze this scan result and create an analysis plan:
73
+
74
+ Project: ${scanResult.projectRoot}
75
+ Framework: ${stack.framework?.name || 'Unknown'}
76
+ Database: ${stack.database?.name || 'None detected'}
77
+ Testing: ${stack.testing?.unit?.name || 'None detected'}
78
+ Package Manager: ${stack.packageManager?.name || 'npm'}
79
+ Detected Technologies: ${technologies.join(', ') || 'None'}
80
+ ${stack.mcp?.isProject ? 'This is an MCP Server project.' : ''}
81
+
82
+ Create a focused analysis plan that will help understand this codebase.`;
83
+
84
+ try {
85
+ const { generateObject } = getTracedAI();
86
+
87
+ const { object: plan } = await generateObject({
88
+ model,
89
+ schema: analysisPlanSchema,
90
+ system: PLANNING_ORCHESTRATOR_SYSTEM_PROMPT,
91
+ prompt,
92
+ ...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
93
+ experimental_telemetry: {
94
+ isEnabled: true,
95
+ metadata: {
96
+ agent: 'planning-orchestrator',
97
+ projectRoot: scanResult.projectRoot,
98
+ framework: stack.framework?.name || 'unknown',
99
+ },
100
+ },
101
+ });
102
+
103
+ if (verbose) {
104
+ logger.info(`Planning Orchestrator: ${plan.areasToExplore.length} areas, ${plan.technologiesToResearch.length} techs, ${plan.questionsToAnswer.length} questions`);
105
+ }
106
+
107
+ return plan;
108
+ } catch (error) {
109
+ if (verbose) {
110
+ logger.error(`Planning Orchestrator error: ${error instanceof Error ? error.message : String(error)}`);
111
+ }
112
+
113
+ // Return a sensible default plan
114
+ return getDefaultPlan(scanResult);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Get a default analysis plan when the orchestrator fails
120
+ */
121
+ function getDefaultPlan(scanResult: ScanResult): AnalysisPlan {
122
+ const stack = scanResult.stack;
123
+ const technologies: string[] = [];
124
+
125
+ if (stack.framework) technologies.push(stack.framework.name);
126
+ if (stack.database) technologies.push(stack.database.name);
127
+ if (stack.orm) technologies.push(stack.orm.name);
128
+ if (stack.testing?.unit) technologies.push(stack.testing.unit.name);
129
+
130
+ return {
131
+ areasToExplore: ['src/', 'package.json'],
132
+ technologiesToResearch: technologies.length > 0 ? technologies : ['TypeScript'],
133
+ questionsToAnswer: [
134
+ 'What is the project structure?',
135
+ 'What are the main entry points?',
136
+ 'How are tests organized?',
137
+ ],
138
+ estimatedComplexity: 'medium',
139
+ };
140
+ }
@@ -2,6 +2,9 @@
2
2
  * Stack Researcher Agent
3
3
  * Researches best practices and tools for the detected stack
4
4
  * Gracefully degrades when optional services are unavailable
5
+ *
6
+ * @deprecated Use runTechResearcher/runTechResearchPool from tech-researcher.ts instead.
7
+ * This agent is kept for backward compatibility.
5
8
  */
6
9
 
7
10
  import { stepCountIs, type LanguageModel, type Tool } from 'ai';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Stack Utilities
3
+ * Shared helper functions for working with DetectedStack
4
+ */
5
+
6
+ import type { DetectedStack } from '../../scanner/types.js';
7
+
8
+ /**
9
+ * Detect project type from the stack
10
+ * Returns a human-readable project type string
11
+ */
12
+ export function detectProjectType(stack: DetectedStack | undefined): string {
13
+ if (!stack) {
14
+ return 'Unknown';
15
+ }
16
+
17
+ if (stack.mcp?.isProject) {
18
+ return 'MCP Server';
19
+ }
20
+
21
+ if (stack.framework?.name?.includes('Next')) {
22
+ return 'Next.js App';
23
+ }
24
+
25
+ if (stack.framework?.name?.includes('React')) {
26
+ return 'React SPA';
27
+ }
28
+
29
+ if (stack.framework?.name) {
30
+ return `${stack.framework.name} Project`;
31
+ }
32
+
33
+ return 'Unknown';
34
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Synthesis Agent (Phase 3)
3
+ * Merges results from parallel workers and generates implementation guidelines
4
+ */
5
+
6
+ import { type LanguageModel } from 'ai';
7
+ import { z } from 'zod';
8
+ import type {
9
+ SynthesisInput,
10
+ MultiAgentAnalysis,
11
+ CodebaseAnalysis,
12
+ StackResearch,
13
+ McpRecommendations,
14
+ } from './types.js';
15
+ import { convertToLegacyMcpRecommendations } from './mcp-detector.js';
16
+ import { isReasoningModel } from '../providers.js';
17
+ import { logger } from '../../utils/logger.js';
18
+ import { parseJsonSafe } from '../../utils/json-repair.js';
19
+ import { getTracedAI } from '../../utils/tracing.js';
20
+
21
+ /**
22
+ * Schema for synthesis output (implementation guidelines)
23
+ */
24
+ const synthesisOutputSchema = z.object({
25
+ implementationGuidelines: z.array(z.string()).describe('Short, actionable implementation guidelines'),
26
+ possibleMissedTechnologies: z.array(z.string()).optional().describe('Technologies that may have been missed'),
27
+ });
28
+
29
+ /**
30
+ * System prompt for the Synthesis Agent
31
+ */
32
+ const SYNTHESIS_AGENT_SYSTEM_PROMPT = `You are a Synthesis Agent that merges analysis results into actionable implementation guidelines.
33
+
34
+ ## Your Mission
35
+ Based on the enriched context and technology research, generate:
36
+ 1. Short, actionable implementation guidelines (5-10 words each)
37
+ 2. List any technologies that may have been missed
38
+
39
+ ## Guidelines Style
40
+ - Start with action verbs: "Run", "Use", "Follow", "Avoid"
41
+ - Be specific to the detected stack
42
+ - Include testing commands
43
+ - Mention key patterns from the research
44
+ - Max 7 guidelines, prioritize the most important
45
+
46
+ ## Example Output
47
+ {
48
+ "implementationGuidelines": [
49
+ "Run npm test after changes",
50
+ "Use App Router for new pages",
51
+ "Follow existing component patterns in src/components",
52
+ "Use Zod for API validation",
53
+ "Run npx playwright test for E2E"
54
+ ],
55
+ "possibleMissedTechnologies": ["Redis caching"]
56
+ }`;
57
+
58
+ /**
59
+ * Run the Synthesis Agent to merge results and generate guidelines
60
+ */
61
+ export async function runSynthesisAgent(
62
+ model: LanguageModel,
63
+ modelId: string,
64
+ input: SynthesisInput,
65
+ verbose: boolean = false
66
+ ): Promise<MultiAgentAnalysis> {
67
+ // Build context summary for the LLM
68
+ const techSummary = input.techResearch
69
+ .map(r => `### ${r.technology}\nBest practices: ${r.bestPractices.slice(0, 3).join(', ')}\nAnti-patterns: ${r.antiPatterns.slice(0, 2).join(', ')}`)
70
+ .join('\n\n');
71
+
72
+ const prompt = `Synthesize these analysis results into implementation guidelines.
73
+
74
+ ## Project Context
75
+ - Type: ${input.enrichedContext.projectType}
76
+ - Entry Points: ${input.enrichedContext.entryPoints.join(', ')}
77
+ - Key Directories: ${Object.entries(input.enrichedContext.keyDirectories).map(([k, v]) => `${k} (${v})`).join(', ')}
78
+
79
+ ## Commands Available
80
+ ${Object.entries(input.enrichedContext.commands).map(([k, v]) => `- ${k}: ${v}`).join('\n')}
81
+
82
+ ## Technology Research
83
+ ${techSummary || 'No specific technology research available.'}
84
+
85
+ ## MCP Servers
86
+ - E2E Testing: ${input.mcpServers.e2eTesting}
87
+ - Database: ${input.mcpServers.database || 'None detected'}
88
+ - Additional: ${input.mcpServers.additional.join(', ') || 'None'}
89
+
90
+ ## Analysis Plan Questions & Answers
91
+ ${Object.entries(input.enrichedContext.answeredQuestions).map(([q, a]) => `Q: ${q}\nA: ${a}`).join('\n\n') || 'No questions answered.'}
92
+
93
+ Generate concise, actionable implementation guidelines based on this analysis.`;
94
+
95
+ try {
96
+ const { generateObject } = getTracedAI();
97
+
98
+ const { object: synthesis } = await generateObject({
99
+ model,
100
+ schema: synthesisOutputSchema,
101
+ system: SYNTHESIS_AGENT_SYSTEM_PROMPT,
102
+ prompt,
103
+ ...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
104
+ experimental_telemetry: {
105
+ isEnabled: true,
106
+ metadata: {
107
+ agent: 'synthesis-agent',
108
+ projectType: input.enrichedContext.projectType,
109
+ techCount: input.techResearch.length,
110
+ },
111
+ },
112
+ });
113
+
114
+ if (verbose) {
115
+ logger.info(`Synthesis Agent: Generated ${synthesis.implementationGuidelines.length} guidelines`);
116
+ }
117
+
118
+ // Convert to MultiAgentAnalysis format for backward compatibility
119
+ return buildMultiAgentAnalysis(input, synthesis.implementationGuidelines, synthesis.possibleMissedTechnologies);
120
+ } catch (error) {
121
+ if (verbose) {
122
+ logger.error(`Synthesis Agent error: ${error instanceof Error ? error.message : String(error)}`);
123
+ }
124
+
125
+ // Return with default guidelines
126
+ return buildMultiAgentAnalysis(input, getDefaultGuidelines(input), []);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Build MultiAgentAnalysis from synthesis input and generated guidelines
132
+ */
133
+ function buildMultiAgentAnalysis(
134
+ input: SynthesisInput,
135
+ implementationGuidelines: string[],
136
+ possibleMissedTechnologies?: string[]
137
+ ): MultiAgentAnalysis {
138
+ // Convert EnrichedContext to CodebaseAnalysis format
139
+ const codebaseAnalysis: CodebaseAnalysis = {
140
+ projectContext: {
141
+ entryPoints: input.enrichedContext.entryPoints,
142
+ keyDirectories: input.enrichedContext.keyDirectories,
143
+ namingConventions: input.enrichedContext.namingConventions,
144
+ projectType: input.enrichedContext.projectType,
145
+ },
146
+ commands: {
147
+ test: input.enrichedContext.commands.test,
148
+ lint: input.enrichedContext.commands.lint,
149
+ typecheck: input.enrichedContext.commands.typecheck,
150
+ build: input.enrichedContext.commands.build,
151
+ dev: input.enrichedContext.commands.dev,
152
+ format: input.enrichedContext.commands.format,
153
+ },
154
+ implementationGuidelines,
155
+ possibleMissedTechnologies,
156
+ };
157
+
158
+ // Merge tech research into StackResearch format
159
+ const stackResearch: StackResearch = mergeTechResearch(input.techResearch);
160
+
161
+ // Convert MCP servers to legacy format
162
+ const mcpServers: McpRecommendations = convertToLegacyMcpRecommendations(input.mcpServers);
163
+
164
+ return {
165
+ codebaseAnalysis,
166
+ stackResearch,
167
+ mcpServers,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Merge multiple TechResearchResult into a single StackResearch
173
+ */
174
+ function mergeTechResearch(techResearch: SynthesisInput['techResearch']): StackResearch {
175
+ if (techResearch.length === 0) {
176
+ return {
177
+ bestPractices: ['Follow project conventions'],
178
+ antiPatterns: ['Avoid skipping tests'],
179
+ testingTools: ['npm test'],
180
+ debuggingTools: ['console.log'],
181
+ documentationHints: ['Check official docs'],
182
+ researchMode: 'knowledge-only',
183
+ };
184
+ }
185
+
186
+ // Merge all research results
187
+ const bestPractices: string[] = [];
188
+ const antiPatterns: string[] = [];
189
+ const testingTips: string[] = [];
190
+ const documentationHints: string[] = [];
191
+ let researchMode: StackResearch['researchMode'] = 'knowledge-only';
192
+
193
+ for (const research of techResearch) {
194
+ bestPractices.push(...research.bestPractices);
195
+ antiPatterns.push(...research.antiPatterns);
196
+ testingTips.push(...research.testingTips);
197
+ documentationHints.push(...research.documentationHints);
198
+
199
+ // Use the most complete research mode
200
+ if (research.researchMode === 'full') researchMode = 'full';
201
+ else if (research.researchMode === 'web-only' && researchMode !== 'full') researchMode = 'web-only';
202
+ else if (research.researchMode === 'docs-only' && researchMode === 'knowledge-only') researchMode = 'docs-only';
203
+ }
204
+
205
+ // Deduplicate and limit
206
+ return {
207
+ bestPractices: [...new Set(bestPractices)].slice(0, 10),
208
+ antiPatterns: [...new Set(antiPatterns)].slice(0, 10),
209
+ testingTools: [...new Set(testingTips)].slice(0, 5),
210
+ debuggingTools: [], // Not collected in new format
211
+ documentationHints: [...new Set(documentationHints)].slice(0, 5),
212
+ researchMode,
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Get default implementation guidelines when synthesis fails
218
+ */
219
+ function getDefaultGuidelines(input: SynthesisInput): string[] {
220
+ const guidelines: string[] = [];
221
+
222
+ // Add test command if available
223
+ if (input.enrichedContext.commands.test) {
224
+ guidelines.push(`Run ${input.enrichedContext.commands.test} after changes`);
225
+ }
226
+
227
+ // Add build command if available
228
+ if (input.enrichedContext.commands.build) {
229
+ guidelines.push(`Run ${input.enrichedContext.commands.build} before committing`);
230
+ }
231
+
232
+ // Add E2E testing
233
+ guidelines.push(`Run npx playwright test for E2E testing`);
234
+
235
+ // Add generic guidelines
236
+ guidelines.push('Follow existing code patterns');
237
+ guidelines.push('Use TypeScript strict mode');
238
+
239
+ return guidelines.slice(0, 7);
240
+ }