pi-herdsman 0.17.0 → 0.18.0
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 +13 -0
- package/dist/index.js +140 -78
- package/dist/index.js.map +2 -2
- package/docs/development/smoke-testing.md +22 -2
- package/docs/development/validation.md +3 -1
- package/docs/getting-started.md +8 -1
- package/package.json +15 -1
package/README.md
CHANGED
|
@@ -46,6 +46,19 @@ to you.
|
|
|
46
46
|
|
|
47
47
|
## Install
|
|
48
48
|
|
|
49
|
+
### Linux / macOS bootstrap
|
|
50
|
+
|
|
51
|
+
Install the released, tested Pi / Herdr / Pi Herdsman stack:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
curl -fsSL https://raw.githubusercontent.com/boadij/pi-herdsman/main/install.sh | sh
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The bootstrapper requires Node `>=22.19.0` and npm. It reconciles the exact Pi
|
|
58
|
+
and Herdr versions declared by the current Pi Herdsman release, pins that Pi
|
|
59
|
+
Herdsman release, verifies the Herdr download, and refreshes the Herdr Pi
|
|
60
|
+
integration. Re-run it to update or repair the stack.
|
|
61
|
+
|
|
49
62
|
### Existing Pi / herdr
|
|
50
63
|
|
|
51
64
|
```sh
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
31
31
|
// package.json
|
|
32
32
|
var package_default = {
|
|
33
33
|
name: "pi-herdsman",
|
|
34
|
-
version: "0.
|
|
34
|
+
version: "0.18.0",
|
|
35
35
|
type: "module",
|
|
36
36
|
description: "Asynchronous Pi subagents and agent fleet orchestration for parallel coding agents with nested delegation, background work, and supervision in herdr.",
|
|
37
37
|
license: "Apache-2.0",
|
|
@@ -77,6 +77,20 @@ var package_default = {
|
|
|
77
77
|
],
|
|
78
78
|
image: "https://raw.githubusercontent.com/boadij/pi-herdsman/main/docs/assets/thumbnail.webp"
|
|
79
79
|
},
|
|
80
|
+
piHerdsman: {
|
|
81
|
+
runtime: {
|
|
82
|
+
pi: "0.87.1",
|
|
83
|
+
herdr: {
|
|
84
|
+
version: "0.9.1",
|
|
85
|
+
sha256: {
|
|
86
|
+
"linux-x86_64": "2a02fed16beb651ef006e1d43f048f652ca4dc58ad053cd2d44450563d5c54b7",
|
|
87
|
+
"linux-aarch64": "f4ccf4de745f2cb9a39a983e9ba3703dad50ec2a58dea83026ceab721bbd8d9e",
|
|
88
|
+
"macos-x86_64": "053be0639935fe54ab5efbdb46651054e4f6a753a5b43153c88bd6912bce1e94",
|
|
89
|
+
"macos-aarch64": "5fc7a7e7adfaca56fa80aa89dcb025693357268dab8285b9ce2d08a2313c89de"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
80
94
|
scripts: {
|
|
81
95
|
test: "node --experimental-test-module-mocks --import=./scripts/test-env.mjs --test --test-timeout=10000",
|
|
82
96
|
smoke: "node scripts/smoke.mjs",
|
|
@@ -1805,6 +1819,7 @@ var HerdrStartFailure = class extends Error {
|
|
|
1805
1819
|
};
|
|
1806
1820
|
var SETTLE_TIMEOUT = 2e3;
|
|
1807
1821
|
var START_DIAGNOSTIC_TIMEOUT = 2e3;
|
|
1822
|
+
var FRESH_PANE_BUSY_RETRY_TIMEOUT = 2e3;
|
|
1808
1823
|
var HERDR_START_TIMEOUT_MIN = 3001;
|
|
1809
1824
|
var HERDR_START_TIMEOUT_MAX = 3e5;
|
|
1810
1825
|
var STARTUP_TIMEOUT_MIN = HERDR_START_TIMEOUT_MIN + START_DIAGNOSTIC_TIMEOUT;
|
|
@@ -2044,6 +2059,12 @@ function sameCwd(observed, expected) {
|
|
|
2044
2059
|
if (typeof observed !== "string") return false;
|
|
2045
2060
|
return canonicalCwd(observed) === canonicalCwd(expected);
|
|
2046
2061
|
}
|
|
2062
|
+
function matchesAttemptTerminal(pane, attempt) {
|
|
2063
|
+
return pane?.pane_id === attempt.paneId && pane.workspace_id === attempt.workspaceId && pane.tab_id === attempt.tabId && pane.terminal_id === attempt.terminalId;
|
|
2064
|
+
}
|
|
2065
|
+
function matchesAttemptPane(pane, attempt) {
|
|
2066
|
+
return matchesAttemptTerminal(pane, attempt) && sameCwd(pane.cwd, attempt.cwd);
|
|
2067
|
+
}
|
|
2047
2068
|
function capturedShellProcess(process2) {
|
|
2048
2069
|
return process2.foreground_processes?.find(
|
|
2049
2070
|
(candidate) => candidate.pid === process2.shell_pid
|
|
@@ -2383,6 +2404,7 @@ async function startHerdrAgent(pi, ctx, options) {
|
|
|
2383
2404
|
const env = validateEnvironment(options.env ?? []);
|
|
2384
2405
|
const release = await lockLifecycle(ctx, options.signal);
|
|
2385
2406
|
let attempt;
|
|
2407
|
+
let retryAttempted = false;
|
|
2386
2408
|
let stage = "topology";
|
|
2387
2409
|
try {
|
|
2388
2410
|
const cwd = canonicalCwd(options.cwd);
|
|
@@ -2397,7 +2419,7 @@ async function startHerdrAgent(pi, ctx, options) {
|
|
|
2397
2419
|
error("start", "caller pane is required for split placement");
|
|
2398
2420
|
let tab;
|
|
2399
2421
|
let panes;
|
|
2400
|
-
let
|
|
2422
|
+
let createdPane;
|
|
2401
2423
|
let createdTab = false;
|
|
2402
2424
|
const tabs = placement.kind === "tab" && !placement.tabId ? void 0 : (await runHerdr(
|
|
2403
2425
|
pi,
|
|
@@ -2461,7 +2483,7 @@ async function startHerdrAgent(pi, ctx, options) {
|
|
|
2461
2483
|
}
|
|
2462
2484
|
);
|
|
2463
2485
|
tab = made.tab;
|
|
2464
|
-
|
|
2486
|
+
createdPane = made.root_pane;
|
|
2465
2487
|
createdTab = true;
|
|
2466
2488
|
} else {
|
|
2467
2489
|
const existingPanes = panes ?? (await runHerdr(
|
|
@@ -2511,31 +2533,44 @@ async function startHerdrAgent(pi, ctx, options) {
|
|
|
2511
2533
|
timeout: startupCallTimeout(startupDeadline)
|
|
2512
2534
|
}
|
|
2513
2535
|
);
|
|
2514
|
-
|
|
2536
|
+
createdPane = split.pane;
|
|
2515
2537
|
}
|
|
2516
2538
|
}
|
|
2517
|
-
|
|
2539
|
+
const paneId = createdPane?.pane_id;
|
|
2540
|
+
const terminalId = createdPane?.terminal_id;
|
|
2541
|
+
if (typeof paneId !== "string" || !paneId)
|
|
2542
|
+
error("start", "Herdr did not return a pane");
|
|
2543
|
+
if (typeof terminalId !== "string" || !terminalId)
|
|
2544
|
+
error("start", `pane ${paneId} did not include terminal identity`);
|
|
2518
2545
|
attempt = {
|
|
2519
2546
|
herdrAgent: alias(workspaceId, options.label, options.runId),
|
|
2520
2547
|
workspaceId,
|
|
2521
2548
|
tabId: tab.tab_id,
|
|
2522
2549
|
paneId,
|
|
2550
|
+
terminalId,
|
|
2523
2551
|
cwd,
|
|
2524
|
-
createdTab
|
|
2552
|
+
createdTab,
|
|
2553
|
+
launchMayHaveStarted: false
|
|
2525
2554
|
};
|
|
2526
2555
|
let started;
|
|
2527
2556
|
stage = "pane_readiness";
|
|
2528
|
-
let shell;
|
|
2529
2557
|
try {
|
|
2530
|
-
|
|
2558
|
+
await waitForShellMarker(
|
|
2531
2559
|
pi,
|
|
2532
2560
|
ctx,
|
|
2533
2561
|
paneId,
|
|
2534
|
-
void 0,
|
|
2535
2562
|
startupDeadline,
|
|
2536
2563
|
"start",
|
|
2537
2564
|
options.signal
|
|
2538
2565
|
);
|
|
2566
|
+
attempt.shellProcess = await paneProcess(
|
|
2567
|
+
pi,
|
|
2568
|
+
ctx,
|
|
2569
|
+
paneId,
|
|
2570
|
+
options.signal,
|
|
2571
|
+
startupDeadline,
|
|
2572
|
+
true
|
|
2573
|
+
);
|
|
2539
2574
|
} catch (failure) {
|
|
2540
2575
|
if (failure instanceof OperationError)
|
|
2541
2576
|
throw withStartupDiagnostic(
|
|
@@ -2551,7 +2586,6 @@ async function startHerdrAgent(pi, ctx, options) {
|
|
|
2551
2586
|
throw failure;
|
|
2552
2587
|
}
|
|
2553
2588
|
stage = "ownership_capture";
|
|
2554
|
-
attempt.shellProcess = shell;
|
|
2555
2589
|
const currentPanes = (await runHerdr(pi, ctx, ["pane", "list", "--workspace", workspaceId], {
|
|
2556
2590
|
signal: options.signal,
|
|
2557
2591
|
timeout: startupCallTimeout(startupDeadline)
|
|
@@ -2559,8 +2593,8 @@ async function startHerdrAgent(pi, ctx, options) {
|
|
|
2559
2593
|
const currentPane = currentPanes.find(
|
|
2560
2594
|
(item) => item.pane_id === paneId
|
|
2561
2595
|
);
|
|
2562
|
-
if (!currentPane
|
|
2563
|
-
error("start", `pane ${paneId}
|
|
2596
|
+
if (!matchesAttemptPane(currentPane, attempt))
|
|
2597
|
+
error("start", `pane ${paneId} identity changed before launch`);
|
|
2564
2598
|
if (createdTab) {
|
|
2565
2599
|
const tabPanes = currentPanes.filter(
|
|
2566
2600
|
(item) => item.tab_id === tab.tab_id
|
|
@@ -2568,67 +2602,81 @@ async function startHerdrAgent(pi, ctx, options) {
|
|
|
2568
2602
|
if (tabPanes.length !== 1 || tabPanes[0]?.pane_id !== paneId)
|
|
2569
2603
|
error("start", `tab ${tab.tab_id} topology changed before launch`);
|
|
2570
2604
|
}
|
|
2571
|
-
if (!sameCwd(currentPane.cwd, cwd))
|
|
2572
|
-
error("start", `pane ${paneId} cwd does not match requested cwd`, {
|
|
2573
|
-
expected_cwd: cwd,
|
|
2574
|
-
observed_cwd: typeof currentPane.cwd === "string" ? currentPane.cwd : null
|
|
2575
|
-
});
|
|
2576
2605
|
stage = "agent_start";
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
paneId,
|
|
2581
|
-
options.signal,
|
|
2582
|
-
startupDeadline,
|
|
2583
|
-
true
|
|
2606
|
+
const retryDeadline = Math.min(
|
|
2607
|
+
Date.now() + FRESH_PANE_BUSY_RETRY_TIMEOUT,
|
|
2608
|
+
startupDeadline - START_DIAGNOSTIC_TIMEOUT
|
|
2584
2609
|
);
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
"--pane",
|
|
2601
|
-
paneId,
|
|
2602
|
-
"--timeout",
|
|
2603
|
-
String(Math.min(remaining, childTimeout)),
|
|
2604
|
-
"--",
|
|
2605
|
-
...options.extensionPath ? ["--extension", options.extensionPath] : [],
|
|
2606
|
-
"--extension",
|
|
2607
|
-
HERDR_AGENT_STATE_EXTENSION,
|
|
2608
|
-
...options.agentArgs ?? []
|
|
2609
|
-
],
|
|
2610
|
-
{
|
|
2611
|
-
signal: options.signal,
|
|
2612
|
-
// Outlive Herdr's own readiness deadline by the diagnostic window so
|
|
2613
|
-
// its structured failure is the one reported. With the identical
|
|
2614
|
-
// value this command's kill always won the race, and "Herdr command
|
|
2615
|
-
// was killed" replaced Herdr's reason.
|
|
2616
|
-
timeout: Math.min(remaining, childTimeout) + START_DIAGNOSTIC_TIMEOUT
|
|
2617
|
-
}
|
|
2618
|
-
);
|
|
2619
|
-
} catch (failure) {
|
|
2620
|
-
if (isUnstructuredResultFailure(failure))
|
|
2621
|
-
throw withStartupDiagnostic(
|
|
2622
|
-
failure,
|
|
2623
|
-
await captureStartupDiagnostic(
|
|
2624
|
-
pi,
|
|
2625
|
-
ctx,
|
|
2610
|
+
for (; ; ) {
|
|
2611
|
+
const remaining = startupCallTimeout(startupDeadline, childTimeout);
|
|
2612
|
+
if (remaining < HERDR_START_TIMEOUT_MIN)
|
|
2613
|
+
error("start", "startup deadline exhausted before agent start");
|
|
2614
|
+
try {
|
|
2615
|
+
started = await runHerdr(
|
|
2616
|
+
pi,
|
|
2617
|
+
ctx,
|
|
2618
|
+
[
|
|
2619
|
+
"agent",
|
|
2620
|
+
"start",
|
|
2621
|
+
attempt.herdrAgent,
|
|
2622
|
+
"--kind",
|
|
2623
|
+
"pi",
|
|
2624
|
+
"--pane",
|
|
2626
2625
|
paneId,
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2626
|
+
"--timeout",
|
|
2627
|
+
String(Math.min(remaining, childTimeout)),
|
|
2628
|
+
"--",
|
|
2629
|
+
...options.extensionPath ? ["--extension", options.extensionPath] : [],
|
|
2630
|
+
"--extension",
|
|
2631
|
+
HERDR_AGENT_STATE_EXTENSION,
|
|
2632
|
+
...options.agentArgs ?? []
|
|
2633
|
+
],
|
|
2634
|
+
{
|
|
2635
|
+
signal: options.signal,
|
|
2636
|
+
// Outlive Herdr's own readiness deadline by the diagnostic window so
|
|
2637
|
+
// its structured failure is the one reported. With the identical
|
|
2638
|
+
// value this command's kill always won the race, and "Herdr command
|
|
2639
|
+
// was killed" replaced Herdr's reason.
|
|
2640
|
+
timeout: Math.min(remaining, childTimeout) + START_DIAGNOSTIC_TIMEOUT
|
|
2641
|
+
}
|
|
2630
2642
|
);
|
|
2631
|
-
|
|
2643
|
+
attempt.launchMayHaveStarted = true;
|
|
2644
|
+
break;
|
|
2645
|
+
} catch (failure) {
|
|
2646
|
+
const busy = failure instanceof OperationError && failure.detail.details?.herdrCode === "agent_pane_busy";
|
|
2647
|
+
if (busy && Date.now() < retryDeadline) {
|
|
2648
|
+
retryAttempted = true;
|
|
2649
|
+
const pane = (await runHerdr(pi, ctx, ["pane", "get", paneId], {
|
|
2650
|
+
signal: options.signal,
|
|
2651
|
+
timeout: startupCallTimeout(startupDeadline)
|
|
2652
|
+
})).pane;
|
|
2653
|
+
if (!matchesAttemptPane(pane, attempt))
|
|
2654
|
+
error(
|
|
2655
|
+
"start",
|
|
2656
|
+
`pane ${paneId} identity changed during startup retry`
|
|
2657
|
+
);
|
|
2658
|
+
const remainingRetry = retryDeadline - Date.now();
|
|
2659
|
+
if (remainingRetry > 0)
|
|
2660
|
+
await sleep(Math.min(POLL_INTERVAL, remainingRetry), void 0, {
|
|
2661
|
+
signal: options.signal
|
|
2662
|
+
});
|
|
2663
|
+
if (Date.now() >= retryDeadline) throw failure;
|
|
2664
|
+
continue;
|
|
2665
|
+
}
|
|
2666
|
+
if (!busy) attempt.launchMayHaveStarted = true;
|
|
2667
|
+
if (isUnstructuredResultFailure(failure))
|
|
2668
|
+
throw withStartupDiagnostic(
|
|
2669
|
+
failure,
|
|
2670
|
+
await captureStartupDiagnostic(
|
|
2671
|
+
pi,
|
|
2672
|
+
ctx,
|
|
2673
|
+
paneId,
|
|
2674
|
+
startupDeadline,
|
|
2675
|
+
options.signal
|
|
2676
|
+
)
|
|
2677
|
+
);
|
|
2678
|
+
throw failure;
|
|
2679
|
+
}
|
|
2632
2680
|
}
|
|
2633
2681
|
stage = "agent_result";
|
|
2634
2682
|
const agent = started.agent;
|
|
@@ -2640,7 +2688,8 @@ async function startHerdrAgent(pi, ctx, options) {
|
|
|
2640
2688
|
attempt.agent = agent;
|
|
2641
2689
|
return attempt;
|
|
2642
2690
|
} catch (cause) {
|
|
2643
|
-
if (attempt)
|
|
2691
|
+
if (attempt)
|
|
2692
|
+
throw new HerdrStartFailure(cause, stage, attempt, retryAttempted);
|
|
2644
2693
|
throw cause;
|
|
2645
2694
|
} finally {
|
|
2646
2695
|
release();
|
|
@@ -2718,7 +2767,7 @@ function normalizePaneProcess(value2, paneId, required) {
|
|
|
2718
2767
|
...foreground !== void 0 ? { foreground_processes: Object.freeze(foreground) } : {}
|
|
2719
2768
|
});
|
|
2720
2769
|
}
|
|
2721
|
-
async function
|
|
2770
|
+
async function waitForShellMarker(pi, ctx, paneId, deadline, operation, signal) {
|
|
2722
2771
|
const timeout = () => {
|
|
2723
2772
|
if (operation === "start") return startupCallTimeout(deadline);
|
|
2724
2773
|
const remaining = deadline - Date.now();
|
|
@@ -2751,11 +2800,17 @@ async function proveShellReady(pi, ctx, paneId, expected, deadline, operation, s
|
|
|
2751
2800
|
noResult: true
|
|
2752
2801
|
}
|
|
2753
2802
|
);
|
|
2803
|
+
}
|
|
2804
|
+
async function proveShellReady(pi, ctx, paneId, expected, deadline, operation, signal) {
|
|
2805
|
+
await waitForShellMarker(pi, ctx, paneId, deadline, operation, signal);
|
|
2806
|
+
const timeout = operation === "start" ? startupCallTimeout(deadline) : deadline - Date.now();
|
|
2807
|
+
if (timeout <= 0)
|
|
2808
|
+
error(operation, `pane ${paneId} shell readiness deadline exhausted`);
|
|
2754
2809
|
const result = await runHerdr(
|
|
2755
2810
|
pi,
|
|
2756
2811
|
ctx,
|
|
2757
2812
|
["pane", "process-info", "--pane", paneId],
|
|
2758
|
-
{ signal, timeout
|
|
2813
|
+
{ signal, timeout }
|
|
2759
2814
|
);
|
|
2760
2815
|
const value2 = result?.process_info;
|
|
2761
2816
|
const shell = normalizePaneProcess(value2, paneId, true);
|
|
@@ -3026,7 +3081,7 @@ async function rollbackHerdrStart(pi, ctx, started, signal) {
|
|
|
3026
3081
|
const release = await lockLifecycle(ctx, signal);
|
|
3027
3082
|
try {
|
|
3028
3083
|
const expectedShell = started.shellProcess;
|
|
3029
|
-
if (!expectedShell)
|
|
3084
|
+
if (started.launchMayHaveStarted && !expectedShell)
|
|
3030
3085
|
error("rollback", `pane ${started.paneId} process ownership is unproven`);
|
|
3031
3086
|
const listedAgents2 = await runHerdr(pi, ctx, ["agent", "list"], {
|
|
3032
3087
|
signal
|
|
@@ -3042,7 +3097,9 @@ async function rollbackHerdrStart(pi, ctx, started, signal) {
|
|
|
3042
3097
|
);
|
|
3043
3098
|
if (replacement)
|
|
3044
3099
|
error("rollback", `pane ${started.paneId} has a replacement agent`);
|
|
3045
|
-
if (managed)
|
|
3100
|
+
if (!started.launchMayHaveStarted && managed)
|
|
3101
|
+
error("rollback", `pane ${started.paneId} agent ownership changed`);
|
|
3102
|
+
if (started.launchMayHaveStarted && managed) {
|
|
3046
3103
|
const observedSession = sessionIdentity(managed.agent_session);
|
|
3047
3104
|
const sessionReference = started.sessionReference ?? (observedSession?.kind === "id" ? { id: observedSession.value } : observedSession?.kind === "path" ? { path: observedSession.value } : void 0);
|
|
3048
3105
|
const initial = await proveExactRunningAgent(
|
|
@@ -3128,11 +3185,16 @@ async function rollbackHerdrStart(pi, ctx, started, signal) {
|
|
|
3128
3185
|
error("rollback", `tab ${started.tabId} pane ownership is unproven`);
|
|
3129
3186
|
}
|
|
3130
3187
|
const pane = (await runHerdr(pi, ctx, ["pane", "get", started.paneId], { signal })).pane;
|
|
3131
|
-
if (!pane
|
|
3188
|
+
if (!matchesAttemptTerminal(pane, started))
|
|
3132
3189
|
error("rollback", `pane ${started.paneId} ownership is unproven`);
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3190
|
+
if (started.launchMayHaveStarted || !sameCwd(pane.cwd, started.cwd)) {
|
|
3191
|
+
const observed = await paneProcess(pi, ctx, started.paneId, signal);
|
|
3192
|
+
if (!expectedShell || !observed || !sameShellProcessOwner(expectedShell, observed))
|
|
3193
|
+
error(
|
|
3194
|
+
"rollback",
|
|
3195
|
+
`pane ${started.paneId} process ownership is unproven`
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3136
3198
|
if (!managed) {
|
|
3137
3199
|
const listedAgents3 = await runHerdr(pi, ctx, ["agent", "list"], {
|
|
3138
3200
|
signal
|