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 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)
@@ -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
- return parsed;
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 formatFixFailure = (result) => {
91
- if (result.verification?.failure) {
92
- return result.verification.failure.message;
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
- return result.message ?? 'Fix did not complete successfully.';
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
- const message = formatFixFailure(result);
151
- process.stdout.write(`✗ Error #${result.errorId} not fixed (status=${result.status}).\n${message}\n`);
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
  }
@@ -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;
@@ -127,16 +127,24 @@ ${options.contextBlock}
127
127
 
128
128
  ## Instructions
129
129
 
130
- 1. Investigate the project structure to understand the codebase
131
- 2. Identify the root cause of this error
132
- 3. Determine what files need to be modified
133
- 4. Assess your confidence in the fix
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
- summary: One sentence summary of the problem
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
  };
@@ -18,6 +18,7 @@ export type FixResult = {
18
18
  fix?: FixOutput;
19
19
  verification?: VerificationResult;
20
20
  message?: string;
21
+ diagnostic?: string;
21
22
  };
22
23
  type FixOrchestratorOptions = {
23
24
  agent?: Agent;
@@ -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
- return parsed;
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 }));
@@ -1,5 +1,6 @@
1
1
  type ConfidenceLevel = 'high' | 'medium' | 'low';
2
2
  type AnalysisOutput = {
3
+ already_fixed: boolean;
3
4
  summary: string;
4
5
  root_cause: string;
5
6
  suggested_fix: string;
@@ -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,
@@ -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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "watchfix",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI tool that watches logs, detects errors, and dispatches AI agents to fix them",
5
5
  "type": "module",
6
6
  "bin": {