@litmers/cursorflow-orchestrator 0.2.3 → 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 (67) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -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/logs.js +51 -61
  7. package/dist/cli/logs.js.map +1 -1
  8. package/dist/cli/monitor.js +56 -44
  9. package/dist/cli/monitor.js.map +1 -1
  10. package/dist/cli/resume.js +2 -2
  11. package/dist/cli/resume.js.map +1 -1
  12. package/dist/core/git-lifecycle-manager.js +2 -2
  13. package/dist/core/git-lifecycle-manager.js.map +1 -1
  14. package/dist/core/git-pipeline-coordinator.js +25 -25
  15. package/dist/core/git-pipeline-coordinator.js.map +1 -1
  16. package/dist/core/orchestrator.js +8 -7
  17. package/dist/core/orchestrator.js.map +1 -1
  18. package/dist/core/runner/pipeline.js +3 -3
  19. package/dist/core/runner/pipeline.js.map +1 -1
  20. package/dist/hooks/data-accessor.js +2 -2
  21. package/dist/hooks/data-accessor.js.map +1 -1
  22. package/dist/services/logging/buffer.d.ts +1 -2
  23. package/dist/services/logging/buffer.js +22 -63
  24. package/dist/services/logging/buffer.js.map +1 -1
  25. package/dist/services/logging/formatter.d.ts +4 -0
  26. package/dist/services/logging/formatter.js +201 -33
  27. package/dist/services/logging/formatter.js.map +1 -1
  28. package/dist/services/logging/paths.d.ts +0 -3
  29. package/dist/services/logging/paths.js +0 -3
  30. package/dist/services/logging/paths.js.map +1 -1
  31. package/dist/types/config.d.ts +1 -9
  32. package/dist/types/logging.d.ts +1 -1
  33. package/dist/utils/config.js +2 -6
  34. package/dist/utils/config.js.map +1 -1
  35. package/dist/utils/enhanced-logger.d.ts +17 -37
  36. package/dist/utils/enhanced-logger.js +237 -267
  37. package/dist/utils/enhanced-logger.js.map +1 -1
  38. package/dist/utils/logger.js +17 -4
  39. package/dist/utils/logger.js.map +1 -1
  40. package/dist/utils/repro-thinking-logs.js +4 -4
  41. package/dist/utils/repro-thinking-logs.js.map +1 -1
  42. package/package.json +2 -2
  43. package/scripts/monitor-lanes.sh +5 -5
  44. package/scripts/stream-logs.sh +1 -1
  45. package/scripts/test-log-parser.ts +8 -42
  46. package/src/cli/complete.ts +305 -0
  47. package/src/cli/logs.ts +46 -60
  48. package/src/cli/monitor.ts +64 -46
  49. package/src/cli/resume.ts +1 -1
  50. package/src/core/git-lifecycle-manager.ts +2 -2
  51. package/src/core/git-pipeline-coordinator.ts +25 -25
  52. package/src/core/orchestrator.ts +7 -7
  53. package/src/core/runner/pipeline.ts +3 -3
  54. package/src/hooks/data-accessor.ts +2 -2
  55. package/src/services/logging/buffer.ts +20 -68
  56. package/src/services/logging/formatter.ts +199 -32
  57. package/src/services/logging/paths.ts +0 -3
  58. package/src/types/config.ts +1 -13
  59. package/src/types/logging.ts +2 -0
  60. package/src/utils/config.ts +2 -6
  61. package/src/utils/enhanced-logger.ts +239 -290
  62. package/src/utils/logger.ts +18 -3
  63. package/src/utils/repro-thinking-logs.ts +4 -4
  64. package/dist/utils/log-formatter.d.ts +0 -26
  65. package/dist/utils/log-formatter.js +0 -274
  66. package/dist/utils/log-formatter.js.map +0 -1
  67. package/src/utils/log-formatter.ts +0 -287
package/src/cli/logs.ts CHANGED
@@ -10,10 +10,9 @@ import { safeJoin } from '../utils/path';
10
10
  import {
11
11
  readJsonLog,
12
12
  exportLogs,
13
- stripAnsi,
14
13
  JsonLogEntry
15
14
  } from '../utils/enhanced-logger';
16
- import { formatPotentialJsonMessage } from '../utils/log-formatter';
15
+ import { formatMessageForConsole, formatPotentialJsonMessage, stripAnsi } from '../services/logging/formatter';
17
16
  import { MAIN_LOG_FILENAME } from '../utils/log-constants';
18
17
  import { startLogViewer } from '../ui/log-viewer';
19
18
 
@@ -152,32 +151,33 @@ function listLanes(runDir: string): string[] {
152
151
  }
153
152
 
154
153
  /**
155
- * Read and display text logs
154
+ * Read and display text logs (converted from JSONL)
156
155
  */
157
156
  function displayTextLogs(
158
157
  laneDir: string,
159
158
  options: LogsOptions
160
159
  ): void {
161
- let logFile: string;
162
- const readableLog = safeJoin(laneDir, 'terminal-readable.log');
163
- const rawLog = safeJoin(laneDir, 'terminal-raw.log');
164
-
165
- if (options.raw) {
166
- logFile = rawLog;
167
- } else {
168
- // Default to readable log (clean option also uses readable now)
169
- logFile = readableLog;
170
- }
160
+ const logFile = safeJoin(laneDir, 'terminal.jsonl');
171
161
 
172
162
  if (!fs.existsSync(logFile)) {
173
163
  console.log('No log file found.');
174
164
  return;
175
165
  }
176
166
 
177
- let content = fs.readFileSync(logFile, 'utf8');
178
- let lines = content.split('\n');
167
+ const entries = readJsonLog(logFile);
168
+ let lines = entries.map(entry => {
169
+ const ts = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
170
+ const level = entry.level || 'info';
171
+ const content = entry.content || entry.message || '';
172
+
173
+ if (options.raw) {
174
+ // In "raw" mode for JSONL, we show a basic text representation
175
+ return `[${ts}] [${level.toUpperCase()}] ${content}`;
176
+ }
177
+ return `[${ts}] [${level.toUpperCase()}] ${stripAnsi(content)}`;
178
+ });
179
179
 
180
- // Apply filter (case-insensitive string match to avoid ReDoS)
180
+ // Apply filter
181
181
  if (options.filter) {
182
182
  const filterLower = options.filter.toLowerCase();
183
183
  lines = lines.filter(line => line.toLowerCase().includes(filterLower));
@@ -188,11 +188,6 @@ function displayTextLogs(
188
188
  lines = lines.slice(-options.tail);
189
189
  }
190
190
 
191
- // Clean ANSI if needed (for clean mode or default fallback)
192
- if (!options.raw) {
193
- lines = lines.map(line => stripAnsi(line));
194
- }
195
-
196
191
  console.log(lines.join('\n'));
197
192
  }
198
193
 
@@ -708,16 +703,7 @@ function escapeHtml(text: string): string {
708
703
  * Follow logs in real-time
709
704
  */
710
705
  function followLogs(laneDir: string, options: LogsOptions): void {
711
- let logFile: string;
712
- const readableLog = safeJoin(laneDir, 'terminal-readable.log');
713
- const rawLog = safeJoin(laneDir, 'terminal-raw.log');
714
-
715
- if (options.raw) {
716
- logFile = rawLog;
717
- } else {
718
- // Default to readable log
719
- logFile = readableLog;
720
- }
706
+ const logFile = safeJoin(laneDir, 'terminal.jsonl');
721
707
 
722
708
  if (!fs.existsSync(logFile)) {
723
709
  console.log('Waiting for log file...');
@@ -725,17 +711,14 @@ function followLogs(laneDir: string, options: LogsOptions): void {
725
711
 
726
712
  let lastSize = 0;
727
713
  try {
728
- // Use statSync directly to avoid TOCTOU race condition
729
714
  lastSize = fs.statSync(logFile).size;
730
715
  } catch {
731
- // File doesn't exist yet or other error - start from 0
732
716
  lastSize = 0;
733
717
  }
734
718
 
735
719
  console.log(`${logger.COLORS.cyan}Following ${logFile}... (Ctrl+C to stop)${logger.COLORS.reset}\n`);
736
720
 
737
721
  const checkInterval = setInterval(() => {
738
- // Use fstat on open fd to avoid TOCTOU race condition
739
722
  let fd: number | null = null;
740
723
  try {
741
724
  fd = fs.openSync(logFile, 'r');
@@ -744,28 +727,37 @@ function followLogs(laneDir: string, options: LogsOptions): void {
744
727
  const buffer = Buffer.alloc(stats.size - lastSize);
745
728
  fs.readSync(fd, buffer, 0, buffer.length, lastSize);
746
729
 
747
- let content = buffer.toString();
748
-
749
- // Apply filter (case-insensitive string match to avoid ReDoS)
750
- if (options.filter) {
751
- const filterLower = options.filter.toLowerCase();
752
- const lines = content.split('\n');
753
- content = lines.filter(line => line.toLowerCase().includes(filterLower)).join('\n');
754
- }
755
-
756
- // Clean ANSI if needed (unless raw mode)
757
- if (!options.raw) {
758
- content = stripAnsi(content);
759
- }
730
+ const content = buffer.toString();
731
+ const lines = content.split('\n').filter(l => l.trim());
760
732
 
761
- if (content.trim()) {
762
- process.stdout.write(content);
733
+ for (const line of lines) {
734
+ try {
735
+ const entry = JSON.parse(line);
736
+ const ts = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
737
+ const level = entry.level || 'info';
738
+ const levelColor = getLevelColor(level);
739
+ const message = entry.content || entry.message || '';
740
+
741
+ // Apply level filter
742
+ if (options.level && level !== options.level) continue;
743
+
744
+ // Apply filter
745
+ if (options.filter) {
746
+ const filterLower = options.filter.toLowerCase();
747
+ if (!message.toLowerCase().includes(filterLower)) continue;
748
+ }
749
+
750
+ const displayMsg = options.raw ? message : stripAnsi(message);
751
+ console.log(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${levelColor}[${level.toUpperCase().padEnd(6)}]${logger.COLORS.reset} ${displayMsg}`);
752
+ } catch {
753
+ // Skip invalid JSON
754
+ }
763
755
  }
764
756
 
765
757
  lastSize = stats.size;
766
758
  }
767
759
  } catch {
768
- // Ignore errors (file might be rotating)
760
+ // Ignore errors
769
761
  } finally {
770
762
  if (fd !== null) {
771
763
  try { fs.closeSync(fd); } catch { /* ignore */ }
@@ -860,19 +852,13 @@ function displaySummary(runDir: string): void {
860
852
 
861
853
  for (const lane of lanes) {
862
854
  const laneDir = safeJoin(runDir, 'lanes', lane);
863
- const rawLog = safeJoin(laneDir, 'terminal-raw.log');
864
- const readableLog = safeJoin(laneDir, 'terminal-readable.log');
855
+ const jsonlLog = safeJoin(laneDir, 'terminal.jsonl');
865
856
 
866
857
  console.log(` ${logger.COLORS.green}📁 ${lane}${logger.COLORS.reset}`);
867
858
 
868
- if (fs.existsSync(readableLog)) {
869
- const stats = fs.statSync(readableLog);
870
- console.log(` └─ terminal-readable.log ${formatSize(stats.size)} ${logger.COLORS.yellow}(default)${logger.COLORS.reset}`);
871
- }
872
-
873
- if (fs.existsSync(rawLog)) {
874
- const stats = fs.statSync(rawLog);
875
- console.log(` └─ terminal-raw.log ${formatSize(stats.size)}`);
859
+ if (fs.existsSync(jsonlLog)) {
860
+ const stats = fs.statSync(jsonlLog);
861
+ console.log(` └─ terminal.jsonl ${formatSize(stats.size)} ${logger.COLORS.yellow}(unified)${logger.COLORS.reset}`);
876
862
  }
877
863
 
878
864
  console.log('');
@@ -672,6 +672,7 @@ class InteractiveMonitor {
672
672
  private getLaneActions(lane: LaneInfo): ActionItem[] {
673
673
  const status = this.getLaneStatus(lane.path, lane.name);
674
674
  const isRunning = status.status === 'running';
675
+ const isCompleted = status.status === 'completed' || status.status === 'success';
675
676
 
676
677
  return [
677
678
  {
@@ -690,6 +691,14 @@ class InteractiveMonitor {
690
691
  disabled: !isRunning,
691
692
  disabledReason: 'Lane not running',
692
693
  },
694
+ {
695
+ id: 'resume',
696
+ label: 'Resume Lane',
697
+ icon: '▶️',
698
+ action: () => this.resumeLane(lane),
699
+ disabled: isRunning || isCompleted,
700
+ disabledReason: isRunning ? 'Lane already running' : 'Lane already completed',
701
+ },
693
702
  {
694
703
  id: 'stop',
695
704
  label: 'Stop Lane',
@@ -716,6 +725,8 @@ class InteractiveMonitor {
716
725
 
717
726
  private getFlowActions(flow: FlowInfo): ActionItem[] {
718
727
  const isCurrent = flow.runDir === this.runDir;
728
+ const isAlive = flow.isAlive;
729
+ const isCompleted = flow.summary.completed === flow.summary.total && flow.summary.total > 0;
719
730
 
720
731
  return [
721
732
  {
@@ -726,6 +737,14 @@ class InteractiveMonitor {
726
737
  disabled: isCurrent,
727
738
  disabledReason: 'Already viewing this flow',
728
739
  },
740
+ {
741
+ id: 'resume',
742
+ label: 'Resume Flow',
743
+ icon: '▶️',
744
+ action: () => this.resumeFlow(flow),
745
+ disabled: isAlive || isCompleted,
746
+ disabledReason: isAlive ? 'Flow is already running' : 'Flow is already completed',
747
+ },
729
748
  {
730
749
  id: 'delete',
731
750
  label: 'Delete Flow',
@@ -874,6 +893,45 @@ class InteractiveMonitor {
874
893
  this.render();
875
894
  }
876
895
 
896
+ private resumeFlow(flow: FlowInfo) {
897
+ this.runResumeCommand(['--all', '--run-dir', flow.runDir]);
898
+ }
899
+
900
+ private resumeLane(lane: LaneInfo) {
901
+ this.runResumeCommand([lane.name, '--run-dir', this.runDir]);
902
+ }
903
+
904
+ private runResumeCommand(args: string[]) {
905
+ try {
906
+ const { spawn } = require('child_process');
907
+
908
+ // Determine the script to run
909
+ // In production, it's dist/cli/index.js. In dev, it's src/cli/index.ts.
910
+ let entryPoint = path.resolve(__dirname, 'index.js');
911
+ if (!fs.existsSync(entryPoint)) {
912
+ entryPoint = path.resolve(__dirname, 'index.ts');
913
+ }
914
+
915
+ const spawnArgs = [entryPoint, 'resume', ...args, '--skip-doctor'];
916
+
917
+ // If it's a .ts file, we need ts-node or similar (assuming it's available)
918
+ const nodeArgs = entryPoint.endsWith('.ts') ? ['-r', 'ts-node/register'] : [];
919
+
920
+ const child = spawn(process.execPath, [...nodeArgs, ...spawnArgs], {
921
+ detached: true,
922
+ stdio: 'ignore',
923
+ env: { ...process.env, NODE_OPTIONS: '' }
924
+ });
925
+
926
+ child.unref();
927
+
928
+ const target = args[0] === '--all' ? 'flow' : `lane ${args[0]}`;
929
+ this.showNotification(`Resume started for ${target}`, 'success');
930
+ } catch (error: any) {
931
+ this.showNotification(`Failed to spawn resume: ${error.message}`, 'error');
932
+ }
933
+ }
934
+
877
935
  private switchToFlow(flow: FlowInfo) {
878
936
  this.runDir = flow.runDir;
879
937
 
@@ -1352,55 +1410,15 @@ class InteractiveMonitor {
1352
1410
  }
1353
1411
 
1354
1412
  private getTerminalLines(lanePath: string, maxLines: number): string[] {
1355
- const { dim, reset, cyan, green, yellow, red, gray } = UI.COLORS;
1413
+ const { dim, reset } = UI.COLORS;
1356
1414
 
1357
- // Choose log source based on format setting
1358
- if (this.state.readableFormat) {
1359
- // Try JSONL first for structured readable format
1360
- const jsonlPath = safeJoin(lanePath, 'terminal.jsonl');
1361
- if (fs.existsSync(jsonlPath)) {
1362
- return this.getJsonlLogLines(jsonlPath, maxLines);
1363
- }
1364
- }
1365
-
1366
- // Fallback to raw terminal log
1367
- const logPath = safeJoin(lanePath, 'terminal-readable.log');
1368
- if (!fs.existsSync(logPath)) {
1369
- return [`${dim}(No output yet)${reset}`];
1415
+ // Use terminal.jsonl as the only source for structured readable format
1416
+ const jsonlPath = safeJoin(lanePath, 'terminal.jsonl');
1417
+ if (fs.existsSync(jsonlPath)) {
1418
+ return this.getJsonlLogLines(jsonlPath, maxLines);
1370
1419
  }
1371
1420
 
1372
- try {
1373
- const content = fs.readFileSync(logPath, 'utf8');
1374
- const allLines = content.split('\n');
1375
- const totalLines = allLines.length;
1376
-
1377
- // Calculate visible range (from end, accounting for scroll offset)
1378
- const end = Math.max(0, totalLines - this.state.terminalScrollOffset);
1379
- const start = Math.max(0, end - maxLines);
1380
- const visibleLines = allLines.slice(start, end);
1381
-
1382
- // Format lines with syntax highlighting
1383
- return visibleLines.map(line => {
1384
- if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
1385
- return `${yellow}${line}${reset}`;
1386
- }
1387
- if (line.includes('=== Task:') || line.includes('Starting task:')) {
1388
- return `${green}${line}${reset}`;
1389
- }
1390
- if (line.includes('Executing cursor-agent') || line.includes('cursor-agent-v')) {
1391
- return `${cyan}${line}${reset}`;
1392
- }
1393
- if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
1394
- return `${red}${line}${reset}`;
1395
- }
1396
- if (line.toLowerCase().includes('success') || line.toLowerCase().includes('completed')) {
1397
- return `${green}${line}${reset}`;
1398
- }
1399
- return line;
1400
- });
1401
- } catch {
1402
- return [`${dim}(Error reading log)${reset}`];
1403
- }
1421
+ return [`${dim}(No output yet)${reset}`];
1404
1422
  }
1405
1423
 
1406
1424
  /**
package/src/cli/resume.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  createLogManager,
18
18
  ParsedMessage
19
19
  } from '../utils/enhanced-logger';
20
- import { formatMessageForConsole } from '../utils/log-formatter';
20
+ import { formatMessageForConsole } from '../services/logging/formatter';
21
21
  import { MAIN_LOG_FILENAME } from '../utils/log-constants';
22
22
 
23
23
  interface ResumeOptions {
@@ -977,9 +977,9 @@ export class GitLifecycleManager {
977
977
  */
978
978
  private log(message: string): void {
979
979
  if (this.verbose) {
980
- logger.debug(`[GitLifecycle] ${message}`);
980
+ logger.debug(`[GitLifecycle] ${message}`, { context: 'git' });
981
981
  } else {
982
- logger.info(`[GitLifecycle] ${message}`);
982
+ logger.info(`[GitLifecycle] ${message}`, { context: 'git' });
983
983
  }
984
984
  }
985
985
 
@@ -21,12 +21,12 @@ export class GitPipelineCoordinator {
21
21
  const worktreeIsInvalid = !worktreeNeedsCreation && !git.isValidWorktree(worktreeDir);
22
22
 
23
23
  if (worktreeIsInvalid) {
24
- logger.warn(`⚠️ Directory exists but is not a valid worktree: ${worktreeDir}`);
25
- logger.info(` Cleaning up invalid directory and recreating worktree...`);
24
+ logger.warn(`⚠️ Directory exists but is not a valid worktree: ${worktreeDir}`, { context: 'git' });
25
+ logger.info(` Cleaning up invalid directory and recreating worktree...`, { context: 'git' });
26
26
  try {
27
27
  git.cleanupInvalidWorktreeDir(worktreeDir);
28
28
  } catch (e: any) {
29
- logger.error(`Failed to cleanup invalid worktree directory: ${e.message}`);
29
+ logger.error(`Failed to cleanup invalid worktree directory: ${e.message}`, { context: 'git' });
30
30
  throw new Error(`Cannot proceed: worktree directory is invalid and cleanup failed`);
31
31
  }
32
32
  }
@@ -52,7 +52,7 @@ export class GitPipelineCoordinator {
52
52
  retries--;
53
53
  if (retries > 0) {
54
54
  const delay = Math.floor(Math.random() * 1000) + 500;
55
- logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
55
+ logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`, { context: 'git' });
56
56
  await new Promise(resolve => setTimeout(resolve, delay));
57
57
  }
58
58
  }
@@ -62,11 +62,11 @@ export class GitPipelineCoordinator {
62
62
  throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
63
63
  }
64
64
  } else {
65
- logger.info(`Reusing existing worktree: ${worktreeDir}`);
65
+ logger.info(`Reusing existing worktree: ${worktreeDir}`, { context: 'git' });
66
66
  try {
67
67
  git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
68
68
  } catch (e) {
69
- logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
69
+ logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`, { context: 'git' });
70
70
  }
71
71
  }
72
72
  }
@@ -82,7 +82,7 @@ export class GitPipelineCoordinator {
82
82
  const lanesRoot = path.dirname(runDir);
83
83
  const lanesToMerge = new Set(deps.map(d => d.split(':')[0]!));
84
84
 
85
- logger.info(`🔄 Syncing with ${pipelineBranch} before merging dependencies`);
85
+ logger.info(`🔄 Syncing with ${pipelineBranch} before merging dependencies`, { context: 'git' });
86
86
  git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
87
87
 
88
88
  for (const laneName of lanesToMerge) {
@@ -93,15 +93,15 @@ export class GitPipelineCoordinator {
93
93
  const state = loadState<LaneState>(depStatePath);
94
94
  if (!state?.pipelineBranch) continue;
95
95
 
96
- logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
96
+ logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`, { context: 'git' });
97
97
  git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
98
98
 
99
99
  const remoteBranchRef = `origin/${state.pipelineBranch}`;
100
100
  const conflictCheck = git.checkMergeConflict(remoteBranchRef, { cwd: worktreeDir });
101
101
 
102
102
  if (conflictCheck.willConflict) {
103
- logger.warn(`⚠️ Pre-check: Merge conflict detected with ${laneName}`);
104
- logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
103
+ logger.warn(`⚠️ Pre-check: Merge conflict detected with ${laneName}`, { context: 'git' });
104
+ logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`, { context: 'git' });
105
105
 
106
106
  events.emit('merge.conflict_detected', {
107
107
  laneName,
@@ -125,15 +125,15 @@ export class GitPipelineCoordinator {
125
125
 
126
126
  if (!mergeResult.success) {
127
127
  if (mergeResult.conflict) {
128
- logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
128
+ logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`, { context: 'git' });
129
129
  throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
130
130
  }
131
131
  throw new Error(mergeResult.error || 'Merge failed');
132
132
  }
133
133
 
134
- logger.success(`✓ Merged ${laneName}`);
134
+ logger.success(`✓ Merged ${laneName}`, { context: 'git' });
135
135
  } catch (e) {
136
- logger.error(`Failed to merge branch from ${laneName}: ${e}`);
136
+ logger.error(`Failed to merge branch from ${laneName}: ${e}`, { context: 'git' });
137
137
  throw e;
138
138
  }
139
139
  }
@@ -150,15 +150,15 @@ export class GitPipelineCoordinator {
150
150
  pipelineBranch: string;
151
151
  worktreeDir: string;
152
152
  }): void {
153
- logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
154
- logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
153
+ logger.info(`Merging ${taskBranch} → ${pipelineBranch}`, { context: 'git' });
154
+ logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`, { context: 'git' });
155
155
  git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
156
156
 
157
157
  const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
158
158
  if (conflictCheck.willConflict) {
159
- logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`);
160
- logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
161
- logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
159
+ logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`, { context: 'git' });
160
+ logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`, { context: 'git' });
161
+ logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`, { context: 'git' });
162
162
 
163
163
  events.emit('merge.conflict_detected', {
164
164
  taskName,
@@ -169,7 +169,7 @@ export class GitPipelineCoordinator {
169
169
  });
170
170
  }
171
171
 
172
- logger.info(`🔀 Merging task ${taskName} (${taskBranch}) into ${pipelineBranch}`);
172
+ logger.info(`🔀 Merging task ${taskName} (${taskBranch}) into ${pipelineBranch}`, { context: 'git' });
173
173
  const mergeResult = git.safeMerge(taskBranch, {
174
174
  cwd: worktreeDir,
175
175
  noFf: true,
@@ -179,7 +179,7 @@ export class GitPipelineCoordinator {
179
179
 
180
180
  if (!mergeResult.success) {
181
181
  if (mergeResult.conflict) {
182
- logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
182
+ logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`, { context: 'git' });
183
183
  throw new Error(
184
184
  `Merge conflict when integrating task ${taskName}: ${mergeResult.conflictingFiles.join(', ')}`
185
185
  );
@@ -189,7 +189,7 @@ export class GitPipelineCoordinator {
189
189
 
190
190
  const stats = git.getLastOperationStats(worktreeDir);
191
191
  if (stats) {
192
- logger.info('Changed files:\n' + stats);
192
+ logger.info('Changed files:\n' + stats, { context: 'git' });
193
193
  }
194
194
  }
195
195
 
@@ -204,18 +204,18 @@ export class GitPipelineCoordinator {
204
204
  }): void {
205
205
  if (flowBranch === pipelineBranch) return;
206
206
 
207
- logger.info(`🌿 Creating final flow branch: ${flowBranch}`);
207
+ logger.info(`🌿 Creating final flow branch: ${flowBranch}`, { context: 'git' });
208
208
  try {
209
209
  git.runGit(['checkout', '-B', flowBranch, pipelineBranch], { cwd: worktreeDir });
210
210
  git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
211
211
 
212
- logger.info(`🗑️ Deleting local pipeline branch: ${pipelineBranch}`);
212
+ logger.info(`🗑️ Deleting local pipeline branch: ${pipelineBranch}`, { context: 'git' });
213
213
  git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
214
214
  git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
215
215
 
216
- logger.success(`✓ Flow branch '${flowBranch}' created. Remote pipeline branch preserved for dependencies.`);
216
+ logger.success(`✓ Flow branch '${flowBranch}' created. Remote pipeline branch preserved for dependencies.`, { context: 'git' });
217
217
  } catch (e) {
218
- logger.error(`❌ Failed during final consolidation: ${e}`);
218
+ logger.error(`❌ Failed during final consolidation: ${e}`, { context: 'git' });
219
219
  }
220
220
  }
221
221
  }
@@ -26,11 +26,10 @@ import {
26
26
  EnhancedLogManager,
27
27
  createLogManager,
28
28
  DEFAULT_LOG_CONFIG,
29
- ParsedMessage,
30
- stripAnsi
29
+ ParsedMessage
31
30
  } from '../utils/enhanced-logger';
32
31
  import { MAIN_LOG_FILENAME } from '../utils/log-constants';
33
- import { formatMessageForConsole } from '../utils/log-formatter';
32
+ import { formatMessageForConsole, stripAnsi } from '../services/logging/formatter';
34
33
  import { FailureType, analyzeFailure as analyzeFailureFromPolicy } from './failure-policy';
35
34
  import {
36
35
  savePOF,
@@ -293,6 +292,7 @@ export function spawnLane({
293
292
  // Build environment for child process
294
293
  const childEnv = {
295
294
  ...process.env,
295
+ CURSORFLOW_LANE: 'true',
296
296
  };
297
297
 
298
298
  if (logConfig.enabled) {
@@ -392,7 +392,7 @@ export function spawnLane({
392
392
  return { child, logPath, logManager, info };
393
393
  } else {
394
394
  // Fallback to simple file logging
395
- logPath = getLaneLogPath(laneRunDir, 'raw');
395
+ logPath = getLaneLogPath(laneRunDir, 'jsonl');
396
396
  const logFd = fs.openSync(logPath, 'a');
397
397
 
398
398
  child = spawn('node', args, {
@@ -519,12 +519,12 @@ async function resolveAllDependencies(
519
519
  const worktreeDir = state?.worktreeDir || safeJoin(runRoot, 'resolution-worktree');
520
520
 
521
521
  if (!fs.existsSync(worktreeDir)) {
522
- logger.info(`🏗️ Creating resolution worktree at ${worktreeDir}`);
522
+ logger.info(`🏗️ Creating resolution worktree at ${worktreeDir}`, { context: 'git' });
523
523
  git.createWorktree(worktreeDir, pipelineBranch, { baseBranch: git.getCurrentBranch() });
524
524
  }
525
525
 
526
526
  // 3. Resolve on pipeline branch
527
- logger.info(`🔄 Resolving dependencies on branch ${pipelineBranch}`);
527
+ logger.info(`🔄 Resolving dependencies on branch ${pipelineBranch}`, { context: 'git' });
528
528
  git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
529
529
 
530
530
  for (const cmd of uniqueCommands) {
@@ -566,7 +566,7 @@ async function resolveAllDependencies(
566
566
  if (task) {
567
567
  const lanePipelineBranch = `${pipelineBranch}/${lane.name}`;
568
568
  const taskBranch = `${lanePipelineBranch}--${String(currentIdx + 1).padStart(2, '0')}-${task.name}`;
569
- logger.info(`Syncing lane ${lane.name} branch ${taskBranch}`);
569
+ logger.info(`Syncing lane ${lane.name} branch ${taskBranch}`, { context: 'git' });
570
570
 
571
571
  try {
572
572
  // If task branch doesn't exist yet, it will be created from pipelineBranch when the lane starts
@@ -156,7 +156,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
156
156
  // ALWAYS use current branch as base - ignore config.baseBranch
157
157
  // This ensures dependency structure is maintained in the worktree
158
158
  const currentBranch = git.getCurrentBranch(repoRoot);
159
- logger.info(`📍 Base branch: ${currentBranch} (current branch)`);
159
+ logger.info(`📍 Base branch: ${currentBranch} (current branch)`, { context: 'git' });
160
160
 
161
161
  // Load existing state if resuming
162
162
  const statePath = safeJoin(runDir, 'state.json');
@@ -207,8 +207,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
207
207
  logger.section(`🔁 Resuming Pipeline from task ${startIndex + 1}`);
208
208
  }
209
209
 
210
- logger.info(`Pipeline Branch: ${pipelineBranch}`);
211
- logger.info(`Worktree: ${worktreeDir}`);
210
+ logger.info(`Pipeline Branch: ${pipelineBranch}`, { context: 'git' });
211
+ logger.info(`Worktree: ${worktreeDir}`, { context: 'git' });
212
212
  logger.info(`Tasks: ${config.tasks.length}`);
213
213
 
214
214
  const gitCoordinator = new GitPipelineCoordinator();
@@ -333,9 +333,9 @@ export class HookDataAccessorImpl implements HookDataAccessor {
333
333
  }
334
334
 
335
335
  try {
336
- // Try multiple possible log file locations
336
+ // Try multiple possible log file locations, preferring terminal.jsonl
337
337
  const possiblePaths = [
338
- safeJoin(this.options.runDir, 'terminal-raw.log'),
338
+ safeJoin(this.options.runDir, 'terminal.jsonl'),
339
339
  safeJoin(this.options.runDir, 'terminal.log'),
340
340
  safeJoin(this.options.runDir, 'agent-output.log'),
341
341
  ];