@litmers/cursorflow-orchestrator 0.1.28 → 0.1.30
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 +17 -2
- package/dist/cli/clean.js +122 -0
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/prepare.js +0 -83
- package/dist/cli/prepare.js.map +1 -1
- package/dist/core/auto-recovery.js +3 -1
- package/dist/core/auto-recovery.js.map +1 -1
- package/dist/core/failure-policy.js +7 -1
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/orchestrator.js +452 -366
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/utils/config.js +3 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +5 -1
- package/dist/utils/enhanced-logger.js +65 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +25 -0
- package/dist/utils/git.js +97 -2
- package/dist/utils/git.js.map +1 -1
- package/package.json +11 -3
- package/scripts/local-security-gate.sh +37 -7
- package/scripts/release.sh +15 -0
- package/src/cli/clean.ts +146 -0
- package/src/cli/prepare.ts +0 -93
- package/src/core/auto-recovery.ts +3 -1
- package/src/core/failure-policy.ts +8 -1
- package/src/core/orchestrator.ts +175 -82
- package/src/utils/config.ts +3 -1
- package/src/utils/enhanced-logger.ts +61 -20
- package/src/utils/git.ts +115 -2
package/src/cli/prepare.ts
CHANGED
|
@@ -11,7 +11,6 @@ import { loadConfig, getTasksDir } from '../utils/config';
|
|
|
11
11
|
import { Task, RunnerConfig } from '../utils/types';
|
|
12
12
|
import { safeJoin } from '../utils/path';
|
|
13
13
|
import { resolveTemplate } from '../utils/template';
|
|
14
|
-
import * as git from '../utils/git';
|
|
15
14
|
|
|
16
15
|
// Preset template types
|
|
17
16
|
type PresetType = 'complex' | 'simple' | 'merge';
|
|
@@ -33,9 +32,6 @@ interface PrepareOptions {
|
|
|
33
32
|
addLane: string | null; // Add lane to existing task dir
|
|
34
33
|
addTask: string | null; // Add task to existing lane file
|
|
35
34
|
dependsOnLanes: string[]; // --depends-on for new lane
|
|
36
|
-
// Git options
|
|
37
|
-
commit: boolean; // Commit and push current changes before prepare
|
|
38
|
-
commitMessage: string | null; // Custom commit message
|
|
39
35
|
force: boolean;
|
|
40
36
|
help: boolean;
|
|
41
37
|
}
|
|
@@ -118,10 +114,6 @@ Prepare task files for a new feature - Terminal-first workflow.
|
|
|
118
114
|
--add-lane <dir> Add a new lane to existing task directory
|
|
119
115
|
--add-task <file> Append task(s) to existing lane JSON file
|
|
120
116
|
|
|
121
|
-
Git (commit before prepare):
|
|
122
|
-
--commit Commit and push current changes before prepare
|
|
123
|
-
--commit-message <msg> Custom commit message (default: auto-generated)
|
|
124
|
-
|
|
125
117
|
Advanced:
|
|
126
118
|
--template <path|url|name> External template JSON file, URL, or built-in name
|
|
127
119
|
--force Overwrite existing files
|
|
@@ -165,8 +157,6 @@ function parseArgs(args: string[]): PrepareOptions {
|
|
|
165
157
|
addLane: null,
|
|
166
158
|
addTask: null,
|
|
167
159
|
dependsOnLanes: [],
|
|
168
|
-
commit: false,
|
|
169
|
-
commitMessage: null,
|
|
170
160
|
force: false,
|
|
171
161
|
help: false,
|
|
172
162
|
};
|
|
@@ -208,11 +198,6 @@ function parseArgs(args: string[]): PrepareOptions {
|
|
|
208
198
|
result.addTask = args[++i];
|
|
209
199
|
} else if (arg === '--depends-on' && args[i + 1]) {
|
|
210
200
|
result.dependsOnLanes = args[++i].split(',').map(d => d.trim()).filter(d => d);
|
|
211
|
-
} else if (arg === '--commit') {
|
|
212
|
-
result.commit = true;
|
|
213
|
-
} else if ((arg === '--commit-message' || arg === '-m') && args[i + 1]) {
|
|
214
|
-
result.commitMessage = args[++i];
|
|
215
|
-
result.commit = true; // Implicitly enable commit if message is provided
|
|
216
201
|
} else if (!arg.startsWith('--') && !result.featureName) {
|
|
217
202
|
result.featureName = arg;
|
|
218
203
|
}
|
|
@@ -867,75 +852,6 @@ cursorflow prepare --add-task ${path.relative(config.projectRoot, taskDir)}/01-l
|
|
|
867
852
|
console.log('');
|
|
868
853
|
}
|
|
869
854
|
|
|
870
|
-
/**
|
|
871
|
-
* Commit and push current changes before prepare
|
|
872
|
-
*/
|
|
873
|
-
async function commitAndPush(featureName: string, customMessage: string | null): Promise<boolean> {
|
|
874
|
-
// Check if there are uncommitted changes
|
|
875
|
-
const statusResult = git.runGitResult(['status', '--porcelain']);
|
|
876
|
-
if (!statusResult.success) {
|
|
877
|
-
logger.error('Failed to check git status');
|
|
878
|
-
return false;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
const hasChanges = statusResult.stdout.trim().length > 0;
|
|
882
|
-
|
|
883
|
-
if (!hasChanges) {
|
|
884
|
-
logger.info('No uncommitted changes to commit');
|
|
885
|
-
return true;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Show what will be committed
|
|
889
|
-
logger.section('📦 Committing Current Changes');
|
|
890
|
-
const changedFiles = statusResult.stdout.trim().split('\n');
|
|
891
|
-
logger.info(`${changedFiles.length} file(s) to commit:`);
|
|
892
|
-
for (const file of changedFiles.slice(0, 5)) {
|
|
893
|
-
console.log(` ${file}`);
|
|
894
|
-
}
|
|
895
|
-
if (changedFiles.length > 5) {
|
|
896
|
-
console.log(` ... and ${changedFiles.length - 5} more`);
|
|
897
|
-
}
|
|
898
|
-
console.log('');
|
|
899
|
-
|
|
900
|
-
// Stage all changes
|
|
901
|
-
const addResult = git.runGitResult(['add', '-A']);
|
|
902
|
-
if (!addResult.success) {
|
|
903
|
-
logger.error(`Failed to stage changes: ${addResult.stderr}`);
|
|
904
|
-
return false;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Generate commit message
|
|
908
|
-
const message = customMessage || `chore: pre-prepare checkpoint for ${featureName}`;
|
|
909
|
-
|
|
910
|
-
// Commit
|
|
911
|
-
const commitResult = git.runGitResult(['commit', '-m', message]);
|
|
912
|
-
if (!commitResult.success) {
|
|
913
|
-
logger.error(`Failed to commit: ${commitResult.stderr}`);
|
|
914
|
-
return false;
|
|
915
|
-
}
|
|
916
|
-
logger.success(`Committed: ${message}`);
|
|
917
|
-
|
|
918
|
-
// Push
|
|
919
|
-
logger.info('Pushing to remote...');
|
|
920
|
-
const currentBranch = git.getCurrentBranch();
|
|
921
|
-
const pushResult = git.runGitResult(['push', 'origin', currentBranch]);
|
|
922
|
-
if (!pushResult.success) {
|
|
923
|
-
// Try to provide helpful error message
|
|
924
|
-
if (pushResult.stderr.includes('rejected') || pushResult.stderr.includes('non-fast-forward')) {
|
|
925
|
-
logger.warn('Push rejected. Try pulling first:');
|
|
926
|
-
console.log(` git pull --rebase origin ${currentBranch}`);
|
|
927
|
-
console.log(' Then run prepare again');
|
|
928
|
-
} else {
|
|
929
|
-
logger.error(`Failed to push: ${pushResult.stderr}`);
|
|
930
|
-
}
|
|
931
|
-
return false;
|
|
932
|
-
}
|
|
933
|
-
logger.success(`Pushed to origin/${currentBranch}`);
|
|
934
|
-
console.log('');
|
|
935
|
-
|
|
936
|
-
return true;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
855
|
async function prepare(args: string[]): Promise<void> {
|
|
940
856
|
const options = parseArgs(args);
|
|
941
857
|
|
|
@@ -944,15 +860,6 @@ async function prepare(args: string[]): Promise<void> {
|
|
|
944
860
|
return;
|
|
945
861
|
}
|
|
946
862
|
|
|
947
|
-
// Handle --commit option first (before any prepare operation)
|
|
948
|
-
if (options.commit) {
|
|
949
|
-
const featureName = options.featureName || options.addLane || options.addTask || 'tasks';
|
|
950
|
-
const success = await commitAndPush(featureName, options.commitMessage);
|
|
951
|
-
if (!success) {
|
|
952
|
-
throw new Error('Failed to commit and push. Fix the issue and try again.');
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
863
|
// Mode 1: Add task to existing lane
|
|
957
864
|
if (options.addTask) {
|
|
958
865
|
await addTaskToLane(options);
|
|
@@ -283,9 +283,11 @@ export class AutoRecoveryManager {
|
|
|
283
283
|
if (!state) return;
|
|
284
284
|
|
|
285
285
|
const now = Date.now();
|
|
286
|
-
state.lastActivityTime = now;
|
|
287
286
|
|
|
287
|
+
// Only update activity time if we actually received bytes
|
|
288
|
+
// This allows heartbeats to be recorded (for logs/bytes) without resetting the idle timer
|
|
288
289
|
if (bytesReceived > 0) {
|
|
290
|
+
state.lastActivityTime = now;
|
|
289
291
|
state.lastBytesReceived = bytesReceived;
|
|
290
292
|
state.totalBytesReceived += bytesReceived;
|
|
291
293
|
}
|
|
@@ -149,7 +149,14 @@ export function analyzeStall(context: StallContext, config: StallDetectionConfig
|
|
|
149
149
|
|
|
150
150
|
// Check if this might be a long operation
|
|
151
151
|
const isLongOperation = lastOutput && config.longOperationPatterns.some(p => p.test(lastOutput));
|
|
152
|
-
|
|
152
|
+
|
|
153
|
+
// If it's a long operation but we've received 0 real bytes for a while,
|
|
154
|
+
// reduce the grace period to avoid waiting forever for a hung process.
|
|
155
|
+
// We use 2x the normal idle timeout as a "sanity check" for silent long operations.
|
|
156
|
+
const silentLongOpCappedTimeout = config.idleTimeoutMs * 2;
|
|
157
|
+
const effectiveIdleTimeout = isLongOperation
|
|
158
|
+
? (bytesReceived === 0 ? Math.min(config.longOperationGraceMs, silentLongOpCappedTimeout) : config.longOperationGraceMs)
|
|
159
|
+
: config.idleTimeoutMs;
|
|
153
160
|
|
|
154
161
|
// Check for task timeout
|
|
155
162
|
if (taskStartTimeMs && (Date.now() - taskStartTimeMs) > config.taskTimeoutMs) {
|
package/src/core/orchestrator.ts
CHANGED
|
@@ -86,6 +86,26 @@ interface RunningLaneInfo {
|
|
|
86
86
|
continueSignalsSent: number; // Number of continue signals sent
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Log the tail of a file
|
|
91
|
+
*/
|
|
92
|
+
function logFileTail(filePath: string, lines: number = 10): void {
|
|
93
|
+
if (!fs.existsSync(filePath)) return;
|
|
94
|
+
try {
|
|
95
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
96
|
+
const allLines = content.split('\n');
|
|
97
|
+
const tail = allLines.slice(-lines).filter(l => l.trim());
|
|
98
|
+
if (tail.length > 0) {
|
|
99
|
+
logger.error(` Last ${tail.length} lines of log:`);
|
|
100
|
+
for (const line of tail) {
|
|
101
|
+
logger.error(` ${line}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Ignore log reading errors
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
89
109
|
/**
|
|
90
110
|
* Spawn a lane process
|
|
91
111
|
*/
|
|
@@ -651,7 +671,28 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
651
671
|
printLaneStatus(lanes, laneRunDirs);
|
|
652
672
|
}, options.pollInterval || 60000);
|
|
653
673
|
|
|
654
|
-
|
|
674
|
+
// Handle process interruption
|
|
675
|
+
const sigIntHandler = () => {
|
|
676
|
+
logger.warn('\n⚠️ Orchestration interrupted! Stopping all lanes...');
|
|
677
|
+
for (const [name, info] of running.entries()) {
|
|
678
|
+
logger.info(`Stopping lane: ${name}`);
|
|
679
|
+
try {
|
|
680
|
+
info.child.kill('SIGTERM');
|
|
681
|
+
} catch {
|
|
682
|
+
// Ignore kill errors
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
printLaneStatus(lanes, laneRunDirs);
|
|
686
|
+
process.exit(130);
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
process.on('SIGINT', sigIntHandler);
|
|
690
|
+
process.on('SIGTERM', sigIntHandler);
|
|
691
|
+
|
|
692
|
+
let lastStallCheck = Date.now();
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
while (completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length || (blockedLanes.size > 0 && running.size === 0)) {
|
|
655
696
|
// 1. Identify lanes ready to start
|
|
656
697
|
const readyToStart = lanes.filter(lane => {
|
|
657
698
|
// Not already running or completed or failed or blocked
|
|
@@ -692,6 +733,23 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
692
733
|
|
|
693
734
|
logger.info(`Lane started: ${lane.name}${lane.startIndex ? ` (resuming from ${lane.startIndex})` : ''}`);
|
|
694
735
|
|
|
736
|
+
const now = Date.now();
|
|
737
|
+
// Pre-register lane in running map so onActivity can find it immediately
|
|
738
|
+
running.set(lane.name, {
|
|
739
|
+
child: {} as any, // Placeholder, will be replaced below
|
|
740
|
+
logManager: undefined,
|
|
741
|
+
logPath: '',
|
|
742
|
+
lastActivity: now,
|
|
743
|
+
lastStateUpdate: now,
|
|
744
|
+
stallPhase: 0,
|
|
745
|
+
taskStartTime: now,
|
|
746
|
+
lastOutput: '',
|
|
747
|
+
statePath: laneStatePath,
|
|
748
|
+
bytesReceived: 0,
|
|
749
|
+
lastBytesCheck: 0,
|
|
750
|
+
continueSignalsSent: 0,
|
|
751
|
+
});
|
|
752
|
+
|
|
695
753
|
let lastOutput = '';
|
|
696
754
|
const spawnResult = spawnLane({
|
|
697
755
|
laneName: lane.name,
|
|
@@ -706,44 +764,45 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
706
764
|
onActivity: () => {
|
|
707
765
|
const info = running.get(lane.name);
|
|
708
766
|
if (info) {
|
|
709
|
-
const
|
|
710
|
-
info.lastActivity =
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
info.lastStateUpdate = now;
|
|
714
|
-
info.stallPhase = 0; // Reset stall phase since agent is responding
|
|
767
|
+
const actNow = Date.now();
|
|
768
|
+
info.lastActivity = actNow;
|
|
769
|
+
info.lastStateUpdate = actNow;
|
|
770
|
+
info.stallPhase = 0;
|
|
715
771
|
}
|
|
716
772
|
}
|
|
717
773
|
});
|
|
718
774
|
|
|
775
|
+
// Update with actual spawn result
|
|
776
|
+
const existingInfo = running.get(lane.name)!;
|
|
777
|
+
Object.assign(existingInfo, spawnResult);
|
|
778
|
+
|
|
719
779
|
// Track last output and bytes received for long operation and stall detection
|
|
720
780
|
if (spawnResult.child.stdout) {
|
|
721
781
|
spawnResult.child.stdout.on('data', (data: Buffer) => {
|
|
722
782
|
const info = running.get(lane.name);
|
|
723
783
|
if (info) {
|
|
724
|
-
|
|
725
|
-
|
|
784
|
+
const output = data.toString();
|
|
785
|
+
const lines = output.split('\n').filter(l => l.trim());
|
|
786
|
+
|
|
787
|
+
// Filter out heartbeats from activity tracking to avoid resetting stall detection
|
|
788
|
+
const realLines = lines.filter(line => !(line.includes('Heartbeat') && line.includes('bytes received')));
|
|
726
789
|
|
|
727
|
-
|
|
728
|
-
|
|
790
|
+
if (realLines.length > 0) {
|
|
791
|
+
// Real activity detected
|
|
792
|
+
const lastRealLine = realLines[realLines.length - 1]!;
|
|
793
|
+
info.lastOutput = lastRealLine;
|
|
794
|
+
info.bytesReceived += data.length;
|
|
795
|
+
|
|
796
|
+
// Update auto-recovery manager with real activity
|
|
797
|
+
autoRecoveryManager.recordActivity(lane.name, data.length, info.lastOutput);
|
|
798
|
+
} else if (lines.length > 0) {
|
|
799
|
+
// Only heartbeats received - update auto-recovery manager with 0 bytes to avoid resetting idle timer
|
|
800
|
+
autoRecoveryManager.recordActivity(lane.name, 0, info.lastOutput);
|
|
801
|
+
}
|
|
729
802
|
}
|
|
730
803
|
});
|
|
731
804
|
}
|
|
732
805
|
|
|
733
|
-
const now = Date.now();
|
|
734
|
-
running.set(lane.name, {
|
|
735
|
-
...spawnResult,
|
|
736
|
-
lastActivity: now,
|
|
737
|
-
lastStateUpdate: now,
|
|
738
|
-
stallPhase: 0,
|
|
739
|
-
taskStartTime: now,
|
|
740
|
-
lastOutput: '',
|
|
741
|
-
statePath: laneStatePath,
|
|
742
|
-
bytesReceived: 0,
|
|
743
|
-
lastBytesCheck: 0,
|
|
744
|
-
continueSignalsSent: 0,
|
|
745
|
-
});
|
|
746
|
-
|
|
747
806
|
// Register lane with auto-recovery manager
|
|
748
807
|
autoRecoveryManager.registerLane(lane.name);
|
|
749
808
|
|
|
@@ -773,13 +832,19 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
773
832
|
const result = await Promise.race([...promises, pollPromise]);
|
|
774
833
|
if (pollTimeout) clearTimeout(pollTimeout);
|
|
775
834
|
|
|
776
|
-
|
|
835
|
+
const now = Date.now();
|
|
836
|
+
if (result.name === '__poll__' || (now - lastStallCheck >= 10000)) {
|
|
837
|
+
lastStallCheck = now;
|
|
838
|
+
|
|
777
839
|
// Periodic stall check with multi-layer detection and escalating recovery
|
|
778
840
|
for (const [laneName, info] of running.entries()) {
|
|
779
|
-
const now = Date.now();
|
|
780
841
|
const idleTime = now - info.lastActivity;
|
|
781
842
|
const lane = lanes.find(l => l.name === laneName)!;
|
|
782
843
|
|
|
844
|
+
if (process.env['DEBUG_STALL']) {
|
|
845
|
+
logger.debug(`[${laneName}] Stall check: idle=${Math.round(idleTime/1000)}s, bytesDelta=${info.bytesReceived - info.lastBytesCheck}, phase=${info.stallPhase}`);
|
|
846
|
+
}
|
|
847
|
+
|
|
783
848
|
// Check state file for progress updates
|
|
784
849
|
let progressTime = 0;
|
|
785
850
|
try {
|
|
@@ -1002,74 +1067,98 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
1002
1067
|
}
|
|
1003
1068
|
}
|
|
1004
1069
|
continue;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
exitCodes[finished.name] = finished.code;
|
|
1011
|
-
|
|
1012
|
-
// Unregister from auto-recovery manager
|
|
1013
|
-
autoRecoveryManager.unregisterLane(finished.name);
|
|
1014
|
-
|
|
1015
|
-
if (finished.code === 0) {
|
|
1016
|
-
completedLanes.add(finished.name);
|
|
1017
|
-
events.emit('lane.completed', {
|
|
1018
|
-
laneName: finished.name,
|
|
1019
|
-
exitCode: finished.code,
|
|
1020
|
-
});
|
|
1021
|
-
} else if (finished.code === 2) {
|
|
1022
|
-
// Blocked by dependency
|
|
1023
|
-
const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
|
|
1024
|
-
const state = loadState<LaneState>(statePath);
|
|
1070
|
+
} else {
|
|
1071
|
+
const finished = result;
|
|
1072
|
+
const info = running.get(finished.name)!;
|
|
1073
|
+
running.delete(finished.name);
|
|
1074
|
+
exitCodes[finished.name] = finished.code;
|
|
1025
1075
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
events.emit('lane.blocked', {
|
|
1076
|
+
// Unregister from auto-recovery manager
|
|
1077
|
+
autoRecoveryManager.unregisterLane(finished.name);
|
|
1078
|
+
|
|
1079
|
+
if (finished.code === 0) {
|
|
1080
|
+
completedLanes.add(finished.name);
|
|
1081
|
+
events.emit('lane.completed', {
|
|
1034
1082
|
laneName: finished.name,
|
|
1035
|
-
|
|
1083
|
+
exitCode: finished.code,
|
|
1036
1084
|
});
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
failedLanes.add(finished.name);
|
|
1040
|
-
logger.error(`Lane ${finished.name} exited with code 2 but no dependency request found`);
|
|
1041
|
-
}
|
|
1042
|
-
} else {
|
|
1043
|
-
// Check if it was a restart request
|
|
1044
|
-
if (info.stallPhase === 2) {
|
|
1045
|
-
logger.info(`🔄 Lane ${finished.name} is being restarted due to stall...`);
|
|
1046
|
-
|
|
1047
|
-
// Update startIndex from current state to resume from the same task
|
|
1085
|
+
} else if (finished.code === 2) {
|
|
1086
|
+
// Blocked by dependency
|
|
1048
1087
|
const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
|
|
1049
1088
|
const state = loadState<LaneState>(statePath);
|
|
1050
|
-
|
|
1089
|
+
|
|
1090
|
+
if (state && state.dependencyRequest) {
|
|
1091
|
+
blockedLanes.set(finished.name, state.dependencyRequest);
|
|
1051
1092
|
const lane = lanes.find(l => l.name === finished.name);
|
|
1052
1093
|
if (lane) {
|
|
1053
|
-
lane.startIndex = state.currentTaskIndex;
|
|
1094
|
+
lane.startIndex = Math.max(0, state.currentTaskIndex - 1); // Task was blocked, retry it
|
|
1054
1095
|
}
|
|
1096
|
+
|
|
1097
|
+
events.emit('lane.blocked', {
|
|
1098
|
+
laneName: finished.name,
|
|
1099
|
+
dependencyRequest: state.dependencyRequest,
|
|
1100
|
+
});
|
|
1101
|
+
logger.warn(`Lane ${finished.name} is blocked on dependency change request`);
|
|
1102
|
+
} else {
|
|
1103
|
+
failedLanes.add(finished.name);
|
|
1104
|
+
logger.error(`Lane ${finished.name} exited with code 2 but no dependency request found`);
|
|
1055
1105
|
}
|
|
1106
|
+
} else {
|
|
1107
|
+
// Check if it was a restart request
|
|
1108
|
+
if (info.stallPhase === 2) {
|
|
1109
|
+
logger.info(`🔄 Lane ${finished.name} is being restarted due to stall...`);
|
|
1110
|
+
|
|
1111
|
+
// Update startIndex from current state to resume from the same task
|
|
1112
|
+
const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
|
|
1113
|
+
const state = loadState<LaneState>(statePath);
|
|
1114
|
+
if (state) {
|
|
1115
|
+
const lane = lanes.find(l => l.name === finished.name);
|
|
1116
|
+
if (lane) {
|
|
1117
|
+
lane.startIndex = state.currentTaskIndex;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Note: we don't add to failedLanes or completedLanes,
|
|
1122
|
+
// so it will be eligible to start again in the next iteration.
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
failedLanes.add(finished.name);
|
|
1056
1127
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1128
|
+
let errorMsg = 'Process exited with non-zero code';
|
|
1129
|
+
if (info.stallPhase === 3) {
|
|
1130
|
+
errorMsg = 'Stopped due to repeated stall';
|
|
1131
|
+
} else if (info.logManager) {
|
|
1132
|
+
const lastError = info.logManager.getLastError();
|
|
1133
|
+
if (lastError) {
|
|
1134
|
+
errorMsg = `Process failed: ${lastError}`;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1061
1137
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1138
|
+
logger.error(`[${finished.name}] Lane failed with exit code ${finished.code}: ${errorMsg}`);
|
|
1139
|
+
|
|
1140
|
+
// Log log tail for visibility
|
|
1141
|
+
if (info.logPath) {
|
|
1142
|
+
logFileTail(info.logPath, 15);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
events.emit('lane.failed', {
|
|
1146
|
+
laneName: finished.name,
|
|
1147
|
+
exitCode: finished.code,
|
|
1148
|
+
error: errorMsg,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
printLaneStatus(lanes, laneRunDirs);
|
|
1068
1153
|
}
|
|
1069
|
-
|
|
1070
|
-
printLaneStatus(lanes, laneRunDirs);
|
|
1071
1154
|
} else {
|
|
1072
1155
|
// Nothing running. Are we blocked?
|
|
1156
|
+
|
|
1157
|
+
// Wait a bit to avoid busy-spin while waiting for dependencies or new slots
|
|
1158
|
+
if (completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length) {
|
|
1159
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1073
1162
|
if (blockedLanes.size > 0 && autoResolve) {
|
|
1074
1163
|
logger.section('🛠 Auto-Resolving Dependencies');
|
|
1075
1164
|
|
|
@@ -1098,10 +1187,14 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
1098
1187
|
// All finished
|
|
1099
1188
|
break;
|
|
1100
1189
|
}
|
|
1190
|
+
}
|
|
1101
1191
|
}
|
|
1192
|
+
} finally {
|
|
1193
|
+
clearInterval(monitorInterval);
|
|
1194
|
+
process.removeListener('SIGINT', sigIntHandler);
|
|
1195
|
+
process.removeListener('SIGTERM', sigIntHandler);
|
|
1102
1196
|
}
|
|
1103
1197
|
|
|
1104
|
-
clearInterval(monitorInterval);
|
|
1105
1198
|
printLaneStatus(lanes, laneRunDirs);
|
|
1106
1199
|
|
|
1107
1200
|
// Check for failures
|
package/src/utils/config.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import * as fs from 'fs';
|
|
9
|
+
import * as git from './git';
|
|
9
10
|
import { CursorFlowConfig } from './types';
|
|
10
11
|
import { safeJoin } from './path';
|
|
11
12
|
export { CursorFlowConfig };
|
|
@@ -167,6 +168,7 @@ export function validateConfig(config: CursorFlowConfig): boolean {
|
|
|
167
168
|
*/
|
|
168
169
|
export function createDefaultConfig(projectRoot: string, force = false): string {
|
|
169
170
|
const configPath = safeJoin(projectRoot, 'cursorflow.config.js');
|
|
171
|
+
const currentBranch = git.getCurrentBranch() || 'main';
|
|
170
172
|
|
|
171
173
|
const template = `module.exports = {
|
|
172
174
|
// Directory configuration
|
|
@@ -175,7 +177,7 @@ export function createDefaultConfig(projectRoot: string, force = false): string
|
|
|
175
177
|
pofDir: '_cursorflow/pof',
|
|
176
178
|
|
|
177
179
|
// Git configuration
|
|
178
|
-
baseBranch:
|
|
180
|
+
baseBranch: '${currentBranch}',
|
|
179
181
|
branchPrefix: 'feature/',
|
|
180
182
|
|
|
181
183
|
// Execution configuration
|
|
@@ -1024,7 +1024,7 @@ export class EnhancedLogManager {
|
|
|
1024
1024
|
}
|
|
1025
1025
|
|
|
1026
1026
|
/**
|
|
1027
|
-
* Close all log files
|
|
1027
|
+
* Close all log files and ensure all data is flushed to disk
|
|
1028
1028
|
*/
|
|
1029
1029
|
public close(): void {
|
|
1030
1030
|
// Flush transform stream
|
|
@@ -1051,46 +1051,87 @@ export class EnhancedLogManager {
|
|
|
1051
1051
|
`;
|
|
1052
1052
|
|
|
1053
1053
|
if (this.cleanLogFd !== null) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1054
|
+
try {
|
|
1055
|
+
fs.writeSync(this.cleanLogFd, endMarker);
|
|
1056
|
+
fs.fsyncSync(this.cleanLogFd);
|
|
1057
|
+
fs.closeSync(this.cleanLogFd);
|
|
1058
|
+
} catch {}
|
|
1056
1059
|
this.cleanLogFd = null;
|
|
1057
1060
|
}
|
|
1058
1061
|
|
|
1059
1062
|
if (this.rawLogFd !== null) {
|
|
1060
|
-
|
|
1061
|
-
|
|
1063
|
+
try {
|
|
1064
|
+
fs.writeSync(this.rawLogFd, endMarker);
|
|
1065
|
+
fs.fsyncSync(this.rawLogFd);
|
|
1066
|
+
fs.closeSync(this.rawLogFd);
|
|
1067
|
+
} catch {}
|
|
1062
1068
|
this.rawLogFd = null;
|
|
1063
1069
|
}
|
|
1064
1070
|
|
|
1065
1071
|
if (this.absoluteRawLogFd !== null) {
|
|
1066
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
fs.fsyncSync(this.absoluteRawLogFd);
|
|
1074
|
+
fs.closeSync(this.absoluteRawLogFd);
|
|
1075
|
+
} catch {}
|
|
1067
1076
|
this.absoluteRawLogFd = null;
|
|
1068
1077
|
}
|
|
1069
1078
|
|
|
1070
1079
|
if (this.jsonLogFd !== null) {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1080
|
+
try {
|
|
1081
|
+
this.writeJsonEntry({
|
|
1082
|
+
timestamp: new Date().toISOString(),
|
|
1083
|
+
level: 'session',
|
|
1084
|
+
source: 'system',
|
|
1085
|
+
lane: this.session.laneName,
|
|
1086
|
+
message: 'Session ended',
|
|
1087
|
+
metadata: {
|
|
1088
|
+
sessionId: this.session.id,
|
|
1089
|
+
duration: Date.now() - this.session.startTime,
|
|
1090
|
+
},
|
|
1091
|
+
});
|
|
1092
|
+
fs.fsyncSync(this.jsonLogFd);
|
|
1093
|
+
fs.closeSync(this.jsonLogFd);
|
|
1094
|
+
} catch {}
|
|
1083
1095
|
this.jsonLogFd = null;
|
|
1084
1096
|
}
|
|
1085
1097
|
|
|
1086
1098
|
// Close readable log
|
|
1087
1099
|
if (this.readableLogFd !== null) {
|
|
1088
|
-
|
|
1089
|
-
|
|
1100
|
+
try {
|
|
1101
|
+
fs.writeSync(this.readableLogFd, endMarker);
|
|
1102
|
+
fs.fsyncSync(this.readableLogFd);
|
|
1103
|
+
fs.closeSync(this.readableLogFd);
|
|
1104
|
+
} catch {}
|
|
1090
1105
|
this.readableLogFd = null;
|
|
1091
1106
|
}
|
|
1092
1107
|
}
|
|
1093
1108
|
|
|
1109
|
+
/**
|
|
1110
|
+
* Extract the last error message from the clean log file
|
|
1111
|
+
*/
|
|
1112
|
+
public getLastError(): string | null {
|
|
1113
|
+
try {
|
|
1114
|
+
if (!fs.existsSync(this.cleanLogPath)) return null;
|
|
1115
|
+
const content = fs.readFileSync(this.cleanLogPath, 'utf8');
|
|
1116
|
+
// Look for lines containing error markers
|
|
1117
|
+
const lines = content.split('\n').filter(l =>
|
|
1118
|
+
l.includes('[ERROR]') ||
|
|
1119
|
+
l.includes('❌') ||
|
|
1120
|
+
l.includes('error:') ||
|
|
1121
|
+
l.includes('Fatal') ||
|
|
1122
|
+
l.includes('fail')
|
|
1123
|
+
);
|
|
1124
|
+
if (lines.length === 0) {
|
|
1125
|
+
// Fallback to last 5 lines if no specific error marker found
|
|
1126
|
+
const allLines = content.split('\n').filter(l => l.trim());
|
|
1127
|
+
return allLines.slice(-5).join('\n');
|
|
1128
|
+
}
|
|
1129
|
+
return lines[lines.length - 1]!.trim();
|
|
1130
|
+
} catch {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1094
1135
|
/**
|
|
1095
1136
|
* Format duration for display
|
|
1096
1137
|
*/
|