codebakers 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,989 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import * as fs from 'fs-extra';
4
+ import * as path from 'path';
5
+ import Anthropic from '@anthropic-ai/sdk';
6
+ import { execa } from 'execa';
7
+ import { Config } from '../utils/config.js';
8
+ import { EventEmitter } from 'events';
9
+
10
+ // ============================================================================
11
+ // TYPES
12
+ // ============================================================================
13
+
14
+ interface AgentTask {
15
+ id: string;
16
+ name: string;
17
+ description: string;
18
+ folder: string;
19
+ dependencies: string[];
20
+ exports: string[];
21
+ status: 'waiting' | 'running' | 'done' | 'error' | 'healing' | 'asking';
22
+ progress: number;
23
+ currentAction: string;
24
+ branch: string;
25
+ files: string[];
26
+ error?: string;
27
+ healAttempts: number;
28
+ }
29
+
30
+ interface BuildWave {
31
+ wave: number;
32
+ agents: AgentTask[];
33
+ parallel: boolean;
34
+ }
35
+
36
+ interface BuildPlan {
37
+ projectName: string;
38
+ description: string;
39
+ sharedSetup: {
40
+ types: string[];
41
+ components: string[];
42
+ conventions: string;
43
+ };
44
+ waves: BuildWave[];
45
+ }
46
+
47
+ interface HealerSolution {
48
+ canFix: boolean;
49
+ solution: string;
50
+ type: 'retry' | 'modify-code' | 'wait' | 'skip-optional' | 'generate-missing';
51
+ newCode?: string;
52
+ waitTime?: number;
53
+ description: string;
54
+ }
55
+
56
+ interface UserQuestion {
57
+ question: string;
58
+ options: string[];
59
+ context?: string;
60
+ }
61
+
62
+ // Global flag to pause/resume display during questions
63
+ let displayPaused = false;
64
+
65
+ // ============================================================================
66
+ // MAIN BUILD COMMAND
67
+ // ============================================================================
68
+
69
+ export async function buildCommand(prdPath?: string, options: { sequential?: boolean } = {}): Promise<void> {
70
+ const config = new Config();
71
+
72
+ if (!config.isConfigured()) {
73
+ p.log.error('Please run `codebakers setup` first.');
74
+ return;
75
+ }
76
+
77
+ const anthropicCreds = config.getCredentials('anthropic');
78
+ if (!anthropicCreds?.apiKey) {
79
+ p.log.error('Anthropic API key not configured.');
80
+ return;
81
+ }
82
+
83
+ // Get PRD file
84
+ let prdFile = prdPath;
85
+ if (!prdFile) {
86
+ const file = await p.text({
87
+ message: 'Path to PRD file:',
88
+ placeholder: './my-app-prd.md',
89
+ validate: (v) => !v ? 'PRD file required' : undefined,
90
+ });
91
+ if (p.isCancel(file)) return;
92
+ prdFile = file as string;
93
+ }
94
+
95
+ // Read PRD
96
+ if (!await fs.pathExists(prdFile)) {
97
+ p.log.error(`PRD file not found: ${prdFile}`);
98
+ return;
99
+ }
100
+
101
+ const prdContent = await fs.readFile(prdFile, 'utf-8');
102
+
103
+ console.log(chalk.cyan(`
104
+ ╔═══════════════════════════════════════════════════════════════╗
105
+ ║ 🚀 CODEBAKERS PARALLEL BUILD ║
106
+ ╚═══════════════════════════════════════════════════════════════╝
107
+ `));
108
+
109
+ const anthropic = new Anthropic({ apiKey: anthropicCreds.apiKey });
110
+
111
+ // Step 1: Analyze PRD and create build plan
112
+ const spinner = p.spinner();
113
+ spinner.start('Analyzing PRD...');
114
+
115
+ const buildPlan = await analyzePRD(anthropic, prdContent);
116
+
117
+ spinner.stop('PRD analyzed');
118
+
119
+ // Show build plan
120
+ displayBuildPlan(buildPlan);
121
+
122
+ // Check if parallel makes sense
123
+ const totalAgents = buildPlan.waves.reduce((sum, w) => sum + w.agents.length, 0);
124
+ const useParallel = !options.sequential && totalAgents > 2;
125
+
126
+ if (!useParallel) {
127
+ console.log(chalk.dim('\nUsing sequential build (parallel not needed for small projects)\n'));
128
+ }
129
+
130
+ // Confirm
131
+ const proceed = await p.confirm({
132
+ message: `Start ${useParallel ? 'parallel' : 'sequential'} build?`,
133
+ initialValue: true,
134
+ });
135
+
136
+ if (!proceed || p.isCancel(proceed)) {
137
+ p.cancel('Build cancelled');
138
+ return;
139
+ }
140
+
141
+ // Step 2: Create project directory
142
+ const projectPath = path.join(process.cwd(), buildPlan.projectName);
143
+
144
+ if (await fs.pathExists(projectPath)) {
145
+ const overwrite = await p.confirm({
146
+ message: `${buildPlan.projectName} already exists. Overwrite?`,
147
+ initialValue: false,
148
+ });
149
+ if (!overwrite || p.isCancel(overwrite)) return;
150
+ await fs.remove(projectPath);
151
+ }
152
+
153
+ await fs.ensureDir(projectPath);
154
+ process.chdir(projectPath);
155
+
156
+ // Step 3: Initialize git
157
+ spinner.start('Initializing project...');
158
+ await execa('git', ['init'], { cwd: projectPath });
159
+ spinner.stop('Project initialized');
160
+
161
+ // Step 4: Run setup phase (shared code)
162
+ await runSetupPhase(anthropic, buildPlan, projectPath);
163
+
164
+ // Step 5: Execute build waves
165
+ const startTime = Date.now();
166
+
167
+ if (useParallel) {
168
+ await executeParallelBuild(anthropic, buildPlan, projectPath, config);
169
+ } else {
170
+ await executeSequentialBuild(anthropic, buildPlan, projectPath, config);
171
+ }
172
+
173
+ // Step 6: Integration phase
174
+ await runIntegrationPhase(anthropic, buildPlan, projectPath);
175
+
176
+ // Step 7: Final setup
177
+ spinner.start('Installing dependencies...');
178
+ await execa('npm', ['install'], { cwd: projectPath, reject: false });
179
+ spinner.stop('Dependencies installed');
180
+
181
+ // Done!
182
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
183
+ const minutes = Math.floor(elapsed / 60);
184
+ const seconds = elapsed % 60;
185
+
186
+ console.log(chalk.green(`
187
+ ╔═══════════════════════════════════════════════════════════════╗
188
+ ║ ✅ BUILD COMPLETE ║
189
+ ╠═══════════════════════════════════════════════════════════════╣
190
+ ║ ║
191
+ ║ Project: ${buildPlan.projectName.padEnd(46)}║
192
+ ║ Time: ${(minutes + 'm ' + seconds + 's').padEnd(46)}║
193
+ ║ Features: ${(buildPlan.waves.reduce((s, w) => s + w.agents.length, 0) + ' built').padEnd(46)}║
194
+ ║ ║
195
+ ║ Next steps: ║
196
+ ║ cd ${buildPlan.projectName.padEnd(52)}║
197
+ ║ npm run dev ║
198
+ ║ ║
199
+ ╚═══════════════════════════════════════════════════════════════╝
200
+ `));
201
+ }
202
+
203
+ // ============================================================================
204
+ // PRD ANALYZER
205
+ // ============================================================================
206
+
207
+ async function analyzePRD(anthropic: Anthropic, prdContent: string): Promise<BuildPlan> {
208
+ const response = await anthropic.messages.create({
209
+ model: 'claude-sonnet-4-20250514',
210
+ max_tokens: 4096,
211
+ messages: [{
212
+ role: 'user',
213
+ content: `Analyze this PRD and create a build plan with parallel-safe task assignment.
214
+
215
+ PRD:
216
+ ${prdContent}
217
+
218
+ Rules:
219
+ 1. Each feature gets its own folder (src/features/[name])
220
+ 2. Features with no dependencies can run in parallel
221
+ 3. Features that depend on others must wait
222
+ 4. Group into "waves" - each wave runs after previous completes
223
+ 5. Maximum 3 agents per wave
224
+
225
+ Return JSON only:
226
+ {
227
+ "projectName": "kebab-case-name",
228
+ "description": "one line description",
229
+ "sharedSetup": {
230
+ "types": ["User", "Invoice", etc - shared types needed],
231
+ "components": ["Button", "Card", etc - shared UI needed],
232
+ "conventions": "Next.js App Router, TypeScript, Tailwind, shadcn/ui"
233
+ },
234
+ "waves": [
235
+ {
236
+ "wave": 1,
237
+ "parallel": true,
238
+ "agents": [
239
+ {
240
+ "id": "auth",
241
+ "name": "Authentication",
242
+ "description": "Login, signup, session management",
243
+ "folder": "src/features/auth",
244
+ "dependencies": [],
245
+ "exports": ["AuthProvider", "useAuth", "LoginForm"]
246
+ }
247
+ ]
248
+ }
249
+ ]
250
+ }
251
+
252
+ Think about:
253
+ - What MUST come first (auth, database setup)?
254
+ - What can run at the same time (independent features)?
255
+ - What needs data from other features (dashboards, reports)?`
256
+ }],
257
+ });
258
+
259
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
260
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
261
+
262
+ if (!jsonMatch) {
263
+ throw new Error('Failed to parse PRD analysis');
264
+ }
265
+
266
+ const plan = JSON.parse(jsonMatch[0]) as BuildPlan;
267
+
268
+ // Initialize agent task fields
269
+ for (const wave of plan.waves) {
270
+ for (const agent of wave.agents) {
271
+ (agent as AgentTask).status = 'waiting';
272
+ (agent as AgentTask).progress = 0;
273
+ (agent as AgentTask).currentAction = '';
274
+ (agent as AgentTask).branch = `codebakers/${agent.id}`;
275
+ (agent as AgentTask).files = [];
276
+ (agent as AgentTask).healAttempts = 0;
277
+ }
278
+ }
279
+
280
+ return plan;
281
+ }
282
+
283
+ // ============================================================================
284
+ // DISPLAY
285
+ // ============================================================================
286
+
287
+ function displayBuildPlan(plan: BuildPlan): void {
288
+ console.log(chalk.bold(`\n📋 Build Plan: ${plan.projectName}\n`));
289
+ console.log(chalk.dim(` ${plan.description}\n`));
290
+
291
+ for (const wave of plan.waves) {
292
+ const parallel = wave.parallel && wave.agents.length > 1;
293
+ const mode = parallel ? chalk.green('parallel') : chalk.yellow('sequential');
294
+
295
+ console.log(chalk.bold(` Wave ${wave.wave} (${mode}):`));
296
+
297
+ for (const agent of wave.agents) {
298
+ const deps = agent.dependencies.length > 0
299
+ ? chalk.dim(` ← needs: ${agent.dependencies.join(', ')}`)
300
+ : '';
301
+ console.log(` • ${agent.name} ${deps}`);
302
+ }
303
+ console.log('');
304
+ }
305
+
306
+ const totalAgents = plan.waves.reduce((sum, w) => sum + w.agents.length, 0);
307
+ const parallelWaves = plan.waves.filter(w => w.parallel && w.agents.length > 1).length;
308
+
309
+ console.log(chalk.dim(` Total: ${totalAgents} features, ${plan.waves.length} waves, ${parallelWaves} parallel\n`));
310
+ }
311
+
312
+ // ============================================================================
313
+ // SETUP PHASE
314
+ // ============================================================================
315
+
316
+ async function runSetupPhase(
317
+ anthropic: Anthropic,
318
+ plan: BuildPlan,
319
+ projectPath: string
320
+ ): Promise<void> {
321
+ const spinner = p.spinner();
322
+ spinner.start('Setting up project structure...');
323
+
324
+ // Create base structure
325
+ await fs.ensureDir(path.join(projectPath, 'src/app'));
326
+ await fs.ensureDir(path.join(projectPath, 'src/components/ui'));
327
+ await fs.ensureDir(path.join(projectPath, 'src/lib'));
328
+ await fs.ensureDir(path.join(projectPath, 'src/types'));
329
+ await fs.ensureDir(path.join(projectPath, 'src/features'));
330
+ await fs.ensureDir(path.join(projectPath, '.codebakers'));
331
+
332
+ // Generate shared setup code
333
+ const setupResponse = await anthropic.messages.create({
334
+ model: 'claude-sonnet-4-20250514',
335
+ max_tokens: 8192,
336
+ messages: [{
337
+ role: 'user',
338
+ content: `Generate the shared setup files for this project.
339
+
340
+ Project: ${plan.projectName}
341
+ Description: ${plan.description}
342
+ Conventions: ${plan.sharedSetup.conventions}
343
+ Shared Types: ${plan.sharedSetup.types.join(', ')}
344
+ Shared Components: ${plan.sharedSetup.components.join(', ')}
345
+
346
+ Generate these files:
347
+
348
+ 1. package.json (Next.js 14, TypeScript, Tailwind, shadcn/ui)
349
+ 2. tsconfig.json
350
+ 3. tailwind.config.ts
351
+ 4. src/lib/utils.ts (cn helper)
352
+ 5. src/types/index.ts (shared types)
353
+ 6. src/app/layout.tsx (basic layout)
354
+ 7. src/app/page.tsx (simple home page placeholder)
355
+ 8. .env.example
356
+ 9. CLAUDE.md (CodeBakers patterns)
357
+
358
+ Output format:
359
+ <<<FILE: path/to/file>>>
360
+ content
361
+ <<<END_FILE>>>
362
+
363
+ Use TypeScript. Include all necessary imports.`
364
+ }],
365
+ });
366
+
367
+ const setupText = setupResponse.content[0].type === 'text' ? setupResponse.content[0].text : '';
368
+ await writeFilesFromResponse(setupText, projectPath);
369
+
370
+ // Initial git commit
371
+ await execa('git', ['add', '.'], { cwd: projectPath });
372
+ await execa('git', ['commit', '-m', 'Initial setup'], { cwd: projectPath });
373
+
374
+ spinner.stop('Project structure ready');
375
+ }
376
+
377
+ // ============================================================================
378
+ // PARALLEL BUILD EXECUTION
379
+ // ============================================================================
380
+
381
+ async function executeParallelBuild(
382
+ anthropic: Anthropic,
383
+ plan: BuildPlan,
384
+ projectPath: string,
385
+ config: Config
386
+ ): Promise<void> {
387
+ for (const wave of plan.waves) {
388
+ console.log(chalk.bold(`\n🏗️ Wave ${wave.wave} of ${plan.waves.length}`));
389
+
390
+ if (wave.parallel && wave.agents.length > 1) {
391
+ console.log(chalk.dim(` Running ${wave.agents.length} agents in parallel\n`));
392
+ await executeWaveParallel(anthropic, wave.agents as AgentTask[], projectPath, plan);
393
+ } else {
394
+ console.log(chalk.dim(` Running sequentially\n`));
395
+ for (const agent of wave.agents) {
396
+ await executeAgent(anthropic, agent as AgentTask, projectPath, plan);
397
+ }
398
+ }
399
+
400
+ // Merge all branches from this wave
401
+ await mergeWaveBranches(wave.agents as AgentTask[], projectPath);
402
+ }
403
+ }
404
+
405
+ async function executeWaveParallel(
406
+ anthropic: Anthropic,
407
+ agents: AgentTask[],
408
+ projectPath: string,
409
+ plan: BuildPlan
410
+ ): Promise<void> {
411
+ // Create progress display
412
+ const display = new ProgressDisplay(agents);
413
+ display.start();
414
+
415
+ // Run agents in parallel
416
+ const promises = agents.map(agent =>
417
+ executeAgentWithProgress(anthropic, agent, projectPath, plan, display)
418
+ );
419
+
420
+ await Promise.all(promises);
421
+
422
+ display.stop();
423
+ }
424
+
425
+ async function executeAgentWithProgress(
426
+ anthropic: Anthropic,
427
+ agent: AgentTask,
428
+ projectPath: string,
429
+ plan: BuildPlan,
430
+ display: ProgressDisplay
431
+ ): Promise<void> {
432
+ agent.status = 'running';
433
+ display.update();
434
+
435
+ try {
436
+ await executeAgent(anthropic, agent, projectPath, plan, (progress, action) => {
437
+ agent.progress = progress;
438
+ agent.currentAction = action;
439
+ display.update();
440
+ });
441
+
442
+ agent.status = 'done';
443
+ agent.progress = 100;
444
+ display.update();
445
+ } catch (error) {
446
+ agent.status = 'error';
447
+ agent.error = error instanceof Error ? error.message : 'Unknown error';
448
+ display.update();
449
+ }
450
+ }
451
+
452
+ // ============================================================================
453
+ // SEQUENTIAL BUILD EXECUTION
454
+ // ============================================================================
455
+
456
+ async function executeSequentialBuild(
457
+ anthropic: Anthropic,
458
+ plan: BuildPlan,
459
+ projectPath: string,
460
+ config: Config
461
+ ): Promise<void> {
462
+ for (const wave of plan.waves) {
463
+ for (const agent of wave.agents) {
464
+ const spinner = p.spinner();
465
+ spinner.start(`Building ${agent.name}...`);
466
+
467
+ try {
468
+ await executeAgent(anthropic, agent as AgentTask, projectPath, plan, (progress, action) => {
469
+ spinner.message = `${agent.name}: ${action} (${progress}%)`;
470
+ });
471
+ spinner.stop(`✓ ${agent.name} complete`);
472
+ } catch (error) {
473
+ spinner.stop(`✗ ${agent.name} failed`);
474
+ throw error;
475
+ }
476
+
477
+ // Merge branch
478
+ await mergeWaveBranches([agent as AgentTask], projectPath);
479
+ }
480
+ }
481
+ }
482
+
483
+ // ============================================================================
484
+ // AGENT EXECUTION WITH SELF-HEALING
485
+ // ============================================================================
486
+
487
+ async function executeAgent(
488
+ anthropic: Anthropic,
489
+ agent: AgentTask,
490
+ projectPath: string,
491
+ plan: BuildPlan,
492
+ onProgress?: (progress: number, action: string) => void
493
+ ): Promise<void> {
494
+ const maxAttempts = 3;
495
+
496
+ // Create branch for this agent
497
+ await execa('git', ['checkout', '-b', agent.branch], { cwd: projectPath });
498
+
499
+ while (agent.healAttempts < maxAttempts) {
500
+ try {
501
+ await runAgentTask(anthropic, agent, projectPath, plan, onProgress);
502
+
503
+ // Commit changes
504
+ await execa('git', ['add', '.'], { cwd: projectPath });
505
+ await execa('git', ['commit', '-m', `feat: ${agent.name}`], { cwd: projectPath, reject: false });
506
+
507
+ // Switch back to main
508
+ await execa('git', ['checkout', 'main'], { cwd: projectPath });
509
+
510
+ return; // Success!
511
+
512
+ } catch (error) {
513
+ agent.healAttempts++;
514
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
515
+
516
+ if (agent.healAttempts >= maxAttempts) {
517
+ // Switch back to main before throwing
518
+ await execa('git', ['checkout', 'main'], { cwd: projectPath, reject: false });
519
+ throw new Error(`Agent ${agent.name} failed after ${maxAttempts} attempts: ${errorMsg}`);
520
+ }
521
+
522
+ // Try to heal
523
+ agent.status = 'healing';
524
+ if (onProgress) onProgress(agent.progress, 'Self-healing...');
525
+
526
+ const solution = await healerAgent(anthropic, agent, errorMsg, projectPath, plan);
527
+
528
+ if (solution.canFix) {
529
+ if (onProgress) onProgress(agent.progress, `Healing: ${solution.description}`);
530
+
531
+ if (solution.type === 'wait' && solution.waitTime) {
532
+ await sleep(solution.waitTime);
533
+ } else if (solution.type === 'modify-code' && solution.newCode) {
534
+ // Apply the fix
535
+ await writeFilesFromResponse(solution.newCode, projectPath);
536
+ }
537
+
538
+ agent.status = 'running';
539
+ // Loop continues with fix applied
540
+ } else {
541
+ // Healer can't fix, switch back and throw
542
+ await execa('git', ['checkout', 'main'], { cwd: projectPath, reject: false });
543
+ throw new Error(`Agent ${agent.name} failed and could not self-heal: ${errorMsg}`);
544
+ }
545
+ }
546
+ }
547
+ }
548
+
549
+ async function runAgentTask(
550
+ anthropic: Anthropic,
551
+ agent: AgentTask,
552
+ projectPath: string,
553
+ plan: BuildPlan,
554
+ onProgress?: (progress: number, action: string) => void
555
+ ): Promise<void> {
556
+ if (onProgress) onProgress(10, 'Analyzing requirements...');
557
+
558
+ // Create feature folder
559
+ const featurePath = path.join(projectPath, agent.folder);
560
+ await fs.ensureDir(featurePath);
561
+
562
+ if (onProgress) onProgress(20, 'Generating code...');
563
+
564
+ // Conversation loop to handle questions
565
+ const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [];
566
+ let userAnswers: Record<string, string> = {};
567
+ let iteration = 0;
568
+ const maxIterations = 5; // Prevent infinite loops
569
+
570
+ // Initial prompt
571
+ messages.push({
572
+ role: 'user',
573
+ content: `You are Agent "${agent.id}" building the "${agent.name}" feature.
574
+
575
+ Feature: ${agent.name}
576
+ Description: ${agent.description}
577
+ Your folder: ${agent.folder}
578
+ Exports needed: ${agent.exports.join(', ')}
579
+
580
+ Project context:
581
+ - Project: ${plan.projectName}
582
+ - Conventions: ${plan.sharedSetup.conventions}
583
+ - Shared types available: ${plan.sharedSetup.types.join(', ')}
584
+
585
+ Rules:
586
+ 1. ONLY create files in your folder: ${agent.folder}/
587
+ 2. Export everything from ${agent.folder}/index.ts
588
+ 3. Import shared types from @/types
589
+ 4. Import shared components from @/components/ui
590
+ 5. Use TypeScript, proper error handling, loading states
591
+ 6. Every form needs Zod validation
592
+ 7. Every async operation needs try/catch
593
+ 8. Every list needs empty state
594
+
595
+ IMPORTANT: If you need to make a significant decision (auth provider, database choice,
596
+ styling approach, etc.) and it's not specified, ASK THE USER first:
597
+
598
+ <<<ASK_USER>>>
599
+ question: Your question here?
600
+ options: Option 1, Option 2, Option 3
601
+ context: Brief context for why you're asking
602
+ <<<END_ASK>>>
603
+
604
+ Only ask for IMPORTANT decisions, not minor implementation details.
605
+
606
+ Generate ALL files needed for this feature.
607
+
608
+ Output format:
609
+ <<<FILE: ${agent.folder}/path/to/file.ts>>>
610
+ content
611
+ <<<END_FILE>>>
612
+
613
+ Start with the index.ts that exports everything.`
614
+ });
615
+
616
+ while (iteration < maxIterations) {
617
+ iteration++;
618
+
619
+ const response = await anthropic.messages.create({
620
+ model: 'claude-sonnet-4-20250514',
621
+ max_tokens: 8192,
622
+ messages,
623
+ });
624
+
625
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
626
+ messages.push({ role: 'assistant', content: text });
627
+
628
+ // Check if agent is asking a question
629
+ const questionMatch = text.match(/<<<ASK_USER>>>([\s\S]*?)<<<END_ASK>>>/);
630
+
631
+ if (questionMatch) {
632
+ // Parse the question
633
+ const questionBlock = questionMatch[1];
634
+ const questionLine = questionBlock.match(/question:\s*(.+)/);
635
+ const optionsLine = questionBlock.match(/options:\s*(.+)/);
636
+ const contextLine = questionBlock.match(/context:\s*(.+)/);
637
+
638
+ if (questionLine && optionsLine) {
639
+ const question = questionLine[1].trim();
640
+ const options = optionsLine[1].split(',').map(o => o.trim());
641
+ const context = contextLine ? contextLine[1].trim() : '';
642
+
643
+ // Ask the user
644
+ const answer = await askUserQuestion(agent, {
645
+ question,
646
+ options,
647
+ context,
648
+ }, onProgress);
649
+
650
+ if (answer) {
651
+ userAnswers[question] = answer;
652
+
653
+ // Send answer back to agent
654
+ messages.push({
655
+ role: 'user',
656
+ content: `User answered: "${answer}"
657
+
658
+ Now continue building the feature with this choice. Generate the code files.`
659
+ });
660
+
661
+ if (onProgress) onProgress(30 + (iteration * 10), 'Continuing with your choice...');
662
+ continue; // Loop to get the code
663
+ }
664
+ }
665
+ }
666
+
667
+ // No question found, check for files
668
+ if (onProgress) onProgress(60, 'Writing files...');
669
+
670
+ const files = await writeFilesFromResponse(text, projectPath);
671
+ agent.files = files;
672
+
673
+ if (onProgress) onProgress(80, 'Validating...');
674
+
675
+ // Basic validation - check if files were created
676
+ if (files.length === 0) {
677
+ throw new Error('No files generated');
678
+ }
679
+
680
+ // Check for index.ts
681
+ const hasIndex = files.some(f => f.endsWith('index.ts'));
682
+ if (!hasIndex) {
683
+ throw new Error('Missing index.ts export file');
684
+ }
685
+
686
+ if (onProgress) onProgress(100, 'Complete');
687
+ return; // Success!
688
+ }
689
+
690
+ throw new Error('Agent exceeded maximum iterations');
691
+ }
692
+
693
+ // ============================================================================
694
+ // ASK USER QUESTION
695
+ // ============================================================================
696
+
697
+ async function askUserQuestion(
698
+ agent: AgentTask,
699
+ question: UserQuestion,
700
+ onProgress?: (progress: number, action: string) => void
701
+ ): Promise<string | null> {
702
+ // Pause display and update status
703
+ displayPaused = true;
704
+ const previousStatus = agent.status;
705
+ agent.status = 'asking';
706
+
707
+ if (onProgress) onProgress(agent.progress, 'Waiting for your input...');
708
+
709
+ // Clear some space
710
+ console.log('\n');
711
+
712
+ // Show the question
713
+ console.log(chalk.cyan(` ┌─────────────────────────────────────────────────────────────`));
714
+ console.log(chalk.cyan(` │ 🤖 ${agent.name} needs your input`));
715
+ console.log(chalk.cyan(` └─────────────────────────────────────────────────────────────`));
716
+
717
+ if (question.context) {
718
+ console.log(chalk.dim(` ${question.context}\n`));
719
+ }
720
+
721
+ const answer = await p.select({
722
+ message: question.question,
723
+ options: question.options.map(o => ({ value: o, label: o })),
724
+ });
725
+
726
+ if (p.isCancel(answer)) {
727
+ // User cancelled - use first option as default
728
+ console.log(chalk.yellow(` Using default: ${question.options[0]}`));
729
+ agent.status = previousStatus;
730
+ displayPaused = false;
731
+ return question.options[0];
732
+ }
733
+
734
+ console.log(chalk.green(` ✓ Got it: ${answer}\n`));
735
+
736
+ // Resume
737
+ agent.status = previousStatus;
738
+ displayPaused = false;
739
+
740
+ return answer as string;
741
+ }
742
+
743
+ // ============================================================================
744
+ // HEALER AGENT
745
+ // ============================================================================
746
+
747
+ async function healerAgent(
748
+ anthropic: Anthropic,
749
+ agent: AgentTask,
750
+ error: string,
751
+ projectPath: string,
752
+ plan: BuildPlan
753
+ ): Promise<HealerSolution> {
754
+ const response = await anthropic.messages.create({
755
+ model: 'claude-sonnet-4-20250514',
756
+ max_tokens: 4096,
757
+ messages: [{
758
+ role: 'user',
759
+ content: `You are the Healer Agent. An agent failed and you need to diagnose and fix it.
760
+
761
+ Failed Agent: ${agent.name} (${agent.id})
762
+ Folder: ${agent.folder}
763
+ Error: ${error}
764
+ Attempt: ${agent.healAttempts + 1} of 3
765
+
766
+ Files created so far: ${agent.files.join(', ') || 'none'}
767
+
768
+ Project context:
769
+ - Other features: ${plan.waves.flatMap(w => w.agents.map(a => a.id)).join(', ')}
770
+ - Shared types: ${plan.sharedSetup.types.join(', ')}
771
+
772
+ Diagnose the error and provide a solution.
773
+
774
+ Return JSON only:
775
+ {
776
+ "canFix": true,
777
+ "solution": "explanation of what went wrong",
778
+ "type": "retry" | "modify-code" | "wait" | "skip-optional" | "generate-missing",
779
+ "description": "short description for UI",
780
+ "waitTime": 5000, // if type is "wait", milliseconds
781
+ "newCode": "<<<FILE: path>>>\\ncontent\\n<<<END_FILE>>>" // if type is "modify-code" or "generate-missing"
782
+ }
783
+
784
+ Common fixes:
785
+ - Missing import → generate the missing file
786
+ - Type error → fix the types
787
+ - Module not found → check path or generate stub
788
+ - Syntax error → fix the syntax
789
+ - Timeout → retry with simpler approach`
790
+ }],
791
+ });
792
+
793
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
794
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
795
+
796
+ if (!jsonMatch) {
797
+ return {
798
+ canFix: false,
799
+ solution: 'Could not parse healer response',
800
+ type: 'retry',
801
+ description: 'Unknown error',
802
+ };
803
+ }
804
+
805
+ return JSON.parse(jsonMatch[0]);
806
+ }
807
+
808
+ // ============================================================================
809
+ // INTEGRATION PHASE
810
+ // ============================================================================
811
+
812
+ async function runIntegrationPhase(
813
+ anthropic: Anthropic,
814
+ plan: BuildPlan,
815
+ projectPath: string
816
+ ): Promise<void> {
817
+ const spinner = p.spinner();
818
+ spinner.start('Running integration...');
819
+
820
+ // Get all feature exports
821
+ const features = plan.waves.flatMap(w => w.agents);
822
+
823
+ const response = await anthropic.messages.create({
824
+ model: 'claude-sonnet-4-20250514',
825
+ max_tokens: 8192,
826
+ messages: [{
827
+ role: 'user',
828
+ content: `You are the Integration Agent. Wire together all the features.
829
+
830
+ Features built:
831
+ ${features.map(f => `- ${f.name}: ${f.folder} exports [${f.exports.join(', ')}]`).join('\n')}
832
+
833
+ Generate:
834
+ 1. src/app/layout.tsx - Wire up providers (AuthProvider, etc.)
835
+ 2. src/app/page.tsx - Main dashboard/home linking to features
836
+ 3. src/app/[feature]/page.tsx - Route for each feature
837
+ 4. src/components/Navigation.tsx - Nav linking all features
838
+ 5. src/types/index.ts - Re-export all feature types
839
+
840
+ Output format:
841
+ <<<FILE: path>>>
842
+ content
843
+ <<<END_FILE>>>
844
+
845
+ Make sure all features are accessible and properly connected.`
846
+ }],
847
+ });
848
+
849
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
850
+ await writeFilesFromResponse(text, projectPath);
851
+
852
+ // Final commit
853
+ await execa('git', ['add', '.'], { cwd: projectPath });
854
+ await execa('git', ['commit', '-m', 'Integration: wire up all features'], { cwd: projectPath, reject: false });
855
+
856
+ spinner.stop('Integration complete');
857
+ }
858
+
859
+ // ============================================================================
860
+ // GIT HELPERS
861
+ // ============================================================================
862
+
863
+ async function mergeWaveBranches(agents: AgentTask[], projectPath: string): Promise<void> {
864
+ for (const agent of agents) {
865
+ if (agent.status === 'done') {
866
+ try {
867
+ await execa('git', ['merge', agent.branch, '--no-edit'], { cwd: projectPath });
868
+ await execa('git', ['branch', '-d', agent.branch], { cwd: projectPath, reject: false });
869
+ } catch (error) {
870
+ // Merge conflict - shouldn't happen with isolated folders
871
+ console.log(chalk.yellow(` Warning: Merge issue with ${agent.name}, continuing...`));
872
+ await execa('git', ['merge', '--abort'], { cwd: projectPath, reject: false });
873
+ }
874
+ }
875
+ }
876
+ }
877
+
878
+ // ============================================================================
879
+ // PROGRESS DISPLAY
880
+ // ============================================================================
881
+
882
+ class ProgressDisplay {
883
+ private agents: AgentTask[];
884
+ private interval: ReturnType<typeof setInterval> | null = null;
885
+ private lastLineCount: number = 0;
886
+
887
+ constructor(agents: AgentTask[]) {
888
+ this.agents = agents;
889
+ }
890
+
891
+ start(): void {
892
+ this.render();
893
+ this.interval = setInterval(() => {
894
+ if (!displayPaused) {
895
+ this.render();
896
+ }
897
+ }, 500);
898
+ }
899
+
900
+ stop(): void {
901
+ if (this.interval) {
902
+ clearInterval(this.interval);
903
+ this.interval = null;
904
+ }
905
+ if (!displayPaused) {
906
+ this.render();
907
+ }
908
+ console.log('');
909
+ }
910
+
911
+ update(): void {
912
+ // Will be rendered on next interval
913
+ }
914
+
915
+ pause(): void {
916
+ displayPaused = true;
917
+ }
918
+
919
+ resume(): void {
920
+ displayPaused = false;
921
+ // Re-render after question is answered
922
+ console.log(''); // Add spacing
923
+ this.render();
924
+ }
925
+
926
+ private render(): void {
927
+ if (displayPaused) return;
928
+
929
+ // Move cursor up and clear previous render
930
+ if (this.lastLineCount > 0) {
931
+ process.stdout.write(`\x1b[${this.lastLineCount}A\x1b[0J`);
932
+ }
933
+
934
+ for (const agent of this.agents) {
935
+ const icon = this.getStatusIcon(agent.status);
936
+ const bar = this.getProgressBar(agent.progress);
937
+ const action = agent.currentAction ? chalk.dim(` ${agent.currentAction}`) : '';
938
+
939
+ console.log(` ${icon} ${agent.name.padEnd(15)} ${bar} ${agent.progress.toString().padStart(3)}%${action}`);
940
+ }
941
+ console.log('');
942
+
943
+ this.lastLineCount = this.agents.length + 1;
944
+ }
945
+
946
+ private getStatusIcon(status: AgentTask['status']): string {
947
+ switch (status) {
948
+ case 'waiting': return chalk.gray('○');
949
+ case 'running': return chalk.blue('●');
950
+ case 'healing': return chalk.yellow('⚕');
951
+ case 'asking': return chalk.magenta('?');
952
+ case 'done': return chalk.green('✓');
953
+ case 'error': return chalk.red('✗');
954
+ default: return '?';
955
+ }
956
+ }
957
+
958
+ private getProgressBar(progress: number): string {
959
+ const width = 20;
960
+ const filled = Math.round((progress / 100) * width);
961
+ const empty = width - filled;
962
+ return chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
963
+ }
964
+ }
965
+
966
+ // ============================================================================
967
+ // FILE HELPERS
968
+ // ============================================================================
969
+
970
+ async function writeFilesFromResponse(text: string, projectPath: string): Promise<string[]> {
971
+ const fileRegex = /<<<FILE:\s*(.+?)>>>([\s\S]*?)<<<END_FILE>>>/g;
972
+ const files: string[] = [];
973
+ let match;
974
+
975
+ while ((match = fileRegex.exec(text)) !== null) {
976
+ const filePath = path.join(projectPath, match[1].trim());
977
+ const content = match[2].trim();
978
+
979
+ await fs.ensureDir(path.dirname(filePath));
980
+ await fs.writeFile(filePath, content);
981
+ files.push(match[1].trim());
982
+ }
983
+
984
+ return files;
985
+ }
986
+
987
+ function sleep(ms: number): Promise<void> {
988
+ return new Promise(resolve => setTimeout(resolve, ms));
989
+ }