taskplane 0.9.0 → 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.
@@ -174,7 +174,7 @@ const DEFAULT_CONFIG: TaskConfig = {
174
174
  standards_overrides: {},
175
175
  task_areas: {},
176
176
  worker: { model: "", tools: "read,write,edit,bash,grep,find,ls", thinking: "off" },
177
- reviewer: { model: "openai/gpt-5.3-codex", tools: "read,bash,grep,find,ls", thinking: "on" },
177
+ reviewer: { model: "", tools: "read,bash,grep,find,ls", thinking: "on" },
178
178
  context: {
179
179
  worker_context_window: 0, warn_percent: 85, kill_percent: 95,
180
180
  max_worker_iterations: 20, max_review_cycles: 2, no_progress_limit: 3,
@@ -633,56 +633,6 @@ function resolveRpcWrapperPath(): string {
633
633
  );
634
634
  }
635
635
 
636
- /**
637
- * Resolve the path to this extension file (task-runner.ts).
638
- * Used to pass the extension to worker subprocesses so they have access
639
- * to the review_step tool in orchestrated mode.
640
- *
641
- * Resolution strategy:
642
- * 1. Derive from -e argument that loaded this extension
643
- * 2. Package root + extensions/task-runner.ts
644
- * 3. cwd/extensions/task-runner.ts (development fallback)
645
- *
646
- * Returns null if the extension path cannot be found (non-fatal — worker
647
- * runs without review_step tool).
648
- */
649
- function resolveExtensionPath(): string | null {
650
- const extRelPath = join("extensions", "task-runner.ts");
651
-
652
- // 1. Derive from the -e argument that loaded this file
653
- try {
654
- const args = process.argv;
655
- for (let i = 0; i < args.length - 1; i++) {
656
- if (args[i] === "-e" && args[i + 1]?.includes("task-runner")) {
657
- const extPath = resolve(args[i + 1]);
658
- if (existsSync(extPath)) return extPath;
659
- }
660
- }
661
- } catch { /* ignore argv parsing errors */ }
662
-
663
- // 2. Package root
664
- const root = findPackageRoot();
665
- if (root) {
666
- const p = join(root, extRelPath);
667
- if (existsSync(p)) return p;
668
- }
669
-
670
- // 3. Development fallback
671
- const devPath = join(process.cwd(), extRelPath);
672
- if (existsSync(devPath)) return devPath;
673
-
674
- return null;
675
- }
676
-
677
- /**
678
- * Detect whether this extension instance is running inside a worker subprocess
679
- * (set via TASK_RUNNER_WORKER_TOOL_MODE env var). When true, the extension only
680
- * registers the review_step tool — no commands, widgets, or auto-start.
681
- */
682
- function isWorkerToolMode(): boolean {
683
- return process.env.TASK_RUNNER_WORKER_TOOL_MODE === "1";
684
- }
685
-
686
636
  /**
687
637
  * Load an agent definition with prompt inheritance.
688
638
  *
@@ -2080,6 +2030,7 @@ export default function (pi: ExtensionAPI) {
2080
2030
  "Call review_step at step boundaries based on the task's Review Level (from STATUS.md header).",
2081
2031
  "Review Level 0: skip all reviews. Level 1: plan review before implementing. Level 2: plan + code review. Level 3: plan + code + test review.",
2082
2032
  "Skip reviews for Step 0 (Preflight) and the final documentation/delivery step.",
2033
+ "For code reviews: before starting a step, capture the current HEAD commit with `git rev-parse HEAD` and pass it as the `baseline` parameter. This lets the reviewer see only that step's changes.",
2083
2034
  "On REVISE: read the review file in .reviews/ for detailed feedback, address the issues, commit fixes, then proceed.",
2084
2035
  "On RETHINK: reconsider your plan approach, adjust, then implement.",
2085
2036
  "On UNAVAILABLE: reviewer failed — proceed with caution.",
@@ -2090,9 +2041,14 @@ export default function (pi: ExtensionAPI) {
2090
2041
  [Type.Literal("plan"), Type.Literal("code")],
2091
2042
  { description: 'Review type: "plan" or "code"' },
2092
2043
  ),
2044
+ baseline: Type.Optional(Type.String({
2045
+ description: "Git commit SHA to use as the diff baseline for code reviews. " +
2046
+ "Capture HEAD before starting a step and pass it here so the reviewer " +
2047
+ "sees only that step's changes. If omitted, the reviewer sees the full diff against HEAD.",
2048
+ })),
2093
2049
  }),
2094
2050
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
2095
- const { step: stepNum, type: reviewType } = params;
2051
+ const { step: stepNum, type: reviewType, baseline } = params;
2096
2052
 
2097
2053
  if (!state.task || !state.config) {
2098
2054
  return {
@@ -2123,11 +2079,12 @@ export default function (pi: ExtensionAPI) {
2123
2079
  const requestPath = join(reviewsDir, `request-R${num}.md`);
2124
2080
  const outputPath = join(reviewsDir, `R${num}-${reviewType}-step${stepNum}.md`);
2125
2081
 
2126
- // Find step baseline commit for code reviews
2127
- let stepBaselineCommit: string | undefined;
2128
- if (reviewType === "code") {
2129
- stepBaselineCommit = getHeadCommitSha();
2130
- }
2082
+ // Resolve step baseline commit for code reviews.
2083
+ // The worker should pass the pre-step HEAD SHA as `baseline` so the
2084
+ // reviewer sees only this step's changes (not cumulative diff).
2085
+ // Falls back to undefined (full diff) if baseline is not provided.
2086
+ const stepBaselineCommit: string | undefined =
2087
+ reviewType === "code" ? (baseline || undefined) : undefined;
2131
2088
 
2132
2089
  // Find step info for the name
2133
2090
  const stepInfo = task.steps.find(s => s.number === stepNum);
@@ -2143,7 +2100,7 @@ export default function (pi: ExtensionAPI) {
2143
2100
  const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
2144
2101
  const reviewerModel = config.reviewer.model
2145
2102
  || reviewerDef?.model
2146
- || "openai/gpt-5.3-codex";
2103
+ || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
2147
2104
  const reviewerPrompt = reviewerDef?.systemPrompt
2148
2105
  || "You are a code reviewer. Read the request and write your review to the specified output file.";
2149
2106
  const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
@@ -2947,7 +2904,7 @@ export default function (pi: ExtensionAPI) {
2947
2904
  writeFileSync(requestPath, request);
2948
2905
 
2949
2906
  const reviewerDef = loadAgentDef(ctx.cwd, "task-reviewer");
2950
- const reviewerModel = config.reviewer.model || reviewerDef?.model || "openai/gpt-5.3-codex";
2907
+ const reviewerModel = config.reviewer.model || reviewerDef?.model || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
2951
2908
  const reviewerPrompt = reviewerDef?.systemPrompt || "You are a code reviewer. Read the request and write your review to the specified output file.";
2952
2909
  const systemPrompt = reviewerPrompt + "\n\n" + buildProjectContext(config, task.taskFolder);
2953
2910
 
@@ -3086,7 +3043,7 @@ export default function (pi: ExtensionAPI) {
3086
3043
  const reviewModel = config.quality_gate.review_model
3087
3044
  || config.reviewer.model
3088
3045
  || reviewerDef?.model
3089
- || "openai/gpt-5.3-codex";
3046
+ || (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "anthropic/claude-sonnet-4-20250514");
3090
3047
 
3091
3048
  const reviewerPrompt = reviewerDef?.systemPrompt
3092
3049
  || "You are a quality gate reviewer. Read the review request and write your JSON verdict to the specified file.";
@@ -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
- return executeIntegration(mode as IntegrateMode, {
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 2: Pre-integration summary ──────────────────────
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(` ℹ All work is on orch branch: ${orchBranch}`);
55
- lines.push(` Your ${baseBranch || "working"} branch was not modified.`);
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 apply the changes:");
61
- lines.push(" • /orch-integrate Apply now (fast-forward, recommended)");
62
- lines.push(" /orch-integrate --pr Push orch branch & open a PR for team review");
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.lastHeartbeat ?? snap.observedAt,
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
- // Notify the operator that conversational mode is back
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
- `🔀 **Supervisor returning to conversational mode.**\n\n` +
2626
- routingContext.contextMessage,
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
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -187,11 +187,12 @@ value.
187
187
  **Example flow for a Review Level 2 task, Step 3:**
188
188
  1. Read Step 3 requirements
189
189
  2. Call `review_step(step=3, type="plan")` → get plan feedback
190
- 3. Implement Step 3
191
- 4. Commit changes
192
- 5. Call `review_step(step=3, type="code")` → get code feedback
193
- 6. If REVISE: fix issues, commit again
194
- 7. Move to Step 4
190
+ 3. Capture baseline: run `git rev-parse HEAD` and save the SHA
191
+ 4. Implement Step 3
192
+ 5. Commit changes
193
+ 6. Call `review_step(step=3, type="code", baseline="<saved SHA>")` → get code feedback
194
+ 7. If REVISE: fix issues, commit again
195
+ 8. Move to Step 4
195
196
 
196
197
  If the `review_step` tool is not available (e.g., non-orchestrated mode), skip
197
198
  this protocol entirely — the task-runner handles reviews externally.