@litmers/cursorflow-orchestrator 0.2.5 → 0.2.7

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 (76) hide show
  1. package/CHANGELOG.md +29 -20
  2. package/README.md +13 -8
  3. package/dist/cli/complete.js +22 -5
  4. package/dist/cli/complete.js.map +1 -1
  5. package/dist/cli/index.js +2 -0
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/cli/logs.js +61 -51
  8. package/dist/cli/logs.js.map +1 -1
  9. package/dist/cli/monitor.js +45 -56
  10. package/dist/cli/monitor.js.map +1 -1
  11. package/dist/cli/resume.js +2 -2
  12. package/dist/cli/resume.js.map +1 -1
  13. package/dist/core/git-lifecycle-manager.js +2 -2
  14. package/dist/core/git-lifecycle-manager.js.map +1 -1
  15. package/dist/core/git-pipeline-coordinator.js +25 -25
  16. package/dist/core/git-pipeline-coordinator.js.map +1 -1
  17. package/dist/core/orchestrator.d.ts +17 -0
  18. package/dist/core/orchestrator.js +186 -8
  19. package/dist/core/orchestrator.js.map +1 -1
  20. package/dist/core/runner/pipeline.js +3 -3
  21. package/dist/core/runner/pipeline.js.map +1 -1
  22. package/dist/hooks/data-accessor.js +2 -2
  23. package/dist/hooks/data-accessor.js.map +1 -1
  24. package/dist/services/logging/buffer.d.ts +2 -1
  25. package/dist/services/logging/buffer.js +63 -22
  26. package/dist/services/logging/buffer.js.map +1 -1
  27. package/dist/services/logging/formatter.d.ts +0 -4
  28. package/dist/services/logging/formatter.js +33 -201
  29. package/dist/services/logging/formatter.js.map +1 -1
  30. package/dist/services/logging/paths.d.ts +3 -0
  31. package/dist/services/logging/paths.js +3 -0
  32. package/dist/services/logging/paths.js.map +1 -1
  33. package/dist/types/config.d.ts +9 -1
  34. package/dist/types/flow.d.ts +6 -0
  35. package/dist/types/logging.d.ts +1 -1
  36. package/dist/utils/config.js +6 -2
  37. package/dist/utils/config.js.map +1 -1
  38. package/dist/utils/enhanced-logger.d.ts +37 -17
  39. package/dist/utils/enhanced-logger.js +267 -237
  40. package/dist/utils/enhanced-logger.js.map +1 -1
  41. package/dist/utils/events.d.ts +18 -15
  42. package/dist/utils/events.js +8 -5
  43. package/dist/utils/events.js.map +1 -1
  44. package/dist/utils/log-formatter.d.ts +26 -0
  45. package/dist/utils/log-formatter.js +274 -0
  46. package/dist/utils/log-formatter.js.map +1 -0
  47. package/dist/utils/logger.js +4 -17
  48. package/dist/utils/logger.js.map +1 -1
  49. package/dist/utils/repro-thinking-logs.js +4 -4
  50. package/dist/utils/repro-thinking-logs.js.map +1 -1
  51. package/package.json +2 -2
  52. package/scripts/monitor-lanes.sh +5 -5
  53. package/scripts/stream-logs.sh +1 -1
  54. package/scripts/test-log-parser.ts +42 -8
  55. package/src/cli/complete.ts +21 -6
  56. package/src/cli/index.ts +2 -0
  57. package/src/cli/logs.ts +60 -46
  58. package/src/cli/monitor.ts +47 -64
  59. package/src/cli/resume.ts +1 -1
  60. package/src/core/git-lifecycle-manager.ts +2 -2
  61. package/src/core/git-pipeline-coordinator.ts +25 -25
  62. package/src/core/orchestrator.ts +214 -7
  63. package/src/core/runner/pipeline.ts +3 -3
  64. package/src/hooks/data-accessor.ts +2 -2
  65. package/src/services/logging/buffer.ts +68 -20
  66. package/src/services/logging/formatter.ts +32 -199
  67. package/src/services/logging/paths.ts +3 -0
  68. package/src/types/config.ts +13 -1
  69. package/src/types/flow.ts +6 -0
  70. package/src/types/logging.ts +0 -2
  71. package/src/utils/config.ts +6 -2
  72. package/src/utils/enhanced-logger.ts +290 -239
  73. package/src/utils/events.ts +21 -18
  74. package/src/utils/log-formatter.ts +287 -0
  75. package/src/utils/logger.ts +3 -18
  76. package/src/utils/repro-thinking-logs.ts +4 -4
@@ -57,7 +57,7 @@ trap 'kill $(jobs -p) 2>/dev/null' EXIT
57
57
  for lane_dir in "${LATEST_RUN}/lanes"/*; do
58
58
  if [ -d "$lane_dir" ]; then
59
59
  lane_name=$(basename "$lane_dir")
60
- log_file="${lane_dir}/terminal.jsonl"
60
+ log_file="${lane_dir}/terminal-readable.log"
61
61
  color=${COLORS[$color_idx]}
62
62
 
63
63
  # Start streaming in background
@@ -70,13 +70,15 @@ async function main() {
70
70
  console.log(' 📦 Test 2: StreamingMessageParser (JSONL Parsing)');
71
71
  console.log('━'.repeat(80) + '\n');
72
72
 
73
- const jsonlLogPath = path.join(FIXTURES_DIR, 'lanes/test-lane/terminal.jsonl');
74
- if (fs.existsSync(jsonlLogPath)) {
75
- const jsonlLog = fs.readFileSync(jsonlLogPath, 'utf8');
76
- const lines = jsonlLog.split('\n').filter(l => l.trim());
73
+ const rawLogPath = path.join(FIXTURES_DIR, 'lanes/test-lane/terminal-raw.log');
74
+ if (fs.existsSync(rawLogPath)) {
75
+ const rawLog = fs.readFileSync(rawLogPath, 'utf8');
76
+ const lines = rawLog.split('\n');
77
+ const jsonLines = lines.filter(l => l.trim().startsWith('{') && l.trim().endsWith('}'));
77
78
 
78
- console.log(`📄 JSONL log: ${jsonlLogPath}`);
79
+ console.log(`📄 Raw log: ${rawLogPath}`);
79
80
  console.log(` Total lines: ${lines.length}`);
81
+ console.log(` JSON lines: ${jsonLines.length}`);
80
82
 
81
83
  // Parse with StreamingMessageParser
82
84
  const parsedMessages: ParsedMessage[] = [];
@@ -84,7 +86,7 @@ async function main() {
84
86
  parsedMessages.push(msg);
85
87
  });
86
88
 
87
- for (const line of lines) {
89
+ for (const line of jsonLines) {
88
90
  parser.parseLine(line);
89
91
  }
90
92
  parser.flush();
@@ -115,11 +117,43 @@ async function main() {
115
117
  console.log(stripAnsi(formatted).substring(0, 100));
116
118
  }
117
119
  } else {
118
- console.log('⚠️ JSONL log not found at:', jsonlLogPath);
120
+ console.log('⚠️ Raw log not found at:', rawLogPath);
119
121
  }
120
122
 
121
123
  // ===========================================================================
122
- // Test 3: Summary
124
+ // Test 3: Compare with actual readable log
125
+ // ===========================================================================
126
+ console.log('\n' + '━'.repeat(80));
127
+ console.log(' 🔍 Test 3: Compare Parser Output with Actual Log');
128
+ console.log('━'.repeat(80) + '\n');
129
+
130
+ const readableLogPath = path.join(FIXTURES_DIR, 'lanes/test-lane/terminal-readable.log');
131
+ if (fs.existsSync(readableLogPath)) {
132
+ const readableLog = fs.readFileSync(readableLogPath, 'utf8');
133
+ const readableLines = readableLog.split('\n');
134
+
135
+ // Find agent message lines
136
+ const agentLines = readableLines.filter(line =>
137
+ line.includes('⚙️ SYS') ||
138
+ line.includes('🧑 USER') ||
139
+ line.includes('🤖 ASST') ||
140
+ line.includes('🔧 TOOL') ||
141
+ line.includes('📄 RESL')
142
+ );
143
+
144
+ console.log(`📄 Readable log: ${readableLogPath}`);
145
+ console.log(` Total lines: ${readableLines.length}`);
146
+ console.log(` Agent message lines: ${agentLines.length}`);
147
+
148
+ console.log(`\n📝 Actual terminal output (first 15 agent messages):`);
149
+ console.log('-'.repeat(60));
150
+ for (const line of agentLines.slice(0, 15)) {
151
+ console.log(stripAnsi(line).substring(0, 100));
152
+ }
153
+ }
154
+
155
+ // ===========================================================================
156
+ // Summary
123
157
  // ===========================================================================
124
158
  console.log('\n' + '='.repeat(80));
125
159
  console.log('✅ Log Parser Test Complete');
@@ -200,17 +200,32 @@ async function complete(args: string[]): Promise<void> {
200
200
  for (const branch of laneBranches) {
201
201
  logger.info(`Merging ${branch}...`);
202
202
 
203
- // Fetch remote branch if needed
204
- if (!git.branchExists(branch, { cwd: repoRoot })) {
203
+ // Determine what ref to use for merge
204
+ let branchRef: string;
205
+
206
+ if (git.branchExists(branch, { cwd: repoRoot })) {
207
+ // Local branch exists, use it directly
208
+ branchRef = branch;
209
+ } else {
210
+ // Local branch doesn't exist - fetch from remote with proper refspec
211
+ // Note: `git fetch origin <branch>` only updates FETCH_HEAD, not origin/<branch>
212
+ // We must use refspec to update the remote tracking ref
205
213
  logger.info(` Fetching ${branch} from remote...`);
206
214
  try {
207
- git.runGit(['fetch', 'origin', branch], { cwd: repoRoot });
215
+ git.runGit(['fetch', 'origin', `${branch}:refs/remotes/origin/${branch}`], { cwd: repoRoot });
216
+ branchRef = `origin/${branch}`;
208
217
  } catch (e) {
209
- logger.warn(` Failed to fetch ${branch}: ${e}`);
218
+ // Fallback: try fetching and use FETCH_HEAD directly
219
+ logger.warn(` Failed to fetch with refspec, trying FETCH_HEAD: ${e}`);
220
+ try {
221
+ git.runGit(['fetch', 'origin', branch], { cwd: repoRoot });
222
+ branchRef = 'FETCH_HEAD';
223
+ } catch (e2) {
224
+ logger.warn(` Failed to fetch ${branch}: ${e2}`);
225
+ throw new Error(`Cannot fetch branch ${branch} from remote`);
226
+ }
210
227
  }
211
228
  }
212
-
213
- const branchRef = git.branchExists(branch, { cwd: repoRoot }) ? branch : `origin/${branch}`;
214
229
 
215
230
  const mergeResult = git.safeMerge(branchRef, {
216
231
  cwd: repoRoot,
package/src/cli/index.ts CHANGED
@@ -22,6 +22,7 @@ const COMMANDS: Record<string, CommandFn> = {
22
22
  run: require('./run'),
23
23
  monitor: require('./monitor'),
24
24
  clean: require('./clean'),
25
+ complete: require('./complete'),
25
26
  resume: require('./resume'),
26
27
  doctor: require('./doctor'),
27
28
  signal: require('./signal'),
@@ -47,6 +48,7 @@ function printHelp(): void {
47
48
  \x1b[1mEXECUTION\x1b[0m
48
49
  \x1b[33mrun\x1b[0m <flow> [options] Run orchestration (DAG-based)
49
50
  \x1b[33mmonitor\x1b[0m [run-dir] [options] \x1b[36mInteractive\x1b[0m lane dashboard
51
+ \x1b[33mcomplete\x1b[0m <flow> [options] Consolidate all lanes into one branch
50
52
  \x1b[33mstop\x1b[0m [run-id] [options] Stop running workflows
51
53
  \x1b[33mresume\x1b[0m [lane] [options] Resume lane(s)
52
54
 
package/src/cli/logs.ts CHANGED
@@ -10,9 +10,10 @@ import { safeJoin } from '../utils/path';
10
10
  import {
11
11
  readJsonLog,
12
12
  exportLogs,
13
+ stripAnsi,
13
14
  JsonLogEntry
14
15
  } from '../utils/enhanced-logger';
15
- import { formatMessageForConsole, formatPotentialJsonMessage, stripAnsi } from '../services/logging/formatter';
16
+ import { formatPotentialJsonMessage } from '../utils/log-formatter';
16
17
  import { MAIN_LOG_FILENAME } from '../utils/log-constants';
17
18
  import { startLogViewer } from '../ui/log-viewer';
18
19
 
@@ -151,33 +152,32 @@ function listLanes(runDir: string): string[] {
151
152
  }
152
153
 
153
154
  /**
154
- * Read and display text logs (converted from JSONL)
155
+ * Read and display text logs
155
156
  */
156
157
  function displayTextLogs(
157
158
  laneDir: string,
158
159
  options: LogsOptions
159
160
  ): void {
160
- const logFile = safeJoin(laneDir, 'terminal.jsonl');
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
+ }
161
171
 
162
172
  if (!fs.existsSync(logFile)) {
163
173
  console.log('No log file found.');
164
174
  return;
165
175
  }
166
176
 
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
- });
177
+ let content = fs.readFileSync(logFile, 'utf8');
178
+ let lines = content.split('\n');
179
179
 
180
- // Apply filter
180
+ // Apply filter (case-insensitive string match to avoid ReDoS)
181
181
  if (options.filter) {
182
182
  const filterLower = options.filter.toLowerCase();
183
183
  lines = lines.filter(line => line.toLowerCase().includes(filterLower));
@@ -188,6 +188,11 @@ 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
+
191
196
  console.log(lines.join('\n'));
192
197
  }
193
198
 
@@ -703,7 +708,16 @@ function escapeHtml(text: string): string {
703
708
  * Follow logs in real-time
704
709
  */
705
710
  function followLogs(laneDir: string, options: LogsOptions): void {
706
- const logFile = safeJoin(laneDir, 'terminal.jsonl');
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
+ }
707
721
 
708
722
  if (!fs.existsSync(logFile)) {
709
723
  console.log('Waiting for log file...');
@@ -711,14 +725,17 @@ function followLogs(laneDir: string, options: LogsOptions): void {
711
725
 
712
726
  let lastSize = 0;
713
727
  try {
728
+ // Use statSync directly to avoid TOCTOU race condition
714
729
  lastSize = fs.statSync(logFile).size;
715
730
  } catch {
731
+ // File doesn't exist yet or other error - start from 0
716
732
  lastSize = 0;
717
733
  }
718
734
 
719
735
  console.log(`${logger.COLORS.cyan}Following ${logFile}... (Ctrl+C to stop)${logger.COLORS.reset}\n`);
720
736
 
721
737
  const checkInterval = setInterval(() => {
738
+ // Use fstat on open fd to avoid TOCTOU race condition
722
739
  let fd: number | null = null;
723
740
  try {
724
741
  fd = fs.openSync(logFile, 'r');
@@ -727,37 +744,28 @@ function followLogs(laneDir: string, options: LogsOptions): void {
727
744
  const buffer = Buffer.alloc(stats.size - lastSize);
728
745
  fs.readSync(fd, buffer, 0, buffer.length, lastSize);
729
746
 
730
- const content = buffer.toString();
731
- const lines = content.split('\n').filter(l => l.trim());
747
+ let content = buffer.toString();
732
748
 
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
- }
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
+ }
760
+
761
+ if (content.trim()) {
762
+ process.stdout.write(content);
755
763
  }
756
764
 
757
765
  lastSize = stats.size;
758
766
  }
759
767
  } catch {
760
- // Ignore errors
768
+ // Ignore errors (file might be rotating)
761
769
  } finally {
762
770
  if (fd !== null) {
763
771
  try { fs.closeSync(fd); } catch { /* ignore */ }
@@ -852,13 +860,19 @@ function displaySummary(runDir: string): void {
852
860
 
853
861
  for (const lane of lanes) {
854
862
  const laneDir = safeJoin(runDir, 'lanes', lane);
855
- const jsonlLog = safeJoin(laneDir, 'terminal.jsonl');
863
+ const rawLog = safeJoin(laneDir, 'terminal-raw.log');
864
+ const readableLog = safeJoin(laneDir, 'terminal-readable.log');
856
865
 
857
866
  console.log(` ${logger.COLORS.green}📁 ${lane}${logger.COLORS.reset}`);
858
867
 
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}`);
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)}`);
862
876
  }
863
877
 
864
878
  console.log('');
@@ -672,7 +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
+ const isCompleted = status.status === 'completed';
676
676
 
677
677
  return [
678
678
  {
@@ -691,14 +691,6 @@ class InteractiveMonitor {
691
691
  disabled: !isRunning,
692
692
  disabledReason: 'Lane not running',
693
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
- },
702
694
  {
703
695
  id: 'stop',
704
696
  label: 'Stop Lane',
@@ -725,8 +717,6 @@ class InteractiveMonitor {
725
717
 
726
718
  private getFlowActions(flow: FlowInfo): ActionItem[] {
727
719
  const isCurrent = flow.runDir === this.runDir;
728
- const isAlive = flow.isAlive;
729
- const isCompleted = flow.summary.completed === flow.summary.total && flow.summary.total > 0;
730
720
 
731
721
  return [
732
722
  {
@@ -737,14 +727,6 @@ class InteractiveMonitor {
737
727
  disabled: isCurrent,
738
728
  disabledReason: 'Already viewing this flow',
739
729
  },
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
- },
748
730
  {
749
731
  id: 'delete',
750
732
  label: 'Delete Flow',
@@ -893,45 +875,6 @@ class InteractiveMonitor {
893
875
  this.render();
894
876
  }
895
877
 
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
-
935
878
  private switchToFlow(flow: FlowInfo) {
936
879
  this.runDir = flow.runDir;
937
880
 
@@ -1410,15 +1353,55 @@ class InteractiveMonitor {
1410
1353
  }
1411
1354
 
1412
1355
  private getTerminalLines(lanePath: string, maxLines: number): string[] {
1413
- const { dim, reset } = UI.COLORS;
1356
+ const { dim, reset, cyan, green, yellow, red, gray } = UI.COLORS;
1414
1357
 
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);
1358
+ // Choose log source based on format setting
1359
+ if (this.state.readableFormat) {
1360
+ // Try JSONL first for structured readable format
1361
+ const jsonlPath = safeJoin(lanePath, 'terminal.jsonl');
1362
+ if (fs.existsSync(jsonlPath)) {
1363
+ return this.getJsonlLogLines(jsonlPath, maxLines);
1364
+ }
1365
+ }
1366
+
1367
+ // Fallback to raw terminal log
1368
+ const logPath = safeJoin(lanePath, 'terminal-readable.log');
1369
+ if (!fs.existsSync(logPath)) {
1370
+ return [`${dim}(No output yet)${reset}`];
1419
1371
  }
1420
1372
 
1421
- return [`${dim}(No output yet)${reset}`];
1373
+ try {
1374
+ const content = fs.readFileSync(logPath, 'utf8');
1375
+ const allLines = content.split('\n');
1376
+ const totalLines = allLines.length;
1377
+
1378
+ // Calculate visible range (from end, accounting for scroll offset)
1379
+ const end = Math.max(0, totalLines - this.state.terminalScrollOffset);
1380
+ const start = Math.max(0, end - maxLines);
1381
+ const visibleLines = allLines.slice(start, end);
1382
+
1383
+ // Format lines with syntax highlighting
1384
+ return visibleLines.map(line => {
1385
+ if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
1386
+ return `${yellow}${line}${reset}`;
1387
+ }
1388
+ if (line.includes('=== Task:') || line.includes('Starting task:')) {
1389
+ return `${green}${line}${reset}`;
1390
+ }
1391
+ if (line.includes('Executing cursor-agent') || line.includes('cursor-agent-v')) {
1392
+ return `${cyan}${line}${reset}`;
1393
+ }
1394
+ if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
1395
+ return `${red}${line}${reset}`;
1396
+ }
1397
+ if (line.toLowerCase().includes('success') || line.toLowerCase().includes('completed')) {
1398
+ return `${green}${line}${reset}`;
1399
+ }
1400
+ return line;
1401
+ });
1402
+ } catch {
1403
+ return [`${dim}(Error reading log)${reset}`];
1404
+ }
1422
1405
  }
1423
1406
 
1424
1407
  /**
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 '../services/logging/formatter';
20
+ import { formatMessageForConsole } from '../utils/log-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}`, { context: 'git' });
980
+ logger.debug(`[GitLifecycle] ${message}`);
981
981
  } else {
982
- logger.info(`[GitLifecycle] ${message}`, { context: 'git' });
982
+ logger.info(`[GitLifecycle] ${message}`);
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}`, { context: 'git' });
25
- logger.info(` Cleaning up invalid directory and recreating worktree...`, { context: 'git' });
24
+ logger.warn(`⚠️ Directory exists but is not a valid worktree: ${worktreeDir}`);
25
+ logger.info(` Cleaning up invalid directory and recreating worktree...`);
26
26
  try {
27
27
  git.cleanupInvalidWorktreeDir(worktreeDir);
28
28
  } catch (e: any) {
29
- logger.error(`Failed to cleanup invalid worktree directory: ${e.message}`, { context: 'git' });
29
+ logger.error(`Failed to cleanup invalid worktree directory: ${e.message}`);
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)`, { context: 'git' });
55
+ logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
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}`, { context: 'git' });
65
+ logger.info(`Reusing existing worktree: ${worktreeDir}`);
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}`, { context: 'git' });
69
+ logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
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`, { context: 'git' });
85
+ logger.info(`🔄 Syncing with ${pipelineBranch} before merging dependencies`);
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}`, { context: 'git' });
96
+ logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
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}`, { context: 'git' });
104
- logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`, { context: 'git' });
103
+ logger.warn(`⚠️ Pre-check: Merge conflict detected with ${laneName}`);
104
+ logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
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(', ')}`, { context: 'git' });
128
+ logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
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}`, { context: 'git' });
134
+ logger.success(`✓ Merged ${laneName}`);
135
135
  } catch (e) {
136
- logger.error(`Failed to merge branch from ${laneName}: ${e}`, { context: 'git' });
136
+ logger.error(`Failed to merge branch from ${laneName}: ${e}`);
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}`, { context: 'git' });
154
- logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`, { context: 'git' });
153
+ logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
154
+ logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
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}`, { context: 'git' });
160
- logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`, { context: 'git' });
161
- logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`, { context: 'git' });
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}`);
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}`, { context: 'git' });
172
+ logger.info(`🔀 Merging task ${taskName} (${taskBranch}) into ${pipelineBranch}`);
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(', ')}`, { context: 'git' });
182
+ logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
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, { context: 'git' });
192
+ logger.info('Changed files:\n' + stats);
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}`, { context: 'git' });
207
+ logger.info(`🌿 Creating final flow branch: ${flowBranch}`);
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}`, { context: 'git' });
212
+ logger.info(`🗑️ Deleting local pipeline branch: ${pipelineBranch}`);
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.`, { context: 'git' });
216
+ logger.success(`✓ Flow branch '${flowBranch}' created. Remote pipeline branch preserved for dependencies.`);
217
217
  } catch (e) {
218
- logger.error(`❌ Failed during final consolidation: ${e}`, { context: 'git' });
218
+ logger.error(`❌ Failed during final consolidation: ${e}`);
219
219
  }
220
220
  }
221
221
  }