@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 +6 -0
- package/package.json +1 -1
- package/packages/herdr-subagents/CHANGELOG.md +4 -1
- package/packages/herdr-subagents/TESTING.md +5 -4
- package/packages/herdr-subagents/package.json +2 -1
- package/packages/herdr-subagents/src/live-agent.ts +1 -3
- package/packages/herdr-subagents/tests/real-herdr-smoke.mjs +77 -0
- package/packages/herdr-subagents/tests/run-tests.mjs +10 -0
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,9 @@
|
|
|
1
|
-
##
|
|
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
|
|
8
|
-
4. `
|
|
9
|
-
5.
|
|
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.
|
|
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.
|
|
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 ||
|
|
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);
|