@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26
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 +25 -0
- package/README.md +25 -7
- package/commands/cursorflow-clean.md +19 -0
- package/commands/cursorflow-runs.md +59 -0
- package/commands/cursorflow-stop.md +55 -0
- package/dist/cli/clean.js +178 -6
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +8 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +126 -77
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -0
- package/dist/cli/monitor.js +1021 -202
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +39 -21
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +268 -163
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +11 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runs.d.ts +5 -0
- package/dist/cli/runs.js +214 -0
- package/dist/cli/runs.js.map +1 -0
- package/dist/cli/setup-commands.js +0 -0
- package/dist/cli/signal.js +8 -8
- package/dist/cli/signal.js.map +1 -1
- package/dist/cli/stop.d.ts +5 -0
- package/dist/cli/stop.js +215 -0
- package/dist/cli/stop.js.map +1 -0
- package/dist/cli/tasks.d.ts +10 -0
- package/dist/cli/tasks.js +165 -0
- package/dist/cli/tasks.js.map +1 -0
- package/dist/core/auto-recovery.d.ts +212 -0
- package/dist/core/auto-recovery.js +737 -0
- package/dist/core/auto-recovery.js.map +1 -0
- package/dist/core/failure-policy.d.ts +156 -0
- package/dist/core/failure-policy.js +488 -0
- package/dist/core/failure-policy.js.map +1 -0
- package/dist/core/orchestrator.d.ts +16 -2
- package/dist/core/orchestrator.js +439 -105
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +2 -0
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +33 -10
- package/dist/core/runner.js +374 -164
- package/dist/core/runner.js.map +1 -1
- package/dist/services/logging/buffer.d.ts +67 -0
- package/dist/services/logging/buffer.js +309 -0
- package/dist/services/logging/buffer.js.map +1 -0
- package/dist/services/logging/console.d.ts +89 -0
- package/dist/services/logging/console.js +169 -0
- package/dist/services/logging/console.js.map +1 -0
- package/dist/services/logging/file-writer.d.ts +71 -0
- package/dist/services/logging/file-writer.js +516 -0
- package/dist/services/logging/file-writer.js.map +1 -0
- package/dist/services/logging/formatter.d.ts +39 -0
- package/dist/services/logging/formatter.js +227 -0
- package/dist/services/logging/formatter.js.map +1 -0
- package/dist/services/logging/index.d.ts +11 -0
- package/dist/services/logging/index.js +30 -0
- package/dist/services/logging/index.js.map +1 -0
- package/dist/services/logging/parser.d.ts +31 -0
- package/dist/services/logging/parser.js +222 -0
- package/dist/services/logging/parser.js.map +1 -0
- package/dist/services/process/index.d.ts +59 -0
- package/dist/services/process/index.js +257 -0
- package/dist/services/process/index.js.map +1 -0
- package/dist/types/agent.d.ts +20 -0
- package/dist/types/agent.js +6 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/config.d.ts +65 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/events.d.ts +125 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lane.d.ts +43 -0
- package/dist/types/lane.js +6 -0
- package/dist/types/lane.js.map +1 -0
- package/dist/types/logging.d.ts +71 -0
- package/dist/types/logging.js +16 -0
- package/dist/types/logging.js.map +1 -0
- package/dist/types/review.d.ts +17 -0
- package/dist/types/review.js +6 -0
- package/dist/types/review.js.map +1 -0
- package/dist/types/run.d.ts +32 -0
- package/dist/types/run.js +6 -0
- package/dist/types/run.js.map +1 -0
- package/dist/types/task.d.ts +71 -0
- package/dist/types/task.js +6 -0
- package/dist/types/task.js.map +1 -0
- package/dist/ui/components.d.ts +134 -0
- package/dist/ui/components.js +389 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/log-viewer.d.ts +49 -0
- package/dist/ui/log-viewer.js +449 -0
- package/dist/ui/log-viewer.js.map +1 -0
- package/dist/utils/checkpoint.d.ts +87 -0
- package/dist/utils/checkpoint.js +317 -0
- package/dist/utils/checkpoint.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +18 -8
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/dependency.d.ts +74 -0
- package/dist/utils/dependency.js +420 -0
- package/dist/utils/dependency.js.map +1 -0
- package/dist/utils/doctor.js +17 -11
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +10 -33
- package/dist/utils/enhanced-logger.js +108 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +121 -0
- package/dist/utils/git.js +484 -11
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/health.d.ts +91 -0
- package/dist/utils/health.js +556 -0
- package/dist/utils/health.js.map +1 -0
- package/dist/utils/lock.d.ts +95 -0
- package/dist/utils/lock.js +332 -0
- package/dist/utils/lock.js.map +1 -0
- package/dist/utils/log-buffer.d.ts +17 -0
- package/dist/utils/log-buffer.js +14 -0
- package/dist/utils/log-buffer.js.map +1 -0
- package/dist/utils/log-constants.d.ts +23 -0
- package/dist/utils/log-constants.js +28 -0
- package/dist/utils/log-constants.js.map +1 -0
- package/dist/utils/log-formatter.d.ts +25 -0
- package/dist/utils/log-formatter.js +237 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/log-service.d.ts +19 -0
- package/dist/utils/log-service.js +47 -0
- package/dist/utils/log-service.js.map +1 -0
- package/dist/utils/logger.d.ts +46 -27
- package/dist/utils/logger.js +82 -60
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/process-manager.d.ts +21 -0
- package/dist/utils/process-manager.js +138 -0
- package/dist/utils/process-manager.js.map +1 -0
- package/dist/utils/retry.d.ts +121 -0
- package/dist/utils/retry.js +374 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/run-service.d.ts +88 -0
- package/dist/utils/run-service.js +412 -0
- package/dist/utils/run-service.js.map +1 -0
- package/dist/utils/state.d.ts +62 -3
- package/dist/utils/state.js +317 -11
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +82 -0
- package/dist/utils/task-service.js +348 -0
- package/dist/utils/task-service.js.map +1 -0
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +2 -271
- package/dist/utils/types.js +16 -0
- package/dist/utils/types.js.map +1 -1
- package/package.json +38 -23
- package/scripts/ai-security-check.js +0 -1
- package/scripts/local-security-gate.sh +0 -0
- package/scripts/monitor-lanes.sh +94 -0
- package/scripts/patches/test-cursor-agent.js +0 -1
- package/scripts/release.sh +0 -0
- package/scripts/setup-security.sh +0 -0
- package/scripts/stream-logs.sh +72 -0
- package/scripts/verify-and-fix.sh +0 -0
- package/src/cli/clean.ts +187 -6
- package/src/cli/index.ts +12 -1
- package/src/cli/init.ts +8 -7
- package/src/cli/logs.ts +124 -77
- package/src/cli/monitor.ts +1815 -898
- package/src/cli/prepare.ts +41 -21
- package/src/cli/resume.ts +753 -626
- package/src/cli/run.ts +12 -5
- package/src/cli/runs.ts +212 -0
- package/src/cli/setup-commands.ts +0 -0
- package/src/cli/signal.ts +8 -7
- package/src/cli/stop.ts +209 -0
- package/src/cli/tasks.ts +154 -0
- package/src/core/auto-recovery.ts +909 -0
- package/src/core/failure-policy.ts +592 -0
- package/src/core/orchestrator.ts +1131 -704
- package/src/core/reviewer.ts +4 -0
- package/src/core/runner.ts +444 -180
- package/src/services/logging/buffer.ts +326 -0
- package/src/services/logging/console.ts +193 -0
- package/src/services/logging/file-writer.ts +526 -0
- package/src/services/logging/formatter.ts +268 -0
- package/src/services/logging/index.ts +16 -0
- package/src/services/logging/parser.ts +232 -0
- package/src/services/process/index.ts +261 -0
- package/src/types/agent.ts +24 -0
- package/src/types/config.ts +79 -0
- package/src/types/events.ts +156 -0
- package/src/types/index.ts +29 -0
- package/src/types/lane.ts +56 -0
- package/src/types/logging.ts +96 -0
- package/src/types/review.ts +20 -0
- package/src/types/run.ts +37 -0
- package/src/types/task.ts +79 -0
- package/src/ui/components.ts +430 -0
- package/src/ui/log-viewer.ts +485 -0
- package/src/utils/checkpoint.ts +374 -0
- package/src/utils/config.ts +18 -8
- package/src/utils/cursor-agent.ts +1 -1
- package/src/utils/dependency.ts +482 -0
- package/src/utils/doctor.ts +18 -11
- package/src/utils/enhanced-logger.ts +122 -60
- package/src/utils/git.ts +517 -11
- package/src/utils/health.ts +596 -0
- package/src/utils/lock.ts +346 -0
- package/src/utils/log-buffer.ts +28 -0
- package/src/utils/log-constants.ts +26 -0
- package/src/utils/log-formatter.ts +245 -0
- package/src/utils/log-service.ts +49 -0
- package/src/utils/logger.ts +100 -51
- package/src/utils/path.ts +45 -0
- package/src/utils/process-manager.ts +100 -0
- package/src/utils/retry.ts +413 -0
- package/src/utils/run-service.ts +433 -0
- package/src/utils/state.ts +385 -11
- package/src/utils/task-service.ts +370 -0
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +2 -314
- package/templates/basic.json +21 -0
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Orchestrator - Parallel lane execution with dependency management
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Multi-layer stall detection
|
|
7
|
+
* - Cyclic dependency detection
|
|
8
|
+
* - Enhanced recovery strategies
|
|
9
|
+
* - Health checks before start
|
|
6
10
|
*/
|
|
7
11
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
12
|
if (k2 === undefined) k2 = k;
|
|
@@ -53,11 +57,26 @@ const webhook_1 = require("../utils/webhook");
|
|
|
53
57
|
const config_1 = require("../utils/config");
|
|
54
58
|
const git = __importStar(require("../utils/git"));
|
|
55
59
|
const child_process_2 = require("child_process");
|
|
60
|
+
const path_1 = require("../utils/path");
|
|
56
61
|
const enhanced_logger_1 = require("../utils/enhanced-logger");
|
|
62
|
+
const log_formatter_1 = require("../utils/log-formatter");
|
|
63
|
+
const failure_policy_1 = require("./failure-policy");
|
|
64
|
+
const auto_recovery_1 = require("./auto-recovery");
|
|
65
|
+
const dependency_1 = require("../utils/dependency");
|
|
66
|
+
const health_1 = require("../utils/health");
|
|
67
|
+
const checkpoint_1 = require("../utils/checkpoint");
|
|
68
|
+
const lock_1 = require("../utils/lock");
|
|
69
|
+
/** Default stall detection configuration - 1 minute idle timeout for fast recovery */
|
|
70
|
+
const DEFAULT_ORCHESTRATOR_STALL_CONFIG = {
|
|
71
|
+
...failure_policy_1.DEFAULT_STALL_CONFIG,
|
|
72
|
+
idleTimeoutMs: 60 * 1000, // 1 minute (quick detection for continue signal)
|
|
73
|
+
progressTimeoutMs: 10 * 60 * 1000, // 10 minutes
|
|
74
|
+
maxRestarts: 2,
|
|
75
|
+
};
|
|
57
76
|
/**
|
|
58
77
|
* Spawn a lane process
|
|
59
78
|
*/
|
|
60
|
-
function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0, pipelineBranch, enhancedLogConfig, noGit = false, }) {
|
|
79
|
+
function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0, pipelineBranch, worktreeDir, enhancedLogConfig, noGit = false, onActivity, }) {
|
|
61
80
|
fs.mkdirSync(laneRunDir, { recursive: true });
|
|
62
81
|
// Use extension-less resolve to handle both .ts (dev) and .js (dist)
|
|
63
82
|
const runnerPath = require.resolve('./runner');
|
|
@@ -71,6 +90,9 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
|
|
|
71
90
|
if (pipelineBranch) {
|
|
72
91
|
args.push('--pipeline-branch', pipelineBranch);
|
|
73
92
|
}
|
|
93
|
+
if (worktreeDir) {
|
|
94
|
+
args.push('--worktree-dir', worktreeDir);
|
|
95
|
+
}
|
|
74
96
|
if (noGit) {
|
|
75
97
|
args.push('--no-git');
|
|
76
98
|
}
|
|
@@ -86,86 +108,13 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
|
|
|
86
108
|
if (logConfig.enabled) {
|
|
87
109
|
// Create callback for clean console output
|
|
88
110
|
const onParsedMessage = (msg) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
prefix = `${logger.COLORS.cyan}🧑 USER${logger.COLORS.reset}`;
|
|
97
|
-
// No truncation for user prompt to ensure full command visibility
|
|
98
|
-
content = content.replace(/\n/g, ' ');
|
|
99
|
-
break;
|
|
100
|
-
case 'assistant':
|
|
101
|
-
prefix = `${logger.COLORS.green}🤖 ASST${logger.COLORS.reset}`;
|
|
102
|
-
break;
|
|
103
|
-
case 'tool':
|
|
104
|
-
prefix = `${logger.COLORS.yellow}🔧 TOOL${logger.COLORS.reset}`;
|
|
105
|
-
// Simplify tool call: [Tool: read_file] {"target_file":"..."} -> read_file(target_file: ...)
|
|
106
|
-
const toolMatch = content.match(/\[Tool: ([^\]]+)\] (.*)/);
|
|
107
|
-
if (toolMatch) {
|
|
108
|
-
const [, name, args] = toolMatch;
|
|
109
|
-
try {
|
|
110
|
-
const parsedArgs = JSON.parse(args);
|
|
111
|
-
let argStr = '';
|
|
112
|
-
if (name === 'read_file' && parsedArgs.target_file) {
|
|
113
|
-
argStr = parsedArgs.target_file;
|
|
114
|
-
}
|
|
115
|
-
else if (name === 'run_terminal_cmd' && parsedArgs.command) {
|
|
116
|
-
argStr = parsedArgs.command;
|
|
117
|
-
}
|
|
118
|
-
else if (name === 'write' && parsedArgs.file_path) {
|
|
119
|
-
argStr = parsedArgs.file_path;
|
|
120
|
-
}
|
|
121
|
-
else if (name === 'search_replace' && parsedArgs.file_path) {
|
|
122
|
-
argStr = parsedArgs.file_path;
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
// Generic summary for other tools
|
|
126
|
-
const keys = Object.keys(parsedArgs);
|
|
127
|
-
if (keys.length > 0) {
|
|
128
|
-
argStr = String(parsedArgs[keys[0]]).substring(0, 50);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}(${argStr})`;
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
content = `${logger.COLORS.bold}${name}${logger.COLORS.reset}: ${args}`;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
break;
|
|
138
|
-
case 'tool_result':
|
|
139
|
-
prefix = `${logger.COLORS.gray}📄 RESL${logger.COLORS.reset}`;
|
|
140
|
-
// Simplify tool result: [Tool Result: read_file] ... -> read_file OK
|
|
141
|
-
const resMatch = content.match(/\[Tool Result: ([^\]]+)\]/);
|
|
142
|
-
content = resMatch ? `${resMatch[1]} OK` : 'result';
|
|
143
|
-
break;
|
|
144
|
-
case 'result':
|
|
145
|
-
prefix = `${logger.COLORS.green}✅ DONE${logger.COLORS.reset}`;
|
|
146
|
-
break;
|
|
147
|
-
case 'system':
|
|
148
|
-
prefix = `${logger.COLORS.gray}⚙️ SYS${logger.COLORS.reset}`;
|
|
149
|
-
break;
|
|
150
|
-
case 'thinking':
|
|
151
|
-
prefix = `${logger.COLORS.gray}🤔 THNK${logger.COLORS.reset}`;
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
if (prefix) {
|
|
155
|
-
const lines = content.split('\n');
|
|
156
|
-
const tsPrefix = `${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}${laneLabel}${logger.COLORS.reset}`;
|
|
157
|
-
if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'result' || msg.type === 'thinking') {
|
|
158
|
-
const header = `${prefix} ┌${'─'.repeat(60)}`;
|
|
159
|
-
process.stdout.write(`${tsPrefix} ${header}\n`);
|
|
160
|
-
for (const line of lines) {
|
|
161
|
-
process.stdout.write(`${tsPrefix} ${' '.repeat((0, enhanced_logger_1.stripAnsi)(prefix).length)} │ ${line}\n`);
|
|
162
|
-
}
|
|
163
|
-
process.stdout.write(`${tsPrefix} ${' '.repeat((0, enhanced_logger_1.stripAnsi)(prefix).length)} └${'─'.repeat(60)}\n`);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
process.stdout.write(`${tsPrefix} ${prefix} ${content}\n`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
111
|
+
if (onActivity)
|
|
112
|
+
onActivity();
|
|
113
|
+
const formatted = (0, log_formatter_1.formatMessageForConsole)(msg, {
|
|
114
|
+
laneLabel: `[${laneName}]`,
|
|
115
|
+
includeTimestamp: true
|
|
116
|
+
});
|
|
117
|
+
process.stdout.write(formatted + '\n');
|
|
169
118
|
};
|
|
170
119
|
logManager = (0, enhanced_logger_1.createLogManager)(laneRunDir, laneName, logConfig, onParsedMessage);
|
|
171
120
|
logPath = logManager.getLogPaths().clean;
|
|
@@ -188,12 +137,23 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
|
|
|
188
137
|
lineBuffer = lines.pop() || '';
|
|
189
138
|
for (const line of lines) {
|
|
190
139
|
const trimmed = line.trim();
|
|
191
|
-
//
|
|
192
|
-
if
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
140
|
+
// Show if it's a timestamped log line (starts with [YYYY-MM-DD... or [HH:MM:SS])
|
|
141
|
+
// or if it's NOT a noisy JSON line
|
|
142
|
+
const hasTimestamp = /^\[\d{4}-\d{2}-\d{2}T|\^\[\d{2}:\d{2}:\d{2}\]/.test(trimmed);
|
|
143
|
+
const isJson = trimmed.startsWith('{') || trimmed.includes('{"type"');
|
|
144
|
+
if (trimmed && !isJson) {
|
|
145
|
+
if (onActivity)
|
|
146
|
+
onActivity();
|
|
147
|
+
// If line already has timestamp format, just add lane prefix
|
|
148
|
+
if (hasTimestamp) {
|
|
149
|
+
// Insert lane name after first timestamp
|
|
150
|
+
const formatted = trimmed.replace(/^(\[[^\]]+\])/, `$1 ${logger.COLORS.magenta}[${laneName}]${logger.COLORS.reset}`);
|
|
151
|
+
process.stdout.write(formatted + '\n');
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// Add full prefix: timestamp + lane
|
|
155
|
+
process.stdout.write(`${logger.COLORS.gray}[${new Date().toLocaleTimeString('en-US', { hour12: false })}]${logger.COLORS.reset} ${logger.COLORS.magenta}[${laneName}]${logger.COLORS.reset} ${line}\n`);
|
|
156
|
+
}
|
|
197
157
|
}
|
|
198
158
|
}
|
|
199
159
|
});
|
|
@@ -211,11 +171,14 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
|
|
|
211
171
|
trimmed.startsWith('Switched to a new branch') ||
|
|
212
172
|
trimmed.startsWith('HEAD is now at') ||
|
|
213
173
|
trimmed.includes('actual output');
|
|
174
|
+
const ts = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
214
175
|
if (isStatus) {
|
|
215
|
-
process.stdout.write(`${logger.COLORS.gray}[${
|
|
176
|
+
process.stdout.write(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}[${laneName}]${logger.COLORS.reset} ${trimmed}\n`);
|
|
216
177
|
}
|
|
217
178
|
else {
|
|
218
|
-
|
|
179
|
+
if (onActivity)
|
|
180
|
+
onActivity();
|
|
181
|
+
process.stderr.write(`${logger.COLORS.gray}[${ts}]${logger.COLORS.reset} ${logger.COLORS.magenta}[${laneName}]${logger.COLORS.reset} ${logger.COLORS.red}❌ ERR ${trimmed}${logger.COLORS.reset}\n`);
|
|
219
182
|
}
|
|
220
183
|
}
|
|
221
184
|
}
|
|
@@ -228,7 +191,7 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor, startIndex = 0,
|
|
|
228
191
|
}
|
|
229
192
|
else {
|
|
230
193
|
// Fallback to simple file logging
|
|
231
|
-
logPath =
|
|
194
|
+
logPath = (0, path_1.safeJoin)(laneRunDir, 'terminal.log');
|
|
232
195
|
const logFd = fs.openSync(logPath, 'a');
|
|
233
196
|
child = (0, child_process_1.spawn)('node', args, {
|
|
234
197
|
stdio: ['ignore', logFd, logFd],
|
|
@@ -269,7 +232,7 @@ function listLaneFiles(tasksDir) {
|
|
|
269
232
|
.filter(f => f.endsWith('.json'))
|
|
270
233
|
.sort()
|
|
271
234
|
.map(f => {
|
|
272
|
-
const filePath =
|
|
235
|
+
const filePath = (0, path_1.safeJoin)(tasksDir, f);
|
|
273
236
|
const name = path.basename(f, '.json');
|
|
274
237
|
let dependsOn = [];
|
|
275
238
|
try {
|
|
@@ -294,7 +257,7 @@ function printLaneStatus(lanes, laneRunDirs) {
|
|
|
294
257
|
const dir = laneRunDirs[lane.name];
|
|
295
258
|
if (!dir)
|
|
296
259
|
return { lane: lane.name, status: '(unknown)', task: '-' };
|
|
297
|
-
const statePath =
|
|
260
|
+
const statePath = (0, path_1.safeJoin)(dir, 'state.json');
|
|
298
261
|
const state = (0, state_1.loadState)(statePath);
|
|
299
262
|
if (!state) {
|
|
300
263
|
const isWaiting = lane.dependsOn.length > 0;
|
|
@@ -331,12 +294,12 @@ async function resolveAllDependencies(blockedLanes, allLanes, laneRunDirs, pipel
|
|
|
331
294
|
return;
|
|
332
295
|
// 2. Setup a temporary worktree for resolution if needed, or use the first available one
|
|
333
296
|
const firstLaneName = Array.from(blockedLanes.keys())[0];
|
|
334
|
-
const statePath =
|
|
297
|
+
const statePath = (0, path_1.safeJoin)(laneRunDirs[firstLaneName], 'state.json');
|
|
335
298
|
const state = (0, state_1.loadState)(statePath);
|
|
336
|
-
const worktreeDir = state?.worktreeDir ||
|
|
299
|
+
const worktreeDir = state?.worktreeDir || (0, path_1.safeJoin)(runRoot, 'resolution-worktree');
|
|
337
300
|
if (!fs.existsSync(worktreeDir)) {
|
|
338
301
|
logger.info(`Creating resolution worktree at ${worktreeDir}`);
|
|
339
|
-
git.createWorktree(worktreeDir, pipelineBranch, { baseBranch:
|
|
302
|
+
git.createWorktree(worktreeDir, pipelineBranch, { baseBranch: git.getCurrentBranch() });
|
|
340
303
|
}
|
|
341
304
|
// 3. Resolve on pipeline branch
|
|
342
305
|
logger.info(`Resolving dependencies on ${pipelineBranch}`);
|
|
@@ -367,7 +330,7 @@ async function resolveAllDependencies(blockedLanes, allLanes, laneRunDirs, pipel
|
|
|
367
330
|
const laneDir = laneRunDirs[lane.name];
|
|
368
331
|
if (!laneDir)
|
|
369
332
|
continue;
|
|
370
|
-
const laneState = (0, state_1.loadState)(
|
|
333
|
+
const laneState = (0, state_1.loadState)((0, path_1.safeJoin)(laneDir, 'state.json'));
|
|
371
334
|
if (!laneState || laneState.status === 'completed' || laneState.status === 'failed')
|
|
372
335
|
continue;
|
|
373
336
|
// Merge pipelineBranch into the lane's current task branch
|
|
@@ -406,16 +369,82 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
406
369
|
if (lanes.length === 0) {
|
|
407
370
|
throw new Error(`No lane task files found in ${tasksDir}`);
|
|
408
371
|
}
|
|
372
|
+
// Run preflight checks
|
|
373
|
+
if (!options.skipPreflight) {
|
|
374
|
+
logger.section('🔍 Preflight Checks');
|
|
375
|
+
const preflight = await (0, health_1.preflightCheck)({
|
|
376
|
+
requireRemote: !options.noGit,
|
|
377
|
+
requireAuth: true,
|
|
378
|
+
});
|
|
379
|
+
if (!preflight.canProceed) {
|
|
380
|
+
(0, health_1.printPreflightReport)(preflight);
|
|
381
|
+
throw new Error('Preflight check failed. Please fix the blockers above.');
|
|
382
|
+
}
|
|
383
|
+
// Auto-repair if there are warnings
|
|
384
|
+
if (preflight.warnings.length > 0) {
|
|
385
|
+
logger.info('Attempting auto-repair...');
|
|
386
|
+
const repair = await (0, health_1.autoRepair)();
|
|
387
|
+
if (repair.repaired.length > 0) {
|
|
388
|
+
for (const r of repair.repaired) {
|
|
389
|
+
logger.success(`✓ ${r}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
logger.success('✓ Preflight checks passed');
|
|
394
|
+
}
|
|
395
|
+
// Validate dependencies and detect cycles
|
|
396
|
+
logger.section('📊 Dependency Analysis');
|
|
397
|
+
const depInfos = lanes.map(l => ({
|
|
398
|
+
name: l.name,
|
|
399
|
+
dependsOn: l.dependsOn,
|
|
400
|
+
}));
|
|
401
|
+
const depValidation = (0, dependency_1.validateDependencies)(depInfos);
|
|
402
|
+
if (!depValidation.valid) {
|
|
403
|
+
logger.error('❌ Dependency validation failed:');
|
|
404
|
+
for (const err of depValidation.errors) {
|
|
405
|
+
logger.error(` • ${err}`);
|
|
406
|
+
}
|
|
407
|
+
throw new Error('Invalid dependency configuration');
|
|
408
|
+
}
|
|
409
|
+
if (depValidation.warnings.length > 0) {
|
|
410
|
+
for (const warn of depValidation.warnings) {
|
|
411
|
+
logger.warn(`⚠️ ${warn}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Print dependency graph
|
|
415
|
+
(0, dependency_1.printDependencyGraph)(depInfos);
|
|
409
416
|
const config = (0, config_1.loadConfig)();
|
|
410
417
|
const logsDir = (0, config_1.getLogsDir)(config);
|
|
411
418
|
const runId = `run-${Date.now()}`;
|
|
412
419
|
// Use absolute path for runRoot to avoid issues with subfolders
|
|
413
420
|
const runRoot = options.runDir
|
|
414
|
-
? (path.isAbsolute(options.runDir) ? options.runDir : path.resolve(process.cwd(), options.runDir))
|
|
415
|
-
:
|
|
421
|
+
? (path.isAbsolute(options.runDir) ? options.runDir : path.resolve(process.cwd(), options.runDir)) // nosemgrep
|
|
422
|
+
: (0, path_1.safeJoin)(logsDir, 'runs', runId);
|
|
416
423
|
fs.mkdirSync(runRoot, { recursive: true });
|
|
424
|
+
// Clean stale locks before starting
|
|
425
|
+
try {
|
|
426
|
+
const lockDir = (0, lock_1.getLockDir)(git.getRepoRoot());
|
|
427
|
+
const cleaned = (0, lock_1.cleanStaleLocks)(lockDir);
|
|
428
|
+
if (cleaned > 0) {
|
|
429
|
+
logger.info(`Cleaned ${cleaned} stale lock(s)`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// Ignore lock cleanup errors
|
|
434
|
+
}
|
|
417
435
|
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
418
436
|
const pipelineBranch = `cursorflow/run-${Date.now().toString(36)}-${randomSuffix}`;
|
|
437
|
+
// Stall detection configuration
|
|
438
|
+
const stallConfig = {
|
|
439
|
+
...DEFAULT_ORCHESTRATOR_STALL_CONFIG,
|
|
440
|
+
...options.stallConfig,
|
|
441
|
+
};
|
|
442
|
+
// Initialize auto-recovery manager
|
|
443
|
+
const autoRecoveryManager = (0, auto_recovery_1.getAutoRecoveryManager)({
|
|
444
|
+
...auto_recovery_1.DEFAULT_AUTO_RECOVERY_CONFIG,
|
|
445
|
+
idleTimeoutMs: stallConfig.idleTimeoutMs, // Sync with stall config
|
|
446
|
+
...options.autoRecoveryConfig,
|
|
447
|
+
});
|
|
419
448
|
// Initialize event system
|
|
420
449
|
events_1.events.setRunId(runId);
|
|
421
450
|
if (options.webhooks) {
|
|
@@ -436,11 +465,38 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
436
465
|
// Track start index for each lane (initially 0)
|
|
437
466
|
for (const lane of lanes) {
|
|
438
467
|
lane.startIndex = 0;
|
|
468
|
+
lane.restartCount = 0;
|
|
439
469
|
}
|
|
440
470
|
const laneRunDirs = {};
|
|
471
|
+
const laneWorktreeDirs = {};
|
|
472
|
+
const repoRoot = git.getRepoRoot();
|
|
441
473
|
for (const lane of lanes) {
|
|
442
|
-
laneRunDirs[lane.name] =
|
|
474
|
+
laneRunDirs[lane.name] = (0, path_1.safeJoin)(runRoot, 'lanes', lane.name);
|
|
443
475
|
fs.mkdirSync(laneRunDirs[lane.name], { recursive: true });
|
|
476
|
+
// Create initial state for ALL lanes so resume can find them even if they didn't start
|
|
477
|
+
try {
|
|
478
|
+
const taskConfig = JSON.parse(fs.readFileSync(lane.path, 'utf8'));
|
|
479
|
+
// Calculate unique branch and worktree for this lane
|
|
480
|
+
const lanePipelineBranch = `${pipelineBranch}/${lane.name}`;
|
|
481
|
+
// Use a flat worktree directory name to avoid race conditions in parent directory creation
|
|
482
|
+
// repoRoot/_cursorflow/worktrees/cursorflow-run-xxx-lane-name
|
|
483
|
+
const laneWorktreeDir = (0, path_1.safeJoin)(repoRoot, taskConfig.worktreeRoot || '_cursorflow/worktrees', lanePipelineBranch.replace(/\//g, '-'));
|
|
484
|
+
// Ensure the parent directory exists before spawning the runner
|
|
485
|
+
// to avoid race conditions in git worktree add or fs operations
|
|
486
|
+
const worktreeParent = path.dirname(laneWorktreeDir);
|
|
487
|
+
if (!fs.existsSync(worktreeParent)) {
|
|
488
|
+
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
489
|
+
}
|
|
490
|
+
laneWorktreeDirs[lane.name] = laneWorktreeDir;
|
|
491
|
+
const initialState = (0, state_1.createLaneState)(lane.name, taskConfig, lane.path, {
|
|
492
|
+
pipelineBranch: lanePipelineBranch,
|
|
493
|
+
worktreeDir: laneWorktreeDir
|
|
494
|
+
});
|
|
495
|
+
(0, state_1.saveState)((0, path_1.safeJoin)(laneRunDirs[lane.name], 'state.json'), initialState);
|
|
496
|
+
}
|
|
497
|
+
catch (e) {
|
|
498
|
+
logger.warn(`Failed to create initial state for lane ${lane.name}: ${e}`);
|
|
499
|
+
}
|
|
444
500
|
}
|
|
445
501
|
logger.section('🧭 Starting Orchestration');
|
|
446
502
|
logger.info(`Tasks directory: ${tasksDir}`);
|
|
@@ -497,7 +553,14 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
497
553
|
for (const lane of readyToStart) {
|
|
498
554
|
if (running.size >= maxConcurrent)
|
|
499
555
|
break;
|
|
556
|
+
const laneStatePath = (0, path_1.safeJoin)(laneRunDirs[lane.name], 'state.json');
|
|
557
|
+
// Validate and repair state before starting
|
|
558
|
+
const validation = (0, state_1.validateLaneState)(laneStatePath, { autoRepair: true });
|
|
559
|
+
if (!validation.valid && !validation.repaired) {
|
|
560
|
+
logger.warn(`[${lane.name}] State validation issues: ${validation.issues.join(', ')}`);
|
|
561
|
+
}
|
|
500
562
|
logger.info(`Lane started: ${lane.name}${lane.startIndex ? ` (resuming from ${lane.startIndex})` : ''}`);
|
|
563
|
+
let lastOutput = '';
|
|
501
564
|
const spawnResult = spawnLane({
|
|
502
565
|
laneName: lane.name,
|
|
503
566
|
tasksFile: lane.path,
|
|
@@ -505,25 +568,280 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
505
568
|
executor: options.executor || 'cursor-agent',
|
|
506
569
|
startIndex: lane.startIndex,
|
|
507
570
|
pipelineBranch: `${pipelineBranch}/${lane.name}`,
|
|
571
|
+
worktreeDir: laneWorktreeDirs[lane.name],
|
|
508
572
|
enhancedLogConfig: options.enhancedLogging,
|
|
509
573
|
noGit: options.noGit,
|
|
574
|
+
onActivity: () => {
|
|
575
|
+
const info = running.get(lane.name);
|
|
576
|
+
if (info) {
|
|
577
|
+
info.lastActivity = Date.now();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
510
580
|
});
|
|
511
|
-
|
|
581
|
+
// Track last output and bytes received for long operation and stall detection
|
|
582
|
+
if (spawnResult.child.stdout) {
|
|
583
|
+
spawnResult.child.stdout.on('data', (data) => {
|
|
584
|
+
const info = running.get(lane.name);
|
|
585
|
+
if (info) {
|
|
586
|
+
info.lastOutput = data.toString().trim().split('\n').pop() || '';
|
|
587
|
+
info.bytesReceived += data.length;
|
|
588
|
+
// Update auto-recovery manager
|
|
589
|
+
autoRecoveryManager.recordActivity(lane.name, data.length, info.lastOutput);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
const now = Date.now();
|
|
594
|
+
running.set(lane.name, {
|
|
595
|
+
...spawnResult,
|
|
596
|
+
lastActivity: now,
|
|
597
|
+
lastStateUpdate: now,
|
|
598
|
+
stallPhase: 0,
|
|
599
|
+
taskStartTime: now,
|
|
600
|
+
lastOutput: '',
|
|
601
|
+
statePath: laneStatePath,
|
|
602
|
+
bytesReceived: 0,
|
|
603
|
+
lastBytesCheck: 0,
|
|
604
|
+
continueSignalsSent: 0,
|
|
605
|
+
});
|
|
606
|
+
// Register lane with auto-recovery manager
|
|
607
|
+
autoRecoveryManager.registerLane(lane.name);
|
|
608
|
+
// Update lane tracking
|
|
609
|
+
lane.taskStartTime = now;
|
|
512
610
|
events_1.events.emit('lane.started', {
|
|
513
611
|
laneName: lane.name,
|
|
514
612
|
pid: spawnResult.child.pid,
|
|
515
613
|
logPath: spawnResult.logPath,
|
|
516
614
|
});
|
|
517
615
|
}
|
|
518
|
-
// 3. Wait for any running lane to finish
|
|
616
|
+
// 3. Wait for any running lane to finish OR check for stalls
|
|
519
617
|
if (running.size > 0) {
|
|
618
|
+
// Polling timeout for stall detection
|
|
619
|
+
let pollTimeout;
|
|
620
|
+
const pollPromise = new Promise(resolve => {
|
|
621
|
+
pollTimeout = setTimeout(() => resolve({ name: '__poll__', code: 0 }), 10000);
|
|
622
|
+
});
|
|
520
623
|
const promises = Array.from(running.entries()).map(async ([name, { child }]) => {
|
|
521
624
|
const code = await waitChild(child);
|
|
522
625
|
return { name, code };
|
|
523
626
|
});
|
|
524
|
-
const
|
|
627
|
+
const result = await Promise.race([...promises, pollPromise]);
|
|
628
|
+
if (pollTimeout)
|
|
629
|
+
clearTimeout(pollTimeout);
|
|
630
|
+
if (result.name === '__poll__') {
|
|
631
|
+
// Periodic stall check with multi-layer detection and escalating recovery
|
|
632
|
+
for (const [laneName, info] of running.entries()) {
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
const idleTime = now - info.lastActivity;
|
|
635
|
+
const lane = lanes.find(l => l.name === laneName);
|
|
636
|
+
// Check state file for progress updates
|
|
637
|
+
let progressTime = 0;
|
|
638
|
+
try {
|
|
639
|
+
const stateStat = fs.statSync(info.statePath);
|
|
640
|
+
const stateUpdateTime = stateStat.mtimeMs;
|
|
641
|
+
if (stateUpdateTime > info.lastStateUpdate) {
|
|
642
|
+
info.lastStateUpdate = stateUpdateTime;
|
|
643
|
+
}
|
|
644
|
+
progressTime = now - info.lastStateUpdate;
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// State file might not exist yet
|
|
648
|
+
}
|
|
649
|
+
// Calculate bytes received since last check
|
|
650
|
+
const bytesDelta = info.bytesReceived - info.lastBytesCheck;
|
|
651
|
+
info.lastBytesCheck = info.bytesReceived;
|
|
652
|
+
// Use multi-layer stall analysis with enhanced context
|
|
653
|
+
const analysis = (0, failure_policy_1.analyzeStall)({
|
|
654
|
+
stallPhase: info.stallPhase,
|
|
655
|
+
idleTimeMs: idleTime,
|
|
656
|
+
progressTimeMs: progressTime,
|
|
657
|
+
lastOutput: info.lastOutput,
|
|
658
|
+
restartCount: lane.restartCount || 0,
|
|
659
|
+
taskStartTimeMs: info.taskStartTime,
|
|
660
|
+
bytesReceived: bytesDelta, // Bytes since last check
|
|
661
|
+
continueSignalsSent: info.continueSignalsSent,
|
|
662
|
+
}, stallConfig);
|
|
663
|
+
// Only act if action is not NONE
|
|
664
|
+
if (analysis.action !== failure_policy_1.RecoveryAction.NONE) {
|
|
665
|
+
(0, failure_policy_1.logFailure)(laneName, analysis);
|
|
666
|
+
info.logManager?.log('error', analysis.message);
|
|
667
|
+
if (analysis.action === failure_policy_1.RecoveryAction.CONTINUE_SIGNAL) {
|
|
668
|
+
const interventionPath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'intervention.txt');
|
|
669
|
+
try {
|
|
670
|
+
fs.writeFileSync(interventionPath, 'continue');
|
|
671
|
+
info.stallPhase = 1;
|
|
672
|
+
info.lastActivity = now;
|
|
673
|
+
info.continueSignalsSent++;
|
|
674
|
+
logger.info(`[${laneName}] Sent continue signal (#${info.continueSignalsSent})`);
|
|
675
|
+
events_1.events.emit('recovery.continue_signal', {
|
|
676
|
+
laneName,
|
|
677
|
+
idleSeconds: Math.round(idleTime / 1000),
|
|
678
|
+
signalCount: info.continueSignalsSent,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
catch (e) {
|
|
682
|
+
logger.error(`Failed to write intervention file for ${laneName}: ${e}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
else if (analysis.action === failure_policy_1.RecoveryAction.STRONGER_PROMPT) {
|
|
686
|
+
const interventionPath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'intervention.txt');
|
|
687
|
+
const strongerPrompt = `[SYSTEM INTERVENTION] You seem to be stuck. Please continue with your current task immediately. If you're waiting for something, explain what you need and proceed with what you can do now. If you've completed the task, summarize your work and finish.`;
|
|
688
|
+
try {
|
|
689
|
+
fs.writeFileSync(interventionPath, strongerPrompt);
|
|
690
|
+
info.stallPhase = 2;
|
|
691
|
+
info.lastActivity = now;
|
|
692
|
+
logger.warn(`[${laneName}] Sent stronger prompt after continue signal failed`);
|
|
693
|
+
events_1.events.emit('recovery.stronger_prompt', { laneName });
|
|
694
|
+
}
|
|
695
|
+
catch (e) {
|
|
696
|
+
logger.error(`Failed to write intervention file for ${laneName}: ${e}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else if (analysis.action === failure_policy_1.RecoveryAction.KILL_AND_RESTART ||
|
|
700
|
+
analysis.action === failure_policy_1.RecoveryAction.RESTART_LANE ||
|
|
701
|
+
analysis.action === failure_policy_1.RecoveryAction.RESTART_LANE_FROM_CHECKPOINT) {
|
|
702
|
+
lane.restartCount = (lane.restartCount || 0) + 1;
|
|
703
|
+
info.stallPhase = 3;
|
|
704
|
+
// Try to get checkpoint info
|
|
705
|
+
const checkpoint = (0, checkpoint_1.getLatestCheckpoint)(laneRunDirs[laneName]);
|
|
706
|
+
if (checkpoint) {
|
|
707
|
+
logger.info(`[${laneName}] Checkpoint available: ${checkpoint.id} (task ${checkpoint.taskIndex})`);
|
|
708
|
+
}
|
|
709
|
+
// Kill the process
|
|
710
|
+
try {
|
|
711
|
+
info.child.kill('SIGKILL');
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
// Process might already be dead
|
|
715
|
+
}
|
|
716
|
+
logger.warn(`[${laneName}] Killing and restarting lane (restart #${lane.restartCount})`);
|
|
717
|
+
events_1.events.emit('recovery.restart', {
|
|
718
|
+
laneName,
|
|
719
|
+
restartCount: lane.restartCount,
|
|
720
|
+
maxRestarts: stallConfig.maxRestarts,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
else if (analysis.action === failure_policy_1.RecoveryAction.RUN_DOCTOR) {
|
|
724
|
+
info.stallPhase = 4;
|
|
725
|
+
// Run diagnostics
|
|
726
|
+
logger.error(`[${laneName}] Running diagnostics due to persistent failures...`);
|
|
727
|
+
// Import health check dynamically to avoid circular dependency
|
|
728
|
+
const { checkAgentHealth, checkAuthHealth } = await Promise.resolve().then(() => __importStar(require('../utils/health')));
|
|
729
|
+
const [agentHealth, authHealth] = await Promise.all([
|
|
730
|
+
checkAgentHealth(),
|
|
731
|
+
checkAuthHealth(),
|
|
732
|
+
]);
|
|
733
|
+
const issues = [];
|
|
734
|
+
if (!agentHealth.ok)
|
|
735
|
+
issues.push(`Agent: ${agentHealth.message}`);
|
|
736
|
+
if (!authHealth.ok)
|
|
737
|
+
issues.push(`Auth: ${authHealth.message}`);
|
|
738
|
+
if (issues.length > 0) {
|
|
739
|
+
logger.error(`[${laneName}] Diagnostic issues found:\n ${issues.join('\n ')}`);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
logger.warn(`[${laneName}] No obvious issues found. The problem may be with the AI model or network.`);
|
|
743
|
+
}
|
|
744
|
+
// Save diagnostic to file
|
|
745
|
+
const diagnosticPath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'diagnostic.json');
|
|
746
|
+
fs.writeFileSync(diagnosticPath, JSON.stringify({
|
|
747
|
+
timestamp: Date.now(),
|
|
748
|
+
agentHealthy: agentHealth.ok,
|
|
749
|
+
authHealthy: authHealth.ok,
|
|
750
|
+
issues,
|
|
751
|
+
analysis,
|
|
752
|
+
}, null, 2));
|
|
753
|
+
// Kill the process
|
|
754
|
+
try {
|
|
755
|
+
info.child.kill('SIGKILL');
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
// Process might already be dead
|
|
759
|
+
}
|
|
760
|
+
logger.error(`[${laneName}] Aborting lane after diagnostic. Check ${diagnosticPath} for details.`);
|
|
761
|
+
// Save POF for failed recovery
|
|
762
|
+
const recoveryState = autoRecoveryManager.getState(laneName);
|
|
763
|
+
if (recoveryState) {
|
|
764
|
+
try {
|
|
765
|
+
const laneStatePath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'state.json');
|
|
766
|
+
const laneState = (0, state_1.loadState)(laneStatePath);
|
|
767
|
+
const pofDir = (0, path_1.safeJoin)(runRoot, '..', '..', 'pof');
|
|
768
|
+
const diagnosticInfo = {
|
|
769
|
+
timestamp: Date.now(),
|
|
770
|
+
agentHealthy: agentHealth.ok,
|
|
771
|
+
authHealthy: authHealth.ok,
|
|
772
|
+
systemHealthy: true,
|
|
773
|
+
suggestedAction: issues.length > 0 ? 'Fix the issues above and retry' : 'Try with a different model',
|
|
774
|
+
details: issues.join('\n') || 'No obvious issues found',
|
|
775
|
+
};
|
|
776
|
+
const pofEntry = (0, auto_recovery_1.createPOFFromRecoveryState)(runId, runRoot, laneName, recoveryState, laneState, diagnosticInfo);
|
|
777
|
+
(0, auto_recovery_1.savePOF)(runId, pofDir, pofEntry);
|
|
778
|
+
}
|
|
779
|
+
catch (pofError) {
|
|
780
|
+
logger.warn(`[${laneName}] Failed to save POF: ${pofError.message}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
events_1.events.emit('recovery.diagnosed', {
|
|
784
|
+
laneName,
|
|
785
|
+
diagnostic: { agentHealthy: agentHealth.ok, authHealthy: authHealth.ok, issues },
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
else if (analysis.action === failure_policy_1.RecoveryAction.ABORT_LANE) {
|
|
789
|
+
info.stallPhase = 5;
|
|
790
|
+
try {
|
|
791
|
+
info.child.kill('SIGKILL');
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
// Process might already be dead
|
|
795
|
+
}
|
|
796
|
+
logger.error(`[${laneName}] Aborting lane due to repeated stalls`);
|
|
797
|
+
// Save POF for failed recovery
|
|
798
|
+
const recoveryState = autoRecoveryManager.getState(laneName);
|
|
799
|
+
if (recoveryState) {
|
|
800
|
+
try {
|
|
801
|
+
const laneStatePath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'state.json');
|
|
802
|
+
const laneState = (0, state_1.loadState)(laneStatePath);
|
|
803
|
+
const pofDir = (0, path_1.safeJoin)(runRoot, '..', '..', 'pof');
|
|
804
|
+
const pofEntry = (0, auto_recovery_1.createPOFFromRecoveryState)(runId, runRoot, laneName, recoveryState, laneState, recoveryState.diagnosticInfo);
|
|
805
|
+
(0, auto_recovery_1.savePOF)(runId, pofDir, pofEntry);
|
|
806
|
+
}
|
|
807
|
+
catch (pofError) {
|
|
808
|
+
logger.warn(`[${laneName}] Failed to save POF: ${pofError.message}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
else if (analysis.action === failure_policy_1.RecoveryAction.SEND_GIT_GUIDANCE) {
|
|
813
|
+
// Send guidance message to agent for git issues
|
|
814
|
+
const interventionPath = (0, path_1.safeJoin)(laneRunDirs[laneName], 'intervention.txt');
|
|
815
|
+
// Determine which guidance to send based on the failure type
|
|
816
|
+
let guidance;
|
|
817
|
+
if (analysis.type === failure_policy_1.FailureType.GIT_PUSH_REJECTED) {
|
|
818
|
+
guidance = (0, auto_recovery_1.getGitPushFailureGuidance)();
|
|
819
|
+
}
|
|
820
|
+
else if (analysis.type === failure_policy_1.FailureType.MERGE_CONFLICT) {
|
|
821
|
+
guidance = (0, auto_recovery_1.getMergeConflictGuidance)();
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
guidance = (0, auto_recovery_1.getGitErrorGuidance)(analysis.message);
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
fs.writeFileSync(interventionPath, guidance);
|
|
828
|
+
info.lastActivity = now;
|
|
829
|
+
logger.info(`[${laneName}] Sent git issue guidance to agent`);
|
|
830
|
+
}
|
|
831
|
+
catch (e) {
|
|
832
|
+
logger.error(`[${laneName}] Failed to send guidance: ${e.message}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const finished = result;
|
|
840
|
+
const info = running.get(finished.name);
|
|
525
841
|
running.delete(finished.name);
|
|
526
842
|
exitCodes[finished.name] = finished.code;
|
|
843
|
+
// Unregister from auto-recovery manager
|
|
844
|
+
autoRecoveryManager.unregisterLane(finished.name);
|
|
527
845
|
if (finished.code === 0) {
|
|
528
846
|
completedLanes.add(finished.name);
|
|
529
847
|
events_1.events.emit('lane.completed', {
|
|
@@ -533,7 +851,7 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
533
851
|
}
|
|
534
852
|
else if (finished.code === 2) {
|
|
535
853
|
// Blocked by dependency
|
|
536
|
-
const statePath =
|
|
854
|
+
const statePath = (0, path_1.safeJoin)(laneRunDirs[finished.name], 'state.json');
|
|
537
855
|
const state = (0, state_1.loadState)(statePath);
|
|
538
856
|
if (state && state.dependencyRequest) {
|
|
539
857
|
blockedLanes.set(finished.name, state.dependencyRequest);
|
|
@@ -553,11 +871,27 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
553
871
|
}
|
|
554
872
|
}
|
|
555
873
|
else {
|
|
874
|
+
// Check if it was a restart request
|
|
875
|
+
if (info.stallPhase === 2) {
|
|
876
|
+
logger.info(`🔄 Lane ${finished.name} is being restarted due to stall...`);
|
|
877
|
+
// Update startIndex from current state to resume from the same task
|
|
878
|
+
const statePath = (0, path_1.safeJoin)(laneRunDirs[finished.name], 'state.json');
|
|
879
|
+
const state = (0, state_1.loadState)(statePath);
|
|
880
|
+
if (state) {
|
|
881
|
+
const lane = lanes.find(l => l.name === finished.name);
|
|
882
|
+
if (lane) {
|
|
883
|
+
lane.startIndex = state.currentTaskIndex;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// Note: we don't add to failedLanes or completedLanes,
|
|
887
|
+
// so it will be eligible to start again in the next iteration.
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
556
890
|
failedLanes.add(finished.name);
|
|
557
891
|
events_1.events.emit('lane.failed', {
|
|
558
892
|
laneName: finished.name,
|
|
559
893
|
exitCode: finished.code,
|
|
560
|
-
error: 'Process exited with non-zero code',
|
|
894
|
+
error: info.stallPhase === 3 ? 'Stopped due to repeated stall' : 'Process exited with non-zero code',
|
|
561
895
|
});
|
|
562
896
|
}
|
|
563
897
|
printLaneStatus(lanes, laneRunDirs);
|