pi-herdr-subagents 0.1.0
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/.github/workflows/publish.yml +55 -0
- package/.pi/settings.json +8 -0
- package/.pi/skills/run-integration-tests/SKILL.md +28 -0
- package/LICENSE +21 -0
- package/README.md +483 -0
- package/RELEASING.md +103 -0
- package/agents/planner.md +546 -0
- package/agents/reviewer.md +150 -0
- package/agents/scout.md +104 -0
- package/agents/visual-tester.md +197 -0
- package/agents/worker.md +103 -0
- package/config.json.example +5 -0
- package/package.json +34 -0
- package/pi-extension/subagents/activity.ts +511 -0
- package/pi-extension/subagents/completion.ts +114 -0
- package/pi-extension/subagents/herdr.ts +200 -0
- package/pi-extension/subagents/index.ts +2182 -0
- package/pi-extension/subagents/plan-skill.md +203 -0
- package/pi-extension/subagents/plugin/.claude-plugin/plugin.json +5 -0
- package/pi-extension/subagents/plugin/hooks/hooks.json +15 -0
- package/pi-extension/subagents/plugin/hooks/on-stop.sh +68 -0
- package/pi-extension/subagents/session.ts +180 -0
- package/pi-extension/subagents/status.ts +513 -0
- package/pi-extension/subagents/subagent-done.ts +324 -0
- package/pi-extension/subagents/terminal.ts +106 -0
- package/test/integration/agents/test-echo.md +13 -0
- package/test/integration/agents/test-ping.md +11 -0
- package/test/integration/harness.ts +319 -0
- package/test/integration/mux-surface.test.ts +225 -0
- package/test/integration/subagent-lifecycle.test.ts +329 -0
- package/test/system-prompt-mode.test.ts +163 -0
- package/test/test.ts +2190 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { execFile, execSync, execFileSync } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
|
|
6
|
+
const commandAvailability = new Map<string, boolean>();
|
|
7
|
+
|
|
8
|
+
function hasCommand(command: string): boolean {
|
|
9
|
+
if (commandAvailability.has(command)) {
|
|
10
|
+
return commandAvailability.get(command)!;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let available = false;
|
|
14
|
+
if (process.platform === "win32") {
|
|
15
|
+
try {
|
|
16
|
+
execFileSync("where.exe", [command], { stdio: "ignore" });
|
|
17
|
+
available = true;
|
|
18
|
+
} catch {
|
|
19
|
+
try {
|
|
20
|
+
execSync(`command -v ${command}`, { stdio: "ignore" });
|
|
21
|
+
available = true;
|
|
22
|
+
} catch {
|
|
23
|
+
available = false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
try {
|
|
28
|
+
execSync(`command -v ${command}`, { stdio: "ignore" });
|
|
29
|
+
available = true;
|
|
30
|
+
} catch {
|
|
31
|
+
available = false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
commandAvailability.set(command, available);
|
|
36
|
+
return available;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isHerdrAvailable(): boolean {
|
|
40
|
+
return process.env.HERDR_ENV === "1" && hasCommand("herdr");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseHerdrJson(value: string): unknown {
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(value);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function extractHerdrPaneId(output: string, context: string): string {
|
|
52
|
+
const parsed = parseHerdrJson(output);
|
|
53
|
+
const paneId = (parsed as { result?: { pane?: { pane_id?: unknown } } })?.result?.pane?.pane_id;
|
|
54
|
+
if (typeof paneId !== "string" || !paneId) {
|
|
55
|
+
throw new Error(`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`);
|
|
56
|
+
}
|
|
57
|
+
return paneId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function extractHerdrRootPaneId(output: string, context: string): string {
|
|
61
|
+
const parsed = parseHerdrJson(output);
|
|
62
|
+
const paneId = (parsed as { result?: { root_pane?: { pane_id?: unknown } } })?.result?.root_pane
|
|
63
|
+
?.pane_id;
|
|
64
|
+
if (typeof paneId !== "string" || !paneId) {
|
|
65
|
+
throw new Error(`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`);
|
|
66
|
+
}
|
|
67
|
+
return paneId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function herdrExec(args: string[]): string {
|
|
71
|
+
return execFileSync("herdr", args, { encoding: "utf8" });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function herdrExecAsync(args: string[]): Promise<string> {
|
|
75
|
+
const { stdout } = await execFileAsync("herdr", args, { encoding: "utf8" });
|
|
76
|
+
return stdout;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getHerdrParentPaneId(): string {
|
|
80
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
81
|
+
if (!paneId) {
|
|
82
|
+
throw new Error("HERDR_PANE_ID not set");
|
|
83
|
+
}
|
|
84
|
+
return paneId;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getHerdrCurrentPaneInfo(): {
|
|
88
|
+
pane_id: string;
|
|
89
|
+
tab_id: string;
|
|
90
|
+
workspace_id: string;
|
|
91
|
+
} {
|
|
92
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
93
|
+
const tabId = process.env.HERDR_TAB_ID;
|
|
94
|
+
const workspaceId = process.env.HERDR_WORKSPACE_ID;
|
|
95
|
+
|
|
96
|
+
// Fall back to `herdr pane current` if any identity env var is missing —
|
|
97
|
+
// older herdr versions may not set all three.
|
|
98
|
+
if (!paneId || !tabId || !workspaceId) {
|
|
99
|
+
const output = herdrExec(["pane", "current"]);
|
|
100
|
+
const parsed = parseHerdrJson(output);
|
|
101
|
+
const pane = (parsed as { result?: { pane?: unknown } } | null)?.result?.pane as
|
|
102
|
+
| { pane_id?: string; tab_id?: string; workspace_id?: string }
|
|
103
|
+
| undefined;
|
|
104
|
+
if (!pane?.pane_id || !pane?.tab_id || !pane?.workspace_id) {
|
|
105
|
+
throw new Error(`Unexpected herdr pane current output: ${output.trim() || "(empty)"}`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
pane_id: pane.pane_id,
|
|
109
|
+
tab_id: pane.tab_id,
|
|
110
|
+
workspace_id: pane.workspace_id,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { pane_id: paneId, tab_id: tabId, workspace_id: workspaceId };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createHerdrSurface(name: string): string {
|
|
118
|
+
// Create a new tab per subagent so parallel spawns each get a full tab
|
|
119
|
+
// instead of ever-narrower splits of the parent pane.
|
|
120
|
+
const output = herdrExec([
|
|
121
|
+
"tab",
|
|
122
|
+
"create",
|
|
123
|
+
"--label",
|
|
124
|
+
name,
|
|
125
|
+
"--cwd",
|
|
126
|
+
process.cwd(),
|
|
127
|
+
"--no-focus",
|
|
128
|
+
]);
|
|
129
|
+
const paneId = extractHerdrRootPaneId(output, "tab create");
|
|
130
|
+
try {
|
|
131
|
+
herdrExec(["pane", "rename", paneId, name]);
|
|
132
|
+
} catch {
|
|
133
|
+
// Optional — pane label is cosmetic.
|
|
134
|
+
}
|
|
135
|
+
return paneId;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createHerdrSurfaceSplit(
|
|
139
|
+
name: string,
|
|
140
|
+
direction: "right" | "down",
|
|
141
|
+
): string {
|
|
142
|
+
const parentPaneId = getHerdrParentPaneId();
|
|
143
|
+
const output = herdrExec([
|
|
144
|
+
"pane",
|
|
145
|
+
"split",
|
|
146
|
+
parentPaneId,
|
|
147
|
+
"--direction",
|
|
148
|
+
direction,
|
|
149
|
+
"--no-focus",
|
|
150
|
+
"--cwd",
|
|
151
|
+
process.cwd(),
|
|
152
|
+
]);
|
|
153
|
+
const paneId = extractHerdrPaneId(output, "pane split");
|
|
154
|
+
try {
|
|
155
|
+
herdrExec(["pane", "rename", paneId, name]);
|
|
156
|
+
} catch {
|
|
157
|
+
// Optional.
|
|
158
|
+
}
|
|
159
|
+
return paneId;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function readHerdrScreen(surface: string, lines = 50): string {
|
|
163
|
+
// `visible` is reliable for freshly created panes where herdr's `recent`
|
|
164
|
+
// scrollback may not be populated yet.
|
|
165
|
+
return herdrExec(["pane", "read", surface, "--source", "visible", "--lines", String(lines)]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function readHerdrScreenAsync(surface: string, lines = 50): Promise<string> {
|
|
169
|
+
return herdrExecAsync(["pane", "read", surface, "--source", "visible", "--lines", String(lines)]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function sendHerdrCommand(surface: string, command: string): void {
|
|
173
|
+
// pane run sends the text and Enter in a single socket request, avoiding
|
|
174
|
+
// a race where Enter could arrive before the text is fully processed.
|
|
175
|
+
herdrExec(["pane", "run", surface, command]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function sendHerdrEscape(surface: string): void {
|
|
179
|
+
herdrExec(["pane", "send-keys", surface, "Escape"]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function closeHerdrSurface(surface: string): void {
|
|
183
|
+
herdrExec(["pane", "close", surface]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function renameHerdrTab(title: string): void {
|
|
187
|
+
const { tab_id: tabId } = getHerdrCurrentPaneInfo();
|
|
188
|
+
herdrExec(["tab", "rename", tabId, title]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function renameHerdrWorkspace(title: string): void {
|
|
192
|
+
const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
|
|
193
|
+
herdrExec(["workspace", "rename", workspaceId, title]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const __herdrTest__ = {
|
|
197
|
+
parseHerdrJson,
|
|
198
|
+
extractHerdrPaneId,
|
|
199
|
+
extractHerdrRootPaneId,
|
|
200
|
+
};
|