popeye-cli 1.4.1 → 1.4.2
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/dist/adapters/claude.d.ts.map +1 -1
- package/dist/adapters/claude.js +6 -0
- package/dist/adapters/claude.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +82 -5
- package/dist/cli/interactive.js.map +1 -1
- package/dist/types/workflow.d.ts +12 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +4 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/auto-fix.d.ts +17 -1
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +97 -16
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts +24 -0
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +292 -19
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +1 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +1 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/milestone-workflow.d.ts.map +1 -1
- package/dist/workflow/milestone-workflow.js +63 -3
- package/dist/workflow/milestone-workflow.js.map +1 -1
- package/dist/workflow/project-structure.d.ts +29 -0
- package/dist/workflow/project-structure.d.ts.map +1 -0
- package/dist/workflow/project-structure.js +193 -0
- package/dist/workflow/project-structure.js.map +1 -0
- package/dist/workflow/project-verification.d.ts +24 -1
- package/dist/workflow/project-verification.d.ts.map +1 -1
- package/dist/workflow/project-verification.js +105 -22
- package/dist/workflow/project-verification.js.map +1 -1
- package/dist/workflow/remediation.d.ts +124 -0
- package/dist/workflow/remediation.d.ts.map +1 -0
- package/dist/workflow/remediation.js +510 -0
- package/dist/workflow/remediation.js.map +1 -0
- package/dist/workflow/task-workflow.d.ts.map +1 -1
- package/dist/workflow/task-workflow.js +202 -4
- package/dist/workflow/task-workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude.ts +6 -0
- package/src/cli/interactive.ts +85 -5
- package/src/types/workflow.ts +9 -0
- package/src/workflow/auto-fix.ts +123 -18
- package/src/workflow/execution-mode.ts +364 -20
- package/src/workflow/index.ts +1 -0
- package/src/workflow/milestone-workflow.ts +80 -3
- package/src/workflow/project-structure.ts +233 -0
- package/src/workflow/project-verification.ts +128 -21
- package/src/workflow/remediation.ts +711 -0
- package/src/workflow/task-workflow.ts +237 -4
- package/tests/workflow/auto-fix-enhanced.test.ts +351 -0
- package/tests/workflow/project-structure.test.ts +131 -0
- package/tests/workflow/project-verification.test.ts +130 -0
- package/tests/workflow/remediation.test.ts +238 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remediation loop for consensus-driven failure recovery
|
|
3
|
+
* When a task fails during execution, this module:
|
|
4
|
+
* 1. Gathers full failure context
|
|
5
|
+
* 2. Analyzes root cause via Claude
|
|
6
|
+
* 3. Creates a fix plan
|
|
7
|
+
* 4. Gets consensus from reviewers (with full failure context)
|
|
8
|
+
* 5. Implements the approved fix
|
|
9
|
+
* 6. Retries the task
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import type { ProjectState, Task, Milestone } from '../types/workflow.js';
|
|
15
|
+
import type { ConsensusConfig } from '../types/consensus.js';
|
|
16
|
+
import { isWorkspace } from '../types/project.js';
|
|
17
|
+
import { createPlan as claudeCreatePlan, generateCode } from '../adapters/claude.js';
|
|
18
|
+
import { loadProject, updateState } from '../state/index.js';
|
|
19
|
+
import {
|
|
20
|
+
iterateUntilConsensus,
|
|
21
|
+
runOptimizedConsensusProcess,
|
|
22
|
+
type ConsensusProcessResult,
|
|
23
|
+
} from './consensus.js';
|
|
24
|
+
import { runTaskWorkflow, type TaskWorkflowResult } from './task-workflow.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Maximum number of remediation attempts per task
|
|
28
|
+
*/
|
|
29
|
+
export const MAX_REMEDIATION_ATTEMPTS = 2;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result of a remediation attempt
|
|
33
|
+
*/
|
|
34
|
+
export interface RemediationResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
taskResult?: TaskWorkflowResult;
|
|
37
|
+
remediationPlan?: string;
|
|
38
|
+
consensusResult?: ConsensusProcessResult;
|
|
39
|
+
failureAnalysis?: string;
|
|
40
|
+
attempt: number;
|
|
41
|
+
maxAttempts: number;
|
|
42
|
+
error?: string;
|
|
43
|
+
rateLimitPaused?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Options for remediation
|
|
48
|
+
*/
|
|
49
|
+
export interface RemediationOptions {
|
|
50
|
+
projectDir: string;
|
|
51
|
+
consensusConfig?: Partial<ConsensusConfig>;
|
|
52
|
+
onProgress?: (phase: string, message: string) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update a task in state with new data
|
|
57
|
+
*/
|
|
58
|
+
async function updateTaskInState(
|
|
59
|
+
projectDir: string,
|
|
60
|
+
taskId: string,
|
|
61
|
+
updates: Partial<Task>
|
|
62
|
+
): Promise<ProjectState> {
|
|
63
|
+
const state = await loadProject(projectDir);
|
|
64
|
+
|
|
65
|
+
const updatedMilestones = state.milestones.map(milestone => ({
|
|
66
|
+
...milestone,
|
|
67
|
+
tasks: milestone.tasks.map(task =>
|
|
68
|
+
task.id === taskId ? { ...task, ...updates } : task
|
|
69
|
+
),
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
return updateState(projectDir, { milestones: updatedMilestones });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build comprehensive failure context for reviewers
|
|
77
|
+
* Gathers all available information about why a task failed
|
|
78
|
+
*
|
|
79
|
+
* @param task - The failed task
|
|
80
|
+
* @param milestone - The parent milestone
|
|
81
|
+
* @param taskResult - The task workflow result with failure details
|
|
82
|
+
* @param state - Current project state
|
|
83
|
+
* @param projectDir - Project directory for reading docs
|
|
84
|
+
* @returns Structured markdown string with full failure context
|
|
85
|
+
*/
|
|
86
|
+
export async function buildFailureContext(
|
|
87
|
+
task: Task,
|
|
88
|
+
milestone: Milestone,
|
|
89
|
+
taskResult: TaskWorkflowResult,
|
|
90
|
+
state: ProjectState,
|
|
91
|
+
projectDir: string
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
const sections: string[] = [];
|
|
94
|
+
|
|
95
|
+
// Section 1: Task info
|
|
96
|
+
sections.push(`## Failed Task
|
|
97
|
+
- **Name**: ${task.name}
|
|
98
|
+
- **Description**: ${task.description}
|
|
99
|
+
- **Milestone**: ${milestone.name}
|
|
100
|
+
- **Task ID**: ${task.id}`);
|
|
101
|
+
|
|
102
|
+
if (task.plan) {
|
|
103
|
+
sections.push(`### Task Plan (that was executed)
|
|
104
|
+
${task.plan.slice(0, 2000)}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Section 2: Error details
|
|
108
|
+
sections.push(`## Error Details
|
|
109
|
+
${taskResult.error || 'No error message available'}`);
|
|
110
|
+
|
|
111
|
+
// Section 3: Test results (if available)
|
|
112
|
+
if (taskResult.testResult) {
|
|
113
|
+
const tr = taskResult.testResult;
|
|
114
|
+
const isCrash = tr.passed === 0 && tr.failed > 20;
|
|
115
|
+
|
|
116
|
+
sections.push(`## Test Results
|
|
117
|
+
- **Status**: ${tr.success ? 'PASSED' : 'FAILED'}
|
|
118
|
+
- **Passed**: ${tr.passed}
|
|
119
|
+
- **Failed**: ${tr.failed}
|
|
120
|
+
- **Total**: ${tr.total}
|
|
121
|
+
${isCrash ? '- **CRASH DETECTED**: 0 passed with many failures indicates a startup/import error' : ''}`);
|
|
122
|
+
|
|
123
|
+
if (tr.failedTests && tr.failedTests.length > 0) {
|
|
124
|
+
sections.push(`### Failed Tests
|
|
125
|
+
${tr.failedTests.slice(0, 10).map(t => `- ${t}`).join('\n')}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (tr.output) {
|
|
129
|
+
sections.push(`### Test Output (truncated)
|
|
130
|
+
\`\`\`
|
|
131
|
+
${tr.output.slice(0, 3000)}
|
|
132
|
+
\`\`\``);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Section 4: Consensus history (if available)
|
|
137
|
+
if (taskResult.consensusResult) {
|
|
138
|
+
const cr = taskResult.consensusResult;
|
|
139
|
+
if (cr.finalConcerns.length > 0 || cr.finalRecommendations.length > 0) {
|
|
140
|
+
sections.push(`## Previous Consensus Feedback
|
|
141
|
+
### Concerns Raised by Reviewers
|
|
142
|
+
${cr.finalConcerns.map(c => `- ${c}`).join('\n') || 'None'}
|
|
143
|
+
|
|
144
|
+
### Recommendations from Reviewers
|
|
145
|
+
${cr.finalRecommendations.map(r => `- ${r}`).join('\n') || 'None'}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Section 5: Previous remediation attempts
|
|
150
|
+
if (task.remediationAttempts && task.remediationAttempts > 0) {
|
|
151
|
+
sections.push(`## Previous Remediation Attempts (${task.remediationAttempts})
|
|
152
|
+
### Previous Root Cause Analysis
|
|
153
|
+
${task.lastFailureAnalysis || 'Not available'}
|
|
154
|
+
|
|
155
|
+
### Previous Fix Plan (did not resolve the issue)
|
|
156
|
+
${task.lastRemediationPlan || 'Not available'}
|
|
157
|
+
|
|
158
|
+
**IMPORTANT**: The previous fix did not work. The new fix must take a DIFFERENT approach.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Section 6: Read disk docs if they exist
|
|
162
|
+
if (task.planDoc) {
|
|
163
|
+
try {
|
|
164
|
+
const planDocPath = path.join(projectDir, task.planDoc);
|
|
165
|
+
const planContent = await fs.readFile(planDocPath, 'utf-8');
|
|
166
|
+
sections.push(`## Task Plan Document
|
|
167
|
+
${planContent.slice(0, 1500)}`);
|
|
168
|
+
} catch {
|
|
169
|
+
// File may not exist, skip
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (task.testResultsDoc) {
|
|
174
|
+
try {
|
|
175
|
+
const testDocPath = path.join(projectDir, task.testResultsDoc);
|
|
176
|
+
const testContent = await fs.readFile(testDocPath, 'utf-8');
|
|
177
|
+
sections.push(`## Test Results Document
|
|
178
|
+
${testContent.slice(0, 1500)}`);
|
|
179
|
+
} catch {
|
|
180
|
+
// File may not exist, skip
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Section 7: Milestone context
|
|
185
|
+
const completedTasks = milestone.tasks.filter(t => t.status === 'complete');
|
|
186
|
+
const remainingTasks = milestone.tasks.filter(
|
|
187
|
+
t => t.status !== 'complete' && t.id !== task.id
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
sections.push(`## Milestone Context
|
|
191
|
+
- **Project**: ${state.name}
|
|
192
|
+
- **Language**: ${state.language}
|
|
193
|
+
- **Completed Tasks**: ${completedTasks.map(t => t.name).join(', ') || 'None'}
|
|
194
|
+
- **Remaining Tasks**: ${remainingTasks.map(t => t.name).join(', ') || 'None'}`);
|
|
195
|
+
|
|
196
|
+
return sections.join('\n\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Use Claude to analyze the failure and create a remediation plan
|
|
201
|
+
*
|
|
202
|
+
* @param failureContext - Full failure context markdown
|
|
203
|
+
* @param task - The failed task
|
|
204
|
+
* @param state - Current project state
|
|
205
|
+
* @param onProgress - Progress callback
|
|
206
|
+
* @returns The remediation plan
|
|
207
|
+
*/
|
|
208
|
+
export async function buildRemediationPlan(
|
|
209
|
+
failureContext: string,
|
|
210
|
+
task: Task,
|
|
211
|
+
state: ProjectState,
|
|
212
|
+
onProgress?: (message: string) => void
|
|
213
|
+
): Promise<string> {
|
|
214
|
+
onProgress?.('Analyzing failure and creating remediation plan...');
|
|
215
|
+
|
|
216
|
+
const prompt = `
|
|
217
|
+
You are analyzing a task failure and must create a remediation plan.
|
|
218
|
+
|
|
219
|
+
${failureContext}
|
|
220
|
+
|
|
221
|
+
Based on the failure context above, provide:
|
|
222
|
+
|
|
223
|
+
### Root Cause Analysis
|
|
224
|
+
Identify the specific root cause of the failure. Be precise - don't just restate the error.
|
|
225
|
+
|
|
226
|
+
### Fix Plan
|
|
227
|
+
Step-by-step plan to fix the issue:
|
|
228
|
+
1. [Specific action]
|
|
229
|
+
2. [Specific action]
|
|
230
|
+
...
|
|
231
|
+
|
|
232
|
+
### Files to Modify
|
|
233
|
+
List the exact files that need to be changed and what changes are needed.
|
|
234
|
+
|
|
235
|
+
### Verification Steps
|
|
236
|
+
How to verify the fix works:
|
|
237
|
+
1. [Verification step]
|
|
238
|
+
2. [Verification step]
|
|
239
|
+
|
|
240
|
+
${task.remediationAttempts && task.remediationAttempts > 0
|
|
241
|
+
? '\nIMPORTANT: Previous remediation attempts failed. You MUST take a DIFFERENT approach this time.'
|
|
242
|
+
: ''}
|
|
243
|
+
|
|
244
|
+
Be specific and actionable. This plan will be reviewed before implementation.
|
|
245
|
+
`.trim();
|
|
246
|
+
|
|
247
|
+
const context = `
|
|
248
|
+
Project: ${state.name}
|
|
249
|
+
Language: ${state.language}
|
|
250
|
+
Phase: REMEDIATION
|
|
251
|
+
`.trim();
|
|
252
|
+
|
|
253
|
+
const result = await claudeCreatePlan(prompt, context, state.language, onProgress);
|
|
254
|
+
|
|
255
|
+
if (!result.success) {
|
|
256
|
+
// If Claude analysis fails (e.g., rate limit), return a basic plan
|
|
257
|
+
// based on the available failure context
|
|
258
|
+
onProgress?.('Analysis failed, creating basic remediation plan from available context');
|
|
259
|
+
return `### Root Cause Analysis
|
|
260
|
+
Based on error: ${task.error || 'Unknown error'}
|
|
261
|
+
|
|
262
|
+
### Fix Plan
|
|
263
|
+
1. Review the error output and identify the failing component
|
|
264
|
+
2. Fix the identified issue
|
|
265
|
+
3. Re-run tests to verify
|
|
266
|
+
|
|
267
|
+
### Files to Modify
|
|
268
|
+
See task plan for relevant files.
|
|
269
|
+
|
|
270
|
+
### Verification Steps
|
|
271
|
+
1. Run tests and verify they pass
|
|
272
|
+
2. Check that no regressions were introduced`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return result.response;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get consensus on the remediation plan with full failure context
|
|
280
|
+
*
|
|
281
|
+
* @param plan - The remediation plan
|
|
282
|
+
* @param failureContext - Full failure context for reviewers
|
|
283
|
+
* @param task - The failed task
|
|
284
|
+
* @param milestone - Parent milestone
|
|
285
|
+
* @param state - Current project state
|
|
286
|
+
* @param options - Remediation options
|
|
287
|
+
* @returns Consensus process result
|
|
288
|
+
*/
|
|
289
|
+
export async function runRemediationConsensus(
|
|
290
|
+
plan: string,
|
|
291
|
+
failureContext: string,
|
|
292
|
+
task: Task,
|
|
293
|
+
milestone: Milestone,
|
|
294
|
+
state: ProjectState,
|
|
295
|
+
options: RemediationOptions
|
|
296
|
+
): Promise<ConsensusProcessResult> {
|
|
297
|
+
const { projectDir, consensusConfig, onProgress } = options;
|
|
298
|
+
|
|
299
|
+
const attempt = (task.remediationAttempts || 0) + 1;
|
|
300
|
+
|
|
301
|
+
// Build context that includes failure information for reviewers
|
|
302
|
+
const context = `
|
|
303
|
+
Project: ${state.name}
|
|
304
|
+
Language: ${state.language}
|
|
305
|
+
Phase: REMEDIATION (attempt ${attempt}/${MAX_REMEDIATION_ATTEMPTS})
|
|
306
|
+
Milestone: ${milestone.name}
|
|
307
|
+
Task: ${task.name}
|
|
308
|
+
|
|
309
|
+
## FAILURE CONTEXT (Why this task needs remediation)
|
|
310
|
+
${failureContext.slice(0, 4000)}
|
|
311
|
+
`.trim();
|
|
312
|
+
|
|
313
|
+
const useOptimized = consensusConfig?.useOptimizedConsensus !== false;
|
|
314
|
+
|
|
315
|
+
if (useOptimized) {
|
|
316
|
+
onProgress?.('remediation-consensus', 'Getting consensus on remediation plan (optimized)...');
|
|
317
|
+
const result = await runOptimizedConsensusProcess(
|
|
318
|
+
plan,
|
|
319
|
+
context,
|
|
320
|
+
{
|
|
321
|
+
projectDir,
|
|
322
|
+
config: consensusConfig,
|
|
323
|
+
milestoneId: milestone.id,
|
|
324
|
+
milestoneName: milestone.name,
|
|
325
|
+
taskId: task.id,
|
|
326
|
+
taskName: `${task.name} - Remediation ${attempt}`,
|
|
327
|
+
parallelReviews: true,
|
|
328
|
+
isFullstack: isWorkspace(state.language),
|
|
329
|
+
onIteration: (iteration, result) => {
|
|
330
|
+
onProgress?.('remediation-consensus', `Remediation consensus iteration ${iteration}: ${result.score}%`);
|
|
331
|
+
},
|
|
332
|
+
onProgress,
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
// runOptimizedConsensusProcess returns ConsensusProcessResult or FullstackConsensusProcessResult
|
|
336
|
+
// Both satisfy ConsensusProcessResult
|
|
337
|
+
return result as ConsensusProcessResult;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
onProgress?.('remediation-consensus', 'Getting consensus on remediation plan...');
|
|
341
|
+
return iterateUntilConsensus(
|
|
342
|
+
plan,
|
|
343
|
+
context,
|
|
344
|
+
{
|
|
345
|
+
projectDir,
|
|
346
|
+
config: consensusConfig,
|
|
347
|
+
isFullstack: isWorkspace(state.language),
|
|
348
|
+
language: state.language,
|
|
349
|
+
onIteration: (iteration, result) => {
|
|
350
|
+
onProgress?.('remediation-consensus', `Remediation consensus iteration ${iteration}: ${result.score}%`);
|
|
351
|
+
},
|
|
352
|
+
onProgress,
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Implement the approved remediation fix
|
|
359
|
+
*
|
|
360
|
+
* @param task - The task to fix
|
|
361
|
+
* @param plan - The approved remediation plan
|
|
362
|
+
* @param projectDir - Project directory
|
|
363
|
+
* @param onProgress - Progress callback
|
|
364
|
+
* @returns Code generation result
|
|
365
|
+
*/
|
|
366
|
+
export async function executeRemediation(
|
|
367
|
+
task: Task,
|
|
368
|
+
plan: string,
|
|
369
|
+
projectDir: string,
|
|
370
|
+
onProgress?: (message: string) => void
|
|
371
|
+
): Promise<{ success: boolean; rateLimitPaused?: boolean; rateLimitInfo?: { message?: string }; error?: string }> {
|
|
372
|
+
onProgress?.('Implementing remediation fix...');
|
|
373
|
+
|
|
374
|
+
const prompt = `
|
|
375
|
+
## Remediation Fix Required
|
|
376
|
+
|
|
377
|
+
### Task: ${task.name}
|
|
378
|
+
${task.description}
|
|
379
|
+
|
|
380
|
+
### Approved Fix Plan
|
|
381
|
+
${plan}
|
|
382
|
+
|
|
383
|
+
### Instructions
|
|
384
|
+
1. Implement the fix plan above
|
|
385
|
+
2. Focus only on the changes specified in the plan
|
|
386
|
+
3. Do NOT make unrelated changes
|
|
387
|
+
4. Ensure the fix addresses the root cause
|
|
388
|
+
`.trim();
|
|
389
|
+
|
|
390
|
+
const context = `Remediation fix for task: ${task.name}`;
|
|
391
|
+
|
|
392
|
+
const result = await generateCode(prompt, context, {
|
|
393
|
+
cwd: projectDir,
|
|
394
|
+
onProgress,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (result.rateLimitPaused) {
|
|
398
|
+
return {
|
|
399
|
+
success: false,
|
|
400
|
+
rateLimitPaused: true,
|
|
401
|
+
rateLimitInfo: result.rateLimitInfo,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
success: result.success,
|
|
407
|
+
error: result.error,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Document a remediation attempt to disk
|
|
413
|
+
*
|
|
414
|
+
* @param projectDir - Project directory
|
|
415
|
+
* @param milestone - Parent milestone
|
|
416
|
+
* @param task - The task
|
|
417
|
+
* @param attempt - Attempt number
|
|
418
|
+
* @param failureAnalysis - Root cause analysis
|
|
419
|
+
* @param remediationPlan - The fix plan
|
|
420
|
+
* @param consensusScore - Consensus score for the plan
|
|
421
|
+
* @param success - Whether the remediation succeeded
|
|
422
|
+
*/
|
|
423
|
+
export async function documentRemediationAttempt(
|
|
424
|
+
projectDir: string,
|
|
425
|
+
milestone: Milestone,
|
|
426
|
+
task: Task,
|
|
427
|
+
attempt: number,
|
|
428
|
+
failureAnalysis: string,
|
|
429
|
+
remediationPlan: string,
|
|
430
|
+
consensusScore: number,
|
|
431
|
+
success: boolean
|
|
432
|
+
): Promise<string> {
|
|
433
|
+
const docsDir = path.join(projectDir, 'docs', 'remediation');
|
|
434
|
+
await fs.mkdir(docsDir, { recursive: true });
|
|
435
|
+
|
|
436
|
+
const milestoneNum = milestone.id.replace('milestone-', '');
|
|
437
|
+
const taskNum = task.id.split('-task-')[1] || '1';
|
|
438
|
+
const filename = `milestone_${milestoneNum}_task_${taskNum}_remediation_${attempt}.md`;
|
|
439
|
+
const docPath = path.join(docsDir, filename);
|
|
440
|
+
|
|
441
|
+
const content = `# Remediation Attempt ${attempt}: ${task.name}
|
|
442
|
+
|
|
443
|
+
## Metadata
|
|
444
|
+
- **Milestone**: ${milestone.name}
|
|
445
|
+
- **Task ID**: ${task.id}
|
|
446
|
+
- **Attempt**: ${attempt}/${MAX_REMEDIATION_ATTEMPTS}
|
|
447
|
+
- **Consensus Score**: ${consensusScore}%
|
|
448
|
+
- **Result**: ${success ? 'SUCCESS' : 'FAILED'}
|
|
449
|
+
- **Timestamp**: ${new Date().toISOString()}
|
|
450
|
+
|
|
451
|
+
## Failure Analysis
|
|
452
|
+
${failureAnalysis}
|
|
453
|
+
|
|
454
|
+
## Remediation Plan
|
|
455
|
+
${remediationPlan}
|
|
456
|
+
`;
|
|
457
|
+
|
|
458
|
+
await fs.writeFile(docPath, content, 'utf-8');
|
|
459
|
+
return `docs/remediation/${filename}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Orchestrate a complete remediation attempt for a failed task
|
|
464
|
+
*
|
|
465
|
+
* Flow:
|
|
466
|
+
* 1. Check attempt count < MAX
|
|
467
|
+
* 2. Build failure context
|
|
468
|
+
* 3. Create fix plan via Claude
|
|
469
|
+
* 4. Get consensus with full failure context
|
|
470
|
+
* 5. Document the attempt
|
|
471
|
+
* 6. Implement the fix
|
|
472
|
+
* 7. Reset task state and re-run task workflow
|
|
473
|
+
*
|
|
474
|
+
* @param milestone - Parent milestone
|
|
475
|
+
* @param task - The failed task
|
|
476
|
+
* @param taskResult - The failed task result
|
|
477
|
+
* @param options - Remediation options
|
|
478
|
+
* @returns Remediation result
|
|
479
|
+
*/
|
|
480
|
+
export async function attemptRemediation(
|
|
481
|
+
milestone: Milestone,
|
|
482
|
+
task: Task,
|
|
483
|
+
taskResult: TaskWorkflowResult,
|
|
484
|
+
options: RemediationOptions
|
|
485
|
+
): Promise<RemediationResult> {
|
|
486
|
+
const { projectDir, consensusConfig, onProgress } = options;
|
|
487
|
+
|
|
488
|
+
// Load fresh state
|
|
489
|
+
let state = await loadProject(projectDir);
|
|
490
|
+
|
|
491
|
+
// Get current task from state (may have been updated)
|
|
492
|
+
const currentMilestone = state.milestones.find(m => m.id === milestone.id);
|
|
493
|
+
const currentTask = currentMilestone?.tasks.find(t => t.id === task.id);
|
|
494
|
+
const attempts = currentTask?.remediationAttempts || 0;
|
|
495
|
+
|
|
496
|
+
// Check if max attempts reached
|
|
497
|
+
if (attempts >= MAX_REMEDIATION_ATTEMPTS) {
|
|
498
|
+
onProgress?.('remediation', `Max remediation attempts (${MAX_REMEDIATION_ATTEMPTS}) reached for: ${task.name}`);
|
|
499
|
+
return {
|
|
500
|
+
success: false,
|
|
501
|
+
attempt: attempts,
|
|
502
|
+
maxAttempts: MAX_REMEDIATION_ATTEMPTS,
|
|
503
|
+
error: `Max remediation attempts (${MAX_REMEDIATION_ATTEMPTS}) reached`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const currentAttempt = attempts + 1;
|
|
508
|
+
onProgress?.('remediation-analysis', `Remediation attempt ${currentAttempt}/${MAX_REMEDIATION_ATTEMPTS} for: ${task.name}`);
|
|
509
|
+
|
|
510
|
+
// Increment attempt counter in state
|
|
511
|
+
state = await updateTaskInState(projectDir, task.id, {
|
|
512
|
+
remediationAttempts: currentAttempt,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Step 1: Build failure context
|
|
516
|
+
onProgress?.('remediation-analysis', 'Gathering failure context...');
|
|
517
|
+
const failureContext = await buildFailureContext(
|
|
518
|
+
currentTask || task,
|
|
519
|
+
currentMilestone || milestone,
|
|
520
|
+
taskResult,
|
|
521
|
+
state,
|
|
522
|
+
projectDir
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
// Step 2: Create fix plan via Claude
|
|
526
|
+
onProgress?.('remediation-plan', 'Creating remediation plan...');
|
|
527
|
+
const remediationPlan = await buildRemediationPlan(
|
|
528
|
+
failureContext,
|
|
529
|
+
currentTask || task,
|
|
530
|
+
state,
|
|
531
|
+
(msg) => onProgress?.('remediation-plan', msg)
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// Extract failure analysis from the plan (first section)
|
|
535
|
+
const analysisMatch = remediationPlan.match(/### Root Cause Analysis\n([\s\S]*?)(?=\n### |$)/);
|
|
536
|
+
const failureAnalysis = analysisMatch ? analysisMatch[1].trim() : remediationPlan.slice(0, 500);
|
|
537
|
+
|
|
538
|
+
// Save analysis to state
|
|
539
|
+
state = await updateTaskInState(projectDir, task.id, {
|
|
540
|
+
lastFailureAnalysis: failureAnalysis,
|
|
541
|
+
lastRemediationPlan: remediationPlan,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Step 3: Get consensus on remediation plan
|
|
545
|
+
onProgress?.('remediation-consensus', 'Getting consensus on remediation plan...');
|
|
546
|
+
let consensusResult: ConsensusProcessResult;
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
consensusResult = await runRemediationConsensus(
|
|
550
|
+
remediationPlan,
|
|
551
|
+
failureContext,
|
|
552
|
+
currentTask || task,
|
|
553
|
+
currentMilestone || milestone,
|
|
554
|
+
state,
|
|
555
|
+
options
|
|
556
|
+
);
|
|
557
|
+
} catch (consensusError) {
|
|
558
|
+
const errorMsg = consensusError instanceof Error ? consensusError.message : 'Unknown error';
|
|
559
|
+
onProgress?.('remediation-consensus', `Consensus failed: ${errorMsg}`);
|
|
560
|
+
return {
|
|
561
|
+
success: false,
|
|
562
|
+
remediationPlan,
|
|
563
|
+
failureAnalysis,
|
|
564
|
+
attempt: currentAttempt,
|
|
565
|
+
maxAttempts: MAX_REMEDIATION_ATTEMPTS,
|
|
566
|
+
error: `Consensus failed: ${errorMsg}`,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Check if consensus was rejected
|
|
571
|
+
if (!consensusResult.approved) {
|
|
572
|
+
onProgress?.('remediation-consensus', `Remediation plan not approved (${consensusResult.finalScore}%)`);
|
|
573
|
+
|
|
574
|
+
// Document the rejected attempt
|
|
575
|
+
await documentRemediationAttempt(
|
|
576
|
+
projectDir,
|
|
577
|
+
currentMilestone || milestone,
|
|
578
|
+
currentTask || task,
|
|
579
|
+
currentAttempt,
|
|
580
|
+
failureAnalysis,
|
|
581
|
+
remediationPlan,
|
|
582
|
+
consensusResult.finalScore,
|
|
583
|
+
false
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
success: false,
|
|
588
|
+
remediationPlan,
|
|
589
|
+
consensusResult,
|
|
590
|
+
failureAnalysis,
|
|
591
|
+
attempt: currentAttempt,
|
|
592
|
+
maxAttempts: MAX_REMEDIATION_ATTEMPTS,
|
|
593
|
+
error: `Remediation plan not approved. Score: ${consensusResult.finalScore}%`,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
onProgress?.('remediation-consensus', `Remediation plan approved (${consensusResult.finalScore}%)`);
|
|
598
|
+
|
|
599
|
+
// Step 4: Document the attempt
|
|
600
|
+
onProgress?.('remediation-doc', 'Documenting remediation attempt...');
|
|
601
|
+
await documentRemediationAttempt(
|
|
602
|
+
projectDir,
|
|
603
|
+
currentMilestone || milestone,
|
|
604
|
+
currentTask || task,
|
|
605
|
+
currentAttempt,
|
|
606
|
+
failureAnalysis,
|
|
607
|
+
consensusResult.bestPlan,
|
|
608
|
+
consensusResult.finalScore,
|
|
609
|
+
false // Not yet known - will update if successful
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
// Step 5: Implement the fix
|
|
613
|
+
onProgress?.('remediation-fix', 'Implementing remediation fix...');
|
|
614
|
+
const fixResult = await executeRemediation(
|
|
615
|
+
currentTask || task,
|
|
616
|
+
consensusResult.bestPlan,
|
|
617
|
+
projectDir,
|
|
618
|
+
(msg) => onProgress?.('remediation-fix', msg)
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// Handle rate limit pause
|
|
622
|
+
if (fixResult.rateLimitPaused) {
|
|
623
|
+
onProgress?.('remediation-fix', 'Rate limit during remediation - pausing gracefully');
|
|
624
|
+
|
|
625
|
+
// Save state for resume
|
|
626
|
+
await updateTaskInState(projectDir, task.id, {
|
|
627
|
+
status: 'paused',
|
|
628
|
+
error: `Rate limit during remediation. ${fixResult.rateLimitInfo?.message || ''}`,
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
success: false,
|
|
633
|
+
remediationPlan: consensusResult.bestPlan,
|
|
634
|
+
consensusResult,
|
|
635
|
+
failureAnalysis,
|
|
636
|
+
attempt: currentAttempt,
|
|
637
|
+
maxAttempts: MAX_REMEDIATION_ATTEMPTS,
|
|
638
|
+
rateLimitPaused: true,
|
|
639
|
+
error: `Rate limit during remediation fix`,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Handle fix failure
|
|
644
|
+
if (!fixResult.success) {
|
|
645
|
+
onProgress?.('remediation-fix', `Remediation fix failed: ${fixResult.error}`);
|
|
646
|
+
return {
|
|
647
|
+
success: false,
|
|
648
|
+
remediationPlan: consensusResult.bestPlan,
|
|
649
|
+
consensusResult,
|
|
650
|
+
failureAnalysis,
|
|
651
|
+
attempt: currentAttempt,
|
|
652
|
+
maxAttempts: MAX_REMEDIATION_ATTEMPTS,
|
|
653
|
+
error: `Remediation fix implementation failed: ${fixResult.error}`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Step 6: Reset task state and re-run task workflow
|
|
658
|
+
onProgress?.('remediation-retry', 'Retrying task after remediation fix...');
|
|
659
|
+
|
|
660
|
+
// Reset task for retry: keep consensus (plan is still valid), reset implementation
|
|
661
|
+
state = await updateTaskInState(projectDir, task.id, {
|
|
662
|
+
status: 'pending',
|
|
663
|
+
implementationComplete: false,
|
|
664
|
+
testsPassed: undefined,
|
|
665
|
+
error: undefined,
|
|
666
|
+
// Keep consensusApproved, plan, consensusScore (the original plan is still valid)
|
|
667
|
+
// Keep remediationAttempts, lastFailureAnalysis, lastRemediationPlan (for tracking)
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Re-run the task workflow (gets fresh 3 test retries)
|
|
671
|
+
const retryResult = await runTaskWorkflow(
|
|
672
|
+
currentMilestone || milestone,
|
|
673
|
+
currentTask || task,
|
|
674
|
+
{
|
|
675
|
+
projectDir,
|
|
676
|
+
consensusConfig,
|
|
677
|
+
onProgress,
|
|
678
|
+
}
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
// Update documentation if successful
|
|
682
|
+
if (retryResult.success) {
|
|
683
|
+
onProgress?.('remediation-retry', `Remediation successful - task "${task.name}" now passes`);
|
|
684
|
+
|
|
685
|
+
// Rewrite doc as successful
|
|
686
|
+
await documentRemediationAttempt(
|
|
687
|
+
projectDir,
|
|
688
|
+
currentMilestone || milestone,
|
|
689
|
+
currentTask || task,
|
|
690
|
+
currentAttempt,
|
|
691
|
+
failureAnalysis,
|
|
692
|
+
consensusResult.bestPlan,
|
|
693
|
+
consensusResult.finalScore,
|
|
694
|
+
true
|
|
695
|
+
);
|
|
696
|
+
} else {
|
|
697
|
+
onProgress?.('remediation-retry', `Task still failing after remediation attempt ${currentAttempt}`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
success: retryResult.success,
|
|
702
|
+
taskResult: retryResult,
|
|
703
|
+
remediationPlan: consensusResult.bestPlan,
|
|
704
|
+
consensusResult,
|
|
705
|
+
failureAnalysis,
|
|
706
|
+
attempt: currentAttempt,
|
|
707
|
+
maxAttempts: MAX_REMEDIATION_ATTEMPTS,
|
|
708
|
+
error: retryResult.success ? undefined : retryResult.error,
|
|
709
|
+
rateLimitPaused: retryResult.rateLimitPaused,
|
|
710
|
+
};
|
|
711
|
+
}
|