lanekeeper 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -266,6 +266,19 @@ Things a sharp reader should already know before they ask:
266
266
  refuses to touch any worktree with a live process's cwd inside it right
267
267
  now. That check needs `lsof` on PATH; if it's missing, pruning fails
268
268
  closed (treats liveness as unknown, never removes) rather than guessing.
269
+ - **The `WorktreeCreate` hook needs the host project's own real install.**
270
+ It runs via `npx lanekeeper hook worktree-create` (a raw hook command has
271
+ no `node_modules/.bin` on its PATH, unlike an `npm run` script) — but npx
272
+ silently falls back to fetching an ephemeral, unpinned copy when it can't
273
+ resolve the package locally, which is exactly what happens if the host
274
+ project's own `node_modules` install of lanekeeper is missing or
275
+ mid-upgrade. That's a real failure mode, not hypothetical: it happened in
276
+ production and the fallback ran silently long enough to mask a broken
277
+ install for two lane-landings. The hook now refuses to run at all if it
278
+ detects it's executing from npx's ephemeral cache rather than the
279
+ project's own installed copy, so a broken install fails loud immediately
280
+ (`npm install` and retry) instead of quietly limping along on a
281
+ mismatched stand-in version.
269
282
 
270
283
  ## 🧬 Where this came from
271
284
 
@@ -10,4 +10,5 @@ export declare function createLane(mainTop: string, cfg: LaneKeeperConfig): {
10
10
  branch: string;
11
11
  lane: number;
12
12
  };
13
+ export declare function isEphemeralNpxCopy(selfPath: string): boolean;
13
14
  export declare function runWorktreeCreateHook(): Promise<void>;
@@ -25,7 +25,8 @@
25
25
  */
26
26
  import { execFileSync } from "node:child_process";
27
27
  import { existsSync, mkdirSync, symlinkSync } from "node:fs";
28
- import { dirname, join, basename } from "node:path";
28
+ import { dirname, join, basename, sep } from "node:path";
29
+ import { fileURLToPath } from "node:url";
29
30
  import { loadConfig, hasConfig, DEFAULTS } from "../lib/config.js";
30
31
  import { resolveMainCheckout } from "../lib/main-checkout.js";
31
32
  // A lane-claim loop with no upper bound is exactly one path-resolution bug
@@ -121,6 +122,24 @@ export function createLane(mainTop, cfg) {
121
122
  return { wt, branch, lane };
122
123
  }
123
124
  }
125
+ // `.claude/settings.json` invokes this hook via `npx lanekeeper hook
126
+ // worktree-create` rather than a project script, precisely because a raw
127
+ // hook command has no `node_modules/.bin` on its PATH the way `npm run`
128
+ // does — npx's own directory-walking local resolution is what makes that
129
+ // work at all. The problem: npx treats a package it can't resolve locally as
130
+ // license to silently fetch an ephemeral, unpinned copy from the registry
131
+ // and run *that* instead of failing — which is exactly what happens when the
132
+ // host project's own install of lanekeeper is missing or mid-upgrade (npm
133
+ // removes the old version's files before extracting the new one; anything
134
+ // that interrupts that leaves precisely this state). That fallback ran
135
+ // silently for long enough in production to block two lanes from landing
136
+ // before anyone noticed node_modules was broken. Refuse to proceed if this
137
+ // module is executing from npx's ephemeral cache instead of the project's
138
+ // own installed copy, so a broken install fails loud immediately instead of
139
+ // limping along on a stand-in version nobody asked for.
140
+ export function isEphemeralNpxCopy(selfPath) {
141
+ return selfPath.includes(`${sep}_npx${sep}`);
142
+ }
124
143
  async function readStdin() {
125
144
  const chunks = [];
126
145
  for await (const chunk of process.stdin)
@@ -137,6 +156,10 @@ export async function runWorktreeCreateHook() {
137
156
  }
138
157
  const fromCwd = input.cwd ?? process.cwd();
139
158
  try {
159
+ if (isEphemeralNpxCopy(fileURLToPath(import.meta.url))) {
160
+ throw new Error("running from npx's ephemeral cache, not this project's own installed dependency — " +
161
+ "node_modules is missing or broken. Run `npm install` in the main checkout and try again.");
162
+ }
140
163
  const mainTop = resolveMainCheckout(fromCwd);
141
164
  const cfg = hasConfig(mainTop) ? await loadConfig(mainTop) : { ...DEFAULTS };
142
165
  const { wt } = createLane(mainTop, cfg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lanekeeper",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",