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