claude-tempo 0.29.0 → 0.29.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-tempo-dashboard",
3
3
  "private": true,
4
- "version": "0.29.0",
4
+ "version": "0.29.1",
5
5
  "type": "module",
6
6
  "description": "Web dashboard for claude-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
7
  "scripts": {
@@ -41,7 +41,7 @@ const helpers_1 = require("./helpers");
41
41
  const worktree_1 = require("../utils/worktree");
42
42
  const validation_1 = require("../utils/validation");
43
43
  function registerWorktreeTool(server, client, config, handle, getPlayerId) {
44
- (0, helpers_1.defineTool)(server, 'worktree', 'Manage git worktrees for player isolation. Conductor only. Actions: create (provision worktree for a player), remove (clean up), list (show active worktrees). Use when multiple players commit to different branches of the same repo simultaneously; skip for read-only work, sequential work, or tasks under ~5 min. See docs/orchestration.md#when-to-use-worktrees.', {
44
+ (0, helpers_1.defineTool)(server, 'worktree', 'Manage git worktrees for player isolation. Conductor only. Actions: create (provision worktree for a player), remove (clean up), list (show active worktrees). Use when multiple players commit to different branches of the same repo simultaneously; skip for read-only work, sequential work, or tasks under ~5 min. IMPORTANT: before `remove`, have the player stop any long-running processes inside the worktree (dev servers, file watchers) — on Windows a memory-mapped native module will block directory removal and `remove` will fail. See docs/orchestration.md#when-to-use-worktrees.', {
45
45
  action: zod_1.z.enum(['create', 'remove', 'list']).describe('Action to perform'),
46
46
  player: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Player name (required for create/remove)'),
47
47
  branch: zod_1.z.string().optional().describe('Git branch for the worktree (defaults to {ensemble}/{player-name})'),
@@ -132,9 +132,22 @@ function registerWorktreeTool(server, client, config, handle, getPlayerId) {
132
132
  if (!entry) {
133
133
  return (0, helpers_1.fail)(`No worktree found for player "${player}".`);
134
134
  }
135
- // Remove from disk
136
- (0, worktree_1.removeWorktree)(entry.path);
137
- // Remove from conductor state
135
+ // Remove from disk. #594: removeWorktree throws if the directory
136
+ // survives the removal (Windows file-lock half-removal). We must
137
+ // NOT signal `removeWorktree` state or cue the player until disk
138
+ // removal is confirmed — otherwise Temporal state records "no
139
+ // worktree" while a locked orphan directory remains on disk, and
140
+ // the next `create` fails with a confusing git fatal.
141
+ try {
142
+ (0, worktree_1.removeWorktree)(entry.path, entry.gitRoot);
143
+ }
144
+ catch (err) {
145
+ return (0, helpers_1.fail)(`Worktree for **${player}** could not be removed: ${(0, helpers_1.formatError)(err)}\n\n` +
146
+ `Conductor state is unchanged — the worktree is still tracked. ` +
147
+ `Have the player stop any long-running processes inside the worktree ` +
148
+ `(dev servers, file watchers), then retry \`worktree remove\`.`);
149
+ }
150
+ // Remove from conductor state (only reached on confirmed disk removal)
138
151
  await handle.signal('removeWorktree', player);
139
152
  // Auto-cue the player
140
153
  try {
@@ -82,5 +82,22 @@ export declare function createWorktree(opts: CreateWorktreeOpts): CreateWorktree
82
82
  export declare function installDependencies(worktreePath: string, timeoutMs?: number): void;
83
83
  /**
84
84
  * Remove a git worktree.
85
+ *
86
+ * Throws if the worktree directory survives the removal (#594). On Windows,
87
+ * `git worktree remove --force` deletes `.git/worktrees/<name>` metadata
88
+ * *first*, then `rmdir`s the directory — so when a native `.node` module
89
+ * inside the worktree is memory-mapped by a live process, the directory
90
+ * deletion fails while the metadata is already gone. The pre-#594 code
91
+ * swallowed that failure (log-only, no throw), so the caller reported
92
+ * success and Temporal state diverged from disk. Now the post-removal
93
+ * `existsSync` check turns a half-removal into a hard error the caller can
94
+ * surface.
95
+ *
96
+ * `gitRoot` scopes the `git worktree remove` invocation to the owning
97
+ * repository. Pre-#594 the command ran with no `cwd`, so it only worked when
98
+ * `process.cwd()` happened to be the right repo — fragile, and wrong outright
99
+ * when the conductor's cwd is a different checkout than the worktree's repo.
100
+ * Callers should pass `entry.gitRoot` from the `WorktreeEntry`; it defaults to
101
+ * `process.cwd()` for backward compatibility.
85
102
  */
86
- export declare function removeWorktree(worktreePath: string): void;
103
+ export declare function removeWorktree(worktreePath: string, gitRoot?: string): void;
@@ -165,6 +165,42 @@ function createWorktree(opts) {
165
165
  const switched = switchWorktreeToBranch(wtPath, branch);
166
166
  return { path: wtPath, branch, created: false, switched };
167
167
  }
168
+ // #594 defect 2: stale-orphan recovery. A half-removed worktree leaves the
169
+ // directory on disk with its `.git` file already deleted (`git worktree
170
+ // remove` deletes metadata before the rmdir, which then fails under a
171
+ // Windows file lock). The reuse guard above keys on `.git` presence, so it
172
+ // is skipped — and `git worktree add` below refuses a non-empty pre-existing
173
+ // directory with a confusing `fatal: '...' already exists`. Detect the
174
+ // orphan and clear it first, with a clear error if the lock is still held.
175
+ if ((0, fs_1.existsSync)(wtPath)) {
176
+ log(`Stale worktree directory at "${wtPath}" (no \`.git\`) — attempting cleanup`);
177
+ try {
178
+ (0, fs_1.rmSync)(wtPath, { recursive: true, force: true });
179
+ }
180
+ catch (err) {
181
+ // Swallow — the existsSync re-check below is the authoritative gate.
182
+ log(`Warning: \`rmSync\` on stale worktree "${wtPath}" failed: ${err?.message ?? err}`);
183
+ }
184
+ if ((0, fs_1.existsSync)(wtPath)) {
185
+ throw new Error(`Stale worktree directory at "${wtPath}" could not be removed — kill any ` +
186
+ `processes running inside it (e.g. a dev server) and retry. On Windows, ` +
187
+ `memory-mapped native \`.node\` modules hold the lock until the owning ` +
188
+ `process exits.`);
189
+ }
190
+ // Directory cleared — prune any dangling git metadata so `git worktree add`
191
+ // doesn't trip over a stale registration for the same path/branch.
192
+ try {
193
+ (0, child_process_1.execFileSync)('git', ['worktree', 'prune'], {
194
+ cwd: gitRoot,
195
+ encoding: 'utf8',
196
+ stdio: ['pipe', 'pipe', 'pipe'],
197
+ });
198
+ }
199
+ catch {
200
+ // Best-effort — prune failure is non-fatal; the add below will surface
201
+ // any real conflict.
202
+ }
203
+ }
168
204
  // Ensure base directory exists
169
205
  (0, fs_1.mkdirSync)(basePath, { recursive: true });
170
206
  // Check if the branch already has a worktree (would cause git error)
@@ -246,17 +282,46 @@ function installDependencies(worktreePath, timeoutMs = validation_1.WORKTREE_INS
246
282
  }
247
283
  /**
248
284
  * Remove a git worktree.
285
+ *
286
+ * Throws if the worktree directory survives the removal (#594). On Windows,
287
+ * `git worktree remove --force` deletes `.git/worktrees/<name>` metadata
288
+ * *first*, then `rmdir`s the directory — so when a native `.node` module
289
+ * inside the worktree is memory-mapped by a live process, the directory
290
+ * deletion fails while the metadata is already gone. The pre-#594 code
291
+ * swallowed that failure (log-only, no throw), so the caller reported
292
+ * success and Temporal state diverged from disk. Now the post-removal
293
+ * `existsSync` check turns a half-removal into a hard error the caller can
294
+ * surface.
295
+ *
296
+ * `gitRoot` scopes the `git worktree remove` invocation to the owning
297
+ * repository. Pre-#594 the command ran with no `cwd`, so it only worked when
298
+ * `process.cwd()` happened to be the right repo — fragile, and wrong outright
299
+ * when the conductor's cwd is a different checkout than the worktree's repo.
300
+ * Callers should pass `entry.gitRoot` from the `WorktreeEntry`; it defaults to
301
+ * `process.cwd()` for backward compatibility.
249
302
  */
250
- function removeWorktree(worktreePath) {
303
+ function removeWorktree(worktreePath, gitRoot = process.cwd()) {
251
304
  try {
252
305
  (0, child_process_1.execFileSync)('git', ['worktree', 'remove', '--force', worktreePath], {
306
+ cwd: gitRoot,
253
307
  encoding: 'utf8',
254
308
  stdio: ['pipe', 'pipe', 'pipe'],
255
309
  });
256
- log(`Removed worktree at "${worktreePath}"`);
257
310
  }
258
311
  catch (err) {
259
312
  const msg = err.stderr || err.message || String(err);
260
- log(`Warning: failed to remove worktree at "${worktreePath}": ${msg.trim()}`);
313
+ log(`Warning: \`git worktree remove\` failed at "${worktreePath}": ${msg.trim()}`);
314
+ // Don't return here — fall through to the post-removal check. git may have
315
+ // deleted the metadata but failed the rmdir; the existsSync gate below is
316
+ // the real success criterion.
317
+ }
318
+ // #594 defect 1: post-removal verification. If the directory is still on
319
+ // disk, the removal half-succeeded — surface it instead of returning void.
320
+ if ((0, fs_1.existsSync)(worktreePath)) {
321
+ throw new Error(`Worktree directory "${worktreePath}" still exists after \`git worktree remove\`. ` +
322
+ `On Windows this usually means a process is holding a file lock — e.g. a dev ` +
323
+ `server with a memory-mapped native \`.node\` module. Stop any processes running ` +
324
+ `inside the worktree and retry.`);
261
325
  }
326
+ log(`Removed worktree at "${worktreePath}"`);
262
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-tempo",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "description": "MCP server for multi-session Claude Code coordination via Temporal",
5
5
  "keywords": [
6
6
  "mcp",