@litmers/cursorflow-orchestrator 0.2.9 → 0.2.10

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.
@@ -0,0 +1,551 @@
1
+ /**
2
+ * CursorFlow status command - View detailed lane status for a run
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import * as fs from 'fs';
7
+ import * as logger from '../utils/logger';
8
+ import { loadConfig, getLogsDir } from '../utils/config';
9
+ import { loadState } from '../utils/state';
10
+ import { LaneState } from '../types';
11
+ import { safeJoin } from '../utils/path';
12
+ import { RunService } from '../utils/run-service';
13
+
14
+ interface StatusOptions {
15
+ runId: string | null;
16
+ lane: string | null;
17
+ json: boolean;
18
+ watch: boolean;
19
+ help: boolean;
20
+ }
21
+
22
+ interface DetailedLaneStatus {
23
+ name: string;
24
+ status: string;
25
+ progress: string;
26
+ currentTask: number;
27
+ totalTasks: number;
28
+ branch: string | null;
29
+ worktree: string | null;
30
+ pid: number | null;
31
+ processAlive: boolean;
32
+ startTime: number | null;
33
+ endTime: number | null;
34
+ duration: string;
35
+ error: string | null;
36
+ waitingFor: string[];
37
+ chatId: string | null;
38
+ completedTasks: string[];
39
+ }
40
+
41
+ interface RunStatusSummary {
42
+ runId: string;
43
+ runDir: string;
44
+ taskName: string;
45
+ status: string;
46
+ startTime: number;
47
+ duration: string;
48
+ lanes: DetailedLaneStatus[];
49
+ summary: {
50
+ total: number;
51
+ running: number;
52
+ completed: number;
53
+ failed: number;
54
+ pending: number;
55
+ waiting: number;
56
+ paused: number;
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Status indicator colors
62
+ */
63
+ const STATUS_COLORS: Record<string, string> = {
64
+ completed: '\x1b[32m', // green
65
+ running: '\x1b[36m', // cyan
66
+ pending: '\x1b[33m', // yellow
67
+ failed: '\x1b[31m', // red
68
+ paused: '\x1b[35m', // magenta
69
+ waiting: '\x1b[33m', // yellow
70
+ unknown: '\x1b[90m', // gray
71
+ };
72
+ const RESET = '\x1b[0m';
73
+ const BOLD = '\x1b[1m';
74
+ const DIM = '\x1b[2m';
75
+
76
+ function printHelp(): void {
77
+ console.log(`
78
+ Usage: cursorflow status [run-id] [options]
79
+
80
+ View detailed status of lanes in a run.
81
+
82
+ Options:
83
+ [run-id] Run ID (default: latest run)
84
+ --lane <name> Show detailed status for specific lane
85
+ --json Output as JSON
86
+ --watch Watch mode - refresh every 2 seconds
87
+ --help, -h Show help
88
+
89
+ Examples:
90
+ cursorflow status # Status of latest run
91
+ cursorflow status run-1234567890 # Status of specific run
92
+ cursorflow status --lane backend # Detailed status of specific lane
93
+ cursorflow status --json # JSON output for scripting
94
+ cursorflow status --watch # Live status updates
95
+ `);
96
+ }
97
+
98
+ function parseArgs(args: string[]): StatusOptions {
99
+ const laneIdx = args.indexOf('--lane');
100
+
101
+ // Find run-id (first arg that starts with 'run-' or isn't an option)
102
+ let runId: string | null = null;
103
+ for (const arg of args) {
104
+ if (arg.startsWith('run-')) {
105
+ runId = arg;
106
+ break;
107
+ }
108
+ if (!arg.startsWith('--') && !arg.startsWith('-') && args.indexOf(arg) === 0) {
109
+ // First positional argument could be run-id or 'latest'
110
+ if (arg !== 'latest') {
111
+ runId = arg;
112
+ }
113
+ break;
114
+ }
115
+ }
116
+
117
+ return {
118
+ runId,
119
+ lane: laneIdx >= 0 ? args[laneIdx + 1] || null : null,
120
+ json: args.includes('--json'),
121
+ watch: args.includes('--watch'),
122
+ help: args.includes('--help') || args.includes('-h'),
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Check if a process is alive by its PID
128
+ */
129
+ function isProcessAlive(pid: number): boolean {
130
+ try {
131
+ process.kill(pid, 0);
132
+ return true;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Format duration in human readable form
140
+ */
141
+ function formatDuration(ms: number): string {
142
+ if (ms < 0) return '-';
143
+
144
+ const seconds = Math.floor(ms / 1000);
145
+ const minutes = Math.floor(seconds / 60);
146
+ const hours = Math.floor(minutes / 60);
147
+
148
+ if (hours > 0) {
149
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
150
+ } else if (minutes > 0) {
151
+ return `${minutes}m ${seconds % 60}s`;
152
+ } else {
153
+ return `${seconds}s`;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Find the latest run directory
159
+ */
160
+ function findLatestRunDir(logsDir: string): string | null {
161
+ const runsDir = safeJoin(logsDir, 'runs');
162
+ if (!fs.existsSync(runsDir)) return null;
163
+
164
+ const runs = fs.readdirSync(runsDir)
165
+ .filter(d => d.startsWith('run-'))
166
+ .sort()
167
+ .reverse();
168
+
169
+ return runs.length > 0 ? runs[0]! : null;
170
+ }
171
+
172
+ /**
173
+ * Get detailed lane status from lane directory
174
+ */
175
+ function getDetailedLaneStatus(laneDir: string, laneName: string): DetailedLaneStatus {
176
+ const statePath = safeJoin(laneDir, 'state.json');
177
+ const state = loadState<LaneState>(statePath);
178
+
179
+ if (!state) {
180
+ return {
181
+ name: laneName,
182
+ status: 'pending',
183
+ progress: '0%',
184
+ currentTask: 0,
185
+ totalTasks: 0,
186
+ branch: null,
187
+ worktree: null,
188
+ pid: null,
189
+ processAlive: false,
190
+ startTime: null,
191
+ endTime: null,
192
+ duration: '-',
193
+ error: null,
194
+ waitingFor: [],
195
+ chatId: null,
196
+ completedTasks: [],
197
+ };
198
+ }
199
+
200
+ const progress = state.totalTasks > 0
201
+ ? Math.round((state.currentTaskIndex / state.totalTasks) * 100)
202
+ : 0;
203
+
204
+ const processAlive = state.pid ? isProcessAlive(state.pid) : false;
205
+
206
+ let duration = 0;
207
+ if (state.startTime) {
208
+ if (state.endTime) {
209
+ duration = state.endTime - state.startTime;
210
+ } else if (state.status === 'running') {
211
+ duration = Date.now() - state.startTime;
212
+ }
213
+ }
214
+
215
+ return {
216
+ name: laneName,
217
+ status: state.status || 'unknown',
218
+ progress: `${progress}%`,
219
+ currentTask: state.currentTaskIndex || 0,
220
+ totalTasks: state.totalTasks || 0,
221
+ branch: state.pipelineBranch,
222
+ worktree: state.worktreeDir,
223
+ pid: state.pid || null,
224
+ processAlive,
225
+ startTime: state.startTime || null,
226
+ endTime: state.endTime || null,
227
+ duration: formatDuration(duration),
228
+ error: state.error,
229
+ waitingFor: state.waitingFor || [],
230
+ chatId: state.chatId || null,
231
+ completedTasks: state.completedTasks || [],
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Get full run status with all lanes
237
+ */
238
+ function getRunStatus(runDir: string, runId: string): RunStatusSummary {
239
+ const lanesDir = safeJoin(runDir, 'lanes');
240
+ const statePath = safeJoin(runDir, 'state.json');
241
+
242
+ let taskName = 'Unknown';
243
+ let runStartTime = 0;
244
+
245
+ // Try to get task name from orchestrator state
246
+ if (fs.existsSync(statePath)) {
247
+ try {
248
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
249
+ taskName = state.taskName || 'Unknown';
250
+ runStartTime = state.startTime || extractTimestampFromRunId(runId);
251
+ } catch {
252
+ runStartTime = extractTimestampFromRunId(runId);
253
+ }
254
+ } else {
255
+ runStartTime = extractTimestampFromRunId(runId);
256
+ }
257
+
258
+ const lanes: DetailedLaneStatus[] = [];
259
+ const summary = {
260
+ total: 0,
261
+ running: 0,
262
+ completed: 0,
263
+ failed: 0,
264
+ pending: 0,
265
+ waiting: 0,
266
+ paused: 0,
267
+ };
268
+
269
+ if (fs.existsSync(lanesDir)) {
270
+ const laneDirs = fs.readdirSync(lanesDir)
271
+ .filter(f => fs.statSync(safeJoin(lanesDir, f)).isDirectory())
272
+ .sort();
273
+
274
+ for (const laneName of laneDirs) {
275
+ const laneDir = safeJoin(lanesDir, laneName);
276
+ const laneStatus = getDetailedLaneStatus(laneDir, laneName);
277
+ lanes.push(laneStatus);
278
+
279
+ summary.total++;
280
+ switch (laneStatus.status) {
281
+ case 'running': summary.running++; break;
282
+ case 'completed': summary.completed++; break;
283
+ case 'failed': summary.failed++; break;
284
+ case 'pending': summary.pending++; break;
285
+ case 'waiting': summary.waiting++; break;
286
+ case 'paused': summary.paused++; break;
287
+ }
288
+ }
289
+ }
290
+
291
+ // Calculate overall run status
292
+ let runStatus = 'pending';
293
+ if (summary.running > 0) {
294
+ runStatus = 'running';
295
+ } else if (summary.completed === summary.total && summary.total > 0) {
296
+ runStatus = 'completed';
297
+ } else if (summary.failed > 0) {
298
+ runStatus = summary.completed > 0 ? 'partial' : 'failed';
299
+ } else if (summary.waiting > 0 || summary.paused > 0) {
300
+ runStatus = 'waiting';
301
+ }
302
+
303
+ // Calculate run duration
304
+ let runDuration = 0;
305
+ if (runStartTime) {
306
+ const latestEndTime = lanes
307
+ .filter(l => l.endTime)
308
+ .reduce((max, l) => Math.max(max, l.endTime!), 0);
309
+
310
+ if (runStatus === 'completed' || runStatus === 'failed') {
311
+ runDuration = latestEndTime - runStartTime;
312
+ } else {
313
+ runDuration = Date.now() - runStartTime;
314
+ }
315
+ }
316
+
317
+ return {
318
+ runId,
319
+ runDir,
320
+ taskName,
321
+ status: runStatus,
322
+ startTime: runStartTime,
323
+ duration: formatDuration(runDuration),
324
+ lanes,
325
+ summary,
326
+ };
327
+ }
328
+
329
+ function extractTimestampFromRunId(runId: string): number {
330
+ const match = runId.match(/run-(\d+)/);
331
+ return match ? parseInt(match[1], 10) : Date.now();
332
+ }
333
+
334
+ /**
335
+ * Print lane status table
336
+ */
337
+ function printLaneTable(runStatus: RunStatusSummary): void {
338
+ const { lanes, summary, runId, taskName, status, duration, startTime } = runStatus;
339
+
340
+ // Header
341
+ console.log('');
342
+ console.log(`${BOLD}📊 Run Status${RESET}`);
343
+ console.log(`${'─'.repeat(80)}`);
344
+ console.log(` ${DIM}Run ID:${RESET} ${runId}`);
345
+ console.log(` ${DIM}Task:${RESET} ${taskName}`);
346
+ console.log(` ${DIM}Status:${RESET} ${STATUS_COLORS[status] || ''}${status}${RESET}`);
347
+ console.log(` ${DIM}Started:${RESET} ${startTime ? new Date(startTime).toLocaleString() : '-'}`);
348
+ console.log(` ${DIM}Duration:${RESET} ${duration}`);
349
+ console.log('');
350
+
351
+ // Summary bar
352
+ const barWidth = 40;
353
+ const completedWidth = Math.round((summary.completed / summary.total) * barWidth) || 0;
354
+ const runningWidth = Math.round((summary.running / summary.total) * barWidth) || 0;
355
+ const failedWidth = Math.round((summary.failed / summary.total) * barWidth) || 0;
356
+ const remainingWidth = barWidth - completedWidth - runningWidth - failedWidth;
357
+
358
+ const progressBar =
359
+ '\x1b[42m' + ' '.repeat(completedWidth) + '\x1b[0m' + // green for completed
360
+ '\x1b[46m' + ' '.repeat(runningWidth) + '\x1b[0m' + // cyan for running
361
+ '\x1b[41m' + ' '.repeat(failedWidth) + '\x1b[0m' + // red for failed
362
+ '\x1b[47m' + ' '.repeat(remainingWidth) + '\x1b[0m'; // white for pending
363
+
364
+ console.log(` [${progressBar}] ${summary.completed}/${summary.total} completed`);
365
+ console.log('');
366
+
367
+ if (lanes.length === 0) {
368
+ console.log(' No lanes found.');
369
+ return;
370
+ }
371
+
372
+ // Table header
373
+ console.log(`${BOLD}${'─'.repeat(80)}${RESET}`);
374
+ console.log(` ${'Lane'.padEnd(20)} ${'Status'.padEnd(12)} ${'Progress'.padEnd(10)} ${'Duration'.padEnd(12)} ${'PID'.padEnd(8)} Branch`);
375
+ console.log(`${'─'.repeat(80)}`);
376
+
377
+ // Lane rows
378
+ for (const lane of lanes) {
379
+ const statusColor = STATUS_COLORS[lane.status] || STATUS_COLORS.unknown;
380
+ const progressStr = `${lane.currentTask}/${lane.totalTasks}`;
381
+ const pidStr = lane.pid
382
+ ? (lane.processAlive ? `${lane.pid}` : `${DIM}${lane.pid}†${RESET}`)
383
+ : '-';
384
+ const branchStr = lane.branch || '-';
385
+
386
+ console.log(` ${lane.name.padEnd(20)} ${statusColor}${lane.status.padEnd(12)}${RESET} ${progressStr.padEnd(10)} ${lane.duration.padEnd(12)} ${pidStr.padEnd(8)} ${branchStr}`);
387
+
388
+ // Show error if failed
389
+ if (lane.status === 'failed' && lane.error) {
390
+ const errorMsg = lane.error.length > 60 ? lane.error.substring(0, 57) + '...' : lane.error;
391
+ console.log(` ${' '.repeat(20)} ${DIM}└─ \x1b[31m${errorMsg}${RESET}`);
392
+ }
393
+
394
+ // Show waiting dependencies
395
+ if (lane.waitingFor && lane.waitingFor.length > 0) {
396
+ console.log(` ${' '.repeat(20)} ${DIM}└─ Waiting: ${lane.waitingFor.join(', ')}${RESET}`);
397
+ }
398
+ }
399
+
400
+ console.log(`${'─'.repeat(80)}`);
401
+
402
+ // Summary
403
+ console.log('');
404
+ const summaryParts = [];
405
+ if (summary.completed > 0) summaryParts.push(`\x1b[32m${summary.completed} completed\x1b[0m`);
406
+ if (summary.running > 0) summaryParts.push(`\x1b[36m${summary.running} running\x1b[0m`);
407
+ if (summary.failed > 0) summaryParts.push(`\x1b[31m${summary.failed} failed\x1b[0m`);
408
+ if (summary.pending > 0) summaryParts.push(`\x1b[33m${summary.pending} pending\x1b[0m`);
409
+ if (summary.waiting > 0) summaryParts.push(`\x1b[33m${summary.waiting} waiting\x1b[0m`);
410
+ if (summary.paused > 0) summaryParts.push(`\x1b[35m${summary.paused} paused\x1b[0m`);
411
+
412
+ console.log(` ${BOLD}Summary:${RESET} ${summaryParts.join(' | ')}`);
413
+
414
+ // Tips
415
+ if (summary.failed > 0 || summary.paused > 0) {
416
+ console.log('');
417
+ console.log(` ${DIM}Tip: Run \x1b[32mcursorflow resume --all\x1b[0m${DIM} to resume incomplete lanes${RESET}`);
418
+ }
419
+ if (summary.running > 0) {
420
+ console.log('');
421
+ console.log(` ${DIM}Tip: Run \x1b[32mcursorflow monitor\x1b[0m${DIM} for interactive monitoring${RESET}`);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Print detailed status for a single lane
427
+ */
428
+ function printDetailedLaneStatus(laneStatus: DetailedLaneStatus, runId: string): void {
429
+ console.log('');
430
+ console.log(`${BOLD}🔍 Lane Details: ${laneStatus.name}${RESET}`);
431
+ console.log(`${'─'.repeat(60)}`);
432
+ console.log(` ${DIM}Run:${RESET} ${runId}`);
433
+ console.log(` ${DIM}Status:${RESET} ${STATUS_COLORS[laneStatus.status] || ''}${laneStatus.status}${RESET}`);
434
+ console.log(` ${DIM}Progress:${RESET} ${laneStatus.currentTask}/${laneStatus.totalTasks} (${laneStatus.progress})`);
435
+ console.log(` ${DIM}Branch:${RESET} ${laneStatus.branch || '-'}`);
436
+ console.log(` ${DIM}Worktree:${RESET} ${laneStatus.worktree || '-'}`);
437
+ console.log(` ${DIM}PID:${RESET} ${laneStatus.pid || '-'}${laneStatus.pid && !laneStatus.processAlive ? ' (dead)' : ''}`);
438
+ console.log(` ${DIM}Duration:${RESET} ${laneStatus.duration}`);
439
+ console.log(` ${DIM}Started:${RESET} ${laneStatus.startTime ? new Date(laneStatus.startTime).toLocaleString() : '-'}`);
440
+ console.log(` ${DIM}Ended:${RESET} ${laneStatus.endTime ? new Date(laneStatus.endTime).toLocaleString() : '-'}`);
441
+ console.log(` ${DIM}Chat ID:${RESET} ${laneStatus.chatId || '-'}`);
442
+
443
+ if (laneStatus.waitingFor && laneStatus.waitingFor.length > 0) {
444
+ console.log(` ${DIM}Waiting For:${RESET} ${laneStatus.waitingFor.join(', ')}`);
445
+ }
446
+
447
+ if (laneStatus.completedTasks && laneStatus.completedTasks.length > 0) {
448
+ console.log(` ${DIM}Completed Tasks:${RESET} ${laneStatus.completedTasks.join(', ')}`);
449
+ }
450
+
451
+ if (laneStatus.error) {
452
+ console.log('');
453
+ console.log(` ${BOLD}\x1b[31mError:${RESET}`);
454
+ console.log(` ${laneStatus.error}`);
455
+ }
456
+
457
+ console.log(`${'─'.repeat(60)}`);
458
+ }
459
+
460
+ /**
461
+ * Clear console and move cursor to top
462
+ */
463
+ function clearScreen(): void {
464
+ process.stdout.write('\x1b[2J\x1b[H');
465
+ }
466
+
467
+ async function status(args: string[]): Promise<void> {
468
+ const options = parseArgs(args);
469
+
470
+ if (options.help) {
471
+ printHelp();
472
+ return;
473
+ }
474
+
475
+ const config = loadConfig();
476
+ const logsDir = getLogsDir(config);
477
+ const runsDir = safeJoin(logsDir, 'runs');
478
+
479
+ // Find run directory
480
+ let runId = options.runId;
481
+ if (!runId) {
482
+ runId = findLatestRunDir(logsDir);
483
+ }
484
+
485
+ if (!runId) {
486
+ logger.warn('No runs found. Run a flow first with: cursorflow run <flow-name>');
487
+ return;
488
+ }
489
+
490
+ // Support both run ID and full path
491
+ let runDir = runId.includes(path.sep) ? runId : safeJoin(runsDir, runId);
492
+ if (!runId.startsWith('run-') && !fs.existsSync(runDir)) {
493
+ // Try adding run- prefix
494
+ runDir = safeJoin(runsDir, `run-${runId}`);
495
+ runId = `run-${runId}`;
496
+ }
497
+
498
+ if (!fs.existsSync(runDir)) {
499
+ throw new Error(`Run not found: ${runId}`);
500
+ }
501
+
502
+ // Extract just the run ID from the path if needed
503
+ runId = path.basename(runDir);
504
+
505
+ const displayStatus = () => {
506
+ const runStatus = getRunStatus(runDir, runId!);
507
+
508
+ if (options.json) {
509
+ console.log(JSON.stringify(runStatus, null, 2));
510
+ return;
511
+ }
512
+
513
+ if (options.lane) {
514
+ const lane = runStatus.lanes.find(l => l.name === options.lane);
515
+ if (!lane) {
516
+ throw new Error(`Lane '${options.lane}' not found in run ${runId}`);
517
+ }
518
+ printDetailedLaneStatus(lane, runId!);
519
+ } else {
520
+ printLaneTable(runStatus);
521
+ }
522
+ };
523
+
524
+ if (options.watch) {
525
+ // Watch mode
526
+ const refreshInterval = 2000;
527
+
528
+ const refresh = () => {
529
+ clearScreen();
530
+ console.log(`${DIM}[Auto-refresh every ${refreshInterval/1000}s - Press Ctrl+C to exit]${RESET}`);
531
+ displayStatus();
532
+ };
533
+
534
+ refresh();
535
+ const interval = setInterval(refresh, refreshInterval);
536
+
537
+ // Handle Ctrl+C gracefully
538
+ process.on('SIGINT', () => {
539
+ clearInterval(interval);
540
+ console.log('\n');
541
+ process.exit(0);
542
+ });
543
+
544
+ // Keep process alive
545
+ await new Promise(() => {});
546
+ } else {
547
+ displayStatus();
548
+ }
549
+ }
550
+
551
+ export = status;
@@ -977,23 +977,60 @@ export async function orchestrate(tasksDir: string, options: {
977
977
  printLaneStatus(lanes, laneRunDirs);
978
978
  }, options.pollInterval || 60000);
979
979
 
980
- // Handle process interruption
981
- const sigIntHandler = () => {
982
- logger.warn('\n⚠️ Orchestration interrupted! Stopping all lanes...');
980
+ // Handle process interruption - ensure proper state cleanup
981
+ const handleGracefulShutdown = async (signal: string) => {
982
+ logger.warn(`\n⚠️ Orchestration interrupted by ${signal}! Stopping all lanes...`);
983
+
984
+ // 1. Stop running lanes and update their state to 'paused'
983
985
  for (const [name, info] of running.entries()) {
984
986
  logger.info(`Stopping lane: ${name}`);
985
987
  try {
988
+ // Update state to 'paused' (not 'completed'!) before killing
989
+ const state = loadState<LaneState>(info.statePath);
990
+ if (state && state.status === 'running') {
991
+ state.status = 'paused';
992
+ state.error = `Orchestration interrupted by ${signal}`;
993
+ state.endTime = Date.now();
994
+ saveState(info.statePath, state);
995
+ }
996
+
986
997
  info.child.kill('SIGTERM');
987
- } catch {
988
- // Ignore kill errors
998
+ } catch (e) {
999
+ // Ignore kill errors but log them for debugging
1000
+ logger.debug(`Error stopping lane ${name}: ${e}`);
989
1001
  }
990
1002
  }
1003
+
1004
+ // 2. Update any pending (not started) lanes to 'pending' with clear status
1005
+ for (const lane of lanes) {
1006
+ if (!completedLanes.has(lane.name) && !failedLanes.has(lane.name) && !running.has(lane.name)) {
1007
+ const statePath = safeJoin(laneRunDirs[lane.name]!, 'state.json');
1008
+ try {
1009
+ const state = loadState<LaneState>(statePath);
1010
+ if (state) {
1011
+ // Only update if not already completed/failed
1012
+ if (state.status !== 'completed' && state.status !== 'failed') {
1013
+ state.status = 'pending';
1014
+ state.error = `Orchestration interrupted before lane started`;
1015
+ saveState(statePath, state);
1016
+ }
1017
+ }
1018
+ } catch {
1019
+ // State file might not exist yet
1020
+ }
1021
+ }
1022
+ }
1023
+
991
1024
  printLaneStatus(lanes, laneRunDirs);
1025
+ logger.info('\n💡 To resume: cursorflow resume --all');
992
1026
  process.exit(130);
993
1027
  };
994
1028
 
1029
+ const sigIntHandler = () => handleGracefulShutdown('SIGINT');
1030
+ const sigTermHandler = () => handleGracefulShutdown('SIGTERM');
1031
+
995
1032
  process.on('SIGINT', sigIntHandler);
996
- process.on('SIGTERM', sigIntHandler);
1033
+ process.on('SIGTERM', sigTermHandler);
997
1034
 
998
1035
  let lastStallCheck = Date.now();
999
1036
 
@@ -1313,7 +1350,7 @@ export async function orchestrate(tasksDir: string, options: {
1313
1350
  } finally {
1314
1351
  clearInterval(monitorInterval);
1315
1352
  process.removeListener('SIGINT', sigIntHandler);
1316
- process.removeListener('SIGTERM', sigIntHandler);
1353
+ process.removeListener('SIGTERM', sigTermHandler);
1317
1354
  }
1318
1355
 
1319
1356
  printLaneStatus(lanes, laneRunDirs);
@@ -287,7 +287,8 @@ export class TaskService {
287
287
  try {
288
288
  const filePath = path.join(taskInfo.path, lane.fileName);
289
289
  const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
290
- const laneName = lane.fileName.replace('.json', '');
290
+ // Use the extracted laneName which already handles numeric prefix removal
291
+ const laneName = lane.laneName;
291
292
 
292
293
  if (Array.isArray(content.tasks)) {
293
294
  for (const task of content.tasks) {