@litmers/cursorflow-orchestrator 0.1.31 → 0.1.34

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 (129) hide show
  1. package/README.md +144 -52
  2. package/commands/cursorflow-add.md +159 -0
  3. package/commands/cursorflow-monitor.md +23 -2
  4. package/commands/cursorflow-new.md +87 -0
  5. package/dist/cli/add.d.ts +7 -0
  6. package/dist/cli/add.js +377 -0
  7. package/dist/cli/add.js.map +1 -0
  8. package/dist/cli/clean.js +1 -0
  9. package/dist/cli/clean.js.map +1 -1
  10. package/dist/cli/config.d.ts +7 -0
  11. package/dist/cli/config.js +181 -0
  12. package/dist/cli/config.js.map +1 -0
  13. package/dist/cli/index.js +34 -30
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/logs.js +7 -33
  16. package/dist/cli/logs.js.map +1 -1
  17. package/dist/cli/monitor.js +51 -62
  18. package/dist/cli/monitor.js.map +1 -1
  19. package/dist/cli/new.d.ts +7 -0
  20. package/dist/cli/new.js +232 -0
  21. package/dist/cli/new.js.map +1 -0
  22. package/dist/cli/prepare.js +95 -193
  23. package/dist/cli/prepare.js.map +1 -1
  24. package/dist/cli/resume.js +11 -47
  25. package/dist/cli/resume.js.map +1 -1
  26. package/dist/cli/run.js +27 -22
  27. package/dist/cli/run.js.map +1 -1
  28. package/dist/cli/tasks.js +1 -2
  29. package/dist/cli/tasks.js.map +1 -1
  30. package/dist/core/failure-policy.d.ts +9 -0
  31. package/dist/core/failure-policy.js +9 -0
  32. package/dist/core/failure-policy.js.map +1 -1
  33. package/dist/core/orchestrator.d.ts +20 -6
  34. package/dist/core/orchestrator.js +213 -333
  35. package/dist/core/orchestrator.js.map +1 -1
  36. package/dist/core/runner/agent.d.ts +27 -0
  37. package/dist/core/runner/agent.js +294 -0
  38. package/dist/core/runner/agent.js.map +1 -0
  39. package/dist/core/runner/index.d.ts +5 -0
  40. package/dist/core/runner/index.js +22 -0
  41. package/dist/core/runner/index.js.map +1 -0
  42. package/dist/core/runner/pipeline.d.ts +9 -0
  43. package/dist/core/runner/pipeline.js +539 -0
  44. package/dist/core/runner/pipeline.js.map +1 -0
  45. package/dist/core/runner/prompt.d.ts +25 -0
  46. package/dist/core/runner/prompt.js +175 -0
  47. package/dist/core/runner/prompt.js.map +1 -0
  48. package/dist/core/runner/task.d.ts +26 -0
  49. package/dist/core/runner/task.js +283 -0
  50. package/dist/core/runner/task.js.map +1 -0
  51. package/dist/core/runner/utils.d.ts +37 -0
  52. package/dist/core/runner/utils.js +161 -0
  53. package/dist/core/runner/utils.js.map +1 -0
  54. package/dist/core/runner.d.ts +2 -96
  55. package/dist/core/runner.js +11 -1136
  56. package/dist/core/runner.js.map +1 -1
  57. package/dist/core/stall-detection.d.ts +326 -0
  58. package/dist/core/stall-detection.js +781 -0
  59. package/dist/core/stall-detection.js.map +1 -0
  60. package/dist/types/config.d.ts +6 -6
  61. package/dist/types/flow.d.ts +84 -0
  62. package/dist/types/flow.js +10 -0
  63. package/dist/types/flow.js.map +1 -0
  64. package/dist/types/index.d.ts +1 -0
  65. package/dist/types/index.js +3 -3
  66. package/dist/types/index.js.map +1 -1
  67. package/dist/types/lane.d.ts +0 -2
  68. package/dist/types/logging.d.ts +5 -1
  69. package/dist/types/task.d.ts +7 -11
  70. package/dist/utils/config.js +7 -15
  71. package/dist/utils/config.js.map +1 -1
  72. package/dist/utils/dependency.d.ts +36 -1
  73. package/dist/utils/dependency.js +256 -1
  74. package/dist/utils/dependency.js.map +1 -1
  75. package/dist/utils/enhanced-logger.d.ts +45 -82
  76. package/dist/utils/enhanced-logger.js +238 -844
  77. package/dist/utils/enhanced-logger.js.map +1 -1
  78. package/dist/utils/git.d.ts +29 -0
  79. package/dist/utils/git.js +115 -5
  80. package/dist/utils/git.js.map +1 -1
  81. package/dist/utils/state.js +0 -2
  82. package/dist/utils/state.js.map +1 -1
  83. package/dist/utils/task-service.d.ts +2 -2
  84. package/dist/utils/task-service.js +40 -31
  85. package/dist/utils/task-service.js.map +1 -1
  86. package/package.json +4 -3
  87. package/src/cli/add.ts +397 -0
  88. package/src/cli/clean.ts +1 -0
  89. package/src/cli/config.ts +177 -0
  90. package/src/cli/index.ts +36 -32
  91. package/src/cli/logs.ts +7 -31
  92. package/src/cli/monitor.ts +55 -71
  93. package/src/cli/new.ts +235 -0
  94. package/src/cli/prepare.ts +98 -205
  95. package/src/cli/resume.ts +13 -56
  96. package/src/cli/run.ts +311 -306
  97. package/src/cli/tasks.ts +1 -2
  98. package/src/core/failure-policy.ts +9 -0
  99. package/src/core/orchestrator.ts +277 -378
  100. package/src/core/runner/agent.ts +314 -0
  101. package/src/core/runner/index.ts +6 -0
  102. package/src/core/runner/pipeline.ts +567 -0
  103. package/src/core/runner/prompt.ts +174 -0
  104. package/src/core/runner/task.ts +320 -0
  105. package/src/core/runner/utils.ts +142 -0
  106. package/src/core/runner.ts +8 -1347
  107. package/src/core/stall-detection.ts +936 -0
  108. package/src/types/config.ts +6 -6
  109. package/src/types/flow.ts +91 -0
  110. package/src/types/index.ts +15 -3
  111. package/src/types/lane.ts +0 -2
  112. package/src/types/logging.ts +5 -1
  113. package/src/types/task.ts +7 -11
  114. package/src/utils/config.ts +8 -16
  115. package/src/utils/dependency.ts +311 -2
  116. package/src/utils/enhanced-logger.ts +263 -927
  117. package/src/utils/git.ts +145 -5
  118. package/src/utils/state.ts +0 -2
  119. package/src/utils/task-service.ts +48 -40
  120. package/commands/cursorflow-review.md +0 -56
  121. package/commands/cursorflow-runs.md +0 -59
  122. package/dist/cli/runs.d.ts +0 -5
  123. package/dist/cli/runs.js +0 -214
  124. package/dist/cli/runs.js.map +0 -1
  125. package/dist/core/reviewer.d.ts +0 -66
  126. package/dist/core/reviewer.js +0 -265
  127. package/dist/core/reviewer.js.map +0 -1
  128. package/src/cli/runs.ts +0 -212
  129. 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,54 @@ 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 [L01-T02]
135
+ */
136
+ private getLaneTaskLabel(): string {
137
+ const laneNum = (this.session.laneIndex ?? 0) + 1;
138
+ const taskNum = (this.session.taskIndex ?? 0) + 1;
139
+ return `L${laneNum.toString().padStart(2, '0')}-T${taskNum.toString().padStart(2, '0')}`;
140
+ }
141
+
410
142
  /**
411
143
  * Initialize log files and write session headers
412
144
  */
413
145
  private initLogFiles(): void {
414
146
  // 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
147
  if (this.config.keepRawLogs) {
148
+ this.rotateIfNeeded(this.rawLogPath);
427
149
  this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
428
150
  }
429
-
430
- if (this.config.keepAbsoluteRawLogs) {
431
- this.absoluteRawLogFd = fs.openSync(this.absoluteRawLogPath, 'a');
432
- }
433
151
 
434
- if (this.config.writeJsonLog) {
435
- this.jsonLogFd = fs.openSync(this.jsonLogPath, 'a');
436
- }
437
-
438
- // Open readable log file (for parsed streaming output)
152
+ this.rotateIfNeeded(this.readableLogPath);
439
153
  this.readableLogFd = fs.openSync(this.readableLogPath, 'a');
440
154
 
441
- // Get initial file sizes
442
- try {
443
- this.cleanLogSize = fs.statSync(this.cleanLogPath).size;
444
- if (this.config.keepRawLogs) {
155
+ // Get initial file size for raw log if enabled
156
+ if (this.rawLogFd !== null) {
157
+ try {
445
158
  this.rawLogSize = fs.statSync(this.rawLogPath).size;
159
+ } catch {
160
+ this.rawLogSize = 0;
446
161
  }
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
162
  }
455
163
 
456
164
  // Write session header
457
165
  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
166
  }
553
167
 
554
168
  /**
@@ -568,32 +182,14 @@ export class EnhancedLogManager {
568
182
 
569
183
  `;
570
184
 
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
- });
185
+ this.writeToRawLog(header);
186
+ this.writeToReadableLog(header);
591
187
  }
592
188
 
593
189
  /**
594
190
  * Rotate log file if it exceeds max size
595
191
  */
596
- private rotateIfNeeded(logPath: string, type: 'clean' | 'raw'): void {
192
+ private rotateIfNeeded(logPath: string): void {
597
193
  if (!fs.existsSync(logPath)) return;
598
194
 
599
195
  try {
@@ -633,32 +229,13 @@ export class EnhancedLogManager {
633
229
  fs.renameSync(logPath, rotatedPath);
634
230
  }
635
231
 
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
232
  /**
656
233
  * Write to raw log with size tracking
657
234
  */
658
- private writeToRawLog(data: string): void {
235
+ private writeToRawLog(data: string | Buffer): void {
659
236
  if (this.rawLogFd === null) return;
660
237
 
661
- const buffer = Buffer.from(data);
238
+ const buffer = typeof data === 'string' ? Buffer.from(data) : data;
662
239
  fs.writeSync(this.rawLogFd, buffer);
663
240
  this.rawLogSize += buffer.length;
664
241
 
@@ -672,32 +249,38 @@ export class EnhancedLogManager {
672
249
  }
673
250
 
674
251
  /**
675
- * Write to absolute raw log with size tracking
252
+ * Write to readable log
676
253
  */
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;
254
+ private writeToReadableLog(data: string): void {
255
+ if (this.readableLogFd === null) return;
683
256
 
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;
257
+ try {
258
+ fs.writeSync(this.readableLogFd, data);
259
+ } catch {
260
+ // Ignore write errors
690
261
  }
691
262
  }
692
263
 
693
264
  /**
694
- * Write a JSON log entry
265
+ * Write a parsed message to the readable log using formatMessageForConsole style
695
266
  */
696
- private writeJsonEntry(entry: JsonLogEntry): void {
697
- if (this.jsonLogFd === null) return;
267
+ public writeReadableMessage(msg: ParsedMessage): void {
268
+ // Use formatMessageForConsole for consistent formatting
269
+ // Use short lane-task label like [L01-T02]
270
+ const formatted = formatMessageForConsole(msg, {
271
+ laneLabel: `[${this.getLaneTaskLabel()}]`,
272
+ includeTimestamp: false, // We'll add our own short timestamp
273
+ });
274
+
275
+ // Strip ANSI codes and add short timestamp for file output
276
+ const clean = stripAnsi(formatted);
277
+ const ts = this.getShortTime();
278
+ this.writeToReadableLog(`[${ts}] ${clean}\n`);
698
279
 
699
- const line = JSON.stringify(entry) + '\n';
700
- fs.writeSync(this.jsonLogFd, line);
280
+ // Callback for console output
281
+ if (this.onParsedMessage) {
282
+ this.onParsedMessage(msg);
283
+ }
701
284
  }
702
285
 
703
286
  /**
@@ -706,142 +289,144 @@ export class EnhancedLogManager {
706
289
  public writeStdout(data: Buffer | string): void {
707
290
  const text = data.toString();
708
291
 
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() || '';
292
+ // Write raw log (original data)
293
+ this.writeToRawLog(data);
728
294
 
295
+ // Parse JSON output and write to readable log
296
+ const lines = text.split('\n');
729
297
  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
298
+ const trimmed = line.trim();
299
+ if (!trimmed) continue;
300
+
301
+ // Try to parse as JSON (cursor-agent output)
302
+ if (trimmed.startsWith('{')) {
303
+ try {
304
+ const json = JSON.parse(trimmed);
305
+ const msg = this.parseJsonToMessage(json);
306
+ if (msg) {
307
+ this.writeReadableMessage(msg);
308
+ continue;
783
309
  }
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 {}
310
+ } catch {
311
+ // Not valid JSON, fall through
823
312
  }
824
313
  }
825
314
 
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
- });
315
+ // Non-JSON line - write as-is with short timestamp and lane-task label
316
+ const cleanLine = stripAnsi(trimmed);
317
+ if (cleanLine && !this.isNoiseLog(cleanLine)) {
318
+ const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
319
+ const label = this.getLaneTaskLabel();
320
+
321
+ if (hasTimestamp) {
322
+ // If already has timestamp, just ensure label is present
323
+ const formatted = cleanLine.includes(`[${label}]`)
324
+ ? cleanLine
325
+ : cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}]`);
326
+ this.writeToReadableLog(`${formatted}\n`);
327
+ } else {
328
+ const ts = this.getShortTime();
329
+ this.writeToReadableLog(`[${ts}] [${label}] ${cleanLine}\n`);
330
+ }
836
331
  }
837
332
  }
838
333
  }
839
-
334
+
840
335
  /**
841
- * Parse streaming JSON data for readable log - legacy, integrated into writeStdout
336
+ * Parse cursor-agent JSON output to ParsedMessage
842
337
  */
843
- private parseStreamingData(text: string): void {
844
- // Legacy method, no longer used but kept for internal references if any
338
+ private parseJsonToMessage(json: any): ParsedMessage | null {
339
+ const type = json.type;
340
+ const timestamp = json.timestamp_ms || Date.now();
341
+
342
+ switch (type) {
343
+ case 'system':
344
+ return {
345
+ type: 'system',
346
+ role: 'system',
347
+ content: `Model: ${json.model || 'unknown'}, Mode: ${json.permissionMode || 'default'}`,
348
+ timestamp,
349
+ };
350
+
351
+ case 'user':
352
+ if (json.message?.content) {
353
+ const textContent = json.message.content
354
+ .filter((c: any) => c.type === 'text')
355
+ .map((c: any) => c.text)
356
+ .join('');
357
+ return {
358
+ type: 'user',
359
+ role: 'user',
360
+ content: textContent,
361
+ timestamp,
362
+ };
363
+ }
364
+ return null;
365
+
366
+ case 'assistant':
367
+ if (json.message?.content) {
368
+ const textContent = json.message.content
369
+ .filter((c: any) => c.type === 'text')
370
+ .map((c: any) => c.text)
371
+ .join('');
372
+ return {
373
+ type: 'assistant',
374
+ role: 'assistant',
375
+ content: textContent,
376
+ timestamp,
377
+ };
378
+ }
379
+ return null;
380
+
381
+ case 'tool_call':
382
+ if (json.subtype === 'started' && json.tool_call) {
383
+ const toolName = Object.keys(json.tool_call)[0] || 'unknown';
384
+ const toolArgs = json.tool_call[toolName]?.args || {};
385
+ return {
386
+ type: 'tool',
387
+ role: 'tool',
388
+ content: `[Tool: ${toolName}] ${JSON.stringify(toolArgs)}`,
389
+ timestamp,
390
+ metadata: { callId: json.call_id, toolName },
391
+ };
392
+ } else if (json.subtype === 'completed' && json.tool_call) {
393
+ const toolName = Object.keys(json.tool_call)[0] || 'unknown';
394
+ return {
395
+ type: 'tool_result',
396
+ role: 'tool',
397
+ content: `[Tool Result: ${toolName}]`,
398
+ timestamp,
399
+ metadata: { callId: json.call_id, toolName },
400
+ };
401
+ }
402
+ return null;
403
+
404
+ case 'result':
405
+ return {
406
+ type: 'result',
407
+ role: 'assistant',
408
+ content: json.result || '',
409
+ timestamp,
410
+ metadata: {
411
+ duration_ms: json.duration_ms,
412
+ is_error: json.is_error,
413
+ },
414
+ };
415
+
416
+ case 'thinking':
417
+ if (json.text) {
418
+ return {
419
+ type: 'thinking',
420
+ role: 'assistant',
421
+ content: json.text,
422
+ timestamp,
423
+ };
424
+ }
425
+ return null;
426
+
427
+ default:
428
+ return null;
429
+ }
845
430
  }
846
431
 
847
432
  /**
@@ -850,87 +435,41 @@ export class EnhancedLogManager {
850
435
  public writeStderr(data: Buffer | string): void {
851
436
  const text = data.toString();
852
437
 
853
- // Write absolute raw log
854
- if (this.config.keepAbsoluteRawLogs) {
855
- this.writeToAbsoluteRawLog(data);
856
- }
857
-
858
438
  // Write raw log
859
- if (this.config.keepRawLogs) {
860
- this.writeToRawLog(text);
861
- }
439
+ this.writeToRawLog(data);
862
440
 
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 {}
441
+ // Write to readable log with error prefix
442
+ const lines = text.split('\n');
443
+ for (const line of lines) {
444
+ const cleanLine = stripAnsi(line).trim();
445
+ if (cleanLine && !this.isNoiseLog(cleanLine)) {
446
+ const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
447
+ const label = this.getLaneTaskLabel();
448
+
449
+ if (hasTimestamp) {
450
+ const formatted = cleanLine.includes(`[${label}]`)
451
+ ? cleanLine
452
+ : cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}] ❌ ERR`);
453
+ this.writeToReadableLog(`${formatted}\n`);
454
+ } else {
455
+ const ts = this.getShortTime();
456
+ this.writeToReadableLog(`[${ts}] [${label}] ❌ ERR ${cleanLine}\n`);
878
457
  }
879
458
  }
880
459
  }
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
460
  }
896
461
 
897
462
  /**
898
463
  * Write a custom log entry
899
464
  */
900
465
  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);
466
+ const ts = this.getShortTime();
467
+ const label = this.getLaneTaskLabel();
468
+ const emoji = level === 'error' ? '❌' : level === 'info' ? 'ℹ️' : '🔍';
469
+ const line = `[${ts}] [${label}] ${emoji} ${level.toUpperCase()} ${message}\n`;
906
470
 
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
- }
471
+ this.writeToRawLog(line);
472
+ this.writeToReadableLog(line);
934
473
  }
935
474
 
936
475
  /**
@@ -940,62 +479,37 @@ export class EnhancedLogManager {
940
479
  const divider = '═'.repeat(78);
941
480
  const line = `\n${divider}\n ${title}\n${divider}\n`;
942
481
 
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
- }
482
+ this.writeToRawLog(line);
483
+ this.writeToReadableLog(line);
959
484
  }
960
485
 
961
486
  /**
962
487
  * Update task context
963
488
  */
964
- public setTask(taskName: string, model?: string): void {
489
+ public setTask(taskName: string, model?: string, taskIndex?: number): void {
965
490
  this.session.taskName = taskName;
966
491
  if (model) {
967
492
  this.session.model = model;
968
493
  }
494
+ if (taskIndex !== undefined) {
495
+ this.session.taskIndex = taskIndex;
496
+ }
969
497
 
970
498
  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
499
  }
984
500
 
985
501
  /**
986
- * Check if a log line is noise (progress bars, spinners, etc.)
502
+ * Check if a log line is noise
987
503
  */
988
504
  private isNoiseLog(text: string): boolean {
989
- // Skip empty or whitespace-only
990
505
  if (!text.trim()) return true;
991
506
 
992
- // Skip common progress/spinner patterns
993
507
  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
508
+ /^[\s│├└─┌┐┘┴┬┤]+$/,
509
+ /^[.\s]+$/,
510
+ /^[=>\s-]+$/,
511
+ /^\d+%$/,
512
+ /^⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/,
999
513
  ];
1000
514
 
1001
515
  return noisePatterns.some(p => p.test(text));
@@ -1004,12 +518,10 @@ export class EnhancedLogManager {
1004
518
  /**
1005
519
  * Get paths to all log files
1006
520
  */
1007
- public getLogPaths(): { clean: string; raw?: string; absoluteRaw?: string; json?: string; readable: string } {
521
+ public getLogPaths(): { clean: string; raw: string; readable: string } {
1008
522
  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,
523
+ clean: this.readableLogPath, // For backward compatibility
524
+ raw: this.rawLogPath,
1013
525
  readable: this.readableLogPath,
1014
526
  };
1015
527
  }
@@ -1018,29 +530,14 @@ export class EnhancedLogManager {
1018
530
  * Create file descriptors for process stdio redirection
1019
531
  */
1020
532
  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!;
533
+ const fd = this.rawLogFd!;
1023
534
  return { stdout: fd, stderr: fd };
1024
535
  }
1025
536
 
1026
537
  /**
1027
- * Close all log files and ensure all data is flushed to disk
538
+ * Close all log files
1028
539
  */
1029
540
  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
541
  // Write session end marker
1045
542
  const endMarker = `
1046
543
  ╔══════════════════════════════════════════════════════════════════════════════╗
@@ -1050,15 +547,6 @@ export class EnhancedLogManager {
1050
547
 
1051
548
  `;
1052
549
 
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
550
  if (this.rawLogFd !== null) {
1063
551
  try {
1064
552
  fs.writeSync(this.rawLogFd, endMarker);
@@ -1068,34 +556,6 @@ export class EnhancedLogManager {
1068
556
  this.rawLogFd = null;
1069
557
  }
1070
558
 
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
559
  if (this.readableLogFd !== null) {
1100
560
  try {
1101
561
  fs.writeSync(this.readableLogFd, endMarker);
@@ -1107,22 +567,20 @@ export class EnhancedLogManager {
1107
567
  }
1108
568
 
1109
569
  /**
1110
- * Extract the last error message from the clean log file
570
+ * Extract the last error message from the log
1111
571
  */
1112
572
  public getLastError(): string | null {
1113
573
  try {
1114
- if (!fs.existsSync(this.cleanLogPath)) return null;
1115
- const content = fs.readFileSync(this.cleanLogPath, 'utf8');
1116
- // Look for lines containing error markers
574
+ if (!fs.existsSync(this.readableLogPath)) return null;
575
+ const content = fs.readFileSync(this.readableLogPath, 'utf8');
1117
576
  const lines = content.split('\n').filter(l =>
1118
- l.includes('[ERROR]') ||
1119
577
  l.includes('❌') ||
578
+ l.includes('[ERROR]') ||
1120
579
  l.includes('error:') ||
1121
580
  l.includes('Fatal') ||
1122
581
  l.includes('fail')
1123
582
  );
1124
583
  if (lines.length === 0) {
1125
- // Fallback to last 5 lines if no specific error marker found
1126
584
  const allLines = content.split('\n').filter(l => l.trim());
1127
585
  return allLines.slice(-5).join('\n');
1128
586
  }
@@ -1156,75 +614,41 @@ export function createLogManager(
1156
614
  laneRunDir: string,
1157
615
  laneName: string,
1158
616
  config?: Partial<EnhancedLogConfig>,
1159
- onParsedMessage?: (msg: ParsedMessage) => void
617
+ onParsedMessage?: (msg: ParsedMessage) => void,
618
+ laneIndex?: number
1160
619
  ): EnhancedLogManager {
1161
620
  const session: LogSession = {
1162
621
  id: `${laneName}-${Date.now().toString(36)}`,
1163
622
  laneName,
1164
623
  startTime: Date.now(),
624
+ laneIndex: laneIndex ?? 0,
625
+ taskIndex: 0,
1165
626
  };
1166
627
 
1167
628
  return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
1168
629
  }
1169
630
 
1170
631
  /**
1171
- * Read and parse JSON log file
632
+ * Read and parse JSON log file (legacy compatibility - returns empty array)
1172
633
  */
1173
634
  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
- }
635
+ return [];
1194
636
  }
1195
637
 
1196
638
  /**
1197
- * Export logs to various formats
639
+ * Export logs (legacy compatibility)
1198
640
  */
1199
641
  export function exportLogs(
1200
642
  laneRunDir: string,
1201
643
  format: 'text' | 'json' | 'markdown' | 'html',
1202
644
  outputPath?: string
1203
645
  ): string {
1204
- const cleanLogPath = safeJoin(laneRunDir, 'terminal.log');
1205
- const jsonLogPath = safeJoin(laneRunDir, 'terminal.jsonl');
646
+ const readableLogPath = safeJoin(laneRunDir, 'terminal-readable.log');
1206
647
 
1207
648
  let output = '';
1208
649
 
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;
650
+ if (fs.existsSync(readableLogPath)) {
651
+ output = fs.readFileSync(readableLogPath, 'utf8');
1228
652
  }
1229
653
 
1230
654
  if (outputPath) {
@@ -1234,101 +658,13 @@ export function exportLogs(
1234
658
  return output;
1235
659
  }
1236
660
 
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;
661
+ // Legacy exports for backward compatibility
662
+ export class StreamingMessageParser {
663
+ constructor(onMessage: (msg: ParsedMessage) => void) {}
664
+ parseLine(line: string): void {}
665
+ flush(): void {}
1325
666
  }
1326
667
 
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;');
668
+ export class CleanLogTransform {
669
+ constructor(config: EnhancedLogConfig, session: LogSession) {}
1334
670
  }