taskplane 0.19.0 → 0.20.0
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/bin/taskplane.mjs +1 -1
- package/dashboard/public/app.js +41 -0
- package/dashboard/public/index.html +5 -2
- package/dashboard/public/style.css +194 -58
- package/dashboard/public/taskplane-word-color.svg +22 -0
- package/dashboard/public/taskplane-word-white.svg +22 -0
- package/dashboard/server.cjs +65 -0
- package/extensions/task-runner.ts +90 -69
- package/extensions/taskplane/engine-worker.ts +289 -0
- package/extensions/taskplane/execution.ts +298 -25
- package/extensions/taskplane/extension.ts +330 -94
- package/extensions/taskplane/merge.ts +242 -19
- package/extensions/taskplane/supervisor-primer.md +1 -1
- package/extensions/taskplane/supervisor.ts +161 -42
- package/package.json +1 -1
- package/skills/create-taskplane-task/references/prompt-template.md +0 -3
- package/templates/agents/task-worker.md +16 -1
- package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -99
- package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -73
- package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -98
- package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -73
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* @module orch/execution
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync, existsSync, statSync, unlinkSync, mkdirSync, writeFileSync } from "fs";
|
|
6
|
-
import {
|
|
6
|
+
import { access as fsAccess, readFile as fsReadFile, stat as fsStat } from "fs/promises";
|
|
7
|
+
import { spawnSync, spawn } from "child_process";
|
|
7
8
|
import { join, dirname, resolve, relative, delimiter as pathDelimiter } from "path";
|
|
8
9
|
import { userInfo } from "os";
|
|
9
10
|
|
|
@@ -264,6 +265,135 @@ export function killLaneAndChildren(sessionName: string): void {
|
|
|
264
265
|
tmuxKillSession(sessionName);
|
|
265
266
|
}
|
|
266
267
|
|
|
268
|
+
// ── Async TMUX Helpers (TP-070) ──────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Run a tmux command asynchronously, without blocking the event loop.
|
|
272
|
+
*
|
|
273
|
+
* Wraps `child_process.spawn` in a promise. The process is spawned and
|
|
274
|
+
* stdout is collected incrementally; the promise resolves when the process
|
|
275
|
+
* exits.
|
|
276
|
+
*
|
|
277
|
+
* @param args - Arguments to pass to the `tmux` command
|
|
278
|
+
* @param timeoutMs - Optional timeout in milliseconds (default: 5000)
|
|
279
|
+
* @returns Promise resolving to `{ status, stdout }` where status is the exit code (0 = success)
|
|
280
|
+
*
|
|
281
|
+
* @since TP-070
|
|
282
|
+
*/
|
|
283
|
+
export function tmuxAsync(args: string[], timeoutMs: number = 5_000): Promise<{ status: number; stdout: string }> {
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
const proc = spawn("tmux", args, {
|
|
286
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
287
|
+
timeout: timeoutMs,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
let stdout = "";
|
|
291
|
+
|
|
292
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
293
|
+
stdout += chunk.toString("utf-8");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
proc.on("error", () => {
|
|
297
|
+
// Spawn failure — treat as non-zero exit
|
|
298
|
+
resolve({ status: 1, stdout: "" });
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
proc.on("close", (code) => {
|
|
302
|
+
resolve({ status: code ?? 1, stdout });
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Async version of tmuxHasSession — checks if a TMUX session exists
|
|
309
|
+
* without blocking the event loop.
|
|
310
|
+
*
|
|
311
|
+
* @param sessionName - TMUX session name to check
|
|
312
|
+
* @returns Promise resolving to true if session exists
|
|
313
|
+
*
|
|
314
|
+
* @since TP-070
|
|
315
|
+
*/
|
|
316
|
+
export async function tmuxHasSessionAsync(sessionName: string): Promise<boolean> {
|
|
317
|
+
const result = await tmuxAsync(["has-session", "-t", sessionName]);
|
|
318
|
+
return result.status === 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Async version of tmuxKillSession — kills a TMUX session without
|
|
323
|
+
* blocking the event loop.
|
|
324
|
+
*
|
|
325
|
+
* Idempotent: resolves to true if session was killed or was already absent.
|
|
326
|
+
*
|
|
327
|
+
* @param sessionName - TMUX session name to kill
|
|
328
|
+
* @returns Promise resolving to true if session is now absent
|
|
329
|
+
*
|
|
330
|
+
* @since TP-070
|
|
331
|
+
*/
|
|
332
|
+
export async function tmuxKillSessionAsync(sessionName: string): Promise<boolean> {
|
|
333
|
+
const wasAlive = await tmuxHasSessionAsync(sessionName);
|
|
334
|
+
if (!wasAlive) return true;
|
|
335
|
+
|
|
336
|
+
await tmuxAsync(["kill-session", "-t", sessionName]);
|
|
337
|
+
return !(await tmuxHasSessionAsync(sessionName));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Async version of captureTmuxPaneTail — captures tail output from a live
|
|
342
|
+
* TMUX pane without blocking the event loop.
|
|
343
|
+
*
|
|
344
|
+
* @param sessionName - TMUX session name
|
|
345
|
+
* @param maxLines - Maximum number of lines to return
|
|
346
|
+
* @param maxChars - Maximum character count
|
|
347
|
+
* @returns Promise resolving to captured text (empty string on failure)
|
|
348
|
+
*
|
|
349
|
+
* @since TP-070
|
|
350
|
+
*/
|
|
351
|
+
export async function captureTmuxPaneTailAsync(
|
|
352
|
+
sessionName: string,
|
|
353
|
+
maxLines: number = 40,
|
|
354
|
+
maxChars: number = 1200,
|
|
355
|
+
): Promise<string> {
|
|
356
|
+
const result = await tmuxAsync(["capture-pane", "-p", "-t", sessionName], 3000);
|
|
357
|
+
if (result.status !== 0) return "";
|
|
358
|
+
const raw = (result.stdout || "").replace(/\r\n/g, "\n").trim();
|
|
359
|
+
if (!raw) return "";
|
|
360
|
+
const tail = raw.split("\n").slice(-maxLines).join("\n").trim();
|
|
361
|
+
if (!tail) return "";
|
|
362
|
+
return tail.length > maxChars ? tail.slice(-maxChars) : tail;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Async version of readTaskStatusTail — reads STATUS.md tail without
|
|
367
|
+
* blocking the event loop.
|
|
368
|
+
*
|
|
369
|
+
* @param statusPath - Path to STATUS.md
|
|
370
|
+
* @param maxLines - Maximum number of lines to return
|
|
371
|
+
* @param maxChars - Maximum character count
|
|
372
|
+
* @returns Promise resolving to status tail text (empty string if missing/unreadable)
|
|
373
|
+
*
|
|
374
|
+
* @since TP-070
|
|
375
|
+
*/
|
|
376
|
+
export async function readTaskStatusTailAsync(
|
|
377
|
+
statusPath: string,
|
|
378
|
+
maxLines: number = 40,
|
|
379
|
+
maxChars: number = 1200,
|
|
380
|
+
): Promise<string> {
|
|
381
|
+
try {
|
|
382
|
+
await fsAccess(statusPath);
|
|
383
|
+
} catch {
|
|
384
|
+
return "";
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const raw = (await fsReadFile(statusPath, "utf-8")).replace(/\r\n/g, "\n").trim();
|
|
388
|
+
if (!raw) return "";
|
|
389
|
+
const tail = raw.split("\n").slice(-maxLines).join("\n").trim();
|
|
390
|
+
if (!tail) return "";
|
|
391
|
+
return tail.length > maxChars ? tail.slice(-maxChars) : tail;
|
|
392
|
+
} catch {
|
|
393
|
+
return "";
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
267
397
|
/**
|
|
268
398
|
* Build environment variables for a lane task execution.
|
|
269
399
|
*
|
|
@@ -496,6 +626,50 @@ export function readLaneLogTail(
|
|
|
496
626
|
}
|
|
497
627
|
}
|
|
498
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Async version of readLaneLogTail — reads lane log tail without
|
|
631
|
+
* blocking the event loop.
|
|
632
|
+
*
|
|
633
|
+
* @since TP-070
|
|
634
|
+
*/
|
|
635
|
+
export async function readLaneLogTailAsync(
|
|
636
|
+
logPath: string,
|
|
637
|
+
maxLines: number = 40,
|
|
638
|
+
maxChars: number = 1200,
|
|
639
|
+
): Promise<string> {
|
|
640
|
+
try {
|
|
641
|
+
await fsAccess(logPath);
|
|
642
|
+
} catch {
|
|
643
|
+
return "";
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const raw = (await fsReadFile(logPath, "utf-8")).replace(/\r\n/g, "\n");
|
|
647
|
+
const tail = raw.split("\n").slice(-maxLines).join("\n").trim();
|
|
648
|
+
if (!tail) return "";
|
|
649
|
+
return tail.length > maxChars ? tail.slice(-maxChars) : tail;
|
|
650
|
+
} catch {
|
|
651
|
+
return "";
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Async file existence check — non-blocking replacement for existsSync
|
|
657
|
+
* in polling paths.
|
|
658
|
+
*
|
|
659
|
+
* @param filePath - Path to check
|
|
660
|
+
* @returns Promise resolving to true if file exists
|
|
661
|
+
*
|
|
662
|
+
* @since TP-070
|
|
663
|
+
*/
|
|
664
|
+
export async function fileExistsAsync(filePath: string): Promise<boolean> {
|
|
665
|
+
try {
|
|
666
|
+
await fsAccess(filePath);
|
|
667
|
+
return true;
|
|
668
|
+
} catch {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
499
673
|
/**
|
|
500
674
|
* Capture tail output from a live TMUX pane for diagnostics.
|
|
501
675
|
*
|
|
@@ -838,13 +1012,13 @@ export async function pollUntilTaskComplete(
|
|
|
838
1012
|
};
|
|
839
1013
|
}
|
|
840
1014
|
|
|
841
|
-
// Check file-based abort signal
|
|
842
|
-
if (
|
|
1015
|
+
// Check file-based abort signal (TP-070: async)
|
|
1016
|
+
if (await fileExistsAsync(abortSignalFile)) {
|
|
843
1017
|
execLog(laneId, task.taskId, "abort signal file detected — killing session and aborting");
|
|
844
|
-
|
|
1018
|
+
await tmuxKillSessionAsync(sessionName);
|
|
845
1019
|
// Also kill child sessions (worker, reviewer)
|
|
846
|
-
|
|
847
|
-
|
|
1020
|
+
await tmuxKillSessionAsync(`${sessionName}-worker`);
|
|
1021
|
+
await tmuxKillSessionAsync(`${sessionName}-reviewer`);
|
|
848
1022
|
return {
|
|
849
1023
|
status: "failed",
|
|
850
1024
|
exitReason: "Aborted by signal file (.pi/orch-abort-signal)",
|
|
@@ -852,14 +1026,14 @@ export async function pollUntilTaskComplete(
|
|
|
852
1026
|
};
|
|
853
1027
|
}
|
|
854
1028
|
|
|
855
|
-
// Capture live pane output for diagnostics (best effort).
|
|
856
|
-
const paneTail =
|
|
1029
|
+
// Capture live pane output for diagnostics (best effort) — async to avoid blocking.
|
|
1030
|
+
const paneTail = await captureTmuxPaneTailAsync(sessionName);
|
|
857
1031
|
if (paneTail) {
|
|
858
1032
|
lastPaneTail = paneTail;
|
|
859
1033
|
}
|
|
860
1034
|
|
|
861
|
-
// Priority 1: Check for .DONE file
|
|
862
|
-
if (
|
|
1035
|
+
// Priority 1: Check for .DONE file (TP-070: async)
|
|
1036
|
+
if (await fileExistsAsync(donePath)) {
|
|
863
1037
|
execLog(laneId, task.taskId, ".DONE file found — task succeeded", {
|
|
864
1038
|
session: sessionName,
|
|
865
1039
|
});
|
|
@@ -870,8 +1044,8 @@ export async function pollUntilTaskComplete(
|
|
|
870
1044
|
};
|
|
871
1045
|
}
|
|
872
1046
|
|
|
873
|
-
// Priority 2: Check if TMUX session is still alive
|
|
874
|
-
if (!
|
|
1047
|
+
// Priority 2: Check if TMUX session is still alive — async to avoid blocking
|
|
1048
|
+
if (!(await tmuxHasSessionAsync(sessionName))) {
|
|
875
1049
|
// Session exited — start grace period for .DONE file
|
|
876
1050
|
execLog(laneId, task.taskId, "TMUX session exited, entering grace period", {
|
|
877
1051
|
session: sessionName,
|
|
@@ -883,7 +1057,7 @@ export async function pollUntilTaskComplete(
|
|
|
883
1057
|
while (Date.now() - graceStart < DONE_GRACE_MS) {
|
|
884
1058
|
await new Promise((r) => setTimeout(r, 500));
|
|
885
1059
|
|
|
886
|
-
if (
|
|
1060
|
+
if (await fileExistsAsync(donePath)) {
|
|
887
1061
|
execLog(laneId, task.taskId, ".DONE file found during grace period — task succeeded", {
|
|
888
1062
|
session: sessionName,
|
|
889
1063
|
});
|
|
@@ -895,8 +1069,8 @@ export async function pollUntilTaskComplete(
|
|
|
895
1069
|
}
|
|
896
1070
|
}
|
|
897
1071
|
|
|
898
|
-
// Grace period expired without .DONE → task failed
|
|
899
|
-
const logTail =
|
|
1072
|
+
// Grace period expired without .DONE → task failed (TP-070: async)
|
|
1073
|
+
const logTail = await readLaneLogTailAsync(laneLogPath);
|
|
900
1074
|
execLog(laneId, task.taskId, "grace period expired without .DONE — task failed", {
|
|
901
1075
|
session: sessionName,
|
|
902
1076
|
logPath: laneLogPath,
|
|
@@ -904,8 +1078,8 @@ export async function pollUntilTaskComplete(
|
|
|
904
1078
|
if (logTail) {
|
|
905
1079
|
execLog(laneId, task.taskId, `lane session output (tail):\n${logTail}`);
|
|
906
1080
|
}
|
|
907
|
-
const statusTail =
|
|
908
|
-
const hasLogFile =
|
|
1081
|
+
const statusTail = await readTaskStatusTailAsync(statusPath);
|
|
1082
|
+
const hasLogFile = await fileExistsAsync(laneLogPath);
|
|
909
1083
|
const outputForHint = logTail || lastPaneTail || statusTail;
|
|
910
1084
|
const logHint = outputForHint
|
|
911
1085
|
? ` Last output: ${outputForHint.replace(/\s+/g, " ").slice(-300)}`
|
|
@@ -1284,6 +1458,105 @@ export function parseWorktreeStatusMd(
|
|
|
1284
1458
|
};
|
|
1285
1459
|
}
|
|
1286
1460
|
|
|
1461
|
+
/**
|
|
1462
|
+
* Async version of parseWorktreeStatusMd — reads and parses STATUS.md
|
|
1463
|
+
* without blocking the event loop. Used in monitoring poll loops.
|
|
1464
|
+
*
|
|
1465
|
+
* @since TP-070
|
|
1466
|
+
*/
|
|
1467
|
+
export async function parseWorktreeStatusMdAsync(
|
|
1468
|
+
taskFolder: string,
|
|
1469
|
+
worktreePath: string,
|
|
1470
|
+
repoRoot: string,
|
|
1471
|
+
isWorkspaceMode?: boolean,
|
|
1472
|
+
): Promise<{ parsed: ParsedWorktreeStatus | null; error: string | null }> {
|
|
1473
|
+
const resolved = resolveCanonicalTaskPaths(taskFolder, worktreePath, repoRoot, isWorkspaceMode);
|
|
1474
|
+
const statusPath = resolved.statusPath;
|
|
1475
|
+
|
|
1476
|
+
if (!(await fileExistsAsync(statusPath))) {
|
|
1477
|
+
return { parsed: null, error: `STATUS.md not found at ${statusPath}` };
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
let content: string;
|
|
1481
|
+
let mtime: number;
|
|
1482
|
+
try {
|
|
1483
|
+
content = await fsReadFile(statusPath, "utf-8");
|
|
1484
|
+
mtime = (await fsStat(statusPath)).mtimeMs;
|
|
1485
|
+
} catch (err: unknown) {
|
|
1486
|
+
return { parsed: null, error: `Cannot read STATUS.md: ${err instanceof Error ? err.message : String(err)}` };
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Parse logic is identical to the sync version
|
|
1490
|
+
const text = content.replace(/\r\n/g, "\n");
|
|
1491
|
+
const steps: ParsedWorktreeStatus["steps"] = [];
|
|
1492
|
+
let currentStep: {
|
|
1493
|
+
number: number;
|
|
1494
|
+
name: string;
|
|
1495
|
+
status: "not-started" | "in-progress" | "complete";
|
|
1496
|
+
checkboxes: boolean[];
|
|
1497
|
+
} | null = null;
|
|
1498
|
+
let reviewCounter = 0;
|
|
1499
|
+
let iteration = 0;
|
|
1500
|
+
|
|
1501
|
+
for (const line of text.split("\n")) {
|
|
1502
|
+
const rcMatch = line.match(/\*\*Review Counter:\*\*\s*(\d+)/);
|
|
1503
|
+
if (rcMatch) reviewCounter = parseInt(rcMatch[1]);
|
|
1504
|
+
const itMatch = line.match(/\*\*Iteration:\*\*\s*(\d+)/);
|
|
1505
|
+
if (itMatch) iteration = parseInt(itMatch[1]);
|
|
1506
|
+
|
|
1507
|
+
const stepMatch = line.match(/^###\s+Step\s+(\d+):\s*(.+)/);
|
|
1508
|
+
if (stepMatch) {
|
|
1509
|
+
if (currentStep) {
|
|
1510
|
+
const totalChecked = currentStep.checkboxes.filter(c => c).length;
|
|
1511
|
+
steps.push({
|
|
1512
|
+
number: currentStep.number,
|
|
1513
|
+
name: currentStep.name,
|
|
1514
|
+
status: currentStep.status,
|
|
1515
|
+
totalChecked,
|
|
1516
|
+
totalItems: currentStep.checkboxes.length,
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
currentStep = {
|
|
1520
|
+
number: parseInt(stepMatch[1]),
|
|
1521
|
+
name: stepMatch[2].trim(),
|
|
1522
|
+
status: "not-started",
|
|
1523
|
+
checkboxes: [],
|
|
1524
|
+
};
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (currentStep) {
|
|
1528
|
+
const ss = line.match(/\*\*Status:\*\*\s*(.*)/);
|
|
1529
|
+
if (ss) {
|
|
1530
|
+
const s = ss[1];
|
|
1531
|
+
if (s.includes("✅") || s.toLowerCase().includes("complete")) {
|
|
1532
|
+
currentStep.status = "complete";
|
|
1533
|
+
} else if (s.includes("🟨") || s.includes("🟡") || s.toLowerCase().includes("progress")) {
|
|
1534
|
+
currentStep.status = "in-progress";
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
const cb = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)/);
|
|
1538
|
+
if (cb) {
|
|
1539
|
+
currentStep.checkboxes.push(cb[1].toLowerCase() === "x");
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (currentStep) {
|
|
1544
|
+
const totalChecked = currentStep.checkboxes.filter(c => c).length;
|
|
1545
|
+
steps.push({
|
|
1546
|
+
number: currentStep.number,
|
|
1547
|
+
name: currentStep.name,
|
|
1548
|
+
status: currentStep.status,
|
|
1549
|
+
totalChecked,
|
|
1550
|
+
totalItems: currentStep.checkboxes.length,
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
return {
|
|
1555
|
+
parsed: { steps, reviewCounter, iteration, mtime },
|
|
1556
|
+
error: null,
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1287
1560
|
|
|
1288
1561
|
// ── State Resolution ─────────────────────────────────────────────────
|
|
1289
1562
|
|
|
@@ -1307,7 +1580,7 @@ export function parseWorktreeStatusMd(
|
|
|
1307
1580
|
* @param stallTimeoutMs - Stall timeout in milliseconds
|
|
1308
1581
|
* @param now - Current timestamp (epoch ms) for deterministic testing
|
|
1309
1582
|
*/
|
|
1310
|
-
export function resolveTaskMonitorState(
|
|
1583
|
+
export async function resolveTaskMonitorState(
|
|
1311
1584
|
taskId: string,
|
|
1312
1585
|
donePath: string,
|
|
1313
1586
|
sessionName: string,
|
|
@@ -1315,9 +1588,9 @@ export function resolveTaskMonitorState(
|
|
|
1315
1588
|
tracker: MtimeTracker,
|
|
1316
1589
|
stallTimeoutMs: number,
|
|
1317
1590
|
now: number,
|
|
1318
|
-
): TaskMonitorSnapshot {
|
|
1319
|
-
const sessionAlive =
|
|
1320
|
-
const doneFileFound =
|
|
1591
|
+
): Promise<TaskMonitorSnapshot> {
|
|
1592
|
+
const sessionAlive = await tmuxHasSessionAsync(sessionName);
|
|
1593
|
+
const doneFileFound = await fileExistsAsync(donePath);
|
|
1321
1594
|
|
|
1322
1595
|
// Build base snapshot from parsed status
|
|
1323
1596
|
let currentStepName: string | null = null;
|
|
@@ -1606,9 +1879,9 @@ export async function monitorLanes(
|
|
|
1606
1879
|
|
|
1607
1880
|
const tracker = getOrCreateTracker(task.taskId, now);
|
|
1608
1881
|
const donePath = resolveTaskDonePath(task.task.taskFolder, lane.worktreePath, repoRoot, isWorkspaceMode);
|
|
1609
|
-
const statusResult =
|
|
1882
|
+
const statusResult = await parseWorktreeStatusMdAsync(task.task.taskFolder, lane.worktreePath, repoRoot, isWorkspaceMode);
|
|
1610
1883
|
|
|
1611
|
-
const snapshot = resolveTaskMonitorState(
|
|
1884
|
+
const snapshot = await resolveTaskMonitorState(
|
|
1612
1885
|
task.taskId,
|
|
1613
1886
|
donePath,
|
|
1614
1887
|
lane.tmuxSessionName,
|
|
@@ -1654,7 +1927,7 @@ export async function monitorLanes(
|
|
|
1654
1927
|
allTerminal = false;
|
|
1655
1928
|
}
|
|
1656
1929
|
|
|
1657
|
-
const sessionAlive =
|
|
1930
|
+
const sessionAlive = await tmuxHasSessionAsync(lane.tmuxSessionName);
|
|
1658
1931
|
|
|
1659
1932
|
laneSnapshots.push({
|
|
1660
1933
|
laneId: lane.laneId,
|
|
@@ -1718,7 +1991,7 @@ export async function monitorLanes(
|
|
|
1718
1991
|
laneId: lane.laneId,
|
|
1719
1992
|
laneNumber: lane.laneNumber,
|
|
1720
1993
|
sessionName: lane.tmuxSessionName,
|
|
1721
|
-
sessionAlive:
|
|
1994
|
+
sessionAlive: false, // Best-effort during pause — don't block with tmux call
|
|
1722
1995
|
currentTaskId: null,
|
|
1723
1996
|
currentTaskSnapshot: null,
|
|
1724
1997
|
completedTasks: [],
|