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.
- package/.env.example +24 -1
- package/CONTRIBUTING.md +275 -0
- package/OPEN_SOURCE_MANIFESTO.md +172 -0
- package/README.md +832 -123
- package/dist/adapters/claude.d.ts +19 -4
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +908 -42
- package/dist/adapters/claude.js.map +1 -1
- package/dist/adapters/gemini.d.ts +55 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +318 -0
- package/dist/adapters/gemini.js.map +1 -0
- package/dist/adapters/grok.d.ts +73 -0
- package/dist/adapters/grok.d.ts.map +1 -0
- package/dist/adapters/grok.js +430 -0
- package/dist/adapters/grok.js.map +1 -0
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +47 -8
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/claude.d.ts +11 -9
- package/dist/auth/claude.d.ts.map +1 -1
- package/dist/auth/claude.js +107 -71
- package/dist/auth/claude.js.map +1 -1
- package/dist/auth/gemini.d.ts +58 -0
- package/dist/auth/gemini.d.ts.map +1 -0
- package/dist/auth/gemini.js +172 -0
- package/dist/auth/gemini.js.map +1 -0
- package/dist/auth/grok.d.ts +73 -0
- package/dist/auth/grok.d.ts.map +1 -0
- package/dist/auth/grok.js +211 -0
- package/dist/auth/grok.js.map +1 -0
- package/dist/auth/index.d.ts +14 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +41 -6
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/keychain.d.ts +20 -7
- package/dist/auth/keychain.d.ts.map +1 -1
- package/dist/auth/keychain.js +85 -29
- package/dist/auth/keychain.js.map +1 -1
- package/dist/auth/openai.d.ts +2 -2
- package/dist/auth/openai.d.ts.map +1 -1
- package/dist/auth/openai.js +30 -32
- package/dist/auth/openai.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +79 -8
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +15 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +1494 -114
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts +9 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +19 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +19 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +33 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +47 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +29 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/fullstack.d.ts +32 -0
- package/dist/generators/fullstack.d.ts.map +1 -0
- package/dist/generators/fullstack.js +497 -0
- package/dist/generators/fullstack.js.map +1 -0
- package/dist/generators/index.d.ts +4 -3
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +15 -1
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/python.d.ts +17 -1
- package/dist/generators/python.d.ts.map +1 -1
- package/dist/generators/python.js +34 -20
- package/dist/generators/python.js.map +1 -1
- package/dist/generators/templates/fullstack.d.ts +113 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -0
- package/dist/generators/templates/fullstack.js +1004 -0
- package/dist/generators/templates/fullstack.js.map +1 -0
- package/dist/generators/typescript.d.ts +19 -1
- package/dist/generators/typescript.d.ts.map +1 -1
- package/dist/generators/typescript.js +37 -20
- package/dist/generators/typescript.js.map +1 -1
- package/dist/state/index.d.ts +108 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +551 -4
- package/dist/state/index.js.map +1 -1
- package/dist/state/registry.d.ts +52 -0
- package/dist/state/registry.d.ts.map +1 -0
- package/dist/state/registry.js +215 -0
- package/dist/state/registry.js.map +1 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js.map +1 -1
- package/dist/types/consensus.d.ts +186 -4
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +35 -3
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/project.d.ts +76 -0
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +1 -1
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +217 -16
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +40 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/auto-fix.d.ts +45 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -0
- package/dist/workflow/auto-fix.js +274 -0
- package/dist/workflow/auto-fix.js.map +1 -0
- package/dist/workflow/consensus.d.ts +70 -2
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +872 -17
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts +10 -4
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +547 -58
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +14 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +69 -6
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts +34 -0
- package/dist/workflow/milestone-workflow.d.ts.map +1 -0
- package/dist/workflow/milestone-workflow.js +414 -0
- package/dist/workflow/milestone-workflow.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +80 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +767 -49
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-storage.d.ts +386 -0
- package/dist/workflow/plan-storage.d.ts.map +1 -0
- package/dist/workflow/plan-storage.js +878 -0
- package/dist/workflow/plan-storage.js.map +1 -0
- package/dist/workflow/project-verification.d.ts +37 -0
- package/dist/workflow/project-verification.d.ts.map +1 -0
- package/dist/workflow/project-verification.js +381 -0
- package/dist/workflow/project-verification.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts +37 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -0
- package/dist/workflow/task-workflow.js +386 -0
- package/dist/workflow/task-workflow.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +9 -0
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +101 -5
- package/dist/workflow/test-runner.js.map +1 -1
- package/dist/workflow/ui-designer.d.ts +82 -0
- package/dist/workflow/ui-designer.d.ts.map +1 -0
- package/dist/workflow/ui-designer.js +234 -0
- package/dist/workflow/ui-designer.js.map +1 -0
- package/dist/workflow/ui-setup.d.ts +58 -0
- package/dist/workflow/ui-setup.d.ts.map +1 -0
- package/dist/workflow/ui-setup.js +685 -0
- package/dist/workflow/ui-setup.js.map +1 -0
- package/dist/workflow/ui-verification.d.ts +114 -0
- package/dist/workflow/ui-verification.d.ts.map +1 -0
- package/dist/workflow/ui-verification.js +258 -0
- package/dist/workflow/ui-verification.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +110 -0
- package/dist/workflow/workflow-logger.d.ts.map +1 -0
- package/dist/workflow/workflow-logger.js +267 -0
- package/dist/workflow/workflow-logger.js.map +1 -0
- package/dist/workflow/workspace-manager.d.ts +342 -0
- package/dist/workflow/workspace-manager.d.ts.map +1 -0
- package/dist/workflow/workspace-manager.js +733 -0
- package/dist/workflow/workspace-manager.js.map +1 -0
- package/package.json +2 -2
- package/src/adapters/claude.ts +1067 -47
- package/src/adapters/gemini.ts +373 -0
- package/src/adapters/grok.ts +492 -0
- package/src/adapters/openai.ts +48 -9
- package/src/auth/claude.ts +120 -78
- package/src/auth/gemini.ts +207 -0
- package/src/auth/grok.ts +255 -0
- package/src/auth/index.ts +47 -9
- package/src/auth/keychain.ts +95 -28
- package/src/auth/openai.ts +29 -36
- package/src/cli/commands/auth.ts +89 -10
- package/src/cli/commands/create.ts +13 -4
- package/src/cli/interactive.ts +1774 -142
- package/src/config/defaults.ts +19 -2
- package/src/config/index.ts +36 -1
- package/src/config/schema.ts +30 -1
- package/src/generators/fullstack.ts +551 -0
- package/src/generators/index.ts +25 -1
- package/src/generators/python.ts +65 -20
- package/src/generators/templates/fullstack.ts +1047 -0
- package/src/generators/typescript.ts +69 -20
- package/src/state/index.ts +713 -4
- package/src/state/registry.ts +278 -0
- package/src/types/cli.ts +8 -0
- package/src/types/consensus.ts +197 -6
- package/src/types/project.ts +82 -1
- package/src/types/workflow.ts +90 -1
- package/src/workflow/auto-fix.ts +340 -0
- package/src/workflow/consensus.ts +1180 -16
- package/src/workflow/execution-mode.ts +673 -74
- package/src/workflow/index.ts +95 -6
- package/src/workflow/milestone-workflow.ts +576 -0
- package/src/workflow/plan-mode.ts +924 -50
- package/src/workflow/plan-storage.ts +1282 -0
- package/src/workflow/project-verification.ts +471 -0
- package/src/workflow/task-workflow.ts +528 -0
- package/src/workflow/test-runner.ts +120 -5
- package/src/workflow/ui-designer.ts +337 -0
- package/src/workflow/ui-setup.ts +797 -0
- package/src/workflow/ui-verification.ts +357 -0
- package/src/workflow/workflow-logger.ts +353 -0
- package/src/workflow/workspace-manager.ts +912 -0
- package/tests/config/config.test.ts +1 -1
- package/tests/types/consensus.test.ts +3 -3
- package/tests/workflow/plan-mode.test.ts +213 -0
- 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
|
-
|
|
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)`;
|