@litmers/cursorflow-orchestrator 0.1.36 → 0.1.39
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 +8 -0
- package/commands/cursorflow-init.md +113 -32
- package/commands/cursorflow-prepare.md +146 -339
- package/commands/cursorflow-run.md +148 -131
- package/dist/cli/add.js +8 -4
- package/dist/cli/add.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/new.js +3 -5
- package/dist/cli/new.js.map +1 -1
- package/dist/cli/resume.js +26 -15
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +1 -6
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/setup-commands.d.ts +1 -0
- package/dist/cli/setup-commands.js +1 -0
- package/dist/cli/setup-commands.js.map +1 -1
- package/dist/core/runner/agent.d.ts +5 -1
- package/dist/core/runner/agent.js +34 -2
- package/dist/core/runner/agent.js.map +1 -1
- package/dist/core/runner/pipeline.d.ts +0 -1
- package/dist/core/runner/pipeline.js +116 -167
- package/dist/core/runner/pipeline.js.map +1 -1
- package/dist/core/runner/prompt.d.ts +0 -1
- package/dist/core/runner/prompt.js +11 -16
- package/dist/core/runner/prompt.js.map +1 -1
- package/dist/core/runner/task.d.ts +1 -2
- package/dist/core/runner/task.js +24 -36
- package/dist/core/runner/task.js.map +1 -1
- package/dist/core/runner.js +12 -2
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +16 -4
- package/dist/core/stall-detection.js +97 -148
- package/dist/core/stall-detection.js.map +1 -1
- package/dist/services/logging/console.d.ts +7 -1
- package/dist/services/logging/console.js +15 -3
- package/dist/services/logging/console.js.map +1 -1
- package/dist/services/logging/formatter.js +2 -0
- package/dist/services/logging/formatter.js.map +1 -1
- package/dist/types/config.d.ts +1 -1
- package/dist/types/logging.d.ts +1 -1
- package/dist/types/task.d.ts +2 -7
- package/dist/utils/doctor.js +4 -4
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/git.js +2 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/health.js +13 -13
- package/dist/utils/health.js.map +1 -1
- package/dist/utils/log-formatter.js +44 -7
- package/dist/utils/log-formatter.js.map +1 -1
- package/dist/utils/logger.js +2 -2
- package/dist/utils/logger.js.map +1 -1
- package/package.json +2 -1
- package/src/cli/add.ts +9 -4
- package/src/cli/index.ts +3 -0
- package/src/cli/new.ts +3 -5
- package/src/cli/resume.ts +30 -19
- package/src/cli/run.ts +1 -6
- package/src/cli/setup-commands.ts +1 -1
- package/src/core/runner/agent.ts +40 -5
- package/src/core/runner/pipeline.ts +127 -176
- package/src/core/runner/prompt.ts +11 -18
- package/src/core/runner/task.ts +24 -37
- package/src/core/runner.ts +13 -2
- package/src/core/stall-detection.ts +190 -146
- package/src/services/logging/console.ts +15 -3
- package/src/services/logging/formatter.ts +2 -0
- package/src/types/config.ts +1 -1
- package/src/types/logging.ts +4 -2
- package/src/types/task.ts +2 -7
- package/src/utils/doctor.ts +5 -5
- package/src/utils/git.ts +2 -0
- package/src/utils/health.ts +15 -15
- package/src/utils/log-formatter.ts +50 -7
- package/src/utils/logger.ts +2 -2
- package/commands/cursorflow-add.md +0 -159
- package/commands/cursorflow-clean.md +0 -84
- package/commands/cursorflow-doctor.md +0 -102
- package/commands/cursorflow-models.md +0 -51
- package/commands/cursorflow-monitor.md +0 -90
- package/commands/cursorflow-new.md +0 -87
- package/commands/cursorflow-resume.md +0 -205
- package/commands/cursorflow-signal.md +0 -52
- package/commands/cursorflow-stop.md +0 -55
- package/commands/cursorflow-triggers.md +0 -250
|
@@ -40,18 +40,13 @@ function validateTaskConfig(config: RunnerConfig): void {
|
|
|
40
40
|
/**
|
|
41
41
|
* Run all tasks in sequence
|
|
42
42
|
*/
|
|
43
|
-
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number;
|
|
43
|
+
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; skipPreflight?: boolean } = {}): Promise<TaskExecutionResult[]> {
|
|
44
44
|
const startIndex = options.startIndex || 0;
|
|
45
|
-
const noGit = options.noGit || config.noGit || false;
|
|
46
45
|
|
|
47
46
|
// Ensure paths are absolute before potentially changing directory
|
|
48
47
|
runDir = path.resolve(runDir);
|
|
49
48
|
tasksFile = path.resolve(tasksFile);
|
|
50
49
|
|
|
51
|
-
if (noGit) {
|
|
52
|
-
logger.info('🚫 Running in noGit mode - Git operations will be skipped');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
50
|
// Validate configuration before starting
|
|
56
51
|
logger.info('Validating task configuration...');
|
|
57
52
|
try {
|
|
@@ -67,7 +62,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
67
62
|
if (!options.skipPreflight && startIndex === 0) {
|
|
68
63
|
logger.info('Running preflight checks...');
|
|
69
64
|
const preflight = await preflightCheck({
|
|
70
|
-
requireRemote:
|
|
65
|
+
requireRemote: true,
|
|
71
66
|
requireAuth: true,
|
|
72
67
|
});
|
|
73
68
|
|
|
@@ -120,12 +115,11 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
120
115
|
|
|
121
116
|
logger.success('✓ Cursor authentication OK');
|
|
122
117
|
|
|
123
|
-
|
|
124
|
-
const repoRoot = noGit ? process.cwd() : git.getMainRepoRoot();
|
|
118
|
+
const repoRoot = git.getMainRepoRoot();
|
|
125
119
|
|
|
126
120
|
// ALWAYS use current branch as base - ignore config.baseBranch
|
|
127
121
|
// This ensures dependency structure is maintained in the worktree
|
|
128
|
-
const currentBranch =
|
|
122
|
+
const currentBranch = git.getCurrentBranch(repoRoot);
|
|
129
123
|
logger.info(`📍 Base branch: ${currentBranch} (current branch)`);
|
|
130
124
|
|
|
131
125
|
// Load existing state if resuming
|
|
@@ -149,8 +143,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
149
143
|
// Validate loaded state
|
|
150
144
|
if (state) {
|
|
151
145
|
const validation = validateLaneState(statePath, {
|
|
152
|
-
checkWorktree:
|
|
153
|
-
checkBranch:
|
|
146
|
+
checkWorktree: true,
|
|
147
|
+
checkBranch: true,
|
|
154
148
|
autoRepair: true,
|
|
155
149
|
});
|
|
156
150
|
|
|
@@ -168,11 +162,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
168
162
|
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
169
163
|
const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
|
|
170
164
|
|
|
171
|
-
// In noGit mode, use a simple local directory instead of worktree
|
|
172
165
|
// Flatten the path by replacing slashes with hyphens to avoid race conditions in parent directory creation
|
|
173
|
-
const worktreeDir = state?.worktreeDir || config.worktreeDir || (
|
|
174
|
-
? safeJoin(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
|
|
175
|
-
: safeJoin(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch.replace(/\//g, '-')));
|
|
166
|
+
const worktreeDir = state?.worktreeDir || config.worktreeDir || safeJoin(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch.replace(/\//g, '-'));
|
|
176
167
|
|
|
177
168
|
if (startIndex === 0) {
|
|
178
169
|
logger.section('🚀 Starting Pipeline');
|
|
@@ -186,46 +177,40 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
186
177
|
|
|
187
178
|
// Create worktree only if starting fresh and worktree doesn't exist
|
|
188
179
|
if (!fs.existsSync(worktreeDir)) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// Ensure parent directory exists before calling git worktree
|
|
201
|
-
const worktreeParent = path.dirname(worktreeDir);
|
|
202
|
-
if (!fs.existsSync(worktreeParent)) {
|
|
203
|
-
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
204
|
-
}
|
|
180
|
+
// Use a simple retry mechanism for Git worktree creation to handle potential race conditions
|
|
181
|
+
let retries = 3;
|
|
182
|
+
let lastError: Error | null = null;
|
|
183
|
+
|
|
184
|
+
while (retries > 0) {
|
|
185
|
+
try {
|
|
186
|
+
// Ensure parent directory exists before calling git worktree
|
|
187
|
+
const worktreeParent = path.dirname(worktreeDir);
|
|
188
|
+
if (!fs.existsSync(worktreeParent)) {
|
|
189
|
+
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
190
|
+
}
|
|
205
191
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
192
|
+
// Always use the current branch (already captured at start) as the base branch
|
|
193
|
+
await git.createWorktreeAsync(worktreeDir, pipelineBranch, {
|
|
194
|
+
baseBranch: currentBranch,
|
|
195
|
+
cwd: repoRoot,
|
|
196
|
+
});
|
|
197
|
+
break; // Success
|
|
198
|
+
} catch (e: any) {
|
|
199
|
+
lastError = e;
|
|
200
|
+
retries--;
|
|
201
|
+
if (retries > 0) {
|
|
202
|
+
const delay = Math.floor(Math.random() * 1000) + 500;
|
|
203
|
+
logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
|
|
204
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
220
205
|
}
|
|
221
206
|
}
|
|
222
|
-
|
|
223
|
-
if (retries === 0 && lastError) {
|
|
224
|
-
throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
|
|
225
|
-
}
|
|
226
207
|
}
|
|
227
|
-
|
|
228
|
-
|
|
208
|
+
|
|
209
|
+
if (retries === 0 && lastError) {
|
|
210
|
+
throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
// If it exists, ensure it's actually a worktree and on the right branch
|
|
229
214
|
logger.info(`Reusing existing worktree: ${worktreeDir}`);
|
|
230
215
|
try {
|
|
231
216
|
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
@@ -310,7 +295,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
310
295
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
311
296
|
|
|
312
297
|
// Delete previous task branch if it exists (Task 1 deleted when Task 2 starts, etc.)
|
|
313
|
-
if (
|
|
298
|
+
if (previousTaskBranch) {
|
|
314
299
|
logger.info(`🧹 Deleting previous task branch: ${previousTaskBranch}`);
|
|
315
300
|
try {
|
|
316
301
|
// Only delete if it's not the current branch
|
|
@@ -325,7 +310,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
325
310
|
|
|
326
311
|
// Create checkpoint before each task
|
|
327
312
|
try {
|
|
328
|
-
await createCheckpoint(laneName, runDir,
|
|
313
|
+
await createCheckpoint(laneName, runDir, worktreeDir, {
|
|
329
314
|
description: `Before task ${i + 1}: ${task.name}`,
|
|
330
315
|
maxCheckpoints: 5,
|
|
331
316
|
});
|
|
@@ -346,9 +331,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
346
331
|
onTimeout: 'fail',
|
|
347
332
|
});
|
|
348
333
|
|
|
349
|
-
|
|
350
|
-
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
|
|
351
|
-
}
|
|
334
|
+
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
|
|
352
335
|
|
|
353
336
|
state.status = 'running';
|
|
354
337
|
state.waitingFor = [];
|
|
@@ -380,7 +363,6 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
380
363
|
taskBranch,
|
|
381
364
|
chatId,
|
|
382
365
|
runDir,
|
|
383
|
-
noGit,
|
|
384
366
|
});
|
|
385
367
|
|
|
386
368
|
results.push(result);
|
|
@@ -418,105 +400,99 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
418
400
|
process.exit(1);
|
|
419
401
|
}
|
|
420
402
|
|
|
421
|
-
// Merge into pipeline
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
|
|
435
|
-
|
|
436
|
-
events.emit('merge.conflict_detected', {
|
|
437
|
-
taskName: task.name,
|
|
438
|
-
taskBranch,
|
|
439
|
-
pipelineBranch,
|
|
440
|
-
conflictingFiles: conflictCheck.conflictingFiles,
|
|
441
|
-
preCheck: true,
|
|
442
|
-
});
|
|
443
|
-
}
|
|
403
|
+
// Merge into pipeline
|
|
404
|
+
logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
|
|
405
|
+
|
|
406
|
+
// Ensure we are on the pipeline branch before merging the task branch
|
|
407
|
+
logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
|
|
408
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
409
|
+
|
|
410
|
+
// Pre-check for conflicts (should be rare since task branch was created from pipeline)
|
|
411
|
+
const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
|
|
412
|
+
if (conflictCheck.willConflict) {
|
|
413
|
+
logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`);
|
|
414
|
+
logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
|
|
415
|
+
logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
|
|
444
416
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
abortOnConflict: true,
|
|
417
|
+
events.emit('merge.conflict_detected', {
|
|
418
|
+
taskName: task.name,
|
|
419
|
+
taskBranch,
|
|
420
|
+
pipelineBranch,
|
|
421
|
+
conflictingFiles: conflictCheck.conflictingFiles,
|
|
422
|
+
preCheck: true,
|
|
452
423
|
});
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Use safeMerge instead of plain merge for better error handling
|
|
427
|
+
logger.info(`🔀 Merging task ${task.name} (${taskBranch}) into ${pipelineBranch}`);
|
|
428
|
+
const mergeResult = git.safeMerge(taskBranch, {
|
|
429
|
+
cwd: worktreeDir,
|
|
430
|
+
noFf: true,
|
|
431
|
+
message: `chore: merge task ${task.name} into pipeline`,
|
|
432
|
+
abortOnConflict: true,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
if (!mergeResult.success) {
|
|
436
|
+
if (mergeResult.conflict) {
|
|
437
|
+
logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
438
|
+
state.status = 'failed';
|
|
439
|
+
state.error = `Merge conflict when integrating task ${task.name}: ${mergeResult.conflictingFiles.join(', ')}`;
|
|
440
|
+
saveState(statePath, state);
|
|
441
|
+
process.exit(1);
|
|
469
442
|
}
|
|
470
|
-
|
|
471
|
-
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
472
|
-
} else {
|
|
473
|
-
logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
|
|
443
|
+
throw new Error(mergeResult.error || 'Merge failed');
|
|
474
444
|
}
|
|
475
445
|
|
|
446
|
+
// Log changed files
|
|
447
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
448
|
+
if (stats) {
|
|
449
|
+
logger.info('Changed files:\n' + stats);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
453
|
+
|
|
476
454
|
// Set previousTaskBranch for cleanup in the next iteration
|
|
477
|
-
previousTaskBranch =
|
|
455
|
+
previousTaskBranch = taskBranch;
|
|
478
456
|
}
|
|
479
457
|
|
|
480
458
|
// Final Consolidation: Create flow branch and cleanup
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
logger.warn(` Failed to delete last task branch: ${e}`);
|
|
492
|
-
}
|
|
459
|
+
const flowBranch = laneName;
|
|
460
|
+
logger.section(`🏁 Final Consolidation: ${flowBranch}`);
|
|
461
|
+
|
|
462
|
+
// 1. Delete the very last task branch
|
|
463
|
+
if (previousTaskBranch) {
|
|
464
|
+
logger.info(`🧹 Deleting last task branch: ${previousTaskBranch}`);
|
|
465
|
+
try {
|
|
466
|
+
git.deleteBranch(previousTaskBranch, { cwd: worktreeDir, force: true });
|
|
467
|
+
} catch (e) {
|
|
468
|
+
logger.warn(` Failed to delete last task branch: ${e}`);
|
|
493
469
|
}
|
|
470
|
+
}
|
|
494
471
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
472
|
+
// 2. Create flow branch from pipelineBranch and cleanup
|
|
473
|
+
if (flowBranch !== pipelineBranch) {
|
|
474
|
+
logger.info(`🌿 Creating final flow branch: ${flowBranch}`);
|
|
475
|
+
try {
|
|
476
|
+
// Create/Overwrite flow branch from pipeline branch
|
|
477
|
+
git.runGit(['checkout', '-B', flowBranch, pipelineBranch], { cwd: worktreeDir });
|
|
478
|
+
git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
|
|
479
|
+
|
|
480
|
+
// 3. Delete temporary pipeline branch
|
|
481
|
+
logger.info(`🗑️ Deleting temporary pipeline branch: ${pipelineBranch}`);
|
|
482
|
+
// Must be on another branch to delete pipelineBranch
|
|
483
|
+
git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
|
|
484
|
+
git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
|
|
485
|
+
|
|
498
486
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
// 3. Delete temporary pipeline branch
|
|
504
|
-
logger.info(`🗑️ Deleting temporary pipeline branch: ${pipelineBranch}`);
|
|
505
|
-
// Must be on another branch to delete pipelineBranch
|
|
506
|
-
git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
|
|
507
|
-
git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
|
|
508
|
-
|
|
509
|
-
try {
|
|
510
|
-
git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true, remote: true });
|
|
511
|
-
logger.info(` Deleted remote branch: origin/${pipelineBranch}`);
|
|
512
|
-
} catch {
|
|
513
|
-
// May not exist on remote or delete failed
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
logger.success(`✓ Flow branch '${flowBranch}' is now the only remaining branch.`);
|
|
517
|
-
} catch (e) {
|
|
518
|
-
logger.error(`❌ Failed during final consolidation: ${e}`);
|
|
487
|
+
git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true, remote: true });
|
|
488
|
+
logger.info(` Deleted remote branch: origin/${pipelineBranch}`);
|
|
489
|
+
} catch {
|
|
490
|
+
// May not exist on remote or delete failed
|
|
519
491
|
}
|
|
492
|
+
|
|
493
|
+
logger.success(`✓ Flow branch '${flowBranch}' is now the only remaining branch.`);
|
|
494
|
+
} catch (e) {
|
|
495
|
+
logger.error(`❌ Failed during final consolidation: ${e}`);
|
|
520
496
|
}
|
|
521
497
|
}
|
|
522
498
|
|
|
@@ -526,39 +502,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
526
502
|
saveState(statePath, state);
|
|
527
503
|
|
|
528
504
|
// Log final file summary
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
535
|
-
for (const entry of entries) {
|
|
536
|
-
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
|
|
537
|
-
|
|
538
|
-
if (entry.isDirectory()) {
|
|
539
|
-
stats.dirs++;
|
|
540
|
-
const sub = getFileSummary(safeJoin(dir, entry.name));
|
|
541
|
-
stats.files += sub.files;
|
|
542
|
-
stats.dirs += sub.dirs;
|
|
543
|
-
} else {
|
|
544
|
-
stats.files++;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return stats;
|
|
548
|
-
};
|
|
549
|
-
|
|
550
|
-
const summary = getFileSummary(worktreeDir);
|
|
551
|
-
logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
|
|
552
|
-
} else {
|
|
553
|
-
try {
|
|
554
|
-
// Always use current branch for comparison (already captured at start)
|
|
555
|
-
const stats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
|
|
556
|
-
if (stats) {
|
|
557
|
-
logger.info('Final Workspace Summary (Git):\n' + stats);
|
|
558
|
-
}
|
|
559
|
-
} catch (e) {
|
|
560
|
-
// Ignore
|
|
505
|
+
try {
|
|
506
|
+
// Always use current branch for comparison (already captured at start)
|
|
507
|
+
const finalStats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
|
|
508
|
+
if (finalStats) {
|
|
509
|
+
logger.info('Final Workspace Summary:\n' + finalStats);
|
|
561
510
|
}
|
|
511
|
+
} catch (e) {
|
|
512
|
+
// Ignore
|
|
562
513
|
}
|
|
563
514
|
|
|
564
515
|
logger.success('All tasks completed!');
|
|
@@ -32,7 +32,6 @@ export function wrapPrompt(
|
|
|
32
32
|
prompt: string,
|
|
33
33
|
config: RunnerConfig,
|
|
34
34
|
options: {
|
|
35
|
-
noGit?: boolean;
|
|
36
35
|
isWorktree?: boolean;
|
|
37
36
|
dependencyResults?: DependencyResult[];
|
|
38
37
|
worktreePath?: string;
|
|
@@ -40,18 +39,18 @@ export function wrapPrompt(
|
|
|
40
39
|
pipelineBranch?: string;
|
|
41
40
|
} = {}
|
|
42
41
|
): string {
|
|
43
|
-
const {
|
|
42
|
+
const { isWorktree = true, dependencyResults = [], worktreePath, taskBranch, pipelineBranch } = options;
|
|
44
43
|
|
|
45
44
|
// 1. PREFIX: Environment & Worktree context
|
|
46
45
|
let wrapped = `### 🛠 Environment & Context\n`;
|
|
47
46
|
wrapped += `- **Workspace**: 당신은 독립된 **Git 워크트리** (프로젝트 루트)에서 작업 중입니다.\n`;
|
|
48
47
|
wrapped += `- **CWD**: 현재 터미널과 작업 경로는 이미 워크트리 루트(\`${worktreePath || 'current'}\`)로 설정되어 있습니다.\n`;
|
|
49
48
|
|
|
50
|
-
if (taskBranch
|
|
49
|
+
if (taskBranch) {
|
|
51
50
|
wrapped += `- **Current Branch**: \`${taskBranch}\` (현재 작업 중인 브랜치)\n`;
|
|
52
51
|
wrapped += `- **Branch Check**: 만약 브랜치가 다르다면 \`git checkout ${taskBranch}\`를 실행하세요.\n`;
|
|
53
52
|
}
|
|
54
|
-
if (pipelineBranch
|
|
53
|
+
if (pipelineBranch) {
|
|
55
54
|
wrapped += `- **Base Branch**: \`${pipelineBranch}\` (이 작업의 기준이 되는 상위 브랜치)\n`;
|
|
56
55
|
}
|
|
57
56
|
|
|
@@ -97,10 +96,6 @@ export function wrapPrompt(
|
|
|
97
96
|
wrapped += `\n### 📦 Dependency Policy\n`;
|
|
98
97
|
wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
99
98
|
wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
|
|
100
|
-
|
|
101
|
-
if (noGit) {
|
|
102
|
-
wrapped += `- NO_GIT_MODE: Git 명령어를 사용하지 마세요. 파일 수정만 가능합니다.\n`;
|
|
103
|
-
}
|
|
104
99
|
|
|
105
100
|
wrapped += `\n**📦 Dependency Change Rules:**\n`;
|
|
106
101
|
wrapped += `1. 코드를 수정하기 전, 의존성 변경이 필요한지 **먼저** 판단하세요.\n`;
|
|
@@ -124,16 +119,14 @@ export function wrapPrompt(
|
|
|
124
119
|
wrapped += `\n### 📝 Task Completion Requirements\n`;
|
|
125
120
|
wrapped += `**반드시 다음 순서로 작업을 마무리하세요 (매우 중요):**\n\n`;
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
wrapped += ` ⚠️ **주의**: 커밋과 푸시를 생략하면 오케스트레이터가 변경 사항을 인식하지 못하며 작업이 손실됩니다.\n\n`;
|
|
136
|
-
}
|
|
122
|
+
wrapped += `1. **변경 사항 확인**: \`git status\`와 \`git diff\`로 수정된 내용을 최종 확인하세요.\n`;
|
|
123
|
+
wrapped += `2. **Git Commit & Push** (필수!):\n`;
|
|
124
|
+
wrapped += ` \`\`\`bash\n`;
|
|
125
|
+
wrapped += ` git add -A\n`;
|
|
126
|
+
wrapped += ` git commit -m "feat: <작업 내용 요약>"\n`;
|
|
127
|
+
wrapped += ` git push origin HEAD\n`;
|
|
128
|
+
wrapped += ` \`\`\`\n`;
|
|
129
|
+
wrapped += ` ⚠️ **주의**: 커밋과 푸시를 생략하면 오케스트레이터가 변경 사항을 인식하지 못하며 작업이 손실됩니다.\n\n`;
|
|
137
130
|
|
|
138
131
|
wrapped += `3. **최종 요약**: 작업 완료 후 아래 형식을 포함하여 요약해 주세요:\n`;
|
|
139
132
|
wrapped += ` - **수정된 파일**: [파일명1, 파일명2, ...]\n`;
|
package/src/core/runner/task.ts
CHANGED
|
@@ -137,7 +137,6 @@ export async function runTask({
|
|
|
137
137
|
chatId,
|
|
138
138
|
runDir,
|
|
139
139
|
runRoot,
|
|
140
|
-
noGit = false,
|
|
141
140
|
}: {
|
|
142
141
|
task: Task;
|
|
143
142
|
config: RunnerConfig;
|
|
@@ -148,7 +147,6 @@ export async function runTask({
|
|
|
148
147
|
chatId: string;
|
|
149
148
|
runDir: string;
|
|
150
149
|
runRoot?: string;
|
|
151
|
-
noGit?: boolean;
|
|
152
150
|
}): Promise<TaskExecutionResult> {
|
|
153
151
|
// Calculate runRoot if not provided (runDir is lanes/{laneName}/, runRoot is parent of lanes/)
|
|
154
152
|
const calculatedRunRoot = runRoot || path.dirname(path.dirname(runDir));
|
|
@@ -158,11 +156,7 @@ export async function runTask({
|
|
|
158
156
|
|
|
159
157
|
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
160
158
|
logger.info(`Model: ${model}`);
|
|
161
|
-
|
|
162
|
-
logger.info('🚫 noGit mode: skipping branch operations');
|
|
163
|
-
} else {
|
|
164
|
-
logger.info(`Branch: ${taskBranch}`);
|
|
165
|
-
}
|
|
159
|
+
logger.info(`Branch: ${taskBranch}`);
|
|
166
160
|
|
|
167
161
|
events.emit('task.started', {
|
|
168
162
|
taskName: task.name,
|
|
@@ -170,32 +164,28 @@ export async function runTask({
|
|
|
170
164
|
index,
|
|
171
165
|
});
|
|
172
166
|
|
|
173
|
-
// Sync pipelineBranch with remote before starting
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
logger.info(` Branch ${pipelineBranch} not yet on remote, skipping sync`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Try to fast-forward if behind
|
|
186
|
-
const syncResult = git.syncBranchWithRemote(pipelineBranch, { cwd: worktreeDir, createIfMissing: true });
|
|
187
|
-
if (syncResult.updated) {
|
|
188
|
-
logger.info(` ✓ Updated ${pipelineBranch} with ${syncResult.behind || 0} new commits from remote`);
|
|
189
|
-
} else if (syncResult.error) {
|
|
190
|
-
logger.warn(` ⚠️ Could not sync: ${syncResult.error}`);
|
|
191
|
-
}
|
|
167
|
+
// Sync pipelineBranch with remote before starting
|
|
168
|
+
logger.info(`🔄 Syncing ${pipelineBranch} with remote...`);
|
|
169
|
+
|
|
170
|
+
// Fetch latest from remote
|
|
171
|
+
try {
|
|
172
|
+
git.runGit(['fetch', 'origin', pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
173
|
+
} catch {
|
|
174
|
+
// Branch might not exist on remote yet - that's OK
|
|
175
|
+
logger.info(` Branch ${pipelineBranch} not yet on remote, skipping sync`);
|
|
192
176
|
}
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
177
|
+
|
|
178
|
+
// Try to fast-forward if behind
|
|
179
|
+
const syncResult = git.syncBranchWithRemote(pipelineBranch, { cwd: worktreeDir, createIfMissing: true });
|
|
180
|
+
if (syncResult.updated) {
|
|
181
|
+
logger.info(` ✓ Updated ${pipelineBranch} with ${syncResult.behind || 0} new commits from remote`);
|
|
182
|
+
} else if (syncResult.error) {
|
|
183
|
+
logger.warn(` ⚠️ Could not sync: ${syncResult.error}`);
|
|
198
184
|
}
|
|
185
|
+
|
|
186
|
+
// Checkout task branch from pipeline branch
|
|
187
|
+
logger.info(`🌿 Forking task branch: ${taskBranch} from ${pipelineBranch}`);
|
|
188
|
+
git.runGit(['checkout', '-B', taskBranch, pipelineBranch], { cwd: worktreeDir });
|
|
199
189
|
|
|
200
190
|
// Apply dependency permissions
|
|
201
191
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
@@ -208,8 +198,7 @@ export async function runTask({
|
|
|
208
198
|
|
|
209
199
|
// Wrap prompt with context, dependency results, and completion instructions
|
|
210
200
|
const wrappedPrompt = wrapPrompt(task.prompt, config, {
|
|
211
|
-
|
|
212
|
-
isWorktree: !noGit,
|
|
201
|
+
isWorktree: true,
|
|
213
202
|
dependencyResults,
|
|
214
203
|
worktreePath: worktreeDir,
|
|
215
204
|
taskBranch,
|
|
@@ -297,10 +286,8 @@ export async function runTask({
|
|
|
297
286
|
}
|
|
298
287
|
}
|
|
299
288
|
|
|
300
|
-
// Push task branch
|
|
301
|
-
|
|
302
|
-
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
303
|
-
}
|
|
289
|
+
// Push task branch
|
|
290
|
+
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
304
291
|
|
|
305
292
|
// Save task result for dependency handoff
|
|
306
293
|
saveTaskResult(runDir, index, task.name, r1.resultText || '');
|
package/src/core/runner.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from './runner/index';
|
|
|
18
18
|
|
|
19
19
|
// Import necessary parts for the CLI entry point
|
|
20
20
|
import { runTasks } from './runner/pipeline';
|
|
21
|
+
import { cleanupAgentChildren } from './runner/agent';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* CLI entry point
|
|
@@ -35,7 +36,6 @@ if (require.main === module) {
|
|
|
35
36
|
const startIdxIdx = args.indexOf('--start-index');
|
|
36
37
|
const pipelineBranchIdx = args.indexOf('--pipeline-branch');
|
|
37
38
|
const worktreeDirIdx = args.indexOf('--worktree-dir');
|
|
38
|
-
const noGit = args.includes('--no-git');
|
|
39
39
|
|
|
40
40
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
41
41
|
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
@@ -89,8 +89,19 @@ if (require.main === module) {
|
|
|
89
89
|
// Add agent output format default
|
|
90
90
|
config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'json';
|
|
91
91
|
|
|
92
|
+
// Handle process interruption to ensure cleanup
|
|
93
|
+
const handleSignal = (signal: string) => {
|
|
94
|
+
logger.warn(`\n⚠️ Runner received ${signal}. Shutting down...`);
|
|
95
|
+
// Cleanup any active agent child processes
|
|
96
|
+
cleanupAgentChildren();
|
|
97
|
+
process.exit(1);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
process.on('SIGINT', () => handleSignal('SIGINT'));
|
|
101
|
+
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
102
|
+
|
|
92
103
|
// Run tasks
|
|
93
|
-
runTasks(tasksFile, config, runDir, { startIndex
|
|
104
|
+
runTasks(tasksFile, config, runDir, { startIndex })
|
|
94
105
|
.then(() => {
|
|
95
106
|
process.exit(0);
|
|
96
107
|
})
|