@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/src/core/runner.ts
CHANGED
|
@@ -1,22 +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';
|
|
19
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';
|
|
20
28
|
import {
|
|
21
29
|
RunnerConfig,
|
|
22
30
|
Task,
|
|
@@ -25,7 +33,7 @@ import {
|
|
|
25
33
|
DependencyPolicy,
|
|
26
34
|
DependencyRequestPlan,
|
|
27
35
|
LaneState
|
|
28
|
-
} from '../
|
|
36
|
+
} from '../types';
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
39
|
* Execute cursor-agent command with timeout and better error handling
|
|
@@ -174,19 +182,18 @@ export function validateTaskConfig(config: RunnerConfig): void {
|
|
|
174
182
|
}
|
|
175
183
|
|
|
176
184
|
/**
|
|
177
|
-
* Execute cursor-agent command with streaming
|
|
185
|
+
* Internal: Execute cursor-agent command with streaming
|
|
178
186
|
*/
|
|
179
|
-
|
|
187
|
+
async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat, taskName }: {
|
|
180
188
|
workspaceDir: string;
|
|
181
189
|
chatId: string;
|
|
182
190
|
prompt: string;
|
|
183
191
|
model?: string;
|
|
184
192
|
signalDir?: string;
|
|
185
193
|
timeout?: number;
|
|
186
|
-
/** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
|
|
187
194
|
enableIntervention?: boolean;
|
|
188
|
-
/** Output format for cursor-agent (default: 'stream-json') */
|
|
189
195
|
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
196
|
+
taskName?: string;
|
|
190
197
|
}): Promise<AgentSendResult> {
|
|
191
198
|
// Use stream-json format for structured output with tool calls and results
|
|
192
199
|
const format = outputFormat || 'stream-json';
|
|
@@ -202,24 +209,15 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
202
209
|
];
|
|
203
210
|
|
|
204
211
|
const timeoutMs = timeout || DEFAULT_TIMEOUT_MS;
|
|
205
|
-
logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
|
|
206
212
|
|
|
207
213
|
// Determine stdio mode based on intervention setting
|
|
208
|
-
// When intervention is enabled, we pipe stdin for message injection
|
|
209
|
-
// When disabled (default), we ignore stdin to avoid buffering issues
|
|
210
214
|
const stdinMode = enableIntervention ? 'pipe' : 'ignore';
|
|
211
215
|
|
|
212
|
-
if (enableIntervention) {
|
|
213
|
-
logger.info('Intervention mode enabled (stdin piped)');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
216
|
return new Promise((resolve) => {
|
|
217
217
|
// Build environment, preserving user's NODE_OPTIONS but disabling problematic flags
|
|
218
218
|
const childEnv = { ...process.env };
|
|
219
219
|
|
|
220
|
-
// Only filter out specific problematic NODE_OPTIONS, don't clear entirely
|
|
221
220
|
if (childEnv.NODE_OPTIONS) {
|
|
222
|
-
// Remove flags that might interfere with cursor-agent
|
|
223
221
|
const filtered = childEnv.NODE_OPTIONS
|
|
224
222
|
.split(' ')
|
|
225
223
|
.filter(opt => !opt.includes('--inspect') && !opt.includes('--debug'))
|
|
@@ -227,7 +225,6 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
227
225
|
childEnv.NODE_OPTIONS = filtered;
|
|
228
226
|
}
|
|
229
227
|
|
|
230
|
-
// Disable Python buffering in case cursor-agent uses Python
|
|
231
228
|
childEnv.PYTHONUNBUFFERED = '1';
|
|
232
229
|
|
|
233
230
|
const child = spawn('cursor-agent', args, {
|
|
@@ -235,18 +232,15 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
235
232
|
env: childEnv,
|
|
236
233
|
});
|
|
237
234
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Save PID to state if possible (avoid TOCTOU by reading directly)
|
|
235
|
+
// Save PID to state if possible
|
|
241
236
|
if (child.pid && signalDir) {
|
|
242
237
|
try {
|
|
243
238
|
const statePath = safeJoin(signalDir, 'state.json');
|
|
244
|
-
// Read directly without existence check to avoid race condition
|
|
245
239
|
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
246
240
|
state.pid = child.pid;
|
|
247
241
|
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
248
242
|
} catch {
|
|
249
|
-
// Best effort
|
|
243
|
+
// Best effort
|
|
250
244
|
}
|
|
251
245
|
}
|
|
252
246
|
|
|
@@ -254,24 +248,23 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
254
248
|
let fullStderr = '';
|
|
255
249
|
let timeoutHandle: NodeJS.Timeout;
|
|
256
250
|
|
|
257
|
-
// Heartbeat logging
|
|
251
|
+
// Heartbeat logging
|
|
258
252
|
let lastHeartbeat = Date.now();
|
|
259
253
|
let bytesReceived = 0;
|
|
254
|
+
const startTime = Date.now();
|
|
260
255
|
const heartbeatInterval = setInterval(() => {
|
|
261
|
-
const elapsed = Math.round((Date.now() - lastHeartbeat) / 1000);
|
|
262
256
|
const totalElapsed = Math.round((Date.now() - startTime) / 1000);
|
|
263
|
-
|
|
257
|
+
// Output without timestamp - orchestrator will add it
|
|
258
|
+
console.log(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
|
|
264
259
|
}, HEARTBEAT_INTERVAL_MS);
|
|
265
|
-
const startTime = Date.now();
|
|
266
260
|
|
|
267
|
-
//
|
|
261
|
+
// Signal watchers (intervention, timeout)
|
|
268
262
|
const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
|
|
269
263
|
const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
|
|
270
264
|
let signalWatcher: fs.FSWatcher | null = null;
|
|
271
265
|
|
|
272
266
|
if (signalDir && fs.existsSync(signalDir)) {
|
|
273
267
|
signalWatcher = fs.watch(signalDir, (event, filename) => {
|
|
274
|
-
// Handle intervention
|
|
275
268
|
if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
|
|
276
269
|
try {
|
|
277
270
|
const message = fs.readFileSync(interventionPath, 'utf8').trim();
|
|
@@ -279,59 +272,48 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
279
272
|
if (enableIntervention && child.stdin) {
|
|
280
273
|
logger.info(`Injecting intervention: ${message}`);
|
|
281
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
|
+
}
|
|
282
284
|
} else {
|
|
283
285
|
logger.warn(`Intervention requested but stdin not available: ${message}`);
|
|
284
|
-
logger.warn('To enable intervention, set enableIntervention: true in config');
|
|
285
286
|
}
|
|
286
|
-
fs.unlinkSync(interventionPath);
|
|
287
|
+
fs.unlinkSync(interventionPath);
|
|
287
288
|
}
|
|
288
|
-
} catch
|
|
289
|
-
logger.warn('Failed to read intervention file');
|
|
290
|
-
}
|
|
289
|
+
} catch {}
|
|
291
290
|
}
|
|
292
291
|
|
|
293
|
-
// Handle dynamic timeout update
|
|
294
292
|
if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
|
|
295
293
|
try {
|
|
296
294
|
const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
|
|
297
295
|
const newTimeoutMs = parseInt(newTimeoutStr);
|
|
298
|
-
|
|
299
296
|
if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
|
|
300
297
|
logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
|
|
301
|
-
|
|
302
|
-
// Clear old timeout
|
|
303
298
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
304
|
-
|
|
305
|
-
// Set new timeout based on total elapsed time
|
|
306
299
|
const elapsed = Date.now() - startTime;
|
|
307
300
|
const remaining = Math.max(1000, newTimeoutMs - elapsed);
|
|
308
|
-
|
|
309
301
|
timeoutHandle = setTimeout(() => {
|
|
310
302
|
clearInterval(heartbeatInterval);
|
|
311
303
|
child.kill();
|
|
312
|
-
|
|
313
|
-
resolve({
|
|
314
|
-
ok: false,
|
|
315
|
-
exitCode: -1,
|
|
316
|
-
error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
|
|
317
|
-
});
|
|
304
|
+
resolve({ ok: false, exitCode: -1, error: `cursor-agent timed out after updated limit.` });
|
|
318
305
|
}, remaining);
|
|
319
|
-
|
|
320
|
-
fs.unlinkSync(timeoutPath); // Clear it
|
|
306
|
+
fs.unlinkSync(timeoutPath);
|
|
321
307
|
}
|
|
322
|
-
} catch
|
|
323
|
-
logger.warn('Failed to read timeout update file');
|
|
324
|
-
}
|
|
308
|
+
} catch {}
|
|
325
309
|
}
|
|
326
310
|
});
|
|
327
311
|
}
|
|
328
312
|
|
|
329
313
|
if (child.stdout) {
|
|
330
314
|
child.stdout.on('data', (data) => {
|
|
331
|
-
|
|
332
|
-
fullStdout += str;
|
|
315
|
+
fullStdout += data.toString();
|
|
333
316
|
bytesReceived += data.length;
|
|
334
|
-
// Also pipe to our own stdout so it goes to terminal.log
|
|
335
317
|
process.stdout.write(data);
|
|
336
318
|
});
|
|
337
319
|
}
|
|
@@ -339,7 +321,6 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
339
321
|
if (child.stderr) {
|
|
340
322
|
child.stderr.on('data', (data) => {
|
|
341
323
|
fullStderr += data.toString();
|
|
342
|
-
// Pipe to our own stderr so it goes to terminal.log
|
|
343
324
|
process.stderr.write(data);
|
|
344
325
|
});
|
|
345
326
|
}
|
|
@@ -347,11 +328,10 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
347
328
|
timeoutHandle = setTimeout(() => {
|
|
348
329
|
clearInterval(heartbeatInterval);
|
|
349
330
|
child.kill();
|
|
350
|
-
const timeoutSec = Math.round(timeoutMs / 1000);
|
|
351
331
|
resolve({
|
|
352
332
|
ok: false,
|
|
353
333
|
exitCode: -1,
|
|
354
|
-
error: `cursor-agent timed out after ${
|
|
334
|
+
error: `cursor-agent timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
|
|
355
335
|
});
|
|
356
336
|
}, timeoutMs);
|
|
357
337
|
|
|
@@ -364,21 +344,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
364
344
|
|
|
365
345
|
if (code !== 0 || !json || json.type !== 'result') {
|
|
366
346
|
let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
|
|
367
|
-
|
|
368
|
-
// Check for common errors
|
|
369
|
-
if (errorMsg.includes('not authenticated') || errorMsg.includes('login') || errorMsg.includes('auth')) {
|
|
370
|
-
errorMsg = 'Authentication error. Please sign in to Cursor IDE.';
|
|
371
|
-
} else if (errorMsg.includes('rate limit') || errorMsg.includes('quota')) {
|
|
372
|
-
errorMsg = 'API rate limit or quota exceeded.';
|
|
373
|
-
} else if (errorMsg.includes('model')) {
|
|
374
|
-
errorMsg = `Model error (requested: ${model || 'default'}). Check your subscription.`;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
resolve({
|
|
378
|
-
ok: false,
|
|
379
|
-
exitCode: code ?? -1,
|
|
380
|
-
error: errorMsg,
|
|
381
|
-
});
|
|
347
|
+
resolve({ ok: false, exitCode: code ?? -1, error: errorMsg });
|
|
382
348
|
} else {
|
|
383
349
|
resolve({
|
|
384
350
|
ok: !json.is_error,
|
|
@@ -392,15 +358,35 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
392
358
|
child.on('error', (err) => {
|
|
393
359
|
clearTimeout(timeoutHandle);
|
|
394
360
|
clearInterval(heartbeatInterval);
|
|
395
|
-
resolve({
|
|
396
|
-
ok: false,
|
|
397
|
-
exitCode: -1,
|
|
398
|
-
error: `Failed to start cursor-agent: ${err.message}`,
|
|
399
|
-
});
|
|
361
|
+
resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
|
|
400
362
|
});
|
|
401
363
|
});
|
|
402
364
|
}
|
|
403
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
|
+
|
|
404
390
|
/**
|
|
405
391
|
* Extract dependency change request from agent response
|
|
406
392
|
*/
|
|
@@ -431,33 +417,155 @@ export function extractDependencyRequest(text: string): { required: boolean; pla
|
|
|
431
417
|
}
|
|
432
418
|
|
|
433
419
|
/**
|
|
434
|
-
*
|
|
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
|
|
426
|
+
*/
|
|
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
|
+
}
|
|
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
|
|
435
459
|
*/
|
|
436
|
-
export function
|
|
437
|
-
const
|
|
460
|
+
export function clearDependencyRequestFile(worktreeDir: string): void {
|
|
461
|
+
const filePath = safeJoin(worktreeDir, DEPENDENCY_REQUEST_FILE);
|
|
438
462
|
|
|
439
|
-
if (
|
|
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) {
|
|
440
478
|
return prompt;
|
|
441
479
|
}
|
|
442
480
|
|
|
443
|
-
let
|
|
481
|
+
let wrapped = `### 📦 Dependency Policy\n`;
|
|
482
|
+
wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
483
|
+
wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n\n`;
|
|
484
|
+
wrapped += prompt;
|
|
444
485
|
|
|
445
|
-
|
|
446
|
-
|
|
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`;
|
|
447
524
|
|
|
448
525
|
if (noGit) {
|
|
449
|
-
|
|
526
|
+
wrapped += `- NO_GIT_MODE: Git 명령어를 사용하지 마세요. 파일 수정만 가능합니다.\n`;
|
|
450
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`;
|
|
451
550
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
+
}
|
|
459
560
|
|
|
460
|
-
|
|
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;
|
|
461
569
|
}
|
|
462
570
|
|
|
463
571
|
/**
|
|
@@ -490,49 +598,38 @@ export function applyDependencyFilePermissions(worktreeDir: string, policy: Depe
|
|
|
490
598
|
|
|
491
599
|
/**
|
|
492
600
|
* Wait for task-level dependencies to be completed by other lanes
|
|
601
|
+
* Now uses the enhanced dependency module with timeout support
|
|
493
602
|
*/
|
|
494
|
-
export async function waitForTaskDependencies(
|
|
603
|
+
export async function waitForTaskDependencies(
|
|
604
|
+
deps: string[],
|
|
605
|
+
runDir: string,
|
|
606
|
+
options: DependencyWaitOptions = {}
|
|
607
|
+
): Promise<void> {
|
|
495
608
|
if (!deps || deps.length === 0) return;
|
|
496
609
|
|
|
497
610
|
const lanesRoot = path.dirname(runDir);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
|
|
507
|
-
pendingDeps.delete(dep);
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const depStatePath = safeJoin(lanesRoot, laneName, 'state.json');
|
|
512
|
-
if (fs.existsSync(depStatePath)) {
|
|
513
|
-
try {
|
|
514
|
-
const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
|
|
515
|
-
if (state.completedTasks && state.completedTasks.includes(taskName)) {
|
|
516
|
-
logger.info(`✓ Dependency met: ${dep}`);
|
|
517
|
-
pendingDeps.delete(dep);
|
|
518
|
-
} else if (state.status === 'failed') {
|
|
519
|
-
throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
|
|
520
|
-
}
|
|
521
|
-
} catch (e: any) {
|
|
522
|
-
if (e.message.includes('Dependency failed')) throw e;
|
|
523
|
-
// Ignore parse errors, file might be being written
|
|
524
|
-
}
|
|
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`);
|
|
525
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(', ')}`);
|
|
526
626
|
}
|
|
527
|
-
|
|
528
|
-
if (pendingDeps.size > 0) {
|
|
529
|
-
await new Promise(resolve => setTimeout(resolve, 5000)); // Poll every 5 seconds
|
|
530
|
-
}
|
|
627
|
+
throw new Error(`Dependencies failed: ${result.failedDependencies.join(', ')}`);
|
|
531
628
|
}
|
|
532
629
|
}
|
|
533
630
|
|
|
534
631
|
/**
|
|
535
|
-
* Merge branches from dependency lanes
|
|
632
|
+
* Merge branches from dependency lanes with safe merge
|
|
536
633
|
*/
|
|
537
634
|
export async function mergeDependencyBranches(deps: string[], runDir: string, worktreeDir: string): Promise<void> {
|
|
538
635
|
if (!deps || deps.length === 0) return;
|
|
@@ -545,21 +642,34 @@ export async function mergeDependencyBranches(deps: string[], runDir: string, wo
|
|
|
545
642
|
if (!fs.existsSync(depStatePath)) continue;
|
|
546
643
|
|
|
547
644
|
try {
|
|
548
|
-
const state =
|
|
549
|
-
if (state
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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');
|
|
560
667
|
}
|
|
668
|
+
|
|
669
|
+
logger.success(`✓ Merged ${laneName}`);
|
|
561
670
|
} catch (e) {
|
|
562
671
|
logger.error(`Failed to merge branch from ${laneName}: ${e}`);
|
|
672
|
+
throw e;
|
|
563
673
|
}
|
|
564
674
|
}
|
|
565
675
|
}
|
|
@@ -614,10 +724,27 @@ export async function runTask({
|
|
|
614
724
|
// Apply dependency permissions
|
|
615
725
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
616
726
|
|
|
617
|
-
//
|
|
618
|
-
|
|
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
|
+
});
|
|
619
745
|
|
|
620
|
-
|
|
746
|
+
// Log ONLY the original prompt to keep logs clean
|
|
747
|
+
appendLog(convoPath, createConversationEntry('user', task.prompt, {
|
|
621
748
|
task: task.name,
|
|
622
749
|
model,
|
|
623
750
|
}));
|
|
@@ -627,18 +754,19 @@ export async function runTask({
|
|
|
627
754
|
events.emit('agent.prompt_sent', {
|
|
628
755
|
taskName: task.name,
|
|
629
756
|
model,
|
|
630
|
-
promptLength:
|
|
757
|
+
promptLength: wrappedPrompt.length,
|
|
631
758
|
});
|
|
632
759
|
|
|
633
760
|
const r1 = await cursorAgentSend({
|
|
634
761
|
workspaceDir: worktreeDir,
|
|
635
762
|
chatId,
|
|
636
|
-
prompt:
|
|
763
|
+
prompt: wrappedPrompt,
|
|
637
764
|
model,
|
|
638
765
|
signalDir: runDir,
|
|
639
766
|
timeout,
|
|
640
767
|
enableIntervention: config.enableIntervention,
|
|
641
768
|
outputFormat: config.agentOutputFormat,
|
|
769
|
+
taskName: task.name,
|
|
642
770
|
});
|
|
643
771
|
|
|
644
772
|
const duration = Date.now() - startTime;
|
|
@@ -669,15 +797,31 @@ export async function runTask({
|
|
|
669
797
|
};
|
|
670
798
|
}
|
|
671
799
|
|
|
672
|
-
// Check for dependency request
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
};
|
|
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
|
+
}
|
|
681
825
|
}
|
|
682
826
|
|
|
683
827
|
// Push task branch (skip in noGit mode)
|
|
@@ -732,7 +876,7 @@ export async function runTask({
|
|
|
732
876
|
/**
|
|
733
877
|
* Run all tasks in sequence
|
|
734
878
|
*/
|
|
735
|
-
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[]> {
|
|
736
880
|
const startIndex = options.startIndex || 0;
|
|
737
881
|
const noGit = options.noGit || config.noGit || false;
|
|
738
882
|
|
|
@@ -751,6 +895,33 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
751
895
|
throw validationError;
|
|
752
896
|
}
|
|
753
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
|
+
|
|
754
925
|
// Ensure cursor-agent is installed
|
|
755
926
|
ensureCursorAgent();
|
|
756
927
|
|
|
@@ -779,17 +950,47 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
779
950
|
logger.success('✓ Cursor authentication OK');
|
|
780
951
|
|
|
781
952
|
// In noGit mode, we don't need repoRoot - use current directory
|
|
782
|
-
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)`);
|
|
783
959
|
|
|
784
960
|
// Load existing state if resuming
|
|
785
961
|
const statePath = safeJoin(runDir, 'state.json');
|
|
786
962
|
let state: LaneState | null = null;
|
|
787
963
|
|
|
788
964
|
if (fs.existsSync(statePath)) {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
+
}
|
|
793
994
|
}
|
|
794
995
|
}
|
|
795
996
|
|
|
@@ -831,8 +1032,9 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
831
1032
|
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
832
1033
|
}
|
|
833
1034
|
|
|
1035
|
+
// Always use the current branch (already captured at start) as the base branch
|
|
834
1036
|
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
835
|
-
baseBranch:
|
|
1037
|
+
baseBranch: currentBranch,
|
|
836
1038
|
cwd: repoRoot,
|
|
837
1039
|
});
|
|
838
1040
|
break; // Success
|
|
@@ -993,11 +1195,22 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
993
1195
|
|
|
994
1196
|
// Run tasks
|
|
995
1197
|
const results: TaskExecutionResult[] = [];
|
|
1198
|
+
const laneName = state.label || path.basename(runDir);
|
|
996
1199
|
|
|
997
1200
|
for (let i = startIndex; i < config.tasks.length; i++) {
|
|
998
1201
|
const task = config.tasks[i]!;
|
|
999
1202
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
1000
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
|
+
|
|
1001
1214
|
// Handle task-level dependencies
|
|
1002
1215
|
if (task.dependsOn && task.dependsOn.length > 0) {
|
|
1003
1216
|
state.status = 'waiting';
|
|
@@ -1005,7 +1218,11 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
1005
1218
|
saveState(statePath, state);
|
|
1006
1219
|
|
|
1007
1220
|
try {
|
|
1008
|
-
|
|
1221
|
+
// Use enhanced dependency wait with timeout
|
|
1222
|
+
await waitForTaskDependencies(task.dependsOn, runDir, {
|
|
1223
|
+
timeoutMs: config.timeout || 30 * 60 * 1000,
|
|
1224
|
+
onTimeout: 'fail',
|
|
1225
|
+
});
|
|
1009
1226
|
|
|
1010
1227
|
if (!noGit) {
|
|
1011
1228
|
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
|
|
@@ -1020,6 +1237,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
1020
1237
|
state.error = e.message;
|
|
1021
1238
|
saveState(statePath, state);
|
|
1022
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
|
+
|
|
1023
1248
|
process.exit(1);
|
|
1024
1249
|
}
|
|
1025
1250
|
}
|
|
@@ -1119,7 +1344,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
1119
1344
|
logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
|
|
1120
1345
|
} else {
|
|
1121
1346
|
try {
|
|
1122
|
-
|
|
1347
|
+
// Always use current branch for comparison (already captured at start)
|
|
1348
|
+
const stats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
|
|
1123
1349
|
if (stats) {
|
|
1124
1350
|
logger.info('Final Workspace Summary (Git):\n' + stats);
|
|
1125
1351
|
}
|