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