@litmers/cursorflow-orchestrator 0.1.37 → 0.1.40
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/README.md +13 -13
- 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/prepare.js +0 -1
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +24 -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/orchestrator.js +13 -5
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +5 -1
- package/dist/core/runner/agent.js +31 -1
- package/dist/core/runner/agent.js.map +1 -1
- package/dist/core/runner/pipeline.d.ts +0 -1
- package/dist/core/runner/pipeline.js +136 -173
- 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 +31 -40
- package/dist/core/runner/task.js.map +1 -1
- package/dist/core/runner.js +15 -2
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +32 -4
- package/dist/core/stall-detection.js +151 -149
- package/dist/core/stall-detection.js.map +1 -1
- package/dist/services/logging/console.d.ts +7 -1
- package/dist/services/logging/console.js +13 -3
- package/dist/services/logging/console.js.map +1 -1
- package/dist/services/logging/formatter.d.ts +1 -0
- package/dist/services/logging/formatter.js +6 -3
- package/dist/services/logging/formatter.js.map +1 -1
- package/dist/types/config.d.ts +3 -1
- package/dist/types/logging.d.ts +1 -1
- package/dist/types/task.d.ts +3 -8
- package/dist/utils/config.js +5 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +4 -4
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +1 -1
- package/dist/utils/enhanced-logger.js +3 -3
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +12 -1
- package/dist/utils/git.js +56 -1
- 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.d.ts +1 -1
- package/dist/utils/log-formatter.js +45 -8
- 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 +1 -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/prepare.ts +0 -1
- package/src/cli/resume.ts +28 -19
- package/src/cli/run.ts +1 -6
- package/src/cli/setup-commands.ts +1 -1
- package/src/core/orchestrator.ts +14 -5
- package/src/core/runner/agent.ts +36 -4
- package/src/core/runner/pipeline.ts +149 -182
- package/src/core/runner/prompt.ts +11 -18
- package/src/core/runner/task.ts +32 -41
- package/src/core/runner.ts +17 -2
- package/src/core/stall-detection.ts +263 -147
- package/src/services/logging/console.ts +13 -3
- package/src/services/logging/formatter.ts +6 -3
- package/src/types/config.ts +3 -1
- package/src/types/logging.ts +4 -2
- package/src/types/task.ts +3 -8
- package/src/utils/config.ts +6 -0
- package/src/utils/doctor.ts +5 -5
- package/src/utils/enhanced-logger.ts +3 -3
- package/src/utils/flow.ts +1 -0
- package/src/utils/git.ts +61 -1
- package/src/utils/health.ts +15 -15
- package/src/utils/log-formatter.ts +51 -8
- 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');
|
|
@@ -184,54 +175,65 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
184
175
|
logger.info(`Worktree: ${worktreeDir}`);
|
|
185
176
|
logger.info(`Tasks: ${config.tasks.length}`);
|
|
186
177
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
178
|
+
// Check if worktree needs to be created or repaired
|
|
179
|
+
const worktreeNeedsCreation = !fs.existsSync(worktreeDir);
|
|
180
|
+
const worktreeIsInvalid = !worktreeNeedsCreation && !git.isValidWorktree(worktreeDir);
|
|
181
|
+
|
|
182
|
+
if (worktreeIsInvalid) {
|
|
183
|
+
// Directory exists but is NOT a valid worktree - this can cause branch leakage!
|
|
184
|
+
// Clean it up and recreate
|
|
185
|
+
logger.warn(`⚠️ Directory exists but is not a valid worktree: ${worktreeDir}`);
|
|
186
|
+
logger.info(` Cleaning up invalid directory and recreating worktree...`);
|
|
187
|
+
try {
|
|
188
|
+
git.cleanupInvalidWorktreeDir(worktreeDir);
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
logger.error(`Failed to cleanup invalid worktree directory: ${e.message}`);
|
|
191
|
+
throw new Error(`Cannot proceed: worktree directory is invalid and cleanup failed`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Create worktree if it doesn't exist or was just cleaned up
|
|
196
|
+
if (worktreeNeedsCreation || worktreeIsInvalid) {
|
|
197
|
+
// Use a simple retry mechanism for Git worktree creation to handle potential race conditions
|
|
198
|
+
let retries = 3;
|
|
199
|
+
let lastError: Error | null = null;
|
|
200
|
+
|
|
201
|
+
while (retries > 0) {
|
|
202
|
+
try {
|
|
203
|
+
// Ensure parent directory exists before calling git worktree
|
|
204
|
+
const worktreeParent = path.dirname(worktreeDir);
|
|
205
|
+
if (!fs.existsSync(worktreeParent)) {
|
|
206
|
+
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
207
|
+
}
|
|
205
208
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
209
|
+
// Always use the current branch (already captured at start) as the base branch
|
|
210
|
+
await git.createWorktreeAsync(worktreeDir, pipelineBranch, {
|
|
211
|
+
baseBranch: currentBranch,
|
|
212
|
+
cwd: repoRoot,
|
|
213
|
+
});
|
|
214
|
+
break; // Success
|
|
215
|
+
} catch (e: any) {
|
|
216
|
+
lastError = e;
|
|
217
|
+
retries--;
|
|
218
|
+
if (retries > 0) {
|
|
219
|
+
const delay = Math.floor(Math.random() * 1000) + 500;
|
|
220
|
+
logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
|
|
221
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
|
-
|
|
223
|
-
if (retries === 0 && lastError) {
|
|
224
|
-
throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
|
|
225
|
-
}
|
|
226
224
|
}
|
|
227
|
-
|
|
228
|
-
|
|
225
|
+
|
|
226
|
+
if (retries === 0 && lastError) {
|
|
227
|
+
throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
// Worktree exists and is valid - reuse it
|
|
229
231
|
logger.info(`Reusing existing worktree: ${worktreeDir}`);
|
|
230
232
|
try {
|
|
231
233
|
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
232
234
|
} catch (e) {
|
|
233
|
-
// If checkout fails
|
|
234
|
-
//
|
|
235
|
+
// If checkout fails in a valid worktree, log warning but continue
|
|
236
|
+
// The worktree might be on a different branch that will be handled later
|
|
235
237
|
logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
|
|
236
238
|
}
|
|
237
239
|
}
|
|
@@ -310,7 +312,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
310
312
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
311
313
|
|
|
312
314
|
// Delete previous task branch if it exists (Task 1 deleted when Task 2 starts, etc.)
|
|
313
|
-
if (
|
|
315
|
+
if (previousTaskBranch) {
|
|
314
316
|
logger.info(`🧹 Deleting previous task branch: ${previousTaskBranch}`);
|
|
315
317
|
try {
|
|
316
318
|
// Only delete if it's not the current branch
|
|
@@ -325,7 +327,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
325
327
|
|
|
326
328
|
// Create checkpoint before each task
|
|
327
329
|
try {
|
|
328
|
-
await createCheckpoint(laneName, runDir,
|
|
330
|
+
await createCheckpoint(laneName, runDir, worktreeDir, {
|
|
329
331
|
description: `Before task ${i + 1}: ${task.name}`,
|
|
330
332
|
maxCheckpoints: 5,
|
|
331
333
|
});
|
|
@@ -346,9 +348,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
346
348
|
onTimeout: 'fail',
|
|
347
349
|
});
|
|
348
350
|
|
|
349
|
-
|
|
350
|
-
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
|
|
351
|
-
}
|
|
351
|
+
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
|
|
352
352
|
|
|
353
353
|
state.status = 'running';
|
|
354
354
|
state.waitingFor = [];
|
|
@@ -380,7 +380,6 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
380
380
|
taskBranch,
|
|
381
381
|
chatId,
|
|
382
382
|
runDir,
|
|
383
|
-
noGit,
|
|
384
383
|
});
|
|
385
384
|
|
|
386
385
|
results.push(result);
|
|
@@ -418,105 +417,98 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
418
417
|
process.exit(1);
|
|
419
418
|
}
|
|
420
419
|
|
|
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
|
-
}
|
|
420
|
+
// Merge into pipeline
|
|
421
|
+
logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
|
|
422
|
+
|
|
423
|
+
// Ensure we are on the pipeline branch before merging the task branch
|
|
424
|
+
logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
|
|
425
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
426
|
+
|
|
427
|
+
// Pre-check for conflicts (should be rare since task branch was created from pipeline)
|
|
428
|
+
const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
|
|
429
|
+
if (conflictCheck.willConflict) {
|
|
430
|
+
logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`);
|
|
431
|
+
logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
|
|
432
|
+
logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
|
|
444
433
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
abortOnConflict: true,
|
|
434
|
+
events.emit('merge.conflict_detected', {
|
|
435
|
+
taskName: task.name,
|
|
436
|
+
taskBranch,
|
|
437
|
+
pipelineBranch,
|
|
438
|
+
conflictingFiles: conflictCheck.conflictingFiles,
|
|
439
|
+
preCheck: true,
|
|
452
440
|
});
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Use safeMerge instead of plain merge for better error handling
|
|
444
|
+
logger.info(`🔀 Merging task ${task.name} (${taskBranch}) into ${pipelineBranch}`);
|
|
445
|
+
const mergeResult = git.safeMerge(taskBranch, {
|
|
446
|
+
cwd: worktreeDir,
|
|
447
|
+
noFf: true,
|
|
448
|
+
message: `chore: merge task ${task.name} into pipeline`,
|
|
449
|
+
abortOnConflict: true,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
if (!mergeResult.success) {
|
|
453
|
+
if (mergeResult.conflict) {
|
|
454
|
+
logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
455
|
+
state.status = 'failed';
|
|
456
|
+
state.error = `Merge conflict when integrating task ${task.name}: ${mergeResult.conflictingFiles.join(', ')}`;
|
|
457
|
+
saveState(statePath, state);
|
|
458
|
+
process.exit(1);
|
|
469
459
|
}
|
|
470
|
-
|
|
471
|
-
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
472
|
-
} else {
|
|
473
|
-
logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
|
|
460
|
+
throw new Error(mergeResult.error || 'Merge failed');
|
|
474
461
|
}
|
|
475
462
|
|
|
463
|
+
// Log changed files
|
|
464
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
465
|
+
if (stats) {
|
|
466
|
+
logger.info('Changed files:\n' + stats);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
470
|
+
|
|
476
471
|
// Set previousTaskBranch for cleanup in the next iteration
|
|
477
|
-
previousTaskBranch =
|
|
472
|
+
previousTaskBranch = taskBranch;
|
|
478
473
|
}
|
|
479
474
|
|
|
480
475
|
// 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
|
-
}
|
|
476
|
+
const flowBranch = laneName;
|
|
477
|
+
logger.section(`🏁 Final Consolidation: ${flowBranch}`);
|
|
478
|
+
|
|
479
|
+
// 1. Delete the very last task branch
|
|
480
|
+
if (previousTaskBranch) {
|
|
481
|
+
logger.info(`🧹 Deleting last task branch: ${previousTaskBranch}`);
|
|
482
|
+
try {
|
|
483
|
+
git.deleteBranch(previousTaskBranch, { cwd: worktreeDir, force: true });
|
|
484
|
+
} catch (e) {
|
|
485
|
+
logger.warn(` Failed to delete last task branch: ${e}`);
|
|
493
486
|
}
|
|
487
|
+
}
|
|
494
488
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
logger.error(`❌ Failed during final consolidation: ${e}`);
|
|
519
|
-
}
|
|
489
|
+
// 2. Create flow branch from pipelineBranch and cleanup
|
|
490
|
+
if (flowBranch !== pipelineBranch) {
|
|
491
|
+
logger.info(`🌿 Creating final flow branch: ${flowBranch}`);
|
|
492
|
+
try {
|
|
493
|
+
// Create/Overwrite flow branch from pipeline branch
|
|
494
|
+
git.runGit(['checkout', '-B', flowBranch, pipelineBranch], { cwd: worktreeDir });
|
|
495
|
+
git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
|
|
496
|
+
|
|
497
|
+
// 3. Delete temporary pipeline branch (LOCAL ONLY)
|
|
498
|
+
// Keep remote branch for dependency lanes that may need to merge it!
|
|
499
|
+
logger.info(`🗑️ Deleting local pipeline branch: ${pipelineBranch}`);
|
|
500
|
+
// Must be on another branch to delete pipelineBranch
|
|
501
|
+
git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
|
|
502
|
+
git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
|
|
503
|
+
|
|
504
|
+
// NOTE: We intentionally keep the remote pipeline branch alive
|
|
505
|
+
// because other lanes with dependsOn may need to merge it.
|
|
506
|
+
// The pipeline branch on remote serves as the "official" completion branch
|
|
507
|
+
// that dependency-tracking code in task.ts uses (via state.pipelineBranch).
|
|
508
|
+
|
|
509
|
+
logger.success(`✓ Flow branch '${flowBranch}' created. Remote pipeline branch preserved for dependencies.`);
|
|
510
|
+
} catch (e) {
|
|
511
|
+
logger.error(`❌ Failed during final consolidation: ${e}`);
|
|
520
512
|
}
|
|
521
513
|
}
|
|
522
514
|
|
|
@@ -526,39 +518,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
526
518
|
saveState(statePath, state);
|
|
527
519
|
|
|
528
520
|
// 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
|
|
521
|
+
try {
|
|
522
|
+
// Always use current branch for comparison (already captured at start)
|
|
523
|
+
const finalStats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
|
|
524
|
+
if (finalStats) {
|
|
525
|
+
logger.info('Final Workspace Summary:\n' + finalStats);
|
|
561
526
|
}
|
|
527
|
+
} catch (e) {
|
|
528
|
+
// Ignore
|
|
562
529
|
}
|
|
563
530
|
|
|
564
531
|
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
|
@@ -79,11 +79,15 @@ export async function mergeDependencyBranches(deps: string[], runDir: string, wo
|
|
|
79
79
|
|
|
80
80
|
logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
|
|
81
81
|
|
|
82
|
-
// Ensure we have the latest
|
|
82
|
+
// Ensure we have the latest from remote
|
|
83
83
|
git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
84
84
|
|
|
85
|
+
// Use the remote ref for merging (origin/<branch>) since dependency branches
|
|
86
|
+
// are pushed to remote by other lanes and may not exist as local branches
|
|
87
|
+
const remoteBranchRef = `origin/${state.pipelineBranch}`;
|
|
88
|
+
|
|
85
89
|
// Pre-check for conflicts before attempting merge
|
|
86
|
-
const conflictCheck = git.checkMergeConflict(
|
|
90
|
+
const conflictCheck = git.checkMergeConflict(remoteBranchRef, { cwd: worktreeDir });
|
|
87
91
|
|
|
88
92
|
if (conflictCheck.willConflict) {
|
|
89
93
|
logger.warn(`⚠️ Pre-check: Merge conflict detected with ${laneName}`);
|
|
@@ -100,8 +104,8 @@ export async function mergeDependencyBranches(deps: string[], runDir: string, wo
|
|
|
100
104
|
throw new Error(`Pre-merge conflict check failed: ${conflictCheck.conflictingFiles.join(', ')}. Consider rebasing or resolving conflicts manually.`);
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
// Use safe merge with conflict detection
|
|
104
|
-
const mergeResult = git.safeMerge(
|
|
107
|
+
// Use safe merge with conflict detection - merge from remote ref
|
|
108
|
+
const mergeResult = git.safeMerge(remoteBranchRef, {
|
|
105
109
|
cwd: worktreeDir,
|
|
106
110
|
noFf: true,
|
|
107
111
|
message: `chore: merge task dependency from ${laneName}`,
|
|
@@ -137,7 +141,6 @@ export async function runTask({
|
|
|
137
141
|
chatId,
|
|
138
142
|
runDir,
|
|
139
143
|
runRoot,
|
|
140
|
-
noGit = false,
|
|
141
144
|
}: {
|
|
142
145
|
task: Task;
|
|
143
146
|
config: RunnerConfig;
|
|
@@ -148,7 +151,6 @@ export async function runTask({
|
|
|
148
151
|
chatId: string;
|
|
149
152
|
runDir: string;
|
|
150
153
|
runRoot?: string;
|
|
151
|
-
noGit?: boolean;
|
|
152
154
|
}): Promise<TaskExecutionResult> {
|
|
153
155
|
// Calculate runRoot if not provided (runDir is lanes/{laneName}/, runRoot is parent of lanes/)
|
|
154
156
|
const calculatedRunRoot = runRoot || path.dirname(path.dirname(runDir));
|
|
@@ -158,11 +160,7 @@ export async function runTask({
|
|
|
158
160
|
|
|
159
161
|
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
160
162
|
logger.info(`Model: ${model}`);
|
|
161
|
-
|
|
162
|
-
logger.info('🚫 noGit mode: skipping branch operations');
|
|
163
|
-
} else {
|
|
164
|
-
logger.info(`Branch: ${taskBranch}`);
|
|
165
|
-
}
|
|
163
|
+
logger.info(`Branch: ${taskBranch}`);
|
|
166
164
|
|
|
167
165
|
events.emit('task.started', {
|
|
168
166
|
taskName: task.name,
|
|
@@ -170,32 +168,28 @@ export async function runTask({
|
|
|
170
168
|
index,
|
|
171
169
|
});
|
|
172
170
|
|
|
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
|
-
}
|
|
171
|
+
// Sync pipelineBranch with remote before starting
|
|
172
|
+
logger.info(`🔄 Syncing ${pipelineBranch} with remote...`);
|
|
173
|
+
|
|
174
|
+
// Fetch latest from remote
|
|
175
|
+
try {
|
|
176
|
+
git.runGit(['fetch', 'origin', pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
177
|
+
} catch {
|
|
178
|
+
// Branch might not exist on remote yet - that's OK
|
|
179
|
+
logger.info(` Branch ${pipelineBranch} not yet on remote, skipping sync`);
|
|
192
180
|
}
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
181
|
+
|
|
182
|
+
// Try to fast-forward if behind
|
|
183
|
+
const syncResult = git.syncBranchWithRemote(pipelineBranch, { cwd: worktreeDir, createIfMissing: true });
|
|
184
|
+
if (syncResult.updated) {
|
|
185
|
+
logger.info(` ✓ Updated ${pipelineBranch} with ${syncResult.behind || 0} new commits from remote`);
|
|
186
|
+
} else if (syncResult.error) {
|
|
187
|
+
logger.warn(` ⚠️ Could not sync: ${syncResult.error}`);
|
|
198
188
|
}
|
|
189
|
+
|
|
190
|
+
// Checkout task branch from pipeline branch
|
|
191
|
+
logger.info(`🌿 Forking task branch: ${taskBranch} from ${pipelineBranch}`);
|
|
192
|
+
git.runGit(['checkout', '-B', taskBranch, pipelineBranch], { cwd: worktreeDir });
|
|
199
193
|
|
|
200
194
|
// Apply dependency permissions
|
|
201
195
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
@@ -208,8 +202,7 @@ export async function runTask({
|
|
|
208
202
|
|
|
209
203
|
// Wrap prompt with context, dependency results, and completion instructions
|
|
210
204
|
const wrappedPrompt = wrapPrompt(task.prompt, config, {
|
|
211
|
-
|
|
212
|
-
isWorktree: !noGit,
|
|
205
|
+
isWorktree: true,
|
|
213
206
|
dependencyResults,
|
|
214
207
|
worktreePath: worktreeDir,
|
|
215
208
|
taskBranch,
|
|
@@ -297,10 +290,8 @@ export async function runTask({
|
|
|
297
290
|
}
|
|
298
291
|
}
|
|
299
292
|
|
|
300
|
-
// Push task branch
|
|
301
|
-
|
|
302
|
-
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
303
|
-
}
|
|
293
|
+
// Push task branch
|
|
294
|
+
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
304
295
|
|
|
305
296
|
// Save task result for dependency handoff
|
|
306
297
|
saveTaskResult(runDir, index, task.name, r1.resultText || '');
|