@litmers/cursorflow-orchestrator 0.1.29 → 0.1.31
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 -0
- package/dist/cli/clean.js +122 -0
- package/dist/cli/clean.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 +459 -367
- 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/core/auto-recovery.ts +3 -1
- package/src/core/failure-policy.ts +8 -1
- package/src/core/orchestrator.ts +183 -83
- package/src/utils/config.ts +3 -1
- package/src/utils/enhanced-logger.ts +61 -20
- package/src/utils/git.ts +115 -2
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
|
*/
|
|
@@ -189,9 +209,12 @@ export function spawnLane({
|
|
|
189
209
|
// or if it's NOT a noisy JSON line
|
|
190
210
|
const hasTimestamp = /^\[\d{4}-\d{2}-\d{2}T|\^\[\d{2}:\d{2}:\d{2}\]/.test(trimmed);
|
|
191
211
|
const isJson = trimmed.startsWith('{') || trimmed.includes('{"type"');
|
|
212
|
+
// Filter out heartbeats - they should NOT reset the idle timer
|
|
213
|
+
const isHeartbeat = trimmed.includes('Heartbeat') && trimmed.includes('bytes received');
|
|
192
214
|
|
|
193
215
|
if (trimmed && !isJson) {
|
|
194
|
-
|
|
216
|
+
// Only trigger activity for non-heartbeat lines
|
|
217
|
+
if (onActivity && !isHeartbeat) onActivity();
|
|
195
218
|
// If line alreedy has timestamp format, just add lane prefix
|
|
196
219
|
if (hasTimestamp) {
|
|
197
220
|
// Insert lane name after first timestamp
|
|
@@ -651,7 +674,28 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
651
674
|
printLaneStatus(lanes, laneRunDirs);
|
|
652
675
|
}, options.pollInterval || 60000);
|
|
653
676
|
|
|
654
|
-
|
|
677
|
+
// Handle process interruption
|
|
678
|
+
const sigIntHandler = () => {
|
|
679
|
+
logger.warn('\n⚠️ Orchestration interrupted! Stopping all lanes...');
|
|
680
|
+
for (const [name, info] of running.entries()) {
|
|
681
|
+
logger.info(`Stopping lane: ${name}`);
|
|
682
|
+
try {
|
|
683
|
+
info.child.kill('SIGTERM');
|
|
684
|
+
} catch {
|
|
685
|
+
// Ignore kill errors
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
printLaneStatus(lanes, laneRunDirs);
|
|
689
|
+
process.exit(130);
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
process.on('SIGINT', sigIntHandler);
|
|
693
|
+
process.on('SIGTERM', sigIntHandler);
|
|
694
|
+
|
|
695
|
+
let lastStallCheck = Date.now();
|
|
696
|
+
|
|
697
|
+
try {
|
|
698
|
+
while (completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length || (blockedLanes.size > 0 && running.size === 0)) {
|
|
655
699
|
// 1. Identify lanes ready to start
|
|
656
700
|
const readyToStart = lanes.filter(lane => {
|
|
657
701
|
// Not already running or completed or failed or blocked
|
|
@@ -692,6 +736,23 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
692
736
|
|
|
693
737
|
logger.info(`Lane started: ${lane.name}${lane.startIndex ? ` (resuming from ${lane.startIndex})` : ''}`);
|
|
694
738
|
|
|
739
|
+
const now = Date.now();
|
|
740
|
+
// Pre-register lane in running map so onActivity can find it immediately
|
|
741
|
+
running.set(lane.name, {
|
|
742
|
+
child: {} as any, // Placeholder, will be replaced below
|
|
743
|
+
logManager: undefined,
|
|
744
|
+
logPath: '',
|
|
745
|
+
lastActivity: now,
|
|
746
|
+
lastStateUpdate: now,
|
|
747
|
+
stallPhase: 0,
|
|
748
|
+
taskStartTime: now,
|
|
749
|
+
lastOutput: '',
|
|
750
|
+
statePath: laneStatePath,
|
|
751
|
+
bytesReceived: 0,
|
|
752
|
+
lastBytesCheck: 0,
|
|
753
|
+
continueSignalsSent: 0,
|
|
754
|
+
});
|
|
755
|
+
|
|
695
756
|
let lastOutput = '';
|
|
696
757
|
const spawnResult = spawnLane({
|
|
697
758
|
laneName: lane.name,
|
|
@@ -706,44 +767,49 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
706
767
|
onActivity: () => {
|
|
707
768
|
const info = running.get(lane.name);
|
|
708
769
|
if (info) {
|
|
709
|
-
const
|
|
710
|
-
info.lastActivity =
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
info.lastStateUpdate = now;
|
|
714
|
-
info.stallPhase = 0; // Reset stall phase since agent is responding
|
|
770
|
+
const actNow = Date.now();
|
|
771
|
+
info.lastActivity = actNow;
|
|
772
|
+
info.lastStateUpdate = actNow;
|
|
773
|
+
info.stallPhase = 0;
|
|
715
774
|
}
|
|
716
775
|
}
|
|
717
776
|
});
|
|
718
777
|
|
|
778
|
+
// Update with actual spawn result
|
|
779
|
+
const existingInfo = running.get(lane.name)!;
|
|
780
|
+
Object.assign(existingInfo, spawnResult);
|
|
781
|
+
|
|
719
782
|
// Track last output and bytes received for long operation and stall detection
|
|
720
783
|
if (spawnResult.child.stdout) {
|
|
721
784
|
spawnResult.child.stdout.on('data', (data: Buffer) => {
|
|
722
785
|
const info = running.get(lane.name);
|
|
723
786
|
if (info) {
|
|
724
|
-
|
|
725
|
-
|
|
787
|
+
const output = data.toString();
|
|
788
|
+
const lines = output.split('\n').filter(l => l.trim());
|
|
789
|
+
|
|
790
|
+
// Filter out heartbeats from activity tracking to avoid resetting stall detection
|
|
791
|
+
const realLines = lines.filter(line => !(line.includes('Heartbeat') && line.includes('bytes received')));
|
|
726
792
|
|
|
727
|
-
|
|
728
|
-
|
|
793
|
+
if (realLines.length > 0) {
|
|
794
|
+
// Real activity detected - update lastActivity to reset stall timer
|
|
795
|
+
const actNow = Date.now();
|
|
796
|
+
info.lastActivity = actNow;
|
|
797
|
+
info.stallPhase = 0; // Reset stall phase on real activity
|
|
798
|
+
|
|
799
|
+
const lastRealLine = realLines[realLines.length - 1]!;
|
|
800
|
+
info.lastOutput = lastRealLine;
|
|
801
|
+
info.bytesReceived += data.length;
|
|
802
|
+
|
|
803
|
+
// Update auto-recovery manager with real activity
|
|
804
|
+
autoRecoveryManager.recordActivity(lane.name, data.length, info.lastOutput);
|
|
805
|
+
} else if (lines.length > 0) {
|
|
806
|
+
// Only heartbeats received - do NOT update lastActivity (keep stall timer running)
|
|
807
|
+
autoRecoveryManager.recordActivity(lane.name, 0, info.lastOutput);
|
|
808
|
+
}
|
|
729
809
|
}
|
|
730
810
|
});
|
|
731
811
|
}
|
|
732
812
|
|
|
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
813
|
// Register lane with auto-recovery manager
|
|
748
814
|
autoRecoveryManager.registerLane(lane.name);
|
|
749
815
|
|
|
@@ -773,13 +839,19 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
773
839
|
const result = await Promise.race([...promises, pollPromise]);
|
|
774
840
|
if (pollTimeout) clearTimeout(pollTimeout);
|
|
775
841
|
|
|
776
|
-
|
|
842
|
+
const now = Date.now();
|
|
843
|
+
if (result.name === '__poll__' || (now - lastStallCheck >= 10000)) {
|
|
844
|
+
lastStallCheck = now;
|
|
845
|
+
|
|
777
846
|
// Periodic stall check with multi-layer detection and escalating recovery
|
|
778
847
|
for (const [laneName, info] of running.entries()) {
|
|
779
|
-
const now = Date.now();
|
|
780
848
|
const idleTime = now - info.lastActivity;
|
|
781
849
|
const lane = lanes.find(l => l.name === laneName)!;
|
|
782
850
|
|
|
851
|
+
if (process.env['DEBUG_STALL']) {
|
|
852
|
+
logger.debug(`[${laneName}] Stall check: idle=${Math.round(idleTime/1000)}s, bytesDelta=${info.bytesReceived - info.lastBytesCheck}, phase=${info.stallPhase}`);
|
|
853
|
+
}
|
|
854
|
+
|
|
783
855
|
// Check state file for progress updates
|
|
784
856
|
let progressTime = 0;
|
|
785
857
|
try {
|
|
@@ -1002,74 +1074,98 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
1002
1074
|
}
|
|
1003
1075
|
}
|
|
1004
1076
|
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);
|
|
1077
|
+
} else {
|
|
1078
|
+
const finished = result;
|
|
1079
|
+
const info = running.get(finished.name)!;
|
|
1080
|
+
running.delete(finished.name);
|
|
1081
|
+
exitCodes[finished.name] = finished.code;
|
|
1025
1082
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
events.emit('lane.blocked', {
|
|
1083
|
+
// Unregister from auto-recovery manager
|
|
1084
|
+
autoRecoveryManager.unregisterLane(finished.name);
|
|
1085
|
+
|
|
1086
|
+
if (finished.code === 0) {
|
|
1087
|
+
completedLanes.add(finished.name);
|
|
1088
|
+
events.emit('lane.completed', {
|
|
1034
1089
|
laneName: finished.name,
|
|
1035
|
-
|
|
1090
|
+
exitCode: finished.code,
|
|
1036
1091
|
});
|
|
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
|
|
1092
|
+
} else if (finished.code === 2) {
|
|
1093
|
+
// Blocked by dependency
|
|
1048
1094
|
const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
|
|
1049
1095
|
const state = loadState<LaneState>(statePath);
|
|
1050
|
-
|
|
1096
|
+
|
|
1097
|
+
if (state && state.dependencyRequest) {
|
|
1098
|
+
blockedLanes.set(finished.name, state.dependencyRequest);
|
|
1051
1099
|
const lane = lanes.find(l => l.name === finished.name);
|
|
1052
1100
|
if (lane) {
|
|
1053
|
-
lane.startIndex = state.currentTaskIndex;
|
|
1101
|
+
lane.startIndex = Math.max(0, state.currentTaskIndex - 1); // Task was blocked, retry it
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
events.emit('lane.blocked', {
|
|
1105
|
+
laneName: finished.name,
|
|
1106
|
+
dependencyRequest: state.dependencyRequest,
|
|
1107
|
+
});
|
|
1108
|
+
logger.warn(`Lane ${finished.name} is blocked on dependency change request`);
|
|
1109
|
+
} else {
|
|
1110
|
+
failedLanes.add(finished.name);
|
|
1111
|
+
logger.error(`Lane ${finished.name} exited with code 2 but no dependency request found`);
|
|
1112
|
+
}
|
|
1113
|
+
} else {
|
|
1114
|
+
// Check if it was a restart request
|
|
1115
|
+
if (info.stallPhase === 2) {
|
|
1116
|
+
logger.info(`🔄 Lane ${finished.name} is being restarted due to stall...`);
|
|
1117
|
+
|
|
1118
|
+
// Update startIndex from current state to resume from the same task
|
|
1119
|
+
const statePath = safeJoin(laneRunDirs[finished.name]!, 'state.json');
|
|
1120
|
+
const state = loadState<LaneState>(statePath);
|
|
1121
|
+
if (state) {
|
|
1122
|
+
const lane = lanes.find(l => l.name === finished.name);
|
|
1123
|
+
if (lane) {
|
|
1124
|
+
lane.startIndex = state.currentTaskIndex;
|
|
1125
|
+
}
|
|
1054
1126
|
}
|
|
1127
|
+
|
|
1128
|
+
// Note: we don't add to failedLanes or completedLanes,
|
|
1129
|
+
// so it will be eligible to start again in the next iteration.
|
|
1130
|
+
continue;
|
|
1055
1131
|
}
|
|
1132
|
+
|
|
1133
|
+
failedLanes.add(finished.name);
|
|
1056
1134
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1135
|
+
let errorMsg = 'Process exited with non-zero code';
|
|
1136
|
+
if (info.stallPhase === 3) {
|
|
1137
|
+
errorMsg = 'Stopped due to repeated stall';
|
|
1138
|
+
} else if (info.logManager) {
|
|
1139
|
+
const lastError = info.logManager.getLastError();
|
|
1140
|
+
if (lastError) {
|
|
1141
|
+
errorMsg = `Process failed: ${lastError}`;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1061
1144
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1145
|
+
logger.error(`[${finished.name}] Lane failed with exit code ${finished.code}: ${errorMsg}`);
|
|
1146
|
+
|
|
1147
|
+
// Log log tail for visibility
|
|
1148
|
+
if (info.logPath) {
|
|
1149
|
+
logFileTail(info.logPath, 15);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
events.emit('lane.failed', {
|
|
1153
|
+
laneName: finished.name,
|
|
1154
|
+
exitCode: finished.code,
|
|
1155
|
+
error: errorMsg,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
printLaneStatus(lanes, laneRunDirs);
|
|
1068
1160
|
}
|
|
1069
|
-
|
|
1070
|
-
printLaneStatus(lanes, laneRunDirs);
|
|
1071
1161
|
} else {
|
|
1072
1162
|
// Nothing running. Are we blocked?
|
|
1163
|
+
|
|
1164
|
+
// Wait a bit to avoid busy-spin while waiting for dependencies or new slots
|
|
1165
|
+
if (completedLanes.size + failedLanes.size + blockedLanes.size < lanes.length) {
|
|
1166
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1073
1169
|
if (blockedLanes.size > 0 && autoResolve) {
|
|
1074
1170
|
logger.section('🛠 Auto-Resolving Dependencies');
|
|
1075
1171
|
|
|
@@ -1098,10 +1194,14 @@ export async function orchestrate(tasksDir: string, options: {
|
|
|
1098
1194
|
// All finished
|
|
1099
1195
|
break;
|
|
1100
1196
|
}
|
|
1197
|
+
}
|
|
1101
1198
|
}
|
|
1199
|
+
} finally {
|
|
1200
|
+
clearInterval(monitorInterval);
|
|
1201
|
+
process.removeListener('SIGINT', sigIntHandler);
|
|
1202
|
+
process.removeListener('SIGTERM', sigIntHandler);
|
|
1102
1203
|
}
|
|
1103
1204
|
|
|
1104
|
-
clearInterval(monitorInterval);
|
|
1105
1205
|
printLaneStatus(lanes, laneRunDirs);
|
|
1106
1206
|
|
|
1107
1207
|
// 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
|
*/
|
package/src/utils/git.ts
CHANGED
|
@@ -132,6 +132,10 @@ function filterGitStderr(stderr: string): string {
|
|
|
132
132
|
export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
133
133
|
const { cwd, silent = false } = options;
|
|
134
134
|
|
|
135
|
+
if (process.env['DEBUG_GIT']) {
|
|
136
|
+
console.log(`[DEBUG_GIT] Running: git ${args.join(' ')} (cwd: ${cwd || process.cwd()})`);
|
|
137
|
+
}
|
|
138
|
+
|
|
135
139
|
try {
|
|
136
140
|
const stdioMode = silent ? 'pipe' : ['inherit', 'inherit', 'pipe'];
|
|
137
141
|
|
|
@@ -141,6 +145,13 @@ export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
|
141
145
|
stdio: stdioMode as any,
|
|
142
146
|
});
|
|
143
147
|
|
|
148
|
+
if (result.error) {
|
|
149
|
+
if (!silent) {
|
|
150
|
+
console.error(`[ERROR_GIT] Failed to execute git command: ${result.error.message}`);
|
|
151
|
+
}
|
|
152
|
+
throw result.error;
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
if (!silent && result.stderr) {
|
|
145
156
|
const filteredStderr = filterGitStderr(result.stderr);
|
|
146
157
|
if (filteredStderr) {
|
|
@@ -149,12 +160,16 @@ export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
if (result.status !== 0 && !silent) {
|
|
152
|
-
|
|
163
|
+
const errorMsg = `Git command failed (exit code ${result.status}): git ${args.join(' ')}\n${result.stderr || ''}`;
|
|
164
|
+
throw new Error(errorMsg);
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
return result.stdout ? result.stdout.trim() : '';
|
|
156
168
|
} catch (error) {
|
|
157
169
|
if (silent) {
|
|
170
|
+
if (process.env['DEBUG_GIT']) {
|
|
171
|
+
console.error(`[DEBUG_GIT] Command failed (silent mode): ${error instanceof Error ? error.message : String(error)}`);
|
|
172
|
+
}
|
|
158
173
|
return '';
|
|
159
174
|
}
|
|
160
175
|
throw error;
|
|
@@ -167,18 +182,28 @@ export function runGit(args: string[], options: GitRunOptions = {}): string {
|
|
|
167
182
|
export function runGitResult(args: string[], options: GitRunOptions = {}): GitResult {
|
|
168
183
|
const { cwd } = options;
|
|
169
184
|
|
|
185
|
+
if (process.env['DEBUG_GIT']) {
|
|
186
|
+
console.log(`[DEBUG_GIT] Running: git ${args.join(' ')} (result mode, cwd: ${cwd || process.cwd()})`);
|
|
187
|
+
}
|
|
188
|
+
|
|
170
189
|
const result = spawnSync('git', args, {
|
|
171
190
|
cwd: cwd || process.cwd(),
|
|
172
191
|
encoding: 'utf8',
|
|
173
192
|
stdio: 'pipe',
|
|
174
193
|
});
|
|
175
194
|
|
|
176
|
-
|
|
195
|
+
const gitResult = {
|
|
177
196
|
exitCode: result.status ?? 1,
|
|
178
197
|
stdout: (result.stdout || '').toString().trim(),
|
|
179
198
|
stderr: (result.stderr || '').toString().trim(),
|
|
180
199
|
success: result.status === 0,
|
|
181
200
|
};
|
|
201
|
+
|
|
202
|
+
if (process.env['DEBUG_GIT'] && !gitResult.success) {
|
|
203
|
+
console.error(`[DEBUG_GIT] Command failed: git ${args.join(' ')}\nstderr: ${gitResult.stderr}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return gitResult;
|
|
182
207
|
}
|
|
183
208
|
|
|
184
209
|
/**
|
|
@@ -829,6 +854,94 @@ export function getWorktreeForPath(targetPath: string, cwd?: string): WorktreeIn
|
|
|
829
854
|
/**
|
|
830
855
|
* Sync branch with remote (fetch + merge or rebase)
|
|
831
856
|
*/
|
|
857
|
+
/**
|
|
858
|
+
* Push branch to remote, creating it if it doesn't exist
|
|
859
|
+
* Returns success status and any error message
|
|
860
|
+
*/
|
|
861
|
+
export function pushBranchSafe(branchName: string, options: { cwd?: string; force?: boolean } = {}): { success: boolean; error?: string } {
|
|
862
|
+
const { cwd, force = false } = options;
|
|
863
|
+
|
|
864
|
+
// Check if origin exists
|
|
865
|
+
if (!remoteExists('origin', { cwd })) {
|
|
866
|
+
return { success: false, error: 'No remote "origin" configured' };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const args = ['push'];
|
|
870
|
+
if (force) {
|
|
871
|
+
args.push('--force');
|
|
872
|
+
}
|
|
873
|
+
args.push('-u', 'origin', branchName);
|
|
874
|
+
|
|
875
|
+
const result = runGitResult(args, { cwd });
|
|
876
|
+
|
|
877
|
+
if (result.success) {
|
|
878
|
+
return { success: true };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return { success: false, error: result.stderr };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Auto-commit any uncommitted changes and push to remote
|
|
886
|
+
* Used for checkpoint before destructive operations
|
|
887
|
+
*/
|
|
888
|
+
export function checkpointAndPush(options: {
|
|
889
|
+
cwd?: string;
|
|
890
|
+
message?: string;
|
|
891
|
+
branchName?: string;
|
|
892
|
+
} = {}): { success: boolean; committed: boolean; pushed: boolean; error?: string } {
|
|
893
|
+
const { cwd, message = '[cursorflow] checkpoint before clean' } = options;
|
|
894
|
+
|
|
895
|
+
let committed = false;
|
|
896
|
+
let pushed = false;
|
|
897
|
+
|
|
898
|
+
// Get current branch if not specified
|
|
899
|
+
let branchName = options.branchName;
|
|
900
|
+
if (!branchName) {
|
|
901
|
+
try {
|
|
902
|
+
branchName = getCurrentBranch(cwd);
|
|
903
|
+
} catch {
|
|
904
|
+
return { success: false, committed: false, pushed: false, error: 'Failed to get current branch' };
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Check for uncommitted changes
|
|
909
|
+
if (hasUncommittedChanges(cwd)) {
|
|
910
|
+
// Stage all changes
|
|
911
|
+
const addResult = runGitResult(['add', '-A'], { cwd });
|
|
912
|
+
if (!addResult.success) {
|
|
913
|
+
return { success: false, committed: false, pushed: false, error: `Failed to stage changes: ${addResult.stderr}` };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Commit
|
|
917
|
+
const commitResult = runGitResult(['commit', '-m', message], { cwd });
|
|
918
|
+
if (!commitResult.success) {
|
|
919
|
+
return { success: false, committed: false, pushed: false, error: `Failed to commit: ${commitResult.stderr}` };
|
|
920
|
+
}
|
|
921
|
+
committed = true;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Push to remote
|
|
925
|
+
const pushResult = pushBranchSafe(branchName, { cwd });
|
|
926
|
+
if (pushResult.success) {
|
|
927
|
+
pushed = true;
|
|
928
|
+
} else {
|
|
929
|
+
// Push failed but commit succeeded - partial success
|
|
930
|
+
if (committed) {
|
|
931
|
+
return { success: true, committed: true, pushed: false, error: `Commit succeeded but push failed: ${pushResult.error}` };
|
|
932
|
+
}
|
|
933
|
+
// Nothing to commit and push failed - check if there's anything to push
|
|
934
|
+
const localCommits = runGitResult(['rev-list', `origin/${branchName}..HEAD`], { cwd });
|
|
935
|
+
if (localCommits.success && localCommits.stdout.trim()) {
|
|
936
|
+
return { success: false, committed: false, pushed: false, error: `Push failed: ${pushResult.error}` };
|
|
937
|
+
}
|
|
938
|
+
// Nothing to push
|
|
939
|
+
pushed = true;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return { success: true, committed, pushed };
|
|
943
|
+
}
|
|
944
|
+
|
|
832
945
|
export function syncWithRemote(branch: string, options: {
|
|
833
946
|
cwd?: string;
|
|
834
947
|
strategy?: 'merge' | 'rebase';
|