watchfix 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli/commands/fix.js +164 -33
- package/dist/cli/commands/watch.js +1 -0
- package/dist/db/queries.d.ts +1 -1
- package/dist/fixer/context.js +14 -5
- package/dist/fixer/index.js +49 -4
- package/dist/fixer/output.d.ts +1 -0
- package/dist/fixer/output.js +13 -1
- package/dist/utils/errors.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,8 @@ watchfix fix <error-id>
|
|
|
94
94
|
|
|
95
95
|
For detailed configuration options and advanced usage, see the [specification document](./spec/watchfix-spec-v8.md).
|
|
96
96
|
|
|
97
|
+
For a working example project, see [watchfix-example](https://github.com/CaseyHaralson/watchfix-example).
|
|
98
|
+
|
|
97
99
|
## License
|
|
98
100
|
|
|
99
101
|
[MIT](./LICENSE)
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -39,13 +39,34 @@ const parseStoredAnalysis = (value) => {
|
|
|
39
39
|
const parsed = JSON.parse(value);
|
|
40
40
|
if (!parsed ||
|
|
41
41
|
typeof parsed.summary !== 'string' ||
|
|
42
|
-
typeof parsed.root_cause !== 'string' ||
|
|
43
|
-
typeof parsed.suggested_fix !== 'string' ||
|
|
44
|
-
!Array.isArray(parsed.files_to_modify) ||
|
|
45
42
|
typeof parsed.confidence !== 'string') {
|
|
46
43
|
return null;
|
|
47
44
|
}
|
|
48
|
-
|
|
45
|
+
// For already_fixed analyses, root_cause/suggested_fix/files_to_modify may be empty
|
|
46
|
+
if (parsed.already_fixed === true) {
|
|
47
|
+
return {
|
|
48
|
+
already_fixed: true,
|
|
49
|
+
summary: parsed.summary,
|
|
50
|
+
root_cause: parsed.root_cause ?? '',
|
|
51
|
+
suggested_fix: parsed.suggested_fix ?? '',
|
|
52
|
+
files_to_modify: parsed.files_to_modify ?? [],
|
|
53
|
+
confidence: parsed.confidence,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// For regular analyses, ensure all fields are present
|
|
57
|
+
if (typeof parsed.root_cause !== 'string' ||
|
|
58
|
+
typeof parsed.suggested_fix !== 'string' ||
|
|
59
|
+
!Array.isArray(parsed.files_to_modify)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
already_fixed: parsed.already_fixed ?? false,
|
|
64
|
+
summary: parsed.summary,
|
|
65
|
+
root_cause: parsed.root_cause,
|
|
66
|
+
suggested_fix: parsed.suggested_fix,
|
|
67
|
+
files_to_modify: parsed.files_to_modify,
|
|
68
|
+
confidence: parsed.confidence,
|
|
69
|
+
};
|
|
49
70
|
}
|
|
50
71
|
catch {
|
|
51
72
|
return null;
|
|
@@ -54,6 +75,11 @@ const parseStoredAnalysis = (value) => {
|
|
|
54
75
|
const formatAnalysisSummary = (analysis) => {
|
|
55
76
|
const lines = ['Analysis summary:'];
|
|
56
77
|
lines.push(` Summary: ${analysis.summary}`);
|
|
78
|
+
if (analysis.already_fixed) {
|
|
79
|
+
lines.push(' Status: Issue already fixed (no action needed)');
|
|
80
|
+
lines.push(` Confidence: ${analysis.confidence}`);
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
57
83
|
lines.push(' Root cause:');
|
|
58
84
|
lines.push(...analysis.root_cause.split('\n').map((line) => ` ${line}`));
|
|
59
85
|
lines.push(' Suggested fix:');
|
|
@@ -87,11 +113,90 @@ const promptForConfirmation = async (label) => {
|
|
|
87
113
|
rl.close();
|
|
88
114
|
}
|
|
89
115
|
};
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
return
|
|
116
|
+
const formatFilesChanged = (files, indent = ' ') => {
|
|
117
|
+
if (!files || files.length === 0) {
|
|
118
|
+
return [`${indent}(no files changed)`];
|
|
119
|
+
}
|
|
120
|
+
return files.map((f) => `${indent}- ${f.path}: ${f.change}`);
|
|
121
|
+
};
|
|
122
|
+
const formatVerificationSummary = (result, verbosity) => {
|
|
123
|
+
if (!result) {
|
|
124
|
+
return [' Verification: not run'];
|
|
125
|
+
}
|
|
126
|
+
if (result.success) {
|
|
127
|
+
return [' Verification: PASSED'];
|
|
128
|
+
}
|
|
129
|
+
const lines = [' Verification: FAILED'];
|
|
130
|
+
if (result.failure) {
|
|
131
|
+
lines.push(` ${result.failure.message}`);
|
|
132
|
+
if (verbosity === 'verbose' && result.failure.type === 'command') {
|
|
133
|
+
const stdout = result.failure.stdout.trim();
|
|
134
|
+
const stderr = result.failure.stderr.trim();
|
|
135
|
+
if (stdout) {
|
|
136
|
+
lines.push(' stdout:');
|
|
137
|
+
lines.push(...stdout.split('\n').map((line) => ` ${line}`));
|
|
138
|
+
}
|
|
139
|
+
if (stderr) {
|
|
140
|
+
lines.push(' stderr:');
|
|
141
|
+
lines.push(...stderr.split('\n').map((line) => ` ${line}`));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return lines;
|
|
146
|
+
};
|
|
147
|
+
const formatStatusLine = (result, maxAttempts) => {
|
|
148
|
+
if (result.status === 'fixed') {
|
|
149
|
+
return 'Status: fixed';
|
|
93
150
|
}
|
|
94
|
-
|
|
151
|
+
if (result.status === 'resolved') {
|
|
152
|
+
return 'Status: resolved (issue already fixed)';
|
|
153
|
+
}
|
|
154
|
+
const retryInfo = result.attempts < maxAttempts ? ', will retry' : ', max attempts reached';
|
|
155
|
+
return `Status: ${result.status} (attempt ${result.attempts} of ${maxAttempts}${retryInfo})`;
|
|
156
|
+
};
|
|
157
|
+
const formatFixOutcome = (result, verbosity, maxAttempts) => {
|
|
158
|
+
if (verbosity === 'quiet') {
|
|
159
|
+
if (result.status === 'fixed') {
|
|
160
|
+
return [`Error #${result.errorId}: fixed`];
|
|
161
|
+
}
|
|
162
|
+
if (result.status === 'resolved') {
|
|
163
|
+
return [`Error #${result.errorId}: resolved (already fixed)`];
|
|
164
|
+
}
|
|
165
|
+
const mode = result.fix?.success ? 'verification' : 'agent';
|
|
166
|
+
return [`Error #${result.errorId}: failed (${mode})`];
|
|
167
|
+
}
|
|
168
|
+
const lines = [];
|
|
169
|
+
// Header
|
|
170
|
+
if (result.status === 'fixed') {
|
|
171
|
+
lines.push(`Error #${result.errorId}: Fix verified successfully`);
|
|
172
|
+
}
|
|
173
|
+
else if (result.status === 'resolved') {
|
|
174
|
+
lines.push(`Error #${result.errorId}: Issue already fixed`);
|
|
175
|
+
}
|
|
176
|
+
else if (!result.fix?.success) {
|
|
177
|
+
lines.push(`Error #${result.errorId}: Agent could not apply fix`);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
lines.push(`Error #${result.errorId}: Fix attempt completed`);
|
|
181
|
+
}
|
|
182
|
+
// Agent details
|
|
183
|
+
if (result.fix) {
|
|
184
|
+
lines.push(` Agent applied fix: ${result.fix.success ? 'yes' : 'no'}`);
|
|
185
|
+
if (result.fix.files_changed?.length) {
|
|
186
|
+
lines.push(' Files changed:');
|
|
187
|
+
lines.push(...formatFilesChanged(result.fix.files_changed, ' '));
|
|
188
|
+
}
|
|
189
|
+
if (result.fix.notes && (verbosity === 'verbose' || !result.fix.success)) {
|
|
190
|
+
lines.push(` Agent notes: ${result.fix.notes}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Verification
|
|
194
|
+
if (result.verification) {
|
|
195
|
+
lines.push(...formatVerificationSummary(result.verification, verbosity));
|
|
196
|
+
}
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push(formatStatusLine(result, maxAttempts));
|
|
199
|
+
return lines;
|
|
95
200
|
};
|
|
96
201
|
const checkDaemonConflict = (db, logger) => {
|
|
97
202
|
const state = getWatcherState(db);
|
|
@@ -138,20 +243,23 @@ const reportAnalysisFromResult = (db, errorId, result, note) => {
|
|
|
138
243
|
parseStoredAnalysis(getError(db, errorId)?.suggestion ?? null);
|
|
139
244
|
reportAnalysis(errorId, analysis, note);
|
|
140
245
|
};
|
|
141
|
-
const reportFixResult = (result) => {
|
|
246
|
+
const reportFixResult = (result, verbosity = 'normal', maxAttempts = 3) => {
|
|
142
247
|
if (!result.lockAcquired) {
|
|
143
248
|
process.stdout.write(`Skipped error #${result.errorId}: already locked by another process.\n`);
|
|
144
249
|
return 'skipped';
|
|
145
250
|
}
|
|
251
|
+
const lines = formatFixOutcome(result, verbosity, maxAttempts);
|
|
252
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
146
253
|
if (result.status === 'fixed') {
|
|
147
|
-
process.stdout.write(`✓ Error #${result.errorId} fixed successfully.\n`);
|
|
148
254
|
return 'fixed';
|
|
149
255
|
}
|
|
150
|
-
|
|
151
|
-
|
|
256
|
+
if (result.status === 'resolved') {
|
|
257
|
+
return 'resolved';
|
|
258
|
+
}
|
|
152
259
|
return 'failed';
|
|
153
260
|
};
|
|
154
|
-
const runSingleFix = async (db, error, options, orchestrator) => {
|
|
261
|
+
const runSingleFix = async (db, error, options, orchestrator, ctx) => {
|
|
262
|
+
const { verbosity, maxAttempts } = ctx;
|
|
155
263
|
const reanalyze = Boolean(options.reanalyze);
|
|
156
264
|
const analyzeOnly = Boolean(options.analyzeOnly);
|
|
157
265
|
const shouldPrompt = !options.yes && !analyzeOnly;
|
|
@@ -163,11 +271,11 @@ const runSingleFix = async (db, error, options, orchestrator) => {
|
|
|
163
271
|
});
|
|
164
272
|
if (!result.lockAcquired) {
|
|
165
273
|
process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
|
|
166
|
-
return reportFixResult(result);
|
|
274
|
+
return reportFixResult(result, verbosity, maxAttempts);
|
|
167
275
|
}
|
|
168
276
|
if (result.status !== 'suggested') {
|
|
169
277
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
170
|
-
reportFixResult(result);
|
|
278
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
171
279
|
return 'failed';
|
|
172
280
|
}
|
|
173
281
|
reportAnalysis(error.id, result.analysis ??
|
|
@@ -184,7 +292,7 @@ const runSingleFix = async (db, error, options, orchestrator) => {
|
|
|
184
292
|
});
|
|
185
293
|
if (!analysisResult.lockAcquired) {
|
|
186
294
|
process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
|
|
187
|
-
return reportFixResult(analysisResult);
|
|
295
|
+
return reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
188
296
|
}
|
|
189
297
|
analysisNote = analysisResult.message;
|
|
190
298
|
analysis =
|
|
@@ -192,7 +300,7 @@ const runSingleFix = async (db, error, options, orchestrator) => {
|
|
|
192
300
|
parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null);
|
|
193
301
|
if (analysisResult.status !== 'suggested') {
|
|
194
302
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
195
|
-
return reportFixResult(analysisResult);
|
|
303
|
+
return reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
196
304
|
}
|
|
197
305
|
reanalyzeForFix = false;
|
|
198
306
|
}
|
|
@@ -213,15 +321,16 @@ const runSingleFix = async (db, error, options, orchestrator) => {
|
|
|
213
321
|
});
|
|
214
322
|
if (!result.lockAcquired) {
|
|
215
323
|
process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
|
|
216
|
-
return reportFixResult(result);
|
|
324
|
+
return reportFixResult(result, verbosity, maxAttempts);
|
|
217
325
|
}
|
|
218
326
|
reportAnalysisFromResult(db, error.id, result);
|
|
219
|
-
if (result.status !== 'fixed') {
|
|
327
|
+
if (result.status !== 'fixed' && result.status !== 'resolved') {
|
|
220
328
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
221
329
|
}
|
|
222
|
-
return reportFixResult(result);
|
|
330
|
+
return reportFixResult(result, verbosity, maxAttempts);
|
|
223
331
|
};
|
|
224
|
-
const runAllFixes = async (db, options, orchestrator) => {
|
|
332
|
+
const runAllFixes = async (db, options, orchestrator, ctx) => {
|
|
333
|
+
const { verbosity, maxAttempts } = ctx;
|
|
225
334
|
const errors = getErrorsByStatus(db, FIXABLE_STATUSES);
|
|
226
335
|
if (errors.length === 0) {
|
|
227
336
|
process.stdout.write('No pending or suggested errors to fix.\n');
|
|
@@ -234,6 +343,7 @@ const runAllFixes = async (db, options, orchestrator) => {
|
|
|
234
343
|
let failedCount = 0;
|
|
235
344
|
let skippedCount = 0;
|
|
236
345
|
let analyzedCount = 0;
|
|
346
|
+
let resolvedCount = 0;
|
|
237
347
|
for (const error of errors) {
|
|
238
348
|
const fixabilityIssue = ensureFixable(error);
|
|
239
349
|
if (fixabilityIssue) {
|
|
@@ -248,12 +358,17 @@ const runAllFixes = async (db, options, orchestrator) => {
|
|
|
248
358
|
});
|
|
249
359
|
if (!result.lockAcquired) {
|
|
250
360
|
skippedCount += 1;
|
|
251
|
-
reportFixResult(result);
|
|
361
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (result.status === 'resolved') {
|
|
365
|
+
resolvedCount += 1;
|
|
366
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
252
367
|
continue;
|
|
253
368
|
}
|
|
254
369
|
if (result.status !== 'suggested') {
|
|
255
370
|
failedCount += 1;
|
|
256
|
-
reportFixResult(result);
|
|
371
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
257
372
|
continue;
|
|
258
373
|
}
|
|
259
374
|
reportAnalysis(error.id, result.analysis ?? parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null), result.message);
|
|
@@ -271,16 +386,21 @@ const runAllFixes = async (db, options, orchestrator) => {
|
|
|
271
386
|
});
|
|
272
387
|
if (!analysisResult.lockAcquired) {
|
|
273
388
|
skippedCount += 1;
|
|
274
|
-
reportFixResult(analysisResult);
|
|
389
|
+
reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
275
390
|
continue;
|
|
276
391
|
}
|
|
277
392
|
note = analysisResult.message;
|
|
278
393
|
analysis =
|
|
279
394
|
analysisResult.analysis ??
|
|
280
395
|
parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null);
|
|
396
|
+
if (analysisResult.status === 'resolved') {
|
|
397
|
+
resolvedCount += 1;
|
|
398
|
+
reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
281
401
|
if (analysisResult.status !== 'suggested') {
|
|
282
402
|
failedCount += 1;
|
|
283
|
-
reportFixResult(analysisResult);
|
|
403
|
+
reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
284
404
|
continue;
|
|
285
405
|
}
|
|
286
406
|
reanalyzeForFix = false;
|
|
@@ -302,36 +422,43 @@ const runAllFixes = async (db, options, orchestrator) => {
|
|
|
302
422
|
});
|
|
303
423
|
if (!result.lockAcquired) {
|
|
304
424
|
skippedCount += 1;
|
|
305
|
-
reportFixResult(result);
|
|
425
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
306
426
|
continue;
|
|
307
427
|
}
|
|
308
428
|
if (result.status === 'fixed') {
|
|
309
429
|
fixedCount += 1;
|
|
310
430
|
}
|
|
431
|
+
else if (result.status === 'resolved') {
|
|
432
|
+
resolvedCount += 1;
|
|
433
|
+
}
|
|
311
434
|
else {
|
|
312
435
|
failedCount += 1;
|
|
313
436
|
}
|
|
314
|
-
reportFixResult(result);
|
|
437
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
315
438
|
continue;
|
|
316
439
|
}
|
|
317
440
|
const result = await orchestrator.fixError(error.id, { reanalyze });
|
|
318
441
|
if (!result.lockAcquired) {
|
|
319
442
|
skippedCount += 1;
|
|
320
|
-
reportFixResult(result);
|
|
443
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
321
444
|
continue;
|
|
322
445
|
}
|
|
323
446
|
reportAnalysisFromResult(db, error.id, result);
|
|
324
447
|
if (result.status === 'fixed') {
|
|
325
448
|
fixedCount += 1;
|
|
326
449
|
}
|
|
450
|
+
else if (result.status === 'resolved') {
|
|
451
|
+
resolvedCount += 1;
|
|
452
|
+
}
|
|
327
453
|
else {
|
|
328
454
|
failedCount += 1;
|
|
329
455
|
}
|
|
330
|
-
reportFixResult(result);
|
|
456
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
331
457
|
}
|
|
458
|
+
const resolvedSuffix = resolvedCount > 0 ? `, resolved ${resolvedCount}` : '';
|
|
332
459
|
const summary = analyzeOnly
|
|
333
|
-
? `Analyzed ${analyzedCount} errors, failed ${failedCount}, skipped ${skippedCount}.`
|
|
334
|
-
: `Summary: fixed ${fixedCount}, failed ${failedCount}, skipped ${skippedCount}.`;
|
|
460
|
+
? `Analyzed ${analyzedCount} errors, failed ${failedCount}, skipped ${skippedCount}${resolvedSuffix}.`
|
|
461
|
+
: `Summary: fixed ${fixedCount}, failed ${failedCount}, skipped ${skippedCount}${resolvedSuffix}.`;
|
|
335
462
|
process.stdout.write(`${summary}\n`);
|
|
336
463
|
if (!analyzeOnly && failedCount > 0) {
|
|
337
464
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
@@ -366,8 +493,12 @@ export const fixCommand = async (id, options) => {
|
|
|
366
493
|
logger,
|
|
367
494
|
terminalEnabled: true,
|
|
368
495
|
});
|
|
496
|
+
const ctx = {
|
|
497
|
+
verbosity,
|
|
498
|
+
maxAttempts: config.limits.max_attempts_per_error,
|
|
499
|
+
};
|
|
369
500
|
if (options.all) {
|
|
370
|
-
await runAllFixes(db, options, orchestrator);
|
|
501
|
+
await runAllFixes(db, options, orchestrator, ctx);
|
|
371
502
|
return;
|
|
372
503
|
}
|
|
373
504
|
const errorId = parsePositiveInt(id);
|
|
@@ -383,7 +514,7 @@ export const fixCommand = async (id, options) => {
|
|
|
383
514
|
process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
|
|
384
515
|
return;
|
|
385
516
|
}
|
|
386
|
-
await runSingleFix(db, error, options, orchestrator);
|
|
517
|
+
await runSingleFix(db, error, options, orchestrator, ctx);
|
|
387
518
|
}
|
|
388
519
|
finally {
|
|
389
520
|
db.close();
|
|
@@ -197,6 +197,7 @@ export const watchCommand = async (options) => {
|
|
|
197
197
|
clearWatcherState: () => clearWatcherState(db),
|
|
198
198
|
});
|
|
199
199
|
await watcher.start();
|
|
200
|
+
logger.info('Watching for errors... (Ctrl+C to stop)');
|
|
200
201
|
if (options.autonomous) {
|
|
201
202
|
void fixQueue?.processQueueIfReady();
|
|
202
203
|
}
|
package/dist/db/queries.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type ErrorInsert = {
|
|
|
35
35
|
createdAt?: string;
|
|
36
36
|
updatedAt?: string;
|
|
37
37
|
};
|
|
38
|
-
export type ActivityAction = 'watcher_start' | 'watcher_stop' | 'error_detected' | 'error_deduplicated' | 'analysis_start' | 'analysis_complete' | 'analysis_failed' | 'analysis_timeout' | 'fix_start' | 'fix_complete' | 'fix_failed' | 'fix_timeout' | 'verification_start' | 'verification_pass' | 'verification_fail' | 'error_ignored' | 'lock_acquired' | 'lock_released' | 'lock_expired' | 'stale_recovery';
|
|
38
|
+
export type ActivityAction = 'watcher_start' | 'watcher_stop' | 'error_detected' | 'error_deduplicated' | 'analysis_start' | 'analysis_complete' | 'analysis_failed' | 'analysis_timeout' | 'already_fixed_detected' | 'fix_start' | 'fix_complete' | 'fix_failed' | 'fix_timeout' | 'verification_start' | 'verification_pass' | 'verification_fail' | 'error_ignored' | 'lock_acquired' | 'lock_released' | 'lock_expired' | 'stale_recovery';
|
|
39
39
|
export declare function insertError(db: Database, error: ErrorInsert): number;
|
|
40
40
|
export declare function getError(db: Database, id: number): ErrorRecord | null;
|
|
41
41
|
export declare function getErrorByHash(db: Database, hash: string): ErrorRecord | null;
|
package/dist/fixer/context.js
CHANGED
|
@@ -127,16 +127,24 @@ ${options.contextBlock}
|
|
|
127
127
|
|
|
128
128
|
## Instructions
|
|
129
129
|
|
|
130
|
-
1.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
1. **First, check if this issue still exists in the code**
|
|
131
|
+
- Look at the file(s) mentioned in the stack trace
|
|
132
|
+
- Determine if the error condition is still present
|
|
133
|
+
- If the code has been modified and the issue is gone, report it as already_fixed
|
|
134
|
+
|
|
135
|
+
2. If the issue still exists:
|
|
136
|
+
- Investigate the project structure to understand the codebase
|
|
137
|
+
- Identify the root cause of this error
|
|
138
|
+
- Determine what files need to be modified
|
|
139
|
+
- Assess your confidence in the fix
|
|
134
140
|
|
|
135
141
|
Write your analysis to: \`${analysisPath}\`
|
|
136
142
|
|
|
137
143
|
Use this exact YAML format:
|
|
138
144
|
\`\`\`yaml
|
|
139
|
-
|
|
145
|
+
already_fixed: true | false
|
|
146
|
+
summary: One sentence summary of the problem (or that it was already fixed)
|
|
147
|
+
# The following fields are only required if already_fixed is false:
|
|
140
148
|
root_cause: |
|
|
141
149
|
Detailed explanation of root cause
|
|
142
150
|
Can be multiple lines
|
|
@@ -153,6 +161,7 @@ confidence: high | medium | low
|
|
|
153
161
|
- Do NOT modify any files during analysis
|
|
154
162
|
- If you cannot determine the cause, set confidence to "low"
|
|
155
163
|
- Be specific about file paths relative to project root
|
|
164
|
+
- Set already_fixed to true if the issue no longer exists in the code (e.g., fixed by a previous error fix)
|
|
156
165
|
- WARNING: If a fix fails and is retried, any file modifications from previous attempts will persist
|
|
157
166
|
`;
|
|
158
167
|
};
|
package/dist/fixer/index.js
CHANGED
|
@@ -39,13 +39,34 @@ const parseSuggestion = (value) => {
|
|
|
39
39
|
const parsed = JSON.parse(value);
|
|
40
40
|
if (!parsed ||
|
|
41
41
|
typeof parsed.summary !== 'string' ||
|
|
42
|
-
typeof parsed.root_cause !== 'string' ||
|
|
43
|
-
typeof parsed.suggested_fix !== 'string' ||
|
|
44
|
-
!Array.isArray(parsed.files_to_modify) ||
|
|
45
42
|
typeof parsed.confidence !== 'string') {
|
|
46
43
|
throw new UserError('Stored analysis output is invalid');
|
|
47
44
|
}
|
|
48
|
-
|
|
45
|
+
// For already_fixed analyses, other fields may be empty
|
|
46
|
+
if (parsed.already_fixed === true) {
|
|
47
|
+
return {
|
|
48
|
+
already_fixed: true,
|
|
49
|
+
summary: parsed.summary,
|
|
50
|
+
root_cause: parsed.root_cause ?? '',
|
|
51
|
+
suggested_fix: parsed.suggested_fix ?? '',
|
|
52
|
+
files_to_modify: parsed.files_to_modify ?? [],
|
|
53
|
+
confidence: parsed.confidence,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// For regular analyses, ensure all fields are present
|
|
57
|
+
if (typeof parsed.root_cause !== 'string' ||
|
|
58
|
+
typeof parsed.suggested_fix !== 'string' ||
|
|
59
|
+
!Array.isArray(parsed.files_to_modify)) {
|
|
60
|
+
throw new UserError('Stored analysis output is invalid');
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
already_fixed: parsed.already_fixed ?? false,
|
|
64
|
+
summary: parsed.summary,
|
|
65
|
+
root_cause: parsed.root_cause,
|
|
66
|
+
suggested_fix: parsed.suggested_fix,
|
|
67
|
+
files_to_modify: parsed.files_to_modify,
|
|
68
|
+
confidence: parsed.confidence,
|
|
69
|
+
};
|
|
49
70
|
};
|
|
50
71
|
export class FixOrchestrator {
|
|
51
72
|
db;
|
|
@@ -140,6 +161,30 @@ export class FixOrchestrator {
|
|
|
140
161
|
const analysisResult = await this.processAgentOutput(agentResult, outputPath, parseAnalysisOutput);
|
|
141
162
|
if (analysisResult.success) {
|
|
142
163
|
analysisOutput = analysisResult.data;
|
|
164
|
+
if (analysisOutput.already_fixed) {
|
|
165
|
+
if (!transitionStatus(this.db, errorId, 'analyzing', 'resolved', lockId)) {
|
|
166
|
+
throw new UserError(`Failed to transition error ${errorId} into resolved status`);
|
|
167
|
+
}
|
|
168
|
+
this.db.run('UPDATE errors SET suggestion = ?, updated_at = ? WHERE id = ?', [
|
|
169
|
+
JSON.stringify(analysisOutput),
|
|
170
|
+
new Date().toISOString(),
|
|
171
|
+
errorId,
|
|
172
|
+
]);
|
|
173
|
+
logActivity(this.db, 'already_fixed_detected', errorId, JSON.stringify({
|
|
174
|
+
attempt: attempts,
|
|
175
|
+
summary: analysisOutput.summary,
|
|
176
|
+
}));
|
|
177
|
+
await releaseLock(this.db, errorId, lockId);
|
|
178
|
+
lockHeld = false;
|
|
179
|
+
return {
|
|
180
|
+
errorId,
|
|
181
|
+
status: 'resolved',
|
|
182
|
+
lockAcquired: true,
|
|
183
|
+
attempts,
|
|
184
|
+
analysis: analysisOutput,
|
|
185
|
+
message: 'Issue already fixed',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
143
188
|
if (!transitionStatus(this.db, errorId, 'analyzing', 'suggested', lockId)) {
|
|
144
189
|
throw new UserError(`Failed to transition error ${errorId} into suggested status`);
|
|
145
190
|
}
|
package/dist/fixer/output.d.ts
CHANGED
package/dist/fixer/output.js
CHANGED
|
@@ -69,12 +69,24 @@ const validateConfidence = (value) => {
|
|
|
69
69
|
const parseAnalysisOutput = (content) => {
|
|
70
70
|
const raw = parseYaml(content);
|
|
71
71
|
const data = assertRecord(raw);
|
|
72
|
+
const already_fixed = data.already_fixed === true;
|
|
72
73
|
const summary = requireStringField(data, 'summary');
|
|
74
|
+
const confidence = validateConfidence(data.confidence);
|
|
75
|
+
if (already_fixed) {
|
|
76
|
+
return {
|
|
77
|
+
already_fixed: true,
|
|
78
|
+
summary,
|
|
79
|
+
root_cause: '',
|
|
80
|
+
suggested_fix: '',
|
|
81
|
+
files_to_modify: [],
|
|
82
|
+
confidence,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
73
85
|
const root_cause = requireStringField(data, 'root_cause');
|
|
74
86
|
const suggested_fix = requireStringField(data, 'suggested_fix');
|
|
75
87
|
const files_to_modify = requireStringArrayField(data, 'files_to_modify');
|
|
76
|
-
const confidence = validateConfidence(data.confidence);
|
|
77
88
|
return {
|
|
89
|
+
already_fixed: false,
|
|
78
90
|
summary,
|
|
79
91
|
root_cause,
|
|
80
92
|
suggested_fix,
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare class InternalError extends Error {
|
|
|
6
6
|
cause?: unknown;
|
|
7
7
|
});
|
|
8
8
|
}
|
|
9
|
-
export type ErrorStatus = 'pending' | 'analyzing' | 'suggested' | 'fixing' | 'fixed' | 'failed' | 'ignored';
|
|
9
|
+
export type ErrorStatus = 'pending' | 'analyzing' | 'suggested' | 'fixing' | 'fixed' | 'failed' | 'ignored' | 'resolved';
|
|
10
10
|
export declare const EXIT_CODES: {
|
|
11
11
|
readonly SUCCESS: 0;
|
|
12
12
|
readonly GENERAL_ERROR: 1;
|