wiggum-cli 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/agents/context-enricher.d.ts +10 -0
- package/dist/ai/agents/context-enricher.d.ts.map +1 -1
- package/dist/ai/agents/context-enricher.js +73 -13
- package/dist/ai/agents/context-enricher.js.map +1 -1
- package/dist/ai/agents/index.js +3 -2
- package/dist/ai/agents/index.js.map +1 -1
- package/dist/ai/agents/mcp-detector.d.ts +3 -2
- package/dist/ai/agents/mcp-detector.d.ts.map +1 -1
- package/dist/ai/agents/mcp-detector.js +23 -3
- package/dist/ai/agents/mcp-detector.js.map +1 -1
- package/dist/ai/agents/stack-researcher.js +2 -0
- package/dist/ai/agents/stack-researcher.js.map +1 -1
- package/dist/ai/agents/synthesis-agent.d.ts.map +1 -1
- package/dist/ai/agents/synthesis-agent.js +32 -9
- package/dist/ai/agents/synthesis-agent.js.map +1 -1
- package/dist/ai/agents/tech-researcher.d.ts +11 -0
- package/dist/ai/agents/tech-researcher.d.ts.map +1 -1
- package/dist/ai/agents/tech-researcher.js +71 -1
- package/dist/ai/agents/tech-researcher.js.map +1 -1
- package/dist/ai/agents/types.d.ts +2 -0
- package/dist/ai/agents/types.d.ts.map +1 -1
- package/dist/ai/enhancer.js +1 -1
- package/dist/ai/enhancer.js.map +1 -1
- package/dist/ai/tools.d.ts +1 -1
- package/dist/ai/tools.d.ts.map +1 -1
- package/dist/ai/tools.js +16 -5
- package/dist/ai/tools.js.map +1 -1
- package/package.json +1 -1
- package/src/ai/agents/context-enricher.test.ts +190 -0
- package/src/ai/agents/context-enricher.ts +79 -13
- package/src/ai/agents/index.ts +3 -2
- package/src/ai/agents/mcp-detector.test.ts +64 -9
- package/src/ai/agents/mcp-detector.ts +23 -3
- package/src/ai/agents/stack-researcher.ts +2 -0
- package/src/ai/agents/synthesis-agent.ts +46 -9
- package/src/ai/agents/tech-researcher.test.ts +156 -0
- package/src/ai/agents/tech-researcher.ts +86 -1
- package/src/ai/agents/types.ts +2 -0
- package/src/ai/enhancer.ts +1 -1
- package/src/ai/tools.ts +16 -5
|
@@ -27,17 +27,18 @@ Based on the analysis plan, explore the codebase to:
|
|
|
27
27
|
4. Find available commands (from package.json)
|
|
28
28
|
5. Answer the specific questions provided
|
|
29
29
|
|
|
30
|
-
##
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
30
|
+
## CRITICAL: You have a maximum of 6 tool calls before you MUST output JSON
|
|
31
|
+
- Call getPackageInfo (no field) FIRST - this returns bin, main, scripts
|
|
32
|
+
- Call listDirectory on root to see actual structure
|
|
33
|
+
- Maybe explore ONE key directory if needed
|
|
34
|
+
- After 3-4 tool calls, STOP exploring and OUTPUT your JSON
|
|
35
|
+
- Better to have partial info than no output at all
|
|
35
36
|
|
|
36
37
|
## Tools Available
|
|
37
|
-
-
|
|
38
|
-
- readFile: Read file contents
|
|
38
|
+
- getPackageInfo: Get package.json info - call with NO field parameter to get bin, main, scripts all at once
|
|
39
39
|
- listDirectory: List directory structure
|
|
40
|
-
-
|
|
40
|
+
- readFile: Read file contents
|
|
41
|
+
- searchCode: Search using ripgrep patterns
|
|
41
42
|
|
|
42
43
|
## Exploration Strategy
|
|
43
44
|
1. Read package.json FIRST for main/bin entries - these are authoritative entry points
|
|
@@ -69,6 +70,13 @@ Based on the analysis plan, explore the codebase to:
|
|
|
69
70
|
- Map each real directory to its purpose based on file contents
|
|
70
71
|
- Example: {"src/commands": "CLI commands", "app": "Next.js app router"}
|
|
71
72
|
|
|
73
|
+
## Architecture Discovery
|
|
74
|
+
Identify the data/control flow and include in answeredQuestions:
|
|
75
|
+
- For CLI tools: "CLI entry → command parser → handlers → output"
|
|
76
|
+
- For MCP servers: "Transport → request router → tool handlers → API client"
|
|
77
|
+
- For web apps: "Routes → controllers → services → database"
|
|
78
|
+
- For APIs: "HTTP server → middleware → routes → handlers → data layer"
|
|
79
|
+
|
|
72
80
|
## Output Format
|
|
73
81
|
Output ONLY valid JSON with discovered facts, not exploration instructions:
|
|
74
82
|
{
|
|
@@ -80,7 +88,10 @@ Output ONLY valid JSON with discovered facts, not exploration instructions:
|
|
|
80
88
|
},
|
|
81
89
|
"namingConventions": "camelCase files, PascalCase components",
|
|
82
90
|
"commands": {"test": "npm test", "build": "npm run build"},
|
|
83
|
-
"answeredQuestions": {
|
|
91
|
+
"answeredQuestions": {
|
|
92
|
+
"What is the auth strategy?": "NextAuth with JWT",
|
|
93
|
+
"architecture": "CLI parses commands via commander → calls API handlers → outputs results"
|
|
94
|
+
},
|
|
84
95
|
"projectType": "CLI Tool"
|
|
85
96
|
}`;
|
|
86
97
|
|
|
@@ -115,7 +126,7 @@ Start by exploring the specified areas, then answer the questions and produce yo
|
|
|
115
126
|
system: CONTEXT_ENRICHER_SYSTEM_PROMPT,
|
|
116
127
|
prompt,
|
|
117
128
|
tools,
|
|
118
|
-
stopWhen: stepCountIs(
|
|
129
|
+
stopWhen: stepCountIs(7), // Balance between exploration and ensuring JSON output
|
|
119
130
|
maxOutputTokens: 3000,
|
|
120
131
|
...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
|
|
121
132
|
experimental_telemetry: {
|
|
@@ -186,17 +197,71 @@ function parseEnrichedContext(
|
|
|
186
197
|
return getDefaultEnrichedContext(input);
|
|
187
198
|
}
|
|
188
199
|
|
|
200
|
+
// Derive commands from package.json as fallback if AI didn't find them
|
|
201
|
+
const derivedCommands = input ? deriveCommandsFromScripts(input.scanResult.projectRoot) : {};
|
|
202
|
+
const commands = parsed.commands && Object.keys(parsed.commands).length > 0
|
|
203
|
+
? parsed.commands
|
|
204
|
+
: derivedCommands;
|
|
205
|
+
|
|
189
206
|
// Build result with empty defaults for missing fields (don't guess)
|
|
190
207
|
return {
|
|
191
208
|
entryPoints: parsed.entryPoints || [], // Empty = not found, not guessed
|
|
192
209
|
keyDirectories: parsed.keyDirectories || {}, // Empty = not found
|
|
193
210
|
namingConventions: parsed.namingConventions || 'unknown',
|
|
194
|
-
commands
|
|
211
|
+
commands, // Derived from package.json if AI didn't find them
|
|
195
212
|
answeredQuestions: parsed.answeredQuestions || {},
|
|
196
213
|
projectType: parsed.projectType || 'Unknown',
|
|
197
214
|
};
|
|
198
215
|
}
|
|
199
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Script name patterns for command detection
|
|
219
|
+
* Exported for testing
|
|
220
|
+
*/
|
|
221
|
+
export const SCRIPT_MAPPINGS: Record<string, string[]> = {
|
|
222
|
+
test: ['test', 'test:unit', 'vitest', 'jest'],
|
|
223
|
+
lint: ['lint', 'eslint', 'lint:fix'],
|
|
224
|
+
typecheck: ['typecheck', 'tsc', 'type-check', 'types'],
|
|
225
|
+
build: ['build', 'compile'],
|
|
226
|
+
dev: ['dev', 'start:dev', 'develop', 'watch'],
|
|
227
|
+
format: ['format', 'prettier', 'fmt'],
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Derive commands from package.json scripts when AI fails to discover them
|
|
232
|
+
* Exported for testing
|
|
233
|
+
*/
|
|
234
|
+
export function deriveCommandsFromScripts(projectRoot: string): Record<string, string> {
|
|
235
|
+
const commands: Record<string, string> = {};
|
|
236
|
+
const packageJsonPath = join(projectRoot, 'package.json');
|
|
237
|
+
|
|
238
|
+
if (!existsSync(packageJsonPath)) {
|
|
239
|
+
return commands;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const content = readFileSync(packageJsonPath, 'utf-8');
|
|
244
|
+
const pkg = JSON.parse(content) as Record<string, unknown>;
|
|
245
|
+
const scripts = pkg.scripts as Record<string, string> | undefined;
|
|
246
|
+
|
|
247
|
+
if (!scripts) {
|
|
248
|
+
return commands;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const [command, patterns] of Object.entries(SCRIPT_MAPPINGS)) {
|
|
252
|
+
for (const pattern of patterns) {
|
|
253
|
+
if (scripts[pattern]) {
|
|
254
|
+
commands[command] = `npm run ${pattern}`;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return commands;
|
|
260
|
+
} catch {
|
|
261
|
+
return commands;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
200
265
|
/**
|
|
201
266
|
* Derive entry points from package.json when AI fails to discover them
|
|
202
267
|
*/
|
|
@@ -238,17 +303,18 @@ function deriveEntryPointsFromPackageJson(projectRoot: string): string[] {
|
|
|
238
303
|
|
|
239
304
|
/**
|
|
240
305
|
* Get default enriched context when parsing fails
|
|
241
|
-
* Returns empty arrays instead of guesses, but derives entry points from package.json
|
|
306
|
+
* Returns empty arrays instead of guesses, but derives entry points and commands from package.json
|
|
242
307
|
*/
|
|
243
308
|
function getDefaultEnrichedContext(input?: ContextEnricherInput): EnrichedContext {
|
|
244
309
|
const projectType = detectProjectType(input?.scanResult.stack);
|
|
245
310
|
const entryPoints = input ? deriveEntryPointsFromPackageJson(input.scanResult.projectRoot) : [];
|
|
311
|
+
const commands = input ? deriveCommandsFromScripts(input.scanResult.projectRoot) : {};
|
|
246
312
|
|
|
247
313
|
return {
|
|
248
314
|
entryPoints, // Derived from package.json, not guessed
|
|
249
315
|
keyDirectories: {}, // Empty = not discovered
|
|
250
316
|
namingConventions: 'unknown',
|
|
251
|
-
commands
|
|
317
|
+
commands, // Derived from package.json scripts
|
|
252
318
|
answeredQuestions: {},
|
|
253
319
|
projectType,
|
|
254
320
|
};
|
package/src/ai/agents/index.ts
CHANGED
|
@@ -162,8 +162,8 @@ export async function runMultiAgentAnalysis(
|
|
|
162
162
|
// ═══════════════════════════════════════════════════════════════
|
|
163
163
|
report('Phase 3/4: Synthesizing');
|
|
164
164
|
|
|
165
|
-
// Detect MCPs (pure function, no LLM)
|
|
166
|
-
const mcpServers = detectRalphMcpServers(scanResult.stack);
|
|
165
|
+
// Detect MCPs (pure function, no LLM) - pass project type for context-aware recommendations
|
|
166
|
+
const mcpServers = detectRalphMcpServers(scanResult.stack, enrichedContext.projectType);
|
|
167
167
|
|
|
168
168
|
// Run synthesis agent
|
|
169
169
|
const synthesizedResult = await runSynthesisAgent(
|
|
@@ -238,6 +238,7 @@ function getDefaultMultiAgentAnalysis(scanResult: ScanResult): MultiAgentAnalysi
|
|
|
238
238
|
antiPatterns: ['Avoid skipping tests'],
|
|
239
239
|
testingTools: ['npm test'],
|
|
240
240
|
debuggingTools: ['console.log'],
|
|
241
|
+
validationTools: ['npm run lint', 'npx tsc --noEmit'],
|
|
241
242
|
documentationHints: ['Check official docs'],
|
|
242
243
|
researchMode: 'knowledge-only',
|
|
243
244
|
},
|
|
@@ -28,10 +28,61 @@ function createStack(overrides: Partial<DetectedStack> = {}): DetectedStack {
|
|
|
28
28
|
|
|
29
29
|
describe('detectRalphMcpServers', () => {
|
|
30
30
|
describe('e2eTesting', () => {
|
|
31
|
-
it('
|
|
31
|
+
it('returns playwright by default', () => {
|
|
32
32
|
const result = detectRalphMcpServers(createStack());
|
|
33
33
|
expect(result.e2eTesting).toBe('playwright');
|
|
34
34
|
});
|
|
35
|
+
|
|
36
|
+
it('returns mcp-inspector for MCP projects via stack.mcp.isProject', () => {
|
|
37
|
+
const stack = createStack({
|
|
38
|
+
mcp: { isProject: true },
|
|
39
|
+
});
|
|
40
|
+
const result = detectRalphMcpServers(stack);
|
|
41
|
+
expect(result.e2eTesting).toBe('mcp-inspector');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns mcp-inspector for MCP projects via projectType parameter', () => {
|
|
45
|
+
const result = detectRalphMcpServers(createStack(), 'MCP Server');
|
|
46
|
+
expect(result.e2eTesting).toBe('mcp-inspector');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns mcp-inspector when projectType contains "mcp" (case-insensitive)', () => {
|
|
50
|
+
const result = detectRalphMcpServers(createStack(), 'mcp tool');
|
|
51
|
+
expect(result.e2eTesting).toBe('mcp-inspector');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns playwright for Next.js projects', () => {
|
|
55
|
+
const stack = createStack({
|
|
56
|
+
framework: detection('Next.js'),
|
|
57
|
+
});
|
|
58
|
+
const result = detectRalphMcpServers(stack);
|
|
59
|
+
expect(result.e2eTesting).toBe('playwright');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns playwright for React projects', () => {
|
|
63
|
+
const stack = createStack({
|
|
64
|
+
framework: detection('React'),
|
|
65
|
+
});
|
|
66
|
+
const result = detectRalphMcpServers(stack);
|
|
67
|
+
expect(result.e2eTesting).toBe('playwright');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns playwright for Vue projects', () => {
|
|
71
|
+
const stack = createStack({
|
|
72
|
+
framework: detection('Vue'),
|
|
73
|
+
});
|
|
74
|
+
const result = detectRalphMcpServers(stack);
|
|
75
|
+
expect(result.e2eTesting).toBe('playwright');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('prefers MCP detection over web framework detection', () => {
|
|
79
|
+
const stack = createStack({
|
|
80
|
+
mcp: { isProject: true },
|
|
81
|
+
framework: detection('Next.js'),
|
|
82
|
+
});
|
|
83
|
+
const result = detectRalphMcpServers(stack);
|
|
84
|
+
expect(result.e2eTesting).toBe('mcp-inspector');
|
|
85
|
+
});
|
|
35
86
|
});
|
|
36
87
|
|
|
37
88
|
describe('database detection', () => {
|
|
@@ -242,13 +293,16 @@ describe('detectRalphMcpServers', () => {
|
|
|
242
293
|
});
|
|
243
294
|
|
|
244
295
|
describe('convertToLegacyMcpRecommendations', () => {
|
|
245
|
-
|
|
296
|
+
// Note: filesystem and git are assumed available in Claude Code, so not included
|
|
297
|
+
it('only includes e2eTesting in essential (no filesystem/git)', () => {
|
|
246
298
|
const result = convertToLegacyMcpRecommendations({
|
|
247
299
|
e2eTesting: 'playwright',
|
|
248
300
|
additional: [],
|
|
249
301
|
});
|
|
250
|
-
|
|
251
|
-
expect(result.essential).
|
|
302
|
+
// Ralph loop focuses on essentials only - filesystem/git assumed available
|
|
303
|
+
expect(result.essential).toEqual(['playwright']);
|
|
304
|
+
expect(result.essential).not.toContain('filesystem');
|
|
305
|
+
expect(result.essential).not.toContain('git');
|
|
252
306
|
});
|
|
253
307
|
|
|
254
308
|
it('includes playwright in essential', () => {
|
|
@@ -268,13 +322,13 @@ describe('convertToLegacyMcpRecommendations', () => {
|
|
|
268
322
|
expect(result.essential).toContain('supabase');
|
|
269
323
|
});
|
|
270
324
|
|
|
271
|
-
it('
|
|
325
|
+
it('keeps recommended empty to focus on Ralph loop essentials', () => {
|
|
272
326
|
const result = convertToLegacyMcpRecommendations({
|
|
273
327
|
e2eTesting: 'playwright',
|
|
274
328
|
additional: ['docker', 'vercel'],
|
|
275
329
|
});
|
|
276
|
-
|
|
277
|
-
expect(result.recommended).
|
|
330
|
+
// Additional MCPs are not moved to recommended - keeping focus on essentials
|
|
331
|
+
expect(result.recommended).toEqual([]);
|
|
278
332
|
});
|
|
279
333
|
|
|
280
334
|
it('returns correct structure for full stack', () => {
|
|
@@ -284,7 +338,8 @@ describe('convertToLegacyMcpRecommendations', () => {
|
|
|
284
338
|
additional: ['docker', 'stripe'],
|
|
285
339
|
});
|
|
286
340
|
|
|
287
|
-
|
|
288
|
-
expect(result.
|
|
341
|
+
// Only e2eTesting and database in essential, no recommended MCPs
|
|
342
|
+
expect(result.essential).toEqual(['playwright', 'postgres']);
|
|
343
|
+
expect(result.recommended).toEqual([]);
|
|
289
344
|
});
|
|
290
345
|
});
|
|
@@ -66,13 +66,33 @@ const SERVICE_MCP_MAP: Record<string, string> = {
|
|
|
66
66
|
* Detect ralph-essential MCP servers from the stack
|
|
67
67
|
*
|
|
68
68
|
* Ralph loop essentials:
|
|
69
|
-
* -
|
|
69
|
+
* - For MCP Server projects: mcp-inspector for testing
|
|
70
|
+
* - For web apps with E2E: playwright for E2E testing
|
|
70
71
|
* - Database MCP: If database is detected
|
|
71
72
|
* - Additional MCPs based on services and deployment
|
|
72
73
|
*/
|
|
73
|
-
export function detectRalphMcpServers(stack: DetectedStack): RalphMcpServers {
|
|
74
|
+
export function detectRalphMcpServers(stack: DetectedStack, projectType?: string): RalphMcpServers {
|
|
75
|
+
// Determine appropriate E2E testing tool based on project type
|
|
76
|
+
const isMcpProject = stack.mcp?.isProject || projectType?.toLowerCase().includes('mcp');
|
|
77
|
+
const isWebApp = stack.framework?.name?.toLowerCase().includes('next') ||
|
|
78
|
+
stack.framework?.name?.toLowerCase().includes('react') ||
|
|
79
|
+
stack.framework?.name?.toLowerCase().includes('vue') ||
|
|
80
|
+
stack.framework?.name?.toLowerCase().includes('svelte') ||
|
|
81
|
+
stack.framework?.name?.toLowerCase().includes('nuxt') ||
|
|
82
|
+
stack.framework?.name?.toLowerCase().includes('remix');
|
|
83
|
+
|
|
84
|
+
// Choose E2E testing tool based on project type
|
|
85
|
+
let e2eTesting: string;
|
|
86
|
+
if (isMcpProject) {
|
|
87
|
+
e2eTesting = 'mcp-inspector'; // MCP projects use MCP Inspector
|
|
88
|
+
} else if (isWebApp) {
|
|
89
|
+
e2eTesting = 'playwright'; // Web apps use Playwright
|
|
90
|
+
} else {
|
|
91
|
+
e2eTesting = 'playwright'; // Default to Playwright for CLI/other projects
|
|
92
|
+
}
|
|
93
|
+
|
|
74
94
|
const result: RalphMcpServers = {
|
|
75
|
-
e2eTesting
|
|
95
|
+
e2eTesting,
|
|
76
96
|
additional: [],
|
|
77
97
|
};
|
|
78
98
|
|
|
@@ -295,6 +295,7 @@ function parseStackResearch(
|
|
|
295
295
|
antiPatterns: parsed.antiPatterns || [],
|
|
296
296
|
testingTools: parsed.testingTools || [],
|
|
297
297
|
debuggingTools: parsed.debuggingTools || [],
|
|
298
|
+
validationTools: parsed.validationTools || [],
|
|
298
299
|
documentationHints: parsed.documentationHints || [],
|
|
299
300
|
researchMode: researchMode,
|
|
300
301
|
};
|
|
@@ -309,6 +310,7 @@ function getDefaultStackResearch(researchMode: StackResearch['researchMode']): S
|
|
|
309
310
|
antiPatterns: ['Avoid skipping tests', 'Don\'t ignore type errors'],
|
|
310
311
|
testingTools: ['npm test'],
|
|
311
312
|
debuggingTools: ['console.log', 'debugger statement'],
|
|
313
|
+
validationTools: ['npm run lint', 'npx tsc --noEmit'],
|
|
312
314
|
documentationHints: ['Check package.json for dependencies'],
|
|
313
315
|
researchMode,
|
|
314
316
|
};
|
|
@@ -24,6 +24,11 @@ import { getTracedAI } from '../../utils/tracing.js';
|
|
|
24
24
|
const synthesisOutputSchema = z.object({
|
|
25
25
|
implementationGuidelines: z.array(z.string()).describe('Short, actionable implementation guidelines'),
|
|
26
26
|
possibleMissedTechnologies: z.array(z.string()).describe('Technologies that may have been missed (empty array if none)'),
|
|
27
|
+
technologyTools: z.object({
|
|
28
|
+
testing: z.array(z.string()).describe('Commands to run tests (e.g., "npm test", "npx vitest")'),
|
|
29
|
+
debugging: z.array(z.string()).describe('Debug flags, env vars, tools (e.g., "DEBUG=* npm run dev")'),
|
|
30
|
+
validation: z.array(z.string()).describe('Type checking, linting commands (e.g., "npm run lint", "npx tsc --noEmit")'),
|
|
31
|
+
}).optional(),
|
|
27
32
|
});
|
|
28
33
|
|
|
29
34
|
/**
|
|
@@ -35,6 +40,7 @@ const SYNTHESIS_AGENT_SYSTEM_PROMPT = `You are a Synthesis Agent that merges ana
|
|
|
35
40
|
Based on the enriched context and technology research, generate:
|
|
36
41
|
1. Short implementation guidelines (5-10 words each) describing DISCOVERED patterns
|
|
37
42
|
2. List any technologies that may have been missed
|
|
43
|
+
3. Technology tools for testing, debugging, and validation
|
|
38
44
|
|
|
39
45
|
## Guidelines Style
|
|
40
46
|
- Describe DISCOVERED patterns, not instructions to follow
|
|
@@ -48,6 +54,12 @@ Based on the enriched context and technology research, generate:
|
|
|
48
54
|
- Bad: "Use Zod for validation"
|
|
49
55
|
- Max 7 patterns, prioritize most distinctive features
|
|
50
56
|
|
|
57
|
+
## Technology Tools
|
|
58
|
+
Based on the detected stack and available commands, provide:
|
|
59
|
+
- testing: Commands to verify changes (e.g., "npm test", "npx vitest")
|
|
60
|
+
- debugging: How to debug (e.g., "DEBUG=* npm run dev", "--verbose flag", "NODE_DEBUG=http")
|
|
61
|
+
- validation: Pre-commit checks (e.g., "npm run lint", "npx tsc --noEmit")
|
|
62
|
+
|
|
51
63
|
## Example Output
|
|
52
64
|
{
|
|
53
65
|
"implementationGuidelines": [
|
|
@@ -57,7 +69,12 @@ Based on the enriched context and technology research, generate:
|
|
|
57
69
|
"Zod schemas in src/schemas for API validation",
|
|
58
70
|
"Playwright E2E tests in tests/e2e"
|
|
59
71
|
],
|
|
60
|
-
"possibleMissedTechnologies": ["Redis caching"]
|
|
72
|
+
"possibleMissedTechnologies": ["Redis caching"],
|
|
73
|
+
"technologyTools": {
|
|
74
|
+
"testing": ["npm test", "npx vitest --watch"],
|
|
75
|
+
"debugging": ["DEBUG=* npm run dev", "--verbose flag"],
|
|
76
|
+
"validation": ["npm run lint", "npx tsc --noEmit"]
|
|
77
|
+
}
|
|
61
78
|
}`;
|
|
62
79
|
|
|
63
80
|
/**
|
|
@@ -121,7 +138,7 @@ Generate concise, actionable implementation guidelines based on this analysis.`;
|
|
|
121
138
|
}
|
|
122
139
|
|
|
123
140
|
// Convert to MultiAgentAnalysis format for backward compatibility
|
|
124
|
-
return buildMultiAgentAnalysis(input, synthesis.implementationGuidelines, synthesis.possibleMissedTechnologies);
|
|
141
|
+
return buildMultiAgentAnalysis(input, synthesis.implementationGuidelines, synthesis.possibleMissedTechnologies, synthesis.technologyTools);
|
|
125
142
|
} catch (error) {
|
|
126
143
|
if (verbose) {
|
|
127
144
|
logger.error(`Synthesis Agent error: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -132,13 +149,23 @@ Generate concise, actionable implementation guidelines based on this analysis.`;
|
|
|
132
149
|
}
|
|
133
150
|
}
|
|
134
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Technology tools output from synthesis
|
|
154
|
+
*/
|
|
155
|
+
interface TechnologyTools {
|
|
156
|
+
testing?: string[];
|
|
157
|
+
debugging?: string[];
|
|
158
|
+
validation?: string[];
|
|
159
|
+
}
|
|
160
|
+
|
|
135
161
|
/**
|
|
136
162
|
* Build MultiAgentAnalysis from synthesis input and generated guidelines
|
|
137
163
|
*/
|
|
138
164
|
function buildMultiAgentAnalysis(
|
|
139
165
|
input: SynthesisInput,
|
|
140
166
|
implementationGuidelines: string[],
|
|
141
|
-
possibleMissedTechnologies?: string[]
|
|
167
|
+
possibleMissedTechnologies?: string[],
|
|
168
|
+
technologyTools?: TechnologyTools
|
|
142
169
|
): MultiAgentAnalysis {
|
|
143
170
|
// Convert EnrichedContext to CodebaseAnalysis format
|
|
144
171
|
const codebaseAnalysis: CodebaseAnalysis = {
|
|
@@ -161,7 +188,7 @@ function buildMultiAgentAnalysis(
|
|
|
161
188
|
};
|
|
162
189
|
|
|
163
190
|
// Merge tech research into StackResearch format
|
|
164
|
-
const stackResearch: StackResearch = mergeTechResearch(input.techResearch);
|
|
191
|
+
const stackResearch: StackResearch = mergeTechResearch(input.techResearch, technologyTools);
|
|
165
192
|
|
|
166
193
|
// Convert MCP servers to legacy format
|
|
167
194
|
const mcpServers: McpRecommendations = convertToLegacyMcpRecommendations(input.mcpServers);
|
|
@@ -176,13 +203,22 @@ function buildMultiAgentAnalysis(
|
|
|
176
203
|
/**
|
|
177
204
|
* Merge multiple TechResearchResult into a single StackResearch
|
|
178
205
|
*/
|
|
179
|
-
function mergeTechResearch(
|
|
206
|
+
function mergeTechResearch(
|
|
207
|
+
techResearch: SynthesisInput['techResearch'],
|
|
208
|
+
technologyTools?: TechnologyTools
|
|
209
|
+
): StackResearch {
|
|
210
|
+
// Use technologyTools from synthesis if available
|
|
211
|
+
const synthesisTesting = technologyTools?.testing || [];
|
|
212
|
+
const synthesisDebugging = technologyTools?.debugging || [];
|
|
213
|
+
const synthesisValidation = technologyTools?.validation || [];
|
|
214
|
+
|
|
180
215
|
if (techResearch.length === 0) {
|
|
181
216
|
return {
|
|
182
217
|
bestPractices: ['Follow project conventions'],
|
|
183
218
|
antiPatterns: ['Avoid skipping tests'],
|
|
184
|
-
testingTools: ['npm test'],
|
|
185
|
-
debuggingTools: ['console.log'],
|
|
219
|
+
testingTools: synthesisTesting.length > 0 ? synthesisTesting : ['npm test'],
|
|
220
|
+
debuggingTools: synthesisDebugging.length > 0 ? synthesisDebugging : ['console.log'],
|
|
221
|
+
validationTools: synthesisValidation.length > 0 ? synthesisValidation : ['npm run lint'],
|
|
186
222
|
documentationHints: ['Check official docs'],
|
|
187
223
|
researchMode: 'knowledge-only',
|
|
188
224
|
};
|
|
@@ -191,7 +227,7 @@ function mergeTechResearch(techResearch: SynthesisInput['techResearch']): StackR
|
|
|
191
227
|
// Merge all research results
|
|
192
228
|
const bestPractices: string[] = [];
|
|
193
229
|
const antiPatterns: string[] = [];
|
|
194
|
-
const testingTips: string[] = [];
|
|
230
|
+
const testingTips: string[] = [...synthesisTesting]; // Start with synthesis testing tools
|
|
195
231
|
const documentationHints: string[] = [];
|
|
196
232
|
let researchMode: StackResearch['researchMode'] = 'knowledge-only';
|
|
197
233
|
|
|
@@ -212,7 +248,8 @@ function mergeTechResearch(techResearch: SynthesisInput['techResearch']): StackR
|
|
|
212
248
|
bestPractices: [...new Set(bestPractices)].slice(0, 10),
|
|
213
249
|
antiPatterns: [...new Set(antiPatterns)].slice(0, 10),
|
|
214
250
|
testingTools: [...new Set(testingTips)].slice(0, 5),
|
|
215
|
-
debuggingTools: [],
|
|
251
|
+
debuggingTools: [...new Set(synthesisDebugging)].slice(0, 5),
|
|
252
|
+
validationTools: [...new Set(synthesisValidation)].slice(0, 5),
|
|
216
253
|
documentationHints: [...new Set(documentationHints)].slice(0, 5),
|
|
217
254
|
researchMode,
|
|
218
255
|
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Tech Researcher
|
|
3
|
+
*
|
|
4
|
+
* Run with: npx vitest run src/ai/agents/tech-researcher.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { getDocumentationHints, DOCUMENTATION_HINTS } from './tech-researcher.js';
|
|
9
|
+
|
|
10
|
+
describe('getDocumentationHints', () => {
|
|
11
|
+
describe('direct matches', () => {
|
|
12
|
+
it('returns hints for exact match "Next.js"', () => {
|
|
13
|
+
const result = getDocumentationHints('Next.js');
|
|
14
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['Next.js']);
|
|
15
|
+
expect(result).toContain('https://nextjs.org/docs/app');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns hints for exact match "React"', () => {
|
|
19
|
+
const result = getDocumentationHints('React');
|
|
20
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['React']);
|
|
21
|
+
expect(result).toContain('https://react.dev');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns hints for exact match "MCP"', () => {
|
|
25
|
+
const result = getDocumentationHints('MCP');
|
|
26
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['MCP']);
|
|
27
|
+
expect(result).toContain('https://modelcontextprotocol.io/docs');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns hints for exact match "Vitest"', () => {
|
|
31
|
+
const result = getDocumentationHints('Vitest');
|
|
32
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['Vitest']);
|
|
33
|
+
expect(result).toContain('https://vitest.dev/guide');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns hints for exact match "TypeScript"', () => {
|
|
37
|
+
const result = getDocumentationHints('TypeScript');
|
|
38
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['TypeScript']);
|
|
39
|
+
expect(result).toContain('https://www.typescriptlang.org/docs');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('partial matches (case-insensitive)', () => {
|
|
44
|
+
it('matches "next.js" (lowercase) to Next.js', () => {
|
|
45
|
+
const result = getDocumentationHints('next.js');
|
|
46
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['Next.js']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('matches "REACT" (uppercase) to React', () => {
|
|
50
|
+
const result = getDocumentationHints('REACT');
|
|
51
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['React']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('matches "typescript" (lowercase) to TypeScript', () => {
|
|
55
|
+
const result = getDocumentationHints('typescript');
|
|
56
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['TypeScript']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('matches "vitest" (lowercase) to Vitest', () => {
|
|
60
|
+
const result = getDocumentationHints('vitest');
|
|
61
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['Vitest']);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('partial string matches', () => {
|
|
66
|
+
it('matches "Next.js 14" to Next.js', () => {
|
|
67
|
+
const result = getDocumentationHints('Next.js 14');
|
|
68
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['Next.js']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('matches "React 18" to React', () => {
|
|
72
|
+
const result = getDocumentationHints('React 18');
|
|
73
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['React']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('matches "MCP Server" key exactly', () => {
|
|
77
|
+
const result = getDocumentationHints('MCP Server');
|
|
78
|
+
expect(result).toEqual(DOCUMENTATION_HINTS['MCP Server']);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('fallback behavior', () => {
|
|
83
|
+
it('returns generic hint for unknown technology', () => {
|
|
84
|
+
const result = getDocumentationHints('UnknownFramework');
|
|
85
|
+
expect(result).toEqual(['Check official UnknownFramework documentation']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns generic hint for empty string', () => {
|
|
89
|
+
const result = getDocumentationHints('');
|
|
90
|
+
expect(result).toEqual(['Check official documentation']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns generic hint for technology not in mapping', () => {
|
|
94
|
+
const result = getDocumentationHints('SomeRandomLib');
|
|
95
|
+
expect(result).toEqual(['Check official SomeRandomLib documentation']);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('specific technology coverage', () => {
|
|
100
|
+
it('has MCP ecosystem hints', () => {
|
|
101
|
+
expect(DOCUMENTATION_HINTS['MCP']).toBeDefined();
|
|
102
|
+
expect(DOCUMENTATION_HINTS['MCP Server']).toBeDefined();
|
|
103
|
+
expect(DOCUMENTATION_HINTS['@modelcontextprotocol/sdk']).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('has frontend framework hints', () => {
|
|
107
|
+
expect(DOCUMENTATION_HINTS['Next.js']).toBeDefined();
|
|
108
|
+
expect(DOCUMENTATION_HINTS['React']).toBeDefined();
|
|
109
|
+
expect(DOCUMENTATION_HINTS['Vue']).toBeDefined();
|
|
110
|
+
expect(DOCUMENTATION_HINTS['Svelte']).toBeDefined();
|
|
111
|
+
expect(DOCUMENTATION_HINTS['Nuxt']).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('has backend framework hints', () => {
|
|
115
|
+
expect(DOCUMENTATION_HINTS['Express']).toBeDefined();
|
|
116
|
+
expect(DOCUMENTATION_HINTS['Fastify']).toBeDefined();
|
|
117
|
+
expect(DOCUMENTATION_HINTS['Hono']).toBeDefined();
|
|
118
|
+
expect(DOCUMENTATION_HINTS['NestJS']).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('has testing tool hints', () => {
|
|
122
|
+
expect(DOCUMENTATION_HINTS['Vitest']).toBeDefined();
|
|
123
|
+
expect(DOCUMENTATION_HINTS['Jest']).toBeDefined();
|
|
124
|
+
expect(DOCUMENTATION_HINTS['Playwright']).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('has database/ORM hints', () => {
|
|
128
|
+
expect(DOCUMENTATION_HINTS['Prisma']).toBeDefined();
|
|
129
|
+
expect(DOCUMENTATION_HINTS['Drizzle']).toBeDefined();
|
|
130
|
+
expect(DOCUMENTATION_HINTS['Supabase']).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('has CLI tool hints', () => {
|
|
134
|
+
expect(DOCUMENTATION_HINTS['Commander']).toBeDefined();
|
|
135
|
+
expect(DOCUMENTATION_HINTS['Yargs']).toBeDefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('DOCUMENTATION_HINTS', () => {
|
|
141
|
+
it('has valid URLs for all entries', () => {
|
|
142
|
+
for (const [tech, hints] of Object.entries(DOCUMENTATION_HINTS)) {
|
|
143
|
+
expect(Array.isArray(hints)).toBe(true);
|
|
144
|
+
expect(hints.length).toBeGreaterThan(0);
|
|
145
|
+
for (const hint of hints) {
|
|
146
|
+
expect(hint).toMatch(/^https?:\/\//);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('has no duplicate entries', () => {
|
|
152
|
+
const keys = Object.keys(DOCUMENTATION_HINTS);
|
|
153
|
+
const uniqueKeys = new Set(keys);
|
|
154
|
+
expect(keys.length).toBe(uniqueKeys.size);
|
|
155
|
+
});
|
|
156
|
+
});
|