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