@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.
- package/CHANGELOG.md +14 -0
- package/README.md +7 -11
- package/dist/cli/complete.d.ts +7 -0
- package/dist/cli/complete.js +304 -0
- package/dist/cli/complete.js.map +1 -0
- package/dist/cli/logs.js +51 -61
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +56 -44
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/resume.js +2 -2
- package/dist/cli/resume.js.map +1 -1
- package/dist/core/git-lifecycle-manager.js +2 -2
- package/dist/core/git-lifecycle-manager.js.map +1 -1
- package/dist/core/git-pipeline-coordinator.js +25 -25
- package/dist/core/git-pipeline-coordinator.js.map +1 -1
- package/dist/core/orchestrator.js +8 -7
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/pipeline.js +3 -3
- package/dist/core/runner/pipeline.js.map +1 -1
- package/dist/hooks/data-accessor.js +2 -2
- package/dist/hooks/data-accessor.js.map +1 -1
- package/dist/services/logging/buffer.d.ts +1 -2
- package/dist/services/logging/buffer.js +22 -63
- package/dist/services/logging/buffer.js.map +1 -1
- package/dist/services/logging/formatter.d.ts +4 -0
- package/dist/services/logging/formatter.js +201 -33
- package/dist/services/logging/formatter.js.map +1 -1
- package/dist/services/logging/paths.d.ts +0 -3
- package/dist/services/logging/paths.js +0 -3
- package/dist/services/logging/paths.js.map +1 -1
- package/dist/types/config.d.ts +1 -9
- package/dist/types/logging.d.ts +1 -1
- package/dist/utils/config.js +2 -6
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +17 -37
- package/dist/utils/enhanced-logger.js +237 -267
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/logger.js +17 -4
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/repro-thinking-logs.js +4 -4
- package/dist/utils/repro-thinking-logs.js.map +1 -1
- package/package.json +2 -2
- package/scripts/monitor-lanes.sh +5 -5
- package/scripts/stream-logs.sh +1 -1
- package/scripts/test-log-parser.ts +8 -42
- package/src/cli/complete.ts +305 -0
- package/src/cli/logs.ts +46 -60
- package/src/cli/monitor.ts +64 -46
- package/src/cli/resume.ts +1 -1
- package/src/core/git-lifecycle-manager.ts +2 -2
- package/src/core/git-pipeline-coordinator.ts +25 -25
- package/src/core/orchestrator.ts +7 -7
- package/src/core/runner/pipeline.ts +3 -3
- package/src/hooks/data-accessor.ts +2 -2
- package/src/services/logging/buffer.ts +20 -68
- package/src/services/logging/formatter.ts +199 -32
- package/src/services/logging/paths.ts +0 -3
- package/src/types/config.ts +1 -13
- package/src/types/logging.ts +2 -0
- package/src/utils/config.ts +2 -6
- package/src/utils/enhanced-logger.ts +239 -290
- package/src/utils/logger.ts +18 -3
- package/src/utils/repro-thinking-logs.ts +4 -4
- package/dist/utils/log-formatter.d.ts +0 -26
- package/dist/utils/log-formatter.js +0 -274
- package/dist/utils/log-formatter.js.map +0 -1
- 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 '../
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
let lines =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
762
|
-
|
|
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
|
|
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
|
|
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(
|
|
869
|
-
const stats = fs.statSync(
|
|
870
|
-
console.log(` └─ terminal
|
|
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('');
|
package/src/cli/monitor.ts
CHANGED
|
@@ -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
|
|
1413
|
+
const { dim, reset } = UI.COLORS;
|
|
1356
1414
|
|
|
1357
|
-
//
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
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 '../
|
|
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
|
}
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -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 '../
|
|
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, '
|
|
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
|
|
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
|
];
|