@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.
- package/CHANGELOG.md +29 -20
- package/README.md +13 -8
- package/dist/cli/complete.js +22 -5
- package/dist/cli/complete.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +61 -51
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +45 -56
- 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.d.ts +17 -0
- package/dist/core/orchestrator.js +186 -8
- 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 +2 -1
- package/dist/services/logging/buffer.js +63 -22
- package/dist/services/logging/buffer.js.map +1 -1
- package/dist/services/logging/formatter.d.ts +0 -4
- package/dist/services/logging/formatter.js +33 -201
- package/dist/services/logging/formatter.js.map +1 -1
- package/dist/services/logging/paths.d.ts +3 -0
- package/dist/services/logging/paths.js +3 -0
- package/dist/services/logging/paths.js.map +1 -1
- package/dist/types/config.d.ts +9 -1
- package/dist/types/flow.d.ts +6 -0
- package/dist/types/logging.d.ts +1 -1
- package/dist/utils/config.js +6 -2
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +37 -17
- package/dist/utils/enhanced-logger.js +267 -237
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/events.d.ts +18 -15
- package/dist/utils/events.js +8 -5
- package/dist/utils/events.js.map +1 -1
- package/dist/utils/log-formatter.d.ts +26 -0
- package/dist/utils/log-formatter.js +274 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/logger.js +4 -17
- 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 +42 -8
- package/src/cli/complete.ts +21 -6
- package/src/cli/index.ts +2 -0
- package/src/cli/logs.ts +60 -46
- package/src/cli/monitor.ts +47 -64
- 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 +214 -7
- package/src/core/runner/pipeline.ts +3 -3
- package/src/hooks/data-accessor.ts +2 -2
- package/src/services/logging/buffer.ts +68 -20
- package/src/services/logging/formatter.ts +32 -199
- package/src/services/logging/paths.ts +3 -0
- package/src/types/config.ts +13 -1
- package/src/types/flow.ts +6 -0
- package/src/types/logging.ts +0 -2
- package/src/utils/config.ts +6 -2
- package/src/utils/enhanced-logger.ts +290 -239
- package/src/utils/events.ts +21 -18
- package/src/utils/log-formatter.ts +287 -0
- package/src/utils/logger.ts +3 -18
- package/src/utils/repro-thinking-logs.ts +4 -4
package/scripts/stream-logs.sh
CHANGED
|
@@ -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.
|
|
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
|
|
74
|
-
if (fs.existsSync(
|
|
75
|
-
const
|
|
76
|
-
const lines =
|
|
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(`📄
|
|
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
|
|
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('⚠️
|
|
120
|
+
console.log('⚠️ Raw log not found at:', rawLogPath);
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
// ===========================================================================
|
|
122
|
-
// Test 3:
|
|
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');
|
package/src/cli/complete.ts
CHANGED
|
@@ -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
|
-
//
|
|
204
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
155
|
+
* Read and display text logs
|
|
155
156
|
*/
|
|
156
157
|
function displayTextLogs(
|
|
157
158
|
laneDir: string,
|
|
158
159
|
options: LogsOptions
|
|
159
160
|
): void {
|
|
160
|
-
|
|
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
|
-
|
|
168
|
-
let lines =
|
|
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
|
-
|
|
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
|
-
|
|
731
|
-
const lines = content.split('\n').filter(l => l.trim());
|
|
747
|
+
let content = buffer.toString();
|
|
732
748
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
|
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(
|
|
860
|
-
const stats = fs.statSync(
|
|
861
|
-
console.log(` └─ terminal.
|
|
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('');
|
package/src/cli/monitor.ts
CHANGED
|
@@ -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'
|
|
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
|
-
//
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
-
|
|
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 '../
|
|
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}
|
|
980
|
+
logger.debug(`[GitLifecycle] ${message}`);
|
|
981
981
|
} else {
|
|
982
|
-
logger.info(`[GitLifecycle] ${message}
|
|
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}
|
|
25
|
-
logger.info(` Cleaning up invalid directory and recreating worktree
|
|
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}
|
|
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)
|
|
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}
|
|
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}
|
|
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
|
|
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}
|
|
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}
|
|
104
|
-
logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}
|
|
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(', ')}
|
|
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}
|
|
134
|
+
logger.success(`✓ Merged ${laneName}`);
|
|
135
135
|
} catch (e) {
|
|
136
|
-
logger.error(`Failed to merge branch from ${laneName}: ${e}
|
|
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}
|
|
154
|
-
logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes
|
|
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}
|
|
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}`);
|
|
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}
|
|
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(', ')}
|
|
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
|
|
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}
|
|
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}
|
|
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
|
|
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}
|
|
218
|
+
logger.error(`❌ Failed during final consolidation: ${e}`);
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
}
|