@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/src/core/runner.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core Runner - Execute tasks sequentially in a lane
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Enhanced retry with circuit breaker
|
|
6
|
+
* - Checkpoint system for recovery
|
|
7
|
+
* - State validation and repair
|
|
8
|
+
* - Improved dependency management
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import * as fs from 'fs';
|
|
8
12
|
import * as path from 'path';
|
|
9
|
-
import {
|
|
13
|
+
import { spawn, spawnSync } from 'child_process';
|
|
10
14
|
|
|
11
15
|
import * as git from '../utils/git';
|
|
12
16
|
import * as logger from '../utils/logger';
|
|
13
17
|
import { ensureCursorAgent, checkCursorAuth, printAuthHelp } from '../utils/cursor-agent';
|
|
14
|
-
import { saveState, appendLog, createConversationEntry } from '../utils/state';
|
|
18
|
+
import { saveState, appendLog, createConversationEntry, loadState, validateLaneState, repairLaneState, stateNeedsRecovery } from '../utils/state';
|
|
15
19
|
import { events } from '../utils/events';
|
|
16
20
|
import { loadConfig } from '../utils/config';
|
|
17
21
|
import { registerWebhooks } from '../utils/webhook';
|
|
18
22
|
import { runReviewLoop } from './reviewer';
|
|
23
|
+
import { safeJoin } from '../utils/path';
|
|
24
|
+
import { analyzeFailure, RecoveryAction, logFailure, withRetry } from './failure-policy';
|
|
25
|
+
import { createCheckpoint, getLatestCheckpoint, restoreFromCheckpoint } from '../utils/checkpoint';
|
|
26
|
+
import { waitForTaskDependencies as waitForDeps, DependencyWaitOptions } from '../utils/dependency';
|
|
27
|
+
import { preflightCheck, printPreflightReport } from '../utils/health';
|
|
19
28
|
import {
|
|
20
29
|
RunnerConfig,
|
|
21
30
|
Task,
|
|
@@ -24,7 +33,7 @@ import {
|
|
|
24
33
|
DependencyPolicy,
|
|
25
34
|
DependencyRequestPlan,
|
|
26
35
|
LaneState
|
|
27
|
-
} from '../
|
|
36
|
+
} from '../types';
|
|
28
37
|
|
|
29
38
|
/**
|
|
30
39
|
* Execute cursor-agent command with timeout and better error handling
|
|
@@ -173,19 +182,18 @@ export function validateTaskConfig(config: RunnerConfig): void {
|
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
/**
|
|
176
|
-
* Execute cursor-agent command with streaming
|
|
185
|
+
* Internal: Execute cursor-agent command with streaming
|
|
177
186
|
*/
|
|
178
|
-
|
|
187
|
+
async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat, taskName }: {
|
|
179
188
|
workspaceDir: string;
|
|
180
189
|
chatId: string;
|
|
181
190
|
prompt: string;
|
|
182
191
|
model?: string;
|
|
183
192
|
signalDir?: string;
|
|
184
193
|
timeout?: number;
|
|
185
|
-
/** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
|
|
186
194
|
enableIntervention?: boolean;
|
|
187
|
-
/** Output format for cursor-agent (default: 'stream-json') */
|
|
188
195
|
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
196
|
+
taskName?: string;
|
|
189
197
|
}): Promise<AgentSendResult> {
|
|
190
198
|
// Use stream-json format for structured output with tool calls and results
|
|
191
199
|
const format = outputFormat || 'stream-json';
|
|
@@ -201,24 +209,15 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
201
209
|
];
|
|
202
210
|
|
|
203
211
|
const timeoutMs = timeout || DEFAULT_TIMEOUT_MS;
|
|
204
|
-
logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
|
|
205
212
|
|
|
206
213
|
// Determine stdio mode based on intervention setting
|
|
207
|
-
// When intervention is enabled, we pipe stdin for message injection
|
|
208
|
-
// When disabled (default), we ignore stdin to avoid buffering issues
|
|
209
214
|
const stdinMode = enableIntervention ? 'pipe' : 'ignore';
|
|
210
215
|
|
|
211
|
-
if (enableIntervention) {
|
|
212
|
-
logger.info('Intervention mode enabled (stdin piped)');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
216
|
return new Promise((resolve) => {
|
|
216
217
|
// Build environment, preserving user's NODE_OPTIONS but disabling problematic flags
|
|
217
218
|
const childEnv = { ...process.env };
|
|
218
219
|
|
|
219
|
-
// Only filter out specific problematic NODE_OPTIONS, don't clear entirely
|
|
220
220
|
if (childEnv.NODE_OPTIONS) {
|
|
221
|
-
// Remove flags that might interfere with cursor-agent
|
|
222
221
|
const filtered = childEnv.NODE_OPTIONS
|
|
223
222
|
.split(' ')
|
|
224
223
|
.filter(opt => !opt.includes('--inspect') && !opt.includes('--debug'))
|
|
@@ -226,7 +225,6 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
226
225
|
childEnv.NODE_OPTIONS = filtered;
|
|
227
226
|
}
|
|
228
227
|
|
|
229
|
-
// Disable Python buffering in case cursor-agent uses Python
|
|
230
228
|
childEnv.PYTHONUNBUFFERED = '1';
|
|
231
229
|
|
|
232
230
|
const child = spawn('cursor-agent', args, {
|
|
@@ -234,18 +232,15 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
234
232
|
env: childEnv,
|
|
235
233
|
});
|
|
236
234
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// Save PID to state if possible (avoid TOCTOU by reading directly)
|
|
235
|
+
// Save PID to state if possible
|
|
240
236
|
if (child.pid && signalDir) {
|
|
241
237
|
try {
|
|
242
|
-
const statePath =
|
|
243
|
-
// Read directly without existence check to avoid race condition
|
|
238
|
+
const statePath = safeJoin(signalDir, 'state.json');
|
|
244
239
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
245
240
|
state.pid = child.pid;
|
|
246
241
|
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
247
242
|
} catch {
|
|
248
|
-
// Best effort
|
|
243
|
+
// Best effort
|
|
249
244
|
}
|
|
250
245
|
}
|
|
251
246
|
|
|
@@ -253,24 +248,23 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
253
248
|
let fullStderr = '';
|
|
254
249
|
let timeoutHandle: NodeJS.Timeout;
|
|
255
250
|
|
|
256
|
-
// Heartbeat logging
|
|
251
|
+
// Heartbeat logging
|
|
257
252
|
let lastHeartbeat = Date.now();
|
|
258
253
|
let bytesReceived = 0;
|
|
254
|
+
const startTime = Date.now();
|
|
259
255
|
const heartbeatInterval = setInterval(() => {
|
|
260
|
-
const elapsed = Math.round((Date.now() - lastHeartbeat) / 1000);
|
|
261
256
|
const totalElapsed = Math.round((Date.now() - startTime) / 1000);
|
|
262
|
-
|
|
257
|
+
// Output without timestamp - orchestrator will add it
|
|
258
|
+
console.log(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
|
|
263
259
|
}, HEARTBEAT_INTERVAL_MS);
|
|
264
|
-
const startTime = Date.now();
|
|
265
260
|
|
|
266
|
-
//
|
|
261
|
+
// Signal watchers (intervention, timeout)
|
|
267
262
|
const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
|
|
268
263
|
const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
|
|
269
264
|
let signalWatcher: fs.FSWatcher | null = null;
|
|
270
265
|
|
|
271
266
|
if (signalDir && fs.existsSync(signalDir)) {
|
|
272
267
|
signalWatcher = fs.watch(signalDir, (event, filename) => {
|
|
273
|
-
// Handle intervention
|
|
274
268
|
if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
|
|
275
269
|
try {
|
|
276
270
|
const message = fs.readFileSync(interventionPath, 'utf8').trim();
|
|
@@ -278,59 +272,48 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
278
272
|
if (enableIntervention && child.stdin) {
|
|
279
273
|
logger.info(`Injecting intervention: ${message}`);
|
|
280
274
|
child.stdin.write(message + '\n');
|
|
275
|
+
|
|
276
|
+
// Log to conversation history for visibility in monitor/logs
|
|
277
|
+
if (signalDir) {
|
|
278
|
+
const convoPath = path.join(signalDir, 'conversation.jsonl');
|
|
279
|
+
appendLog(convoPath, createConversationEntry('intervention', `[HUMAN INTERVENTION]: ${message}`, {
|
|
280
|
+
task: taskName || 'AGENT_TURN',
|
|
281
|
+
model: 'manual'
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
281
284
|
} else {
|
|
282
285
|
logger.warn(`Intervention requested but stdin not available: ${message}`);
|
|
283
|
-
logger.warn('To enable intervention, set enableIntervention: true in config');
|
|
284
286
|
}
|
|
285
|
-
fs.unlinkSync(interventionPath);
|
|
287
|
+
fs.unlinkSync(interventionPath);
|
|
286
288
|
}
|
|
287
|
-
} catch
|
|
288
|
-
logger.warn('Failed to read intervention file');
|
|
289
|
-
}
|
|
289
|
+
} catch {}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
// Handle dynamic timeout update
|
|
293
292
|
if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
|
|
294
293
|
try {
|
|
295
294
|
const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
|
|
296
295
|
const newTimeoutMs = parseInt(newTimeoutStr);
|
|
297
|
-
|
|
298
296
|
if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
|
|
299
297
|
logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
|
|
300
|
-
|
|
301
|
-
// Clear old timeout
|
|
302
298
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
303
|
-
|
|
304
|
-
// Set new timeout based on total elapsed time
|
|
305
299
|
const elapsed = Date.now() - startTime;
|
|
306
300
|
const remaining = Math.max(1000, newTimeoutMs - elapsed);
|
|
307
|
-
|
|
308
301
|
timeoutHandle = setTimeout(() => {
|
|
309
302
|
clearInterval(heartbeatInterval);
|
|
310
303
|
child.kill();
|
|
311
|
-
|
|
312
|
-
resolve({
|
|
313
|
-
ok: false,
|
|
314
|
-
exitCode: -1,
|
|
315
|
-
error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
|
|
316
|
-
});
|
|
304
|
+
resolve({ ok: false, exitCode: -1, error: `cursor-agent timed out after updated limit.` });
|
|
317
305
|
}, remaining);
|
|
318
|
-
|
|
319
|
-
fs.unlinkSync(timeoutPath); // Clear it
|
|
306
|
+
fs.unlinkSync(timeoutPath);
|
|
320
307
|
}
|
|
321
|
-
} catch
|
|
322
|
-
logger.warn('Failed to read timeout update file');
|
|
323
|
-
}
|
|
308
|
+
} catch {}
|
|
324
309
|
}
|
|
325
310
|
});
|
|
326
311
|
}
|
|
327
312
|
|
|
328
313
|
if (child.stdout) {
|
|
329
314
|
child.stdout.on('data', (data) => {
|
|
330
|
-
|
|
331
|
-
fullStdout += str;
|
|
315
|
+
fullStdout += data.toString();
|
|
332
316
|
bytesReceived += data.length;
|
|
333
|
-
// Also pipe to our own stdout so it goes to terminal.log
|
|
334
317
|
process.stdout.write(data);
|
|
335
318
|
});
|
|
336
319
|
}
|
|
@@ -338,7 +321,6 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
338
321
|
if (child.stderr) {
|
|
339
322
|
child.stderr.on('data', (data) => {
|
|
340
323
|
fullStderr += data.toString();
|
|
341
|
-
// Pipe to our own stderr so it goes to terminal.log
|
|
342
324
|
process.stderr.write(data);
|
|
343
325
|
});
|
|
344
326
|
}
|
|
@@ -346,11 +328,10 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
346
328
|
timeoutHandle = setTimeout(() => {
|
|
347
329
|
clearInterval(heartbeatInterval);
|
|
348
330
|
child.kill();
|
|
349
|
-
const timeoutSec = Math.round(timeoutMs / 1000);
|
|
350
331
|
resolve({
|
|
351
332
|
ok: false,
|
|
352
333
|
exitCode: -1,
|
|
353
|
-
error: `cursor-agent timed out after ${
|
|
334
|
+
error: `cursor-agent timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
|
|
354
335
|
});
|
|
355
336
|
}, timeoutMs);
|
|
356
337
|
|
|
@@ -363,21 +344,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
363
344
|
|
|
364
345
|
if (code !== 0 || !json || json.type !== 'result') {
|
|
365
346
|
let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
|
|
366
|
-
|
|
367
|
-
// Check for common errors
|
|
368
|
-
if (errorMsg.includes('not authenticated') || errorMsg.includes('login') || errorMsg.includes('auth')) {
|
|
369
|
-
errorMsg = 'Authentication error. Please sign in to Cursor IDE.';
|
|
370
|
-
} else if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
|
|
371
|
-
errorMsg = 'API rate limit or quota exceeded.';
|
|
372
|
-
} else if (errorMsg.includes('model')) {
|
|
373
|
-
errorMsg = `Model error (requested: ${model || 'default'}). Check your subscription.`;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
resolve({
|
|
377
|
-
ok: false,
|
|
378
|
-
exitCode: code ?? -1,
|
|
379
|
-
error: errorMsg,
|
|
380
|
-
});
|
|
347
|
+
resolve({ ok: false, exitCode: code ?? -1, error: errorMsg });
|
|
381
348
|
} else {
|
|
382
349
|
resolve({
|
|
383
350
|
ok: !json.is_error,
|
|
@@ -391,15 +358,35 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
391
358
|
child.on('error', (err) => {
|
|
392
359
|
clearTimeout(timeoutHandle);
|
|
393
360
|
clearInterval(heartbeatInterval);
|
|
394
|
-
resolve({
|
|
395
|
-
ok: false,
|
|
396
|
-
exitCode: -1,
|
|
397
|
-
error: `Failed to start cursor-agent: ${err.message}`,
|
|
398
|
-
});
|
|
361
|
+
resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
|
|
399
362
|
});
|
|
400
363
|
});
|
|
401
364
|
}
|
|
402
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Execute cursor-agent command with retries for transient errors
|
|
368
|
+
*/
|
|
369
|
+
export async function cursorAgentSend(options: {
|
|
370
|
+
workspaceDir: string;
|
|
371
|
+
chatId: string;
|
|
372
|
+
prompt: string;
|
|
373
|
+
model?: string;
|
|
374
|
+
signalDir?: string;
|
|
375
|
+
timeout?: number;
|
|
376
|
+
enableIntervention?: boolean;
|
|
377
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
378
|
+
taskName?: string;
|
|
379
|
+
}): Promise<AgentSendResult> {
|
|
380
|
+
const laneName = options.signalDir ? path.basename(path.dirname(options.signalDir)) : 'agent';
|
|
381
|
+
|
|
382
|
+
return withRetry(
|
|
383
|
+
laneName,
|
|
384
|
+
() => cursorAgentSendRaw(options),
|
|
385
|
+
(res) => ({ ok: res.ok, error: res.error }),
|
|
386
|
+
{ maxRetries: 3 }
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
403
390
|
/**
|
|
404
391
|
* Extract dependency change request from agent response
|
|
405
392
|
*/
|
|
@@ -430,33 +417,155 @@ export function extractDependencyRequest(text: string): { required: boolean; pla
|
|
|
430
417
|
}
|
|
431
418
|
|
|
432
419
|
/**
|
|
433
|
-
*
|
|
420
|
+
* Inter-task state file name
|
|
421
|
+
*/
|
|
422
|
+
const LANE_STATE_FILE = '_cursorflow/lane-state.json';
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Dependency request file name - agent writes here when dependency changes are needed
|
|
434
426
|
*/
|
|
435
|
-
|
|
436
|
-
|
|
427
|
+
const DEPENDENCY_REQUEST_FILE = '_cursorflow/dependency-request.json';
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Read dependency request from file if it exists
|
|
431
|
+
*/
|
|
432
|
+
export function readDependencyRequestFile(worktreeDir: string): { required: boolean; plan?: DependencyRequestPlan } {
|
|
433
|
+
const filePath = safeJoin(worktreeDir, DEPENDENCY_REQUEST_FILE);
|
|
434
|
+
|
|
435
|
+
if (!fs.existsSync(filePath)) {
|
|
436
|
+
return { required: false };
|
|
437
|
+
}
|
|
437
438
|
|
|
438
|
-
|
|
439
|
+
try {
|
|
440
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
441
|
+
const plan = JSON.parse(content) as DependencyRequestPlan;
|
|
442
|
+
|
|
443
|
+
// Validate required fields
|
|
444
|
+
if (plan.reason && Array.isArray(plan.commands) && plan.commands.length > 0) {
|
|
445
|
+
logger.info(`📦 Dependency request file detected: ${filePath}`);
|
|
446
|
+
return { required: true, plan };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
logger.warn(`Invalid dependency request file format: ${filePath}`);
|
|
450
|
+
return { required: false };
|
|
451
|
+
} catch (e) {
|
|
452
|
+
logger.warn(`Failed to parse dependency request file: ${e}`);
|
|
453
|
+
return { required: false };
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Clear dependency request file after processing
|
|
459
|
+
*/
|
|
460
|
+
export function clearDependencyRequestFile(worktreeDir: string): void {
|
|
461
|
+
const filePath = safeJoin(worktreeDir, DEPENDENCY_REQUEST_FILE);
|
|
462
|
+
|
|
463
|
+
if (fs.existsSync(filePath)) {
|
|
464
|
+
try {
|
|
465
|
+
fs.unlinkSync(filePath);
|
|
466
|
+
logger.info(`🗑️ Cleared dependency request file: ${filePath}`);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
logger.warn(`Failed to clear dependency request file: ${e}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Wrap prompt with dependency policy instructions (legacy, used by tests)
|
|
475
|
+
*/
|
|
476
|
+
export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
|
|
477
|
+
if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
|
|
439
478
|
return prompt;
|
|
440
479
|
}
|
|
441
480
|
|
|
442
|
-
let
|
|
481
|
+
let wrapped = `### 📦 Dependency Policy\n`;
|
|
482
|
+
wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
483
|
+
wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n\n`;
|
|
484
|
+
wrapped += prompt;
|
|
443
485
|
|
|
444
|
-
|
|
445
|
-
|
|
486
|
+
return wrapped;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Wrap prompt with global context, dependency policy, and worktree instructions
|
|
491
|
+
*/
|
|
492
|
+
export function wrapPrompt(
|
|
493
|
+
prompt: string,
|
|
494
|
+
config: RunnerConfig,
|
|
495
|
+
options: {
|
|
496
|
+
noGit?: boolean;
|
|
497
|
+
isWorktree?: boolean;
|
|
498
|
+
previousState?: string | null;
|
|
499
|
+
} = {}
|
|
500
|
+
): string {
|
|
501
|
+
const { noGit = false, isWorktree = true, previousState = null } = options;
|
|
502
|
+
|
|
503
|
+
// 1. PREFIX: Environment & Worktree context
|
|
504
|
+
let wrapped = `### 🛠 Environment & Context\n`;
|
|
505
|
+
wrapped += `- **Workspace**: 당신은 독립된 **Git 워크트리** (프로젝트 루트)에서 작업 중입니다.\n`;
|
|
506
|
+
wrapped += `- **Path Rule**: 모든 파일 참조 및 터미널 명령어는 **현재 디렉토리(./)**를 기준으로 하세요.\n`;
|
|
507
|
+
|
|
508
|
+
if (isWorktree) {
|
|
509
|
+
wrapped += `- **File Availability**: Git 추적 파일만 존재합니다. (node_modules, .env 등은 기본적으로 없음)\n`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// 2. Previous Task State (if available)
|
|
513
|
+
if (previousState) {
|
|
514
|
+
wrapped += `\n### 💡 Previous Task State\n`;
|
|
515
|
+
wrapped += `이전 태스크에서 전달된 상태 정보입니다:\n`;
|
|
516
|
+
wrapped += `\`\`\`json\n${previousState}\n\`\`\`\n`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 3. Dependency Policy (Integrated)
|
|
520
|
+
const policy = config.dependencyPolicy;
|
|
521
|
+
wrapped += `\n### 📦 Dependency Policy\n`;
|
|
522
|
+
wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
523
|
+
wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
|
|
446
524
|
|
|
447
525
|
if (noGit) {
|
|
448
|
-
|
|
526
|
+
wrapped += `- NO_GIT_MODE: Git 명령어를 사용하지 마세요. 파일 수정만 가능합니다.\n`;
|
|
449
527
|
}
|
|
528
|
+
|
|
529
|
+
wrapped += `\n**📦 Dependency Change Rules:**\n`;
|
|
530
|
+
wrapped += `1. 코드를 수정하기 전, 의존성 변경이 필요한지 **먼저** 판단하세요.\n`;
|
|
531
|
+
wrapped += `2. 의존성 변경이 필요하다면:\n`;
|
|
532
|
+
wrapped += ` - **다른 파일을 절대 수정하지 마세요.**\n`;
|
|
533
|
+
wrapped += ` - 아래 JSON을 \`./${DEPENDENCY_REQUEST_FILE}\` 파일에 저장하세요:\n`;
|
|
534
|
+
wrapped += ` \`\`\`json\n`;
|
|
535
|
+
wrapped += ` {\n`;
|
|
536
|
+
wrapped += ` "reason": "왜 이 의존성이 필요한지 설명",\n`;
|
|
537
|
+
wrapped += ` "changes": ["add lodash@^4.17.21", "remove unused-pkg"],\n`;
|
|
538
|
+
wrapped += ` "commands": ["pnpm add lodash@^4.17.21", "pnpm remove unused-pkg"],\n`;
|
|
539
|
+
wrapped += ` "notes": "추가 참고사항 (선택)" \n`;
|
|
540
|
+
wrapped += ` }\n`;
|
|
541
|
+
wrapped += ` \`\`\`\n`;
|
|
542
|
+
wrapped += ` - 파일 저장 후 **즉시 작업을 종료**하세요. 오케스트레이터가 처리합니다.\n`;
|
|
543
|
+
wrapped += `3. 의존성 변경이 불필요하면 바로 본 작업을 진행하세요.\n`;
|
|
544
|
+
|
|
545
|
+
wrapped += `\n---\n\n${prompt}\n\n---\n`;
|
|
546
|
+
|
|
547
|
+
// 4. SUFFIX: Task Completion & Git Requirements
|
|
548
|
+
wrapped += `\n### 📝 Task Completion Requirements\n`;
|
|
549
|
+
wrapped += `**반드시 다음 순서로 작업을 마무리하세요:**\n\n`;
|
|
450
550
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
551
|
+
if (!noGit) {
|
|
552
|
+
wrapped += `1. **Git Commit & Push** (필수!):\n`;
|
|
553
|
+
wrapped += ` \`\`\`bash\n`;
|
|
554
|
+
wrapped += ` git add -A\n`;
|
|
555
|
+
wrapped += ` git commit -m "feat: <작업 내용 요약>"\n`;
|
|
556
|
+
wrapped += ` git push origin HEAD\n`;
|
|
557
|
+
wrapped += ` \`\`\`\n`;
|
|
558
|
+
wrapped += ` ⚠️ 커밋과 푸시 없이 작업을 종료하면 변경사항이 손실됩니다!\n\n`;
|
|
559
|
+
}
|
|
458
560
|
|
|
459
|
-
|
|
561
|
+
wrapped += `2. **State Passing**: 다음 태스크로 전달할 정보가 있다면 \`./${LANE_STATE_FILE}\`에 JSON으로 저장하세요.\n\n`;
|
|
562
|
+
wrapped += `3. **Summary**: 작업 완료 후 다음을 요약해 주세요:\n`;
|
|
563
|
+
wrapped += ` - 생성/수정된 파일 목록\n`;
|
|
564
|
+
wrapped += ` - 주요 변경 사항\n`;
|
|
565
|
+
wrapped += ` - 커밋 해시 (git log --oneline -1)\n\n`;
|
|
566
|
+
wrapped += `4. 지시된 문서(docs/...)를 찾을 수 없다면 즉시 보고하세요.\n`;
|
|
567
|
+
|
|
568
|
+
return wrapped;
|
|
460
569
|
}
|
|
461
570
|
|
|
462
571
|
/**
|
|
@@ -474,7 +583,7 @@ export function applyDependencyFilePermissions(worktreeDir: string, policy: Depe
|
|
|
474
583
|
}
|
|
475
584
|
|
|
476
585
|
for (const file of targets) {
|
|
477
|
-
const filePath =
|
|
586
|
+
const filePath = safeJoin(worktreeDir, file);
|
|
478
587
|
if (!fs.existsSync(filePath)) continue;
|
|
479
588
|
|
|
480
589
|
try {
|
|
@@ -489,49 +598,38 @@ export function applyDependencyFilePermissions(worktreeDir: string, policy: Depe
|
|
|
489
598
|
|
|
490
599
|
/**
|
|
491
600
|
* Wait for task-level dependencies to be completed by other lanes
|
|
601
|
+
* Now uses the enhanced dependency module with timeout support
|
|
492
602
|
*/
|
|
493
|
-
export async function waitForTaskDependencies(
|
|
603
|
+
export async function waitForTaskDependencies(
|
|
604
|
+
deps: string[],
|
|
605
|
+
runDir: string,
|
|
606
|
+
options: DependencyWaitOptions = {}
|
|
607
|
+
): Promise<void> {
|
|
494
608
|
if (!deps || deps.length === 0) return;
|
|
495
609
|
|
|
496
610
|
const lanesRoot = path.dirname(runDir);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
|
|
506
|
-
pendingDeps.delete(dep);
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const depStatePath = path.join(lanesRoot, laneName, 'state.json');
|
|
511
|
-
if (fs.existsSync(depStatePath)) {
|
|
512
|
-
try {
|
|
513
|
-
const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
|
|
514
|
-
if (state.completedTasks && state.completedTasks.includes(taskName)) {
|
|
515
|
-
logger.info(`✓ Dependency met: ${dep}`);
|
|
516
|
-
pendingDeps.delete(dep);
|
|
517
|
-
} else if (state.status === 'failed') {
|
|
518
|
-
throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
|
|
519
|
-
}
|
|
520
|
-
} catch (e: any) {
|
|
521
|
-
if (e.message.includes('Dependency failed')) throw e;
|
|
522
|
-
// Ignore parse errors, file might be being written
|
|
523
|
-
}
|
|
611
|
+
|
|
612
|
+
const result = await waitForDeps(deps, lanesRoot, {
|
|
613
|
+
timeoutMs: options.timeoutMs || 30 * 60 * 1000, // 30 minutes default
|
|
614
|
+
pollIntervalMs: options.pollIntervalMs || 5000,
|
|
615
|
+
onTimeout: options.onTimeout || 'fail',
|
|
616
|
+
onProgress: (pending, completed) => {
|
|
617
|
+
if (completed.length > 0) {
|
|
618
|
+
logger.info(`Dependencies progress: ${completed.length}/${deps.length} completed`);
|
|
524
619
|
}
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
if (!result.success) {
|
|
624
|
+
if (result.timedOut) {
|
|
625
|
+
throw new Error(`Dependency wait timed out after ${Math.round(result.elapsedMs / 1000)}s. Pending: ${result.failedDependencies.join(', ')}`);
|
|
525
626
|
}
|
|
526
|
-
|
|
527
|
-
if (pendingDeps.size > 0) {
|
|
528
|
-
await new Promise(resolve => setTimeout(resolve, 5000)); // Poll every 5 seconds
|
|
529
|
-
}
|
|
627
|
+
throw new Error(`Dependencies failed: ${result.failedDependencies.join(', ')}`);
|
|
530
628
|
}
|
|
531
629
|
}
|
|
532
630
|
|
|
533
631
|
/**
|
|
534
|
-
* Merge branches from dependency lanes
|
|
632
|
+
* Merge branches from dependency lanes with safe merge
|
|
535
633
|
*/
|
|
536
634
|
export async function mergeDependencyBranches(deps: string[], runDir: string, worktreeDir: string): Promise<void> {
|
|
537
635
|
if (!deps || deps.length === 0) return;
|
|
@@ -540,25 +638,38 @@ export async function mergeDependencyBranches(deps: string[], runDir: string, wo
|
|
|
540
638
|
const lanesToMerge = new Set(deps.map(d => d.split(':')[0]!));
|
|
541
639
|
|
|
542
640
|
for (const laneName of lanesToMerge) {
|
|
543
|
-
const depStatePath =
|
|
641
|
+
const depStatePath = safeJoin(lanesRoot, laneName, 'state.json');
|
|
544
642
|
if (!fs.existsSync(depStatePath)) continue;
|
|
545
643
|
|
|
546
644
|
try {
|
|
547
|
-
const state =
|
|
548
|
-
if (state
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
645
|
+
const state = loadState<LaneState>(depStatePath);
|
|
646
|
+
if (!state?.pipelineBranch) continue;
|
|
647
|
+
|
|
648
|
+
logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
|
|
649
|
+
|
|
650
|
+
// Ensure we have the latest
|
|
651
|
+
git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
652
|
+
|
|
653
|
+
// Use safe merge with conflict detection
|
|
654
|
+
const mergeResult = git.safeMerge(state.pipelineBranch, {
|
|
655
|
+
cwd: worktreeDir,
|
|
656
|
+
noFf: true,
|
|
657
|
+
message: `chore: merge task dependency from ${laneName}`,
|
|
658
|
+
abortOnConflict: true,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
if (!mergeResult.success) {
|
|
662
|
+
if (mergeResult.conflict) {
|
|
663
|
+
logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
664
|
+
throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
665
|
+
}
|
|
666
|
+
throw new Error(mergeResult.error || 'Merge failed');
|
|
559
667
|
}
|
|
668
|
+
|
|
669
|
+
logger.success(`✓ Merged ${laneName}`);
|
|
560
670
|
} catch (e) {
|
|
561
671
|
logger.error(`Failed to merge branch from ${laneName}: ${e}`);
|
|
672
|
+
throw e;
|
|
562
673
|
}
|
|
563
674
|
}
|
|
564
675
|
}
|
|
@@ -589,7 +700,7 @@ export async function runTask({
|
|
|
589
700
|
}): Promise<TaskExecutionResult> {
|
|
590
701
|
const model = task.model || config.model || 'sonnet-4.5';
|
|
591
702
|
const timeout = task.timeout || config.timeout;
|
|
592
|
-
const convoPath =
|
|
703
|
+
const convoPath = safeJoin(runDir, 'conversation.jsonl');
|
|
593
704
|
|
|
594
705
|
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
595
706
|
logger.info(`Model: ${model}`);
|
|
@@ -613,10 +724,27 @@ export async function runTask({
|
|
|
613
724
|
// Apply dependency permissions
|
|
614
725
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
615
726
|
|
|
616
|
-
//
|
|
617
|
-
|
|
727
|
+
// Read previous task state if available
|
|
728
|
+
let previousState: string | null = null;
|
|
729
|
+
const stateFilePath = safeJoin(worktreeDir, LANE_STATE_FILE);
|
|
730
|
+
if (fs.existsSync(stateFilePath)) {
|
|
731
|
+
try {
|
|
732
|
+
previousState = fs.readFileSync(stateFilePath, 'utf8');
|
|
733
|
+
logger.info('Loaded previous task state from _cursorflow/lane-state.json');
|
|
734
|
+
} catch (e) {
|
|
735
|
+
logger.warn(`Failed to read inter-task state: ${e}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Wrap prompt with context, previous state, and completion instructions
|
|
740
|
+
const wrappedPrompt = wrapPrompt(task.prompt, config, {
|
|
741
|
+
noGit,
|
|
742
|
+
isWorktree: !noGit,
|
|
743
|
+
previousState
|
|
744
|
+
});
|
|
618
745
|
|
|
619
|
-
|
|
746
|
+
// Log ONLY the original prompt to keep logs clean
|
|
747
|
+
appendLog(convoPath, createConversationEntry('user', task.prompt, {
|
|
620
748
|
task: task.name,
|
|
621
749
|
model,
|
|
622
750
|
}));
|
|
@@ -626,18 +754,19 @@ export async function runTask({
|
|
|
626
754
|
events.emit('agent.prompt_sent', {
|
|
627
755
|
taskName: task.name,
|
|
628
756
|
model,
|
|
629
|
-
promptLength:
|
|
757
|
+
promptLength: wrappedPrompt.length,
|
|
630
758
|
});
|
|
631
759
|
|
|
632
760
|
const r1 = await cursorAgentSend({
|
|
633
761
|
workspaceDir: worktreeDir,
|
|
634
762
|
chatId,
|
|
635
|
-
prompt:
|
|
763
|
+
prompt: wrappedPrompt,
|
|
636
764
|
model,
|
|
637
765
|
signalDir: runDir,
|
|
638
766
|
timeout,
|
|
639
767
|
enableIntervention: config.enableIntervention,
|
|
640
768
|
outputFormat: config.agentOutputFormat,
|
|
769
|
+
taskName: task.name,
|
|
641
770
|
});
|
|
642
771
|
|
|
643
772
|
const duration = Date.now() - startTime;
|
|
@@ -668,15 +797,31 @@ export async function runTask({
|
|
|
668
797
|
};
|
|
669
798
|
}
|
|
670
799
|
|
|
671
|
-
// Check for dependency request
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
};
|
|
800
|
+
// Check for dependency request (file-based takes priority, then text-based)
|
|
801
|
+
const fileDepReq = readDependencyRequestFile(worktreeDir);
|
|
802
|
+
const textDepReq = extractDependencyRequest(r1.resultText || '');
|
|
803
|
+
|
|
804
|
+
// Determine which request to use (file-based is preferred as it's more structured)
|
|
805
|
+
const depReq = fileDepReq.required ? fileDepReq : textDepReq;
|
|
806
|
+
|
|
807
|
+
if (depReq.required) {
|
|
808
|
+
logger.info(`📦 Dependency change requested: ${depReq.plan?.reason || 'No reason provided'}`);
|
|
809
|
+
|
|
810
|
+
if (depReq.plan) {
|
|
811
|
+
logger.info(` Commands: ${depReq.plan.commands.join(', ')}`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (!config.dependencyPolicy.allowDependencyChange) {
|
|
815
|
+
// Clear the file so it doesn't persist after resolution
|
|
816
|
+
clearDependencyRequestFile(worktreeDir);
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
taskName: task.name,
|
|
820
|
+
taskBranch,
|
|
821
|
+
status: 'BLOCKED_DEPENDENCY',
|
|
822
|
+
dependencyRequest: depReq.plan || null,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
680
825
|
}
|
|
681
826
|
|
|
682
827
|
// Push task branch (skip in noGit mode)
|
|
@@ -731,7 +876,7 @@ export async function runTask({
|
|
|
731
876
|
/**
|
|
732
877
|
* Run all tasks in sequence
|
|
733
878
|
*/
|
|
734
|
-
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean } = {}): Promise<TaskExecutionResult[]> {
|
|
879
|
+
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean; skipPreflight?: boolean } = {}): Promise<TaskExecutionResult[]> {
|
|
735
880
|
const startIndex = options.startIndex || 0;
|
|
736
881
|
const noGit = options.noGit || config.noGit || false;
|
|
737
882
|
|
|
@@ -750,6 +895,33 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
750
895
|
throw validationError;
|
|
751
896
|
}
|
|
752
897
|
|
|
898
|
+
// Run preflight checks (can be skipped for resume)
|
|
899
|
+
if (!options.skipPreflight && startIndex === 0) {
|
|
900
|
+
logger.info('Running preflight checks...');
|
|
901
|
+
const preflight = await preflightCheck({
|
|
902
|
+
requireRemote: !noGit,
|
|
903
|
+
requireAuth: true,
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
if (!preflight.canProceed) {
|
|
907
|
+
printPreflightReport(preflight);
|
|
908
|
+
throw new Error('Preflight check failed. Please fix the blockers above.');
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (preflight.warnings.length > 0) {
|
|
912
|
+
for (const warning of preflight.warnings) {
|
|
913
|
+
logger.warn(`⚠️ ${warning}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
logger.success('✓ Preflight checks passed');
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Warn if baseBranch is set in config (it will be ignored)
|
|
921
|
+
if (config.baseBranch) {
|
|
922
|
+
logger.warn(`⚠️ config.baseBranch="${config.baseBranch}" will be ignored. Using current branch instead.`);
|
|
923
|
+
}
|
|
924
|
+
|
|
753
925
|
// Ensure cursor-agent is installed
|
|
754
926
|
ensureCursorAgent();
|
|
755
927
|
|
|
@@ -778,26 +950,58 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
778
950
|
logger.success('✓ Cursor authentication OK');
|
|
779
951
|
|
|
780
952
|
// In noGit mode, we don't need repoRoot - use current directory
|
|
781
|
-
const repoRoot = noGit ? process.cwd() : git.
|
|
953
|
+
const repoRoot = noGit ? process.cwd() : git.getMainRepoRoot();
|
|
954
|
+
|
|
955
|
+
// ALWAYS use current branch as base - ignore config.baseBranch
|
|
956
|
+
// This ensures dependency structure is maintained in the worktree
|
|
957
|
+
const currentBranch = noGit ? 'main' : git.getCurrentBranch(repoRoot);
|
|
958
|
+
logger.info(`📍 Base branch: ${currentBranch} (current branch)`);
|
|
782
959
|
|
|
783
960
|
// Load existing state if resuming
|
|
784
|
-
const statePath =
|
|
961
|
+
const statePath = safeJoin(runDir, 'state.json');
|
|
785
962
|
let state: LaneState | null = null;
|
|
786
963
|
|
|
787
964
|
if (fs.existsSync(statePath)) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
965
|
+
// Check if state needs recovery
|
|
966
|
+
if (stateNeedsRecovery(statePath)) {
|
|
967
|
+
logger.warn('State file indicates incomplete previous run. Attempting recovery...');
|
|
968
|
+
const repairedState = repairLaneState(statePath);
|
|
969
|
+
if (repairedState) {
|
|
970
|
+
state = repairedState;
|
|
971
|
+
logger.success('✓ State recovered');
|
|
972
|
+
} else {
|
|
973
|
+
logger.warn('Could not recover state. Starting fresh.');
|
|
974
|
+
}
|
|
975
|
+
} else {
|
|
976
|
+
state = loadState<LaneState>(statePath);
|
|
977
|
+
|
|
978
|
+
// Validate loaded state
|
|
979
|
+
if (state) {
|
|
980
|
+
const validation = validateLaneState(statePath, {
|
|
981
|
+
checkWorktree: !noGit,
|
|
982
|
+
checkBranch: !noGit,
|
|
983
|
+
autoRepair: true,
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
if (!validation.valid) {
|
|
987
|
+
logger.warn(`State validation issues: ${validation.issues.join(', ')}`);
|
|
988
|
+
if (validation.repaired) {
|
|
989
|
+
logger.info('State was auto-repaired');
|
|
990
|
+
state = validation.repairedState || state;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
792
994
|
}
|
|
793
995
|
}
|
|
794
996
|
|
|
795
997
|
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
796
998
|
const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
|
|
999
|
+
|
|
797
1000
|
// In noGit mode, use a simple local directory instead of worktree
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1001
|
+
// Flatten the path by replacing slashes with hyphens to avoid race conditions in parent directory creation
|
|
1002
|
+
const worktreeDir = state?.worktreeDir || config.worktreeDir || (noGit
|
|
1003
|
+
? safeJoin(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
|
|
1004
|
+
: safeJoin(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch.replace(/\//g, '-')));
|
|
801
1005
|
|
|
802
1006
|
if (startIndex === 0) {
|
|
803
1007
|
logger.section('🚀 Starting Pipeline');
|
|
@@ -816,10 +1020,38 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
816
1020
|
logger.info(`Creating work directory: ${worktreeDir}`);
|
|
817
1021
|
fs.mkdirSync(worktreeDir, { recursive: true });
|
|
818
1022
|
} else {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1023
|
+
// Use a simple retry mechanism for Git worktree creation to handle potential race conditions
|
|
1024
|
+
let retries = 3;
|
|
1025
|
+
let lastError: Error | null = null;
|
|
1026
|
+
|
|
1027
|
+
while (retries > 0) {
|
|
1028
|
+
try {
|
|
1029
|
+
// Ensure parent directory exists before calling git worktree
|
|
1030
|
+
const worktreeParent = path.dirname(worktreeDir);
|
|
1031
|
+
if (!fs.existsSync(worktreeParent)) {
|
|
1032
|
+
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Always use the current branch (already captured at start) as the base branch
|
|
1036
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
1037
|
+
baseBranch: currentBranch,
|
|
1038
|
+
cwd: repoRoot,
|
|
1039
|
+
});
|
|
1040
|
+
break; // Success
|
|
1041
|
+
} catch (e: any) {
|
|
1042
|
+
lastError = e;
|
|
1043
|
+
retries--;
|
|
1044
|
+
if (retries > 0) {
|
|
1045
|
+
const delay = Math.floor(Math.random() * 1000) + 500;
|
|
1046
|
+
logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
|
|
1047
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (retries === 0 && lastError) {
|
|
1053
|
+
throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
|
|
1054
|
+
}
|
|
823
1055
|
}
|
|
824
1056
|
} else if (!noGit) {
|
|
825
1057
|
// If it exists but we are in Git mode, ensure it's actually a worktree and on the right branch
|
|
@@ -858,6 +1090,9 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
858
1090
|
state.status = 'running';
|
|
859
1091
|
state.error = null;
|
|
860
1092
|
state.dependencyRequest = null;
|
|
1093
|
+
state.pipelineBranch = pipelineBranch;
|
|
1094
|
+
state.worktreeDir = worktreeDir;
|
|
1095
|
+
state.label = state.label || pipelineBranch;
|
|
861
1096
|
state.dependsOn = config.dependsOn || [];
|
|
862
1097
|
state.completedTasks = state.completedTasks || [];
|
|
863
1098
|
}
|
|
@@ -872,8 +1107,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
872
1107
|
const lanesRoot = path.dirname(runDir);
|
|
873
1108
|
|
|
874
1109
|
for (const depName of config.dependsOn) {
|
|
875
|
-
const depRunDir = path.join(lanesRoot, depName);
|
|
876
|
-
const depStatePath = path.join(depRunDir, 'state.json');
|
|
1110
|
+
const depRunDir = path.join(lanesRoot, depName); // nosemgrep
|
|
1111
|
+
const depStatePath = path.join(depRunDir, 'state.json'); // nosemgrep
|
|
877
1112
|
|
|
878
1113
|
if (!fs.existsSync(depStatePath)) {
|
|
879
1114
|
logger.warn(`Dependency state not found for ${depName} at ${depStatePath}`);
|
|
@@ -919,8 +1154,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
919
1154
|
const lanesRoot = path.dirname(runDir);
|
|
920
1155
|
|
|
921
1156
|
for (const depName of config.dependsOn) {
|
|
922
|
-
const depRunDir =
|
|
923
|
-
const depStatePath =
|
|
1157
|
+
const depRunDir = safeJoin(lanesRoot, depName);
|
|
1158
|
+
const depStatePath = safeJoin(depRunDir, 'state.json');
|
|
924
1159
|
|
|
925
1160
|
if (!fs.existsSync(depStatePath)) {
|
|
926
1161
|
continue;
|
|
@@ -939,8 +1174,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
939
1174
|
for (const entry of entries) {
|
|
940
1175
|
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
|
|
941
1176
|
|
|
942
|
-
const srcPath =
|
|
943
|
-
const destPath =
|
|
1177
|
+
const srcPath = safeJoin(src, entry.name);
|
|
1178
|
+
const destPath = safeJoin(dest, entry.name);
|
|
944
1179
|
|
|
945
1180
|
if (entry.isDirectory()) {
|
|
946
1181
|
copyFiles(srcPath, destPath);
|
|
@@ -960,11 +1195,22 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
960
1195
|
|
|
961
1196
|
// Run tasks
|
|
962
1197
|
const results: TaskExecutionResult[] = [];
|
|
1198
|
+
const laneName = state.label || path.basename(runDir);
|
|
963
1199
|
|
|
964
1200
|
for (let i = startIndex; i < config.tasks.length; i++) {
|
|
965
1201
|
const task = config.tasks[i]!;
|
|
966
1202
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
967
1203
|
|
|
1204
|
+
// Create checkpoint before each task
|
|
1205
|
+
try {
|
|
1206
|
+
await createCheckpoint(laneName, runDir, noGit ? null : worktreeDir, {
|
|
1207
|
+
description: `Before task ${i + 1}: ${task.name}`,
|
|
1208
|
+
maxCheckpoints: 5,
|
|
1209
|
+
});
|
|
1210
|
+
} catch (e: any) {
|
|
1211
|
+
logger.warn(`Failed to create checkpoint: ${e.message}`);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
968
1214
|
// Handle task-level dependencies
|
|
969
1215
|
if (task.dependsOn && task.dependsOn.length > 0) {
|
|
970
1216
|
state.status = 'waiting';
|
|
@@ -972,7 +1218,11 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
972
1218
|
saveState(statePath, state);
|
|
973
1219
|
|
|
974
1220
|
try {
|
|
975
|
-
|
|
1221
|
+
// Use enhanced dependency wait with timeout
|
|
1222
|
+
await waitForTaskDependencies(task.dependsOn, runDir, {
|
|
1223
|
+
timeoutMs: config.timeout || 30 * 60 * 1000,
|
|
1224
|
+
onTimeout: 'fail',
|
|
1225
|
+
});
|
|
976
1226
|
|
|
977
1227
|
if (!noGit) {
|
|
978
1228
|
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
|
|
@@ -987,6 +1237,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
987
1237
|
state.error = e.message;
|
|
988
1238
|
saveState(statePath, state);
|
|
989
1239
|
logger.error(`Task dependency wait/merge failed: ${e.message}`);
|
|
1240
|
+
|
|
1241
|
+
// Try to restore from checkpoint
|
|
1242
|
+
const latestCheckpoint = getLatestCheckpoint(runDir);
|
|
1243
|
+
if (latestCheckpoint) {
|
|
1244
|
+
logger.info(`💾 Checkpoint available: ${latestCheckpoint.id}`);
|
|
1245
|
+
logger.info(` Resume with: cursorflow resume --checkpoint ${latestCheckpoint.id}`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
990
1248
|
process.exit(1);
|
|
991
1249
|
}
|
|
992
1250
|
}
|
|
@@ -1072,7 +1330,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
1072
1330
|
|
|
1073
1331
|
if (entry.isDirectory()) {
|
|
1074
1332
|
stats.dirs++;
|
|
1075
|
-
const sub = getFileSummary(
|
|
1333
|
+
const sub = getFileSummary(safeJoin(dir, entry.name));
|
|
1076
1334
|
stats.files += sub.files;
|
|
1077
1335
|
stats.dirs += sub.dirs;
|
|
1078
1336
|
} else {
|
|
@@ -1086,7 +1344,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
1086
1344
|
logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
|
|
1087
1345
|
} else {
|
|
1088
1346
|
try {
|
|
1089
|
-
|
|
1347
|
+
// Always use current branch for comparison (already captured at start)
|
|
1348
|
+
const stats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
|
|
1090
1349
|
if (stats) {
|
|
1091
1350
|
logger.info('Final Workspace Summary (Git):\n' + stats);
|
|
1092
1351
|
}
|
|
@@ -1114,11 +1373,13 @@ if (require.main === module) {
|
|
|
1114
1373
|
const runDirIdx = args.indexOf('--run-dir');
|
|
1115
1374
|
const startIdxIdx = args.indexOf('--start-index');
|
|
1116
1375
|
const pipelineBranchIdx = args.indexOf('--pipeline-branch');
|
|
1376
|
+
const worktreeDirIdx = args.indexOf('--worktree-dir');
|
|
1117
1377
|
const noGit = args.includes('--no-git');
|
|
1118
1378
|
|
|
1119
1379
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
1120
1380
|
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
1121
1381
|
const forcedPipelineBranch = pipelineBranchIdx >= 0 ? args[pipelineBranchIdx + 1] : null;
|
|
1382
|
+
const forcedWorktreeDir = worktreeDirIdx >= 0 ? args[worktreeDirIdx + 1] : null;
|
|
1122
1383
|
|
|
1123
1384
|
// Extract runId from runDir (format: .../runs/run-123/lanes/lane-name)
|
|
1124
1385
|
const parts = runDir.split(path.sep);
|
|
@@ -1150,6 +1411,9 @@ if (require.main === module) {
|
|
|
1150
1411
|
if (forcedPipelineBranch) {
|
|
1151
1412
|
config.pipelineBranch = forcedPipelineBranch;
|
|
1152
1413
|
}
|
|
1414
|
+
if (forcedWorktreeDir) {
|
|
1415
|
+
config.worktreeDir = forcedWorktreeDir;
|
|
1416
|
+
}
|
|
1153
1417
|
} catch (error: any) {
|
|
1154
1418
|
console.error(`Failed to load tasks file: ${error.message}`);
|
|
1155
1419
|
process.exit(1);
|