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
@@ -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
 
@@ -49,7 +52,7 @@ export interface PlanModeResult {
49
52
  */
50
53
  export async function expandIdea(
51
54
  idea: string,
52
- language: 'python' | 'typescript',
55
+ language: 'python' | 'typescript' | 'fullstack',
53
56
  onProgress?: (message: string) => void
54
57
  ): Promise<string> {
55
58
  onProgress?.('Expanding idea into specification...');
@@ -65,17 +68,19 @@ export async function expandIdea(
65
68
  *
66
69
  * @param specification - The project specification
67
70
  * @param context - Additional context
71
+ * @param language - Target programming language
68
72
  * @param onProgress - Progress callback
69
73
  * @returns Development plan
70
74
  */
71
75
  export async function createPlan(
72
76
  specification: string,
73
77
  context: string = '',
78
+ language: 'python' | 'typescript' | 'fullstack' = 'python',
74
79
  onProgress?: (message: string) => void
75
80
  ): Promise<string> {
76
81
  onProgress?.('Creating development plan...');
77
82
 
78
- const result = await claudeCreatePlan(specification, context);
83
+ const result = await claudeCreatePlan(specification, context, language, onProgress);
79
84
 
80
85
  if (!result.success) {
81
86
  throw new Error(`Failed to create plan: ${result.error}`);
@@ -110,7 +115,7 @@ export async function getProjectContext(
110
115
  return 'New project - no existing codebase';
111
116
  }
112
117
 
113
- const result = await analyzeCodebase(projectDir);
118
+ const result = await analyzeCodebase(projectDir, onProgress);
114
119
 
115
120
  if (result.success) {
116
121
  onProgress?.('Codebase analysis complete');
@@ -124,7 +129,7 @@ export async function getProjectContext(
124
129
  }
125
130
 
126
131
  /**
127
- * Save the plan to a markdown file
132
+ * Save the plan to a markdown file in docs folder
128
133
  *
129
134
  * @param projectDir - The project directory
130
135
  * @param plan - The plan content
@@ -135,7 +140,15 @@ export async function documentPlan(
135
140
  plan: string,
136
141
  filename: string = 'PLAN.md'
137
142
  ): Promise<string> {
138
- const planPath = path.join(projectDir, filename);
143
+ // Create docs directory if it doesn't exist
144
+ const docsDir = path.join(projectDir, 'docs');
145
+ try {
146
+ await fs.mkdir(docsDir, { recursive: true });
147
+ } catch {
148
+ // Directory might already exist
149
+ }
150
+
151
+ const planPath = path.join(docsDir, filename);
139
152
 
140
153
  const content = `# Development Plan
141
154
 
@@ -145,11 +158,376 @@ ${plan}
145
158
  `;
146
159
 
147
160
  await fs.writeFile(planPath, content, 'utf-8');
161
+
162
+ // Also save a timestamped version for history
163
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
164
+ const historyFilename = `PLAN-${timestamp}.md`;
165
+ const historyPath = path.join(docsDir, historyFilename);
166
+ await fs.writeFile(historyPath, content, 'utf-8');
167
+
148
168
  return planPath;
149
169
  }
150
170
 
171
+ /**
172
+ * Check if a task name represents an actionable implementation task
173
+ * Tasks should start with verbs like: Implement, Create, Build, Set up, Add, etc.
174
+ *
175
+ * @param name - The potential task name
176
+ * @returns True if this looks like an implementation task
177
+ */
178
+ function isActionableTask(name: string): boolean {
179
+ const nameLower = name.toLowerCase().trim();
180
+
181
+ // Actionable verb prefixes that indicate real implementation tasks
182
+ const actionableVerbs = [
183
+ 'implement', 'create', 'build', 'develop', 'write', 'add', 'set up', 'setup',
184
+ 'configure', 'install', 'integrate', 'design', 'define', 'establish',
185
+ 'generate', 'construct', 'deploy', 'test', 'validate', 'fix', 'update',
186
+ 'refactor', 'optimize', 'extend', 'enhance', 'modify', 'initialize',
187
+ 'bootstrap', 'scaffold', 'connect', 'wire', 'hook', 'enable', 'disable',
188
+ ];
189
+
190
+ // Check if starts with an actionable verb
191
+ const startsWithAction = actionableVerbs.some((verb) =>
192
+ nameLower.startsWith(verb + ' ') || nameLower.startsWith(verb + ':')
193
+ );
194
+
195
+ // Non-actionable patterns to exclude (plan metadata, not tasks)
196
+ const nonActionablePatterns = [
197
+ /^(background|context|overview|introduction|summary)/i,
198
+ /^(goal|objective|requirement|constraint|assumption|risk)/i,
199
+ /^(timeline|schedule|estimate|duration|deadline)/i,
200
+ /^(note|example|reference|appendix|glossary)/i,
201
+ /^(file structure|project structure|directory)/i,
202
+ /^(showing|displays?|contains?|includes?|describes?)/i,
203
+ /^(the |this |a |an )/i, // Descriptions, not actions
204
+ /^\d+[-.]?\s*(week|day|hour|month)/i, // Time estimates
205
+ /^([\w\s]+):$/, // Labels ending with colon
206
+ ];
207
+
208
+ const isNonActionable = nonActionablePatterns.some((pattern) => pattern.test(nameLower));
209
+
210
+ return startsWithAction && !isNonActionable;
211
+ }
212
+
213
+ /**
214
+ * Task app tag for fullstack projects
215
+ */
216
+ export type TaskAppTag = 'FE' | 'BE' | 'INT';
217
+
218
+ /**
219
+ * Task with app targeting information for fullstack projects
220
+ */
221
+ export interface ParsedFullstackTask {
222
+ name: string;
223
+ description: string;
224
+ appTag?: TaskAppTag;
225
+ appTarget?: 'frontend' | 'backend' | 'unified';
226
+ files?: string[];
227
+ dependencies?: string[];
228
+ acceptanceCriteria?: string[];
229
+ testPlan?: string;
230
+ }
231
+
232
+ /**
233
+ * Parse task tag from task name
234
+ * e.g., "Task 1.1 [FE]: Create Button component" -> 'FE'
235
+ *
236
+ * @param taskName - The task name to parse
237
+ * @returns The parsed app tag or undefined
238
+ */
239
+ export function parseTaskTag(taskName: string): TaskAppTag | undefined {
240
+ const tagMatch = taskName.match(/\[(FE|BE|INT)\]/i);
241
+ if (tagMatch) {
242
+ return tagMatch[1].toUpperCase() as TaskAppTag;
243
+ }
244
+ return undefined;
245
+ }
246
+
247
+ /**
248
+ * Derive app target from tag
249
+ *
250
+ * @param tag - The task tag
251
+ * @returns The app target
252
+ */
253
+ export function tagToAppTarget(tag: TaskAppTag): 'frontend' | 'backend' | 'unified' {
254
+ switch (tag) {
255
+ case 'FE': return 'frontend';
256
+ case 'BE': return 'backend';
257
+ case 'INT': return 'unified';
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Validation result for fullstack task
263
+ */
264
+ export interface FullstackTaskValidation {
265
+ valid: boolean;
266
+ issues: string[];
267
+ }
268
+
269
+ /**
270
+ * Validate task has proper app targeting for fullstack projects
271
+ *
272
+ * @param task - The parsed task to validate
273
+ * @returns Validation result with issues
274
+ */
275
+ export function validateFullstackTask(task: ParsedFullstackTask): FullstackTaskValidation {
276
+ const issues: string[] = [];
277
+
278
+ if (!task.appTag) {
279
+ issues.push(`Task "${task.name.slice(0, 50)}" missing [FE], [BE], or [INT] tag`);
280
+ }
281
+
282
+ if (!task.appTarget) {
283
+ issues.push(`Task "${task.name.slice(0, 50)}" missing App: field (frontend/backend/unified)`);
284
+ }
285
+
286
+ // Validate consistency between tag and target
287
+ if (task.appTag && task.appTarget) {
288
+ const expectedTarget = tagToAppTarget(task.appTag);
289
+ if (task.appTarget !== expectedTarget) {
290
+ issues.push(`Task "${task.name.slice(0, 50)}" has [${task.appTag}] tag but App: is "${task.appTarget}" (expected "${expectedTarget}")`);
291
+ }
292
+ }
293
+
294
+ // Validate file paths match app
295
+ if (task.files && task.appTag === 'FE') {
296
+ const invalidFiles = task.files.filter(f => !f.includes('frontend'));
297
+ if (invalidFiles.length > 0) {
298
+ issues.push(`[FE] task has files outside apps/frontend: ${invalidFiles.slice(0, 2).join(', ')}`);
299
+ }
300
+ }
301
+ if (task.files && task.appTag === 'BE') {
302
+ const invalidFiles = task.files.filter(f => !f.includes('backend'));
303
+ if (invalidFiles.length > 0) {
304
+ issues.push(`[BE] task has files outside apps/backend: ${invalidFiles.slice(0, 2).join(', ')}`);
305
+ }
306
+ }
307
+
308
+ return {
309
+ valid: issues.length === 0,
310
+ issues,
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Validate all tasks in a fullstack plan
316
+ *
317
+ * @param plan - The plan content
318
+ * @returns Validation result with all issues
319
+ */
320
+ export function validateFullstackPlan(plan: string): {
321
+ valid: boolean;
322
+ issues: string[];
323
+ stats: {
324
+ totalTasks: number;
325
+ feTasks: number;
326
+ beTasks: number;
327
+ intTasks: number;
328
+ untaggedTasks: number;
329
+ };
330
+ } {
331
+ const issues: string[] = [];
332
+ let totalTasks = 0;
333
+ let feTasks = 0;
334
+ let beTasks = 0;
335
+ let intTasks = 0;
336
+ let untaggedTasks = 0;
337
+
338
+ // Find all task headers
339
+ const taskPattern = /^#{2,4}\s*Task\s+(?:[\d.]+[:\s]+)?(.+)$/gim;
340
+ let match;
341
+
342
+ while ((match = taskPattern.exec(plan)) !== null) {
343
+ totalTasks++;
344
+ const taskName = match[1].trim();
345
+ const tag = parseTaskTag(taskName);
346
+
347
+ if (tag) {
348
+ switch (tag) {
349
+ case 'FE': feTasks++; break;
350
+ case 'BE': beTasks++; break;
351
+ case 'INT': intTasks++; break;
352
+ }
353
+ } else {
354
+ untaggedTasks++;
355
+ // Only report first few untagged tasks
356
+ if (untaggedTasks <= 3) {
357
+ issues.push(`Task missing tag: "${taskName.slice(0, 50)}..."`);
358
+ }
359
+ }
360
+ }
361
+
362
+ // Report summary if many untagged
363
+ if (untaggedTasks > 3) {
364
+ issues.push(`... and ${untaggedTasks - 3} more tasks missing tags`);
365
+ }
366
+
367
+ // Check for balance
368
+ if (totalTasks > 0 && feTasks === 0) {
369
+ issues.push('No frontend [FE] tasks found in fullstack plan');
370
+ }
371
+ if (totalTasks > 0 && beTasks === 0) {
372
+ issues.push('No backend [BE] tasks found in fullstack plan');
373
+ }
374
+ if (totalTasks > 0 && intTasks === 0) {
375
+ issues.push('No integration [INT] tasks found - consider adding integration tests');
376
+ }
377
+
378
+ return {
379
+ valid: issues.length === 0,
380
+ issues,
381
+ stats: {
382
+ totalTasks,
383
+ feTasks,
384
+ beTasks,
385
+ intTasks,
386
+ untaggedTasks,
387
+ },
388
+ };
389
+ }
390
+
391
+ /**
392
+ * Extract task description from content following a task header
393
+ *
394
+ * @param content - Content following the task header
395
+ * @returns Extracted description
396
+ */
397
+ function extractTaskDescription(content: string): string {
398
+ // Look for Description field or first paragraph
399
+ const descMatch = content.match(/\*\*Description\*\*:\s*(.+?)(?=\n\*\*|\n###|\n##|$)/is);
400
+ if (descMatch) {
401
+ return descMatch[1].trim().slice(0, 500);
402
+ }
403
+
404
+ // Use first non-empty line
405
+ const lines = content.split('\n').filter((l) => l.trim() && !l.trim().startsWith('-'));
406
+ if (lines.length > 0) {
407
+ return lines[0].trim().slice(0, 500);
408
+ }
409
+
410
+ return '';
411
+ }
412
+
413
+ /**
414
+ * Extract acceptance criteria from task content
415
+ *
416
+ * @param content - Task content
417
+ * @returns Array of acceptance criteria
418
+ */
419
+ function extractAcceptanceCriteria(content: string): string[] {
420
+ const criteria: string[] = [];
421
+
422
+ // Look for Acceptance Criteria section
423
+ const acMatch = content.match(/\*\*Acceptance Criteria\*\*:?\s*([\s\S]+?)(?=\n\*\*|\n###|\n##|$)/i);
424
+ if (acMatch) {
425
+ const acContent = acMatch[1];
426
+ const bulletMatch = acContent.match(/^[-*]\s+(.+)$/gm);
427
+ if (bulletMatch) {
428
+ for (const bullet of bulletMatch) {
429
+ const cleaned = bullet.replace(/^[-*]\s+/, '').trim();
430
+ if (cleaned.length > 5) {
431
+ criteria.push(cleaned);
432
+ }
433
+ }
434
+ }
435
+ }
436
+
437
+ return criteria;
438
+ }
439
+
440
+ /**
441
+ * Detect if a plan is actually Claude's thinking/conversation instead of a real plan
442
+ * This happens when Claude outputs its reasoning instead of the plan content
443
+ *
444
+ * @param plan - The plan content
445
+ * @returns Object indicating if garbage and why
446
+ */
447
+ export function detectGarbagePlan(plan: string): { isGarbage: boolean; reason?: string } {
448
+ const planLower = plan.toLowerCase();
449
+
450
+ // Get just the first ~500 chars to check for intro meta-commentary
451
+ // This is where Claude's "thinking" typically appears
452
+ const planStart = planLower.slice(0, 500);
453
+
454
+ // Phrases that indicate Claude's thinking when at the START of output
455
+ // These are problematic only in the intro, not in plan content
456
+ const introGarbagePhrases = [
457
+ 'let me ',
458
+ 'i will ',
459
+ 'i\'ll ',
460
+ 'now i have',
461
+ 'i now have',
462
+ 'let me launch',
463
+ 'let me create',
464
+ 'let me write',
465
+ 'let me analyze',
466
+ 'based on my analysis',
467
+ 'before i proceed',
468
+ 'i\'ve created',
469
+ 'i\'ve analyzed',
470
+ 'i should ',
471
+ 'i need to',
472
+ 'first, i',
473
+ ];
474
+
475
+ // Check only the intro for thinking phrases
476
+ for (const phrase of introGarbagePhrases) {
477
+ if (planStart.includes(phrase)) {
478
+ return {
479
+ isGarbage: true,
480
+ reason: `Plan starts with Claude's thinking ("${phrase}") instead of actual plan content`,
481
+ };
482
+ }
483
+ }
484
+
485
+ // These phrases indicate the plan was saved elsewhere, not output directly
486
+ // Check the entire plan for these since they're unambiguous meta-commentary
487
+ const metaCommentaryPhrases = [
488
+ 'the plan is saved',
489
+ 'the plan has been saved',
490
+ 'i\'ve saved the plan',
491
+ 'plan saved to',
492
+ 'saved the plan to',
493
+ 'created the plan at',
494
+ 'plan is now available at',
495
+ '.claude/plans/', // Reference to Claude's internal plan storage
496
+ ];
497
+
498
+ for (const phrase of metaCommentaryPhrases) {
499
+ if (planLower.includes(phrase)) {
500
+ return {
501
+ isGarbage: true,
502
+ reason: `Plan contains meta-commentary ("${phrase}") instead of actual plan content`,
503
+ };
504
+ }
505
+ }
506
+
507
+ // Check if plan has actual structure
508
+ const hasTaskHeaders = /^#{2,4}\s*Task\s+[\d.]+/im.test(plan);
509
+ const hasMilestoneHeaders = /^#{1,3}\s*Milestone\s+\d/im.test(plan);
510
+ const hasActionableBullets = /^[-*]\s+(implement|create|build|add|set up|configure|design|write)/im.test(plan);
511
+
512
+ if (!hasTaskHeaders && !hasMilestoneHeaders && !hasActionableBullets) {
513
+ // Check if it at least has some structure
514
+ const hasAnyHeaders = /^#{1,4}\s+.+$/m.test(plan);
515
+ const hasBulletPoints = /^[-*+]\s+.+$/m.test(plan);
516
+
517
+ if (!hasAnyHeaders && !hasBulletPoints) {
518
+ return {
519
+ isGarbage: true,
520
+ reason: 'Plan has no recognizable structure (no headers, no bullet points)',
521
+ };
522
+ }
523
+ }
524
+
525
+ return { isGarbage: false };
526
+ }
527
+
151
528
  /**
152
529
  * Parse milestones and tasks from a plan
530
+ * Extracts only actionable implementation tasks, not plan metadata
153
531
  *
154
532
  * @param plan - The plan content
155
533
  * @returns Parsed milestones with tasks
@@ -157,54 +535,278 @@ ${plan}
157
535
  export function parsePlanMilestones(plan: string): Omit<Milestone, 'id'>[] {
158
536
  const milestones: Omit<Milestone, 'id'>[] = [];
159
537
 
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;
538
+ // First pass: Look for explicit task markers per the spec format
539
+ // Format: "### Task [M].N: [Title]" or "Task N: [Title]"
540
+ const explicitTaskPattern = /^#{2,4}\s*Task\s+(?:[\d.]+[:\s]+)?(.+)$/gim;
541
+ const explicitTasks: Array<{ name: string; description: string; testPlan?: string }> = [];
163
542
 
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;
543
+ let taskMatch;
544
+ const taskPositions: Array<{ name: string; index: number; endIndex: number }> = [];
545
+
546
+ // Find all task headers
547
+ while ((taskMatch = explicitTaskPattern.exec(plan)) !== null) {
548
+ const name = taskMatch[1].trim()
549
+ .replace(/^\*\*(.+)\*\*$/, '$1') // Remove bold
550
+ .replace(/^:/, '') // Remove leading colon
551
+ .trim();
552
+
553
+ if (name.length > 3 && isActionableTask(name)) {
554
+ taskPositions.push({
555
+ name,
556
+ index: taskMatch.index + taskMatch[0].length,
557
+ endIndex: plan.length, // Will be updated
558
+ });
175
559
  }
560
+ }
561
+
562
+ // Update end indices
563
+ for (let i = 0; i < taskPositions.length - 1; i++) {
564
+ taskPositions[i].endIndex = taskPositions[i + 1].index - 50; // Approximate
565
+ }
566
+
567
+ // Extract task details
568
+ for (const pos of taskPositions) {
569
+ const content = plan.slice(pos.index, pos.endIndex);
570
+ const description = extractTaskDescription(content);
571
+ const criteria = extractAcceptanceCriteria(content);
572
+
573
+ explicitTasks.push({
574
+ name: pos.name,
575
+ description: description || pos.name,
576
+ testPlan: criteria.length > 0 ? criteria.join('\n') : undefined,
577
+ });
578
+ }
176
579
 
177
- const tasks: Omit<Task, 'id' | 'status' | 'testsPassed'>[] = [];
178
- let taskMatch;
580
+ // Second pass: Look for milestone sections containing implementation tasks
581
+ const milestoneSectionPattern = /^#{1,3}\s*(?:Milestone|Phase|Sprint|Stage)\s*[\d.]*[:\s]+(.+)$/gim;
582
+ const milestoneMatches: Array<{ name: string; index: number }> = [];
179
583
 
180
- while ((taskMatch = taskPattern.exec(content)) !== null) {
181
- const taskName = taskMatch[1].trim();
182
- if (taskName && !taskName.toLowerCase().startsWith('test')) {
183
- tasks.push({
584
+ let msMatch;
585
+ while ((msMatch = milestoneSectionPattern.exec(plan)) !== null) {
586
+ milestoneMatches.push({
587
+ name: msMatch[1].trim().replace(/^\*\*(.+)\*\*$/, '$1'),
588
+ index: msMatch.index,
589
+ });
590
+ }
591
+
592
+ // Third pass: If no explicit tasks found, look for actionable bullet points
593
+ if (explicitTasks.length === 0) {
594
+ // Look for bullet points that start with actionable verbs
595
+ const bulletPattern = /^[-*+]\s+(.+)$/gm;
596
+ let bulletMatch;
597
+
598
+ while ((bulletMatch = bulletPattern.exec(plan)) !== null) {
599
+ const taskName = bulletMatch[1].trim()
600
+ .replace(/^\*\*(.+)\*\*:?\s*/, '$1: ')
601
+ .replace(/\*\*(.+)\*\*/g, '$1')
602
+ .slice(0, 200);
603
+
604
+ if (taskName.length >= 10 && isActionableTask(taskName)) {
605
+ explicitTasks.push({
184
606
  name: taskName,
185
607
  description: taskName,
186
608
  });
187
609
  }
188
610
  }
611
+ }
612
+
613
+ // Fourth pass: If still no tasks, look for numbered implementation items
614
+ if (explicitTasks.length === 0) {
615
+ const numberedPattern = /^\d+[.)]\s+(.+)$/gm;
616
+ let numMatch;
617
+
618
+ while ((numMatch = numberedPattern.exec(plan)) !== null) {
619
+ const taskName = numMatch[1].trim()
620
+ .replace(/^\*\*(.+)\*\*:?\s*/, '$1: ')
621
+ .replace(/\*\*(.+)\*\*/g, '$1')
622
+ .slice(0, 200);
623
+
624
+ if (taskName.length >= 10 && isActionableTask(taskName)) {
625
+ explicitTasks.push({
626
+ name: taskName,
627
+ description: taskName,
628
+ });
629
+ }
630
+ }
631
+ }
632
+
633
+ // Build milestones from collected data
634
+ if (milestoneMatches.length > 0 && explicitTasks.length > 0) {
635
+ // Distribute tasks to milestones based on position
636
+ const tasksPerMilestone = Math.ceil(explicitTasks.length / milestoneMatches.length);
637
+
638
+ for (let i = 0; i < milestoneMatches.length; i++) {
639
+ const startIdx = i * tasksPerMilestone;
640
+ const endIdx = Math.min(startIdx + tasksPerMilestone, explicitTasks.length);
641
+ const milestoneTasks = explicitTasks.slice(startIdx, endIdx);
642
+
643
+ if (milestoneTasks.length > 0) {
644
+ milestones.push({
645
+ name: milestoneMatches[i].name,
646
+ description: `Implementation phase ${i + 1}`,
647
+ tasks: milestoneTasks as Task[],
648
+ status: 'pending',
649
+ });
650
+ }
651
+ }
652
+ } else if (explicitTasks.length > 0) {
653
+ // No milestone headers found, group tasks into phases
654
+ const tasksPerMilestone = 5;
655
+ for (let i = 0; i < explicitTasks.length; i += tasksPerMilestone) {
656
+ const milestoneTasks = explicitTasks.slice(i, i + tasksPerMilestone);
657
+ const milestoneNum = Math.floor(i / tasksPerMilestone) + 1;
189
658
 
190
- if (tasks.length > 0 || name.toLowerCase().includes('milestone')) {
191
659
  milestones.push({
192
- name,
193
- description: content.slice(0, 200).trim(),
194
- tasks: tasks as Task[],
660
+ name: `Implementation Phase ${milestoneNum}`,
661
+ description: `Tasks ${i + 1} to ${Math.min(i + tasksPerMilestone, explicitTasks.length)}`,
662
+ tasks: milestoneTasks as Task[],
195
663
  status: 'pending',
196
664
  });
197
665
  }
198
- }
666
+ } else {
667
+ // Fifth pass: Look for any headers that might be tasks (less strict matching)
668
+ const anyHeaderPattern = /^#{2,4}\s+(.+)$/gm;
669
+ const headerTasks: Array<{ name: string; description: string }> = [];
670
+ let headerMatch;
199
671
 
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
- });
672
+ while ((headerMatch = anyHeaderPattern.exec(plan)) !== null) {
673
+ const name = headerMatch[1].trim()
674
+ .replace(/^\*\*(.+)\*\*$/, '$1')
675
+ .replace(/^[:*-]\s*/, '');
676
+
677
+ // Skip obvious non-task headers
678
+ const skipPatterns = [
679
+ /^(background|context|overview|introduction|summary)/i,
680
+ /^(goal|objective|requirement|risk|assumption)/i,
681
+ /^(timeline|schedule|test plan|acceptance)/i,
682
+ /^(table of contents|toc|appendix|reference)/i,
683
+ /^(project|specification|design|architecture)$/i,
684
+ ];
685
+
686
+ const shouldSkip = skipPatterns.some(p => p.test(name));
687
+
688
+ if (!shouldSkip && name.length >= 5 && name.length <= 200) {
689
+ headerTasks.push({
690
+ name: name.slice(0, 100),
691
+ description: name,
692
+ });
693
+ }
694
+ }
695
+
696
+ if (headerTasks.length >= 2) {
697
+ // Use headers as tasks, grouped into milestones
698
+ const tasksPerMilestone = 5;
699
+ for (let i = 0; i < headerTasks.length; i += tasksPerMilestone) {
700
+ const milestoneTasks = headerTasks.slice(i, i + tasksPerMilestone);
701
+ const milestoneNum = Math.floor(i / tasksPerMilestone) + 1;
702
+
703
+ milestones.push({
704
+ name: `Implementation Phase ${milestoneNum}`,
705
+ description: `Tasks ${i + 1} to ${Math.min(i + tasksPerMilestone, headerTasks.length)}`,
706
+ tasks: milestoneTasks as Task[],
707
+ status: 'pending',
708
+ });
709
+ }
710
+ } else {
711
+ // Sixth pass: Parse any section with implementation keywords
712
+ const implKeywords = [
713
+ 'implement', 'create', 'build', 'add', 'develop', 'write',
714
+ 'set up', 'configure', 'design', 'test', 'api', 'component',
715
+ 'service', 'module', 'function', 'class', 'feature',
716
+ 'database', 'model', 'controller', 'view', 'route', 'endpoint',
717
+ ];
718
+
719
+ const lines = plan.split('\n');
720
+ const implTasks: Array<{ name: string; description: string }> = [];
721
+
722
+ for (const line of lines) {
723
+ const trimmed = line.trim();
724
+ if (trimmed.length < 10 || trimmed.length > 200) continue;
725
+
726
+ const hasKeyword = implKeywords.some(kw =>
727
+ trimmed.toLowerCase().includes(kw)
728
+ );
729
+
730
+ // Check if it looks like an item (starts with bullet, number, or header)
731
+ const isItem = /^[-*+#\d.]/.test(trimmed) ||
732
+ /^(Task|Step|Item|Feature|Component)/i.test(trimmed);
733
+
734
+ if (hasKeyword && isItem) {
735
+ const name = trimmed
736
+ .replace(/^[-*+#]+\s*/, '')
737
+ .replace(/^\d+[.)]\s*/, '')
738
+ .replace(/^\*\*(.+?)\*\*:?\s*/, '$1: ')
739
+ .slice(0, 100);
740
+
741
+ if (name.length >= 10 && !implTasks.some(t => t.name === name)) {
742
+ implTasks.push({
743
+ name,
744
+ description: name,
745
+ });
746
+ }
747
+ }
748
+ }
749
+
750
+ if (implTasks.length > 0) {
751
+ // Group implementation tasks
752
+ const tasksPerMilestone = 5;
753
+ for (let i = 0; i < implTasks.length; i += tasksPerMilestone) {
754
+ const milestoneTasks = implTasks.slice(i, i + tasksPerMilestone);
755
+ const milestoneNum = Math.floor(i / tasksPerMilestone) + 1;
756
+
757
+ milestones.push({
758
+ name: `Implementation Phase ${milestoneNum}`,
759
+ description: `Tasks ${i + 1} to ${Math.min(i + tasksPerMilestone, implTasks.length)}`,
760
+ tasks: milestoneTasks as Task[],
761
+ status: 'pending',
762
+ });
763
+ }
764
+ } else {
765
+ // Final fallback: Create structured tasks based on common project phases
766
+ // This should rarely happen if the plan is well-structured
767
+ console.warn('[plan-parser] Warning: Could not parse tasks from plan. Using default structure.');
768
+
769
+ milestones.push({
770
+ name: 'Core Implementation',
771
+ description: 'Implement core functionality based on the plan',
772
+ tasks: [
773
+ {
774
+ name: 'Set up project structure and dependencies',
775
+ description: 'Initialize project with required structure, dependencies, and configuration',
776
+ },
777
+ {
778
+ name: 'Implement core features',
779
+ description: 'Build the main features as described in the development plan',
780
+ },
781
+ {
782
+ name: 'Add data models and storage',
783
+ description: 'Create data models, database schema, and storage layer',
784
+ },
785
+ ] as Task[],
786
+ status: 'pending',
787
+ });
788
+
789
+ milestones.push({
790
+ name: 'Integration and Testing',
791
+ description: 'Connect components and verify functionality',
792
+ tasks: [
793
+ {
794
+ name: 'Integrate components',
795
+ description: 'Connect all components and ensure they work together',
796
+ },
797
+ {
798
+ name: 'Write and run tests',
799
+ description: 'Create unit tests, integration tests, and verify all tests pass',
800
+ },
801
+ {
802
+ name: 'Final verification and documentation',
803
+ description: 'Run final verification, update documentation, ensure project works correctly',
804
+ },
805
+ ] as Task[],
806
+ status: 'pending',
807
+ });
808
+ }
809
+ }
208
810
  }
209
811
 
210
812
  return milestones;
@@ -221,24 +823,44 @@ export async function runPlanMode(
221
823
  spec: ProjectSpec,
222
824
  options: PlanModeOptions
223
825
  ): Promise<PlanModeResult> {
224
- const { projectDir, consensusConfig, onProgress } = options;
826
+ const { projectDir, consensusConfig, additionalContext, onProgress } = options;
827
+
828
+ // Initialize workflow logger
829
+ const logger = getWorkflowLogger(projectDir);
225
830
 
226
831
  try {
227
832
  // Create or load project
228
833
  onProgress?.('plan-init', 'Initializing project...');
834
+ await logger.stageStart('init', 'Plan Mode initialization', {
835
+ projectName: spec.name,
836
+ language: spec.language,
837
+ idea: spec.idea.slice(0, 200),
838
+ });
229
839
 
230
840
  let state: ProjectState;
231
841
  try {
232
842
  state = await loadProject(projectDir);
233
843
  onProgress?.('plan-init', 'Loaded existing project');
844
+ await logger.info('init', 'project_loaded', 'Loaded existing project', {
845
+ projectName: state.name,
846
+ phase: state.phase,
847
+ hasPlan: !!state.plan,
848
+ hasSpecification: !!state.specification,
849
+ });
234
850
  } catch {
235
851
  state = await createProject(spec, projectDir);
236
852
  onProgress?.('plan-init', 'Created new project');
853
+ await logger.success('init', 'project_created', 'Created new project', {
854
+ projectName: state.name,
855
+ language: state.language,
856
+ });
237
857
  }
238
858
 
239
859
  // Expand idea if we don't have a specification
240
860
  if (!state.specification) {
241
861
  onProgress?.('expand-idea', 'Expanding idea into specification...');
862
+ await logger.stageStart('plan-generation', 'Expanding idea into specification');
863
+
242
864
  const specification = await expandIdea(
243
865
  spec.idea,
244
866
  spec.language,
@@ -247,36 +869,97 @@ export async function runPlanMode(
247
869
 
248
870
  state = await storeSpecification(projectDir, specification);
249
871
  onProgress?.('expand-idea', 'Specification complete');
872
+ await logger.stageComplete('plan-generation', 'Specification created', {
873
+ specificationLength: specification.length,
874
+ specificationPreview: specification.slice(0, 300),
875
+ });
876
+ }
877
+
878
+ // Design UI early in the process
879
+ onProgress?.('ui-design', 'Designing UI from project idea...');
880
+ try {
881
+ const uiSpec = await designUI(spec.idea, (msg) => onProgress?.('ui-design', msg));
882
+ await saveUISpecification(projectDir, uiSpec);
883
+ onProgress?.('ui-design', `UI design complete: ${uiSpec.themeName} theme, ${uiSpec.recommendedComponents.length} components`);
884
+ await logger.success('ui-design', 'ui_design_complete', 'UI design specification created', {
885
+ theme: uiSpec.themeName,
886
+ projectType: uiSpec.projectType,
887
+ components: uiSpec.recommendedComponents.length,
888
+ });
889
+ } catch (uiError) {
890
+ // Non-blocking - UI design failures shouldn't stop the workflow
891
+ onProgress?.('ui-design', `UI design skipped: ${uiError instanceof Error ? uiError.message : 'Unknown error'}`);
892
+ await logger.warn('ui-design', 'ui_design_skipped', 'UI design was skipped', {
893
+ error: uiError instanceof Error ? uiError.message : 'Unknown error',
894
+ });
250
895
  }
251
896
 
252
897
  // Get project context
253
898
  onProgress?.('get-context', 'Gathering project context...');
254
- const context = await getProjectContext(
899
+ let context = await getProjectContext(
255
900
  projectDir,
256
901
  (msg) => onProgress?.('get-context', msg)
257
902
  );
258
903
 
904
+ // Append additional context if provided (e.g., when resuming with guidance)
905
+ if (additionalContext) {
906
+ onProgress?.('get-context', 'Incorporating additional guidance...');
907
+ context = `${context}\n\nADDITIONAL GUIDANCE FROM USER:\n${additionalContext}`;
908
+ }
909
+
259
910
  // Create initial plan if we don't have one
260
911
  if (!state.plan) {
261
912
  onProgress?.('create-plan', 'Creating development plan...');
913
+ await logger.stageStart('plan-generation', 'Creating development plan');
914
+
262
915
  const plan = await createPlan(
263
916
  state.specification!,
264
917
  context,
918
+ spec.language,
265
919
  (msg) => onProgress?.('create-plan', msg)
266
920
  );
267
921
 
268
922
  state = await storePlan(projectDir, plan);
269
923
  onProgress?.('create-plan', 'Initial plan created');
924
+ await logger.stageComplete('plan-generation', 'Development plan created', {
925
+ planLength: plan.length,
926
+ planPreview: plan.slice(0, 500),
927
+ });
928
+
929
+ // Validate fullstack plan structure
930
+ if (spec.language === 'fullstack') {
931
+ onProgress?.('create-plan', 'Validating fullstack plan structure...');
932
+ const validation = validateFullstackPlan(plan);
933
+
934
+ await logger.info('plan-generation', 'fullstack_validation', 'Fullstack plan validation', {
935
+ valid: validation.valid,
936
+ stats: validation.stats,
937
+ issueCount: validation.issues.length,
938
+ });
939
+
940
+ if (!validation.valid) {
941
+ onProgress?.('create-plan', `Fullstack plan validation warnings: ${validation.issues.length} issues`);
942
+ for (const issue of validation.issues.slice(0, 3)) {
943
+ onProgress?.('create-plan', ` - ${issue}`);
944
+ }
945
+ } else {
946
+ onProgress?.('create-plan', `Fullstack plan validated: ${validation.stats.feTasks} FE, ${validation.stats.beTasks} BE, ${validation.stats.intTasks} INT tasks`);
947
+ }
948
+ }
270
949
  }
271
950
 
272
951
  // Run consensus loop
273
952
  onProgress?.('consensus', 'Starting consensus review...');
953
+ await logger.stageStart('consensus', 'Starting consensus review process');
954
+
274
955
  const consensusResult = await iterateUntilConsensus(
275
956
  state.plan!,
276
957
  context,
277
958
  {
278
959
  projectDir,
279
960
  config: consensusConfig,
961
+ isFullstack: spec.language === 'fullstack',
962
+ language: spec.language,
280
963
  onIteration: (iteration, result) => {
281
964
  onProgress?.(
282
965
  'consensus',
@@ -286,29 +969,214 @@ export async function runPlanMode(
286
969
  onRevision: (iteration, _plan) => {
287
970
  onProgress?.('consensus', `Revising plan (iteration ${iteration})...`);
288
971
  },
972
+ onConcerns: (concerns, recommendations) => {
973
+ if (concerns.length > 0) {
974
+ onProgress?.('concerns', `Concerns: ${concerns.slice(0, 2).join('; ')}`);
975
+ }
976
+ if (recommendations.length > 0) {
977
+ onProgress?.('recommendations', `Suggestions: ${recommendations.slice(0, 2).join('; ')}`);
978
+ }
979
+ },
980
+ onArbitration: (result) => {
981
+ onProgress?.('arbitration', `Arbitrator decision: ${result.approved ? 'APPROVED' : 'REVISE'} (${result.score}%)`);
982
+ if (!result.approved && result.suggestedChanges.length > 0) {
983
+ onProgress?.('arbitration', `Changes: ${result.suggestedChanges.slice(0, 2).join('; ')}`);
984
+ }
985
+ },
986
+ onProgress,
289
987
  }
290
988
  );
291
989
 
292
- // Store final plan
293
- if (consensusResult.approved) {
294
- state = await storePlan(projectDir, consensusResult.finalPlan);
990
+ // Log consensus result
991
+ await logger.info('consensus', 'consensus_complete', 'Consensus process completed', {
992
+ approved: consensusResult.approved,
993
+ finalScore: consensusResult.finalScore,
994
+ bestScore: consensusResult.bestScore,
995
+ totalIterations: consensusResult.totalIterations,
996
+ arbitrated: consensusResult.arbitrated,
997
+ });
998
+
999
+ // Check if the plan is garbage (Claude's thinking instead of actual content)
1000
+ const garbageCheck = detectGarbagePlan(consensusResult.bestPlan);
1001
+ if (garbageCheck.isGarbage) {
1002
+ onProgress?.(
1003
+ 'error',
1004
+ `PLAN VALIDATION FAILED: ${garbageCheck.reason}`
1005
+ );
1006
+ onProgress?.(
1007
+ 'error',
1008
+ 'The plan contains Claude\'s thinking/conversation instead of actual plan content.'
1009
+ );
1010
+ onProgress?.(
1011
+ 'info',
1012
+ 'This typically happens when Claude describes what it will do instead of outputting the plan.'
1013
+ );
1014
+ onProgress?.(
1015
+ 'info',
1016
+ 'Saving garbage plan for debugging. Try running again or provide more specific requirements.'
1017
+ );
1018
+
1019
+ // Still save the plan for debugging
1020
+ await documentPlan(projectDir, consensusResult.bestPlan, 'PLAN-FAILED.md');
1021
+
1022
+ await logger.stageFailed('plan-parsing', 'Plan validation', garbageCheck.reason!, {
1023
+ planLength: consensusResult.bestPlan.length,
1024
+ reason: garbageCheck.reason,
1025
+ });
1026
+
1027
+ return {
1028
+ success: false,
1029
+ state,
1030
+ consensusResult,
1031
+ error: `Plan generation failed: ${garbageCheck.reason}`,
1032
+ };
1033
+ }
1034
+
1035
+ // Always store the best plan (even if consensus failed)
1036
+ state = await storePlan(projectDir, consensusResult.bestPlan);
1037
+
1038
+ // Parse and add milestones from best plan
1039
+ await logger.stageStart('plan-parsing', 'Parsing plan into milestones and tasks');
1040
+ const milestones = parsePlanMilestones(consensusResult.bestPlan);
1041
+
1042
+ // Log parsed milestones for debugging
1043
+ const totalTasks = milestones.reduce((sum, m) => sum + m.tasks.length, 0);
1044
+ onProgress?.(
1045
+ 'plan-structure',
1046
+ `Parsed plan: ${milestones.length} milestones, ${totalTasks} tasks`
1047
+ );
1048
+
1049
+ // Log detailed parsing results
1050
+ const parsedMilestones = milestones.map(m => ({
1051
+ name: m.name,
1052
+ taskCount: m.tasks.length,
1053
+ taskNames: m.tasks.map(t => t.name),
1054
+ }));
1055
+ await logger.info('plan-parsing', 'plan_parsed', 'Parsed plan structure', {
1056
+ milestonesCount: milestones.length,
1057
+ totalTasks: totalTasks,
1058
+ milestones: parsedMilestones,
1059
+ });
1060
+
1061
+ // VALIDATION: Fail if too few milestones/tasks for a real project
1062
+ if (milestones.length <= 1 && totalTasks <= 2) {
1063
+ onProgress?.(
1064
+ 'error',
1065
+ `PLAN VALIDATION FAILED: Only ${milestones.length} milestone(s) and ${totalTasks} task(s) extracted.`
1066
+ );
1067
+ onProgress?.(
1068
+ 'error',
1069
+ 'A valid plan should have at least 2 milestones with 3+ tasks each.'
1070
+ );
1071
+ onProgress?.(
1072
+ 'info',
1073
+ 'Expected format: "## Milestone N: Name" and "### Task N.N: Name"'
1074
+ );
295
1075
 
296
- // Parse and add milestones
297
- const milestones = parsePlanMilestones(consensusResult.finalPlan);
298
- state = await addMilestones(projectDir, milestones);
1076
+ // Save the problematic plan for debugging
1077
+ await documentPlan(projectDir, consensusResult.bestPlan, 'PLAN-INSUFFICIENT.md');
299
1078
 
300
- // Document the plan
301
- await documentPlan(projectDir, consensusResult.finalPlan);
1079
+ // Show what was found in the plan
1080
+ onProgress?.('debug', 'Tasks extracted from plan:');
1081
+ for (const m of milestones) {
1082
+ for (const t of m.tasks) {
1083
+ onProgress?.('debug', ` - ${t.name}`);
1084
+ }
1085
+ }
302
1086
 
1087
+ await logger.stageFailed('plan-parsing', 'Plan validation', 'Insufficient tasks extracted', {
1088
+ milestonesCount: milestones.length,
1089
+ totalTasks: totalTasks,
1090
+ expectedMinTasks: 3,
1091
+ extractedTasks: milestones.flatMap(m => m.tasks.map(t => t.name)),
1092
+ });
1093
+
1094
+ return {
1095
+ success: false,
1096
+ state,
1097
+ consensusResult,
1098
+ error: `Plan parsing failed: only ${totalTasks} task(s) extracted. Plan needs more structure.`,
1099
+ };
1100
+ }
1101
+
1102
+ // Warn if suspiciously few tasks (but don't block)
1103
+ if (milestones.length <= 2 || totalTasks <= 5) {
1104
+ onProgress?.(
1105
+ 'warning',
1106
+ `Warning: Only ${milestones.length} milestone(s) and ${totalTasks} task(s) parsed. ` +
1107
+ `This seems low for a complete project. Consider reviewing the plan.`
1108
+ );
1109
+ }
1110
+
1111
+ // Log each milestone and its tasks
1112
+ for (const milestone of milestones) {
1113
+ onProgress?.(
1114
+ 'plan-detail',
1115
+ ` Milestone: ${milestone.name} (${milestone.tasks.length} tasks)`
1116
+ );
1117
+ for (const task of milestone.tasks.slice(0, 3)) {
1118
+ onProgress?.('plan-detail', ` - ${task.name}`);
1119
+ }
1120
+ if (milestone.tasks.length > 3) {
1121
+ onProgress?.('plan-detail', ` ... and ${milestone.tasks.length - 3} more tasks`);
1122
+ }
1123
+ }
1124
+
1125
+ state = await addMilestones(projectDir, milestones);
1126
+
1127
+ // Always document the plan (so user can see what was achieved)
1128
+ const planFilename = consensusResult.approved ? 'PLAN.md' : 'PLAN-DRAFT.md';
1129
+ await documentPlan(projectDir, consensusResult.bestPlan, planFilename);
1130
+
1131
+ if (consensusResult.approved) {
303
1132
  // Transition to execution phase
304
1133
  state = await setPhase(projectDir, 'execution');
305
1134
 
306
- onProgress?.('complete', `Plan approved with ${consensusResult.finalScore}% consensus`);
1135
+ if (consensusResult.arbitrated) {
1136
+ onProgress?.('complete', `Plan approved via arbitration with ${consensusResult.finalScore}% confidence`);
1137
+ } else {
1138
+ onProgress?.('complete', `Plan approved with ${consensusResult.finalScore}% consensus`);
1139
+ }
1140
+ onProgress?.('info', `Plan saved to docs/PLAN.md`);
1141
+
1142
+ await logger.stageComplete('plan-generation', 'Plan Mode completed successfully', {
1143
+ consensusScore: consensusResult.finalScore,
1144
+ arbitrated: consensusResult.arbitrated,
1145
+ milestonesCount: milestones.length,
1146
+ totalTasks: totalTasks,
1147
+ nextPhase: 'execution',
1148
+ });
307
1149
  } else {
1150
+ // Show why consensus failed
308
1151
  onProgress?.(
309
1152
  'failed',
310
- `Consensus not reached after ${consensusResult.totalIterations} iterations (${consensusResult.finalScore}%)`
1153
+ `Consensus not reached after ${consensusResult.totalIterations} iterations (best: ${consensusResult.bestScore}% at iteration ${consensusResult.bestIteration})`
311
1154
  );
1155
+
1156
+ // Show remaining concerns
1157
+ if (consensusResult.finalConcerns.length > 0) {
1158
+ onProgress?.('concerns', `Remaining concerns:`);
1159
+ for (const concern of consensusResult.finalConcerns.slice(0, 3)) {
1160
+ onProgress?.('concerns', ` - ${concern}`);
1161
+ }
1162
+ }
1163
+
1164
+ // Show recommendations
1165
+ if (consensusResult.finalRecommendations.length > 0) {
1166
+ onProgress?.('recommendations', `Recommendations:`);
1167
+ for (const rec of consensusResult.finalRecommendations.slice(0, 3)) {
1168
+ onProgress?.('recommendations', ` - ${rec}`);
1169
+ }
1170
+ }
1171
+
1172
+ onProgress?.('info', `Draft plan saved to docs/${planFilename}`);
1173
+
1174
+ await logger.warn('plan-generation', 'consensus_failed', 'Plan Mode incomplete - consensus not reached', {
1175
+ bestScore: consensusResult.bestScore,
1176
+ totalIterations: consensusResult.totalIterations,
1177
+ finalConcerns: consensusResult.finalConcerns,
1178
+ finalRecommendations: consensusResult.finalRecommendations,
1179
+ });
312
1180
  }
313
1181
 
314
1182
  return {
@@ -320,6 +1188,12 @@ export async function runPlanMode(
320
1188
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
321
1189
  onProgress?.('error', errorMessage);
322
1190
 
1191
+ // Log the error
1192
+ await logger.stageFailed('plan-generation', 'Plan Mode execution', errorMessage, {
1193
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
1194
+ stack: error instanceof Error ? error.stack : undefined,
1195
+ });
1196
+
323
1197
  return {
324
1198
  success: false,
325
1199
  state: await loadProject(projectDir).catch(() => ({} as ProjectState)),