pi-herdr-agents 0.0.1
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/AGENTS.md +116 -0
- package/CONTEXT.md +159 -0
- package/LICENSE +21 -0
- package/README.md +874 -0
- package/RELEASING.md +139 -0
- package/agents/adversarial-reviewer.md +80 -0
- package/agents/claude-reviewer.md +23 -0
- package/agents/planner.md +539 -0
- package/agents/poteto.md +32 -0
- package/agents/reviewer.md +164 -0
- package/agents/scout.md +106 -0
- package/agents/visual-tester.md +224 -0
- package/agents/worker.md +132 -0
- package/config.json.example +8 -0
- package/docs/README.md +42 -0
- package/docs/adr/0001-btw-ephemeral-side-questions.md +142 -0
- package/docs/adr/0002-agent-workflow-skill-runtime-taxonomy.md +265 -0
- package/docs/adr/0003-installable-role-packs.md +135 -0
- package/docs/adr/0004-require-active-user-approval-for-workflow-execution.md +17 -0
- package/docs/adr/0005-parent-owns-workflow-script-authority.md +17 -0
- package/docs/adr/0006-limit-v1-execution-effects-to-isolated-worktrees.md +18 -0
- package/docs/adr/0007-require-fresh-review-for-workflow-scripts.md +19 -0
- package/docs/orchestrated-review-workflow-plan.md +479 -0
- package/docs/research/pdw-architecture-assessment.md +525 -0
- package/docs/research/pi-workflows-sol-advisor.md +255 -0
- package/docs/research/worktree-subagent-orchestration.md +317 -0
- package/docs/worktree-subagents.md +196 -0
- package/examples/role-pack/extension.ts +18 -0
- package/examples/role-pack/package.json +16 -0
- package/examples/role-pack/roles/example-reviewer.md +12 -0
- package/package.json +58 -0
- package/pi-extension/subagents/activity.ts +511 -0
- package/pi-extension/subagents/completion.ts +177 -0
- package/pi-extension/subagents/herdr.ts +541 -0
- package/pi-extension/subagents/index.ts +4730 -0
- package/pi-extension/subagents/lifecycle.ts +477 -0
- package/pi-extension/subagents/model-config.ts +95 -0
- package/pi-extension/subagents/plan-skill.md +262 -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/runtime-routing.ts +313 -0
- package/pi-extension/subagents/session.ts +216 -0
- package/pi-extension/subagents/status.ts +513 -0
- package/pi-extension/subagents/subagent-done.ts +326 -0
- package/pi-extension/subagents/terminal.ts +163 -0
- package/pi-extension/subagents/workflow-worker.js +56 -0
- package/pi-extension/subagents/workflow.ts +1210 -0
- package/skills/orchestrate/SKILL.md +184 -0
|
@@ -0,0 +1,541 @@
|
|
|
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 } } })
|
|
54
|
+
?.result?.pane?.pane_id;
|
|
55
|
+
if (typeof paneId !== "string" || !paneId) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return paneId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractHerdrRootPaneId(output: string, context: string): string {
|
|
64
|
+
const parsed = parseHerdrJson(output);
|
|
65
|
+
const paneId = (parsed as { result?: { root_pane?: { pane_id?: unknown } } })
|
|
66
|
+
?.result?.root_pane?.pane_id;
|
|
67
|
+
if (typeof paneId !== "string" || !paneId) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return paneId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface HerdrWorktreeSurface {
|
|
76
|
+
path: string;
|
|
77
|
+
branch: string;
|
|
78
|
+
workspaceId: string;
|
|
79
|
+
paneId: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractHerdrWorktree(output: string): HerdrWorktreeSurface {
|
|
83
|
+
const parsed = parseHerdrJson(output) as {
|
|
84
|
+
result?: {
|
|
85
|
+
type?: unknown;
|
|
86
|
+
workspace?: { workspace_id?: unknown };
|
|
87
|
+
root_pane?: { pane_id?: unknown };
|
|
88
|
+
worktree?: { path?: unknown; branch?: unknown };
|
|
89
|
+
};
|
|
90
|
+
} | null;
|
|
91
|
+
const result = parsed?.result;
|
|
92
|
+
if (
|
|
93
|
+
result?.type !== "worktree_created" ||
|
|
94
|
+
typeof result.workspace?.workspace_id !== "string" ||
|
|
95
|
+
!result.workspace.workspace_id ||
|
|
96
|
+
typeof result.root_pane?.pane_id !== "string" ||
|
|
97
|
+
!result.root_pane.pane_id ||
|
|
98
|
+
typeof result.worktree?.path !== "string" ||
|
|
99
|
+
!result.worktree.path ||
|
|
100
|
+
typeof result.worktree.branch !== "string" ||
|
|
101
|
+
!result.worktree.branch
|
|
102
|
+
) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Unexpected herdr worktree create output: ${output.trim() || "(empty)"}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
path: result.worktree.path,
|
|
109
|
+
branch: result.worktree.branch,
|
|
110
|
+
workspaceId: result.workspace.workspace_id,
|
|
111
|
+
paneId: result.root_pane.pane_id,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function herdrExec(args: string[]): string {
|
|
116
|
+
return execFileSync("herdr", args, { encoding: "utf8" });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function herdrExecAsync(args: string[]): Promise<string> {
|
|
120
|
+
const { stdout } = await execFileAsync("herdr", args, { encoding: "utf8" });
|
|
121
|
+
return stdout;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getHerdrParentPaneId(): string {
|
|
125
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
126
|
+
if (!paneId) {
|
|
127
|
+
throw new Error("HERDR_PANE_ID not set");
|
|
128
|
+
}
|
|
129
|
+
return paneId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getHerdrCurrentPaneInfo(): {
|
|
133
|
+
pane_id: string;
|
|
134
|
+
tab_id: string;
|
|
135
|
+
workspace_id: string;
|
|
136
|
+
} {
|
|
137
|
+
const paneId = process.env.HERDR_PANE_ID;
|
|
138
|
+
const tabId = process.env.HERDR_TAB_ID;
|
|
139
|
+
const workspaceId = process.env.HERDR_WORKSPACE_ID;
|
|
140
|
+
|
|
141
|
+
// Fall back to `herdr pane current` if any identity env var is missing —
|
|
142
|
+
// older herdr versions may not set all three.
|
|
143
|
+
if (!paneId || !tabId || !workspaceId) {
|
|
144
|
+
const output = herdrExec(["pane", "current"]);
|
|
145
|
+
const parsed = parseHerdrJson(output);
|
|
146
|
+
const pane = (parsed as { result?: { pane?: unknown } } | null)?.result
|
|
147
|
+
?.pane as
|
|
148
|
+
| { pane_id?: string; tab_id?: string; workspace_id?: string }
|
|
149
|
+
| undefined;
|
|
150
|
+
if (!pane?.pane_id || !pane?.tab_id || !pane?.workspace_id) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Unexpected herdr pane current output: ${output.trim() || "(empty)"}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
pane_id: pane.pane_id,
|
|
157
|
+
tab_id: pane.tab_id,
|
|
158
|
+
workspace_id: pane.workspace_id,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { pane_id: paneId, tab_id: tabId, workspace_id: workspaceId };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildTabCreateArgs(
|
|
166
|
+
name: string,
|
|
167
|
+
cwd: string,
|
|
168
|
+
workspaceId: string,
|
|
169
|
+
): string[] {
|
|
170
|
+
return [
|
|
171
|
+
"tab",
|
|
172
|
+
"create",
|
|
173
|
+
"--workspace",
|
|
174
|
+
workspaceId,
|
|
175
|
+
"--label",
|
|
176
|
+
name,
|
|
177
|
+
"--cwd",
|
|
178
|
+
cwd,
|
|
179
|
+
"--no-focus",
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildWorktreeCreateArgs(
|
|
184
|
+
name: string,
|
|
185
|
+
cwd: string,
|
|
186
|
+
branch: string,
|
|
187
|
+
base: string,
|
|
188
|
+
): string[] {
|
|
189
|
+
return [
|
|
190
|
+
"worktree",
|
|
191
|
+
"create",
|
|
192
|
+
"--cwd",
|
|
193
|
+
cwd,
|
|
194
|
+
"--branch",
|
|
195
|
+
branch,
|
|
196
|
+
"--base",
|
|
197
|
+
base,
|
|
198
|
+
"--label",
|
|
199
|
+
name,
|
|
200
|
+
"--no-focus",
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function createHerdrSurface(name: string): string {
|
|
205
|
+
// Create a new tab per subagent so parallel spawns each get a full tab
|
|
206
|
+
// instead of ever-narrower splits of the parent pane. Target the current
|
|
207
|
+
// workspace explicitly because Herdr's implicit default may be another space.
|
|
208
|
+
const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
|
|
209
|
+
const output = herdrExec(
|
|
210
|
+
buildTabCreateArgs(name, process.cwd(), workspaceId),
|
|
211
|
+
);
|
|
212
|
+
const paneId = extractHerdrRootPaneId(output, "tab create");
|
|
213
|
+
try {
|
|
214
|
+
herdrExec(["pane", "rename", paneId, name]);
|
|
215
|
+
} catch {
|
|
216
|
+
// Optional — pane label is cosmetic.
|
|
217
|
+
}
|
|
218
|
+
return paneId;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function createHerdrWorktree(
|
|
222
|
+
name: string,
|
|
223
|
+
cwd: string,
|
|
224
|
+
branch: string,
|
|
225
|
+
base: string,
|
|
226
|
+
): HerdrWorktreeSurface {
|
|
227
|
+
const output = herdrExec(buildWorktreeCreateArgs(name, cwd, branch, base));
|
|
228
|
+
return extractHerdrWorktree(output);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function createHerdrSurfaceSplit(
|
|
232
|
+
name: string,
|
|
233
|
+
direction: "right" | "down",
|
|
234
|
+
): string {
|
|
235
|
+
const parentPaneId = getHerdrParentPaneId();
|
|
236
|
+
const output = herdrExec([
|
|
237
|
+
"pane",
|
|
238
|
+
"split",
|
|
239
|
+
parentPaneId,
|
|
240
|
+
"--direction",
|
|
241
|
+
direction,
|
|
242
|
+
"--no-focus",
|
|
243
|
+
"--cwd",
|
|
244
|
+
process.cwd(),
|
|
245
|
+
]);
|
|
246
|
+
const paneId = extractHerdrPaneId(output, "pane split");
|
|
247
|
+
try {
|
|
248
|
+
herdrExec(["pane", "rename", paneId, name]);
|
|
249
|
+
} catch {
|
|
250
|
+
// Optional.
|
|
251
|
+
}
|
|
252
|
+
return paneId;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function readHerdrScreen(surface: string, lines = 50): string {
|
|
256
|
+
// `visible` is reliable for freshly created panes where herdr's `recent`
|
|
257
|
+
// scrollback may not be populated yet.
|
|
258
|
+
return herdrExec([
|
|
259
|
+
"pane",
|
|
260
|
+
"read",
|
|
261
|
+
surface,
|
|
262
|
+
"--source",
|
|
263
|
+
"visible",
|
|
264
|
+
"--lines",
|
|
265
|
+
String(lines),
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function readHerdrScreenAsync(
|
|
270
|
+
surface: string,
|
|
271
|
+
lines = 50,
|
|
272
|
+
): Promise<string> {
|
|
273
|
+
return herdrExecAsync([
|
|
274
|
+
"pane",
|
|
275
|
+
"read",
|
|
276
|
+
surface,
|
|
277
|
+
"--source",
|
|
278
|
+
"visible",
|
|
279
|
+
"--lines",
|
|
280
|
+
String(lines),
|
|
281
|
+
]);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export type { PaneInspection, HerdrAgentStatus } from "./lifecycle.ts";
|
|
285
|
+
|
|
286
|
+
type PaneInspectionResult =
|
|
287
|
+
| {
|
|
288
|
+
kind: "present";
|
|
289
|
+
agent?: string;
|
|
290
|
+
agentStatus: "idle" | "working" | "blocked" | "done" | "unknown";
|
|
291
|
+
}
|
|
292
|
+
| { kind: "missing"; error?: string }
|
|
293
|
+
| { kind: "unavailable"; error: string };
|
|
294
|
+
|
|
295
|
+
function parsePaneGetOutput(
|
|
296
|
+
output: string,
|
|
297
|
+
surface: string,
|
|
298
|
+
): PaneInspectionResult {
|
|
299
|
+
const parsed = parseHerdrJson(output) as {
|
|
300
|
+
result?: { pane?: unknown };
|
|
301
|
+
error?: { code?: unknown; message?: unknown };
|
|
302
|
+
} | null;
|
|
303
|
+
const errorObj = parsed?.error;
|
|
304
|
+
if (errorObj?.code === "pane_not_found" || errorObj?.code === "not_found") {
|
|
305
|
+
return {
|
|
306
|
+
kind: "missing",
|
|
307
|
+
error:
|
|
308
|
+
typeof errorObj.message === "string"
|
|
309
|
+
? errorObj.message
|
|
310
|
+
: "pane not found",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const pane = parsed?.result?.pane;
|
|
314
|
+
if (!pane || typeof pane !== "object")
|
|
315
|
+
return { kind: "unavailable", error: "pane get returned no pane record" };
|
|
316
|
+
const record = pane as {
|
|
317
|
+
pane_id?: unknown;
|
|
318
|
+
agent?: unknown;
|
|
319
|
+
agent_status?: unknown;
|
|
320
|
+
};
|
|
321
|
+
if (record.pane_id !== surface)
|
|
322
|
+
return { kind: "unavailable", error: "pane id mismatch" };
|
|
323
|
+
const agent = typeof record.agent === "string" ? record.agent : undefined;
|
|
324
|
+
const rawStatus =
|
|
325
|
+
typeof record.agent_status === "string" ? record.agent_status : "unknown";
|
|
326
|
+
const agentStatus =
|
|
327
|
+
rawStatus === "idle" ||
|
|
328
|
+
rawStatus === "working" ||
|
|
329
|
+
rawStatus === "blocked" ||
|
|
330
|
+
rawStatus === "done" ||
|
|
331
|
+
rawStatus === "unknown"
|
|
332
|
+
? rawStatus
|
|
333
|
+
: "unknown";
|
|
334
|
+
return { kind: "present", ...(agent ? { agent } : {}), agentStatus };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function parsePaneGetError(error: any): PaneInspectionResult {
|
|
338
|
+
for (const raw of [error?.stderr, error?.stdout]) {
|
|
339
|
+
if (typeof raw !== "string" || !raw.trim()) continue;
|
|
340
|
+
try {
|
|
341
|
+
const parsed = parsePaneGetOutput(raw, "");
|
|
342
|
+
if (parsed.kind === "missing") return parsed;
|
|
343
|
+
} catch {
|
|
344
|
+
// A CLI may emit plain diagnostics on one stream and structured JSON on
|
|
345
|
+
// the other. Parse each stream independently before giving up.
|
|
346
|
+
}
|
|
347
|
+
// Older/alternate Herdr builds may print the stable error code as plain
|
|
348
|
+
// text rather than JSON. Only match explicit identifiers, not generic
|
|
349
|
+
// prose such as "pane unavailable".
|
|
350
|
+
if (/\b(?:pane_not_found|not_found)\b/.test(raw)) {
|
|
351
|
+
return { kind: "missing", error: raw.trim() };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const message = error?.message
|
|
355
|
+
? String(error.message)
|
|
356
|
+
: "herdr pane get failed";
|
|
357
|
+
return { kind: "unavailable", error: message };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Structured pane query.
|
|
362
|
+
* - present: pane is reachable; agent/agentStatus may be present when detected
|
|
363
|
+
* - missing: server responded, pane is gone
|
|
364
|
+
* - unavailable: server command failed; caller should keep polling
|
|
365
|
+
*/
|
|
366
|
+
export async function inspectHerdrPane(
|
|
367
|
+
surface: string,
|
|
368
|
+
): Promise<PaneInspectionResult> {
|
|
369
|
+
try {
|
|
370
|
+
return parsePaneGetOutput(
|
|
371
|
+
await herdrExecAsync(["pane", "get", surface]),
|
|
372
|
+
surface,
|
|
373
|
+
);
|
|
374
|
+
} catch (error: any) {
|
|
375
|
+
return parsePaneGetError(error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface HerdrPaneProcessInfo {
|
|
380
|
+
paneId: string;
|
|
381
|
+
shellPid?: number;
|
|
382
|
+
foregroundProcessGroupId?: number;
|
|
383
|
+
pids: number[];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function parsePaneProcessInfo(
|
|
387
|
+
output: string,
|
|
388
|
+
paneId: string,
|
|
389
|
+
): HerdrPaneProcessInfo {
|
|
390
|
+
const parsed = parseHerdrJson(output) as {
|
|
391
|
+
result?: {
|
|
392
|
+
process_info?: {
|
|
393
|
+
pane_id?: unknown;
|
|
394
|
+
shell_pid?: unknown;
|
|
395
|
+
foreground_process_group_id?: unknown;
|
|
396
|
+
foreground_processes?: Array<{ pid?: unknown }>;
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
} | null;
|
|
400
|
+
const info = parsed?.result?.process_info;
|
|
401
|
+
if (!info || typeof info !== "object") {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`Unexpected herdr pane process-info output: ${output.trim() || "(empty)"}`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (typeof info.pane_id === "string" && info.pane_id !== paneId) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`herdr pane process-info pane id mismatch: ${info.pane_id} != ${paneId}`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const pids = new Set<number>();
|
|
412
|
+
if (
|
|
413
|
+
typeof info.shell_pid === "number" &&
|
|
414
|
+
Number.isInteger(info.shell_pid) &&
|
|
415
|
+
info.shell_pid > 0
|
|
416
|
+
) {
|
|
417
|
+
pids.add(info.shell_pid);
|
|
418
|
+
}
|
|
419
|
+
if (
|
|
420
|
+
typeof info.foreground_process_group_id === "number" &&
|
|
421
|
+
Number.isInteger(info.foreground_process_group_id) &&
|
|
422
|
+
info.foreground_process_group_id > 0
|
|
423
|
+
) {
|
|
424
|
+
pids.add(info.foreground_process_group_id);
|
|
425
|
+
}
|
|
426
|
+
for (const process of info.foreground_processes ?? []) {
|
|
427
|
+
if (
|
|
428
|
+
typeof process?.pid === "number" &&
|
|
429
|
+
Number.isInteger(process.pid) &&
|
|
430
|
+
process.pid > 0
|
|
431
|
+
) {
|
|
432
|
+
pids.add(process.pid);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
paneId,
|
|
437
|
+
...(typeof info.shell_pid === "number" ? { shellPid: info.shell_pid } : {}),
|
|
438
|
+
...(typeof info.foreground_process_group_id === "number"
|
|
439
|
+
? { foregroundProcessGroupId: info.foreground_process_group_id }
|
|
440
|
+
: {}),
|
|
441
|
+
pids: [...pids],
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function getHerdrPaneProcessInfo(surface: string): HerdrPaneProcessInfo {
|
|
446
|
+
return parsePaneProcessInfo(
|
|
447
|
+
herdrExec(["pane", "process-info", "--pane", surface]),
|
|
448
|
+
surface,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function isProcessAlive(pid: number): boolean {
|
|
453
|
+
try {
|
|
454
|
+
process.kill(pid, 0);
|
|
455
|
+
return true;
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export async function waitForProcessesExit(
|
|
462
|
+
pids: readonly number[],
|
|
463
|
+
options: {
|
|
464
|
+
timeoutMs?: number;
|
|
465
|
+
intervalMs?: number;
|
|
466
|
+
isAlive?: (pid: number) => boolean;
|
|
467
|
+
} = {},
|
|
468
|
+
): Promise<number[]> {
|
|
469
|
+
const isAlive = options.isAlive ?? isProcessAlive;
|
|
470
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
471
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
472
|
+
const remaining = new Set(
|
|
473
|
+
pids.filter((pid) => Number.isInteger(pid) && pid > 0 && isAlive(pid)),
|
|
474
|
+
);
|
|
475
|
+
const deadline = Date.now() + timeoutMs;
|
|
476
|
+
while (remaining.size > 0 && Date.now() < deadline) {
|
|
477
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
478
|
+
for (const pid of remaining) {
|
|
479
|
+
if (!isAlive(pid)) remaining.delete(pid);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return [...remaining];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export async function waitForHerdrPaneAbsence(
|
|
486
|
+
surface: string,
|
|
487
|
+
options: {
|
|
488
|
+
timeoutMs?: number;
|
|
489
|
+
intervalMs?: number;
|
|
490
|
+
inspect?: (surface: string) => Promise<PaneInspectionResult>;
|
|
491
|
+
} = {},
|
|
492
|
+
): Promise<boolean> {
|
|
493
|
+
const inspect = options.inspect ?? inspectHerdrPane;
|
|
494
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
495
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
496
|
+
const deadline = Date.now() + timeoutMs;
|
|
497
|
+
while (Date.now() <= deadline) {
|
|
498
|
+
const inspection = await inspect(surface);
|
|
499
|
+
if (inspection.kind === "missing") return true;
|
|
500
|
+
if (Date.now() >= deadline) break;
|
|
501
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
502
|
+
}
|
|
503
|
+
const finalInspection = await inspect(surface);
|
|
504
|
+
return finalInspection.kind === "missing";
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function sendHerdrCommand(surface: string, command: string): void {
|
|
508
|
+
// pane run sends the text and Enter in a single socket request, avoiding
|
|
509
|
+
// a race where Enter could arrive before the text is fully processed.
|
|
510
|
+
herdrExec(["pane", "run", surface, command]);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function sendHerdrEscape(surface: string): void {
|
|
514
|
+
herdrExec(["pane", "send-keys", surface, "Escape"]);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function closeHerdrSurface(surface: string): void {
|
|
518
|
+
herdrExec(["pane", "close", surface]);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export function renameHerdrTab(title: string): void {
|
|
522
|
+
const { tab_id: tabId } = getHerdrCurrentPaneInfo();
|
|
523
|
+
herdrExec(["tab", "rename", tabId, title]);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function renameHerdrWorkspace(title: string): void {
|
|
527
|
+
const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
|
|
528
|
+
herdrExec(["workspace", "rename", workspaceId, title]);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export const __herdrTest__ = {
|
|
532
|
+
buildTabCreateArgs,
|
|
533
|
+
buildWorktreeCreateArgs,
|
|
534
|
+
parseHerdrJson,
|
|
535
|
+
extractHerdrPaneId,
|
|
536
|
+
extractHerdrRootPaneId,
|
|
537
|
+
extractHerdrWorktree,
|
|
538
|
+
parsePaneGetOutput,
|
|
539
|
+
parsePaneGetError,
|
|
540
|
+
parsePaneProcessInfo,
|
|
541
|
+
};
|