@litmers/cursorflow-orchestrator 0.1.20 → 0.1.28

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 (224) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/commands/cursorflow-clean.md +19 -0
  3. package/commands/cursorflow-runs.md +59 -0
  4. package/commands/cursorflow-stop.md +55 -0
  5. package/dist/cli/clean.js +171 -0
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +7 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +1 -1
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +83 -42
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.d.ts +7 -0
  14. package/dist/cli/monitor.js +1007 -189
  15. package/dist/cli/monitor.js.map +1 -1
  16. package/dist/cli/prepare.js +87 -3
  17. package/dist/cli/prepare.js.map +1 -1
  18. package/dist/cli/resume.js +188 -236
  19. package/dist/cli/resume.js.map +1 -1
  20. package/dist/cli/run.js +125 -3
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/runs.d.ts +5 -0
  23. package/dist/cli/runs.js +214 -0
  24. package/dist/cli/runs.js.map +1 -0
  25. package/dist/cli/setup-commands.js +0 -0
  26. package/dist/cli/signal.js +1 -1
  27. package/dist/cli/signal.js.map +1 -1
  28. package/dist/cli/stop.d.ts +5 -0
  29. package/dist/cli/stop.js +215 -0
  30. package/dist/cli/stop.js.map +1 -0
  31. package/dist/cli/tasks.d.ts +10 -0
  32. package/dist/cli/tasks.js +165 -0
  33. package/dist/cli/tasks.js.map +1 -0
  34. package/dist/core/auto-recovery.d.ts +212 -0
  35. package/dist/core/auto-recovery.js +737 -0
  36. package/dist/core/auto-recovery.js.map +1 -0
  37. package/dist/core/failure-policy.d.ts +156 -0
  38. package/dist/core/failure-policy.js +488 -0
  39. package/dist/core/failure-policy.js.map +1 -0
  40. package/dist/core/orchestrator.d.ts +15 -2
  41. package/dist/core/orchestrator.js +397 -15
  42. package/dist/core/orchestrator.js.map +1 -1
  43. package/dist/core/reviewer.d.ts +2 -0
  44. package/dist/core/reviewer.js +2 -0
  45. package/dist/core/reviewer.js.map +1 -1
  46. package/dist/core/runner.d.ts +33 -10
  47. package/dist/core/runner.js +321 -146
  48. package/dist/core/runner.js.map +1 -1
  49. package/dist/services/logging/buffer.d.ts +67 -0
  50. package/dist/services/logging/buffer.js +309 -0
  51. package/dist/services/logging/buffer.js.map +1 -0
  52. package/dist/services/logging/console.d.ts +89 -0
  53. package/dist/services/logging/console.js +169 -0
  54. package/dist/services/logging/console.js.map +1 -0
  55. package/dist/services/logging/file-writer.d.ts +71 -0
  56. package/dist/services/logging/file-writer.js +516 -0
  57. package/dist/services/logging/file-writer.js.map +1 -0
  58. package/dist/services/logging/formatter.d.ts +39 -0
  59. package/dist/services/logging/formatter.js +227 -0
  60. package/dist/services/logging/formatter.js.map +1 -0
  61. package/dist/services/logging/index.d.ts +11 -0
  62. package/dist/services/logging/index.js +30 -0
  63. package/dist/services/logging/index.js.map +1 -0
  64. package/dist/services/logging/parser.d.ts +31 -0
  65. package/dist/services/logging/parser.js +222 -0
  66. package/dist/services/logging/parser.js.map +1 -0
  67. package/dist/services/process/index.d.ts +59 -0
  68. package/dist/services/process/index.js +257 -0
  69. package/dist/services/process/index.js.map +1 -0
  70. package/dist/types/agent.d.ts +20 -0
  71. package/dist/types/agent.js +6 -0
  72. package/dist/types/agent.js.map +1 -0
  73. package/dist/types/config.d.ts +65 -0
  74. package/dist/types/config.js +6 -0
  75. package/dist/types/config.js.map +1 -0
  76. package/dist/types/events.d.ts +125 -0
  77. package/dist/types/events.js +6 -0
  78. package/dist/types/events.js.map +1 -0
  79. package/dist/types/index.d.ts +12 -0
  80. package/dist/types/index.js +37 -0
  81. package/dist/types/index.js.map +1 -0
  82. package/dist/types/lane.d.ts +43 -0
  83. package/dist/types/lane.js +6 -0
  84. package/dist/types/lane.js.map +1 -0
  85. package/dist/types/logging.d.ts +71 -0
  86. package/dist/types/logging.js +16 -0
  87. package/dist/types/logging.js.map +1 -0
  88. package/dist/types/review.d.ts +17 -0
  89. package/dist/types/review.js +6 -0
  90. package/dist/types/review.js.map +1 -0
  91. package/dist/types/run.d.ts +32 -0
  92. package/dist/types/run.js +6 -0
  93. package/dist/types/run.js.map +1 -0
  94. package/dist/types/task.d.ts +71 -0
  95. package/dist/types/task.js +6 -0
  96. package/dist/types/task.js.map +1 -0
  97. package/dist/ui/components.d.ts +134 -0
  98. package/dist/ui/components.js +389 -0
  99. package/dist/ui/components.js.map +1 -0
  100. package/dist/ui/log-viewer.d.ts +49 -0
  101. package/dist/ui/log-viewer.js +449 -0
  102. package/dist/ui/log-viewer.js.map +1 -0
  103. package/dist/utils/checkpoint.d.ts +87 -0
  104. package/dist/utils/checkpoint.js +317 -0
  105. package/dist/utils/checkpoint.js.map +1 -0
  106. package/dist/utils/config.d.ts +4 -0
  107. package/dist/utils/config.js +11 -2
  108. package/dist/utils/config.js.map +1 -1
  109. package/dist/utils/cursor-agent.js.map +1 -1
  110. package/dist/utils/dependency.d.ts +74 -0
  111. package/dist/utils/dependency.js +420 -0
  112. package/dist/utils/dependency.js.map +1 -0
  113. package/dist/utils/doctor.js +10 -5
  114. package/dist/utils/doctor.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +10 -33
  116. package/dist/utils/enhanced-logger.js +94 -9
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/git.d.ts +121 -0
  119. package/dist/utils/git.js +322 -2
  120. package/dist/utils/git.js.map +1 -1
  121. package/dist/utils/health.d.ts +91 -0
  122. package/dist/utils/health.js +556 -0
  123. package/dist/utils/health.js.map +1 -0
  124. package/dist/utils/lock.d.ts +95 -0
  125. package/dist/utils/lock.js +332 -0
  126. package/dist/utils/lock.js.map +1 -0
  127. package/dist/utils/log-buffer.d.ts +17 -0
  128. package/dist/utils/log-buffer.js +14 -0
  129. package/dist/utils/log-buffer.js.map +1 -0
  130. package/dist/utils/log-constants.d.ts +23 -0
  131. package/dist/utils/log-constants.js +28 -0
  132. package/dist/utils/log-constants.js.map +1 -0
  133. package/dist/utils/log-formatter.d.ts +9 -0
  134. package/dist/utils/log-formatter.js +113 -70
  135. package/dist/utils/log-formatter.js.map +1 -1
  136. package/dist/utils/log-service.d.ts +19 -0
  137. package/dist/utils/log-service.js +47 -0
  138. package/dist/utils/log-service.js.map +1 -0
  139. package/dist/utils/logger.d.ts +46 -27
  140. package/dist/utils/logger.js +82 -60
  141. package/dist/utils/logger.js.map +1 -1
  142. package/dist/utils/process-manager.d.ts +21 -0
  143. package/dist/utils/process-manager.js +138 -0
  144. package/dist/utils/process-manager.js.map +1 -0
  145. package/dist/utils/retry.d.ts +121 -0
  146. package/dist/utils/retry.js +374 -0
  147. package/dist/utils/retry.js.map +1 -0
  148. package/dist/utils/run-service.d.ts +88 -0
  149. package/dist/utils/run-service.js +412 -0
  150. package/dist/utils/run-service.js.map +1 -0
  151. package/dist/utils/state.d.ts +58 -2
  152. package/dist/utils/state.js +306 -3
  153. package/dist/utils/state.js.map +1 -1
  154. package/dist/utils/task-service.d.ts +82 -0
  155. package/dist/utils/task-service.js +348 -0
  156. package/dist/utils/task-service.js.map +1 -0
  157. package/dist/utils/types.d.ts +2 -272
  158. package/dist/utils/types.js +16 -0
  159. package/dist/utils/types.js.map +1 -1
  160. package/package.json +38 -23
  161. package/scripts/ai-security-check.js +0 -1
  162. package/scripts/local-security-gate.sh +0 -0
  163. package/scripts/monitor-lanes.sh +94 -0
  164. package/scripts/patches/test-cursor-agent.js +0 -1
  165. package/scripts/release.sh +0 -0
  166. package/scripts/setup-security.sh +0 -0
  167. package/scripts/stream-logs.sh +72 -0
  168. package/scripts/verify-and-fix.sh +0 -0
  169. package/src/cli/clean.ts +180 -0
  170. package/src/cli/index.ts +7 -0
  171. package/src/cli/init.ts +1 -1
  172. package/src/cli/logs.ts +79 -42
  173. package/src/cli/monitor.ts +1815 -899
  174. package/src/cli/prepare.ts +97 -3
  175. package/src/cli/resume.ts +220 -277
  176. package/src/cli/run.ts +154 -3
  177. package/src/cli/runs.ts +212 -0
  178. package/src/cli/setup-commands.ts +0 -0
  179. package/src/cli/signal.ts +1 -1
  180. package/src/cli/stop.ts +209 -0
  181. package/src/cli/tasks.ts +154 -0
  182. package/src/core/auto-recovery.ts +909 -0
  183. package/src/core/failure-policy.ts +592 -0
  184. package/src/core/orchestrator.ts +1136 -675
  185. package/src/core/reviewer.ts +4 -0
  186. package/src/core/runner.ts +1443 -1217
  187. package/src/services/logging/buffer.ts +326 -0
  188. package/src/services/logging/console.ts +193 -0
  189. package/src/services/logging/file-writer.ts +526 -0
  190. package/src/services/logging/formatter.ts +268 -0
  191. package/src/services/logging/index.ts +16 -0
  192. package/src/services/logging/parser.ts +232 -0
  193. package/src/services/process/index.ts +261 -0
  194. package/src/types/agent.ts +24 -0
  195. package/src/types/config.ts +79 -0
  196. package/src/types/events.ts +156 -0
  197. package/src/types/index.ts +29 -0
  198. package/src/types/lane.ts +56 -0
  199. package/src/types/logging.ts +96 -0
  200. package/src/types/review.ts +20 -0
  201. package/src/types/run.ts +37 -0
  202. package/src/types/task.ts +79 -0
  203. package/src/ui/components.ts +430 -0
  204. package/src/ui/log-viewer.ts +485 -0
  205. package/src/utils/checkpoint.ts +374 -0
  206. package/src/utils/config.ts +11 -2
  207. package/src/utils/cursor-agent.ts +1 -1
  208. package/src/utils/dependency.ts +482 -0
  209. package/src/utils/doctor.ts +11 -5
  210. package/src/utils/enhanced-logger.ts +108 -49
  211. package/src/utils/git.ts +871 -499
  212. package/src/utils/health.ts +596 -0
  213. package/src/utils/lock.ts +346 -0
  214. package/src/utils/log-buffer.ts +28 -0
  215. package/src/utils/log-constants.ts +26 -0
  216. package/src/utils/log-formatter.ts +120 -37
  217. package/src/utils/log-service.ts +49 -0
  218. package/src/utils/logger.ts +100 -51
  219. package/src/utils/process-manager.ts +100 -0
  220. package/src/utils/retry.ts +413 -0
  221. package/src/utils/run-service.ts +433 -0
  222. package/src/utils/state.ts +369 -3
  223. package/src/utils/task-service.ts +370 -0
  224. package/src/utils/types.ts +2 -315
@@ -0,0 +1,526 @@
1
+ /**
2
+ * Log File Writer - Write logs to files with rotation
3
+ *
4
+ * Manages log file writing with support for multiple formats.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { Transform, TransformCallback } from 'stream';
10
+ import { LogSession, JsonLogEntry, ParsedMessage } from '../../types/logging';
11
+ import { EnhancedLogConfig } from '../../types/config';
12
+ import { StreamingMessageParser } from './parser';
13
+ import { stripAnsi, formatTimestampISO } from './formatter';
14
+
15
+ export const DEFAULT_LOG_CONFIG: EnhancedLogConfig = {
16
+ enabled: true,
17
+ stripAnsi: true,
18
+ addTimestamps: true,
19
+ maxFileSize: 50 * 1024 * 1024,
20
+ maxFiles: 5,
21
+ keepRawLogs: true,
22
+ writeJsonLog: true,
23
+ timestampFormat: 'iso',
24
+ };
25
+
26
+ /**
27
+ * Transform stream for clean log output
28
+ */
29
+ export class CleanLogTransform extends Transform {
30
+ private config: EnhancedLogConfig;
31
+ private session: LogSession;
32
+ private buffer = '';
33
+
34
+ constructor(config: EnhancedLogConfig, session: LogSession) {
35
+ super({ encoding: 'utf8' });
36
+ this.config = config;
37
+ this.session = session;
38
+ }
39
+
40
+ _transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void {
41
+ let text = chunk.toString();
42
+ this.buffer += text;
43
+ const lines = this.buffer.split('\n');
44
+ this.buffer = lines.pop() || '';
45
+
46
+ for (const line of lines) {
47
+ let processed = line;
48
+ if (this.config.stripAnsi) processed = stripAnsi(processed);
49
+ if (this.config.addTimestamps && processed.trim() && !this.hasTimestamp(processed)) {
50
+ const ts = formatTimestampISO(this.config.timestampFormat, this.session.startTime);
51
+ processed = `[${ts}] ${processed}`;
52
+ }
53
+ this.push(processed + '\n');
54
+ }
55
+ callback();
56
+ }
57
+
58
+ _flush(callback: TransformCallback): void {
59
+ if (this.buffer.trim()) {
60
+ let processed = this.buffer;
61
+ if (this.config.stripAnsi) processed = stripAnsi(processed);
62
+ if (this.config.addTimestamps && processed.trim() && !this.hasTimestamp(processed)) {
63
+ const ts = formatTimestampISO(this.config.timestampFormat, this.session.startTime);
64
+ processed = `[${ts}] ${processed}`;
65
+ }
66
+ this.push(processed + '\n');
67
+ }
68
+ callback();
69
+ }
70
+
71
+ private hasTimestamp(line: string): boolean {
72
+ return /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(line.trim());
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Enhanced Log Manager - Handles all log file operations
78
+ */
79
+ export class EnhancedLogManager {
80
+ private config: EnhancedLogConfig;
81
+ private session: LogSession;
82
+ private logDir: string;
83
+
84
+ private cleanLogPath: string;
85
+ private rawLogPath: string;
86
+ private jsonLogPath: string;
87
+ private readableLogPath: string;
88
+
89
+ private cleanLogFd: number | null = null;
90
+ private rawLogFd: number | null = null;
91
+ private jsonLogFd: number | null = null;
92
+ private readableLogFd: number | null = null;
93
+
94
+ private cleanLogSize = 0;
95
+ private rawLogSize = 0;
96
+
97
+ private cleanTransform: CleanLogTransform | null = null;
98
+ private streamingParser: StreamingMessageParser | null = null;
99
+ private lineBuffer = '';
100
+ private onParsedMessage?: (msg: ParsedMessage) => void;
101
+
102
+ constructor(
103
+ logDir: string,
104
+ session: LogSession,
105
+ config: Partial<EnhancedLogConfig> = {},
106
+ onParsedMessage?: (msg: ParsedMessage) => void
107
+ ) {
108
+ this.config = { ...DEFAULT_LOG_CONFIG, ...config };
109
+ this.session = session;
110
+ this.logDir = logDir;
111
+ this.onParsedMessage = onParsedMessage;
112
+
113
+ fs.mkdirSync(logDir, { recursive: true });
114
+
115
+ this.cleanLogPath = path.join(logDir, 'terminal.log');
116
+ this.rawLogPath = path.join(logDir, 'terminal-raw.log');
117
+ this.jsonLogPath = path.join(logDir, 'terminal.jsonl');
118
+ this.readableLogPath = path.join(logDir, 'terminal-readable.log');
119
+
120
+ this.initLogFiles();
121
+ }
122
+
123
+ private initLogFiles(): void {
124
+ this.rotateIfNeeded(this.cleanLogPath);
125
+ if (this.config.keepRawLogs) this.rotateIfNeeded(this.rawLogPath);
126
+
127
+ this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
128
+ if (this.config.keepRawLogs) this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
129
+ if (this.config.writeJsonLog) this.jsonLogFd = fs.openSync(this.jsonLogPath, 'a');
130
+ this.readableLogFd = fs.openSync(this.readableLogPath, 'a');
131
+
132
+ try {
133
+ this.cleanLogSize = fs.statSync(this.cleanLogPath).size;
134
+ if (this.config.keepRawLogs) this.rawLogSize = fs.statSync(this.rawLogPath).size;
135
+ } catch {
136
+ this.cleanLogSize = 0;
137
+ this.rawLogSize = 0;
138
+ }
139
+
140
+ this.writeSessionHeader();
141
+
142
+ this.cleanTransform = new CleanLogTransform(this.config, this.session);
143
+ this.cleanTransform.on('data', (data: string) => this.writeToCleanLog(data));
144
+
145
+ this.streamingParser = new StreamingMessageParser(msg => {
146
+ this.writeReadableMessage(msg);
147
+ if (this.onParsedMessage) this.onParsedMessage(msg);
148
+ });
149
+ }
150
+
151
+ private writeSessionHeader(): void {
152
+ const header = `
153
+ ╔══════════════════════════════════════════════════════════════════════════════╗
154
+ ║ CursorFlow Session Log ║
155
+ ╠══════════════════════════════════════════════════════════════════════════════╣
156
+ ║ Session ID: ${this.session.id.padEnd(62)}║
157
+ ║ Lane: ${this.session.laneName.padEnd(62)}║
158
+ ║ Task: ${(this.session.taskName || '-').padEnd(62)}║
159
+ ║ Model: ${(this.session.model || 'default').padEnd(62)}║
160
+ ║ Started: ${new Date(this.session.startTime).toISOString().padEnd(62)}║
161
+ ╚══════════════════════════════════════════════════════════════════════════════╝
162
+
163
+ `;
164
+ this.writeToCleanLog(header);
165
+ if (this.config.keepRawLogs && this.rawLogFd !== null) {
166
+ fs.writeSync(this.rawLogFd, header);
167
+ }
168
+
169
+ this.writeJsonEntry({
170
+ timestamp: new Date(this.session.startTime).toISOString(),
171
+ level: 'session',
172
+ source: 'system',
173
+ lane: this.session.laneName,
174
+ task: this.session.taskName,
175
+ message: 'Session started',
176
+ metadata: { sessionId: this.session.id, model: this.session.model, ...this.session.metadata },
177
+ });
178
+ }
179
+
180
+ private writeReadableMessage(msg: ParsedMessage): void {
181
+ if (this.readableLogFd === null) return;
182
+
183
+ const ts = new Date(msg.timestamp).toISOString();
184
+ let formatted: string;
185
+
186
+ switch (msg.type) {
187
+ case 'system':
188
+ formatted = `[${ts}] ⚙️ SYSTEM: ${msg.content}\n`;
189
+ break;
190
+
191
+ case 'user':
192
+ case 'assistant':
193
+ case 'result': {
194
+ const isUser = msg.type === 'user';
195
+ const isResult = msg.type === 'result';
196
+ const headerText = isUser ? '🧑 USER' : isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
197
+ const duration = msg.metadata?.duration_ms ? ` (${Math.round(msg.metadata.duration_ms / 1000)}s)` : '';
198
+ const label = `[ ${headerText}${duration} ] `;
199
+ const totalWidth = 80;
200
+ const topBorder = `┌─${label}${'─'.repeat(Math.max(0, totalWidth - label.length - 2))}`;
201
+ const bottomBorder = `└─${'─'.repeat(totalWidth - 2)}`;
202
+
203
+ formatted = `[${ts}] ${topBorder}\n`;
204
+ for (const line of msg.content.split('\n')) {
205
+ formatted += `[${ts}] │ ${line}\n`;
206
+ }
207
+ formatted += `[${ts}] ${bottomBorder}\n`;
208
+ break;
209
+ }
210
+
211
+ case 'tool':
212
+ formatted = `[${ts}] 🔧 TOOL: ${msg.content}\n`;
213
+ break;
214
+
215
+ case 'tool_result': {
216
+ const lines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
217
+ formatted = `[${ts}] 📄 RESL: ${msg.metadata?.toolName || 'Tool'}${lines}\n`;
218
+ break;
219
+ }
220
+
221
+ case 'thinking': {
222
+ const thinkLabel = `[ 🤔 THINKING ] `;
223
+ const thinkWidth = 80;
224
+ const thinkTop = `┌─${thinkLabel}${'─'.repeat(Math.max(0, thinkWidth - thinkLabel.length - 2))}`;
225
+ const thinkBottom = `└─${'─'.repeat(thinkWidth - 2)}`;
226
+
227
+ formatted = `[${ts}] ${thinkTop}\n`;
228
+ for (const line of msg.content.trim().split('\n')) {
229
+ formatted += `[${ts}] │ ${line}\n`;
230
+ }
231
+ formatted += `[${ts}] ${thinkBottom}\n`;
232
+ break;
233
+ }
234
+
235
+ default:
236
+ formatted = `[${ts}] ${msg.content}\n`;
237
+ }
238
+
239
+ try {
240
+ fs.writeSync(this.readableLogFd, formatted);
241
+ } catch { /* Ignore write errors */ }
242
+ }
243
+
244
+ private rotateIfNeeded(logPath: string): void {
245
+ if (!fs.existsSync(logPath)) return;
246
+ try {
247
+ if (fs.statSync(logPath).size >= this.config.maxFileSize) {
248
+ this.rotateLog(logPath);
249
+ }
250
+ } catch { /* Ignore */ }
251
+ }
252
+
253
+ private rotateLog(logPath: string): void {
254
+ const dir = path.dirname(logPath);
255
+ const ext = path.extname(logPath);
256
+ const base = path.basename(logPath, ext);
257
+
258
+ for (let i = this.config.maxFiles - 1; i >= 1; i--) {
259
+ const oldPath = path.join(dir, `${base}.${i}${ext}`);
260
+ const newPath = path.join(dir, `${base}.${i + 1}${ext}`);
261
+ if (fs.existsSync(oldPath)) {
262
+ if (i === this.config.maxFiles - 1) {
263
+ fs.unlinkSync(oldPath);
264
+ } else {
265
+ fs.renameSync(oldPath, newPath);
266
+ }
267
+ }
268
+ }
269
+ fs.renameSync(logPath, path.join(dir, `${base}.1${ext}`));
270
+ }
271
+
272
+ private writeToCleanLog(data: string): void {
273
+ if (this.cleanLogFd === null) return;
274
+ const buffer = Buffer.from(data);
275
+ fs.writeSync(this.cleanLogFd, buffer);
276
+ this.cleanLogSize += buffer.length;
277
+
278
+ if (this.cleanLogSize >= this.config.maxFileSize) {
279
+ fs.closeSync(this.cleanLogFd);
280
+ this.rotateLog(this.cleanLogPath);
281
+ this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
282
+ this.cleanLogSize = 0;
283
+ }
284
+ }
285
+
286
+ private writeToRawLog(data: string): void {
287
+ if (this.rawLogFd === null) return;
288
+ const buffer = Buffer.from(data);
289
+ fs.writeSync(this.rawLogFd, buffer);
290
+ this.rawLogSize += buffer.length;
291
+
292
+ if (this.rawLogSize >= this.config.maxFileSize) {
293
+ fs.closeSync(this.rawLogFd);
294
+ this.rotateLog(this.rawLogPath);
295
+ this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
296
+ this.rawLogSize = 0;
297
+ }
298
+ }
299
+
300
+ private writeJsonEntry(entry: JsonLogEntry): void {
301
+ if (this.jsonLogFd === null) return;
302
+ fs.writeSync(this.jsonLogFd, JSON.stringify(entry) + '\n');
303
+ }
304
+
305
+ writeStdout(data: Buffer | string): void {
306
+ const text = data.toString();
307
+ if (this.config.keepRawLogs) this.writeToRawLog(text);
308
+ if (this.cleanTransform) this.cleanTransform.write(data);
309
+
310
+ this.lineBuffer += text;
311
+ const lines = this.lineBuffer.split('\n');
312
+ this.lineBuffer = lines.pop() || '';
313
+
314
+ for (const line of lines) {
315
+ const cleanLine = stripAnsi(line).trim();
316
+ if (!cleanLine) continue;
317
+
318
+ if (cleanLine.startsWith('{')) {
319
+ if (this.streamingParser) this.streamingParser.parseLine(cleanLine);
320
+ if (this.config.writeJsonLog) {
321
+ try {
322
+ const json = JSON.parse(cleanLine);
323
+ this.writeJsonEntry({
324
+ timestamp: new Date().toISOString(),
325
+ level: 'stdout',
326
+ lane: this.session.laneName,
327
+ task: this.session.taskName,
328
+ message: cleanLine.substring(0, 2000),
329
+ metadata: json,
330
+ });
331
+ continue;
332
+ } catch { /* Not JSON */ }
333
+ }
334
+ }
335
+
336
+ if (this.config.writeJsonLog && !this.isNoise(cleanLine)) {
337
+ this.writeJsonEntry({
338
+ timestamp: new Date().toISOString(),
339
+ level: 'stdout',
340
+ lane: this.session.laneName,
341
+ task: this.session.taskName,
342
+ message: cleanLine.substring(0, 1000),
343
+ });
344
+ }
345
+ }
346
+ }
347
+
348
+ writeStderr(data: Buffer | string): void {
349
+ const text = data.toString();
350
+ if (this.config.keepRawLogs) this.writeToRawLog(text);
351
+ if (this.cleanTransform) this.cleanTransform.write(data);
352
+
353
+ if (this.readableLogFd !== null) {
354
+ for (const line of text.split('\n')) {
355
+ const cleanLine = stripAnsi(line).trim();
356
+ if (cleanLine && !this.isNoise(cleanLine)) {
357
+ try {
358
+ fs.writeSync(this.readableLogFd, `[${new Date().toISOString()}] ❌ STDERR: ${cleanLine}\n`);
359
+ } catch { /* Ignore */ }
360
+ }
361
+ }
362
+ }
363
+
364
+ if (this.config.writeJsonLog) {
365
+ const cleanText = stripAnsi(text).trim();
366
+ if (cleanText) {
367
+ this.writeJsonEntry({
368
+ timestamp: new Date().toISOString(),
369
+ level: 'stderr',
370
+ lane: this.session.laneName,
371
+ task: this.session.taskName,
372
+ message: cleanText.substring(0, 1000),
373
+ });
374
+ }
375
+ }
376
+ }
377
+
378
+ log(level: 'info' | 'error' | 'debug', message: string, metadata?: Record<string, any>): void {
379
+ const ts = formatTimestampISO(this.config.timestampFormat, this.session.startTime);
380
+ const line = `[${ts}] [${level.toUpperCase().padEnd(5)}] ${message}\n`;
381
+ this.writeToCleanLog(line);
382
+ if (this.config.keepRawLogs) this.writeToRawLog(line);
383
+
384
+ if (this.readableLogFd !== null) {
385
+ const icon = level === 'error' ? '❌ ERROR' : level === 'info' ? 'ℹ️ INFO' : '🔍 DEBUG';
386
+ try {
387
+ fs.writeSync(this.readableLogFd, `${new Date().toISOString()} ${icon}: ${message}\n`);
388
+ } catch { /* Ignore */ }
389
+ }
390
+
391
+ if (this.config.writeJsonLog) {
392
+ this.writeJsonEntry({
393
+ timestamp: new Date().toISOString(),
394
+ level,
395
+ lane: this.session.laneName,
396
+ task: this.session.taskName,
397
+ message,
398
+ metadata,
399
+ });
400
+ }
401
+ }
402
+
403
+ section(title: string): void {
404
+ const divider = '═'.repeat(78);
405
+ const line = `\n${divider}\n ${title}\n${divider}\n`;
406
+ this.writeToCleanLog(line);
407
+ if (this.config.keepRawLogs) this.writeToRawLog(line);
408
+
409
+ if (this.readableLogFd !== null) {
410
+ const ts = new Date().toISOString();
411
+ try {
412
+ fs.writeSync(this.readableLogFd, `[${ts}] ━━━ ${title} ${'━'.repeat(60)}\n`);
413
+ } catch { /* Ignore */ }
414
+ }
415
+ }
416
+
417
+ setTask(taskName: string, model?: string): void {
418
+ this.session.taskName = taskName;
419
+ if (model) this.session.model = model;
420
+ this.section(`Task: ${taskName}${model ? ` (Model: ${model})` : ''}`);
421
+
422
+ if (this.config.writeJsonLog) {
423
+ this.writeJsonEntry({
424
+ timestamp: new Date().toISOString(),
425
+ level: 'info',
426
+ source: 'system',
427
+ lane: this.session.laneName,
428
+ task: taskName,
429
+ message: `Task started: ${taskName}`,
430
+ metadata: { model },
431
+ });
432
+ }
433
+ }
434
+
435
+ private isNoise(text: string): boolean {
436
+ if (!text.trim()) return true;
437
+ const patterns = [/^[\s│├└─┌┐┘┴┬┤]+$/, /^[.\s]+$/, /^[=>\s-]+$/, /^\d+%$/, /^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/];
438
+ return patterns.some(p => p.test(text));
439
+ }
440
+
441
+ getLogPaths(): { clean: string; raw?: string; json?: string; readable: string } {
442
+ return {
443
+ clean: this.cleanLogPath,
444
+ raw: this.config.keepRawLogs ? this.rawLogPath : undefined,
445
+ json: this.config.writeJsonLog ? this.jsonLogPath : undefined,
446
+ readable: this.readableLogPath,
447
+ };
448
+ }
449
+
450
+ getFileDescriptors(): { stdout: number; stderr: number } {
451
+ const fd = this.rawLogFd !== null ? this.rawLogFd : this.cleanLogFd!;
452
+ return { stdout: fd, stderr: fd };
453
+ }
454
+
455
+ close(): void {
456
+ if (this.cleanTransform) this.cleanTransform.end();
457
+
458
+ if (this.streamingParser) {
459
+ if (this.lineBuffer.trim()) this.streamingParser.parseLine(this.lineBuffer);
460
+ this.streamingParser.flush();
461
+ }
462
+
463
+ const endMarker = `
464
+ ╔══════════════════════════════════════════════════════════════════════════════╗
465
+ ║ Session Ended: ${new Date().toISOString().padEnd(60)}║
466
+ ║ Duration: ${this.formatDuration(Date.now() - this.session.startTime).padEnd(65)}║
467
+ ╚══════════════════════════════════════════════════════════════════════════════╝
468
+
469
+ `;
470
+
471
+ if (this.cleanLogFd !== null) {
472
+ fs.writeSync(this.cleanLogFd, endMarker);
473
+ fs.closeSync(this.cleanLogFd);
474
+ this.cleanLogFd = null;
475
+ }
476
+
477
+ if (this.rawLogFd !== null) {
478
+ fs.writeSync(this.rawLogFd, endMarker);
479
+ fs.closeSync(this.rawLogFd);
480
+ this.rawLogFd = null;
481
+ }
482
+
483
+ if (this.jsonLogFd !== null) {
484
+ this.writeJsonEntry({
485
+ timestamp: new Date().toISOString(),
486
+ level: 'session',
487
+ source: 'system',
488
+ lane: this.session.laneName,
489
+ message: 'Session ended',
490
+ metadata: { sessionId: this.session.id, duration: Date.now() - this.session.startTime },
491
+ });
492
+ fs.closeSync(this.jsonLogFd);
493
+ this.jsonLogFd = null;
494
+ }
495
+
496
+ if (this.readableLogFd !== null) {
497
+ fs.writeSync(this.readableLogFd, endMarker);
498
+ fs.closeSync(this.readableLogFd);
499
+ this.readableLogFd = null;
500
+ }
501
+ }
502
+
503
+ private formatDuration(ms: number): string {
504
+ const seconds = Math.floor((ms / 1000) % 60);
505
+ const minutes = Math.floor((ms / (1000 * 60)) % 60);
506
+ const hours = Math.floor(ms / (1000 * 60 * 60));
507
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
508
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
509
+ return `${seconds}s`;
510
+ }
511
+ }
512
+
513
+ export function createLogManager(
514
+ laneRunDir: string,
515
+ laneName: string,
516
+ config?: Partial<EnhancedLogConfig>,
517
+ onParsedMessage?: (msg: ParsedMessage) => void
518
+ ): EnhancedLogManager {
519
+ const session: LogSession = {
520
+ id: `${laneName}-${Date.now().toString(36)}`,
521
+ laneName,
522
+ startTime: Date.now(),
523
+ };
524
+ return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
525
+ }
526
+