watchfix 0.1.0 → 0.2.1
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 +19 -1
- package/dist/cli/commands/fix.js +172 -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.d.ts +1 -0
- package/dist/fixer/index.js +51 -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/dist/watcher/parser.js +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,12 +64,28 @@ watchfix watch
|
|
|
64
64
|
watchfix fix <error-id>
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
## Autonomous Mode
|
|
68
|
+
|
|
69
|
+
For fully automated error fixing without manual approval:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
watchfix watch --autonomous
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
In autonomous mode, watchfix automatically dispatches AI agents to fix detected errors. Combine with daemon mode for background operation (Linux/macOS):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
watchfix watch --daemon --autonomous
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Note:** Manual `watchfix fix` commands are blocked while running in autonomous mode.
|
|
82
|
+
|
|
67
83
|
## CLI Commands
|
|
68
84
|
|
|
69
85
|
| Command | Description |
|
|
70
86
|
|---------|-------------|
|
|
71
87
|
| `watchfix init` | Create `watchfix.yaml` in current directory |
|
|
72
|
-
| `watchfix watch` | Watch logs in foreground (use `--daemon` for background) |
|
|
88
|
+
| `watchfix watch` | Watch logs in foreground (use `--daemon` for background, `--autonomous` for auto-fix) |
|
|
73
89
|
| `watchfix fix [id]` | Analyze and fix a specific error (or `--all` for all pending) |
|
|
74
90
|
| `watchfix show <id>` | Show full error details and analysis |
|
|
75
91
|
| `watchfix status` | Show watcher state and pending errors |
|
|
@@ -94,6 +110,8 @@ watchfix fix <error-id>
|
|
|
94
110
|
|
|
95
111
|
For detailed configuration options and advanced usage, see the [specification document](./spec/watchfix-spec-v8.md).
|
|
96
112
|
|
|
113
|
+
For a working example project, see [watchfix-example](https://github.com/CaseyHaralson/watchfix-example).
|
|
114
|
+
|
|
97
115
|
## License
|
|
98
116
|
|
|
99
117
|
[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,98 @@ 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
|
+
if (result.diagnostic) {
|
|
179
|
+
// Extract key info from diagnostic
|
|
180
|
+
const diagLines = result.diagnostic.split('\n');
|
|
181
|
+
const reason = diagLines.find(l => l.includes('timed out') || l.includes('Exit code'));
|
|
182
|
+
if (reason) {
|
|
183
|
+
lines.push(` Reason: ${reason.trim()}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
lines.push(`Error #${result.errorId}: Fix attempt completed`);
|
|
189
|
+
}
|
|
190
|
+
// Agent details
|
|
191
|
+
if (result.fix) {
|
|
192
|
+
lines.push(` Agent applied fix: ${result.fix.success ? 'yes' : 'no'}`);
|
|
193
|
+
if (result.fix.files_changed?.length) {
|
|
194
|
+
lines.push(' Files changed:');
|
|
195
|
+
lines.push(...formatFilesChanged(result.fix.files_changed, ' '));
|
|
196
|
+
}
|
|
197
|
+
if (result.fix.notes && (verbosity === 'verbose' || !result.fix.success)) {
|
|
198
|
+
lines.push(` Agent notes: ${result.fix.notes}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Verification
|
|
202
|
+
if (result.verification) {
|
|
203
|
+
lines.push(...formatVerificationSummary(result.verification, verbosity));
|
|
204
|
+
}
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push(formatStatusLine(result, maxAttempts));
|
|
207
|
+
return lines;
|
|
95
208
|
};
|
|
96
209
|
const checkDaemonConflict = (db, logger) => {
|
|
97
210
|
const state = getWatcherState(db);
|
|
@@ -138,20 +251,23 @@ const reportAnalysisFromResult = (db, errorId, result, note) => {
|
|
|
138
251
|
parseStoredAnalysis(getError(db, errorId)?.suggestion ?? null);
|
|
139
252
|
reportAnalysis(errorId, analysis, note);
|
|
140
253
|
};
|
|
141
|
-
const reportFixResult = (result) => {
|
|
254
|
+
const reportFixResult = (result, verbosity = 'normal', maxAttempts = 3) => {
|
|
142
255
|
if (!result.lockAcquired) {
|
|
143
256
|
process.stdout.write(`Skipped error #${result.errorId}: already locked by another process.\n`);
|
|
144
257
|
return 'skipped';
|
|
145
258
|
}
|
|
259
|
+
const lines = formatFixOutcome(result, verbosity, maxAttempts);
|
|
260
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
146
261
|
if (result.status === 'fixed') {
|
|
147
|
-
process.stdout.write(`✓ Error #${result.errorId} fixed successfully.\n`);
|
|
148
262
|
return 'fixed';
|
|
149
263
|
}
|
|
150
|
-
|
|
151
|
-
|
|
264
|
+
if (result.status === 'resolved') {
|
|
265
|
+
return 'resolved';
|
|
266
|
+
}
|
|
152
267
|
return 'failed';
|
|
153
268
|
};
|
|
154
|
-
const runSingleFix = async (db, error, options, orchestrator) => {
|
|
269
|
+
const runSingleFix = async (db, error, options, orchestrator, ctx) => {
|
|
270
|
+
const { verbosity, maxAttempts } = ctx;
|
|
155
271
|
const reanalyze = Boolean(options.reanalyze);
|
|
156
272
|
const analyzeOnly = Boolean(options.analyzeOnly);
|
|
157
273
|
const shouldPrompt = !options.yes && !analyzeOnly;
|
|
@@ -163,11 +279,11 @@ const runSingleFix = async (db, error, options, orchestrator) => {
|
|
|
163
279
|
});
|
|
164
280
|
if (!result.lockAcquired) {
|
|
165
281
|
process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
|
|
166
|
-
return reportFixResult(result);
|
|
282
|
+
return reportFixResult(result, verbosity, maxAttempts);
|
|
167
283
|
}
|
|
168
284
|
if (result.status !== 'suggested') {
|
|
169
285
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
170
|
-
reportFixResult(result);
|
|
286
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
171
287
|
return 'failed';
|
|
172
288
|
}
|
|
173
289
|
reportAnalysis(error.id, result.analysis ??
|
|
@@ -184,7 +300,7 @@ const runSingleFix = async (db, error, options, orchestrator) => {
|
|
|
184
300
|
});
|
|
185
301
|
if (!analysisResult.lockAcquired) {
|
|
186
302
|
process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
|
|
187
|
-
return reportFixResult(analysisResult);
|
|
303
|
+
return reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
188
304
|
}
|
|
189
305
|
analysisNote = analysisResult.message;
|
|
190
306
|
analysis =
|
|
@@ -192,7 +308,7 @@ const runSingleFix = async (db, error, options, orchestrator) => {
|
|
|
192
308
|
parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null);
|
|
193
309
|
if (analysisResult.status !== 'suggested') {
|
|
194
310
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
195
|
-
return reportFixResult(analysisResult);
|
|
311
|
+
return reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
196
312
|
}
|
|
197
313
|
reanalyzeForFix = false;
|
|
198
314
|
}
|
|
@@ -213,15 +329,16 @@ const runSingleFix = async (db, error, options, orchestrator) => {
|
|
|
213
329
|
});
|
|
214
330
|
if (!result.lockAcquired) {
|
|
215
331
|
process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
|
|
216
|
-
return reportFixResult(result);
|
|
332
|
+
return reportFixResult(result, verbosity, maxAttempts);
|
|
217
333
|
}
|
|
218
334
|
reportAnalysisFromResult(db, error.id, result);
|
|
219
|
-
if (result.status !== 'fixed') {
|
|
335
|
+
if (result.status !== 'fixed' && result.status !== 'resolved') {
|
|
220
336
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
221
337
|
}
|
|
222
|
-
return reportFixResult(result);
|
|
338
|
+
return reportFixResult(result, verbosity, maxAttempts);
|
|
223
339
|
};
|
|
224
|
-
const runAllFixes = async (db, options, orchestrator) => {
|
|
340
|
+
const runAllFixes = async (db, options, orchestrator, ctx) => {
|
|
341
|
+
const { verbosity, maxAttempts } = ctx;
|
|
225
342
|
const errors = getErrorsByStatus(db, FIXABLE_STATUSES);
|
|
226
343
|
if (errors.length === 0) {
|
|
227
344
|
process.stdout.write('No pending or suggested errors to fix.\n');
|
|
@@ -234,6 +351,7 @@ const runAllFixes = async (db, options, orchestrator) => {
|
|
|
234
351
|
let failedCount = 0;
|
|
235
352
|
let skippedCount = 0;
|
|
236
353
|
let analyzedCount = 0;
|
|
354
|
+
let resolvedCount = 0;
|
|
237
355
|
for (const error of errors) {
|
|
238
356
|
const fixabilityIssue = ensureFixable(error);
|
|
239
357
|
if (fixabilityIssue) {
|
|
@@ -248,12 +366,17 @@ const runAllFixes = async (db, options, orchestrator) => {
|
|
|
248
366
|
});
|
|
249
367
|
if (!result.lockAcquired) {
|
|
250
368
|
skippedCount += 1;
|
|
251
|
-
reportFixResult(result);
|
|
369
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (result.status === 'resolved') {
|
|
373
|
+
resolvedCount += 1;
|
|
374
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
252
375
|
continue;
|
|
253
376
|
}
|
|
254
377
|
if (result.status !== 'suggested') {
|
|
255
378
|
failedCount += 1;
|
|
256
|
-
reportFixResult(result);
|
|
379
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
257
380
|
continue;
|
|
258
381
|
}
|
|
259
382
|
reportAnalysis(error.id, result.analysis ?? parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null), result.message);
|
|
@@ -271,16 +394,21 @@ const runAllFixes = async (db, options, orchestrator) => {
|
|
|
271
394
|
});
|
|
272
395
|
if (!analysisResult.lockAcquired) {
|
|
273
396
|
skippedCount += 1;
|
|
274
|
-
reportFixResult(analysisResult);
|
|
397
|
+
reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
275
398
|
continue;
|
|
276
399
|
}
|
|
277
400
|
note = analysisResult.message;
|
|
278
401
|
analysis =
|
|
279
402
|
analysisResult.analysis ??
|
|
280
403
|
parseStoredAnalysis(getError(db, error.id)?.suggestion ?? null);
|
|
404
|
+
if (analysisResult.status === 'resolved') {
|
|
405
|
+
resolvedCount += 1;
|
|
406
|
+
reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
281
409
|
if (analysisResult.status !== 'suggested') {
|
|
282
410
|
failedCount += 1;
|
|
283
|
-
reportFixResult(analysisResult);
|
|
411
|
+
reportFixResult(analysisResult, verbosity, maxAttempts);
|
|
284
412
|
continue;
|
|
285
413
|
}
|
|
286
414
|
reanalyzeForFix = false;
|
|
@@ -302,36 +430,43 @@ const runAllFixes = async (db, options, orchestrator) => {
|
|
|
302
430
|
});
|
|
303
431
|
if (!result.lockAcquired) {
|
|
304
432
|
skippedCount += 1;
|
|
305
|
-
reportFixResult(result);
|
|
433
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
306
434
|
continue;
|
|
307
435
|
}
|
|
308
436
|
if (result.status === 'fixed') {
|
|
309
437
|
fixedCount += 1;
|
|
310
438
|
}
|
|
439
|
+
else if (result.status === 'resolved') {
|
|
440
|
+
resolvedCount += 1;
|
|
441
|
+
}
|
|
311
442
|
else {
|
|
312
443
|
failedCount += 1;
|
|
313
444
|
}
|
|
314
|
-
reportFixResult(result);
|
|
445
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
315
446
|
continue;
|
|
316
447
|
}
|
|
317
448
|
const result = await orchestrator.fixError(error.id, { reanalyze });
|
|
318
449
|
if (!result.lockAcquired) {
|
|
319
450
|
skippedCount += 1;
|
|
320
|
-
reportFixResult(result);
|
|
451
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
321
452
|
continue;
|
|
322
453
|
}
|
|
323
454
|
reportAnalysisFromResult(db, error.id, result);
|
|
324
455
|
if (result.status === 'fixed') {
|
|
325
456
|
fixedCount += 1;
|
|
326
457
|
}
|
|
458
|
+
else if (result.status === 'resolved') {
|
|
459
|
+
resolvedCount += 1;
|
|
460
|
+
}
|
|
327
461
|
else {
|
|
328
462
|
failedCount += 1;
|
|
329
463
|
}
|
|
330
|
-
reportFixResult(result);
|
|
464
|
+
reportFixResult(result, verbosity, maxAttempts);
|
|
331
465
|
}
|
|
466
|
+
const resolvedSuffix = resolvedCount > 0 ? `, resolved ${resolvedCount}` : '';
|
|
332
467
|
const summary = analyzeOnly
|
|
333
|
-
? `Analyzed ${analyzedCount} errors, failed ${failedCount}, skipped ${skippedCount}.`
|
|
334
|
-
: `Summary: fixed ${fixedCount}, failed ${failedCount}, skipped ${skippedCount}.`;
|
|
468
|
+
? `Analyzed ${analyzedCount} errors, failed ${failedCount}, skipped ${skippedCount}${resolvedSuffix}.`
|
|
469
|
+
: `Summary: fixed ${fixedCount}, failed ${failedCount}, skipped ${skippedCount}${resolvedSuffix}.`;
|
|
335
470
|
process.stdout.write(`${summary}\n`);
|
|
336
471
|
if (!analyzeOnly && failedCount > 0) {
|
|
337
472
|
process.exitCode = EXIT_CODES.GENERAL_ERROR;
|
|
@@ -366,8 +501,12 @@ export const fixCommand = async (id, options) => {
|
|
|
366
501
|
logger,
|
|
367
502
|
terminalEnabled: true,
|
|
368
503
|
});
|
|
504
|
+
const ctx = {
|
|
505
|
+
verbosity,
|
|
506
|
+
maxAttempts: config.limits.max_attempts_per_error,
|
|
507
|
+
};
|
|
369
508
|
if (options.all) {
|
|
370
|
-
await runAllFixes(db, options, orchestrator);
|
|
509
|
+
await runAllFixes(db, options, orchestrator, ctx);
|
|
371
510
|
return;
|
|
372
511
|
}
|
|
373
512
|
const errorId = parsePositiveInt(id);
|
|
@@ -383,7 +522,7 @@ export const fixCommand = async (id, options) => {
|
|
|
383
522
|
process.exitCode = EXIT_CODES.NOT_ACTIONABLE;
|
|
384
523
|
return;
|
|
385
524
|
}
|
|
386
|
-
await runSingleFix(db, error, options, orchestrator);
|
|
525
|
+
await runSingleFix(db, error, options, orchestrator, ctx);
|
|
387
526
|
}
|
|
388
527
|
finally {
|
|
389
528
|
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.d.ts
CHANGED
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
|
}
|
|
@@ -201,6 +246,7 @@ export class FixOrchestrator {
|
|
|
201
246
|
lockAcquired: true,
|
|
202
247
|
attempts,
|
|
203
248
|
message: 'Analysis failed',
|
|
249
|
+
diagnostic: analysisResult.diagnostic,
|
|
204
250
|
};
|
|
205
251
|
}
|
|
206
252
|
}
|
|
@@ -303,6 +349,7 @@ export class FixOrchestrator {
|
|
|
303
349
|
attempts,
|
|
304
350
|
analysis: analysisOutput,
|
|
305
351
|
message: 'Fix failed',
|
|
352
|
+
diagnostic: fixResult.diagnostic,
|
|
306
353
|
};
|
|
307
354
|
}
|
|
308
355
|
logActivity(this.db, 'verification_start', errorId, JSON.stringify({ attempt: attempts }));
|
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;
|
package/dist/watcher/parser.js
CHANGED
|
@@ -11,6 +11,14 @@ const CONTINUATION_PATTERNS = [
|
|
|
11
11
|
/^\s+\.\.\./,
|
|
12
12
|
];
|
|
13
13
|
const isContinuationLine = (line) => CONTINUATION_PATTERNS.some((pattern) => pattern.test(line));
|
|
14
|
+
const extractCoreMessage = (line) => {
|
|
15
|
+
// "TypeError: Cannot read..." → "Cannot read..."
|
|
16
|
+
const colonIndex = line.indexOf(': ');
|
|
17
|
+
if (colonIndex > 0 && colonIndex < 30) {
|
|
18
|
+
return line.slice(colonIndex + 2).trim();
|
|
19
|
+
}
|
|
20
|
+
return line.trim();
|
|
21
|
+
};
|
|
14
22
|
const truncateLine = (line) => {
|
|
15
23
|
if (line.length <= MAX_LINE_LENGTH) {
|
|
16
24
|
return line;
|
|
@@ -58,6 +66,21 @@ export class ErrorParser {
|
|
|
58
66
|
return;
|
|
59
67
|
}
|
|
60
68
|
if (isError) {
|
|
69
|
+
// Check if this is the same error with a more specific type
|
|
70
|
+
if (this.current && this.current.stackLines.length === 0) {
|
|
71
|
+
const currentCore = extractCoreMessage(this.current.message);
|
|
72
|
+
const newCore = extractCoreMessage(line);
|
|
73
|
+
if (currentCore === newCore) {
|
|
74
|
+
// Same error - discard shallow entry, use the more detailed one
|
|
75
|
+
if (this.flushTimer) {
|
|
76
|
+
clearTimeout(this.flushTimer);
|
|
77
|
+
this.flushTimer = undefined;
|
|
78
|
+
}
|
|
79
|
+
this.current = undefined;
|
|
80
|
+
this.startError(event, line);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
61
84
|
await this.flushCurrent('new_error');
|
|
62
85
|
this.startError(event, line);
|
|
63
86
|
return;
|