lanekeeper 0.1.2 → 0.1.3

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.
@@ -5,4 +5,4 @@ import type { LaneKeeperConfig } from "./config.js";
5
5
  * worktree (dirty, diverged, busy) just skips that one; this never blocks
6
6
  * or fails the `land` it's running as part of.
7
7
  */
8
- export declare function pruneLandedLanes(mainTop: string, cfg: Pick<LaneKeeperConfig, "worktreeSuffix" | "branchPrefix" | "integrationBranch">, currentWorktree: string): string[];
8
+ export declare function pruneLandedLanes(mainTop: string, cfg: Pick<LaneKeeperConfig, "worktreeSuffix" | "branchPrefix" | "integrationBranch" | "regenerableFiles">, currentWorktree: string): string[];
@@ -26,7 +26,13 @@
26
26
  * another lane's land before its own first commit.
27
27
  * 3. `git worktree remove` (no `--force`) refuses on its own if the
28
28
  * worktree has uncommitted changes — dirty work is never discarded
29
- * just because its branch happens to be merged.
29
+ * just because its branch happens to be merged. The ONE exception,
30
+ * matching `sync`/`land`: files listed in `regenerableFiles`
31
+ * (next-env.d.ts and the like) are discarded first and the removal
32
+ * retried, since a build tool rewriting its own output shouldn't be
33
+ * the thing that leaves an otherwise fully-landed lane stuck forever.
34
+ * Any OTHER dirty file blocks pruning exactly as before — real
35
+ * uncommitted work is never discarded just to tidy up disk space.
30
36
  * Deleting the now-redundant local branch (`git branch -d`, never `-D`) is a
31
37
  * separate, best-effort tidiness step AFTER the worktree is already gone —
32
38
  * it checks merge state against local HEAD rather than origin, so it can
@@ -69,6 +75,15 @@ function existsRealpath(path) {
69
75
  return path;
70
76
  }
71
77
  }
78
+ function tryRemoveWorktree(mainTop, wt) {
79
+ try {
80
+ execFileSync("git", ["worktree", "remove", wt], { cwd: mainTop, stdio: "ignore" });
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
72
87
  function listWorktrees(mainTop) {
73
88
  const out = execFileSync("git", ["worktree", "list", "--porcelain"], { cwd: mainTop, encoding: "utf8" });
74
89
  const entries = [];
@@ -109,6 +124,7 @@ export function pruneLandedLanes(mainTop, cfg, currentWorktree) {
109
124
  const laneDirPrefix = `${basename(mainTopReal)}${cfg.worktreeSuffix}`;
110
125
  const parent = dirname(mainTopReal);
111
126
  const upstream = `origin/${cfg.integrationBranch}`;
127
+ const regenerable = new Set(cfg.regenerableFiles);
112
128
  let worktrees;
113
129
  try {
114
130
  worktrees = listWorktrees(mainTop);
@@ -134,12 +150,23 @@ export function pruneLandedLanes(mainTop, cfg, currentWorktree) {
134
150
  }
135
151
  if (hasLiveProcessInside(wt))
136
152
  continue; // someone's actively in here — never touch it
137
- try {
138
- execFileSync("git", ["worktree", "remove", wt], { cwd: mainTop, stdio: "ignore" });
139
- }
140
- catch {
141
- continue; // dirty or busy worktree remove refused on its own, nothing removed
153
+ let removed = tryRemoveWorktree(mainTop, wt);
154
+ if (!removed) {
155
+ // Blocked by dirty files? Only retry if EVERY one of them is a
156
+ // configured regenerable file — anything else is real uncommitted
157
+ // work, and this lane is left alone exactly as before.
158
+ const dirty = execFileSync("git", ["status", "--porcelain"], { cwd: wt, encoding: "utf8" })
159
+ .split("\n")
160
+ .filter(Boolean)
161
+ .map((line) => line.slice(3).trim());
162
+ const blocking = dirty.filter((f) => !regenerable.has(f));
163
+ if (dirty.length > 0 && blocking.length === 0) {
164
+ execFileSync("git", ["checkout", "--", ...dirty], { cwd: wt, stdio: "ignore" });
165
+ removed = tryRemoveWorktree(mainTop, wt);
166
+ }
142
167
  }
168
+ if (!removed)
169
+ continue; // still dirty (real work) or otherwise busy — leave it alone
143
170
  // The worktree is gone — this is now unconditionally a pruned lane,
144
171
  // regardless of what happens to the branch ref below.
145
172
  pruned.push(wt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lanekeeper",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "The local, zero-cost merge queue for parallel Claude Code agents. Plugs into Claude Code's native worktree isolation; one build at a time, one landing at a time, zero races.",
5
5
  "type": "module",
6
6
  "license": "MIT",