@maxiaochao/pi-toolkit 0.11.22 → 0.12.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/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 2026-10-15 - v0.12.0
2
+
3
+ - herdr-subagents v0.6.0: require explicit task Personas and clarify concise tool descriptions
4
+ - Require an explicit Herdr Subagents Persona for every new task, clarify concise tool descriptions, and identify tasks with conflicting write access in parallel requests.
5
+
1
6
  ## 2026-10-15 - v0.11.22
2
7
 
3
8
  - herdr-subagents v0.5.4: preserve configured model and authentication extensions in isolated real-Herdr smoke tests
package/README.md CHANGED
@@ -39,7 +39,7 @@ Restart Pi after updating. `pi update --extensions` has the same effect when upd
39
39
 
40
40
  Each child README owns its feature description, motivation or upstream alignment, examples, screenshots/demos, configuration, and test documentation.
41
41
 
42
- Herdr Subagents personas can opt into local extension files or npm/git Pi packages through `extensions`. This uses native Pi package semantics: package-provided skills, prompt templates, and themes may also load, and `--no-skills` does not exclude explicitly selected package skills. See the [extension-source contract](packages/herdr-subagents/README.md#explicit-extension-sources-and-pi-packages) for precedence, isolation, and examples. The [real Herdr smoke test](packages/herdr-subagents/TESTING.md) preserves the configured analyst model and explicit authentication extensions while isolating unrelated settings.
42
+ Herdr Subagents tasks require an explicit `agent` Persona; no default `worker` is selected. Personas can opt into local extension files or npm/git Pi packages through `extensions`. This uses native Pi package semantics: package-provided skills, prompt templates, and themes may also load, and `--no-skills` does not exclude explicitly selected package skills. See the [extension-source contract](packages/herdr-subagents/README.md#explicit-extension-sources-and-pi-packages) for precedence, isolation, and examples. The [real Herdr smoke test](packages/herdr-subagents/TESTING.md) preserves the configured analyst model and explicit authentication extensions while isolating unrelated settings.
43
43
 
44
44
  Multiple standalone children may be installed together. `pi-openai-account-pool` may also be installed alongside the complete toolkit because the toolkit does not embed it. Every other child in the table above is embedded, so a second install exposes the same resource twice.
45
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.11.22",
3
+ "version": "0.12.0",
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,3 +1,8 @@
1
+ ## 2026-10-15 - v0.6.0
2
+
3
+ - require explicit task Personas and clarify concise tool descriptions
4
+ - Require an explicit Herdr Subagents Persona for every new task, clarify concise tool descriptions, and identify tasks with conflicting write access in parallel requests.
5
+
1
6
  ## 2026-10-15 - v0.5.4
2
7
 
3
8
  - preserve configured model and authentication extensions in isolated real-Herdr smoke tests
@@ -26,7 +26,7 @@ The tools and command are registered only when Pi is running inside Herdr with a
26
26
 
27
27
  ## Execution contract
28
28
 
29
- `herdr_subagents` accepts an ordered, non-empty `tasks` array.
29
+ `herdr_subagents` accepts an ordered, non-empty `tasks` array. Every task must explicitly set `agent`; there is no default Persona.
30
30
 
31
31
  - `concurrency: 1` runs tasks strictly in input order.
32
32
  - `concurrency: N` runs at most N read-only tasks at once.
@@ -110,7 +110,7 @@ Starts one ordered batch and waits until every task reaches a terminal state. It
110
110
  | `concurrency` | No | `defaultConcurrency` | Maximum number of simultaneously active read-only tasks. It is capped by `maxConcurrency`. |
111
111
  | `tasks[].name` | Yes | — | Short pane/task name, at most 48 characters. |
112
112
  | `tasks[].prompt` | Yes | — | Self-contained task instructions. |
113
- | `tasks[].agent` | No | `"worker"` | Effective Persona name. |
113
+ | `tasks[].agent` | Yes | — | Effective Persona name; use control `list` to inspect available Personas. |
114
114
  | `tasks[].model` | No | Persona/default model | Model override for this task. |
115
115
 
116
116
  A batch containing a write-capable Persona must explicitly or effectively use `concurrency: 1`; an unsafe parallel request is rejected.
@@ -12,7 +12,9 @@ It verifies:
12
12
  - bounded read-only parallelism;
13
13
  - ordered results despite concurrent completion;
14
14
  - sibling continuation after failure and all-failed aggregation;
15
- - explicit rejection of parallel write-capable tasks;
15
+ - required explicit Personas in the tool schema and execution, including legacy single-task argument normalization;
16
+ - rejection of missing, blank, invalid, and unknown Personas before creating runs, while custom Personas remain supported;
17
+ - explicit rejection of parallel write-capable tasks with the conflicting task, Persona, and corrective action;
16
18
  - waiting through a delayed authoritative report after a prompt transport error while the Pi process is alive;
17
19
  - explicit failure when the process exits without a report;
18
20
  - parent cancellation of running and queued tasks;
@@ -42,7 +42,7 @@ export function normalizeHerdrSubagentsArguments(args: unknown): HerdrSubagentsR
42
42
  tasks: [{
43
43
  name: label || "task",
44
44
  prompt: legacy.prompt,
45
- ...(typeof legacy.agent === "string" ? { agent: legacy.agent } : {}),
45
+ agent: legacy.agent as string,
46
46
  ...(typeof legacy.model === "string" ? { model: legacy.model } : {}),
47
47
  }],
48
48
  ...(label ? { label } : {}),
@@ -148,7 +148,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
148
148
  name: "herdr_subagents",
149
149
  ...(exposure ? { exposure } : {}),
150
150
  label: "Herdr Subagents",
151
- description: "Run one or more ordered tasks in visible, focusable Herdr Pi panes. concurrency 1 is strict serial execution; higher values form a bounded pool for read-only tasks. Write-capable tasks require concurrency 1. The call returns only after every task has one terminal completed, failed, or cancelled result. Complete results and Pi sessions stay in project-local records.",
151
+ description: "Run ordered tasks in visible Herdr Pi panes, with an explicit agent for each task. concurrency 1 is serial; higher values require read-only Personas. Returns after all tasks complete, fail, or cancel; full results and sessions remain in project-local records.",
152
152
  promptSnippet: "Run focused work in visible Herdr subagents",
153
153
  parameters: HerdrSubagentsParams,
154
154
  prepareArguments: normalizeHerdrSubagentsArguments,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-herdr-subagents",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Run inspectable interactive Pi subagent batches through Herdr.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -92,7 +92,8 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
92
92
  const runId = options.runId ?? randomUUID();
93
93
  const requestedConcurrency = Math.max(1, Math.floor(options.concurrency));
94
94
  if (requestedConcurrency > 1 && tasks.some((task) => task.access === "write")) {
95
- throw new Error("Write-capable tasks require concurrency 1; parallel writes are not safe.");
95
+ const writers = tasks.filter((task) => task.access === "write").map((task) => `${JSON.stringify(task.name)} (agent=${task.agent})`).join(", ");
96
+ throw new Error(`Write-capable tasks require concurrency 1: ${writers}. Use concurrency: 1, or select a read-only Persona for read-only work.`);
96
97
  }
97
98
  const effectiveConcurrency = Math.min(requestedConcurrency, options.maxConcurrency, tasks.length);
98
99
  const runDirectory = join(options.cwd, ".pi", "herdr-subagents", "runs", `${timestamp().replace(/[-:.]/g, "").replace("Z", "Z-")}${runId.slice(0, 8)}`);
@@ -4,14 +4,14 @@ import { Type } from "typebox";
4
4
  const HerdrSubagentsTaskParams = Type.Object({
5
5
  name: Type.String({ minLength: 1, maxLength: 48, description: "Short task name shown in the pane and task list." }),
6
6
  prompt: Type.String({ minLength: 1, description: "Self-contained prompt for this task." }),
7
- agent: Type.Optional(Type.String({ minLength: 1, description: "Persona for this task. Defaults to worker." })),
7
+ agent: Type.String({ minLength: 1, description: "Persona name, use control list for available Personas." }),
8
8
  model: Type.Optional(Type.String({ minLength: 1, description: "Optional model override for this task." })),
9
9
  }, { additionalProperties: false });
10
10
 
11
11
  export const HerdrSubagentsParams = Type.Object({
12
12
  tasks: Type.Array(HerdrSubagentsTaskParams, { minItems: 1, description: "Ordered tasks to execute. Use one item for a single task; independent read-only tasks may run concurrently." }),
13
13
  label: Type.Optional(Type.String({ minLength: 1, maxLength: 48, description: "Short Herdr run-tab label." })),
14
- concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Maximum simultaneously active Agents. Write-capable tasks force concurrency one." })),
14
+ concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Maximum active Agents. Values above 1 require read-only Personas; write-capable Personas require 1 or the batch is rejected." })),
15
15
  }, { additionalProperties: false });
16
16
 
17
17
  export const HerdrSubagentsControlParams = Type.Object({
@@ -7,7 +7,7 @@ import type { OwnerIdentity } from "./ownership.ts";
7
7
  import { cancelActiveSubagentRuns } from "./reconcile.ts";
8
8
  import { getSubagentRunStatus, runStatusText } from "./status.ts";
9
9
 
10
- export interface HerdrSubagentsTaskParams { name: string; prompt: string; agent?: string; model?: string; }
10
+ export interface HerdrSubagentsTaskParams { name: string; prompt: string; agent: string; model?: string; }
11
11
 
12
12
  export interface HerdrSubagentsRunParams {
13
13
  tasks: HerdrSubagentsTaskParams[];
@@ -140,7 +140,7 @@ async function executeBatch({ params, signal, onUpdate, ctx, dependencies, disco
140
140
  const tasks = params.tasks as HerdrSubagentsTaskParams[];
141
141
  const resolved: BatchTask[] = [];
142
142
  for (const task of tasks) {
143
- const personaName = task.agent?.trim() || "worker";
143
+ const personaName = task.agent.trim();
144
144
  const persona = discovery.personas.find((candidate) => candidate.name === personaName);
145
145
  if (!persona) return errorResponse(`Unknown persona '${personaName}'.\n\n${listText(discovery)}`);
146
146
  resolved.push({ name: task.name, prompt: task.prompt, agent: persona.name, access: persona.access, model: task.model?.trim() || persona.model, thinking: persona.thinking ?? (persona.modelSource === "parent session" && !task.model ? ctx.thinkingLevel : undefined), tools: persona.tools, skills: persona.skills, extensions: persona.extensions, systemPrompt: persona.systemPrompt });
@@ -10,6 +10,9 @@ export const HERDR_ACTIONS = {
10
10
 
11
11
  export function validateRunParams(params: HerdrSubagentsRunParams): string | undefined {
12
12
  if (!params.tasks?.length) return "Provide a non-empty tasks list.";
13
+ for (const [index, task] of params.tasks.entries()) {
14
+ if (typeof task.agent !== "string" || !task.agent.trim()) return `tasks[${index}].agent is required; select a Persona explicitly (use herdr_subagents_control action=list).`;
15
+ }
13
16
  return undefined;
14
17
  }
15
18
 
@@ -1,11 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { runHerdrSubagentsBatch } from "../src/batch-runner.ts";
6
7
  import registerSubagentReport from "../extensions/subagent-report.ts";
7
- import { registerHerdrSubagents } from "../extensions/herdr-subagents.ts";
8
+ import { normalizeHerdrSubagentsArguments, registerHerdrSubagents } from "../extensions/herdr-subagents.ts";
8
9
  import { applyTaskStatus } from "../src/records.ts";
10
+ import { executeHerdrSubagents } from "../src/tool.ts";
9
11
 
10
12
  process.env.HERDR_ENV = "1";
11
13
  process.env.HERDR_WORKSPACE_ID = "test-workspace";
@@ -92,6 +94,45 @@ async function run(tasks, concurrency, herdr = fakeHerdr(), signal) {
92
94
  return { result, herdr };
93
95
  }
94
96
 
97
+ {
98
+ const herdr = fakeHerdr();
99
+ const execute = (params) => executeHerdrSubagents(params, undefined, undefined, { cwd: root }, {
100
+ agentsDirectory: fileURLToPath(new URL("../agents/", import.meta.url)),
101
+ configurationPaths: { globalConfig: join(root, "global-config.json") },
102
+ herdr,
103
+ });
104
+ const missing = { name: "review", prompt: "Read-only review" };
105
+ for (const params of [
106
+ { tasks: [missing] },
107
+ ...["", " \t", null, 42].map((agent) => ({ tasks: [{ ...missing, agent }] })),
108
+ normalizeHerdrSubagentsArguments({ prompt: "Read-only review" }),
109
+ normalizeHerdrSubagentsArguments({ prompt: "Read-only review", agent: 42 }),
110
+ ]) {
111
+ const response = await execute(params);
112
+ assert.equal(response.isError, true, "Missing or invalid agent must not silently select worker");
113
+ assert.match(response.content[0].text, /tasks\[0\]\.agent.*required/);
114
+ }
115
+ const laterMissing = await execute({ tasks: [{ ...missing, agent: "analyst" }, missing], concurrency: 2 });
116
+ assert.match(laterMissing.content[0].text, /tasks\[1\]\.agent.*required/);
117
+ const unknown = await execute({ tasks: [{ ...missing, agent: "unknown-persona" }] });
118
+ assert.equal(unknown.isError, true);
119
+ assert.match(unknown.content[0].text, /Unknown persona 'unknown-persona'/);
120
+ assert.deepEqual(herdr.starts, [], "Invalid requests must not start an Agent");
121
+ assert.equal(existsSync(join(root, ".pi", "herdr-subagents", "runs")), false, "Invalid requests must not create run records");
122
+
123
+ const serial = await execute(normalizeHerdrSubagentsArguments({ prompt: "make change", agent: "worker" }));
124
+ assert.equal(serial.details.status, "completed", "Explicit worker must still run serially through legacy argument normalization");
125
+ const parallel = await execute({ tasks: ["analyst", "reviewer", "explorer"].map((agent) => ({ ...missing, agent })), concurrency: 2 });
126
+ assert.equal(parallel.details.status, "completed");
127
+ assert.equal(parallel.details.effectiveConcurrency, 2);
128
+
129
+ const projectAgents = join(root, ".pi", "herdr-subagents", "agents");
130
+ mkdirSync(projectAgents, { recursive: true });
131
+ writeFileSync(join(projectAgents, "auditor.md"), "---\nname: auditor\ndescription: Custom read-only reviewer\naccess: read\n---\nReview the task.\n");
132
+ const custom = await execute({ tasks: [{ ...missing, agent: " auditor " }], concurrency: 2 });
133
+ assert.equal(custom.details.status, "completed", "Custom Persona names must remain valid and trim whitespace");
134
+ }
135
+
95
136
  {
96
137
  const { result, herdr } = await run([task("one", "first slow"), task("two", "second")], 1);
97
138
  assert.equal(result.status, "completed");
@@ -146,7 +187,12 @@ async function run(tasks, concurrency, herdr = fakeHerdr(), signal) {
146
187
  assert.deepEqual(result.activity.map((entry) => entry.status), ["cancelled", "cancelled"]);
147
188
  }
148
189
 
149
- await assert.rejects(() => run([task("writer", "edit", "write"), task("reader", "read")], 2), /require concurrency 1/);
190
+ await assert.rejects(() => run([task("writer", "edit", "write"), task("reader", "read")], 2), (error) => {
191
+ assert.match(error.message, /writer.*worker/);
192
+ assert.match(error.message, /concurrency: 1/);
193
+ assert.match(error.message, /read-only Persona/);
194
+ return true;
195
+ });
150
196
 
151
197
  const terminal = { id: "task", runId: "run", order: 1, status: "completed" };
152
198
  assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are immutable/);
@@ -184,6 +230,9 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
184
230
  });
185
231
  assert.deepEqual(herdrCalls, [], "Pi startup must not inspect or close historical Subagent tabs");
186
232
  assert.deepEqual(JSON.parse(readFileSync(join(runDirectory, "run.json"), "utf8")), historicalRun, "Pi startup must not mutate historical Subagent records");
233
+ const taskSchema = registeredTools.get("herdr_subagents").parameters.properties.tasks.items;
234
+ assert.ok(taskSchema.required.includes("agent"), "The model-facing schema must require agent");
235
+ assert.equal(taskSchema.properties.agent.enum, undefined, "Custom Persona names must remain valid");
187
236
  assert.equal(registeredTools.get("herdr_subagents")?.exposure, undefined, "Unconfigured Subagent execution must inherit Pi's exposure default");
188
237
  assert.equal(registeredTools.get("herdr_subagents_control")?.exposure, undefined, "Unconfigured Subagent control must inherit Pi's exposure default");
189
238
  mkdirSync(join(lifecycleRoot, ".pi"), { recursive: true });