@ferris1225/pi-subagents 4.2.1 → 4.2.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 +4 -2
- package/agents/explorer.md +6 -9
- package/package.json +55 -55
- package/src/prompt.ts +3 -2
- package/src/recovery.ts +20 -2
- package/src/rpc-run.ts +993 -993
- package/src/thread-lifecycle.ts +1 -0
- package/src/worktree.ts +66 -30
package/src/thread-lifecycle.ts
CHANGED
|
@@ -927,6 +927,7 @@ export function installThreadLifecycle(thread: SubagentThread, deps: ThreadLifec
|
|
|
927
927
|
status: "retained",
|
|
928
928
|
integrated: false,
|
|
929
929
|
hadChanges: false,
|
|
930
|
+
originalRoot: candidate.originalRoot,
|
|
930
931
|
...(retainedPath ? { worktreePath: retainedPath } : {}),
|
|
931
932
|
...(existsSync(candidate.patchPath) ? { patchPath: candidate.patchPath } : {}),
|
|
932
933
|
error: `Discarding unused continuation failed: ${error instanceof Error ? error.message : String(error)}`,
|
package/src/worktree.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { copyFile, mkdir, mkdtemp, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
14
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
14
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
15
15
|
import { writeTempOwnerMarker } from "./temp-hygiene.ts";
|
|
16
16
|
|
|
17
17
|
export type IsolationMode = "shared" | "worktree";
|
|
@@ -233,6 +233,8 @@ export interface WorktreeFinalization {
|
|
|
233
233
|
/** True once the patch was successfully applied to the original worktree. */
|
|
234
234
|
integrated: boolean;
|
|
235
235
|
hadChanges: boolean;
|
|
236
|
+
/** Repository a recovery path can prune stale worktree metadata against. */
|
|
237
|
+
originalRoot?: string;
|
|
236
238
|
worktreePath?: string;
|
|
237
239
|
patchPath?: string;
|
|
238
240
|
error?: string;
|
|
@@ -323,6 +325,63 @@ export function isPathInside(root: string, candidate: string): boolean {
|
|
|
323
325
|
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
324
326
|
}
|
|
325
327
|
|
|
328
|
+
/** The temp group directory backing a worktree path, when the path actually
|
|
329
|
+
* names our `<group>/worktree` layout. Recovery deletes only paths read back
|
|
330
|
+
* from the manifest through this guard. */
|
|
331
|
+
export function worktreeGroupDir(worktreePath: string): string | undefined {
|
|
332
|
+
const group = dirname(worktreePath);
|
|
333
|
+
return basename(worktreePath) === "worktree" && basename(group).startsWith(WORKTREE_TEMP_DIR_PREFIX)
|
|
334
|
+
? group
|
|
335
|
+
: undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Delete one isolated worktree group: Git's own removal keeps metadata
|
|
339
|
+
* authoritative, but Git on Windows cannot always delete deep checkouts
|
|
340
|
+
* ("Filename too long"), so the Node removal decides the outcome and the prune
|
|
341
|
+
* clears any stale registration left behind. Returns an error string when
|
|
342
|
+
* artifacts still exist afterwards. */
|
|
343
|
+
export async function removeWorktreeGroup(
|
|
344
|
+
paths: { originalRoot?: string; tempDir: string; worktreePath: string },
|
|
345
|
+
runner: CommandRunner = runCommand,
|
|
346
|
+
): Promise<string | undefined> {
|
|
347
|
+
let removeError: string | undefined;
|
|
348
|
+
if (paths.originalRoot && existsSync(paths.worktreePath)) {
|
|
349
|
+
// core.longpaths only exists in Git for Windows, and some POSIX builds
|
|
350
|
+
// reject unknown -c core keys, so the flag stays platform-gated.
|
|
351
|
+
const longPaths = process.platform === "win32" ? ["-c", "core.longpaths=true"] : [];
|
|
352
|
+
try {
|
|
353
|
+
await runGit(
|
|
354
|
+
runner,
|
|
355
|
+
paths.originalRoot,
|
|
356
|
+
[...longPaths, "worktree", "remove", "--force", paths.worktreePath],
|
|
357
|
+
`Removing isolated worktree ${paths.worktreePath}`,
|
|
358
|
+
);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
removeError = error instanceof Error ? error.message : String(error);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
await rm(paths.tempDir, { recursive: true, force: true });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const rmError = error instanceof Error ? error.message : String(error);
|
|
367
|
+
return removeError
|
|
368
|
+
? `${removeError}; removing temporary directory failed: ${rmError}`
|
|
369
|
+
: `Removing temporary directory failed: ${rmError}`;
|
|
370
|
+
}
|
|
371
|
+
if (!paths.originalRoot) return undefined;
|
|
372
|
+
try {
|
|
373
|
+
await runGit(
|
|
374
|
+
runner,
|
|
375
|
+
paths.originalRoot,
|
|
376
|
+
["worktree", "prune"],
|
|
377
|
+
`Pruning Git worktree metadata for ${paths.originalRoot}`,
|
|
378
|
+
);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return error instanceof Error ? error.message : String(error);
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
326
385
|
interface RepositoryLocation {
|
|
327
386
|
originalCwd: string;
|
|
328
387
|
originalRoot: string;
|
|
@@ -588,6 +647,7 @@ class GitWorktreeIsolation implements WorktreeIsolation {
|
|
|
588
647
|
status: "retained",
|
|
589
648
|
integrated,
|
|
590
649
|
hadChanges,
|
|
650
|
+
originalRoot: this.originalRoot,
|
|
591
651
|
...(existsSync(this.worktreePath) ? { worktreePath: this.worktreePath } : {}),
|
|
592
652
|
...(patchWritten && existsSync(this.patchPath) ? { patchPath: this.patchPath } : {}),
|
|
593
653
|
error,
|
|
@@ -649,35 +709,11 @@ class GitWorktreeIsolation implements WorktreeIsolation {
|
|
|
649
709
|
}
|
|
650
710
|
|
|
651
711
|
/** Return an error string instead of throwing so applied work is never retried. */
|
|
652
|
-
private
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
["worktree", "remove", "--force", this.worktreePath],
|
|
658
|
-
`Removing isolated worktree ${this.worktreePath}`,
|
|
659
|
-
);
|
|
660
|
-
} catch (error) {
|
|
661
|
-
return error instanceof Error ? error.message : String(error);
|
|
662
|
-
}
|
|
663
|
-
let pruneError: string | undefined;
|
|
664
|
-
try {
|
|
665
|
-
await runGit(
|
|
666
|
-
this.runner,
|
|
667
|
-
this.originalRoot,
|
|
668
|
-
["worktree", "prune"],
|
|
669
|
-
`Pruning Git worktree metadata for ${this.originalRoot}`,
|
|
670
|
-
);
|
|
671
|
-
} catch (error) {
|
|
672
|
-
pruneError = error instanceof Error ? error.message : String(error);
|
|
673
|
-
}
|
|
674
|
-
try {
|
|
675
|
-
await rm(this.tempDir, { recursive: true, force: true });
|
|
676
|
-
} catch (error) {
|
|
677
|
-
const rmError = error instanceof Error ? error.message : String(error);
|
|
678
|
-
return pruneError ? `${pruneError}; removing temporary directory failed: ${rmError}` : `Removing temporary directory failed: ${rmError}`;
|
|
679
|
-
}
|
|
680
|
-
return pruneError;
|
|
712
|
+
private removeAndPrune(): Promise<string | undefined> {
|
|
713
|
+
return removeWorktreeGroup(
|
|
714
|
+
{ originalRoot: this.originalRoot, worktreePath: this.worktreePath, tempDir: this.tempDir },
|
|
715
|
+
this.runner,
|
|
716
|
+
);
|
|
681
717
|
}
|
|
682
718
|
}
|
|
683
719
|
|