@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26
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 +25 -0
- package/README.md +25 -7
- package/commands/cursorflow-clean.md +19 -0
- package/commands/cursorflow-runs.md +59 -0
- package/commands/cursorflow-stop.md +55 -0
- package/dist/cli/clean.js +178 -6
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +8 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +126 -77
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -0
- package/dist/cli/monitor.js +1021 -202
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +39 -21
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +268 -163
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +11 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runs.d.ts +5 -0
- package/dist/cli/runs.js +214 -0
- package/dist/cli/runs.js.map +1 -0
- package/dist/cli/setup-commands.js +0 -0
- package/dist/cli/signal.js +8 -8
- package/dist/cli/signal.js.map +1 -1
- package/dist/cli/stop.d.ts +5 -0
- package/dist/cli/stop.js +215 -0
- package/dist/cli/stop.js.map +1 -0
- package/dist/cli/tasks.d.ts +10 -0
- package/dist/cli/tasks.js +165 -0
- package/dist/cli/tasks.js.map +1 -0
- package/dist/core/auto-recovery.d.ts +212 -0
- package/dist/core/auto-recovery.js +737 -0
- package/dist/core/auto-recovery.js.map +1 -0
- package/dist/core/failure-policy.d.ts +156 -0
- package/dist/core/failure-policy.js +488 -0
- package/dist/core/failure-policy.js.map +1 -0
- package/dist/core/orchestrator.d.ts +16 -2
- package/dist/core/orchestrator.js +439 -105
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +2 -0
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +33 -10
- package/dist/core/runner.js +374 -164
- package/dist/core/runner.js.map +1 -1
- package/dist/services/logging/buffer.d.ts +67 -0
- package/dist/services/logging/buffer.js +309 -0
- package/dist/services/logging/buffer.js.map +1 -0
- package/dist/services/logging/console.d.ts +89 -0
- package/dist/services/logging/console.js +169 -0
- package/dist/services/logging/console.js.map +1 -0
- package/dist/services/logging/file-writer.d.ts +71 -0
- package/dist/services/logging/file-writer.js +516 -0
- package/dist/services/logging/file-writer.js.map +1 -0
- package/dist/services/logging/formatter.d.ts +39 -0
- package/dist/services/logging/formatter.js +227 -0
- package/dist/services/logging/formatter.js.map +1 -0
- package/dist/services/logging/index.d.ts +11 -0
- package/dist/services/logging/index.js +30 -0
- package/dist/services/logging/index.js.map +1 -0
- package/dist/services/logging/parser.d.ts +31 -0
- package/dist/services/logging/parser.js +222 -0
- package/dist/services/logging/parser.js.map +1 -0
- package/dist/services/process/index.d.ts +59 -0
- package/dist/services/process/index.js +257 -0
- package/dist/services/process/index.js.map +1 -0
- package/dist/types/agent.d.ts +20 -0
- package/dist/types/agent.js +6 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/config.d.ts +65 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/events.d.ts +125 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lane.d.ts +43 -0
- package/dist/types/lane.js +6 -0
- package/dist/types/lane.js.map +1 -0
- package/dist/types/logging.d.ts +71 -0
- package/dist/types/logging.js +16 -0
- package/dist/types/logging.js.map +1 -0
- package/dist/types/review.d.ts +17 -0
- package/dist/types/review.js +6 -0
- package/dist/types/review.js.map +1 -0
- package/dist/types/run.d.ts +32 -0
- package/dist/types/run.js +6 -0
- package/dist/types/run.js.map +1 -0
- package/dist/types/task.d.ts +71 -0
- package/dist/types/task.js +6 -0
- package/dist/types/task.js.map +1 -0
- package/dist/ui/components.d.ts +134 -0
- package/dist/ui/components.js +389 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/log-viewer.d.ts +49 -0
- package/dist/ui/log-viewer.js +449 -0
- package/dist/ui/log-viewer.js.map +1 -0
- package/dist/utils/checkpoint.d.ts +87 -0
- package/dist/utils/checkpoint.js +317 -0
- package/dist/utils/checkpoint.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +18 -8
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/dependency.d.ts +74 -0
- package/dist/utils/dependency.js +420 -0
- package/dist/utils/dependency.js.map +1 -0
- package/dist/utils/doctor.js +17 -11
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +10 -33
- package/dist/utils/enhanced-logger.js +108 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +121 -0
- package/dist/utils/git.js +484 -11
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/health.d.ts +91 -0
- package/dist/utils/health.js +556 -0
- package/dist/utils/health.js.map +1 -0
- package/dist/utils/lock.d.ts +95 -0
- package/dist/utils/lock.js +332 -0
- package/dist/utils/lock.js.map +1 -0
- package/dist/utils/log-buffer.d.ts +17 -0
- package/dist/utils/log-buffer.js +14 -0
- package/dist/utils/log-buffer.js.map +1 -0
- package/dist/utils/log-constants.d.ts +23 -0
- package/dist/utils/log-constants.js +28 -0
- package/dist/utils/log-constants.js.map +1 -0
- package/dist/utils/log-formatter.d.ts +25 -0
- package/dist/utils/log-formatter.js +237 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/log-service.d.ts +19 -0
- package/dist/utils/log-service.js +47 -0
- package/dist/utils/log-service.js.map +1 -0
- package/dist/utils/logger.d.ts +46 -27
- package/dist/utils/logger.js +82 -60
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/process-manager.d.ts +21 -0
- package/dist/utils/process-manager.js +138 -0
- package/dist/utils/process-manager.js.map +1 -0
- package/dist/utils/retry.d.ts +121 -0
- package/dist/utils/retry.js +374 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/run-service.d.ts +88 -0
- package/dist/utils/run-service.js +412 -0
- package/dist/utils/run-service.js.map +1 -0
- package/dist/utils/state.d.ts +62 -3
- package/dist/utils/state.js +317 -11
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +82 -0
- package/dist/utils/task-service.js +348 -0
- package/dist/utils/task-service.js.map +1 -0
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +2 -271
- package/dist/utils/types.js +16 -0
- package/dist/utils/types.js.map +1 -1
- package/package.json +38 -23
- package/scripts/ai-security-check.js +0 -1
- package/scripts/local-security-gate.sh +0 -0
- package/scripts/monitor-lanes.sh +94 -0
- package/scripts/patches/test-cursor-agent.js +0 -1
- package/scripts/release.sh +0 -0
- package/scripts/setup-security.sh +0 -0
- package/scripts/stream-logs.sh +72 -0
- package/scripts/verify-and-fix.sh +0 -0
- package/src/cli/clean.ts +187 -6
- package/src/cli/index.ts +12 -1
- package/src/cli/init.ts +8 -7
- package/src/cli/logs.ts +124 -77
- package/src/cli/monitor.ts +1815 -898
- package/src/cli/prepare.ts +41 -21
- package/src/cli/resume.ts +753 -626
- package/src/cli/run.ts +12 -5
- package/src/cli/runs.ts +212 -0
- package/src/cli/setup-commands.ts +0 -0
- package/src/cli/signal.ts +8 -7
- package/src/cli/stop.ts +209 -0
- package/src/cli/tasks.ts +154 -0
- package/src/core/auto-recovery.ts +909 -0
- package/src/core/failure-policy.ts +592 -0
- package/src/core/orchestrator.ts +1131 -704
- package/src/core/reviewer.ts +4 -0
- package/src/core/runner.ts +444 -180
- package/src/services/logging/buffer.ts +326 -0
- package/src/services/logging/console.ts +193 -0
- package/src/services/logging/file-writer.ts +526 -0
- package/src/services/logging/formatter.ts +268 -0
- package/src/services/logging/index.ts +16 -0
- package/src/services/logging/parser.ts +232 -0
- package/src/services/process/index.ts +261 -0
- package/src/types/agent.ts +24 -0
- package/src/types/config.ts +79 -0
- package/src/types/events.ts +156 -0
- package/src/types/index.ts +29 -0
- package/src/types/lane.ts +56 -0
- package/src/types/logging.ts +96 -0
- package/src/types/review.ts +20 -0
- package/src/types/run.ts +37 -0
- package/src/types/task.ts +79 -0
- package/src/ui/components.ts +430 -0
- package/src/ui/log-viewer.ts +485 -0
- package/src/utils/checkpoint.ts +374 -0
- package/src/utils/config.ts +18 -8
- package/src/utils/cursor-agent.ts +1 -1
- package/src/utils/dependency.ts +482 -0
- package/src/utils/doctor.ts +18 -11
- package/src/utils/enhanced-logger.ts +122 -60
- package/src/utils/git.ts +517 -11
- package/src/utils/health.ts +596 -0
- package/src/utils/lock.ts +346 -0
- package/src/utils/log-buffer.ts +28 -0
- package/src/utils/log-constants.ts +26 -0
- package/src/utils/log-formatter.ts +245 -0
- package/src/utils/log-service.ts +49 -0
- package/src/utils/logger.ts +100 -51
- package/src/utils/path.ts +45 -0
- package/src/utils/process-manager.ts +100 -0
- package/src/utils/retry.ts +413 -0
- package/src/utils/run-service.ts +433 -0
- package/src/utils/state.ts +385 -11
- package/src/utils/task-service.ts +370 -0
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +2 -314
- package/templates/basic.json +21 -0
package/src/utils/state.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* State management utilities for CursorFlow
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Atomic writes to prevent corruption
|
|
6
|
+
* - State validation and repair
|
|
7
|
+
* - Versioned state with conflict detection
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
import * as fs from 'fs';
|
|
6
11
|
import * as path from 'path';
|
|
12
|
+
import { safeJoin } from './path';
|
|
7
13
|
import {
|
|
8
14
|
LaneState,
|
|
9
15
|
ConversationEntry,
|
|
@@ -11,10 +17,33 @@ import {
|
|
|
11
17
|
EventEntry,
|
|
12
18
|
RunnerConfig
|
|
13
19
|
} from './types';
|
|
20
|
+
import { tryAcquireLockSync, releaseLockSync, LockOptions } from './lock';
|
|
21
|
+
import * as git from './git';
|
|
22
|
+
|
|
14
23
|
export { LaneState, ConversationEntry, GitLogEntry, EventEntry };
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
|
-
*
|
|
26
|
+
* Extended state with metadata for versioning
|
|
27
|
+
*/
|
|
28
|
+
export interface VersionedState<T> {
|
|
29
|
+
_version: number;
|
|
30
|
+
_updatedAt: number;
|
|
31
|
+
_pid: number;
|
|
32
|
+
data: T;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* State validation result
|
|
37
|
+
*/
|
|
38
|
+
export interface StateValidationResult {
|
|
39
|
+
valid: boolean;
|
|
40
|
+
issues: string[];
|
|
41
|
+
repaired: boolean;
|
|
42
|
+
repairedState?: LaneState;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Save state to JSON file with atomic write
|
|
18
47
|
*/
|
|
19
48
|
export function saveState(statePath: string, state: any): void {
|
|
20
49
|
const stateDir = path.dirname(statePath);
|
|
@@ -23,11 +52,64 @@ export function saveState(statePath: string, state: any): void {
|
|
|
23
52
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
24
53
|
}
|
|
25
54
|
|
|
26
|
-
|
|
55
|
+
// Atomic write: write to temp file, then rename
|
|
56
|
+
const tempPath = `${statePath}.tmp.${process.pid}`;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), 'utf8');
|
|
60
|
+
fs.renameSync(tempPath, statePath);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Clean up temp file if rename fails
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(tempPath)) {
|
|
65
|
+
fs.unlinkSync(tempPath);
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Ignore cleanup errors
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Save state with file lock to prevent race conditions
|
|
76
|
+
*/
|
|
77
|
+
export function saveStateWithLock(statePath: string, state: any, lockOptions?: LockOptions): void {
|
|
78
|
+
const lockPath = `${statePath}.lock`;
|
|
79
|
+
|
|
80
|
+
// Try to acquire lock
|
|
81
|
+
const maxRetries = 50;
|
|
82
|
+
let acquired = false;
|
|
83
|
+
let retries = 0;
|
|
84
|
+
|
|
85
|
+
while (retries < maxRetries && !acquired) {
|
|
86
|
+
acquired = tryAcquireLockSync(lockPath, {
|
|
87
|
+
operation: 'saveState',
|
|
88
|
+
staleTimeoutMs: 10000,
|
|
89
|
+
...lockOptions
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!acquired) {
|
|
93
|
+
retries++;
|
|
94
|
+
// Sync sleep
|
|
95
|
+
const end = Date.now() + 100;
|
|
96
|
+
while (Date.now() < end) { /* wait */ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!acquired) {
|
|
101
|
+
throw new Error(`Failed to acquire lock for state file: ${statePath}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
saveState(statePath, state);
|
|
106
|
+
} finally {
|
|
107
|
+
releaseLockSync(lockPath);
|
|
108
|
+
}
|
|
27
109
|
}
|
|
28
110
|
|
|
29
111
|
/**
|
|
30
|
-
* Load state from JSON file
|
|
112
|
+
* Load state from JSON file with corruption recovery
|
|
31
113
|
*/
|
|
32
114
|
export function loadState<T = any>(statePath: string): T | null {
|
|
33
115
|
if (!fs.existsSync(statePath)) {
|
|
@@ -38,11 +120,296 @@ export function loadState<T = any>(statePath: string): T | null {
|
|
|
38
120
|
const content = fs.readFileSync(statePath, 'utf8');
|
|
39
121
|
return JSON.parse(content) as T;
|
|
40
122
|
} catch (error: any) {
|
|
123
|
+
// Try to recover from backup if exists
|
|
124
|
+
const backupPath = `${statePath}.backup`;
|
|
125
|
+
if (fs.existsSync(backupPath)) {
|
|
126
|
+
try {
|
|
127
|
+
console.warn(`Warning: Main state file corrupted, trying backup: ${statePath}`);
|
|
128
|
+
const backupContent = fs.readFileSync(backupPath, 'utf8');
|
|
129
|
+
const backupState = JSON.parse(backupContent) as T;
|
|
130
|
+
|
|
131
|
+
// Restore from backup
|
|
132
|
+
saveState(statePath, backupState);
|
|
133
|
+
return backupState;
|
|
134
|
+
} catch {
|
|
135
|
+
// Backup also corrupted
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
41
139
|
console.warn(`Warning: Failed to parse state file ${statePath}: ${error.message}`);
|
|
42
140
|
return null;
|
|
43
141
|
}
|
|
44
142
|
}
|
|
45
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Load state with automatic backup creation
|
|
146
|
+
*/
|
|
147
|
+
export function loadStateWithBackup<T = any>(statePath: string): T | null {
|
|
148
|
+
const state = loadState<T>(statePath);
|
|
149
|
+
|
|
150
|
+
if (state) {
|
|
151
|
+
// Create backup
|
|
152
|
+
const backupPath = `${statePath}.backup`;
|
|
153
|
+
try {
|
|
154
|
+
fs.copyFileSync(statePath, backupPath);
|
|
155
|
+
} catch {
|
|
156
|
+
// Ignore backup failures
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Update state with optimistic locking
|
|
165
|
+
*/
|
|
166
|
+
export function updateStateAtomic(
|
|
167
|
+
statePath: string,
|
|
168
|
+
updater: (state: LaneState | null) => LaneState
|
|
169
|
+
): LaneState {
|
|
170
|
+
const lockPath = `${statePath}.lock`;
|
|
171
|
+
|
|
172
|
+
// Acquire lock
|
|
173
|
+
const maxRetries = 50;
|
|
174
|
+
let acquired = false;
|
|
175
|
+
let retries = 0;
|
|
176
|
+
|
|
177
|
+
while (retries < maxRetries && !acquired) {
|
|
178
|
+
acquired = tryAcquireLockSync(lockPath, { operation: 'updateState', staleTimeoutMs: 10000 });
|
|
179
|
+
if (!acquired) {
|
|
180
|
+
retries++;
|
|
181
|
+
const end = Date.now() + 100;
|
|
182
|
+
while (Date.now() < end) { /* wait */ }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!acquired) {
|
|
187
|
+
throw new Error(`Failed to acquire lock for state update: ${statePath}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const currentState = loadState<LaneState>(statePath);
|
|
192
|
+
const newState = updater(currentState);
|
|
193
|
+
newState.updatedAt = Date.now();
|
|
194
|
+
saveState(statePath, newState);
|
|
195
|
+
return newState;
|
|
196
|
+
} finally {
|
|
197
|
+
releaseLockSync(lockPath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Validate lane state and check for inconsistencies
|
|
203
|
+
*/
|
|
204
|
+
export function validateLaneState(
|
|
205
|
+
statePath: string,
|
|
206
|
+
options: {
|
|
207
|
+
checkWorktree?: boolean;
|
|
208
|
+
checkBranch?: boolean;
|
|
209
|
+
autoRepair?: boolean;
|
|
210
|
+
} = {}
|
|
211
|
+
): StateValidationResult {
|
|
212
|
+
const state = loadState<LaneState>(statePath);
|
|
213
|
+
const issues: string[] = [];
|
|
214
|
+
let repaired = false;
|
|
215
|
+
let repairedState: LaneState | undefined;
|
|
216
|
+
|
|
217
|
+
if (!state) {
|
|
218
|
+
return {
|
|
219
|
+
valid: false,
|
|
220
|
+
issues: ['State file not found or corrupted'],
|
|
221
|
+
repaired: false,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check required fields
|
|
226
|
+
if (!state.label) {
|
|
227
|
+
issues.push('Missing label field');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (state.status === undefined) {
|
|
231
|
+
issues.push('Missing status field');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (state.currentTaskIndex === undefined || state.currentTaskIndex < 0) {
|
|
235
|
+
issues.push(`Invalid currentTaskIndex: ${state.currentTaskIndex}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (state.totalTasks === undefined || state.totalTasks < 0) {
|
|
239
|
+
issues.push(`Invalid totalTasks: ${state.totalTasks}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (state.currentTaskIndex > state.totalTasks) {
|
|
243
|
+
issues.push(`currentTaskIndex (${state.currentTaskIndex}) exceeds totalTasks (${state.totalTasks})`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check worktree existence
|
|
247
|
+
if (options.checkWorktree && state.worktreeDir) {
|
|
248
|
+
if (!fs.existsSync(state.worktreeDir)) {
|
|
249
|
+
issues.push(`Worktree directory not found: ${state.worktreeDir}`);
|
|
250
|
+
|
|
251
|
+
if (options.autoRepair) {
|
|
252
|
+
state.worktreeDir = null;
|
|
253
|
+
repaired = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check branch existence
|
|
259
|
+
if (options.checkBranch && state.pipelineBranch) {
|
|
260
|
+
try {
|
|
261
|
+
if (!git.branchExists(state.pipelineBranch)) {
|
|
262
|
+
issues.push(`Branch not found: ${state.pipelineBranch}`);
|
|
263
|
+
|
|
264
|
+
if (options.autoRepair) {
|
|
265
|
+
state.pipelineBranch = null;
|
|
266
|
+
repaired = true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// Git check failed, don't add issue
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check status consistency
|
|
275
|
+
if (state.status === 'completed' && !state.endTime) {
|
|
276
|
+
issues.push('Status is completed but endTime is not set');
|
|
277
|
+
|
|
278
|
+
if (options.autoRepair) {
|
|
279
|
+
state.endTime = Date.now();
|
|
280
|
+
repaired = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (state.status === 'failed' && !state.error) {
|
|
285
|
+
issues.push('Status is failed but no error message');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Save repaired state if auto-repair was enabled
|
|
289
|
+
if (repaired && options.autoRepair) {
|
|
290
|
+
state.updatedAt = Date.now();
|
|
291
|
+
saveState(statePath, state);
|
|
292
|
+
repairedState = state;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
valid: issues.length === 0,
|
|
297
|
+
issues,
|
|
298
|
+
repaired,
|
|
299
|
+
repairedState,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Repair lane state by resetting inconsistent values
|
|
305
|
+
*/
|
|
306
|
+
export function repairLaneState(statePath: string): LaneState | null {
|
|
307
|
+
const result = validateLaneState(statePath, {
|
|
308
|
+
checkWorktree: true,
|
|
309
|
+
checkBranch: true,
|
|
310
|
+
autoRepair: true,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (result.repairedState) {
|
|
314
|
+
return result.repairedState;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// If validation failed completely, try to create minimal valid state
|
|
318
|
+
const state = loadState<LaneState>(statePath);
|
|
319
|
+
if (!state) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Ensure all required fields have valid values
|
|
324
|
+
const repairedState: LaneState = {
|
|
325
|
+
label: state.label || path.basename(path.dirname(statePath)),
|
|
326
|
+
status: 'pending',
|
|
327
|
+
currentTaskIndex: Math.max(0, state.currentTaskIndex || 0),
|
|
328
|
+
totalTasks: Math.max(0, state.totalTasks || 0),
|
|
329
|
+
worktreeDir: state.worktreeDir && fs.existsSync(state.worktreeDir) ? state.worktreeDir : null,
|
|
330
|
+
pipelineBranch: state.pipelineBranch,
|
|
331
|
+
startTime: state.startTime || Date.now(),
|
|
332
|
+
endTime: null,
|
|
333
|
+
error: null,
|
|
334
|
+
dependencyRequest: null,
|
|
335
|
+
tasksFile: state.tasksFile,
|
|
336
|
+
dependsOn: state.dependsOn || [],
|
|
337
|
+
completedTasks: state.completedTasks || [],
|
|
338
|
+
updatedAt: Date.now(),
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Ensure currentTaskIndex doesn't exceed totalTasks
|
|
342
|
+
if (repairedState.currentTaskIndex > repairedState.totalTasks) {
|
|
343
|
+
repairedState.currentTaskIndex = repairedState.totalTasks;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
saveState(statePath, repairedState);
|
|
347
|
+
return repairedState;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Check if state needs recovery (e.g., after crash)
|
|
352
|
+
*/
|
|
353
|
+
export function stateNeedsRecovery(statePath: string): boolean {
|
|
354
|
+
const state = loadState<LaneState>(statePath);
|
|
355
|
+
|
|
356
|
+
if (!state) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Running state with no recent update might indicate a crash
|
|
361
|
+
if (state.status === 'running') {
|
|
362
|
+
const lastUpdate = state.updatedAt || state.startTime;
|
|
363
|
+
const staleThresholdMs = 5 * 60 * 1000; // 5 minutes
|
|
364
|
+
|
|
365
|
+
if (Date.now() - lastUpdate > staleThresholdMs) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check for temp files indicating incomplete write
|
|
371
|
+
const tempPattern = `${statePath}.tmp.`;
|
|
372
|
+
const stateDir = path.dirname(statePath);
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const files = fs.readdirSync(stateDir);
|
|
376
|
+
for (const file of files) {
|
|
377
|
+
if (file.startsWith(path.basename(tempPattern))) {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
// Ignore
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Clean up temp files from incomplete writes
|
|
390
|
+
*/
|
|
391
|
+
export function cleanupTempFiles(stateDir: string): number {
|
|
392
|
+
let cleaned = 0;
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const files = fs.readdirSync(stateDir);
|
|
396
|
+
for (const file of files) {
|
|
397
|
+
if (file.includes('.tmp.')) {
|
|
398
|
+
try {
|
|
399
|
+
fs.unlinkSync(safeJoin(stateDir, file));
|
|
400
|
+
cleaned++;
|
|
401
|
+
} catch {
|
|
402
|
+
// Ignore
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
// Ignore
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return cleaned;
|
|
411
|
+
}
|
|
412
|
+
|
|
46
413
|
/**
|
|
47
414
|
* Append to JSONL log file
|
|
48
415
|
*/
|
|
@@ -80,18 +447,25 @@ export function readLog<T = any>(logPath: string): T[] {
|
|
|
80
447
|
/**
|
|
81
448
|
* Create initial lane state
|
|
82
449
|
*/
|
|
83
|
-
export function createLaneState(
|
|
450
|
+
export function createLaneState(
|
|
451
|
+
laneName: string,
|
|
452
|
+
config: RunnerConfig,
|
|
453
|
+
tasksFile?: string,
|
|
454
|
+
options: { pipelineBranch?: string; worktreeDir?: string } = {}
|
|
455
|
+
): LaneState {
|
|
84
456
|
return {
|
|
85
457
|
label: laneName,
|
|
86
458
|
status: 'pending',
|
|
87
459
|
currentTaskIndex: 0,
|
|
88
460
|
totalTasks: config.tasks ? config.tasks.length : 0,
|
|
89
|
-
worktreeDir: null,
|
|
90
|
-
pipelineBranch: null,
|
|
461
|
+
worktreeDir: options.worktreeDir || null,
|
|
462
|
+
pipelineBranch: options.pipelineBranch || null,
|
|
91
463
|
startTime: Date.now(),
|
|
92
464
|
endTime: null,
|
|
93
465
|
error: null,
|
|
94
466
|
dependencyRequest: null,
|
|
467
|
+
tasksFile,
|
|
468
|
+
dependsOn: config.dependsOn || [],
|
|
95
469
|
};
|
|
96
470
|
}
|
|
97
471
|
|
|
@@ -151,7 +525,7 @@ export function getLatestRunDir(logsDir: string): string | null {
|
|
|
151
525
|
}
|
|
152
526
|
|
|
153
527
|
const runs = fs.readdirSync(logsDir)
|
|
154
|
-
.filter(f => fs.statSync(
|
|
528
|
+
.filter(f => fs.statSync(safeJoin(logsDir, f)).isDirectory())
|
|
155
529
|
.sort()
|
|
156
530
|
.reverse();
|
|
157
531
|
|
|
@@ -159,7 +533,7 @@ export function getLatestRunDir(logsDir: string): string | null {
|
|
|
159
533
|
return null;
|
|
160
534
|
}
|
|
161
535
|
|
|
162
|
-
return
|
|
536
|
+
return safeJoin(logsDir, runs[0]!);
|
|
163
537
|
}
|
|
164
538
|
|
|
165
539
|
/**
|
|
@@ -171,11 +545,11 @@ export function listLanesInRun(runDir: string): { name: string; dir: string; sta
|
|
|
171
545
|
}
|
|
172
546
|
|
|
173
547
|
return fs.readdirSync(runDir)
|
|
174
|
-
.filter(f => fs.statSync(
|
|
548
|
+
.filter(f => fs.statSync(safeJoin(runDir, f)).isDirectory())
|
|
175
549
|
.map(laneName => ({
|
|
176
550
|
name: laneName,
|
|
177
|
-
dir:
|
|
178
|
-
statePath:
|
|
551
|
+
dir: safeJoin(runDir, laneName),
|
|
552
|
+
statePath: safeJoin(runDir, laneName, 'state.json'),
|
|
179
553
|
}));
|
|
180
554
|
}
|
|
181
555
|
|