@ferris1225/pi-subagents 4.1.18 → 4.1.21
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 +384 -337
- package/agents/cleaner.md +50 -45
- package/agents/documenter.md +40 -42
- package/agents/explorer.md +40 -45
- package/agents/reviewer.md +82 -82
- package/agents/synthesizer.md +39 -0
- package/agents/worker.md +43 -45
- package/package.json +55 -55
- package/src/agents.ts +25 -5
- package/src/announcements.ts +78 -75
- package/src/background.ts +11 -0
- package/src/completion.ts +19 -9
- package/src/config.ts +3 -10
- package/src/dispatch.ts +817 -647
- package/src/durable.ts +443 -402
- package/src/format.ts +173 -179
- package/src/index.ts +6 -6
- package/src/models.ts +4 -6
- package/src/monitor.ts +56 -5
- package/src/prompt.ts +14 -21
- package/src/recovery.ts +145 -145
- package/src/rpc-run.ts +993 -993
- package/src/runtime.ts +22 -4
- package/src/session-fork.ts +2 -0
- package/src/setup.ts +23 -43
- package/src/spawn.ts +668 -654
- package/src/temp-hygiene.ts +230 -174
- package/src/thread-lifecycle.ts +1487 -1399
- package/src/tools.ts +384 -712
- package/src/widget.ts +195 -157
- package/src/workflow.ts +24 -8
- package/src/worktree.ts +18 -0
package/src/prompt.ts
CHANGED
|
@@ -24,56 +24,49 @@ export function buildDelegationDirective(
|
|
|
24
24
|
const hasCleaner = agents.some((agent) => agent.name === "cleaner");
|
|
25
25
|
const hasDocumenter = agents.some((agent) => agent.name === "documenter");
|
|
26
26
|
const hasReviewer = agents.some((agent) => agent.name === "reviewer");
|
|
27
|
+
const hasSynthesizer = agents.some((agent) => agent.name === "synthesizer");
|
|
27
28
|
const codeWriterNames = [
|
|
28
29
|
...(hasWorker ? ["worker"] : []),
|
|
29
30
|
...(hasCleaner ? ["cleaner"] : []),
|
|
30
31
|
];
|
|
31
|
-
const namedWorktreeTargets = [
|
|
32
|
-
...(hasWorker ? ["worker"] : []),
|
|
33
|
-
...(hasCleaner ? ["cleaner"] : []),
|
|
34
|
-
...(hasDocumenter ? ["documenter"] : []),
|
|
35
|
-
];
|
|
36
|
-
const worktreeTargets = namedWorktreeTargets.length === 0
|
|
37
|
-
? "a"
|
|
38
|
-
: namedWorktreeTargets.length === 1
|
|
39
|
-
? `${namedWorktreeTargets[0]} or another`
|
|
40
|
-
: `${namedWorktreeTargets.slice(0, -1).join(", ")}, ${namedWorktreeTargets.at(-1)}, or another`;
|
|
41
32
|
|
|
42
33
|
const dispatchRules = [
|
|
43
|
-
`Delegate aggressively: child contexts are cheap, yours is scarce. Inline only trivial work — a
|
|
34
|
+
`Delegate aggressively: child contexts are cheap, yours is scarce. Inline only trivial work — a lookup, a single focused edit, an answer already in context${hasWorker ? "; default every non-trivial implementation, fix, refactor, or test task to `worker`" : ""}.`,
|
|
44
35
|
...(hasExplorer
|
|
45
36
|
? [
|
|
46
|
-
"`explorer`: broad
|
|
37
|
+
"`explorer`: split a broad question into parallel explorers with disjoint scopes. Its findings are leads, never proof — re-read load-bearing files before acting yourself (a child you brief re-verifies).",
|
|
47
38
|
]
|
|
48
39
|
: []),
|
|
49
40
|
...(hasCleaner
|
|
50
|
-
? ["`cleaner`:
|
|
41
|
+
? ["`cleaner`: dispatch for requested cleanup AND proactively when finished work leaves dead code or duplication. Your brief is its edit authorization — every safe proven cut applies without per-item approval; never a gate. Scope: the uncommitted diff, or whatever your brief names (Git range, directory)."]
|
|
51
42
|
: []),
|
|
52
43
|
...(hasDocumenter
|
|
53
|
-
? ["`documenter`: standalone docs/comment work
|
|
44
|
+
? ["`documenter`: standalone docs/comment work; dispatch it proactively when a change — yours or a child's — leaves README/docs/comment drift no writer already synced; cheap and may make zero edits."]
|
|
54
45
|
: []),
|
|
55
46
|
...(hasReviewer
|
|
56
47
|
? [
|
|
57
|
-
`\`reviewer\`: read-only assessments and gates${codeWriterNames.length > 0 ? `; successful ${codeWriterNames.join("/")} runs get one fresh gate, and failing gates are fixed by the reviewer itself in bounded fix/re-review rounds
|
|
48
|
+
`\`reviewer\`: read-only assessments and gates${codeWriterNames.length > 0 ? `; successful ${codeWriterNames.join("/")} runs get one fresh gate, and failing gates are fixed by the reviewer itself in bounded fix/re-review rounds (a still-failing gate returns to you). Pass \`review: "none"\` for mechanical, low-risk edits you verify yourself; keep the default gate whenever behavior can change` : ""}. Advisory output has no VERDICT and cannot authorize edits.`,
|
|
58
49
|
]
|
|
59
50
|
: []),
|
|
51
|
+
...(hasSynthesizer
|
|
52
|
+
? ["`synthesizer`: after a wide fan-out, pass the result-artifact paths to one synthesizer and read its brief instead of every result yourself."]
|
|
53
|
+
: []),
|
|
60
54
|
`Parallelize by default: map the todo list onto ONE \`tasks\` dispatch. One child owns one deliverable and its files; only genuinely dependent work waits for its prerequisite.`,
|
|
61
55
|
"Brief each child completely — goal, exact paths, constraints, expected output; it has no conversation memory and cannot delegate. Resume parked threads with `subagent_control resume`.",
|
|
62
|
-
`Request \`isolation: "worktree"\` only for ${worktreeTargets} write-capable agent in a repo with committed HEAD.`,
|
|
63
56
|
];
|
|
64
57
|
|
|
65
58
|
const handoffRules = [
|
|
66
|
-
"Dispatch never blocks or ends your turn — keep working; each completion resumes you automatically. Never sleep
|
|
59
|
+
"Dispatch never blocks or ends your turn — keep working; each completion resumes you automatically. Never sleep or poll for it.",
|
|
67
60
|
"Results are already shown; add only your conclusion or next action, never a restatement.",
|
|
68
|
-
"
|
|
61
|
+
"Never declare the overall task done while a dispatched run is still active.",
|
|
69
62
|
];
|
|
70
63
|
|
|
71
64
|
const verificationRules = [
|
|
72
65
|
"Never report an unrun check as passed; surface unavailable checks and pre-existing failures, and inspect actual changes before reporting completion.",
|
|
73
66
|
...(hasReviewer
|
|
74
67
|
? [
|
|
75
|
-
"A REVIEW_FAIL from a gate you dispatched directly returns its findings to you: fix them inline or via a briefed worker without waiting for the user
|
|
76
|
-
"Multi-model cross-review only when explicitly requested or for high-risk security,
|
|
68
|
+
"A REVIEW_FAIL from a gate you dispatched directly returns its findings to you: fix them inline or via a briefed worker without waiting for the user, then re-verify ONCE. If the gate still fails, report the remaining findings and move on — never loop gate dispatches.",
|
|
69
|
+
"Multi-model cross-review only when explicitly requested or for high-risk security, FFI, migration, or concurrency changes.",
|
|
77
70
|
]
|
|
78
71
|
: []),
|
|
79
72
|
"Commit or push only when explicitly requested, applicable checks pass, and no review finding remains unresolved.",
|
|
@@ -82,7 +75,7 @@ export function buildDelegationDirective(
|
|
|
82
75
|
return `
|
|
83
76
|
## Sub-agent delegation (pi-subagents)
|
|
84
77
|
|
|
85
|
-
\`subagent\` runs isolated leaf Pi child processes in the background
|
|
78
|
+
\`subagent\` runs isolated leaf Pi child processes in the background.
|
|
86
79
|
|
|
87
80
|
Agents:
|
|
88
81
|
${catalog}
|
package/src/recovery.ts
CHANGED
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
/** Durable handoff for worktree integration/cleanup failures across sessions. */
|
|
2
|
-
|
|
3
|
-
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
7
|
-
import { stripVTControlCharacters } from "node:util";
|
|
8
|
-
import type { WorktreeFinalization } from "./worktree.ts";
|
|
9
|
-
|
|
10
|
-
export const RECOVERY_MANIFEST_FILE_NAME = "pi-subagents-recovery.json";
|
|
11
|
-
const RECOVERY_MANIFEST_VERSION = 1;
|
|
12
|
-
|
|
13
|
-
export interface RecoveryRecord {
|
|
14
|
-
runId: number;
|
|
15
|
-
createdAt: number;
|
|
16
|
-
integrated: boolean;
|
|
17
|
-
worktreePath?: string;
|
|
18
|
-
patchPath?: string;
|
|
19
|
-
error?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface RecoveryManifest {
|
|
23
|
-
version: number;
|
|
24
|
-
records: RecoveryRecord[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function getRecoveryManifestPath(configPath: string): string {
|
|
28
|
-
return join(dirname(configPath), RECOVERY_MANIFEST_FILE_NAME);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function normalizeRecord(value: unknown): RecoveryRecord | undefined {
|
|
32
|
-
if (!value || typeof value !== "object") return undefined;
|
|
33
|
-
const raw = value as Record<string, unknown>;
|
|
34
|
-
if (typeof raw.runId !== "number" || !Number.isInteger(raw.runId) || raw.runId < 1) return undefined;
|
|
35
|
-
if (typeof raw.createdAt !== "number" || !Number.isFinite(raw.createdAt)) return undefined;
|
|
36
|
-
return {
|
|
37
|
-
runId: raw.runId,
|
|
38
|
-
createdAt: raw.createdAt,
|
|
39
|
-
integrated: raw.integrated === true,
|
|
40
|
-
...(typeof raw.worktreePath === "string" && raw.worktreePath ? { worktreePath: raw.worktreePath } : {}),
|
|
41
|
-
...(typeof raw.patchPath === "string" && raw.patchPath ? { patchPath: raw.patchPath } : {}),
|
|
42
|
-
...(typeof raw.error === "string" && raw.error ? { error: raw.error } : {}),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function readRecoveryRecords(configPath: string): Promise<RecoveryRecord[]> {
|
|
47
|
-
try {
|
|
48
|
-
const parsed = JSON.parse(await readFile(getRecoveryManifestPath(configPath), "utf8")) as {
|
|
49
|
-
records?: unknown;
|
|
50
|
-
};
|
|
51
|
-
if (!Array.isArray(parsed.records)) return [];
|
|
52
|
-
return parsed.records.flatMap((record) => {
|
|
53
|
-
const normalized = normalizeRecord(record);
|
|
54
|
-
return normalized ? [normalized] : [];
|
|
55
|
-
});
|
|
56
|
-
} catch {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function recoveryKey(record: RecoveryRecord): string {
|
|
62
|
-
return `${record.runId}\0${record.worktreePath ?? ""}\0${record.patchPath ?? ""}\0${record.error ?? ""}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function writeManifest(path: string, records: readonly RecoveryRecord[]): Promise<void> {
|
|
66
|
-
if (records.length === 0) {
|
|
67
|
-
await rm(path, { force: true });
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
await mkdir(dirname(path), { recursive: true });
|
|
71
|
-
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
72
|
-
try {
|
|
73
|
-
const manifest: RecoveryManifest = {
|
|
74
|
-
version: RECOVERY_MANIFEST_VERSION,
|
|
75
|
-
records: [...records],
|
|
76
|
-
};
|
|
77
|
-
await writeFile(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
78
|
-
await rename(temporaryPath, path);
|
|
79
|
-
} finally {
|
|
80
|
-
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Merge retained artifacts into the durable manifest. */
|
|
85
|
-
export async function persistRecoveryRecords(
|
|
86
|
-
configPath: string,
|
|
87
|
-
records: readonly RecoveryRecord[],
|
|
88
|
-
): Promise<void> {
|
|
89
|
-
if (records.length === 0) return;
|
|
90
|
-
const path = getRecoveryManifestPath(configPath);
|
|
91
|
-
await withFileMutationQueue(path, async () => {
|
|
92
|
-
const merged = new Map<string, RecoveryRecord>();
|
|
93
|
-
for (const record of await readRecoveryRecords(configPath)) merged.set(recoveryKey(record), record);
|
|
94
|
-
for (const record of records) merged.set(recoveryKey(record), record);
|
|
95
|
-
await writeManifest(path, [...merged.values()]);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function recoveryRecordFromFinalization(
|
|
100
|
-
runId: number,
|
|
101
|
-
finalization: WorktreeFinalization,
|
|
102
|
-
now = Date.now(),
|
|
103
|
-
): RecoveryRecord {
|
|
104
|
-
return {
|
|
105
|
-
runId,
|
|
106
|
-
createdAt: now,
|
|
107
|
-
integrated: finalization.integrated,
|
|
108
|
-
...(finalization.worktreePath ? { worktreePath: finalization.worktreePath } : {}),
|
|
109
|
-
...(finalization.patchPath ? { patchPath: finalization.patchPath } : {}),
|
|
110
|
-
...(finalization.error ? { error: finalization.error } : {}),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Show retained recovery paths on every later session start until the user
|
|
115
|
-
* removes the artifacts. Stale records are pruned automatically. */
|
|
116
|
-
export async function announceRecoveryRecords(
|
|
117
|
-
configPath: string,
|
|
118
|
-
ctx: {
|
|
119
|
-
hasUI?: boolean;
|
|
120
|
-
ui: { notify(message: string, kind: "info" | "warning" | "error"): void };
|
|
121
|
-
},
|
|
122
|
-
): Promise<void> {
|
|
123
|
-
if (ctx.hasUI === false) return;
|
|
124
|
-
const records = await readRecoveryRecords(configPath);
|
|
125
|
-
if (records.length === 0) return;
|
|
126
|
-
const live = records.filter((record) =>
|
|
127
|
-
(record.worktreePath ? existsSync(record.worktreePath) : false) ||
|
|
128
|
-
(record.patchPath ? existsSync(record.patchPath) : false),
|
|
129
|
-
);
|
|
130
|
-
if (live.length !== records.length) {
|
|
131
|
-
const path = getRecoveryManifestPath(configPath);
|
|
132
|
-
await withFileMutationQueue(path, () => writeManifest(path, live)).catch(() => undefined);
|
|
133
|
-
}
|
|
134
|
-
for (const record of live) {
|
|
135
|
-
const paths = [
|
|
136
|
-
record.worktreePath ? `worktree ${stripVTControlCharacters(record.worktreePath)}` : undefined,
|
|
137
|
-
record.patchPath ? `patch ${stripVTControlCharacters(record.patchPath)}` : undefined,
|
|
138
|
-
].filter(Boolean).join(" · ");
|
|
139
|
-
const reason = record.error ? ` · ${stripVTControlCharacters(record.error)}` : "";
|
|
140
|
-
ctx.ui.notify(
|
|
141
|
-
`pi-subagents recovery for run #${record.runId}: ${record.integrated ? "changes were applied but cleanup failed" : "integration failed"}${paths ? ` · retained ${paths}` : ""}${reason}`,
|
|
142
|
-
"error",
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
1
|
+
/** Durable handoff for worktree integration/cleanup failures across sessions. */
|
|
2
|
+
|
|
3
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { stripVTControlCharacters } from "node:util";
|
|
8
|
+
import type { WorktreeFinalization } from "./worktree.ts";
|
|
9
|
+
|
|
10
|
+
export const RECOVERY_MANIFEST_FILE_NAME = "pi-subagents-recovery.json";
|
|
11
|
+
const RECOVERY_MANIFEST_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
export interface RecoveryRecord {
|
|
14
|
+
runId: number;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
integrated: boolean;
|
|
17
|
+
worktreePath?: string;
|
|
18
|
+
patchPath?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface RecoveryManifest {
|
|
23
|
+
version: number;
|
|
24
|
+
records: RecoveryRecord[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getRecoveryManifestPath(configPath: string): string {
|
|
28
|
+
return join(dirname(configPath), RECOVERY_MANIFEST_FILE_NAME);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeRecord(value: unknown): RecoveryRecord | undefined {
|
|
32
|
+
if (!value || typeof value !== "object") return undefined;
|
|
33
|
+
const raw = value as Record<string, unknown>;
|
|
34
|
+
if (typeof raw.runId !== "number" || !Number.isInteger(raw.runId) || raw.runId < 1) return undefined;
|
|
35
|
+
if (typeof raw.createdAt !== "number" || !Number.isFinite(raw.createdAt)) return undefined;
|
|
36
|
+
return {
|
|
37
|
+
runId: raw.runId,
|
|
38
|
+
createdAt: raw.createdAt,
|
|
39
|
+
integrated: raw.integrated === true,
|
|
40
|
+
...(typeof raw.worktreePath === "string" && raw.worktreePath ? { worktreePath: raw.worktreePath } : {}),
|
|
41
|
+
...(typeof raw.patchPath === "string" && raw.patchPath ? { patchPath: raw.patchPath } : {}),
|
|
42
|
+
...(typeof raw.error === "string" && raw.error ? { error: raw.error } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function readRecoveryRecords(configPath: string): Promise<RecoveryRecord[]> {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(await readFile(getRecoveryManifestPath(configPath), "utf8")) as {
|
|
49
|
+
records?: unknown;
|
|
50
|
+
};
|
|
51
|
+
if (!Array.isArray(parsed.records)) return [];
|
|
52
|
+
return parsed.records.flatMap((record) => {
|
|
53
|
+
const normalized = normalizeRecord(record);
|
|
54
|
+
return normalized ? [normalized] : [];
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function recoveryKey(record: RecoveryRecord): string {
|
|
62
|
+
return `${record.runId}\0${record.worktreePath ?? ""}\0${record.patchPath ?? ""}\0${record.error ?? ""}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function writeManifest(path: string, records: readonly RecoveryRecord[]): Promise<void> {
|
|
66
|
+
if (records.length === 0) {
|
|
67
|
+
await rm(path, { force: true });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await mkdir(dirname(path), { recursive: true });
|
|
71
|
+
const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
72
|
+
try {
|
|
73
|
+
const manifest: RecoveryManifest = {
|
|
74
|
+
version: RECOVERY_MANIFEST_VERSION,
|
|
75
|
+
records: [...records],
|
|
76
|
+
};
|
|
77
|
+
await writeFile(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
78
|
+
await rename(temporaryPath, path);
|
|
79
|
+
} finally {
|
|
80
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Merge retained artifacts into the durable manifest. */
|
|
85
|
+
export async function persistRecoveryRecords(
|
|
86
|
+
configPath: string,
|
|
87
|
+
records: readonly RecoveryRecord[],
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
if (records.length === 0) return;
|
|
90
|
+
const path = getRecoveryManifestPath(configPath);
|
|
91
|
+
await withFileMutationQueue(path, async () => {
|
|
92
|
+
const merged = new Map<string, RecoveryRecord>();
|
|
93
|
+
for (const record of await readRecoveryRecords(configPath)) merged.set(recoveryKey(record), record);
|
|
94
|
+
for (const record of records) merged.set(recoveryKey(record), record);
|
|
95
|
+
await writeManifest(path, [...merged.values()]);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function recoveryRecordFromFinalization(
|
|
100
|
+
runId: number,
|
|
101
|
+
finalization: WorktreeFinalization,
|
|
102
|
+
now = Date.now(),
|
|
103
|
+
): RecoveryRecord {
|
|
104
|
+
return {
|
|
105
|
+
runId,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
integrated: finalization.integrated,
|
|
108
|
+
...(finalization.worktreePath ? { worktreePath: finalization.worktreePath } : {}),
|
|
109
|
+
...(finalization.patchPath ? { patchPath: finalization.patchPath } : {}),
|
|
110
|
+
...(finalization.error ? { error: finalization.error } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Show retained recovery paths on every later session start until the user
|
|
115
|
+
* removes the artifacts. Stale records are pruned automatically. */
|
|
116
|
+
export async function announceRecoveryRecords(
|
|
117
|
+
configPath: string,
|
|
118
|
+
ctx: {
|
|
119
|
+
hasUI?: boolean;
|
|
120
|
+
ui: { notify(message: string, kind: "info" | "warning" | "error"): void };
|
|
121
|
+
},
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
if (ctx.hasUI === false) return;
|
|
124
|
+
const records = await readRecoveryRecords(configPath);
|
|
125
|
+
if (records.length === 0) return;
|
|
126
|
+
const live = records.filter((record) =>
|
|
127
|
+
(record.worktreePath ? existsSync(record.worktreePath) : false) ||
|
|
128
|
+
(record.patchPath ? existsSync(record.patchPath) : false),
|
|
129
|
+
);
|
|
130
|
+
if (live.length !== records.length) {
|
|
131
|
+
const path = getRecoveryManifestPath(configPath);
|
|
132
|
+
await withFileMutationQueue(path, () => writeManifest(path, live)).catch(() => undefined);
|
|
133
|
+
}
|
|
134
|
+
for (const record of live) {
|
|
135
|
+
const paths = [
|
|
136
|
+
record.worktreePath ? `worktree ${stripVTControlCharacters(record.worktreePath)}` : undefined,
|
|
137
|
+
record.patchPath ? `patch ${stripVTControlCharacters(record.patchPath)}` : undefined,
|
|
138
|
+
].filter(Boolean).join(" · ");
|
|
139
|
+
const reason = record.error ? ` · ${stripVTControlCharacters(record.error)}` : "";
|
|
140
|
+
ctx.ui.notify(
|
|
141
|
+
`pi-subagents recovery for run #${record.runId}: ${record.integrated ? "changes were applied but cleanup failed" : "integration failed"}${paths ? ` · retained ${paths}` : ""}${reason}`,
|
|
142
|
+
"error",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|