popeye-cli 1.0.0 → 1.1.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/README.md +521 -125
- package/dist/adapters/claude.d.ts +16 -4
- package/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +679 -33
- 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/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +41 -7
- 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/index.d.ts +11 -7
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +23 -5
- 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/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts +2 -2
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +1380 -183
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts +6 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +10 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +19 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +20 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +7 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/generators/python.d.ts.map +1 -1
- package/dist/generators/python.js +1 -0
- package/dist/generators/python.js.map +1 -1
- package/dist/generators/typescript.d.ts.map +1 -1
- package/dist/generators/typescript.js +1 -0
- 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 +4 -0
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/cli.js.map +1 -1
- package/dist/types/consensus.d.ts +69 -4
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +24 -3
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/workflow.d.ts +55 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +16 -0
- 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 +44 -2
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +565 -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 +14 -1
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +589 -47
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/plan-storage.d.ts +142 -0
- package/dist/workflow/plan-storage.d.ts.map +1 -0
- package/dist/workflow/plan-storage.js +331 -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 +383 -0
- package/dist/workflow/task-workflow.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +1 -0
- package/dist/workflow/test-runner.d.ts.map +1 -1
- package/dist/workflow/test-runner.js +9 -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/package.json +2 -2
- package/src/adapters/claude.ts +815 -34
- package/src/adapters/gemini.ts +373 -0
- package/src/adapters/openai.ts +40 -7
- package/src/auth/claude.ts +120 -78
- package/src/auth/gemini.ts +207 -0
- package/src/auth/index.ts +28 -8
- package/src/auth/keychain.ts +95 -28
- package/src/auth/openai.ts +29 -36
- package/src/cli/index.ts +4 -7
- package/src/cli/interactive.ts +1641 -216
- package/src/config/defaults.ts +10 -2
- package/src/config/index.ts +21 -0
- package/src/config/schema.ts +7 -0
- package/src/generators/python.ts +1 -0
- package/src/generators/typescript.ts +1 -0
- package/src/state/index.ts +713 -4
- package/src/state/registry.ts +278 -0
- package/src/types/cli.ts +4 -0
- package/src/types/consensus.ts +65 -6
- package/src/types/workflow.ts +35 -0
- package/src/workflow/auto-fix.ts +340 -0
- package/src/workflow/consensus.ts +750 -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 +696 -50
- package/src/workflow/plan-storage.ts +482 -0
- package/src/workflow/project-verification.ts +471 -0
- package/src/workflow/task-workflow.ts +525 -0
- package/src/workflow/test-runner.ts +10 -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/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
|
|
|
@@ -75,7 +78,7 @@ export async function createPlan(
|
|
|
75
78
|
): Promise<string> {
|
|
76
79
|
onProgress?.('Creating development plan...');
|
|
77
80
|
|
|
78
|
-
const result = await claudeCreatePlan(specification, context);
|
|
81
|
+
const result = await claudeCreatePlan(specification, context, onProgress);
|
|
79
82
|
|
|
80
83
|
if (!result.success) {
|
|
81
84
|
throw new Error(`Failed to create plan: ${result.error}`);
|
|
@@ -110,7 +113,7 @@ export async function getProjectContext(
|
|
|
110
113
|
return 'New project - no existing codebase';
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
const result = await analyzeCodebase(projectDir);
|
|
116
|
+
const result = await analyzeCodebase(projectDir, onProgress);
|
|
114
117
|
|
|
115
118
|
if (result.success) {
|
|
116
119
|
onProgress?.('Codebase analysis complete');
|
|
@@ -124,7 +127,7 @@ export async function getProjectContext(
|
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/**
|
|
127
|
-
* Save the plan to a markdown file
|
|
130
|
+
* Save the plan to a markdown file in docs folder
|
|
128
131
|
*
|
|
129
132
|
* @param projectDir - The project directory
|
|
130
133
|
* @param plan - The plan content
|
|
@@ -135,7 +138,15 @@ export async function documentPlan(
|
|
|
135
138
|
plan: string,
|
|
136
139
|
filename: string = 'PLAN.md'
|
|
137
140
|
): Promise<string> {
|
|
138
|
-
|
|
141
|
+
// Create docs directory if it doesn't exist
|
|
142
|
+
const docsDir = path.join(projectDir, 'docs');
|
|
143
|
+
try {
|
|
144
|
+
await fs.mkdir(docsDir, { recursive: true });
|
|
145
|
+
} catch {
|
|
146
|
+
// Directory might already exist
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const planPath = path.join(docsDir, filename);
|
|
139
150
|
|
|
140
151
|
const content = `# Development Plan
|
|
141
152
|
|
|
@@ -145,11 +156,174 @@ ${plan}
|
|
|
145
156
|
`;
|
|
146
157
|
|
|
147
158
|
await fs.writeFile(planPath, content, 'utf-8');
|
|
159
|
+
|
|
160
|
+
// Also save a timestamped version for history
|
|
161
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
162
|
+
const historyFilename = `PLAN-${timestamp}.md`;
|
|
163
|
+
const historyPath = path.join(docsDir, historyFilename);
|
|
164
|
+
await fs.writeFile(historyPath, content, 'utf-8');
|
|
165
|
+
|
|
148
166
|
return planPath;
|
|
149
167
|
}
|
|
150
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Check if a task name represents an actionable implementation task
|
|
171
|
+
* Tasks should start with verbs like: Implement, Create, Build, Set up, Add, etc.
|
|
172
|
+
*
|
|
173
|
+
* @param name - The potential task name
|
|
174
|
+
* @returns True if this looks like an implementation task
|
|
175
|
+
*/
|
|
176
|
+
function isActionableTask(name: string): boolean {
|
|
177
|
+
const nameLower = name.toLowerCase().trim();
|
|
178
|
+
|
|
179
|
+
// Actionable verb prefixes that indicate real implementation tasks
|
|
180
|
+
const actionableVerbs = [
|
|
181
|
+
'implement', 'create', 'build', 'develop', 'write', 'add', 'set up', 'setup',
|
|
182
|
+
'configure', 'install', 'integrate', 'design', 'define', 'establish',
|
|
183
|
+
'generate', 'construct', 'deploy', 'test', 'validate', 'fix', 'update',
|
|
184
|
+
'refactor', 'optimize', 'extend', 'enhance', 'modify', 'initialize',
|
|
185
|
+
'bootstrap', 'scaffold', 'connect', 'wire', 'hook', 'enable', 'disable',
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
// Check if starts with an actionable verb
|
|
189
|
+
const startsWithAction = actionableVerbs.some((verb) =>
|
|
190
|
+
nameLower.startsWith(verb + ' ') || nameLower.startsWith(verb + ':')
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Non-actionable patterns to exclude (plan metadata, not tasks)
|
|
194
|
+
const nonActionablePatterns = [
|
|
195
|
+
/^(background|context|overview|introduction|summary)/i,
|
|
196
|
+
/^(goal|objective|requirement|constraint|assumption|risk)/i,
|
|
197
|
+
/^(timeline|schedule|estimate|duration|deadline)/i,
|
|
198
|
+
/^(note|example|reference|appendix|glossary)/i,
|
|
199
|
+
/^(file structure|project structure|directory)/i,
|
|
200
|
+
/^(showing|displays?|contains?|includes?|describes?)/i,
|
|
201
|
+
/^(the |this |a |an )/i, // Descriptions, not actions
|
|
202
|
+
/^\d+[-.]?\s*(week|day|hour|month)/i, // Time estimates
|
|
203
|
+
/^([\w\s]+):$/, // Labels ending with colon
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const isNonActionable = nonActionablePatterns.some((pattern) => pattern.test(nameLower));
|
|
207
|
+
|
|
208
|
+
return startsWithAction && !isNonActionable;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extract task description from content following a task header
|
|
213
|
+
*
|
|
214
|
+
* @param content - Content following the task header
|
|
215
|
+
* @returns Extracted description
|
|
216
|
+
*/
|
|
217
|
+
function extractTaskDescription(content: string): string {
|
|
218
|
+
// Look for Description field or first paragraph
|
|
219
|
+
const descMatch = content.match(/\*\*Description\*\*:\s*(.+?)(?=\n\*\*|\n###|\n##|$)/is);
|
|
220
|
+
if (descMatch) {
|
|
221
|
+
return descMatch[1].trim().slice(0, 500);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Use first non-empty line
|
|
225
|
+
const lines = content.split('\n').filter((l) => l.trim() && !l.trim().startsWith('-'));
|
|
226
|
+
if (lines.length > 0) {
|
|
227
|
+
return lines[0].trim().slice(0, 500);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return '';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Extract acceptance criteria from task content
|
|
235
|
+
*
|
|
236
|
+
* @param content - Task content
|
|
237
|
+
* @returns Array of acceptance criteria
|
|
238
|
+
*/
|
|
239
|
+
function extractAcceptanceCriteria(content: string): string[] {
|
|
240
|
+
const criteria: string[] = [];
|
|
241
|
+
|
|
242
|
+
// Look for Acceptance Criteria section
|
|
243
|
+
const acMatch = content.match(/\*\*Acceptance Criteria\*\*:?\s*([\s\S]+?)(?=\n\*\*|\n###|\n##|$)/i);
|
|
244
|
+
if (acMatch) {
|
|
245
|
+
const acContent = acMatch[1];
|
|
246
|
+
const bulletMatch = acContent.match(/^[-*]\s+(.+)$/gm);
|
|
247
|
+
if (bulletMatch) {
|
|
248
|
+
for (const bullet of bulletMatch) {
|
|
249
|
+
const cleaned = bullet.replace(/^[-*]\s+/, '').trim();
|
|
250
|
+
if (cleaned.length > 5) {
|
|
251
|
+
criteria.push(cleaned);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return criteria;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Detect if a plan is actually Claude's thinking/conversation instead of a real plan
|
|
262
|
+
* This happens when Claude outputs its reasoning instead of the plan content
|
|
263
|
+
*
|
|
264
|
+
* @param plan - The plan content
|
|
265
|
+
* @returns Object indicating if garbage and why
|
|
266
|
+
*/
|
|
267
|
+
export function detectGarbagePlan(plan: string): { isGarbage: boolean; reason?: string } {
|
|
268
|
+
const planLower = plan.toLowerCase();
|
|
269
|
+
|
|
270
|
+
// Phrases that indicate Claude's thinking, not actual plan content
|
|
271
|
+
const garbagePhrases = [
|
|
272
|
+
'let me ',
|
|
273
|
+
'i will ',
|
|
274
|
+
'i\'ll ',
|
|
275
|
+
'now i have',
|
|
276
|
+
'i now have',
|
|
277
|
+
'let me launch',
|
|
278
|
+
'let me create',
|
|
279
|
+
'let me write',
|
|
280
|
+
'comprehensive understanding',
|
|
281
|
+
'let me analyze',
|
|
282
|
+
'based on my analysis',
|
|
283
|
+
'before i proceed',
|
|
284
|
+
'i\'ve created',
|
|
285
|
+
'i\'ve analyzed',
|
|
286
|
+
'i should',
|
|
287
|
+
'i need to',
|
|
288
|
+
'first, i',
|
|
289
|
+
'the plan is saved',
|
|
290
|
+
'saved to',
|
|
291
|
+
'saved at',
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
for (const phrase of garbagePhrases) {
|
|
295
|
+
if (planLower.includes(phrase)) {
|
|
296
|
+
return {
|
|
297
|
+
isGarbage: true,
|
|
298
|
+
reason: `Plan contains Claude's thinking ("${phrase}") instead of actual plan content`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check if plan has actual structure
|
|
304
|
+
const hasTaskHeaders = /^#{2,4}\s*Task\s+[\d.]+/im.test(plan);
|
|
305
|
+
const hasMilestoneHeaders = /^#{1,3}\s*Milestone\s+\d/im.test(plan);
|
|
306
|
+
const hasActionableBullets = /^[-*]\s+(implement|create|build|add|set up|configure|design|write)/im.test(plan);
|
|
307
|
+
|
|
308
|
+
if (!hasTaskHeaders && !hasMilestoneHeaders && !hasActionableBullets) {
|
|
309
|
+
// Check if it at least has some structure
|
|
310
|
+
const hasAnyHeaders = /^#{1,4}\s+.+$/m.test(plan);
|
|
311
|
+
const hasBulletPoints = /^[-*+]\s+.+$/m.test(plan);
|
|
312
|
+
|
|
313
|
+
if (!hasAnyHeaders && !hasBulletPoints) {
|
|
314
|
+
return {
|
|
315
|
+
isGarbage: true,
|
|
316
|
+
reason: 'Plan has no recognizable structure (no headers, no bullet points)',
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { isGarbage: false };
|
|
322
|
+
}
|
|
323
|
+
|
|
151
324
|
/**
|
|
152
325
|
* Parse milestones and tasks from a plan
|
|
326
|
+
* Extracts only actionable implementation tasks, not plan metadata
|
|
153
327
|
*
|
|
154
328
|
* @param plan - The plan content
|
|
155
329
|
* @returns Parsed milestones with tasks
|
|
@@ -157,54 +331,278 @@ ${plan}
|
|
|
157
331
|
export function parsePlanMilestones(plan: string): Omit<Milestone, 'id'>[] {
|
|
158
332
|
const milestones: Omit<Milestone, 'id'>[] = [];
|
|
159
333
|
|
|
160
|
-
// Look for
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
334
|
+
// First pass: Look for explicit task markers per the spec format
|
|
335
|
+
// Format: "### Task [M].N: [Title]" or "Task N: [Title]"
|
|
336
|
+
const explicitTaskPattern = /^#{2,4}\s*Task\s+(?:[\d.]+[:\s]+)?(.+)$/gim;
|
|
337
|
+
const explicitTasks: Array<{ name: string; description: string; testPlan?: string }> = [];
|
|
338
|
+
|
|
339
|
+
let taskMatch;
|
|
340
|
+
const taskPositions: Array<{ name: string; index: number; endIndex: number }> = [];
|
|
341
|
+
|
|
342
|
+
// Find all task headers
|
|
343
|
+
while ((taskMatch = explicitTaskPattern.exec(plan)) !== null) {
|
|
344
|
+
const name = taskMatch[1].trim()
|
|
345
|
+
.replace(/^\*\*(.+)\*\*$/, '$1') // Remove bold
|
|
346
|
+
.replace(/^:/, '') // Remove leading colon
|
|
347
|
+
.trim();
|
|
348
|
+
|
|
349
|
+
if (name.length > 3 && isActionableTask(name)) {
|
|
350
|
+
taskPositions.push({
|
|
351
|
+
name,
|
|
352
|
+
index: taskMatch.index + taskMatch[0].length,
|
|
353
|
+
endIndex: plan.length, // Will be updated
|
|
354
|
+
});
|
|
175
355
|
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Update end indices
|
|
359
|
+
for (let i = 0; i < taskPositions.length - 1; i++) {
|
|
360
|
+
taskPositions[i].endIndex = taskPositions[i + 1].index - 50; // Approximate
|
|
361
|
+
}
|
|
176
362
|
|
|
177
|
-
|
|
178
|
-
|
|
363
|
+
// Extract task details
|
|
364
|
+
for (const pos of taskPositions) {
|
|
365
|
+
const content = plan.slice(pos.index, pos.endIndex);
|
|
366
|
+
const description = extractTaskDescription(content);
|
|
367
|
+
const criteria = extractAcceptanceCriteria(content);
|
|
179
368
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
369
|
+
explicitTasks.push({
|
|
370
|
+
name: pos.name,
|
|
371
|
+
description: description || pos.name,
|
|
372
|
+
testPlan: criteria.length > 0 ? criteria.join('\n') : undefined,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Second pass: Look for milestone sections containing implementation tasks
|
|
377
|
+
const milestoneSectionPattern = /^#{1,3}\s*(?:Milestone|Phase|Sprint|Stage)\s*[\d.]*[:\s]+(.+)$/gim;
|
|
378
|
+
const milestoneMatches: Array<{ name: string; index: number }> = [];
|
|
379
|
+
|
|
380
|
+
let msMatch;
|
|
381
|
+
while ((msMatch = milestoneSectionPattern.exec(plan)) !== null) {
|
|
382
|
+
milestoneMatches.push({
|
|
383
|
+
name: msMatch[1].trim().replace(/^\*\*(.+)\*\*$/, '$1'),
|
|
384
|
+
index: msMatch.index,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Third pass: If no explicit tasks found, look for actionable bullet points
|
|
389
|
+
if (explicitTasks.length === 0) {
|
|
390
|
+
// Look for bullet points that start with actionable verbs
|
|
391
|
+
const bulletPattern = /^[-*+]\s+(.+)$/gm;
|
|
392
|
+
let bulletMatch;
|
|
393
|
+
|
|
394
|
+
while ((bulletMatch = bulletPattern.exec(plan)) !== null) {
|
|
395
|
+
const taskName = bulletMatch[1].trim()
|
|
396
|
+
.replace(/^\*\*(.+)\*\*:?\s*/, '$1: ')
|
|
397
|
+
.replace(/\*\*(.+)\*\*/g, '$1')
|
|
398
|
+
.slice(0, 200);
|
|
399
|
+
|
|
400
|
+
if (taskName.length >= 10 && isActionableTask(taskName)) {
|
|
401
|
+
explicitTasks.push({
|
|
184
402
|
name: taskName,
|
|
185
403
|
description: taskName,
|
|
186
404
|
});
|
|
187
405
|
}
|
|
188
406
|
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Fourth pass: If still no tasks, look for numbered implementation items
|
|
410
|
+
if (explicitTasks.length === 0) {
|
|
411
|
+
const numberedPattern = /^\d+[.)]\s+(.+)$/gm;
|
|
412
|
+
let numMatch;
|
|
413
|
+
|
|
414
|
+
while ((numMatch = numberedPattern.exec(plan)) !== null) {
|
|
415
|
+
const taskName = numMatch[1].trim()
|
|
416
|
+
.replace(/^\*\*(.+)\*\*:?\s*/, '$1: ')
|
|
417
|
+
.replace(/\*\*(.+)\*\*/g, '$1')
|
|
418
|
+
.slice(0, 200);
|
|
419
|
+
|
|
420
|
+
if (taskName.length >= 10 && isActionableTask(taskName)) {
|
|
421
|
+
explicitTasks.push({
|
|
422
|
+
name: taskName,
|
|
423
|
+
description: taskName,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Build milestones from collected data
|
|
430
|
+
if (milestoneMatches.length > 0 && explicitTasks.length > 0) {
|
|
431
|
+
// Distribute tasks to milestones based on position
|
|
432
|
+
const tasksPerMilestone = Math.ceil(explicitTasks.length / milestoneMatches.length);
|
|
433
|
+
|
|
434
|
+
for (let i = 0; i < milestoneMatches.length; i++) {
|
|
435
|
+
const startIdx = i * tasksPerMilestone;
|
|
436
|
+
const endIdx = Math.min(startIdx + tasksPerMilestone, explicitTasks.length);
|
|
437
|
+
const milestoneTasks = explicitTasks.slice(startIdx, endIdx);
|
|
438
|
+
|
|
439
|
+
if (milestoneTasks.length > 0) {
|
|
440
|
+
milestones.push({
|
|
441
|
+
name: milestoneMatches[i].name,
|
|
442
|
+
description: `Implementation phase ${i + 1}`,
|
|
443
|
+
tasks: milestoneTasks as Task[],
|
|
444
|
+
status: 'pending',
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} else if (explicitTasks.length > 0) {
|
|
449
|
+
// No milestone headers found, group tasks into phases
|
|
450
|
+
const tasksPerMilestone = 5;
|
|
451
|
+
for (let i = 0; i < explicitTasks.length; i += tasksPerMilestone) {
|
|
452
|
+
const milestoneTasks = explicitTasks.slice(i, i + tasksPerMilestone);
|
|
453
|
+
const milestoneNum = Math.floor(i / tasksPerMilestone) + 1;
|
|
189
454
|
|
|
190
|
-
if (tasks.length > 0 || name.toLowerCase().includes('milestone')) {
|
|
191
455
|
milestones.push({
|
|
192
|
-
name
|
|
193
|
-
description:
|
|
194
|
-
tasks:
|
|
456
|
+
name: `Implementation Phase ${milestoneNum}`,
|
|
457
|
+
description: `Tasks ${i + 1} to ${Math.min(i + tasksPerMilestone, explicitTasks.length)}`,
|
|
458
|
+
tasks: milestoneTasks as Task[],
|
|
195
459
|
status: 'pending',
|
|
196
460
|
});
|
|
197
461
|
}
|
|
198
|
-
}
|
|
462
|
+
} else {
|
|
463
|
+
// Fifth pass: Look for any headers that might be tasks (less strict matching)
|
|
464
|
+
const anyHeaderPattern = /^#{2,4}\s+(.+)$/gm;
|
|
465
|
+
const headerTasks: Array<{ name: string; description: string }> = [];
|
|
466
|
+
let headerMatch;
|
|
467
|
+
|
|
468
|
+
while ((headerMatch = anyHeaderPattern.exec(plan)) !== null) {
|
|
469
|
+
const name = headerMatch[1].trim()
|
|
470
|
+
.replace(/^\*\*(.+)\*\*$/, '$1')
|
|
471
|
+
.replace(/^[:*-]\s*/, '');
|
|
472
|
+
|
|
473
|
+
// Skip obvious non-task headers
|
|
474
|
+
const skipPatterns = [
|
|
475
|
+
/^(background|context|overview|introduction|summary)/i,
|
|
476
|
+
/^(goal|objective|requirement|risk|assumption)/i,
|
|
477
|
+
/^(timeline|schedule|test plan|acceptance)/i,
|
|
478
|
+
/^(table of contents|toc|appendix|reference)/i,
|
|
479
|
+
/^(project|specification|design|architecture)$/i,
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
const shouldSkip = skipPatterns.some(p => p.test(name));
|
|
483
|
+
|
|
484
|
+
if (!shouldSkip && name.length >= 5 && name.length <= 200) {
|
|
485
|
+
headerTasks.push({
|
|
486
|
+
name: name.slice(0, 100),
|
|
487
|
+
description: name,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
199
491
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
492
|
+
if (headerTasks.length >= 2) {
|
|
493
|
+
// Use headers as tasks, grouped into milestones
|
|
494
|
+
const tasksPerMilestone = 5;
|
|
495
|
+
for (let i = 0; i < headerTasks.length; i += tasksPerMilestone) {
|
|
496
|
+
const milestoneTasks = headerTasks.slice(i, i + tasksPerMilestone);
|
|
497
|
+
const milestoneNum = Math.floor(i / tasksPerMilestone) + 1;
|
|
498
|
+
|
|
499
|
+
milestones.push({
|
|
500
|
+
name: `Implementation Phase ${milestoneNum}`,
|
|
501
|
+
description: `Tasks ${i + 1} to ${Math.min(i + tasksPerMilestone, headerTasks.length)}`,
|
|
502
|
+
tasks: milestoneTasks as Task[],
|
|
503
|
+
status: 'pending',
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
// Sixth pass: Parse any section with implementation keywords
|
|
508
|
+
const implKeywords = [
|
|
509
|
+
'implement', 'create', 'build', 'add', 'develop', 'write',
|
|
510
|
+
'set up', 'configure', 'design', 'test', 'api', 'component',
|
|
511
|
+
'service', 'module', 'function', 'class', 'feature',
|
|
512
|
+
'database', 'model', 'controller', 'view', 'route', 'endpoint',
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
const lines = plan.split('\n');
|
|
516
|
+
const implTasks: Array<{ name: string; description: string }> = [];
|
|
517
|
+
|
|
518
|
+
for (const line of lines) {
|
|
519
|
+
const trimmed = line.trim();
|
|
520
|
+
if (trimmed.length < 10 || trimmed.length > 200) continue;
|
|
521
|
+
|
|
522
|
+
const hasKeyword = implKeywords.some(kw =>
|
|
523
|
+
trimmed.toLowerCase().includes(kw)
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
// Check if it looks like an item (starts with bullet, number, or header)
|
|
527
|
+
const isItem = /^[-*+#\d.]/.test(trimmed) ||
|
|
528
|
+
/^(Task|Step|Item|Feature|Component)/i.test(trimmed);
|
|
529
|
+
|
|
530
|
+
if (hasKeyword && isItem) {
|
|
531
|
+
const name = trimmed
|
|
532
|
+
.replace(/^[-*+#]+\s*/, '')
|
|
533
|
+
.replace(/^\d+[.)]\s*/, '')
|
|
534
|
+
.replace(/^\*\*(.+?)\*\*:?\s*/, '$1: ')
|
|
535
|
+
.slice(0, 100);
|
|
536
|
+
|
|
537
|
+
if (name.length >= 10 && !implTasks.some(t => t.name === name)) {
|
|
538
|
+
implTasks.push({
|
|
539
|
+
name,
|
|
540
|
+
description: name,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (implTasks.length > 0) {
|
|
547
|
+
// Group implementation tasks
|
|
548
|
+
const tasksPerMilestone = 5;
|
|
549
|
+
for (let i = 0; i < implTasks.length; i += tasksPerMilestone) {
|
|
550
|
+
const milestoneTasks = implTasks.slice(i, i + tasksPerMilestone);
|
|
551
|
+
const milestoneNum = Math.floor(i / tasksPerMilestone) + 1;
|
|
552
|
+
|
|
553
|
+
milestones.push({
|
|
554
|
+
name: `Implementation Phase ${milestoneNum}`,
|
|
555
|
+
description: `Tasks ${i + 1} to ${Math.min(i + tasksPerMilestone, implTasks.length)}`,
|
|
556
|
+
tasks: milestoneTasks as Task[],
|
|
557
|
+
status: 'pending',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
// Final fallback: Create structured tasks based on common project phases
|
|
562
|
+
// This should rarely happen if the plan is well-structured
|
|
563
|
+
console.warn('[plan-parser] Warning: Could not parse tasks from plan. Using default structure.');
|
|
564
|
+
|
|
565
|
+
milestones.push({
|
|
566
|
+
name: 'Core Implementation',
|
|
567
|
+
description: 'Implement core functionality based on the plan',
|
|
568
|
+
tasks: [
|
|
569
|
+
{
|
|
570
|
+
name: 'Set up project structure and dependencies',
|
|
571
|
+
description: 'Initialize project with required structure, dependencies, and configuration',
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: 'Implement core features',
|
|
575
|
+
description: 'Build the main features as described in the development plan',
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: 'Add data models and storage',
|
|
579
|
+
description: 'Create data models, database schema, and storage layer',
|
|
580
|
+
},
|
|
581
|
+
] as Task[],
|
|
582
|
+
status: 'pending',
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
milestones.push({
|
|
586
|
+
name: 'Integration and Testing',
|
|
587
|
+
description: 'Connect components and verify functionality',
|
|
588
|
+
tasks: [
|
|
589
|
+
{
|
|
590
|
+
name: 'Integrate components',
|
|
591
|
+
description: 'Connect all components and ensure they work together',
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: 'Write and run tests',
|
|
595
|
+
description: 'Create unit tests, integration tests, and verify all tests pass',
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: 'Final verification and documentation',
|
|
599
|
+
description: 'Run final verification, update documentation, ensure project works correctly',
|
|
600
|
+
},
|
|
601
|
+
] as Task[],
|
|
602
|
+
status: 'pending',
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
208
606
|
}
|
|
209
607
|
|
|
210
608
|
return milestones;
|
|
@@ -221,24 +619,44 @@ export async function runPlanMode(
|
|
|
221
619
|
spec: ProjectSpec,
|
|
222
620
|
options: PlanModeOptions
|
|
223
621
|
): Promise<PlanModeResult> {
|
|
224
|
-
const { projectDir, consensusConfig, onProgress } = options;
|
|
622
|
+
const { projectDir, consensusConfig, additionalContext, onProgress } = options;
|
|
623
|
+
|
|
624
|
+
// Initialize workflow logger
|
|
625
|
+
const logger = getWorkflowLogger(projectDir);
|
|
225
626
|
|
|
226
627
|
try {
|
|
227
628
|
// Create or load project
|
|
228
629
|
onProgress?.('plan-init', 'Initializing project...');
|
|
630
|
+
await logger.stageStart('init', 'Plan Mode initialization', {
|
|
631
|
+
projectName: spec.name,
|
|
632
|
+
language: spec.language,
|
|
633
|
+
idea: spec.idea.slice(0, 200),
|
|
634
|
+
});
|
|
229
635
|
|
|
230
636
|
let state: ProjectState;
|
|
231
637
|
try {
|
|
232
638
|
state = await loadProject(projectDir);
|
|
233
639
|
onProgress?.('plan-init', 'Loaded existing project');
|
|
640
|
+
await logger.info('init', 'project_loaded', 'Loaded existing project', {
|
|
641
|
+
projectName: state.name,
|
|
642
|
+
phase: state.phase,
|
|
643
|
+
hasPlan: !!state.plan,
|
|
644
|
+
hasSpecification: !!state.specification,
|
|
645
|
+
});
|
|
234
646
|
} catch {
|
|
235
647
|
state = await createProject(spec, projectDir);
|
|
236
648
|
onProgress?.('plan-init', 'Created new project');
|
|
649
|
+
await logger.success('init', 'project_created', 'Created new project', {
|
|
650
|
+
projectName: state.name,
|
|
651
|
+
language: state.language,
|
|
652
|
+
});
|
|
237
653
|
}
|
|
238
654
|
|
|
239
655
|
// Expand idea if we don't have a specification
|
|
240
656
|
if (!state.specification) {
|
|
241
657
|
onProgress?.('expand-idea', 'Expanding idea into specification...');
|
|
658
|
+
await logger.stageStart('plan-generation', 'Expanding idea into specification');
|
|
659
|
+
|
|
242
660
|
const specification = await expandIdea(
|
|
243
661
|
spec.idea,
|
|
244
662
|
spec.language,
|
|
@@ -247,18 +665,49 @@ export async function runPlanMode(
|
|
|
247
665
|
|
|
248
666
|
state = await storeSpecification(projectDir, specification);
|
|
249
667
|
onProgress?.('expand-idea', 'Specification complete');
|
|
668
|
+
await logger.stageComplete('plan-generation', 'Specification created', {
|
|
669
|
+
specificationLength: specification.length,
|
|
670
|
+
specificationPreview: specification.slice(0, 300),
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Design UI early in the process
|
|
675
|
+
onProgress?.('ui-design', 'Designing UI from project idea...');
|
|
676
|
+
try {
|
|
677
|
+
const uiSpec = await designUI(spec.idea, (msg) => onProgress?.('ui-design', msg));
|
|
678
|
+
await saveUISpecification(projectDir, uiSpec);
|
|
679
|
+
onProgress?.('ui-design', `UI design complete: ${uiSpec.themeName} theme, ${uiSpec.recommendedComponents.length} components`);
|
|
680
|
+
await logger.success('ui-design', 'ui_design_complete', 'UI design specification created', {
|
|
681
|
+
theme: uiSpec.themeName,
|
|
682
|
+
projectType: uiSpec.projectType,
|
|
683
|
+
components: uiSpec.recommendedComponents.length,
|
|
684
|
+
});
|
|
685
|
+
} catch (uiError) {
|
|
686
|
+
// Non-blocking - UI design failures shouldn't stop the workflow
|
|
687
|
+
onProgress?.('ui-design', `UI design skipped: ${uiError instanceof Error ? uiError.message : 'Unknown error'}`);
|
|
688
|
+
await logger.warn('ui-design', 'ui_design_skipped', 'UI design was skipped', {
|
|
689
|
+
error: uiError instanceof Error ? uiError.message : 'Unknown error',
|
|
690
|
+
});
|
|
250
691
|
}
|
|
251
692
|
|
|
252
693
|
// Get project context
|
|
253
694
|
onProgress?.('get-context', 'Gathering project context...');
|
|
254
|
-
|
|
695
|
+
let context = await getProjectContext(
|
|
255
696
|
projectDir,
|
|
256
697
|
(msg) => onProgress?.('get-context', msg)
|
|
257
698
|
);
|
|
258
699
|
|
|
700
|
+
// Append additional context if provided (e.g., when resuming with guidance)
|
|
701
|
+
if (additionalContext) {
|
|
702
|
+
onProgress?.('get-context', 'Incorporating additional guidance...');
|
|
703
|
+
context = `${context}\n\nADDITIONAL GUIDANCE FROM USER:\n${additionalContext}`;
|
|
704
|
+
}
|
|
705
|
+
|
|
259
706
|
// Create initial plan if we don't have one
|
|
260
707
|
if (!state.plan) {
|
|
261
708
|
onProgress?.('create-plan', 'Creating development plan...');
|
|
709
|
+
await logger.stageStart('plan-generation', 'Creating development plan');
|
|
710
|
+
|
|
262
711
|
const plan = await createPlan(
|
|
263
712
|
state.specification!,
|
|
264
713
|
context,
|
|
@@ -267,10 +716,16 @@ export async function runPlanMode(
|
|
|
267
716
|
|
|
268
717
|
state = await storePlan(projectDir, plan);
|
|
269
718
|
onProgress?.('create-plan', 'Initial plan created');
|
|
719
|
+
await logger.stageComplete('plan-generation', 'Development plan created', {
|
|
720
|
+
planLength: plan.length,
|
|
721
|
+
planPreview: plan.slice(0, 500),
|
|
722
|
+
});
|
|
270
723
|
}
|
|
271
724
|
|
|
272
725
|
// Run consensus loop
|
|
273
726
|
onProgress?.('consensus', 'Starting consensus review...');
|
|
727
|
+
await logger.stageStart('consensus', 'Starting consensus review process');
|
|
728
|
+
|
|
274
729
|
const consensusResult = await iterateUntilConsensus(
|
|
275
730
|
state.plan!,
|
|
276
731
|
context,
|
|
@@ -286,29 +741,214 @@ export async function runPlanMode(
|
|
|
286
741
|
onRevision: (iteration, _plan) => {
|
|
287
742
|
onProgress?.('consensus', `Revising plan (iteration ${iteration})...`);
|
|
288
743
|
},
|
|
744
|
+
onConcerns: (concerns, recommendations) => {
|
|
745
|
+
if (concerns.length > 0) {
|
|
746
|
+
onProgress?.('concerns', `Concerns: ${concerns.slice(0, 2).join('; ')}`);
|
|
747
|
+
}
|
|
748
|
+
if (recommendations.length > 0) {
|
|
749
|
+
onProgress?.('recommendations', `Suggestions: ${recommendations.slice(0, 2).join('; ')}`);
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
onArbitration: (result) => {
|
|
753
|
+
onProgress?.('arbitration', `Arbitrator decision: ${result.approved ? 'APPROVED' : 'REVISE'} (${result.score}%)`);
|
|
754
|
+
if (!result.approved && result.suggestedChanges.length > 0) {
|
|
755
|
+
onProgress?.('arbitration', `Changes: ${result.suggestedChanges.slice(0, 2).join('; ')}`);
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
onProgress,
|
|
289
759
|
}
|
|
290
760
|
);
|
|
291
761
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
762
|
+
// Log consensus result
|
|
763
|
+
await logger.info('consensus', 'consensus_complete', 'Consensus process completed', {
|
|
764
|
+
approved: consensusResult.approved,
|
|
765
|
+
finalScore: consensusResult.finalScore,
|
|
766
|
+
bestScore: consensusResult.bestScore,
|
|
767
|
+
totalIterations: consensusResult.totalIterations,
|
|
768
|
+
arbitrated: consensusResult.arbitrated,
|
|
769
|
+
});
|
|
295
770
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
771
|
+
// Check if the plan is garbage (Claude's thinking instead of actual content)
|
|
772
|
+
const garbageCheck = detectGarbagePlan(consensusResult.bestPlan);
|
|
773
|
+
if (garbageCheck.isGarbage) {
|
|
774
|
+
onProgress?.(
|
|
775
|
+
'error',
|
|
776
|
+
`PLAN VALIDATION FAILED: ${garbageCheck.reason}`
|
|
777
|
+
);
|
|
778
|
+
onProgress?.(
|
|
779
|
+
'error',
|
|
780
|
+
'The plan contains Claude\'s thinking/conversation instead of actual plan content.'
|
|
781
|
+
);
|
|
782
|
+
onProgress?.(
|
|
783
|
+
'info',
|
|
784
|
+
'This typically happens when Claude describes what it will do instead of outputting the plan.'
|
|
785
|
+
);
|
|
786
|
+
onProgress?.(
|
|
787
|
+
'info',
|
|
788
|
+
'Saving garbage plan for debugging. Try running again or provide more specific requirements.'
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
// Still save the plan for debugging
|
|
792
|
+
await documentPlan(projectDir, consensusResult.bestPlan, 'PLAN-FAILED.md');
|
|
793
|
+
|
|
794
|
+
await logger.stageFailed('plan-parsing', 'Plan validation', garbageCheck.reason!, {
|
|
795
|
+
planLength: consensusResult.bestPlan.length,
|
|
796
|
+
reason: garbageCheck.reason,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
success: false,
|
|
801
|
+
state,
|
|
802
|
+
consensusResult,
|
|
803
|
+
error: `Plan generation failed: ${garbageCheck.reason}`,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
299
806
|
|
|
300
|
-
|
|
301
|
-
|
|
807
|
+
// Always store the best plan (even if consensus failed)
|
|
808
|
+
state = await storePlan(projectDir, consensusResult.bestPlan);
|
|
302
809
|
|
|
810
|
+
// Parse and add milestones from best plan
|
|
811
|
+
await logger.stageStart('plan-parsing', 'Parsing plan into milestones and tasks');
|
|
812
|
+
const milestones = parsePlanMilestones(consensusResult.bestPlan);
|
|
813
|
+
|
|
814
|
+
// Log parsed milestones for debugging
|
|
815
|
+
const totalTasks = milestones.reduce((sum, m) => sum + m.tasks.length, 0);
|
|
816
|
+
onProgress?.(
|
|
817
|
+
'plan-structure',
|
|
818
|
+
`Parsed plan: ${milestones.length} milestones, ${totalTasks} tasks`
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
// Log detailed parsing results
|
|
822
|
+
const parsedMilestones = milestones.map(m => ({
|
|
823
|
+
name: m.name,
|
|
824
|
+
taskCount: m.tasks.length,
|
|
825
|
+
taskNames: m.tasks.map(t => t.name),
|
|
826
|
+
}));
|
|
827
|
+
await logger.info('plan-parsing', 'plan_parsed', 'Parsed plan structure', {
|
|
828
|
+
milestonesCount: milestones.length,
|
|
829
|
+
totalTasks: totalTasks,
|
|
830
|
+
milestones: parsedMilestones,
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// VALIDATION: Fail if too few milestones/tasks for a real project
|
|
834
|
+
if (milestones.length <= 1 && totalTasks <= 2) {
|
|
835
|
+
onProgress?.(
|
|
836
|
+
'error',
|
|
837
|
+
`PLAN VALIDATION FAILED: Only ${milestones.length} milestone(s) and ${totalTasks} task(s) extracted.`
|
|
838
|
+
);
|
|
839
|
+
onProgress?.(
|
|
840
|
+
'error',
|
|
841
|
+
'A valid plan should have at least 2 milestones with 3+ tasks each.'
|
|
842
|
+
);
|
|
843
|
+
onProgress?.(
|
|
844
|
+
'info',
|
|
845
|
+
'Expected format: "## Milestone N: Name" and "### Task N.N: Name"'
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
// Save the problematic plan for debugging
|
|
849
|
+
await documentPlan(projectDir, consensusResult.bestPlan, 'PLAN-INSUFFICIENT.md');
|
|
850
|
+
|
|
851
|
+
// Show what was found in the plan
|
|
852
|
+
onProgress?.('debug', 'Tasks extracted from plan:');
|
|
853
|
+
for (const m of milestones) {
|
|
854
|
+
for (const t of m.tasks) {
|
|
855
|
+
onProgress?.('debug', ` - ${t.name}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
await logger.stageFailed('plan-parsing', 'Plan validation', 'Insufficient tasks extracted', {
|
|
860
|
+
milestonesCount: milestones.length,
|
|
861
|
+
totalTasks: totalTasks,
|
|
862
|
+
expectedMinTasks: 3,
|
|
863
|
+
extractedTasks: milestones.flatMap(m => m.tasks.map(t => t.name)),
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
success: false,
|
|
868
|
+
state,
|
|
869
|
+
consensusResult,
|
|
870
|
+
error: `Plan parsing failed: only ${totalTasks} task(s) extracted. Plan needs more structure.`,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Warn if suspiciously few tasks (but don't block)
|
|
875
|
+
if (milestones.length <= 2 || totalTasks <= 5) {
|
|
876
|
+
onProgress?.(
|
|
877
|
+
'warning',
|
|
878
|
+
`Warning: Only ${milestones.length} milestone(s) and ${totalTasks} task(s) parsed. ` +
|
|
879
|
+
`This seems low for a complete project. Consider reviewing the plan.`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Log each milestone and its tasks
|
|
884
|
+
for (const milestone of milestones) {
|
|
885
|
+
onProgress?.(
|
|
886
|
+
'plan-detail',
|
|
887
|
+
` Milestone: ${milestone.name} (${milestone.tasks.length} tasks)`
|
|
888
|
+
);
|
|
889
|
+
for (const task of milestone.tasks.slice(0, 3)) {
|
|
890
|
+
onProgress?.('plan-detail', ` - ${task.name}`);
|
|
891
|
+
}
|
|
892
|
+
if (milestone.tasks.length > 3) {
|
|
893
|
+
onProgress?.('plan-detail', ` ... and ${milestone.tasks.length - 3} more tasks`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
state = await addMilestones(projectDir, milestones);
|
|
898
|
+
|
|
899
|
+
// Always document the plan (so user can see what was achieved)
|
|
900
|
+
const planFilename = consensusResult.approved ? 'PLAN.md' : 'PLAN-DRAFT.md';
|
|
901
|
+
await documentPlan(projectDir, consensusResult.bestPlan, planFilename);
|
|
902
|
+
|
|
903
|
+
if (consensusResult.approved) {
|
|
303
904
|
// Transition to execution phase
|
|
304
905
|
state = await setPhase(projectDir, 'execution');
|
|
305
906
|
|
|
306
|
-
|
|
907
|
+
if (consensusResult.arbitrated) {
|
|
908
|
+
onProgress?.('complete', `Plan approved via arbitration with ${consensusResult.finalScore}% confidence`);
|
|
909
|
+
} else {
|
|
910
|
+
onProgress?.('complete', `Plan approved with ${consensusResult.finalScore}% consensus`);
|
|
911
|
+
}
|
|
912
|
+
onProgress?.('info', `Plan saved to docs/PLAN.md`);
|
|
913
|
+
|
|
914
|
+
await logger.stageComplete('plan-generation', 'Plan Mode completed successfully', {
|
|
915
|
+
consensusScore: consensusResult.finalScore,
|
|
916
|
+
arbitrated: consensusResult.arbitrated,
|
|
917
|
+
milestonesCount: milestones.length,
|
|
918
|
+
totalTasks: totalTasks,
|
|
919
|
+
nextPhase: 'execution',
|
|
920
|
+
});
|
|
307
921
|
} else {
|
|
922
|
+
// Show why consensus failed
|
|
308
923
|
onProgress?.(
|
|
309
924
|
'failed',
|
|
310
|
-
`Consensus not reached after ${consensusResult.totalIterations} iterations (${consensusResult.
|
|
925
|
+
`Consensus not reached after ${consensusResult.totalIterations} iterations (best: ${consensusResult.bestScore}% at iteration ${consensusResult.bestIteration})`
|
|
311
926
|
);
|
|
927
|
+
|
|
928
|
+
// Show remaining concerns
|
|
929
|
+
if (consensusResult.finalConcerns.length > 0) {
|
|
930
|
+
onProgress?.('concerns', `Remaining concerns:`);
|
|
931
|
+
for (const concern of consensusResult.finalConcerns.slice(0, 3)) {
|
|
932
|
+
onProgress?.('concerns', ` - ${concern}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Show recommendations
|
|
937
|
+
if (consensusResult.finalRecommendations.length > 0) {
|
|
938
|
+
onProgress?.('recommendations', `Recommendations:`);
|
|
939
|
+
for (const rec of consensusResult.finalRecommendations.slice(0, 3)) {
|
|
940
|
+
onProgress?.('recommendations', ` - ${rec}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
onProgress?.('info', `Draft plan saved to docs/${planFilename}`);
|
|
945
|
+
|
|
946
|
+
await logger.warn('plan-generation', 'consensus_failed', 'Plan Mode incomplete - consensus not reached', {
|
|
947
|
+
bestScore: consensusResult.bestScore,
|
|
948
|
+
totalIterations: consensusResult.totalIterations,
|
|
949
|
+
finalConcerns: consensusResult.finalConcerns,
|
|
950
|
+
finalRecommendations: consensusResult.finalRecommendations,
|
|
951
|
+
});
|
|
312
952
|
}
|
|
313
953
|
|
|
314
954
|
return {
|
|
@@ -320,6 +960,12 @@ export async function runPlanMode(
|
|
|
320
960
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
321
961
|
onProgress?.('error', errorMessage);
|
|
322
962
|
|
|
963
|
+
// Log the error
|
|
964
|
+
await logger.stageFailed('plan-generation', 'Plan Mode execution', errorMessage, {
|
|
965
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
966
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
967
|
+
});
|
|
968
|
+
|
|
323
969
|
return {
|
|
324
970
|
success: false,
|
|
325
971
|
state: await loadProject(projectDir).catch(() => ({} as ProjectState)),
|