@litmers/cursorflow-orchestrator 0.2.2 → 0.2.5

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 (95) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +8 -11
  3. package/dist/cli/complete.d.ts +7 -0
  4. package/dist/cli/complete.js +304 -0
  5. package/dist/cli/complete.js.map +1 -0
  6. package/dist/cli/index.js +0 -6
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/logs.js +51 -61
  9. package/dist/cli/logs.js.map +1 -1
  10. package/dist/cli/monitor.js +74 -46
  11. package/dist/cli/monitor.js.map +1 -1
  12. package/dist/cli/resume.js +2 -2
  13. package/dist/cli/resume.js.map +1 -1
  14. package/dist/cli/signal.js +33 -29
  15. package/dist/cli/signal.js.map +1 -1
  16. package/dist/core/auto-recovery.d.ts +2 -117
  17. package/dist/core/auto-recovery.js +4 -487
  18. package/dist/core/auto-recovery.js.map +1 -1
  19. package/dist/core/failure-policy.d.ts +0 -52
  20. package/dist/core/failure-policy.js +7 -174
  21. package/dist/core/failure-policy.js.map +1 -1
  22. package/dist/core/git-lifecycle-manager.js +2 -2
  23. package/dist/core/git-lifecycle-manager.js.map +1 -1
  24. package/dist/core/git-pipeline-coordinator.js +25 -25
  25. package/dist/core/git-pipeline-coordinator.js.map +1 -1
  26. package/dist/core/intervention.d.ts +0 -6
  27. package/dist/core/intervention.js +1 -17
  28. package/dist/core/intervention.js.map +1 -1
  29. package/dist/core/orchestrator.js +18 -10
  30. package/dist/core/orchestrator.js.map +1 -1
  31. package/dist/core/runner/agent.js +18 -15
  32. package/dist/core/runner/agent.js.map +1 -1
  33. package/dist/core/runner/pipeline.js +3 -3
  34. package/dist/core/runner/pipeline.js.map +1 -1
  35. package/dist/core/stall-detection.js +9 -7
  36. package/dist/core/stall-detection.js.map +1 -1
  37. package/dist/hooks/data-accessor.js +2 -2
  38. package/dist/hooks/data-accessor.js.map +1 -1
  39. package/dist/services/logging/buffer.d.ts +1 -2
  40. package/dist/services/logging/buffer.js +22 -63
  41. package/dist/services/logging/buffer.js.map +1 -1
  42. package/dist/services/logging/formatter.d.ts +4 -0
  43. package/dist/services/logging/formatter.js +201 -33
  44. package/dist/services/logging/formatter.js.map +1 -1
  45. package/dist/services/logging/paths.d.ts +0 -3
  46. package/dist/services/logging/paths.js +0 -3
  47. package/dist/services/logging/paths.js.map +1 -1
  48. package/dist/types/config.d.ts +1 -9
  49. package/dist/types/logging.d.ts +1 -1
  50. package/dist/utils/config.js +2 -6
  51. package/dist/utils/config.js.map +1 -1
  52. package/dist/utils/enhanced-logger.d.ts +17 -37
  53. package/dist/utils/enhanced-logger.js +237 -267
  54. package/dist/utils/enhanced-logger.js.map +1 -1
  55. package/dist/utils/logger.js +17 -4
  56. package/dist/utils/logger.js.map +1 -1
  57. package/dist/utils/repro-thinking-logs.js +4 -4
  58. package/dist/utils/repro-thinking-logs.js.map +1 -1
  59. package/package.json +3 -14
  60. package/scripts/monitor-lanes.sh +5 -5
  61. package/scripts/stream-logs.sh +1 -1
  62. package/scripts/test-log-parser.ts +8 -42
  63. package/src/cli/complete.ts +305 -0
  64. package/src/cli/index.ts +0 -6
  65. package/src/cli/logs.ts +46 -60
  66. package/src/cli/monitor.ts +82 -48
  67. package/src/cli/resume.ts +1 -1
  68. package/src/cli/signal.ts +38 -34
  69. package/src/core/auto-recovery.ts +13 -595
  70. package/src/core/failure-policy.ts +7 -228
  71. package/src/core/git-lifecycle-manager.ts +2 -2
  72. package/src/core/git-pipeline-coordinator.ts +25 -25
  73. package/src/core/intervention.ts +0 -18
  74. package/src/core/orchestrator.ts +20 -10
  75. package/src/core/runner/agent.ts +21 -16
  76. package/src/core/runner/pipeline.ts +3 -3
  77. package/src/core/stall-detection.ts +11 -9
  78. package/src/hooks/data-accessor.ts +2 -2
  79. package/src/services/logging/buffer.ts +20 -68
  80. package/src/services/logging/formatter.ts +199 -32
  81. package/src/services/logging/paths.ts +0 -3
  82. package/src/types/config.ts +1 -13
  83. package/src/types/logging.ts +2 -0
  84. package/src/utils/config.ts +2 -6
  85. package/src/utils/enhanced-logger.ts +239 -290
  86. package/src/utils/logger.ts +18 -3
  87. package/src/utils/repro-thinking-logs.ts +4 -4
  88. package/dist/cli/prepare.d.ts +0 -7
  89. package/dist/cli/prepare.js +0 -690
  90. package/dist/cli/prepare.js.map +0 -1
  91. package/dist/utils/log-formatter.d.ts +0 -26
  92. package/dist/utils/log-formatter.js +0 -274
  93. package/dist/utils/log-formatter.js.map +0 -1
  94. package/src/cli/prepare.ts +0 -777
  95. package/src/utils/log-formatter.ts +0 -287
@@ -1,15 +1,11 @@
1
1
  /**
2
- * Enhanced Logger - Simplified terminal output capture
3
- *
4
- * Features:
5
- * - Raw log: Original output as-is
6
- * - Readable log: Formatted with formatMessageForConsole style
2
+ * Enhanced Logger - Simplified JSONL terminal output capture
7
3
  */
8
4
 
9
5
  import * as fs from 'fs';
10
6
  import * as path from 'path';
11
7
  import { EnhancedLogConfig, ParsedMessage, LogSession } from '../types';
12
- import { formatMessageForConsole } from './log-formatter';
8
+ import { formatMessageForConsole, stripAnsi } from '../services/logging/formatter';
13
9
  import { safeJoin } from './path';
14
10
  import { getLaneLogPath } from '../services/logging/paths';
15
11
 
@@ -30,80 +26,22 @@ export interface JsonLogEntry {
30
26
  export const DEFAULT_LOG_CONFIG: EnhancedLogConfig = {
31
27
  enabled: true,
32
28
  stripAnsi: true,
33
- addTimestamps: true,
34
29
  maxFileSize: 50 * 1024 * 1024, // 50MB
35
30
  maxFiles: 5,
36
- keepRawLogs: true,
37
- keepAbsoluteRawLogs: false,
38
- raw: false,
39
- writeJsonLog: false, // Disabled by default now
31
+ writeJsonLog: true,
40
32
  timestampFormat: 'iso',
41
33
  };
42
34
 
43
35
  /**
44
- * ANSI escape sequence regex pattern
45
- */
46
- const ANSI_REGEX = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
47
- const EXTENDED_ANSI_REGEX = /(?:\x1B[@-Z\\-_]|\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07]*(?:\x07|\x1B\\)|\x1B[PX^_][^\x1B]*\x1B\\|\x1B.)/g;
48
-
49
- /**
50
- * Strip ANSI escape sequences from text
51
- */
52
- export function stripAnsi(text: string): string {
53
- return text
54
- .replace(EXTENDED_ANSI_REGEX, '')
55
- .replace(ANSI_REGEX, '')
56
- .replace(/\r[^\n]/g, '\n')
57
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
58
- }
59
-
60
- /**
61
- * Format timestamp
62
- */
63
- export function formatTimestamp(format: 'iso' | 'relative' | 'short', startTime?: number): string {
64
- const now = Date.now();
65
-
66
- switch (format) {
67
- case 'iso':
68
- return new Date(now).toISOString();
69
- case 'relative':
70
- if (startTime) {
71
- const elapsed = now - startTime;
72
- const seconds = Math.floor(elapsed / 1000);
73
- const minutes = Math.floor(seconds / 60);
74
- const hours = Math.floor(minutes / 60);
75
-
76
- if (hours > 0) {
77
- return `+${hours}h${minutes % 60}m${seconds % 60}s`;
78
- } else if (minutes > 0) {
79
- return `+${minutes}m${seconds % 60}s`;
80
- } else {
81
- return `+${seconds}s`;
82
- }
83
- }
84
- return new Date(now).toISOString();
85
- case 'short':
86
- return new Date(now).toLocaleTimeString('en-US', { hour12: false });
87
- default:
88
- return new Date(now).toISOString();
89
- }
90
- }
91
-
92
- /**
93
- * Simplified Log Manager - Only raw and readable logs
36
+ * Enhanced Log Manager - JSONL format only
94
37
  */
95
38
  export class EnhancedLogManager {
96
39
  private config: EnhancedLogConfig;
97
40
  private session: LogSession;
98
41
  private logDir: string;
99
42
 
100
- private rawLogPath: string;
101
- private readableLogPath: string;
102
-
103
- private rawLogFd: number | null = null;
104
- private readableLogFd: number | null = null;
105
-
106
- private rawLogSize: number = 0;
43
+ private jsonlLogPath: string;
44
+ private jsonlLogFd: number | null = null;
107
45
 
108
46
  private onParsedMessage?: (msg: ParsedMessage) => void;
109
47
 
@@ -117,10 +55,7 @@ export class EnhancedLogManager {
117
55
  fs.mkdirSync(logDir, { recursive: true });
118
56
 
119
57
  // Subprocess (lane) logs live in the lane run directory to avoid nesting with main logs.
120
- // Main process logs are written separately to a run-level file (see utils/logger.ts).
121
- // Set up log file paths (simplified)
122
- this.rawLogPath = getLaneLogPath(logDir, 'raw');
123
- this.readableLogPath = getLaneLogPath(logDir, 'readable');
58
+ this.jsonlLogPath = getLaneLogPath(logDir, 'jsonl');
124
59
 
125
60
  // Initialize log files
126
61
  this.initLogFiles();
@@ -147,47 +82,37 @@ export class EnhancedLogManager {
147
82
  * Initialize log files and write session headers
148
83
  */
149
84
  private initLogFiles(): void {
150
- // Check and rotate if necessary
151
- if (this.config.keepRawLogs) {
152
- this.rotateIfNeeded(this.rawLogPath);
153
- this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
154
- }
155
-
156
- this.rotateIfNeeded(this.readableLogPath);
157
- this.readableLogFd = fs.openSync(this.readableLogPath, 'a');
158
-
159
- // Get initial file size for raw log if enabled
160
- if (this.rawLogFd !== null) {
161
- try {
162
- this.rawLogSize = fs.statSync(this.rawLogPath).size;
163
- } catch {
164
- this.rawLogSize = 0;
165
- }
85
+ if (this.config.writeJsonLog) {
86
+ // Ensure parent directory exists before opening file
87
+ const logDir = path.dirname(this.jsonlLogPath);
88
+ fs.mkdirSync(logDir, { recursive: true });
89
+
90
+ this.rotateIfNeeded(this.jsonlLogPath);
91
+ this.jsonlLogFd = fs.openSync(this.jsonlLogPath, 'a');
166
92
  }
167
93
 
168
- // Write session header
169
- this.writeSessionHeader();
94
+ // Write session start to JSON log
95
+ this.writeSessionStart();
170
96
  }
171
97
 
172
98
  /**
173
- * Write session header to logs
99
+ * Write session start to JSON log
174
100
  */
175
- private writeSessionHeader(): void {
176
- const header = `
177
- ╔══════════════════════════════════════════════════════════════════════════════╗
178
- ║ CursorFlow Session Log ║
179
- ╠══════════════════════════════════════════════════════════════════════════════╣
180
- Session ID: ${this.session.id.padEnd(62)}║
181
- ║ Lane: ${this.session.laneName.padEnd(62)}║
182
- ║ Task: ${(this.session.taskName || '-').padEnd(62)}║
183
- ║ Model: ${(this.session.model || 'default').padEnd(62)}║
184
- ║ Started: ${new Date(this.session.startTime).toISOString().padEnd(62)}║
185
- ╚══════════════════════════════════════════════════════════════════════════════╝
186
-
187
- `;
188
-
189
- this.writeToRawLog(header);
190
- this.writeToReadableLog(header);
101
+ private writeSessionStart(): void {
102
+ if (this.config.writeJsonLog) {
103
+ this.writeToJsonLog({
104
+ type: 'session',
105
+ role: 'system',
106
+ content: 'Session started',
107
+ timestamp: this.session.startTime,
108
+ metadata: {
109
+ sessionId: this.session.id,
110
+ laneName: this.session.laneName,
111
+ taskName: this.session.taskName,
112
+ model: this.session.model,
113
+ }
114
+ });
115
+ }
191
116
  }
192
117
 
193
118
  /**
@@ -234,53 +159,37 @@ export class EnhancedLogManager {
234
159
  }
235
160
 
236
161
  /**
237
- * Write to raw log with size tracking
162
+ * Write to JSONL log
238
163
  */
239
- private writeToRawLog(data: string | Buffer): void {
240
- if (this.rawLogFd === null) return;
241
-
242
- const buffer = typeof data === 'string' ? Buffer.from(data) : data;
243
- fs.writeSync(this.rawLogFd, buffer);
244
- this.rawLogSize += buffer.length;
245
-
246
- // Check if rotation needed
247
- if (this.rawLogSize >= this.config.maxFileSize) {
248
- fs.closeSync(this.rawLogFd);
249
- this.rotateLog(this.rawLogPath);
250
- this.rawLogFd = fs.openSync(this.rawLogPath, 'a');
251
- this.rawLogSize = 0;
252
- }
253
- }
254
-
255
- /**
256
- * Write to readable log
257
- */
258
- private writeToReadableLog(data: string): void {
259
- if (this.readableLogFd === null) return;
164
+ private writeToJsonLog(msg: ParsedMessage): void {
165
+ if (this.jsonlLogFd === null) return;
260
166
 
261
167
  try {
262
- fs.writeSync(this.readableLogFd, data);
168
+ // Add lane and task context to JSON log
169
+ const entry = {
170
+ ...msg,
171
+ lane: this.session.laneName,
172
+ task: this.session.taskName,
173
+ laneIndex: this.session.laneIndex,
174
+ taskIndex: this.session.taskIndex,
175
+ timestamp_iso: new Date(msg.timestamp).toISOString(),
176
+ };
177
+
178
+ fs.writeSync(this.jsonlLogFd, JSON.stringify(entry) + '\n');
263
179
  } catch {
264
180
  // Ignore write errors
265
181
  }
266
182
  }
267
183
 
268
184
  /**
269
- * Write a parsed message to the readable log using formatMessageForConsole style
185
+ * Write a parsed message to the JSON log and console
270
186
  */
271
187
  public writeReadableMessage(msg: ParsedMessage): void {
272
- // Use formatMessageForConsole for consistent formatting
273
- // Use short lane-task label like [1-1-lanename10]
274
- const formatted = formatMessageForConsole(msg, {
275
- laneLabel: `[${this.getLaneTaskLabel()}]`,
276
- includeTimestamp: false, // We'll add our own short timestamp
277
- });
278
-
279
- // Strip ANSI codes and add short timestamp for file output
280
- const clean = stripAnsi(formatted);
281
- const ts = this.getShortTime();
282
- this.writeToReadableLog(`[${ts}] ${clean}\n`);
283
-
188
+ // Write to JSON log
189
+ if (this.config.writeJsonLog) {
190
+ this.writeToJsonLog(msg);
191
+ }
192
+
284
193
  // Callback for console output
285
194
  if (this.onParsedMessage) {
286
195
  this.onParsedMessage(msg);
@@ -292,12 +201,8 @@ export class EnhancedLogManager {
292
201
  */
293
202
  public writeStdout(data: Buffer | string): void {
294
203
  const text = data.toString();
295
-
296
- // Write raw log (original data)
297
- this.writeToRawLog(data);
298
-
299
- // Parse JSON output and write to readable log
300
204
  const lines = text.split('\n');
205
+
301
206
  for (const line of lines) {
302
207
  const trimmed = line.trim();
303
208
  if (!trimmed) continue;
@@ -313,6 +218,12 @@ export class EnhancedLogManager {
313
218
  }
314
219
  // parseJsonToMessage returned null - create fallback message for known JSON
315
220
  if (json.type) {
221
+ // Skip noisy events that we don't want to show
222
+ if (json.type === 'thinking' || json.type === 'call' ||
223
+ (json.type === 'tool_call' && !json.tool_call)) {
224
+ continue;
225
+ }
226
+
316
227
  const fallbackMsg: ParsedMessage = {
317
228
  type: 'info',
318
229
  role: 'system',
@@ -327,43 +238,31 @@ export class EnhancedLogManager {
327
238
  }
328
239
  }
329
240
 
330
- // Non-JSON line - write as-is with short timestamp and lane-task label
241
+ // Non-JSON line
331
242
  const cleanLine = stripAnsi(trimmed);
332
243
  if (cleanLine && !this.isNoiseLog(cleanLine)) {
333
- const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
334
- const label = this.getLaneTaskLabel();
335
- const ts = this.getShortTime();
336
-
337
- // For file output, use clean line (no ANSI codes)
338
- let fileFormattedLine: string;
339
- if (hasTimestamp) {
340
- fileFormattedLine = cleanLine.includes(`[${label}]`)
341
- ? cleanLine
342
- : cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}]`);
343
- } else {
344
- fileFormattedLine = `[${ts}] [${label}] ${cleanLine}`;
345
- }
244
+ // Detect if this looks like Git output
245
+ const isGit = this.isGitOutput(cleanLine);
246
+ const type = isGit ? 'git' : 'stdout';
346
247
 
347
- this.writeToReadableLog(`${fileFormattedLine}\n`);
348
-
349
- // For console output, preserve ANSI colors from original line
248
+ // Output to console
350
249
  if (this.onParsedMessage) {
351
- let consoleFormattedLine: string;
352
- if (hasTimestamp) {
353
- consoleFormattedLine = trimmed.includes(`[${label}]`)
354
- ? trimmed
355
- : trimmed.replace(/^(\[[^\]]+\])/, `$1 [${label}]`);
356
- } else {
357
- consoleFormattedLine = `[${ts}] [${label}] ${trimmed}`;
358
- }
359
-
360
- const rawMsg: ParsedMessage = {
361
- type: 'raw',
250
+ this.onParsedMessage({
251
+ type,
362
252
  role: 'system',
363
- content: consoleFormattedLine,
253
+ content: trimmed,
364
254
  timestamp: Date.now(),
365
- };
366
- this.onParsedMessage(rawMsg);
255
+ });
256
+ }
257
+
258
+ // Write to JSON log
259
+ if (this.config.writeJsonLog) {
260
+ this.writeToJsonLog({
261
+ type,
262
+ role: 'system',
263
+ content: cleanLine,
264
+ timestamp: Date.now(),
265
+ });
367
266
  }
368
267
  }
369
268
  }
@@ -377,14 +276,33 @@ export class EnhancedLogManager {
377
276
  const timestamp = json.timestamp_ms || Date.now();
378
277
 
379
278
  switch (type) {
279
+ case 'info':
280
+ case 'warn':
281
+ case 'error':
282
+ case 'success':
283
+ case 'debug':
284
+ case 'progress':
380
285
  case 'system':
381
- return {
382
- type: 'system',
383
- role: 'system',
384
- content: `Model: ${json.model || 'unknown'}, Mode: ${json.permissionMode || 'default'}`,
385
- timestamp,
386
- };
387
-
286
+ case 'git':
287
+ // Handle internal logger JSON or cursor-agent system events
288
+ if (json.content && typeof json.content === 'string') {
289
+ return {
290
+ type: type as any,
291
+ role: 'system',
292
+ content: json.content,
293
+ timestamp,
294
+ };
295
+ }
296
+ if (type === 'system') {
297
+ return {
298
+ type: 'system',
299
+ role: 'system',
300
+ content: `Model: ${json.model || 'unknown'}, Mode: ${json.permissionMode || 'default'}`,
301
+ timestamp,
302
+ };
303
+ }
304
+ return null;
305
+
388
306
  case 'user':
389
307
  if (json.message?.content) {
390
308
  const textContent = json.message.content
@@ -480,51 +398,73 @@ export class EnhancedLogManager {
480
398
  */
481
399
  public writeStderr(data: Buffer | string): void {
482
400
  const text = data.toString();
483
-
484
- // Write raw log
485
- this.writeToRawLog(data);
486
-
487
- // Write to readable log - treat stderr same as stdout
488
- // Git and many tools use stderr for non-error output
489
401
  const lines = text.split('\n');
490
402
  for (const line of lines) {
491
403
  const cleanLine = stripAnsi(line).trim();
492
404
  if (cleanLine && !this.isNoiseLog(cleanLine)) {
493
- const hasTimestamp = /^\[(\d{4}-\d{2}-\d{2}T|\d{2}:\d{2}:\d{2})\]/.test(cleanLine);
494
- const label = this.getLaneTaskLabel();
495
- const ts = this.getShortTime();
496
-
497
- // Determine if this is actually an error message
498
405
  const isError = this.isErrorLine(cleanLine);
499
- const prefix = isError ? '❌ ERR' : '';
406
+ const isGit = this.isGitOutput(cleanLine);
407
+ const type = isError ? 'stderr' : (isGit ? 'git' : 'stderr');
500
408
 
501
- let formattedLine: string;
502
- if (hasTimestamp) {
503
- formattedLine = cleanLine.includes(`[${label}]`)
504
- ? cleanLine
505
- : cleanLine.replace(/^(\[[^\]]+\])/, `$1 [${label}]${prefix ? ' ' + prefix : ''}`);
506
- } else {
507
- formattedLine = prefix
508
- ? `[${ts}] [${label}] ${prefix} ${cleanLine}`
509
- : `[${ts}] [${label}] ${cleanLine}`;
409
+ // Write to JSON log
410
+ if (this.config.writeJsonLog) {
411
+ this.writeToJsonLog({
412
+ type,
413
+ role: 'system',
414
+ content: cleanLine,
415
+ timestamp: Date.now(),
416
+ metadata: { isError, isGit }
417
+ });
510
418
  }
511
419
 
512
- this.writeToReadableLog(`${formattedLine}\n`);
513
-
514
420
  // Output to console
515
421
  if (this.onParsedMessage) {
516
- const rawMsg: ParsedMessage = {
517
- type: 'raw',
422
+ this.onParsedMessage({
423
+ type,
518
424
  role: 'system',
519
- content: formattedLine,
425
+ content: line.trim(),
520
426
  timestamp: Date.now(),
521
- };
522
- this.onParsedMessage(rawMsg);
427
+ });
523
428
  }
524
429
  }
525
430
  }
526
431
  }
527
432
 
433
+ /**
434
+ * Check if a line is likely Git output
435
+ */
436
+ private isGitOutput(text: string): boolean {
437
+ const gitPatterns = [
438
+ /^Running: git /i,
439
+ /^git version /i,
440
+ /^On branch /i,
441
+ /^Your branch is /i,
442
+ /^Changes to be committed:/i,
443
+ /^Changes not staged for commit:/i,
444
+ /^Untracked files:/i,
445
+ /^\s*\((use "git |use --|use -)/i,
446
+ /^\s*modified:\s+/i,
447
+ /^\s*new file:\s+/i,
448
+ /^\s*deleted:\s+/i,
449
+ /^\s*renamed:\s+/i,
450
+ /^Switched to branch /i,
451
+ /^Switched to a new branch /i,
452
+ /^Merge made by the /i,
453
+ /^Everything up-to-date/i,
454
+ /^Already up to date/i,
455
+ /^branch '.*' set up to track remote branch '.*' from 'origin'/i,
456
+ /^Counting objects: /i,
457
+ /^Compressing objects: /i,
458
+ /^Writing objects: /i,
459
+ /^Total \d+ \(delta \d+\)/i,
460
+ /^remote: Resolving deltas:/i,
461
+ /^To .*\.git/i,
462
+ /^[a-f0-9]{7,40}\.\.[a-f0-9]{7,40}\s+.*->\s+.*/i,
463
+ ];
464
+
465
+ return gitPatterns.some(p => p.test(text));
466
+ }
467
+
528
468
  /**
529
469
  * Check if a line is actually an error message
530
470
  */
@@ -550,24 +490,43 @@ export class EnhancedLogManager {
550
490
  * Write a custom log entry
551
491
  */
552
492
  public log(level: 'info' | 'error' | 'debug', message: string, metadata?: Record<string, any>): void {
553
- const ts = this.getShortTime();
554
- const label = this.getLaneTaskLabel();
555
- const emoji = level === 'error' ? '❌' : level === 'info' ? 'ℹ️' : '🔍';
556
- const line = `[${ts}] [${label}] ${emoji} ${level.toUpperCase()} ${message}\n`;
557
-
558
- this.writeToRawLog(line);
559
- this.writeToReadableLog(line);
493
+ const isGit = metadata?.context === 'git';
494
+ const type = isGit ? 'git' : (level as any);
495
+
496
+ if (this.config.writeJsonLog) {
497
+ this.writeToJsonLog({
498
+ type,
499
+ role: 'system',
500
+ content: message,
501
+ timestamp: Date.now(),
502
+ metadata
503
+ });
504
+ }
505
+
506
+ if (this.onParsedMessage) {
507
+ this.onParsedMessage({
508
+ type,
509
+ role: 'system',
510
+ content: message,
511
+ timestamp: Date.now(),
512
+ metadata
513
+ });
514
+ }
560
515
  }
561
516
 
562
517
  /**
563
518
  * Add a section marker
564
519
  */
565
520
  public section(title: string): void {
566
- const divider = '═'.repeat(78);
567
- const line = `\n${divider}\n ${title}\n${divider}\n`;
568
-
569
- this.writeToRawLog(line);
570
- this.writeToReadableLog(line);
521
+ if (this.config.writeJsonLog) {
522
+ this.writeToJsonLog({
523
+ type: 'info',
524
+ role: 'system',
525
+ content: `--- ${title} ---`,
526
+ timestamp: Date.now(),
527
+ metadata: { isSection: true }
528
+ });
529
+ }
571
530
  }
572
531
 
573
532
  /**
@@ -605,11 +564,10 @@ export class EnhancedLogManager {
605
564
  /**
606
565
  * Get paths to all log files
607
566
  */
608
- public getLogPaths(): { clean: string; raw: string; readable: string } {
567
+ public getLogPaths(): { jsonl: string; clean: string } {
609
568
  return {
610
- clean: this.readableLogPath, // For backward compatibility
611
- raw: this.rawLogPath,
612
- readable: this.readableLogPath,
569
+ jsonl: this.jsonlLogPath,
570
+ clean: this.jsonlLogPath, // Backward compatibility
613
571
  };
614
572
  }
615
573
 
@@ -617,7 +575,7 @@ export class EnhancedLogManager {
617
575
  * Create file descriptors for process stdio redirection
618
576
  */
619
577
  public getFileDescriptors(): { stdout: number; stderr: number } {
620
- const fd = this.rawLogFd!;
578
+ const fd = this.jsonlLogFd!;
621
579
  return { stdout: fd, stderr: fd };
622
580
  }
623
581
 
@@ -625,31 +583,23 @@ export class EnhancedLogManager {
625
583
  * Close all log files
626
584
  */
627
585
  public close(): void {
628
- // Write session end marker
629
- const endMarker = `
630
- ╔══════════════════════════════════════════════════════════════════════════════╗
631
- ║ Session Ended: ${new Date().toISOString().padEnd(60)}║
632
- ║ Duration: ${this.formatDuration(Date.now() - this.session.startTime).padEnd(65)}║
633
- ╚══════════════════════════════════════════════════════════════════════════════╝
634
-
635
- `;
636
-
637
- if (this.rawLogFd !== null) {
586
+ if (this.jsonlLogFd !== null) {
638
587
  try {
639
- fs.writeSync(this.rawLogFd, endMarker);
640
- fs.fsyncSync(this.rawLogFd);
641
- fs.closeSync(this.rawLogFd);
642
- } catch {}
643
- this.rawLogFd = null;
644
- }
645
-
646
- if (this.readableLogFd !== null) {
647
- try {
648
- fs.writeSync(this.readableLogFd, endMarker);
649
- fs.fsyncSync(this.readableLogFd);
650
- fs.closeSync(this.readableLogFd);
588
+ if (this.config.writeJsonLog) {
589
+ this.writeToJsonLog({
590
+ type: 'session',
591
+ role: 'system',
592
+ content: 'Session ended',
593
+ timestamp: Date.now(),
594
+ metadata: {
595
+ duration: Date.now() - this.session.startTime,
596
+ }
597
+ });
598
+ }
599
+ fs.fsyncSync(this.jsonlLogFd);
600
+ fs.closeSync(this.jsonlLogFd);
651
601
  } catch {}
652
- this.readableLogFd = null;
602
+ this.jsonlLogFd = null;
653
603
  }
654
604
  }
655
605
 
@@ -658,40 +608,22 @@ export class EnhancedLogManager {
658
608
  */
659
609
  public getLastError(): string | null {
660
610
  try {
661
- if (!fs.existsSync(this.readableLogPath)) return null;
662
- const content = fs.readFileSync(this.readableLogPath, 'utf8');
663
- const lines = content.split('\n').filter(l =>
664
- l.includes('❌') ||
665
- l.includes('[ERROR]') ||
666
- l.includes('error:') ||
667
- l.includes('Fatal') ||
668
- l.includes('fail')
669
- );
670
- if (lines.length === 0) {
671
- const allLines = content.split('\n').filter(l => l.trim());
672
- return allLines.slice(-5).join('\n');
611
+ if (!fs.existsSync(this.jsonlLogPath)) return null;
612
+ const content = fs.readFileSync(this.jsonlLogPath, 'utf8');
613
+ const lines = content.split('\n').filter(l => l.trim());
614
+ for (let i = lines.length - 1; i >= 0; i--) {
615
+ try {
616
+ const entry = JSON.parse(lines[i]!);
617
+ if (entry.type === 'error' || entry.type === 'stderr' && entry.metadata?.isError) {
618
+ return entry.content;
619
+ }
620
+ } catch {}
673
621
  }
674
- return lines[lines.length - 1]!.trim();
622
+ return null;
675
623
  } catch {
676
624
  return null;
677
625
  }
678
626
  }
679
-
680
- /**
681
- * Format duration for display
682
- */
683
- private formatDuration(ms: number): string {
684
- const seconds = Math.floor((ms / 1000) % 60);
685
- const minutes = Math.floor((ms / (1000 * 60)) % 60);
686
- const hours = Math.floor(ms / (1000 * 60 * 60));
687
-
688
- if (hours > 0) {
689
- return `${hours}h ${minutes}m ${seconds}s`;
690
- } else if (minutes > 0) {
691
- return `${minutes}m ${seconds}s`;
692
- }
693
- return `${seconds}s`;
694
- }
695
627
  }
696
628
 
697
629
  /**
@@ -716,26 +648,43 @@ export function createLogManager(
716
648
  }
717
649
 
718
650
  /**
719
- * Read and parse JSON log file (legacy compatibility - returns empty array)
651
+ * Read and parse JSON log file
720
652
  */
721
- export function readJsonLog(logPath: string): JsonLogEntry[] {
722
- return [];
653
+ export function readJsonLog(logPath: string): any[] {
654
+ try {
655
+ if (!fs.existsSync(logPath)) return [];
656
+ const content = fs.readFileSync(logPath, 'utf8');
657
+ return content.split('\n')
658
+ .filter(l => l.trim())
659
+ .map(l => {
660
+ try { return JSON.parse(l); } catch { return null; }
661
+ })
662
+ .filter(Boolean);
663
+ } catch {
664
+ return [];
665
+ }
723
666
  }
724
667
 
725
668
  /**
726
- * Export logs (legacy compatibility)
669
+ * Export logs
727
670
  */
728
671
  export function exportLogs(
729
672
  laneRunDir: string,
730
673
  format: 'text' | 'json' | 'markdown' | 'html',
731
674
  outputPath?: string
732
675
  ): string {
733
- const logPath = getLaneLogPath(laneRunDir, 'raw');
676
+ const logPath = getLaneLogPath(laneRunDir, 'jsonl');
734
677
 
735
678
  let output = '';
736
-
737
679
  if (fs.existsSync(logPath)) {
738
- output = fs.readFileSync(logPath, 'utf8');
680
+ if (format === 'json') {
681
+ const logs = readJsonLog(logPath);
682
+ output = JSON.stringify(logs, null, 2);
683
+ } else {
684
+ // Basic text conversion for other formats
685
+ const logs = readJsonLog(logPath);
686
+ output = logs.map(l => `[${l.timestamp_iso}] [${l.type}] ${l.content}`).join('\n');
687
+ }
739
688
  }
740
689
 
741
690
  if (outputPath) {