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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/agents/base.d.ts +19 -0
  4. package/dist/agents/base.js +140 -0
  5. package/dist/agents/claude.d.ts +11 -0
  6. package/dist/agents/claude.js +6 -0
  7. package/dist/agents/codex.d.ts +11 -0
  8. package/dist/agents/codex.js +6 -0
  9. package/dist/agents/defaults.d.ts +21 -0
  10. package/dist/agents/defaults.js +21 -0
  11. package/dist/agents/gemini.d.ts +11 -0
  12. package/dist/agents/gemini.js +6 -0
  13. package/dist/agents/index.d.ts +19 -0
  14. package/dist/agents/index.js +63 -0
  15. package/dist/agents/types.d.ts +22 -0
  16. package/dist/agents/types.js +1 -0
  17. package/dist/cli/commands/clean.d.ts +9 -0
  18. package/dist/cli/commands/clean.js +173 -0
  19. package/dist/cli/commands/config.d.ts +7 -0
  20. package/dist/cli/commands/config.js +134 -0
  21. package/dist/cli/commands/fix.d.ts +12 -0
  22. package/dist/cli/commands/fix.js +391 -0
  23. package/dist/cli/commands/ignore.d.ts +7 -0
  24. package/dist/cli/commands/ignore.js +74 -0
  25. package/dist/cli/commands/init.d.ts +5 -0
  26. package/dist/cli/commands/init.js +115 -0
  27. package/dist/cli/commands/logs.d.ts +9 -0
  28. package/dist/cli/commands/logs.js +106 -0
  29. package/dist/cli/commands/show.d.ts +8 -0
  30. package/dist/cli/commands/show.js +165 -0
  31. package/dist/cli/commands/status.d.ts +7 -0
  32. package/dist/cli/commands/status.js +110 -0
  33. package/dist/cli/commands/stop.d.ts +7 -0
  34. package/dist/cli/commands/stop.js +106 -0
  35. package/dist/cli/commands/version.d.ts +7 -0
  36. package/dist/cli/commands/version.js +36 -0
  37. package/dist/cli/commands/watch.d.ts +10 -0
  38. package/dist/cli/commands/watch.js +204 -0
  39. package/dist/cli/index.d.ts +2 -0
  40. package/dist/cli/index.js +152 -0
  41. package/dist/config/loader.d.ts +4 -0
  42. package/dist/config/loader.js +96 -0
  43. package/dist/config/schema.d.ts +375 -0
  44. package/dist/config/schema.js +99 -0
  45. package/dist/db/index.d.ts +15 -0
  46. package/dist/db/index.js +71 -0
  47. package/dist/db/queries.d.ts +45 -0
  48. package/dist/db/queries.js +111 -0
  49. package/dist/db/schema.d.ts +4 -0
  50. package/dist/db/schema.js +84 -0
  51. package/dist/fixer/context.d.ts +9 -0
  52. package/dist/fixer/context.js +361 -0
  53. package/dist/fixer/index.d.ts +37 -0
  54. package/dist/fixer/index.js +398 -0
  55. package/dist/fixer/lock.d.ts +7 -0
  56. package/dist/fixer/lock.js +49 -0
  57. package/dist/fixer/output.d.ts +21 -0
  58. package/dist/fixer/output.js +108 -0
  59. package/dist/fixer/queue.d.ts +15 -0
  60. package/dist/fixer/queue.js +53 -0
  61. package/dist/fixer/verifier.d.ts +44 -0
  62. package/dist/fixer/verifier.js +133 -0
  63. package/dist/utils/daemon.d.ts +22 -0
  64. package/dist/utils/daemon.js +143 -0
  65. package/dist/utils/duration.d.ts +2 -0
  66. package/dist/utils/duration.js +31 -0
  67. package/dist/utils/errors.d.ts +17 -0
  68. package/dist/utils/errors.js +20 -0
  69. package/dist/utils/hash.d.ts +2 -0
  70. package/dist/utils/hash.js +18 -0
  71. package/dist/utils/http.d.ts +6 -0
  72. package/dist/utils/http.js +61 -0
  73. package/dist/utils/logger.d.ts +25 -0
  74. package/dist/utils/logger.js +85 -0
  75. package/dist/utils/process.d.ts +16 -0
  76. package/dist/utils/process.js +146 -0
  77. package/dist/watcher/index.d.ts +55 -0
  78. package/dist/watcher/index.js +234 -0
  79. package/dist/watcher/parser.d.ts +42 -0
  80. package/dist/watcher/parser.js +162 -0
  81. package/dist/watcher/patterns.d.ts +5 -0
  82. package/dist/watcher/patterns.js +92 -0
  83. package/dist/watcher/sources/command.d.ts +27 -0
  84. package/dist/watcher/sources/command.js +143 -0
  85. package/dist/watcher/sources/docker.d.ts +28 -0
  86. package/dist/watcher/sources/docker.js +183 -0
  87. package/dist/watcher/sources/file.d.ts +30 -0
  88. package/dist/watcher/sources/file.js +177 -0
  89. package/dist/watcher/sources/types.d.ts +27 -0
  90. package/dist/watcher/sources/types.js +1 -0
  91. 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,4 @@
1
+ import type { Database } from './index.js';
2
+ export declare const SCHEMA_VERSION = 1;
3
+ export declare function initializeSchema(db: Database): void;
4
+ export declare function checkSchemaVersion(db: Database): void;
@@ -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 {};