popeye-cli 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/.env.example +24 -1
  2. package/CONTRIBUTING.md +275 -0
  3. package/OPEN_SOURCE_MANIFESTO.md +172 -0
  4. package/README.md +832 -123
  5. package/dist/adapters/claude.d.ts +19 -4
  6. package/dist/adapters/claude.d.ts.map +1 -1
  7. package/dist/adapters/claude.js +908 -42
  8. package/dist/adapters/claude.js.map +1 -1
  9. package/dist/adapters/gemini.d.ts +55 -0
  10. package/dist/adapters/gemini.d.ts.map +1 -0
  11. package/dist/adapters/gemini.js +318 -0
  12. package/dist/adapters/gemini.js.map +1 -0
  13. package/dist/adapters/grok.d.ts +73 -0
  14. package/dist/adapters/grok.d.ts.map +1 -0
  15. package/dist/adapters/grok.js +430 -0
  16. package/dist/adapters/grok.js.map +1 -0
  17. package/dist/adapters/openai.d.ts +1 -1
  18. package/dist/adapters/openai.d.ts.map +1 -1
  19. package/dist/adapters/openai.js +47 -8
  20. package/dist/adapters/openai.js.map +1 -1
  21. package/dist/auth/claude.d.ts +11 -9
  22. package/dist/auth/claude.d.ts.map +1 -1
  23. package/dist/auth/claude.js +107 -71
  24. package/dist/auth/claude.js.map +1 -1
  25. package/dist/auth/gemini.d.ts +58 -0
  26. package/dist/auth/gemini.d.ts.map +1 -0
  27. package/dist/auth/gemini.js +172 -0
  28. package/dist/auth/gemini.js.map +1 -0
  29. package/dist/auth/grok.d.ts +73 -0
  30. package/dist/auth/grok.d.ts.map +1 -0
  31. package/dist/auth/grok.js +211 -0
  32. package/dist/auth/grok.js.map +1 -0
  33. package/dist/auth/index.d.ts +14 -7
  34. package/dist/auth/index.d.ts.map +1 -1
  35. package/dist/auth/index.js +41 -6
  36. package/dist/auth/index.js.map +1 -1
  37. package/dist/auth/keychain.d.ts +20 -7
  38. package/dist/auth/keychain.d.ts.map +1 -1
  39. package/dist/auth/keychain.js +85 -29
  40. package/dist/auth/keychain.js.map +1 -1
  41. package/dist/auth/openai.d.ts +2 -2
  42. package/dist/auth/openai.d.ts.map +1 -1
  43. package/dist/auth/openai.js +30 -32
  44. package/dist/auth/openai.js.map +1 -1
  45. package/dist/cli/commands/auth.d.ts +1 -1
  46. package/dist/cli/commands/auth.d.ts.map +1 -1
  47. package/dist/cli/commands/auth.js +79 -8
  48. package/dist/cli/commands/auth.js.map +1 -1
  49. package/dist/cli/commands/create.d.ts.map +1 -1
  50. package/dist/cli/commands/create.js +15 -4
  51. package/dist/cli/commands/create.js.map +1 -1
  52. package/dist/cli/interactive.d.ts.map +1 -1
  53. package/dist/cli/interactive.js +1494 -114
  54. package/dist/cli/interactive.js.map +1 -1
  55. package/dist/config/defaults.d.ts +9 -1
  56. package/dist/config/defaults.d.ts.map +1 -1
  57. package/dist/config/defaults.js +19 -2
  58. package/dist/config/defaults.js.map +1 -1
  59. package/dist/config/index.d.ts +19 -0
  60. package/dist/config/index.d.ts.map +1 -1
  61. package/dist/config/index.js +33 -1
  62. package/dist/config/index.js.map +1 -1
  63. package/dist/config/schema.d.ts +47 -0
  64. package/dist/config/schema.d.ts.map +1 -1
  65. package/dist/config/schema.js +29 -1
  66. package/dist/config/schema.js.map +1 -1
  67. package/dist/generators/fullstack.d.ts +32 -0
  68. package/dist/generators/fullstack.d.ts.map +1 -0
  69. package/dist/generators/fullstack.js +497 -0
  70. package/dist/generators/fullstack.js.map +1 -0
  71. package/dist/generators/index.d.ts +4 -3
  72. package/dist/generators/index.d.ts.map +1 -1
  73. package/dist/generators/index.js +15 -1
  74. package/dist/generators/index.js.map +1 -1
  75. package/dist/generators/python.d.ts +17 -1
  76. package/dist/generators/python.d.ts.map +1 -1
  77. package/dist/generators/python.js +34 -20
  78. package/dist/generators/python.js.map +1 -1
  79. package/dist/generators/templates/fullstack.d.ts +113 -0
  80. package/dist/generators/templates/fullstack.d.ts.map +1 -0
  81. package/dist/generators/templates/fullstack.js +1004 -0
  82. package/dist/generators/templates/fullstack.js.map +1 -0
  83. package/dist/generators/typescript.d.ts +19 -1
  84. package/dist/generators/typescript.d.ts.map +1 -1
  85. package/dist/generators/typescript.js +37 -20
  86. package/dist/generators/typescript.js.map +1 -1
  87. package/dist/state/index.d.ts +108 -0
  88. package/dist/state/index.d.ts.map +1 -1
  89. package/dist/state/index.js +551 -4
  90. package/dist/state/index.js.map +1 -1
  91. package/dist/state/registry.d.ts +52 -0
  92. package/dist/state/registry.d.ts.map +1 -0
  93. package/dist/state/registry.js +215 -0
  94. package/dist/state/registry.js.map +1 -0
  95. package/dist/types/cli.d.ts +8 -0
  96. package/dist/types/cli.d.ts.map +1 -1
  97. package/dist/types/cli.js.map +1 -1
  98. package/dist/types/consensus.d.ts +186 -4
  99. package/dist/types/consensus.d.ts.map +1 -1
  100. package/dist/types/consensus.js +35 -3
  101. package/dist/types/consensus.js.map +1 -1
  102. package/dist/types/project.d.ts +76 -0
  103. package/dist/types/project.d.ts.map +1 -1
  104. package/dist/types/project.js +1 -1
  105. package/dist/types/project.js.map +1 -1
  106. package/dist/types/workflow.d.ts +217 -16
  107. package/dist/types/workflow.d.ts.map +1 -1
  108. package/dist/types/workflow.js +40 -1
  109. package/dist/types/workflow.js.map +1 -1
  110. package/dist/workflow/auto-fix.d.ts +45 -0
  111. package/dist/workflow/auto-fix.d.ts.map +1 -0
  112. package/dist/workflow/auto-fix.js +274 -0
  113. package/dist/workflow/auto-fix.js.map +1 -0
  114. package/dist/workflow/consensus.d.ts +70 -2
  115. package/dist/workflow/consensus.d.ts.map +1 -1
  116. package/dist/workflow/consensus.js +872 -17
  117. package/dist/workflow/consensus.js.map +1 -1
  118. package/dist/workflow/execution-mode.d.ts +10 -4
  119. package/dist/workflow/execution-mode.d.ts.map +1 -1
  120. package/dist/workflow/execution-mode.js +547 -58
  121. package/dist/workflow/execution-mode.js.map +1 -1
  122. package/dist/workflow/index.d.ts +14 -2
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +69 -6
  125. package/dist/workflow/index.js.map +1 -1
  126. package/dist/workflow/milestone-workflow.d.ts +34 -0
  127. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  128. package/dist/workflow/milestone-workflow.js +414 -0
  129. package/dist/workflow/milestone-workflow.js.map +1 -0
  130. package/dist/workflow/plan-mode.d.ts +80 -3
  131. package/dist/workflow/plan-mode.d.ts.map +1 -1
  132. package/dist/workflow/plan-mode.js +767 -49
  133. package/dist/workflow/plan-mode.js.map +1 -1
  134. package/dist/workflow/plan-storage.d.ts +386 -0
  135. package/dist/workflow/plan-storage.d.ts.map +1 -0
  136. package/dist/workflow/plan-storage.js +878 -0
  137. package/dist/workflow/plan-storage.js.map +1 -0
  138. package/dist/workflow/project-verification.d.ts +37 -0
  139. package/dist/workflow/project-verification.d.ts.map +1 -0
  140. package/dist/workflow/project-verification.js +381 -0
  141. package/dist/workflow/project-verification.js.map +1 -0
  142. package/dist/workflow/task-workflow.d.ts +37 -0
  143. package/dist/workflow/task-workflow.d.ts.map +1 -0
  144. package/dist/workflow/task-workflow.js +386 -0
  145. package/dist/workflow/task-workflow.js.map +1 -0
  146. package/dist/workflow/test-runner.d.ts +9 -0
  147. package/dist/workflow/test-runner.d.ts.map +1 -1
  148. package/dist/workflow/test-runner.js +101 -5
  149. package/dist/workflow/test-runner.js.map +1 -1
  150. package/dist/workflow/ui-designer.d.ts +82 -0
  151. package/dist/workflow/ui-designer.d.ts.map +1 -0
  152. package/dist/workflow/ui-designer.js +234 -0
  153. package/dist/workflow/ui-designer.js.map +1 -0
  154. package/dist/workflow/ui-setup.d.ts +58 -0
  155. package/dist/workflow/ui-setup.d.ts.map +1 -0
  156. package/dist/workflow/ui-setup.js +685 -0
  157. package/dist/workflow/ui-setup.js.map +1 -0
  158. package/dist/workflow/ui-verification.d.ts +114 -0
  159. package/dist/workflow/ui-verification.d.ts.map +1 -0
  160. package/dist/workflow/ui-verification.js +258 -0
  161. package/dist/workflow/ui-verification.js.map +1 -0
  162. package/dist/workflow/workflow-logger.d.ts +110 -0
  163. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  164. package/dist/workflow/workflow-logger.js +267 -0
  165. package/dist/workflow/workflow-logger.js.map +1 -0
  166. package/dist/workflow/workspace-manager.d.ts +342 -0
  167. package/dist/workflow/workspace-manager.d.ts.map +1 -0
  168. package/dist/workflow/workspace-manager.js +733 -0
  169. package/dist/workflow/workspace-manager.js.map +1 -0
  170. package/package.json +2 -2
  171. package/src/adapters/claude.ts +1067 -47
  172. package/src/adapters/gemini.ts +373 -0
  173. package/src/adapters/grok.ts +492 -0
  174. package/src/adapters/openai.ts +48 -9
  175. package/src/auth/claude.ts +120 -78
  176. package/src/auth/gemini.ts +207 -0
  177. package/src/auth/grok.ts +255 -0
  178. package/src/auth/index.ts +47 -9
  179. package/src/auth/keychain.ts +95 -28
  180. package/src/auth/openai.ts +29 -36
  181. package/src/cli/commands/auth.ts +89 -10
  182. package/src/cli/commands/create.ts +13 -4
  183. package/src/cli/interactive.ts +1774 -142
  184. package/src/config/defaults.ts +19 -2
  185. package/src/config/index.ts +36 -1
  186. package/src/config/schema.ts +30 -1
  187. package/src/generators/fullstack.ts +551 -0
  188. package/src/generators/index.ts +25 -1
  189. package/src/generators/python.ts +65 -20
  190. package/src/generators/templates/fullstack.ts +1047 -0
  191. package/src/generators/typescript.ts +69 -20
  192. package/src/state/index.ts +713 -4
  193. package/src/state/registry.ts +278 -0
  194. package/src/types/cli.ts +8 -0
  195. package/src/types/consensus.ts +197 -6
  196. package/src/types/project.ts +82 -1
  197. package/src/types/workflow.ts +90 -1
  198. package/src/workflow/auto-fix.ts +340 -0
  199. package/src/workflow/consensus.ts +1180 -16
  200. package/src/workflow/execution-mode.ts +673 -74
  201. package/src/workflow/index.ts +95 -6
  202. package/src/workflow/milestone-workflow.ts +576 -0
  203. package/src/workflow/plan-mode.ts +924 -50
  204. package/src/workflow/plan-storage.ts +1282 -0
  205. package/src/workflow/project-verification.ts +471 -0
  206. package/src/workflow/task-workflow.ts +528 -0
  207. package/src/workflow/test-runner.ts +120 -5
  208. package/src/workflow/ui-designer.ts +337 -0
  209. package/src/workflow/ui-setup.ts +797 -0
  210. package/src/workflow/ui-verification.ts +357 -0
  211. package/src/workflow/workflow-logger.ts +353 -0
  212. package/src/workflow/workspace-manager.ts +912 -0
  213. package/tests/config/config.test.ts +1 -1
  214. package/tests/types/consensus.test.ts +3 -3
  215. package/tests/workflow/plan-mode.test.ts +213 -0
  216. package/tests/workflow/test-runner.test.ts +5 -3
@@ -3,6 +3,9 @@
3
3
  * Wraps the Claude Agent SDK for code execution and generation
4
4
  */
5
5
 
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import { homedir } from 'os';
6
9
  import { query, type SDKMessage } from '@anthropic-ai/claude-agent-sdk';
7
10
 
8
11
  /**
@@ -15,6 +18,287 @@ export interface ClaudeExecuteOptions {
15
18
  systemPrompt?: string;
16
19
  timeout?: number;
17
20
  onMessage?: (message: SDKMessage) => void;
21
+ onProgress?: (message: string) => void;
22
+ /**
23
+ * Rate limit handling configuration
24
+ * Set to false to disable rate limit retries
25
+ */
26
+ rateLimitConfig?: {
27
+ maxRetries?: number;
28
+ baseWaitMs?: number;
29
+ maxWaitMs?: number;
30
+ } | false;
31
+ }
32
+
33
+ /**
34
+ * Log directory for debug information
35
+ */
36
+ const LOG_DIR = path.join(homedir(), '.popeye', 'logs');
37
+
38
+ /**
39
+ * Rate limit handling configuration
40
+ */
41
+ interface RateLimitConfig {
42
+ maxRetries: number;
43
+ baseWaitMs: number;
44
+ maxWaitMs: number;
45
+ }
46
+
47
+ const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig = {
48
+ maxRetries: 3,
49
+ baseWaitMs: 60_000, // 1 minute
50
+ maxWaitMs: 10 * 60_000, // 10 minutes max - don't wait longer than this
51
+ };
52
+
53
+ /**
54
+ * Parse rate limit reset time from error message
55
+ * Messages like: "You've hit your limit · resets 3pm (Asia/Jerusalem)"
56
+ */
57
+ function parseRateLimitResetTime(message: string): Date | null {
58
+ // Try to parse time like "3pm", "3:30pm", "15:00"
59
+ const timePatterns = [
60
+ /resets?\s+(\d{1,2}):?(\d{2})?\s*(am|pm)?/i,
61
+ /until\s+(\d{1,2}):?(\d{2})?\s*(am|pm)?/i,
62
+ /wait\s+until\s+(\d{1,2}):?(\d{2})?\s*(am|pm)?/i,
63
+ ];
64
+
65
+ for (const pattern of timePatterns) {
66
+ const match = message.match(pattern);
67
+ if (match) {
68
+ let hours = parseInt(match[1], 10);
69
+ const minutes = match[2] ? parseInt(match[2], 10) : 0;
70
+ const ampm = match[3]?.toLowerCase();
71
+
72
+ if (ampm === 'pm' && hours < 12) hours += 12;
73
+ if (ampm === 'am' && hours === 12) hours = 0;
74
+
75
+ const resetTime = new Date();
76
+ resetTime.setHours(hours, minutes, 0, 0);
77
+
78
+ // If the time has passed today, assume tomorrow
79
+ if (resetTime.getTime() <= Date.now()) {
80
+ resetTime.setDate(resetTime.getDate() + 1);
81
+ }
82
+
83
+ return resetTime;
84
+ }
85
+ }
86
+
87
+ // Try to parse duration like "30 minutes", "1 hour"
88
+ const durationPatterns = [
89
+ /(\d+)\s*minutes?/i,
90
+ /(\d+)\s*hours?/i,
91
+ ];
92
+
93
+ for (let i = 0; i < durationPatterns.length; i++) {
94
+ const match = message.match(durationPatterns[i]);
95
+ if (match) {
96
+ const value = parseInt(match[1], 10);
97
+ const multiplier = i === 0 ? 60_000 : 60 * 60_000; // minutes or hours
98
+ return new Date(Date.now() + value * multiplier);
99
+ }
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Format wait time for display
107
+ */
108
+ function formatWaitTime(ms: number): string {
109
+ if (ms < 60_000) {
110
+ return `${Math.ceil(ms / 1000)} seconds`;
111
+ } else if (ms < 60 * 60_000) {
112
+ const minutes = Math.ceil(ms / 60_000);
113
+ return `${minutes} minute${minutes > 1 ? 's' : ''}`;
114
+ } else {
115
+ const hours = Math.floor(ms / (60 * 60_000));
116
+ const minutes = Math.ceil((ms % (60 * 60_000)) / 60_000);
117
+ return `${hours} hour${hours > 1 ? 's' : ''}${minutes > 0 ? ` ${minutes} minute${minutes > 1 ? 's' : ''}` : ''}`;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Sleep for a specified duration with progress updates
123
+ */
124
+ async function sleepWithProgress(
125
+ ms: number,
126
+ onProgress?: (message: string) => void
127
+ ): Promise<void> {
128
+ const startTime = Date.now();
129
+ const endTime = startTime + ms;
130
+ const updateInterval = Math.min(ms / 10, 60_000); // Update every 10% or minute, whichever is smaller
131
+
132
+ while (Date.now() < endTime) {
133
+ const remaining = endTime - Date.now();
134
+ if (remaining <= 0) break;
135
+
136
+ onProgress?.(`Rate limit: waiting ${formatWaitTime(remaining)} before retry...`);
137
+
138
+ const sleepTime = Math.min(updateInterval, remaining);
139
+ await new Promise(resolve => setTimeout(resolve, sleepTime));
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Extract just the rate limit message from a larger string
145
+ * e.g., "Some content... You've hit your limit · resets 3pm (Asia/Jerusalem)" -> "You've hit your limit · resets 3pm (Asia/Jerusalem)"
146
+ */
147
+ function extractRateLimitMessage(content: string): string {
148
+ // Look for specific rate limit error message patterns
149
+ // These patterns are designed to match actual error messages, not plan content
150
+ const patterns = [
151
+ // "You've hit your limit" patterns - common Claude error
152
+ /You['']ve hit your limit[^.\n]*(?:\.[\s]*(?:resets?|try again)[^.\n]*)?/i,
153
+ // "Rate limit exceeded" - explicit error message
154
+ /rate limit exceeded[^.\n]*/i,
155
+ // "rate limited" as verb - "you have been rate limited"
156
+ /(?:you\s+(?:have\s+)?(?:been\s+)?)?rate\s+limited[^.\n]*/i,
157
+ // "too many requests" - HTTP 429 style
158
+ /too many requests[^.\n]*/i,
159
+ // "quota exceeded" - usage limit
160
+ /quota exceeded[^.\n]*/i,
161
+ // "API rate limit" - specific to API errors
162
+ /api\s+rate\s+limit[^.\n]*/i,
163
+ // "request limit" patterns
164
+ /request\s+limit[^.\n]*(?:reached|exceeded|hit)[^.\n]*/i,
165
+ // "usage limit" patterns
166
+ /usage\s+limit[^.\n]*(?:reached|exceeded|hit)[^.\n]*/i,
167
+ ];
168
+
169
+ for (const pattern of patterns) {
170
+ const match = content.match(pattern);
171
+ if (match) {
172
+ // Limit matched content to 200 chars to prevent capturing run-on text
173
+ const matched = match[0].trim();
174
+ return matched.length > 200 ? matched.slice(0, 197) + '...' : matched;
175
+ }
176
+ }
177
+
178
+ // If no pattern matches, try to find the first line that looks like an error
179
+ const lines = content.split('\n').filter(line => line.trim());
180
+ for (const line of lines.slice(0, 5)) {
181
+ const trimmedLine = line.trim();
182
+ // Look for lines that start with error indicators
183
+ if (/^(error|failed|limit|exceeded|denied)/i.test(trimmedLine)) {
184
+ return trimmedLine.length > 200 ? trimmedLine.slice(0, 197) + '...' : trimmedLine;
185
+ }
186
+ }
187
+
188
+ // If content is short, return it (but cap at 200 chars)
189
+ if (content.length < 200) {
190
+ return content;
191
+ }
192
+
193
+ // Otherwise return a generic message - don't include potentially huge content
194
+ return 'Rate limit detected (details unavailable)';
195
+ }
196
+
197
+ /**
198
+ * Check if an error indicates a rate limit
199
+ * Uses specific patterns to avoid false positives from plan content mentioning rate limiting
200
+ */
201
+ function isRateLimitError(error: unknown, message?: string): boolean {
202
+ // Patterns that indicate actual rate limit errors (not just mentions of rate limiting)
203
+ // These are more specific than just "rate limit" to avoid matching plan content
204
+ const rateLimitPatterns = [
205
+ /you['']ve hit your limit/i,
206
+ /rate_limit_exceeded/i,
207
+ /rate limit exceeded/i,
208
+ /you have been rate limited/i,
209
+ /too many requests/i,
210
+ /quota exceeded/i,
211
+ /\b429\b/, // HTTP 429 status code
212
+ /rate limited/i, // "rate limited" as a verb phrase
213
+ /api rate limit/i,
214
+ /request limit reached/i,
215
+ /usage limit exceeded/i,
216
+ /limit reached.*try again/i,
217
+ /exceeded.*limit.*retry/i,
218
+ ];
219
+
220
+ const checkString = (str: string): boolean => {
221
+ return rateLimitPatterns.some(pattern => pattern.test(str));
222
+ };
223
+
224
+ if (message && checkString(message)) return true;
225
+
226
+ if (error instanceof Error) {
227
+ if (checkString(error.message)) return true;
228
+ if ('code' in error && typeof error.code === 'string' && checkString(error.code)) return true;
229
+ }
230
+
231
+ if (typeof error === 'object' && error !== null) {
232
+ const obj = error as Record<string, unknown>;
233
+ if (typeof obj.error === 'string' && checkString(obj.error)) return true;
234
+ if (typeof obj.code === 'string' && checkString(obj.code)) return true;
235
+ if (typeof obj.message === 'string' && checkString(obj.message)) return true;
236
+ }
237
+
238
+ return false;
239
+ }
240
+
241
+ /**
242
+ * Write error details to a log file for debugging
243
+ */
244
+ async function logErrorDetails(
245
+ error: unknown,
246
+ context: {
247
+ prompt?: string;
248
+ lastMessages?: SDKMessage[];
249
+ response?: string;
250
+ }
251
+ ): Promise<string> {
252
+ try {
253
+ await fs.mkdir(LOG_DIR, { recursive: true });
254
+
255
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
256
+ const logFile = path.join(LOG_DIR, `claude-error-${timestamp}.log`);
257
+
258
+ const errorDetails = [
259
+ '='.repeat(80),
260
+ `CLAUDE ERROR LOG - ${new Date().toISOString()}`,
261
+ '='.repeat(80),
262
+ '',
263
+ '## Error Details',
264
+ `Type: ${error instanceof Error ? error.constructor.name : typeof error}`,
265
+ `Message: ${error instanceof Error ? error.message : String(error)}`,
266
+ '',
267
+ ];
268
+
269
+ if (error instanceof Error && error.stack) {
270
+ errorDetails.push('## Stack Trace', error.stack, '');
271
+ }
272
+
273
+ if (context.prompt) {
274
+ errorDetails.push(
275
+ '## Prompt (truncated)',
276
+ context.prompt.slice(0, 2000),
277
+ ''
278
+ );
279
+ }
280
+
281
+ if (context.response) {
282
+ errorDetails.push(
283
+ '## Response Before Error (truncated)',
284
+ context.response.slice(-2000),
285
+ ''
286
+ );
287
+ }
288
+
289
+ if (context.lastMessages && context.lastMessages.length > 0) {
290
+ errorDetails.push(
291
+ '## Last Messages',
292
+ JSON.stringify(context.lastMessages.slice(-5), null, 2),
293
+ ''
294
+ );
295
+ }
296
+
297
+ await fs.writeFile(logFile, errorDetails.join('\n'), 'utf-8');
298
+ return logFile;
299
+ } catch {
300
+ return '';
301
+ }
18
302
  }
19
303
 
20
304
  /**
@@ -53,29 +337,31 @@ export const DEFAULT_ALLOWED_TOOLS = [
53
337
  ];
54
338
 
55
339
  /**
56
- * Execute a prompt through the Claude Agent SDK
57
- *
58
- * @param prompt - The prompt to execute
59
- * @param options - Execution options
60
- * @returns The execution result
340
+ * Execute a prompt through the Claude Agent SDK (internal implementation)
61
341
  */
62
- export async function executePrompt(
342
+ async function executePromptInternal(
63
343
  prompt: string,
64
344
  options: ClaudeExecuteOptions = {}
65
- ): Promise<ClaudeExecuteResult> {
345
+ ): Promise<ClaudeExecuteResult & { rateLimitInfo?: { isRateLimit: boolean; resetTime?: Date; message?: string } }> {
66
346
  const {
67
347
  cwd,
68
348
  allowedTools = DEFAULT_ALLOWED_TOOLS,
69
349
  permissionMode = 'bypassPermissions',
70
350
  systemPrompt,
71
351
  onMessage,
352
+ onProgress,
72
353
  } = options;
73
354
 
74
355
  const toolCalls: ToolCallRecord[] = [];
356
+ const recentMessages: SDKMessage[] = [];
75
357
  let response = '';
76
358
  let error: string | undefined;
359
+ let rateLimitInfo: { isRateLimit: boolean; resetTime?: Date; message?: string } | undefined;
360
+ let lastProgressTime = Date.now();
77
361
 
78
362
  try {
363
+ onProgress?.('Connecting to Claude...');
364
+
79
365
  const result = query({
80
366
  prompt,
81
367
  options: {
@@ -86,49 +372,318 @@ export async function executePrompt(
86
372
  },
87
373
  });
88
374
 
375
+ onProgress?.('Claude is thinking...');
376
+
89
377
  for await (const message of result) {
378
+ // Keep track of recent messages for error logging
379
+ recentMessages.push(message);
380
+ if (recentMessages.length > 10) {
381
+ recentMessages.shift();
382
+ }
383
+
90
384
  // Call the message handler if provided
91
385
  if (onMessage) {
92
386
  onMessage(message);
93
387
  }
94
388
 
389
+ // Report progress based on message type
390
+ const now = Date.now();
391
+ if (now - lastProgressTime > 2000) {
392
+ // Report progress every 2 seconds
393
+ lastProgressTime = now;
394
+ }
395
+
396
+ // Check for rate limit in message error field
397
+ const messageWithError = message as { error?: string | { message?: string; code?: string }; message?: { content?: Array<{ text?: string }> } };
398
+ if (messageWithError.error) {
399
+ const errorStr = typeof messageWithError.error === 'string'
400
+ ? messageWithError.error
401
+ : messageWithError.error.message || '';
402
+
403
+ if (isRateLimitError(null, errorStr)) {
404
+ // Extract rate limit message from response content
405
+ let rateLimitMessage = errorStr;
406
+ if (messageWithError.message?.content) {
407
+ const textContent = messageWithError.message.content.find(c => c.text);
408
+ if (textContent?.text) {
409
+ rateLimitMessage = textContent.text;
410
+ }
411
+ }
412
+
413
+ const extractedMessage = extractRateLimitMessage(rateLimitMessage);
414
+ rateLimitInfo = {
415
+ isRateLimit: true,
416
+ resetTime: parseRateLimitResetTime(rateLimitMessage) ?? undefined,
417
+ message: extractedMessage,
418
+ };
419
+ error = `Rate limit exceeded: ${extractedMessage}`;
420
+ onProgress?.(`Rate limit hit: ${rateLimitMessage}`);
421
+ continue;
422
+ }
423
+ }
424
+
95
425
  // Process different message types
96
426
  if (message.type === 'assistant') {
97
427
  const assistantMessage = message as { type: 'assistant'; message: { content: string | Array<{ type: string; text?: string }> } };
98
428
  if (typeof assistantMessage.message.content === 'string') {
99
429
  response += assistantMessage.message.content;
430
+
431
+ // Check for rate limit in text response
432
+ if (isRateLimitError(null, assistantMessage.message.content)) {
433
+ const extractedMsg = extractRateLimitMessage(assistantMessage.message.content);
434
+ rateLimitInfo = {
435
+ isRateLimit: true,
436
+ resetTime: parseRateLimitResetTime(assistantMessage.message.content) ?? undefined,
437
+ message: extractedMsg,
438
+ };
439
+ error = `Rate limit exceeded: ${extractedMsg}`;
440
+ onProgress?.(`Rate limit hit: ${assistantMessage.message.content}`);
441
+ } else {
442
+ onProgress?.('Claude is writing...');
443
+ }
100
444
  } else if (Array.isArray(assistantMessage.message.content)) {
101
445
  for (const block of assistantMessage.message.content) {
102
446
  if (block.type === 'text' && block.text) {
103
447
  response += block.text;
448
+
449
+ // Check for rate limit in text block
450
+ if (isRateLimitError(null, block.text)) {
451
+ const extractedBlockMsg = extractRateLimitMessage(block.text);
452
+ rateLimitInfo = {
453
+ isRateLimit: true,
454
+ resetTime: parseRateLimitResetTime(block.text) ?? undefined,
455
+ message: extractedBlockMsg,
456
+ };
457
+ error = `Rate limit exceeded: ${extractedBlockMsg}`;
458
+ onProgress?.(`Rate limit hit: ${block.text}`);
459
+ }
460
+ }
461
+ if (block.type === 'tool_use') {
462
+ const toolBlock = block as { type: 'tool_use'; name?: string };
463
+ onProgress?.(`Using tool: ${toolBlock.name || 'unknown'}...`);
104
464
  }
105
465
  }
106
466
  }
107
467
  } else if (message.type === 'result') {
108
- // Handle result messages which may contain tool information
109
- const resultMessage = message as { type: 'result'; result?: string; error?: { message: string } };
110
- if (resultMessage.error) {
111
- error = resultMessage.error.message;
468
+ // Handle result messages which may contain tool information or errors
469
+ const resultMessage = message as {
470
+ type: 'result';
471
+ result?: string;
472
+ error?: { message?: string; code?: string };
473
+ subtype?: string;
474
+ };
475
+ if (resultMessage.error && !rateLimitInfo) {
476
+ const errMsg = resultMessage.error.message || 'Unknown error';
477
+ const errCode = resultMessage.error.code || 'ERROR';
478
+ error = `${errCode}: ${errMsg}`;
479
+ onProgress?.(`Claude returned error: ${error}`);
112
480
  }
113
481
  }
482
+
483
+ // Check for any error property on the message (handles various error formats)
484
+ if (messageWithError.error && !error && !rateLimitInfo) {
485
+ const errMsg = typeof messageWithError.error === 'string'
486
+ ? messageWithError.error
487
+ : messageWithError.error.message || 'Unknown error';
488
+ const errCode = typeof messageWithError.error === 'object'
489
+ ? messageWithError.error.code || 'ERROR'
490
+ : 'ERROR';
491
+ error = `${errCode}: ${errMsg}`;
492
+ onProgress?.(`Error detected: ${error}`);
493
+ }
114
494
  }
115
495
 
496
+ onProgress?.('Claude finished');
497
+
116
498
  return {
117
499
  success: !error,
118
500
  response: response.trim(),
119
501
  toolCalls,
120
502
  error,
503
+ rateLimitInfo,
121
504
  };
122
505
  } catch (err) {
506
+ // First, check if we already detected a rate limit during message processing
507
+ // This happens when rate limit is detected in the stream but process still exits with code 1
508
+ if (rateLimitInfo?.isRateLimit) {
509
+ onProgress?.(`Rate limit detected (process exited): ${rateLimitInfo.message || 'Unknown'}`);
510
+ return {
511
+ success: false,
512
+ response: response.trim(),
513
+ toolCalls,
514
+ error: `Rate limit exceeded: ${rateLimitInfo.message || 'Rate limit hit'}`,
515
+ rateLimitInfo,
516
+ };
517
+ }
518
+
519
+ // Check if the exception itself indicates a rate limit
520
+ const errMsg = err instanceof Error ? err.message : String(err);
521
+
522
+ if (isRateLimitError(err, errMsg) || isRateLimitError(null, response)) {
523
+ const combinedMessage = response || errMsg;
524
+ const extractedRateLimitMsg = extractRateLimitMessage(combinedMessage);
525
+ return {
526
+ success: false,
527
+ response: response.trim(),
528
+ toolCalls,
529
+ error: `Rate limit exceeded: ${extractedRateLimitMsg}`,
530
+ rateLimitInfo: {
531
+ isRateLimit: true,
532
+ resetTime: parseRateLimitResetTime(combinedMessage) ?? undefined,
533
+ message: extractedRateLimitMsg,
534
+ },
535
+ };
536
+ }
537
+
538
+ // Log detailed error information for debugging
539
+ const logFile = await logErrorDetails(err, {
540
+ prompt: prompt.slice(0, 5000),
541
+ lastMessages: recentMessages,
542
+ response: response.slice(-3000),
543
+ });
544
+
545
+ // Build a detailed error message
546
+ let errorMsg = err instanceof Error ? err.message : 'Unknown error executing prompt';
547
+
548
+ // Check for common error patterns and provide helpful messages
549
+ if (errorMsg.includes('exited with code 1')) {
550
+ errorMsg = `Claude Code process failed (exit code 1). `;
551
+ if (response) {
552
+ // Try to extract any error indicators from the response
553
+ const lastLines = response.split('\n').slice(-10).join('\n');
554
+ if (lastLines.includes('error') || lastLines.includes('Error') || lastLines.includes('failed')) {
555
+ errorMsg += `Last output: ${lastLines.slice(0, 500)}`;
556
+ }
557
+ }
558
+ if (logFile) {
559
+ errorMsg += ` Debug log: ${logFile}`;
560
+ }
561
+ } else if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ENOTFOUND')) {
562
+ errorMsg = 'Cannot connect to Claude Code CLI. Is it installed and running?';
563
+ } else if (errorMsg.includes('timeout') || errorMsg.includes('ETIMEDOUT')) {
564
+ errorMsg = 'Claude Code request timed out. The task may be too complex.';
565
+ } else if (errorMsg.includes('permission') || errorMsg.includes('Permission')) {
566
+ errorMsg = `Permission error: ${errorMsg}. Check tool permissions.`;
567
+ }
568
+
569
+ onProgress?.(`Error: ${errorMsg}`);
570
+
123
571
  return {
124
572
  success: false,
125
- response: '',
573
+ response: response.trim(),
126
574
  toolCalls,
127
- error: err instanceof Error ? err.message : 'Unknown error executing prompt',
575
+ error: errorMsg,
128
576
  };
129
577
  }
130
578
  }
131
579
 
580
+ /**
581
+ * Execute a prompt through the Claude Agent SDK with rate limit retry handling
582
+ *
583
+ * @param prompt - The prompt to execute
584
+ * @param options - Execution options
585
+ * @returns The execution result
586
+ */
587
+ export async function executePrompt(
588
+ prompt: string,
589
+ options: ClaudeExecuteOptions = {}
590
+ ): Promise<ClaudeExecuteResult> {
591
+ const { onProgress, rateLimitConfig: userRateLimitConfig } = options;
592
+
593
+ // If rate limit handling is disabled, run once without retry
594
+ if (userRateLimitConfig === false) {
595
+ const result = await executePromptInternal(prompt, options);
596
+ return {
597
+ success: result.success,
598
+ response: result.response,
599
+ toolCalls: result.toolCalls,
600
+ error: result.error,
601
+ };
602
+ }
603
+
604
+ // Merge user config with defaults
605
+ const rateLimitConfig: RateLimitConfig = {
606
+ ...DEFAULT_RATE_LIMIT_CONFIG,
607
+ ...userRateLimitConfig,
608
+ };
609
+
610
+ let attempt = 0;
611
+
612
+ while (attempt < rateLimitConfig.maxRetries) {
613
+ const result = await executePromptInternal(prompt, options);
614
+
615
+ // If no rate limit, return the result
616
+ if (!result.rateLimitInfo?.isRateLimit) {
617
+ return {
618
+ success: result.success,
619
+ response: result.response,
620
+ toolCalls: result.toolCalls,
621
+ error: result.error,
622
+ };
623
+ }
624
+
625
+ // Rate limit detected - calculate wait time
626
+ attempt++;
627
+
628
+ if (attempt >= rateLimitConfig.maxRetries) {
629
+ onProgress?.(`Rate limit: max retries (${rateLimitConfig.maxRetries}) exceeded`);
630
+ return {
631
+ success: false,
632
+ response: result.response,
633
+ toolCalls: result.toolCalls,
634
+ error: `Rate limit exceeded after ${attempt} retries. ${result.rateLimitInfo.message || ''}`,
635
+ };
636
+ }
637
+
638
+ // Calculate wait time
639
+ let waitMs: number;
640
+
641
+ if (result.rateLimitInfo.resetTime) {
642
+ // Use parsed reset time
643
+ waitMs = result.rateLimitInfo.resetTime.getTime() - Date.now();
644
+ // Add a small buffer
645
+ waitMs += 30_000;
646
+ } else {
647
+ // Use exponential backoff
648
+ waitMs = Math.min(
649
+ rateLimitConfig.baseWaitMs * Math.pow(2, attempt - 1),
650
+ rateLimitConfig.maxWaitMs
651
+ );
652
+ }
653
+
654
+ // Ensure minimum wait time
655
+ waitMs = Math.max(waitMs, 30_000);
656
+
657
+ // IMPORTANT: Cap wait time to maxWaitMs - don't wait hours for rate limits
658
+ if (waitMs > rateLimitConfig.maxWaitMs) {
659
+ onProgress?.(`Rate limit reset time is too far in the future (${formatWaitTime(waitMs)})`);
660
+ onProgress?.(`Maximum wait time is ${formatWaitTime(rateLimitConfig.maxWaitMs)}. Please try again later.`);
661
+ return {
662
+ success: false,
663
+ response: result.response,
664
+ toolCalls: result.toolCalls,
665
+ error: `Rate limit exceeded. Reset time is ${formatWaitTime(waitMs)} away - too long to wait. Please try again later.`,
666
+ };
667
+ }
668
+
669
+ onProgress?.(`Rate limit hit (attempt ${attempt}/${rateLimitConfig.maxRetries}). ${result.rateLimitInfo.message || ''}`);
670
+ onProgress?.(`Waiting ${formatWaitTime(waitMs)} before retry...`);
671
+
672
+ // Wait with progress updates
673
+ await sleepWithProgress(waitMs, onProgress);
674
+
675
+ onProgress?.(`Retrying after rate limit (attempt ${attempt + 1}/${rateLimitConfig.maxRetries})...`);
676
+ }
677
+
678
+ // Should not reach here, but just in case
679
+ return {
680
+ success: false,
681
+ response: '',
682
+ toolCalls: [],
683
+ error: 'Rate limit handling failed unexpectedly',
684
+ };
685
+ }
686
+
132
687
  /**
133
688
  * Execute code generation for a specific task
134
689
  *
@@ -202,8 +757,12 @@ After running the tests:
202
757
  * Analyze codebase to understand structure and patterns
203
758
  *
204
759
  * @param cwd - Working directory of the project
760
+ * @param onProgress - Progress callback
205
761
  */
206
- export async function analyzeCodebase(cwd: string): Promise<ClaudeExecuteResult> {
762
+ export async function analyzeCodebase(
763
+ cwd: string,
764
+ onProgress?: (message: string) => void
765
+ ): Promise<ClaudeExecuteResult> {
207
766
  const prompt = `
208
767
  Analyze this codebase and provide:
209
768
 
@@ -221,78 +780,539 @@ Be concise but thorough in your analysis.
221
780
  cwd,
222
781
  allowedTools: ['Read', 'Glob', 'Grep', 'LS'],
223
782
  permissionMode: 'default', // Read-only analysis
783
+ onProgress,
224
784
  });
225
785
  }
226
786
 
227
787
  /**
228
- * Create a development plan from a specification
788
+ * Extract plan file path from Claude's response
789
+ * Claude sometimes saves the plan to a file and responds with a summary
790
+ */
791
+ function extractPlanFilePath(response: string): string | null {
792
+ // Look for plan file paths like /Users/.../.claude/plans/...
793
+ const patterns = [
794
+ /`([^`]*\.claude\/plans\/[^`]+\.md)`/i,
795
+ /saved to\s+`?([^\s`]+\.claude\/plans\/[^\s`]+\.md)`?/i,
796
+ /created at\s+`?([^\s`]+\.claude\/plans\/[^\s`]+\.md)`?/i,
797
+ /plan.*at\s+`?([^\s`]+\.claude\/plans\/[^\s`]+\.md)`?/i,
798
+ /(\/[^\s]+\.claude\/plans\/[^\s]+\.md)/i,
799
+ ];
800
+
801
+ for (const pattern of patterns) {
802
+ const match = response.match(pattern);
803
+ if (match && match[1]) {
804
+ return match[1];
805
+ }
806
+ }
807
+
808
+ return null;
809
+ }
810
+
811
+ /**
812
+ * Check if response is Claude's thinking/conversation instead of actual plan
813
+ */
814
+ function isConversationalResponse(response: string): boolean {
815
+ const conversationalPhrases = [
816
+ 'let me ',
817
+ 'i will ',
818
+ 'i\'ll ',
819
+ 'now i have',
820
+ 'i now have',
821
+ 'let me launch',
822
+ 'let me create',
823
+ 'i\'ve created',
824
+ 'i\'ve analyzed',
825
+ 'has been created',
826
+ 'has been saved',
827
+ 'the plan is structured',
828
+ ];
829
+
830
+ const responseLower = response.toLowerCase();
831
+ return conversationalPhrases.some(phrase => responseLower.includes(phrase));
832
+ }
833
+
834
+ /**
835
+ * Build the appropriate prompt for plan creation based on project language
229
836
  *
230
837
  * @param specification - The project specification
231
- * @param context - Additional context (existing code, etc.)
838
+ * @param context - Additional context
839
+ * @param language - Target programming language
840
+ * @returns The prompt string
232
841
  */
233
- export async function createPlan(
842
+ function buildPlanPrompt(
234
843
  specification: string,
235
- context: string = ''
236
- ): Promise<ClaudeExecuteResult> {
237
- const prompt = `
238
- Create a detailed development plan for the following specification:
844
+ context: string,
845
+ language: 'python' | 'typescript' | 'fullstack'
846
+ ): string {
847
+ // Base instructions that apply to all projects
848
+ const baseInstructions = `
849
+ You are a software architect. Create a detailed, actionable development plan.
850
+
851
+ CRITICAL INSTRUCTION: You must output the COMPLETE plan content directly in your response as markdown.
852
+ Do NOT use tools to save the plan to a file.
853
+ Do NOT just describe what the plan contains - output the ACTUAL plan with all milestones and tasks.
854
+ Do NOT say "Let me...", "I will...", "I've created...", or any conversational text.
855
+
856
+ Start your response with "# Development Plan:" and include the FULL plan content.
857
+ `.trim();
858
+
859
+ // Fullstack-specific format with app tagging
860
+ if (language === 'fullstack') {
861
+ return `
862
+ ${baseInstructions}
863
+
864
+ ## Project Type: FULLSTACK MONOREPO
865
+ - **Frontend**: React + Vite + TypeScript + Tailwind CSS + shadcn/ui
866
+ - **Backend**: FastAPI (Python) + PostgreSQL
867
+ - **Structure**: Monorepo with apps/frontend and apps/backend
239
868
 
240
869
  ## Specification
241
870
  ${specification}
242
871
 
243
872
  ${context ? `## Additional Context\n${context}` : ''}
244
873
 
245
- ## Required Plan Sections
874
+ ## Required Plan Format for Fullstack Projects
875
+
876
+ Your response MUST be the complete plan in this EXACT format:
877
+
878
+ # Development Plan: [Project Name]
879
+
880
+ ## Overview
881
+ [2-3 sentence summary mentioning both frontend and backend]
246
882
 
247
- 1. **Background & Context**: Summarize the project requirements
248
- 2. **Goals & Objectives**: List measurable objectives
249
- 3. **Milestones**: Break down into major phases
250
- 4. **Tasks**: Detail specific tasks for each milestone
251
- 5. **Test Plan**: Define tests for each task
252
- 6. **Risks & Mitigations**: Identify potential issues
883
+ ## Architecture
884
+ - **Frontend App**: React SPA at apps/frontend/
885
+ - **Backend App**: FastAPI service at apps/backend/
886
+ - **Communication**: REST API (OpenAPI contract)
253
887
 
254
- Format the plan as markdown with clear sections and bullet points.
888
+ ---
889
+
890
+ ## Milestone 1: [Name]
891
+ **Description**: [What this milestone achieves]
892
+
893
+ ### Frontend Tasks
894
+
895
+ #### Task 1.1 [FE]: [Actionable task name]
896
+ **App**: frontend
897
+ **Files**:
898
+ - \`apps/frontend/src/components/...\`
899
+ - \`apps/frontend/src/pages/...\`
900
+ **Dependencies**: None
901
+ **Acceptance Criteria**:
902
+ - [ ] Criterion 1
903
+ - [ ] Criterion 2
904
+
905
+ ### Backend Tasks
906
+
907
+ #### Task 1.2 [BE]: [Actionable task name]
908
+ **App**: backend
909
+ **Files**:
910
+ - \`apps/backend/src/api/routes/...\`
911
+ - \`apps/backend/src/models/...\`
912
+ **Dependencies**: None
913
+ **Acceptance Criteria**:
914
+ - [ ] Criterion 1
915
+
916
+ ### Integration Tasks
917
+
918
+ #### Task 1.3 [INT]: [Actionable task name]
919
+ **App**: unified
920
+ **Dependencies**: Task 1.1, Task 1.2
921
+ **Acceptance Criteria**:
922
+ - [ ] Frontend calls backend API successfully
923
+ - [ ] E2E test passes
924
+
925
+ ---
926
+
927
+ ## Milestone 2: [Name]
928
+ [Continue same pattern...]
929
+
930
+ ---
931
+
932
+ ## Test Plan
933
+
934
+ ### Frontend Tests (apps/frontend)
935
+ - **Unit**: Vitest + Testing Library
936
+ - **E2E**: Playwright
937
+
938
+ ### Backend Tests (apps/backend)
939
+ - **Unit**: pytest
940
+ - **Integration**: pytest + TestClient
941
+
942
+ ### Integration Tests
943
+ - API contract validation
944
+ - E2E user flows
945
+
946
+ ## Risks & Mitigations
947
+ [Include frontend, backend, and integration risks separately]
948
+
949
+ ---
950
+
951
+ ## CRITICAL FULLSTACK REQUIREMENTS:
952
+ 1. **Tag every task** with [FE], [BE], or [INT]
953
+ 2. **Specify App field** for each task (frontend, backend, or unified)
954
+ 3. **List exact file paths** under apps/frontend/ or apps/backend/
955
+ 4. **Group tasks** under "Frontend Tasks", "Backend Tasks", or "Integration Tasks" headers
956
+ 5. **Include at least 3 milestones** with tasks distributed across FE/BE/INT
957
+ 6. Each task MUST start with an action verb: Implement, Create, Build, Add, Configure, Set up, Write, Design, etc.
958
+ 7. Each task MUST be specific and implementable
959
+
960
+ IMPORTANT: Output the COMPLETE plan now. Start with "# Development Plan:" on the first line.
255
961
  `.trim();
962
+ }
256
963
 
257
- return executePrompt(prompt, {
964
+ // Python-specific format
965
+ if (language === 'python') {
966
+ return `
967
+ ${baseInstructions}
968
+
969
+ ## Project Type: PYTHON
970
+ - **Language**: Python 3.11+
971
+ - **Framework**: FastAPI (if API) or CLI
972
+ - **Testing**: pytest
973
+
974
+ ## Specification
975
+ ${specification}
976
+
977
+ ${context ? `## Additional Context\n${context}` : ''}
978
+
979
+ ## Required Plan Format
980
+
981
+ Your response MUST be the complete plan in this EXACT format:
982
+
983
+ # Development Plan: [Project Name]
984
+
985
+ ## Overview
986
+ [2-3 sentence summary of what will be built]
987
+
988
+ ## Milestone 1: [Name]
989
+ **Description**: [What this milestone achieves]
990
+
991
+ ### Task 1.1: [Actionable task name starting with verb]
992
+ **Description**: [What this task accomplishes]
993
+ **Files to create/modify**: [List specific Python files in src/]
994
+ **Acceptance Criteria**:
995
+ - [Specific, testable criterion]
996
+ - [Another criterion]
997
+
998
+ ### Task 1.2: [Another actionable task]
999
+ ...
1000
+
1001
+ ## Milestone 2: [Name]
1002
+ ...
1003
+
1004
+ ## Test Plan
1005
+ - pytest for unit tests in tests/
1006
+ - httpx for API integration tests
1007
+
1008
+ ## Risks & Mitigations
1009
+ [Potential issues and how to address them]
1010
+
1011
+ ## Requirements for Tasks
1012
+
1013
+ 1. Each task MUST start with an action verb: Implement, Create, Build, Add, Configure, Set up, Write, Design, etc.
1014
+ 2. Each task MUST be specific and implementable
1015
+ 3. Each milestone MUST have at least 3-5 specific tasks
1016
+ 4. The plan MUST have at least 3 milestones for any non-trivial project
1017
+ 5. Files to create/modify MUST be listed for each task
1018
+ 6. Acceptance criteria MUST be testable
1019
+
1020
+ IMPORTANT: Output the COMPLETE plan now. Start with "# Development Plan:" on the first line.
1021
+ `.trim();
1022
+ }
1023
+
1024
+ // TypeScript/default format
1025
+ return `
1026
+ ${baseInstructions}
1027
+
1028
+ ## Project Type: TYPESCRIPT
1029
+ - **Language**: TypeScript
1030
+ - **Framework**: React + Vite (if frontend) or Node.js
1031
+ - **Testing**: Vitest
1032
+
1033
+ ## Specification
1034
+ ${specification}
1035
+
1036
+ ${context ? `## Additional Context\n${context}` : ''}
1037
+
1038
+ ## Required Plan Format
1039
+
1040
+ Your response MUST be the complete plan in this EXACT format:
1041
+
1042
+ # Development Plan: [Project Name]
1043
+
1044
+ ## Overview
1045
+ [2-3 sentence summary of what will be built]
1046
+
1047
+ ## Milestone 1: [Name]
1048
+ **Description**: [What this milestone achieves]
1049
+
1050
+ ### Task 1.1: [Actionable task name starting with verb]
1051
+ **Description**: [What this task accomplishes]
1052
+ **Files to create/modify**: [List specific TypeScript files in src/]
1053
+ **Acceptance Criteria**:
1054
+ - [Specific, testable criterion]
1055
+ - [Another criterion]
1056
+
1057
+ ### Task 1.2: [Another actionable task]
1058
+ ...
1059
+
1060
+ ## Milestone 2: [Name]
1061
+ ...
1062
+
1063
+ ## Test Plan
1064
+ - Vitest for unit tests
1065
+ - Playwright for E2E tests
1066
+
1067
+ ## Risks & Mitigations
1068
+ [Potential issues and how to address them]
1069
+
1070
+ ## Requirements for Tasks
1071
+
1072
+ 1. Each task MUST start with an action verb: Implement, Create, Build, Add, Configure, Set up, Write, Design, etc.
1073
+ 2. Each task MUST be specific and implementable
1074
+ 3. Each milestone MUST have at least 3-5 specific tasks
1075
+ 4. The plan MUST have at least 3 milestones for any non-trivial project
1076
+ 5. Files to create/modify MUST be listed for each task
1077
+ 6. Acceptance criteria MUST be testable
1078
+
1079
+ IMPORTANT: Output the COMPLETE plan now. Start with "# Development Plan:" on the first line.
1080
+ `.trim();
1081
+ }
1082
+
1083
+ /**
1084
+ * Create a development plan from a specification
1085
+ *
1086
+ * @param specification - The project specification
1087
+ * @param context - Additional context (existing code, etc.)
1088
+ * @param language - Target programming language (default: 'python')
1089
+ * @param onProgress - Progress callback
1090
+ */
1091
+ export async function createPlan(
1092
+ specification: string,
1093
+ context: string = '',
1094
+ language: 'python' | 'typescript' | 'fullstack' = 'python',
1095
+ onProgress?: (message: string) => void
1096
+ ): Promise<ClaudeExecuteResult> {
1097
+ const prompt = buildPlanPrompt(specification, context, language);
1098
+
1099
+ const result = await executePrompt(prompt, {
258
1100
  allowedTools: ['Read', 'Glob'],
259
1101
  permissionMode: 'plan',
1102
+ onProgress,
260
1103
  });
1104
+
1105
+ // If Claude's response is conversational (describes the plan but doesn't contain it),
1106
+ // try to extract the plan from the file it may have created
1107
+ if (result.success && isConversationalResponse(result.response)) {
1108
+ onProgress?.('Detected conversational response, looking for plan file...');
1109
+
1110
+ // Try to find and read the plan file
1111
+ const planFilePath = extractPlanFilePath(result.response);
1112
+
1113
+ if (planFilePath) {
1114
+ try {
1115
+ onProgress?.(`Found plan file reference: ${planFilePath}`);
1116
+ const planContent = await fs.readFile(planFilePath, 'utf-8');
1117
+
1118
+ // Verify the plan content is actually a plan
1119
+ if (planContent.includes('# Development Plan') ||
1120
+ planContent.includes('## Milestone') ||
1121
+ planContent.includes('### Task')) {
1122
+ onProgress?.('Successfully extracted plan from file');
1123
+ return {
1124
+ ...result,
1125
+ response: planContent,
1126
+ };
1127
+ }
1128
+ } catch (readError) {
1129
+ onProgress?.(`Could not read plan file: ${readError instanceof Error ? readError.message : 'Unknown error'}`);
1130
+ }
1131
+ }
1132
+
1133
+ // Also try to find any recent .claude/plans files
1134
+ try {
1135
+ const claudePlansDir = path.join(homedir(), '.claude', 'plans');
1136
+ const files = await fs.readdir(claudePlansDir);
1137
+ const mdFiles = files.filter(f => f.endsWith('.md'));
1138
+
1139
+ if (mdFiles.length > 0) {
1140
+ // Sort by modification time (most recent first)
1141
+ const fileStats = await Promise.all(
1142
+ mdFiles.map(async f => {
1143
+ const filePath = path.join(claudePlansDir, f);
1144
+ const stat = await fs.stat(filePath);
1145
+ return { name: f, path: filePath, mtime: stat.mtime };
1146
+ })
1147
+ );
1148
+
1149
+ fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1150
+
1151
+ // Check the most recent file (created in the last 5 minutes)
1152
+ const recentFile = fileStats[0];
1153
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
1154
+
1155
+ if (recentFile.mtime.getTime() > fiveMinutesAgo) {
1156
+ onProgress?.(`Found recent plan file: ${recentFile.name}`);
1157
+ const planContent = await fs.readFile(recentFile.path, 'utf-8');
1158
+
1159
+ if (planContent.includes('# Development Plan') ||
1160
+ planContent.includes('## Milestone') ||
1161
+ planContent.includes('### Task')) {
1162
+ onProgress?.('Successfully extracted plan from recent file');
1163
+ return {
1164
+ ...result,
1165
+ response: planContent,
1166
+ };
1167
+ }
1168
+ }
1169
+ }
1170
+ } catch {
1171
+ // Could not access .claude/plans directory
1172
+ }
1173
+
1174
+ // Log warning that we couldn't extract the plan
1175
+ onProgress?.('WARNING: Could not extract actual plan content from file');
1176
+ }
1177
+
1178
+ return result;
261
1179
  }
262
1180
 
263
1181
  /**
264
- * Revise a plan based on feedback
265
- *
266
- * @param originalPlan - The original plan
267
- * @param feedback - Feedback to incorporate
268
- * @param concerns - Specific concerns to address
1182
+ * Build revision prompt with language-specific instructions
269
1183
  */
270
- export async function revisePlan(
1184
+ function buildRevisionPrompt(
271
1185
  originalPlan: string,
272
1186
  feedback: string,
273
- concerns: string[]
274
- ): Promise<ClaudeExecuteResult> {
275
- const prompt = `
276
- Revise the following plan based on the feedback provided:
1187
+ concerns: string[],
1188
+ language: 'python' | 'typescript' | 'fullstack'
1189
+ ): string {
1190
+ const basePrompt = `
1191
+ CRITICAL: You must output the COMPLETE revised plan in your response.
1192
+ Do NOT describe what you changed - output the FULL plan with all changes incorporated.
1193
+ Do NOT say "Let me...", "I will...", "I've revised...", or any conversational text.
1194
+ Start your response directly with "# Development Plan:" and include the ENTIRE revised plan.
277
1195
 
278
- ## Original Plan
1196
+ ## Original Plan to Revise
279
1197
  ${originalPlan}
280
1198
 
281
- ## Feedback
1199
+ ## Feedback to Address
282
1200
  ${feedback}
283
1201
 
284
1202
  ## Specific Concerns to Address
285
1203
  ${concerns.map((c, i) => `${i + 1}. ${c}`).join('\n')}
286
1204
 
287
1205
  ## Instructions
288
- 1. Address each concern specifically
289
- 2. Maintain the same plan structure
290
- 3. Note what changed from the original
291
- 4. Ensure the revised plan is complete and actionable
1206
+ 1. Address each concern by incorporating changes into the plan
1207
+ 2. Maintain the same plan structure (Overview, Milestones, Tasks, Test Plan, Risks)
1208
+ 3. Output the COMPLETE revised plan - not just the changes
1209
+ 4. Start with "# Development Plan:" and include ALL milestones and tasks
292
1210
  `.trim();
293
1211
 
294
- return executePrompt(prompt, {
1212
+ if (language === 'fullstack') {
1213
+ return `
1214
+ ${basePrompt}
1215
+
1216
+ ## FULLSTACK-SPECIFIC REQUIREMENTS:
1217
+ - Maintain [FE], [BE], [INT] tags on all tasks
1218
+ - Keep App: field (frontend/backend/unified) for each task
1219
+ - Group tasks under "Frontend Tasks", "Backend Tasks", "Integration Tasks" headers
1220
+ - Ensure file paths use apps/frontend/ or apps/backend/ prefixes
1221
+ - If adding new tasks, tag them appropriately
1222
+
1223
+ OUTPUT THE COMPLETE REVISED PLAN NOW:
1224
+ `.trim();
1225
+ }
1226
+
1227
+ return `${basePrompt}
1228
+
1229
+ OUTPUT THE COMPLETE REVISED PLAN NOW:
1230
+ `.trim();
1231
+ }
1232
+
1233
+ /**
1234
+ * Revise a plan based on feedback
1235
+ *
1236
+ * @param originalPlan - The original plan
1237
+ * @param feedback - Feedback to incorporate
1238
+ * @param concerns - Specific concerns to address
1239
+ * @param language - Target programming language (default: 'python')
1240
+ * @param onProgress - Progress callback
1241
+ */
1242
+ export async function revisePlan(
1243
+ originalPlan: string,
1244
+ feedback: string,
1245
+ concerns: string[],
1246
+ language: 'python' | 'typescript' | 'fullstack' = 'python',
1247
+ onProgress?: (message: string) => void
1248
+ ): Promise<ClaudeExecuteResult> {
1249
+ const prompt = buildRevisionPrompt(originalPlan, feedback, concerns, language);
1250
+
1251
+ onProgress?.('Claude is revising the plan...');
1252
+
1253
+ const result = await executePrompt(prompt, {
295
1254
  allowedTools: [],
296
1255
  permissionMode: 'plan',
1256
+ onProgress,
297
1257
  });
1258
+
1259
+ // Check if response is conversational and try to extract actual plan
1260
+ if (result.success && isConversationalResponse(result.response)) {
1261
+ // Try to find the plan file
1262
+ const planFilePath = extractPlanFilePath(result.response);
1263
+
1264
+ if (planFilePath) {
1265
+ try {
1266
+ const planContent = await fs.readFile(planFilePath, 'utf-8');
1267
+ if (planContent.includes('# Development Plan') ||
1268
+ planContent.includes('## Milestone') ||
1269
+ planContent.includes('### Task')) {
1270
+ return {
1271
+ ...result,
1272
+ response: planContent,
1273
+ };
1274
+ }
1275
+ } catch {
1276
+ // Could not read file, fall through
1277
+ }
1278
+ }
1279
+
1280
+ // Try recent .claude/plans files
1281
+ try {
1282
+ const claudePlansDir = path.join(homedir(), '.claude', 'plans');
1283
+ const files = await fs.readdir(claudePlansDir);
1284
+ const mdFiles = files.filter(f => f.endsWith('.md'));
1285
+
1286
+ if (mdFiles.length > 0) {
1287
+ const fileStats = await Promise.all(
1288
+ mdFiles.map(async f => {
1289
+ const filePath = path.join(claudePlansDir, f);
1290
+ const stat = await fs.stat(filePath);
1291
+ return { name: f, path: filePath, mtime: stat.mtime };
1292
+ })
1293
+ );
1294
+
1295
+ fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1296
+
1297
+ const recentFile = fileStats[0];
1298
+ const twoMinutesAgo = Date.now() - 2 * 60 * 1000;
1299
+
1300
+ if (recentFile.mtime.getTime() > twoMinutesAgo) {
1301
+ const planContent = await fs.readFile(recentFile.path, 'utf-8');
1302
+ if (planContent.includes('# Development Plan') ||
1303
+ planContent.includes('## Milestone') ||
1304
+ planContent.includes('### Task')) {
1305
+ return {
1306
+ ...result,
1307
+ response: planContent,
1308
+ };
1309
+ }
1310
+ }
1311
+ }
1312
+ } catch {
1313
+ // Could not access .claude/plans directory
1314
+ }
1315
+ }
1316
+
1317
+ return result;
298
1318
  }