taskplane 0.29.2 → 0.30.1

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.
Files changed (43) hide show
  1. package/bin/gitignore-patterns.mjs +11 -8
  2. package/bin/rpc-wrapper.mjs +410 -357
  3. package/bin/taskplane.mjs +533 -250
  4. package/dashboard/public/app.js +124 -15
  5. package/dashboard/public/style.css +83 -2
  6. package/extensions/reviewer-extension.ts +17 -11
  7. package/extensions/taskplane/abort.ts +50 -18
  8. package/extensions/taskplane/agent-bridge-extension.ts +232 -105
  9. package/extensions/taskplane/agent-host.ts +224 -97
  10. package/extensions/taskplane/cleanup.ts +71 -42
  11. package/extensions/taskplane/config-loader.ts +142 -58
  12. package/extensions/taskplane/config-schema.ts +6 -13
  13. package/extensions/taskplane/config.ts +10 -2
  14. package/extensions/taskplane/diagnostic-reports.ts +59 -47
  15. package/extensions/taskplane/diagnostics.ts +13 -13
  16. package/extensions/taskplane/discovery.ts +78 -63
  17. package/extensions/taskplane/engine-worker.ts +53 -46
  18. package/extensions/taskplane/engine.ts +1760 -602
  19. package/extensions/taskplane/execution.ts +469 -207
  20. package/extensions/taskplane/extension.ts +1073 -598
  21. package/extensions/taskplane/formatting.ts +136 -124
  22. package/extensions/taskplane/git.ts +0 -2
  23. package/extensions/taskplane/lane-runner.ts +652 -319
  24. package/extensions/taskplane/mailbox.ts +57 -49
  25. package/extensions/taskplane/merge.ts +662 -383
  26. package/extensions/taskplane/messages.ts +109 -51
  27. package/extensions/taskplane/migrations.ts +1 -1
  28. package/extensions/taskplane/path-resolver.ts +8 -9
  29. package/extensions/taskplane/persistence.ts +425 -262
  30. package/extensions/taskplane/process-registry.ts +36 -7
  31. package/extensions/taskplane/quality-gate.ts +107 -55
  32. package/extensions/taskplane/resume.ts +832 -280
  33. package/extensions/taskplane/sessions.ts +1 -1
  34. package/extensions/taskplane/settings-tui.ts +505 -164
  35. package/extensions/taskplane/sidecar-telemetry.ts +25 -10
  36. package/extensions/taskplane/supervisor.ts +477 -270
  37. package/extensions/taskplane/task-executor-core.ts +178 -53
  38. package/extensions/taskplane/types.ts +209 -108
  39. package/extensions/taskplane/verification.ts +27 -22
  40. package/extensions/taskplane/waves.ts +59 -43
  41. package/extensions/taskplane/workspace.ts +14 -12
  42. package/extensions/taskplane/worktree.ts +218 -196
  43. package/package.json +14 -2
@@ -10,7 +10,20 @@ import { execLog } from "./execution.ts";
10
10
  import { runGit } from "./git.ts";
11
11
  import { resolveOperatorId } from "./naming.ts";
12
12
  import { DEFAULT_ORCHESTRATOR_CONFIG, WorktreeError } from "./types.ts";
13
- import type { AllocatedLane, BulkWorktreeError, CreateLaneWorktreesResult, CreateWorktreeOptions, LaneTaskOutcome, OrchestratorConfig, PreflightCheck, PreflightResult, RemoveAllWorktreesResult, RemoveWorktreeOutcome, RemoveWorktreeResult, WorktreeInfo } from "./types.ts";
13
+ import type {
14
+ AllocatedLane,
15
+ BulkWorktreeError,
16
+ CreateLaneWorktreesResult,
17
+ CreateWorktreeOptions,
18
+ LaneTaskOutcome,
19
+ OrchestratorConfig,
20
+ PreflightCheck,
21
+ PreflightResult,
22
+ RemoveAllWorktreesResult,
23
+ RemoveWorktreeOutcome,
24
+ RemoveWorktreeResult,
25
+ WorktreeInfo,
26
+ } from "./types.ts";
14
27
 
15
28
  // ── Worktree Helpers ─────────────────────────────────────────────────
16
29
 
@@ -42,10 +55,7 @@ export function generateBranchName(laneNumber: number, batchId: string, opId: st
42
55
  * @param repoRoot - Absolute path to the main repository root
43
56
  * @param config - Orchestrator config (reads `worktree_location`)
44
57
  */
45
- export function resolveWorktreeBasePath(
46
- repoRoot: string,
47
- config: OrchestratorConfig,
48
- ): string {
58
+ export function resolveWorktreeBasePath(repoRoot: string, config: OrchestratorConfig): string {
49
59
  const location = config.orchestrator.worktree_location;
50
60
  if (location === "sibling") {
51
61
  return resolve(repoRoot, "..");
@@ -301,12 +311,9 @@ export function normalizePath(p: string): string {
301
311
  export function isRegisteredWorktree(targetPath: string, cwd: string): boolean {
302
312
  const entries = parseWorktreeList(cwd);
303
313
  const normalized = normalizePath(targetPath);
304
- return entries.some(
305
- (e) => normalizePath(e.path) === normalized,
306
- );
314
+ return entries.some((e) => normalizePath(e.path) === normalized);
307
315
  }
308
316
 
309
-
310
317
  // ── Worktree CRUD Operations ─────────────────────────────────────────
311
318
 
312
319
  /**
@@ -337,15 +344,12 @@ export function createWorktree(opts: CreateWorktreeOptions, repoRoot: string): W
337
344
  const worktreePath = generateWorktreePath(prefix, laneNumber, repoRoot, opId, config, batchId);
338
345
 
339
346
  // ── Pre-check 1: Validate base branch exists ─────────────────
340
- const baseBranchCheck = runGit(
341
- ["rev-parse", "--verify", `refs/heads/${baseBranch}`],
342
- repoRoot,
343
- );
347
+ const baseBranchCheck = runGit(["rev-parse", "--verify", `refs/heads/${baseBranch}`], repoRoot);
344
348
  if (!baseBranchCheck.ok) {
345
349
  throw new WorktreeError(
346
350
  "WORKTREE_INVALID_BASE",
347
351
  `Base branch "${baseBranch}" does not exist locally. ` +
348
- `Verify the branch exists: git branch --list ${baseBranch}`,
352
+ `Verify the branch exists: git branch --list ${baseBranch}`,
349
353
  );
350
354
  }
351
355
  const baseBranchHead = baseBranchCheck.stdout.trim();
@@ -355,7 +359,7 @@ export function createWorktree(opts: CreateWorktreeOptions, repoRoot: string): W
355
359
  throw new WorktreeError(
356
360
  "WORKTREE_PATH_IS_WORKTREE",
357
361
  `Path "${worktreePath}" is already registered as a git worktree. ` +
358
- `Remove it first: git worktree remove "${worktreePath}"`,
362
+ `Remove it first: git worktree remove "${worktreePath}"`,
359
363
  );
360
364
  }
361
365
 
@@ -367,7 +371,7 @@ export function createWorktree(opts: CreateWorktreeOptions, repoRoot: string): W
367
371
  throw new WorktreeError(
368
372
  "WORKTREE_PATH_NOT_EMPTY",
369
373
  `Path "${worktreePath}" exists and is not empty. ` +
370
- `It is not a registered git worktree. Remove or rename it before creating a worktree here.`,
374
+ `It is not a registered git worktree. Remove or rename it before creating a worktree here.`,
371
375
  );
372
376
  }
373
377
  } catch (err) {
@@ -381,16 +385,13 @@ export function createWorktree(opts: CreateWorktreeOptions, repoRoot: string): W
381
385
  }
382
386
 
383
387
  // ── Pre-check 4: Check if branch already exists ──────────────
384
- const branchCheck = runGit(
385
- ["rev-parse", "--verify", `refs/heads/${branch}`],
386
- repoRoot,
387
- );
388
+ const branchCheck = runGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot);
388
389
  if (branchCheck.ok) {
389
390
  throw new WorktreeError(
390
391
  "WORKTREE_BRANCH_EXISTS",
391
392
  `Branch "${branch}" already exists. ` +
392
- `This may indicate a stale worktree from a previous batch. ` +
393
- `Delete it: git branch -D ${branch}`,
393
+ `This may indicate a stale worktree from a previous batch. ` +
394
+ `Delete it: git branch -D ${branch}`,
394
395
  );
395
396
  }
396
397
 
@@ -401,29 +402,23 @@ export function createWorktree(opts: CreateWorktreeOptions, repoRoot: string): W
401
402
  ensureBatchContainerDir(containerDir);
402
403
 
403
404
  // ── Create worktree ──────────────────────────────────────────
404
- const createResult = runGit(
405
- ["worktree", "add", "-b", branch, worktreePath, baseBranch],
406
- repoRoot,
407
- );
405
+ const createResult = runGit(["worktree", "add", "-b", branch, worktreePath, baseBranch], repoRoot);
408
406
  if (!createResult.ok) {
409
407
  throw new WorktreeError(
410
408
  "WORKTREE_GIT_ERROR",
411
409
  `Failed to create worktree at "${worktreePath}" on branch "${branch}" ` +
412
- `from "${baseBranch}": ${createResult.stderr}`,
410
+ `from "${baseBranch}": ${createResult.stderr}`,
413
411
  );
414
412
  }
415
413
 
416
414
  // ── Post-creation verification (R002 requirements) ───────────
417
415
  // Verify 1: Correct branch is checked out
418
- const headBranchResult = runGit(
419
- ["rev-parse", "--abbrev-ref", "HEAD"],
420
- worktreePath,
421
- );
416
+ const headBranchResult = runGit(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
422
417
  if (!headBranchResult.ok || headBranchResult.stdout !== branch) {
423
418
  throw new WorktreeError(
424
419
  "WORKTREE_VERIFY_FAILED",
425
420
  `Verification failed: expected branch "${branch}" checked out ` +
426
- `in worktree, but got "${headBranchResult.stdout || "(unknown)"}".`,
421
+ `in worktree, but got "${headBranchResult.stdout || "(unknown)"}".`,
427
422
  );
428
423
  }
429
424
 
@@ -433,7 +428,7 @@ export function createWorktree(opts: CreateWorktreeOptions, repoRoot: string): W
433
428
  throw new WorktreeError(
434
429
  "WORKTREE_VERIFY_FAILED",
435
430
  `Verification failed: worktree HEAD (${headCommitResult.stdout?.slice(0, 8) || "?"}) ` +
436
- `does not match baseBranch "${baseBranch}" HEAD (${baseBranchHead.slice(0, 8)}).`,
431
+ `does not match baseBranch "${baseBranch}" HEAD (${baseBranchHead.slice(0, 8)}).`,
437
432
  );
438
433
  }
439
434
 
@@ -484,7 +479,7 @@ export function resetWorktree(
484
479
  throw new WorktreeError(
485
480
  "WORKTREE_NOT_FOUND",
486
481
  `Worktree path "${worktreePath}" does not exist on disk. ` +
487
- `It may have been removed externally.`,
482
+ `It may have been removed externally.`,
488
483
  );
489
484
  }
490
485
 
@@ -493,20 +488,17 @@ export function resetWorktree(
493
488
  throw new WorktreeError(
494
489
  "WORKTREE_NOT_REGISTERED",
495
490
  `Path "${worktreePath}" exists but is not a registered git worktree. ` +
496
- `It may have been removed from git tracking. Check: git worktree list`,
491
+ `It may have been removed from git tracking. Check: git worktree list`,
497
492
  );
498
493
  }
499
494
 
500
495
  // ── Pre-check 3: Target branch resolves ──────────────────────
501
- const targetCheck = runGit(
502
- ["rev-parse", "--verify", `refs/heads/${targetBranch}`],
503
- repoRoot,
504
- );
496
+ const targetCheck = runGit(["rev-parse", "--verify", `refs/heads/${targetBranch}`], repoRoot);
505
497
  if (!targetCheck.ok) {
506
498
  throw new WorktreeError(
507
499
  "WORKTREE_INVALID_BASE",
508
500
  `Target branch "${targetBranch}" does not exist locally. ` +
509
- `Verify the branch exists: git branch --list ${targetBranch}`,
501
+ `Verify the branch exists: git branch --list ${targetBranch}`,
510
502
  );
511
503
  }
512
504
  const targetCommit = targetCheck.stdout.trim();
@@ -523,35 +515,29 @@ export function resetWorktree(
523
515
  throw new WorktreeError(
524
516
  "WORKTREE_DIRTY",
525
517
  `Worktree at "${worktreePath}" has uncommitted changes. ` +
526
- `Workers must commit or discard all changes before a reset can proceed. ` +
527
- `Dirty files:\n${statusCheck.stdout}`,
518
+ `Workers must commit or discard all changes before a reset can proceed. ` +
519
+ `Dirty files:\n${statusCheck.stdout}`,
528
520
  );
529
521
  }
530
522
 
531
523
  // ── Reset: git checkout -B <laneBranch> <targetBranch> ───────
532
- const resetResult = runGit(
533
- ["checkout", "-B", branch, targetBranch],
534
- worktreePath,
535
- );
524
+ const resetResult = runGit(["checkout", "-B", branch, targetBranch], worktreePath);
536
525
  if (!resetResult.ok) {
537
526
  throw new WorktreeError(
538
527
  "WORKTREE_RESET_FAILED",
539
528
  `Failed to reset worktree at "${worktreePath}" ` +
540
- `(branch "${branch}" → "${targetBranch}"): ${resetResult.stderr}`,
529
+ `(branch "${branch}" → "${targetBranch}"): ${resetResult.stderr}`,
541
530
  );
542
531
  }
543
532
 
544
533
  // ── Post-reset verification ──────────────────────────────────
545
534
  // Verify 1: Current branch equals expected lane branch
546
- const headBranchResult = runGit(
547
- ["rev-parse", "--abbrev-ref", "HEAD"],
548
- worktreePath,
549
- );
535
+ const headBranchResult = runGit(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
550
536
  if (!headBranchResult.ok || headBranchResult.stdout !== branch) {
551
537
  throw new WorktreeError(
552
538
  "WORKTREE_VERIFY_FAILED",
553
539
  `Post-reset verification failed: expected branch "${branch}" ` +
554
- `checked out, but got "${headBranchResult.stdout || "(unknown)"}".`,
540
+ `checked out, but got "${headBranchResult.stdout || "(unknown)"}".`,
555
541
  );
556
542
  }
557
543
 
@@ -561,8 +547,8 @@ export function resetWorktree(
561
547
  throw new WorktreeError(
562
548
  "WORKTREE_VERIFY_FAILED",
563
549
  `Post-reset verification failed: worktree HEAD ` +
564
- `(${headCommitResult.stdout?.slice(0, 8) || "?"}) does not match ` +
565
- `target "${targetBranch}" commit (${targetCommit.slice(0, 8)}).`,
550
+ `(${headCommitResult.stdout?.slice(0, 8) || "?"}) does not match ` +
551
+ `target "${targetBranch}" commit (${targetCommit.slice(0, 8)}).`,
566
552
  );
567
553
  }
568
554
 
@@ -669,16 +655,20 @@ export function isWindowsMaxPathError(stderr: string): boolean {
669
655
  * @returns { ok, stdout, stderr }
670
656
  * @since TP-188 (#543)
671
657
  */
672
- export function runWindowsCmdRd(
673
- absolutePath: string,
674
- ): { ok: boolean; stdout: string; stderr: string } {
658
+ export function runWindowsCmdRd(absolutePath: string): {
659
+ ok: boolean;
660
+ stdout: string;
661
+ stderr: string;
662
+ } {
675
663
  const winPath = absolutePath.replace(/\//g, "\\");
676
664
  try {
677
665
  const stdout = execFileSync("cmd", ["/c", "rd", "/s", "/q", winPath], {
678
666
  encoding: "utf-8",
679
667
  timeout: 60_000,
680
668
  stdio: ["pipe", "pipe", "pipe"],
681
- }).toString().trim();
669
+ })
670
+ .toString()
671
+ .trim();
682
672
  return { ok: true, stdout, stderr: "" };
683
673
  } catch (err: unknown) {
684
674
  const e = err as { stdout?: string; stderr?: string; message?: string };
@@ -772,10 +762,7 @@ export function removeWorktree(
772
762
  let lastError = "";
773
763
 
774
764
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
775
- const removeResult = runGit(
776
- ["worktree", "remove", "--force", worktreePath],
777
- repoRoot,
778
- );
765
+ const removeResult = runGit(["worktree", "remove", "--force", worktreePath], repoRoot);
779
766
 
780
767
  if (removeResult.ok) {
781
768
  // Successful removal — proceed to branch cleanup
@@ -793,12 +780,10 @@ export function removeWorktree(
793
780
  // the error as terminal/retriable so other error classes still
794
781
  // surface unchanged.
795
782
  if (isWindowsMaxPathError(lastError)) {
796
- execLog(
797
- "cleanup",
798
- "worktree",
799
- `Windows MAX_PATH detected — falling back to cmd "rd /s /q"`,
800
- { path: worktreePath, attempt },
801
- );
783
+ execLog("cleanup", "worktree", `Windows MAX_PATH detected — falling back to cmd "rd /s /q"`, {
784
+ path: worktreePath,
785
+ attempt,
786
+ });
802
787
  const fallback = runWindowsCmdRd(worktreePath);
803
788
  if (fallback.ok) {
804
789
  execLog(
@@ -817,12 +802,10 @@ export function removeWorktree(
817
802
  // attempts, then fall through to the existing terminal/retry
818
803
  // classification (which will throw because "Filename too long"
819
804
  // is non-retriable per isRetriableRemoveError).
820
- execLog(
821
- "cleanup",
822
- "worktree",
823
- `cmd "rd /s /q" fallback failed`,
824
- { path: worktreePath, error: fallback.stderr.slice(0, 200) },
825
- );
805
+ execLog("cleanup", "worktree", `cmd "rd /s /q" fallback failed`, {
806
+ path: worktreePath,
807
+ error: fallback.stderr.slice(0, 200),
808
+ });
826
809
  lastError =
827
810
  `git worktree remove failed: ${lastError}; ` +
828
811
  `cmd rd /s /q fallback failed: ${fallback.stderr}`;
@@ -833,7 +816,7 @@ export function removeWorktree(
833
816
  throw new WorktreeError(
834
817
  "WORKTREE_REMOVE_FAILED",
835
818
  `Failed to remove worktree at "${worktreePath}" ` +
836
- `(terminal error, not retried): ${lastError}`,
819
+ `(terminal error, not retried): ${lastError}`,
837
820
  );
838
821
  }
839
822
 
@@ -842,9 +825,9 @@ export function removeWorktree(
842
825
  throw new WorktreeError(
843
826
  "WORKTREE_REMOVE_RETRY_EXHAUSTED",
844
827
  `Failed to remove worktree at "${worktreePath}" after ` +
845
- `${MAX_ATTEMPTS} attempts. Last error: ${lastError}. ` +
846
- `This is likely a Windows file locking issue. ` +
847
- `Close any programs accessing "${worktreePath}" and try again.`,
828
+ `${MAX_ATTEMPTS} attempts. Last error: ${lastError}. ` +
829
+ `This is likely a Windows file locking issue. ` +
830
+ `Close any programs accessing "${worktreePath}" and try again.`,
848
831
  );
849
832
  }
850
833
 
@@ -858,7 +841,7 @@ export function removeWorktree(
858
841
  throw new WorktreeError(
859
842
  "WORKTREE_VERIFY_FAILED",
860
843
  `Post-removal verification failed: path "${worktreePath}" ` +
861
- `still exists on disk after successful git worktree remove.`,
844
+ `still exists on disk after successful git worktree remove.`,
862
845
  );
863
846
  }
864
847
 
@@ -869,7 +852,7 @@ export function removeWorktree(
869
852
  throw new WorktreeError(
870
853
  "WORKTREE_VERIFY_FAILED",
871
854
  `Post-removal verification failed: path "${worktreePath}" ` +
872
- `is still registered as a git worktree after removal and prune.`,
855
+ `is still registered as a git worktree after removal and prune.`,
873
856
  );
874
857
  }
875
858
  }
@@ -959,7 +942,7 @@ export function ensureBranchDeleted(
959
942
  throw new WorktreeError(
960
943
  "WORKTREE_BRANCH_DELETE_FAILED",
961
944
  `Worktree "${worktreePath}" was removed, but failed to delete lane branch ` +
962
- `"${branch}". Delete it manually: git branch -D ${branch}`,
945
+ `"${branch}". Delete it manually: git branch -D ${branch}`,
963
946
  );
964
947
  }
965
948
  return { deleted: true, preserved: false };
@@ -979,10 +962,7 @@ export function ensureBranchDeleted(
979
962
  */
980
963
  export function deleteBranchBestEffort(branch: string, repoRoot: string): boolean {
981
964
  // Check if branch exists first
982
- const branchCheck = runGit(
983
- ["rev-parse", "--verify", `refs/heads/${branch}`],
984
- repoRoot,
985
- );
965
+ const branchCheck = runGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot);
986
966
 
987
967
  if (!branchCheck.ok) {
988
968
  // Branch doesn't exist — idempotent success
@@ -997,10 +977,7 @@ export function deleteBranchBestEffort(branch: string, repoRoot: string): boolea
997
977
  }
998
978
 
999
979
  // If delete failed but branch is now gone (race condition), treat as success
1000
- const recheckResult = runGit(
1001
- ["rev-parse", "--verify", `refs/heads/${branch}`],
1002
- repoRoot,
1003
- );
980
+ const recheckResult = runGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot);
1004
981
  if (!recheckResult.ok) {
1005
982
  return true;
1006
983
  }
@@ -1009,7 +986,6 @@ export function deleteBranchBestEffort(branch: string, repoRoot: string): boolea
1009
986
  return false;
1010
987
  }
1011
988
 
1012
-
1013
989
  // ── Branch Protection Helpers ────────────────────────────────────────
1014
990
 
1015
991
  /** Typed error codes for unmerged commit checks */
@@ -1054,35 +1030,46 @@ export function hasUnmergedCommits(
1054
1030
  repoRoot: string,
1055
1031
  ): UnmergedCommitsResult {
1056
1032
  // Verify branch exists
1057
- const branchCheck = runGit(
1058
- ["rev-parse", "--verify", `refs/heads/${branch}`],
1059
- repoRoot,
1060
- );
1033
+ const branchCheck = runGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot);
1061
1034
  if (!branchCheck.ok) {
1062
- return { ok: false, count: 0, code: "BRANCH_NOT_FOUND", error: `Branch "${branch}" does not exist` };
1035
+ return {
1036
+ ok: false,
1037
+ count: 0,
1038
+ code: "BRANCH_NOT_FOUND",
1039
+ error: `Branch "${branch}" does not exist`,
1040
+ };
1063
1041
  }
1064
1042
 
1065
1043
  // Verify target branch exists
1066
- const targetCheck = runGit(
1067
- ["rev-parse", "--verify", `refs/heads/${targetBranch}`],
1068
- repoRoot,
1069
- );
1044
+ const targetCheck = runGit(["rev-parse", "--verify", `refs/heads/${targetBranch}`], repoRoot);
1070
1045
  if (!targetCheck.ok) {
1071
- return { ok: false, count: 0, code: "TARGET_BRANCH_MISSING", error: `Target branch "${targetBranch}" does not exist` };
1046
+ return {
1047
+ ok: false,
1048
+ count: 0,
1049
+ code: "TARGET_BRANCH_MISSING",
1050
+ error: `Target branch "${targetBranch}" does not exist`,
1051
+ };
1072
1052
  }
1073
1053
 
1074
1054
  // Count commits on branch not reachable from target
1075
- const countResult = runGit(
1076
- ["rev-list", "--count", `${targetBranch}..${branch}`],
1077
- repoRoot,
1078
- );
1055
+ const countResult = runGit(["rev-list", "--count", `${targetBranch}..${branch}`], repoRoot);
1079
1056
  if (!countResult.ok) {
1080
- return { ok: false, count: 0, code: "UNMERGED_COUNT_FAILED", error: `Failed to count unmerged commits: ${countResult.stderr}` };
1057
+ return {
1058
+ ok: false,
1059
+ count: 0,
1060
+ code: "UNMERGED_COUNT_FAILED",
1061
+ error: `Failed to count unmerged commits: ${countResult.stderr}`,
1062
+ };
1081
1063
  }
1082
1064
 
1083
1065
  const count = parseInt(countResult.stdout.trim(), 10);
1084
1066
  if (isNaN(count)) {
1085
- return { ok: false, count: 0, code: "UNMERGED_COUNT_PARSE_FAILED", error: `Failed to parse commit count: "${countResult.stdout}"` };
1067
+ return {
1068
+ ok: false,
1069
+ count: 0,
1070
+ code: "UNMERGED_COUNT_PARSE_FAILED",
1071
+ error: `Failed to parse commit count: "${countResult.stdout}"`,
1072
+ };
1086
1073
  }
1087
1074
 
1088
1075
  return { ok: true, count };
@@ -1197,10 +1184,7 @@ export function preserveBranch(
1197
1184
  repoRoot: string,
1198
1185
  ): PreserveBranchResult {
1199
1186
  // Check if branch exists
1200
- const branchCheck = runGit(
1201
- ["rev-parse", "--verify", `refs/heads/${branch}`],
1202
- repoRoot,
1203
- );
1187
+ const branchCheck = runGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot);
1204
1188
  if (!branchCheck.ok) {
1205
1189
  return { ok: true, action: "no-branch" };
1206
1190
  }
@@ -1212,7 +1196,9 @@ export function preserveBranch(
1212
1196
  // Target branch missing or git error — skip preservation gracefully
1213
1197
  // Map unmerged error codes to preserve error codes
1214
1198
  const preserveCode: PreserveBranchErrorCode =
1215
- unmergedResult.code === "TARGET_BRANCH_MISSING" ? "TARGET_BRANCH_MISSING" : "UNMERGED_COUNT_FAILED";
1199
+ unmergedResult.code === "TARGET_BRANCH_MISSING"
1200
+ ? "TARGET_BRANCH_MISSING"
1201
+ : "UNMERGED_COUNT_FAILED";
1216
1202
  return {
1217
1203
  ok: false,
1218
1204
  action: "error",
@@ -1229,10 +1215,7 @@ export function preserveBranch(
1229
1215
  const savedName = computeSavedBranchName(branch);
1230
1216
 
1231
1217
  // Check for collision
1232
- const existingCheck = runGit(
1233
- ["rev-parse", "--verify", `refs/heads/${savedName}`],
1234
- repoRoot,
1235
- );
1218
+ const existingCheck = runGit(["rev-parse", "--verify", `refs/heads/${savedName}`], repoRoot);
1236
1219
  const existingSHA = existingCheck.ok ? existingCheck.stdout.trim() : "";
1237
1220
 
1238
1221
  const resolution = resolveSavedBranchCollision(savedName, existingSHA, branchSHA);
@@ -1249,10 +1232,7 @@ export function preserveBranch(
1249
1232
  case "create":
1250
1233
  case "create-suffixed": {
1251
1234
  // Create saved branch at same SHA
1252
- const createResult = runGit(
1253
- ["branch", resolution.savedName, branchSHA],
1254
- repoRoot,
1255
- );
1235
+ const createResult = runGit(["branch", resolution.savedName, branchSHA], repoRoot);
1256
1236
  if (!createResult.ok) {
1257
1237
  return {
1258
1238
  ok: false,
@@ -1271,11 +1251,15 @@ export function preserveBranch(
1271
1251
  }
1272
1252
 
1273
1253
  default:
1274
- return { ok: false, action: "error", code: "UNKNOWN_RESOLUTION", error: `Unknown resolution action` };
1254
+ return {
1255
+ ok: false,
1256
+ action: "error",
1257
+ code: "UNKNOWN_RESOLUTION",
1258
+ error: `Unknown resolution action`,
1259
+ };
1275
1260
  }
1276
1261
  }
1277
1262
 
1278
-
1279
1263
  // ── Bulk Worktree Operations ─────────────────────────────────────────
1280
1264
 
1281
1265
  /**
@@ -1306,7 +1290,12 @@ export function preserveBranch(
1306
1290
  * only returns worktrees inside the `{opId}-{batchId}/` container
1307
1291
  * @returns - WorktreeInfo[] sorted by laneNumber (ascending)
1308
1292
  */
1309
- export function listWorktrees(prefix: string, repoRoot: string, opId: string, batchId?: string): WorktreeInfo[] {
1293
+ export function listWorktrees(
1294
+ prefix: string,
1295
+ repoRoot: string,
1296
+ opId: string,
1297
+ batchId?: string,
1298
+ ): WorktreeInfo[] {
1310
1299
  const entries = parseWorktreeList(repoRoot);
1311
1300
  const results: WorktreeInfo[] = [];
1312
1301
 
@@ -1317,9 +1306,7 @@ export function listWorktrees(prefix: string, repoRoot: string, opId: string, ba
1317
1306
 
1318
1307
  // Legacy pattern: {prefix}-{N} (only matched when opId is the default fallback)
1319
1308
  // This allows cleanup of worktrees from prior batches without operator IDs.
1320
- const legacyPattern = opId === "op"
1321
- ? new RegExp(`^${escapeRegex(prefix)}-(\\d+)$`)
1322
- : null;
1309
+ const legacyPattern = opId === "op" ? new RegExp(`^${escapeRegex(prefix)}-(\\d+)$`) : null;
1323
1310
 
1324
1311
  // ── New batch-scoped nested pattern ──────────────────────────
1325
1312
  // Basename: lane-{N}
@@ -1617,7 +1604,12 @@ export function removeAllWorktrees(
1617
1604
  const outcomes: RemoveWorktreeOutcome[] = [];
1618
1605
  const removed: WorktreeInfo[] = [];
1619
1606
  const failed: RemoveWorktreeOutcome[] = [];
1620
- const preserved: Array<{ branch: string; savedBranch: string; laneNumber: number; unmergedCount?: number }> = [];
1607
+ const preserved: Array<{
1608
+ branch: string;
1609
+ savedBranch: string;
1610
+ laneNumber: number;
1611
+ unmergedCount?: number;
1612
+ }> = [];
1621
1613
 
1622
1614
  for (const wt of worktrees) {
1623
1615
  try {
@@ -1692,7 +1684,9 @@ export function removeAllWorktrees(
1692
1684
  rmdirSync(basePath);
1693
1685
  }
1694
1686
  }
1695
- } catch { /* safe default — leave it alone */ }
1687
+ } catch {
1688
+ /* safe default — leave it alone */
1689
+ }
1696
1690
  }
1697
1691
 
1698
1692
  return {
@@ -1756,12 +1750,21 @@ export function execCheck(command: string, cwd?: string, timeoutMs = 10_000): Ex
1756
1750
  // platform). We attribute SIGTERM to the timeout because `execCheck` is
1757
1751
  // the one setting the timeout option — there's no other realistic source
1758
1752
  // of SIGTERM for a short-lived diagnostic command we just spawned.
1759
- const e = err as { code?: string | number; status?: number | null; signal?: NodeJS.Signals | null; errno?: number; message?: string; path?: string; stderr?: string | Buffer };
1760
- const stderrText = typeof e?.stderr === "string"
1761
- ? e.stderr
1762
- : e?.stderr instanceof Buffer
1763
- ? e.stderr.toString("utf-8")
1764
- : "";
1753
+ const e = err as {
1754
+ code?: string | number;
1755
+ status?: number | null;
1756
+ signal?: NodeJS.Signals | null;
1757
+ errno?: number;
1758
+ message?: string;
1759
+ path?: string;
1760
+ stderr?: string | Buffer;
1761
+ };
1762
+ const stderrText =
1763
+ typeof e?.stderr === "string"
1764
+ ? e.stderr
1765
+ : e?.stderr instanceof Buffer
1766
+ ? e.stderr.toString("utf-8")
1767
+ : "";
1765
1768
  const commandName = command.split(/\s+/)[0];
1766
1769
  if (e?.code === "ENOENT") {
1767
1770
  return { ok: false, stdout: "", errorKind: "not-found", errorDetail: e.path ?? commandName };
@@ -1770,11 +1773,19 @@ export function execCheck(command: string, cwd?: string, timeoutMs = 10_000): Ex
1770
1773
  return { ok: false, stdout: "", errorKind: "not-found", errorDetail: commandName };
1771
1774
  }
1772
1775
  // Windows cmd.exe pattern: exit 1 + "is not recognized" in stderr.
1773
- if (e?.signal !== "SIGTERM" && /is not recognized as an internal or external command|command not found/i.test(stderrText)) {
1776
+ if (
1777
+ e?.signal !== "SIGTERM" &&
1778
+ /is not recognized as an internal or external command|command not found/i.test(stderrText)
1779
+ ) {
1774
1780
  return { ok: false, stdout: "", errorKind: "not-found", errorDetail: commandName };
1775
1781
  }
1776
1782
  if (e?.signal === "SIGTERM") {
1777
- return { ok: false, stdout: "", errorKind: "timeout", errorDetail: `exceeded ${timeoutMs}ms timeout` };
1783
+ return {
1784
+ ok: false,
1785
+ stdout: "",
1786
+ errorKind: "timeout",
1787
+ errorDetail: `exceeded ${timeoutMs}ms timeout`,
1788
+ };
1778
1789
  }
1779
1790
  if (typeof e?.status === "number") {
1780
1791
  return { ok: false, stdout: "", errorKind: "exit-code", errorDetail: `exit ${e.status}` };
@@ -1782,7 +1793,12 @@ export function execCheck(command: string, cwd?: string, timeoutMs = 10_000): Ex
1782
1793
  if (e?.signal) {
1783
1794
  return { ok: false, stdout: "", errorKind: "signal", errorDetail: String(e.signal) };
1784
1795
  }
1785
- return { ok: false, stdout: "", errorKind: "unknown", errorDetail: e?.message ?? "unknown error" };
1796
+ return {
1797
+ ok: false,
1798
+ stdout: "",
1799
+ errorKind: "unknown",
1800
+ errorDetail: e?.message ?? "unknown error",
1801
+ };
1786
1802
  }
1787
1803
  }
1788
1804
 
@@ -1853,9 +1869,7 @@ export function runPreflight(config: OrchestratorConfig, repoRoot?: string): Pre
1853
1869
  checks.push({
1854
1870
  name: "git-worktree",
1855
1871
  status: worktreeResult.ok ? "pass" : "fail",
1856
- message: worktreeResult.ok
1857
- ? "Worktree support available"
1858
- : "Git worktree not available",
1872
+ message: worktreeResult.ok ? "Worktree support available" : "Git worktree not available",
1859
1873
  hint: worktreeResult.ok
1860
1874
  ? undefined
1861
1875
  : repoRoot
@@ -1905,11 +1919,13 @@ export function runPreflight(config: OrchestratorConfig, repoRoot?: string): Pre
1905
1919
  // in v0.74.0. Recommend the new scope for new installs; the legacy
1906
1920
  // scope still resolves at runtime via Pi's bundled aliasing if a
1907
1921
  // transitional install has it.
1908
- hint = "Install Pi: npm install -g @earendil-works/pi-coding-agent (legacy: @mariozechner/pi-coding-agent)";
1922
+ hint =
1923
+ "Install Pi: npm install -g @earendil-works/pi-coding-agent (legacy: @mariozechner/pi-coding-agent)";
1909
1924
  break;
1910
1925
  case "timeout":
1911
1926
  message = `Pi did not respond within ${PI_PREFLIGHT_TIMEOUT_MS / 1000}s (retried once)`;
1912
- hint = "Pi appears installed but is responding slowly. Common causes: antivirus scanning the Node binary on first launch, slow disk, a zombie pi process holding a lock, or a stale mise shim. Try running `pi --version` directly to see how long it takes.";
1927
+ hint =
1928
+ "Pi appears installed but is responding slowly. Common causes: antivirus scanning the Node binary on first launch, slow disk, a zombie pi process holding a lock, or a stale mise shim. Try running `pi --version` directly to see how long it takes.";
1913
1929
  break;
1914
1930
  case "exit-code":
1915
1931
  message = `Pi exited with error (${piResult.errorDetail ?? "non-zero status"})`;
@@ -1917,7 +1933,8 @@ export function runPreflight(config: OrchestratorConfig, repoRoot?: string): Pre
1917
1933
  break;
1918
1934
  case "signal":
1919
1935
  message = `Pi was killed by signal (${piResult.errorDetail ?? "unknown"})`;
1920
- hint = "The pi process was killed externally. Check for OOM, antivirus quarantine, or interrupted shell.";
1936
+ hint =
1937
+ "The pi process was killed externally. Check for OOM, antivirus quarantine, or interrupted shell.";
1921
1938
  break;
1922
1939
  default:
1923
1940
  message = `Pi check failed (${piResult.errorDetail ?? "unknown error"})`;
@@ -1939,10 +1956,7 @@ export function formatPreflightResults(result: PreflightResult): string {
1939
1956
  const lines: string[] = ["Preflight Check:"];
1940
1957
 
1941
1958
  for (const check of result.checks) {
1942
- const icon =
1943
- check.status === "pass" ? "✅" :
1944
- check.status === "warn" ? "⚠️ " :
1945
- "❌";
1959
+ const icon = check.status === "pass" ? "✅" : check.status === "warn" ? "⚠️ " : "❌";
1946
1960
  const nameCol = check.name.padEnd(18);
1947
1961
  lines.push(` ${icon} ${nameCol} ${check.message}`);
1948
1962
  if (check.hint && check.status !== "pass") {
@@ -1968,7 +1982,6 @@ export function formatPreflightResults(result: PreflightResult): string {
1968
1982
  return lines.join("\n");
1969
1983
  }
1970
1984
 
1971
-
1972
1985
  // ── Worktree Reset with Safety ───────────────────────────────────────
1973
1986
 
1974
1987
  /**
@@ -2014,9 +2027,14 @@ export function safeResetWorktree(
2014
2027
  // of failing on the exit code.
2015
2028
  const cleanResult = runGit(["clean", "-fd"], worktree.path);
2016
2029
  if (!cleanResult.ok) {
2017
- execLog("reset", `lane-${worktree.laneNumber}`, "git clean -fd returned non-zero (may be partial)", {
2018
- stderr: cleanResult.stderr.slice(0, 200),
2019
- });
2030
+ execLog(
2031
+ "reset",
2032
+ `lane-${worktree.laneNumber}`,
2033
+ "git clean -fd returned non-zero (may be partial)",
2034
+ {
2035
+ stderr: cleanResult.stderr.slice(0, 200),
2036
+ },
2037
+ );
2020
2038
  }
2021
2039
 
2022
2040
  // Check if the worktree is clean enough to proceed.
@@ -2025,8 +2043,8 @@ export function safeResetWorktree(
2025
2043
  const statusCheck = runGit(["status", "--porcelain"], worktree.path);
2026
2044
  if (statusCheck.ok && statusCheck.stdout.length > 0) {
2027
2045
  // Still dirty after cleaning — check if only untracked files remain
2028
- const lines = statusCheck.stdout.split("\n").filter(l => l.trim());
2029
- const onlyUntracked = lines.every(l => l.startsWith("??"));
2046
+ const lines = statusCheck.stdout.split("\n").filter((l) => l.trim());
2047
+ const onlyUntracked = lines.every((l) => l.startsWith("??"));
2030
2048
  if (!onlyUntracked) {
2031
2049
  return {
2032
2050
  success: false,
@@ -2034,9 +2052,14 @@ export function safeResetWorktree(
2034
2052
  };
2035
2053
  }
2036
2054
  // Only untracked files remain (e.g., undeletable "nul") — safe to proceed
2037
- execLog("reset", `lane-${worktree.laneNumber}`, "untracked files remain after clean (non-blocking)", {
2038
- files: lines.map(l => l.slice(3)).join(", "),
2039
- });
2055
+ execLog(
2056
+ "reset",
2057
+ `lane-${worktree.laneNumber}`,
2058
+ "untracked files remain after clean (non-blocking)",
2059
+ {
2060
+ files: lines.map((l) => l.slice(3)).join(", "),
2061
+ },
2062
+ );
2040
2063
  }
2041
2064
 
2042
2065
  // Retry reset after cleaning
@@ -2058,7 +2081,6 @@ export function safeResetWorktree(
2058
2081
  }
2059
2082
  }
2060
2083
 
2061
-
2062
2084
  // ── Force Cleanup ────────────────────────────────────────────────────
2063
2085
 
2064
2086
  /**
@@ -2093,11 +2115,15 @@ export function forceCleanupWorktree(
2093
2115
  // special handling. Try rmSync first, then fall back to OS-specific
2094
2116
  // removal for stubborn files.
2095
2117
  rmSync(worktreePath, { recursive: true, force: true });
2096
- execLog("cleanup", `lane-${laneNumber}`, `force-removed worktree directory`, { path: worktreePath });
2118
+ execLog("cleanup", `lane-${laneNumber}`, `force-removed worktree directory`, {
2119
+ path: worktreePath,
2120
+ });
2097
2121
  } catch (rmErr: unknown) {
2098
2122
  // If Node's rmSync fails (e.g., Windows reserved names), try platform-specific
2099
2123
  const rmMsg = rmErr instanceof Error ? rmErr.message : String(rmErr);
2100
- execLog("cleanup", `lane-${laneNumber}`, `rmSync failed, trying OS-level removal`, { error: rmMsg });
2124
+ execLog("cleanup", `lane-${laneNumber}`, `rmSync failed, trying OS-level removal`, {
2125
+ error: rmMsg,
2126
+ });
2101
2127
 
2102
2128
  try {
2103
2129
  if (process.platform === "win32") {
@@ -2109,10 +2135,15 @@ export function forceCleanupWorktree(
2109
2135
  execLog("cleanup", `lane-${laneNumber}`, `OS-level removal succeeded`, { path: worktreePath });
2110
2136
  } catch (osErr: unknown) {
2111
2137
  const osMsg = osErr instanceof Error ? osErr.message : String(osErr);
2112
- execLog("cleanup", `lane-${laneNumber}`, `OS-level removal also failed — manual cleanup needed`, {
2113
- path: worktreePath,
2114
- error: osMsg,
2115
- });
2138
+ execLog(
2139
+ "cleanup",
2140
+ `lane-${laneNumber}`,
2141
+ `OS-level removal also failed — manual cleanup needed`,
2142
+ {
2143
+ path: worktreePath,
2144
+ error: osMsg,
2145
+ },
2146
+ );
2116
2147
  }
2117
2148
  }
2118
2149
  }
@@ -2145,12 +2176,13 @@ export function forceCleanupWorktree(
2145
2176
  if (containerName.includes("-")) {
2146
2177
  const containerRemoved = removeBatchContainerIfEmpty(containerDir);
2147
2178
  if (containerRemoved) {
2148
- execLog("cleanup", `lane-${laneNumber}`, `removed empty batch container`, { path: containerDir });
2179
+ execLog("cleanup", `lane-${laneNumber}`, `removed empty batch container`, {
2180
+ path: containerDir,
2181
+ });
2149
2182
  }
2150
2183
  }
2151
2184
  }
2152
2185
 
2153
-
2154
2186
  // ── Partial Progress Preservation ────────────────────────────────────
2155
2187
 
2156
2188
  /**
@@ -2225,10 +2257,7 @@ export function savePartialProgress(
2225
2257
  repoId?: string,
2226
2258
  ): SavePartialProgressResult {
2227
2259
  // Check if lane branch exists
2228
- const branchCheck = runGit(
2229
- ["rev-parse", "--verify", `refs/heads/${laneBranch}`],
2230
- repoRoot,
2231
- );
2260
+ const branchCheck = runGit(["rev-parse", "--verify", `refs/heads/${laneBranch}`], repoRoot);
2232
2261
  if (!branchCheck.ok) {
2233
2262
  return { saved: false, commitCount: 0, taskId, error: `Lane branch "${laneBranch}" not found` };
2234
2263
  }
@@ -2254,10 +2283,7 @@ export function savePartialProgress(
2254
2283
  const savedName = computePartialProgressBranchName(opId, taskId, batchId, repoId);
2255
2284
 
2256
2285
  // Check for collision (idempotent re-runs, retries)
2257
- const existingCheck = runGit(
2258
- ["rev-parse", "--verify", `refs/heads/${savedName}`],
2259
- repoRoot,
2260
- );
2286
+ const existingCheck = runGit(["rev-parse", "--verify", `refs/heads/${savedName}`], repoRoot);
2261
2287
  const existingSHA = existingCheck.ok ? existingCheck.stdout.trim() : "";
2262
2288
 
2263
2289
  const resolution = resolveSavedBranchCollision(savedName, existingSHA, branchSHA);
@@ -2274,10 +2300,7 @@ export function savePartialProgress(
2274
2300
 
2275
2301
  case "create":
2276
2302
  case "create-suffixed": {
2277
- const createResult = runGit(
2278
- ["branch", resolution.savedName, branchSHA],
2279
- repoRoot,
2280
- );
2303
+ const createResult = runGit(["branch", resolution.savedName, branchSHA], repoRoot);
2281
2304
  if (!createResult.ok) {
2282
2305
  return {
2283
2306
  saved: false,
@@ -2386,9 +2409,7 @@ export function preserveFailedLaneProgress(
2386
2409
  }
2387
2410
 
2388
2411
  // Find failed/stalled tasks
2389
- const failedTasks = taskOutcomes.filter(
2390
- (to) => to.status === "failed" || to.status === "stalled",
2391
- );
2412
+ const failedTasks = taskOutcomes.filter((to) => to.status === "failed" || to.status === "stalled");
2392
2413
 
2393
2414
  // Track which lane branches we've already processed (a lane may have
2394
2415
  // multiple tasks; only save once per branch since all commits are shared)
@@ -2432,7 +2453,9 @@ export function preserveFailedLaneProgress(
2432
2453
  // Track the saved branch name for caller visibility
2433
2454
  preservedBranches.add(result.savedBranch!);
2434
2455
 
2435
- execLog("partial-progress", failedTask.taskId,
2456
+ execLog(
2457
+ "partial-progress",
2458
+ failedTask.taskId,
2436
2459
  `Task ${failedTask.taskId} failed but has ${result.commitCount} commit(s) of partial progress on branch ${result.savedBranch}`,
2437
2460
  {
2438
2461
  laneBranch: laneInfo.branch,
@@ -2447,9 +2470,11 @@ export function preserveFailedLaneProgress(
2447
2470
  // irreversibly lose the partial work.
2448
2471
  unsafeBranches.add(laneInfo.branch);
2449
2472
 
2450
- execLog("partial-progress", failedTask.taskId,
2473
+ execLog(
2474
+ "partial-progress",
2475
+ failedTask.taskId,
2451
2476
  `WARNING: Failed to preserve partial progress for task ${failedTask.taskId} ` +
2452
- `(${result.commitCount} commit(s) at risk on branch "${laneInfo.branch}")`,
2477
+ `(${result.commitCount} commit(s) at risk on branch "${laneInfo.branch}")`,
2453
2478
  {
2454
2479
  laneBranch: laneInfo.branch,
2455
2480
  commitCount: result.commitCount,
@@ -2463,7 +2488,6 @@ export function preserveFailedLaneProgress(
2463
2488
  return { results, preservedBranches, unsafeBranches };
2464
2489
  }
2465
2490
 
2466
-
2467
2491
  /**
2468
2492
  * TP-147: Preserve partial progress for all skipped tasks before cleanup/reset.
2469
2493
  *
@@ -2505,9 +2529,7 @@ export function preserveSkippedLaneProgress(
2505
2529
  }
2506
2530
 
2507
2531
  // Find skipped tasks
2508
- const skippedTasks = taskOutcomes.filter(
2509
- (to) => to.status === "skipped",
2510
- );
2532
+ const skippedTasks = taskOutcomes.filter((to) => to.status === "skipped");
2511
2533
 
2512
2534
  // Track which lane branches we've already processed (a lane may have
2513
2535
  // multiple tasks; only save once per branch since all commits are shared)
@@ -2549,7 +2571,9 @@ export function preserveSkippedLaneProgress(
2549
2571
  if (result.saved) {
2550
2572
  preservedBranches.add(result.savedBranch!);
2551
2573
 
2552
- execLog("partial-progress", skippedTask.taskId,
2574
+ execLog(
2575
+ "partial-progress",
2576
+ skippedTask.taskId,
2553
2577
  `Task ${skippedTask.taskId} was skipped but has ${result.commitCount} commit(s) of partial progress preserved on branch ${result.savedBranch}`,
2554
2578
  {
2555
2579
  laneBranch: laneInfo.branch,
@@ -2561,9 +2585,11 @@ export function preserveSkippedLaneProgress(
2561
2585
  } else if (result.commitCount > 0 || result.error) {
2562
2586
  unsafeBranches.add(laneInfo.branch);
2563
2587
 
2564
- execLog("partial-progress", skippedTask.taskId,
2588
+ execLog(
2589
+ "partial-progress",
2590
+ skippedTask.taskId,
2565
2591
  `WARNING: Failed to preserve partial progress for skipped task ${skippedTask.taskId} ` +
2566
- `(${result.commitCount} commit(s) at risk on branch "${laneInfo.branch}")`,
2592
+ `(${result.commitCount} commit(s) at risk on branch "${laneInfo.branch}")`,
2567
2593
  {
2568
2594
  laneBranch: laneInfo.branch,
2569
2595
  commitCount: result.commitCount,
@@ -2577,7 +2603,6 @@ export function preserveSkippedLaneProgress(
2577
2603
  return { results, preservedBranches, unsafeBranches };
2578
2604
  }
2579
2605
 
2580
-
2581
2606
  // ── Stale Branch Cleanup (TP-051) ────────────────────────────────────
2582
2607
 
2583
2608
  /**
@@ -2630,7 +2655,7 @@ export function deleteStaleBranches(
2630
2655
  if (taskBranchResult.ok && taskBranchResult.stdout.trim()) {
2631
2656
  const branches = taskBranchResult.stdout
2632
2657
  .split("\n")
2633
- .map(b => b.replace(/^\*?\s+/, "").trim())
2658
+ .map((b) => b.replace(/^\*?\s+/, "").trim())
2634
2659
  .filter(Boolean);
2635
2660
 
2636
2661
  for (const branch of branches) {
@@ -2648,7 +2673,7 @@ export function deleteStaleBranches(
2648
2673
  if (savedTaskResult.ok && savedTaskResult.stdout.trim()) {
2649
2674
  const branches = savedTaskResult.stdout
2650
2675
  .split("\n")
2651
- .map(b => b.replace(/^\*?\s+/, "").trim())
2676
+ .map((b) => b.replace(/^\*?\s+/, "").trim())
2652
2677
  .filter(Boolean);
2653
2678
 
2654
2679
  for (const branch of branches) {
@@ -2669,7 +2694,7 @@ export function deleteStaleBranches(
2669
2694
  if (savedProgressResult.ok && savedProgressResult.stdout.trim()) {
2670
2695
  const branches = savedProgressResult.stdout
2671
2696
  .split("\n")
2672
- .map(b => b.replace(/^\*?\s+/, "").trim())
2697
+ .map((b) => b.replace(/^\*?\s+/, "").trim())
2673
2698
  .filter(Boolean);
2674
2699
 
2675
2700
  const batchSuffix = `-${batchId}`;
@@ -2698,6 +2723,3 @@ export function deleteStaleBranches(
2698
2723
 
2699
2724
  return { deletedTaskBranches, deletedSavedBranches, failedDeletes };
2700
2725
  }
2701
-
2702
-
2703
-