@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
package/dist/core/runner.js
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Core Runner - Execute tasks sequentially in a lane
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Enhanced retry with circuit breaker
|
|
7
|
+
* - Checkpoint system for recovery
|
|
8
|
+
* - State validation and repair
|
|
9
|
+
* - Improved dependency management
|
|
6
10
|
*/
|
|
7
11
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
12
|
if (k2 === undefined) k2 = k;
|
|
@@ -42,7 +46,10 @@ exports.cursorAgentCreateChat = cursorAgentCreateChat;
|
|
|
42
46
|
exports.validateTaskConfig = validateTaskConfig;
|
|
43
47
|
exports.cursorAgentSend = cursorAgentSend;
|
|
44
48
|
exports.extractDependencyRequest = extractDependencyRequest;
|
|
49
|
+
exports.readDependencyRequestFile = readDependencyRequestFile;
|
|
50
|
+
exports.clearDependencyRequestFile = clearDependencyRequestFile;
|
|
45
51
|
exports.wrapPromptForDependencyPolicy = wrapPromptForDependencyPolicy;
|
|
52
|
+
exports.wrapPrompt = wrapPrompt;
|
|
46
53
|
exports.applyDependencyFilePermissions = applyDependencyFilePermissions;
|
|
47
54
|
exports.waitForTaskDependencies = waitForTaskDependencies;
|
|
48
55
|
exports.mergeDependencyBranches = mergeDependencyBranches;
|
|
@@ -59,6 +66,11 @@ const events_1 = require("../utils/events");
|
|
|
59
66
|
const config_1 = require("../utils/config");
|
|
60
67
|
const webhook_1 = require("../utils/webhook");
|
|
61
68
|
const reviewer_1 = require("./reviewer");
|
|
69
|
+
const path_1 = require("../utils/path");
|
|
70
|
+
const failure_policy_1 = require("./failure-policy");
|
|
71
|
+
const checkpoint_1 = require("../utils/checkpoint");
|
|
72
|
+
const dependency_1 = require("../utils/dependency");
|
|
73
|
+
const health_1 = require("../utils/health");
|
|
62
74
|
/**
|
|
63
75
|
* Execute cursor-agent command with timeout and better error handling
|
|
64
76
|
*/
|
|
@@ -174,9 +186,9 @@ function validateTaskConfig(config) {
|
|
|
174
186
|
}
|
|
175
187
|
}
|
|
176
188
|
/**
|
|
177
|
-
* Execute cursor-agent command with streaming
|
|
189
|
+
* Internal: Execute cursor-agent command with streaming
|
|
178
190
|
*/
|
|
179
|
-
async function
|
|
191
|
+
async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat, taskName }) {
|
|
180
192
|
// Use stream-json format for structured output with tool calls and results
|
|
181
193
|
const format = outputFormat || 'stream-json';
|
|
182
194
|
const args = [
|
|
@@ -190,65 +202,53 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
|
|
|
190
202
|
prompt,
|
|
191
203
|
];
|
|
192
204
|
const timeoutMs = timeout || DEFAULT_TIMEOUT_MS;
|
|
193
|
-
logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
|
|
194
205
|
// Determine stdio mode based on intervention setting
|
|
195
|
-
// When intervention is enabled, we pipe stdin for message injection
|
|
196
|
-
// When disabled (default), we ignore stdin to avoid buffering issues
|
|
197
206
|
const stdinMode = enableIntervention ? 'pipe' : 'ignore';
|
|
198
|
-
if (enableIntervention) {
|
|
199
|
-
logger.info('Intervention mode enabled (stdin piped)');
|
|
200
|
-
}
|
|
201
207
|
return new Promise((resolve) => {
|
|
202
208
|
// Build environment, preserving user's NODE_OPTIONS but disabling problematic flags
|
|
203
209
|
const childEnv = { ...process.env };
|
|
204
|
-
// Only filter out specific problematic NODE_OPTIONS, don't clear entirely
|
|
205
210
|
if (childEnv.NODE_OPTIONS) {
|
|
206
|
-
// Remove flags that might interfere with cursor-agent
|
|
207
211
|
const filtered = childEnv.NODE_OPTIONS
|
|
208
212
|
.split(' ')
|
|
209
213
|
.filter(opt => !opt.includes('--inspect') && !opt.includes('--debug'))
|
|
210
214
|
.join(' ');
|
|
211
215
|
childEnv.NODE_OPTIONS = filtered;
|
|
212
216
|
}
|
|
213
|
-
// Disable Python buffering in case cursor-agent uses Python
|
|
214
217
|
childEnv.PYTHONUNBUFFERED = '1';
|
|
215
218
|
const child = (0, child_process_1.spawn)('cursor-agent', args, {
|
|
216
219
|
stdio: [stdinMode, 'pipe', 'pipe'],
|
|
217
220
|
env: childEnv,
|
|
218
221
|
});
|
|
219
|
-
|
|
220
|
-
// Save PID to state if possible (avoid TOCTOU by reading directly)
|
|
222
|
+
// Save PID to state if possible
|
|
221
223
|
if (child.pid && signalDir) {
|
|
222
224
|
try {
|
|
223
|
-
const statePath =
|
|
224
|
-
// Read directly without existence check to avoid race condition
|
|
225
|
+
const statePath = (0, path_1.safeJoin)(signalDir, 'state.json');
|
|
225
226
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
226
227
|
state.pid = child.pid;
|
|
227
228
|
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
228
229
|
}
|
|
229
230
|
catch {
|
|
230
|
-
// Best effort
|
|
231
|
+
// Best effort
|
|
231
232
|
}
|
|
232
233
|
}
|
|
233
234
|
let fullStdout = '';
|
|
234
235
|
let fullStderr = '';
|
|
235
236
|
let timeoutHandle;
|
|
236
|
-
// Heartbeat logging
|
|
237
|
+
// Heartbeat logging
|
|
237
238
|
let lastHeartbeat = Date.now();
|
|
238
239
|
let bytesReceived = 0;
|
|
240
|
+
const startTime = Date.now();
|
|
239
241
|
const heartbeatInterval = setInterval(() => {
|
|
240
|
-
const elapsed = Math.round((Date.now() - lastHeartbeat) / 1000);
|
|
241
242
|
const totalElapsed = Math.round((Date.now() - startTime) / 1000);
|
|
242
|
-
|
|
243
|
+
// Output without timestamp - orchestrator will add it
|
|
244
|
+
console.log(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
|
|
243
245
|
}, HEARTBEAT_INTERVAL_MS);
|
|
244
|
-
|
|
245
|
-
// Watch for "intervention.txt" or "timeout.txt" signal files
|
|
246
|
+
// Signal watchers (intervention, timeout)
|
|
246
247
|
const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
|
|
247
248
|
const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
|
|
248
249
|
let signalWatcher = null;
|
|
249
250
|
if (signalDir && fs.existsSync(signalDir)) {
|
|
250
251
|
signalWatcher = fs.watch(signalDir, (event, filename) => {
|
|
251
|
-
// Handle intervention
|
|
252
252
|
if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
|
|
253
253
|
try {
|
|
254
254
|
const message = fs.readFileSync(interventionPath, 'utf8').trim();
|
|
@@ -256,74 +256,65 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
|
|
|
256
256
|
if (enableIntervention && child.stdin) {
|
|
257
257
|
logger.info(`Injecting intervention: ${message}`);
|
|
258
258
|
child.stdin.write(message + '\n');
|
|
259
|
+
// Log to conversation history for visibility in monitor/logs
|
|
260
|
+
if (signalDir) {
|
|
261
|
+
const convoPath = path.join(signalDir, 'conversation.jsonl');
|
|
262
|
+
(0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('intervention', `[HUMAN INTERVENTION]: ${message}`, {
|
|
263
|
+
task: taskName || 'AGENT_TURN',
|
|
264
|
+
model: 'manual'
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
259
267
|
}
|
|
260
268
|
else {
|
|
261
269
|
logger.warn(`Intervention requested but stdin not available: ${message}`);
|
|
262
|
-
logger.warn('To enable intervention, set enableIntervention: true in config');
|
|
263
270
|
}
|
|
264
|
-
fs.unlinkSync(interventionPath);
|
|
271
|
+
fs.unlinkSync(interventionPath);
|
|
265
272
|
}
|
|
266
273
|
}
|
|
267
|
-
catch
|
|
268
|
-
logger.warn('Failed to read intervention file');
|
|
269
|
-
}
|
|
274
|
+
catch { }
|
|
270
275
|
}
|
|
271
|
-
// Handle dynamic timeout update
|
|
272
276
|
if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
|
|
273
277
|
try {
|
|
274
278
|
const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
|
|
275
279
|
const newTimeoutMs = parseInt(newTimeoutStr);
|
|
276
280
|
if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
|
|
277
281
|
logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
|
|
278
|
-
// Clear old timeout
|
|
279
282
|
if (timeoutHandle)
|
|
280
283
|
clearTimeout(timeoutHandle);
|
|
281
|
-
// Set new timeout based on total elapsed time
|
|
282
284
|
const elapsed = Date.now() - startTime;
|
|
283
285
|
const remaining = Math.max(1000, newTimeoutMs - elapsed);
|
|
284
286
|
timeoutHandle = setTimeout(() => {
|
|
285
287
|
clearInterval(heartbeatInterval);
|
|
286
288
|
child.kill();
|
|
287
|
-
|
|
288
|
-
resolve({
|
|
289
|
-
ok: false,
|
|
290
|
-
exitCode: -1,
|
|
291
|
-
error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
|
|
292
|
-
});
|
|
289
|
+
resolve({ ok: false, exitCode: -1, error: `cursor-agent timed out after updated limit.` });
|
|
293
290
|
}, remaining);
|
|
294
|
-
fs.unlinkSync(timeoutPath);
|
|
291
|
+
fs.unlinkSync(timeoutPath);
|
|
295
292
|
}
|
|
296
293
|
}
|
|
297
|
-
catch
|
|
298
|
-
logger.warn('Failed to read timeout update file');
|
|
299
|
-
}
|
|
294
|
+
catch { }
|
|
300
295
|
}
|
|
301
296
|
});
|
|
302
297
|
}
|
|
303
298
|
if (child.stdout) {
|
|
304
299
|
child.stdout.on('data', (data) => {
|
|
305
|
-
|
|
306
|
-
fullStdout += str;
|
|
300
|
+
fullStdout += data.toString();
|
|
307
301
|
bytesReceived += data.length;
|
|
308
|
-
// Also pipe to our own stdout so it goes to terminal.log
|
|
309
302
|
process.stdout.write(data);
|
|
310
303
|
});
|
|
311
304
|
}
|
|
312
305
|
if (child.stderr) {
|
|
313
306
|
child.stderr.on('data', (data) => {
|
|
314
307
|
fullStderr += data.toString();
|
|
315
|
-
// Pipe to our own stderr so it goes to terminal.log
|
|
316
308
|
process.stderr.write(data);
|
|
317
309
|
});
|
|
318
310
|
}
|
|
319
311
|
timeoutHandle = setTimeout(() => {
|
|
320
312
|
clearInterval(heartbeatInterval);
|
|
321
313
|
child.kill();
|
|
322
|
-
const timeoutSec = Math.round(timeoutMs / 1000);
|
|
323
314
|
resolve({
|
|
324
315
|
ok: false,
|
|
325
316
|
exitCode: -1,
|
|
326
|
-
error: `cursor-agent timed out after ${
|
|
317
|
+
error: `cursor-agent timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
|
|
327
318
|
});
|
|
328
319
|
}, timeoutMs);
|
|
329
320
|
child.on('close', (code) => {
|
|
@@ -334,21 +325,7 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
|
|
|
334
325
|
const json = parseJsonFromStdout(fullStdout);
|
|
335
326
|
if (code !== 0 || !json || json.type !== 'result') {
|
|
336
327
|
let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
|
|
337
|
-
|
|
338
|
-
if (errorMsg.includes('not authenticated') || errorMsg.includes('login') || errorMsg.includes('auth')) {
|
|
339
|
-
errorMsg = 'Authentication error. Please sign in to Cursor IDE.';
|
|
340
|
-
}
|
|
341
|
-
else if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
|
|
342
|
-
errorMsg = 'API rate limit or quota exceeded.';
|
|
343
|
-
}
|
|
344
|
-
else if (errorMsg.includes('model')) {
|
|
345
|
-
errorMsg = `Model error (requested: ${model || 'default'}). Check your subscription.`;
|
|
346
|
-
}
|
|
347
|
-
resolve({
|
|
348
|
-
ok: false,
|
|
349
|
-
exitCode: code ?? -1,
|
|
350
|
-
error: errorMsg,
|
|
351
|
-
});
|
|
328
|
+
resolve({ ok: false, exitCode: code ?? -1, error: errorMsg });
|
|
352
329
|
}
|
|
353
330
|
else {
|
|
354
331
|
resolve({
|
|
@@ -362,14 +339,17 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
|
|
|
362
339
|
child.on('error', (err) => {
|
|
363
340
|
clearTimeout(timeoutHandle);
|
|
364
341
|
clearInterval(heartbeatInterval);
|
|
365
|
-
resolve({
|
|
366
|
-
ok: false,
|
|
367
|
-
exitCode: -1,
|
|
368
|
-
error: `Failed to start cursor-agent: ${err.message}`,
|
|
369
|
-
});
|
|
342
|
+
resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
|
|
370
343
|
});
|
|
371
344
|
});
|
|
372
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Execute cursor-agent command with retries for transient errors
|
|
348
|
+
*/
|
|
349
|
+
async function cursorAgentSend(options) {
|
|
350
|
+
const laneName = options.signalDir ? path.basename(path.dirname(options.signalDir)) : 'agent';
|
|
351
|
+
return (0, failure_policy_1.withRetry)(laneName, () => cursorAgentSendRaw(options), (res) => ({ ok: res.ok, error: res.error }), { maxRetries: 3 });
|
|
352
|
+
}
|
|
373
353
|
/**
|
|
374
354
|
* Extract dependency change request from agent response
|
|
375
355
|
*/
|
|
@@ -396,27 +376,126 @@ function extractDependencyRequest(text) {
|
|
|
396
376
|
return { required: true, raw: t };
|
|
397
377
|
}
|
|
398
378
|
/**
|
|
399
|
-
*
|
|
379
|
+
* Inter-task state file name
|
|
380
|
+
*/
|
|
381
|
+
const LANE_STATE_FILE = '_cursorflow/lane-state.json';
|
|
382
|
+
/**
|
|
383
|
+
* Dependency request file name - agent writes here when dependency changes are needed
|
|
384
|
+
*/
|
|
385
|
+
const DEPENDENCY_REQUEST_FILE = '_cursorflow/dependency-request.json';
|
|
386
|
+
/**
|
|
387
|
+
* Read dependency request from file if it exists
|
|
400
388
|
*/
|
|
401
|
-
function
|
|
402
|
-
const
|
|
403
|
-
if (
|
|
389
|
+
function readDependencyRequestFile(worktreeDir) {
|
|
390
|
+
const filePath = (0, path_1.safeJoin)(worktreeDir, DEPENDENCY_REQUEST_FILE);
|
|
391
|
+
if (!fs.existsSync(filePath)) {
|
|
392
|
+
return { required: false };
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
396
|
+
const plan = JSON.parse(content);
|
|
397
|
+
// Validate required fields
|
|
398
|
+
if (plan.reason && Array.isArray(plan.commands) && plan.commands.length > 0) {
|
|
399
|
+
logger.info(`📦 Dependency request file detected: ${filePath}`);
|
|
400
|
+
return { required: true, plan };
|
|
401
|
+
}
|
|
402
|
+
logger.warn(`Invalid dependency request file format: ${filePath}`);
|
|
403
|
+
return { required: false };
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
logger.warn(`Failed to parse dependency request file: ${e}`);
|
|
407
|
+
return { required: false };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Clear dependency request file after processing
|
|
412
|
+
*/
|
|
413
|
+
function clearDependencyRequestFile(worktreeDir) {
|
|
414
|
+
const filePath = (0, path_1.safeJoin)(worktreeDir, DEPENDENCY_REQUEST_FILE);
|
|
415
|
+
if (fs.existsSync(filePath)) {
|
|
416
|
+
try {
|
|
417
|
+
fs.unlinkSync(filePath);
|
|
418
|
+
logger.info(`🗑️ Cleared dependency request file: ${filePath}`);
|
|
419
|
+
}
|
|
420
|
+
catch (e) {
|
|
421
|
+
logger.warn(`Failed to clear dependency request file: ${e}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Wrap prompt with dependency policy instructions (legacy, used by tests)
|
|
427
|
+
*/
|
|
428
|
+
function wrapPromptForDependencyPolicy(prompt, policy) {
|
|
429
|
+
if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
|
|
404
430
|
return prompt;
|
|
405
431
|
}
|
|
406
|
-
let
|
|
407
|
-
|
|
408
|
-
|
|
432
|
+
let wrapped = `### 📦 Dependency Policy\n`;
|
|
433
|
+
wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
434
|
+
wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n\n`;
|
|
435
|
+
wrapped += prompt;
|
|
436
|
+
return wrapped;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Wrap prompt with global context, dependency policy, and worktree instructions
|
|
440
|
+
*/
|
|
441
|
+
function wrapPrompt(prompt, config, options = {}) {
|
|
442
|
+
const { noGit = false, isWorktree = true, previousState = null } = options;
|
|
443
|
+
// 1. PREFIX: Environment & Worktree context
|
|
444
|
+
let wrapped = `### 🛠 Environment & Context\n`;
|
|
445
|
+
wrapped += `- **Workspace**: 당신은 독립된 **Git 워크트리** (프로젝트 루트)에서 작업 중입니다.\n`;
|
|
446
|
+
wrapped += `- **Path Rule**: 모든 파일 참조 및 터미널 명령어는 **현재 디렉토리(./)**를 기준으로 하세요.\n`;
|
|
447
|
+
if (isWorktree) {
|
|
448
|
+
wrapped += `- **File Availability**: Git 추적 파일만 존재합니다. (node_modules, .env 등은 기본적으로 없음)\n`;
|
|
449
|
+
}
|
|
450
|
+
// 2. Previous Task State (if available)
|
|
451
|
+
if (previousState) {
|
|
452
|
+
wrapped += `\n### 💡 Previous Task State\n`;
|
|
453
|
+
wrapped += `이전 태스크에서 전달된 상태 정보입니다:\n`;
|
|
454
|
+
wrapped += `\`\`\`json\n${previousState}\n\`\`\`\n`;
|
|
455
|
+
}
|
|
456
|
+
// 3. Dependency Policy (Integrated)
|
|
457
|
+
const policy = config.dependencyPolicy;
|
|
458
|
+
wrapped += `\n### 📦 Dependency Policy\n`;
|
|
459
|
+
wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
460
|
+
wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
|
|
409
461
|
if (noGit) {
|
|
410
|
-
|
|
462
|
+
wrapped += `- NO_GIT_MODE: Git 명령어를 사용하지 마세요. 파일 수정만 가능합니다.\n`;
|
|
463
|
+
}
|
|
464
|
+
wrapped += `\n**📦 Dependency Change Rules:**\n`;
|
|
465
|
+
wrapped += `1. 코드를 수정하기 전, 의존성 변경이 필요한지 **먼저** 판단하세요.\n`;
|
|
466
|
+
wrapped += `2. 의존성 변경이 필요하다면:\n`;
|
|
467
|
+
wrapped += ` - **다른 파일을 절대 수정하지 마세요.**\n`;
|
|
468
|
+
wrapped += ` - 아래 JSON을 \`./${DEPENDENCY_REQUEST_FILE}\` 파일에 저장하세요:\n`;
|
|
469
|
+
wrapped += ` \`\`\`json\n`;
|
|
470
|
+
wrapped += ` {\n`;
|
|
471
|
+
wrapped += ` "reason": "왜 이 의존성이 필요한지 설명",\n`;
|
|
472
|
+
wrapped += ` "changes": ["add lodash@^4.17.21", "remove unused-pkg"],\n`;
|
|
473
|
+
wrapped += ` "commands": ["pnpm add lodash@^4.17.21", "pnpm remove unused-pkg"],\n`;
|
|
474
|
+
wrapped += ` "notes": "추가 참고사항 (선택)" \n`;
|
|
475
|
+
wrapped += ` }\n`;
|
|
476
|
+
wrapped += ` \`\`\`\n`;
|
|
477
|
+
wrapped += ` - 파일 저장 후 **즉시 작업을 종료**하세요. 오케스트레이터가 처리합니다.\n`;
|
|
478
|
+
wrapped += `3. 의존성 변경이 불필요하면 바로 본 작업을 진행하세요.\n`;
|
|
479
|
+
wrapped += `\n---\n\n${prompt}\n\n---\n`;
|
|
480
|
+
// 4. SUFFIX: Task Completion & Git Requirements
|
|
481
|
+
wrapped += `\n### 📝 Task Completion Requirements\n`;
|
|
482
|
+
wrapped += `**반드시 다음 순서로 작업을 마무리하세요:**\n\n`;
|
|
483
|
+
if (!noGit) {
|
|
484
|
+
wrapped += `1. **Git Commit & Push** (필수!):\n`;
|
|
485
|
+
wrapped += ` \`\`\`bash\n`;
|
|
486
|
+
wrapped += ` git add -A\n`;
|
|
487
|
+
wrapped += ` git commit -m "feat: <작업 내용 요약>"\n`;
|
|
488
|
+
wrapped += ` git push origin HEAD\n`;
|
|
489
|
+
wrapped += ` \`\`\`\n`;
|
|
490
|
+
wrapped += ` ⚠️ 커밋과 푸시 없이 작업을 종료하면 변경사항이 손실됩니다!\n\n`;
|
|
411
491
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return `${rules}\n---\n\n${prompt}`;
|
|
492
|
+
wrapped += `2. **State Passing**: 다음 태스크로 전달할 정보가 있다면 \`./${LANE_STATE_FILE}\`에 JSON으로 저장하세요.\n\n`;
|
|
493
|
+
wrapped += `3. **Summary**: 작업 완료 후 다음을 요약해 주세요:\n`;
|
|
494
|
+
wrapped += ` - 생성/수정된 파일 목록\n`;
|
|
495
|
+
wrapped += ` - 주요 변경 사항\n`;
|
|
496
|
+
wrapped += ` - 커밋 해시 (git log --oneline -1)\n\n`;
|
|
497
|
+
wrapped += `4. 지시된 문서(docs/...)를 찾을 수 없다면 즉시 보고하세요.\n`;
|
|
498
|
+
return wrapped;
|
|
420
499
|
}
|
|
421
500
|
/**
|
|
422
501
|
* Apply file permissions based on dependency policy
|
|
@@ -430,7 +509,7 @@ function applyDependencyFilePermissions(worktreeDir, policy) {
|
|
|
430
509
|
targets.push('pnpm-lock.yaml', 'package-lock.json', 'yarn.lock');
|
|
431
510
|
}
|
|
432
511
|
for (const file of targets) {
|
|
433
|
-
const filePath =
|
|
512
|
+
const filePath = (0, path_1.safeJoin)(worktreeDir, file);
|
|
434
513
|
if (!fs.existsSync(filePath))
|
|
435
514
|
continue;
|
|
436
515
|
try {
|
|
@@ -445,47 +524,31 @@ function applyDependencyFilePermissions(worktreeDir, policy) {
|
|
|
445
524
|
}
|
|
446
525
|
/**
|
|
447
526
|
* Wait for task-level dependencies to be completed by other lanes
|
|
527
|
+
* Now uses the enhanced dependency module with timeout support
|
|
448
528
|
*/
|
|
449
|
-
async function waitForTaskDependencies(deps, runDir) {
|
|
529
|
+
async function waitForTaskDependencies(deps, runDir, options = {}) {
|
|
450
530
|
if (!deps || deps.length === 0)
|
|
451
531
|
return;
|
|
452
532
|
const lanesRoot = path.dirname(runDir);
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (
|
|
459
|
-
logger.
|
|
460
|
-
pendingDeps.delete(dep);
|
|
461
|
-
continue;
|
|
533
|
+
const result = await (0, dependency_1.waitForTaskDependencies)(deps, lanesRoot, {
|
|
534
|
+
timeoutMs: options.timeoutMs || 30 * 60 * 1000, // 30 minutes default
|
|
535
|
+
pollIntervalMs: options.pollIntervalMs || 5000,
|
|
536
|
+
onTimeout: options.onTimeout || 'fail',
|
|
537
|
+
onProgress: (pending, completed) => {
|
|
538
|
+
if (completed.length > 0) {
|
|
539
|
+
logger.info(`Dependencies progress: ${completed.length}/${deps.length} completed`);
|
|
462
540
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
logger.info(`✓ Dependency met: ${dep}`);
|
|
469
|
-
pendingDeps.delete(dep);
|
|
470
|
-
}
|
|
471
|
-
else if (state.status === 'failed') {
|
|
472
|
-
throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
catch (e) {
|
|
476
|
-
if (e.message.includes('Dependency failed'))
|
|
477
|
-
throw e;
|
|
478
|
-
// Ignore parse errors, file might be being written
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
if (pendingDeps.size > 0) {
|
|
483
|
-
await new Promise(resolve => setTimeout(resolve, 5000)); // Poll every 5 seconds
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
if (!result.success) {
|
|
544
|
+
if (result.timedOut) {
|
|
545
|
+
throw new Error(`Dependency wait timed out after ${Math.round(result.elapsedMs / 1000)}s. Pending: ${result.failedDependencies.join(', ')}`);
|
|
484
546
|
}
|
|
547
|
+
throw new Error(`Dependencies failed: ${result.failedDependencies.join(', ')}`);
|
|
485
548
|
}
|
|
486
549
|
}
|
|
487
550
|
/**
|
|
488
|
-
* Merge branches from dependency lanes
|
|
551
|
+
* Merge branches from dependency lanes with safe merge
|
|
489
552
|
*/
|
|
490
553
|
async function mergeDependencyBranches(deps, runDir, worktreeDir) {
|
|
491
554
|
if (!deps || deps.length === 0)
|
|
@@ -493,24 +556,35 @@ async function mergeDependencyBranches(deps, runDir, worktreeDir) {
|
|
|
493
556
|
const lanesRoot = path.dirname(runDir);
|
|
494
557
|
const lanesToMerge = new Set(deps.map(d => d.split(':')[0]));
|
|
495
558
|
for (const laneName of lanesToMerge) {
|
|
496
|
-
const depStatePath =
|
|
559
|
+
const depStatePath = (0, path_1.safeJoin)(lanesRoot, laneName, 'state.json');
|
|
497
560
|
if (!fs.existsSync(depStatePath))
|
|
498
561
|
continue;
|
|
499
562
|
try {
|
|
500
|
-
const state =
|
|
501
|
-
if (state
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
563
|
+
const state = (0, state_1.loadState)(depStatePath);
|
|
564
|
+
if (!state?.pipelineBranch)
|
|
565
|
+
continue;
|
|
566
|
+
logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
|
|
567
|
+
// Ensure we have the latest
|
|
568
|
+
git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
569
|
+
// Use safe merge with conflict detection
|
|
570
|
+
const mergeResult = git.safeMerge(state.pipelineBranch, {
|
|
571
|
+
cwd: worktreeDir,
|
|
572
|
+
noFf: true,
|
|
573
|
+
message: `chore: merge task dependency from ${laneName}`,
|
|
574
|
+
abortOnConflict: true,
|
|
575
|
+
});
|
|
576
|
+
if (!mergeResult.success) {
|
|
577
|
+
if (mergeResult.conflict) {
|
|
578
|
+
logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
579
|
+
throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
580
|
+
}
|
|
581
|
+
throw new Error(mergeResult.error || 'Merge failed');
|
|
510
582
|
}
|
|
583
|
+
logger.success(`✓ Merged ${laneName}`);
|
|
511
584
|
}
|
|
512
585
|
catch (e) {
|
|
513
586
|
logger.error(`Failed to merge branch from ${laneName}: ${e}`);
|
|
587
|
+
throw e;
|
|
514
588
|
}
|
|
515
589
|
}
|
|
516
590
|
}
|
|
@@ -520,7 +594,7 @@ async function mergeDependencyBranches(deps, runDir, worktreeDir) {
|
|
|
520
594
|
async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskBranch, chatId, runDir, noGit = false, }) {
|
|
521
595
|
const model = task.model || config.model || 'sonnet-4.5';
|
|
522
596
|
const timeout = task.timeout || config.timeout;
|
|
523
|
-
const convoPath =
|
|
597
|
+
const convoPath = (0, path_1.safeJoin)(runDir, 'conversation.jsonl');
|
|
524
598
|
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
525
599
|
logger.info(`Model: ${model}`);
|
|
526
600
|
if (noGit) {
|
|
@@ -540,9 +614,26 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
|
|
|
540
614
|
}
|
|
541
615
|
// Apply dependency permissions
|
|
542
616
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
617
|
+
// Read previous task state if available
|
|
618
|
+
let previousState = null;
|
|
619
|
+
const stateFilePath = (0, path_1.safeJoin)(worktreeDir, LANE_STATE_FILE);
|
|
620
|
+
if (fs.existsSync(stateFilePath)) {
|
|
621
|
+
try {
|
|
622
|
+
previousState = fs.readFileSync(stateFilePath, 'utf8');
|
|
623
|
+
logger.info('Loaded previous task state from _cursorflow/lane-state.json');
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
logger.warn(`Failed to read inter-task state: ${e}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// Wrap prompt with context, previous state, and completion instructions
|
|
630
|
+
const wrappedPrompt = wrapPrompt(task.prompt, config, {
|
|
631
|
+
noGit,
|
|
632
|
+
isWorktree: !noGit,
|
|
633
|
+
previousState
|
|
634
|
+
});
|
|
635
|
+
// Log ONLY the original prompt to keep logs clean
|
|
636
|
+
(0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('user', task.prompt, {
|
|
546
637
|
task: task.name,
|
|
547
638
|
model,
|
|
548
639
|
}));
|
|
@@ -551,17 +642,18 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
|
|
|
551
642
|
events_1.events.emit('agent.prompt_sent', {
|
|
552
643
|
taskName: task.name,
|
|
553
644
|
model,
|
|
554
|
-
promptLength:
|
|
645
|
+
promptLength: wrappedPrompt.length,
|
|
555
646
|
});
|
|
556
647
|
const r1 = await cursorAgentSend({
|
|
557
648
|
workspaceDir: worktreeDir,
|
|
558
649
|
chatId,
|
|
559
|
-
prompt:
|
|
650
|
+
prompt: wrappedPrompt,
|
|
560
651
|
model,
|
|
561
652
|
signalDir: runDir,
|
|
562
653
|
timeout,
|
|
563
654
|
enableIntervention: config.enableIntervention,
|
|
564
655
|
outputFormat: config.agentOutputFormat,
|
|
656
|
+
taskName: task.name,
|
|
565
657
|
});
|
|
566
658
|
const duration = Date.now() - startTime;
|
|
567
659
|
events_1.events.emit('agent.response_received', {
|
|
@@ -588,15 +680,26 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
|
|
|
588
680
|
error: r1.error,
|
|
589
681
|
};
|
|
590
682
|
}
|
|
591
|
-
// Check for dependency request
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
683
|
+
// Check for dependency request (file-based takes priority, then text-based)
|
|
684
|
+
const fileDepReq = readDependencyRequestFile(worktreeDir);
|
|
685
|
+
const textDepReq = extractDependencyRequest(r1.resultText || '');
|
|
686
|
+
// Determine which request to use (file-based is preferred as it's more structured)
|
|
687
|
+
const depReq = fileDepReq.required ? fileDepReq : textDepReq;
|
|
688
|
+
if (depReq.required) {
|
|
689
|
+
logger.info(`📦 Dependency change requested: ${depReq.plan?.reason || 'No reason provided'}`);
|
|
690
|
+
if (depReq.plan) {
|
|
691
|
+
logger.info(` Commands: ${depReq.plan.commands.join(', ')}`);
|
|
692
|
+
}
|
|
693
|
+
if (!config.dependencyPolicy.allowDependencyChange) {
|
|
694
|
+
// Clear the file so it doesn't persist after resolution
|
|
695
|
+
clearDependencyRequestFile(worktreeDir);
|
|
696
|
+
return {
|
|
697
|
+
taskName: task.name,
|
|
698
|
+
taskBranch,
|
|
699
|
+
status: 'BLOCKED_DEPENDENCY',
|
|
700
|
+
dependencyRequest: depReq.plan || null,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
600
703
|
}
|
|
601
704
|
// Push task branch (skip in noGit mode)
|
|
602
705
|
if (!noGit) {
|
|
@@ -661,6 +764,28 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
661
764
|
logger.error(` ${validationError.message}`);
|
|
662
765
|
throw validationError;
|
|
663
766
|
}
|
|
767
|
+
// Run preflight checks (can be skipped for resume)
|
|
768
|
+
if (!options.skipPreflight && startIndex === 0) {
|
|
769
|
+
logger.info('Running preflight checks...');
|
|
770
|
+
const preflight = await (0, health_1.preflightCheck)({
|
|
771
|
+
requireRemote: !noGit,
|
|
772
|
+
requireAuth: true,
|
|
773
|
+
});
|
|
774
|
+
if (!preflight.canProceed) {
|
|
775
|
+
(0, health_1.printPreflightReport)(preflight);
|
|
776
|
+
throw new Error('Preflight check failed. Please fix the blockers above.');
|
|
777
|
+
}
|
|
778
|
+
if (preflight.warnings.length > 0) {
|
|
779
|
+
for (const warning of preflight.warnings) {
|
|
780
|
+
logger.warn(`⚠️ ${warning}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
logger.success('✓ Preflight checks passed');
|
|
784
|
+
}
|
|
785
|
+
// Warn if baseBranch is set in config (it will be ignored)
|
|
786
|
+
if (config.baseBranch) {
|
|
787
|
+
logger.warn(`⚠️ config.baseBranch="${config.baseBranch}" will be ignored. Using current branch instead.`);
|
|
788
|
+
}
|
|
664
789
|
// Ensure cursor-agent is installed
|
|
665
790
|
(0, cursor_agent_1.ensureCursorAgent)();
|
|
666
791
|
// Check authentication before starting
|
|
@@ -681,24 +806,53 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
681
806
|
}
|
|
682
807
|
logger.success('✓ Cursor authentication OK');
|
|
683
808
|
// In noGit mode, we don't need repoRoot - use current directory
|
|
684
|
-
const repoRoot = noGit ? process.cwd() : git.
|
|
809
|
+
const repoRoot = noGit ? process.cwd() : git.getMainRepoRoot();
|
|
810
|
+
// ALWAYS use current branch as base - ignore config.baseBranch
|
|
811
|
+
// This ensures dependency structure is maintained in the worktree
|
|
812
|
+
const currentBranch = noGit ? 'main' : git.getCurrentBranch(repoRoot);
|
|
813
|
+
logger.info(`📍 Base branch: ${currentBranch} (current branch)`);
|
|
685
814
|
// Load existing state if resuming
|
|
686
|
-
const statePath =
|
|
815
|
+
const statePath = (0, path_1.safeJoin)(runDir, 'state.json');
|
|
687
816
|
let state = null;
|
|
688
817
|
if (fs.existsSync(statePath)) {
|
|
689
|
-
|
|
690
|
-
|
|
818
|
+
// Check if state needs recovery
|
|
819
|
+
if ((0, state_1.stateNeedsRecovery)(statePath)) {
|
|
820
|
+
logger.warn('State file indicates incomplete previous run. Attempting recovery...');
|
|
821
|
+
const repairedState = (0, state_1.repairLaneState)(statePath);
|
|
822
|
+
if (repairedState) {
|
|
823
|
+
state = repairedState;
|
|
824
|
+
logger.success('✓ State recovered');
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
logger.warn('Could not recover state. Starting fresh.');
|
|
828
|
+
}
|
|
691
829
|
}
|
|
692
|
-
|
|
693
|
-
|
|
830
|
+
else {
|
|
831
|
+
state = (0, state_1.loadState)(statePath);
|
|
832
|
+
// Validate loaded state
|
|
833
|
+
if (state) {
|
|
834
|
+
const validation = (0, state_1.validateLaneState)(statePath, {
|
|
835
|
+
checkWorktree: !noGit,
|
|
836
|
+
checkBranch: !noGit,
|
|
837
|
+
autoRepair: true,
|
|
838
|
+
});
|
|
839
|
+
if (!validation.valid) {
|
|
840
|
+
logger.warn(`State validation issues: ${validation.issues.join(', ')}`);
|
|
841
|
+
if (validation.repaired) {
|
|
842
|
+
logger.info('State was auto-repaired');
|
|
843
|
+
state = validation.repairedState || state;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
694
847
|
}
|
|
695
848
|
}
|
|
696
849
|
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
697
850
|
const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
|
|
698
851
|
// In noGit mode, use a simple local directory instead of worktree
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
852
|
+
// Flatten the path by replacing slashes with hyphens to avoid race conditions in parent directory creation
|
|
853
|
+
const worktreeDir = state?.worktreeDir || config.worktreeDir || (noGit
|
|
854
|
+
? (0, path_1.safeJoin)(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
|
|
855
|
+
: (0, path_1.safeJoin)(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch.replace(/\//g, '-')));
|
|
702
856
|
if (startIndex === 0) {
|
|
703
857
|
logger.section('🚀 Starting Pipeline');
|
|
704
858
|
}
|
|
@@ -716,10 +870,36 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
716
870
|
fs.mkdirSync(worktreeDir, { recursive: true });
|
|
717
871
|
}
|
|
718
872
|
else {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
873
|
+
// Use a simple retry mechanism for Git worktree creation to handle potential race conditions
|
|
874
|
+
let retries = 3;
|
|
875
|
+
let lastError = null;
|
|
876
|
+
while (retries > 0) {
|
|
877
|
+
try {
|
|
878
|
+
// Ensure parent directory exists before calling git worktree
|
|
879
|
+
const worktreeParent = path.dirname(worktreeDir);
|
|
880
|
+
if (!fs.existsSync(worktreeParent)) {
|
|
881
|
+
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
882
|
+
}
|
|
883
|
+
// Always use the current branch (already captured at start) as the base branch
|
|
884
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
885
|
+
baseBranch: currentBranch,
|
|
886
|
+
cwd: repoRoot,
|
|
887
|
+
});
|
|
888
|
+
break; // Success
|
|
889
|
+
}
|
|
890
|
+
catch (e) {
|
|
891
|
+
lastError = e;
|
|
892
|
+
retries--;
|
|
893
|
+
if (retries > 0) {
|
|
894
|
+
const delay = Math.floor(Math.random() * 1000) + 500;
|
|
895
|
+
logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
|
|
896
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (retries === 0 && lastError) {
|
|
901
|
+
throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
|
|
902
|
+
}
|
|
723
903
|
}
|
|
724
904
|
}
|
|
725
905
|
else if (!noGit) {
|
|
@@ -759,6 +939,9 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
759
939
|
state.status = 'running';
|
|
760
940
|
state.error = null;
|
|
761
941
|
state.dependencyRequest = null;
|
|
942
|
+
state.pipelineBranch = pipelineBranch;
|
|
943
|
+
state.worktreeDir = worktreeDir;
|
|
944
|
+
state.label = state.label || pipelineBranch;
|
|
762
945
|
state.dependsOn = config.dependsOn || [];
|
|
763
946
|
state.completedTasks = state.completedTasks || [];
|
|
764
947
|
}
|
|
@@ -769,8 +952,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
769
952
|
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
770
953
|
const lanesRoot = path.dirname(runDir);
|
|
771
954
|
for (const depName of config.dependsOn) {
|
|
772
|
-
const depRunDir = path.join(lanesRoot, depName);
|
|
773
|
-
const depStatePath = path.join(depRunDir, 'state.json');
|
|
955
|
+
const depRunDir = path.join(lanesRoot, depName); // nosemgrep
|
|
956
|
+
const depStatePath = path.join(depRunDir, 'state.json'); // nosemgrep
|
|
774
957
|
if (!fs.existsSync(depStatePath)) {
|
|
775
958
|
logger.warn(`Dependency state not found for ${depName} at ${depStatePath}`);
|
|
776
959
|
continue;
|
|
@@ -809,8 +992,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
809
992
|
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
810
993
|
const lanesRoot = path.dirname(runDir);
|
|
811
994
|
for (const depName of config.dependsOn) {
|
|
812
|
-
const depRunDir =
|
|
813
|
-
const depStatePath =
|
|
995
|
+
const depRunDir = (0, path_1.safeJoin)(lanesRoot, depName);
|
|
996
|
+
const depStatePath = (0, path_1.safeJoin)(depRunDir, 'state.json');
|
|
814
997
|
if (!fs.existsSync(depStatePath)) {
|
|
815
998
|
continue;
|
|
816
999
|
}
|
|
@@ -826,8 +1009,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
826
1009
|
for (const entry of entries) {
|
|
827
1010
|
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules')
|
|
828
1011
|
continue;
|
|
829
|
-
const srcPath =
|
|
830
|
-
const destPath =
|
|
1012
|
+
const srcPath = (0, path_1.safeJoin)(src, entry.name);
|
|
1013
|
+
const destPath = (0, path_1.safeJoin)(dest, entry.name);
|
|
831
1014
|
if (entry.isDirectory()) {
|
|
832
1015
|
copyFiles(srcPath, destPath);
|
|
833
1016
|
}
|
|
@@ -846,16 +1029,31 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
846
1029
|
}
|
|
847
1030
|
// Run tasks
|
|
848
1031
|
const results = [];
|
|
1032
|
+
const laneName = state.label || path.basename(runDir);
|
|
849
1033
|
for (let i = startIndex; i < config.tasks.length; i++) {
|
|
850
1034
|
const task = config.tasks[i];
|
|
851
1035
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
1036
|
+
// Create checkpoint before each task
|
|
1037
|
+
try {
|
|
1038
|
+
await (0, checkpoint_1.createCheckpoint)(laneName, runDir, noGit ? null : worktreeDir, {
|
|
1039
|
+
description: `Before task ${i + 1}: ${task.name}`,
|
|
1040
|
+
maxCheckpoints: 5,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
catch (e) {
|
|
1044
|
+
logger.warn(`Failed to create checkpoint: ${e.message}`);
|
|
1045
|
+
}
|
|
852
1046
|
// Handle task-level dependencies
|
|
853
1047
|
if (task.dependsOn && task.dependsOn.length > 0) {
|
|
854
1048
|
state.status = 'waiting';
|
|
855
1049
|
state.waitingFor = task.dependsOn;
|
|
856
1050
|
(0, state_1.saveState)(statePath, state);
|
|
857
1051
|
try {
|
|
858
|
-
|
|
1052
|
+
// Use enhanced dependency wait with timeout
|
|
1053
|
+
await waitForTaskDependencies(task.dependsOn, runDir, {
|
|
1054
|
+
timeoutMs: config.timeout || 30 * 60 * 1000,
|
|
1055
|
+
onTimeout: 'fail',
|
|
1056
|
+
});
|
|
859
1057
|
if (!noGit) {
|
|
860
1058
|
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
|
|
861
1059
|
}
|
|
@@ -869,6 +1067,12 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
869
1067
|
state.error = e.message;
|
|
870
1068
|
(0, state_1.saveState)(statePath, state);
|
|
871
1069
|
logger.error(`Task dependency wait/merge failed: ${e.message}`);
|
|
1070
|
+
// Try to restore from checkpoint
|
|
1071
|
+
const latestCheckpoint = (0, checkpoint_1.getLatestCheckpoint)(runDir);
|
|
1072
|
+
if (latestCheckpoint) {
|
|
1073
|
+
logger.info(`💾 Checkpoint available: ${latestCheckpoint.id}`);
|
|
1074
|
+
logger.info(` Resume with: cursorflow resume --checkpoint ${latestCheckpoint.id}`);
|
|
1075
|
+
}
|
|
872
1076
|
process.exit(1);
|
|
873
1077
|
}
|
|
874
1078
|
}
|
|
@@ -943,7 +1147,7 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
943
1147
|
continue;
|
|
944
1148
|
if (entry.isDirectory()) {
|
|
945
1149
|
stats.dirs++;
|
|
946
|
-
const sub = getFileSummary(
|
|
1150
|
+
const sub = getFileSummary((0, path_1.safeJoin)(dir, entry.name));
|
|
947
1151
|
stats.files += sub.files;
|
|
948
1152
|
stats.dirs += sub.dirs;
|
|
949
1153
|
}
|
|
@@ -958,7 +1162,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
958
1162
|
}
|
|
959
1163
|
else {
|
|
960
1164
|
try {
|
|
961
|
-
|
|
1165
|
+
// Always use current branch for comparison (already captured at start)
|
|
1166
|
+
const stats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
|
|
962
1167
|
if (stats) {
|
|
963
1168
|
logger.info('Final Workspace Summary (Git):\n' + stats);
|
|
964
1169
|
}
|
|
@@ -983,10 +1188,12 @@ if (require.main === module) {
|
|
|
983
1188
|
const runDirIdx = args.indexOf('--run-dir');
|
|
984
1189
|
const startIdxIdx = args.indexOf('--start-index');
|
|
985
1190
|
const pipelineBranchIdx = args.indexOf('--pipeline-branch');
|
|
1191
|
+
const worktreeDirIdx = args.indexOf('--worktree-dir');
|
|
986
1192
|
const noGit = args.includes('--no-git');
|
|
987
1193
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1] : '.';
|
|
988
1194
|
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
989
1195
|
const forcedPipelineBranch = pipelineBranchIdx >= 0 ? args[pipelineBranchIdx + 1] : null;
|
|
1196
|
+
const forcedWorktreeDir = worktreeDirIdx >= 0 ? args[worktreeDirIdx + 1] : null;
|
|
990
1197
|
// Extract runId from runDir (format: .../runs/run-123/lanes/lane-name)
|
|
991
1198
|
const parts = runDir.split(path.sep);
|
|
992
1199
|
const runsIdx = parts.lastIndexOf('runs');
|
|
@@ -1014,6 +1221,9 @@ if (require.main === module) {
|
|
|
1014
1221
|
if (forcedPipelineBranch) {
|
|
1015
1222
|
config.pipelineBranch = forcedPipelineBranch;
|
|
1016
1223
|
}
|
|
1224
|
+
if (forcedWorktreeDir) {
|
|
1225
|
+
config.worktreeDir = forcedWorktreeDir;
|
|
1226
|
+
}
|
|
1017
1227
|
}
|
|
1018
1228
|
catch (error) {
|
|
1019
1229
|
console.error(`Failed to load tasks file: ${error.message}`);
|