@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/dist/core/runner.js
CHANGED
|
@@ -44,6 +44,8 @@ exports.cursorAgentSend = cursorAgentSend;
|
|
|
44
44
|
exports.extractDependencyRequest = extractDependencyRequest;
|
|
45
45
|
exports.wrapPromptForDependencyPolicy = wrapPromptForDependencyPolicy;
|
|
46
46
|
exports.applyDependencyFilePermissions = applyDependencyFilePermissions;
|
|
47
|
+
exports.waitForTaskDependencies = waitForTaskDependencies;
|
|
48
|
+
exports.mergeDependencyBranches = mergeDependencyBranches;
|
|
47
49
|
exports.runTask = runTask;
|
|
48
50
|
exports.runTasks = runTasks;
|
|
49
51
|
const fs = __importStar(require("fs"));
|
|
@@ -56,6 +58,7 @@ const state_1 = require("../utils/state");
|
|
|
56
58
|
const events_1 = require("../utils/events");
|
|
57
59
|
const config_1 = require("../utils/config");
|
|
58
60
|
const webhook_1 = require("../utils/webhook");
|
|
61
|
+
const reviewer_1 = require("./reviewer");
|
|
59
62
|
/**
|
|
60
63
|
* Execute cursor-agent command with timeout and better error handling
|
|
61
64
|
*/
|
|
@@ -173,11 +176,14 @@ function validateTaskConfig(config) {
|
|
|
173
176
|
/**
|
|
174
177
|
* Execute cursor-agent command with streaming and better error handling
|
|
175
178
|
*/
|
|
176
|
-
async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention }) {
|
|
179
|
+
async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }) {
|
|
177
180
|
// Use stream-json format for structured output with tool calls and results
|
|
181
|
+
const format = outputFormat || 'stream-json';
|
|
178
182
|
const args = [
|
|
179
183
|
'--print',
|
|
180
|
-
'--
|
|
184
|
+
'--force',
|
|
185
|
+
'--approve-mcps',
|
|
186
|
+
'--output-format', format,
|
|
181
187
|
'--workspace', workspaceDir,
|
|
182
188
|
...(model ? ['--model', model] : []),
|
|
183
189
|
'--resume', chatId,
|
|
@@ -210,18 +216,18 @@ async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir,
|
|
|
210
216
|
stdio: [stdinMode, 'pipe', 'pipe'],
|
|
211
217
|
env: childEnv,
|
|
212
218
|
});
|
|
213
|
-
|
|
219
|
+
logger.info(`Executing cursor-agent... (timeout: ${Math.round(timeoutMs / 1000)}s)`);
|
|
220
|
+
// Save PID to state if possible (avoid TOCTOU by reading directly)
|
|
214
221
|
if (child.pid && signalDir) {
|
|
215
222
|
try {
|
|
216
223
|
const statePath = path.join(signalDir, 'state.json');
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
224
|
+
// Read directly without existence check to avoid race condition
|
|
225
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
226
|
+
state.pid = child.pid;
|
|
227
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
222
228
|
}
|
|
223
|
-
catch
|
|
224
|
-
// Best effort
|
|
229
|
+
catch {
|
|
230
|
+
// Best effort - file may not exist yet
|
|
225
231
|
}
|
|
226
232
|
}
|
|
227
233
|
let fullStdout = '';
|
|
@@ -392,32 +398,25 @@ function extractDependencyRequest(text) {
|
|
|
392
398
|
/**
|
|
393
399
|
* Wrap prompt with dependency policy
|
|
394
400
|
*/
|
|
395
|
-
function wrapPromptForDependencyPolicy(prompt, policy) {
|
|
396
|
-
|
|
401
|
+
function wrapPromptForDependencyPolicy(prompt, policy, options = {}) {
|
|
402
|
+
const { noGit = false } = options;
|
|
403
|
+
if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
|
|
397
404
|
return prompt;
|
|
398
405
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
{
|
|
413
|
-
\`\`\`
|
|
414
|
-
|
|
415
|
-
Then STOP.
|
|
416
|
-
- If dependency changes are NOT required, proceed normally.
|
|
417
|
-
|
|
418
|
-
---
|
|
419
|
-
|
|
420
|
-
${prompt}`;
|
|
406
|
+
let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
|
|
407
|
+
rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
408
|
+
rules += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
|
|
409
|
+
if (noGit) {
|
|
410
|
+
rules += '- NO_GIT_MODE: Git is disabled. DO NOT run any git commands (commit, push, etc.). Just edit files.\n';
|
|
411
|
+
}
|
|
412
|
+
rules += '\nRules:\n';
|
|
413
|
+
rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
|
|
414
|
+
rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
|
|
415
|
+
rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
|
|
416
|
+
rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
|
|
417
|
+
rules += 'Then STOP.\n';
|
|
418
|
+
rules += '- If dependency changes are NOT required, proceed normally.\n';
|
|
419
|
+
return `${rules}\n---\n\n${prompt}`;
|
|
421
420
|
}
|
|
422
421
|
/**
|
|
423
422
|
* Apply file permissions based on dependency policy
|
|
@@ -444,26 +443,105 @@ function applyDependencyFilePermissions(worktreeDir, policy) {
|
|
|
444
443
|
}
|
|
445
444
|
}
|
|
446
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Wait for task-level dependencies to be completed by other lanes
|
|
448
|
+
*/
|
|
449
|
+
async function waitForTaskDependencies(deps, runDir) {
|
|
450
|
+
if (!deps || deps.length === 0)
|
|
451
|
+
return;
|
|
452
|
+
const lanesRoot = path.dirname(runDir);
|
|
453
|
+
const pendingDeps = new Set(deps);
|
|
454
|
+
logger.info(`Waiting for task dependencies: ${deps.join(', ')}`);
|
|
455
|
+
while (pendingDeps.size > 0) {
|
|
456
|
+
for (const dep of pendingDeps) {
|
|
457
|
+
const [laneName, taskName] = dep.split(':');
|
|
458
|
+
if (!laneName || !taskName) {
|
|
459
|
+
logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
|
|
460
|
+
pendingDeps.delete(dep);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const depStatePath = path.join(lanesRoot, laneName, 'state.json');
|
|
464
|
+
if (fs.existsSync(depStatePath)) {
|
|
465
|
+
try {
|
|
466
|
+
const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
|
|
467
|
+
if (state.completedTasks && state.completedTasks.includes(taskName)) {
|
|
468
|
+
logger.info(`✓ Dependency met: ${dep}`);
|
|
469
|
+
pendingDeps.delete(dep);
|
|
470
|
+
}
|
|
471
|
+
else if (state.status === 'failed') {
|
|
472
|
+
throw new Error(`Dependency failed: ${dep} (Lane ${laneName} failed)`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
if (e.message.includes('Dependency failed'))
|
|
477
|
+
throw e;
|
|
478
|
+
// Ignore parse errors, file might be being written
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (pendingDeps.size > 0) {
|
|
483
|
+
await new Promise(resolve => setTimeout(resolve, 5000)); // Poll every 5 seconds
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Merge branches from dependency lanes
|
|
489
|
+
*/
|
|
490
|
+
async function mergeDependencyBranches(deps, runDir, worktreeDir) {
|
|
491
|
+
if (!deps || deps.length === 0)
|
|
492
|
+
return;
|
|
493
|
+
const lanesRoot = path.dirname(runDir);
|
|
494
|
+
const lanesToMerge = new Set(deps.map(d => d.split(':')[0]));
|
|
495
|
+
for (const laneName of lanesToMerge) {
|
|
496
|
+
const depStatePath = path.join(lanesRoot, laneName, 'state.json');
|
|
497
|
+
if (!fs.existsSync(depStatePath))
|
|
498
|
+
continue;
|
|
499
|
+
try {
|
|
500
|
+
const state = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
|
|
501
|
+
if (state.pipelineBranch) {
|
|
502
|
+
logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
|
|
503
|
+
// Ensure we have the latest
|
|
504
|
+
git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
|
|
505
|
+
git.merge(state.pipelineBranch, {
|
|
506
|
+
cwd: worktreeDir,
|
|
507
|
+
noFf: true,
|
|
508
|
+
message: `chore: merge task dependency from ${laneName}`
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
catch (e) {
|
|
513
|
+
logger.error(`Failed to merge branch from ${laneName}: ${e}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
447
517
|
/**
|
|
448
518
|
* Run a single task
|
|
449
519
|
*/
|
|
450
|
-
async function runTask({ task, config, index, worktreeDir, taskBranch, chatId, runDir, }) {
|
|
520
|
+
async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskBranch, chatId, runDir, noGit = false, }) {
|
|
451
521
|
const model = task.model || config.model || 'sonnet-4.5';
|
|
522
|
+
const timeout = task.timeout || config.timeout;
|
|
452
523
|
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
453
524
|
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
454
525
|
logger.info(`Model: ${model}`);
|
|
455
|
-
|
|
526
|
+
if (noGit) {
|
|
527
|
+
logger.info('🚫 noGit mode: skipping branch operations');
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
logger.info(`Branch: ${taskBranch}`);
|
|
531
|
+
}
|
|
456
532
|
events_1.events.emit('task.started', {
|
|
457
533
|
taskName: task.name,
|
|
458
534
|
taskBranch,
|
|
459
535
|
index,
|
|
460
536
|
});
|
|
461
|
-
// Checkout task branch
|
|
462
|
-
|
|
537
|
+
// Checkout task branch (skip in noGit mode)
|
|
538
|
+
if (!noGit) {
|
|
539
|
+
git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
|
|
540
|
+
}
|
|
463
541
|
// Apply dependency permissions
|
|
464
542
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
465
543
|
// Run prompt
|
|
466
|
-
const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy);
|
|
544
|
+
const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
|
|
467
545
|
(0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('user', prompt1, {
|
|
468
546
|
task: task.name,
|
|
469
547
|
model,
|
|
@@ -481,8 +559,9 @@ async function runTask({ task, config, index, worktreeDir, taskBranch, chatId, r
|
|
|
481
559
|
prompt: prompt1,
|
|
482
560
|
model,
|
|
483
561
|
signalDir: runDir,
|
|
484
|
-
timeout
|
|
562
|
+
timeout,
|
|
485
563
|
enableIntervention: config.enableIntervention,
|
|
564
|
+
outputFormat: config.agentOutputFormat,
|
|
486
565
|
});
|
|
487
566
|
const duration = Date.now() - startTime;
|
|
488
567
|
events_1.events.emit('agent.response_received', {
|
|
@@ -519,8 +598,38 @@ async function runTask({ task, config, index, worktreeDir, taskBranch, chatId, r
|
|
|
519
598
|
dependencyRequest: depReq.plan || null,
|
|
520
599
|
};
|
|
521
600
|
}
|
|
522
|
-
// Push task branch
|
|
523
|
-
|
|
601
|
+
// Push task branch (skip in noGit mode)
|
|
602
|
+
if (!noGit) {
|
|
603
|
+
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
604
|
+
}
|
|
605
|
+
// Automatic Review
|
|
606
|
+
const reviewEnabled = config.reviewAllTasks || task.acceptanceCriteria?.length || config.enableReview;
|
|
607
|
+
if (reviewEnabled) {
|
|
608
|
+
logger.section(`🔍 Reviewing Task: ${task.name}`);
|
|
609
|
+
const reviewResult = await (0, reviewer_1.runReviewLoop)({
|
|
610
|
+
taskResult: {
|
|
611
|
+
taskName: task.name,
|
|
612
|
+
taskBranch: taskBranch,
|
|
613
|
+
acceptanceCriteria: task.acceptanceCriteria,
|
|
614
|
+
},
|
|
615
|
+
worktreeDir,
|
|
616
|
+
runDir,
|
|
617
|
+
config,
|
|
618
|
+
workChatId: chatId,
|
|
619
|
+
model, // Use the same model as requested
|
|
620
|
+
cursorAgentSend,
|
|
621
|
+
cursorAgentCreateChat,
|
|
622
|
+
});
|
|
623
|
+
if (!reviewResult.approved) {
|
|
624
|
+
logger.error(`❌ Task review failed after ${reviewResult.iterations} iterations`);
|
|
625
|
+
return {
|
|
626
|
+
taskName: task.name,
|
|
627
|
+
taskBranch,
|
|
628
|
+
status: 'ERROR',
|
|
629
|
+
error: reviewResult.error || 'Task failed to pass review criteria',
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
524
633
|
events_1.events.emit('task.completed', {
|
|
525
634
|
taskName: task.name,
|
|
526
635
|
taskBranch,
|
|
@@ -537,6 +646,10 @@ async function runTask({ task, config, index, worktreeDir, taskBranch, chatId, r
|
|
|
537
646
|
*/
|
|
538
647
|
async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
539
648
|
const startIndex = options.startIndex || 0;
|
|
649
|
+
const noGit = options.noGit || config.noGit || false;
|
|
650
|
+
if (noGit) {
|
|
651
|
+
logger.info('🚫 Running in noGit mode - Git operations will be skipped');
|
|
652
|
+
}
|
|
540
653
|
// Validate configuration before starting
|
|
541
654
|
logger.info('Validating task configuration...');
|
|
542
655
|
try {
|
|
@@ -567,15 +680,25 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
567
680
|
throw new Error('Cursor authentication required. Please authenticate and try again.');
|
|
568
681
|
}
|
|
569
682
|
logger.success('✓ Cursor authentication OK');
|
|
570
|
-
|
|
683
|
+
// In noGit mode, we don't need repoRoot - use current directory
|
|
684
|
+
const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
|
|
571
685
|
// Load existing state if resuming
|
|
572
686
|
const statePath = path.join(runDir, 'state.json');
|
|
573
687
|
let state = null;
|
|
574
|
-
if (
|
|
575
|
-
|
|
688
|
+
if (fs.existsSync(statePath)) {
|
|
689
|
+
try {
|
|
690
|
+
state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
691
|
+
}
|
|
692
|
+
catch (e) {
|
|
693
|
+
logger.warn(`Failed to load existing state from ${statePath}: ${e}`);
|
|
694
|
+
}
|
|
576
695
|
}
|
|
577
|
-
const
|
|
578
|
-
const
|
|
696
|
+
const randomSuffix = Math.random().toString(36).substring(2, 7);
|
|
697
|
+
const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
|
|
698
|
+
// In noGit mode, use a simple local directory instead of worktree
|
|
699
|
+
const worktreeDir = state?.worktreeDir || (noGit
|
|
700
|
+
? path.join(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
|
|
701
|
+
: path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch));
|
|
579
702
|
if (startIndex === 0) {
|
|
580
703
|
logger.section('🚀 Starting Pipeline');
|
|
581
704
|
}
|
|
@@ -585,12 +708,31 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
585
708
|
logger.info(`Pipeline Branch: ${pipelineBranch}`);
|
|
586
709
|
logger.info(`Worktree: ${worktreeDir}`);
|
|
587
710
|
logger.info(`Tasks: ${config.tasks.length}`);
|
|
588
|
-
// Create worktree only if starting fresh
|
|
589
|
-
if (
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
711
|
+
// Create worktree only if starting fresh and worktree doesn't exist
|
|
712
|
+
if (!fs.existsSync(worktreeDir)) {
|
|
713
|
+
if (noGit) {
|
|
714
|
+
// In noGit mode, just create the directory
|
|
715
|
+
logger.info(`Creating work directory: ${worktreeDir}`);
|
|
716
|
+
fs.mkdirSync(worktreeDir, { recursive: true });
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
720
|
+
baseBranch: config.baseBranch || 'main',
|
|
721
|
+
cwd: repoRoot,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
else if (!noGit) {
|
|
726
|
+
// If it exists but we are in Git mode, ensure it's actually a worktree and on the right branch
|
|
727
|
+
logger.info(`Reusing existing worktree: ${worktreeDir}`);
|
|
728
|
+
try {
|
|
729
|
+
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
730
|
+
}
|
|
731
|
+
catch (e) {
|
|
732
|
+
// If checkout fails, maybe the worktree is in a weird state.
|
|
733
|
+
// For now, just log it. In a more robust impl, we might want to repair it.
|
|
734
|
+
logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
|
|
735
|
+
}
|
|
594
736
|
}
|
|
595
737
|
// Create chat
|
|
596
738
|
logger.info('Creating chat session...');
|
|
@@ -610,6 +752,7 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
610
752
|
dependencyRequest: null,
|
|
611
753
|
tasksFile, // Store tasks file for resume
|
|
612
754
|
dependsOn: config.dependsOn || [],
|
|
755
|
+
completedTasks: [],
|
|
613
756
|
};
|
|
614
757
|
}
|
|
615
758
|
else {
|
|
@@ -617,10 +760,11 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
617
760
|
state.error = null;
|
|
618
761
|
state.dependencyRequest = null;
|
|
619
762
|
state.dependsOn = config.dependsOn || [];
|
|
763
|
+
state.completedTasks = state.completedTasks || [];
|
|
620
764
|
}
|
|
621
765
|
(0, state_1.saveState)(statePath, state);
|
|
622
|
-
// Merge dependencies if any
|
|
623
|
-
if (startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
766
|
+
// Merge dependencies if any (skip in noGit mode)
|
|
767
|
+
if (!noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
624
768
|
logger.section('🔗 Merging Dependencies');
|
|
625
769
|
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
626
770
|
const lanesRoot = path.dirname(runDir);
|
|
@@ -660,11 +804,74 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
660
804
|
// Push the merged state
|
|
661
805
|
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
662
806
|
}
|
|
807
|
+
else if (noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
808
|
+
logger.info('⚠️ Dependencies specified but Git is disabled - copying files instead of merging');
|
|
809
|
+
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
810
|
+
const lanesRoot = path.dirname(runDir);
|
|
811
|
+
for (const depName of config.dependsOn) {
|
|
812
|
+
const depRunDir = path.join(lanesRoot, depName);
|
|
813
|
+
const depStatePath = path.join(depRunDir, 'state.json');
|
|
814
|
+
if (!fs.existsSync(depStatePath)) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
|
|
819
|
+
if (depState.worktreeDir && fs.existsSync(depState.worktreeDir)) {
|
|
820
|
+
logger.info(`Copying files from dependency ${depName}: ${depState.worktreeDir} → ${worktreeDir}`);
|
|
821
|
+
// Use a simple recursive copy (excluding Git and internal dirs)
|
|
822
|
+
const copyFiles = (src, dest) => {
|
|
823
|
+
if (!fs.existsSync(dest))
|
|
824
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
825
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
826
|
+
for (const entry of entries) {
|
|
827
|
+
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules')
|
|
828
|
+
continue;
|
|
829
|
+
const srcPath = path.join(src, entry.name);
|
|
830
|
+
const destPath = path.join(dest, entry.name);
|
|
831
|
+
if (entry.isDirectory()) {
|
|
832
|
+
copyFiles(srcPath, destPath);
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
fs.copyFileSync(srcPath, destPath);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
copyFiles(depState.worktreeDir, worktreeDir);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
catch (e) {
|
|
843
|
+
logger.error(`Failed to copy dependency ${depName}: ${e}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
663
847
|
// Run tasks
|
|
664
848
|
const results = [];
|
|
665
849
|
for (let i = startIndex; i < config.tasks.length; i++) {
|
|
666
850
|
const task = config.tasks[i];
|
|
667
851
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
852
|
+
// Handle task-level dependencies
|
|
853
|
+
if (task.dependsOn && task.dependsOn.length > 0) {
|
|
854
|
+
state.status = 'waiting';
|
|
855
|
+
state.waitingFor = task.dependsOn;
|
|
856
|
+
(0, state_1.saveState)(statePath, state);
|
|
857
|
+
try {
|
|
858
|
+
await waitForTaskDependencies(task.dependsOn, runDir);
|
|
859
|
+
if (!noGit) {
|
|
860
|
+
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
|
|
861
|
+
}
|
|
862
|
+
state.status = 'running';
|
|
863
|
+
state.waitingFor = [];
|
|
864
|
+
(0, state_1.saveState)(statePath, state);
|
|
865
|
+
}
|
|
866
|
+
catch (e) {
|
|
867
|
+
state.status = 'failed';
|
|
868
|
+
state.waitingFor = [];
|
|
869
|
+
state.error = e.message;
|
|
870
|
+
(0, state_1.saveState)(statePath, state);
|
|
871
|
+
logger.error(`Task dependency wait/merge failed: ${e.message}`);
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
668
875
|
const result = await runTask({
|
|
669
876
|
task,
|
|
670
877
|
config,
|
|
@@ -674,10 +881,15 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
674
881
|
taskBranch,
|
|
675
882
|
chatId,
|
|
676
883
|
runDir,
|
|
884
|
+
noGit,
|
|
677
885
|
});
|
|
678
886
|
results.push(result);
|
|
679
887
|
// Update state
|
|
680
888
|
state.currentTaskIndex = i + 1;
|
|
889
|
+
state.completedTasks = state.completedTasks || [];
|
|
890
|
+
if (!state.completedTasks.includes(task.name)) {
|
|
891
|
+
state.completedTasks.push(task.name);
|
|
892
|
+
}
|
|
681
893
|
(0, state_1.saveState)(statePath, state);
|
|
682
894
|
// Handle blocked or error
|
|
683
895
|
if (result.status === 'BLOCKED_DEPENDENCY') {
|
|
@@ -700,20 +912,61 @@ async function runTasks(tasksFile, config, runDir, options = {}) {
|
|
|
700
912
|
logger.error(`Task failed: ${result.error}`);
|
|
701
913
|
process.exit(1);
|
|
702
914
|
}
|
|
703
|
-
// Merge into pipeline
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
915
|
+
// Merge into pipeline (skip in noGit mode)
|
|
916
|
+
if (!noGit) {
|
|
917
|
+
logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
|
|
918
|
+
git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
|
|
919
|
+
// Log changed files
|
|
920
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
921
|
+
if (stats) {
|
|
922
|
+
logger.info('Changed files:\n' + stats);
|
|
923
|
+
}
|
|
924
|
+
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
|
|
710
928
|
}
|
|
711
|
-
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
712
929
|
}
|
|
713
930
|
// Complete
|
|
714
931
|
state.status = 'completed';
|
|
715
932
|
state.endTime = Date.now();
|
|
716
933
|
(0, state_1.saveState)(statePath, state);
|
|
934
|
+
// Log final file summary
|
|
935
|
+
if (noGit) {
|
|
936
|
+
const getFileSummary = (dir) => {
|
|
937
|
+
let stats = { files: 0, dirs: 0 };
|
|
938
|
+
if (!fs.existsSync(dir))
|
|
939
|
+
return stats;
|
|
940
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
941
|
+
for (const entry of entries) {
|
|
942
|
+
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules')
|
|
943
|
+
continue;
|
|
944
|
+
if (entry.isDirectory()) {
|
|
945
|
+
stats.dirs++;
|
|
946
|
+
const sub = getFileSummary(path.join(dir, entry.name));
|
|
947
|
+
stats.files += sub.files;
|
|
948
|
+
stats.dirs += sub.dirs;
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
stats.files++;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return stats;
|
|
955
|
+
};
|
|
956
|
+
const summary = getFileSummary(worktreeDir);
|
|
957
|
+
logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
try {
|
|
961
|
+
const stats = git.runGit(['diff', '--stat', config.baseBranch || 'main', pipelineBranch], { cwd: repoRoot, silent: true });
|
|
962
|
+
if (stats) {
|
|
963
|
+
logger.info('Final Workspace Summary (Git):\n' + stats);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
catch (e) {
|
|
967
|
+
// Ignore
|
|
968
|
+
}
|
|
969
|
+
}
|
|
717
970
|
logger.success('All tasks completed!');
|
|
718
971
|
return results;
|
|
719
972
|
}
|
|
@@ -730,6 +983,7 @@ if (require.main === module) {
|
|
|
730
983
|
const runDirIdx = args.indexOf('--run-dir');
|
|
731
984
|
const startIdxIdx = args.indexOf('--start-index');
|
|
732
985
|
const pipelineBranchIdx = args.indexOf('--pipeline-branch');
|
|
986
|
+
const noGit = args.includes('--no-git');
|
|
733
987
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1] : '.';
|
|
734
988
|
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
735
989
|
const forcedPipelineBranch = pipelineBranchIdx >= 0 ? args[pipelineBranchIdx + 1] : null;
|
|
@@ -738,9 +992,10 @@ if (require.main === module) {
|
|
|
738
992
|
const runsIdx = parts.lastIndexOf('runs');
|
|
739
993
|
const runId = runsIdx >= 0 && parts[runsIdx + 1] ? parts[runsIdx + 1] : `run-${Date.now()}`;
|
|
740
994
|
events_1.events.setRunId(runId);
|
|
741
|
-
// Load global config
|
|
995
|
+
// Load global config for defaults and webhooks
|
|
996
|
+
let globalConfig;
|
|
742
997
|
try {
|
|
743
|
-
|
|
998
|
+
globalConfig = (0, config_1.loadConfig)();
|
|
744
999
|
if (globalConfig.webhooks) {
|
|
745
1000
|
(0, webhook_1.registerWebhooks)(globalConfig.webhooks);
|
|
746
1001
|
}
|
|
@@ -764,13 +1019,15 @@ if (require.main === module) {
|
|
|
764
1019
|
console.error(`Failed to load tasks file: ${error.message}`);
|
|
765
1020
|
process.exit(1);
|
|
766
1021
|
}
|
|
767
|
-
// Add
|
|
1022
|
+
// Add defaults from global config or hardcoded
|
|
768
1023
|
config.dependencyPolicy = config.dependencyPolicy || {
|
|
769
|
-
allowDependencyChange: false,
|
|
770
|
-
lockfileReadOnly: true,
|
|
1024
|
+
allowDependencyChange: globalConfig?.allowDependencyChange ?? false,
|
|
1025
|
+
lockfileReadOnly: globalConfig?.lockfileReadOnly ?? true,
|
|
771
1026
|
};
|
|
1027
|
+
// Add agent output format default
|
|
1028
|
+
config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
|
|
772
1029
|
// Run tasks
|
|
773
|
-
runTasks(tasksFile, config, runDir, { startIndex })
|
|
1030
|
+
runTasks(tasksFile, config, runDir, { startIndex, noGit })
|
|
774
1031
|
.then(() => {
|
|
775
1032
|
process.exit(0);
|
|
776
1033
|
})
|