pi-agent-replicate 0.0.0-stage → 1.4.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 (82) hide show
  1. package/AGENTS.md +27 -0
  2. package/CHANGELOG.md +152 -0
  3. package/CONTEXT.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +68 -2
  6. package/RELEASING.md +132 -0
  7. package/assets/banner.png +0 -0
  8. package/config.json.example +6 -0
  9. package/docs/README.md +23 -0
  10. package/docs/adr/0003-installable-role-packs.md +29 -0
  11. package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
  12. package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
  13. package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
  14. package/docs/configuration.md +14 -0
  15. package/docs/development.md +27 -0
  16. package/docs/public-subagent-tree-api.md +58 -0
  17. package/docs/retained-roles.md +71 -0
  18. package/docs/settled-lifecycle-contract.md +71 -0
  19. package/docs/worktree-subagents.md +58 -0
  20. package/examples/role-pack/extension.ts +18 -0
  21. package/examples/role-pack/package.json +16 -0
  22. package/examples/role-pack/roles/example-reviewer.md +12 -0
  23. package/index.d.ts +116 -0
  24. package/index.js +1064 -0
  25. package/index.ts +155 -0
  26. package/package.json +70 -4
  27. package/pi-extension/subagents/activity.js +575 -0
  28. package/pi-extension/subagents/activity.ts +980 -0
  29. package/pi-extension/subagents/completion.js +184 -0
  30. package/pi-extension/subagents/completion.ts +229 -0
  31. package/pi-extension/subagents/delivery-drain.js +15 -0
  32. package/pi-extension/subagents/delivery-drain.ts +20 -0
  33. package/pi-extension/subagents/delivery-files.js +86 -0
  34. package/pi-extension/subagents/delivery-files.ts +105 -0
  35. package/pi-extension/subagents/delivery-log.js +51 -0
  36. package/pi-extension/subagents/delivery-log.ts +51 -0
  37. package/pi-extension/subagents/delivery-payload.ts +55 -0
  38. package/pi-extension/subagents/delivery.ts +219 -0
  39. package/pi-extension/subagents/execution-owner.js +30 -0
  40. package/pi-extension/subagents/execution-owner.ts +38 -0
  41. package/pi-extension/subagents/herdr.js +645 -0
  42. package/pi-extension/subagents/herdr.ts +927 -0
  43. package/pi-extension/subagents/index.ts +3400 -0
  44. package/pi-extension/subagents/launch.js +774 -0
  45. package/pi-extension/subagents/launch.ts +1124 -0
  46. package/pi-extension/subagents/lifecycle.js +477 -0
  47. package/pi-extension/subagents/lifecycle.ts +582 -0
  48. package/pi-extension/subagents/lineage.js +806 -0
  49. package/pi-extension/subagents/lineage.ts +841 -0
  50. package/pi-extension/subagents/replica-background.js +32 -0
  51. package/pi-extension/subagents/replica-background.ts +38 -0
  52. package/pi-extension/subagents/replica-branch.ts +45 -0
  53. package/pi-extension/subagents/replica-directory-settings.ts +51 -0
  54. package/pi-extension/subagents/replica-history.js +22 -0
  55. package/pi-extension/subagents/replica-history.ts +21 -0
  56. package/pi-extension/subagents/replica-work.js +326 -0
  57. package/pi-extension/subagents/replica-work.ts +336 -0
  58. package/pi-extension/subagents/replica-worker.js +74 -0
  59. package/pi-extension/subagents/replica-worker.ts +71 -0
  60. package/pi-extension/subagents/replication.ts +99 -0
  61. package/pi-extension/subagents/result-delivery-queue.ts +33 -0
  62. package/pi-extension/subagents/role-catalog.js +123 -0
  63. package/pi-extension/subagents/role-catalog.ts +24 -0
  64. package/pi-extension/subagents/runtime-routing.js +154 -0
  65. package/pi-extension/subagents/runtime-routing.ts +239 -0
  66. package/pi-extension/subagents/session-binding.js +346 -0
  67. package/pi-extension/subagents/session-binding.ts +339 -0
  68. package/pi-extension/subagents/session-work.js +219 -0
  69. package/pi-extension/subagents/session-work.ts +167 -0
  70. package/pi-extension/subagents/session.js +318 -0
  71. package/pi-extension/subagents/session.ts +448 -0
  72. package/pi-extension/subagents/settled-contract.js +47 -0
  73. package/pi-extension/subagents/settled-contract.ts +175 -0
  74. package/pi-extension/subagents/source-state.js +180 -0
  75. package/pi-extension/subagents/source-state.ts +184 -0
  76. package/pi-extension/subagents/status.ts +493 -0
  77. package/pi-extension/subagents/subagent-done.js +805 -0
  78. package/pi-extension/subagents/subagent-done.ts +878 -0
  79. package/pi-extension/subagents/terminal.js +111 -0
  80. package/pi-extension/subagents/terminal.ts +208 -0
  81. package/pi-extension/subagents/watchdog.js +122 -0
  82. package/pi-extension/subagents/watchdog.ts +171 -0
@@ -0,0 +1,927 @@
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
+ const surfaceTabs = new Map<string, string>();
9
+ function hasCommand(command: string): boolean {
10
+ if (commandAvailability.has(command)) {
11
+ return commandAvailability.get(command)!;
12
+ }
13
+
14
+ let available = false;
15
+ if (process.platform === "win32") {
16
+ try {
17
+ execFileSync("where.exe", [command], { stdio: "ignore" });
18
+ available = true;
19
+ } catch {
20
+ try {
21
+ execSync(`command -v ${command}`, { stdio: "ignore" });
22
+ available = true;
23
+ } catch {
24
+ available = false;
25
+ }
26
+ }
27
+ } else {
28
+ try {
29
+ execSync(`command -v ${command}`, { stdio: "ignore" });
30
+ available = true;
31
+ } catch {
32
+ available = false;
33
+ }
34
+ }
35
+
36
+ commandAvailability.set(command, available);
37
+ return available;
38
+ }
39
+
40
+ export function isHerdrAvailable(): boolean {
41
+ return process.env.HERDR_ENV === "1" && hasCommand("herdr");
42
+ }
43
+
44
+ function parseHerdrJson(value: string): unknown {
45
+ try {
46
+ return JSON.parse(value);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function extractHerdrPaneId(output: string, context: string): string {
53
+ const parsed = parseHerdrJson(output);
54
+ const paneId = (parsed as { result?: { pane?: { pane_id?: unknown } } })
55
+ ?.result?.pane?.pane_id;
56
+ if (typeof paneId !== "string" || !paneId) {
57
+ throw new Error(
58
+ `Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
59
+ );
60
+ }
61
+ return paneId;
62
+ }
63
+
64
+ function extractHerdrRootPaneId(output: string, context: string): string {
65
+ const parsed = parseHerdrJson(output);
66
+ const paneId = (parsed as { result?: { root_pane?: { pane_id?: unknown } } })
67
+ ?.result?.root_pane?.pane_id;
68
+ if (typeof paneId !== "string" || !paneId) {
69
+ throw new Error(
70
+ `Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
71
+ );
72
+ }
73
+ return paneId;
74
+ }
75
+
76
+
77
+ function extractHerdrTabSurface(
78
+ output: string,
79
+ context: string,
80
+ ): { paneId: string; tabId: string } {
81
+ const parsed = parseHerdrJson(output) as {
82
+ result?: {
83
+ tab?: { tab_id?: unknown };
84
+ root_pane?: { pane_id?: unknown; tab_id?: unknown };
85
+ };
86
+ } | null;
87
+ const paneId = parsed?.result?.root_pane?.pane_id;
88
+ const tabId =
89
+ parsed?.result?.tab?.tab_id ?? parsed?.result?.root_pane?.tab_id;
90
+ if (
91
+ typeof paneId !== "string" ||
92
+ !paneId ||
93
+ typeof tabId !== "string" ||
94
+ !tabId
95
+ ) {
96
+ throw new Error(
97
+ `Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`,
98
+ );
99
+ }
100
+ return { paneId, tabId };
101
+ }
102
+
103
+ /** Return the remembered owning tab for a Herdr surface created by this process. */
104
+ export function getHerdrSurfaceTabId(surface: string): string | undefined {
105
+ return surfaceTabs.get(surface);
106
+ }
107
+
108
+ /** Restore a durable surface-to-tab association before retrying cleanup. */
109
+ export function rememberHerdrSurfaceTab(surface: string, tabId: string): void {
110
+ if (surface && tabId) surfaceTabs.set(surface, tabId);
111
+ }
112
+
113
+ function forgetHerdrTab(tabId: string): void {
114
+ for (const [surface, rememberedTab] of surfaceTabs) {
115
+ if (rememberedTab === tabId) surfaceTabs.delete(surface);
116
+ }
117
+ }
118
+ export interface HerdrWorktreeSurface {
119
+ path: string;
120
+ branch: string;
121
+ workspaceId: string;
122
+ paneId: string;
123
+ }
124
+
125
+ function extractHerdrWorktree(output: string): HerdrWorktreeSurface {
126
+ const parsed = parseHerdrJson(output) as {
127
+ result?: {
128
+ type?: unknown;
129
+ workspace?: { workspace_id?: unknown };
130
+ root_pane?: { pane_id?: unknown };
131
+ worktree?: { path?: unknown; branch?: unknown };
132
+ };
133
+ } | null;
134
+ const result = parsed?.result;
135
+ if (
136
+ result?.type !== "worktree_created" ||
137
+ typeof result.workspace?.workspace_id !== "string" ||
138
+ !result.workspace.workspace_id ||
139
+ typeof result.root_pane?.pane_id !== "string" ||
140
+ !result.root_pane.pane_id ||
141
+ typeof result.worktree?.path !== "string" ||
142
+ !result.worktree.path ||
143
+ typeof result.worktree.branch !== "string" ||
144
+ !result.worktree.branch
145
+ ) {
146
+ throw new Error(
147
+ `Unexpected herdr worktree create output: ${output.trim() || "(empty)"}`,
148
+ );
149
+ }
150
+ return {
151
+ path: result.worktree.path,
152
+ branch: result.worktree.branch,
153
+ workspaceId: result.workspace.workspace_id,
154
+ paneId: result.root_pane.pane_id,
155
+ };
156
+ }
157
+
158
+ function herdrExec(args: string[]): string {
159
+ return execFileSync("herdr", args, {
160
+ encoding: "utf8",
161
+ stdio: ["ignore", "pipe", "pipe"],
162
+ });
163
+ }
164
+
165
+ async function herdrExecAsync(args: string[]): Promise<string> {
166
+ const { stdout } = await execFileAsync("herdr", args, { encoding: "utf8" });
167
+ return stdout;
168
+ }
169
+
170
+ function getHerdrParentPaneId(): string {
171
+ const paneId = process.env.HERDR_PANE_ID;
172
+ if (!paneId) {
173
+ throw new Error("HERDR_PANE_ID not set");
174
+ }
175
+ return paneId;
176
+ }
177
+
178
+ function buildCurrentPaneArgs(): string[] {
179
+ return ["pane", "current", "--current"];
180
+ }
181
+
182
+ function getHerdrCurrentPaneInfo(run = herdrExec): {
183
+ pane_id: string;
184
+ tab_id: string;
185
+ workspace_id: string;
186
+ } {
187
+ // Inherited IDs can outlive a pane move; Herdr resolves the caller identity.
188
+ const output = run(buildCurrentPaneArgs());
189
+ const parsed = parseHerdrJson(output);
190
+ const pane = (parsed as { result?: { pane?: unknown } } | null)?.result?.pane as
191
+ | { pane_id?: string; tab_id?: string; workspace_id?: string }
192
+ | undefined;
193
+ if (!pane?.pane_id || !pane?.tab_id || !pane?.workspace_id) {
194
+ throw new Error(`Unexpected herdr pane current output: ${output.trim() || "(empty)"}`);
195
+ }
196
+ return { pane_id: pane.pane_id, tab_id: pane.tab_id, workspace_id: pane.workspace_id };
197
+ }
198
+
199
+ /** Read the current pane's manual name; unavailable metadata must not block a launch. */
200
+ export function getHerdrCurrentPaneTitle(run = herdrExec): string | undefined {
201
+ try {
202
+ const parsed = parseHerdrJson(run(buildCurrentPaneArgs())) as { result?: { pane?: { title?: unknown; label?: unknown } } } | null;
203
+ const pane = parsed?.result?.pane;
204
+ return [pane?.title, pane?.label].find((value): value is string => typeof value === "string" && !!value.trim());
205
+ } catch {
206
+ return undefined;
207
+ }
208
+ }
209
+
210
+ function buildTabCreateArgs(
211
+ name: string,
212
+ cwd: string,
213
+ workspaceId: string,
214
+ ): string[] {
215
+ return [
216
+ "tab",
217
+ "create",
218
+ "--workspace",
219
+ workspaceId,
220
+ "--label",
221
+ name,
222
+ "--cwd",
223
+ cwd,
224
+ "--no-focus",
225
+ ];
226
+ }
227
+
228
+ function buildWorktreeCreateArgs(
229
+ name: string,
230
+ cwd: string,
231
+ branch: string,
232
+ base: string,
233
+ ): string[] {
234
+ return [
235
+ "worktree",
236
+ "create",
237
+ "--cwd",
238
+ cwd,
239
+ "--branch",
240
+ branch,
241
+ "--base",
242
+ base,
243
+ "--label",
244
+ name,
245
+ "--no-focus",
246
+ ];
247
+ }
248
+
249
+ export function createHerdrSurfaceInWorkspace(
250
+ name: string,
251
+ cwd: string,
252
+ workspaceId: string,
253
+ ): string {
254
+ const output = herdrExec(buildTabCreateArgs(name, cwd, workspaceId));
255
+ const { paneId, tabId } = extractHerdrTabSurface(output, "tab create");
256
+ rememberHerdrSurfaceTab(paneId, tabId);
257
+ try {
258
+ herdrExec(["pane", "rename", paneId, name]);
259
+ } catch {
260
+ // Optional — pane label is cosmetic.
261
+ }
262
+ return paneId;
263
+ }
264
+
265
+ export function createHerdrSurface(name: string): string {
266
+ // Create a new tab per subagent so parallel spawns each get a full tab
267
+ // instead of ever-narrower splits of the parent pane. Target the current
268
+ // workspace explicitly because Herdr's implicit default may be another space.
269
+ const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
270
+ return createHerdrSurfaceInWorkspace(name, process.cwd(), workspaceId);
271
+ }
272
+
273
+ /** Branch-backed worktree records returned by `herdr worktree list`. */
274
+ export interface HerdrWorktreeInfo {
275
+ branch: string;
276
+ path: string;
277
+ label?: string;
278
+ workspaceId?: string;
279
+ isLinkedWorktree: boolean;
280
+ }
281
+
282
+ export class HerdrWorktreeCreateError extends Error {
283
+ readonly recoveredWorktree: Pick<
284
+ HerdrWorktreeInfo,
285
+ "path" | "branch" | "workspaceId"
286
+ >;
287
+
288
+ constructor(
289
+ message: string,
290
+ recoveredWorktree: Pick<
291
+ HerdrWorktreeInfo,
292
+ "path" | "branch" | "workspaceId"
293
+ >,
294
+ ) {
295
+ super(message);
296
+ this.name = "HerdrWorktreeCreateError";
297
+ this.recoveredWorktree = recoveredWorktree;
298
+ }
299
+ }
300
+
301
+ export function parseHerdrWorktreeList(output: string): HerdrWorktreeInfo[] {
302
+ const parsed = parseHerdrJson(output) as {
303
+ result?: {
304
+ type?: unknown;
305
+ worktrees?: Array<{
306
+ branch?: unknown;
307
+ path?: unknown;
308
+ label?: unknown;
309
+ open_workspace_id?: unknown;
310
+ is_linked_worktree?: unknown;
311
+ is_detached?: unknown;
312
+ }>;
313
+ };
314
+ } | null;
315
+ const worktrees = parsed?.result?.worktrees;
316
+ if (parsed?.result?.type !== "worktree_list" || !Array.isArray(worktrees)) {
317
+ throw new Error("Unexpected herdr worktree list output");
318
+ }
319
+ // Detached checkouts have no branch and cannot match an owned replica.
320
+ return worktrees.filter((worktree) => !(
321
+ worktree.is_detached === true && worktree.branch === undefined && typeof worktree.path === "string"
322
+ )).map((worktree) => {
323
+ if (
324
+ typeof worktree.branch !== "string" ||
325
+ typeof worktree.path !== "string"
326
+ ) {
327
+ throw new Error("Unexpected herdr worktree list entry");
328
+ }
329
+ return {
330
+ branch: worktree.branch,
331
+ path: worktree.path,
332
+ ...(typeof worktree.label === "string" ? { label: worktree.label } : {}),
333
+ ...(typeof worktree.open_workspace_id === "string"
334
+ ? { workspaceId: worktree.open_workspace_id }
335
+ : {}),
336
+ isLinkedWorktree: worktree.is_linked_worktree === true,
337
+ };
338
+ });
339
+ }
340
+
341
+ export function listHerdrWorktrees(cwd?: string): HerdrWorktreeInfo[] {
342
+ const args = ["worktree", "list"];
343
+ if (cwd) args.push("--cwd", cwd);
344
+ return parseHerdrWorktreeList(herdrExec(args));
345
+ }
346
+
347
+ function parseHerdrPaneList(output: string, workspaceId: string): string[] {
348
+ const parsed = parseHerdrJson(output) as {
349
+ result?: {
350
+ type?: unknown;
351
+ panes?: Array<{ pane_id?: unknown; workspace_id?: unknown }>;
352
+ };
353
+ } | null;
354
+ if (
355
+ parsed?.result?.type !== "pane_list" ||
356
+ !Array.isArray(parsed.result.panes)
357
+ ) {
358
+ throw new Error("Unexpected herdr pane list output");
359
+ }
360
+ return parsed.result.panes
361
+ .filter((pane) => pane.workspace_id === workspaceId)
362
+ .map((pane) => pane.pane_id)
363
+ .filter((paneId): paneId is string => typeof paneId === "string");
364
+ }
365
+
366
+ function recoverHerdrWorktree(
367
+ cwd: string,
368
+ branch: string,
369
+ ): HerdrWorktreeSurface | HerdrWorktreeInfo | undefined {
370
+ const matches = listHerdrWorktrees(cwd).filter(
371
+ (worktree) => worktree.branch === branch,
372
+ );
373
+ if (matches.length !== 1) return undefined;
374
+ const worktree = matches[0];
375
+ if (!worktree.workspaceId) return worktree;
376
+ const panes = parseHerdrPaneList(
377
+ herdrExec(["pane", "list", "--workspace", worktree.workspaceId]),
378
+ worktree.workspaceId,
379
+ );
380
+ if (panes.length !== 1) return worktree;
381
+ return {
382
+ path: worktree.path,
383
+ branch: worktree.branch,
384
+ workspaceId: worktree.workspaceId,
385
+ paneId: panes[0],
386
+ };
387
+ }
388
+
389
+ export function createHerdrWorktree(
390
+ name: string,
391
+ cwd: string,
392
+ branch: string,
393
+ base: string,
394
+ ): HerdrWorktreeSurface {
395
+ const output = herdrExec(buildWorktreeCreateArgs(name, cwd, branch, base));
396
+ try {
397
+ return extractHerdrWorktree(output);
398
+ } catch (parseError) {
399
+ let recovered: HerdrWorktreeSurface | HerdrWorktreeInfo | undefined;
400
+ try {
401
+ recovered = recoverHerdrWorktree(cwd, branch);
402
+ } catch {
403
+ throw parseError;
404
+ }
405
+ if (recovered?.workspaceId && "paneId" in recovered) return recovered;
406
+ if (recovered) {
407
+ throw new HerdrWorktreeCreateError(
408
+ `Herdr created branch ${branch}, but its workspace response was incomplete`,
409
+ recovered,
410
+ );
411
+ }
412
+ throw parseError;
413
+ }
414
+ }
415
+
416
+ export function createHerdrSurfaceSplit(
417
+ name: string,
418
+ direction: "right" | "down",
419
+ ): string {
420
+ const parentPaneId = getHerdrParentPaneId();
421
+ const output = herdrExec([
422
+ "pane",
423
+ "split",
424
+ parentPaneId,
425
+ "--direction",
426
+ direction,
427
+ "--no-focus",
428
+ "--cwd",
429
+ process.cwd(),
430
+ ]);
431
+ const paneId = extractHerdrPaneId(output, "pane split");
432
+ try {
433
+ herdrExec(["pane", "rename", paneId, name]);
434
+ } catch {
435
+ // Optional.
436
+ }
437
+ return paneId;
438
+ }
439
+
440
+ export function readHerdrScreen(surface: string, lines = 50): string {
441
+ // `visible` is reliable for freshly created panes where herdr's `recent`
442
+ // scrollback may not be populated yet.
443
+ return herdrExec([
444
+ "pane",
445
+ "read",
446
+ surface,
447
+ "--source",
448
+ "visible",
449
+ "--lines",
450
+ String(lines),
451
+ ]);
452
+ }
453
+
454
+ export async function readHerdrScreenAsync(
455
+ surface: string,
456
+ lines = 50,
457
+ source: "visible" | "recent-unwrapped" = "visible",
458
+ ): Promise<string> {
459
+ return herdrExecAsync([
460
+ "pane",
461
+ "read",
462
+ surface,
463
+ "--source",
464
+ source,
465
+ "--lines",
466
+ String(lines),
467
+ ]);
468
+ }
469
+
470
+ export type { PaneInspection, HerdrAgentStatus } from "./lifecycle.ts";
471
+
472
+ type PaneInspectionResult =
473
+ | {
474
+ kind: "present";
475
+ agent?: string;
476
+ agentStatus: "idle" | "working" | "blocked" | "done" | "unknown";
477
+ }
478
+ | { kind: "missing"; error?: string }
479
+ | { kind: "unavailable"; error: string };
480
+
481
+ function parsePaneGetOutput(
482
+ output: string,
483
+ surface: string,
484
+ ): PaneInspectionResult {
485
+ const parsed = parseHerdrJson(output) as {
486
+ result?: { pane?: unknown };
487
+ error?: { code?: unknown; message?: unknown };
488
+ } | null;
489
+ const errorObj = parsed?.error;
490
+ if (errorObj?.code === "pane_not_found" || errorObj?.code === "not_found") {
491
+ return {
492
+ kind: "missing",
493
+ error:
494
+ typeof errorObj.message === "string"
495
+ ? errorObj.message
496
+ : "pane not found",
497
+ };
498
+ }
499
+ const pane = parsed?.result?.pane;
500
+ if (!pane || typeof pane !== "object")
501
+ return { kind: "unavailable", error: "pane get returned no pane record" };
502
+ const record = pane as {
503
+ pane_id?: unknown;
504
+ agent?: unknown;
505
+ agent_status?: unknown;
506
+ };
507
+ if (record.pane_id !== surface)
508
+ return { kind: "unavailable", error: "pane id mismatch" };
509
+ const agent = typeof record.agent === "string" ? record.agent : undefined;
510
+ const rawStatus =
511
+ typeof record.agent_status === "string" ? record.agent_status : "unknown";
512
+ const agentStatus =
513
+ rawStatus === "idle" ||
514
+ rawStatus === "working" ||
515
+ rawStatus === "blocked" ||
516
+ rawStatus === "done" ||
517
+ rawStatus === "unknown"
518
+ ? rawStatus
519
+ : "unknown";
520
+ return { kind: "present", ...(agent ? { agent } : {}), agentStatus };
521
+ }
522
+
523
+ function parsePaneGetError(error: any): PaneInspectionResult {
524
+ for (const raw of [error?.stderr, error?.stdout]) {
525
+ if (typeof raw !== "string" || !raw.trim()) continue;
526
+ try {
527
+ const parsed = parsePaneGetOutput(raw, "");
528
+ if (parsed.kind === "missing") return parsed;
529
+ } catch {
530
+ // A CLI may emit plain diagnostics on one stream and structured JSON on
531
+ // the other. Parse each stream independently before giving up.
532
+ }
533
+ // Older/alternate Herdr builds may print the stable error code as plain
534
+ // text rather than JSON. Only match explicit identifiers, not generic
535
+ // prose such as "pane unavailable".
536
+ if (/\b(?:pane_not_found|not_found)\b/.test(raw)) {
537
+ return { kind: "missing", error: raw.trim() };
538
+ }
539
+ }
540
+ const message = error?.message
541
+ ? String(error.message)
542
+ : "herdr pane get failed";
543
+ return { kind: "unavailable", error: message };
544
+ }
545
+
546
+ /**
547
+ * Structured pane query.
548
+ * - present: pane is reachable; agent/agentStatus may be present when detected
549
+ * - missing: server responded, pane is gone
550
+ * - unavailable: server command failed; caller should keep polling
551
+ */
552
+ export async function inspectHerdrPane(
553
+ surface: string,
554
+ ): Promise<PaneInspectionResult> {
555
+ try {
556
+ return parsePaneGetOutput(
557
+ await herdrExecAsync(["pane", "get", surface]),
558
+ surface,
559
+ );
560
+ } catch (error: any) {
561
+ return parsePaneGetError(error);
562
+ }
563
+ }
564
+
565
+ export interface HerdrForegroundProcess {
566
+ pid: number;
567
+ name?: string;
568
+ argv0?: string;
569
+ argv?: string[];
570
+ cwd?: string;
571
+ }
572
+
573
+ export interface HerdrPaneProcessInfo {
574
+ paneId: string;
575
+ shellPid?: number;
576
+ foregroundProcessGroupId?: number;
577
+ pids: number[];
578
+ foregroundProcesses: HerdrForegroundProcess[];
579
+ }
580
+
581
+ export function parsePaneProcessInfo(
582
+ output: string,
583
+ paneId: string,
584
+ ): HerdrPaneProcessInfo {
585
+ const parsed = parseHerdrJson(output) as {
586
+ result?: {
587
+ process_info?: {
588
+ pane_id?: unknown;
589
+ shell_pid?: unknown;
590
+ foreground_process_group_id?: unknown;
591
+ foreground_processes?: Array<{
592
+ pid?: unknown;
593
+ name?: unknown;
594
+ argv0?: unknown;
595
+ argv?: unknown;
596
+ cwd?: unknown;
597
+ }>;
598
+ };
599
+ };
600
+ } | null;
601
+ const info = parsed?.result?.process_info;
602
+ if (!info || typeof info !== "object") {
603
+ throw new Error(
604
+ `Unexpected herdr pane process-info output: ${output.trim() || "(empty)"}`,
605
+ );
606
+ }
607
+ if (typeof info.pane_id === "string" && info.pane_id !== paneId) {
608
+ throw new Error(
609
+ `herdr pane process-info pane id mismatch: ${info.pane_id} != ${paneId}`,
610
+ );
611
+ }
612
+ const pids = new Set<number>();
613
+ if (
614
+ typeof info.shell_pid === "number" &&
615
+ Number.isInteger(info.shell_pid) &&
616
+ info.shell_pid > 0
617
+ ) {
618
+ pids.add(info.shell_pid);
619
+ }
620
+ if (
621
+ typeof info.foreground_process_group_id === "number" &&
622
+ Number.isInteger(info.foreground_process_group_id) &&
623
+ info.foreground_process_group_id > 0
624
+ ) {
625
+ pids.add(info.foreground_process_group_id);
626
+ }
627
+ const foregroundProcesses: HerdrForegroundProcess[] = [];
628
+ for (const process of info.foreground_processes ?? []) {
629
+ if (
630
+ typeof process?.pid === "number" &&
631
+ Number.isInteger(process.pid) &&
632
+ process.pid > 0
633
+ ) {
634
+ pids.add(process.pid);
635
+ foregroundProcesses.push({
636
+ pid: process.pid,
637
+ ...(typeof process.name === "string" ? { name: process.name } : {}),
638
+ ...(typeof process.argv0 === "string" ? { argv0: process.argv0 } : {}),
639
+ ...(Array.isArray(process.argv) &&
640
+ process.argv.every((value) => typeof value === "string")
641
+ ? { argv: process.argv as string[] }
642
+ : {}),
643
+ ...(typeof process.cwd === "string" ? { cwd: process.cwd } : {}),
644
+ });
645
+ }
646
+ }
647
+ return {
648
+ paneId,
649
+ ...(typeof info.shell_pid === "number" ? { shellPid: info.shell_pid } : {}),
650
+ ...(typeof info.foreground_process_group_id === "number"
651
+ ? { foregroundProcessGroupId: info.foreground_process_group_id }
652
+ : {}),
653
+ pids: [...pids],
654
+ foregroundProcesses,
655
+ };
656
+ }
657
+
658
+ export function getHerdrPaneProcessInfo(surface: string): HerdrPaneProcessInfo {
659
+ return parsePaneProcessInfo(
660
+ herdrExec(["pane", "process-info", "--pane", surface]),
661
+ surface,
662
+ );
663
+ }
664
+
665
+ async function getHerdrPaneProcessInfoAsync(
666
+ surface: string,
667
+ ): Promise<HerdrPaneProcessInfo> {
668
+ return parsePaneProcessInfo(
669
+ await herdrExecAsync(["pane", "process-info", "--pane", surface]),
670
+ surface,
671
+ );
672
+ }
673
+
674
+ function isHerdrShellReady(info: HerdrPaneProcessInfo): boolean {
675
+ return (
676
+ info.shellPid != null && info.foregroundProcessGroupId === info.shellPid
677
+ );
678
+ }
679
+
680
+ function isExpectedPiProcess(
681
+ process: HerdrForegroundProcess,
682
+ sessionFile: string,
683
+ cwd: string,
684
+ ): boolean {
685
+ const sessionIndex = process.argv?.indexOf("--session") ?? -1;
686
+ return (
687
+ (process.name === "pi" || process.argv0?.split("/").pop() === "pi") &&
688
+ sessionIndex >= 0 &&
689
+ process.argv?.[sessionIndex + 1] === sessionFile &&
690
+ process.cwd === cwd
691
+ );
692
+ }
693
+
694
+ export async function waitForHerdrPiReady(
695
+ surface: string,
696
+ sessionFile: string,
697
+ cwd: string,
698
+ options: { timeoutMs?: number; intervalMs?: number } = {},
699
+ ): Promise<void> {
700
+ const timeoutMs = options.timeoutMs ?? 10_000;
701
+ const intervalMs = options.intervalMs ?? 50;
702
+ const deadline = Date.now() + timeoutMs;
703
+ let lastError = "expected Pi process not observed";
704
+
705
+ while (Date.now() <= deadline) {
706
+ try {
707
+ const info = await getHerdrPaneProcessInfoAsync(surface);
708
+ if (
709
+ info.foregroundProcesses.some((process) =>
710
+ isExpectedPiProcess(process, sessionFile, cwd),
711
+ )
712
+ ) {
713
+ return;
714
+ }
715
+ } catch (error) {
716
+ lastError = error instanceof Error ? error.message : String(error);
717
+ }
718
+ if (Date.now() >= deadline) break;
719
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
720
+ }
721
+ throw new Error(
722
+ `Timed out waiting for Pi session ${sessionFile} in Herdr pane ${surface}: ${lastError}`,
723
+ );
724
+ }
725
+
726
+ export async function waitForHerdrShellReady(
727
+ surface: string,
728
+ options: {
729
+ timeoutMs?: number;
730
+ intervalMs?: number;
731
+ signal?: AbortSignal;
732
+ } = {},
733
+ ): Promise<void> {
734
+ const timeoutMs = options.timeoutMs ?? 10_000;
735
+ const intervalMs = options.intervalMs ?? 50;
736
+ const deadline = Date.now() + timeoutMs;
737
+ let lastError = "no interactive shell foreground process";
738
+
739
+ while (Date.now() <= deadline) {
740
+ if (options.signal?.aborted)
741
+ throw new Error("Shell readiness wait cancelled.");
742
+ try {
743
+ if (isHerdrShellReady(await getHerdrPaneProcessInfoAsync(surface)))
744
+ return;
745
+ } catch (error) {
746
+ lastError = error instanceof Error ? error.message : String(error);
747
+ }
748
+ if (Date.now() >= deadline) break;
749
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
750
+ }
751
+ throw new Error(
752
+ `Timed out waiting for interactive shell in Herdr pane ${surface}: ${lastError}`,
753
+ );
754
+ }
755
+
756
+ export function isProcessAlive(pid: number): boolean {
757
+ try {
758
+ process.kill(pid, 0);
759
+ return true;
760
+ } catch (error) {
761
+ return (error as NodeJS.ErrnoException).code === "EPERM";
762
+ }
763
+ }
764
+
765
+ export async function waitForProcessesExit(
766
+ pids: readonly number[],
767
+ options: {
768
+ timeoutMs?: number;
769
+ intervalMs?: number;
770
+ isAlive?: (pid: number) => boolean;
771
+ } = {},
772
+ ): Promise<number[]> {
773
+ const isAlive = options.isAlive ?? isProcessAlive;
774
+ const timeoutMs = options.timeoutMs ?? 5_000;
775
+ const intervalMs = options.intervalMs ?? 50;
776
+ const remaining = new Set(
777
+ pids.filter((pid) => Number.isInteger(pid) && pid > 0 && isAlive(pid)),
778
+ );
779
+ const deadline = Date.now() + timeoutMs;
780
+ while (remaining.size > 0 && Date.now() < deadline) {
781
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
782
+ for (const pid of remaining) {
783
+ if (!isAlive(pid)) remaining.delete(pid);
784
+ }
785
+ }
786
+ return [...remaining];
787
+ }
788
+
789
+ export async function waitForHerdrPaneAbsence(
790
+ surface: string,
791
+ options: {
792
+ timeoutMs?: number;
793
+ intervalMs?: number;
794
+ inspect?: (surface: string) => Promise<PaneInspectionResult>;
795
+ } = {},
796
+ ): Promise<boolean> {
797
+ const inspect = options.inspect ?? inspectHerdrPane;
798
+ const timeoutMs = options.timeoutMs ?? 5_000;
799
+ const intervalMs = options.intervalMs ?? 50;
800
+ const deadline = Date.now() + timeoutMs;
801
+ while (Date.now() <= deadline) {
802
+ const inspection = await inspect(surface);
803
+ if (inspection.kind === "missing") return true;
804
+ if (Date.now() >= deadline) break;
805
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
806
+ }
807
+ const finalInspection = await inspect(surface);
808
+ return finalInspection.kind === "missing";
809
+ }
810
+
811
+ export function sendHerdrCommand(surface: string, command: string): void {
812
+ // pane run sends the text and Enter in a single socket request, avoiding
813
+ // a race where Enter could arrive before the text is fully processed.
814
+ herdrExec(["pane", "run", surface, command]);
815
+ }
816
+
817
+ export function sendHerdrEscape(surface: string): void {
818
+ herdrExec(["pane", "send-keys", surface, "Escape"]);
819
+ }
820
+
821
+ function hasHerdrErrorCode(error: any, codes: readonly string[]): boolean {
822
+ const accepted = new Set(codes);
823
+ for (const raw of [error?.stderr, error?.stdout, error?.message]) {
824
+ if (typeof raw !== "string" || !raw.trim()) continue;
825
+ const parsed = parseHerdrJson(raw) as {
826
+ error?: { code?: unknown };
827
+ } | null;
828
+ if (
829
+ typeof parsed?.error?.code === "string" &&
830
+ accepted.has(parsed.error.code)
831
+ ) {
832
+ return true;
833
+ }
834
+ for (const code of accepted) {
835
+ if (new RegExp(`\\b${code}\\b`).test(raw)) return true;
836
+ }
837
+ }
838
+ return false;
839
+ }
840
+
841
+ function lookupHerdrSurfaceTab(surface: string): string | undefined {
842
+ try {
843
+ const output = herdrExec(["pane", "get", surface]);
844
+ const parsed = parseHerdrJson(output) as {
845
+ result?: { pane?: { pane_id?: unknown; tab_id?: unknown } };
846
+ error?: { code?: unknown };
847
+ } | null;
848
+ if (
849
+ parsed?.error?.code === "pane_not_found" ||
850
+ parsed?.error?.code === "not_found"
851
+ ) {
852
+ return undefined;
853
+ }
854
+ const pane = parsed?.result?.pane;
855
+ if (
856
+ pane?.pane_id !== surface ||
857
+ typeof pane.tab_id !== "string" ||
858
+ !pane.tab_id
859
+ ) {
860
+ throw new Error(
861
+ `Unexpected herdr pane get output: ${output.trim() || "(empty)"}`,
862
+ );
863
+ }
864
+ rememberHerdrSurfaceTab(surface, pane.tab_id);
865
+ return pane.tab_id;
866
+ } catch (error) {
867
+ if (hasHerdrErrorCode(error, ["pane_not_found", "not_found"])) {
868
+ return undefined;
869
+ }
870
+ throw error;
871
+ }
872
+ }
873
+
874
+ /** Close the dedicated tab for a surface; an already missing target is success. */
875
+ export function closeHerdrSurface(surface: string): void {
876
+ const rememberedTab = getHerdrSurfaceTabId(surface);
877
+ const tabId = rememberedTab ?? lookupHerdrSurfaceTab(surface);
878
+ if (!tabId) return;
879
+ try {
880
+ herdrExec(["tab", "close", tabId]);
881
+ forgetHerdrTab(tabId);
882
+ } catch (error) {
883
+ if (
884
+ hasHerdrErrorCode(error, [
885
+ "tab_not_found",
886
+ "pane_not_found",
887
+ "not_found",
888
+ ])
889
+ ) {
890
+ forgetHerdrTab(tabId);
891
+ return;
892
+ }
893
+ throw error;
894
+ }
895
+ }
896
+
897
+ export function renameHerdrTab(title: string): void {
898
+ const { tab_id: tabId } = getHerdrCurrentPaneInfo();
899
+ herdrExec(["tab", "rename", tabId, title]);
900
+ }
901
+
902
+ export function renameHerdrWorkspace(title: string): void {
903
+ const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
904
+ herdrExec(["workspace", "rename", workspaceId, title]);
905
+ }
906
+
907
+ export function focusHerdrWorkspace(workspaceId: string): void {
908
+ herdrExec(["workspace", "focus", workspaceId]);
909
+ }
910
+
911
+ export const __herdrTest__ = {
912
+ buildCurrentPaneArgs,
913
+ getHerdrCurrentPaneInfo,
914
+ buildTabCreateArgs,
915
+ buildWorktreeCreateArgs,
916
+ parseHerdrJson,
917
+ extractHerdrPaneId,
918
+ extractHerdrRootPaneId,
919
+ extractHerdrWorktree,
920
+ parseHerdrWorktreeList,
921
+ parseHerdrPaneList,
922
+ parsePaneGetOutput,
923
+ parsePaneGetError,
924
+ parsePaneProcessInfo,
925
+ isHerdrShellReady,
926
+ isExpectedPiProcess,
927
+ };