wiggum-cli 0.8.0 → 0.9.1

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 (80) hide show
  1. package/dist/ai/conversation/conversation-manager.d.ts +11 -0
  2. package/dist/ai/conversation/conversation-manager.d.ts.map +1 -1
  3. package/dist/ai/conversation/conversation-manager.js +14 -0
  4. package/dist/ai/conversation/conversation-manager.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +4 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/new.d.ts +2 -0
  9. package/dist/commands/new.d.ts.map +1 -1
  10. package/dist/commands/new.js +63 -22
  11. package/dist/commands/new.js.map +1 -1
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +97 -22
  15. package/dist/index.js.map +1 -1
  16. package/dist/tui/app.d.ts +46 -36
  17. package/dist/tui/app.d.ts.map +1 -1
  18. package/dist/tui/app.js +136 -37
  19. package/dist/tui/app.js.map +1 -1
  20. package/dist/tui/components/WiggumBanner.d.ts +30 -0
  21. package/dist/tui/components/WiggumBanner.d.ts.map +1 -0
  22. package/dist/tui/components/WiggumBanner.js +34 -0
  23. package/dist/tui/components/WiggumBanner.js.map +1 -0
  24. package/dist/tui/demo.d.ts +8 -0
  25. package/dist/tui/demo.d.ts.map +1 -0
  26. package/dist/tui/demo.js +69 -0
  27. package/dist/tui/demo.js.map +1 -0
  28. package/dist/tui/hooks/useSpecGenerator.d.ts +16 -0
  29. package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -1
  30. package/dist/tui/hooks/useSpecGenerator.js +47 -0
  31. package/dist/tui/hooks/useSpecGenerator.js.map +1 -1
  32. package/dist/tui/orchestration/index.d.ts +6 -0
  33. package/dist/tui/orchestration/index.d.ts.map +1 -0
  34. package/dist/tui/orchestration/index.js +6 -0
  35. package/dist/tui/orchestration/index.js.map +1 -0
  36. package/dist/tui/orchestration/interview-orchestrator.d.ts +136 -0
  37. package/dist/tui/orchestration/interview-orchestrator.d.ts.map +1 -0
  38. package/dist/tui/orchestration/interview-orchestrator.js +437 -0
  39. package/dist/tui/orchestration/interview-orchestrator.js.map +1 -0
  40. package/dist/tui/screens/InitScreen.d.ts +26 -0
  41. package/dist/tui/screens/InitScreen.d.ts.map +1 -0
  42. package/dist/tui/screens/InitScreen.js +30 -0
  43. package/dist/tui/screens/InitScreen.js.map +1 -0
  44. package/dist/tui/screens/InterviewScreen.d.ts +2 -13
  45. package/dist/tui/screens/InterviewScreen.d.ts.map +1 -1
  46. package/dist/tui/screens/InterviewScreen.js +162 -34
  47. package/dist/tui/screens/InterviewScreen.js.map +1 -1
  48. package/dist/tui/screens/MainShell.d.ts +46 -0
  49. package/dist/tui/screens/MainShell.d.ts.map +1 -0
  50. package/dist/tui/screens/MainShell.js +196 -0
  51. package/dist/tui/screens/MainShell.js.map +1 -0
  52. package/dist/tui/screens/WelcomeScreen.d.ts +45 -0
  53. package/dist/tui/screens/WelcomeScreen.d.ts.map +1 -0
  54. package/dist/tui/screens/WelcomeScreen.js +56 -0
  55. package/dist/tui/screens/WelcomeScreen.js.map +1 -0
  56. package/dist/tui/theme.d.ts +4 -0
  57. package/dist/tui/theme.d.ts.map +1 -1
  58. package/dist/tui/theme.js +4 -0
  59. package/dist/tui/theme.js.map +1 -1
  60. package/dist/utils/repl-prompts.d.ts +1 -1
  61. package/dist/utils/repl-prompts.d.ts.map +1 -1
  62. package/dist/utils/repl-prompts.js +77 -22
  63. package/dist/utils/repl-prompts.js.map +1 -1
  64. package/package.json +1 -1
  65. package/src/ai/conversation/conversation-manager.ts +22 -0
  66. package/src/cli.ts +4 -0
  67. package/src/commands/new.ts +79 -27
  68. package/src/index.ts +109 -27
  69. package/src/tui/app.tsx +222 -63
  70. package/src/tui/components/WiggumBanner.tsx +66 -0
  71. package/src/tui/demo.tsx +111 -0
  72. package/src/tui/hooks/useSpecGenerator.ts +73 -0
  73. package/src/tui/orchestration/index.ts +10 -0
  74. package/src/tui/orchestration/interview-orchestrator.ts +559 -0
  75. package/src/tui/screens/InitScreen.tsx +63 -0
  76. package/src/tui/screens/InterviewScreen.tsx +201 -46
  77. package/src/tui/screens/MainShell.tsx +290 -0
  78. package/src/tui/screens/WelcomeScreen.tsx +141 -0
  79. package/src/tui/theme.ts +4 -0
  80. package/src/utils/repl-prompts.ts +83 -25
@@ -178,6 +178,28 @@ export interface UseSpecGeneratorReturn {
178
178
  * Complete a tool execution
179
179
  */
180
180
  completeToolCall: (toolId: string, output?: string, error?: string) => void;
181
+
182
+ // Orchestrator-specific actions
183
+
184
+ /**
185
+ * Set the current phase (used by orchestrator)
186
+ */
187
+ setPhase: (phase: GeneratorPhase) => void;
188
+
189
+ /**
190
+ * Set the generated spec (used by orchestrator on completion)
191
+ */
192
+ setGeneratedSpec: (spec: string) => void;
193
+
194
+ /**
195
+ * Set an error state (used by orchestrator on error)
196
+ */
197
+ setError: (error: string) => void;
198
+
199
+ /**
200
+ * Set working state with status message (used by orchestrator)
201
+ */
202
+ setWorking: (isWorking: boolean, status: string) => void;
181
203
  }
182
204
 
183
205
  /**
@@ -571,6 +593,52 @@ export function useSpecGenerator(): UseSpecGeneratorReturn {
571
593
  setState(initialState);
572
594
  }, []);
573
595
 
596
+ /**
597
+ * Set the current phase (used by orchestrator)
598
+ */
599
+ const setPhase = useCallback((phase: GeneratorPhase) => {
600
+ setState((prev) => ({
601
+ ...prev,
602
+ phase,
603
+ }));
604
+ }, []);
605
+
606
+ /**
607
+ * Set the generated spec (used by orchestrator on completion)
608
+ */
609
+ const setGeneratedSpec = useCallback((spec: string) => {
610
+ setState((prev) => ({
611
+ ...prev,
612
+ generatedSpec: spec,
613
+ phase: 'complete',
614
+ isWorking: false,
615
+ awaitingInput: false,
616
+ }));
617
+ }, []);
618
+
619
+ /**
620
+ * Set an error state (used by orchestrator on error)
621
+ */
622
+ const setError = useCallback((error: string) => {
623
+ setState((prev) => ({
624
+ ...prev,
625
+ error,
626
+ isWorking: false,
627
+ }));
628
+ }, []);
629
+
630
+ /**
631
+ * Set working state with status message (used by orchestrator)
632
+ */
633
+ const setWorking = useCallback((isWorking: boolean, status: string) => {
634
+ setState((prev) => ({
635
+ ...prev,
636
+ isWorking,
637
+ workingStatus: status,
638
+ awaitingInput: !isWorking && prev.phase !== 'complete',
639
+ }));
640
+ }, []);
641
+
574
642
  return {
575
643
  state,
576
644
  submitAnswer,
@@ -585,5 +653,10 @@ export function useSpecGenerator(): UseSpecGeneratorReturn {
585
653
  setReady,
586
654
  startToolCall,
587
655
  completeToolCall,
656
+ // Orchestrator-specific actions
657
+ setPhase,
658
+ setGeneratedSpec,
659
+ setError,
660
+ setWorking,
588
661
  };
589
662
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * TUI Orchestration
3
+ * Exports orchestration classes that bridge TUI components to AI services
4
+ */
5
+
6
+ export {
7
+ InterviewOrchestrator,
8
+ type InterviewOrchestratorOptions,
9
+ type SessionContext,
10
+ } from './interview-orchestrator.js';
@@ -0,0 +1,559 @@
1
+ /**
2
+ * Interview Orchestrator
3
+ * Bridges the event-based TUI hook to ConversationManager
4
+ *
5
+ * This class manages the interview flow for spec generation,
6
+ * translating ConversationManager events into TUI-friendly callbacks.
7
+ */
8
+
9
+ import { ConversationManager } from '../../ai/conversation/conversation-manager.js';
10
+ import { fetchContent } from '../../ai/conversation/url-fetcher.js';
11
+ import { createInterviewTools } from '../../ai/conversation/interview-tools.js';
12
+ import { createTavilySearchTool, canUseTavily } from '../../ai/tools/tavily.js';
13
+ import { createContext7Tools, canUseContext7 } from '../../ai/tools/context7.js';
14
+ import type { AIProvider } from '../../ai/providers.js';
15
+ import type { ScanResult } from '../../scanner/types.js';
16
+ import type { GeneratorPhase } from '../hooks/useSpecGenerator.js';
17
+
18
+ /** Maximum number of interview questions before auto-completing */
19
+ const MAX_INTERVIEW_QUESTIONS = 10;
20
+
21
+ /** Minimum number of questions before AI can indicate "enough information" */
22
+ const MIN_INTERVIEW_QUESTIONS = 2;
23
+
24
+ /**
25
+ * Session context from /init analysis
26
+ */
27
+ export interface SessionContext {
28
+ entryPoints?: string[];
29
+ keyDirectories?: Record<string, string>;
30
+ commands?: { build?: string; dev?: string; test?: string };
31
+ namingConventions?: string;
32
+ implementationGuidelines?: string[];
33
+ }
34
+
35
+ /**
36
+ * Options for the InterviewOrchestrator
37
+ */
38
+ export interface InterviewOrchestratorOptions {
39
+ /** Name of the feature being specified */
40
+ featureName: string;
41
+ /** Project root directory path */
42
+ projectRoot: string;
43
+ /** AI provider to use */
44
+ provider: AIProvider;
45
+ /** Model ID to use */
46
+ model: string;
47
+ /** Optional scan result with detected tech stack */
48
+ scanResult?: ScanResult;
49
+ /** Rich session context from /init */
50
+ sessionContext?: SessionContext;
51
+ /** Tavily API key for web search */
52
+ tavilyApiKey?: string;
53
+ /** Context7 API key for docs lookup */
54
+ context7ApiKey?: string;
55
+
56
+ // Event callbacks for TUI
57
+ /** Called when a message should be added to the conversation */
58
+ onMessage: (role: 'user' | 'assistant' | 'system', content: string) => void;
59
+ /** Called when streaming text should be appended */
60
+ onStreamChunk: (chunk: string) => void;
61
+ /** Called when streaming is complete */
62
+ onStreamComplete: () => void;
63
+ /** Called when a tool starts executing */
64
+ onToolStart: (toolName: string, input: Record<string, unknown>) => string;
65
+ /** Called when a tool completes */
66
+ onToolEnd: (toolId: string, output?: string, error?: string) => void;
67
+ /** Called when the phase changes */
68
+ onPhaseChange: (phase: GeneratorPhase) => void;
69
+ /** Called when spec generation is complete */
70
+ onComplete: (spec: string) => void;
71
+ /** Called when an error occurs */
72
+ onError: (error: string) => void;
73
+ /** Called when working state changes */
74
+ onWorkingChange: (isWorking: boolean, status: string) => void;
75
+ /** Called when ready for user input */
76
+ onReady: () => void;
77
+ }
78
+
79
+ /**
80
+ * Build enhanced system prompt with project context and tool awareness
81
+ */
82
+ function buildSystemPrompt(
83
+ sessionContext?: SessionContext,
84
+ hasTools?: { codebase: boolean; tavily: boolean; context7: boolean }
85
+ ): string {
86
+ const parts: string[] = [];
87
+
88
+ // Base prompt
89
+ parts.push(`You are an expert product manager and technical writer helping to create detailed feature specifications.
90
+
91
+ Your role is to:
92
+ 1. Understand the user's feature goals through targeted questions
93
+ 2. Identify edge cases and potential issues
94
+ 3. Generate a comprehensive, actionable specification
95
+
96
+ When interviewing:
97
+ - Ask one focused question at a time
98
+ - Acknowledge answers before asking the next question
99
+ - Stop asking when you have enough information (usually 3-5 questions)
100
+ - Say "I have enough information to generate the spec" when ready`);
101
+
102
+ // Add tool awareness
103
+ if (hasTools) {
104
+ const toolList: string[] = [];
105
+ if (hasTools.codebase) {
106
+ toolList.push('- read_file: Read project files to understand existing code');
107
+ toolList.push('- search_codebase: Search for patterns, functions, or imports');
108
+ toolList.push('- list_directory: Explore project structure');
109
+ }
110
+ if (hasTools.tavily) {
111
+ toolList.push('- tavily_search: Search the web for best practices and documentation');
112
+ }
113
+ if (hasTools.context7) {
114
+ toolList.push('- resolveLibraryId/queryDocs: Look up library documentation');
115
+ }
116
+
117
+ if (toolList.length > 0) {
118
+ parts.push(`
119
+ ## Available Tools
120
+ You have access to the following tools to help understand the project and gather information:
121
+ ${toolList.join('\n')}
122
+
123
+ USE THESE TOOLS PROACTIVELY:
124
+ - When the user describes a feature, read relevant files to understand existing patterns
125
+ - When unsure about implementation, search the codebase for similar code
126
+ - When discussing best practices, search the web for current recommendations
127
+ - Don't ask the user to paste code - read it yourself`);
128
+ }
129
+
130
+ // Add emphatic web search guidance when Tavily is available
131
+ if (hasTools.tavily) {
132
+ parts.push(`
133
+ ## IMPORTANT: Web Search Available
134
+ You have tavily_search to look up current best practices and documentation.
135
+
136
+ WHEN YOU MUST USE WEB SEARCH:
137
+ - User mentions a library, framework, or API you should verify
138
+ - User asks about "best practices" or "how to" patterns
139
+ - You need current (2026+) information not in your training data
140
+ - Discussing implementation approaches for modern libraries`);
141
+ }
142
+ }
143
+
144
+ // Add project context from /init
145
+ if (sessionContext) {
146
+ const contextParts: string[] = ['## Project Context (from analysis)'];
147
+
148
+ if (sessionContext.entryPoints && sessionContext.entryPoints.length > 0) {
149
+ contextParts.push(`\nEntry Points:\n${sessionContext.entryPoints.map(e => `- ${e}`).join('\n')}`);
150
+ }
151
+
152
+ if (sessionContext.keyDirectories && Object.keys(sessionContext.keyDirectories).length > 0) {
153
+ contextParts.push(`\nKey Directories:`);
154
+ for (const [dir, purpose] of Object.entries(sessionContext.keyDirectories)) {
155
+ contextParts.push(`- ${dir}: ${purpose}`);
156
+ }
157
+ }
158
+
159
+ if (sessionContext.commands) {
160
+ const cmds = sessionContext.commands;
161
+ const cmdList = Object.entries(cmds).filter(([_, v]) => v);
162
+ if (cmdList.length > 0) {
163
+ contextParts.push(`\nCommands:`);
164
+ for (const [name, cmd] of cmdList) {
165
+ contextParts.push(`- ${name}: ${cmd}`);
166
+ }
167
+ }
168
+ }
169
+
170
+ if (contextParts.length > 1) {
171
+ parts.push(contextParts.join('\n'));
172
+ }
173
+ }
174
+
175
+ // Spec format
176
+ parts.push(`
177
+ ## Spec Format
178
+ When generating the spec, use this format:
179
+
180
+ # [Feature Name] Feature Specification
181
+
182
+ **Status:** Planned
183
+ **Version:** 1.0
184
+ **Last Updated:** [date]
185
+
186
+ ## Purpose
187
+ [Brief description]
188
+
189
+ ## User Stories
190
+ - As a [user], I want [action] so that [benefit]
191
+
192
+ ## Requirements
193
+
194
+ ### Functional Requirements
195
+ - [ ] Requirement with clear acceptance criteria
196
+
197
+ ### Non-Functional Requirements
198
+ - [ ] Performance, security, accessibility requirements
199
+
200
+ ## Technical Notes
201
+ - Implementation approach
202
+ - Key dependencies
203
+ - Database changes if needed
204
+
205
+ ## Acceptance Criteria
206
+ - [ ] Specific, testable conditions
207
+
208
+ ## Out of Scope
209
+ - Items explicitly not included`);
210
+
211
+ return parts.join('\n\n');
212
+ }
213
+
214
+ /**
215
+ * Extract session context from EnhancedScanResult
216
+ */
217
+ function extractSessionContext(scanResult: ScanResult): SessionContext | undefined {
218
+ // Check if this is an EnhancedScanResult with aiAnalysis
219
+ const enhanced = scanResult as ScanResult & { aiAnalysis?: { projectContext?: SessionContext; commands?: SessionContext['commands']; implementationGuidelines?: string[] } };
220
+ if (!enhanced.aiAnalysis) {
221
+ return undefined;
222
+ }
223
+
224
+ const ai = enhanced.aiAnalysis;
225
+ return {
226
+ entryPoints: ai.projectContext?.entryPoints,
227
+ keyDirectories: ai.projectContext?.keyDirectories,
228
+ commands: ai.commands,
229
+ namingConventions: ai.projectContext?.namingConventions,
230
+ implementationGuidelines: ai.implementationGuidelines,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * InterviewOrchestrator
236
+ *
237
+ * Manages the interview flow for spec generation, bridging
238
+ * the ConversationManager to TUI callbacks.
239
+ */
240
+ export class InterviewOrchestrator {
241
+ private conversation: ConversationManager;
242
+ private phase: GeneratorPhase = 'context';
243
+ private readonly featureName: string;
244
+ private readonly projectRoot: string;
245
+ private generatedSpec: string = '';
246
+ private questionCount: number = 0;
247
+ private readonly hasTools: { codebase: boolean; tavily: boolean; context7: boolean };
248
+ private readonly sessionContext?: SessionContext;
249
+
250
+ // Callbacks
251
+ private readonly onMessage: InterviewOrchestratorOptions['onMessage'];
252
+ private readonly onStreamChunk: InterviewOrchestratorOptions['onStreamChunk'];
253
+ private readonly onStreamComplete: InterviewOrchestratorOptions['onStreamComplete'];
254
+ private readonly onToolStart: InterviewOrchestratorOptions['onToolStart'];
255
+ private readonly onToolEnd: InterviewOrchestratorOptions['onToolEnd'];
256
+ private readonly onPhaseChange: InterviewOrchestratorOptions['onPhaseChange'];
257
+ private readonly onComplete: InterviewOrchestratorOptions['onComplete'];
258
+ private readonly onError: InterviewOrchestratorOptions['onError'];
259
+ private readonly onWorkingChange: InterviewOrchestratorOptions['onWorkingChange'];
260
+ private readonly onReady: InterviewOrchestratorOptions['onReady'];
261
+
262
+ // Track active tool calls for result mapping
263
+ // Uses a queue per tool name to handle multiple calls of the same tool
264
+ private activeToolCalls: Map<string, string[]> = new Map();
265
+
266
+ constructor(options: InterviewOrchestratorOptions) {
267
+ this.featureName = options.featureName;
268
+ this.projectRoot = options.projectRoot;
269
+
270
+ // Store callbacks
271
+ this.onMessage = options.onMessage;
272
+ this.onStreamChunk = options.onStreamChunk;
273
+ this.onStreamComplete = options.onStreamComplete;
274
+ this.onToolStart = options.onToolStart;
275
+ this.onToolEnd = options.onToolEnd;
276
+ this.onPhaseChange = options.onPhaseChange;
277
+ this.onComplete = options.onComplete;
278
+ this.onError = options.onError;
279
+ this.onWorkingChange = options.onWorkingChange;
280
+ this.onReady = options.onReady;
281
+
282
+ // Get API keys from options or environment
283
+ const tavilyApiKey = options.tavilyApiKey || process.env.TAVILY_API_KEY;
284
+ const context7ApiKey = options.context7ApiKey || process.env.CONTEXT7_API_KEY;
285
+
286
+ // Track which tools are available
287
+ this.hasTools = {
288
+ codebase: true, // Always available
289
+ tavily: canUseTavily(tavilyApiKey),
290
+ context7: canUseContext7(context7ApiKey),
291
+ };
292
+
293
+ // Build tools object
294
+ const tools: Record<string, unknown> = {};
295
+
296
+ // Add codebase tools
297
+ const codebaseTools = createInterviewTools(options.projectRoot);
298
+ Object.assign(tools, codebaseTools);
299
+
300
+ // Add Tavily search if available
301
+ if (this.hasTools.tavily && tavilyApiKey) {
302
+ tools.tavily_search = createTavilySearchTool(tavilyApiKey);
303
+ }
304
+
305
+ // Add Context7 tools if available
306
+ if (this.hasTools.context7 && context7ApiKey) {
307
+ const context7Tools = createContext7Tools(context7ApiKey);
308
+ Object.assign(tools, context7Tools);
309
+ }
310
+
311
+ // Extract session context from scan result or use provided
312
+ this.sessionContext = options.sessionContext || (
313
+ options.scanResult ? extractSessionContext(options.scanResult) : undefined
314
+ );
315
+
316
+ // Build enhanced system prompt
317
+ const systemPrompt = buildSystemPrompt(this.sessionContext, this.hasTools);
318
+
319
+ // Create conversation manager with tools
320
+ this.conversation = new ConversationManager({
321
+ provider: options.provider,
322
+ model: options.model,
323
+ systemPrompt,
324
+ tools: tools as Record<string, never>,
325
+ onToolUse: (toolName, args) => {
326
+ // Create tool ID and notify TUI
327
+ const toolId = this.onToolStart(toolName, args);
328
+ // Store in queue for this tool name (handles multiple concurrent calls)
329
+ const queue = this.activeToolCalls.get(toolName) || [];
330
+ queue.push(toolId);
331
+ this.activeToolCalls.set(toolName, queue);
332
+ },
333
+ onToolResult: (toolName, result) => {
334
+ const queue = this.activeToolCalls.get(toolName);
335
+ // Get the first (oldest) tool ID from the queue (FIFO order)
336
+ const toolId = queue?.shift();
337
+ if (toolId) {
338
+ // Format result for display
339
+ const output = typeof result === 'string'
340
+ ? result
341
+ : JSON.stringify(result, null, 2).slice(0, 200);
342
+ this.onToolEnd(toolId, output);
343
+ // Clean up empty queues
344
+ if (queue && queue.length === 0) {
345
+ this.activeToolCalls.delete(toolName);
346
+ }
347
+ }
348
+ },
349
+ maxToolSteps: 8,
350
+ });
351
+
352
+ if (options.scanResult) {
353
+ this.conversation.setCodebaseContext(options.scanResult);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Start the interview flow
359
+ * Called after the component mounts
360
+ */
361
+ async start(): Promise<void> {
362
+ try {
363
+ // Set initial phase
364
+ this.onPhaseChange('context');
365
+ this.onMessage('system', `Spec Generator initialized for feature: ${this.featureName}`);
366
+
367
+ // Ready for context input
368
+ this.onReady();
369
+ } catch (error) {
370
+ this.onError(error instanceof Error ? error.message : String(error));
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Add a reference URL or file path
376
+ */
377
+ async addReference(refUrl: string): Promise<void> {
378
+ try {
379
+ this.onWorkingChange(true, 'Fetching reference...');
380
+
381
+ const result = await fetchContent(refUrl, this.projectRoot);
382
+
383
+ if (result.error) {
384
+ this.onMessage('system', `Error: ${result.error}`);
385
+ } else {
386
+ this.conversation.addReference(result.content, result.source);
387
+ const preview = result.content.slice(0, 100).replace(/\n/g, ' ').trim();
388
+ this.onMessage('system', `Added reference from ${result.source}: "${preview}..."`);
389
+ }
390
+
391
+ this.onReady();
392
+ } catch (error) {
393
+ this.onError(error instanceof Error ? error.message : String(error));
394
+ this.onReady();
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Advance to the goals phase
400
+ * Called when user is done adding context
401
+ */
402
+ async advanceToGoals(): Promise<void> {
403
+ this.phase = 'goals';
404
+ this.onPhaseChange('goals');
405
+ this.onMessage('system', 'Phase 2: Goals - Describe what you want to build');
406
+ this.onReady();
407
+ }
408
+
409
+ /**
410
+ * Submit user's goals and start the interview
411
+ */
412
+ async submitGoals(goals: string): Promise<void> {
413
+ try {
414
+ this.onWorkingChange(true, 'Exploring project...');
415
+
416
+ // Add user message to conversation
417
+ const userMessage = goals
418
+ ? `I want to create a feature called "${this.featureName}". Here's what I'm thinking:\n\n${goals}`
419
+ : `I want to create a feature called "${this.featureName}".`;
420
+
421
+ this.conversation.addToHistory({ role: 'user', content: userMessage });
422
+
423
+ // Phase 2a: Explore project silently
424
+ const explorePrompt = `Explore the codebase to understand the project structure for the feature "${this.featureName}".
425
+ Use your tools to read key files that are relevant to this feature.
426
+ DO NOT ask any questions yet - just gather information silently.
427
+ Respond with a VERY brief (1-2 sentence) summary of what you found relevant to this feature.`;
428
+
429
+ const summary = await this.conversation.chat(explorePrompt);
430
+ const shortSummary = summary.slice(0, 120).replace(/\n/g, ' ');
431
+ this.onMessage('system', `Context: ${shortSummary}${summary.length > 120 ? '...' : ''}`);
432
+
433
+ // Phase 2b: Start interview with first question
434
+ this.onWorkingChange(true, 'Preparing first question...');
435
+
436
+ const interviewPrompt = `Based on what you learned about the project, briefly acknowledge the user's goals for "${this.featureName}" and ask your FIRST clarifying question.
437
+ Ask only ONE question. Be concise.`;
438
+
439
+ const response = await this.conversation.chat(interviewPrompt);
440
+ this.onMessage('assistant', response);
441
+
442
+ // Transition to interview phase
443
+ this.phase = 'interview';
444
+ this.onPhaseChange('interview');
445
+ this.onReady();
446
+ } catch (error) {
447
+ this.onError(error instanceof Error ? error.message : String(error));
448
+ this.onReady();
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Submit an answer during the interview phase
454
+ */
455
+ async submitAnswer(answer: string): Promise<void> {
456
+ try {
457
+ this.onWorkingChange(true, 'Thinking...');
458
+
459
+ const response = await this.conversation.chat(answer);
460
+ this.onMessage('assistant', response);
461
+
462
+ this.questionCount++;
463
+
464
+ // Check if AI indicates it has enough information
465
+ if (this.questionCount >= MIN_INTERVIEW_QUESTIONS) {
466
+ const lowerResponse = response.toLowerCase();
467
+ if (
468
+ lowerResponse.includes('enough information') ||
469
+ lowerResponse.includes('ready to generate') ||
470
+ lowerResponse.includes("let me generate") ||
471
+ lowerResponse.includes("i'll now generate") ||
472
+ lowerResponse.includes("i will now generate")
473
+ ) {
474
+ await this.generateSpec();
475
+ return;
476
+ }
477
+ }
478
+
479
+ // Check if max questions reached
480
+ if (this.questionCount >= MAX_INTERVIEW_QUESTIONS) {
481
+ await this.generateSpec();
482
+ return;
483
+ }
484
+
485
+ this.onReady();
486
+ } catch (error) {
487
+ this.onError(error instanceof Error ? error.message : String(error));
488
+ this.onReady();
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Skip to generation phase
494
+ */
495
+ async skipToGeneration(): Promise<void> {
496
+ await this.generateSpec();
497
+ }
498
+
499
+ /**
500
+ * Generate the specification
501
+ */
502
+ private async generateSpec(): Promise<void> {
503
+ try {
504
+ this.phase = 'generation';
505
+ this.onPhaseChange('generation');
506
+ this.onWorkingChange(true, 'Generating specification...');
507
+
508
+ const prompt = `Based on our conversation, generate a complete feature specification for "${this.featureName}".
509
+
510
+ Use the format from your instructions. Be specific and actionable. Include:
511
+ - Clear user stories
512
+ - Detailed requirements with acceptance criteria
513
+ - Technical notes based on the project's tech stack
514
+ - Specific acceptance criteria that can be tested
515
+
516
+ Today's date is ${new Date().toISOString().split('T')[0]}.`;
517
+
518
+ // Use streaming for generation
519
+ const stream = this.conversation.chatStream(prompt);
520
+ let fullSpec = '';
521
+
522
+ for await (const chunk of stream) {
523
+ fullSpec += chunk;
524
+ this.onStreamChunk(chunk);
525
+ }
526
+
527
+ this.onStreamComplete();
528
+ this.generatedSpec = fullSpec;
529
+
530
+ // Complete
531
+ this.phase = 'complete';
532
+ this.onPhaseChange('complete');
533
+ this.onComplete(fullSpec);
534
+ } catch (error) {
535
+ this.onError(error instanceof Error ? error.message : String(error));
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Get current phase
541
+ */
542
+ getPhase(): GeneratorPhase {
543
+ return this.phase;
544
+ }
545
+
546
+ /**
547
+ * Get question count
548
+ */
549
+ getQuestionCount(): number {
550
+ return this.questionCount;
551
+ }
552
+
553
+ /**
554
+ * Get generated spec
555
+ */
556
+ getSpec(): string {
557
+ return this.generatedSpec;
558
+ }
559
+ }