@litmers/cursorflow-orchestrator 0.2.2 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +8 -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/index.js +0 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.js +51 -61
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +74 -46
- 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/cli/signal.js +33 -29
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/auto-recovery.d.ts +2 -117
- package/dist/core/auto-recovery.js +4 -487
- package/dist/core/auto-recovery.js.map +1 -1
- package/dist/core/failure-policy.d.ts +0 -52
- package/dist/core/failure-policy.js +7 -174
- package/dist/core/failure-policy.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/intervention.d.ts +0 -6
- package/dist/core/intervention.js +1 -17
- package/dist/core/intervention.js.map +1 -1
- package/dist/core/orchestrator.js +18 -10
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.js +18 -15
- package/dist/core/runner/agent.js.map +1 -1
- package/dist/core/runner/pipeline.js +3 -3
- package/dist/core/runner/pipeline.js.map +1 -1
- package/dist/core/stall-detection.js +9 -7
- package/dist/core/stall-detection.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 +3 -14
- 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/index.ts +0 -6
- package/src/cli/logs.ts +46 -60
- package/src/cli/monitor.ts +82 -48
- package/src/cli/resume.ts +1 -1
- package/src/cli/signal.ts +38 -34
- package/src/core/auto-recovery.ts +13 -595
- package/src/core/failure-policy.ts +7 -228
- package/src/core/git-lifecycle-manager.ts +2 -2
- package/src/core/git-pipeline-coordinator.ts +25 -25
- package/src/core/intervention.ts +0 -18
- package/src/core/orchestrator.ts +20 -10
- package/src/core/runner/agent.ts +21 -16
- package/src/core/runner/pipeline.ts +3 -3
- package/src/core/stall-detection.ts +11 -9
- 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/cli/prepare.d.ts +0 -7
- package/dist/cli/prepare.js +0 -690
- package/dist/cli/prepare.js.map +0 -1
- 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/cli/prepare.ts +0 -777
- package/src/utils/log-formatter.ts +0 -287
package/src/cli/index.ts
CHANGED
|
@@ -19,8 +19,6 @@ const COMMANDS: Record<string, CommandFn> = {
|
|
|
19
19
|
new: require('./new'),
|
|
20
20
|
add: require('./add'),
|
|
21
21
|
config: require('./config'),
|
|
22
|
-
// Legacy prepare command (deprecated)
|
|
23
|
-
prepare: require('./prepare'),
|
|
24
22
|
run: require('./run'),
|
|
25
23
|
monitor: require('./monitor'),
|
|
26
24
|
clean: require('./clean'),
|
|
@@ -32,7 +30,6 @@ const COMMANDS: Record<string, CommandFn> = {
|
|
|
32
30
|
tasks: require('./tasks'),
|
|
33
31
|
stop: require('./stop'),
|
|
34
32
|
setup: require('./setup-commands').main,
|
|
35
|
-
'setup-commands': require('./setup-commands').main,
|
|
36
33
|
};
|
|
37
34
|
|
|
38
35
|
function printHelp(): void {
|
|
@@ -65,9 +62,6 @@ function printHelp(): void {
|
|
|
65
62
|
\x1b[33msignal\x1b[0m <lane> <msg> Directly intervene in a running lane
|
|
66
63
|
\x1b[33mmodels\x1b[0m [options] List available AI models
|
|
67
64
|
|
|
68
|
-
\x1b[1mLEGACY\x1b[0m
|
|
69
|
-
\x1b[33mprepare\x1b[0m <feature> [opts] (deprecated) Use 'new' + 'add' instead
|
|
70
|
-
|
|
71
65
|
\x1b[1mQUICK START\x1b[0m
|
|
72
66
|
$ \x1b[32mcursorflow new MyFeature --lanes "backend,frontend"\x1b[0m
|
|
73
67
|
$ \x1b[32mcursorflow add MyFeature backend --task "name=impl|model=sonnet-4.5|prompt=API 구현"\x1b[0m
|
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
|
@@ -19,6 +19,7 @@ import { safeJoin } from '../utils/path';
|
|
|
19
19
|
import { getLaneProcessStatus, getFlowSummary, LaneProcessStatus } from '../services/process';
|
|
20
20
|
import { LogBufferService, BufferedLogEntry } from '../services/logging/buffer';
|
|
21
21
|
import { formatReadableEntry, stripAnsi } from '../services/logging/formatter';
|
|
22
|
+
import { createInterventionRequest, InterventionType, wrapUserIntervention } from '../core/intervention';
|
|
22
23
|
|
|
23
24
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
24
25
|
// UI Constants
|
|
@@ -671,6 +672,7 @@ class InteractiveMonitor {
|
|
|
671
672
|
private getLaneActions(lane: LaneInfo): ActionItem[] {
|
|
672
673
|
const status = this.getLaneStatus(lane.path, lane.name);
|
|
673
674
|
const isRunning = status.status === 'running';
|
|
675
|
+
const isCompleted = status.status === 'completed' || status.status === 'success';
|
|
674
676
|
|
|
675
677
|
return [
|
|
676
678
|
{
|
|
@@ -689,6 +691,14 @@ class InteractiveMonitor {
|
|
|
689
691
|
disabled: !isRunning,
|
|
690
692
|
disabledReason: 'Lane not running',
|
|
691
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
|
+
},
|
|
692
702
|
{
|
|
693
703
|
id: 'stop',
|
|
694
704
|
label: 'Stop Lane',
|
|
@@ -715,6 +725,8 @@ class InteractiveMonitor {
|
|
|
715
725
|
|
|
716
726
|
private getFlowActions(flow: FlowInfo): ActionItem[] {
|
|
717
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;
|
|
718
730
|
|
|
719
731
|
return [
|
|
720
732
|
{
|
|
@@ -725,6 +737,14 @@ class InteractiveMonitor {
|
|
|
725
737
|
disabled: isCurrent,
|
|
726
738
|
disabledReason: 'Already viewing this flow',
|
|
727
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
|
+
},
|
|
728
748
|
{
|
|
729
749
|
id: 'delete',
|
|
730
750
|
label: 'Delete Flow',
|
|
@@ -805,9 +825,24 @@ class InteractiveMonitor {
|
|
|
805
825
|
if (!lane || !message.trim()) return;
|
|
806
826
|
|
|
807
827
|
try {
|
|
808
|
-
|
|
809
|
-
|
|
828
|
+
// Create pending-intervention.json for the system
|
|
829
|
+
createInterventionRequest(lane.path, {
|
|
830
|
+
type: InterventionType.USER_MESSAGE,
|
|
831
|
+
message: wrapUserIntervention(message),
|
|
832
|
+
source: 'user',
|
|
833
|
+
priority: 10
|
|
834
|
+
});
|
|
810
835
|
|
|
836
|
+
// Kill the process if it's running - this triggers the restart in orchestrator
|
|
837
|
+
const status = this.laneProcessStatuses.get(lane.name);
|
|
838
|
+
if (status && status.pid && status.actualStatus === 'running') {
|
|
839
|
+
try {
|
|
840
|
+
process.kill(status.pid, 'SIGTERM');
|
|
841
|
+
} catch {
|
|
842
|
+
// Ignore kill errors
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
811
846
|
const convoPath = safeJoin(lane.path, 'conversation.jsonl');
|
|
812
847
|
const entry = {
|
|
813
848
|
timestamp: new Date().toISOString(),
|
|
@@ -858,6 +893,45 @@ class InteractiveMonitor {
|
|
|
858
893
|
this.render();
|
|
859
894
|
}
|
|
860
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
|
+
|
|
861
935
|
private switchToFlow(flow: FlowInfo) {
|
|
862
936
|
this.runDir = flow.runDir;
|
|
863
937
|
|
|
@@ -1336,55 +1410,15 @@ class InteractiveMonitor {
|
|
|
1336
1410
|
}
|
|
1337
1411
|
|
|
1338
1412
|
private getTerminalLines(lanePath: string, maxLines: number): string[] {
|
|
1339
|
-
const { dim, reset
|
|
1340
|
-
|
|
1341
|
-
// Choose log source based on format setting
|
|
1342
|
-
if (this.state.readableFormat) {
|
|
1343
|
-
// Try JSONL first for structured readable format
|
|
1344
|
-
const jsonlPath = safeJoin(lanePath, 'terminal.jsonl');
|
|
1345
|
-
if (fs.existsSync(jsonlPath)) {
|
|
1346
|
-
return this.getJsonlLogLines(jsonlPath, maxLines);
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1413
|
+
const { dim, reset } = UI.COLORS;
|
|
1349
1414
|
|
|
1350
|
-
//
|
|
1351
|
-
const
|
|
1352
|
-
if (
|
|
1353
|
-
return
|
|
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);
|
|
1354
1419
|
}
|
|
1355
1420
|
|
|
1356
|
-
|
|
1357
|
-
const content = fs.readFileSync(logPath, 'utf8');
|
|
1358
|
-
const allLines = content.split('\n');
|
|
1359
|
-
const totalLines = allLines.length;
|
|
1360
|
-
|
|
1361
|
-
// Calculate visible range (from end, accounting for scroll offset)
|
|
1362
|
-
const end = Math.max(0, totalLines - this.state.terminalScrollOffset);
|
|
1363
|
-
const start = Math.max(0, end - maxLines);
|
|
1364
|
-
const visibleLines = allLines.slice(start, end);
|
|
1365
|
-
|
|
1366
|
-
// Format lines with syntax highlighting
|
|
1367
|
-
return visibleLines.map(line => {
|
|
1368
|
-
if (line.includes('[HUMAN INTERVENTION]') || line.includes('Injecting intervention:')) {
|
|
1369
|
-
return `${yellow}${line}${reset}`;
|
|
1370
|
-
}
|
|
1371
|
-
if (line.includes('=== Task:') || line.includes('Starting task:')) {
|
|
1372
|
-
return `${green}${line}${reset}`;
|
|
1373
|
-
}
|
|
1374
|
-
if (line.includes('Executing cursor-agent') || line.includes('cursor-agent-v')) {
|
|
1375
|
-
return `${cyan}${line}${reset}`;
|
|
1376
|
-
}
|
|
1377
|
-
if (line.toLowerCase().includes('error') || line.toLowerCase().includes('failed')) {
|
|
1378
|
-
return `${red}${line}${reset}`;
|
|
1379
|
-
}
|
|
1380
|
-
if (line.toLowerCase().includes('success') || line.toLowerCase().includes('completed')) {
|
|
1381
|
-
return `${green}${line}${reset}`;
|
|
1382
|
-
}
|
|
1383
|
-
return line;
|
|
1384
|
-
});
|
|
1385
|
-
} catch {
|
|
1386
|
-
return [`${dim}(Error reading log)${reset}`];
|
|
1387
|
-
}
|
|
1421
|
+
return [`${dim}(No output yet)${reset}`];
|
|
1388
1422
|
}
|
|
1389
1423
|
|
|
1390
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 {
|
package/src/cli/signal.ts
CHANGED
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
executeUserIntervention,
|
|
20
20
|
isProcessAlive,
|
|
21
21
|
InterventionResult,
|
|
22
|
+
createInterventionRequest,
|
|
23
|
+
InterventionType,
|
|
24
|
+
wrapUserIntervention,
|
|
22
25
|
} from '../core/intervention';
|
|
23
26
|
|
|
24
27
|
interface SignalOptions {
|
|
@@ -26,7 +29,6 @@ interface SignalOptions {
|
|
|
26
29
|
message: string | null;
|
|
27
30
|
timeout: number | null;
|
|
28
31
|
runDir: string | null;
|
|
29
|
-
force: boolean; // 프로세스 종료 없이 대기 모드로 전송
|
|
30
32
|
help: boolean;
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -35,8 +37,12 @@ function printHelp(): void {
|
|
|
35
37
|
Usage: cursorflow signal <lane> "<message>" [options]
|
|
36
38
|
cursorflow signal <lane> --timeout <ms>
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
and resume with your
|
|
40
|
+
Send an intervention message to a lane. For running lanes, the agent will be
|
|
41
|
+
interrupted immediately and resume with your message. For pending/waiting/failed
|
|
42
|
+
lanes, the message will be applied when the lane starts or resumes.
|
|
43
|
+
|
|
44
|
+
Note: Completed lanes cannot receive signals.
|
|
45
|
+
To re-run a completed lane, start a new run with: cursorflow run
|
|
40
46
|
|
|
41
47
|
Arguments:
|
|
42
48
|
<lane> Lane name to signal
|
|
@@ -45,15 +51,12 @@ Arguments:
|
|
|
45
51
|
Options:
|
|
46
52
|
--timeout <ms> Update execution timeout (in milliseconds)
|
|
47
53
|
--run-dir <path> Use a specific run directory (default: latest)
|
|
48
|
-
--force Send signal without interrupting current process
|
|
49
|
-
(message will be picked up on next task)
|
|
50
54
|
--help, -h Show help
|
|
51
55
|
|
|
52
56
|
Examples:
|
|
53
57
|
cursorflow signal lane-1 "Please focus on error handling first"
|
|
54
58
|
cursorflow signal lane-2 "Skip the optional tasks and finish"
|
|
55
59
|
cursorflow signal lane-1 --timeout 600000 # Set 10 minute timeout
|
|
56
|
-
cursorflow signal lane-1 "Continue" --force # Don't interrupt, wait for next turn
|
|
57
60
|
`);
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -61,15 +64,24 @@ function parseArgs(args: string[]): SignalOptions {
|
|
|
61
64
|
const runDirIdx = args.indexOf('--run-dir');
|
|
62
65
|
const timeoutIdx = args.indexOf('--timeout');
|
|
63
66
|
|
|
67
|
+
// Collect indices of option values to exclude from nonOptions
|
|
68
|
+
const optionValueIndices = new Set<number>();
|
|
69
|
+
if (runDirIdx >= 0 && runDirIdx + 1 < args.length) {
|
|
70
|
+
optionValueIndices.add(runDirIdx + 1);
|
|
71
|
+
}
|
|
72
|
+
if (timeoutIdx >= 0 && timeoutIdx + 1 < args.length) {
|
|
73
|
+
optionValueIndices.add(timeoutIdx + 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
// First non-option is lane, second (or rest joined) is message
|
|
65
|
-
|
|
77
|
+
// Exclude option flags and their values
|
|
78
|
+
const nonOptions = args.filter((a, i) => !a.startsWith('--') && !optionValueIndices.has(i));
|
|
66
79
|
|
|
67
80
|
return {
|
|
68
81
|
lane: nonOptions[0] || null,
|
|
69
82
|
message: nonOptions.slice(1).join(' ') || null,
|
|
70
83
|
timeout: timeoutIdx >= 0 ? parseInt(args[timeoutIdx + 1] || '0') || null : null,
|
|
71
84
|
runDir: runDirIdx >= 0 ? args[runDirIdx + 1] || null : null,
|
|
72
|
-
force: args.includes('--force'),
|
|
73
85
|
help: args.includes('--help') || args.includes('-h'),
|
|
74
86
|
};
|
|
75
87
|
}
|
|
@@ -108,18 +120,15 @@ function getLaneStatus(laneDir: string): { state: LaneState | null; isRunning: b
|
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
/**
|
|
111
|
-
*
|
|
123
|
+
* 개입 요청 파일 작성 (비실행 중인 lane용)
|
|
112
124
|
*/
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const entry = createConversationEntry('intervention', `[HUMAN INTERVENTION]: ${message}`, {
|
|
120
|
-
task: 'DIRECT_SIGNAL'
|
|
125
|
+
function sendInterventionRequest(laneDir: string, message: string): void {
|
|
126
|
+
createInterventionRequest(laneDir, {
|
|
127
|
+
type: InterventionType.USER_MESSAGE,
|
|
128
|
+
message: wrapUserIntervention(message),
|
|
129
|
+
source: 'user',
|
|
130
|
+
priority: 10
|
|
121
131
|
});
|
|
122
|
-
appendLog(convoPath, entry);
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
async function signal(args: string[]): Promise<void> {
|
|
@@ -167,30 +176,25 @@ async function signal(args: string[]): Promise<void> {
|
|
|
167
176
|
logger.info(`📨 Sending intervention to lane: ${options.lane}`);
|
|
168
177
|
logger.info(` Message: "${options.message.substring(0, 50)}${options.message.length > 50 ? '...' : ''}"`);
|
|
169
178
|
|
|
179
|
+
// Completed 레인은 signal 거부 (브랜치 충돌 방지)
|
|
180
|
+
if (state?.status === 'completed') {
|
|
181
|
+
logger.error(`❌ Cannot signal a completed lane.`);
|
|
182
|
+
logger.info(' To re-run this lane, start a new run with: cursorflow run');
|
|
183
|
+
throw new Error('Lane is already completed');
|
|
184
|
+
}
|
|
185
|
+
|
|
170
186
|
// Log to conversation for history
|
|
171
187
|
const entry = createConversationEntry('intervention', `[HUMAN INTERVENTION]: ${options.message}`, {
|
|
172
188
|
task: 'DIRECT_SIGNAL'
|
|
173
189
|
});
|
|
174
190
|
appendLog(convoPath, entry);
|
|
175
191
|
|
|
176
|
-
//
|
|
177
|
-
if (options.force) {
|
|
178
|
-
sendLegacyIntervention(laneDir, options.message);
|
|
179
|
-
logger.success('✅ Signal queued (--force mode). Message will be applied on next task.');
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Lane이 실행 중이 아닌 경우
|
|
192
|
+
// Lane이 실행 중이 아닌 경우 (pending/waiting/failed/paused)
|
|
184
193
|
if (!isRunning) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// 실행 중이 아니면 다음 resume 시 적용되도록 파일만 작성
|
|
191
|
-
sendLegacyIntervention(laneDir, options.message);
|
|
194
|
+
// 실행 중이 아니면 다음 시작/resume 시 적용되도록 파일만 작성
|
|
195
|
+
sendInterventionRequest(laneDir, options.message);
|
|
192
196
|
logger.info(`ℹ Lane ${options.lane} is not currently running (status: ${state?.status || 'unknown'}).`);
|
|
193
|
-
logger.success('✅ Signal queued. Message will be applied when lane resumes.');
|
|
197
|
+
logger.success('✅ Signal queued. Message will be applied when lane starts or resumes.');
|
|
194
198
|
return;
|
|
195
199
|
}
|
|
196
200
|
|