@litmers/cursorflow-orchestrator 0.1.31 → 0.1.36

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 (150) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +182 -59
  3. package/commands/cursorflow-add.md +159 -0
  4. package/commands/cursorflow-doctor.md +45 -23
  5. package/commands/cursorflow-monitor.md +23 -2
  6. package/commands/cursorflow-new.md +87 -0
  7. package/commands/cursorflow-run.md +60 -111
  8. package/dist/cli/add.d.ts +7 -0
  9. package/dist/cli/add.js +377 -0
  10. package/dist/cli/add.js.map +1 -0
  11. package/dist/cli/clean.js +1 -0
  12. package/dist/cli/clean.js.map +1 -1
  13. package/dist/cli/config.d.ts +7 -0
  14. package/dist/cli/config.js +181 -0
  15. package/dist/cli/config.js.map +1 -0
  16. package/dist/cli/doctor.js +47 -4
  17. package/dist/cli/doctor.js.map +1 -1
  18. package/dist/cli/index.js +34 -30
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/logs.js +17 -34
  21. package/dist/cli/logs.js.map +1 -1
  22. package/dist/cli/monitor.js +62 -65
  23. package/dist/cli/monitor.js.map +1 -1
  24. package/dist/cli/new.d.ts +7 -0
  25. package/dist/cli/new.js +232 -0
  26. package/dist/cli/new.js.map +1 -0
  27. package/dist/cli/prepare.js +95 -193
  28. package/dist/cli/prepare.js.map +1 -1
  29. package/dist/cli/resume.js +57 -68
  30. package/dist/cli/resume.js.map +1 -1
  31. package/dist/cli/run.js +60 -30
  32. package/dist/cli/run.js.map +1 -1
  33. package/dist/cli/stop.js +6 -0
  34. package/dist/cli/stop.js.map +1 -1
  35. package/dist/cli/tasks.d.ts +5 -3
  36. package/dist/cli/tasks.js +181 -29
  37. package/dist/cli/tasks.js.map +1 -1
  38. package/dist/core/failure-policy.d.ts +9 -0
  39. package/dist/core/failure-policy.js +9 -0
  40. package/dist/core/failure-policy.js.map +1 -1
  41. package/dist/core/orchestrator.d.ts +20 -6
  42. package/dist/core/orchestrator.js +215 -334
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/runner/agent.d.ts +27 -0
  45. package/dist/core/runner/agent.js +294 -0
  46. package/dist/core/runner/agent.js.map +1 -0
  47. package/dist/core/runner/index.d.ts +5 -0
  48. package/dist/core/runner/index.js +22 -0
  49. package/dist/core/runner/index.js.map +1 -0
  50. package/dist/core/runner/pipeline.d.ts +9 -0
  51. package/dist/core/runner/pipeline.js +539 -0
  52. package/dist/core/runner/pipeline.js.map +1 -0
  53. package/dist/core/runner/prompt.d.ts +25 -0
  54. package/dist/core/runner/prompt.js +175 -0
  55. package/dist/core/runner/prompt.js.map +1 -0
  56. package/dist/core/runner/task.d.ts +26 -0
  57. package/dist/core/runner/task.js +283 -0
  58. package/dist/core/runner/task.js.map +1 -0
  59. package/dist/core/runner/utils.d.ts +37 -0
  60. package/dist/core/runner/utils.js +161 -0
  61. package/dist/core/runner/utils.js.map +1 -0
  62. package/dist/core/runner.d.ts +2 -96
  63. package/dist/core/runner.js +11 -1136
  64. package/dist/core/runner.js.map +1 -1
  65. package/dist/core/stall-detection.d.ts +326 -0
  66. package/dist/core/stall-detection.js +781 -0
  67. package/dist/core/stall-detection.js.map +1 -0
  68. package/dist/services/logging/console.js +2 -1
  69. package/dist/services/logging/console.js.map +1 -1
  70. package/dist/types/config.d.ts +6 -6
  71. package/dist/types/flow.d.ts +84 -0
  72. package/dist/types/flow.js +10 -0
  73. package/dist/types/flow.js.map +1 -0
  74. package/dist/types/index.d.ts +1 -0
  75. package/dist/types/index.js +3 -3
  76. package/dist/types/index.js.map +1 -1
  77. package/dist/types/lane.d.ts +0 -2
  78. package/dist/types/logging.d.ts +5 -1
  79. package/dist/types/task.d.ts +7 -11
  80. package/dist/utils/config.d.ts +5 -1
  81. package/dist/utils/config.js +15 -16
  82. package/dist/utils/config.js.map +1 -1
  83. package/dist/utils/dependency.d.ts +36 -1
  84. package/dist/utils/dependency.js +256 -1
  85. package/dist/utils/dependency.js.map +1 -1
  86. package/dist/utils/doctor.js +40 -8
  87. package/dist/utils/doctor.js.map +1 -1
  88. package/dist/utils/enhanced-logger.d.ts +45 -82
  89. package/dist/utils/enhanced-logger.js +239 -844
  90. package/dist/utils/enhanced-logger.js.map +1 -1
  91. package/dist/utils/flow.d.ts +9 -0
  92. package/dist/utils/flow.js +73 -0
  93. package/dist/utils/flow.js.map +1 -0
  94. package/dist/utils/git.d.ts +29 -0
  95. package/dist/utils/git.js +115 -5
  96. package/dist/utils/git.js.map +1 -1
  97. package/dist/utils/state.js +0 -2
  98. package/dist/utils/state.js.map +1 -1
  99. package/dist/utils/task-service.d.ts +2 -2
  100. package/dist/utils/task-service.js +40 -31
  101. package/dist/utils/task-service.js.map +1 -1
  102. package/package.json +4 -3
  103. package/src/cli/add.ts +397 -0
  104. package/src/cli/clean.ts +1 -0
  105. package/src/cli/config.ts +177 -0
  106. package/src/cli/doctor.ts +48 -4
  107. package/src/cli/index.ts +36 -32
  108. package/src/cli/logs.ts +20 -33
  109. package/src/cli/monitor.ts +70 -75
  110. package/src/cli/new.ts +235 -0
  111. package/src/cli/prepare.ts +98 -205
  112. package/src/cli/resume.ts +61 -76
  113. package/src/cli/run.ts +333 -306
  114. package/src/cli/stop.ts +8 -0
  115. package/src/cli/tasks.ts +200 -21
  116. package/src/core/failure-policy.ts +9 -0
  117. package/src/core/orchestrator.ts +279 -379
  118. package/src/core/runner/agent.ts +314 -0
  119. package/src/core/runner/index.ts +6 -0
  120. package/src/core/runner/pipeline.ts +567 -0
  121. package/src/core/runner/prompt.ts +174 -0
  122. package/src/core/runner/task.ts +320 -0
  123. package/src/core/runner/utils.ts +142 -0
  124. package/src/core/runner.ts +8 -1347
  125. package/src/core/stall-detection.ts +936 -0
  126. package/src/services/logging/console.ts +2 -1
  127. package/src/types/config.ts +6 -6
  128. package/src/types/flow.ts +91 -0
  129. package/src/types/index.ts +15 -3
  130. package/src/types/lane.ts +0 -2
  131. package/src/types/logging.ts +5 -1
  132. package/src/types/task.ts +7 -11
  133. package/src/utils/config.ts +16 -17
  134. package/src/utils/dependency.ts +311 -2
  135. package/src/utils/doctor.ts +36 -8
  136. package/src/utils/enhanced-logger.ts +264 -927
  137. package/src/utils/flow.ts +42 -0
  138. package/src/utils/git.ts +145 -5
  139. package/src/utils/state.ts +0 -2
  140. package/src/utils/task-service.ts +48 -40
  141. package/commands/cursorflow-review.md +0 -56
  142. package/commands/cursorflow-runs.md +0 -59
  143. package/dist/cli/runs.d.ts +0 -5
  144. package/dist/cli/runs.js +0 -214
  145. package/dist/cli/runs.js.map +0 -1
  146. package/dist/core/reviewer.d.ts +0 -66
  147. package/dist/core/reviewer.js +0 -265
  148. package/dist/core/reviewer.js.map +0 -1
  149. package/src/cli/runs.ts +0 -212
  150. package/src/core/reviewer.ts +0 -285
@@ -1,24 +1,31 @@
1
1
  /**
2
- * Enhanced Logger - Comprehensive terminal output capture and management
2
+ * Enhanced Logger - Simplified terminal output capture
3
3
  *
4
4
  * Features:
5
- * - ANSI escape sequence stripping for clean logs
6
- * - Automatic timestamps on each line
7
- * - Log rotation and size management
8
- * - Session headers with context
9
- * - Raw and clean log file options
10
- * - Structured JSON logs for programmatic access
11
- * - Streaming output support for real-time capture
5
+ * - Raw log: Original output as-is
6
+ * - Readable log: Formatted with formatMessageForConsole style
12
7
  */
13
8
 
14
9
  import * as fs from 'fs';
15
10
  import * as path from 'path';
16
- import { Transform, TransformCallback } from 'stream';
17
- import * as logger from './logger';
18
- import { EnhancedLogConfig, ParsedMessage, JsonLogEntry, LogSession } from '../types';
19
- export { EnhancedLogConfig, ParsedMessage, JsonLogEntry, LogSession };
11
+ import { EnhancedLogConfig, ParsedMessage, LogSession } from '../types';
12
+ import { formatMessageForConsole } from './log-formatter';
20
13
  import { safeJoin } from './path';
21
14
 
15
+ export { EnhancedLogConfig, ParsedMessage, LogSession };
16
+
17
+ // Re-export JsonLogEntry for backward compatibility (empty type)
18
+ export interface JsonLogEntry {
19
+ timestamp: string;
20
+ level: string;
21
+ lane?: string;
22
+ task?: string;
23
+ message: string;
24
+ metadata?: Record<string, any>;
25
+ source?: string;
26
+ raw?: string;
27
+ }
28
+
22
29
  export const DEFAULT_LOG_CONFIG: EnhancedLogConfig = {
23
30
  enabled: true,
24
31
  stripAnsi: true,
@@ -28,199 +35,14 @@ export const DEFAULT_LOG_CONFIG: EnhancedLogConfig = {
28
35
  keepRawLogs: true,
29
36
  keepAbsoluteRawLogs: false,
30
37
  raw: false,
31
- writeJsonLog: true,
38
+ writeJsonLog: false, // Disabled by default now
32
39
  timestampFormat: 'iso',
33
40
  };
34
41
 
35
- /**
36
- * Streaming JSON Parser - Parses cursor-agent stream-json output
37
- * and combines tokens into readable messages
38
- */
39
- export class StreamingMessageParser {
40
- private currentMessage: string = '';
41
- private currentRole: string = '';
42
- private messageStartTime: number = 0;
43
- private onMessage: (msg: ParsedMessage) => void;
44
-
45
- constructor(onMessage: (msg: ParsedMessage) => void) {
46
- this.onMessage = onMessage;
47
- }
48
-
49
- /**
50
- * Parse a line of JSON output from cursor-agent
51
- */
52
- parseLine(line: string): void {
53
- const trimmed = line.trim();
54
- if (!trimmed || !trimmed.startsWith('{')) return;
55
-
56
- try {
57
- const json = JSON.parse(trimmed);
58
- this.handleJsonMessage(json);
59
- } catch {
60
- // Not valid JSON, ignore
61
- }
62
- }
63
-
64
- private handleJsonMessage(json: any): void {
65
- const type = json.type;
66
-
67
- switch (type) {
68
- case 'system':
69
- // System init message
70
- this.emitMessage({
71
- type: 'system',
72
- role: 'system',
73
- content: `[System] Model: ${json.model || 'unknown'}, Mode: ${json.permissionMode || 'default'}`,
74
- timestamp: json.timestamp_ms || Date.now(),
75
- });
76
- break;
77
-
78
- case 'user':
79
- // User message - emit as complete message
80
- if (json.message?.content) {
81
- const textContent = json.message.content
82
- .filter((c: any) => c.type === 'text')
83
- .map((c: any) => c.text)
84
- .join('');
85
-
86
- this.emitMessage({
87
- type: 'user',
88
- role: 'user',
89
- content: textContent,
90
- timestamp: json.timestamp_ms || Date.now(),
91
- });
92
- }
93
- break;
94
-
95
- case 'assistant':
96
- // Streaming assistant message - accumulate tokens
97
- if (json.message?.content) {
98
- const textContent = json.message.content
99
- .filter((c: any) => c.type === 'text')
100
- .map((c: any) => c.text)
101
- .join('');
102
-
103
- // Check if this is a new message or continuation
104
- if (this.currentRole !== 'assistant') {
105
- // Flush previous message if any
106
- this.flush();
107
- this.currentRole = 'assistant';
108
- this.messageStartTime = json.timestamp_ms || Date.now();
109
- }
110
-
111
- this.currentMessage += textContent;
112
- }
113
- break;
114
-
115
- case 'tool_call':
116
- // Tool call - emit as formatted message
117
- if (json.subtype === 'started' && json.tool_call) {
118
- const toolName = Object.keys(json.tool_call)[0] || 'unknown';
119
- const toolArgs = json.tool_call[toolName]?.args || {};
120
-
121
- this.flush(); // Flush any pending assistant message
122
-
123
- this.emitMessage({
124
- type: 'tool',
125
- role: 'tool',
126
- content: `[Tool: ${toolName}] ${JSON.stringify(toolArgs)}`,
127
- timestamp: json.timestamp_ms || Date.now(),
128
- metadata: { callId: json.call_id, toolName },
129
- });
130
- } else if (json.subtype === 'completed' && json.tool_call) {
131
- const toolName = Object.keys(json.tool_call)[0] || 'unknown';
132
- const result = json.tool_call[toolName]?.result;
133
-
134
- if (result?.success) {
135
- // Truncate large results
136
- const content = result.success.content || '';
137
- const truncated = content.length > 500
138
- ? content.substring(0, 500) + '... (truncated)'
139
- : content;
140
-
141
- this.emitMessage({
142
- type: 'tool_result',
143
- role: 'tool',
144
- content: `[Tool Result: ${toolName}] ${truncated}`,
145
- timestamp: json.timestamp_ms || Date.now(),
146
- metadata: { callId: json.call_id, toolName, lines: result.success.totalLines },
147
- });
148
- }
149
- }
150
- break;
151
-
152
- case 'result':
153
- // Final result - flush any pending and emit result
154
- this.flush();
155
-
156
- this.emitMessage({
157
- type: 'result',
158
- role: 'assistant',
159
- content: json.result || '',
160
- timestamp: json.timestamp_ms || Date.now(),
161
- metadata: {
162
- duration_ms: json.duration_ms,
163
- is_error: json.is_error,
164
- subtype: json.subtype,
165
- },
166
- });
167
- break;
168
-
169
- case 'thinking':
170
- // Thinking message (Claude 3.7+ etc.)
171
- if (json.subtype === 'delta' && json.text) {
172
- // Check if this is a new message or continuation
173
- if (this.currentRole !== 'thinking') {
174
- // Flush previous message if any
175
- this.flush();
176
- this.currentRole = 'thinking';
177
- this.messageStartTime = json.timestamp_ms || Date.now();
178
- }
179
- this.currentMessage += json.text;
180
- } else if (json.subtype === 'completed') {
181
- // Thinking completed - flush immediately
182
- this.flush();
183
- }
184
- break;
185
- }
186
- }
187
-
188
- /**
189
- * Flush accumulated message
190
- */
191
- flush(): void {
192
- if (this.currentMessage && this.currentRole) {
193
- this.emitMessage({
194
- type: this.currentRole as any,
195
- role: this.currentRole,
196
- content: this.currentMessage,
197
- timestamp: this.messageStartTime,
198
- });
199
- }
200
- this.currentMessage = '';
201
- this.currentRole = '';
202
- this.messageStartTime = 0;
203
- }
204
-
205
- private emitMessage(msg: ParsedMessage): void {
206
- if (msg.content.trim()) {
207
- this.onMessage(msg);
208
- }
209
- }
210
- }
211
-
212
42
  /**
213
43
  * ANSI escape sequence regex pattern
214
- * Matches:
215
- * - CSI sequences (colors, cursor movement, etc.)
216
- * - OSC sequences (terminal titles, etc.)
217
- * - Single-character escape codes
218
44
  */
219
45
  const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
220
-
221
- /**
222
- * Extended ANSI regex for more complete stripping
223
- */
224
46
  const EXTENDED_ANSI_REGEX = /(?:\x1B[@-Z\\-_]|\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07]*(?:\x07|\x1B\\)|\x1B[PX^_][^\x1B]*\x1B\\|\x1B.)/g;
225
47
 
226
48
  /**
@@ -230,14 +52,12 @@ export function stripAnsi(text: string): string {
230
52
  return text
231
53
  .replace(EXTENDED_ANSI_REGEX, '')
232
54
  .replace(ANSI_REGEX, '')
233
- // Also remove carriage returns that overwrite lines (progress bars, etc.)
234
55
  .replace(/\r[^\n]/g, '\n')
235
- // Clean up any remaining control characters except newlines/tabs
236
56
  .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
237
57
  }
238
58
 
239
59
  /**
240
- * Format timestamp based on format preference
60
+ * Format timestamp
241
61
  */
242
62
  export function formatTimestamp(format: 'iso' | 'relative' | 'short', startTime?: number): string {
243
63
  const now = Date.now();
@@ -269,122 +89,25 @@ export function formatTimestamp(format: 'iso' | 'relative' | 'short', startTime?
269
89
  }
270
90
 
271
91
  /**
272
- * Regex to detect if a line already has a timestamp at the start
273
- * Matches:
274
- * - ISO format: [2024-01-01T12:34:56...]
275
- * - Short format: [12:34:56]
276
- */
277
- const EXISTING_TIMESTAMP_REGEX = /^\[(\d{4}-\d{2}-\d{2}T)?\d{2}:\d{2}:\d{2}/;
278
-
279
- /**
280
- * Check if a line already has a timestamp
281
- */
282
- function hasExistingTimestamp(line: string): boolean {
283
- return EXISTING_TIMESTAMP_REGEX.test(line.trim());
284
- }
285
-
286
- /**
287
- * Transform stream that strips ANSI and adds timestamps
288
- */
289
- export class CleanLogTransform extends Transform {
290
- private config: EnhancedLogConfig;
291
- private session: LogSession;
292
- private buffer: string = '';
293
-
294
- constructor(config: EnhancedLogConfig, session: LogSession) {
295
- super({ encoding: 'utf8' });
296
- this.config = config;
297
- this.session = session;
298
- }
299
-
300
- _transform(chunk: Buffer, encoding: BufferEncoding, callback: TransformCallback): void {
301
- let text = chunk.toString();
302
-
303
- // Buffer partial lines
304
- this.buffer += text;
305
- const lines = this.buffer.split('\n');
306
-
307
- // Keep the last incomplete line in buffer
308
- this.buffer = lines.pop() || '';
309
-
310
- for (const line of lines) {
311
- let processed = line;
312
-
313
- // Strip ANSI if enabled
314
- if (this.config.stripAnsi) {
315
- processed = stripAnsi(processed);
316
- }
317
-
318
- // Add timestamp if enabled AND line doesn't already have one
319
- if (this.config.addTimestamps && processed.trim() && !hasExistingTimestamp(processed)) {
320
- const ts = formatTimestamp(this.config.timestampFormat, this.session.startTime);
321
- processed = `[${ts}] ${processed}`;
322
- }
323
-
324
- this.push(processed + '\n');
325
- }
326
-
327
- callback();
328
- }
329
-
330
- _flush(callback: TransformCallback): void {
331
- // Process any remaining buffered content
332
- if (this.buffer.trim()) {
333
- let processed = this.buffer;
334
-
335
- if (this.config.stripAnsi) {
336
- processed = stripAnsi(processed);
337
- }
338
-
339
- if (this.config.addTimestamps && processed.trim() && !hasExistingTimestamp(processed)) {
340
- const ts = formatTimestamp(this.config.timestampFormat, this.session.startTime);
341
- processed = `[${ts}] ${processed}`;
342
- }
343
-
344
- this.push(processed + '\n');
345
- }
346
-
347
- callback();
348
- }
349
- }
350
-
351
- /**
352
- * Enhanced Log Manager - Manages log files with rotation and multiple outputs
92
+ * Simplified Log Manager - Only raw and readable logs
353
93
  */
354
94
  export class EnhancedLogManager {
355
95
  private config: EnhancedLogConfig;
356
96
  private session: LogSession;
357
97
  private logDir: string;
358
98
 
359
- private cleanLogPath: string;
360
99
  private rawLogPath: string;
361
- private absoluteRawLogPath: string;
362
- private jsonLogPath: string;
363
100
  private readableLogPath: string;
364
101
 
365
- private cleanLogFd: number | null = null;
366
102
  private rawLogFd: number | null = null;
367
- private absoluteRawLogFd: number | null = null;
368
- private jsonLogFd: number | null = null;
369
103
  private readableLogFd: number | null = null;
370
104
 
371
- private cleanLogSize: number = 0;
372
105
  private rawLogSize: number = 0;
373
- private absoluteRawLogSize: number = 0;
374
106
 
375
- private cleanTransform: CleanLogTransform | null = null;
376
- private streamingParser: StreamingMessageParser | null = null;
377
- private lineBuffer: string = '';
378
107
  private onParsedMessage?: (msg: ParsedMessage) => void;
379
108
 
380
109
  constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}, onParsedMessage?: (msg: ParsedMessage) => void) {
381
110
  this.config = { ...DEFAULT_LOG_CONFIG, ...config };
382
-
383
- // Support 'raw' as alias for 'keepAbsoluteRawLogs'
384
- if (this.config.raw) {
385
- this.config.keepAbsoluteRawLogs = true;
386
- }
387
-
388
111
  this.session = session;
389
112
  this.logDir = logDir;
390
113
  this.onParsedMessage = onParsedMessage;
@@ -392,163 +115,55 @@ export class EnhancedLogManager {
392
115
  // Ensure log directory exists
393
116
  fs.mkdirSync(logDir, { recursive: true });
394
117
 
395
- // Set up log file paths
396
- this.cleanLogPath = safeJoin(logDir, 'terminal.log');
118
+ // Set up log file paths (simplified)
397
119
  this.rawLogPath = safeJoin(logDir, 'terminal-raw.log');
398
- this.absoluteRawLogPath = safeJoin(logDir, 'terminal-absolute-raw.log');
399
- this.jsonLogPath = safeJoin(logDir, 'terminal.jsonl');
400
120
  this.readableLogPath = safeJoin(logDir, 'terminal-readable.log');
401
-
402
- if (this.config.raw) {
403
- logger.info(`[${session.laneName}] 📄 Raw data capture enabled: ${this.absoluteRawLogPath}`);
404
- }
405
121
 
406
122
  // Initialize log files
407
123
  this.initLogFiles();
408
124
  }
409
125
 
126
+ /**
127
+ * Get short time format (HH:MM:SS)
128
+ */
129
+ private getShortTime(): string {
130
+ return new Date().toLocaleTimeString('en-US', { hour12: false });
131
+ }
132
+
133
+ /**
134
+ * Get lane-task label like [L1-T2-lanename10]
135
+ */
136
+ private getLaneTaskLabel(): string {
137
+ const laneNum = (this.session.laneIndex ?? 0) + 1;
138
+ const taskNum = (this.session.taskIndex ?? 0) + 1;
139
+ const shortLaneName = this.session.laneName.substring(0, 10);
140
+ return `L${laneNum}-T${taskNum}-${shortLaneName}`;
141
+ }
142
+
410
143
  /**
411
144
  * Initialize log files and write session headers
412
145
  */
413
146
  private initLogFiles(): void {
414
147
  // Check and rotate if necessary
415
- this.rotateIfNeeded(this.cleanLogPath, 'clean');
416
- if (this.config.keepRawLogs) {
417
- this.rotateIfNeeded(this.rawLogPath, 'raw');
418
- }
419
- if (this.config.keepAbsoluteRawLogs) {
420
- this.rotateIfNeeded(this.absoluteRawLogPath, 'raw');
421
- }
422
-
423
- // Open file descriptors
424
- this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
425
-
426
148
  if (this.config.keepRawLogs) {
149
+ this.rotateIfNeeded(this.rawLogPath);
427
150
  this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
428
151
  }
429
-
430
- if (this.config.keepAbsoluteRawLogs) {
431
- this.absoluteRawLogFd = fs.openSync(this.absoluteRawLogPath, 'a');
432
- }
433
152
 
434
- if (this.config.writeJsonLog) {
435
- this.jsonLogFd = fs.openSync(this.jsonLogPath, 'a');
436
- }
437
-
438
- // Open readable log file (for parsed streaming output)
153
+ this.rotateIfNeeded(this.readableLogPath);
439
154
  this.readableLogFd = fs.openSync(this.readableLogPath, 'a');
440
155
 
441
- // Get initial file sizes
442
- try {
443
- this.cleanLogSize = fs.statSync(this.cleanLogPath).size;
444
- if (this.config.keepRawLogs) {
156
+ // Get initial file size for raw log if enabled
157
+ if (this.rawLogFd !== null) {
158
+ try {
445
159
  this.rawLogSize = fs.statSync(this.rawLogPath).size;
160
+ } catch {
161
+ this.rawLogSize = 0;
446
162
  }
447
- if (this.config.keepAbsoluteRawLogs) {
448
- this.absoluteRawLogSize = fs.statSync(this.absoluteRawLogPath).size;
449
- }
450
- } catch {
451
- this.cleanLogSize = 0;
452
- this.rawLogSize = 0;
453
- this.absoluteRawLogSize = 0;
454
163
  }
455
164
 
456
165
  // Write session header
457
166
  this.writeSessionHeader();
458
-
459
- // Create transform stream
460
- this.cleanTransform = new CleanLogTransform(this.config, this.session);
461
- this.cleanTransform.on('data', (data: string) => {
462
- this.writeToCleanLog(data);
463
- });
464
-
465
- // Create streaming parser for readable log
466
- this.streamingParser = new StreamingMessageParser((msg) => {
467
- this.writeReadableMessage(msg);
468
- if (this.onParsedMessage) {
469
- this.onParsedMessage(msg);
470
- }
471
- });
472
- }
473
-
474
- /**
475
- * Write a parsed message to the readable log
476
- */
477
- private writeReadableMessage(msg: ParsedMessage): void {
478
- if (this.readableLogFd === null) return;
479
-
480
- const ts = new Date(msg.timestamp).toISOString();
481
- let formatted: string;
482
-
483
- switch (msg.type) {
484
- case 'system':
485
- formatted = `[${ts}] ⚙️ SYSTEM: ${msg.content}\n`;
486
- break;
487
-
488
- case 'user':
489
- case 'assistant':
490
- case 'result':
491
- // Format with brackets and line (compact)
492
- const isUser = msg.type === 'user';
493
- const isResult = msg.type === 'result';
494
- const headerText = isUser ? '🧑 USER' : isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
495
- const duration = msg.metadata?.duration_ms
496
- ? ` (${Math.round(msg.metadata.duration_ms / 1000)}s)`
497
- : '';
498
-
499
- const label = `[ ${headerText}${duration} ] `;
500
- const totalWidth = 80;
501
- const topBorder = `┌─${label}${'─'.repeat(Math.max(0, totalWidth - label.length - 2))}`;
502
- const bottomBorder = `└─${'─'.repeat(totalWidth - 2)}`;
503
-
504
- const lines = msg.content.split('\n');
505
- formatted = `[${ts}] ${topBorder}\n`;
506
- for (const line of lines) {
507
- formatted += `[${ts}] │ ${line}\n`;
508
- }
509
- formatted += `[${ts}] ${bottomBorder}\n`;
510
- break;
511
-
512
- case 'tool':
513
- formatted = `[${ts}] 🔧 TOOL: ${msg.content}\n`;
514
- break;
515
-
516
- case 'tool_result':
517
- const toolResultLines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
518
- formatted = `[${ts}] 📄 RESL: ${msg.metadata?.toolName || 'Tool'}${toolResultLines}\n`;
519
- break;
520
-
521
- case 'thinking':
522
- // Format thinking block
523
- const thinkLabel = `[ 🤔 THINKING ] `;
524
- const thinkWidth = 80;
525
- const thinkTop = `┌─${thinkLabel}${'─'.repeat(Math.max(0, thinkWidth - thinkLabel.length - 2))}`;
526
- const thinkBottom = `└─${'─'.repeat(thinkWidth - 2)}`;
527
-
528
- const thinkLines = msg.content.trim().split('\n');
529
- formatted = `[${ts}] ${thinkTop}\n`;
530
- for (const line of thinkLines) {
531
- formatted += `[${ts}] │ ${line}\n`;
532
- }
533
- formatted += `[${ts}] ${thinkBottom}\n`;
534
- break;
535
-
536
- default:
537
- formatted = `[${ts}] ${msg.content}\n`;
538
- }
539
-
540
- try {
541
- fs.writeSync(this.readableLogFd, formatted);
542
- } catch {
543
- // Ignore write errors
544
- }
545
- }
546
-
547
- /**
548
- * Indent text with a prefix
549
- */
550
- private indentText(text: string, prefix: string): string {
551
- return text.split('\n').map(line => prefix + line).join('\n');
552
167
  }
553
168
 
554
169
  /**
@@ -568,32 +183,14 @@ export class EnhancedLogManager {
568
183
 
569
184
  `;
570
185
 
571
- this.writeToCleanLog(header);
572
-
573
- if (this.config.keepRawLogs && this.rawLogFd !== null) {
574
- fs.writeSync(this.rawLogFd, header);
575
- }
576
-
577
- // Write JSON session entry
578
- this.writeJsonEntry({
579
- timestamp: new Date(this.session.startTime).toISOString(),
580
- level: 'session',
581
- source: 'system',
582
- lane: this.session.laneName,
583
- task: this.session.taskName,
584
- message: 'Session started',
585
- metadata: {
586
- sessionId: this.session.id,
587
- model: this.session.model,
588
- ...this.session.metadata,
589
- },
590
- });
186
+ this.writeToRawLog(header);
187
+ this.writeToReadableLog(header);
591
188
  }
592
189
 
593
190
  /**
594
191
  * Rotate log file if it exceeds max size
595
192
  */
596
- private rotateIfNeeded(logPath: string, type: 'clean' | 'raw'): void {
193
+ private rotateIfNeeded(logPath: string): void {
597
194
  if (!fs.existsSync(logPath)) return;
598
195
 
599
196
  try {
@@ -633,32 +230,13 @@ export class EnhancedLogManager {
633
230
  fs.renameSync(logPath, rotatedPath);
634
231
  }
635
232
 
636
- /**
637
- * Write to clean log with size tracking
638
- */
639
- private writeToCleanLog(data: string): void {
640
- if (this.cleanLogFd === null) return;
641
-
642
- const buffer = Buffer.from(data);
643
- fs.writeSync(this.cleanLogFd, buffer);
644
- this.cleanLogSize += buffer.length;
645
-
646
- // Check if rotation needed
647
- if (this.cleanLogSize >= this.config.maxFileSize) {
648
- fs.closeSync(this.cleanLogFd);
649
- this.rotateLog(this.cleanLogPath);
650
- this.cleanLogFd = fs.openSync(this.cleanLogPath, 'a');
651
- this.cleanLogSize = 0;
652
- }
653
- }
654
-
655
233
  /**
656
234
  * Write to raw log with size tracking
657
235
  */
658
- private writeToRawLog(data: string): void {
236
+ private writeToRawLog(data: string | Buffer): void {
659
237
  if (this.rawLogFd === null) return;
660
238
 
661
- const buffer = Buffer.from(data);
239
+ const buffer = typeof data === 'string' ? Buffer.from(data) : data;
662
240
  fs.writeSync(this.rawLogFd, buffer);
663
241
  this.rawLogSize += buffer.length;
664
242
 
@@ -672,32 +250,38 @@ export class EnhancedLogManager {
672
250
  }
673
251
 
674
252
  /**
675
- * Write to absolute raw log with size tracking
253
+ * Write to readable log
676
254
  */
677
- private writeToAbsoluteRawLog(data: string | Buffer): void {
678
- if (this.absoluteRawLogFd === null) return;
679
-
680
- const buffer = typeof data === 'string' ? Buffer.from(data) : data;
681
- fs.writeSync(this.absoluteRawLogFd, buffer);
682
- this.absoluteRawLogSize += buffer.length;
255
+ private writeToReadableLog(data: string): void {
256
+ if (this.readableLogFd === null) return;
683
257
 
684
- // Check if rotation needed
685
- if (this.absoluteRawLogSize >= this.config.maxFileSize) {
686
- fs.closeSync(this.absoluteRawLogFd);
687
- this.rotateLog(this.absoluteRawLogPath);
688
- this.absoluteRawLogFd = fs.openSync(this.absoluteRawLogPath, 'a');
689
- this.absoluteRawLogSize = 0;
258
+ try {
259
+ fs.writeSync(this.readableLogFd, data);
260
+ } catch {
261
+ // Ignore write errors
690
262
  }
691
263
  }
692
264
 
693
265
  /**
694
- * Write a JSON log entry
266
+ * Write a parsed message to the readable log using formatMessageForConsole style
695
267
  */
696
- private writeJsonEntry(entry: JsonLogEntry): void {
697
- if (this.jsonLogFd === null) return;
268
+ public writeReadableMessage(msg: ParsedMessage): void {
269
+ // Use formatMessageForConsole for consistent formatting
270
+ // Use short lane-task label like [L01-T02]
271
+ const formatted = formatMessageForConsole(msg, {
272
+ laneLabel: `[${this.getLaneTaskLabel()}]`,
273
+ includeTimestamp: false, // We'll add our own short timestamp
274
+ });
275
+
276
+ // Strip ANSI codes and add short timestamp for file output
277
+ const clean = stripAnsi(formatted);
278
+ const ts = this.getShortTime();
279
+ this.writeToReadableLog(`[${ts}] ${clean}\n`);
698
280
 
699
- const line = JSON.stringify(entry) + '\n';
700
- fs.writeSync(this.jsonLogFd, line);
281
+ // Callback for console output
282
+ if (this.onParsedMessage) {
283
+ this.onParsedMessage(msg);
284
+ }
701
285
  }
702
286
 
703
287
  /**
@@ -706,142 +290,144 @@ export class EnhancedLogManager {
706
290
  public writeStdout(data: Buffer | string): void {
707
291
  const text = data.toString();
708
292
 
709
- // Write absolute raw log
710
- if (this.config.keepAbsoluteRawLogs) {
711
- this.writeToAbsoluteRawLog(data);
712
- }
713
-
714
- // Write raw log
715
- if (this.config.keepRawLogs) {
716
- this.writeToRawLog(text);
717
- }
718
-
719
- // Process through transform for clean log
720
- if (this.cleanTransform) {
721
- this.cleanTransform.write(data);
722
- }
723
-
724
- // Process lines for readable log and JSON entries
725
- this.lineBuffer += text;
726
- const lines = this.lineBuffer.split('\n');
727
- this.lineBuffer = lines.pop() || '';
293
+ // Write raw log (original data)
294
+ this.writeToRawLog(data);
728
295
 
296
+ // Parse JSON output and write to readable log
297
+ const lines = text.split('\n');
729
298
  for (const line of lines) {
730
- const cleanLine = stripAnsi(line).trim();
731
- if (!cleanLine) continue;
732
-
733
- // Handle streaming JSON messages
734
- if (cleanLine.startsWith('{')) {
735
- if (this.streamingParser) {
736
- this.streamingParser.parseLine(cleanLine);
737
- }
738
-
739
- // Special handling for terminal.jsonl entries for AI messages
740
- if (this.config.writeJsonLog) {
741
- try {
742
- const json = JSON.parse(cleanLine);
743
- let displayMsg = cleanLine;
744
- let metadata = { ...json };
745
-
746
- // Extract cleaner text for significant AI message types
747
- if ((json.type === 'thinking' || json.type === 'thought') && (json.text || json.thought)) {
748
- displayMsg = json.text || json.thought;
749
- // Clean up any double newlines at the end of deltas
750
- displayMsg = displayMsg.replace(/\n+$/, '\n');
751
- } else if (json.type === 'assistant' && json.message?.content) {
752
- displayMsg = json.message.content
753
- .filter((c: any) => c.type === 'text')
754
- .map((c: any) => c.text)
755
- .join('');
756
- } else if (json.type === 'user' && json.message?.content) {
757
- displayMsg = json.message.content
758
- .filter((c: any) => c.type === 'text')
759
- .map((c: any) => c.text)
760
- .join('');
761
- } else if (json.type === 'tool_call' && json.subtype === 'started') {
762
- const toolName = Object.keys(json.tool_call)[0] || 'unknown';
763
- const args = json.tool_call[toolName]?.args || {};
764
- displayMsg = `🔧 CALL: ${toolName}(${JSON.stringify(args)})`;
765
- } else if (json.type === 'tool_call' && json.subtype === 'completed') {
766
- const toolName = Object.keys(json.tool_call)[0] || 'unknown';
767
- displayMsg = `📄 RESL: ${toolName}`;
768
- } else if (json.type === 'result') {
769
- displayMsg = json.result || 'Task completed';
770
- }
771
-
772
- this.writeJsonEntry({
773
- timestamp: new Date().toISOString(),
774
- level: 'stdout',
775
- lane: this.session.laneName,
776
- task: this.session.taskName,
777
- message: displayMsg.substring(0, 2000), // Larger limit for AI text
778
- metadata,
779
- });
780
- continue; // Already logged this JSON line
781
- } catch {
782
- // Not valid JSON or error, fall through to regular logging
299
+ const trimmed = line.trim();
300
+ if (!trimmed) continue;
301
+
302
+ // Try to parse as JSON (cursor-agent output)
303
+ if (trimmed.startsWith('{')) {
304
+ try {
305
+ const json = JSON.parse(trimmed);
306
+ const msg = this.parseJsonToMessage(json);
307
+ if (msg) {
308
+ this.writeReadableMessage(msg);
309
+ continue;
783
310
  }
784
- }
785
- } else {
786
- // Parse standard text logs into ParsedMessage
787
- // Format: [HH:MM:SS] [LANE] ℹ️ INFO message
788
- const textLogRegex = /^\[(\d{2}:\d{2}:\d{2})\]\s+\[(.*?)\]\s+(.*?)\s+(INFO|WARN|ERROR|SUCCESS|DONE|PROGRESS)\s+(.*)/;
789
- const match = cleanLine.match(textLogRegex);
790
-
791
- if (match && this.onParsedMessage) {
792
- const [, time, , emoji, level, content] = match;
793
- // Convert HH:MM:SS to a timestamp for today
794
- const [h, m, s] = time!.split(':').map(Number);
795
- const timestamp = new Date().setHours(h!, m!, s!, 0);
796
-
797
- this.onParsedMessage({
798
- type: level!.toLowerCase().replace('done', 'result').replace('success', 'result') as any,
799
- role: 'system',
800
- content: `${emoji} ${content}`,
801
- timestamp,
802
- });
803
- }
804
- }
805
-
806
- // Also include significant info/status lines in readable log (compact)
807
- if (this.readableLogFd !== null) {
808
- // Look for log lines: [ISO_DATE] [LEVEL] ...
809
- if (!this.isNoiseLog(cleanLine) && /\[\d{4}-\d{2}-\d{2}T/.test(cleanLine)) {
810
- try {
811
- // Check if it has a level marker
812
- if (/\[(INFO|WARN|ERROR|SUCCESS|DEBUG)\]/.test(cleanLine)) {
813
- // Special formatting for summary
814
- if (cleanLine.includes('Final Workspace Summary')) {
815
- const tsMatch = cleanLine.match(/\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]/);
816
- const ts = tsMatch ? tsMatch[1] : new Date().toISOString();
817
- fs.writeSync(this.readableLogFd, `[${ts}] 📊 SUMMARY: ${cleanLine.split(']').slice(2).join(']').trim()}\n`);
818
- } else {
819
- fs.writeSync(this.readableLogFd, `${cleanLine}\n`);
820
- }
821
- }
822
- } catch {}
311
+ } catch {
312
+ // Not valid JSON, fall through
823
313
  }
824
314
  }
825
315
 
826
- // Write regular non-JSON lines to terminal.jsonl
827
- if (this.config.writeJsonLog && !this.isNoiseLog(cleanLine)) {
828
- this.writeJsonEntry({
829
- timestamp: new Date().toISOString(),
830
- level: 'stdout',
831
- lane: this.session.laneName,
832
- task: this.session.taskName,
833
- message: cleanLine.substring(0, 1000),
834
- raw: this.config.keepRawLogs ? undefined : line.substring(0, 1000),
835
- });
316
+ // Non-JSON line - write as-is with short timestamp and lane-task label
317
+ const cleanLine = stripAnsi(trimmed);
318
+ if (cleanLine && !this.isNoiseLog(cleanLine)) {
319
+ const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
320
+ const label = this.getLaneTaskLabel();
321
+
322
+ if (hasTimestamp) {
323
+ // If already has timestamp, just ensure label is present
324
+ const formatted = cleanLine.includes(`[${label}]`)
325
+ ? cleanLine
326
+ : cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}]`);
327
+ this.writeToReadableLog(`${formatted}\n`);
328
+ } else {
329
+ const ts = this.getShortTime();
330
+ this.writeToReadableLog(`[${ts}] [${label}] ${cleanLine}\n`);
331
+ }
836
332
  }
837
333
  }
838
334
  }
839
-
335
+
840
336
  /**
841
- * Parse streaming JSON data for readable log - legacy, integrated into writeStdout
337
+ * Parse cursor-agent JSON output to ParsedMessage
842
338
  */
843
- private parseStreamingData(text: string): void {
844
- // Legacy method, no longer used but kept for internal references if any
339
+ private parseJsonToMessage(json: any): ParsedMessage | null {
340
+ const type = json.type;
341
+ const timestamp = json.timestamp_ms || Date.now();
342
+
343
+ switch (type) {
344
+ case 'system':
345
+ return {
346
+ type: 'system',
347
+ role: 'system',
348
+ content: `Model: ${json.model || 'unknown'}, Mode: ${json.permissionMode || 'default'}`,
349
+ timestamp,
350
+ };
351
+
352
+ case 'user':
353
+ if (json.message?.content) {
354
+ const textContent = json.message.content
355
+ .filter((c: any) => c.type === 'text')
356
+ .map((c: any) => c.text)
357
+ .join('');
358
+ return {
359
+ type: 'user',
360
+ role: 'user',
361
+ content: textContent,
362
+ timestamp,
363
+ };
364
+ }
365
+ return null;
366
+
367
+ case 'assistant':
368
+ if (json.message?.content) {
369
+ const textContent = json.message.content
370
+ .filter((c: any) => c.type === 'text')
371
+ .map((c: any) => c.text)
372
+ .join('');
373
+ return {
374
+ type: 'assistant',
375
+ role: 'assistant',
376
+ content: textContent,
377
+ timestamp,
378
+ };
379
+ }
380
+ return null;
381
+
382
+ case 'tool_call':
383
+ if (json.subtype === 'started' && json.tool_call) {
384
+ const toolName = Object.keys(json.tool_call)[0] || 'unknown';
385
+ const toolArgs = json.tool_call[toolName]?.args || {};
386
+ return {
387
+ type: 'tool',
388
+ role: 'tool',
389
+ content: `[Tool: ${toolName}] ${JSON.stringify(toolArgs)}`,
390
+ timestamp,
391
+ metadata: { callId: json.call_id, toolName },
392
+ };
393
+ } else if (json.subtype === 'completed' && json.tool_call) {
394
+ const toolName = Object.keys(json.tool_call)[0] || 'unknown';
395
+ return {
396
+ type: 'tool_result',
397
+ role: 'tool',
398
+ content: `[Tool Result: ${toolName}]`,
399
+ timestamp,
400
+ metadata: { callId: json.call_id, toolName },
401
+ };
402
+ }
403
+ return null;
404
+
405
+ case 'result':
406
+ return {
407
+ type: 'result',
408
+ role: 'assistant',
409
+ content: json.result || '',
410
+ timestamp,
411
+ metadata: {
412
+ duration_ms: json.duration_ms,
413
+ is_error: json.is_error,
414
+ },
415
+ };
416
+
417
+ case 'thinking':
418
+ if (json.text) {
419
+ return {
420
+ type: 'thinking',
421
+ role: 'assistant',
422
+ content: json.text,
423
+ timestamp,
424
+ };
425
+ }
426
+ return null;
427
+
428
+ default:
429
+ return null;
430
+ }
845
431
  }
846
432
 
847
433
  /**
@@ -850,87 +436,41 @@ export class EnhancedLogManager {
850
436
  public writeStderr(data: Buffer | string): void {
851
437
  const text = data.toString();
852
438
 
853
- // Write absolute raw log
854
- if (this.config.keepAbsoluteRawLogs) {
855
- this.writeToAbsoluteRawLog(data);
856
- }
857
-
858
439
  // Write raw log
859
- if (this.config.keepRawLogs) {
860
- this.writeToRawLog(text);
861
- }
440
+ this.writeToRawLog(data);
862
441
 
863
- // Process through transform for clean log
864
- if (this.cleanTransform) {
865
- this.cleanTransform.write(data);
866
- }
867
-
868
- // Also include error lines in readable log (compact)
869
- if (this.readableLogFd !== null) {
870
- const lines = text.split('\n');
871
- for (const line of lines) {
872
- const cleanLine = stripAnsi(line).trim();
873
- if (cleanLine && !this.isNoiseLog(cleanLine)) {
874
- try {
875
- const ts = new Date().toISOString();
876
- fs.writeSync(this.readableLogFd, `[${ts}] STDERR: ${cleanLine}\n`);
877
- } catch {}
442
+ // Write to readable log with error prefix
443
+ const lines = text.split('\n');
444
+ for (const line of lines) {
445
+ const cleanLine = stripAnsi(line).trim();
446
+ if (cleanLine && !this.isNoiseLog(cleanLine)) {
447
+ const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
448
+ const label = this.getLaneTaskLabel();
449
+
450
+ if (hasTimestamp) {
451
+ const formatted = cleanLine.includes(`[${label}]`)
452
+ ? cleanLine
453
+ : cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}] ❌ ERR`);
454
+ this.writeToReadableLog(`${formatted}\n`);
455
+ } else {
456
+ const ts = this.getShortTime();
457
+ this.writeToReadableLog(`[${ts}] [${label}] ❌ ERR ${cleanLine}\n`);
878
458
  }
879
459
  }
880
460
  }
881
-
882
- // Write JSON entry
883
- if (this.config.writeJsonLog) {
884
- const cleanText = stripAnsi(text).trim();
885
- if (cleanText) {
886
- this.writeJsonEntry({
887
- timestamp: new Date().toISOString(),
888
- level: 'stderr',
889
- lane: this.session.laneName,
890
- task: this.session.taskName,
891
- message: cleanText.substring(0, 1000),
892
- });
893
- }
894
- }
895
461
  }
896
462
 
897
463
  /**
898
464
  * Write a custom log entry
899
465
  */
900
466
  public log(level: 'info' | 'error' | 'debug', message: string, metadata?: Record<string, any>): void {
901
- const ts = formatTimestamp(this.config.timestampFormat, this.session.startTime);
902
- const prefix = level.toUpperCase().padEnd(8);
903
-
904
- const line = `[${ts}] [${prefix}] ${message}\n`;
905
- this.writeToCleanLog(line);
467
+ const ts = this.getShortTime();
468
+ const label = this.getLaneTaskLabel();
469
+ const emoji = level === 'error' ? '❌' : level === 'info' ? 'ℹ️' : '🔍';
470
+ const line = `[${ts}] [${label}] ${emoji} ${level.toUpperCase()} ${message}\n`;
906
471
 
907
- if (this.config.keepRawLogs) {
908
- this.writeToRawLog(line);
909
- }
910
-
911
- if (this.config.keepAbsoluteRawLogs) {
912
- this.writeToAbsoluteRawLog(line);
913
- }
914
-
915
- // Write to readable log (compact)
916
- if (this.readableLogFd !== null) {
917
- const typeLabel = level === 'error' ? '❌ ERROR' : level === 'info' ? 'ℹ️ INFO' : '🔍 DEBUG';
918
- const formatted = `${new Date().toISOString()} ${typeLabel}: ${message}\n`;
919
- try {
920
- fs.writeSync(this.readableLogFd, formatted);
921
- } catch {}
922
- }
923
-
924
- if (this.config.writeJsonLog) {
925
- this.writeJsonEntry({
926
- timestamp: new Date().toISOString(),
927
- level,
928
- lane: this.session.laneName,
929
- task: this.session.taskName,
930
- message,
931
- metadata,
932
- });
933
- }
472
+ this.writeToRawLog(line);
473
+ this.writeToReadableLog(line);
934
474
  }
935
475
 
936
476
  /**
@@ -940,62 +480,37 @@ export class EnhancedLogManager {
940
480
  const divider = '═'.repeat(78);
941
481
  const line = `\n${divider}\n ${title}\n${divider}\n`;
942
482
 
943
- this.writeToCleanLog(line);
944
- if (this.config.keepRawLogs) {
945
- this.writeToRawLog(line);
946
- }
947
- if (this.config.keepAbsoluteRawLogs) {
948
- this.writeToAbsoluteRawLog(line);
949
- }
950
-
951
- // Write to readable log (compact)
952
- if (this.readableLogFd !== null) {
953
- const ts = new Date().toISOString();
954
- const formatted = `[${ts}] ━━━ ${title} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
955
- try {
956
- fs.writeSync(this.readableLogFd, formatted);
957
- } catch {}
958
- }
483
+ this.writeToRawLog(line);
484
+ this.writeToReadableLog(line);
959
485
  }
960
486
 
961
487
  /**
962
488
  * Update task context
963
489
  */
964
- public setTask(taskName: string, model?: string): void {
490
+ public setTask(taskName: string, model?: string, taskIndex?: number): void {
965
491
  this.session.taskName = taskName;
966
492
  if (model) {
967
493
  this.session.model = model;
968
494
  }
495
+ if (taskIndex !== undefined) {
496
+ this.session.taskIndex = taskIndex;
497
+ }
969
498
 
970
499
  this.section(`Task: ${taskName}${model ? ` (Model: ${model})` : ''}`);
971
-
972
- if (this.config.writeJsonLog) {
973
- this.writeJsonEntry({
974
- timestamp: new Date().toISOString(),
975
- level: 'info',
976
- source: 'system',
977
- lane: this.session.laneName,
978
- task: taskName,
979
- message: `Task started: ${taskName}`,
980
- metadata: { model },
981
- });
982
- }
983
500
  }
984
501
 
985
502
  /**
986
- * Check if a log line is noise (progress bars, spinners, etc.)
503
+ * Check if a log line is noise
987
504
  */
988
505
  private isNoiseLog(text: string): boolean {
989
- // Skip empty or whitespace-only
990
506
  if (!text.trim()) return true;
991
507
 
992
- // Skip common progress/spinner patterns
993
508
  const noisePatterns = [
994
- /^[\s│├└─┌┐┘┴┬┤]+$/, // Box drawing only (removed duplicate ├)
995
- /^[.\s]+$/, // Dots only
996
- /^[=>\s-]+$/, // Progress bar characters
997
- /^\d+%$/, // Percentage only
998
- /^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/, // Spinner characters
509
+ /^[\s│├└─┌┐┘┴┬┤]+$/,
510
+ /^[.\s]+$/,
511
+ /^[=>\s-]+$/,
512
+ /^\d+%$/,
513
+ /^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/,
999
514
  ];
1000
515
 
1001
516
  return noisePatterns.some(p => p.test(text));
@@ -1004,12 +519,10 @@ export class EnhancedLogManager {
1004
519
  /**
1005
520
  * Get paths to all log files
1006
521
  */
1007
- public getLogPaths(): { clean: string; raw?: string; absoluteRaw?: string; json?: string; readable: string } {
522
+ public getLogPaths(): { clean: string; raw: string; readable: string } {
1008
523
  return {
1009
- clean: this.cleanLogPath,
1010
- raw: this.config.keepRawLogs ? this.rawLogPath : undefined,
1011
- absoluteRaw: this.config.keepAbsoluteRawLogs ? this.absoluteRawLogPath : undefined,
1012
- json: this.config.writeJsonLog ? this.jsonLogPath : undefined,
524
+ clean: this.readableLogPath, // For backward compatibility
525
+ raw: this.rawLogPath,
1013
526
  readable: this.readableLogPath,
1014
527
  };
1015
528
  }
@@ -1018,29 +531,14 @@ export class EnhancedLogManager {
1018
531
  * Create file descriptors for process stdio redirection
1019
532
  */
1020
533
  public getFileDescriptors(): { stdout: number; stderr: number } {
1021
- // For process spawning, use the raw log fd if available, otherwise clean
1022
- const fd = this.rawLogFd !== null ? this.rawLogFd : this.cleanLogFd!;
534
+ const fd = this.rawLogFd!;
1023
535
  return { stdout: fd, stderr: fd };
1024
536
  }
1025
537
 
1026
538
  /**
1027
- * Close all log files and ensure all data is flushed to disk
539
+ * Close all log files
1028
540
  */
1029
541
  public close(): void {
1030
- // Flush transform stream
1031
- if (this.cleanTransform) {
1032
- this.cleanTransform.end();
1033
- }
1034
-
1035
- // Flush streaming parser
1036
- if (this.streamingParser) {
1037
- // Parse any remaining buffered data
1038
- if (this.lineBuffer.trim()) {
1039
- this.streamingParser.parseLine(this.lineBuffer);
1040
- }
1041
- this.streamingParser.flush();
1042
- }
1043
-
1044
542
  // Write session end marker
1045
543
  const endMarker = `
1046
544
  ╔══════════════════════════════════════════════════════════════════════════════╗
@@ -1050,15 +548,6 @@ export class EnhancedLogManager {
1050
548
 
1051
549
  `;
1052
550
 
1053
- if (this.cleanLogFd !== null) {
1054
- try {
1055
- fs.writeSync(this.cleanLogFd, endMarker);
1056
- fs.fsyncSync(this.cleanLogFd);
1057
- fs.closeSync(this.cleanLogFd);
1058
- } catch {}
1059
- this.cleanLogFd = null;
1060
- }
1061
-
1062
551
  if (this.rawLogFd !== null) {
1063
552
  try {
1064
553
  fs.writeSync(this.rawLogFd, endMarker);
@@ -1068,34 +557,6 @@ export class EnhancedLogManager {
1068
557
  this.rawLogFd = null;
1069
558
  }
1070
559
 
1071
- if (this.absoluteRawLogFd !== null) {
1072
- try {
1073
- fs.fsyncSync(this.absoluteRawLogFd);
1074
- fs.closeSync(this.absoluteRawLogFd);
1075
- } catch {}
1076
- this.absoluteRawLogFd = null;
1077
- }
1078
-
1079
- if (this.jsonLogFd !== null) {
1080
- try {
1081
- this.writeJsonEntry({
1082
- timestamp: new Date().toISOString(),
1083
- level: 'session',
1084
- source: 'system',
1085
- lane: this.session.laneName,
1086
- message: 'Session ended',
1087
- metadata: {
1088
- sessionId: this.session.id,
1089
- duration: Date.now() - this.session.startTime,
1090
- },
1091
- });
1092
- fs.fsyncSync(this.jsonLogFd);
1093
- fs.closeSync(this.jsonLogFd);
1094
- } catch {}
1095
- this.jsonLogFd = null;
1096
- }
1097
-
1098
- // Close readable log
1099
560
  if (this.readableLogFd !== null) {
1100
561
  try {
1101
562
  fs.writeSync(this.readableLogFd, endMarker);
@@ -1107,22 +568,20 @@ export class EnhancedLogManager {
1107
568
  }
1108
569
 
1109
570
  /**
1110
- * Extract the last error message from the clean log file
571
+ * Extract the last error message from the log
1111
572
  */
1112
573
  public getLastError(): string | null {
1113
574
  try {
1114
- if (!fs.existsSync(this.cleanLogPath)) return null;
1115
- const content = fs.readFileSync(this.cleanLogPath, 'utf8');
1116
- // Look for lines containing error markers
575
+ if (!fs.existsSync(this.readableLogPath)) return null;
576
+ const content = fs.readFileSync(this.readableLogPath, 'utf8');
1117
577
  const lines = content.split('\n').filter(l =>
1118
- l.includes('[ERROR]') ||
1119
578
  l.includes('❌') ||
579
+ l.includes('[ERROR]') ||
1120
580
  l.includes('error:') ||
1121
581
  l.includes('Fatal') ||
1122
582
  l.includes('fail')
1123
583
  );
1124
584
  if (lines.length === 0) {
1125
- // Fallback to last 5 lines if no specific error marker found
1126
585
  const allLines = content.split('\n').filter(l => l.trim());
1127
586
  return allLines.slice(-5).join('\n');
1128
587
  }
@@ -1156,75 +615,41 @@ export function createLogManager(
1156
615
  laneRunDir: string,
1157
616
  laneName: string,
1158
617
  config?: Partial<EnhancedLogConfig>,
1159
- onParsedMessage?: (msg: ParsedMessage) => void
618
+ onParsedMessage?: (msg: ParsedMessage) => void,
619
+ laneIndex?: number
1160
620
  ): EnhancedLogManager {
1161
621
  const session: LogSession = {
1162
622
  id: `${laneName}-${Date.now().toString(36)}`,
1163
623
  laneName,
1164
624
  startTime: Date.now(),
625
+ laneIndex: laneIndex ?? 0,
626
+ taskIndex: 0,
1165
627
  };
1166
628
 
1167
629
  return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
1168
630
  }
1169
631
 
1170
632
  /**
1171
- * Read and parse JSON log file
633
+ * Read and parse JSON log file (legacy compatibility - returns empty array)
1172
634
  */
1173
635
  export function readJsonLog(logPath: string): JsonLogEntry[] {
1174
- if (!fs.existsSync(logPath)) {
1175
- return [];
1176
- }
1177
-
1178
- try {
1179
- const content = fs.readFileSync(logPath, 'utf8');
1180
- return content
1181
- .split('\n')
1182
- .filter(line => line.trim())
1183
- .map(line => {
1184
- try {
1185
- return JSON.parse(line) as JsonLogEntry;
1186
- } catch {
1187
- return null;
1188
- }
1189
- })
1190
- .filter((entry): entry is JsonLogEntry => entry !== null);
1191
- } catch {
1192
- return [];
1193
- }
636
+ return [];
1194
637
  }
1195
638
 
1196
639
  /**
1197
- * Export logs to various formats
640
+ * Export logs (legacy compatibility)
1198
641
  */
1199
642
  export function exportLogs(
1200
643
  laneRunDir: string,
1201
644
  format: 'text' | 'json' | 'markdown' | 'html',
1202
645
  outputPath?: string
1203
646
  ): string {
1204
- const cleanLogPath = safeJoin(laneRunDir, 'terminal.log');
1205
- const jsonLogPath = safeJoin(laneRunDir, 'terminal.jsonl');
647
+ const readableLogPath = safeJoin(laneRunDir, 'terminal-readable.log');
1206
648
 
1207
649
  let output = '';
1208
650
 
1209
- switch (format) {
1210
- case 'text':
1211
- if (fs.existsSync(cleanLogPath)) {
1212
- output = fs.readFileSync(cleanLogPath, 'utf8');
1213
- }
1214
- break;
1215
-
1216
- case 'json':
1217
- const entries = readJsonLog(jsonLogPath);
1218
- output = JSON.stringify(entries, null, 2);
1219
- break;
1220
-
1221
- case 'markdown':
1222
- output = exportToMarkdown(jsonLogPath, cleanLogPath);
1223
- break;
1224
-
1225
- case 'html':
1226
- output = exportToHtml(jsonLogPath, cleanLogPath);
1227
- break;
651
+ if (fs.existsSync(readableLogPath)) {
652
+ output = fs.readFileSync(readableLogPath, 'utf8');
1228
653
  }
1229
654
 
1230
655
  if (outputPath) {
@@ -1234,101 +659,13 @@ export function exportLogs(
1234
659
  return output;
1235
660
  }
1236
661
 
1237
- /**
1238
- * Export logs to Markdown format
1239
- */
1240
- function exportToMarkdown(jsonLogPath: string, cleanLogPath: string): string {
1241
- const entries = readJsonLog(jsonLogPath);
1242
-
1243
- let md = '# CursorFlow Session Log\n\n';
1244
-
1245
- // Find session info
1246
- const sessionStart = entries.find(e => e.level === 'session' && e.message === 'Session started');
1247
- if (sessionStart?.metadata) {
1248
- md += '## Session Info\n\n';
1249
- md += `- **Session ID**: ${sessionStart.metadata.sessionId}\n`;
1250
- md += `- **Lane**: ${sessionStart.lane}\n`;
1251
- md += `- **Model**: ${sessionStart.metadata.model || 'default'}\n`;
1252
- md += `- **Started**: ${sessionStart.timestamp}\n\n`;
1253
- }
1254
-
1255
- md += '## Log Entries\n\n';
1256
-
1257
- // Group by task
1258
- const byTask = new Map<string, JsonLogEntry[]>();
1259
- for (const entry of entries) {
1260
- const task = entry.task || '(no task)';
1261
- if (!byTask.has(task)) {
1262
- byTask.set(task, []);
1263
- }
1264
- byTask.get(task)!.push(entry);
1265
- }
1266
-
1267
- for (const [task, taskEntries] of byTask) {
1268
- md += `### Task: ${task}\n\n`;
1269
- md += '```\n';
1270
- for (const entry of taskEntries) {
1271
- const level = entry.level || 'info';
1272
- const message = entry.message || '';
1273
- if (level !== 'session') {
1274
- md += `[${entry.timestamp}] [${level.toUpperCase()}] ${message}\n`;
1275
- }
1276
- }
1277
- md += '```\n\n';
1278
- }
1279
-
1280
- return md;
1281
- }
1282
-
1283
- /**
1284
- * Export logs to HTML format
1285
- */
1286
- function exportToHtml(jsonLogPath: string, cleanLogPath: string): string {
1287
- const entries = readJsonLog(jsonLogPath);
1288
-
1289
- let html = `<!DOCTYPE html>
1290
- <html>
1291
- <head>
1292
- <title>CursorFlow Session Log</title>
1293
- <style>
1294
- body { font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; margin: 20px; background: #1e1e1e; color: #d4d4d4; }
1295
- h1, h2 { color: #569cd6; }
1296
- .entry { padding: 4px 8px; margin: 2px 0; border-radius: 4px; }
1297
- .entry.stdout { background: #252526; }
1298
- .entry.stderr { background: #3c1f1f; color: #f48771; }
1299
- .entry.info { background: #1e3a5f; color: #9cdcfe; }
1300
- .entry.error { background: #5f1e1e; color: #f48771; }
1301
- .entry.session { background: #1e4620; color: #6a9955; }
1302
- .timestamp { color: #808080; font-size: 0.9em; }
1303
- .level { font-weight: bold; text-transform: uppercase; }
1304
- .task { color: #dcdcaa; }
1305
- pre { white-space: pre-wrap; word-wrap: break-word; }
1306
- </style>
1307
- </head>
1308
- <body>
1309
- <h1>CursorFlow Session Log</h1>
1310
- `;
1311
-
1312
- for (const entry of entries) {
1313
- const level = entry.level || 'info';
1314
- const message = entry.message || '';
1315
- html += ` <div class="entry ${level}">
1316
- <span class="timestamp">${entry.timestamp}</span>
1317
- <span class="level">[${level}]</span>
1318
- ${entry.task ? `<span class="task">[${entry.task}]</span>` : ''}
1319
- <pre>${escapeHtml(message)}</pre>
1320
- </div>\n`;
1321
- }
1322
-
1323
- html += '</body></html>';
1324
- return html;
662
+ // Legacy exports for backward compatibility
663
+ export class StreamingMessageParser {
664
+ constructor(onMessage: (msg: ParsedMessage) => void) {}
665
+ parseLine(line: string): void {}
666
+ flush(): void {}
1325
667
  }
1326
668
 
1327
- function escapeHtml(text: string): string {
1328
- return text
1329
- .replace(/&/g, '&amp;')
1330
- .replace(/</g, '&lt;')
1331
- .replace(/>/g, '&gt;')
1332
- .replace(/"/g, '&quot;')
1333
- .replace(/'/g, '&#039;');
669
+ export class CleanLogTransform {
670
+ constructor(config: EnhancedLogConfig, session: LogSession) {}
1334
671
  }