@litmers/cursorflow-orchestrator 0.1.20 → 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 +9 -0
- 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 +171 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +83 -42
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -0
- package/dist/cli/monitor.js +1007 -189
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +4 -3
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +188 -236
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +8 -3
- 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 +1 -1
- 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 +15 -2
- package/dist/core/orchestrator.js +392 -15
- 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 +321 -146
- 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 +11 -2
- 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 +10 -5
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +10 -33
- package/dist/utils/enhanced-logger.js +94 -9
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +121 -0
- package/dist/utils/git.js +322 -2
- 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 +9 -0
- package/dist/utils/log-formatter.js +113 -70
- package/dist/utils/log-formatter.js.map +1 -1
- 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/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 +58 -2
- package/dist/utils/state.js +306 -3
- 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/types.d.ts +2 -272
- 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 +180 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/init.ts +1 -1
- package/src/cli/logs.ts +79 -42
- package/src/cli/monitor.ts +1815 -899
- package/src/cli/prepare.ts +4 -3
- package/src/cli/resume.ts +220 -277
- package/src/cli/run.ts +9 -3
- package/src/cli/runs.ts +212 -0
- package/src/cli/setup-commands.ts +0 -0
- package/src/cli/signal.ts +1 -1
- 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 -675
- package/src/core/reviewer.ts +4 -0
- package/src/core/runner.ts +388 -162
- 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 +11 -2
- package/src/utils/cursor-agent.ts +1 -1
- package/src/utils/dependency.ts +482 -0
- package/src/utils/doctor.ts +11 -5
- package/src/utils/enhanced-logger.ts +108 -49
- package/src/utils/git.ts +374 -2
- 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 +120 -37
- package/src/utils/log-service.ts +49 -0
- package/src/utils/logger.ts +100 -51
- 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 +369 -3
- package/src/utils/task-service.ts +370 -0
- package/src/utils/types.ts +2 -315
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;
|
|
@@ -60,6 +67,10 @@ const config_1 = require("../utils/config");
|
|
|
60
67
|
const webhook_1 = require("../utils/webhook");
|
|
61
68
|
const reviewer_1 = require("./reviewer");
|
|
62
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");
|
|
63
74
|
/**
|
|
64
75
|
* Execute cursor-agent command with timeout and better error handling
|
|
65
76
|
*/
|
|
@@ -175,9 +186,9 @@ function validateTaskConfig(config) {
|
|
|
175
186
|
}
|
|
176
187
|
}
|
|
177
188
|
/**
|
|
178
|
-
* Execute cursor-agent command with streaming
|
|
189
|
+
* Internal: Execute cursor-agent command with streaming
|
|
179
190
|
*/
|
|
180
|
-
async function
|
|
191
|
+
async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat, taskName }) {
|
|
181
192
|
// Use stream-json format for structured output with tool calls and results
|
|
182
193
|
const format = outputFormat || 'stream-json';
|
|
183
194
|
const args = [
|
|
@@ -191,65 +202,53 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
|
|
|
191
202
|
prompt,
|
|
192
203
|
];
|
|
193
204
|
const timeoutMs = timeout || DEFAULT_TIMEOUT_MS;
|
|
194
|
-
logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
|
|
195
205
|
// Determine stdio mode based on intervention setting
|
|
196
|
-
// When intervention is enabled, we pipe stdin for message injection
|
|
197
|
-
// When disabled (default), we ignore stdin to avoid buffering issues
|
|
198
206
|
const stdinMode = enableIntervention ? 'pipe' : 'ignore';
|
|
199
|
-
if (enableIntervention) {
|
|
200
|
-
logger.info('Intervention mode enabled (stdin piped)');
|
|
201
|
-
}
|
|
202
207
|
return new Promise((resolve) => {
|
|
203
208
|
// Build environment, preserving user's NODE_OPTIONS but disabling problematic flags
|
|
204
209
|
const childEnv = { ...process.env };
|
|
205
|
-
// Only filter out specific problematic NODE_OPTIONS, don't clear entirely
|
|
206
210
|
if (childEnv.NODE_OPTIONS) {
|
|
207
|
-
// Remove flags that might interfere with cursor-agent
|
|
208
211
|
const filtered = childEnv.NODE_OPTIONS
|
|
209
212
|
.split(' ')
|
|
210
213
|
.filter(opt => !opt.includes('--inspect') && !opt.includes('--debug'))
|
|
211
214
|
.join(' ');
|
|
212
215
|
childEnv.NODE_OPTIONS = filtered;
|
|
213
216
|
}
|
|
214
|
-
// Disable Python buffering in case cursor-agent uses Python
|
|
215
217
|
childEnv.PYTHONUNBUFFERED = '1';
|
|
216
218
|
const child = (0, child_process_1.spawn)('cursor-agent', args, {
|
|
217
219
|
stdio: [stdinMode, 'pipe', 'pipe'],
|
|
218
220
|
env: childEnv,
|
|
219
221
|
});
|
|
220
|
-
|
|
221
|
-
// Save PID to state if possible (avoid TOCTOU by reading directly)
|
|
222
|
+
// Save PID to state if possible
|
|
222
223
|
if (child.pid && signalDir) {
|
|
223
224
|
try {
|
|
224
225
|
const statePath = (0, path_1.safeJoin)(signalDir, 'state.json');
|
|
225
|
-
// Read directly without existence check to avoid race condition
|
|
226
226
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
227
227
|
state.pid = child.pid;
|
|
228
228
|
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
229
229
|
}
|
|
230
230
|
catch {
|
|
231
|
-
// Best effort
|
|
231
|
+
// Best effort
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
let fullStdout = '';
|
|
235
235
|
let fullStderr = '';
|
|
236
236
|
let timeoutHandle;
|
|
237
|
-
// Heartbeat logging
|
|
237
|
+
// Heartbeat logging
|
|
238
238
|
let lastHeartbeat = Date.now();
|
|
239
239
|
let bytesReceived = 0;
|
|
240
|
+
const startTime = Date.now();
|
|
240
241
|
const heartbeatInterval = setInterval(() => {
|
|
241
|
-
const elapsed = Math.round((Date.now() - lastHeartbeat) / 1000);
|
|
242
242
|
const totalElapsed = Math.round((Date.now() - startTime) / 1000);
|
|
243
|
-
|
|
243
|
+
// Output without timestamp - orchestrator will add it
|
|
244
|
+
console.log(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
|
|
244
245
|
}, HEARTBEAT_INTERVAL_MS);
|
|
245
|
-
|
|
246
|
-
// Watch for "intervention.txt" or "timeout.txt" signal files
|
|
246
|
+
// Signal watchers (intervention, timeout)
|
|
247
247
|
const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
|
|
248
248
|
const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
|
|
249
249
|
let signalWatcher = null;
|
|
250
250
|
if (signalDir && fs.existsSync(signalDir)) {
|
|
251
251
|
signalWatcher = fs.watch(signalDir, (event, filename) => {
|
|
252
|
-
// Handle intervention
|
|
253
252
|
if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
|
|
254
253
|
try {
|
|
255
254
|
const message = fs.readFileSync(interventionPath, 'utf8').trim();
|
|
@@ -257,74 +256,65 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
|
|
|
257
256
|
if (enableIntervention && child.stdin) {
|
|
258
257
|
logger.info(`Injecting intervention: ${message}`);
|
|
259
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
|
+
}
|
|
260
267
|
}
|
|
261
268
|
else {
|
|
262
269
|
logger.warn(`Intervention requested but stdin not available: ${message}`);
|
|
263
|
-
logger.warn('To enable intervention, set enableIntervention: true in config');
|
|
264
270
|
}
|
|
265
|
-
fs.unlinkSync(interventionPath);
|
|
271
|
+
fs.unlinkSync(interventionPath);
|
|
266
272
|
}
|
|
267
273
|
}
|
|
268
|
-
catch
|
|
269
|
-
logger.warn('Failed to read intervention file');
|
|
270
|
-
}
|
|
274
|
+
catch { }
|
|
271
275
|
}
|
|
272
|
-
// Handle dynamic timeout update
|
|
273
276
|
if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
|
|
274
277
|
try {
|
|
275
278
|
const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
|
|
276
279
|
const newTimeoutMs = parseInt(newTimeoutStr);
|
|
277
280
|
if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
|
|
278
281
|
logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
|
|
279
|
-
// Clear old timeout
|
|
280
282
|
if (timeoutHandle)
|
|
281
283
|
clearTimeout(timeoutHandle);
|
|
282
|
-
// Set new timeout based on total elapsed time
|
|
283
284
|
const elapsed = Date.now() - startTime;
|
|
284
285
|
const remaining = Math.max(1000, newTimeoutMs - elapsed);
|
|
285
286
|
timeoutHandle = setTimeout(() => {
|
|
286
287
|
clearInterval(heartbeatInterval);
|
|
287
288
|
child.kill();
|
|
288
|
-
|
|
289
|
-
resolve({
|
|
290
|
-
ok: false,
|
|
291
|
-
exitCode: -1,
|
|
292
|
-
error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
|
|
293
|
-
});
|
|
289
|
+
resolve({ ok: false, exitCode: -1, error: `cursor-agent timed out after updated limit.` });
|
|
294
290
|
}, remaining);
|
|
295
|
-
fs.unlinkSync(timeoutPath);
|
|
291
|
+
fs.unlinkSync(timeoutPath);
|
|
296
292
|
}
|
|
297
293
|
}
|
|
298
|
-
catch
|
|
299
|
-
logger.warn('Failed to read timeout update file');
|
|
300
|
-
}
|
|
294
|
+
catch { }
|
|
301
295
|
}
|
|
302
296
|
});
|
|
303
297
|
}
|
|
304
298
|
if (child.stdout) {
|
|
305
299
|
child.stdout.on('data', (data) => {
|
|
306
|
-
|
|
307
|
-
fullStdout += str;
|
|
300
|
+
fullStdout += data.toString();
|
|
308
301
|
bytesReceived += data.length;
|
|
309
|
-
// Also pipe to our own stdout so it goes to terminal.log
|
|
310
302
|
process.stdout.write(data);
|
|
311
303
|
});
|
|
312
304
|
}
|
|
313
305
|
if (child.stderr) {
|
|
314
306
|
child.stderr.on('data', (data) => {
|
|
315
307
|
fullStderr += data.toString();
|
|
316
|
-
// Pipe to our own stderr so it goes to terminal.log
|
|
317
308
|
process.stderr.write(data);
|
|
318
309
|
});
|
|
319
310
|
}
|
|
320
311
|
timeoutHandle = setTimeout(() => {
|
|
321
312
|
clearInterval(heartbeatInterval);
|
|
322
313
|
child.kill();
|
|
323
|
-
const timeoutSec = Math.round(timeoutMs / 1000);
|
|
324
314
|
resolve({
|
|
325
315
|
ok: false,
|
|
326
316
|
exitCode: -1,
|
|
327
|
-
error: `cursor-agent timed out after ${
|
|
317
|
+
error: `cursor-agent timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
|
|
328
318
|
});
|
|
329
319
|
}, timeoutMs);
|
|
330
320
|
child.on('close', (code) => {
|
|
@@ -335,21 +325,7 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
|
|
|
335
325
|
const json = parseJsonFromStdout(fullStdout);
|
|
336
326
|
if (code !== 0 || !json || json.type !== 'result') {
|
|
337
327
|
let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
|
|
338
|
-
|
|
339
|
-
if (errorMsg.includes('not authenticated') || errorMsg.includes('login') || errorMsg.includes('auth')) {
|
|
340
|
-
errorMsg = 'Authentication error. Please sign in to Cursor IDE.';
|
|
341
|
-
}
|
|
342
|
-
else if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
|
|
343
|
-
errorMsg = 'API rate limit or quota exceeded.';
|
|
344
|
-
}
|
|
345
|
-
else if (errorMsg.includes('model')) {
|
|
346
|
-
errorMsg = `Model error (requested: ${model || 'default'}). Check your subscription.`;
|
|
347
|
-
}
|
|
348
|
-
resolve({
|
|
349
|
-
ok: false,
|
|
350
|
-
exitCode: code ?? -1,
|
|
351
|
-
error: errorMsg,
|
|
352
|
-
});
|
|
328
|
+
resolve({ ok: false, exitCode: code ?? -1, error: errorMsg });
|
|
353
329
|
}
|
|
354
330
|
else {
|
|
355
331
|
resolve({
|
|
@@ -363,14 +339,17 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
|
|
|
363
339
|
child.on('error', (err) => {
|
|
364
340
|
clearTimeout(timeoutHandle);
|
|
365
341
|
clearInterval(heartbeatInterval);
|
|
366
|
-
resolve({
|
|
367
|
-
ok: false,
|
|
368
|
-
exitCode: -1,
|
|
369
|
-
error: `Failed to start cursor-agent: ${err.message}`,
|
|
370
|
-
});
|
|
342
|
+
resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
|
|
371
343
|
});
|
|
372
344
|
});
|
|
373
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
|
+
}
|
|
374
353
|
/**
|
|
375
354
|
* Extract dependency change request from agent response
|
|
376
355
|
*/
|
|
@@ -397,27 +376,126 @@ function extractDependencyRequest(text) {
|
|
|
397
376
|
return { required: true, raw: t };
|
|
398
377
|
}
|
|
399
378
|
/**
|
|
400
|
-
*
|
|
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
|
|
388
|
+
*/
|
|
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)
|
|
401
427
|
*/
|
|
402
|
-
function wrapPromptForDependencyPolicy(prompt, policy
|
|
403
|
-
|
|
404
|
-
if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
|
|
428
|
+
function wrapPromptForDependencyPolicy(prompt, policy) {
|
|
429
|
+
if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
|
|
405
430
|
return prompt;
|
|
406
431
|
}
|
|
407
|
-
let
|
|
408
|
-
|
|
409
|
-
|
|
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`;
|
|
410
461
|
if (noGit) {
|
|
411
|
-
|
|
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`;
|
|
412
491
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
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;
|
|
421
499
|
}
|
|
422
500
|
/**
|
|
423
501
|
* Apply file permissions based on dependency policy
|
|
@@ -446,47 +524,31 @@ function applyDependencyFilePermissions(worktreeDir, policy) {
|
|
|
446
524
|
}
|
|
447
525
|
/**
|
|
448
526
|
* Wait for task-level dependencies to be completed by other lanes
|
|
527
|
+
* Now uses the enhanced dependency module with timeout support
|
|
449
528
|
*/
|
|
450
|
-
async function waitForTaskDependencies(deps, runDir) {
|
|
529
|
+
async function waitForTaskDependencies(deps, runDir, options = {}) {
|
|
451
530
|
if (!deps || deps.length === 0)
|
|
452
531
|
return;
|
|
453
532
|
const lanesRoot = path.dirname(runDir);
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (
|
|
460
|
-
logger.
|
|
461
|
-
pendingDeps.delete(dep);
|
|
462
|
-
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`);
|
|
463
540
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
logger.info(`✓ Dependency met: ${dep}`);
|
|
470
|
-
pendingDeps.delete(dep);
|
|
471
|
-
}
|
|
472
|
-
else if (state.status === 'failed') {
|
|
473
|
-
throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
catch (e) {
|
|
477
|
-
if (e.message.includes('Dependency failed'))
|
|
478
|
-
throw e;
|
|
479
|
-
// Ignore parse errors, file might be being written
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
if (pendingDeps.size > 0) {
|
|
484
|
-
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(', ')}`);
|
|
485
546
|
}
|
|
547
|
+
throw new Error(`Dependencies failed: ${result.failedDependencies.join(', ')}`);
|
|
486
548
|
}
|
|
487
549
|
}
|
|
488
550
|
/**
|
|
489
|
-
* Merge branches from dependency lanes
|
|
551
|
+
* Merge branches from dependency lanes with safe merge
|
|
490
552
|
*/
|
|
491
553
|
async function mergeDependencyBranches(deps, runDir, worktreeDir) {
|
|
492
554
|
if (!deps || deps.length === 0)
|
|
@@ -498,20 +560,31 @@ async function mergeDependencyBranches(deps, runDir, worktreeDir) {
|
|
|
498
560
|
if (!fs.existsSync(depStatePath))
|
|
499
561
|
continue;
|
|
500
562
|
try {
|
|
501
|
-
const state =
|
|
502
|
-
if (state
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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');
|
|
511
582
|
}
|
|
583
|
+
logger.success(`✓ Merged ${laneName}`);
|
|
512
584
|
}
|
|
513
585
|
catch (e) {
|
|
514
586
|
logger.error(`Failed to merge branch from ${laneName}: ${e}`);
|
|
587
|
+
throw e;
|
|
515
588
|
}
|
|
516
589
|
}
|
|
517
590
|
}
|
|
@@ -541,9 +614,26 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
|
|
|
541
614
|
}
|
|
542
615
|
// Apply dependency permissions
|
|
543
616
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
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, {
|
|
547
637
|
task: task.name,
|
|
548
638
|
model,
|
|
549
639
|
}));
|
|
@@ -552,17 +642,18 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
|
|
|
552
642
|
events_1.events.emit('agent.prompt_sent', {
|
|
553
643
|
taskName: task.name,
|
|
554
644
|
model,
|
|
555
|
-
promptLength:
|
|
645
|
+
promptLength: wrappedPrompt.length,
|
|
556
646
|
});
|
|
557
647
|
const r1 = await cursorAgentSend({
|
|
558
648
|
workspaceDir: worktreeDir,
|
|
559
649
|
chatId,
|
|
560
|
-
prompt:
|
|
650
|
+
prompt: wrappedPrompt,
|
|
561
651
|
model,
|
|
562
652
|
signalDir: runDir,
|
|
563
653
|
timeout,
|
|
564
654
|
enableIntervention: config.enableIntervention,
|
|
565
655
|
outputFormat: config.agentOutputFormat,
|
|
656
|
+
taskName: task.name,
|
|
566
657
|
});
|
|
567
658
|
const duration = Date.now() - startTime;
|
|
568
659
|
events_1.events.emit('agent.response_received', {
|
|
@@ -589,15 +680,26 @@ async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskB
|
|
|
589
680
|
error: r1.error,
|
|
590
681
|
};
|
|
591
682
|
}
|
|
592
|
-
// Check for dependency request
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
+
}
|
|
601
703
|
}
|
|
602
704
|
// Push task branch (skip in noGit mode)
|
|
603
705
|
if (!noGit) {
|
|
@@ -662,6 +764,28 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
662
764
|
logger.error(` ${validationError.message}`);
|
|
663
765
|
throw validationError;
|
|
664
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
|
+
}
|
|
665
789
|
// Ensure cursor-agent is installed
|
|
666
790
|
(0, cursor_agent_1.ensureCursorAgent)();
|
|
667
791
|
// Check authentication before starting
|
|
@@ -682,16 +806,44 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
682
806
|
}
|
|
683
807
|
logger.success('✓ Cursor authentication OK');
|
|
684
808
|
// In noGit mode, we don't need repoRoot - use current directory
|
|
685
|
-
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)`);
|
|
686
814
|
// Load existing state if resuming
|
|
687
815
|
const statePath = (0, path_1.safeJoin)(runDir, 'state.json');
|
|
688
816
|
let state = null;
|
|
689
817
|
if (fs.existsSync(statePath)) {
|
|
690
|
-
|
|
691
|
-
|
|
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
|
+
}
|
|
692
829
|
}
|
|
693
|
-
|
|
694
|
-
|
|
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
|
+
}
|
|
695
847
|
}
|
|
696
848
|
}
|
|
697
849
|
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
@@ -728,8 +880,9 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
728
880
|
if (!fs.existsSync(worktreeParent)) {
|
|
729
881
|
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
730
882
|
}
|
|
883
|
+
// Always use the current branch (already captured at start) as the base branch
|
|
731
884
|
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
732
|
-
baseBranch:
|
|
885
|
+
baseBranch: currentBranch,
|
|
733
886
|
cwd: repoRoot,
|
|
734
887
|
});
|
|
735
888
|
break; // Success
|
|
@@ -876,16 +1029,31 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
876
1029
|
}
|
|
877
1030
|
// Run tasks
|
|
878
1031
|
const results = [];
|
|
1032
|
+
const laneName = state.label || path.basename(runDir);
|
|
879
1033
|
for (let i = startIndex; i < config.tasks.length; i++) {
|
|
880
1034
|
const task = config.tasks[i];
|
|
881
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
|
+
}
|
|
882
1046
|
// Handle task-level dependencies
|
|
883
1047
|
if (task.dependsOn && task.dependsOn.length > 0) {
|
|
884
1048
|
state.status = 'waiting';
|
|
885
1049
|
state.waitingFor = task.dependsOn;
|
|
886
1050
|
(0, state_1.saveState)(statePath, state);
|
|
887
1051
|
try {
|
|
888
|
-
|
|
1052
|
+
// Use enhanced dependency wait with timeout
|
|
1053
|
+
await waitForTaskDependencies(task.dependsOn, runDir, {
|
|
1054
|
+
timeoutMs: config.timeout || 30 * 60 * 1000,
|
|
1055
|
+
onTimeout: 'fail',
|
|
1056
|
+
});
|
|
889
1057
|
if (!noGit) {
|
|
890
1058
|
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
|
|
891
1059
|
}
|
|
@@ -899,6 +1067,12 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
899
1067
|
state.error = e.message;
|
|
900
1068
|
(0, state_1.saveState)(statePath, state);
|
|
901
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
|
+
}
|
|
902
1076
|
process.exit(1);
|
|
903
1077
|
}
|
|
904
1078
|
}
|
|
@@ -988,7 +1162,8 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
988
1162
|
}
|
|
989
1163
|
else {
|
|
990
1164
|
try {
|
|
991
|
-
|
|
1165
|
+
// Always use current branch for comparison (already captured at start)
|
|
1166
|
+
const stats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
|
|
992
1167
|
if (stats) {
|
|
993
1168
|
logger.info('Final Workspace Summary (Git):\n' + stats);
|
|
994
1169
|
}
|