watchfix 0.1.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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/agents/base.d.ts +19 -0
- package/dist/agents/base.js +140 -0
- package/dist/agents/claude.d.ts +11 -0
- package/dist/agents/claude.js +6 -0
- package/dist/agents/codex.d.ts +11 -0
- package/dist/agents/codex.js +6 -0
- package/dist/agents/defaults.d.ts +21 -0
- package/dist/agents/defaults.js +21 -0
- package/dist/agents/gemini.d.ts +11 -0
- package/dist/agents/gemini.js +6 -0
- package/dist/agents/index.d.ts +19 -0
- package/dist/agents/index.js +63 -0
- package/dist/agents/types.d.ts +22 -0
- package/dist/agents/types.js +1 -0
- package/dist/cli/commands/clean.d.ts +9 -0
- package/dist/cli/commands/clean.js +173 -0
- package/dist/cli/commands/config.d.ts +7 -0
- package/dist/cli/commands/config.js +134 -0
- package/dist/cli/commands/fix.d.ts +12 -0
- package/dist/cli/commands/fix.js +391 -0
- package/dist/cli/commands/ignore.d.ts +7 -0
- package/dist/cli/commands/ignore.js +74 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.js +115 -0
- package/dist/cli/commands/logs.d.ts +9 -0
- package/dist/cli/commands/logs.js +106 -0
- package/dist/cli/commands/show.d.ts +8 -0
- package/dist/cli/commands/show.js +165 -0
- package/dist/cli/commands/status.d.ts +7 -0
- package/dist/cli/commands/status.js +110 -0
- package/dist/cli/commands/stop.d.ts +7 -0
- package/dist/cli/commands/stop.js +106 -0
- package/dist/cli/commands/version.d.ts +7 -0
- package/dist/cli/commands/version.js +36 -0
- package/dist/cli/commands/watch.d.ts +10 -0
- package/dist/cli/commands/watch.js +204 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +152 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.js +96 -0
- package/dist/config/schema.d.ts +375 -0
- package/dist/config/schema.js +99 -0
- package/dist/db/index.d.ts +15 -0
- package/dist/db/index.js +71 -0
- package/dist/db/queries.d.ts +45 -0
- package/dist/db/queries.js +111 -0
- package/dist/db/schema.d.ts +4 -0
- package/dist/db/schema.js +84 -0
- package/dist/fixer/context.d.ts +9 -0
- package/dist/fixer/context.js +361 -0
- package/dist/fixer/index.d.ts +37 -0
- package/dist/fixer/index.js +398 -0
- package/dist/fixer/lock.d.ts +7 -0
- package/dist/fixer/lock.js +49 -0
- package/dist/fixer/output.d.ts +21 -0
- package/dist/fixer/output.js +108 -0
- package/dist/fixer/queue.d.ts +15 -0
- package/dist/fixer/queue.js +53 -0
- package/dist/fixer/verifier.d.ts +44 -0
- package/dist/fixer/verifier.js +133 -0
- package/dist/utils/daemon.d.ts +22 -0
- package/dist/utils/daemon.js +143 -0
- package/dist/utils/duration.d.ts +2 -0
- package/dist/utils/duration.js +31 -0
- package/dist/utils/errors.d.ts +17 -0
- package/dist/utils/errors.js +20 -0
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.js +18 -0
- package/dist/utils/http.d.ts +6 -0
- package/dist/utils/http.js +61 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.js +85 -0
- package/dist/utils/process.d.ts +16 -0
- package/dist/utils/process.js +146 -0
- package/dist/watcher/index.d.ts +55 -0
- package/dist/watcher/index.js +234 -0
- package/dist/watcher/parser.d.ts +42 -0
- package/dist/watcher/parser.js +162 -0
- package/dist/watcher/patterns.d.ts +5 -0
- package/dist/watcher/patterns.js +92 -0
- package/dist/watcher/sources/command.d.ts +27 -0
- package/dist/watcher/sources/command.js +143 -0
- package/dist/watcher/sources/docker.d.ts +28 -0
- package/dist/watcher/sources/docker.js +183 -0
- package/dist/watcher/sources/file.d.ts +30 -0
- package/dist/watcher/sources/file.js +177 -0
- package/dist/watcher/sources/types.d.ts +27 -0
- package/dist/watcher/sources/types.js +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
function mapErrorRow(row) {
|
|
2
|
+
return {
|
|
3
|
+
id: row.id,
|
|
4
|
+
hash: row.hash,
|
|
5
|
+
source: row.source,
|
|
6
|
+
timestamp: row.timestamp,
|
|
7
|
+
errorType: row.error_type,
|
|
8
|
+
message: row.message,
|
|
9
|
+
stackTrace: row.stack_trace,
|
|
10
|
+
rawLog: row.raw_log,
|
|
11
|
+
status: row.status,
|
|
12
|
+
suggestion: row.suggestion,
|
|
13
|
+
fixResult: row.fix_result,
|
|
14
|
+
fixAttempts: row.fix_attempts,
|
|
15
|
+
lockedBy: row.locked_by,
|
|
16
|
+
lockedAt: row.locked_at,
|
|
17
|
+
createdAt: row.created_at,
|
|
18
|
+
updatedAt: row.updated_at,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const ERROR_SELECT_COLUMNS = `
|
|
22
|
+
id,
|
|
23
|
+
hash,
|
|
24
|
+
source,
|
|
25
|
+
timestamp,
|
|
26
|
+
error_type,
|
|
27
|
+
message,
|
|
28
|
+
stack_trace,
|
|
29
|
+
raw_log,
|
|
30
|
+
status,
|
|
31
|
+
suggestion,
|
|
32
|
+
fix_result,
|
|
33
|
+
fix_attempts,
|
|
34
|
+
locked_by,
|
|
35
|
+
locked_at,
|
|
36
|
+
created_at,
|
|
37
|
+
updated_at
|
|
38
|
+
`;
|
|
39
|
+
export function insertError(db, error) {
|
|
40
|
+
const now = new Date().toISOString();
|
|
41
|
+
const createdAt = error.createdAt ?? now;
|
|
42
|
+
const updatedAt = error.updatedAt ?? createdAt;
|
|
43
|
+
const status = error.status ?? 'pending';
|
|
44
|
+
const fixAttempts = error.fixAttempts ?? 0;
|
|
45
|
+
const result = db.run(`INSERT INTO errors (
|
|
46
|
+
hash,
|
|
47
|
+
source,
|
|
48
|
+
timestamp,
|
|
49
|
+
error_type,
|
|
50
|
+
message,
|
|
51
|
+
stack_trace,
|
|
52
|
+
raw_log,
|
|
53
|
+
status,
|
|
54
|
+
suggestion,
|
|
55
|
+
fix_result,
|
|
56
|
+
fix_attempts,
|
|
57
|
+
locked_by,
|
|
58
|
+
locked_at,
|
|
59
|
+
created_at,
|
|
60
|
+
updated_at
|
|
61
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
62
|
+
error.hash,
|
|
63
|
+
error.source,
|
|
64
|
+
error.timestamp,
|
|
65
|
+
error.errorType,
|
|
66
|
+
error.message,
|
|
67
|
+
error.stackTrace ?? null,
|
|
68
|
+
error.rawLog,
|
|
69
|
+
status,
|
|
70
|
+
error.suggestion ?? null,
|
|
71
|
+
error.fixResult ?? null,
|
|
72
|
+
fixAttempts,
|
|
73
|
+
error.lockedBy ?? null,
|
|
74
|
+
error.lockedAt ?? null,
|
|
75
|
+
createdAt,
|
|
76
|
+
updatedAt,
|
|
77
|
+
]);
|
|
78
|
+
return Number(result.lastInsertRowid);
|
|
79
|
+
}
|
|
80
|
+
export function getError(db, id) {
|
|
81
|
+
const row = db.get(`SELECT ${ERROR_SELECT_COLUMNS} FROM errors WHERE id = ?`, [id]);
|
|
82
|
+
if (!row) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return mapErrorRow(row);
|
|
86
|
+
}
|
|
87
|
+
export function getErrorByHash(db, hash) {
|
|
88
|
+
const row = db.get(`SELECT ${ERROR_SELECT_COLUMNS} FROM errors WHERE hash = ? ORDER BY created_at DESC LIMIT 1`, [hash]);
|
|
89
|
+
if (!row) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return mapErrorRow(row);
|
|
93
|
+
}
|
|
94
|
+
export function updateErrorStatus(db, id, status) {
|
|
95
|
+
const result = db.run('UPDATE errors SET status = ?, updated_at = ? WHERE id = ?', [status, new Date().toISOString(), id]);
|
|
96
|
+
return result.changes > 0;
|
|
97
|
+
}
|
|
98
|
+
export function getErrorsByStatus(db, statuses) {
|
|
99
|
+
if (statuses.length === 0) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
const placeholders = statuses.map(() => '?').join(', ');
|
|
103
|
+
const rows = db.all(`SELECT ${ERROR_SELECT_COLUMNS} FROM errors WHERE status IN (${placeholders}) ORDER BY created_at ASC`, statuses);
|
|
104
|
+
return rows.map(mapErrorRow);
|
|
105
|
+
}
|
|
106
|
+
export function getPendingErrors(db) {
|
|
107
|
+
return getErrorsByStatus(db, ['pending']);
|
|
108
|
+
}
|
|
109
|
+
export function logActivity(db, action, errorId, details) {
|
|
110
|
+
db.run('INSERT INTO activity_log (timestamp, action, error_id, details) VALUES (?, ?, ?, ?)', [new Date().toISOString(), action, errorId ?? null, details ?? null]);
|
|
111
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { EXIT_CODES } from '../utils/errors.js';
|
|
2
|
+
export const SCHEMA_VERSION = 1;
|
|
3
|
+
const CREATE_ERRORS_TABLE = `
|
|
4
|
+
CREATE TABLE IF NOT EXISTS errors (
|
|
5
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
+
hash TEXT NOT NULL,
|
|
7
|
+
source TEXT NOT NULL,
|
|
8
|
+
timestamp TEXT NOT NULL,
|
|
9
|
+
error_type TEXT NOT NULL,
|
|
10
|
+
message TEXT NOT NULL,
|
|
11
|
+
stack_trace TEXT,
|
|
12
|
+
raw_log TEXT NOT NULL,
|
|
13
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
14
|
+
suggestion TEXT,
|
|
15
|
+
fix_result TEXT,
|
|
16
|
+
fix_attempts INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
locked_by TEXT,
|
|
18
|
+
locked_at TEXT,
|
|
19
|
+
created_at TEXT NOT NULL,
|
|
20
|
+
updated_at TEXT NOT NULL
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_errors_hash ON errors(hash);
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_errors_status ON errors(status);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_errors_created_at ON errors(created_at);
|
|
26
|
+
`;
|
|
27
|
+
const CREATE_WATCHER_STATE_TABLE = `
|
|
28
|
+
CREATE TABLE IF NOT EXISTS watcher_state (
|
|
29
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
30
|
+
pid INTEGER NOT NULL,
|
|
31
|
+
started_at TEXT NOT NULL,
|
|
32
|
+
autonomous INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
project_root TEXT NOT NULL,
|
|
34
|
+
command_line TEXT NOT NULL
|
|
35
|
+
);
|
|
36
|
+
`;
|
|
37
|
+
const CREATE_ACTIVITY_LOG_TABLE = `
|
|
38
|
+
CREATE TABLE IF NOT EXISTS activity_log (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
timestamp TEXT NOT NULL,
|
|
41
|
+
action TEXT NOT NULL,
|
|
42
|
+
error_id INTEGER,
|
|
43
|
+
details TEXT,
|
|
44
|
+
FOREIGN KEY (error_id) REFERENCES errors(id)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_activity_log_timestamp ON activity_log(timestamp);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_activity_log_error_id ON activity_log(error_id);
|
|
49
|
+
`;
|
|
50
|
+
const CREATE_SCHEMA_VERSION_TABLE = `
|
|
51
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
52
|
+
version INTEGER PRIMARY KEY,
|
|
53
|
+
applied_at TEXT NOT NULL
|
|
54
|
+
);
|
|
55
|
+
`;
|
|
56
|
+
export function initializeSchema(db) {
|
|
57
|
+
db.exec('BEGIN');
|
|
58
|
+
try {
|
|
59
|
+
db.exec(CREATE_SCHEMA_VERSION_TABLE);
|
|
60
|
+
db.exec(CREATE_ERRORS_TABLE);
|
|
61
|
+
db.exec(CREATE_WATCHER_STATE_TABLE);
|
|
62
|
+
db.exec(CREATE_ACTIVITY_LOG_TABLE);
|
|
63
|
+
const existing = db.get('SELECT version FROM schema_version LIMIT 1');
|
|
64
|
+
if (!existing) {
|
|
65
|
+
db.run('INSERT INTO schema_version (version, applied_at) VALUES (?, ?)', [
|
|
66
|
+
SCHEMA_VERSION,
|
|
67
|
+
new Date().toISOString(),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
db.exec('COMMIT');
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
db.exec('ROLLBACK');
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function checkSchemaVersion(db) {
|
|
78
|
+
const row = db.get('SELECT version FROM schema_version LIMIT 1');
|
|
79
|
+
if (!row || row.version !== SCHEMA_VERSION) {
|
|
80
|
+
const found = row ? String(row.version) : 'none';
|
|
81
|
+
console.error(`Database schema version mismatch. Expected ${SCHEMA_VERSION}, found ${found}.`);
|
|
82
|
+
process.exit(EXIT_CODES.SCHEMA_MISMATCH);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Config } from '../config/schema.js';
|
|
2
|
+
import type { ErrorRecord } from '../db/queries.js';
|
|
3
|
+
type GeneratedContext = {
|
|
4
|
+
path: string;
|
|
5
|
+
content: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const generateAnalyzeContext: (error: ErrorRecord, config: Config, attempt: number) => GeneratedContext;
|
|
8
|
+
export declare const generateFixContext: (error: ErrorRecord, analysis: string, config: Config, attempt: number) => GeneratedContext;
|
|
9
|
+
export type { GeneratedContext };
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
const CONTEXT_DIR = path.posix.join('.watchfix', 'context');
|
|
3
|
+
const STACK_TRACE_MAX_BYTES = 32 * 1024;
|
|
4
|
+
const STACK_TRACE_SLICE_BYTES = 16 * 1024;
|
|
5
|
+
const STACK_TRACE_TRUNCATION_MARKER = '[...truncated...]';
|
|
6
|
+
const formatDate = (date = new Date()) => date.toISOString().slice(0, 10);
|
|
7
|
+
const sliceUtf8ByBytes = (value, bytes, position) => {
|
|
8
|
+
const buffer = Buffer.from(value, 'utf8');
|
|
9
|
+
if (buffer.length <= bytes) {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
return position === 'start'
|
|
13
|
+
? buffer.subarray(0, bytes).toString('utf8')
|
|
14
|
+
: buffer.subarray(buffer.length - bytes).toString('utf8');
|
|
15
|
+
};
|
|
16
|
+
const truncateStackTrace = (stackTrace) => {
|
|
17
|
+
if (Buffer.byteLength(stackTrace, 'utf8') <= STACK_TRACE_MAX_BYTES) {
|
|
18
|
+
return stackTrace;
|
|
19
|
+
}
|
|
20
|
+
const head = sliceUtf8ByBytes(stackTrace, STACK_TRACE_SLICE_BYTES, 'start');
|
|
21
|
+
const tail = sliceUtf8ByBytes(stackTrace, STACK_TRACE_SLICE_BYTES, 'end');
|
|
22
|
+
return `${head}\n${STACK_TRACE_TRUNCATION_MARKER}\n${tail}`;
|
|
23
|
+
};
|
|
24
|
+
const sanitizeUtf8 = (value) => {
|
|
25
|
+
let result = '';
|
|
26
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
27
|
+
const code = value.charCodeAt(i);
|
|
28
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
29
|
+
const next = value.charCodeAt(i + 1);
|
|
30
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
31
|
+
result += value[i] + value[i + 1];
|
|
32
|
+
i += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
result += '\uFFFD';
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (code >= 0xdc00 && code <= 0xdfff) {
|
|
39
|
+
result += '\uFFFD';
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
result += value[i];
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
const splitRawLog = (error) => {
|
|
47
|
+
if (!error.rawLog) {
|
|
48
|
+
return { before: [], after: [] };
|
|
49
|
+
}
|
|
50
|
+
const lines = error.rawLog.split('\n');
|
|
51
|
+
const errorIndex = lines.indexOf(error.message);
|
|
52
|
+
if (errorIndex < 0) {
|
|
53
|
+
return { before: lines, after: [] };
|
|
54
|
+
}
|
|
55
|
+
const afterStart = errorIndex + 1;
|
|
56
|
+
const stackLines = error.stackTrace ? error.stackTrace.split('\n') : [];
|
|
57
|
+
let afterLines = lines.slice(afterStart);
|
|
58
|
+
if (stackLines.length > 0) {
|
|
59
|
+
let matchCount = 0;
|
|
60
|
+
while (matchCount < stackLines.length &&
|
|
61
|
+
afterLines[matchCount] === stackLines[matchCount]) {
|
|
62
|
+
matchCount += 1;
|
|
63
|
+
}
|
|
64
|
+
if (matchCount > 0) {
|
|
65
|
+
afterLines = afterLines.slice(matchCount);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
before: lines.slice(0, errorIndex),
|
|
70
|
+
after: afterLines,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const buildContextBlock = (beforeLines, errorLines, afterLines, truncatedLineCount) => {
|
|
74
|
+
const lines = [];
|
|
75
|
+
if (truncatedLineCount > 0) {
|
|
76
|
+
lines.push(`[...${truncatedLineCount} lines truncated due to size limit...]`);
|
|
77
|
+
}
|
|
78
|
+
lines.push(...beforeLines);
|
|
79
|
+
lines.push('---ERROR---');
|
|
80
|
+
lines.push(...errorLines);
|
|
81
|
+
lines.push('---END ERROR---');
|
|
82
|
+
lines.push(...afterLines);
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
};
|
|
85
|
+
const truncateStackTraceToBytes = (stackTrace, maxBytes) => {
|
|
86
|
+
if (maxBytes <= 0) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
if (Buffer.byteLength(stackTrace, 'utf8') <= maxBytes) {
|
|
90
|
+
return stackTrace;
|
|
91
|
+
}
|
|
92
|
+
const markerBytes = Buffer.byteLength(STACK_TRACE_TRUNCATION_MARKER, 'utf8');
|
|
93
|
+
if (maxBytes <= markerBytes) {
|
|
94
|
+
return sliceUtf8ByBytes(stackTrace, maxBytes, 'start');
|
|
95
|
+
}
|
|
96
|
+
const headBytes = Math.max(0, maxBytes - markerBytes - 1);
|
|
97
|
+
const head = headBytes > 0 ? sliceUtf8ByBytes(stackTrace, headBytes, 'start') : '';
|
|
98
|
+
return head ? `${head}\n${STACK_TRACE_TRUNCATION_MARKER}` : STACK_TRACE_TRUNCATION_MARKER;
|
|
99
|
+
};
|
|
100
|
+
const buildAnalyzeContent = (options) => {
|
|
101
|
+
const { projectName, projectRoot, error, attempt, date, stackTrace } = options;
|
|
102
|
+
const analysisPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt}-analysis.yaml`);
|
|
103
|
+
return `# WatchFix Task
|
|
104
|
+
|
|
105
|
+
## Mode
|
|
106
|
+
analyze
|
|
107
|
+
|
|
108
|
+
## Project
|
|
109
|
+
- Name: ${projectName}
|
|
110
|
+
- Root: ${projectRoot}
|
|
111
|
+
|
|
112
|
+
## Error Details
|
|
113
|
+
- ID: ${error.id}
|
|
114
|
+
- Source: ${error.source}
|
|
115
|
+
- Type: ${error.errorType}
|
|
116
|
+
- Detected: ${error.timestamp}
|
|
117
|
+
- Fix Attempts: ${error.fixAttempts}
|
|
118
|
+
|
|
119
|
+
### Message
|
|
120
|
+
${error.message}
|
|
121
|
+
|
|
122
|
+
### Stack Trace
|
|
123
|
+
${stackTrace}
|
|
124
|
+
|
|
125
|
+
### Context (surrounding log lines)
|
|
126
|
+
${options.contextBlock}
|
|
127
|
+
|
|
128
|
+
## Instructions
|
|
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
|
|
134
|
+
|
|
135
|
+
Write your analysis to: \`${analysisPath}\`
|
|
136
|
+
|
|
137
|
+
Use this exact YAML format:
|
|
138
|
+
\`\`\`yaml
|
|
139
|
+
summary: One sentence summary of the problem
|
|
140
|
+
root_cause: |
|
|
141
|
+
Detailed explanation of root cause
|
|
142
|
+
Can be multiple lines
|
|
143
|
+
suggested_fix: |
|
|
144
|
+
What changes to make
|
|
145
|
+
Step by step if needed
|
|
146
|
+
files_to_modify:
|
|
147
|
+
- path/to/file1
|
|
148
|
+
- path/to/file2
|
|
149
|
+
confidence: high | medium | low
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
## Constraints
|
|
153
|
+
- Do NOT modify any files during analysis
|
|
154
|
+
- If you cannot determine the cause, set confidence to "low"
|
|
155
|
+
- Be specific about file paths relative to project root
|
|
156
|
+
- WARNING: If a fix fails and is retried, any file modifications from previous attempts will persist
|
|
157
|
+
`;
|
|
158
|
+
};
|
|
159
|
+
const buildFixContent = (options) => {
|
|
160
|
+
const { projectName, projectRoot, error, attempt, date, stackTrace, analysis } = options;
|
|
161
|
+
const resultPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt}-result.yaml`);
|
|
162
|
+
return `# WatchFix Task
|
|
163
|
+
|
|
164
|
+
## Mode
|
|
165
|
+
fix
|
|
166
|
+
|
|
167
|
+
## Project
|
|
168
|
+
- Name: ${projectName}
|
|
169
|
+
- Root: ${projectRoot}
|
|
170
|
+
|
|
171
|
+
## Error Details
|
|
172
|
+
- ID: ${error.id}
|
|
173
|
+
- Source: ${error.source}
|
|
174
|
+
- Type: ${error.errorType}
|
|
175
|
+
- Detected: ${error.timestamp}
|
|
176
|
+
- Fix Attempts: ${error.fixAttempts}
|
|
177
|
+
|
|
178
|
+
### Message
|
|
179
|
+
${error.message}
|
|
180
|
+
|
|
181
|
+
### Stack Trace
|
|
182
|
+
${stackTrace}
|
|
183
|
+
|
|
184
|
+
## Previous Analysis
|
|
185
|
+
${analysis}
|
|
186
|
+
|
|
187
|
+
## Instructions
|
|
188
|
+
|
|
189
|
+
1. Read the previous analysis above
|
|
190
|
+
2. Implement the suggested fix
|
|
191
|
+
3. Follow existing code style and conventions
|
|
192
|
+
4. Make minimal, targeted changes
|
|
193
|
+
|
|
194
|
+
Write your results to: \`${resultPath}\`
|
|
195
|
+
|
|
196
|
+
Use this exact YAML format:
|
|
197
|
+
\`\`\`yaml
|
|
198
|
+
success: true | false
|
|
199
|
+
summary: One sentence describing what was done
|
|
200
|
+
files_changed:
|
|
201
|
+
- path: relative/path/to/file
|
|
202
|
+
change: Description of change made
|
|
203
|
+
notes: |
|
|
204
|
+
Optional additional notes
|
|
205
|
+
Can be multiple lines
|
|
206
|
+
\`\`\`
|
|
207
|
+
|
|
208
|
+
## Constraints
|
|
209
|
+
- Make the smallest change that resolves the issue
|
|
210
|
+
- Do NOT change unrelated code
|
|
211
|
+
- If the fix cannot be applied, set success to false and explain in notes
|
|
212
|
+
- WARNING: If this fix fails verification, the modified files will remain changed for the next retry attempt
|
|
213
|
+
`;
|
|
214
|
+
};
|
|
215
|
+
const resolveProjectRoot = (config) => path.resolve(config.project.root);
|
|
216
|
+
const ensureSizeLimit = (options) => {
|
|
217
|
+
let truncatedLines = 0;
|
|
218
|
+
let beforeLines = [...options.beforeLines];
|
|
219
|
+
let afterLines = [...options.afterLines];
|
|
220
|
+
let stackTrace = options.stackTrace;
|
|
221
|
+
let content = options.render(beforeLines, afterLines, stackTrace, truncatedLines);
|
|
222
|
+
// Phase 1: Remove beforeLines (oldest context first)
|
|
223
|
+
while (Buffer.byteLength(content, 'utf8') > options.maxBytes &&
|
|
224
|
+
beforeLines.length > 0) {
|
|
225
|
+
beforeLines = beforeLines.slice(1);
|
|
226
|
+
truncatedLines += 1;
|
|
227
|
+
content = options.render(beforeLines, afterLines, stackTrace, truncatedLines);
|
|
228
|
+
}
|
|
229
|
+
// Phase 2: Remove afterLines (if still over limit)
|
|
230
|
+
while (Buffer.byteLength(content, 'utf8') > options.maxBytes &&
|
|
231
|
+
afterLines.length > 0) {
|
|
232
|
+
afterLines = afterLines.slice(0, -1);
|
|
233
|
+
content = options.render(beforeLines, afterLines, stackTrace, truncatedLines);
|
|
234
|
+
}
|
|
235
|
+
// Phase 3: Further truncate stack trace using binary search (if still over)
|
|
236
|
+
if (Buffer.byteLength(content, 'utf8') > options.maxBytes && stackTrace) {
|
|
237
|
+
let low = 0;
|
|
238
|
+
let high = Buffer.byteLength(stackTrace, 'utf8');
|
|
239
|
+
let best = '';
|
|
240
|
+
while (low <= high) {
|
|
241
|
+
const mid = Math.floor((low + high) / 2);
|
|
242
|
+
const candidate = truncateStackTraceToBytes(stackTrace, mid);
|
|
243
|
+
const candidateContent = options.render(beforeLines, afterLines, candidate, truncatedLines);
|
|
244
|
+
if (Buffer.byteLength(candidateContent, 'utf8') <= options.maxBytes) {
|
|
245
|
+
best = candidate;
|
|
246
|
+
low = mid + 1;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
high = mid - 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
stackTrace = best;
|
|
253
|
+
content = options.render(beforeLines, afterLines, stackTrace, truncatedLines);
|
|
254
|
+
}
|
|
255
|
+
return { content, truncatedLines, beforeLines, afterLines, stackTrace };
|
|
256
|
+
};
|
|
257
|
+
export const generateAnalyzeContext = (error, config, attempt) => {
|
|
258
|
+
const date = formatDate();
|
|
259
|
+
const contextPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt}-analyze.md`);
|
|
260
|
+
const maxBytes = config.cleanup.context_max_size_kb * 1024;
|
|
261
|
+
const { before, after } = splitRawLog(error);
|
|
262
|
+
const buildContent = (stackTraceValue, beforeLines, afterLines, truncated) => {
|
|
263
|
+
const errorLines = [error.message];
|
|
264
|
+
if (stackTraceValue) {
|
|
265
|
+
errorLines.push(...stackTraceValue.split('\n'));
|
|
266
|
+
}
|
|
267
|
+
return buildAnalyzeContent({
|
|
268
|
+
projectName: config.project.name,
|
|
269
|
+
projectRoot: resolveProjectRoot(config),
|
|
270
|
+
error,
|
|
271
|
+
attempt,
|
|
272
|
+
date,
|
|
273
|
+
stackTrace: stackTraceValue,
|
|
274
|
+
contextBlock: buildContextBlock(beforeLines, errorLines, afterLines, truncated),
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
const stackTrace = truncateStackTrace(error.stackTrace ?? '');
|
|
278
|
+
const render = (beforeLines, afterLines, stackTraceValue, truncated) => buildContent(stackTraceValue, beforeLines, afterLines, truncated);
|
|
279
|
+
const { content } = ensureSizeLimit({
|
|
280
|
+
maxBytes,
|
|
281
|
+
beforeLines: before,
|
|
282
|
+
afterLines: after,
|
|
283
|
+
stackTrace,
|
|
284
|
+
render,
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
path: contextPath,
|
|
288
|
+
content: sanitizeUtf8(content),
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
const ensureFixSizeLimit = (options) => {
|
|
292
|
+
let analysis = options.analysis;
|
|
293
|
+
let stackTrace = options.stackTrace;
|
|
294
|
+
let content = options.render(analysis, stackTrace);
|
|
295
|
+
// Phase 1: Truncate analysis content (from end, preserving summary)
|
|
296
|
+
if (Buffer.byteLength(content, 'utf8') > options.maxBytes && analysis) {
|
|
297
|
+
let low = 0;
|
|
298
|
+
let high = Buffer.byteLength(analysis, 'utf8');
|
|
299
|
+
let best = '';
|
|
300
|
+
while (low <= high) {
|
|
301
|
+
const mid = Math.floor((low + high) / 2);
|
|
302
|
+
const candidate = sliceUtf8ByBytes(analysis, mid, 'start');
|
|
303
|
+
const candidateContent = options.render(candidate, stackTrace);
|
|
304
|
+
if (Buffer.byteLength(candidateContent, 'utf8') <= options.maxBytes) {
|
|
305
|
+
best = candidate;
|
|
306
|
+
low = mid + 1;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
high = mid - 1;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
analysis = best;
|
|
313
|
+
content = options.render(analysis, stackTrace);
|
|
314
|
+
}
|
|
315
|
+
// Phase 2: Further truncate stack trace using binary search (if still over)
|
|
316
|
+
if (Buffer.byteLength(content, 'utf8') > options.maxBytes && stackTrace) {
|
|
317
|
+
let low = 0;
|
|
318
|
+
let high = Buffer.byteLength(stackTrace, 'utf8');
|
|
319
|
+
let best = '';
|
|
320
|
+
while (low <= high) {
|
|
321
|
+
const mid = Math.floor((low + high) / 2);
|
|
322
|
+
const candidate = truncateStackTraceToBytes(stackTrace, mid);
|
|
323
|
+
const candidateContent = options.render(analysis, candidate);
|
|
324
|
+
if (Buffer.byteLength(candidateContent, 'utf8') <= options.maxBytes) {
|
|
325
|
+
best = candidate;
|
|
326
|
+
low = mid + 1;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
high = mid - 1;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
stackTrace = best;
|
|
333
|
+
content = options.render(analysis, stackTrace);
|
|
334
|
+
}
|
|
335
|
+
return { content, analysis, stackTrace };
|
|
336
|
+
};
|
|
337
|
+
export const generateFixContext = (error, analysis, config, attempt) => {
|
|
338
|
+
const date = formatDate();
|
|
339
|
+
const contextPath = path.posix.join(CONTEXT_DIR, `${date}-error-${error.id}-attempt-${attempt}-fix.md`);
|
|
340
|
+
const maxBytes = config.cleanup.context_max_size_kb * 1024;
|
|
341
|
+
const stackTrace = truncateStackTrace(error.stackTrace ?? '');
|
|
342
|
+
const render = (analysisValue, stackTraceValue) => buildFixContent({
|
|
343
|
+
projectName: config.project.name,
|
|
344
|
+
projectRoot: resolveProjectRoot(config),
|
|
345
|
+
error,
|
|
346
|
+
attempt,
|
|
347
|
+
date,
|
|
348
|
+
stackTrace: stackTraceValue,
|
|
349
|
+
analysis: analysisValue,
|
|
350
|
+
});
|
|
351
|
+
const { content } = ensureFixSizeLimit({
|
|
352
|
+
maxBytes,
|
|
353
|
+
analysis,
|
|
354
|
+
stackTrace,
|
|
355
|
+
render,
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
path: contextPath,
|
|
359
|
+
content: sanitizeUtf8(content),
|
|
360
|
+
};
|
|
361
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Agent } from '../agents/types.js';
|
|
2
|
+
import type { Config } from '../config/schema.js';
|
|
3
|
+
import type { Database } from '../db/index.js';
|
|
4
|
+
import type { ErrorStatus } from '../utils/errors.js';
|
|
5
|
+
import { Logger } from '../utils/logger.js';
|
|
6
|
+
import type { AnalysisOutput, FixOutput } from './output.js';
|
|
7
|
+
import { type VerificationResult } from './verifier.js';
|
|
8
|
+
type FixOptions = {
|
|
9
|
+
analyzeOnly?: boolean;
|
|
10
|
+
reanalyze?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type FixResult = {
|
|
13
|
+
errorId: number;
|
|
14
|
+
status: ErrorStatus;
|
|
15
|
+
lockAcquired: boolean;
|
|
16
|
+
attempts: number;
|
|
17
|
+
analysis?: AnalysisOutput;
|
|
18
|
+
fix?: FixOutput;
|
|
19
|
+
verification?: VerificationResult;
|
|
20
|
+
message?: string;
|
|
21
|
+
};
|
|
22
|
+
type FixOrchestratorOptions = {
|
|
23
|
+
agent?: Agent;
|
|
24
|
+
logger?: Logger;
|
|
25
|
+
terminalEnabled?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export declare class FixOrchestrator {
|
|
28
|
+
private readonly db;
|
|
29
|
+
private readonly config;
|
|
30
|
+
private readonly logger;
|
|
31
|
+
private readonly agent;
|
|
32
|
+
private readonly terminalEnabled;
|
|
33
|
+
constructor(db: Database, config: Config, options?: FixOrchestratorOptions);
|
|
34
|
+
fixError(errorId: number, options?: FixOptions): Promise<FixResult>;
|
|
35
|
+
private processAgentOutput;
|
|
36
|
+
}
|
|
37
|
+
export {};
|