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 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.17.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 paneId;
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
- paneId = made.root_pane.pane_id;
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
- paneId = split.pane.pane_id;
2536
+ createdPane = split.pane;
2515
2537
  }
2516
2538
  }
2517
- if (!paneId) error("start", "Herdr did not return a pane");
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
- shell = await proveShellReady(
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 || currentPane.workspace_id !== workspaceId || currentPane.tab_id !== tab.tab_id)
2563
- error("start", `pane ${paneId} topology changed before launch`);
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 latest = await paneProcess(
2578
- pi,
2579
- ctx,
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
- if (!latest || latest.pane_id !== paneId || !sameShellProcessOwner(shell, latest))
2586
- error("start", `pane ${paneId} shell identity changed before launch`);
2587
- const remaining = startupCallTimeout(startupDeadline, childTimeout);
2588
- if (remaining < HERDR_START_TIMEOUT_MIN)
2589
- error("start", "startup deadline exhausted before agent start");
2590
- try {
2591
- started = await runHerdr(
2592
- pi,
2593
- ctx,
2594
- [
2595
- "agent",
2596
- "start",
2597
- attempt.herdrAgent,
2598
- "--kind",
2599
- "pi",
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
- startupDeadline,
2628
- options.signal
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
- throw failure;
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) throw new HerdrStartFailure(cause, stage, attempt, false);
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 proveShellReady(pi, ctx, paneId, expected, deadline, operation, signal) {
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: 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 || pane.pane_id !== started.paneId || pane.workspace_id !== started.workspaceId || pane.tab_id !== started.tabId)
3188
+ if (!matchesAttemptTerminal(pane, started))
3132
3189
  error("rollback", `pane ${started.paneId} ownership is unproven`);
3133
- const observed = await paneProcess(pi, ctx, started.paneId, signal);
3134
- if (!observed || !sameShellProcessOwner(expectedShell, observed))
3135
- error("rollback", `pane ${started.paneId} process ownership is unproven`);
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