@litmers/cursorflow-orchestrator 0.1.14 → 0.1.18
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/README.md +1 -0
- package/commands/cursorflow-run.md +2 -0
- package/commands/cursorflow-triggers.md +250 -0
- package/dist/cli/clean.js +1 -1
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/init.js +13 -8
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +66 -44
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +12 -3
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +36 -13
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +7 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/core/orchestrator.d.ts +3 -1
- package/dist/core/orchestrator.js +154 -11
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +8 -4
- package/dist/core/reviewer.js +11 -7
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +17 -3
- package/dist/core/runner.js +326 -69
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +17 -5
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +28 -1
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +5 -4
- package/dist/utils/enhanced-logger.js +178 -43
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +6 -0
- package/dist/utils/git.js +15 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +4 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/repro-thinking-logs.d.ts +1 -0
- package/dist/utils/repro-thinking-logs.js +80 -0
- package/dist/utils/repro-thinking-logs.js.map +1 -0
- package/dist/utils/types.d.ts +22 -0
- package/dist/utils/webhook.js +3 -0
- package/dist/utils/webhook.js.map +1 -1
- package/package.json +4 -1
- package/scripts/ai-security-check.js +3 -0
- package/scripts/local-security-gate.sh +9 -1
- package/scripts/patches/test-cursor-agent.js +1 -1
- package/scripts/verify-and-fix.sh +37 -0
- package/src/cli/clean.ts +1 -1
- package/src/cli/init.ts +12 -9
- package/src/cli/logs.ts +68 -43
- package/src/cli/monitor.ts +13 -4
- package/src/cli/prepare.ts +36 -15
- package/src/cli/resume.ts +1 -1
- package/src/cli/run.ts +8 -0
- package/src/core/orchestrator.ts +171 -11
- package/src/core/reviewer.ts +30 -11
- package/src/core/runner.ts +346 -71
- package/src/utils/config.ts +17 -6
- package/src/utils/doctor.ts +31 -1
- package/src/utils/enhanced-logger.ts +182 -48
- package/src/utils/git.ts +15 -0
- package/src/utils/logger.ts +4 -1
- package/src/utils/repro-thinking-logs.ts +54 -0
- package/src/utils/types.ts +22 -0
- package/src/utils/webhook.ts +3 -0
- package/scripts/simple-logging-test.sh +0 -97
- package/scripts/test-real-logging.sh +0 -289
- package/scripts/test-streaming-multi-task.sh +0 -247
package/src/core/runner.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { saveState, appendLog, createConversationEntry } from '../utils/state';
|
|
|
15
15
|
import { events } from '../utils/events';
|
|
16
16
|
import { loadConfig } from '../utils/config';
|
|
17
17
|
import { registerWebhooks } from '../utils/webhook';
|
|
18
|
-
import {
|
|
18
|
+
import { runReviewLoop } from './reviewer';
|
|
19
19
|
import {
|
|
20
20
|
RunnerConfig,
|
|
21
21
|
Task,
|
|
@@ -175,7 +175,7 @@ export function validateTaskConfig(config: RunnerConfig): void {
|
|
|
175
175
|
/**
|
|
176
176
|
* Execute cursor-agent command with streaming and better error handling
|
|
177
177
|
*/
|
|
178
|
-
export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention }: {
|
|
178
|
+
export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }: {
|
|
179
179
|
workspaceDir: string;
|
|
180
180
|
chatId: string;
|
|
181
181
|
prompt: string;
|
|
@@ -184,11 +184,16 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
184
184
|
timeout?: number;
|
|
185
185
|
/** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
|
|
186
186
|
enableIntervention?: boolean;
|
|
187
|
+
/** Output format for cursor-agent (default: 'stream-json') */
|
|
188
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
187
189
|
}): Promise<AgentSendResult> {
|
|
188
190
|
// Use stream-json format for structured output with tool calls and results
|
|
191
|
+
const format = outputFormat || 'stream-json';
|
|
189
192
|
const args = [
|
|
190
193
|
'--print',
|
|
191
|
-
'--
|
|
194
|
+
'--force',
|
|
195
|
+
'--approve-mcps',
|
|
196
|
+
'--output-format', format,
|
|
192
197
|
'--workspace', workspaceDir,
|
|
193
198
|
...(model ? ['--model', model] : []),
|
|
194
199
|
'--resume', chatId,
|
|
@@ -229,17 +234,18 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
229
234
|
env: childEnv,
|
|
230
235
|
});
|
|
231
236
|
|
|
232
|
-
|
|
237
|
+
logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
|
|
238
|
+
|
|
239
|
+
// Save PID to state if possible (avoid TOCTOU by reading directly)
|
|
233
240
|
if (child.pid && signalDir) {
|
|
234
241
|
try {
|
|
235
242
|
const statePath = path.join(signalDir, 'state.json');
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Best effort
|
|
243
|
+
// Read directly without existence check to avoid race condition
|
|
244
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
245
|
+
state.pid = child.pid;
|
|
246
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
247
|
+
} catch {
|
|
248
|
+
// Best effort - file may not exist yet
|
|
243
249
|
}
|
|
244
250
|
}
|
|
245
251
|
|
|
@@ -426,33 +432,31 @@ export function extractDependencyRequest(text: string): { required: boolean; pla
|
|
|
426
432
|
/**
|
|
427
433
|
* Wrap prompt with dependency policy
|
|
428
434
|
*/
|
|
429
|
-
export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
|
|
430
|
-
|
|
435
|
+
export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy, options: { noGit?: boolean } = {}): string {
|
|
436
|
+
const { noGit = false } = options;
|
|
437
|
+
|
|
438
|
+
if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
|
|
431
439
|
return prompt;
|
|
432
440
|
}
|
|
433
441
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
---
|
|
454
|
-
|
|
455
|
-
${prompt}`;
|
|
442
|
+
let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
|
|
443
|
+
|
|
444
|
+
rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
445
|
+
rules += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
|
|
446
|
+
|
|
447
|
+
if (noGit) {
|
|
448
|
+
rules += '- NO_GIT_MODE: Git is disabled. DO NOT run any git commands (commit, push, etc.). Just edit files.\n';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
rules += '\nRules:\n';
|
|
452
|
+
rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
|
|
453
|
+
rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
|
|
454
|
+
rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
|
|
455
|
+
rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
|
|
456
|
+
rules += 'Then STOP.\n';
|
|
457
|
+
rules += '- If dependency changes are NOT required, proceed normally.\n';
|
|
458
|
+
|
|
459
|
+
return `${rules}\n---\n\n${prompt}`;
|
|
456
460
|
}
|
|
457
461
|
|
|
458
462
|
/**
|
|
@@ -483,6 +487,82 @@ export function applyDependencyFilePermissions(worktreeDir: string, policy: Depe
|
|
|
483
487
|
}
|
|
484
488
|
}
|
|
485
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Wait for task-level dependencies to be completed by other lanes
|
|
492
|
+
*/
|
|
493
|
+
export async function waitForTaskDependencies(deps: string[], runDir: string): Promise<void> {
|
|
494
|
+
if (!deps || deps.length === 0) return;
|
|
495
|
+
|
|
496
|
+
const lanesRoot = path.dirname(runDir);
|
|
497
|
+
const pendingDeps = new Set(deps);
|
|
498
|
+
|
|
499
|
+
logger.info(`Waiting for task dependencies: ${deps.join(', ')}`);
|
|
500
|
+
|
|
501
|
+
while (pendingDeps.size > 0) {
|
|
502
|
+
for (const dep of pendingDeps) {
|
|
503
|
+
const [laneName, taskName] = dep.split(':');
|
|
504
|
+
if (!laneName || !taskName) {
|
|
505
|
+
logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
|
|
506
|
+
pendingDeps.delete(dep);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const depStatePath = path.join(lanesRoot, laneName, 'state.json');
|
|
511
|
+
if (fs.existsSync(depStatePath)) {
|
|
512
|
+
try {
|
|
513
|
+
const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
|
|
514
|
+
if (state.completedTasks && state.completedTasks.includes(taskName)) {
|
|
515
|
+
logger.info(`✓ Dependency met: ${dep}`);
|
|
516
|
+
pendingDeps.delete(dep);
|
|
517
|
+
} else if (state.status === 'failed') {
|
|
518
|
+
throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
|
|
519
|
+
}
|
|
520
|
+
} catch (e: any) {
|
|
521
|
+
if (e.message.includes('Dependency failed')) throw e;
|
|
522
|
+
// Ignore parse errors, file might be being written
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (pendingDeps.size > 0) {
|
|
528
|
+
await new Promise(resolve => setTimeout(resolve, 5000)); // Poll every 5 seconds
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Merge branches from dependency lanes
|
|
535
|
+
*/
|
|
536
|
+
export async function mergeDependencyBranches(deps: string[], runDir: string, worktreeDir: string): Promise<void> {
|
|
537
|
+
if (!deps || deps.length === 0) return;
|
|
538
|
+
|
|
539
|
+
const lanesRoot = path.dirname(runDir);
|
|
540
|
+
const lanesToMerge = new Set(deps.map(d => d.split(':')[0]!));
|
|
541
|
+
|
|
542
|
+
for (const laneName of lanesToMerge) {
|
|
543
|
+
const depStatePath = path.join(lanesRoot, laneName, 'state.json');
|
|
544
|
+
if (!fs.existsSync(depStatePath)) continue;
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
|
|
548
|
+
if (state.pipelineBranch) {
|
|
549
|
+
logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
|
|
550
|
+
|
|
551
|
+
// Ensure we have the latest
|
|
552
|
+
git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
553
|
+
|
|
554
|
+
git.merge(state.pipelineBranch, {
|
|
555
|
+
cwd: worktreeDir,
|
|
556
|
+
noFf: true,
|
|
557
|
+
message: `chore: merge task dependency from ${laneName}`
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
} catch (e) {
|
|
561
|
+
logger.error(`Failed to merge branch from ${laneName}: ${e}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
486
566
|
/**
|
|
487
567
|
* Run a single task
|
|
488
568
|
*/
|
|
@@ -491,9 +571,11 @@ export async function runTask({
|
|
|
491
571
|
config,
|
|
492
572
|
index,
|
|
493
573
|
worktreeDir,
|
|
574
|
+
pipelineBranch,
|
|
494
575
|
taskBranch,
|
|
495
576
|
chatId,
|
|
496
577
|
runDir,
|
|
578
|
+
noGit = false,
|
|
497
579
|
}: {
|
|
498
580
|
task: Task;
|
|
499
581
|
config: RunnerConfig;
|
|
@@ -503,13 +585,19 @@ export async function runTask({
|
|
|
503
585
|
taskBranch: string;
|
|
504
586
|
chatId: string;
|
|
505
587
|
runDir: string;
|
|
588
|
+
noGit?: boolean;
|
|
506
589
|
}): Promise<TaskExecutionResult> {
|
|
507
590
|
const model = task.model || config.model || 'sonnet-4.5';
|
|
591
|
+
const timeout = task.timeout || config.timeout;
|
|
508
592
|
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
509
593
|
|
|
510
594
|
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
511
595
|
logger.info(`Model: ${model}`);
|
|
512
|
-
|
|
596
|
+
if (noGit) {
|
|
597
|
+
logger.info('🚫 noGit mode: skipping branch operations');
|
|
598
|
+
} else {
|
|
599
|
+
logger.info(`Branch: ${taskBranch}`);
|
|
600
|
+
}
|
|
513
601
|
|
|
514
602
|
events.emit('task.started', {
|
|
515
603
|
taskName: task.name,
|
|
@@ -517,14 +605,16 @@ export async function runTask({
|
|
|
517
605
|
index,
|
|
518
606
|
});
|
|
519
607
|
|
|
520
|
-
// Checkout task branch
|
|
521
|
-
|
|
608
|
+
// Checkout task branch (skip in noGit mode)
|
|
609
|
+
if (!noGit) {
|
|
610
|
+
git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
|
|
611
|
+
}
|
|
522
612
|
|
|
523
613
|
// Apply dependency permissions
|
|
524
614
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
525
615
|
|
|
526
616
|
// Run prompt
|
|
527
|
-
const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy);
|
|
617
|
+
const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
|
|
528
618
|
|
|
529
619
|
appendLog(convoPath, createConversationEntry('user', prompt1, {
|
|
530
620
|
task: task.name,
|
|
@@ -545,8 +635,9 @@ export async function runTask({
|
|
|
545
635
|
prompt: prompt1,
|
|
546
636
|
model,
|
|
547
637
|
signalDir: runDir,
|
|
548
|
-
timeout
|
|
638
|
+
timeout,
|
|
549
639
|
enableIntervention: config.enableIntervention,
|
|
640
|
+
outputFormat: config.agentOutputFormat,
|
|
550
641
|
});
|
|
551
642
|
|
|
552
643
|
const duration = Date.now() - startTime;
|
|
@@ -588,8 +679,41 @@ export async function runTask({
|
|
|
588
679
|
};
|
|
589
680
|
}
|
|
590
681
|
|
|
591
|
-
// Push task branch
|
|
592
|
-
|
|
682
|
+
// Push task branch (skip in noGit mode)
|
|
683
|
+
if (!noGit) {
|
|
684
|
+
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Automatic Review
|
|
688
|
+
const reviewEnabled = config.reviewAllTasks || task.acceptanceCriteria?.length || config.enableReview;
|
|
689
|
+
|
|
690
|
+
if (reviewEnabled) {
|
|
691
|
+
logger.section(`🔍 Reviewing Task: ${task.name}`);
|
|
692
|
+
const reviewResult = await runReviewLoop({
|
|
693
|
+
taskResult: {
|
|
694
|
+
taskName: task.name,
|
|
695
|
+
taskBranch: taskBranch,
|
|
696
|
+
acceptanceCriteria: task.acceptanceCriteria,
|
|
697
|
+
},
|
|
698
|
+
worktreeDir,
|
|
699
|
+
runDir,
|
|
700
|
+
config,
|
|
701
|
+
workChatId: chatId,
|
|
702
|
+
model, // Use the same model as requested
|
|
703
|
+
cursorAgentSend,
|
|
704
|
+
cursorAgentCreateChat,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
if (!reviewResult.approved) {
|
|
708
|
+
logger.error(`❌ Task review failed after ${reviewResult.iterations} iterations`);
|
|
709
|
+
return {
|
|
710
|
+
taskName: task.name,
|
|
711
|
+
taskBranch,
|
|
712
|
+
status: 'ERROR',
|
|
713
|
+
error: reviewResult.error || 'Task failed to pass review criteria',
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
593
717
|
|
|
594
718
|
events.emit('task.completed', {
|
|
595
719
|
taskName: task.name,
|
|
@@ -607,8 +731,13 @@ export async function runTask({
|
|
|
607
731
|
/**
|
|
608
732
|
* Run all tasks in sequence
|
|
609
733
|
*/
|
|
610
|
-
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number } = {}): Promise<TaskExecutionResult[]> {
|
|
734
|
+
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean } = {}): Promise<TaskExecutionResult[]> {
|
|
611
735
|
const startIndex = options.startIndex || 0;
|
|
736
|
+
const noGit = options.noGit || config.noGit || false;
|
|
737
|
+
|
|
738
|
+
if (noGit) {
|
|
739
|
+
logger.info('🚫 Running in noGit mode - Git operations will be skipped');
|
|
740
|
+
}
|
|
612
741
|
|
|
613
742
|
// Validate configuration before starting
|
|
614
743
|
logger.info('Validating task configuration...');
|
|
@@ -648,18 +777,27 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
648
777
|
|
|
649
778
|
logger.success('✓ Cursor authentication OK');
|
|
650
779
|
|
|
651
|
-
|
|
780
|
+
// In noGit mode, we don't need repoRoot - use current directory
|
|
781
|
+
const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
|
|
652
782
|
|
|
653
783
|
// Load existing state if resuming
|
|
654
784
|
const statePath = path.join(runDir, 'state.json');
|
|
655
785
|
let state: LaneState | null = null;
|
|
656
786
|
|
|
657
|
-
if (
|
|
658
|
-
|
|
787
|
+
if (fs.existsSync(statePath)) {
|
|
788
|
+
try {
|
|
789
|
+
state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
790
|
+
} catch (e) {
|
|
791
|
+
logger.warn(`Failed to load existing state from ${statePath}: ${e}`);
|
|
792
|
+
}
|
|
659
793
|
}
|
|
660
794
|
|
|
661
|
-
const
|
|
662
|
-
const
|
|
795
|
+
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
796
|
+
const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
|
|
797
|
+
// In noGit mode, use a simple local directory instead of worktree
|
|
798
|
+
const worktreeDir = state?.worktreeDir || (noGit
|
|
799
|
+
? path.join(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
|
|
800
|
+
: path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch));
|
|
663
801
|
|
|
664
802
|
if (startIndex === 0) {
|
|
665
803
|
logger.section('🚀 Starting Pipeline');
|
|
@@ -671,12 +809,28 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
671
809
|
logger.info(`Worktree: ${worktreeDir}`);
|
|
672
810
|
logger.info(`Tasks: ${config.tasks.length}`);
|
|
673
811
|
|
|
674
|
-
// Create worktree only if starting fresh
|
|
675
|
-
if (
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
812
|
+
// Create worktree only if starting fresh and worktree doesn't exist
|
|
813
|
+
if (!fs.existsSync(worktreeDir)) {
|
|
814
|
+
if (noGit) {
|
|
815
|
+
// In noGit mode, just create the directory
|
|
816
|
+
logger.info(`Creating work directory: ${worktreeDir}`);
|
|
817
|
+
fs.mkdirSync(worktreeDir, { recursive: true });
|
|
818
|
+
} else {
|
|
819
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
820
|
+
baseBranch: config.baseBranch || 'main',
|
|
821
|
+
cwd: repoRoot,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
} else if (!noGit) {
|
|
825
|
+
// If it exists but we are in Git mode, ensure it's actually a worktree and on the right branch
|
|
826
|
+
logger.info(`Reusing existing worktree: ${worktreeDir}`);
|
|
827
|
+
try {
|
|
828
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
829
|
+
} catch (e) {
|
|
830
|
+
// If checkout fails, maybe the worktree is in a weird state.
|
|
831
|
+
// For now, just log it. In a more robust impl, we might want to repair it.
|
|
832
|
+
logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
|
|
833
|
+
}
|
|
680
834
|
}
|
|
681
835
|
|
|
682
836
|
// Create chat
|
|
@@ -698,18 +852,20 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
698
852
|
dependencyRequest: null,
|
|
699
853
|
tasksFile, // Store tasks file for resume
|
|
700
854
|
dependsOn: config.dependsOn || [],
|
|
855
|
+
completedTasks: [],
|
|
701
856
|
};
|
|
702
857
|
} else {
|
|
703
858
|
state.status = 'running';
|
|
704
859
|
state.error = null;
|
|
705
860
|
state.dependencyRequest = null;
|
|
706
861
|
state.dependsOn = config.dependsOn || [];
|
|
862
|
+
state.completedTasks = state.completedTasks || [];
|
|
707
863
|
}
|
|
708
864
|
|
|
709
865
|
saveState(statePath, state);
|
|
710
866
|
|
|
711
|
-
// Merge dependencies if any
|
|
712
|
-
if (startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
867
|
+
// Merge dependencies if any (skip in noGit mode)
|
|
868
|
+
if (!noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
713
869
|
logger.section('🔗 Merging Dependencies');
|
|
714
870
|
|
|
715
871
|
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
@@ -756,6 +912,50 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
756
912
|
|
|
757
913
|
// Push the merged state
|
|
758
914
|
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
915
|
+
} else if (noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
916
|
+
logger.info('⚠️ Dependencies specified but Git is disabled - copying files instead of merging');
|
|
917
|
+
|
|
918
|
+
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
919
|
+
const lanesRoot = path.dirname(runDir);
|
|
920
|
+
|
|
921
|
+
for (const depName of config.dependsOn) {
|
|
922
|
+
const depRunDir = path.join(lanesRoot, depName);
|
|
923
|
+
const depStatePath = path.join(depRunDir, 'state.json');
|
|
924
|
+
|
|
925
|
+
if (!fs.existsSync(depStatePath)) {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
try {
|
|
930
|
+
const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
|
|
931
|
+
if (depState.worktreeDir && fs.existsSync(depState.worktreeDir)) {
|
|
932
|
+
logger.info(`Copying files from dependency ${depName}: ${depState.worktreeDir} → ${worktreeDir}`);
|
|
933
|
+
|
|
934
|
+
// Use a simple recursive copy (excluding Git and internal dirs)
|
|
935
|
+
const copyFiles = (src: string, dest: string) => {
|
|
936
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
937
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
938
|
+
|
|
939
|
+
for (const entry of entries) {
|
|
940
|
+
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
|
|
941
|
+
|
|
942
|
+
const srcPath = path.join(src, entry.name);
|
|
943
|
+
const destPath = path.join(dest, entry.name);
|
|
944
|
+
|
|
945
|
+
if (entry.isDirectory()) {
|
|
946
|
+
copyFiles(srcPath, destPath);
|
|
947
|
+
} else {
|
|
948
|
+
fs.copyFileSync(srcPath, destPath);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
copyFiles(depState.worktreeDir, worktreeDir);
|
|
954
|
+
}
|
|
955
|
+
} catch (e) {
|
|
956
|
+
logger.error(`Failed to copy dependency ${depName}: ${e}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
759
959
|
}
|
|
760
960
|
|
|
761
961
|
// Run tasks
|
|
@@ -764,6 +964,32 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
764
964
|
for (let i = startIndex; i < config.tasks.length; i++) {
|
|
765
965
|
const task = config.tasks[i]!;
|
|
766
966
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
967
|
+
|
|
968
|
+
// Handle task-level dependencies
|
|
969
|
+
if (task.dependsOn && task.dependsOn.length > 0) {
|
|
970
|
+
state.status = 'waiting';
|
|
971
|
+
state.waitingFor = task.dependsOn;
|
|
972
|
+
saveState(statePath, state);
|
|
973
|
+
|
|
974
|
+
try {
|
|
975
|
+
await waitForTaskDependencies(task.dependsOn, runDir);
|
|
976
|
+
|
|
977
|
+
if (!noGit) {
|
|
978
|
+
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
state.status = 'running';
|
|
982
|
+
state.waitingFor = [];
|
|
983
|
+
saveState(statePath, state);
|
|
984
|
+
} catch (e: any) {
|
|
985
|
+
state.status = 'failed';
|
|
986
|
+
state.waitingFor = [];
|
|
987
|
+
state.error = e.message;
|
|
988
|
+
saveState(statePath, state);
|
|
989
|
+
logger.error(`Task dependency wait/merge failed: ${e.message}`);
|
|
990
|
+
process.exit(1);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
767
993
|
|
|
768
994
|
const result = await runTask({
|
|
769
995
|
task,
|
|
@@ -774,12 +1000,17 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
774
1000
|
taskBranch,
|
|
775
1001
|
chatId,
|
|
776
1002
|
runDir,
|
|
1003
|
+
noGit,
|
|
777
1004
|
});
|
|
778
1005
|
|
|
779
1006
|
results.push(result);
|
|
780
1007
|
|
|
781
1008
|
// Update state
|
|
782
1009
|
state.currentTaskIndex = i + 1;
|
|
1010
|
+
state.completedTasks = state.completedTasks || [];
|
|
1011
|
+
if (!state.completedTasks.includes(task.name)) {
|
|
1012
|
+
state.completedTasks.push(task.name);
|
|
1013
|
+
}
|
|
783
1014
|
saveState(statePath, state);
|
|
784
1015
|
|
|
785
1016
|
// Handle blocked or error
|
|
@@ -807,17 +1038,21 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
807
1038
|
process.exit(1);
|
|
808
1039
|
}
|
|
809
1040
|
|
|
810
|
-
// Merge into pipeline
|
|
811
|
-
|
|
812
|
-
|
|
1041
|
+
// Merge into pipeline (skip in noGit mode)
|
|
1042
|
+
if (!noGit) {
|
|
1043
|
+
logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
|
|
1044
|
+
git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
|
|
813
1045
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1046
|
+
// Log changed files
|
|
1047
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
1048
|
+
if (stats) {
|
|
1049
|
+
logger.info('Changed files:\n' + stats);
|
|
1050
|
+
}
|
|
819
1051
|
|
|
820
|
-
|
|
1052
|
+
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
1053
|
+
} else {
|
|
1054
|
+
logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
|
|
1055
|
+
}
|
|
821
1056
|
}
|
|
822
1057
|
|
|
823
1058
|
// Complete
|
|
@@ -825,6 +1060,41 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
825
1060
|
state.endTime = Date.now();
|
|
826
1061
|
saveState(statePath, state);
|
|
827
1062
|
|
|
1063
|
+
// Log final file summary
|
|
1064
|
+
if (noGit) {
|
|
1065
|
+
const getFileSummary = (dir: string): { files: number; dirs: number } => {
|
|
1066
|
+
let stats = { files: 0, dirs: 0 };
|
|
1067
|
+
if (!fs.existsSync(dir)) return stats;
|
|
1068
|
+
|
|
1069
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1070
|
+
for (const entry of entries) {
|
|
1071
|
+
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
|
|
1072
|
+
|
|
1073
|
+
if (entry.isDirectory()) {
|
|
1074
|
+
stats.dirs++;
|
|
1075
|
+
const sub = getFileSummary(path.join(dir, entry.name));
|
|
1076
|
+
stats.files += sub.files;
|
|
1077
|
+
stats.dirs += sub.dirs;
|
|
1078
|
+
} else {
|
|
1079
|
+
stats.files++;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return stats;
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
const summary = getFileSummary(worktreeDir);
|
|
1086
|
+
logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
|
|
1087
|
+
} else {
|
|
1088
|
+
try {
|
|
1089
|
+
const stats = git.runGit(['diff', '--stat', config.baseBranch || 'main', pipelineBranch], { cwd: repoRoot, silent: true });
|
|
1090
|
+
if (stats) {
|
|
1091
|
+
logger.info('Final Workspace Summary (Git):\n' + stats);
|
|
1092
|
+
}
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
// Ignore
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
828
1098
|
logger.success('All tasks completed!');
|
|
829
1099
|
return results;
|
|
830
1100
|
}
|
|
@@ -844,6 +1114,7 @@ if (require.main === module) {
|
|
|
844
1114
|
const runDirIdx = args.indexOf('--run-dir');
|
|
845
1115
|
const startIdxIdx = args.indexOf('--start-index');
|
|
846
1116
|
const pipelineBranchIdx = args.indexOf('--pipeline-branch');
|
|
1117
|
+
const noGit = args.includes('--no-git');
|
|
847
1118
|
|
|
848
1119
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
849
1120
|
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
@@ -856,9 +1127,10 @@ if (require.main === module) {
|
|
|
856
1127
|
|
|
857
1128
|
events.setRunId(runId);
|
|
858
1129
|
|
|
859
|
-
// Load global config
|
|
1130
|
+
// Load global config for defaults and webhooks
|
|
1131
|
+
let globalConfig;
|
|
860
1132
|
try {
|
|
861
|
-
|
|
1133
|
+
globalConfig = loadConfig();
|
|
862
1134
|
if (globalConfig.webhooks) {
|
|
863
1135
|
registerWebhooks(globalConfig.webhooks);
|
|
864
1136
|
}
|
|
@@ -883,14 +1155,17 @@ if (require.main === module) {
|
|
|
883
1155
|
process.exit(1);
|
|
884
1156
|
}
|
|
885
1157
|
|
|
886
|
-
// Add
|
|
1158
|
+
// Add defaults from global config or hardcoded
|
|
887
1159
|
config.dependencyPolicy = config.dependencyPolicy || {
|
|
888
|
-
allowDependencyChange: false,
|
|
889
|
-
lockfileReadOnly: true,
|
|
1160
|
+
allowDependencyChange: globalConfig?.allowDependencyChange ?? false,
|
|
1161
|
+
lockfileReadOnly: globalConfig?.lockfileReadOnly ?? true,
|
|
890
1162
|
};
|
|
891
1163
|
|
|
1164
|
+
// Add agent output format default
|
|
1165
|
+
config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
|
|
1166
|
+
|
|
892
1167
|
// Run tasks
|
|
893
|
-
runTasks(tasksFile, config, runDir, { startIndex })
|
|
1168
|
+
runTasks(tasksFile, config, runDir, { startIndex, noGit })
|
|
894
1169
|
.then(() => {
|
|
895
1170
|
process.exit(0);
|
|
896
1171
|
})
|