taskplane 0.5.11 → 0.6.0

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.
@@ -10,7 +10,7 @@ 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 { BulkWorktreeError, CreateLaneWorktreesResult, CreateWorktreeOptions, OrchestratorConfig, PreflightCheck, PreflightResult, RemoveAllWorktreesResult, RemoveWorktreeOutcome, RemoveWorktreeResult, WorktreeInfo } from "./types.ts";
13
+ import type { AllocatedLane, BulkWorktreeError, CreateLaneWorktreesResult, CreateWorktreeOptions, LaneTaskOutcome, OrchestratorConfig, PreflightCheck, PreflightResult, RemoveAllWorktreesResult, RemoveWorktreeOutcome, RemoveWorktreeResult, WorktreeInfo } from "./types.ts";
14
14
 
15
15
  // ── Worktree Helpers ─────────────────────────────────────────────────
16
16
 
@@ -1572,6 +1572,21 @@ export function removeAllWorktrees(
1572
1572
  removeBatchContainerIfEmpty(containerPath);
1573
1573
  }
1574
1574
 
1575
+ // TP-029: Remove empty .worktrees/ base directory in subdirectory mode.
1576
+ // In sibling mode the base dir is the repo's parent (e.g., "..") — never remove that.
1577
+ // Only attempt removal when empty (same safety as container cleanup).
1578
+ if (config && config.orchestrator.worktree_location !== "sibling") {
1579
+ const basePath = resolveWorktreeBasePath(repoRoot, config);
1580
+ try {
1581
+ if (existsSync(basePath)) {
1582
+ const entries = readdirSync(basePath);
1583
+ if (entries.length === 0) {
1584
+ rmdirSync(basePath);
1585
+ }
1586
+ }
1587
+ } catch { /* safe default — leave it alone */ }
1588
+ }
1589
+
1575
1590
  return {
1576
1591
  totalAttempted: worktrees.length,
1577
1592
  removed,
@@ -1982,3 +1997,316 @@ export function forceCleanupWorktree(
1982
1997
  }
1983
1998
  }
1984
1999
 
2000
+
2001
+ // ── Partial Progress Preservation ────────────────────────────────────
2002
+
2003
+ /**
2004
+ * Result of saving partial progress for a single failed task.
2005
+ */
2006
+ export interface SavePartialProgressResult {
2007
+ /** Whether partial progress was saved (branch created or already existed) */
2008
+ saved: boolean;
2009
+ /** The saved branch name, if saved */
2010
+ savedBranch?: string;
2011
+ /** Number of commits ahead of the target branch */
2012
+ commitCount: number;
2013
+ /** Task ID this progress belongs to */
2014
+ taskId: string;
2015
+ /** Error message if save failed */
2016
+ error?: string;
2017
+ }
2018
+
2019
+ /**
2020
+ * Compute the saved branch name for partial progress from a failed task.
2021
+ *
2022
+ * Naming convention per roadmap Phase 2 section 2a:
2023
+ * - Repo mode: `saved/{opId}-{taskId}-{batchId}`
2024
+ * - Workspace mode: `saved/{opId}-{repoId}-{taskId}-{batchId}`
2025
+ *
2026
+ * Pure function — no side effects.
2027
+ *
2028
+ * @param opId - Operator identifier (sanitized)
2029
+ * @param taskId - Task identifier (e.g., "TP-028")
2030
+ * @param batchId - Batch ID timestamp (e.g., "20260308T111750")
2031
+ * @param repoId - Repo identifier (workspace mode only; omit for repo mode)
2032
+ * @returns Saved branch name
2033
+ */
2034
+ export function computePartialProgressBranchName(
2035
+ opId: string,
2036
+ taskId: string,
2037
+ batchId: string,
2038
+ repoId?: string,
2039
+ ): string {
2040
+ if (repoId) {
2041
+ return `saved/${opId}-${repoId}-${taskId}-${batchId}`;
2042
+ }
2043
+ return `saved/${opId}-${taskId}-${batchId}`;
2044
+ }
2045
+
2046
+ /**
2047
+ * Save partial progress from a failed task's lane branch.
2048
+ *
2049
+ * Checks if the lane branch has commits ahead of the target branch,
2050
+ * and if so, creates a saved branch preserving those commits.
2051
+ *
2052
+ * Uses `resolveSavedBranchCollision()` for idempotent collision handling:
2053
+ * - Same SHA → no-op (keep existing)
2054
+ * - Different SHA → create with timestamp suffix
2055
+ *
2056
+ * @param laneBranch - The lane branch that may have partial commits
2057
+ * @param targetBranch - The base/target branch to compare against
2058
+ * @param opId - Operator identifier
2059
+ * @param taskId - Task identifier
2060
+ * @param batchId - Batch ID
2061
+ * @param repoRoot - Repository root for git operations
2062
+ * @param repoId - Repo identifier (workspace mode only)
2063
+ * @returns SavePartialProgressResult describing what was done
2064
+ */
2065
+ export function savePartialProgress(
2066
+ laneBranch: string,
2067
+ targetBranch: string,
2068
+ opId: string,
2069
+ taskId: string,
2070
+ batchId: string,
2071
+ repoRoot: string,
2072
+ repoId?: string,
2073
+ ): SavePartialProgressResult {
2074
+ // Check if lane branch exists
2075
+ const branchCheck = runGit(
2076
+ ["rev-parse", "--verify", `refs/heads/${laneBranch}`],
2077
+ repoRoot,
2078
+ );
2079
+ if (!branchCheck.ok) {
2080
+ return { saved: false, commitCount: 0, taskId, error: `Lane branch "${laneBranch}" not found` };
2081
+ }
2082
+ const branchSHA = branchCheck.stdout.trim();
2083
+
2084
+ // Count commits ahead of target branch
2085
+ const unmergedResult = hasUnmergedCommits(laneBranch, targetBranch, repoRoot);
2086
+ if (!unmergedResult.ok) {
2087
+ return {
2088
+ saved: false,
2089
+ commitCount: 0,
2090
+ taskId,
2091
+ error: `Failed to count commits: ${unmergedResult.error}`,
2092
+ };
2093
+ }
2094
+
2095
+ if (unmergedResult.count === 0) {
2096
+ // No partial progress — lane branch has no new commits
2097
+ return { saved: false, commitCount: 0, taskId };
2098
+ }
2099
+
2100
+ // Compute saved branch name using task-ID naming convention
2101
+ const savedName = computePartialProgressBranchName(opId, taskId, batchId, repoId);
2102
+
2103
+ // Check for collision (idempotent re-runs, retries)
2104
+ const existingCheck = runGit(
2105
+ ["rev-parse", "--verify", `refs/heads/${savedName}`],
2106
+ repoRoot,
2107
+ );
2108
+ const existingSHA = existingCheck.ok ? existingCheck.stdout.trim() : "";
2109
+
2110
+ const resolution = resolveSavedBranchCollision(savedName, existingSHA, branchSHA);
2111
+
2112
+ switch (resolution.action) {
2113
+ case "keep-existing":
2114
+ // Already preserved at the same SHA — idempotent success
2115
+ return {
2116
+ saved: true,
2117
+ savedBranch: resolution.savedName,
2118
+ commitCount: unmergedResult.count,
2119
+ taskId,
2120
+ };
2121
+
2122
+ case "create":
2123
+ case "create-suffixed": {
2124
+ const createResult = runGit(
2125
+ ["branch", resolution.savedName, branchSHA],
2126
+ repoRoot,
2127
+ );
2128
+ if (!createResult.ok) {
2129
+ return {
2130
+ saved: false,
2131
+ commitCount: unmergedResult.count,
2132
+ taskId,
2133
+ error: `Failed to create saved branch "${resolution.savedName}": ${createResult.stderr}`,
2134
+ };
2135
+ }
2136
+ return {
2137
+ saved: true,
2138
+ savedBranch: resolution.savedName,
2139
+ commitCount: unmergedResult.count,
2140
+ taskId,
2141
+ };
2142
+ }
2143
+
2144
+ default:
2145
+ return {
2146
+ saved: false,
2147
+ commitCount: unmergedResult.count,
2148
+ taskId,
2149
+ error: `Unknown collision resolution action`,
2150
+ };
2151
+ }
2152
+ }
2153
+
2154
+ /**
2155
+ * Result of preserving partial progress across all failed tasks.
2156
+ */
2157
+ export interface PreserveFailedLaneProgressResult {
2158
+ /** Per-task results for each failed task that was checked */
2159
+ results: SavePartialProgressResult[];
2160
+ /**
2161
+ * Set of saved branch names that were created (e.g., `saved/{opId}-{taskId}-{batchId}`).
2162
+ * These branches independently preserve the commits — lane branches can still be
2163
+ * safely deleted during cleanup since the saved refs retain reachability.
2164
+ */
2165
+ preservedBranches: Set<string>;
2166
+ /**
2167
+ * Set of lane branch names where preservation FAILED but commits existed.
2168
+ * These branches are unsafe to reset/delete — doing so would lose commits
2169
+ * that were not successfully saved to a separate branch. Callers should skip
2170
+ * worktree reset and branch deletion for these branches to prevent data loss.
2171
+ */
2172
+ unsafeBranches: Set<string>;
2173
+ }
2174
+
2175
+ /**
2176
+ * Callback for resolving repo root and target branch for a given repoId.
2177
+ *
2178
+ * Allows callers (engine.ts, resume.ts) to pass workspace-aware resolution
2179
+ * logic without creating a circular dependency (worktree.ts → waves.ts → worktree.ts).
2180
+ *
2181
+ * @param repoId - Repo identifier (undefined in repo mode)
2182
+ * @returns { repoRoot, targetBranch } for the given repo
2183
+ */
2184
+ export type ResolveRepoContext = (repoId: string | undefined) => {
2185
+ repoRoot: string;
2186
+ targetBranch: string;
2187
+ };
2188
+
2189
+ /**
2190
+ * Preserve partial progress for all failed tasks before cleanup/reset.
2191
+ *
2192
+ * Iterates task outcomes to find failed/stalled tasks, maps each to its
2193
+ * lane branch via the allocated lanes, and saves any partial commits as
2194
+ * task-ID-named saved branches.
2195
+ *
2196
+ * Returns two branch sets:
2197
+ * - `preservedBranches`: saved branch names that were successfully created
2198
+ * (lane branches can be safely deleted since these refs retain commits)
2199
+ * - `unsafeBranches`: lane branch names where preservation FAILED but commits
2200
+ * existed (callers must NOT reset/delete these to prevent data loss)
2201
+ *
2202
+ * Workspace-aware: uses the provided `resolveRepo` callback to resolve
2203
+ * per-repo target branches and repo roots for correct commit counting
2204
+ * in workspace mode.
2205
+ *
2206
+ * @param allocatedLanes - Lanes from the current/last wave (maps tasks to branches)
2207
+ * @param taskOutcomes - All task outcomes accumulated so far
2208
+ * @param opId - Operator identifier
2209
+ * @param batchId - Batch ID
2210
+ * @param resolveRepo - Callback to resolve repo root and target branch per repoId
2211
+ * @returns PreserveFailedLaneProgressResult with per-task results and preserved branch set
2212
+ */
2213
+ export function preserveFailedLaneProgress(
2214
+ allocatedLanes: AllocatedLane[],
2215
+ taskOutcomes: LaneTaskOutcome[],
2216
+ opId: string,
2217
+ batchId: string,
2218
+ resolveRepo: ResolveRepoContext,
2219
+ ): PreserveFailedLaneProgressResult {
2220
+ const results: SavePartialProgressResult[] = [];
2221
+ const preservedBranches = new Set<string>();
2222
+ const unsafeBranches = new Set<string>();
2223
+
2224
+ // Build a map: taskId → { laneBranch, repoId } from allocated lanes
2225
+ const taskToLane = new Map<string, { branch: string; repoId?: string }>();
2226
+ for (const lane of allocatedLanes) {
2227
+ for (const allocatedTask of lane.tasks) {
2228
+ taskToLane.set(allocatedTask.taskId, {
2229
+ branch: lane.branch,
2230
+ repoId: lane.repoId,
2231
+ });
2232
+ }
2233
+ }
2234
+
2235
+ // Find failed/stalled tasks
2236
+ const failedTasks = taskOutcomes.filter(
2237
+ (to) => to.status === "failed" || to.status === "stalled",
2238
+ );
2239
+
2240
+ // Track which lane branches we've already processed (a lane may have
2241
+ // multiple tasks; only save once per branch since all commits are shared)
2242
+ const processedBranches = new Set<string>();
2243
+
2244
+ for (const failedTask of failedTasks) {
2245
+ const laneInfo = taskToLane.get(failedTask.taskId);
2246
+ if (!laneInfo) {
2247
+ // Task not found in allocated lanes — skip (shouldn't happen)
2248
+ results.push({
2249
+ saved: false,
2250
+ commitCount: 0,
2251
+ taskId: failedTask.taskId,
2252
+ error: "Task not found in allocated lanes",
2253
+ });
2254
+ continue;
2255
+ }
2256
+
2257
+ // Skip if we've already processed this branch (multiple failed tasks on same lane)
2258
+ if (processedBranches.has(laneInfo.branch)) {
2259
+ continue;
2260
+ }
2261
+ processedBranches.add(laneInfo.branch);
2262
+
2263
+ // Resolve repo-specific target branch and repo root
2264
+ const { repoRoot: perRepoRoot, targetBranch } = resolveRepo(laneInfo.repoId);
2265
+
2266
+ const result = savePartialProgress(
2267
+ laneInfo.branch,
2268
+ targetBranch,
2269
+ opId,
2270
+ failedTask.taskId,
2271
+ batchId,
2272
+ perRepoRoot,
2273
+ laneInfo.repoId,
2274
+ );
2275
+
2276
+ results.push(result);
2277
+
2278
+ if (result.saved) {
2279
+ // Track the saved branch name for caller visibility
2280
+ preservedBranches.add(result.savedBranch!);
2281
+
2282
+ execLog("partial-progress", failedTask.taskId,
2283
+ `Task ${failedTask.taskId} failed but has ${result.commitCount} commit(s) of partial progress on branch ${result.savedBranch}`,
2284
+ {
2285
+ laneBranch: laneInfo.branch,
2286
+ savedBranch: result.savedBranch,
2287
+ commitCount: result.commitCount,
2288
+ repoId: laneInfo.repoId ?? "(default)",
2289
+ },
2290
+ );
2291
+ } else if (result.commitCount > 0 || result.error) {
2292
+ // Preservation FAILED but commits may exist on the lane branch.
2293
+ // Mark this branch as unsafe to reset/delete — doing so would
2294
+ // irreversibly lose the partial work.
2295
+ unsafeBranches.add(laneInfo.branch);
2296
+
2297
+ execLog("partial-progress", failedTask.taskId,
2298
+ `WARNING: Failed to preserve partial progress for task ${failedTask.taskId} ` +
2299
+ `(${result.commitCount} commit(s) at risk on branch "${laneInfo.branch}")`,
2300
+ {
2301
+ laneBranch: laneInfo.branch,
2302
+ commitCount: result.commitCount,
2303
+ error: result.error ?? "unknown",
2304
+ repoId: laneInfo.repoId ?? "(default)",
2305
+ },
2306
+ );
2307
+ }
2308
+ }
2309
+
2310
+ return { results, preservedBranches, unsafeBranches };
2311
+ }
2312
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.5.11",
3
+ "version": "0.6.0",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -99,7 +99,6 @@ Copy this template when creating a new task. Replace all `[bracketed]` fields.
99
99
  - [ ] "Check If Affected" docs reviewed
100
100
  - [ ] Discoveries logged in STATUS.md
101
101
  - [ ] `.DONE` created in this folder
102
- - [ ] Task archived (auto — handled by task-runner extension)
103
102
 
104
103
  ## Documentation Requirements
105
104
 
@@ -208,7 +207,6 @@ this from PROMPT.md.
208
207
  - [ ] "Check If Affected" docs reviewed
209
208
  - [ ] Discoveries logged
210
209
  - [ ] `.DONE` created
211
- - [ ] Archive and push
212
210
 
213
211
  ---
214
212
 
@@ -21,10 +21,38 @@ your verdict. If you don't write the file, your review is lost.
21
21
 
22
22
  ## Verdict Criteria
23
23
 
24
- - **APPROVE** — Changes are solid. Minor suggestions are fine but don't block.
25
- - **REVISE** Concrete issues that need fixing. Be specific about what and where.
24
+ - **APPROVE** — Step will achieve its stated outcomes. Minor suggestions belong
25
+ in the Suggestions section they are captured for reference but do NOT block
26
+ progress. **If your only findings are minor or suggestion-level, your verdict
27
+ is APPROVE.**
28
+ - **REVISE** — Step will fail, produce incorrect results, or miss a stated
29
+ requirement without fixes. Use ONLY for issues that would cause the worker to
30
+ need to redo work later if left unaddressed.
26
31
  - **RETHINK** — Approach is fundamentally wrong. Explain why and suggest alternative.
27
32
 
33
+ ### When to APPROVE vs REVISE
34
+
35
+ **APPROVE** (with suggestions) when:
36
+ - The approach will work, but you see a cleaner alternative
37
+ - A checkbox could be more specific, but the existing wording covers the outcome
38
+ - Documentation style or STATUS.md formatting could improve
39
+ - You'd suggest additional tests but the core coverage is adequate
40
+
41
+ **REVISE** when:
42
+ - A requirement from PROMPT.md will not be met by the current plan/code
43
+ - A bug or regression is introduced
44
+ - A critical edge case is unhandled and would cause runtime failure
45
+ - Backward compatibility is broken without migration
46
+
47
+ ### Do NOT issue REVISE for
48
+
49
+ - Missing checkboxes for work that's already covered by a broader item
50
+ - Splitting a single outcome checkbox into implementation sub-steps
51
+ - STATUS.md cleanup, formatting, or wording preferences
52
+ - "Re-run tests and record the result" — test runs are the worker's concern
53
+ - "Check If Affected" docs that turn out to need no changes
54
+ - Suggestions that improve quality but aren't required for correctness
55
+
28
56
  ## Plan Review Format
29
57
 
30
58
  Write to the specified output file using the `write` tool:
@@ -100,6 +128,30 @@ The worker is an LLM with full codebase access — trust it to figure out
100
128
  implementation specifics. Your job is to catch gaps in **what** needs to happen
101
129
  and **why**, not to dictate **how** at the code level.
102
130
 
131
+ ## Checkpoint Granularity Alignment
132
+
133
+ STATUS.md checkboxes represent **meaningful outcomes**, not implementation
134
+ details. A checkbox like "Corrupt state handling (paused + diagnostic)" is a
135
+ single outcome — the worker determines how to achieve it.
136
+
137
+ **Do NOT** request splitting outcome-level checkboxes into implementation
138
+ sub-steps. When adding items via REVISE, only add items that represent genuinely
139
+ **missing outcomes** — things the worker would not have done without your review.
140
+
141
+ Examples:
142
+
143
+ | ❌ Pedantic (don't request) | ✅ Legitimate (request if missing) |
144
+ |---|---|
145
+ | Split "Add retry logic" into 3 checkboxes for timeout, backoff, and counter | "Missing: retry counter must persist across pause/resume" |
146
+ | Add checkbox for "verify types compile" | "Missing: backward compatibility with v2 state files" |
147
+ | Add checkbox for "update STATUS.md formatting" | "Missing: corrupt state should enter paused, not delete" |
148
+
149
+ ## Where Findings Go
150
+
151
+ - **critical / important** → Issues Found section → triggers REVISE if blocking
152
+ - **minor / suggestion** → Suggestions section → captured in STATUS.md Notes
153
+ by the worker, **no checkbox created**, does NOT trigger REVISE
154
+
103
155
  ## Rules
104
156
 
105
157
  - Be specific — reference actual files and line numbers
@@ -116,13 +116,17 @@ from the next item, combine them.
116
116
  When a reviewer returns REVISE with specific feedback items:
117
117
 
118
118
  1. **Read the review file** in `.reviews/`
119
- 2. **Add revision items as new checkboxes** in the current step — group related
120
- fixes into single checkboxes rather than creating one per reviewer sentence
121
- 3. **Commit the hydrated STATUS.md** (see Checkpoint Discipline exceptions):
119
+ 2. **Issues Found items** → add as new checkboxes in the current step. Group
120
+ related fixes into single checkboxes rather than creating one per reviewer
121
+ sentence. These are mandatory they represent things that would cause
122
+ incorrect results if not addressed.
123
+ 3. **Suggestions items** → log in the STATUS.md **Notes** section for reference.
124
+ Do NOT create checkboxes for suggestions. They are advisory, not blocking.
125
+ 4. **Commit the hydrated STATUS.md** (see Checkpoint Discipline exceptions):
122
126
  ```bash
123
127
  git add -A && git commit -m "hydrate: add R00N revision items to Step N"
124
128
  ```
125
- 4. THEN implement the revisions, checking off each item as you go
129
+ 5. THEN implement the revisions, checking off each item as you go
126
130
 
127
131
  ### Rules
128
132