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,645 @@
1
+ import { execFile, execSync, execFileSync } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ const commandAvailability = new Map();
5
+ const surfaceTabs = new Map();
6
+ function hasCommand(command) {
7
+ if (commandAvailability.has(command)) {
8
+ return commandAvailability.get(command);
9
+ }
10
+ let available = false;
11
+ if (process.platform === "win32") {
12
+ try {
13
+ execFileSync("where.exe", [command], { stdio: "ignore" });
14
+ available = true;
15
+ }
16
+ catch {
17
+ try {
18
+ execSync(`command -v ${command}`, { stdio: "ignore" });
19
+ available = true;
20
+ }
21
+ catch {
22
+ available = false;
23
+ }
24
+ }
25
+ }
26
+ else {
27
+ try {
28
+ execSync(`command -v ${command}`, { stdio: "ignore" });
29
+ available = true;
30
+ }
31
+ catch {
32
+ available = false;
33
+ }
34
+ }
35
+ commandAvailability.set(command, available);
36
+ return available;
37
+ }
38
+ export function isHerdrAvailable() {
39
+ return process.env.HERDR_ENV === "1" && hasCommand("herdr");
40
+ }
41
+ function parseHerdrJson(value) {
42
+ try {
43
+ return JSON.parse(value);
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function extractHerdrPaneId(output, context) {
50
+ const parsed = parseHerdrJson(output);
51
+ const paneId = parsed
52
+ ?.result?.pane?.pane_id;
53
+ if (typeof paneId !== "string" || !paneId) {
54
+ throw new Error(`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`);
55
+ }
56
+ return paneId;
57
+ }
58
+ function extractHerdrRootPaneId(output, context) {
59
+ const parsed = parseHerdrJson(output);
60
+ const paneId = parsed
61
+ ?.result?.root_pane?.pane_id;
62
+ if (typeof paneId !== "string" || !paneId) {
63
+ throw new Error(`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`);
64
+ }
65
+ return paneId;
66
+ }
67
+ function extractHerdrTabSurface(output, context) {
68
+ const parsed = parseHerdrJson(output);
69
+ const paneId = parsed?.result?.root_pane?.pane_id;
70
+ const tabId = parsed?.result?.tab?.tab_id ?? parsed?.result?.root_pane?.tab_id;
71
+ if (typeof paneId !== "string" ||
72
+ !paneId ||
73
+ typeof tabId !== "string" ||
74
+ !tabId) {
75
+ throw new Error(`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`);
76
+ }
77
+ return { paneId, tabId };
78
+ }
79
+ /** Return the remembered owning tab for a Herdr surface created by this process. */
80
+ export function getHerdrSurfaceTabId(surface) {
81
+ return surfaceTabs.get(surface);
82
+ }
83
+ /** Restore a durable surface-to-tab association before retrying cleanup. */
84
+ export function rememberHerdrSurfaceTab(surface, tabId) {
85
+ if (surface && tabId)
86
+ surfaceTabs.set(surface, tabId);
87
+ }
88
+ function forgetHerdrTab(tabId) {
89
+ for (const [surface, rememberedTab] of surfaceTabs) {
90
+ if (rememberedTab === tabId)
91
+ surfaceTabs.delete(surface);
92
+ }
93
+ }
94
+ function extractHerdrWorktree(output) {
95
+ const parsed = parseHerdrJson(output);
96
+ const result = parsed?.result;
97
+ if (result?.type !== "worktree_created" ||
98
+ typeof result.workspace?.workspace_id !== "string" ||
99
+ !result.workspace.workspace_id ||
100
+ typeof result.root_pane?.pane_id !== "string" ||
101
+ !result.root_pane.pane_id ||
102
+ typeof result.worktree?.path !== "string" ||
103
+ !result.worktree.path ||
104
+ typeof result.worktree.branch !== "string" ||
105
+ !result.worktree.branch) {
106
+ throw new Error(`Unexpected herdr worktree create output: ${output.trim() || "(empty)"}`);
107
+ }
108
+ return {
109
+ path: result.worktree.path,
110
+ branch: result.worktree.branch,
111
+ workspaceId: result.workspace.workspace_id,
112
+ paneId: result.root_pane.pane_id,
113
+ };
114
+ }
115
+ function herdrExec(args) {
116
+ return execFileSync("herdr", args, {
117
+ encoding: "utf8",
118
+ stdio: ["ignore", "pipe", "pipe"],
119
+ });
120
+ }
121
+ async function herdrExecAsync(args) {
122
+ const { stdout } = await execFileAsync("herdr", args, { encoding: "utf8" });
123
+ return stdout;
124
+ }
125
+ function getHerdrParentPaneId() {
126
+ const paneId = process.env.HERDR_PANE_ID;
127
+ if (!paneId) {
128
+ throw new Error("HERDR_PANE_ID not set");
129
+ }
130
+ return paneId;
131
+ }
132
+ function buildCurrentPaneArgs() {
133
+ return ["pane", "current", "--current"];
134
+ }
135
+ function getHerdrCurrentPaneInfo(run = herdrExec) {
136
+ // Inherited IDs can outlive a pane move; Herdr resolves the caller identity.
137
+ const output = run(buildCurrentPaneArgs());
138
+ const parsed = parseHerdrJson(output);
139
+ const pane = parsed?.result?.pane;
140
+ if (!pane?.pane_id || !pane?.tab_id || !pane?.workspace_id) {
141
+ throw new Error(`Unexpected herdr pane current output: ${output.trim() || "(empty)"}`);
142
+ }
143
+ return { pane_id: pane.pane_id, tab_id: pane.tab_id, workspace_id: pane.workspace_id };
144
+ }
145
+ /** Read the current pane's manual name; unavailable metadata must not block a launch. */
146
+ export function getHerdrCurrentPaneTitle(run = herdrExec) {
147
+ try {
148
+ const parsed = parseHerdrJson(run(buildCurrentPaneArgs()));
149
+ const pane = parsed?.result?.pane;
150
+ return [pane?.title, pane?.label].find((value) => typeof value === "string" && !!value.trim());
151
+ }
152
+ catch {
153
+ return undefined;
154
+ }
155
+ }
156
+ function buildTabCreateArgs(name, cwd, workspaceId) {
157
+ return [
158
+ "tab",
159
+ "create",
160
+ "--workspace",
161
+ workspaceId,
162
+ "--label",
163
+ name,
164
+ "--cwd",
165
+ cwd,
166
+ "--no-focus",
167
+ ];
168
+ }
169
+ function buildWorktreeCreateArgs(name, cwd, branch, base) {
170
+ return [
171
+ "worktree",
172
+ "create",
173
+ "--cwd",
174
+ cwd,
175
+ "--branch",
176
+ branch,
177
+ "--base",
178
+ base,
179
+ "--label",
180
+ name,
181
+ "--no-focus",
182
+ ];
183
+ }
184
+ export function createHerdrSurfaceInWorkspace(name, cwd, workspaceId) {
185
+ const output = herdrExec(buildTabCreateArgs(name, cwd, workspaceId));
186
+ const { paneId, tabId } = extractHerdrTabSurface(output, "tab create");
187
+ rememberHerdrSurfaceTab(paneId, tabId);
188
+ try {
189
+ herdrExec(["pane", "rename", paneId, name]);
190
+ }
191
+ catch {
192
+ // Optional — pane label is cosmetic.
193
+ }
194
+ return paneId;
195
+ }
196
+ export function createHerdrSurface(name) {
197
+ // Create a new tab per subagent so parallel spawns each get a full tab
198
+ // instead of ever-narrower splits of the parent pane. Target the current
199
+ // workspace explicitly because Herdr's implicit default may be another space.
200
+ const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
201
+ return createHerdrSurfaceInWorkspace(name, process.cwd(), workspaceId);
202
+ }
203
+ export class HerdrWorktreeCreateError extends Error {
204
+ recoveredWorktree;
205
+ constructor(message, recoveredWorktree) {
206
+ super(message);
207
+ this.name = "HerdrWorktreeCreateError";
208
+ this.recoveredWorktree = recoveredWorktree;
209
+ }
210
+ }
211
+ export function parseHerdrWorktreeList(output) {
212
+ const parsed = parseHerdrJson(output);
213
+ const worktrees = parsed?.result?.worktrees;
214
+ if (parsed?.result?.type !== "worktree_list" || !Array.isArray(worktrees)) {
215
+ throw new Error("Unexpected herdr worktree list output");
216
+ }
217
+ // Detached checkouts have no branch and cannot match an owned replica.
218
+ return worktrees.filter((worktree) => !(worktree.is_detached === true && worktree.branch === undefined && typeof worktree.path === "string")).map((worktree) => {
219
+ if (typeof worktree.branch !== "string" ||
220
+ typeof worktree.path !== "string") {
221
+ throw new Error("Unexpected herdr worktree list entry");
222
+ }
223
+ return {
224
+ branch: worktree.branch,
225
+ path: worktree.path,
226
+ ...(typeof worktree.label === "string" ? { label: worktree.label } : {}),
227
+ ...(typeof worktree.open_workspace_id === "string"
228
+ ? { workspaceId: worktree.open_workspace_id }
229
+ : {}),
230
+ isLinkedWorktree: worktree.is_linked_worktree === true,
231
+ };
232
+ });
233
+ }
234
+ export function listHerdrWorktrees(cwd) {
235
+ const args = ["worktree", "list"];
236
+ if (cwd)
237
+ args.push("--cwd", cwd);
238
+ return parseHerdrWorktreeList(herdrExec(args));
239
+ }
240
+ function parseHerdrPaneList(output, workspaceId) {
241
+ const parsed = parseHerdrJson(output);
242
+ if (parsed?.result?.type !== "pane_list" ||
243
+ !Array.isArray(parsed.result.panes)) {
244
+ throw new Error("Unexpected herdr pane list output");
245
+ }
246
+ return parsed.result.panes
247
+ .filter((pane) => pane.workspace_id === workspaceId)
248
+ .map((pane) => pane.pane_id)
249
+ .filter((paneId) => typeof paneId === "string");
250
+ }
251
+ function recoverHerdrWorktree(cwd, branch) {
252
+ const matches = listHerdrWorktrees(cwd).filter((worktree) => worktree.branch === branch);
253
+ if (matches.length !== 1)
254
+ return undefined;
255
+ const worktree = matches[0];
256
+ if (!worktree.workspaceId)
257
+ return worktree;
258
+ const panes = parseHerdrPaneList(herdrExec(["pane", "list", "--workspace", worktree.workspaceId]), worktree.workspaceId);
259
+ if (panes.length !== 1)
260
+ return worktree;
261
+ return {
262
+ path: worktree.path,
263
+ branch: worktree.branch,
264
+ workspaceId: worktree.workspaceId,
265
+ paneId: panes[0],
266
+ };
267
+ }
268
+ export function createHerdrWorktree(name, cwd, branch, base) {
269
+ const output = herdrExec(buildWorktreeCreateArgs(name, cwd, branch, base));
270
+ try {
271
+ return extractHerdrWorktree(output);
272
+ }
273
+ catch (parseError) {
274
+ let recovered;
275
+ try {
276
+ recovered = recoverHerdrWorktree(cwd, branch);
277
+ }
278
+ catch {
279
+ throw parseError;
280
+ }
281
+ if (recovered?.workspaceId && "paneId" in recovered)
282
+ return recovered;
283
+ if (recovered) {
284
+ throw new HerdrWorktreeCreateError(`Herdr created branch ${branch}, but its workspace response was incomplete`, recovered);
285
+ }
286
+ throw parseError;
287
+ }
288
+ }
289
+ export function createHerdrSurfaceSplit(name, direction) {
290
+ const parentPaneId = getHerdrParentPaneId();
291
+ const output = herdrExec([
292
+ "pane",
293
+ "split",
294
+ parentPaneId,
295
+ "--direction",
296
+ direction,
297
+ "--no-focus",
298
+ "--cwd",
299
+ process.cwd(),
300
+ ]);
301
+ const paneId = extractHerdrPaneId(output, "pane split");
302
+ try {
303
+ herdrExec(["pane", "rename", paneId, name]);
304
+ }
305
+ catch {
306
+ // Optional.
307
+ }
308
+ return paneId;
309
+ }
310
+ export function readHerdrScreen(surface, lines = 50) {
311
+ // `visible` is reliable for freshly created panes where herdr's `recent`
312
+ // scrollback may not be populated yet.
313
+ return herdrExec([
314
+ "pane",
315
+ "read",
316
+ surface,
317
+ "--source",
318
+ "visible",
319
+ "--lines",
320
+ String(lines),
321
+ ]);
322
+ }
323
+ export async function readHerdrScreenAsync(surface, lines = 50, source = "visible") {
324
+ return herdrExecAsync([
325
+ "pane",
326
+ "read",
327
+ surface,
328
+ "--source",
329
+ source,
330
+ "--lines",
331
+ String(lines),
332
+ ]);
333
+ }
334
+ function parsePaneGetOutput(output, surface) {
335
+ const parsed = parseHerdrJson(output);
336
+ const errorObj = parsed?.error;
337
+ if (errorObj?.code === "pane_not_found" || errorObj?.code === "not_found") {
338
+ return {
339
+ kind: "missing",
340
+ error: typeof errorObj.message === "string"
341
+ ? errorObj.message
342
+ : "pane not found",
343
+ };
344
+ }
345
+ const pane = parsed?.result?.pane;
346
+ if (!pane || typeof pane !== "object")
347
+ return { kind: "unavailable", error: "pane get returned no pane record" };
348
+ const record = pane;
349
+ if (record.pane_id !== surface)
350
+ return { kind: "unavailable", error: "pane id mismatch" };
351
+ const agent = typeof record.agent === "string" ? record.agent : undefined;
352
+ const rawStatus = typeof record.agent_status === "string" ? record.agent_status : "unknown";
353
+ const agentStatus = rawStatus === "idle" ||
354
+ rawStatus === "working" ||
355
+ rawStatus === "blocked" ||
356
+ rawStatus === "done" ||
357
+ rawStatus === "unknown"
358
+ ? rawStatus
359
+ : "unknown";
360
+ return { kind: "present", ...(agent ? { agent } : {}), agentStatus };
361
+ }
362
+ function parsePaneGetError(error) {
363
+ for (const raw of [error?.stderr, error?.stdout]) {
364
+ if (typeof raw !== "string" || !raw.trim())
365
+ continue;
366
+ try {
367
+ const parsed = parsePaneGetOutput(raw, "");
368
+ if (parsed.kind === "missing")
369
+ return parsed;
370
+ }
371
+ catch {
372
+ // A CLI may emit plain diagnostics on one stream and structured JSON on
373
+ // the other. Parse each stream independently before giving up.
374
+ }
375
+ // Older/alternate Herdr builds may print the stable error code as plain
376
+ // text rather than JSON. Only match explicit identifiers, not generic
377
+ // prose such as "pane unavailable".
378
+ if (/\b(?:pane_not_found|not_found)\b/.test(raw)) {
379
+ return { kind: "missing", error: raw.trim() };
380
+ }
381
+ }
382
+ const message = error?.message
383
+ ? String(error.message)
384
+ : "herdr pane get failed";
385
+ return { kind: "unavailable", error: message };
386
+ }
387
+ /**
388
+ * Structured pane query.
389
+ * - present: pane is reachable; agent/agentStatus may be present when detected
390
+ * - missing: server responded, pane is gone
391
+ * - unavailable: server command failed; caller should keep polling
392
+ */
393
+ export async function inspectHerdrPane(surface) {
394
+ try {
395
+ return parsePaneGetOutput(await herdrExecAsync(["pane", "get", surface]), surface);
396
+ }
397
+ catch (error) {
398
+ return parsePaneGetError(error);
399
+ }
400
+ }
401
+ export function parsePaneProcessInfo(output, paneId) {
402
+ const parsed = parseHerdrJson(output);
403
+ const info = parsed?.result?.process_info;
404
+ if (!info || typeof info !== "object") {
405
+ throw new Error(`Unexpected herdr pane process-info output: ${output.trim() || "(empty)"}`);
406
+ }
407
+ if (typeof info.pane_id === "string" && info.pane_id !== paneId) {
408
+ throw new Error(`herdr pane process-info pane id mismatch: ${info.pane_id} != ${paneId}`);
409
+ }
410
+ const pids = new Set();
411
+ if (typeof info.shell_pid === "number" &&
412
+ Number.isInteger(info.shell_pid) &&
413
+ info.shell_pid > 0) {
414
+ pids.add(info.shell_pid);
415
+ }
416
+ if (typeof info.foreground_process_group_id === "number" &&
417
+ Number.isInteger(info.foreground_process_group_id) &&
418
+ info.foreground_process_group_id > 0) {
419
+ pids.add(info.foreground_process_group_id);
420
+ }
421
+ const foregroundProcesses = [];
422
+ for (const process of info.foreground_processes ?? []) {
423
+ if (typeof process?.pid === "number" &&
424
+ Number.isInteger(process.pid) &&
425
+ process.pid > 0) {
426
+ pids.add(process.pid);
427
+ foregroundProcesses.push({
428
+ pid: process.pid,
429
+ ...(typeof process.name === "string" ? { name: process.name } : {}),
430
+ ...(typeof process.argv0 === "string" ? { argv0: process.argv0 } : {}),
431
+ ...(Array.isArray(process.argv) &&
432
+ process.argv.every((value) => typeof value === "string")
433
+ ? { argv: process.argv }
434
+ : {}),
435
+ ...(typeof process.cwd === "string" ? { cwd: process.cwd } : {}),
436
+ });
437
+ }
438
+ }
439
+ return {
440
+ paneId,
441
+ ...(typeof info.shell_pid === "number" ? { shellPid: info.shell_pid } : {}),
442
+ ...(typeof info.foreground_process_group_id === "number"
443
+ ? { foregroundProcessGroupId: info.foreground_process_group_id }
444
+ : {}),
445
+ pids: [...pids],
446
+ foregroundProcesses,
447
+ };
448
+ }
449
+ export function getHerdrPaneProcessInfo(surface) {
450
+ return parsePaneProcessInfo(herdrExec(["pane", "process-info", "--pane", surface]), surface);
451
+ }
452
+ async function getHerdrPaneProcessInfoAsync(surface) {
453
+ return parsePaneProcessInfo(await herdrExecAsync(["pane", "process-info", "--pane", surface]), surface);
454
+ }
455
+ function isHerdrShellReady(info) {
456
+ return (info.shellPid != null && info.foregroundProcessGroupId === info.shellPid);
457
+ }
458
+ function isExpectedPiProcess(process, sessionFile, cwd) {
459
+ const sessionIndex = process.argv?.indexOf("--session") ?? -1;
460
+ return ((process.name === "pi" || process.argv0?.split("/").pop() === "pi") &&
461
+ sessionIndex >= 0 &&
462
+ process.argv?.[sessionIndex + 1] === sessionFile &&
463
+ process.cwd === cwd);
464
+ }
465
+ export async function waitForHerdrPiReady(surface, sessionFile, cwd, options = {}) {
466
+ const timeoutMs = options.timeoutMs ?? 10_000;
467
+ const intervalMs = options.intervalMs ?? 50;
468
+ const deadline = Date.now() + timeoutMs;
469
+ let lastError = "expected Pi process not observed";
470
+ while (Date.now() <= deadline) {
471
+ try {
472
+ const info = await getHerdrPaneProcessInfoAsync(surface);
473
+ if (info.foregroundProcesses.some((process) => isExpectedPiProcess(process, sessionFile, cwd))) {
474
+ return;
475
+ }
476
+ }
477
+ catch (error) {
478
+ lastError = error instanceof Error ? error.message : String(error);
479
+ }
480
+ if (Date.now() >= deadline)
481
+ break;
482
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
483
+ }
484
+ throw new Error(`Timed out waiting for Pi session ${sessionFile} in Herdr pane ${surface}: ${lastError}`);
485
+ }
486
+ export async function waitForHerdrShellReady(surface, options = {}) {
487
+ const timeoutMs = options.timeoutMs ?? 10_000;
488
+ const intervalMs = options.intervalMs ?? 50;
489
+ const deadline = Date.now() + timeoutMs;
490
+ let lastError = "no interactive shell foreground process";
491
+ while (Date.now() <= deadline) {
492
+ if (options.signal?.aborted)
493
+ throw new Error("Shell readiness wait cancelled.");
494
+ try {
495
+ if (isHerdrShellReady(await getHerdrPaneProcessInfoAsync(surface)))
496
+ return;
497
+ }
498
+ catch (error) {
499
+ lastError = error instanceof Error ? error.message : String(error);
500
+ }
501
+ if (Date.now() >= deadline)
502
+ break;
503
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
504
+ }
505
+ throw new Error(`Timed out waiting for interactive shell in Herdr pane ${surface}: ${lastError}`);
506
+ }
507
+ export function isProcessAlive(pid) {
508
+ try {
509
+ process.kill(pid, 0);
510
+ return true;
511
+ }
512
+ catch (error) {
513
+ return error.code === "EPERM";
514
+ }
515
+ }
516
+ export async function waitForProcessesExit(pids, options = {}) {
517
+ const isAlive = options.isAlive ?? isProcessAlive;
518
+ const timeoutMs = options.timeoutMs ?? 5_000;
519
+ const intervalMs = options.intervalMs ?? 50;
520
+ const remaining = new Set(pids.filter((pid) => Number.isInteger(pid) && pid > 0 && isAlive(pid)));
521
+ const deadline = Date.now() + timeoutMs;
522
+ while (remaining.size > 0 && Date.now() < deadline) {
523
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
524
+ for (const pid of remaining) {
525
+ if (!isAlive(pid))
526
+ remaining.delete(pid);
527
+ }
528
+ }
529
+ return [...remaining];
530
+ }
531
+ export async function waitForHerdrPaneAbsence(surface, options = {}) {
532
+ const inspect = options.inspect ?? inspectHerdrPane;
533
+ const timeoutMs = options.timeoutMs ?? 5_000;
534
+ const intervalMs = options.intervalMs ?? 50;
535
+ const deadline = Date.now() + timeoutMs;
536
+ while (Date.now() <= deadline) {
537
+ const inspection = await inspect(surface);
538
+ if (inspection.kind === "missing")
539
+ return true;
540
+ if (Date.now() >= deadline)
541
+ break;
542
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
543
+ }
544
+ const finalInspection = await inspect(surface);
545
+ return finalInspection.kind === "missing";
546
+ }
547
+ export function sendHerdrCommand(surface, command) {
548
+ // pane run sends the text and Enter in a single socket request, avoiding
549
+ // a race where Enter could arrive before the text is fully processed.
550
+ herdrExec(["pane", "run", surface, command]);
551
+ }
552
+ export function sendHerdrEscape(surface) {
553
+ herdrExec(["pane", "send-keys", surface, "Escape"]);
554
+ }
555
+ function hasHerdrErrorCode(error, codes) {
556
+ const accepted = new Set(codes);
557
+ for (const raw of [error?.stderr, error?.stdout, error?.message]) {
558
+ if (typeof raw !== "string" || !raw.trim())
559
+ continue;
560
+ const parsed = parseHerdrJson(raw);
561
+ if (typeof parsed?.error?.code === "string" &&
562
+ accepted.has(parsed.error.code)) {
563
+ return true;
564
+ }
565
+ for (const code of accepted) {
566
+ if (new RegExp(`\\b${code}\\b`).test(raw))
567
+ return true;
568
+ }
569
+ }
570
+ return false;
571
+ }
572
+ function lookupHerdrSurfaceTab(surface) {
573
+ try {
574
+ const output = herdrExec(["pane", "get", surface]);
575
+ const parsed = parseHerdrJson(output);
576
+ if (parsed?.error?.code === "pane_not_found" ||
577
+ parsed?.error?.code === "not_found") {
578
+ return undefined;
579
+ }
580
+ const pane = parsed?.result?.pane;
581
+ if (pane?.pane_id !== surface ||
582
+ typeof pane.tab_id !== "string" ||
583
+ !pane.tab_id) {
584
+ throw new Error(`Unexpected herdr pane get output: ${output.trim() || "(empty)"}`);
585
+ }
586
+ rememberHerdrSurfaceTab(surface, pane.tab_id);
587
+ return pane.tab_id;
588
+ }
589
+ catch (error) {
590
+ if (hasHerdrErrorCode(error, ["pane_not_found", "not_found"])) {
591
+ return undefined;
592
+ }
593
+ throw error;
594
+ }
595
+ }
596
+ /** Close the dedicated tab for a surface; an already missing target is success. */
597
+ export function closeHerdrSurface(surface) {
598
+ const rememberedTab = getHerdrSurfaceTabId(surface);
599
+ const tabId = rememberedTab ?? lookupHerdrSurfaceTab(surface);
600
+ if (!tabId)
601
+ return;
602
+ try {
603
+ herdrExec(["tab", "close", tabId]);
604
+ forgetHerdrTab(tabId);
605
+ }
606
+ catch (error) {
607
+ if (hasHerdrErrorCode(error, [
608
+ "tab_not_found",
609
+ "pane_not_found",
610
+ "not_found",
611
+ ])) {
612
+ forgetHerdrTab(tabId);
613
+ return;
614
+ }
615
+ throw error;
616
+ }
617
+ }
618
+ export function renameHerdrTab(title) {
619
+ const { tab_id: tabId } = getHerdrCurrentPaneInfo();
620
+ herdrExec(["tab", "rename", tabId, title]);
621
+ }
622
+ export function renameHerdrWorkspace(title) {
623
+ const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo();
624
+ herdrExec(["workspace", "rename", workspaceId, title]);
625
+ }
626
+ export function focusHerdrWorkspace(workspaceId) {
627
+ herdrExec(["workspace", "focus", workspaceId]);
628
+ }
629
+ export const __herdrTest__ = {
630
+ buildCurrentPaneArgs,
631
+ getHerdrCurrentPaneInfo,
632
+ buildTabCreateArgs,
633
+ buildWorktreeCreateArgs,
634
+ parseHerdrJson,
635
+ extractHerdrPaneId,
636
+ extractHerdrRootPaneId,
637
+ extractHerdrWorktree,
638
+ parseHerdrWorktreeList,
639
+ parseHerdrPaneList,
640
+ parsePaneGetOutput,
641
+ parsePaneGetError,
642
+ parsePaneProcessInfo,
643
+ isHerdrShellReady,
644
+ isExpectedPiProcess,
645
+ };