@maxiaochao/pi-toolkit 0.12.0 → 0.12.2

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,13 @@
1
+ ## 2026-10-20 - v0.12.2
2
+
3
+ - herdr-subagents v0.7.1: clarify Persona responsibilities and dispatch guidance
4
+ - Clarify Herdr Persona selection and instructions: broad independent reviewer, codebase explorer, file-changing worker, and neutral charter-driven agent-team analyst. Make the Agent Team skill explicitly select analyst when dispatching through Personas.
5
+
6
+ ## 2026-10-19 - v0.12.1
7
+
8
+ - herdr-subagents v0.7.0: add global and project default extension sources with Persona overrides
9
+ - Add Herdr Subagents `defaultExtensions` in global/project settings as a fallback below Persona-specific extension lists, with replacement semantics, explicit `[]` clearing, source diagnostics, and real child-loading coverage.
10
+
1
11
  ## 2026-10-15 - v0.12.0
2
12
 
3
13
  - herdr-subagents v0.6.0: require explicit task Personas and clarify concise tool descriptions
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 maxiaochao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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 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.
42
+ Herdr Subagents tasks require an explicit `agent` Persona; no default `worker` is selected. Choose `reviewer` for independent reviews of proposals, designs, documents, implementations, or analysis; `explorer` for codebase investigation; `worker` for file-changing work; and `analyst` for charter-defined agent-team roles. See [built-in Personas](packages/herdr-subagents/README.md#built-in-personas) for selection guidance and the distinction between parent-facing descriptions and child instructions. Personas can opt into local extension files or npm/git Pi packages through `extensions`. Set top-level `defaultExtensions` in `~/.pi/agent/herdr-subagents.json` (or project `.pi/herdr-subagents.json`) to share a fallback list; Persona-specific lists take precedence, and `[]` clears inherited sources. 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.12.0",
3
+ "version": "0.12.2",
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": [
@@ -69,5 +69,6 @@
69
69
  },
70
70
  "publishConfig": {
71
71
  "access": "public"
72
- }
72
+ },
73
+ "license": "MIT"
73
74
  }
@@ -1,3 +1,13 @@
1
+ ## 2026-10-20 - v0.7.1
2
+
3
+ - clarify Persona responsibilities and dispatch guidance
4
+ - Clarify parent-facing Persona selection and child instructions: reviewer independently reviews proposals, designs, documents, implementations, and analysis; analyst is the neutral agent-team execution persona with task-defined roles; explorer investigates codebase structure, behavior, and dependencies. Preserve worker instructions and all tool/access boundaries.
5
+
6
+ ## 2026-10-19 - v0.7.0
7
+
8
+ - add global and project default extension sources with Persona overrides
9
+ - Add Herdr Subagents `defaultExtensions` in global/project settings as a fallback below Persona-specific extension lists, with replacement semantics, explicit `[]` clearing, source diagnostics, and real child-loading coverage.
10
+
1
11
  ## 2026-10-15 - v0.6.0
2
12
 
3
13
  - require explicit task Personas and clarify concise tool descriptions
@@ -170,6 +170,7 @@ These are all supported top-level fields in `herdr-subagents.json`:
170
170
  | `stalledWarningSeconds` | Positive integer | `300` | Emit a warning after a child runs this long. This is not a timeout and does not stop it. |
171
171
  | `defaultModel` | Non-empty Pi model ID, normally `provider/model` | Unset | Fallback child model. If unset, the parent model is inherited. |
172
172
  | `defaultThinking` | Non-empty Pi thinking value | Unset | Fallback thinking level below Persona-specific settings. Typical Pi values are `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`; model support varies. |
173
+ | `defaultExtensions` | Array of non-empty Pi source strings | Unset (no optional extensions) | Fallback extension sources for Personas without an explicit `extensions` list. Project replaces global; `[]` clears inherited defaults. |
173
174
  | `personas` | Object keyed by an existing Persona name | `{}` | Per-Persona overrides. It cannot create a Persona without a matching Markdown definition. |
174
175
 
175
176
  Each `personas.<name>` object supports:
@@ -213,12 +214,12 @@ Save this as `~/.pi/agent/herdr-subagents.json`. Model IDs, skills, and extensio
213
214
  "stalledWarningSeconds": 300,
214
215
  "defaultModel": "anthropic/claude-sonnet-4-5",
215
216
  "defaultThinking": "medium",
217
+ "defaultExtensions": ["~/.pi/agent/extensions/provider-routing.ts"],
216
218
  "personas": {
217
219
  "explorer": {
218
220
  "model": "anthropic/claude-haiku-4-5",
219
221
  "thinking": "low",
220
- "skills": ["code-search"],
221
- "extensions": ["~/.pi/agent/extensions/provider-routing.ts"]
222
+ "skills": ["code-search"]
222
223
  },
223
224
  "reviewer": {
224
225
  "thinking": "high",
@@ -247,7 +248,7 @@ A project can override only what differs in `.pi/herdr-subagents.json`:
247
248
  }
248
249
  ```
249
250
 
250
- Here the project keeps global `maxConcurrency`, `defaultModel`, and `defaultThinking`; replaces the explorer model and skill list; and replaces the reviewer extension list. It does not append arrays.
251
+ Here the project keeps global `maxConcurrency`, `defaultModel`, `defaultThinking`, and `defaultExtensions`; replaces the explorer model and skill list; and replaces the reviewer extension list. It does not append arrays.
251
252
 
252
253
  ### Built-in Personas
253
254
 
@@ -256,9 +257,13 @@ None of the bundled Personas pins a model, thinking level, skill, or optional ex
256
257
  | Persona | Access | Built-in tools | Intended use |
257
258
  | --- | --- | --- | --- |
258
259
  | `worker` | `write` | `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`, `codemode` | One focused implementation or other file-changing task. |
259
- | `explorer` | `read` | `read`, `bash`, `grep`, `find`, `ls`, `codemode` | Fast codebase investigation without file changes. |
260
- | `analyst` | `read` | `read`, `bash`, `grep`, `find`, `ls`, `codemode` | Neutral analysis where the task prompt supplies the role and evaluation method. |
261
- | `reviewer` | `read` | `read`, `bash`, `grep`, `find`, `ls`, `codemode` | Correctness and maintainability review with actionable evidence. |
260
+ | `explorer` | `read` | `read`, `bash`, `grep`, `find`, `ls`, `codemode` | Codebase structure, behavior, and dependency investigation without file changes. |
261
+ | `analyst` | `read` | `read`, `bash`, `grep`, `find`, `ls`, `codemode` | Neutral agent-team execution where each task supplies the role charter and evaluation method. |
262
+ | `reviewer` | `read` | `read`, `bash`, `grep`, `find`, `ls`, `codemode` | Independent review of proposals, designs, documents, implementations, and analysis against goals and constraints. |
263
+
264
+ Choose `reviewer` for an independent assessment of existing work, `explorer` to investigate how the codebase works, `worker` for file-changing work, and `analyst` for agent-team members whose role is supplied by the task charter. An analyst may perform a review when its charter calls for one; its neutral execution role does not replace the reviewer selection for ordinary review requests.
265
+
266
+ The control tool's `list` action exposes each effective `description` to help the parent choose a Persona. The Markdown body instead becomes the selected child's appended system prompt. Put selection guidance in `description` and execution instructions in the body; bundled definitions keep both in English.
262
267
 
263
268
  `access` is a scheduling declaration: any `write` Persona requires serial execution. It is not by itself a sandbox. For a custom read-only Persona, also omit write-capable tools and load only trusted extensions.
264
269
 
@@ -294,7 +299,11 @@ The Markdown body is the child system prompt. Frontmatter is intentionally a sma
294
299
  | `skills` | Comma-separated list, optionally `[bracketed]` | None | Explicit child skills; normal automatic skill discovery is disabled. |
295
300
  | `extensions` | One source or a comma-separated list; `[]` clears | None | Additional Pi extension/package sources loaded into the child. |
296
301
 
297
- Relative extension paths resolve from the project root. `~` resolves to the home directory. Pi-native local, `npm:`, `git:`, and `builtin:` sources are accepted. Children always load the required report extension and Codemode; configured sources are additional. Pi packages may provide resources beyond extensions, so load only trusted code and pin versions/refs when reproducibility matters.
302
+ ### Explicit extension sources and Pi packages
303
+
304
+ Use top-level `defaultExtensions` in `~/.pi/agent/herdr-subagents.json` to share trusted extension sources across Personas. Set the same field in `<project>/.pi/herdr-subagents.json` to replace the global default list for that project. Defaults are a fallback, not an appended list: any Persona-specific `extensions`, including Markdown frontmatter and explicit `[]`, takes precedence. An empty default list clears inherited defaults but does not remove Persona-specific sources or the mandatory child extensions.
305
+
306
+ Relative extension paths resolve from the project root, including paths in global `defaultExtensions`. `~` resolves to the home directory. Pi-native local, `npm:`, `git:`, and `builtin:` sources are accepted. Children always load the required report extension and Codemode; configured sources are additional. Pi packages may provide resources beyond extensions, so load only trusted code and pin versions/refs when reproducibility matters.
298
307
 
299
308
  ### Effective option precedence
300
309
 
@@ -315,7 +324,18 @@ Thinking is selected in this order:
315
324
  4. effective `defaultThinking` (project, then global)
316
325
  5. parent thinking when the child also inherits the parent model
317
326
 
318
- There is no task-level thinking override. `skills` and `extensions` use project Persona JSON, then global Persona JSON, then effective Persona Markdown. Each winning list replaces the whole lower-precedence list.
327
+ There is no task-level thinking override. `skills` uses project Persona JSON, then global Persona JSON, then effective Persona Markdown.
328
+
329
+ Extension sources are selected in this order:
330
+
331
+ 1. project `personas.<name>.extensions`
332
+ 2. global `personas.<name>.extensions`
333
+ 3. effective Persona Markdown `extensions`
334
+ 4. project `defaultExtensions`
335
+ 5. global `defaultExtensions`
336
+ 6. no optional extensions
337
+
338
+ Each winning list replaces the whole lower-precedence list, including when it is `[]`. The required report extension and Codemode always remain loaded. `/herdr-subagents settings` shows the effective defaults and their source; the agents listing shows each resolved extension list and its source.
319
339
 
320
340
  Use `/herdr-subagents agents`, `/herdr-subagents models`, and `/herdr-subagents settings` to inspect the result before starting work.
321
341
 
@@ -8,11 +8,15 @@ npm test --workspace @maxiaochao/pi-herdr-subagents
8
8
 
9
9
  It verifies:
10
10
 
11
+ - global/project `defaultExtensions` replacement, trimming, source attribution, Persona JSON/Markdown precedence, explicit empty-list clearing, invalid-value diagnostics, and per-call reloads;
12
+ - resolved defaults in control listings, settings output, and child CLI arguments while preserving mandatory extensions;
11
13
  - strict serial start order at concurrency one;
12
14
  - bounded read-only parallelism;
13
15
  - ordered results despite concurrent completion;
14
16
  - sibling continuation after failure and all-failed aggregation;
15
17
  - required explicit Personas in the tool schema and execution, including legacy single-task argument normalization;
18
+ - model-facing role selection guidance for broad independent review, codebase investigation, file-changing work, and charter-driven agent-team execution;
19
+ - effective Persona descriptions in parent listings, corresponding bodies in child system prompts, and unchanged read/write tool boundaries;
16
20
  - rejection of missing, blank, invalid, and unknown Personas before creating runs, while custom Personas remain supported;
17
21
  - explicit rejection of parallel write-capable tasks with the conflicting task, Persona, and corrective action;
18
22
  - waiting through a delayed authoritative report after a prompt transport error while the Pi process is alive;
@@ -28,7 +32,7 @@ It verifies:
28
32
 
29
33
  The previous broad lifecycle suite remains available temporarily as `npm run test:legacy --workspace @maxiaochao/pi-herdr-subagents` for migration auditing. Scenarios for background queues, blocked/respond, and historical resume intentionally no longer describe the public contract.
30
34
 
31
- Run extension source loading. The direct-Pi fixture validates explicit file/package sources; Herdr's `builtin:codemode` alias is validated by the real-Herdr smoke because the alias is translated by `herdr agent start`:
35
+ Run extension source loading. The direct-Pi fixture resolves global `defaultExtensions` and validates real file/package loading; Herdr's `builtin:codemode` alias is validated by the real-Herdr smoke because the alias is translated by `herdr agent start`:
32
36
 
33
37
  ```sh
34
38
  npm run load-test --workspace @maxiaochao/pi-herdr-subagents
@@ -40,7 +44,7 @@ Run the real Herdr smoke test from a Herdr session:
40
44
  npm run test:real-herdr --workspace @maxiaochao/pi-herdr-subagents
41
45
  ```
42
46
 
43
- The smoke reads the effective `analyst` model, thinking level, and explicit `extensions` from the invoking project's normal Herdr configuration (npm's `INIT_CWD`, or the current directory when run directly). It copies only those fields into its temporary configuration. It keeps the bundled read-only Persona, tool allowlist, and test concurrency; user skills, prompts, and unrelated settings are not copied. Local extension paths remain relative to the invoking project. The concurrent package-loading probe appends its test package rather than replacing the configured extensions.
47
+ The smoke reads the effective `analyst` model, thinking level, and explicit `extensions` from the invoking project's normal Herdr configuration (npm's `INIT_CWD`, or the current directory when run directly). It copies only those fields into its temporary configuration. It keeps the bundled read-only Persona, tool allowlist, and test concurrency; user skills, prompts, and unrelated settings are not copied. Local extension paths remain relative to the invoking project. The concurrent package-loading probe appends its test package rather than replacing the configured extensions. The isolated smoke supplies these sources through global `defaultExtensions`, so both single and concurrent runs exercise default resolution, actual child loading, durable task records, and tab cleanup.
44
48
 
45
49
  This preserves authentication extensions such as `npm:@maxiaochao/pi-openai-account-pool` in both the single and concurrent cases. Children still use `--no-extensions`: only the mandatory child extensions, configured explicit sources, and test probe load. The smoke does not copy or edit credentials. If no Herdr model is configured, Pi's normal startup model selection still applies.
46
50
 
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: analyst
3
- description: Neutral read-only analyst for dynamically assigned roles
3
+ description: Neutral read-only execution persona for agent-team, with roles defined by each task
4
4
  access: read
5
5
  tools: read, bash, grep, find, ls, codemode
6
6
  ---
7
7
 
8
- Act as an independent analyst and follow the role and evaluation method in the task prompt. Do not modify project files. Separate direct evidence, inference, and unknowns, and report concrete evidence with useful paths.
8
+ Serve as a neutral execution persona for agent-team. Follow the role charter and evaluation method supplied in the task prompt. Do not modify project files. Separate direct evidence, inference, and unknowns, and report concrete evidence with useful source references.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: explorer
3
- description: Read-only explorer for fast codebase investigation
3
+ description: Read-only explorer for codebase structure, behavior, and dependencies
4
4
  access: read
5
5
  tools: read, bash, grep, find, ls, codemode
6
6
  ---
7
7
 
8
- Investigate the assigned question without modifying files. Report concrete evidence and useful paths.
8
+ Investigate the assigned question without modifying files. Locate relevant files, trace behavior and dependencies, and report concrete evidence with useful paths. Distinguish confirmed behavior from unresolved questions.
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: reviewer
3
- description: Read-only reviewer for correctness and maintainability risks
3
+ description: Independent read-only reviewer for proposals, designs, documents, implementations, and analysis
4
4
  access: read
5
5
  tools: read, bash, grep, find, ls, codemode
6
6
  ---
7
7
 
8
- Review the assigned work without modifying files. Report actionable findings with concrete evidence.
8
+ Independently review the assigned work against its goals and constraints. Examine assumptions, logic, evidence, trade-offs, and risks. Identify gaps and provide actionable recommendations with concrete evidence. Do not modify project files.
@@ -89,7 +89,7 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
89
89
  }
90
90
  if (section === "settings") {
91
91
  const settings = configuration.settings;
92
- const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, defaultModel: settings.defaultModel, defaultThinking: settings.defaultThinking };
92
+ const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, defaultModel: settings.defaultModel, defaultThinking: settings.defaultThinking, defaultExtensions: JSON.stringify(settings.defaultExtensions ?? []) };
93
93
  const lines = [`exposure=${registeredExposure.value ?? "Pi default"} (${registeredExposure.source}; loaded at extension registration)`, ...Object.entries(values).map(([key, value]) => `${key}=${String(value ?? "unset")} (${configuration.settingSources[key] ?? "built-in"})`)];
94
94
  if (configuration.diagnostics.length) lines.push("Diagnostics:", ...configuration.diagnostics);
95
95
  ctx.ui.notify(lines.join("\n"), "info");
@@ -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 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.",
151
+ description: "Run ordered tasks in visible Herdr Pi panes, with an explicit agent for each task. Choose reviewer for independent reviews, explorer for codebase investigation, worker for file changes, or analyst for agent-team tasks with a supplied role charter; use control list to inspect effective and custom Personas. 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.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Run inspectable interactive Pi subagent batches through Herdr.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -37,7 +37,7 @@
37
37
  "typebox": "*"
38
38
  },
39
39
  "scripts": {
40
- "test": "node --experimental-strip-types tests/core-state-machine.mjs && node --experimental-strip-types tests/real-herdr-config.test.mjs",
40
+ "test": "node --experimental-strip-types tests/config.mjs && node --experimental-strip-types tests/core-state-machine.mjs && node --experimental-strip-types tests/real-herdr-config.test.mjs",
41
41
  "test:real-herdr": "node --experimental-strip-types tests/real-herdr-smoke.mjs",
42
42
  "load-test": "env -u PI_HERDR_SUBAGENTS_CHILD \"${PI_TEST_BINARY:-pi}\" -ne -ns --no-session -e ./tests/extension-contract.ts && env -u PI_HERDR_SUBAGENTS_CHILD \"${PI_TEST_BINARY:-pi}\" -ne -ns --no-session -e ./extensions/herdr-subagents.ts && node --experimental-strip-types tests/extension-sources-load.mjs",
43
43
  "test:legacy": "node --experimental-strip-types tests/run-tests.mjs && node --experimental-strip-types tests/cleanup-timing.mjs"
@@ -13,6 +13,7 @@ export interface SubagentConfig {
13
13
  stalledWarningSeconds: number;
14
14
  defaultModel?: string;
15
15
  defaultThinking?: string;
16
+ defaultExtensions?: string[];
16
17
  personas: Record<string, PersonaConfig>;
17
18
  }
18
19
  export interface EffectivePersona extends AgentDefinition {
@@ -40,6 +41,7 @@ interface ConfigFile {
40
41
  stalledWarningSeconds?: number;
41
42
  defaultModel?: string;
42
43
  defaultThinking?: string;
44
+ defaultExtensions?: string[];
43
45
  personas?: Record<string, PersonaConfig>;
44
46
  }
45
47
  export interface ConfigurationPaths {
@@ -81,6 +83,13 @@ function mergeConfig(base: SubagentConfig, input: ConfigFile, source: string, so
81
83
  if (typeof value === "string" && value.trim()) { result[key] = value.trim(); sources[key] = source; }
82
84
  else diagnostics.push(`${source} ${key} must be a non-empty string`);
83
85
  }
86
+ if (input.defaultExtensions !== undefined) {
87
+ const value = input.defaultExtensions;
88
+ if (Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim())) {
89
+ result.defaultExtensions = value.map((item) => item.trim());
90
+ sources.defaultExtensions = source;
91
+ } else diagnostics.push(`${source} defaultExtensions must be an array of non-empty strings`);
92
+ }
84
93
  if (input.personas !== undefined) {
85
94
  if (!input.personas || typeof input.personas !== "object" || Array.isArray(input.personas)) diagnostics.push(`${source} personas must be an object`);
86
95
  else for (const [name, raw] of Object.entries(input.personas)) {
@@ -134,8 +143,8 @@ export function loadSubagentConfiguration(cwd: string, packageAgentsDirectory: s
134
143
  const thinkingSource = projectPersona.thinking ? "project persona" : globalPersona.thinking ? "global persona" : definition.thinking ? `${source} persona` : settings.defaultThinking ? settingSources.defaultThinking : "default";
135
144
  const skills = projectPersona.skills ?? globalPersona.skills ?? definition.skills ?? [];
136
145
  const skillsSource = projectPersona.skills ? "project persona" : globalPersona.skills ? "global persona" : definition.skills ? `${source} persona` : "none";
137
- const extensions = projectPersona.extensions ?? globalPersona.extensions ?? definition.extensions ?? [];
138
- const extensionsSource = projectPersona.extensions ? "project persona" : globalPersona.extensions ? "global persona" : definition.extensions ? `${source} persona` : "none";
146
+ const extensions = projectPersona.extensions ?? globalPersona.extensions ?? definition.extensions ?? settings.defaultExtensions ?? [];
147
+ const extensionsSource = projectPersona.extensions ? "project persona" : globalPersona.extensions ? "global persona" : definition.extensions ? `${source} persona` : settings.defaultExtensions ? settingSources.defaultExtensions : "none";
139
148
  return { ...definition, source, access: definition.access ?? "write", model, modelSource, thinking, thinkingSource, skills, skillsSource, extensions, extensionsSource };
140
149
  }).sort((left, right) => left.name.localeCompare(right.name));
141
150
  return { settings, settingSources, personas, diagnostics };
@@ -0,0 +1,103 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { loadSubagentConfiguration } from "../src/config.ts";
6
+ import { childPiArgs } from "../src/live-agent.ts";
7
+ import { executeHerdrSubagentsControl } from "../src/tool.ts";
8
+
9
+ const root = mkdtempSync(join(tmpdir(), "herdr-default-extensions-"));
10
+ const agents = join(root, "agents");
11
+ const paths = {
12
+ globalConfig: join(root, "global.json"),
13
+ projectConfig: join(root, "project.json"),
14
+ globalAgents: join(root, "global-agents"),
15
+ projectAgents: join(root, "project-agents"),
16
+ };
17
+ const write = (path, value) => writeFileSync(path, JSON.stringify(value));
18
+ const load = () => loadSubagentConfiguration(root, agents, undefined, paths);
19
+ const persona = (configuration, name = "plain") => configuration.personas.find((entry) => entry.name === name);
20
+ try {
21
+ mkdirSync(agents);
22
+ for (const [name, frontmatter] of [["plain", ""], ["pinned", "extensions: ./markdown.ts\n"], ["cleared", "extensions: []\n"]]) {
23
+ writeFileSync(join(agents, `${name}.md`), `---\nname: ${name}\ndescription: Configuration fixture\naccess: read\n${frontmatter}---\nRead only.\n`);
24
+ }
25
+ assert.deepEqual(persona(load()).extensions, []);
26
+ assert.equal(persona(load()).extensionsSource, "none");
27
+ write(paths.globalConfig, { defaultExtensions: [" ./global.ts ", "npm:provider@1.0.0"] });
28
+ let configuration = load();
29
+ assert.deepEqual(configuration.settings.defaultExtensions, ["./global.ts", "npm:provider@1.0.0"]);
30
+ assert.equal(configuration.settingSources.defaultExtensions, "global");
31
+ assert.deepEqual(persona(configuration).extensions, configuration.settings.defaultExtensions);
32
+ assert.equal(persona(configuration).extensionsSource, "global");
33
+ assert.deepEqual(persona(configuration, "pinned").extensions, ["./markdown.ts"]);
34
+ assert.deepEqual(persona(configuration, "cleared").extensions, []);
35
+
36
+ write(paths.projectConfig, { defaultExtensions: ["./project.ts"] });
37
+ configuration = load();
38
+ assert.deepEqual(persona(configuration).extensions, ["./project.ts"]);
39
+ assert.equal(persona(configuration).extensionsSource, "project");
40
+ assert.equal(configuration.settingSources.defaultExtensions, "project");
41
+ assert.deepEqual(persona(configuration, "pinned").extensions, ["./markdown.ts"]);
42
+ assert.deepEqual(persona(configuration, "cleared").extensions, []);
43
+ console.log("PASS project defaults replace global defaults; Markdown lists and [] override defaults");
44
+
45
+ write(paths.globalConfig, { defaultExtensions: ["./global.ts"], personas: { plain: { extensions: ["./global-persona.ts"] }, pinned: { extensions: [] } } });
46
+ assert.deepEqual(persona(load(), "pinned").extensions, []);
47
+ assert.equal(persona(load(), "pinned").extensionsSource, "global persona");
48
+ assert.deepEqual(persona(load()).extensions, ["./global-persona.ts"]);
49
+ assert.equal(persona(load()).extensionsSource, "global persona");
50
+ write(paths.projectConfig, { defaultExtensions: ["./project.ts"], personas: { plain: { extensions: ["./project-persona.ts"] }, pinned: { extensions: ["./project-persona.ts"] } } });
51
+ assert.deepEqual(persona(load(), "pinned").extensions, ["./project-persona.ts"]);
52
+ assert.deepEqual(persona(load()).extensions, ["./project-persona.ts"]);
53
+ assert.equal(persona(load()).extensionsSource, "project persona");
54
+ write(paths.projectConfig, { defaultExtensions: ["./project.ts"], personas: { plain: { extensions: [] } } });
55
+ assert.deepEqual(persona(load()).extensions, []);
56
+ assert.equal(persona(load()).extensionsSource, "project persona");
57
+ console.log("PASS Persona JSON lists win over defaults and [] clears rather than inherits");
58
+
59
+ write(paths.globalConfig, { defaultExtensions: ["./global.ts"] });
60
+ write(paths.projectConfig, { defaultExtensions: [] });
61
+ assert.deepEqual(load().settings.defaultExtensions, []);
62
+ assert.deepEqual(persona(load()).extensions, []);
63
+ assert.equal(persona(load()).extensionsSource, "project");
64
+ assert.deepEqual(persona(load(), "pinned").extensions, ["./markdown.ts"]);
65
+ for (const value of [null, "./invalid.ts", {}, [1], [""], [" \t "], ["./valid.ts", false]]) {
66
+ write(paths.projectConfig, { defaultExtensions: value });
67
+ configuration = load();
68
+ assert.deepEqual(persona(configuration).extensions, ["./global.ts"]);
69
+ assert.equal(configuration.settingSources.defaultExtensions, "global");
70
+ assert.ok(configuration.diagnostics.includes("project defaultExtensions must be an array of non-empty strings"));
71
+ write(paths.globalConfig, { defaultExtensions: value });
72
+ write(paths.projectConfig, {});
73
+ configuration = load();
74
+ assert.deepEqual(persona(configuration).extensions, []);
75
+ assert.ok(configuration.diagnostics.includes("global defaultExtensions must be an array of non-empty strings"));
76
+ write(paths.globalConfig, { defaultExtensions: ["./global.ts"] });
77
+ }
78
+ console.log("PASS empty defaults clear inherited defaults; invalid lists are diagnosed without partial loading");
79
+
80
+ write(paths.projectConfig, { defaultExtensions: ["./updated.ts"] });
81
+ configuration = load();
82
+ const selected = persona(configuration);
83
+ const args = childPiArgs({ ...selected, sessionFile: join(root, "session.jsonl") }, join(root, "system.md"));
84
+ assert.ok(args.includes("--no-extensions"));
85
+ assert.ok(args.includes("builtin:codemode"));
86
+ assert.ok(args.some((arg) => arg.endsWith("/extensions/subagent-report.ts")));
87
+ assert.equal(args[args.indexOf("./updated.ts") - 1], "--extension");
88
+ const response = await executeHerdrSubagentsControl({ action: "list" }, undefined, undefined, { cwd: root }, { agentsDirectory: agents, configurationPaths: paths });
89
+ assert.deepEqual(response.details.agents.find(({ name }) => name === "plain").extensions, ["./updated.ts"]);
90
+ assert.deepEqual(response.details.settings.defaultExtensions, ["./updated.ts"]);
91
+ assert.match(response.content[0].text, /extensions: \.\/updated\.ts \[project\]/);
92
+ write(paths.projectConfig, { defaultExtensions: [] });
93
+ const cleared = persona(load());
94
+ assert.deepEqual(cleared.extensions, []);
95
+ const clearedArgs = childPiArgs({ ...cleared, sessionFile: join(root, "session.jsonl") }, join(root, "system.md"));
96
+ assert.ok(clearedArgs.includes("builtin:codemode"));
97
+ assert.ok(clearedArgs.some((arg) => arg.endsWith("/extensions/subagent-report.ts")));
98
+ assert.ok(!clearedArgs.includes("./updated.ts"));
99
+ assert.deepEqual(selected.extensions, ["./updated.ts"]);
100
+ console.log("PASS defaults reach child arguments and control listings, reload per call, and preserve mandatory extensions");
101
+ } finally {
102
+ rmSync(root, { recursive: true, force: true });
103
+ }
@@ -7,7 +7,7 @@ import { runHerdrSubagentsBatch } from "../src/batch-runner.ts";
7
7
  import registerSubagentReport from "../extensions/subagent-report.ts";
8
8
  import { normalizeHerdrSubagentsArguments, registerHerdrSubagents } from "../extensions/herdr-subagents.ts";
9
9
  import { applyTaskStatus } from "../src/records.ts";
10
- import { executeHerdrSubagents } from "../src/tool.ts";
10
+ import { executeHerdrSubagents, executeHerdrSubagentsControl } from "../src/tool.ts";
11
11
 
12
12
  process.env.HERDR_ENV = "1";
13
13
  process.env.HERDR_WORKSPACE_ID = "test-workspace";
@@ -101,6 +101,32 @@ async function run(tasks, concurrency, herdr = fakeHerdr(), signal) {
101
101
  configurationPaths: { globalConfig: join(root, "global-config.json") },
102
102
  herdr,
103
103
  });
104
+ const listed = await executeHerdrSubagentsControl({ action: "list" }, undefined, undefined, { cwd: root }, {
105
+ agentsDirectory: fileURLToPath(new URL("../agents/", import.meta.url)),
106
+ configurationPaths: { globalConfig: join(root, "global-config.json") },
107
+ });
108
+ const personas = new Map(listed.details.agents.map((persona) => [persona.name, persona]));
109
+ const analyst = personas.get("analyst");
110
+ assert.match(analyst.description, /agent-team.*roles defined by each task/);
111
+ assert.match(analyst.systemPrompt, /role charter and evaluation method/);
112
+ const reviewer = personas.get("reviewer");
113
+ assert.match(reviewer.description, /proposals, designs, documents, implementations, and analysis/);
114
+ assert.match(reviewer.systemPrompt, /goals and constraints/);
115
+ assert.match(reviewer.systemPrompt, /assumptions, logic, evidence, trade-offs, and risks/);
116
+ const explorer = personas.get("explorer");
117
+ assert.match(explorer.description, /codebase structure, behavior, and dependencies/);
118
+ assert.match(explorer.systemPrompt, /Locate relevant files, trace behavior and dependencies/);
119
+ const readTools = ["read", "bash", "grep", "find", "ls", "codemode"];
120
+ for (const name of ["analyst", "reviewer", "explorer"]) {
121
+ const persona = personas.get(name);
122
+ assert.equal(persona.access, "read");
123
+ assert.deepEqual(persona.tools, readTools);
124
+ assert.ok(listed.content[0].text.includes(`${name}: ${persona.description}`), "The parent must see role selection guidance in Persona listings");
125
+ assert.ok(!listed.content[0].text.includes(persona.systemPrompt), "Persona listings must not embed the child instructions");
126
+ }
127
+ assert.equal(personas.get("worker").access, "write");
128
+ assert.equal(personas.get("worker").description, "General-purpose worker for one focused task");
129
+ assert.match(personas.get("worker").systemPrompt, /Do not delegate to another agent/);
104
130
  const missing = { name: "review", prompt: "Read-only review" };
105
131
  for (const params of [
106
132
  { tasks: [missing] },
@@ -125,6 +151,12 @@ async function run(tasks, concurrency, herdr = fakeHerdr(), signal) {
125
151
  const parallel = await execute({ tasks: ["analyst", "reviewer", "explorer"].map((agent) => ({ ...missing, agent })), concurrency: 2 });
126
152
  assert.equal(parallel.details.status, "completed");
127
153
  assert.equal(parallel.details.effectiveConcurrency, 2);
154
+ for (const [index, name] of ["analyst", "reviewer", "explorer"].entries()) {
155
+ const directory = parallel.details.tasks[index].recordDirectory;
156
+ const instructions = readFileSync(join(directory, "system-prompt.md"), "utf8");
157
+ assert.ok(instructions.startsWith(personas.get(name).systemPrompt), "Each child must receive its selected Persona body");
158
+ assert.match(instructions, /call subagent_report exactly once/);
159
+ }
128
160
 
129
161
  const projectAgents = join(root, ".pi", "herdr-subagents", "agents");
130
162
  mkdirSync(projectAgents, { recursive: true });
@@ -231,20 +263,29 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
231
263
  assert.deepEqual(herdrCalls, [], "Pi startup must not inspect or close historical Subagent tabs");
232
264
  assert.deepEqual(JSON.parse(readFileSync(join(runDirectory, "run.json"), "utf8")), historicalRun, "Pi startup must not mutate historical Subagent records");
233
265
  const taskSchema = registeredTools.get("herdr_subagents").parameters.properties.tasks.items;
266
+ const description = registeredTools.get("herdr_subagents").description;
267
+ assert.match(description, /reviewer for independent reviews/);
268
+ assert.match(description, /explorer for codebase investigation/);
269
+ assert.match(description, /worker for file changes/);
270
+ assert.match(description, /analyst for agent-team/);
234
271
  assert.ok(taskSchema.required.includes("agent"), "The model-facing schema must require agent");
235
272
  assert.equal(taskSchema.properties.agent.enum, undefined, "Custom Persona names must remain valid");
236
273
  assert.equal(registeredTools.get("herdr_subagents")?.exposure, undefined, "Unconfigured Subagent execution must inherit Pi's exposure default");
237
274
  assert.equal(registeredTools.get("herdr_subagents_control")?.exposure, undefined, "Unconfigured Subagent control must inherit Pi's exposure default");
238
275
  mkdirSync(join(lifecycleRoot, ".pi"), { recursive: true });
239
- writeFileSync(join(lifecycleRoot, ".pi", "herdr-subagents.json"), JSON.stringify({ exposure: "model-only" }));
276
+ writeFileSync(join(lifecycleRoot, ".pi", "herdr-subagents.json"), JSON.stringify({ exposure: "model-only", defaultExtensions: [] }));
240
277
  const configuredTools = new Map();
278
+ const configuredCommands = new Map();
241
279
  registerHerdrSubagents({
242
280
  on() {},
243
281
  registerTool(tool) { configuredTools.set(tool.name, tool); },
244
- registerCommand() {},
282
+ registerCommand(name, command) { configuredCommands.set(name, command); },
245
283
  }, { herdr: {}, isEditor: () => false, configurationCwd: lifecycleRoot, configurationPaths });
246
284
  assert.equal(configuredTools.get("herdr_subagents")?.exposure, "model-only", "Project configuration can exclude Subagent execution from Codemode");
247
285
  assert.equal(configuredTools.get("herdr_subagents_control")?.exposure, "model-only", "Project configuration applies consistently to both Subagent tools");
286
+ const notifications = [];
287
+ await configuredCommands.get("herdr-subagents").handler("settings", { cwd: lifecycleRoot, ui: { notify(message) { notifications.push(message); } } });
288
+ assert.match(notifications[0], /defaultExtensions=\[\] \(project\)/, "Settings must show empty defaults and their source");
248
289
  }
249
290
 
250
291
  {
@@ -4,6 +4,8 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { childPiArgs } from "../src/live-agent.ts";
7
+ import { loadSubagentConfiguration } from "../src/config.ts";
8
+ import { fileURLToPath } from "node:url";
7
9
 
8
10
  // Exercise native Pi package semantics without provider calls or network installs.
9
11
  const root = mkdtempSync(join(tmpdir(), "pi-herdr-extension-sources-"));
@@ -36,7 +38,12 @@ try {
36
38
  ctx.shutdown();
37
39
  });
38
40
  }`);
39
- const args = childPiArgs({ id: "probe", sessionFile: join(root, "session.jsonl"), tools: ["read"], extensions: [fileExtension, "./local package", probe] }, join(root, "system.md"));
41
+ const globalConfig = join(root, "global.json");
42
+ writeFileSync(globalConfig, JSON.stringify({ defaultExtensions: [fileExtension, "./local package", probe] }));
43
+ const configuration = loadSubagentConfiguration(root, fileURLToPath(new URL("../agents", import.meta.url)), undefined, { globalConfig, globalAgents: join(root, "global-agents") });
44
+ const analyst = configuration.personas.find(({ name }) => name === "analyst");
45
+ assert.equal(analyst.extensionsSource, "global");
46
+ const args = childPiArgs({ id: "probe", sessionFile: join(root, "session.jsonl"), tools: ["read"], extensions: analyst.extensions }, join(root, "system.md"));
40
47
  // `builtin:codemode` is translated by `herdr agent start` and is covered by
41
48
  // the real-Herdr smoke test. Direct Pi has no portable form of that alias.
42
49
  const codemode = args.indexOf("builtin:codemode");
@@ -50,7 +57,7 @@ try {
50
57
  const loaded = JSON.parse(readFileSync(resultPath, "utf8"));
51
58
  for (const command of ["file-extension", "package-extension", "skill:package-skill", "package-prompt", "parent"]) assert.ok(loaded.commands.includes(command), `Missing ${command}: ${JSON.stringify(loaded)}`);
52
59
  assert.deepEqual([...loaded.tools].sort(), ["read", "subagent_report"]);
53
- console.log("PASS real Pi loads explicit file and package sources, including package skills/prompts despite discovery flags, without broadening the direct-Pi tool allowlist");
60
+ console.log("PASS real Pi loads defaultExtensions file and package sources, including package skills/prompts despite discovery flags, without broadening the direct-Pi tool allowlist");
54
61
  } finally {
55
62
  rmSync(root, { recursive: true, force: true });
56
63
  }
@@ -21,8 +21,8 @@ try {
21
21
  maxConcurrency: 1,
22
22
  defaultModel: "openai-codex/test-model",
23
23
  defaultThinking: "high",
24
+ defaultExtensions: ["npm:@maxiaochao/pi-openai-account-pool"],
24
25
  personas: { analyst: {
25
- extensions: ["npm:@maxiaochao/pi-openai-account-pool"],
26
26
  skills: ["unrelated-skill"],
27
27
  } },
28
28
  });
@@ -97,7 +97,7 @@ async function waitForCleanup(result) {
97
97
 
98
98
  async function runCase(label, names, concurrency, extraExtensions = []) {
99
99
  const extensions = [...runtimePersona.extensions, ...extraExtensions];
100
- writeFileSync(configurationPaths.globalConfig, JSON.stringify({ personas: { analyst: { ...runtimePersona, extensions } } }));
100
+ writeFileSync(configurationPaths.globalConfig, JSON.stringify({ defaultExtensions: extensions, personas: { analyst: { model: runtimePersona.model, thinking: runtimePersona.thinking } } }));
101
101
  if (concurrency > 1 && process.env.HERDR_SUBAGENTS_E2E_FOCUS_CLEANUP === "1") {
102
102
  const workspaces = spawnSync(process.env.PI_HERDR_SUBAGENTS_HERDR_BINARY?.trim() || "herdr", ["workspace", "list"], { encoding: "utf8", timeout: 10_000 });
103
103
  assert.equal(workspaces.status, 0, workspaces.stderr);
@@ -598,7 +598,7 @@ try {
598
598
  check("persona discovery and diagnostics", discovery.agents.length === 1 && discovery.agents[0].name === "worker" && discovery.diagnostics.length === 1);
599
599
  const packageDiscovery = discoverPackageAgents(join(process.cwd(), "agents"));
600
600
  const analystPersona = packageDiscovery.agents.find((persona) => persona.name === "analyst");
601
- check("built-in analyst is neutral and read-only", packageDiscovery.diagnostics.length === 0 && analystPersona?.access === "read" && analystPersona.model === undefined && analystPersona.tools?.join(",") === "read,bash,grep,find,ls,codemode" && /independent analyst/.test(analystPersona.systemPrompt) && /direct evidence, inference, and unknowns/.test(analystPersona.systemPrompt) && !/auditor|replicator|adversary/.test(analystPersona.systemPrompt));
601
+ check("built-in analyst is neutral and read-only", packageDiscovery.diagnostics.length === 0 && analystPersona?.access === "read" && analystPersona.model === undefined && analystPersona.tools?.join(",") === "read,bash,grep,find,ls,codemode" && /neutral execution persona for agent-team/.test(analystPersona.systemPrompt) && /role charter and evaluation method/.test(analystPersona.systemPrompt) && /direct evidence, inference, and unknowns/.test(analystPersona.systemPrompt) && !/auditor|replicator|adversary/.test(analystPersona.systemPrompt));
602
602
 
603
603
  const configRoot = join(root, "configuration");
604
604
  const packageAgents = join(configRoot, "package-agents");
@@ -28,7 +28,7 @@ description: 多模型协作分析:主 Agent 按配置的模型和角色调度
28
28
 
29
29
  读取本 skill 同目录的 [models.json](models.json):`members` 是成员与模型的唯一配置来源,`roles` 是角色职责的唯一配置来源。默认将两个数组按顺序一一对应;配置为空或数量不匹配时,报告配置问题后再启动。
30
30
 
31
- 按配置成员数并发启动子代理,显式传入各自的 `model`,并将对应角色的 `charter` 放入任务。成员使用相同的中性只读执行约束。启动后核对并在本轮记录中写下每个任务实际生效的模型;无法确认或与配置不符时,该成员结果无效并报告原因。
31
+ 按配置成员数并发启动子代理,显式传入各自的 `model`,并将对应角色的 `charter` 放入任务。使用 Persona 派发时,成员统一选择 `agent: "analyst"`,作为中立执行载体;具体职责由本轮 `charter` 注入。成员使用相同的中性只读执行约束。启动后核对并在本轮记录中写下每个任务实际生效的模型;无法确认或与配置不符时,该成员结果无效并报告原因。
32
32
 
33
33
  每轮在 `rounds/N.md` 写明成员、模型、角色、输入文件和分派问题。默认沿用角色映射;需要交叉复核时可轮换,并记录新的映射。
34
34