taskplane 0.1.6 → 0.1.8
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/dashboard/public/app.js +1144 -1144
- package/dashboard/public/index.html +98 -98
- package/dashboard/public/style.css +1108 -1108
- package/dashboard/server.cjs +638 -638
- package/extensions/taskplane/execution.ts +160 -17
- package/extensions/taskplane/extension.ts +122 -90
- package/extensions/taskplane/types.ts +4 -0
- package/package.json +57 -57
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lane execution, monitoring, wave execution loop
|
|
3
|
-
* @module orch/execution
|
|
4
|
-
*/
|
|
5
|
-
import { readFileSync, existsSync, statSync, unlinkSync, mkdirSync } from "fs";
|
|
6
|
-
import { spawnSync } from "child_process";
|
|
7
|
-
import { join, dirname, resolve, delimiter as pathDelimiter } from "path";
|
|
8
|
-
|
|
9
|
-
import { DONE_GRACE_MS, EXECUTION_POLL_INTERVAL_MS, ExecutionError, SESSION_SPAWN_RETRY_MAX } from "./types.ts";
|
|
10
|
-
import type { AllocatedLane, AllocatedTask, DependencyGraph, LaneExecutionResult, LaneMonitorSnapshot, LaneTaskOutcome, LaneTaskStatus, MonitorState, MtimeTracker, OrchestratorConfig, ParsedTask, TaskMonitorSnapshot, WaveExecutionResult } from "./types.ts";
|
|
11
|
-
import { allocateLanes } from "./waves.ts";
|
|
12
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Lane execution, monitoring, wave execution loop
|
|
3
|
+
* @module orch/execution
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync, statSync, unlinkSync, mkdirSync } from "fs";
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
7
|
+
import { join, dirname, resolve, relative, delimiter as pathDelimiter } from "path";
|
|
8
|
+
|
|
9
|
+
import { DONE_GRACE_MS, EXECUTION_POLL_INTERVAL_MS, ExecutionError, SESSION_SPAWN_RETRY_MAX } from "./types.ts";
|
|
10
|
+
import type { AllocatedLane, AllocatedTask, DependencyGraph, LaneExecutionResult, LaneMonitorSnapshot, LaneTaskOutcome, LaneTaskStatus, MonitorState, MtimeTracker, OrchestratorConfig, ParsedTask, TaskMonitorSnapshot, WaveExecutionResult } from "./types.ts";
|
|
11
|
+
import { allocateLanes } from "./waves.ts";
|
|
12
|
+
import { runGit } from "./git.ts";
|
|
13
|
+
|
|
13
14
|
// ── Execution Helpers ────────────────────────────────────────────────
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -479,6 +480,12 @@ export async function pollUntilTaskComplete(
|
|
|
479
480
|
|
|
480
481
|
let lastPaneTail = "";
|
|
481
482
|
|
|
483
|
+
// Abort signal file path — checked each poll cycle.
|
|
484
|
+
// Any process can create this file to trigger abort (belt-and-suspenders
|
|
485
|
+
// alongside the in-memory pauseSignal, since /orch-abort may not be able
|
|
486
|
+
// to run concurrently with the /orch command handler).
|
|
487
|
+
const abortSignalFile = join(repoRoot, ".pi", "orch-abort-signal");
|
|
488
|
+
|
|
482
489
|
// Main polling loop
|
|
483
490
|
while (true) {
|
|
484
491
|
// Check pause signal
|
|
@@ -493,6 +500,20 @@ export async function pollUntilTaskComplete(
|
|
|
493
500
|
};
|
|
494
501
|
}
|
|
495
502
|
|
|
503
|
+
// Check file-based abort signal
|
|
504
|
+
if (existsSync(abortSignalFile)) {
|
|
505
|
+
execLog(laneId, task.taskId, "abort signal file detected — killing session and aborting");
|
|
506
|
+
tmuxKillSession(sessionName);
|
|
507
|
+
// Also kill child sessions (worker, reviewer)
|
|
508
|
+
tmuxKillSession(`${sessionName}-worker`);
|
|
509
|
+
tmuxKillSession(`${sessionName}-reviewer`);
|
|
510
|
+
return {
|
|
511
|
+
status: "failed",
|
|
512
|
+
exitReason: "Aborted by signal file (.pi/orch-abort-signal)",
|
|
513
|
+
doneFileFound: false,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
496
517
|
// Capture live pane output for diagnostics (best effort).
|
|
497
518
|
const paneTail = captureTmuxPaneTail(sessionName);
|
|
498
519
|
if (paneTail) {
|
|
@@ -725,7 +746,7 @@ export async function executeLane(
|
|
|
725
746
|
};
|
|
726
747
|
}
|
|
727
748
|
|
|
728
|
-
|
|
749
|
+
|
|
729
750
|
// ── STATUS.md Parsing for Worktree ───────────────────────────────────
|
|
730
751
|
|
|
731
752
|
/**
|
|
@@ -873,7 +894,7 @@ export function parseWorktreeStatusMd(
|
|
|
873
894
|
};
|
|
874
895
|
}
|
|
875
896
|
|
|
876
|
-
|
|
897
|
+
|
|
877
898
|
// ── State Resolution ─────────────────────────────────────────────────
|
|
878
899
|
|
|
879
900
|
/**
|
|
@@ -1063,7 +1084,7 @@ export function resolveTaskMonitorState(
|
|
|
1063
1084
|
};
|
|
1064
1085
|
}
|
|
1065
1086
|
|
|
1066
|
-
|
|
1087
|
+
|
|
1067
1088
|
// ── Core Monitor Loop ────────────────────────────────────────────────
|
|
1068
1089
|
|
|
1069
1090
|
/**
|
|
@@ -1326,7 +1347,7 @@ export async function monitorLanes(
|
|
|
1326
1347
|
};
|
|
1327
1348
|
}
|
|
1328
1349
|
|
|
1329
|
-
|
|
1350
|
+
|
|
1330
1351
|
// ── Transitive Dependent Computation ─────────────────────────────────
|
|
1331
1352
|
|
|
1332
1353
|
/**
|
|
@@ -1370,7 +1391,101 @@ export function computeTransitiveDependents(
|
|
|
1370
1391
|
return blocked;
|
|
1371
1392
|
}
|
|
1372
1393
|
|
|
1373
|
-
|
|
1394
|
+
|
|
1395
|
+
// ── Pre-flight: Commit Untracked Task Files ─────────────────────────
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* Ensure all task files for a wave are committed to git before worktree creation.
|
|
1399
|
+
*
|
|
1400
|
+
* Git worktrees only contain tracked (committed) files. If a user creates
|
|
1401
|
+
* task folders (PROMPT.md, STATUS.md) but doesn't commit them, the worktree
|
|
1402
|
+
* won't have those files and TASK_AUTOSTART will fail with "file not found".
|
|
1403
|
+
*
|
|
1404
|
+
* This function checks each wave task's folder for untracked or modified files,
|
|
1405
|
+
* stages them, and creates a commit on the current branch. This must run BEFORE
|
|
1406
|
+
* allocateLanes() so that worktrees (which are based on the integration branch)
|
|
1407
|
+
* include the task files.
|
|
1408
|
+
*
|
|
1409
|
+
* Only task-specific folders are staged — no other working tree changes are touched.
|
|
1410
|
+
*
|
|
1411
|
+
* @param waveTasks - Task IDs in this wave
|
|
1412
|
+
* @param pending - Full pending task map from discovery
|
|
1413
|
+
* @param repoRoot - Main repository root
|
|
1414
|
+
* @param waveIndex - Wave number for commit message
|
|
1415
|
+
*/
|
|
1416
|
+
export function ensureTaskFilesCommitted(
|
|
1417
|
+
waveTasks: string[],
|
|
1418
|
+
pending: Map<string, ParsedTask>,
|
|
1419
|
+
repoRoot: string,
|
|
1420
|
+
waveIndex: number,
|
|
1421
|
+
): void {
|
|
1422
|
+
// Collect task folder paths for this wave
|
|
1423
|
+
const foldersToCheck: { taskId: string; relPath: string }[] = [];
|
|
1424
|
+
for (const taskId of waveTasks) {
|
|
1425
|
+
const task = pending.get(taskId);
|
|
1426
|
+
if (!task) continue;
|
|
1427
|
+
|
|
1428
|
+
const absFolder = resolve(task.taskFolder);
|
|
1429
|
+
const relPath = relative(resolve(repoRoot), absFolder).replace(/\\/g, "/");
|
|
1430
|
+
|
|
1431
|
+
// Skip if path escapes the repo (shouldn't happen in normal use)
|
|
1432
|
+
if (relPath.startsWith("..")) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
foldersToCheck.push({ taskId, relPath });
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (foldersToCheck.length === 0) return;
|
|
1439
|
+
|
|
1440
|
+
// Check which folders have untracked or uncommitted files
|
|
1441
|
+
const foldersToStage: string[] = [];
|
|
1442
|
+
for (const { taskId, relPath } of foldersToCheck) {
|
|
1443
|
+
const status = runGit(["status", "--porcelain", "--", relPath], repoRoot);
|
|
1444
|
+
if (status.ok && status.stdout.trim()) {
|
|
1445
|
+
execLog("wave", `W${waveIndex}`, `task ${taskId} has uncommitted files, staging`, {
|
|
1446
|
+
folder: relPath,
|
|
1447
|
+
status: status.stdout.trim().split("\n").slice(0, 5).join("; "),
|
|
1448
|
+
});
|
|
1449
|
+
foldersToStage.push(relPath);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (foldersToStage.length === 0) return;
|
|
1454
|
+
|
|
1455
|
+
// Stage only the task folders
|
|
1456
|
+
for (const folder of foldersToStage) {
|
|
1457
|
+
const addResult = runGit(["add", "--", folder], repoRoot);
|
|
1458
|
+
if (!addResult.ok) {
|
|
1459
|
+
execLog("wave", `W${waveIndex}`, `failed to stage task files: ${addResult.stderr}`, { folder });
|
|
1460
|
+
throw new ExecutionError(
|
|
1461
|
+
"EXEC_TASK_STAGE_FAILED",
|
|
1462
|
+
`Failed to stage task files in "${folder}": ${addResult.stderr}`,
|
|
1463
|
+
"wave",
|
|
1464
|
+
folder,
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// Commit
|
|
1470
|
+
const taskIds = foldersToStage.map(f => f.split("/").pop() || f).join(", ");
|
|
1471
|
+
const commitMsg = `chore: stage task files for orchestrator wave ${waveIndex} (${taskIds})`;
|
|
1472
|
+
const commitResult = runGit(["commit", "-m", commitMsg], repoRoot);
|
|
1473
|
+
if (!commitResult.ok) {
|
|
1474
|
+
execLog("wave", `W${waveIndex}`, `failed to commit task files: ${commitResult.stderr}`);
|
|
1475
|
+
throw new ExecutionError(
|
|
1476
|
+
"EXEC_TASK_COMMIT_FAILED",
|
|
1477
|
+
`Failed to commit task files for wave ${waveIndex}: ${commitResult.stderr}`,
|
|
1478
|
+
"wave",
|
|
1479
|
+
`W${waveIndex}`,
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
execLog("wave", `W${waveIndex}`, `committed ${foldersToStage.length} task folder(s) to ensure worktree visibility`, {
|
|
1484
|
+
folders: foldersToStage,
|
|
1485
|
+
commit: commitResult.stdout.trim().split("\n")[0],
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1374
1489
|
// ── Wave Execution Core ──────────────────────────────────────────────
|
|
1375
1490
|
|
|
1376
1491
|
/**
|
|
@@ -1432,6 +1547,34 @@ export async function executeWave(
|
|
|
1432
1547
|
batchId,
|
|
1433
1548
|
});
|
|
1434
1549
|
|
|
1550
|
+
// ── Stage 0: Ensure task files are committed ────────────────
|
|
1551
|
+
// Task folders may contain untracked files (PROMPT.md, STATUS.md) that
|
|
1552
|
+
// won't appear in worktrees unless committed. Stage and commit them now,
|
|
1553
|
+
// before worktree creation, so workers can find their TASK_AUTOSTART paths.
|
|
1554
|
+
try {
|
|
1555
|
+
ensureTaskFilesCommitted(waveTasks, pending, repoRoot, waveIndex);
|
|
1556
|
+
} catch (err: unknown) {
|
|
1557
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1558
|
+
execLog("wave", `W${waveIndex}`, `task file commit failed: ${errMsg}`);
|
|
1559
|
+
|
|
1560
|
+
return {
|
|
1561
|
+
waveIndex,
|
|
1562
|
+
startedAt,
|
|
1563
|
+
endedAt: Date.now(),
|
|
1564
|
+
laneResults: [],
|
|
1565
|
+
policyApplied: policy,
|
|
1566
|
+
stoppedEarly: true,
|
|
1567
|
+
failedTaskIds: waveTasks,
|
|
1568
|
+
skippedTaskIds: [],
|
|
1569
|
+
succeededTaskIds: [],
|
|
1570
|
+
blockedTaskIds: [...computeTransitiveDependents(new Set(waveTasks), dependencyGraph)],
|
|
1571
|
+
laneCount: 0,
|
|
1572
|
+
overallStatus: "failed",
|
|
1573
|
+
finalMonitorState: null,
|
|
1574
|
+
allocatedLanes: [],
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1435
1578
|
// ── Stage 1: Allocate lanes ──────────────────────────────────
|
|
1436
1579
|
const allocResult = allocateLanes(waveTasks, pending, config, repoRoot, batchId);
|
|
1437
1580
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
|
+
import { writeFileSync, unlinkSync, mkdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
DEFAULT_ORCHESTRATOR_CONFIG,
|
|
@@ -10,7 +12,6 @@ import {
|
|
|
10
12
|
createOrchWidget,
|
|
11
13
|
deleteBatchState,
|
|
12
14
|
detectOrphanSessions,
|
|
13
|
-
executeAbort,
|
|
14
15
|
executeLane,
|
|
15
16
|
executeOrchBatch,
|
|
16
17
|
formatDependencyGraph,
|
|
@@ -347,113 +348,144 @@ export default function (pi: ExtensionAPI) {
|
|
|
347
348
|
pi.registerCommand("orch-abort", {
|
|
348
349
|
description: "Abort batch: /orch-abort [--hard]",
|
|
349
350
|
handler: async (args, ctx) => {
|
|
350
|
-
const hard = args?.trim() === "--hard";
|
|
351
|
-
const mode: AbortMode = hard ? "hard" : "graceful";
|
|
352
|
-
const prefix = orchConfig.orchestrator.tmux_prefix;
|
|
353
|
-
const gracePeriodMs = orchConfig.orchestrator.abort_grace_period * 1000;
|
|
354
|
-
|
|
355
|
-
// Check for active in-memory batch
|
|
356
|
-
const hasActiveBatch = orchBatchState.phase !== "idle" &&
|
|
357
|
-
orchBatchState.phase !== "completed" &&
|
|
358
|
-
orchBatchState.phase !== "failed" &&
|
|
359
|
-
orchBatchState.phase !== "stopped";
|
|
360
|
-
|
|
361
|
-
// Also check for persisted state (abort can work on orphaned batches too)
|
|
362
|
-
let persistedState: PersistedBatchState | null = null;
|
|
363
351
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
352
|
+
const hard = args?.trim() === "--hard";
|
|
353
|
+
const mode: AbortMode = hard ? "hard" : "graceful";
|
|
354
|
+
const prefix = orchConfig.orchestrator.tmux_prefix;
|
|
355
|
+
const gracePeriodMs = orchConfig.orchestrator.abort_grace_period * 1000;
|
|
356
|
+
|
|
357
|
+
ctx.ui.notify(`🛑 Abort requested (${mode} mode, prefix: ${prefix})...`, "info");
|
|
358
|
+
|
|
359
|
+
// ── Step 1: Write abort signal file immediately ──────────
|
|
360
|
+
// This is the primary abort mechanism. The orchestrator's polling
|
|
361
|
+
// loop checks for this file on every cycle, so even if this command
|
|
362
|
+
// handler runs concurrently with /orch (or is queued behind it),
|
|
363
|
+
// the signal file will be detected.
|
|
364
|
+
const abortSignalFile = join(ctx.cwd, ".pi", "orch-abort-signal");
|
|
365
|
+
try {
|
|
366
|
+
mkdirSync(join(ctx.cwd, ".pi"), { recursive: true });
|
|
367
|
+
writeFileSync(abortSignalFile, `abort requested at ${new Date().toISOString()} (mode: ${mode})`, "utf-8");
|
|
368
|
+
ctx.ui.notify(" ✓ Abort signal file written (.pi/orch-abort-signal)", "info");
|
|
369
|
+
} catch (err) {
|
|
370
|
+
ctx.ui.notify(` ⚠ Failed to write abort signal file: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
371
|
+
}
|
|
368
372
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return execSync('tmux list-sessions -F "#{session_name}"', {
|
|
376
|
-
encoding: "utf-8",
|
|
377
|
-
timeout: 5000,
|
|
378
|
-
});
|
|
379
|
-
} catch {
|
|
380
|
-
return "";
|
|
381
|
-
}
|
|
382
|
-
})(),
|
|
383
|
-
prefix,
|
|
384
|
-
);
|
|
385
|
-
if (sessionNames.length === 0) {
|
|
386
|
-
ctx.ui.notify(ORCH_MESSAGES.abortNoBatch(), "warning");
|
|
387
|
-
return;
|
|
373
|
+
// ── Step 2: Set pause signal immediately ─────────────────
|
|
374
|
+
// Belt-and-suspenders: if the /orch polling loop can see this
|
|
375
|
+
// shared object, it will stop on the next iteration.
|
|
376
|
+
if (orchBatchState.pauseSignal) {
|
|
377
|
+
orchBatchState.pauseSignal.paused = true;
|
|
378
|
+
ctx.ui.notify(" ✓ Pause signal set on in-memory batch state", "info");
|
|
388
379
|
}
|
|
389
|
-
// If orphan sessions exist, proceed with abort (will kill them)
|
|
390
|
-
}
|
|
391
380
|
|
|
392
|
-
|
|
381
|
+
// ── Step 3: Check what we're aborting ────────────────────
|
|
382
|
+
const hasActiveBatch = orchBatchState.phase !== "idle" &&
|
|
383
|
+
orchBatchState.phase !== "completed" &&
|
|
384
|
+
orchBatchState.phase !== "failed" &&
|
|
385
|
+
orchBatchState.phase !== "stopped";
|
|
386
|
+
|
|
387
|
+
let persistedState: PersistedBatchState | null = null;
|
|
388
|
+
try {
|
|
389
|
+
persistedState = loadBatchState(ctx.cwd);
|
|
390
|
+
} catch {
|
|
391
|
+
// Ignore — we may still have in-memory state or orphan sessions
|
|
392
|
+
}
|
|
393
393
|
|
|
394
|
-
// Notify user of abort start
|
|
395
|
-
if (mode === "graceful") {
|
|
396
|
-
const sessionCount = orchBatchState.currentLanes.length || persistedState?.tasks.length || 0;
|
|
397
|
-
ctx.ui.notify(ORCH_MESSAGES.abortGracefulStarting(batchId, sessionCount), "info");
|
|
398
394
|
ctx.ui.notify(
|
|
399
|
-
|
|
395
|
+
` Batch state: in-memory=${hasActiveBatch ? orchBatchState.phase : "none"}, ` +
|
|
396
|
+
`persisted=${persistedState ? persistedState.batchId : "none"}`,
|
|
400
397
|
"info",
|
|
401
398
|
);
|
|
402
|
-
} else {
|
|
403
|
-
const sessionCount = orchBatchState.currentLanes.length || persistedState?.tasks.length || 0;
|
|
404
|
-
ctx.ui.notify(ORCH_MESSAGES.abortHardStarting(batchId, sessionCount), "info");
|
|
405
|
-
}
|
|
406
399
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
400
|
+
// ── Step 4: Scan for tmux sessions ──────────────────────
|
|
401
|
+
let allSessionNames: string[] = [];
|
|
402
|
+
try {
|
|
403
|
+
const tmuxOutput = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
404
|
+
encoding: "utf-8",
|
|
405
|
+
timeout: 5000,
|
|
406
|
+
}).trim();
|
|
407
|
+
const all = tmuxOutput ? tmuxOutput.split("\n").map(s => s.trim()).filter(Boolean) : [];
|
|
408
|
+
allSessionNames = all.filter(name => name.startsWith(`${prefix}-`));
|
|
409
|
+
ctx.ui.notify(` Found ${allSessionNames.length} session(s) matching prefix "${prefix}-": ${allSessionNames.join(", ") || "(none)"}`, "info");
|
|
410
|
+
} catch {
|
|
411
|
+
ctx.ui.notify(" ⚠ Could not list tmux sessions (tmux not available?)", "warning");
|
|
412
|
+
}
|
|
416
413
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
414
|
+
// If no batch AND no sessions, nothing to abort
|
|
415
|
+
if (!hasActiveBatch && !persistedState && allSessionNames.length === 0) {
|
|
416
|
+
ctx.ui.notify(ORCH_MESSAGES.abortNoBatch(), "warning");
|
|
417
|
+
// Clean up signal file
|
|
418
|
+
try { unlinkSync(abortSignalFile); } catch {}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const batchId = orchBatchState.batchId || persistedState?.batchId || "unknown";
|
|
421
423
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
424
|
+
// ── Step 5: Kill sessions directly (fast path) ──────────
|
|
425
|
+
// For hard mode or when sessions are found, kill them immediately
|
|
426
|
+
// rather than waiting through the full executeAbort flow.
|
|
427
|
+
if (allSessionNames.length > 0) {
|
|
428
|
+
ctx.ui.notify(` Killing ${allSessionNames.length} tmux session(s)...`, "info");
|
|
429
|
+
let killed = 0;
|
|
430
|
+
for (const name of allSessionNames) {
|
|
431
|
+
try {
|
|
432
|
+
// Kill child sessions first (worker, reviewer)
|
|
433
|
+
execSync(`tmux kill-session -t "${name}-worker" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
434
|
+
} catch {}
|
|
435
|
+
try {
|
|
436
|
+
execSync(`tmux kill-session -t "${name}-reviewer" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
437
|
+
} catch {}
|
|
438
|
+
try {
|
|
439
|
+
execSync(`tmux kill-session -t "${name}" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
440
|
+
killed++;
|
|
441
|
+
ctx.ui.notify(` ✓ Killed: ${name}`, "info");
|
|
442
|
+
} catch {
|
|
443
|
+
// Session may have already exited
|
|
444
|
+
ctx.ui.notify(` · ${name} (already exited)`, "info");
|
|
445
|
+
killed++;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
ctx.ui.notify(` ✓ ${killed}/${allSessionNames.length} session(s) terminated`, "info");
|
|
449
|
+
} else {
|
|
450
|
+
ctx.ui.notify(" No tmux sessions to kill", "info");
|
|
431
451
|
}
|
|
452
|
+
|
|
453
|
+
// ── Step 6: Clean up batch state ────────────────────────
|
|
454
|
+
try {
|
|
455
|
+
orchBatchState.phase = "stopped";
|
|
456
|
+
orchBatchState.endedAt = Date.now();
|
|
457
|
+
updateOrchWidget();
|
|
458
|
+
ctx.ui.notify(" ✓ In-memory batch state set to 'stopped'", "info");
|
|
459
|
+
} catch (err) {
|
|
460
|
+
ctx.ui.notify(` ⚠ Failed to update in-memory state: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
deleteBatchState(ctx.cwd);
|
|
465
|
+
ctx.ui.notify(" ✓ Batch state file deleted (.pi/batch-state.json)", "info");
|
|
466
|
+
} catch (err) {
|
|
467
|
+
ctx.ui.notify(` ⚠ Failed to delete batch state file: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── Step 7: Clean up abort signal file ───────────────────
|
|
471
|
+
try { unlinkSync(abortSignalFile); } catch {}
|
|
472
|
+
|
|
473
|
+
// ── Done ─────────────────────────────────────────────────
|
|
432
474
|
ctx.ui.notify(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
);
|
|
436
|
-
} else {
|
|
437
|
-
ctx.ui.notify(
|
|
438
|
-
ORCH_MESSAGES.abortHardComplete(batchId, result.sessionsKilled, durationSec),
|
|
475
|
+
`✅ Abort complete for batch ${batchId}. Sessions killed, state cleaned up.\n` +
|
|
476
|
+
` Worktrees and branches are preserved for inspection.`,
|
|
439
477
|
"info",
|
|
440
478
|
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Report errors if any
|
|
444
|
-
if (result.errors.length > 0) {
|
|
445
|
-
const errorDetails = result.errors.map(e => ` • [${e.code}] ${e.message}`).join("\n");
|
|
479
|
+
} catch (err) {
|
|
480
|
+
// Top-level catch: ensure the user ALWAYS sees something
|
|
446
481
|
ctx.ui.notify(
|
|
447
|
-
|
|
448
|
-
"
|
|
482
|
+
`❌ Abort failed with error: ${err instanceof Error ? err.message : String(err)}\n` +
|
|
483
|
+
` Stack: ${err instanceof Error ? err.stack : "N/A"}\n\n` +
|
|
484
|
+
` Manual cleanup: tmux kill-server (kills ALL tmux sessions)\n` +
|
|
485
|
+
` Or: tmux kill-session -t <session-name> for each session`,
|
|
486
|
+
"error",
|
|
449
487
|
);
|
|
450
488
|
}
|
|
451
|
-
|
|
452
|
-
// Final message
|
|
453
|
-
ctx.ui.notify(
|
|
454
|
-
ORCH_MESSAGES.abortComplete(mode, result.sessionsKilled),
|
|
455
|
-
"info",
|
|
456
|
-
);
|
|
457
489
|
},
|
|
458
490
|
});
|
|
459
491
|
|
|
@@ -559,6 +559,8 @@ export const SESSION_SPAWN_RETRY_MAX = 2;
|
|
|
559
559
|
* - EXEC_SPAWN_FAILED: TMUX session could not be created after retries
|
|
560
560
|
* - EXEC_TASK_FAILED: task completed without .DONE (non-zero exit)
|
|
561
561
|
* - EXEC_TASK_STALLED: STATUS.md unchanged for stall_timeout (handled by Step 3)
|
|
562
|
+
* - EXEC_TASK_STAGE_FAILED: git add failed for task files
|
|
563
|
+
* - EXEC_TASK_COMMIT_FAILED: git commit failed for staged task files
|
|
562
564
|
* - EXEC_TMUX_NOT_AVAILABLE: tmux binary not found
|
|
563
565
|
* - EXEC_WORKTREE_MISSING: lane worktree path doesn't exist
|
|
564
566
|
*/
|
|
@@ -566,6 +568,8 @@ export type ExecutionErrorCode =
|
|
|
566
568
|
| "EXEC_SPAWN_FAILED"
|
|
567
569
|
| "EXEC_TASK_FAILED"
|
|
568
570
|
| "EXEC_TASK_STALLED"
|
|
571
|
+
| "EXEC_TASK_STAGE_FAILED"
|
|
572
|
+
| "EXEC_TASK_COMMIT_FAILED"
|
|
569
573
|
| "EXEC_TMUX_NOT_AVAILABLE"
|
|
570
574
|
| "EXEC_WORKTREE_MISSING";
|
|
571
575
|
|
package/package.json
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "taskplane",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"pi-package",
|
|
7
|
-
"ai",
|
|
8
|
-
"agent",
|
|
9
|
-
"orchestration",
|
|
10
|
-
"task-runner",
|
|
11
|
-
"parallel"
|
|
12
|
-
],
|
|
13
|
-
"bin": {
|
|
14
|
-
"taskplane": "bin/taskplane.mjs"
|
|
15
|
-
},
|
|
16
|
-
"pi": {
|
|
17
|
-
"extensions": [
|
|
18
|
-
"./extensions/task-runner.ts",
|
|
19
|
-
"./extensions/task-orchestrator.ts"
|
|
20
|
-
],
|
|
21
|
-
"skills": [
|
|
22
|
-
"./skills"
|
|
23
|
-
]
|
|
24
|
-
},
|
|
25
|
-
"type": "module",
|
|
26
|
-
"engines": {
|
|
27
|
-
"node": ">=20.0.0"
|
|
28
|
-
},
|
|
29
|
-
"files": [
|
|
30
|
-
"bin/",
|
|
31
|
-
"dashboard/",
|
|
32
|
-
"extensions/task-runner.ts",
|
|
33
|
-
"extensions/task-orchestrator.ts",
|
|
34
|
-
"extensions/taskplane/",
|
|
35
|
-
"skills/",
|
|
36
|
-
"templates/"
|
|
37
|
-
],
|
|
38
|
-
"peerDependencies": {
|
|
39
|
-
"@mariozechner/pi-coding-agent": "*",
|
|
40
|
-
"@mariozechner/pi-tui": "*",
|
|
41
|
-
"@mariozechner/pi-ai": "*",
|
|
42
|
-
"@sinclair/typebox": "*"
|
|
43
|
-
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"yaml": "^2.4.0"
|
|
46
|
-
},
|
|
47
|
-
"license": "MIT",
|
|
48
|
-
"repository": {
|
|
49
|
-
"type": "git",
|
|
50
|
-
"url": "git+https://github.com/HenryLach/taskplane.git"
|
|
51
|
-
},
|
|
52
|
-
"homepage": "https://github.com/HenryLach/taskplane#readme",
|
|
53
|
-
"bugs": {
|
|
54
|
-
"url": "https://github.com/HenryLach/taskplane/issues"
|
|
55
|
-
},
|
|
56
|
-
"author": "Henry Lach"
|
|
57
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "taskplane",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"ai",
|
|
8
|
+
"agent",
|
|
9
|
+
"orchestration",
|
|
10
|
+
"task-runner",
|
|
11
|
+
"parallel"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"taskplane": "bin/taskplane.mjs"
|
|
15
|
+
},
|
|
16
|
+
"pi": {
|
|
17
|
+
"extensions": [
|
|
18
|
+
"./extensions/task-runner.ts",
|
|
19
|
+
"./extensions/task-orchestrator.ts"
|
|
20
|
+
],
|
|
21
|
+
"skills": [
|
|
22
|
+
"./skills"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20.0.0"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"bin/",
|
|
31
|
+
"dashboard/",
|
|
32
|
+
"extensions/task-runner.ts",
|
|
33
|
+
"extensions/task-orchestrator.ts",
|
|
34
|
+
"extensions/taskplane/",
|
|
35
|
+
"skills/",
|
|
36
|
+
"templates/"
|
|
37
|
+
],
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
40
|
+
"@mariozechner/pi-tui": "*",
|
|
41
|
+
"@mariozechner/pi-ai": "*",
|
|
42
|
+
"@sinclair/typebox": "*"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"yaml": "^2.4.0"
|
|
46
|
+
},
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/HenryLach/taskplane.git"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/HenryLach/taskplane#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/HenryLach/taskplane/issues"
|
|
55
|
+
},
|
|
56
|
+
"author": "Henry Lach"
|
|
57
|
+
}
|