taskplane 0.9.1 ā 0.9.2
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.
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
computeWaveAssignments,
|
|
16
16
|
createOrchWidget,
|
|
17
17
|
deleteBatchState,
|
|
18
|
+
deleteStaleBranches,
|
|
18
19
|
detectOrphanSessions,
|
|
19
20
|
executeLane,
|
|
20
21
|
executeOrchBatch,
|
|
@@ -407,6 +408,10 @@ export function executeIntegration(
|
|
|
407
408
|
}
|
|
408
409
|
|
|
409
410
|
if (!result.ok) {
|
|
411
|
+
// TP-052: Include branch protection hint when merge fails
|
|
412
|
+
const protectionHint = result.stderr.includes("protected") || result.stderr.includes("permission")
|
|
413
|
+
? `\n\n š” If the branch is protected, use --pr to create a pull request.`
|
|
414
|
+
: "";
|
|
410
415
|
return {
|
|
411
416
|
success: false,
|
|
412
417
|
integratedLocally: false,
|
|
@@ -417,7 +422,8 @@ export function executeIntegration(
|
|
|
417
422
|
`${result.stderr}\n\n` +
|
|
418
423
|
`Try:\n` +
|
|
419
424
|
` /orch-integrate --merge Create a merge commit\n` +
|
|
420
|
-
` /orch-integrate --pr Create a pull request instead
|
|
425
|
+
` /orch-integrate --pr Create a pull request instead` +
|
|
426
|
+
protectionHint,
|
|
421
427
|
};
|
|
422
428
|
}
|
|
423
429
|
// Count commits that were applied
|
|
@@ -451,6 +457,10 @@ export function executeIntegration(
|
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
if (!result.ok) {
|
|
460
|
+
// TP-052: Include branch protection hint when merge fails
|
|
461
|
+
const mergeProtectionHint = result.stderr.includes("protected") || result.stderr.includes("permission")
|
|
462
|
+
? `\n\n š” If the branch is protected, use --pr to create a pull request.`
|
|
463
|
+
: "";
|
|
454
464
|
return {
|
|
455
465
|
success: false,
|
|
456
466
|
integratedLocally: false,
|
|
@@ -460,7 +470,8 @@ export function executeIntegration(
|
|
|
460
470
|
`ā Merge failed ā there may be conflicts.\n` +
|
|
461
471
|
`${result.stderr}\n\n` +
|
|
462
472
|
`Resolve conflicts manually, or try:\n` +
|
|
463
|
-
` /orch-integrate --pr Create a pull request instead
|
|
473
|
+
` /orch-integrate --pr Create a pull request instead` +
|
|
474
|
+
mergeProtectionHint,
|
|
464
475
|
};
|
|
465
476
|
}
|
|
466
477
|
return performCleanup(deps, orchBranch, {
|
|
@@ -644,7 +655,7 @@ export function collectRepoCleanupFindings(
|
|
|
644
655
|
findings.staleWorktrees = wts.map(wt => wt.path);
|
|
645
656
|
} catch { /* best effort ā git worktree list may fail in unusual states */ }
|
|
646
657
|
|
|
647
|
-
// 2. Lane branches ā task/{opId}-lane-*
|
|
658
|
+
// 2. Lane branches ā task/{opId}-lane-* and saved/task/{opId}-lane-*
|
|
648
659
|
try {
|
|
649
660
|
const branchResult = runGit(["branch", "--list", `task/${opId}-lane-*`], repoRoot);
|
|
650
661
|
if (branchResult.ok && branchResult.stdout.trim()) {
|
|
@@ -653,6 +664,15 @@ export function collectRepoCleanupFindings(
|
|
|
653
664
|
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
654
665
|
.filter(Boolean);
|
|
655
666
|
}
|
|
667
|
+
// Also detect saved lane branches (preserved refs from worktree removal)
|
|
668
|
+
const savedBranchResult = runGit(["branch", "--list", `saved/task/${opId}-lane-*`], repoRoot);
|
|
669
|
+
if (savedBranchResult.ok && savedBranchResult.stdout.trim()) {
|
|
670
|
+
const savedBranches = savedBranchResult.stdout
|
|
671
|
+
.split("\n")
|
|
672
|
+
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
673
|
+
.filter(Boolean);
|
|
674
|
+
findings.staleLaneBranches.push(...savedBranches);
|
|
675
|
+
}
|
|
656
676
|
} catch { /* best effort */ }
|
|
657
677
|
|
|
658
678
|
// 3. Orch branch ā check if the specific orch branch still exists
|
|
@@ -899,7 +919,7 @@ export function startBatchAsync(
|
|
|
899
919
|
*
|
|
900
920
|
* @since TP-043 R002
|
|
901
921
|
*/
|
|
902
|
-
export function buildIntegrationExecutor(repoRoot: string): IntegrationExecutor {
|
|
922
|
+
export function buildIntegrationExecutor(repoRoot: string, opId?: string): IntegrationExecutor {
|
|
903
923
|
return (mode, context) => {
|
|
904
924
|
// Ensure we're on the base branch before integrating
|
|
905
925
|
const currentBranch = getCurrentBranch(repoRoot);
|
|
@@ -942,10 +962,22 @@ export function buildIntegrationExecutor(repoRoot: string): IntegrationExecutor
|
|
|
942
962
|
},
|
|
943
963
|
};
|
|
944
964
|
|
|
945
|
-
|
|
965
|
+
const result = executeIntegration(mode as IntegrateMode, {
|
|
946
966
|
...context,
|
|
947
967
|
currentBranch: context.baseBranch,
|
|
948
968
|
}, deps);
|
|
969
|
+
|
|
970
|
+
// TP-051: Clean up stale task/* and saved/* branches after successful integration.
|
|
971
|
+
// This ensures auto-mode integration (supervisor path) gets the same cleanup
|
|
972
|
+
// as the manual /orch-integrate handler.
|
|
973
|
+
if (result.success && result.integratedLocally && context.batchId && opId) {
|
|
974
|
+
try {
|
|
975
|
+
deleteStaleBranches(repoRoot, opId, context.batchId);
|
|
976
|
+
dropBatchAutostash(repoRoot, context.batchId);
|
|
977
|
+
} catch { /* best effort ā don't fail integration for cleanup errors */ }
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return result;
|
|
949
981
|
};
|
|
950
982
|
}
|
|
951
983
|
|
|
@@ -1512,7 +1544,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1512
1544
|
orchBatchState,
|
|
1513
1545
|
mode,
|
|
1514
1546
|
repoRoot,
|
|
1515
|
-
buildIntegrationExecutor(repoRoot),
|
|
1547
|
+
buildIntegrationExecutor(repoRoot, opId),
|
|
1516
1548
|
buildCiDeps(repoRoot),
|
|
1517
1549
|
sDeps,
|
|
1518
1550
|
);
|
|
@@ -1858,7 +1890,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1858
1890
|
orchBatchState,
|
|
1859
1891
|
mode,
|
|
1860
1892
|
execCtx!.repoRoot,
|
|
1861
|
-
buildIntegrationExecutor(execCtx!.repoRoot),
|
|
1893
|
+
buildIntegrationExecutor(execCtx!.repoRoot, opId),
|
|
1862
1894
|
buildCiDeps(execCtx!.repoRoot),
|
|
1863
1895
|
sDeps,
|
|
1864
1896
|
);
|
|
@@ -1893,12 +1925,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1893
1925
|
`Batch **${orchBatchState.batchId}** completed ā ` +
|
|
1894
1926
|
`${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
|
|
1895
1927
|
`The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
|
|
1896
|
-
`Would you like me to integrate it, or would you prefer to review first
|
|
1928
|
+
`Would you like me to integrate it, or would you prefer to review first?\n\n` +
|
|
1929
|
+
`You can also:\n` +
|
|
1930
|
+
`⢠Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
|
|
1931
|
+
`⢠Create new tasks for the next batch\n` +
|
|
1932
|
+
`⢠Run a health check`,
|
|
1897
1933
|
}
|
|
1898
1934
|
: {
|
|
1899
1935
|
routingState: "no-tasks",
|
|
1900
1936
|
contextMessage:
|
|
1901
1937
|
`Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
|
|
1938
|
+
`${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
|
|
1939
|
+
`${orchBatchState.skippedTasks} skipped.\n\n` +
|
|
1902
1940
|
`What would you like to do next?`,
|
|
1903
1941
|
};
|
|
1904
1942
|
transitionToRoutingMode(pi, supervisorState, postBatchContext);
|
|
@@ -2352,7 +2390,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
2352
2390
|
ctx.ui.notify(notice, "info");
|
|
2353
2391
|
}
|
|
2354
2392
|
|
|
2355
|
-
// āā Step
|
|
2393
|
+
// āā Step 2a: Branch protection pre-check (TP-052) āāāāāāā
|
|
2394
|
+
// When using ff or merge mode (direct push), check if the target
|
|
2395
|
+
// branch has protection rules. If protected, warn and suggest --pr.
|
|
2396
|
+
// Graceful degradation: if gh is unavailable, skip the check.
|
|
2397
|
+
if (parsed.mode !== "pr") {
|
|
2398
|
+
const { detectBranchProtection } = await import("./supervisor.ts");
|
|
2399
|
+
const protectionStatus = detectBranchProtection(baseBranch, repoRoot);
|
|
2400
|
+
if (protectionStatus === "protected") {
|
|
2401
|
+
ctx.ui.notify(
|
|
2402
|
+
`ā ļø Branch \`${baseBranch}\` has branch protection rules enabled.\n` +
|
|
2403
|
+
`Direct merges may be blocked by your repository settings.\n\n` +
|
|
2404
|
+
`Recommended: use \`/orch-integrate --pr\` to create a pull request instead.`,
|
|
2405
|
+
"warning",
|
|
2406
|
+
);
|
|
2407
|
+
// Don't block ā proceed with the attempt. The merge will fail
|
|
2408
|
+
// gracefully and show a clear error if protection blocks it.
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// āā Step 2b: Pre-integration summary āāāāāāāāāāāāāāāāāāāāā
|
|
2356
2413
|
// Count commits ahead
|
|
2357
2414
|
const revListResult = runGit(
|
|
2358
2415
|
["rev-list", "--count", `${currentBranch}..${orchBranch}`],
|
|
@@ -2469,6 +2526,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
2469
2526
|
dropBatchAutostash(repo.root, batchId);
|
|
2470
2527
|
}
|
|
2471
2528
|
|
|
2529
|
+
// TP-051: Delete stale task/* and saved/task/* branches from all repos.
|
|
2530
|
+
// These accumulate after each batch and clutter `git branch` output.
|
|
2531
|
+
// Deletes both current-batch branches and orphans from previous batches.
|
|
2532
|
+
const branchCleanupLines: string[] = [];
|
|
2533
|
+
for (const repo of allRepos) {
|
|
2534
|
+
const branchCleanup = deleteStaleBranches(repo.root, opId, batchId);
|
|
2535
|
+
const totalDeleted = branchCleanup.deletedTaskBranches.length + branchCleanup.deletedSavedBranches.length;
|
|
2536
|
+
if (totalDeleted > 0 || branchCleanup.failedDeletes.length > 0) {
|
|
2537
|
+
const label = repo.id === "(default)" ? "" : ` (${repo.id})`;
|
|
2538
|
+
if (branchCleanup.deletedTaskBranches.length > 0) {
|
|
2539
|
+
branchCleanupLines.push(` šļø Deleted ${branchCleanup.deletedTaskBranches.length} task branch(es)${label}`);
|
|
2540
|
+
}
|
|
2541
|
+
if (branchCleanup.deletedSavedBranches.length > 0) {
|
|
2542
|
+
branchCleanupLines.push(` šļø Deleted ${branchCleanup.deletedSavedBranches.length} saved branch(es)${label}`);
|
|
2543
|
+
}
|
|
2544
|
+
if (branchCleanup.failedDeletes.length > 0) {
|
|
2545
|
+
branchCleanupLines.push(` ā ļø Failed to delete ${branchCleanup.failedDeletes.length} branch(es)${label}: ${branchCleanup.failedDeletes.join(", ")}`);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
if (branchCleanupLines.length > 0) {
|
|
2550
|
+
ctx.ui.notify("Branch cleanup:\n" + branchCleanupLines.join("\n"), "info");
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2472
2553
|
// Run acceptance checks across all workspace repos.
|
|
2473
2554
|
// In PR mode, the orch branch is intentionally preserved for the PR,
|
|
2474
2555
|
// so we skip orch branch detection to avoid contradictory output.
|
|
@@ -51,15 +51,18 @@ export const ORCH_MESSAGES = {
|
|
|
51
51
|
}
|
|
52
52
|
if (orchBranch && succeeded > 0) {
|
|
53
53
|
lines.push("");
|
|
54
|
-
lines.push(
|
|
55
|
-
lines.push(` Your
|
|
54
|
+
lines.push(" āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
55
|
+
lines.push(` ā Your changes are on branch: ${orchBranch}`);
|
|
56
|
+
lines.push(` ā Your ${baseBranch || "working"} branch was not modified.`);
|
|
56
57
|
if (baseBranch) {
|
|
57
|
-
lines.push(` Preview: git log ${baseBranch}..${orchBranch}`);
|
|
58
|
+
lines.push(` ā Preview: git log ${baseBranch}..${orchBranch}`);
|
|
58
59
|
}
|
|
59
|
-
lines.push("");
|
|
60
|
-
lines.push(" To
|
|
61
|
-
lines.push("
|
|
62
|
-
lines.push("
|
|
60
|
+
lines.push(" ā");
|
|
61
|
+
lines.push(" ā š To bring changes into your working branch:");
|
|
62
|
+
lines.push(" ā");
|
|
63
|
+
lines.push(" ā /orch-integrate ā merge directly (recommended)");
|
|
64
|
+
lines.push(" ā /orch-integrate --pr ā create a pull request");
|
|
65
|
+
lines.push(" āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
63
66
|
}
|
|
64
67
|
return lines.join("\n");
|
|
65
68
|
},
|
|
@@ -219,10 +219,14 @@ export function syncTaskOutcomesFromMonitor(
|
|
|
219
219
|
const mappedStatus = monitorToLane[snap.status];
|
|
220
220
|
const terminal = mappedStatus === "succeeded" || mappedStatus === "failed" || mappedStatus === "stalled" || mappedStatus === "skipped";
|
|
221
221
|
|
|
222
|
+
// TP-051: Use snap.observedAt (Date.now() from monitor poll) instead of
|
|
223
|
+
// snap.lastHeartbeat (STATUS.md mtime) for task start time. The mtime
|
|
224
|
+
// reflects when STATUS.md was last edited, which may be long before
|
|
225
|
+
// actual execution started (e.g., during task staging).
|
|
222
226
|
changed = upsertTaskOutcome(outcomes, {
|
|
223
227
|
taskId: lane.currentTaskId,
|
|
224
228
|
status: mappedStatus,
|
|
225
|
-
startTime: existing?.startTime ?? snap.
|
|
229
|
+
startTime: existing?.startTime ?? snap.observedAt,
|
|
226
230
|
endTime: terminal ? (existing?.endTime ?? snap.observedAt) : null,
|
|
227
231
|
exitReason: existing?.exitReason || (mappedStatus === "running" ? "Task in progress" : (snap.stallReason || "Task reached terminal state")),
|
|
228
232
|
sessionName: existing?.sessionName || lane.sessionName,
|
|
@@ -2615,15 +2615,19 @@ export async function transitionToRoutingMode(
|
|
|
2615
2615
|
// Keep batchStateRef/orchConfigRef/stateRoot ā routing prompt may need them
|
|
2616
2616
|
// Keep model override ā don't switch models mid-conversation
|
|
2617
2617
|
|
|
2618
|
-
//
|
|
2618
|
+
// TP-052: Send a prominent conversational message that clearly signals
|
|
2619
|
+
// the supervisor is ready for input. Uses triggerTurn to force an LLM
|
|
2620
|
+
// response, which ensures the pi TUI redraws and shows the input prompt.
|
|
2619
2621
|
pi.sendMessage(
|
|
2620
2622
|
{
|
|
2621
2623
|
customType: "supervisor-routing-transition",
|
|
2622
2624
|
content: [{
|
|
2623
2625
|
type: "text",
|
|
2624
2626
|
text:
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
+
`āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n` +
|
|
2628
|
+
`š **Ready for your input.**\n\n` +
|
|
2629
|
+
routingContext.contextMessage +
|
|
2630
|
+
`\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā`,
|
|
2627
2631
|
}],
|
|
2628
2632
|
display: `Supervisor ā ${routingContext.routingState}`,
|
|
2629
2633
|
},
|
|
@@ -2323,3 +2323,127 @@ export function preserveFailedLaneProgress(
|
|
|
2323
2323
|
return { results, preservedBranches, unsafeBranches };
|
|
2324
2324
|
}
|
|
2325
2325
|
|
|
2326
|
+
|
|
2327
|
+
// āā Stale Branch Cleanup (TP-051) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
2328
|
+
|
|
2329
|
+
/**
|
|
2330
|
+
* Result of stale branch cleanup after integration.
|
|
2331
|
+
*/
|
|
2332
|
+
export interface StaleBranchCleanupResult {
|
|
2333
|
+
/** task/* branches deleted */
|
|
2334
|
+
deletedTaskBranches: string[];
|
|
2335
|
+
/** saved/task/* branches deleted */
|
|
2336
|
+
deletedSavedBranches: string[];
|
|
2337
|
+
/** Branches that failed to delete (best-effort) */
|
|
2338
|
+
failedDeletes: string[];
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Delete stale task/* and saved/* branches after integration.
|
|
2343
|
+
*
|
|
2344
|
+
* After `/orch-integrate` merges or creates a PR, the lane branches
|
|
2345
|
+
* (`task/{opId}-lane-{N}-{batchId}`) and their saved counterparts
|
|
2346
|
+
* are no longer needed. This function cleans them up.
|
|
2347
|
+
*
|
|
2348
|
+
* Cleanup scope:
|
|
2349
|
+
* 1. **Lane branches:** `task/{opId}-lane-*` (any batch from this operator)
|
|
2350
|
+
* 2. **Saved lane branches:** `saved/task/{opId}-lane-*` (preserved lane refs)
|
|
2351
|
+
* 3. **Partial-progress branches:** `saved/{opId}-*` (per-task partial progress refs)
|
|
2352
|
+
*
|
|
2353
|
+
* Targets all branches matching the operator's prefix, not just the current
|
|
2354
|
+
* batch ā this also cleans up orphans from previous batches that were never
|
|
2355
|
+
* cleaned.
|
|
2356
|
+
*
|
|
2357
|
+
* All deletions are best-effort ā individual failures are logged but don't
|
|
2358
|
+
* prevent other branches from being cleaned.
|
|
2359
|
+
*
|
|
2360
|
+
* @param repoRoot - Repository root directory
|
|
2361
|
+
* @param opId - Operator identifier (e.g., "henrylach")
|
|
2362
|
+
* @param batchId - Current batch ID (for logging context)
|
|
2363
|
+
* @returns Cleanup result with lists of deleted and failed branches
|
|
2364
|
+
*/
|
|
2365
|
+
export function deleteStaleBranches(
|
|
2366
|
+
repoRoot: string,
|
|
2367
|
+
opId: string,
|
|
2368
|
+
batchId: string,
|
|
2369
|
+
): StaleBranchCleanupResult {
|
|
2370
|
+
const deletedTaskBranches: string[] = [];
|
|
2371
|
+
const deletedSavedBranches: string[] = [];
|
|
2372
|
+
const failedDeletes: string[] = [];
|
|
2373
|
+
|
|
2374
|
+
// 1. Delete task/{opId}-lane-* branches
|
|
2375
|
+
const taskBranchResult = runGit(["branch", "--list", `task/${opId}-lane-*`], repoRoot);
|
|
2376
|
+
if (taskBranchResult.ok && taskBranchResult.stdout.trim()) {
|
|
2377
|
+
const branches = taskBranchResult.stdout
|
|
2378
|
+
.split("\n")
|
|
2379
|
+
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
2380
|
+
.filter(Boolean);
|
|
2381
|
+
|
|
2382
|
+
for (const branch of branches) {
|
|
2383
|
+
const deleted = deleteBranchBestEffort(branch, repoRoot);
|
|
2384
|
+
if (deleted) {
|
|
2385
|
+
deletedTaskBranches.push(branch);
|
|
2386
|
+
} else {
|
|
2387
|
+
failedDeletes.push(branch);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// 2. Delete saved/task/{opId}-lane-* branches (preserved lane refs)
|
|
2393
|
+
const savedTaskResult = runGit(["branch", "--list", `saved/task/${opId}-lane-*`], repoRoot);
|
|
2394
|
+
if (savedTaskResult.ok && savedTaskResult.stdout.trim()) {
|
|
2395
|
+
const branches = savedTaskResult.stdout
|
|
2396
|
+
.split("\n")
|
|
2397
|
+
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
2398
|
+
.filter(Boolean);
|
|
2399
|
+
|
|
2400
|
+
for (const branch of branches) {
|
|
2401
|
+
const deleted = deleteBranchBestEffort(branch, repoRoot);
|
|
2402
|
+
if (deleted) {
|
|
2403
|
+
deletedSavedBranches.push(branch);
|
|
2404
|
+
} else {
|
|
2405
|
+
failedDeletes.push(branch);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// 3. Delete saved/{opId}-*-{batchId} branches (partial-progress refs from this batch)
|
|
2411
|
+
// Pattern: saved/{opId}-{taskId}-{batchId} or saved/{opId}-{repoId}-{taskId}-{batchId}
|
|
2412
|
+
// Only deletes branches ending with the current batchId to avoid removing
|
|
2413
|
+
// partial-progress refs from other batches that the operator may still need.
|
|
2414
|
+
const savedProgressResult = runGit(["branch", "--list", `saved/${opId}-*`], repoRoot);
|
|
2415
|
+
if (savedProgressResult.ok && savedProgressResult.stdout.trim()) {
|
|
2416
|
+
const branches = savedProgressResult.stdout
|
|
2417
|
+
.split("\n")
|
|
2418
|
+
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
2419
|
+
.filter(Boolean);
|
|
2420
|
+
|
|
2421
|
+
const batchSuffix = `-${batchId}`;
|
|
2422
|
+
for (const branch of branches) {
|
|
2423
|
+
// Avoid double-deleting saved/task/* already handled above
|
|
2424
|
+
if (branch.startsWith("saved/task/")) continue;
|
|
2425
|
+
// Only delete partial-progress refs from the current batch
|
|
2426
|
+
if (!branch.endsWith(batchSuffix)) continue;
|
|
2427
|
+
const deleted = deleteBranchBestEffort(branch, repoRoot);
|
|
2428
|
+
if (deleted) {
|
|
2429
|
+
deletedSavedBranches.push(branch);
|
|
2430
|
+
} else {
|
|
2431
|
+
failedDeletes.push(branch);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
const totalDeleted = deletedTaskBranches.length + deletedSavedBranches.length;
|
|
2437
|
+
if (totalDeleted > 0) {
|
|
2438
|
+
execLog("cleanup", "branches", `deleted ${totalDeleted} stale branch(es) for batch ${batchId}`, {
|
|
2439
|
+
taskBranches: deletedTaskBranches.length,
|
|
2440
|
+
savedBranches: deletedSavedBranches.length,
|
|
2441
|
+
failed: failedDeletes.length,
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
return { deletedTaskBranches, deletedSavedBranches, failedDeletes };
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
|
|
2449
|
+
|