@ferris1225/pi-subagents 4.2.2 → 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 CHANGED
@@ -127,7 +127,9 @@ main agent inspects the actual changes before calling anything done.
127
127
  as a lane wait, not as slot queueing, and its process slot is already released.
128
128
  - Setup and integration failures keep the useful patch and worktree, and record
129
129
  where they are in `~/.pi/agent/pi-subagents-recovery.json`. Every later session
130
- start repeats that notice until you remove the artifacts.
130
+ start repeats that notice until you remove the artifacts. When the changes had
131
+ already been applied and only the cleanup failed, the next session start
132
+ removes the retained copy itself and clears the notice.
131
133
 
132
134
  ## Threads: resume, stop
133
135
 
@@ -26,15 +26,12 @@ Thoroughness scales with the task (default medium): quick = targeted lookups in
26
26
 
27
27
  ## Final response
28
28
 
29
- Return only actionable retrieval results:
29
+ Return only retrieval results, one bare bullet per finding — a single line: path, the fact, nothing else:
30
30
 
31
31
  ```text
32
- ## Findings
33
- - `path/to/file.ts:10-50` — fact the caller needs
34
- ## Start Here
35
- - `path/to/file.ts` — first symbol/section to verify and why
36
- ## Gaps
37
- - unresolved uncertainty (omit this section when none)
32
+ - `path/to/file.ts:10-50` — the fact
33
+ Start here: `path/to/file.ts` — entry symbol and why (only when the caller could not guess it)
34
+ Gaps: unresolved uncertainty (only when real)
38
35
  ```
39
36
 
40
- Do not repeat the task brief, inventory every file opened, paste nonessential code, or narrate search/tool chronology; report only unresolved blockers. Terse and factual: exact paths and line numbers, compressed evidence. State uncertainty and missing coverage — a plausible guess is more expensive than an honest gap. Keep the final response comfortably below the 40-line delivery cap unless the requested findings genuinely require more.
37
+ No preamble or closing summary. Do not repeat the task brief, inventory every file opened, paste nonessential code, or narrate the search; every line must carry a path with a fact or name a gap — delete anything else. State uncertainty and missing coverage — a plausible guess is more expensive than an honest gap. Stay under 15 lines by default; go longer only when the brief genuinely demands a wide survey — the 40-line delivery cap truncates your tail (usually the Gaps) and the caller pays for every line.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "4.2.2",
3
+ "version": "4.2.4",
4
4
  "description": "A managed sub-agent team for pi: specialized roles, pre-commit documentation sync, retained threads, auto-fix chains, model fallback, and Git worktree isolation.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/recovery.ts CHANGED
@@ -5,7 +5,7 @@ import { existsSync } from "node:fs";
5
5
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
6
6
  import { dirname, join } from "node:path";
7
7
  import { stripVTControlCharacters } from "node:util";
8
- import type { WorktreeFinalization } from "./worktree.ts";
8
+ import { removeWorktreeGroup, worktreeGroupDir, type WorktreeFinalization } from "./worktree.ts";
9
9
 
10
10
  export const RECOVERY_MANIFEST_FILE_NAME = "pi-subagents-recovery.json";
11
11
  const RECOVERY_MANIFEST_VERSION = 1;
@@ -14,6 +14,8 @@ export interface RecoveryRecord {
14
14
  runId: number;
15
15
  createdAt: number;
16
16
  integrated: boolean;
17
+ /** Repository a cleanup retry can prune stale worktree metadata against. */
18
+ originalRoot?: string;
17
19
  worktreePath?: string;
18
20
  patchPath?: string;
19
21
  error?: string;
@@ -37,6 +39,7 @@ function normalizeRecord(value: unknown): RecoveryRecord | undefined {
37
39
  runId: raw.runId,
38
40
  createdAt: raw.createdAt,
39
41
  integrated: raw.integrated === true,
42
+ ...(typeof raw.originalRoot === "string" && raw.originalRoot ? { originalRoot: raw.originalRoot } : {}),
40
43
  ...(typeof raw.worktreePath === "string" && raw.worktreePath ? { worktreePath: raw.worktreePath } : {}),
41
44
  ...(typeof raw.patchPath === "string" && raw.patchPath ? { patchPath: raw.patchPath } : {}),
42
45
  ...(typeof raw.error === "string" && raw.error ? { error: raw.error } : {}),
@@ -105,6 +108,7 @@ export function recoveryRecordFromFinalization(
105
108
  runId,
106
109
  createdAt: now,
107
110
  integrated: finalization.integrated,
111
+ ...(finalization.originalRoot ? { originalRoot: finalization.originalRoot } : {}),
108
112
  ...(finalization.worktreePath ? { worktreePath: finalization.worktreePath } : {}),
109
113
  ...(finalization.patchPath ? { patchPath: finalization.patchPath } : {}),
110
114
  ...(finalization.error ? { error: finalization.error } : {}),
@@ -112,7 +116,10 @@ export function recoveryRecordFromFinalization(
112
116
  }
113
117
 
114
118
  /** Show retained recovery paths on every later session start until the user
115
- * removes the artifacts. Stale records are pruned automatically. */
119
+ * removes the artifacts. Records whose changes already landed only need the
120
+ * worktree group deleted — the step whose failure retained them — so each
121
+ * session start retries that removal first and forgets records it completes.
122
+ * Stale records are pruned automatically. */
116
123
  export async function announceRecoveryRecords(
117
124
  configPath: string,
118
125
  ctx: {
@@ -123,6 +130,17 @@ export async function announceRecoveryRecords(
123
130
  if (ctx.hasUI === false) return;
124
131
  const records = await readRecoveryRecords(configPath);
125
132
  if (records.length === 0) return;
133
+ for (const record of records) {
134
+ if (!record.integrated || !record.worktreePath) continue;
135
+ const groupDir = worktreeGroupDir(record.worktreePath);
136
+ if (!groupDir) continue;
137
+ if (!existsSync(record.worktreePath) && !(record.patchPath ? existsSync(record.patchPath) : false)) continue;
138
+ await removeWorktreeGroup({
139
+ originalRoot: record.originalRoot,
140
+ worktreePath: record.worktreePath,
141
+ tempDir: groupDir,
142
+ });
143
+ }
126
144
  const live = records.filter((record) =>
127
145
  (record.worktreePath ? existsSync(record.worktreePath) : false) ||
128
146
  (record.patchPath ? existsSync(record.patchPath) : false),
@@ -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 async removeAndPrune(): Promise<string | undefined> {
653
- try {
654
- await runGit(
655
- this.runner,
656
- this.originalRoot,
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