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.
Files changed (49) hide show
  1. package/AGENTS.md +116 -0
  2. package/CONTEXT.md +159 -0
  3. package/LICENSE +21 -0
  4. package/README.md +874 -0
  5. package/RELEASING.md +139 -0
  6. package/agents/adversarial-reviewer.md +80 -0
  7. package/agents/claude-reviewer.md +23 -0
  8. package/agents/planner.md +539 -0
  9. package/agents/poteto.md +32 -0
  10. package/agents/reviewer.md +164 -0
  11. package/agents/scout.md +106 -0
  12. package/agents/visual-tester.md +224 -0
  13. package/agents/worker.md +132 -0
  14. package/config.json.example +8 -0
  15. package/docs/README.md +42 -0
  16. package/docs/adr/0001-btw-ephemeral-side-questions.md +142 -0
  17. package/docs/adr/0002-agent-workflow-skill-runtime-taxonomy.md +265 -0
  18. package/docs/adr/0003-installable-role-packs.md +135 -0
  19. package/docs/adr/0004-require-active-user-approval-for-workflow-execution.md +17 -0
  20. package/docs/adr/0005-parent-owns-workflow-script-authority.md +17 -0
  21. package/docs/adr/0006-limit-v1-execution-effects-to-isolated-worktrees.md +18 -0
  22. package/docs/adr/0007-require-fresh-review-for-workflow-scripts.md +19 -0
  23. package/docs/orchestrated-review-workflow-plan.md +479 -0
  24. package/docs/research/pdw-architecture-assessment.md +525 -0
  25. package/docs/research/pi-workflows-sol-advisor.md +255 -0
  26. package/docs/research/worktree-subagent-orchestration.md +317 -0
  27. package/docs/worktree-subagents.md +196 -0
  28. package/examples/role-pack/extension.ts +18 -0
  29. package/examples/role-pack/package.json +16 -0
  30. package/examples/role-pack/roles/example-reviewer.md +12 -0
  31. package/package.json +58 -0
  32. package/pi-extension/subagents/activity.ts +511 -0
  33. package/pi-extension/subagents/completion.ts +177 -0
  34. package/pi-extension/subagents/herdr.ts +541 -0
  35. package/pi-extension/subagents/index.ts +4730 -0
  36. package/pi-extension/subagents/lifecycle.ts +477 -0
  37. package/pi-extension/subagents/model-config.ts +95 -0
  38. package/pi-extension/subagents/plan-skill.md +262 -0
  39. package/pi-extension/subagents/plugin/.claude-plugin/plugin.json +5 -0
  40. package/pi-extension/subagents/plugin/hooks/hooks.json +15 -0
  41. package/pi-extension/subagents/plugin/hooks/on-stop.sh +68 -0
  42. package/pi-extension/subagents/runtime-routing.ts +313 -0
  43. package/pi-extension/subagents/session.ts +216 -0
  44. package/pi-extension/subagents/status.ts +513 -0
  45. package/pi-extension/subagents/subagent-done.ts +326 -0
  46. package/pi-extension/subagents/terminal.ts +163 -0
  47. package/pi-extension/subagents/workflow-worker.js +56 -0
  48. package/pi-extension/subagents/workflow.ts +1210 -0
  49. 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
+ };