@maxiaochao/pi-toolkit 0.11.3 → 0.11.4

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/CHANGELOG.md CHANGED
@@ -1,9 +1,15 @@
1
+ ## 2026-10-12 - v0.11.4
2
+
3
+ - bundle herdr-subagents v0.4.13: fix stale-idle child startup race and require real Herdr E2E release smoke
4
+
1
5
  ## 2026-10-12 - v0.11.3
2
6
 
3
7
  - harden coordinated publishing and make local release checks authoritative
4
8
 
5
9
  ## Unreleased
6
10
 
11
+ - Fix Herdr Subagents startup failures caused by stale idle observations and require real child-Agent smoke tests before releasing that package.
12
+
7
13
  - Replace split child/root publishing with one idempotent root-tag workflow, make final-version local checks authoritative, keep CI publication-only, and remove timing-sensitive Herdr lifecycle assertions.
8
14
 
9
15
  ## 2026-10-12 - v0.11.2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "Personal pi coding agent toolkit: scheduler, /btw, /cache_export, apply_patch, browser automation, and bundled theme/skills.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -1,6 +1,9 @@
1
- ## Unreleased
1
+ ## 2026-10-12 - v0.4.13
2
2
 
3
+ - fix stale-idle child startup race and require real Herdr E2E release smoke
3
4
  - Make lifecycle tests accept both valid report-first and observation-first execution orderings.
5
+ - Fix child startup being mistaken for completion when `before_agent_start` records the attempt as working before Herdr leaves its stale idle state.
6
+ - Add mandatory real-Herdr single-Agent and concurrent batch smoke tests to the Herdr Subagents release gate.
4
7
 
5
8
  ## 2026-10-12 - v0.4.12
6
9
 
@@ -4,9 +4,10 @@
4
4
 
5
5
  1. `npm --prefix packages/herdr-subagents test` runs deterministic scheduler, recovery, and persona tests.
6
6
  2. `pi -ne -ns --no-session -e ./packages/herdr-subagents/extensions/herdr-subagents.ts` verifies extension loading.
7
- 3. From a dedicated visible X11 Herdr client/window with the GStreamer X11 and VP8 plugins installed, run `HERDR_MEDIA_WINDOW_ID=<window-id> node scripts/capture-package-media.mjs`. Capture intentionally cycles through the live child panes, so avoid using that client/window for other work while recording; the script restores the previously focused pane afterward when it still exists. The Herdr capture must show a parent Pi conversation, a three-task run tab, each of the three real interactive Agent panes receiving focus in turn, the active parent Fleet footer with yellow running indicators and real metrics, and the final compact parent response. The published screenshot must be the parent Fleet view, while the full WebM preserves the parent → three focused children → Fleet → parent sequence.
8
- 4. `npm pack ./packages/herdr-subagents --dry-run` verifies package contents.
9
- 5. Verify the README renders the GitHub attachment video directly and live Herdr UI media `docs/screenshot.png` and `docs/demo.webm` are present in the tarball; `docs/demo.gif` must be absent. Inspect the WebM metadata and play it through once to confirm the parent → three focused children → Fleet → parent sequence.
7
+ 3. From a Pi session inside Herdr, run `npm --prefix packages/herdr-subagents run test:real-herdr`. This is a mandatory child-package and root-bundle release gate. It starts one real child Pi and then two concurrent read-only child Pi Agents, requires assistant events and matching attempt-scoped reports, and verifies final tab cleanup. `HERDR_SUBAGENTS_E2E_MODEL=provider/model` optionally pins the smoke model. Failed cleanup preserves the temporary durable records and fails the gate.
8
+ 4. From a dedicated visible X11 Herdr client/window with the GStreamer X11 and VP8 plugins installed, run `HERDR_MEDIA_WINDOW_ID=<window-id> node scripts/capture-package-media.mjs`. Capture intentionally cycles through the live child panes, so avoid using that client/window for other work while recording; the script restores the previously focused pane afterward when it still exists. The Herdr capture must show a parent Pi conversation, a three-task run tab, each of the three real interactive Agent panes receiving focus in turn, the active parent Fleet footer with yellow running indicators and real metrics, and the final compact parent response. The published screenshot must be the parent Fleet view, while the full WebM preserves the parent → three focused children → Fleet → parent sequence.
9
+ 5. `npm pack ./packages/herdr-subagents --dry-run` verifies package contents.
10
+ 6. Verify the README renders the GitHub attachment video directly and live Herdr UI media `docs/screenshot.png` and `docs/demo.webm` are present in the tarball; `docs/demo.gif` must be absent. Inspect the WebM metadata and play it through once to confirm the parent → three focused children → Fleet → parent sequence.
10
11
 
11
12
  ## Cases
12
13
 
@@ -45,4 +46,4 @@
45
46
 
46
47
  ## Latest baseline
47
48
 
48
- All 246 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, metrics, responsive-formatting, lifecycle, attempt-isolation, and persona cases plus the extension-contract lifecycle simulations must pass. Release validation also requires extension loading and a real Herdr read-only batch smoke with distinct task panes and final tab cleanup. The latest smoke completed a read-only package inspection with matching durable attempt/report IDs and automatic tab cleanup.
49
+ All 246 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, metrics, responsive-formatting, lifecycle, attempt-isolation, and persona cases plus the extension-contract lifecycle simulations must pass. The startup regression test reproduces the real ordering where `before_agent_start` records the attempt as working while Herdr can still report stale idle. Every release also runs the real single-Agent and concurrent read-only Herdr smoke; a skipped smoke is not a passing release check.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-herdr-subagents",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
4
  "description": "Run inspectable interactive Pi subagent batches through Herdr.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -38,6 +38,7 @@
38
38
  },
39
39
  "scripts": {
40
40
  "test": "node --experimental-strip-types tests/run-tests.mjs",
41
+ "test:real-herdr": "node --experimental-strip-types tests/real-herdr-smoke.mjs",
41
42
  "load-test": "env -u PI_HERDR_SUBAGENTS_CHILD pi -ne -ns --no-session -e ./tests/extension-contract.ts && env -u PI_HERDR_SUBAGENTS_CHILD pi -ne -ns --no-session -e ./extensions/herdr-subagents.ts"
42
43
  },
43
44
  "publishConfig": {
@@ -134,7 +134,6 @@ export async function promptLiveAgent(options: {
134
134
  });
135
135
  const stalledTimer = options.stalledWarningMs ? setTimeout(() => options.onStalled?.(), options.stalledWarningMs) : undefined;
136
136
  let terminalObservedAt: number | undefined;
137
- let turnObserved = false;
138
137
  try {
139
138
  await persistRuntime("working");
140
139
  while (!existsSync(attemptReportFile)) {
@@ -144,7 +143,6 @@ export async function promptLiveAgent(options: {
144
143
  try { agentState = await options.herdr.getAgentState(agentName, options.signal); }
145
144
  catch (error) { observationDiagnostic = `Could not observe Herdr agent state: ${error instanceof Error ? error.message : String(error)}`; }
146
145
  const attemptState = await readAttemptState();
147
- if (agentState === "working" || agentState === "blocked" || attemptState === "working") turnObserved = true;
148
146
  const promptDetail = promptError instanceof Error ? promptError.message : promptError === undefined ? "" : String(promptError);
149
147
  if (agentState === "blocked" || /agent_blocked/i.test(promptDetail)) {
150
148
  await persistRuntime("waiting_interaction", observationDiagnostic ?? "Herdr is waiting for an approval or interaction; no business input request has been reported.");
@@ -160,7 +158,7 @@ export async function promptLiveAgent(options: {
160
158
  } else if (promptError && promptSettled) {
161
159
  terminalObservedAt ??= Date.now();
162
160
  if (Date.now() - terminalObservedAt >= 50) throw promptError;
163
- } else if ((promptSettled || turnObserved || attemptState === "settled") && (agentState === "idle" || agentState === "done" || attemptState === "settled")) {
161
+ } else if ((promptSettled || attemptState === "settled") && (agentState === "idle" || agentState === "done" || attemptState === "settled")) {
164
162
  if (attemptState === "working") {
165
163
  let live = false;
166
164
  try { live = await options.herdr.isTaskRunning(options.paneId, options.signal); } catch { live = true; }
@@ -0,0 +1,77 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { executeHerdrSubagents } from "../src/tool.ts";
7
+ import { CliHerdrAutomation } from "../src/herdr.ts";
8
+
9
+ if (process.env.HERDR_ENV !== "1" || !process.env.HERDR_WORKSPACE_ID) {
10
+ throw new Error("The real Herdr smoke test must run from a Pi session inside Herdr.");
11
+ }
12
+
13
+ const packageDirectory = dirname(dirname(fileURLToPath(import.meta.url)));
14
+ const root = mkdtempSync(join(tmpdir(), "pi-herdr-subagents-real-"));
15
+ const herdr = new CliHerdrAutomation();
16
+ const tabIds = new Set();
17
+ const model = process.env.HERDR_SUBAGENTS_E2E_MODEL?.trim();
18
+ const configurationPaths = {
19
+ globalConfig: join(root, "global-config.json"),
20
+ projectConfig: join(root, "project-config.json"),
21
+ globalAgents: join(root, "global-agents"),
22
+ projectAgents: join(root, "project-agents"),
23
+ };
24
+ const timeout = AbortSignal.timeout(120_000);
25
+
26
+ function hasAssistantMessage(path) {
27
+ return readFileSync(path, "utf8").split("\n").filter(Boolean).some((line) => {
28
+ try { return JSON.parse(line)?.message?.role === "assistant"; }
29
+ catch { return false; }
30
+ });
31
+ }
32
+
33
+ async function runCase(label, names, concurrency) {
34
+ const response = await executeHerdrSubagents({
35
+ label,
36
+ concurrency,
37
+ tasks: names.map((name) => ({
38
+ name,
39
+ agent: "analyst",
40
+ ...(model ? { model } : {}),
41
+ prompt: `Do not inspect files. Call subagent_report with status completed, summary '${name} completed', and result 'hello'.`,
42
+ })),
43
+ }, timeout, (update) => {
44
+ if (update.details?.tabId) tabIds.add(update.details.tabId);
45
+ }, { cwd: root }, {
46
+ agentsDirectory: join(packageDirectory, "agents"),
47
+ configurationPaths,
48
+ herdr,
49
+ });
50
+
51
+ const result = response.details;
52
+ if (result?.tabId) tabIds.add(result.tabId);
53
+ assert.equal(response.isError, undefined, response.content?.[0]?.text);
54
+ assert.equal(result.status, "completed");
55
+ assert.equal(result.effectiveConcurrency, concurrency);
56
+ assert.equal(result.tasks.length, names.length);
57
+ assert.equal(new Set(result.tasks.map((task) => task.paneId)).size, names.length);
58
+
59
+ for (const taskResult of result.tasks) {
60
+ const task = JSON.parse(readFileSync(join(taskResult.recordDirectory, "task.json"), "utf8"));
61
+ const report = JSON.parse(readFileSync(join(taskResult.recordDirectory, "report.json"), "utf8"));
62
+ assert.equal(task.status, "completed");
63
+ assert.equal(report.status, "completed");
64
+ assert.equal(report.attemptId, task.currentAttemptId);
65
+ assert.equal(hasAssistantMessage(task.sessionFile), true, `${taskResult.name} produced no assistant event`);
66
+ assert.equal(await herdr.paneExists(task.paneId), false, `${taskResult.name} pane survived tab cleanup`);
67
+ }
68
+ console.log(`PASS ${label}: ${names.length} real child Agent${names.length === 1 ? "" : "s"} completed and cleaned up`);
69
+ }
70
+
71
+ try {
72
+ await runCase("real-single", ["hello-single"], 1);
73
+ await runCase("real-concurrent", ["hello-one", "hello-two"], 2);
74
+ } finally {
75
+ for (const tabId of tabIds) await herdr.closeTab(tabId).catch(() => undefined);
76
+ rmSync(root, { recursive: true, force: true });
77
+ }
@@ -495,8 +495,18 @@ try {
495
495
  check("missing report is visible failure", !silent.ok && silent.errorMessage?.includes("did not submit") && silentHerdr.calls.at(-1)[0] === "closeTab");
496
496
  const initialIdleHerdr = fakeHerdr();
497
497
  const initialIdlePrompt = initialIdleHerdr.promptAgent.bind(initialIdleHerdr);
498
+ const initialIdleStart = initialIdleHerdr.startAgent.bind(initialIdleHerdr);
499
+ let initialIdleDirectory;
500
+ initialIdleHerdr.startAgent = async (request) => {
501
+ const sessionIndex = request.args.indexOf("--session");
502
+ initialIdleDirectory = join(request.args[sessionIndex + 1], "..");
503
+ return initialIdleStart(request);
504
+ };
498
505
  let initialIdleAccepted = false;
499
506
  initialIdleHerdr.promptAgent = async (request) => {
507
+ const task = JSON.parse(readFileSync(join(initialIdleDirectory, "task.json"), "utf8"));
508
+ mkdirSync(join(initialIdleDirectory, "attempts"), { recursive: true });
509
+ writeFileSync(join(initialIdleDirectory, "attempts", `${task.currentAttemptId}.json`), JSON.stringify({ attemptId: task.currentAttemptId, state: "working" }));
500
510
  await new Promise((resolve) => setTimeout(resolve, 150));
501
511
  initialIdleAccepted = true;
502
512
  return initialIdlePrompt(request);