popeye-cli 1.0.0 → 1.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.
Files changed (171) hide show
  1. package/README.md +521 -125
  2. package/dist/adapters/claude.d.ts +16 -4
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +679 -33
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/gemini.d.ts +55 -0
  7. package/dist/adapters/gemini.d.ts.map +1 -0
  8. package/dist/adapters/gemini.js +318 -0
  9. package/dist/adapters/gemini.js.map +1 -0
  10. package/dist/adapters/openai.d.ts.map +1 -1
  11. package/dist/adapters/openai.js +41 -7
  12. package/dist/adapters/openai.js.map +1 -1
  13. package/dist/auth/claude.d.ts +11 -9
  14. package/dist/auth/claude.d.ts.map +1 -1
  15. package/dist/auth/claude.js +107 -71
  16. package/dist/auth/claude.js.map +1 -1
  17. package/dist/auth/gemini.d.ts +58 -0
  18. package/dist/auth/gemini.d.ts.map +1 -0
  19. package/dist/auth/gemini.js +172 -0
  20. package/dist/auth/gemini.js.map +1 -0
  21. package/dist/auth/index.d.ts +11 -7
  22. package/dist/auth/index.d.ts.map +1 -1
  23. package/dist/auth/index.js +23 -5
  24. package/dist/auth/index.js.map +1 -1
  25. package/dist/auth/keychain.d.ts +20 -7
  26. package/dist/auth/keychain.d.ts.map +1 -1
  27. package/dist/auth/keychain.js +85 -29
  28. package/dist/auth/keychain.js.map +1 -1
  29. package/dist/auth/openai.d.ts +2 -2
  30. package/dist/auth/openai.d.ts.map +1 -1
  31. package/dist/auth/openai.js +30 -32
  32. package/dist/auth/openai.js.map +1 -1
  33. package/dist/cli/index.d.ts.map +1 -1
  34. package/dist/cli/index.js +4 -7
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/interactive.d.ts +2 -2
  37. package/dist/cli/interactive.d.ts.map +1 -1
  38. package/dist/cli/interactive.js +1380 -183
  39. package/dist/cli/interactive.js.map +1 -1
  40. package/dist/config/defaults.d.ts +6 -1
  41. package/dist/config/defaults.d.ts.map +1 -1
  42. package/dist/config/defaults.js +10 -2
  43. package/dist/config/defaults.js.map +1 -1
  44. package/dist/config/index.d.ts +10 -0
  45. package/dist/config/index.d.ts.map +1 -1
  46. package/dist/config/index.js +19 -0
  47. package/dist/config/index.js.map +1 -1
  48. package/dist/config/schema.d.ts +20 -0
  49. package/dist/config/schema.d.ts.map +1 -1
  50. package/dist/config/schema.js +7 -0
  51. package/dist/config/schema.js.map +1 -1
  52. package/dist/generators/python.d.ts.map +1 -1
  53. package/dist/generators/python.js +1 -0
  54. package/dist/generators/python.js.map +1 -1
  55. package/dist/generators/typescript.d.ts.map +1 -1
  56. package/dist/generators/typescript.js +1 -0
  57. package/dist/generators/typescript.js.map +1 -1
  58. package/dist/state/index.d.ts +108 -0
  59. package/dist/state/index.d.ts.map +1 -1
  60. package/dist/state/index.js +551 -4
  61. package/dist/state/index.js.map +1 -1
  62. package/dist/state/registry.d.ts +52 -0
  63. package/dist/state/registry.d.ts.map +1 -0
  64. package/dist/state/registry.js +215 -0
  65. package/dist/state/registry.js.map +1 -0
  66. package/dist/types/cli.d.ts +4 -0
  67. package/dist/types/cli.d.ts.map +1 -1
  68. package/dist/types/cli.js.map +1 -1
  69. package/dist/types/consensus.d.ts +69 -4
  70. package/dist/types/consensus.d.ts.map +1 -1
  71. package/dist/types/consensus.js +24 -3
  72. package/dist/types/consensus.js.map +1 -1
  73. package/dist/types/workflow.d.ts +55 -0
  74. package/dist/types/workflow.d.ts.map +1 -1
  75. package/dist/types/workflow.js +16 -0
  76. package/dist/types/workflow.js.map +1 -1
  77. package/dist/workflow/auto-fix.d.ts +45 -0
  78. package/dist/workflow/auto-fix.d.ts.map +1 -0
  79. package/dist/workflow/auto-fix.js +274 -0
  80. package/dist/workflow/auto-fix.js.map +1 -0
  81. package/dist/workflow/consensus.d.ts +44 -2
  82. package/dist/workflow/consensus.d.ts.map +1 -1
  83. package/dist/workflow/consensus.js +565 -17
  84. package/dist/workflow/consensus.js.map +1 -1
  85. package/dist/workflow/execution-mode.d.ts +10 -4
  86. package/dist/workflow/execution-mode.d.ts.map +1 -1
  87. package/dist/workflow/execution-mode.js +547 -58
  88. package/dist/workflow/execution-mode.js.map +1 -1
  89. package/dist/workflow/index.d.ts +14 -2
  90. package/dist/workflow/index.d.ts.map +1 -1
  91. package/dist/workflow/index.js +69 -6
  92. package/dist/workflow/index.js.map +1 -1
  93. package/dist/workflow/milestone-workflow.d.ts +34 -0
  94. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  95. package/dist/workflow/milestone-workflow.js +414 -0
  96. package/dist/workflow/milestone-workflow.js.map +1 -0
  97. package/dist/workflow/plan-mode.d.ts +14 -1
  98. package/dist/workflow/plan-mode.d.ts.map +1 -1
  99. package/dist/workflow/plan-mode.js +589 -47
  100. package/dist/workflow/plan-mode.js.map +1 -1
  101. package/dist/workflow/plan-storage.d.ts +142 -0
  102. package/dist/workflow/plan-storage.d.ts.map +1 -0
  103. package/dist/workflow/plan-storage.js +331 -0
  104. package/dist/workflow/plan-storage.js.map +1 -0
  105. package/dist/workflow/project-verification.d.ts +37 -0
  106. package/dist/workflow/project-verification.d.ts.map +1 -0
  107. package/dist/workflow/project-verification.js +381 -0
  108. package/dist/workflow/project-verification.js.map +1 -0
  109. package/dist/workflow/task-workflow.d.ts +37 -0
  110. package/dist/workflow/task-workflow.d.ts.map +1 -0
  111. package/dist/workflow/task-workflow.js +383 -0
  112. package/dist/workflow/task-workflow.js.map +1 -0
  113. package/dist/workflow/test-runner.d.ts +1 -0
  114. package/dist/workflow/test-runner.d.ts.map +1 -1
  115. package/dist/workflow/test-runner.js +9 -5
  116. package/dist/workflow/test-runner.js.map +1 -1
  117. package/dist/workflow/ui-designer.d.ts +82 -0
  118. package/dist/workflow/ui-designer.d.ts.map +1 -0
  119. package/dist/workflow/ui-designer.js +234 -0
  120. package/dist/workflow/ui-designer.js.map +1 -0
  121. package/dist/workflow/ui-setup.d.ts +58 -0
  122. package/dist/workflow/ui-setup.d.ts.map +1 -0
  123. package/dist/workflow/ui-setup.js +685 -0
  124. package/dist/workflow/ui-setup.js.map +1 -0
  125. package/dist/workflow/ui-verification.d.ts +114 -0
  126. package/dist/workflow/ui-verification.d.ts.map +1 -0
  127. package/dist/workflow/ui-verification.js +258 -0
  128. package/dist/workflow/ui-verification.js.map +1 -0
  129. package/dist/workflow/workflow-logger.d.ts +110 -0
  130. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  131. package/dist/workflow/workflow-logger.js +267 -0
  132. package/dist/workflow/workflow-logger.js.map +1 -0
  133. package/package.json +2 -2
  134. package/src/adapters/claude.ts +815 -34
  135. package/src/adapters/gemini.ts +373 -0
  136. package/src/adapters/openai.ts +40 -7
  137. package/src/auth/claude.ts +120 -78
  138. package/src/auth/gemini.ts +207 -0
  139. package/src/auth/index.ts +28 -8
  140. package/src/auth/keychain.ts +95 -28
  141. package/src/auth/openai.ts +29 -36
  142. package/src/cli/index.ts +4 -7
  143. package/src/cli/interactive.ts +1641 -216
  144. package/src/config/defaults.ts +10 -2
  145. package/src/config/index.ts +21 -0
  146. package/src/config/schema.ts +7 -0
  147. package/src/generators/python.ts +1 -0
  148. package/src/generators/typescript.ts +1 -0
  149. package/src/state/index.ts +713 -4
  150. package/src/state/registry.ts +278 -0
  151. package/src/types/cli.ts +4 -0
  152. package/src/types/consensus.ts +65 -6
  153. package/src/types/workflow.ts +35 -0
  154. package/src/workflow/auto-fix.ts +340 -0
  155. package/src/workflow/consensus.ts +750 -16
  156. package/src/workflow/execution-mode.ts +673 -74
  157. package/src/workflow/index.ts +95 -6
  158. package/src/workflow/milestone-workflow.ts +576 -0
  159. package/src/workflow/plan-mode.ts +696 -50
  160. package/src/workflow/plan-storage.ts +482 -0
  161. package/src/workflow/project-verification.ts +471 -0
  162. package/src/workflow/task-workflow.ts +525 -0
  163. package/src/workflow/test-runner.ts +10 -5
  164. package/src/workflow/ui-designer.ts +337 -0
  165. package/src/workflow/ui-setup.ts +797 -0
  166. package/src/workflow/ui-verification.ts +357 -0
  167. package/src/workflow/workflow-logger.ts +353 -0
  168. package/tests/config/config.test.ts +1 -1
  169. package/tests/types/consensus.test.ts +3 -3
  170. package/tests/workflow/plan-mode.test.ts +213 -0
  171. package/tests/workflow/test-runner.test.ts +5 -3
@@ -19,6 +19,8 @@ import {
19
19
  addMilestones,
20
20
  } from '../state/index.js';
21
21
  import { iterateUntilConsensus, type ConsensusProcessResult } from './consensus.js';
22
+ import { getWorkflowLogger } from './workflow-logger.js';
23
+ import { designUI, saveUISpecification } from './ui-designer.js';
22
24
 
23
25
  /**
24
26
  * Options for plan mode
@@ -26,6 +28,7 @@ import { iterateUntilConsensus, type ConsensusProcessResult } from './consensus.
26
28
  export interface PlanModeOptions {
27
29
  projectDir: string;
28
30
  consensusConfig?: Partial<ConsensusConfig>;
31
+ additionalContext?: string;
29
32
  onProgress?: (phase: string, message: string) => void;
30
33
  }
31
34
 
@@ -75,7 +78,7 @@ export async function createPlan(
75
78
  ): Promise<string> {
76
79
  onProgress?.('Creating development plan...');
77
80
 
78
- const result = await claudeCreatePlan(specification, context);
81
+ const result = await claudeCreatePlan(specification, context, onProgress);
79
82
 
80
83
  if (!result.success) {
81
84
  throw new Error(`Failed to create plan: ${result.error}`);
@@ -110,7 +113,7 @@ export async function getProjectContext(
110
113
  return 'New project - no existing codebase';
111
114
  }
112
115
 
113
- const result = await analyzeCodebase(projectDir);
116
+ const result = await analyzeCodebase(projectDir, onProgress);
114
117
 
115
118
  if (result.success) {
116
119
  onProgress?.('Codebase analysis complete');
@@ -124,7 +127,7 @@ export async function getProjectContext(
124
127
  }
125
128
 
126
129
  /**
127
- * Save the plan to a markdown file
130
+ * Save the plan to a markdown file in docs folder
128
131
  *
129
132
  * @param projectDir - The project directory
130
133
  * @param plan - The plan content
@@ -135,7 +138,15 @@ export async function documentPlan(
135
138
  plan: string,
136
139
  filename: string = 'PLAN.md'
137
140
  ): Promise<string> {
138
- const planPath = path.join(projectDir, filename);
141
+ // Create docs directory if it doesn't exist
142
+ const docsDir = path.join(projectDir, 'docs');
143
+ try {
144
+ await fs.mkdir(docsDir, { recursive: true });
145
+ } catch {
146
+ // Directory might already exist
147
+ }
148
+
149
+ const planPath = path.join(docsDir, filename);
139
150
 
140
151
  const content = `# Development Plan
141
152
 
@@ -145,11 +156,174 @@ ${plan}
145
156
  `;
146
157
 
147
158
  await fs.writeFile(planPath, content, 'utf-8');
159
+
160
+ // Also save a timestamped version for history
161
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
162
+ const historyFilename = `PLAN-${timestamp}.md`;
163
+ const historyPath = path.join(docsDir, historyFilename);
164
+ await fs.writeFile(historyPath, content, 'utf-8');
165
+
148
166
  return planPath;
149
167
  }
150
168
 
169
+ /**
170
+ * Check if a task name represents an actionable implementation task
171
+ * Tasks should start with verbs like: Implement, Create, Build, Set up, Add, etc.
172
+ *
173
+ * @param name - The potential task name
174
+ * @returns True if this looks like an implementation task
175
+ */
176
+ function isActionableTask(name: string): boolean {
177
+ const nameLower = name.toLowerCase().trim();
178
+
179
+ // Actionable verb prefixes that indicate real implementation tasks
180
+ const actionableVerbs = [
181
+ 'implement', 'create', 'build', 'develop', 'write', 'add', 'set up', 'setup',
182
+ 'configure', 'install', 'integrate', 'design', 'define', 'establish',
183
+ 'generate', 'construct', 'deploy', 'test', 'validate', 'fix', 'update',
184
+ 'refactor', 'optimize', 'extend', 'enhance', 'modify', 'initialize',
185
+ 'bootstrap', 'scaffold', 'connect', 'wire', 'hook', 'enable', 'disable',
186
+ ];
187
+
188
+ // Check if starts with an actionable verb
189
+ const startsWithAction = actionableVerbs.some((verb) =>
190
+ nameLower.startsWith(verb + ' ') || nameLower.startsWith(verb + ':')
191
+ );
192
+
193
+ // Non-actionable patterns to exclude (plan metadata, not tasks)
194
+ const nonActionablePatterns = [
195
+ /^(background|context|overview|introduction|summary)/i,
196
+ /^(goal|objective|requirement|constraint|assumption|risk)/i,
197
+ /^(timeline|schedule|estimate|duration|deadline)/i,
198
+ /^(note|example|reference|appendix|glossary)/i,
199
+ /^(file structure|project structure|directory)/i,
200
+ /^(showing|displays?|contains?|includes?|describes?)/i,
201
+ /^(the |this |a |an )/i, // Descriptions, not actions
202
+ /^\d+[-.]?\s*(week|day|hour|month)/i, // Time estimates
203
+ /^([\w\s]+):$/, // Labels ending with colon
204
+ ];
205
+
206
+ const isNonActionable = nonActionablePatterns.some((pattern) => pattern.test(nameLower));
207
+
208
+ return startsWithAction && !isNonActionable;
209
+ }
210
+
211
+ /**
212
+ * Extract task description from content following a task header
213
+ *
214
+ * @param content - Content following the task header
215
+ * @returns Extracted description
216
+ */
217
+ function extractTaskDescription(content: string): string {
218
+ // Look for Description field or first paragraph
219
+ const descMatch = content.match(/\*\*Description\*\*:\s*(.+?)(?=\n\*\*|\n###|\n##|$)/is);
220
+ if (descMatch) {
221
+ return descMatch[1].trim().slice(0, 500);
222
+ }
223
+
224
+ // Use first non-empty line
225
+ const lines = content.split('\n').filter((l) => l.trim() && !l.trim().startsWith('-'));
226
+ if (lines.length > 0) {
227
+ return lines[0].trim().slice(0, 500);
228
+ }
229
+
230
+ return '';
231
+ }
232
+
233
+ /**
234
+ * Extract acceptance criteria from task content
235
+ *
236
+ * @param content - Task content
237
+ * @returns Array of acceptance criteria
238
+ */
239
+ function extractAcceptanceCriteria(content: string): string[] {
240
+ const criteria: string[] = [];
241
+
242
+ // Look for Acceptance Criteria section
243
+ const acMatch = content.match(/\*\*Acceptance Criteria\*\*:?\s*([\s\S]+?)(?=\n\*\*|\n###|\n##|$)/i);
244
+ if (acMatch) {
245
+ const acContent = acMatch[1];
246
+ const bulletMatch = acContent.match(/^[-*]\s+(.+)$/gm);
247
+ if (bulletMatch) {
248
+ for (const bullet of bulletMatch) {
249
+ const cleaned = bullet.replace(/^[-*]\s+/, '').trim();
250
+ if (cleaned.length > 5) {
251
+ criteria.push(cleaned);
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ return criteria;
258
+ }
259
+
260
+ /**
261
+ * Detect if a plan is actually Claude's thinking/conversation instead of a real plan
262
+ * This happens when Claude outputs its reasoning instead of the plan content
263
+ *
264
+ * @param plan - The plan content
265
+ * @returns Object indicating if garbage and why
266
+ */
267
+ export function detectGarbagePlan(plan: string): { isGarbage: boolean; reason?: string } {
268
+ const planLower = plan.toLowerCase();
269
+
270
+ // Phrases that indicate Claude's thinking, not actual plan content
271
+ const garbagePhrases = [
272
+ 'let me ',
273
+ 'i will ',
274
+ 'i\'ll ',
275
+ 'now i have',
276
+ 'i now have',
277
+ 'let me launch',
278
+ 'let me create',
279
+ 'let me write',
280
+ 'comprehensive understanding',
281
+ 'let me analyze',
282
+ 'based on my analysis',
283
+ 'before i proceed',
284
+ 'i\'ve created',
285
+ 'i\'ve analyzed',
286
+ 'i should',
287
+ 'i need to',
288
+ 'first, i',
289
+ 'the plan is saved',
290
+ 'saved to',
291
+ 'saved at',
292
+ ];
293
+
294
+ for (const phrase of garbagePhrases) {
295
+ if (planLower.includes(phrase)) {
296
+ return {
297
+ isGarbage: true,
298
+ reason: `Plan contains Claude's thinking ("${phrase}") instead of actual plan content`,
299
+ };
300
+ }
301
+ }
302
+
303
+ // Check if plan has actual structure
304
+ const hasTaskHeaders = /^#{2,4}\s*Task\s+[\d.]+/im.test(plan);
305
+ const hasMilestoneHeaders = /^#{1,3}\s*Milestone\s+\d/im.test(plan);
306
+ const hasActionableBullets = /^[-*]\s+(implement|create|build|add|set up|configure|design|write)/im.test(plan);
307
+
308
+ if (!hasTaskHeaders && !hasMilestoneHeaders && !hasActionableBullets) {
309
+ // Check if it at least has some structure
310
+ const hasAnyHeaders = /^#{1,4}\s+.+$/m.test(plan);
311
+ const hasBulletPoints = /^[-*+]\s+.+$/m.test(plan);
312
+
313
+ if (!hasAnyHeaders && !hasBulletPoints) {
314
+ return {
315
+ isGarbage: true,
316
+ reason: 'Plan has no recognizable structure (no headers, no bullet points)',
317
+ };
318
+ }
319
+ }
320
+
321
+ return { isGarbage: false };
322
+ }
323
+
151
324
  /**
152
325
  * Parse milestones and tasks from a plan
326
+ * Extracts only actionable implementation tasks, not plan metadata
153
327
  *
154
328
  * @param plan - The plan content
155
329
  * @returns Parsed milestones with tasks
@@ -157,54 +331,278 @@ ${plan}
157
331
  export function parsePlanMilestones(plan: string): Omit<Milestone, 'id'>[] {
158
332
  const milestones: Omit<Milestone, 'id'>[] = [];
159
333
 
160
- // Look for milestone sections
161
- const milestonePattern = /#+\s*(?:Milestone\s*\d+[:\s]*)?([^\n]+)\n([\s\S]*?)(?=#+\s*(?:Milestone|$)|$)/gi;
162
- const taskPattern = /[-*]\s*(?:\[[ x]\]\s*)?(?:Task[:\s]*)?(.+)/gi;
163
-
164
- let match;
165
- while ((match = milestonePattern.exec(plan)) !== null) {
166
- const name = match[1].trim();
167
- const content = match[2];
168
-
169
- // Skip non-milestone sections
170
- if (name.toLowerCase().includes('background') ||
171
- name.toLowerCase().includes('goal') ||
172
- name.toLowerCase().includes('risk') ||
173
- name.toLowerCase().includes('summary')) {
174
- continue;
334
+ // First pass: Look for explicit task markers per the spec format
335
+ // Format: "### Task [M].N: [Title]" or "Task N: [Title]"
336
+ const explicitTaskPattern = /^#{2,4}\s*Task\s+(?:[\d.]+[:\s]+)?(.+)$/gim;
337
+ const explicitTasks: Array<{ name: string; description: string; testPlan?: string }> = [];
338
+
339
+ let taskMatch;
340
+ const taskPositions: Array<{ name: string; index: number; endIndex: number }> = [];
341
+
342
+ // Find all task headers
343
+ while ((taskMatch = explicitTaskPattern.exec(plan)) !== null) {
344
+ const name = taskMatch[1].trim()
345
+ .replace(/^\*\*(.+)\*\*$/, '$1') // Remove bold
346
+ .replace(/^:/, '') // Remove leading colon
347
+ .trim();
348
+
349
+ if (name.length > 3 && isActionableTask(name)) {
350
+ taskPositions.push({
351
+ name,
352
+ index: taskMatch.index + taskMatch[0].length,
353
+ endIndex: plan.length, // Will be updated
354
+ });
175
355
  }
356
+ }
357
+
358
+ // Update end indices
359
+ for (let i = 0; i < taskPositions.length - 1; i++) {
360
+ taskPositions[i].endIndex = taskPositions[i + 1].index - 50; // Approximate
361
+ }
176
362
 
177
- const tasks: Omit<Task, 'id' | 'status' | 'testsPassed'>[] = [];
178
- let taskMatch;
363
+ // Extract task details
364
+ for (const pos of taskPositions) {
365
+ const content = plan.slice(pos.index, pos.endIndex);
366
+ const description = extractTaskDescription(content);
367
+ const criteria = extractAcceptanceCriteria(content);
179
368
 
180
- while ((taskMatch = taskPattern.exec(content)) !== null) {
181
- const taskName = taskMatch[1].trim();
182
- if (taskName && !taskName.toLowerCase().startsWith('test')) {
183
- tasks.push({
369
+ explicitTasks.push({
370
+ name: pos.name,
371
+ description: description || pos.name,
372
+ testPlan: criteria.length > 0 ? criteria.join('\n') : undefined,
373
+ });
374
+ }
375
+
376
+ // Second pass: Look for milestone sections containing implementation tasks
377
+ const milestoneSectionPattern = /^#{1,3}\s*(?:Milestone|Phase|Sprint|Stage)\s*[\d.]*[:\s]+(.+)$/gim;
378
+ const milestoneMatches: Array<{ name: string; index: number }> = [];
379
+
380
+ let msMatch;
381
+ while ((msMatch = milestoneSectionPattern.exec(plan)) !== null) {
382
+ milestoneMatches.push({
383
+ name: msMatch[1].trim().replace(/^\*\*(.+)\*\*$/, '$1'),
384
+ index: msMatch.index,
385
+ });
386
+ }
387
+
388
+ // Third pass: If no explicit tasks found, look for actionable bullet points
389
+ if (explicitTasks.length === 0) {
390
+ // Look for bullet points that start with actionable verbs
391
+ const bulletPattern = /^[-*+]\s+(.+)$/gm;
392
+ let bulletMatch;
393
+
394
+ while ((bulletMatch = bulletPattern.exec(plan)) !== null) {
395
+ const taskName = bulletMatch[1].trim()
396
+ .replace(/^\*\*(.+)\*\*:?\s*/, '$1: ')
397
+ .replace(/\*\*(.+)\*\*/g, '$1')
398
+ .slice(0, 200);
399
+
400
+ if (taskName.length >= 10 && isActionableTask(taskName)) {
401
+ explicitTasks.push({
184
402
  name: taskName,
185
403
  description: taskName,
186
404
  });
187
405
  }
188
406
  }
407
+ }
408
+
409
+ // Fourth pass: If still no tasks, look for numbered implementation items
410
+ if (explicitTasks.length === 0) {
411
+ const numberedPattern = /^\d+[.)]\s+(.+)$/gm;
412
+ let numMatch;
413
+
414
+ while ((numMatch = numberedPattern.exec(plan)) !== null) {
415
+ const taskName = numMatch[1].trim()
416
+ .replace(/^\*\*(.+)\*\*:?\s*/, '$1: ')
417
+ .replace(/\*\*(.+)\*\*/g, '$1')
418
+ .slice(0, 200);
419
+
420
+ if (taskName.length >= 10 && isActionableTask(taskName)) {
421
+ explicitTasks.push({
422
+ name: taskName,
423
+ description: taskName,
424
+ });
425
+ }
426
+ }
427
+ }
428
+
429
+ // Build milestones from collected data
430
+ if (milestoneMatches.length > 0 && explicitTasks.length > 0) {
431
+ // Distribute tasks to milestones based on position
432
+ const tasksPerMilestone = Math.ceil(explicitTasks.length / milestoneMatches.length);
433
+
434
+ for (let i = 0; i < milestoneMatches.length; i++) {
435
+ const startIdx = i * tasksPerMilestone;
436
+ const endIdx = Math.min(startIdx + tasksPerMilestone, explicitTasks.length);
437
+ const milestoneTasks = explicitTasks.slice(startIdx, endIdx);
438
+
439
+ if (milestoneTasks.length > 0) {
440
+ milestones.push({
441
+ name: milestoneMatches[i].name,
442
+ description: `Implementation phase ${i + 1}`,
443
+ tasks: milestoneTasks as Task[],
444
+ status: 'pending',
445
+ });
446
+ }
447
+ }
448
+ } else if (explicitTasks.length > 0) {
449
+ // No milestone headers found, group tasks into phases
450
+ const tasksPerMilestone = 5;
451
+ for (let i = 0; i < explicitTasks.length; i += tasksPerMilestone) {
452
+ const milestoneTasks = explicitTasks.slice(i, i + tasksPerMilestone);
453
+ const milestoneNum = Math.floor(i / tasksPerMilestone) + 1;
189
454
 
190
- if (tasks.length > 0 || name.toLowerCase().includes('milestone')) {
191
455
  milestones.push({
192
- name,
193
- description: content.slice(0, 200).trim(),
194
- tasks: tasks as Task[],
456
+ name: `Implementation Phase ${milestoneNum}`,
457
+ description: `Tasks ${i + 1} to ${Math.min(i + tasksPerMilestone, explicitTasks.length)}`,
458
+ tasks: milestoneTasks as Task[],
195
459
  status: 'pending',
196
460
  });
197
461
  }
198
- }
462
+ } else {
463
+ // Fifth pass: Look for any headers that might be tasks (less strict matching)
464
+ const anyHeaderPattern = /^#{2,4}\s+(.+)$/gm;
465
+ const headerTasks: Array<{ name: string; description: string }> = [];
466
+ let headerMatch;
467
+
468
+ while ((headerMatch = anyHeaderPattern.exec(plan)) !== null) {
469
+ const name = headerMatch[1].trim()
470
+ .replace(/^\*\*(.+)\*\*$/, '$1')
471
+ .replace(/^[:*-]\s*/, '');
472
+
473
+ // Skip obvious non-task headers
474
+ const skipPatterns = [
475
+ /^(background|context|overview|introduction|summary)/i,
476
+ /^(goal|objective|requirement|risk|assumption)/i,
477
+ /^(timeline|schedule|test plan|acceptance)/i,
478
+ /^(table of contents|toc|appendix|reference)/i,
479
+ /^(project|specification|design|architecture)$/i,
480
+ ];
481
+
482
+ const shouldSkip = skipPatterns.some(p => p.test(name));
483
+
484
+ if (!shouldSkip && name.length >= 5 && name.length <= 200) {
485
+ headerTasks.push({
486
+ name: name.slice(0, 100),
487
+ description: name,
488
+ });
489
+ }
490
+ }
199
491
 
200
- // If no milestones found, create a default one
201
- if (milestones.length === 0) {
202
- milestones.push({
203
- name: 'Implementation',
204
- description: 'Main implementation milestone',
205
- tasks: [],
206
- status: 'pending',
207
- });
492
+ if (headerTasks.length >= 2) {
493
+ // Use headers as tasks, grouped into milestones
494
+ const tasksPerMilestone = 5;
495
+ for (let i = 0; i < headerTasks.length; i += tasksPerMilestone) {
496
+ const milestoneTasks = headerTasks.slice(i, i + tasksPerMilestone);
497
+ const milestoneNum = Math.floor(i / tasksPerMilestone) + 1;
498
+
499
+ milestones.push({
500
+ name: `Implementation Phase ${milestoneNum}`,
501
+ description: `Tasks ${i + 1} to ${Math.min(i + tasksPerMilestone, headerTasks.length)}`,
502
+ tasks: milestoneTasks as Task[],
503
+ status: 'pending',
504
+ });
505
+ }
506
+ } else {
507
+ // Sixth pass: Parse any section with implementation keywords
508
+ const implKeywords = [
509
+ 'implement', 'create', 'build', 'add', 'develop', 'write',
510
+ 'set up', 'configure', 'design', 'test', 'api', 'component',
511
+ 'service', 'module', 'function', 'class', 'feature',
512
+ 'database', 'model', 'controller', 'view', 'route', 'endpoint',
513
+ ];
514
+
515
+ const lines = plan.split('\n');
516
+ const implTasks: Array<{ name: string; description: string }> = [];
517
+
518
+ for (const line of lines) {
519
+ const trimmed = line.trim();
520
+ if (trimmed.length < 10 || trimmed.length > 200) continue;
521
+
522
+ const hasKeyword = implKeywords.some(kw =>
523
+ trimmed.toLowerCase().includes(kw)
524
+ );
525
+
526
+ // Check if it looks like an item (starts with bullet, number, or header)
527
+ const isItem = /^[-*+#\d.]/.test(trimmed) ||
528
+ /^(Task|Step|Item|Feature|Component)/i.test(trimmed);
529
+
530
+ if (hasKeyword && isItem) {
531
+ const name = trimmed
532
+ .replace(/^[-*+#]+\s*/, '')
533
+ .replace(/^\d+[.)]\s*/, '')
534
+ .replace(/^\*\*(.+?)\*\*:?\s*/, '$1: ')
535
+ .slice(0, 100);
536
+
537
+ if (name.length >= 10 && !implTasks.some(t => t.name === name)) {
538
+ implTasks.push({
539
+ name,
540
+ description: name,
541
+ });
542
+ }
543
+ }
544
+ }
545
+
546
+ if (implTasks.length > 0) {
547
+ // Group implementation tasks
548
+ const tasksPerMilestone = 5;
549
+ for (let i = 0; i < implTasks.length; i += tasksPerMilestone) {
550
+ const milestoneTasks = implTasks.slice(i, i + tasksPerMilestone);
551
+ const milestoneNum = Math.floor(i / tasksPerMilestone) + 1;
552
+
553
+ milestones.push({
554
+ name: `Implementation Phase ${milestoneNum}`,
555
+ description: `Tasks ${i + 1} to ${Math.min(i + tasksPerMilestone, implTasks.length)}`,
556
+ tasks: milestoneTasks as Task[],
557
+ status: 'pending',
558
+ });
559
+ }
560
+ } else {
561
+ // Final fallback: Create structured tasks based on common project phases
562
+ // This should rarely happen if the plan is well-structured
563
+ console.warn('[plan-parser] Warning: Could not parse tasks from plan. Using default structure.');
564
+
565
+ milestones.push({
566
+ name: 'Core Implementation',
567
+ description: 'Implement core functionality based on the plan',
568
+ tasks: [
569
+ {
570
+ name: 'Set up project structure and dependencies',
571
+ description: 'Initialize project with required structure, dependencies, and configuration',
572
+ },
573
+ {
574
+ name: 'Implement core features',
575
+ description: 'Build the main features as described in the development plan',
576
+ },
577
+ {
578
+ name: 'Add data models and storage',
579
+ description: 'Create data models, database schema, and storage layer',
580
+ },
581
+ ] as Task[],
582
+ status: 'pending',
583
+ });
584
+
585
+ milestones.push({
586
+ name: 'Integration and Testing',
587
+ description: 'Connect components and verify functionality',
588
+ tasks: [
589
+ {
590
+ name: 'Integrate components',
591
+ description: 'Connect all components and ensure they work together',
592
+ },
593
+ {
594
+ name: 'Write and run tests',
595
+ description: 'Create unit tests, integration tests, and verify all tests pass',
596
+ },
597
+ {
598
+ name: 'Final verification and documentation',
599
+ description: 'Run final verification, update documentation, ensure project works correctly',
600
+ },
601
+ ] as Task[],
602
+ status: 'pending',
603
+ });
604
+ }
605
+ }
208
606
  }
209
607
 
210
608
  return milestones;
@@ -221,24 +619,44 @@ export async function runPlanMode(
221
619
  spec: ProjectSpec,
222
620
  options: PlanModeOptions
223
621
  ): Promise<PlanModeResult> {
224
- const { projectDir, consensusConfig, onProgress } = options;
622
+ const { projectDir, consensusConfig, additionalContext, onProgress } = options;
623
+
624
+ // Initialize workflow logger
625
+ const logger = getWorkflowLogger(projectDir);
225
626
 
226
627
  try {
227
628
  // Create or load project
228
629
  onProgress?.('plan-init', 'Initializing project...');
630
+ await logger.stageStart('init', 'Plan Mode initialization', {
631
+ projectName: spec.name,
632
+ language: spec.language,
633
+ idea: spec.idea.slice(0, 200),
634
+ });
229
635
 
230
636
  let state: ProjectState;
231
637
  try {
232
638
  state = await loadProject(projectDir);
233
639
  onProgress?.('plan-init', 'Loaded existing project');
640
+ await logger.info('init', 'project_loaded', 'Loaded existing project', {
641
+ projectName: state.name,
642
+ phase: state.phase,
643
+ hasPlan: !!state.plan,
644
+ hasSpecification: !!state.specification,
645
+ });
234
646
  } catch {
235
647
  state = await createProject(spec, projectDir);
236
648
  onProgress?.('plan-init', 'Created new project');
649
+ await logger.success('init', 'project_created', 'Created new project', {
650
+ projectName: state.name,
651
+ language: state.language,
652
+ });
237
653
  }
238
654
 
239
655
  // Expand idea if we don't have a specification
240
656
  if (!state.specification) {
241
657
  onProgress?.('expand-idea', 'Expanding idea into specification...');
658
+ await logger.stageStart('plan-generation', 'Expanding idea into specification');
659
+
242
660
  const specification = await expandIdea(
243
661
  spec.idea,
244
662
  spec.language,
@@ -247,18 +665,49 @@ export async function runPlanMode(
247
665
 
248
666
  state = await storeSpecification(projectDir, specification);
249
667
  onProgress?.('expand-idea', 'Specification complete');
668
+ await logger.stageComplete('plan-generation', 'Specification created', {
669
+ specificationLength: specification.length,
670
+ specificationPreview: specification.slice(0, 300),
671
+ });
672
+ }
673
+
674
+ // Design UI early in the process
675
+ onProgress?.('ui-design', 'Designing UI from project idea...');
676
+ try {
677
+ const uiSpec = await designUI(spec.idea, (msg) => onProgress?.('ui-design', msg));
678
+ await saveUISpecification(projectDir, uiSpec);
679
+ onProgress?.('ui-design', `UI design complete: ${uiSpec.themeName} theme, ${uiSpec.recommendedComponents.length} components`);
680
+ await logger.success('ui-design', 'ui_design_complete', 'UI design specification created', {
681
+ theme: uiSpec.themeName,
682
+ projectType: uiSpec.projectType,
683
+ components: uiSpec.recommendedComponents.length,
684
+ });
685
+ } catch (uiError) {
686
+ // Non-blocking - UI design failures shouldn't stop the workflow
687
+ onProgress?.('ui-design', `UI design skipped: ${uiError instanceof Error ? uiError.message : 'Unknown error'}`);
688
+ await logger.warn('ui-design', 'ui_design_skipped', 'UI design was skipped', {
689
+ error: uiError instanceof Error ? uiError.message : 'Unknown error',
690
+ });
250
691
  }
251
692
 
252
693
  // Get project context
253
694
  onProgress?.('get-context', 'Gathering project context...');
254
- const context = await getProjectContext(
695
+ let context = await getProjectContext(
255
696
  projectDir,
256
697
  (msg) => onProgress?.('get-context', msg)
257
698
  );
258
699
 
700
+ // Append additional context if provided (e.g., when resuming with guidance)
701
+ if (additionalContext) {
702
+ onProgress?.('get-context', 'Incorporating additional guidance...');
703
+ context = `${context}\n\nADDITIONAL GUIDANCE FROM USER:\n${additionalContext}`;
704
+ }
705
+
259
706
  // Create initial plan if we don't have one
260
707
  if (!state.plan) {
261
708
  onProgress?.('create-plan', 'Creating development plan...');
709
+ await logger.stageStart('plan-generation', 'Creating development plan');
710
+
262
711
  const plan = await createPlan(
263
712
  state.specification!,
264
713
  context,
@@ -267,10 +716,16 @@ export async function runPlanMode(
267
716
 
268
717
  state = await storePlan(projectDir, plan);
269
718
  onProgress?.('create-plan', 'Initial plan created');
719
+ await logger.stageComplete('plan-generation', 'Development plan created', {
720
+ planLength: plan.length,
721
+ planPreview: plan.slice(0, 500),
722
+ });
270
723
  }
271
724
 
272
725
  // Run consensus loop
273
726
  onProgress?.('consensus', 'Starting consensus review...');
727
+ await logger.stageStart('consensus', 'Starting consensus review process');
728
+
274
729
  const consensusResult = await iterateUntilConsensus(
275
730
  state.plan!,
276
731
  context,
@@ -286,29 +741,214 @@ export async function runPlanMode(
286
741
  onRevision: (iteration, _plan) => {
287
742
  onProgress?.('consensus', `Revising plan (iteration ${iteration})...`);
288
743
  },
744
+ onConcerns: (concerns, recommendations) => {
745
+ if (concerns.length > 0) {
746
+ onProgress?.('concerns', `Concerns: ${concerns.slice(0, 2).join('; ')}`);
747
+ }
748
+ if (recommendations.length > 0) {
749
+ onProgress?.('recommendations', `Suggestions: ${recommendations.slice(0, 2).join('; ')}`);
750
+ }
751
+ },
752
+ onArbitration: (result) => {
753
+ onProgress?.('arbitration', `Arbitrator decision: ${result.approved ? 'APPROVED' : 'REVISE'} (${result.score}%)`);
754
+ if (!result.approved && result.suggestedChanges.length > 0) {
755
+ onProgress?.('arbitration', `Changes: ${result.suggestedChanges.slice(0, 2).join('; ')}`);
756
+ }
757
+ },
758
+ onProgress,
289
759
  }
290
760
  );
291
761
 
292
- // Store final plan
293
- if (consensusResult.approved) {
294
- state = await storePlan(projectDir, consensusResult.finalPlan);
762
+ // Log consensus result
763
+ await logger.info('consensus', 'consensus_complete', 'Consensus process completed', {
764
+ approved: consensusResult.approved,
765
+ finalScore: consensusResult.finalScore,
766
+ bestScore: consensusResult.bestScore,
767
+ totalIterations: consensusResult.totalIterations,
768
+ arbitrated: consensusResult.arbitrated,
769
+ });
295
770
 
296
- // Parse and add milestones
297
- const milestones = parsePlanMilestones(consensusResult.finalPlan);
298
- state = await addMilestones(projectDir, milestones);
771
+ // Check if the plan is garbage (Claude's thinking instead of actual content)
772
+ const garbageCheck = detectGarbagePlan(consensusResult.bestPlan);
773
+ if (garbageCheck.isGarbage) {
774
+ onProgress?.(
775
+ 'error',
776
+ `PLAN VALIDATION FAILED: ${garbageCheck.reason}`
777
+ );
778
+ onProgress?.(
779
+ 'error',
780
+ 'The plan contains Claude\'s thinking/conversation instead of actual plan content.'
781
+ );
782
+ onProgress?.(
783
+ 'info',
784
+ 'This typically happens when Claude describes what it will do instead of outputting the plan.'
785
+ );
786
+ onProgress?.(
787
+ 'info',
788
+ 'Saving garbage plan for debugging. Try running again or provide more specific requirements.'
789
+ );
790
+
791
+ // Still save the plan for debugging
792
+ await documentPlan(projectDir, consensusResult.bestPlan, 'PLAN-FAILED.md');
793
+
794
+ await logger.stageFailed('plan-parsing', 'Plan validation', garbageCheck.reason!, {
795
+ planLength: consensusResult.bestPlan.length,
796
+ reason: garbageCheck.reason,
797
+ });
798
+
799
+ return {
800
+ success: false,
801
+ state,
802
+ consensusResult,
803
+ error: `Plan generation failed: ${garbageCheck.reason}`,
804
+ };
805
+ }
299
806
 
300
- // Document the plan
301
- await documentPlan(projectDir, consensusResult.finalPlan);
807
+ // Always store the best plan (even if consensus failed)
808
+ state = await storePlan(projectDir, consensusResult.bestPlan);
302
809
 
810
+ // Parse and add milestones from best plan
811
+ await logger.stageStart('plan-parsing', 'Parsing plan into milestones and tasks');
812
+ const milestones = parsePlanMilestones(consensusResult.bestPlan);
813
+
814
+ // Log parsed milestones for debugging
815
+ const totalTasks = milestones.reduce((sum, m) => sum + m.tasks.length, 0);
816
+ onProgress?.(
817
+ 'plan-structure',
818
+ `Parsed plan: ${milestones.length} milestones, ${totalTasks} tasks`
819
+ );
820
+
821
+ // Log detailed parsing results
822
+ const parsedMilestones = milestones.map(m => ({
823
+ name: m.name,
824
+ taskCount: m.tasks.length,
825
+ taskNames: m.tasks.map(t => t.name),
826
+ }));
827
+ await logger.info('plan-parsing', 'plan_parsed', 'Parsed plan structure', {
828
+ milestonesCount: milestones.length,
829
+ totalTasks: totalTasks,
830
+ milestones: parsedMilestones,
831
+ });
832
+
833
+ // VALIDATION: Fail if too few milestones/tasks for a real project
834
+ if (milestones.length <= 1 && totalTasks <= 2) {
835
+ onProgress?.(
836
+ 'error',
837
+ `PLAN VALIDATION FAILED: Only ${milestones.length} milestone(s) and ${totalTasks} task(s) extracted.`
838
+ );
839
+ onProgress?.(
840
+ 'error',
841
+ 'A valid plan should have at least 2 milestones with 3+ tasks each.'
842
+ );
843
+ onProgress?.(
844
+ 'info',
845
+ 'Expected format: "## Milestone N: Name" and "### Task N.N: Name"'
846
+ );
847
+
848
+ // Save the problematic plan for debugging
849
+ await documentPlan(projectDir, consensusResult.bestPlan, 'PLAN-INSUFFICIENT.md');
850
+
851
+ // Show what was found in the plan
852
+ onProgress?.('debug', 'Tasks extracted from plan:');
853
+ for (const m of milestones) {
854
+ for (const t of m.tasks) {
855
+ onProgress?.('debug', ` - ${t.name}`);
856
+ }
857
+ }
858
+
859
+ await logger.stageFailed('plan-parsing', 'Plan validation', 'Insufficient tasks extracted', {
860
+ milestonesCount: milestones.length,
861
+ totalTasks: totalTasks,
862
+ expectedMinTasks: 3,
863
+ extractedTasks: milestones.flatMap(m => m.tasks.map(t => t.name)),
864
+ });
865
+
866
+ return {
867
+ success: false,
868
+ state,
869
+ consensusResult,
870
+ error: `Plan parsing failed: only ${totalTasks} task(s) extracted. Plan needs more structure.`,
871
+ };
872
+ }
873
+
874
+ // Warn if suspiciously few tasks (but don't block)
875
+ if (milestones.length <= 2 || totalTasks <= 5) {
876
+ onProgress?.(
877
+ 'warning',
878
+ `Warning: Only ${milestones.length} milestone(s) and ${totalTasks} task(s) parsed. ` +
879
+ `This seems low for a complete project. Consider reviewing the plan.`
880
+ );
881
+ }
882
+
883
+ // Log each milestone and its tasks
884
+ for (const milestone of milestones) {
885
+ onProgress?.(
886
+ 'plan-detail',
887
+ ` Milestone: ${milestone.name} (${milestone.tasks.length} tasks)`
888
+ );
889
+ for (const task of milestone.tasks.slice(0, 3)) {
890
+ onProgress?.('plan-detail', ` - ${task.name}`);
891
+ }
892
+ if (milestone.tasks.length > 3) {
893
+ onProgress?.('plan-detail', ` ... and ${milestone.tasks.length - 3} more tasks`);
894
+ }
895
+ }
896
+
897
+ state = await addMilestones(projectDir, milestones);
898
+
899
+ // Always document the plan (so user can see what was achieved)
900
+ const planFilename = consensusResult.approved ? 'PLAN.md' : 'PLAN-DRAFT.md';
901
+ await documentPlan(projectDir, consensusResult.bestPlan, planFilename);
902
+
903
+ if (consensusResult.approved) {
303
904
  // Transition to execution phase
304
905
  state = await setPhase(projectDir, 'execution');
305
906
 
306
- onProgress?.('complete', `Plan approved with ${consensusResult.finalScore}% consensus`);
907
+ if (consensusResult.arbitrated) {
908
+ onProgress?.('complete', `Plan approved via arbitration with ${consensusResult.finalScore}% confidence`);
909
+ } else {
910
+ onProgress?.('complete', `Plan approved with ${consensusResult.finalScore}% consensus`);
911
+ }
912
+ onProgress?.('info', `Plan saved to docs/PLAN.md`);
913
+
914
+ await logger.stageComplete('plan-generation', 'Plan Mode completed successfully', {
915
+ consensusScore: consensusResult.finalScore,
916
+ arbitrated: consensusResult.arbitrated,
917
+ milestonesCount: milestones.length,
918
+ totalTasks: totalTasks,
919
+ nextPhase: 'execution',
920
+ });
307
921
  } else {
922
+ // Show why consensus failed
308
923
  onProgress?.(
309
924
  'failed',
310
- `Consensus not reached after ${consensusResult.totalIterations} iterations (${consensusResult.finalScore}%)`
925
+ `Consensus not reached after ${consensusResult.totalIterations} iterations (best: ${consensusResult.bestScore}% at iteration ${consensusResult.bestIteration})`
311
926
  );
927
+
928
+ // Show remaining concerns
929
+ if (consensusResult.finalConcerns.length > 0) {
930
+ onProgress?.('concerns', `Remaining concerns:`);
931
+ for (const concern of consensusResult.finalConcerns.slice(0, 3)) {
932
+ onProgress?.('concerns', ` - ${concern}`);
933
+ }
934
+ }
935
+
936
+ // Show recommendations
937
+ if (consensusResult.finalRecommendations.length > 0) {
938
+ onProgress?.('recommendations', `Recommendations:`);
939
+ for (const rec of consensusResult.finalRecommendations.slice(0, 3)) {
940
+ onProgress?.('recommendations', ` - ${rec}`);
941
+ }
942
+ }
943
+
944
+ onProgress?.('info', `Draft plan saved to docs/${planFilename}`);
945
+
946
+ await logger.warn('plan-generation', 'consensus_failed', 'Plan Mode incomplete - consensus not reached', {
947
+ bestScore: consensusResult.bestScore,
948
+ totalIterations: consensusResult.totalIterations,
949
+ finalConcerns: consensusResult.finalConcerns,
950
+ finalRecommendations: consensusResult.finalRecommendations,
951
+ });
312
952
  }
313
953
 
314
954
  return {
@@ -320,6 +960,12 @@ export async function runPlanMode(
320
960
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
321
961
  onProgress?.('error', errorMessage);
322
962
 
963
+ // Log the error
964
+ await logger.stageFailed('plan-generation', 'Plan Mode execution', errorMessage, {
965
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
966
+ stack: error instanceof Error ? error.stack : undefined,
967
+ });
968
+
323
969
  return {
324
970
  success: false,
325
971
  state: await loadProject(projectDir).catch(() => ({} as ProjectState)),