watchfix 0.2.0 → 0.2.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/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 |
@@ -175,6 +175,14 @@ const formatFixOutcome = (result, verbosity, maxAttempts) => {
175
175
  }
176
176
  else if (!result.fix?.success) {
177
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
+ }
178
186
  }
179
187
  else {
180
188
  lines.push(`Error #${result.errorId}: Fix attempt completed`);
@@ -259,6 +259,13 @@ declare const configSchema: z.ZodObject<{
259
259
  context_max_age_days?: number | undefined;
260
260
  context_max_size_kb?: number | undefined;
261
261
  }>>;
262
+ deduplication: z.ZodDefault<z.ZodObject<{
263
+ fixed_grace_period: z.ZodDefault<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
264
+ }, "strip", z.ZodTypeAny, {
265
+ fixed_grace_period: string;
266
+ }, {
267
+ fixed_grace_period?: string | undefined;
268
+ }>>;
262
269
  patterns: z.ZodDefault<z.ZodObject<{
263
270
  match: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
264
271
  ignore: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
@@ -315,6 +322,9 @@ declare const configSchema: z.ZodObject<{
315
322
  context_max_age_days: number;
316
323
  context_max_size_kb: number;
317
324
  };
325
+ deduplication: {
326
+ fixed_grace_period: string;
327
+ };
318
328
  patterns: {
319
329
  ignore: string[];
320
330
  match: string[];
@@ -365,6 +375,9 @@ declare const configSchema: z.ZodObject<{
365
375
  context_max_age_days?: number | undefined;
366
376
  context_max_size_kb?: number | undefined;
367
377
  } | undefined;
378
+ deduplication?: {
379
+ fixed_grace_period?: string | undefined;
380
+ } | undefined;
368
381
  patterns?: {
369
382
  ignore?: string[] | undefined;
370
383
  match?: string[] | undefined;
@@ -89,6 +89,11 @@ const configSchema = z.object({
89
89
  context_max_size_kb: z.number().int().min(64).default(256),
90
90
  })
91
91
  .default({}),
92
+ deduplication: z
93
+ .object({
94
+ fixed_grace_period: durationSchema.default('10m'),
95
+ })
96
+ .default({}),
92
97
  patterns: z
93
98
  .object({
94
99
  match: z.array(patternSchema).default([]),
@@ -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;
@@ -246,6 +246,7 @@ export class FixOrchestrator {
246
246
  lockAcquired: true,
247
247
  attempts,
248
248
  message: 'Analysis failed',
249
+ diagnostic: analysisResult.diagnostic,
249
250
  };
250
251
  }
251
252
  }
@@ -348,6 +349,7 @@ export class FixOrchestrator {
348
349
  attempts,
349
350
  analysis: analysisOutput,
350
351
  message: 'Fix failed',
352
+ diagnostic: fixResult.diagnostic,
351
353
  };
352
354
  }
353
355
  logActivity(this.db, 'verification_start', errorId, JSON.stringify({ attempt: attempts }));
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { getErrorByHash, insertError, logActivity } from '../db/queries.js';
3
+ import { parseDuration } from '../utils/duration.js';
3
4
  import { Logger } from '../utils/logger.js';
4
5
  import { ErrorParser } from './parser.js';
5
6
  import { CommandSource } from './sources/command.js';
@@ -186,6 +187,22 @@ export class WatcherOrchestrator {
186
187
  });
187
188
  return;
188
189
  }
190
+ // Deduplicate fixed errors within grace period to prevent re-detection
191
+ // after a fix when the log file is re-read
192
+ if (existing && existing.status === 'fixed') {
193
+ const gracePeriodMs = parseDuration(this.config.deduplication.fixed_grace_period);
194
+ const fixedAt = new Date(existing.updatedAt).getTime();
195
+ if (Date.now() - fixedAt < gracePeriodMs) {
196
+ logActivity(this.db, 'error_deduplicated', existing.id, `status=${existing.status} grace_period=true`);
197
+ this.logger.info(`Deduplicated error ${error.hash} (within grace period after fix)`);
198
+ this.emitter.emit('error_deduplicated', {
199
+ errorId: existing.id,
200
+ error,
201
+ status: existing.status,
202
+ });
203
+ return;
204
+ }
205
+ }
189
206
  const newId = insertError(this.db, {
190
207
  hash: error.hash,
191
208
  source: error.source,
@@ -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;
@@ -125,14 +125,12 @@ export class FileSource {
125
125
  }
126
126
  const inode = typeof stats.ino === 'number' ? stats.ino : null;
127
127
  const mtimeMs = stats.mtimeMs;
128
- const shouldForceRead = this.forceRead;
129
128
  this.forceRead = false;
130
129
  const inodeChanged = this.lastInode !== null && inode !== null && inode !== this.lastInode;
131
- const mtimeChanged = this.lastMtimeMs !== null && mtimeMs !== this.lastMtimeMs;
132
- if (inodeChanged ||
133
- stats.size < this.position ||
134
- (shouldForceRead && stats.size === this.position && this.position > 0) ||
135
- (mtimeChanged && stats.size === this.position && this.position > 0)) {
130
+ // Only reset position on actual file replacement (inode change) or truncation (size shrunk)
131
+ // Do NOT reset on mtime changes without content changes - this prevents re-reading
132
+ // old errors when the file is touched but no new content is added
133
+ if (inodeChanged || stats.size < this.position) {
136
134
  this.position = 0;
137
135
  this.partialLine = '';
138
136
  }
package/package.json CHANGED
@@ -1,7 +1,21 @@
1
1
  {
2
2
  "name": "watchfix",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "CLI tool that watches logs, detects errors, and dispatches AI agents to fix them",
5
+ "keywords": [
6
+ "cli",
7
+ "log-watcher",
8
+ "error-detection",
9
+ "ai",
10
+ "ai-agent",
11
+ "automation",
12
+ "auto-fix",
13
+ "llm",
14
+ "devtools",
15
+ "monitoring",
16
+ "logs",
17
+ "debugging"
18
+ ],
5
19
  "type": "module",
6
20
  "bin": {
7
21
  "watchfix": "./dist/cli/index.js"