wiggum-cli 0.2.2 → 0.2.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.
@@ -0,0 +1,278 @@
1
+ /**
2
+ * AI Tools Module
3
+ * Defines tools the AI agent can use to explore the codebase
4
+ */
5
+
6
+ import { tool, zodSchema } from 'ai';
7
+ import { z } from 'zod';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
10
+ import { join, relative } from 'node:path';
11
+
12
+ /**
13
+ * Create tools for codebase exploration
14
+ */
15
+ export function createExplorationTools(projectRoot: string) {
16
+ return {
17
+ /**
18
+ * Search code using ripgrep patterns
19
+ */
20
+ searchCode: tool({
21
+ description: `Search the codebase using ripgrep. Use this to find:
22
+ - Function/class definitions: pattern="^(def|function|class) NAME", fileType="py"
23
+ - Symbol usage: pattern="\\bSymbolName\\b" (word boundaries critical!)
24
+ - Imports: pattern="^import.*module", fileType="ts"
25
+ - File patterns: pattern="pattern", glob="*.tsx"
26
+
27
+ Tips:
28
+ - Use fileType for filtering (py, js, ts, rust, go)
29
+ - Use \\b for word boundaries to avoid partial matches
30
+ - Use literal=true for exact strings (faster)
31
+ - Always use word boundaries when searching symbols`,
32
+ inputSchema: zodSchema(z.object({
33
+ pattern: z.string().describe('The regex pattern to search for'),
34
+ fileType: z.string().optional().describe('File type filter (py, js, ts, etc.)'),
35
+ glob: z.string().optional().describe('Glob pattern like "*.tsx" or "src/**/*.ts"'),
36
+ literal: z.boolean().optional().describe('Use literal search (faster for exact strings)'),
37
+ context: z.number().optional().describe('Lines of context (default 0)'),
38
+ maxResults: z.number().optional().describe('Max results to return (default 50)'),
39
+ })),
40
+ execute: async ({ pattern, fileType, glob, literal, context, maxResults }) => {
41
+ try {
42
+ const args: string[] = [];
43
+
44
+ if (literal) args.push('-F');
45
+ if (fileType) args.push('-t', fileType);
46
+ if (glob) args.push('-g', glob);
47
+ if (context) args.push('-C', String(context));
48
+
49
+ args.push('--max-count', String(maxResults || 50));
50
+ args.push('--no-heading');
51
+ args.push('--line-number');
52
+ args.push('--');
53
+ args.push(pattern);
54
+ args.push(projectRoot);
55
+
56
+ // Use spawnSync instead of execSync for safety (no shell injection)
57
+ const result = spawnSync('rg', args, {
58
+ encoding: 'utf-8',
59
+ maxBuffer: 1024 * 1024,
60
+ timeout: 30000,
61
+ });
62
+
63
+ if (result.error) {
64
+ return `Search error: ${result.error.message}`;
65
+ }
66
+
67
+ if (result.status === 1) {
68
+ return 'No matches found';
69
+ }
70
+
71
+ if (result.status !== 0) {
72
+ return `Search error: ${result.stderr || 'Unknown error'}`;
73
+ }
74
+
75
+ // Convert absolute paths to relative
76
+ const lines = result.stdout.split('\n').map(line => {
77
+ if (line.startsWith(projectRoot)) {
78
+ return line.replace(projectRoot + '/', '');
79
+ }
80
+ return line;
81
+ });
82
+
83
+ return lines.slice(0, 100).join('\n');
84
+ } catch (error: unknown) {
85
+ const errMsg = error instanceof Error ? error.message : String(error);
86
+ return `Search error: ${errMsg}`;
87
+ }
88
+ },
89
+ }),
90
+
91
+ /**
92
+ * Read a file's contents
93
+ */
94
+ readFile: tool({
95
+ description: 'Read the contents of a file. Use relative paths from project root.',
96
+ inputSchema: zodSchema(z.object({
97
+ path: z.string().describe('Relative path to the file from project root'),
98
+ startLine: z.number().optional().describe('Start line (1-indexed)'),
99
+ endLine: z.number().optional().describe('End line (inclusive)'),
100
+ })),
101
+ execute: async ({ path: filePath, startLine, endLine }) => {
102
+ try {
103
+ // Prevent path traversal
104
+ const normalizedPath = filePath.replace(/\.\./g, '');
105
+ const fullPath = join(projectRoot, normalizedPath);
106
+
107
+ if (!fullPath.startsWith(projectRoot)) {
108
+ return 'Invalid path: cannot access files outside project';
109
+ }
110
+
111
+ if (!existsSync(fullPath)) {
112
+ return `File not found: ${filePath}`;
113
+ }
114
+
115
+ const stat = statSync(fullPath);
116
+ if (stat.isDirectory()) {
117
+ return `Path is a directory, not a file: ${filePath}`;
118
+ }
119
+
120
+ if (stat.size > 100000) {
121
+ return `File too large (${stat.size} bytes). Use startLine/endLine to read a portion.`;
122
+ }
123
+
124
+ const content = readFileSync(fullPath, 'utf-8');
125
+ const lines = content.split('\n');
126
+
127
+ if (startLine || endLine) {
128
+ const start = (startLine || 1) - 1;
129
+ const end = endLine || lines.length;
130
+ return lines.slice(start, end).join('\n');
131
+ }
132
+
133
+ return content;
134
+ } catch (error) {
135
+ const errMsg = error instanceof Error ? error.message : String(error);
136
+ return `Error reading file: ${errMsg}`;
137
+ }
138
+ },
139
+ }),
140
+
141
+ /**
142
+ * List directory contents
143
+ */
144
+ listDirectory: tool({
145
+ description: 'List contents of a directory. Shows files and subdirectories.',
146
+ inputSchema: zodSchema(z.object({
147
+ path: z.string().describe('Relative path to the directory (use "." for project root)'),
148
+ recursive: z.boolean().optional().describe('List recursively (default false)'),
149
+ maxDepth: z.number().optional().describe('Max depth for recursive listing (default 2)'),
150
+ })),
151
+ execute: async ({ path: dirPath, recursive, maxDepth }) => {
152
+ try {
153
+ // Prevent path traversal
154
+ const normalizedPath = dirPath.replace(/\.\./g, '');
155
+ const fullPath = join(projectRoot, normalizedPath);
156
+
157
+ if (!fullPath.startsWith(projectRoot)) {
158
+ return 'Invalid path: cannot access directories outside project';
159
+ }
160
+
161
+ if (!existsSync(fullPath)) {
162
+ return `Directory not found: ${dirPath}`;
163
+ }
164
+
165
+ const stat = statSync(fullPath);
166
+ if (!stat.isDirectory()) {
167
+ return `Path is not a directory: ${dirPath}`;
168
+ }
169
+
170
+ const results: string[] = [];
171
+ const depth = maxDepth || 2;
172
+
173
+ function scanDir(dir: string, currentDepth: number): void {
174
+ if (currentDepth > depth) return;
175
+
176
+ const entries = readdirSync(dir, { withFileTypes: true });
177
+
178
+ for (const entry of entries) {
179
+ // Skip common ignored directories
180
+ if (['node_modules', '.git', 'dist', 'build', '.next', '__pycache__'].includes(entry.name)) {
181
+ continue;
182
+ }
183
+
184
+ const relativePath = relative(projectRoot, join(dir, entry.name));
185
+ const prefix = entry.isDirectory() ? '📁 ' : '📄 ';
186
+ results.push(prefix + relativePath);
187
+
188
+ if (recursive && entry.isDirectory() && currentDepth < depth) {
189
+ scanDir(join(dir, entry.name), currentDepth + 1);
190
+ }
191
+ }
192
+ }
193
+
194
+ scanDir(fullPath, 1);
195
+ return results.slice(0, 200).join('\n');
196
+ } catch (error) {
197
+ const errMsg = error instanceof Error ? error.message : String(error);
198
+ return `Error listing directory: ${errMsg}`;
199
+ }
200
+ },
201
+ }),
202
+
203
+ /**
204
+ * Get package.json info including scripts
205
+ */
206
+ getPackageInfo: tool({
207
+ description: 'Get package.json contents including scripts, dependencies, and metadata.',
208
+ inputSchema: zodSchema(z.object({
209
+ field: z.string().optional().describe('Specific field to get (scripts, dependencies, etc.)'),
210
+ })),
211
+ execute: async ({ field }) => {
212
+ try {
213
+ const pkgPath = join(projectRoot, 'package.json');
214
+
215
+ if (!existsSync(pkgPath)) {
216
+ return 'No package.json found';
217
+ }
218
+
219
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
220
+
221
+ if (field) {
222
+ return JSON.stringify(pkg[field], null, 2) || `Field "${field}" not found`;
223
+ }
224
+
225
+ // Return relevant parts
226
+ return JSON.stringify({
227
+ name: pkg.name,
228
+ scripts: pkg.scripts,
229
+ dependencies: Object.keys(pkg.dependencies || {}),
230
+ devDependencies: Object.keys(pkg.devDependencies || {}),
231
+ }, null, 2);
232
+ } catch (error) {
233
+ const errMsg = error instanceof Error ? error.message : String(error);
234
+ return `Error reading package.json: ${errMsg}`;
235
+ }
236
+ },
237
+ }),
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Ripgrep skill reference for the AI agent
243
+ */
244
+ export const RIPGREP_SKILL = `
245
+ ## ripgrep Code Search Skill
246
+
247
+ ### Essential Patterns
248
+
249
+ **Find definitions:**
250
+ - Python functions: pattern="^def \\w+\\(", fileType="py"
251
+ - JS/TS functions: pattern="^(export )?(function|const) \\w+", fileType="ts"
252
+ - Classes: pattern="^class \\w+", fileType="py"
253
+
254
+ **Find symbol usage (CRITICAL: use word boundaries):**
255
+ - Exact word: pattern="\\bSymbolName\\b"
256
+ - Literal string: pattern="exact.string", literal=true
257
+
258
+ **Find imports:**
259
+ - ES imports: pattern="^import.*from", fileType="ts"
260
+ - CommonJS: pattern="require\\(", fileType="js"
261
+
262
+ **File type options:**
263
+ - py (Python)
264
+ - js (JavaScript)
265
+ - ts (TypeScript)
266
+ - rust (Rust)
267
+ - go (Go)
268
+
269
+ **Performance tips:**
270
+ 1. Always use fileType when possible
271
+ 2. Use literal=true for exact strings
272
+ 3. Use \\b for word boundaries (prevents partial matches)
273
+ 4. Keep maxResults reasonable
274
+
275
+ **Word boundaries are critical:**
276
+ - WITHOUT: pattern="log" matches "logger", "blogger", "catalog"
277
+ - WITH: pattern="\\blog\\b" matches only "log"
278
+ `;
@@ -23,6 +23,8 @@ export interface InitOptions {
23
23
  ai?: boolean;
24
24
  provider?: AIProvider;
25
25
  yes?: boolean;
26
+ /** Use agentic mode for deep codebase exploration */
27
+ agentic?: boolean;
26
28
  }
27
29
 
28
30
  /**
@@ -175,13 +177,31 @@ export async function initCommand(options: InitOptions): Promise<void> {
175
177
  const selectedModel = modelChoice as string;
176
178
  const modelLabel = modelOptions.find(m => m.value === selectedModel)?.label || selectedModel;
177
179
 
180
+ // Ask about agentic mode (deep exploration)
181
+ let useAgentic = options.agentic;
182
+ if (useAgentic === undefined && !options.yes) {
183
+ const wantAgentic = await prompts.confirm({
184
+ message: 'Enable deep codebase exploration? (AI will search files and directories)',
185
+ initialValue: true,
186
+ });
187
+
188
+ if (prompts.isCancel(wantAgentic)) {
189
+ logger.info('Initialization cancelled');
190
+ return;
191
+ }
192
+
193
+ useAgentic = wantAgentic;
194
+ }
195
+
178
196
  console.log('');
179
- console.log(simpson.yellow(`─── AI Enhancement (${provider} / ${modelLabel}) ───`));
197
+ const modeLabel = useAgentic ? 'agentic' : 'simple';
198
+ console.log(simpson.yellow(`─── AI Enhancement (${provider} / ${modelLabel} / ${modeLabel}) ───`));
180
199
 
181
200
  const aiEnhancer = new AIEnhancer({
182
201
  provider,
183
202
  model: selectedModel,
184
203
  verbose: true,
204
+ agentic: useAgentic,
185
205
  });
186
206
 
187
207
  spinner.start('Running AI analysis...');
@@ -39,7 +39,7 @@ export interface TemplateVariables {
39
39
  stylingVersion: string;
40
40
  stylingVariant: string;
41
41
 
42
- // Commands (derived from package manager)
42
+ // Commands (derived from package manager or AI detected)
43
43
  devCommand: string;
44
44
  buildCommand: string;
45
45
  testCommand: string;
@@ -50,6 +50,16 @@ export interface TemplateVariables {
50
50
  // Paths
51
51
  appDir: string;
52
52
 
53
+ // AI Analysis (optional - populated when AI enhancement is used)
54
+ aiEntryPoints: string;
55
+ aiKeyDirectories: string;
56
+ aiNamingConventions: string;
57
+ aiImplementationGuidelines: string;
58
+ aiMcpEssential: string;
59
+ aiMcpRecommended: string;
60
+ aiMissedTechnologies: string;
61
+ hasAiAnalysis: string;
62
+
53
63
  // Custom variables
54
64
  [key: string]: string;
55
65
  }
@@ -93,6 +103,115 @@ function deriveCommands(packageManager: string): Pick<
93
103
  };
94
104
  }
95
105
 
106
+ /**
107
+ * AI analysis template variables
108
+ */
109
+ type AiTemplateVars = Pick<TemplateVariables,
110
+ 'hasAiAnalysis' | 'aiEntryPoints' | 'aiKeyDirectories' | 'aiNamingConventions' |
111
+ 'aiImplementationGuidelines' | 'aiMcpEssential' | 'aiMcpRecommended' | 'aiMissedTechnologies'
112
+ >;
113
+
114
+ /**
115
+ * Format AI analysis data for templates
116
+ */
117
+ function formatAiAnalysisForTemplates(scanResult: ScanResult): AiTemplateVars {
118
+ // Check if this is an EnhancedScanResult with aiAnalysis
119
+ const aiAnalysis = (scanResult as { aiAnalysis?: {
120
+ projectContext?: {
121
+ entryPoints?: string[];
122
+ keyDirectories?: Record<string, string>;
123
+ namingConventions?: string;
124
+ };
125
+ commands?: Record<string, string>;
126
+ implementationGuidelines?: string[];
127
+ mcpServers?: {
128
+ essential?: string[];
129
+ recommended?: string[];
130
+ };
131
+ possibleMissedTechnologies?: string[];
132
+ } }).aiAnalysis;
133
+
134
+ if (!aiAnalysis) {
135
+ return {
136
+ hasAiAnalysis: '',
137
+ aiEntryPoints: '',
138
+ aiKeyDirectories: '',
139
+ aiNamingConventions: '',
140
+ aiImplementationGuidelines: '',
141
+ aiMcpEssential: '',
142
+ aiMcpRecommended: '',
143
+ aiMissedTechnologies: '',
144
+ };
145
+ }
146
+
147
+ // Format entry points as bullet list
148
+ const entryPoints = aiAnalysis.projectContext?.entryPoints || [];
149
+ const aiEntryPoints = entryPoints.length > 0
150
+ ? entryPoints.map(e => `- \`${e}\``).join('\n')
151
+ : '';
152
+
153
+ // Format key directories as table rows
154
+ const keyDirs = aiAnalysis.projectContext?.keyDirectories || {};
155
+ const aiKeyDirectories = Object.keys(keyDirs).length > 0
156
+ ? Object.entries(keyDirs).map(([dir, purpose]) => `| \`${dir}/\` | ${purpose} |`).join('\n')
157
+ : '';
158
+
159
+ // Naming conventions
160
+ const aiNamingConventions = aiAnalysis.projectContext?.namingConventions || '';
161
+
162
+ // Implementation guidelines as bullet list
163
+ const guidelines = aiAnalysis.implementationGuidelines || [];
164
+ const aiImplementationGuidelines = guidelines.length > 0
165
+ ? guidelines.map(g => `- ${g}`).join('\n')
166
+ : '';
167
+
168
+ // MCP servers
169
+ const aiMcpEssential = (aiAnalysis.mcpServers?.essential || []).join(', ');
170
+ const aiMcpRecommended = (aiAnalysis.mcpServers?.recommended || []).join(', ');
171
+
172
+ // Missed technologies
173
+ const aiMissedTechnologies = (aiAnalysis.possibleMissedTechnologies || []).join(', ');
174
+
175
+ return {
176
+ hasAiAnalysis: 'true',
177
+ aiEntryPoints,
178
+ aiKeyDirectories,
179
+ aiNamingConventions,
180
+ aiImplementationGuidelines,
181
+ aiMcpEssential,
182
+ aiMcpRecommended,
183
+ aiMissedTechnologies,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Extract AI-detected commands or fall back to derived commands
189
+ */
190
+ function getCommands(
191
+ scanResult: ScanResult,
192
+ packageManager: string
193
+ ): Pick<TemplateVariables, 'devCommand' | 'buildCommand' | 'testCommand' | 'lintCommand' | 'typecheckCommand' | 'formatCommand'> {
194
+ // Check for AI-detected commands
195
+ const aiCommands = (scanResult as { aiAnalysis?: { commands?: Record<string, string> } }).aiAnalysis?.commands;
196
+
197
+ // Derive default commands from package manager
198
+ const derived = deriveCommands(packageManager);
199
+
200
+ if (!aiCommands) {
201
+ return derived;
202
+ }
203
+
204
+ // Use AI-detected commands where available, fall back to derived
205
+ return {
206
+ devCommand: aiCommands.dev || derived.devCommand,
207
+ buildCommand: aiCommands.build || derived.buildCommand,
208
+ testCommand: aiCommands.test || derived.testCommand,
209
+ lintCommand: aiCommands.lint || derived.lintCommand,
210
+ typecheckCommand: aiCommands.typecheck || derived.typecheckCommand,
211
+ formatCommand: aiCommands.format || derived.formatCommand,
212
+ };
213
+ }
214
+
96
215
  /**
97
216
  * Extract template variables from scan result
98
217
  */
@@ -122,11 +241,24 @@ export function extractVariables(scanResult: ScanResult, customVars: Record<stri
122
241
  const stylingVersion = stack.styling?.version || DEFAULT_VARIABLES.stylingVersion!;
123
242
  const stylingVariant = stack.styling?.variant || DEFAULT_VARIABLES.stylingVariant!;
124
243
 
125
- // Derive commands
126
- const commands = deriveCommands(packageManager);
127
-
128
- // Determine app directory
129
- const appDir = frameworkVariant === 'app-router' || framework.toLowerCase().includes('next') ? 'app' : 'src';
244
+ // Get commands (AI-detected or derived)
245
+ const commands = getCommands(scanResult, packageManager);
246
+
247
+ // Extract AI analysis data
248
+ const aiData = formatAiAnalysisForTemplates(scanResult);
249
+
250
+ // Determine app directory - use AI entry points if available
251
+ let appDir = 'src';
252
+ if (frameworkVariant === 'app-router' || framework.toLowerCase().includes('next')) {
253
+ appDir = 'app';
254
+ } else if (aiData.aiEntryPoints) {
255
+ // Try to extract common directory from entry points
256
+ const entryPoints = (scanResult as { aiAnalysis?: { projectContext?: { entryPoints?: string[] } } })
257
+ .aiAnalysis?.projectContext?.entryPoints || [];
258
+ if (entryPoints.length > 0 && entryPoints[0].startsWith('src/')) {
259
+ appDir = 'src';
260
+ }
261
+ }
130
262
 
131
263
  return {
132
264
  projectName,
@@ -145,6 +277,7 @@ export function extractVariables(scanResult: ScanResult, customVars: Record<stri
145
277
  stylingVariant,
146
278
  appDir,
147
279
  ...commands,
280
+ ...aiData,
148
281
  ...customVars,
149
282
  };
150
283
  }
@@ -3,7 +3,20 @@
3
3
  ## Stack
4
4
  {{framework}}{{#if frameworkVersion}} {{frameworkVersion}}{{/if}}{{#if frameworkVariant}} ({{frameworkVariant}}){{/if}}, TypeScript, {{styling || css}}{{#if unitTest}}, {{unitTest}}{{/if}}{{#if e2eTest}}, {{e2eTest}}{{/if}}
5
5
 
6
+ {{#if aiEntryPoints}}## Entry Points
7
+ {{aiEntryPoints}}
8
+
9
+ {{/if}}
6
10
  ## Commands
11
+ {{#if hasAiAnalysis}}
12
+ | Action | Command |
13
+ |--------|---------|
14
+ | Dev | `{{devCommand}}` |
15
+ | Build | `{{buildCommand}}` |
16
+ | Test | `{{testCommand}}` |
17
+ | Lint | `{{lintCommand}}` |
18
+ | Typecheck | `{{typecheckCommand}}` |
19
+ {{else}}
7
20
  All commands run from `{{appDir}}/` directory:
8
21
 
9
22
  | Action | Command |
@@ -16,19 +29,20 @@ All commands run from `{{appDir}}/` directory:
16
29
  | Typecheck | `cd {{appDir}} && {{typecheckCommand}}` |
17
30
  {{#if unitTest}}| Test | `cd {{appDir}} && {{testCommand}}` |
18
31
  | Test watch | `cd {{appDir}} && {{testCommand}} -- --watch` |{{/if}}
32
+ {{/if}}
19
33
 
20
34
  ## Validation (run before commit)
21
35
  ```bash
22
- cd {{appDir}} && {{lintCommand}} -- --fix && {{typecheckCommand}}{{#if unitTest}} && {{testCommand}}{{/if}} && {{buildCommand}}
36
+ {{lintCommand}} && {{typecheckCommand}}{{#if unitTest}} && {{testCommand}}{{/if}} && {{buildCommand}}
23
37
  ```
24
38
 
25
- ## Security Review (run before commit)
26
- ```bash
27
- cd {{appDir}} && {{packageManager}} audit # Check vulnerable dependencies
28
- ```
29
- Then review changes against @.ralph/guides/SECURITY.md checklist.
30
- Use `mcp__supabase__get_advisors` with type "security" to check RLS policies.
39
+ {{#if aiKeyDirectories}}## Project Structure
31
40
 
41
+ | Directory | Purpose |
42
+ |-----------|---------|
43
+ {{aiKeyDirectories}}
44
+
45
+ {{else}}
32
46
  ## Patterns
33
47
  - Pages: `{{appDir}}/app/` - Next.js App Router
34
48
  - Components: `{{appDir}}/components/` - see ui/, ai-elements/, survey-editor/
@@ -36,27 +50,30 @@ Use `mcp__supabase__get_advisors` with type "security" to check RLS policies.
36
50
  - Store: `{{appDir}}/store/` - State management
37
51
  - Server Actions: `{{appDir}}/app/actions.ts` and `{{appDir}}/app/actions/`
38
52
  - Libs: `{{appDir}}/lib/` - utilities and integrations
53
+ {{/if}}
54
+
55
+ {{#if aiImplementationGuidelines}}## Implementation Guidelines
56
+ {{aiImplementationGuidelines}}
39
57
 
58
+ {{/if}}
40
59
  ## Search Tools
41
60
  - **Codebase search**: Use `mgrep "query"` (preferred over Grep/Glob)
42
61
  - **Documentation lookup**: Use Context7 MCP for library/framework docs
43
62
  - **Web search**: Use `mgrep --web "query"` for general web lookups
44
63
 
45
64
  ## Code Rules
46
- - Explicit return types on functions
65
+ {{#if aiNamingConventions}}- Naming: {{aiNamingConventions}}
66
+ {{/if}}- Explicit return types on functions
47
67
  - No `any` - use `unknown` if truly unknown
48
68
  - Early returns over nested conditionals
49
69
  - Named exports, not default exports
50
70
  - Path alias `@/` maps to `{{appDir}}/` root
51
- - Follow patterns in existing components
71
+ - Follow patterns in existing code
52
72
  - 2-space indentation, single quotes
53
- - PascalCase for components, `use` prefix for hooks
54
- - kebab-case for folder names (e.g., `survey-editor`)
55
73
 
56
74
  {{#if unitTest}}## Testing
57
75
  - Framework: {{unitTest}}{{#if unitTestVersion}} {{unitTestVersion}}{{/if}}
58
- - Test location: next to component as `*.test.tsx` or in `__tests__/`
59
- - Run: `cd {{appDir}} && {{testCommand}}`
76
+ - Run: `{{testCommand}}`
60
77
  - Each feature task should include tests
61
78
  - Validation fails if tests fail
62
79
  {{/if}}
@@ -71,30 +88,30 @@ Use `mcp__supabase__get_advisors` with type "security" to check RLS policies.
71
88
  - After changes: validate, commit, push
72
89
 
73
90
  ## MCP Servers Available
74
- - **Supabase**: Query DB, manage migrations, generate types
91
+ {{#if aiMcpEssential}}- **Essential**: {{aiMcpEssential}}
92
+ {{/if}}{{#if aiMcpRecommended}}- **Recommended**: {{aiMcpRecommended}}
93
+ {{/if}}- **Supabase**: Query DB, manage migrations, generate types
75
94
  - **PostHog**: Analytics, feature flags, experiments
76
95
  - **Playwright**: Browser automation for E2E testing
77
96
  - Navigate: `browser_navigate`, `browser_snapshot`
78
97
  - Interact: `browser_click`, `browser_type`, `browser_fill_form`
79
98
  - Verify: `browser_wait_for`, `browser_console_messages`
80
99
  - Debug: `browser_take_screenshot`
81
- - **Codex CLI** (shell command, not MCP): PR review
82
- - Command: `echo "prompt" | codex exec --full-auto -`
83
- - For reviews: `codex review --base main` (interactive mode)
84
- - Location: `/opt/homebrew/bin/codex`
85
- - Use for: code review, PR feedback before merge
86
100
  - **Context7**: Documentation lookup for libraries/frameworks
87
101
 
102
+ {{#if aiMissedTechnologies}}## May Also Use
103
+ {{aiMissedTechnologies}}
104
+
105
+ {{/if}}
88
106
  ## Quick Triggers
89
107
  | Keywords | Action |
90
108
  |----------|--------|
91
109
  | type error, ts | Run typecheck |
92
- | lint, format | Run lint --fix and prettier |
110
+ | lint, format | Run lint --fix |
93
111
  | deploy, build | Run full build |
94
- | error, debug | Check PostHog logs or console |
112
+ | error, debug | Check logs or console |
95
113
  | database, table | Use Supabase MCP |
96
114
  | e2e, browser test | Use Playwright MCP |
97
115
 
98
116
  ## Detailed Docs
99
- - Architecture: `{{appDir}}/.claude/CLAUDE.md` - comprehensive codebase details
100
117
  - Frontend: `.ralph/guides/FRONTEND.md` - design patterns, component usage, quality checklist