popeye-cli 1.0.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 +25 -0
- package/.prettierrc +8 -0
- package/README.md +320 -0
- package/dist/adapters/claude.d.ts +82 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +230 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/openai.d.ts +48 -0
- package/dist/adapters/openai.d.ts.map +1 -0
- package/dist/adapters/openai.js +257 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/auth/claude.d.ts +44 -0
- package/dist/auth/claude.d.ts.map +1 -0
- package/dist/auth/claude.js +139 -0
- package/dist/auth/claude.js.map +1 -0
- package/dist/auth/index.d.ts +61 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +141 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/keychain.d.ts +66 -0
- package/dist/auth/keychain.d.ts.map +1 -0
- package/dist/auth/keychain.js +125 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/openai-entry.d.ts +9 -0
- package/dist/auth/openai-entry.d.ts.map +1 -0
- package/dist/auth/openai-entry.js +410 -0
- package/dist/auth/openai-entry.js.map +1 -0
- package/dist/auth/openai.d.ts +71 -0
- package/dist/auth/openai.d.ts.map +1 -0
- package/dist/auth/openai.js +212 -0
- package/dist/auth/openai.js.map +1 -0
- package/dist/auth/server.d.ts +32 -0
- package/dist/auth/server.d.ts.map +1 -0
- package/dist/auth/server.js +213 -0
- package/dist/auth/server.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +10 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +162 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/config.d.ts +10 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +215 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/create.d.ts +10 -0
- package/dist/cli/commands/create.d.ts.map +1 -0
- package/dist/cli/commands/create.js +240 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/index.d.ts +10 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +10 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/resume.d.ts +18 -0
- package/dist/cli/commands/resume.d.ts.map +1 -0
- package/dist/cli/commands/resume.js +241 -0
- package/dist/cli/commands/resume.js.map +1 -0
- package/dist/cli/commands/status.d.ts +18 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +154 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +71 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/interactive.d.ts +9 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +330 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/output.d.ts +182 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +355 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/config/defaults.d.ts +57 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +103 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +138 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +244 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +220 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +141 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/generators/index.d.ts +101 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +200 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/python.d.ts +48 -0
- package/dist/generators/python.d.ts.map +1 -0
- package/dist/generators/python.js +262 -0
- package/dist/generators/python.js.map +1 -0
- package/dist/generators/templates/index.d.ts +6 -0
- package/dist/generators/templates/index.d.ts.map +1 -0
- package/dist/generators/templates/index.js +6 -0
- package/dist/generators/templates/index.js.map +1 -0
- package/dist/generators/templates/python.d.ts +53 -0
- package/dist/generators/templates/python.d.ts.map +1 -0
- package/dist/generators/templates/python.js +454 -0
- package/dist/generators/templates/python.js.map +1 -0
- package/dist/generators/templates/typescript.d.ts +53 -0
- package/dist/generators/templates/typescript.d.ts.map +1 -0
- package/dist/generators/templates/typescript.js +394 -0
- package/dist/generators/templates/typescript.js.map +1 -0
- package/dist/generators/typescript.d.ts +64 -0
- package/dist/generators/typescript.d.ts.map +1 -0
- package/dist/generators/typescript.js +271 -0
- package/dist/generators/typescript.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/state/index.d.ts +168 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +338 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/persistence.d.ts +91 -0
- package/dist/state/persistence.d.ts.map +1 -0
- package/dist/state/persistence.js +201 -0
- package/dist/state/persistence.js.map +1 -0
- package/dist/types/cli.d.ts +132 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +17 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/consensus.d.ts +111 -0
- package/dist/types/consensus.d.ts.map +1 -0
- package/dist/types/consensus.js +29 -0
- package/dist/types/consensus.js.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/project.d.ts +73 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/types/project.js +55 -0
- package/dist/types/project.js.map +1 -0
- package/dist/types/workflow.d.ts +236 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/dist/types/workflow.js +74 -0
- package/dist/types/workflow.js.map +1 -0
- package/dist/workflow/consensus.d.ts +89 -0
- package/dist/workflow/consensus.d.ts.map +1 -0
- package/dist/workflow/consensus.js +220 -0
- package/dist/workflow/consensus.js.map +1 -0
- package/dist/workflow/execution-mode.d.ts +82 -0
- package/dist/workflow/execution-mode.d.ts.map +1 -0
- package/dist/workflow/execution-mode.js +346 -0
- package/dist/workflow/execution-mode.js.map +1 -0
- package/dist/workflow/index.d.ts +110 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +283 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +83 -0
- package/dist/workflow/plan-mode.d.ts.map +1 -0
- package/dist/workflow/plan-mode.js +241 -0
- package/dist/workflow/plan-mode.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +87 -0
- package/dist/workflow/test-runner.d.ts.map +1 -0
- package/dist/workflow/test-runner.js +273 -0
- package/dist/workflow/test-runner.js.map +1 -0
- package/eslint.config.js +25 -0
- package/package.json +66 -0
- package/src/adapters/claude.ts +298 -0
- package/src/adapters/openai.ts +300 -0
- package/src/auth/claude.ts +166 -0
- package/src/auth/index.ts +171 -0
- package/src/auth/keychain.ts +138 -0
- package/src/auth/openai-entry.ts +410 -0
- package/src/auth/openai.ts +260 -0
- package/src/auth/server.ts +252 -0
- package/src/cli/commands/auth.ts +194 -0
- package/src/cli/commands/config.ts +241 -0
- package/src/cli/commands/create.ts +308 -0
- package/src/cli/commands/index.ts +10 -0
- package/src/cli/commands/resume.ts +304 -0
- package/src/cli/commands/status.ts +189 -0
- package/src/cli/index.ts +90 -0
- package/src/cli/interactive.ts +418 -0
- package/src/cli/output.ts +410 -0
- package/src/config/defaults.ts +114 -0
- package/src/config/index.ts +315 -0
- package/src/config/schema.ts +164 -0
- package/src/generators/index.ts +251 -0
- package/src/generators/python.ts +318 -0
- package/src/generators/templates/index.ts +6 -0
- package/src/generators/templates/python.ts +465 -0
- package/src/generators/templates/typescript.ts +417 -0
- package/src/generators/typescript.ts +340 -0
- package/src/index.ts +13 -0
- package/src/state/index.ts +454 -0
- package/src/state/persistence.ts +230 -0
- package/src/types/cli.ts +146 -0
- package/src/types/consensus.ts +116 -0
- package/src/types/index.ts +64 -0
- package/src/types/project.ts +85 -0
- package/src/types/workflow.ts +149 -0
- package/src/workflow/consensus.ts +299 -0
- package/src/workflow/execution-mode.ts +517 -0
- package/src/workflow/index.ts +396 -0
- package/src/workflow/plan-mode.ts +356 -0
- package/src/workflow/test-runner.ts +345 -0
- package/tests/adapters/openai.test.ts +145 -0
- package/tests/config/config.test.ts +208 -0
- package/tests/generators/generators.test.ts +185 -0
- package/tests/types/consensus.test.ts +152 -0
- package/tests/types/project.test.ts +134 -0
- package/tests/workflow/consensus.test.ts +221 -0
- package/tests/workflow/test-runner.test.ts +214 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode workflow
|
|
3
|
+
* Handles idea expansion, plan creation, and consensus building
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import type { ProjectSpec } from '../types/project.js';
|
|
9
|
+
import type { ProjectState, Milestone, Task } from '../types/workflow.js';
|
|
10
|
+
import type { ConsensusConfig } from '../types/consensus.js';
|
|
11
|
+
import { expandIdea as openaiExpandIdea } from '../adapters/openai.js';
|
|
12
|
+
import { createPlan as claudeCreatePlan, analyzeCodebase } from '../adapters/claude.js';
|
|
13
|
+
import {
|
|
14
|
+
createProject,
|
|
15
|
+
loadProject,
|
|
16
|
+
setPhase,
|
|
17
|
+
storePlan,
|
|
18
|
+
storeSpecification,
|
|
19
|
+
addMilestones,
|
|
20
|
+
} from '../state/index.js';
|
|
21
|
+
import { iterateUntilConsensus, type ConsensusProcessResult } from './consensus.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for plan mode
|
|
25
|
+
*/
|
|
26
|
+
export interface PlanModeOptions {
|
|
27
|
+
projectDir: string;
|
|
28
|
+
consensusConfig?: Partial<ConsensusConfig>;
|
|
29
|
+
onProgress?: (phase: string, message: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Result of plan mode
|
|
34
|
+
*/
|
|
35
|
+
export interface PlanModeResult {
|
|
36
|
+
success: boolean;
|
|
37
|
+
state: ProjectState;
|
|
38
|
+
consensusResult?: ConsensusProcessResult;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Expand a brief idea into a detailed specification
|
|
44
|
+
*
|
|
45
|
+
* @param idea - The brief project idea
|
|
46
|
+
* @param language - Target programming language
|
|
47
|
+
* @param onProgress - Progress callback
|
|
48
|
+
* @returns Expanded specification
|
|
49
|
+
*/
|
|
50
|
+
export async function expandIdea(
|
|
51
|
+
idea: string,
|
|
52
|
+
language: 'python' | 'typescript',
|
|
53
|
+
onProgress?: (message: string) => void
|
|
54
|
+
): Promise<string> {
|
|
55
|
+
onProgress?.('Expanding idea into specification...');
|
|
56
|
+
|
|
57
|
+
const specification = await openaiExpandIdea(idea, language);
|
|
58
|
+
|
|
59
|
+
onProgress?.('Specification created');
|
|
60
|
+
return specification;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Create a development plan from a specification
|
|
65
|
+
*
|
|
66
|
+
* @param specification - The project specification
|
|
67
|
+
* @param context - Additional context
|
|
68
|
+
* @param onProgress - Progress callback
|
|
69
|
+
* @returns Development plan
|
|
70
|
+
*/
|
|
71
|
+
export async function createPlan(
|
|
72
|
+
specification: string,
|
|
73
|
+
context: string = '',
|
|
74
|
+
onProgress?: (message: string) => void
|
|
75
|
+
): Promise<string> {
|
|
76
|
+
onProgress?.('Creating development plan...');
|
|
77
|
+
|
|
78
|
+
const result = await claudeCreatePlan(specification, context);
|
|
79
|
+
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
throw new Error(`Failed to create plan: ${result.error}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onProgress?.('Development plan created');
|
|
85
|
+
return result.response;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get existing project context by analyzing the codebase
|
|
90
|
+
*
|
|
91
|
+
* @param projectDir - The project directory
|
|
92
|
+
* @param onProgress - Progress callback
|
|
93
|
+
* @returns Context string
|
|
94
|
+
*/
|
|
95
|
+
export async function getProjectContext(
|
|
96
|
+
projectDir: string,
|
|
97
|
+
onProgress?: (message: string) => void
|
|
98
|
+
): Promise<string> {
|
|
99
|
+
onProgress?.('Analyzing existing codebase...');
|
|
100
|
+
|
|
101
|
+
// Check if directory has any code
|
|
102
|
+
try {
|
|
103
|
+
const files = await fs.readdir(projectDir);
|
|
104
|
+
const hasCode = files.some((f) =>
|
|
105
|
+
['.py', '.ts', '.js', '.tsx', '.jsx'].some((ext) => f.endsWith(ext))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!hasCode) {
|
|
109
|
+
onProgress?.('No existing code found');
|
|
110
|
+
return 'New project - no existing codebase';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const result = await analyzeCodebase(projectDir);
|
|
114
|
+
|
|
115
|
+
if (result.success) {
|
|
116
|
+
onProgress?.('Codebase analysis complete');
|
|
117
|
+
return result.response;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return 'Unable to analyze codebase';
|
|
121
|
+
} catch {
|
|
122
|
+
return 'New project - no existing codebase';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Save the plan to a markdown file
|
|
128
|
+
*
|
|
129
|
+
* @param projectDir - The project directory
|
|
130
|
+
* @param plan - The plan content
|
|
131
|
+
* @param filename - The filename (default: PLAN.md)
|
|
132
|
+
*/
|
|
133
|
+
export async function documentPlan(
|
|
134
|
+
projectDir: string,
|
|
135
|
+
plan: string,
|
|
136
|
+
filename: string = 'PLAN.md'
|
|
137
|
+
): Promise<string> {
|
|
138
|
+
const planPath = path.join(projectDir, filename);
|
|
139
|
+
|
|
140
|
+
const content = `# Development Plan
|
|
141
|
+
|
|
142
|
+
Generated: ${new Date().toISOString()}
|
|
143
|
+
|
|
144
|
+
${plan}
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
await fs.writeFile(planPath, content, 'utf-8');
|
|
148
|
+
return planPath;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse milestones and tasks from a plan
|
|
153
|
+
*
|
|
154
|
+
* @param plan - The plan content
|
|
155
|
+
* @returns Parsed milestones with tasks
|
|
156
|
+
*/
|
|
157
|
+
export function parsePlanMilestones(plan: string): Omit<Milestone, 'id'>[] {
|
|
158
|
+
const milestones: Omit<Milestone, 'id'>[] = [];
|
|
159
|
+
|
|
160
|
+
// Look for milestone sections
|
|
161
|
+
const milestonePattern = /#+\s*(?:Milestone\s*\d+[:\s]*)?([^\n]+)\n([\s\S]*?)(?=#+\s*(?:Milestone|$)|$)/gi;
|
|
162
|
+
const taskPattern = /[-*]\s*(?:\[[ x]\]\s*)?(?:Task[:\s]*)?(.+)/gi;
|
|
163
|
+
|
|
164
|
+
let match;
|
|
165
|
+
while ((match = milestonePattern.exec(plan)) !== null) {
|
|
166
|
+
const name = match[1].trim();
|
|
167
|
+
const content = match[2];
|
|
168
|
+
|
|
169
|
+
// Skip non-milestone sections
|
|
170
|
+
if (name.toLowerCase().includes('background') ||
|
|
171
|
+
name.toLowerCase().includes('goal') ||
|
|
172
|
+
name.toLowerCase().includes('risk') ||
|
|
173
|
+
name.toLowerCase().includes('summary')) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const tasks: Omit<Task, 'id' | 'status' | 'testsPassed'>[] = [];
|
|
178
|
+
let taskMatch;
|
|
179
|
+
|
|
180
|
+
while ((taskMatch = taskPattern.exec(content)) !== null) {
|
|
181
|
+
const taskName = taskMatch[1].trim();
|
|
182
|
+
if (taskName && !taskName.toLowerCase().startsWith('test')) {
|
|
183
|
+
tasks.push({
|
|
184
|
+
name: taskName,
|
|
185
|
+
description: taskName,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (tasks.length > 0 || name.toLowerCase().includes('milestone')) {
|
|
191
|
+
milestones.push({
|
|
192
|
+
name,
|
|
193
|
+
description: content.slice(0, 200).trim(),
|
|
194
|
+
tasks: tasks as Task[],
|
|
195
|
+
status: 'pending',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
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
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return milestones;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Run the complete plan mode workflow
|
|
215
|
+
*
|
|
216
|
+
* @param spec - The project specification
|
|
217
|
+
* @param options - Plan mode options
|
|
218
|
+
* @returns Plan mode result
|
|
219
|
+
*/
|
|
220
|
+
export async function runPlanMode(
|
|
221
|
+
spec: ProjectSpec,
|
|
222
|
+
options: PlanModeOptions
|
|
223
|
+
): Promise<PlanModeResult> {
|
|
224
|
+
const { projectDir, consensusConfig, onProgress } = options;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Create or load project
|
|
228
|
+
onProgress?.('plan-init', 'Initializing project...');
|
|
229
|
+
|
|
230
|
+
let state: ProjectState;
|
|
231
|
+
try {
|
|
232
|
+
state = await loadProject(projectDir);
|
|
233
|
+
onProgress?.('plan-init', 'Loaded existing project');
|
|
234
|
+
} catch {
|
|
235
|
+
state = await createProject(spec, projectDir);
|
|
236
|
+
onProgress?.('plan-init', 'Created new project');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Expand idea if we don't have a specification
|
|
240
|
+
if (!state.specification) {
|
|
241
|
+
onProgress?.('expand-idea', 'Expanding idea into specification...');
|
|
242
|
+
const specification = await expandIdea(
|
|
243
|
+
spec.idea,
|
|
244
|
+
spec.language,
|
|
245
|
+
(msg) => onProgress?.('expand-idea', msg)
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
state = await storeSpecification(projectDir, specification);
|
|
249
|
+
onProgress?.('expand-idea', 'Specification complete');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Get project context
|
|
253
|
+
onProgress?.('get-context', 'Gathering project context...');
|
|
254
|
+
const context = await getProjectContext(
|
|
255
|
+
projectDir,
|
|
256
|
+
(msg) => onProgress?.('get-context', msg)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Create initial plan if we don't have one
|
|
260
|
+
if (!state.plan) {
|
|
261
|
+
onProgress?.('create-plan', 'Creating development plan...');
|
|
262
|
+
const plan = await createPlan(
|
|
263
|
+
state.specification!,
|
|
264
|
+
context,
|
|
265
|
+
(msg) => onProgress?.('create-plan', msg)
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
state = await storePlan(projectDir, plan);
|
|
269
|
+
onProgress?.('create-plan', 'Initial plan created');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Run consensus loop
|
|
273
|
+
onProgress?.('consensus', 'Starting consensus review...');
|
|
274
|
+
const consensusResult = await iterateUntilConsensus(
|
|
275
|
+
state.plan!,
|
|
276
|
+
context,
|
|
277
|
+
{
|
|
278
|
+
projectDir,
|
|
279
|
+
config: consensusConfig,
|
|
280
|
+
onIteration: (iteration, result) => {
|
|
281
|
+
onProgress?.(
|
|
282
|
+
'consensus',
|
|
283
|
+
`Iteration ${iteration}: Score ${result.score}%`
|
|
284
|
+
);
|
|
285
|
+
},
|
|
286
|
+
onRevision: (iteration, _plan) => {
|
|
287
|
+
onProgress?.('consensus', `Revising plan (iteration ${iteration})...`);
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Store final plan
|
|
293
|
+
if (consensusResult.approved) {
|
|
294
|
+
state = await storePlan(projectDir, consensusResult.finalPlan);
|
|
295
|
+
|
|
296
|
+
// Parse and add milestones
|
|
297
|
+
const milestones = parsePlanMilestones(consensusResult.finalPlan);
|
|
298
|
+
state = await addMilestones(projectDir, milestones);
|
|
299
|
+
|
|
300
|
+
// Document the plan
|
|
301
|
+
await documentPlan(projectDir, consensusResult.finalPlan);
|
|
302
|
+
|
|
303
|
+
// Transition to execution phase
|
|
304
|
+
state = await setPhase(projectDir, 'execution');
|
|
305
|
+
|
|
306
|
+
onProgress?.('complete', `Plan approved with ${consensusResult.finalScore}% consensus`);
|
|
307
|
+
} else {
|
|
308
|
+
onProgress?.(
|
|
309
|
+
'failed',
|
|
310
|
+
`Consensus not reached after ${consensusResult.totalIterations} iterations (${consensusResult.finalScore}%)`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
success: consensusResult.approved,
|
|
316
|
+
state,
|
|
317
|
+
consensusResult,
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
321
|
+
onProgress?.('error', errorMessage);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
success: false,
|
|
325
|
+
state: await loadProject(projectDir).catch(() => ({} as ProjectState)),
|
|
326
|
+
error: errorMessage,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Resume plan mode from where it left off
|
|
333
|
+
*
|
|
334
|
+
* @param projectDir - The project directory
|
|
335
|
+
* @param options - Plan mode options
|
|
336
|
+
* @returns Plan mode result
|
|
337
|
+
*/
|
|
338
|
+
export async function resumePlanMode(
|
|
339
|
+
projectDir: string,
|
|
340
|
+
options: Omit<PlanModeOptions, 'projectDir'>
|
|
341
|
+
): Promise<PlanModeResult> {
|
|
342
|
+
const state = await loadProject(projectDir);
|
|
343
|
+
|
|
344
|
+
return runPlanMode(
|
|
345
|
+
{
|
|
346
|
+
idea: state.idea,
|
|
347
|
+
name: state.name,
|
|
348
|
+
language: state.language,
|
|
349
|
+
openaiModel: state.openaiModel,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
...options,
|
|
353
|
+
projectDir,
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test runner module
|
|
3
|
+
* Handles running tests for Python and TypeScript projects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { runTests as claudeRunTests } from '../adapters/claude.js';
|
|
7
|
+
import type { OutputLanguage } from '../types/project.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Test result from running tests
|
|
11
|
+
*/
|
|
12
|
+
export interface TestResult {
|
|
13
|
+
success: boolean;
|
|
14
|
+
passed: number;
|
|
15
|
+
failed: number;
|
|
16
|
+
total: number;
|
|
17
|
+
output: string;
|
|
18
|
+
failedTests?: string[];
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Test configuration
|
|
24
|
+
*/
|
|
25
|
+
export interface TestConfig {
|
|
26
|
+
language: OutputLanguage;
|
|
27
|
+
testDir?: string;
|
|
28
|
+
coverage?: boolean;
|
|
29
|
+
verbose?: boolean;
|
|
30
|
+
timeout?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Default test commands by language
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_TEST_COMMANDS: Record<OutputLanguage, string> = {
|
|
37
|
+
python: 'python -m pytest tests/ -v',
|
|
38
|
+
typescript: 'npm test',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the test command for a language
|
|
43
|
+
*
|
|
44
|
+
* @param config - Test configuration
|
|
45
|
+
* @returns The test command to run
|
|
46
|
+
*/
|
|
47
|
+
export function buildTestCommand(config: TestConfig): string {
|
|
48
|
+
const { language, testDir, coverage, verbose } = config;
|
|
49
|
+
|
|
50
|
+
switch (language) {
|
|
51
|
+
case 'python': {
|
|
52
|
+
const parts = ['python', '-m', 'pytest'];
|
|
53
|
+
|
|
54
|
+
if (testDir) {
|
|
55
|
+
parts.push(testDir);
|
|
56
|
+
} else {
|
|
57
|
+
parts.push('tests/');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (verbose) {
|
|
61
|
+
parts.push('-v');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (coverage) {
|
|
65
|
+
parts.push('--cov=src', '--cov-report=term-missing');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return parts.join(' ');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case 'typescript': {
|
|
72
|
+
const parts = ['npm', 'test'];
|
|
73
|
+
|
|
74
|
+
if (coverage) {
|
|
75
|
+
parts.push('--', '--coverage');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parts.join(' ');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse test output to extract results
|
|
85
|
+
*
|
|
86
|
+
* @param output - The test command output
|
|
87
|
+
* @param language - The project language
|
|
88
|
+
* @returns Parsed test result
|
|
89
|
+
*/
|
|
90
|
+
export function parseTestOutput(output: string, language: OutputLanguage): TestResult {
|
|
91
|
+
let passed = 0;
|
|
92
|
+
let failed = 0;
|
|
93
|
+
let total = 0;
|
|
94
|
+
const failedTests: string[] = [];
|
|
95
|
+
|
|
96
|
+
switch (language) {
|
|
97
|
+
case 'python': {
|
|
98
|
+
// Parse pytest output
|
|
99
|
+
// Example: "5 passed, 2 failed, 1 skipped in 2.34s"
|
|
100
|
+
const summaryMatch = output.match(/(\d+)\s+passed/);
|
|
101
|
+
const failedMatch = output.match(/(\d+)\s+failed/);
|
|
102
|
+
|
|
103
|
+
if (summaryMatch) {
|
|
104
|
+
passed = parseInt(summaryMatch[1], 10);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (failedMatch) {
|
|
108
|
+
failed = parseInt(failedMatch[1], 10);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
total = passed + failed;
|
|
112
|
+
|
|
113
|
+
// Extract failed test names
|
|
114
|
+
const failedTestMatches = output.matchAll(/FAILED\s+([^\s]+)/g);
|
|
115
|
+
for (const match of failedTestMatches) {
|
|
116
|
+
failedTests.push(match[1]);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'typescript': {
|
|
122
|
+
// Parse Jest/Vitest output
|
|
123
|
+
// Example: "Tests: 2 failed, 5 passed, 7 total"
|
|
124
|
+
const summaryMatch = output.match(/Tests:\s*(?:(\d+)\s+failed,\s*)?(\d+)\s+passed,\s*(\d+)\s+total/);
|
|
125
|
+
|
|
126
|
+
if (summaryMatch) {
|
|
127
|
+
failed = summaryMatch[1] ? parseInt(summaryMatch[1], 10) : 0;
|
|
128
|
+
passed = parseInt(summaryMatch[2], 10);
|
|
129
|
+
total = parseInt(summaryMatch[3], 10);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Extract failed test names
|
|
133
|
+
const failedTestMatches = output.matchAll(/✕\s+(.+)/g);
|
|
134
|
+
for (const match of failedTestMatches) {
|
|
135
|
+
failedTests.push(match[1].trim());
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const success = failed === 0 && total > 0;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
success,
|
|
145
|
+
passed,
|
|
146
|
+
failed,
|
|
147
|
+
total,
|
|
148
|
+
output,
|
|
149
|
+
failedTests: failedTests.length > 0 ? failedTests : undefined,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Run Python tests
|
|
155
|
+
*
|
|
156
|
+
* @param cwd - Working directory
|
|
157
|
+
* @param config - Test configuration
|
|
158
|
+
* @returns Test result
|
|
159
|
+
*/
|
|
160
|
+
export async function runPythonTests(
|
|
161
|
+
cwd: string,
|
|
162
|
+
config: Partial<TestConfig> = {}
|
|
163
|
+
): Promise<TestResult> {
|
|
164
|
+
const testCommand = buildTestCommand({
|
|
165
|
+
language: 'python',
|
|
166
|
+
...config,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const result = await claudeRunTests(testCommand, cwd);
|
|
171
|
+
|
|
172
|
+
if (!result.success && result.error) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
passed: 0,
|
|
176
|
+
failed: 0,
|
|
177
|
+
total: 0,
|
|
178
|
+
output: result.response,
|
|
179
|
+
error: result.error,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return parseTestOutput(result.response, 'python');
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
passed: 0,
|
|
188
|
+
failed: 0,
|
|
189
|
+
total: 0,
|
|
190
|
+
output: '',
|
|
191
|
+
error: error instanceof Error ? error.message : 'Unknown error running tests',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Run TypeScript tests
|
|
198
|
+
*
|
|
199
|
+
* @param cwd - Working directory
|
|
200
|
+
* @param config - Test configuration
|
|
201
|
+
* @returns Test result
|
|
202
|
+
*/
|
|
203
|
+
export async function runTypeScriptTests(
|
|
204
|
+
cwd: string,
|
|
205
|
+
config: Partial<TestConfig> = {}
|
|
206
|
+
): Promise<TestResult> {
|
|
207
|
+
const testCommand = buildTestCommand({
|
|
208
|
+
language: 'typescript',
|
|
209
|
+
...config,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const result = await claudeRunTests(testCommand, cwd);
|
|
214
|
+
|
|
215
|
+
if (!result.success && result.error) {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
passed: 0,
|
|
219
|
+
failed: 0,
|
|
220
|
+
total: 0,
|
|
221
|
+
output: result.response,
|
|
222
|
+
error: result.error,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return parseTestOutput(result.response, 'typescript');
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
passed: 0,
|
|
231
|
+
failed: 0,
|
|
232
|
+
total: 0,
|
|
233
|
+
output: '',
|
|
234
|
+
error: error instanceof Error ? error.message : 'Unknown error running tests',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Run tests for a project
|
|
241
|
+
*
|
|
242
|
+
* @param cwd - Working directory
|
|
243
|
+
* @param language - Project language
|
|
244
|
+
* @param config - Test configuration
|
|
245
|
+
* @returns Test result
|
|
246
|
+
*/
|
|
247
|
+
export async function runTests(
|
|
248
|
+
cwd: string,
|
|
249
|
+
language: OutputLanguage,
|
|
250
|
+
config: Partial<TestConfig> = {}
|
|
251
|
+
): Promise<TestResult> {
|
|
252
|
+
switch (language) {
|
|
253
|
+
case 'python':
|
|
254
|
+
return runPythonTests(cwd, config);
|
|
255
|
+
case 'typescript':
|
|
256
|
+
return runTypeScriptTests(cwd, config);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if tests exist in a project
|
|
262
|
+
*
|
|
263
|
+
* @param cwd - Working directory
|
|
264
|
+
* @param language - Project language
|
|
265
|
+
* @returns True if tests exist
|
|
266
|
+
*/
|
|
267
|
+
export async function testsExist(
|
|
268
|
+
cwd: string,
|
|
269
|
+
language: OutputLanguage
|
|
270
|
+
): Promise<boolean> {
|
|
271
|
+
const { promises: fs } = await import('node:fs');
|
|
272
|
+
const path = await import('node:path');
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
switch (language) {
|
|
276
|
+
case 'python': {
|
|
277
|
+
// Check for tests/ directory or test_*.py files
|
|
278
|
+
const testsDir = path.join(cwd, 'tests');
|
|
279
|
+
try {
|
|
280
|
+
await fs.access(testsDir);
|
|
281
|
+
return true;
|
|
282
|
+
} catch {
|
|
283
|
+
// Check for test files in root
|
|
284
|
+
const files = await fs.readdir(cwd);
|
|
285
|
+
return files.some((f) => f.startsWith('test_') && f.endsWith('.py'));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
case 'typescript': {
|
|
290
|
+
// Check for tests/, __tests__, or *.test.ts files
|
|
291
|
+
const testsDir = path.join(cwd, 'tests');
|
|
292
|
+
const testsDirAlt = path.join(cwd, '__tests__');
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
await fs.access(testsDir);
|
|
296
|
+
return true;
|
|
297
|
+
} catch {
|
|
298
|
+
try {
|
|
299
|
+
await fs.access(testsDirAlt);
|
|
300
|
+
return true;
|
|
301
|
+
} catch {
|
|
302
|
+
// Check for test files in src
|
|
303
|
+
const srcDir = path.join(cwd, 'src');
|
|
304
|
+
try {
|
|
305
|
+
const files = await fs.readdir(srcDir, { recursive: true });
|
|
306
|
+
return files.some(
|
|
307
|
+
(f) =>
|
|
308
|
+
(f.toString().endsWith('.test.ts') || f.toString().endsWith('.spec.ts'))
|
|
309
|
+
);
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get test summary string
|
|
324
|
+
*
|
|
325
|
+
* @param result - Test result
|
|
326
|
+
* @returns Human-readable summary
|
|
327
|
+
*/
|
|
328
|
+
export function getTestSummary(result: TestResult): string {
|
|
329
|
+
if (result.error) {
|
|
330
|
+
return `Tests failed to run: ${result.error}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (result.total === 0) {
|
|
334
|
+
return 'No tests found';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const status = result.success ? '✓' : '✗';
|
|
338
|
+
let summary = `${status} ${result.passed}/${result.total} tests passed`;
|
|
339
|
+
|
|
340
|
+
if (result.failed > 0) {
|
|
341
|
+
summary += ` (${result.failed} failed)`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return summary;
|
|
345
|
+
}
|