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.
- package/dashboard/package.json +1 -1
- package/dist/tools/worktree.js +17 -4
- package/dist/utils/worktree.d.ts +18 -1
- package/dist/utils/worktree.js +68 -3
- package/package.json +1 -1
package/dashboard/package.json
CHANGED
package/dist/tools/worktree.js
CHANGED
|
@@ -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
|
-
(
|
|
137
|
-
//
|
|
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 {
|
package/dist/utils/worktree.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/worktree.js
CHANGED
|
@@ -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:
|
|
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
|
}
|