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.
- package/bin/gitignore-patterns.mjs +11 -8
- package/bin/rpc-wrapper.mjs +410 -357
- package/bin/taskplane.mjs +533 -250
- package/dashboard/public/app.js +124 -15
- package/dashboard/public/style.css +83 -2
- package/extensions/reviewer-extension.ts +17 -11
- package/extensions/taskplane/abort.ts +50 -18
- package/extensions/taskplane/agent-bridge-extension.ts +232 -105
- package/extensions/taskplane/agent-host.ts +224 -97
- package/extensions/taskplane/cleanup.ts +71 -42
- package/extensions/taskplane/config-loader.ts +142 -58
- package/extensions/taskplane/config-schema.ts +6 -13
- package/extensions/taskplane/config.ts +10 -2
- package/extensions/taskplane/diagnostic-reports.ts +59 -47
- package/extensions/taskplane/diagnostics.ts +13 -13
- package/extensions/taskplane/discovery.ts +78 -63
- package/extensions/taskplane/engine-worker.ts +53 -46
- package/extensions/taskplane/engine.ts +1760 -602
- package/extensions/taskplane/execution.ts +469 -207
- package/extensions/taskplane/extension.ts +1073 -598
- package/extensions/taskplane/formatting.ts +136 -124
- package/extensions/taskplane/git.ts +0 -2
- package/extensions/taskplane/lane-runner.ts +652 -319
- package/extensions/taskplane/mailbox.ts +57 -49
- package/extensions/taskplane/merge.ts +662 -383
- package/extensions/taskplane/messages.ts +109 -51
- package/extensions/taskplane/migrations.ts +1 -1
- package/extensions/taskplane/path-resolver.ts +8 -9
- package/extensions/taskplane/persistence.ts +425 -262
- package/extensions/taskplane/process-registry.ts +36 -7
- package/extensions/taskplane/quality-gate.ts +107 -55
- package/extensions/taskplane/resume.ts +832 -280
- package/extensions/taskplane/sessions.ts +1 -1
- package/extensions/taskplane/settings-tui.ts +505 -164
- package/extensions/taskplane/sidecar-telemetry.ts +25 -10
- package/extensions/taskplane/supervisor.ts +477 -270
- package/extensions/taskplane/task-executor-core.ts +178 -53
- package/extensions/taskplane/types.ts +209 -108
- package/extensions/taskplane/verification.ts +27 -22
- package/extensions/taskplane/waves.ts +59 -43
- package/extensions/taskplane/workspace.ts +14 -12
- package/extensions/taskplane/worktree.ts +218 -196
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
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
|
-
|
|
674
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
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
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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"
|
|
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 {
|
|
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(
|
|
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<{
|
|
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 {
|
|
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 {
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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 (
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
2018
|
-
|
|
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(
|
|
2038
|
-
|
|
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`, {
|
|
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`, {
|
|
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(
|
|
2113
|
-
|
|
2114
|
-
|
|
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`, {
|
|
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(
|
|
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(
|
|
2473
|
+
execLog(
|
|
2474
|
+
"partial-progress",
|
|
2475
|
+
failedTask.taskId,
|
|
2451
2476
|
`WARNING: Failed to preserve partial progress for task ${failedTask.taskId} ` +
|
|
2452
|
-
|
|
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(
|
|
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(
|
|
2588
|
+
execLog(
|
|
2589
|
+
"partial-progress",
|
|
2590
|
+
skippedTask.taskId,
|
|
2565
2591
|
`WARNING: Failed to preserve partial progress for skipped task ${skippedTask.taskId} ` +
|
|
2566
|
-
|
|
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
|
-
|