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
@@ -0,0 +1,528 @@
1
+ /**
2
+ * Task-level workflow
3
+ * Handles the per-task consensus cycle: Plan → Consensus → Implement → Test
4
+ */
5
+
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import type { ProjectState, Task, Milestone } from '../types/workflow.js';
9
+ import type { ConsensusConfig } from '../types/consensus.js';
10
+ import { createPlan as claudeCreatePlan } from '../adapters/claude.js';
11
+ import {
12
+ loadProject,
13
+ updateState,
14
+ } from '../state/index.js';
15
+ import { iterateUntilConsensus, runOptimizedConsensusProcess, type ConsensusProcessResult } from './consensus.js';
16
+ import { executeTask as executeTaskCode } from './execution-mode.js';
17
+ import { runTests, testsExist, getTestSummary, type TestResult } from './test-runner.js';
18
+
19
+ /**
20
+ * Options for task workflow
21
+ */
22
+ export interface TaskWorkflowOptions {
23
+ projectDir: string;
24
+ consensusConfig?: Partial<ConsensusConfig>;
25
+ maxRetries?: number;
26
+ onProgress?: (phase: string, message: string) => void;
27
+ }
28
+
29
+ /**
30
+ * Result of task workflow
31
+ */
32
+ export interface TaskWorkflowResult {
33
+ success: boolean;
34
+ task: Task;
35
+ consensusResult?: ConsensusProcessResult;
36
+ testResult?: TestResult;
37
+ error?: string;
38
+ }
39
+
40
+ /**
41
+ * Create a detailed implementation plan for a specific task
42
+ *
43
+ * @param task - The task to plan
44
+ * @param milestone - The parent milestone
45
+ * @param state - Current project state
46
+ * @param onProgress - Progress callback
47
+ * @returns The task implementation plan
48
+ */
49
+ async function createTaskPlan(
50
+ task: Task,
51
+ milestone: Milestone,
52
+ state: ProjectState,
53
+ onProgress?: (message: string) => void
54
+ ): Promise<string> {
55
+ onProgress?.('Creating detailed task plan...');
56
+
57
+ const context = `
58
+ ## Project Context
59
+ Project: ${state.name}
60
+ Language: ${state.language}
61
+
62
+ ## Milestone: ${milestone.name}
63
+ ${milestone.description}
64
+
65
+ ## Overall Project Plan
66
+ ${state.plan?.slice(0, 2000) || 'No overall plan available'}
67
+
68
+ ## Completed Tasks in This Milestone
69
+ ${milestone.tasks
70
+ .filter(t => t.status === 'complete')
71
+ .map(t => `- ${t.name}`)
72
+ .join('\n') || 'None yet'}
73
+ `.trim();
74
+
75
+ const prompt = `
76
+ Create a detailed implementation plan for the following task:
77
+
78
+ ## Task: ${task.name}
79
+ ${task.description}
80
+
81
+ ${task.testPlan ? `## Test Requirements\n${task.testPlan}` : ''}
82
+
83
+ Please provide:
84
+ 1. **Implementation Steps**: Specific code changes needed
85
+ 2. **Files to Create/Modify**: List all files that will be touched
86
+ 3. **Dependencies**: Any packages or modules needed
87
+ 4. **Acceptance Criteria**: How to verify the task is complete
88
+ 5. **Test Plan**: Specific tests to write
89
+
90
+ Be specific and actionable. This plan will be reviewed for consensus before implementation.
91
+ `.trim();
92
+
93
+ const result = await claudeCreatePlan(prompt, context, state.language, onProgress);
94
+
95
+ if (!result.success) {
96
+ throw new Error(`Failed to create task plan: ${result.error}`);
97
+ }
98
+
99
+ return result.response;
100
+ }
101
+
102
+ /**
103
+ * Document a task plan to the docs folder
104
+ *
105
+ * @param projectDir - Project directory
106
+ * @param milestone - Parent milestone
107
+ * @param task - The task
108
+ * @param plan - The plan content
109
+ * @param consensusResult - The consensus result
110
+ * @returns Path to the document
111
+ */
112
+ async function documentTaskPlan(
113
+ projectDir: string,
114
+ milestone: Milestone,
115
+ task: Task,
116
+ plan: string,
117
+ consensusResult: ConsensusProcessResult
118
+ ): Promise<string> {
119
+ const docsDir = path.join(projectDir, 'docs', 'tasks');
120
+ await fs.mkdir(docsDir, { recursive: true });
121
+
122
+ const milestoneNum = milestone.id.replace('milestone-', '');
123
+ const taskNum = task.id.split('-task-')[1] || '1';
124
+ const filename = `milestone_${milestoneNum}_task_${taskNum}_plan.md`;
125
+ const docPath = path.join(docsDir, filename);
126
+
127
+ const content = `# Task Plan: ${task.name}
128
+
129
+ ## Metadata
130
+ - **Milestone**: ${milestone.name}
131
+ - **Task ID**: ${task.id}
132
+ - **Consensus Score**: ${consensusResult.finalScore}%
133
+ - **Iterations**: ${consensusResult.totalIterations}
134
+ - **Status**: ${consensusResult.approved ? 'APPROVED' : 'NOT APPROVED'}
135
+ - **Generated**: ${new Date().toISOString()}
136
+
137
+ ## Task Description
138
+ ${task.description}
139
+
140
+ ## Implementation Plan
141
+ ${plan}
142
+
143
+ ${consensusResult.finalConcerns.length > 0 ? `
144
+ ## Notes from Review
145
+ ${consensusResult.finalConcerns.map(c => `- ${c}`).join('\n')}
146
+ ` : ''}
147
+ `;
148
+
149
+ await fs.writeFile(docPath, content, 'utf-8');
150
+ return `docs/tasks/${filename}`;
151
+ }
152
+
153
+ /**
154
+ * Document task test results
155
+ *
156
+ * @param projectDir - Project directory
157
+ * @param milestone - Parent milestone
158
+ * @param task - The task
159
+ * @param testResult - Test results
160
+ * @returns Path to the document
161
+ */
162
+ async function documentTestResults(
163
+ projectDir: string,
164
+ milestone: Milestone,
165
+ task: Task,
166
+ testResult: TestResult
167
+ ): Promise<string> {
168
+ const docsDir = path.join(projectDir, 'docs', 'tests');
169
+ await fs.mkdir(docsDir, { recursive: true });
170
+
171
+ const milestoneNum = milestone.id.replace('milestone-', '');
172
+ const taskNum = task.id.split('-task-')[1] || '1';
173
+ const filename = `milestone_${milestoneNum}_task_${taskNum}_tests.md`;
174
+ const docPath = path.join(docsDir, filename);
175
+
176
+ const content = `# Test Results: ${task.name}
177
+
178
+ ## Summary
179
+ - **Status**: ${testResult.success ? 'PASSED' : 'FAILED'}
180
+ - **Total Tests**: ${testResult.total}
181
+ - **Passed**: ${testResult.passed}
182
+ - **Failed**: ${testResult.failed}
183
+ - **Execution Time**: ${new Date().toISOString()}
184
+
185
+ ## Task Details
186
+ - **Milestone**: ${milestone.name}
187
+ - **Task ID**: ${task.id}
188
+
189
+ ## Test Output
190
+ \`\`\`
191
+ ${testResult.output.slice(0, 5000)}
192
+ \`\`\`
193
+
194
+ ${testResult.failedTests && testResult.failedTests.length > 0 ? `
195
+ ## Failed Tests
196
+ ${testResult.failedTests.map(t => `- ${t}`).join('\n')}
197
+ ` : ''}
198
+ `;
199
+
200
+ await fs.writeFile(docPath, content, 'utf-8');
201
+ return `docs/tests/${filename}`;
202
+ }
203
+
204
+ /**
205
+ * Update task in state with new data
206
+ */
207
+ async function updateTaskInState(
208
+ projectDir: string,
209
+ taskId: string,
210
+ updates: Partial<Task>
211
+ ): Promise<ProjectState> {
212
+ const state = await loadProject(projectDir);
213
+
214
+ const updatedMilestones = state.milestones.map(milestone => ({
215
+ ...milestone,
216
+ tasks: milestone.tasks.map(task =>
217
+ task.id === taskId ? { ...task, ...updates } : task
218
+ ),
219
+ }));
220
+
221
+ return updateState(projectDir, { milestones: updatedMilestones });
222
+ }
223
+
224
+ /**
225
+ * Run the complete task workflow: Plan → Consensus → Implement → Test
226
+ *
227
+ * @param milestone - The parent milestone
228
+ * @param task - The task to execute
229
+ * @param options - Workflow options
230
+ * @returns Task workflow result
231
+ */
232
+ export async function runTaskWorkflow(
233
+ milestone: Milestone,
234
+ task: Task,
235
+ options: TaskWorkflowOptions
236
+ ): Promise<TaskWorkflowResult> {
237
+ const {
238
+ projectDir,
239
+ consensusConfig,
240
+ maxRetries = 3,
241
+ onProgress,
242
+ } = options;
243
+
244
+ try {
245
+ let state = await loadProject(projectDir);
246
+
247
+ // Reload task from state to get latest data (for resume scenarios)
248
+ const currentMilestone = state.milestones.find(m => m.id === milestone.id);
249
+ const currentTask = currentMilestone?.tasks.find(t => t.id === task.id);
250
+ if (currentTask) {
251
+ task = currentTask;
252
+ }
253
+
254
+ // Check if we're resuming from a previous attempt
255
+ const hasApprovedPlan = task.consensusApproved && task.plan;
256
+ const hasCompletedImplementation = task.implementationComplete;
257
+
258
+ // Mark task as in-progress
259
+ state = await updateTaskInState(projectDir, task.id, {
260
+ status: 'in-progress',
261
+ error: undefined, // Clear previous error
262
+ });
263
+
264
+ let consensusResult: ConsensusProcessResult;
265
+
266
+ // ============================================
267
+ // PHASE 1-2: Plan and Consensus (skip if already approved)
268
+ // ============================================
269
+ if (hasApprovedPlan) {
270
+ onProgress?.('task-plan', `Using existing approved plan for: ${task.name} (Score: ${task.consensusScore}%)`);
271
+
272
+ // Create mock consensus result from saved data
273
+ consensusResult = {
274
+ approved: true,
275
+ finalPlan: task.plan!,
276
+ finalScore: task.consensusScore || 95,
277
+ bestPlan: task.plan!,
278
+ bestScore: task.consensusScore || 95,
279
+ bestIteration: task.consensusIterations || 1,
280
+ totalIterations: task.consensusIterations || 1,
281
+ iterations: [],
282
+ finalConcerns: [],
283
+ finalRecommendations: [],
284
+ arbitrated: false,
285
+ };
286
+ } else {
287
+ // ============================================
288
+ // PHASE 1: Create Task Plan
289
+ // ============================================
290
+ onProgress?.('task-plan', `Planning task: ${task.name}`);
291
+
292
+ const taskPlan = await createTaskPlan(
293
+ task,
294
+ milestone,
295
+ state,
296
+ (msg) => onProgress?.('task-plan', msg)
297
+ );
298
+
299
+ // Store plan in task
300
+ state = await updateTaskInState(projectDir, task.id, {
301
+ plan: taskPlan,
302
+ });
303
+
304
+ // ============================================
305
+ // PHASE 2: Get Consensus on Task Plan
306
+ // ============================================
307
+ onProgress?.('task-consensus', `Getting consensus for task: ${task.name}`);
308
+
309
+ const context = `
310
+ Project: ${state.name}
311
+ Language: ${state.language}
312
+ Milestone: ${milestone.name}
313
+ Task: ${task.name}
314
+ `.trim();
315
+
316
+ // Use optimized consensus with batched feedback and plan storage
317
+ const useOptimized = consensusConfig?.useOptimizedConsensus !== false;
318
+
319
+ if (useOptimized) {
320
+ onProgress?.('task-consensus', `Using optimized consensus (batched feedback, file-based tracking)`);
321
+ consensusResult = await runOptimizedConsensusProcess(
322
+ taskPlan,
323
+ context,
324
+ {
325
+ projectDir,
326
+ config: consensusConfig,
327
+ milestoneId: milestone.id,
328
+ milestoneName: milestone.name,
329
+ taskId: task.id,
330
+ taskName: task.name,
331
+ parallelReviews: true,
332
+ isFullstack: state.language === 'fullstack',
333
+ onIteration: (iteration, result) => {
334
+ onProgress?.('task-consensus', `Iteration ${iteration}: ${result.score}%`);
335
+ },
336
+ onProgress,
337
+ }
338
+ );
339
+ } else {
340
+ // Fallback to original consensus
341
+ consensusResult = await iterateUntilConsensus(
342
+ taskPlan,
343
+ context,
344
+ {
345
+ projectDir,
346
+ config: consensusConfig,
347
+ isFullstack: state.language === 'fullstack',
348
+ language: state.language,
349
+ onIteration: (iteration, result) => {
350
+ onProgress?.('task-consensus', `Iteration ${iteration}: ${result.score}%`);
351
+ },
352
+ onProgress,
353
+ }
354
+ );
355
+ }
356
+
357
+ // Document the task plan
358
+ const planDocPath = await documentTaskPlan(
359
+ projectDir,
360
+ milestone,
361
+ task,
362
+ consensusResult.bestPlan,
363
+ consensusResult
364
+ );
365
+
366
+ // Update task with consensus results
367
+ state = await updateTaskInState(projectDir, task.id, {
368
+ plan: consensusResult.bestPlan,
369
+ consensusScore: consensusResult.finalScore,
370
+ consensusIterations: consensusResult.totalIterations,
371
+ consensusApproved: consensusResult.approved,
372
+ planDoc: planDocPath,
373
+ });
374
+
375
+ // Check if consensus was achieved
376
+ if (!consensusResult.approved) {
377
+ onProgress?.('task-consensus', `Consensus not reached for task: ${task.name} (${consensusResult.finalScore}%)`);
378
+
379
+ state = await updateTaskInState(projectDir, task.id, {
380
+ status: 'failed',
381
+ error: `Consensus not reached: ${consensusResult.finalScore}%`,
382
+ });
383
+
384
+ return {
385
+ success: false,
386
+ task: { ...task, status: 'failed' },
387
+ consensusResult,
388
+ error: `Task plan not approved. Score: ${consensusResult.finalScore}%`,
389
+ };
390
+ }
391
+
392
+ onProgress?.('task-consensus', `Task plan approved with ${consensusResult.finalScore}% consensus`);
393
+ }
394
+
395
+ // ============================================
396
+ // PHASE 3: Implement the Task (skip if already complete)
397
+ // ============================================
398
+ if (hasCompletedImplementation) {
399
+ onProgress?.('task-implement', `Implementation already complete for: ${task.name}, skipping to tests...`);
400
+ } else {
401
+ onProgress?.('task-implement', `Implementing task: ${task.name}`);
402
+
403
+ const implementResult = await executeTaskCode(
404
+ task,
405
+ consensusResult.bestPlan, // Use the approved plan as context
406
+ projectDir,
407
+ (msg) => onProgress?.('task-implement', msg)
408
+ );
409
+
410
+ if (!implementResult.success) {
411
+ state = await updateTaskInState(projectDir, task.id, {
412
+ status: 'failed',
413
+ error: implementResult.error,
414
+ });
415
+
416
+ return {
417
+ success: false,
418
+ task: { ...task, status: 'failed' },
419
+ consensusResult,
420
+ error: `Implementation failed: ${implementResult.error}`,
421
+ };
422
+ }
423
+
424
+ // Mark implementation as complete (for resume purposes)
425
+ state = await updateTaskInState(projectDir, task.id, {
426
+ implementationComplete: true,
427
+ });
428
+
429
+ onProgress?.('task-implement', `Implementation complete for: ${task.name}`);
430
+ }
431
+
432
+ // ============================================
433
+ // PHASE 4: Run Tests
434
+ // ============================================
435
+ const hasTests = await testsExist(projectDir, state.language);
436
+
437
+ if (hasTests) {
438
+ onProgress?.('task-test', `Running tests for: ${task.name}`);
439
+
440
+ let retries = 0;
441
+ let testResult: TestResult | undefined;
442
+
443
+ while (retries <= maxRetries) {
444
+ testResult = await runTests(projectDir, state.language);
445
+
446
+ // Document test results
447
+ const testDocPath = await documentTestResults(
448
+ projectDir,
449
+ milestone,
450
+ task,
451
+ testResult
452
+ );
453
+
454
+ state = await updateTaskInState(projectDir, task.id, {
455
+ testResultsDoc: testDocPath,
456
+ });
457
+
458
+ if (testResult.success) {
459
+ onProgress?.('task-test', `Tests passed: ${getTestSummary(testResult)}`);
460
+ break;
461
+ }
462
+
463
+ retries++;
464
+ if (retries <= maxRetries) {
465
+ onProgress?.('task-test', `Tests failed, retry ${retries}/${maxRetries}...`);
466
+ // Could add fix attempt here
467
+ }
468
+ }
469
+
470
+ if (testResult && !testResult.success) {
471
+ state = await updateTaskInState(projectDir, task.id, {
472
+ status: 'failed',
473
+ testsPassed: false,
474
+ error: `Tests failed after ${retries} retries`,
475
+ });
476
+
477
+ return {
478
+ success: false,
479
+ task: { ...task, status: 'failed', testsPassed: false },
480
+ consensusResult,
481
+ testResult,
482
+ error: `Tests failed: ${getTestSummary(testResult)}`,
483
+ };
484
+ }
485
+
486
+ // Mark task as complete
487
+ state = await updateTaskInState(projectDir, task.id, {
488
+ status: 'complete',
489
+ testsPassed: true,
490
+ });
491
+
492
+ return {
493
+ success: true,
494
+ task: { ...task, status: 'complete', testsPassed: true },
495
+ consensusResult,
496
+ testResult,
497
+ };
498
+ }
499
+
500
+ // No tests - mark as complete
501
+ state = await updateTaskInState(projectDir, task.id, {
502
+ status: 'complete',
503
+ });
504
+
505
+ onProgress?.('task-complete', `Task complete: ${task.name}`);
506
+
507
+ return {
508
+ success: true,
509
+ task: { ...task, status: 'complete' },
510
+ consensusResult,
511
+ };
512
+
513
+ } catch (error) {
514
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
515
+ onProgress?.('task-error', errorMessage);
516
+
517
+ await updateTaskInState(projectDir, task.id, {
518
+ status: 'failed',
519
+ error: errorMessage,
520
+ });
521
+
522
+ return {
523
+ success: false,
524
+ task: { ...task, status: 'failed', error: errorMessage },
525
+ error: errorMessage,
526
+ };
527
+ }
528
+ }
@@ -17,6 +17,7 @@ export interface TestResult {
17
17
  output: string;
18
18
  failedTests?: string[];
19
19
  error?: string;
20
+ noTestsFound?: boolean; // True when no tests were found (still counts as success)
20
21
  }
21
22
 
22
23
  /**
@@ -36,6 +37,7 @@ export interface TestConfig {
36
37
  export const DEFAULT_TEST_COMMANDS: Record<OutputLanguage, string> = {
37
38
  python: 'python -m pytest tests/ -v',
38
39
  typescript: 'npm test',
40
+ fullstack: 'npm run test:all', // Runs both frontend and backend tests via workspace.json
39
41
  };
40
42
 
41
43
  /**
@@ -77,6 +79,12 @@ export function buildTestCommand(config: TestConfig): string {
77
79
 
78
80
  return parts.join(' ');
79
81
  }
82
+
83
+ case 'fullstack': {
84
+ // Fullstack projects use workspace.json commands
85
+ // Default to running both frontend and backend tests
86
+ return 'npm run test:all';
87
+ }
80
88
  }
81
89
  }
82
90
 
@@ -136,9 +144,43 @@ export function parseTestOutput(output: string, language: OutputLanguage): TestR
136
144
  }
137
145
  break;
138
146
  }
147
+
148
+ case 'fullstack': {
149
+ // Fullstack combines pytest and jest output
150
+ // Parse both formats
151
+ const pytestMatch = output.match(/(\d+)\s+passed/);
152
+ const pytestFailedMatch = output.match(/(\d+)\s+failed/);
153
+ const jestMatch = output.match(/Tests:\s*(?:(\d+)\s+failed,\s*)?(\d+)\s+passed,\s*(\d+)\s+total/);
154
+
155
+ if (pytestMatch) {
156
+ passed += parseInt(pytestMatch[1], 10);
157
+ }
158
+ if (pytestFailedMatch) {
159
+ failed += parseInt(pytestFailedMatch[1], 10);
160
+ }
161
+ if (jestMatch) {
162
+ failed += jestMatch[1] ? parseInt(jestMatch[1], 10) : 0;
163
+ passed += parseInt(jestMatch[2], 10);
164
+ }
165
+
166
+ total = passed + failed;
167
+
168
+ // Extract failed test names from both pytest and jest
169
+ const pytestFailedMatches = output.matchAll(/FAILED\s+([^\s]+)/g);
170
+ for (const match of pytestFailedMatches) {
171
+ failedTests.push(match[1]);
172
+ }
173
+ const jestFailedMatches = output.matchAll(/✕\s+(.+)/g);
174
+ for (const match of jestFailedMatches) {
175
+ failedTests.push(match[1].trim());
176
+ }
177
+ break;
178
+ }
139
179
  }
140
180
 
141
- const success = failed === 0 && total > 0;
181
+ // Success if no failures - treat "no tests found" as success (not a failure)
182
+ // If total === 0, there are no tests to fail, so it's technically a pass
183
+ const success = failed === 0;
142
184
 
143
185
  return {
144
186
  success,
@@ -147,6 +189,8 @@ export function parseTestOutput(output: string, language: OutputLanguage): TestR
147
189
  total,
148
190
  output,
149
191
  failedTests: failedTests.length > 0 ? failedTests : undefined,
192
+ // Flag to indicate no tests were found (for informational purposes)
193
+ noTestsFound: total === 0,
150
194
  };
151
195
  }
152
196
 
@@ -236,6 +280,48 @@ export async function runTypeScriptTests(
236
280
  }
237
281
  }
238
282
 
283
+ /**
284
+ * Run fullstack tests (both frontend and backend)
285
+ *
286
+ * @param cwd - Working directory
287
+ * @param config - Test configuration
288
+ * @returns Combined test result
289
+ */
290
+ export async function runFullstackTests(
291
+ cwd: string,
292
+ config: Partial<TestConfig> = {}
293
+ ): Promise<TestResult> {
294
+ const path = await import('node:path');
295
+
296
+ // Run backend tests first
297
+ const backendCwd = path.join(cwd, 'apps', 'backend');
298
+ const backendResult = await runPythonTests(backendCwd, config);
299
+
300
+ // Run frontend tests
301
+ const frontendCwd = path.join(cwd, 'apps', 'frontend');
302
+ const frontendResult = await runTypeScriptTests(frontendCwd, config);
303
+
304
+ // Combine results
305
+ const combinedOutput = `=== Backend Tests ===\n${backendResult.output}\n\n=== Frontend Tests ===\n${frontendResult.output}`;
306
+
307
+ return {
308
+ success: backendResult.success && frontendResult.success,
309
+ passed: backendResult.passed + frontendResult.passed,
310
+ failed: backendResult.failed + frontendResult.failed,
311
+ total: backendResult.total + frontendResult.total,
312
+ output: combinedOutput,
313
+ failedTests: [
314
+ ...(backendResult.failedTests || []).map((t) => `[backend] ${t}`),
315
+ ...(frontendResult.failedTests || []).map((t) => `[frontend] ${t}`),
316
+ ].length > 0 ? [
317
+ ...(backendResult.failedTests || []).map((t) => `[backend] ${t}`),
318
+ ...(frontendResult.failedTests || []).map((t) => `[frontend] ${t}`),
319
+ ] : undefined,
320
+ error: backendResult.error || frontendResult.error,
321
+ noTestsFound: backendResult.noTestsFound && frontendResult.noTestsFound,
322
+ };
323
+ }
324
+
239
325
  /**
240
326
  * Run tests for a project
241
327
  *
@@ -254,6 +340,8 @@ export async function runTests(
254
340
  return runPythonTests(cwd, config);
255
341
  case 'typescript':
256
342
  return runTypeScriptTests(cwd, config);
343
+ case 'fullstack':
344
+ return runFullstackTests(cwd, config);
257
345
  }
258
346
  }
259
347
 
@@ -313,6 +401,33 @@ export async function testsExist(
313
401
  }
314
402
  }
315
403
  }
404
+
405
+ case 'fullstack': {
406
+ // Check for tests in both frontend and backend
407
+ const backendTestsDir = path.join(cwd, 'apps', 'backend', 'tests');
408
+ const frontendTestsDir = path.join(cwd, 'apps', 'frontend', 'src');
409
+
410
+ let hasBackendTests = false;
411
+ let hasFrontendTests = false;
412
+
413
+ try {
414
+ await fs.access(backendTestsDir);
415
+ hasBackendTests = true;
416
+ } catch {
417
+ // No backend tests directory
418
+ }
419
+
420
+ try {
421
+ const files = await fs.readdir(frontendTestsDir, { recursive: true });
422
+ hasFrontendTests = files.some(
423
+ (f) => f.toString().endsWith('.test.ts') || f.toString().endsWith('.spec.ts')
424
+ );
425
+ } catch {
426
+ // No frontend test files
427
+ }
428
+
429
+ return hasBackendTests || hasFrontendTests;
430
+ }
316
431
  }
317
432
  } catch {
318
433
  return false;
@@ -330,12 +445,12 @@ export function getTestSummary(result: TestResult): string {
330
445
  return `Tests failed to run: ${result.error}`;
331
446
  }
332
447
 
333
- if (result.total === 0) {
334
- return 'No tests found';
448
+ if (result.total === 0 || result.noTestsFound) {
449
+ return 'No tests found (OK)';
335
450
  }
336
451
 
337
- const status = result.success ? '' : '';
338
- let summary = `${status} ${result.passed}/${result.total} tests passed`;
452
+ const status = result.success ? 'PASS' : 'FAIL';
453
+ let summary = `${status}: ${result.passed}/${result.total} tests passed`;
339
454
 
340
455
  if (result.failed > 0) {
341
456
  summary += ` (${result.failed} failed)`;