pi-herdr-agents 0.1.0 → 0.1.2
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/README.md
CHANGED
|
@@ -78,12 +78,6 @@ pi
|
|
|
78
78
|
|
|
79
79
|
herdr is the only supported terminal environment. The extension requires `HERDR_ENV=1` and the `herdr` CLI to be available.
|
|
80
80
|
|
|
81
|
-
If your shell startup is slow and subagent commands sometimes get dropped before the prompt is ready, set `PI_SUBAGENT_SHELL_READY_DELAY_MS` to a higher value (defaults to `500`):
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
export PI_SUBAGENT_SHELL_READY_DELAY_MS=2500
|
|
85
|
-
```
|
|
86
|
-
|
|
87
81
|
### Troubleshooting completion delivery
|
|
88
82
|
|
|
89
83
|
If a child finishes but the parent returns an empty or unrelated response, first verify that the result reached the parent session:
|
|
@@ -429,7 +423,7 @@ herdr_workflow({ action: "cancel", runId: "run-1" });
|
|
|
429
423
|
|
|
430
424
|
- Cancel claims a process-global terminal gate. Completion, failure, interruption, and cancellation cannot each produce a terminal outcome.
|
|
431
425
|
- Queued `agent()` calls resolve as cancelled; no later reviewer or synthesizer starts.
|
|
432
|
-
-
|
|
426
|
+
- New panes are queried through Herdr process-info until their interactive shell is ready before launch. Active panes are queried again before close so foreground process identities can be waited on.
|
|
433
427
|
- After synchronous pane close, cancel waits for pane absence and captured process exit before disposing the reader checkout.
|
|
434
428
|
- If process identity cannot be captured for an active pane, the pane remains present after close, or any captured process still lives after the bounded wait, the checkout is retained and the run ends `failed` with `cancel_termination_failed`. Successful cancellation is not reported in that case.
|
|
435
429
|
- A successful cancel writes one `cancelled` terminal journal event and one result-free delivery. Repeated cancel is idempotent and returns the authoritative terminal outcome (including a prior fail-closed result).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-herdr-agents",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Asynchronous Pi subagents and approved review workflows in Herdr, with optional isolated Git worktrees",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@earendil-works/pi-ai": "^0.
|
|
53
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
54
|
-
"@earendil-works/pi-tui": "^0.
|
|
52
|
+
"@earendil-works/pi-ai": "^0.84.0",
|
|
53
|
+
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
54
|
+
"@earendil-works/pi-tui": "^0.84.0",
|
|
55
55
|
"@sinclair/typebox": "^0.34.52",
|
|
56
56
|
"oxlint": "^1.73.0"
|
|
57
57
|
}
|
|
@@ -129,6 +129,10 @@ function getHerdrParentPaneId(): string {
|
|
|
129
129
|
return paneId;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function buildCurrentPaneArgs(): string[] {
|
|
133
|
+
return ["pane", "current", "--current"];
|
|
134
|
+
}
|
|
135
|
+
|
|
132
136
|
function getHerdrCurrentPaneInfo(): {
|
|
133
137
|
pane_id: string;
|
|
134
138
|
tab_id: string;
|
|
@@ -141,7 +145,7 @@ function getHerdrCurrentPaneInfo(): {
|
|
|
141
145
|
// Fall back to `herdr pane current` if any identity env var is missing —
|
|
142
146
|
// older herdr versions may not set all three.
|
|
143
147
|
if (!paneId || !tabId || !workspaceId) {
|
|
144
|
-
const output = herdrExec(
|
|
148
|
+
const output = herdrExec(buildCurrentPaneArgs());
|
|
145
149
|
const parsed = parseHerdrJson(output);
|
|
146
150
|
const pane = (parsed as { result?: { pane?: unknown } } | null)?.result
|
|
147
151
|
?.pane as
|
|
@@ -449,6 +453,46 @@ export function getHerdrPaneProcessInfo(surface: string): HerdrPaneProcessInfo {
|
|
|
449
453
|
);
|
|
450
454
|
}
|
|
451
455
|
|
|
456
|
+
async function getHerdrPaneProcessInfoAsync(
|
|
457
|
+
surface: string,
|
|
458
|
+
): Promise<HerdrPaneProcessInfo> {
|
|
459
|
+
return parsePaneProcessInfo(
|
|
460
|
+
await herdrExecAsync(["pane", "process-info", "--pane", surface]),
|
|
461
|
+
surface,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function isHerdrShellReady(info: HerdrPaneProcessInfo): boolean {
|
|
466
|
+
return (
|
|
467
|
+
info.shellPid != null &&
|
|
468
|
+
info.foregroundProcessGroupId === info.shellPid
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export async function waitForHerdrShellReady(
|
|
473
|
+
surface: string,
|
|
474
|
+
options: { timeoutMs?: number; intervalMs?: number; signal?: AbortSignal } = {},
|
|
475
|
+
): Promise<void> {
|
|
476
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
477
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
478
|
+
const deadline = Date.now() + timeoutMs;
|
|
479
|
+
let lastError = "no interactive shell foreground process";
|
|
480
|
+
|
|
481
|
+
while (Date.now() <= deadline) {
|
|
482
|
+
if (options.signal?.aborted) throw new Error("Shell readiness wait cancelled.");
|
|
483
|
+
try {
|
|
484
|
+
if (isHerdrShellReady(await getHerdrPaneProcessInfoAsync(surface))) return;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
487
|
+
}
|
|
488
|
+
if (Date.now() >= deadline) break;
|
|
489
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
490
|
+
}
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Timed out waiting for interactive shell in Herdr pane ${surface}: ${lastError}`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
452
496
|
export function isProcessAlive(pid: number): boolean {
|
|
453
497
|
try {
|
|
454
498
|
process.kill(pid, 0);
|
|
@@ -529,6 +573,7 @@ export function renameHerdrWorkspace(title: string): void {
|
|
|
529
573
|
}
|
|
530
574
|
|
|
531
575
|
export const __herdrTest__ = {
|
|
576
|
+
buildCurrentPaneArgs,
|
|
532
577
|
buildTabCreateArgs,
|
|
533
578
|
buildWorktreeCreateArgs,
|
|
534
579
|
parseHerdrJson,
|
|
@@ -538,4 +583,5 @@ export const __herdrTest__ = {
|
|
|
538
583
|
parsePaneGetOutput,
|
|
539
584
|
parsePaneGetError,
|
|
540
585
|
parsePaneProcessInfo,
|
|
586
|
+
isHerdrShellReady,
|
|
541
587
|
};
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
readPaneAsync,
|
|
40
40
|
inspectPane,
|
|
41
41
|
getPaneProcessInfo,
|
|
42
|
+
waitForShellReady,
|
|
42
43
|
waitForPaneAbsence,
|
|
43
44
|
waitForProcessesExit,
|
|
44
45
|
} from "./terminal.ts";
|
|
@@ -803,19 +804,6 @@ function formatElapsed(seconds: number): string {
|
|
|
803
804
|
return `${m}m ${s}s`;
|
|
804
805
|
}
|
|
805
806
|
|
|
806
|
-
/**
|
|
807
|
-
* Wait long enough for a freshly created pane to finish shell startup.
|
|
808
|
-
*
|
|
809
|
-
* Some environments do extra shell-init work before the prompt is ready
|
|
810
|
-
* (for example direnv/devenv), so the delay is configurable for users who hit
|
|
811
|
-
* dropped commands. Keep the historical default at 500ms.
|
|
812
|
-
*/
|
|
813
|
-
function getShellReadyDelayMs(): number {
|
|
814
|
-
const raw = process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS?.trim();
|
|
815
|
-
const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
|
|
816
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 500;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
807
|
function muxUnavailableResult() {
|
|
820
808
|
return {
|
|
821
809
|
content: [
|
|
@@ -1984,7 +1972,6 @@ function buildWorkflowChildCommand(params: {
|
|
|
1984
1972
|
|
|
1985
1973
|
export const __test__ = {
|
|
1986
1974
|
borderLine,
|
|
1987
|
-
getShellReadyDelayMs,
|
|
1988
1975
|
renderSubagentWidgetLines,
|
|
1989
1976
|
loadAgentDefaults,
|
|
1990
1977
|
discoverAgentDefinitions,
|
|
@@ -2218,13 +2205,8 @@ async function launchSubagent(
|
|
|
2218
2205
|
});
|
|
2219
2206
|
}
|
|
2220
2207
|
|
|
2221
|
-
//
|
|
2222
|
-
|
|
2223
|
-
if (!options?.surface) {
|
|
2224
|
-
await new Promise<void>((resolve) =>
|
|
2225
|
-
setTimeout(resolve, getShellReadyDelayMs()),
|
|
2226
|
-
);
|
|
2227
|
-
}
|
|
2208
|
+
// `pane run` is safe only after the shell owns the foreground process group.
|
|
2209
|
+
await waitForShellReady(surface);
|
|
2228
2210
|
|
|
2229
2211
|
const launchBehavior = resolveLaunchBehavior(params, agentDefs);
|
|
2230
2212
|
|
|
@@ -3009,9 +2991,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
3009
2991
|
mkdirSync(dirname(sessionFile), { recursive: true });
|
|
3010
2992
|
surface = createSubagentPane(`${candidate.runId}: ${roleName}`);
|
|
3011
2993
|
owner.children.set(id, { controller: childController, surface });
|
|
3012
|
-
await
|
|
3013
|
-
setTimeout(done, getShellReadyDelayMs()),
|
|
3014
|
-
);
|
|
2994
|
+
await waitForShellReady(surface, { signal: childController.signal });
|
|
3015
2995
|
if (childController.signal.aborted)
|
|
3016
2996
|
return workflowFailure("cancelled", "Workflow cancelled.");
|
|
3017
2997
|
const command = buildWorkflowChildCommand({
|
|
@@ -4182,9 +4162,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
4182
4162
|
const entryCountBefore = getNewEntries(params.sessionPath, 0).length;
|
|
4183
4163
|
|
|
4184
4164
|
const surface = createSubagentPane(name);
|
|
4185
|
-
await
|
|
4186
|
-
setTimeout(resolve, getShellReadyDelayMs()),
|
|
4187
|
-
);
|
|
4165
|
+
await waitForShellReady(surface);
|
|
4188
4166
|
|
|
4189
4167
|
// Build pi resume command
|
|
4190
4168
|
const parts = ["pi", "--session", shellQuote(params.sessionPath)];
|
|
@@ -4418,9 +4396,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
4418
4396
|
|
|
4419
4397
|
sessionFile = createBtwSessionSnapshot(parentSessionFile, leafId);
|
|
4420
4398
|
surface = createSubagentPane("BTW");
|
|
4421
|
-
await
|
|
4422
|
-
setTimeout(resolve, getShellReadyDelayMs()),
|
|
4423
|
-
);
|
|
4399
|
+
await waitForShellReady(surface);
|
|
4424
4400
|
|
|
4425
4401
|
const artifactDir = getArtifactDir(
|
|
4426
4402
|
ctx.sessionManager.getSessionDir(),
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
createHerdrSurfaceSplit,
|
|
8
8
|
createHerdrWorktree,
|
|
9
9
|
getHerdrPaneProcessInfo,
|
|
10
|
+
waitForHerdrShellReady,
|
|
10
11
|
isHerdrAvailable,
|
|
11
12
|
isProcessAlive,
|
|
12
13
|
readHerdrScreen,
|
|
@@ -152,6 +153,14 @@ export function getPaneProcessInfo(paneId: PaneId): HerdrPaneProcessInfo {
|
|
|
152
153
|
return getHerdrPaneProcessInfo(paneId);
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
export async function waitForShellReady(
|
|
157
|
+
paneId: PaneId,
|
|
158
|
+
options?: { timeoutMs?: number; intervalMs?: number; signal?: AbortSignal },
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
assertTerminalAvailable();
|
|
161
|
+
return waitForHerdrShellReady(paneId, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
155
164
|
export async function waitForPaneAbsence(
|
|
156
165
|
paneId: PaneId,
|
|
157
166
|
options?: { timeoutMs?: number; intervalMs?: number },
|