pi-herdr-agents 0.0.3 → 0.0.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/package.json +1 -1
- package/pi-extension/subagents/claude.ts +164 -0
- package/pi-extension/subagents/index.ts +36 -159
package/package.json
CHANGED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { shellQuote } from "./terminal.ts";
|
|
12
|
+
|
|
13
|
+
const CLAUDE_SESSIONS_DIR = join(
|
|
14
|
+
process.env.HOME ?? "/tmp",
|
|
15
|
+
".pi",
|
|
16
|
+
"agent",
|
|
17
|
+
"sessions",
|
|
18
|
+
"claude-code",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export interface ClaudeWorkspace {
|
|
22
|
+
baseline?: Set<string>;
|
|
23
|
+
cwd?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ClaudeLaunchParams {
|
|
27
|
+
cwd: string;
|
|
28
|
+
sentinelFile: string;
|
|
29
|
+
pluginDir: string;
|
|
30
|
+
model?: string;
|
|
31
|
+
systemPrompt?: string;
|
|
32
|
+
resumeSessionId?: string;
|
|
33
|
+
task: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ClaudeCompletionParams extends ClaudeWorkspace {
|
|
37
|
+
sentinelFile: string;
|
|
38
|
+
exitCode: number;
|
|
39
|
+
readTerminal: () => string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ClaudeCompletion {
|
|
43
|
+
summary: string;
|
|
44
|
+
sessionId?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function requireClaudeAdapter(cli?: string): void {
|
|
48
|
+
if (cli && cli !== "claude") {
|
|
49
|
+
throw new Error(`Unsupported subagent CLI ${JSON.stringify(cli)}.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function captureClaudeWorkspaceBaseline(cwd: string): Set<string> | undefined {
|
|
54
|
+
try {
|
|
55
|
+
const output = execFileSync(
|
|
56
|
+
"git",
|
|
57
|
+
["status", "--porcelain=v1", "--untracked-files=all", "-z"],
|
|
58
|
+
{ cwd, encoding: "utf8" },
|
|
59
|
+
);
|
|
60
|
+
return new Set(
|
|
61
|
+
output
|
|
62
|
+
.split("\0")
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.map((entry) => entry.slice(3)),
|
|
65
|
+
);
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function cleanupClaudeWorkspace({ baseline, cwd }: ClaudeWorkspace): string | undefined {
|
|
72
|
+
if (!baseline || !cwd) return;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const output = execFileSync(
|
|
76
|
+
"git",
|
|
77
|
+
["status", "--porcelain=v1", "--untracked-files=all", "-z"],
|
|
78
|
+
{ cwd, encoding: "utf8" },
|
|
79
|
+
);
|
|
80
|
+
const changed = output
|
|
81
|
+
.split("\0")
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.map((entry) => ({ status: entry.slice(0, 2), path: entry.slice(3) }));
|
|
84
|
+
const cleaned: string[] = [];
|
|
85
|
+
|
|
86
|
+
for (const { status, path } of changed) {
|
|
87
|
+
if (baseline.has(path) || path.startsWith(".reviews/")) continue;
|
|
88
|
+
if (status === "??") {
|
|
89
|
+
rmSync(join(cwd, path), { recursive: true, force: true });
|
|
90
|
+
} else {
|
|
91
|
+
execFileSync("git", ["restore", "--staged", "--worktree", "--", path], {
|
|
92
|
+
cwd,
|
|
93
|
+
stdio: "ignore",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
cleaned.push(path);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return cleaned.length > 0
|
|
100
|
+
? `Claude workspace guard reverted newly introduced paths: ${cleaned.join(", ")}`
|
|
101
|
+
: undefined;
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
return `Claude workspace guard failed: ${error?.message ?? String(error)}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildClaudeLaunchCommand(params: ClaudeLaunchParams): string {
|
|
108
|
+
const parts = [
|
|
109
|
+
`PI_CLAUDE_SENTINEL=${shellQuote(params.sentinelFile)}`,
|
|
110
|
+
"claude",
|
|
111
|
+
"--dangerously-skip-permissions",
|
|
112
|
+
];
|
|
113
|
+
if (existsSync(params.pluginDir)) {
|
|
114
|
+
parts.push("--plugin-dir", shellQuote(params.pluginDir));
|
|
115
|
+
}
|
|
116
|
+
if (params.model) parts.push("--model", shellQuote(params.model));
|
|
117
|
+
if (params.systemPrompt) {
|
|
118
|
+
parts.push("--append-system-prompt", shellQuote(params.systemPrompt));
|
|
119
|
+
}
|
|
120
|
+
if (params.resumeSessionId) parts.push("--resume", shellQuote(params.resumeSessionId));
|
|
121
|
+
parts.push(shellQuote(params.task));
|
|
122
|
+
return `cd ${shellQuote(params.cwd)} && ${parts.join(" ")}; echo '__SUBAGENT_DONE_'$?'__'`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function copyClaudeSession(sentinelFile: string): string | undefined {
|
|
126
|
+
try {
|
|
127
|
+
const transcriptFile = `${sentinelFile}.transcript`;
|
|
128
|
+
if (!existsSync(transcriptFile)) return;
|
|
129
|
+
const transcriptPath = readFileSync(transcriptFile, "utf-8").trim();
|
|
130
|
+
if (!transcriptPath || !existsSync(transcriptPath)) return;
|
|
131
|
+
mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
|
|
132
|
+
const filename = transcriptPath.split("/").pop() ?? `claude-${Date.now()}.jsonl`;
|
|
133
|
+
copyFileSync(transcriptPath, join(CLAUDE_SESSIONS_DIR, filename));
|
|
134
|
+
return filename;
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function completeClaudeRun(params: ClaudeCompletionParams): ClaudeCompletion {
|
|
141
|
+
const guardMessage = cleanupClaudeWorkspace(params);
|
|
142
|
+
let summary = "";
|
|
143
|
+
try {
|
|
144
|
+
summary = readFileSync(params.sentinelFile, "utf-8").trim();
|
|
145
|
+
} catch {}
|
|
146
|
+
if (!summary) {
|
|
147
|
+
summary = params.readTerminal().replace(/__SUBAGENT_DONE_\d+__/, "").trimEnd();
|
|
148
|
+
}
|
|
149
|
+
if (!summary) {
|
|
150
|
+
summary =
|
|
151
|
+
params.exitCode !== 0
|
|
152
|
+
? `Claude Code exited with code ${params.exitCode}`
|
|
153
|
+
: "Claude Code exited without output";
|
|
154
|
+
}
|
|
155
|
+
if (guardMessage) summary += `\n\n${guardMessage}`;
|
|
156
|
+
|
|
157
|
+
const sessionId = copyClaudeSession(params.sentinelFile);
|
|
158
|
+
for (const file of [params.sentinelFile, `${params.sentinelFile}.transcript`]) {
|
|
159
|
+
try {
|
|
160
|
+
unlinkSync(file);
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
163
|
+
return { summary, ...(sessionId ? { sessionId } : {}) };
|
|
164
|
+
}
|
|
@@ -21,8 +21,6 @@ import {
|
|
|
21
21
|
writeFileSync,
|
|
22
22
|
existsSync,
|
|
23
23
|
mkdirSync,
|
|
24
|
-
copyFileSync,
|
|
25
|
-
unlinkSync,
|
|
26
24
|
rmSync,
|
|
27
25
|
renameSync,
|
|
28
26
|
statSync,
|
|
@@ -45,6 +43,13 @@ import {
|
|
|
45
43
|
waitForProcessesExit,
|
|
46
44
|
} from "./terminal.ts";
|
|
47
45
|
import { waitForCompletion } from "./completion.ts";
|
|
46
|
+
import {
|
|
47
|
+
buildClaudeLaunchCommand,
|
|
48
|
+
captureClaudeWorkspaceBaseline,
|
|
49
|
+
cleanupClaudeWorkspace,
|
|
50
|
+
completeClaudeRun,
|
|
51
|
+
requireClaudeAdapter,
|
|
52
|
+
} from "./claude.ts";
|
|
48
53
|
import {
|
|
49
54
|
buildAuthenticatedModelCatalog,
|
|
50
55
|
resolveRuntimePlan,
|
|
@@ -804,69 +809,6 @@ function formatElapsed(seconds: number): string {
|
|
|
804
809
|
* (for example direnv/devenv), so the delay is configurable for users who hit
|
|
805
810
|
* dropped commands. Keep the historical default at 500ms.
|
|
806
811
|
*/
|
|
807
|
-
function captureWorkspaceBaseline(cwd: string): Set<string> | undefined {
|
|
808
|
-
try {
|
|
809
|
-
const output = execFileSync(
|
|
810
|
-
"git",
|
|
811
|
-
["status", "--porcelain=v1", "--untracked-files=all", "-z"],
|
|
812
|
-
{ cwd, encoding: "utf8" },
|
|
813
|
-
);
|
|
814
|
-
return new Set(
|
|
815
|
-
output
|
|
816
|
-
.split("\0")
|
|
817
|
-
.filter(Boolean)
|
|
818
|
-
.map((entry) => entry.slice(3)),
|
|
819
|
-
);
|
|
820
|
-
} catch {
|
|
821
|
-
return undefined;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
function guardClaudeWorkspace(running: RunningSubagent): string | undefined {
|
|
826
|
-
if (!running.workspaceBaseline || !running.workspaceCwd) return;
|
|
827
|
-
|
|
828
|
-
try {
|
|
829
|
-
const output = execFileSync(
|
|
830
|
-
"git",
|
|
831
|
-
["status", "--porcelain=v1", "--untracked-files=all", "-z"],
|
|
832
|
-
{ cwd: running.workspaceCwd, encoding: "utf8" },
|
|
833
|
-
);
|
|
834
|
-
const changed = output
|
|
835
|
-
.split("\0")
|
|
836
|
-
.filter(Boolean)
|
|
837
|
-
.map((entry) => ({
|
|
838
|
-
status: entry.slice(0, 2),
|
|
839
|
-
path: entry.slice(3),
|
|
840
|
-
}));
|
|
841
|
-
const newPaths = changed.filter(
|
|
842
|
-
({ path }) => !running.workspaceBaseline!.has(path),
|
|
843
|
-
);
|
|
844
|
-
const cleaned: string[] = [];
|
|
845
|
-
|
|
846
|
-
for (const { status, path } of newPaths) {
|
|
847
|
-
if (path.startsWith(".reviews/")) continue;
|
|
848
|
-
if (status === "??") {
|
|
849
|
-
rmSync(`${running.workspaceCwd}/${path}`, {
|
|
850
|
-
recursive: true,
|
|
851
|
-
force: true,
|
|
852
|
-
});
|
|
853
|
-
} else {
|
|
854
|
-
execFileSync("git", ["restore", "--staged", "--worktree", "--", path], {
|
|
855
|
-
cwd: running.workspaceCwd,
|
|
856
|
-
stdio: "ignore",
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
cleaned.push(path);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
return cleaned.length > 0
|
|
863
|
-
? `Claude workspace guard reverted newly introduced paths: ${cleaned.join(", ")}`
|
|
864
|
-
: undefined;
|
|
865
|
-
} catch (error: any) {
|
|
866
|
-
return `Claude workspace guard failed: ${error?.message ?? String(error)}`;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
812
|
function getShellReadyDelayMs(): number {
|
|
871
813
|
const raw = process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS?.trim();
|
|
872
814
|
const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
|
|
@@ -2119,6 +2061,7 @@ async function launchSubagent(
|
|
|
2119
2061
|
diagnostic?.message ?? `Agent "${params.agent}" was not found.`,
|
|
2120
2062
|
);
|
|
2121
2063
|
}
|
|
2064
|
+
requireClaudeAdapter(agentDefs?.cli);
|
|
2122
2065
|
if (!ctx.model)
|
|
2123
2066
|
throw new Error("Subagent launch requires a resolved parent model");
|
|
2124
2067
|
const runtimePlan = resolveRuntimePlan(
|
|
@@ -2244,7 +2187,7 @@ async function launchSubagent(
|
|
|
2244
2187
|
);
|
|
2245
2188
|
const workspaceBaseline =
|
|
2246
2189
|
agentDefs?.cli === "claude" && !worktree
|
|
2247
|
-
?
|
|
2190
|
+
? captureClaudeWorkspaceBaseline(targetCwdForSession)
|
|
2248
2191
|
: undefined;
|
|
2249
2192
|
|
|
2250
2193
|
// Generate a deterministic session file path for this subagent.
|
|
@@ -2312,35 +2255,15 @@ async function launchSubagent(
|
|
|
2312
2255
|
const sentinelFile = `/tmp/pi-claude-${id}-done`;
|
|
2313
2256
|
const pluginDir = join(SUBAGENTS_DIR, "plugin");
|
|
2314
2257
|
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
const cliModel = agentDefs.cliModel ?? effectiveModel;
|
|
2325
|
-
if (cliModel) {
|
|
2326
|
-
cmdParts.push("--model", shellQuote(cliModel));
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
const sp = params.systemPrompt ?? agentDefs.body;
|
|
2330
|
-
if (sp) {
|
|
2331
|
-
cmdParts.push("--append-system-prompt", shellQuote(sp));
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
if (params.resumeSessionId) {
|
|
2335
|
-
cmdParts.push("--resume", shellQuote(params.resumeSessionId));
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
// Always pass the task as the prompt — even for resumed sessions,
|
|
2339
|
-
// the caller's task is the follow-up instruction.
|
|
2340
|
-
cmdParts.push(shellQuote(params.task));
|
|
2341
|
-
|
|
2342
|
-
const cdPrefix = `cd ${shellQuote(targetCwdForSession)} && `;
|
|
2343
|
-
const command = `${cdPrefix}${cmdParts.join(" ")}; echo '__SUBAGENT_DONE_'$?'__'`;
|
|
2258
|
+
const command = buildClaudeLaunchCommand({
|
|
2259
|
+
cwd: targetCwdForSession,
|
|
2260
|
+
sentinelFile,
|
|
2261
|
+
pluginDir,
|
|
2262
|
+
model: agentDefs.cliModel ?? effectiveModel,
|
|
2263
|
+
systemPrompt: params.systemPrompt ?? agentDefs.body,
|
|
2264
|
+
resumeSessionId: params.resumeSessionId,
|
|
2265
|
+
task: params.task,
|
|
2266
|
+
});
|
|
2344
2267
|
|
|
2345
2268
|
const launchScriptName = `${
|
|
2346
2269
|
(params.name || "subagent")
|
|
@@ -2561,31 +2484,6 @@ async function launchSubagent(
|
|
|
2561
2484
|
* the summary from the session file, and closes ordinary panes. Worktree
|
|
2562
2485
|
* workspaces are retained for parent review.
|
|
2563
2486
|
*/
|
|
2564
|
-
const CLAUDE_SESSIONS_DIR = join(
|
|
2565
|
-
process.env.HOME ?? "/tmp",
|
|
2566
|
-
".pi",
|
|
2567
|
-
"agent",
|
|
2568
|
-
"sessions",
|
|
2569
|
-
"claude-code",
|
|
2570
|
-
);
|
|
2571
|
-
|
|
2572
|
-
function copyClaudeSession(sentinelFile: string): string | null {
|
|
2573
|
-
try {
|
|
2574
|
-
const transcriptFile = sentinelFile + ".transcript";
|
|
2575
|
-
if (!existsSync(transcriptFile)) return null;
|
|
2576
|
-
const transcriptPath = readFileSync(transcriptFile, "utf-8").trim();
|
|
2577
|
-
if (!transcriptPath || !existsSync(transcriptPath)) return null;
|
|
2578
|
-
mkdirSync(CLAUDE_SESSIONS_DIR, { recursive: true });
|
|
2579
|
-
const filename =
|
|
2580
|
-
transcriptPath.split("/").pop() ?? `claude-${Date.now()}.jsonl`;
|
|
2581
|
-
const dest = join(CLAUDE_SESSIONS_DIR, filename);
|
|
2582
|
-
copyFileSync(transcriptPath, dest);
|
|
2583
|
-
return filename;
|
|
2584
|
-
} catch {
|
|
2585
|
-
return null;
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
2487
|
async function watchSubagent(
|
|
2590
2488
|
running: RunningSubagent,
|
|
2591
2489
|
signal: AbortSignal,
|
|
@@ -2623,41 +2521,13 @@ async function watchSubagent(
|
|
|
2623
2521
|
const elapsed = Math.floor((detectedAt - startTime) / 1000);
|
|
2624
2522
|
|
|
2625
2523
|
if (running.cli === "claude") {
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
} catch {}
|
|
2634
|
-
}
|
|
2635
|
-
|
|
2636
|
-
if (!summary) {
|
|
2637
|
-
summary = readPane(surface, 200)
|
|
2638
|
-
.replace(/__SUBAGENT_DONE_\d+__/, "")
|
|
2639
|
-
.trimEnd();
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
if (!summary) {
|
|
2643
|
-
summary =
|
|
2644
|
-
result.exitCode !== 0
|
|
2645
|
-
? `Claude Code exited with code ${result.exitCode}`
|
|
2646
|
-
: "Claude Code exited without output";
|
|
2647
|
-
}
|
|
2648
|
-
if (guardMessage) summary += `\n\n${guardMessage}`;
|
|
2649
|
-
|
|
2650
|
-
// Copy Claude session transcript
|
|
2651
|
-
let sessionId: string | null = null;
|
|
2652
|
-
if (running.sentinelFile) {
|
|
2653
|
-
sessionId = copyClaudeSession(running.sentinelFile);
|
|
2654
|
-
try {
|
|
2655
|
-
unlinkSync(running.sentinelFile);
|
|
2656
|
-
} catch {}
|
|
2657
|
-
try {
|
|
2658
|
-
unlinkSync(running.sentinelFile + ".transcript");
|
|
2659
|
-
} catch {}
|
|
2660
|
-
}
|
|
2524
|
+
const claudeCompletion = completeClaudeRun({
|
|
2525
|
+
sentinelFile: running.sentinelFile!,
|
|
2526
|
+
exitCode: result.exitCode,
|
|
2527
|
+
baseline: running.workspaceBaseline,
|
|
2528
|
+
cwd: running.workspaceCwd,
|
|
2529
|
+
readTerminal: () => readPane(surface, 200),
|
|
2530
|
+
});
|
|
2661
2531
|
|
|
2662
2532
|
const worktreeHandoff = finalizeSubagentSurface(
|
|
2663
2533
|
running,
|
|
@@ -2668,7 +2538,7 @@ async function watchSubagent(
|
|
|
2668
2538
|
? markCompleted(running.lifecycle, Date.now())
|
|
2669
2539
|
: markFailed(
|
|
2670
2540
|
running.lifecycle,
|
|
2671
|
-
result.errorMessage ?? summary,
|
|
2541
|
+
result.errorMessage ?? claudeCompletion.summary,
|
|
2672
2542
|
Date.now(),
|
|
2673
2543
|
result.exitCode,
|
|
2674
2544
|
);
|
|
@@ -2676,10 +2546,12 @@ async function watchSubagent(
|
|
|
2676
2546
|
return {
|
|
2677
2547
|
name,
|
|
2678
2548
|
task,
|
|
2679
|
-
summary,
|
|
2549
|
+
summary: claudeCompletion.summary,
|
|
2680
2550
|
exitCode: result.exitCode,
|
|
2681
2551
|
elapsed,
|
|
2682
|
-
...(sessionId
|
|
2552
|
+
...(claudeCompletion.sessionId
|
|
2553
|
+
? { claudeSessionId: claudeCompletion.sessionId }
|
|
2554
|
+
: {}),
|
|
2683
2555
|
...(worktreeHandoff ? { worktree: worktreeHandoff } : {}),
|
|
2684
2556
|
};
|
|
2685
2557
|
}
|
|
@@ -2761,7 +2633,12 @@ async function watchSubagent(
|
|
|
2761
2633
|
};
|
|
2762
2634
|
} catch (err: any) {
|
|
2763
2635
|
const guardMessage =
|
|
2764
|
-
running.cli === "claude"
|
|
2636
|
+
running.cli === "claude"
|
|
2637
|
+
? cleanupClaudeWorkspace({
|
|
2638
|
+
baseline: running.workspaceBaseline,
|
|
2639
|
+
cwd: running.workspaceCwd,
|
|
2640
|
+
})
|
|
2641
|
+
: undefined;
|
|
2765
2642
|
const worktreeHandoff = finalizeSubagentSurface(running, "failed", true);
|
|
2766
2643
|
running.lifecycle = markFailed(
|
|
2767
2644
|
running.lifecycle,
|