@maxiaochao/pi-toolkit 0.11.22 → 0.12.1
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 +10 -0
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/package.json +3 -2
- package/packages/herdr-subagents/CHANGELOG.md +10 -0
- package/packages/herdr-subagents/README.md +23 -7
- package/packages/herdr-subagents/TESTING.md +7 -3
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +3 -3
- package/packages/herdr-subagents/package.json +2 -2
- package/packages/herdr-subagents/src/batch-runner.ts +2 -1
- package/packages/herdr-subagents/src/config.ts +11 -2
- package/packages/herdr-subagents/src/parameters.ts +2 -2
- package/packages/herdr-subagents/src/tool.ts +2 -2
- package/packages/herdr-subagents/src/validation.ts +3 -0
- package/packages/herdr-subagents/tests/config.mjs +103 -0
- package/packages/herdr-subagents/tests/core-state-machine.mjs +58 -5
- package/packages/herdr-subagents/tests/extension-sources-load.mjs +9 -2
- package/packages/herdr-subagents/tests/real-herdr-config.test.mjs +1 -1
- package/packages/herdr-subagents/tests/real-herdr-smoke.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 2026-10-19 - v0.12.1
|
|
2
|
+
|
|
3
|
+
- herdr-subagents v0.7.0: add global and project default extension sources with Persona overrides
|
|
4
|
+
- 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.
|
|
5
|
+
|
|
6
|
+
## 2026-10-15 - v0.12.0
|
|
7
|
+
|
|
8
|
+
- herdr-subagents v0.6.0: require explicit task Personas and clarify concise tool descriptions
|
|
9
|
+
- Require an explicit Herdr Subagents Persona for every new task, clarify concise tool descriptions, and identify tasks with conflicting write access in parallel requests.
|
|
10
|
+
|
|
1
11
|
## 2026-10-15 - v0.11.22
|
|
2
12
|
|
|
3
13
|
- herdr-subagents v0.5.4: preserve configured model and authentication extensions in isolated real-Herdr smoke tests
|
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
|
|
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`. 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.
|
|
3
|
+
"version": "0.12.1",
|
|
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-19 - v0.7.0
|
|
2
|
+
|
|
3
|
+
- add global and project default extension sources with Persona overrides
|
|
4
|
+
- 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.
|
|
5
|
+
|
|
6
|
+
## 2026-10-15 - v0.6.0
|
|
7
|
+
|
|
8
|
+
- require explicit task Personas and clarify concise tool descriptions
|
|
9
|
+
- Require an explicit Herdr Subagents Persona for every new task, clarify concise tool descriptions, and identify tasks with conflicting write access in parallel requests.
|
|
10
|
+
|
|
1
11
|
## 2026-10-15 - v0.5.4
|
|
2
12
|
|
|
3
13
|
- 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` |
|
|
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.
|
|
@@ -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 `
|
|
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
|
|
|
@@ -294,7 +295,11 @@ The Markdown body is the child system prompt. Frontmatter is intentionally a sma
|
|
|
294
295
|
| `skills` | Comma-separated list, optionally `[bracketed]` | None | Explicit child skills; normal automatic skill discovery is disabled. |
|
|
295
296
|
| `extensions` | One source or a comma-separated list; `[]` clears | None | Additional Pi extension/package sources loaded into the child. |
|
|
296
297
|
|
|
297
|
-
|
|
298
|
+
### Explicit extension sources and Pi packages
|
|
299
|
+
|
|
300
|
+
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.
|
|
301
|
+
|
|
302
|
+
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
303
|
|
|
299
304
|
### Effective option precedence
|
|
300
305
|
|
|
@@ -315,7 +320,18 @@ Thinking is selected in this order:
|
|
|
315
320
|
4. effective `defaultThinking` (project, then global)
|
|
316
321
|
5. parent thinking when the child also inherits the parent model
|
|
317
322
|
|
|
318
|
-
There is no task-level thinking override. `skills`
|
|
323
|
+
There is no task-level thinking override. `skills` uses project Persona JSON, then global Persona JSON, then effective Persona Markdown.
|
|
324
|
+
|
|
325
|
+
Extension sources are selected in this order:
|
|
326
|
+
|
|
327
|
+
1. project `personas.<name>.extensions`
|
|
328
|
+
2. global `personas.<name>.extensions`
|
|
329
|
+
3. effective Persona Markdown `extensions`
|
|
330
|
+
4. project `defaultExtensions`
|
|
331
|
+
5. global `defaultExtensions`
|
|
332
|
+
6. no optional extensions
|
|
333
|
+
|
|
334
|
+
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
335
|
|
|
320
336
|
Use `/herdr-subagents agents`, `/herdr-subagents models`, and `/herdr-subagents settings` to inspect the result before starting work.
|
|
321
337
|
|
|
@@ -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
|
-
- explicit
|
|
17
|
+
- required explicit Personas in the tool schema and execution, including legacy single-task argument normalization;
|
|
18
|
+
- rejection of missing, blank, invalid, and unknown Personas before creating runs, while custom Personas remain supported;
|
|
19
|
+
- explicit rejection of parallel write-capable tasks with the conflicting task, Persona, and corrective action;
|
|
16
20
|
- waiting through a delayed authoritative report after a prompt transport error while the Pi process is alive;
|
|
17
21
|
- explicit failure when the process exits without a report;
|
|
18
22
|
- parent cancellation of running and queued tasks;
|
|
@@ -26,7 +30,7 @@ It verifies:
|
|
|
26
30
|
|
|
27
31
|
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.
|
|
28
32
|
|
|
29
|
-
Run extension source loading. The direct-Pi fixture validates
|
|
33
|
+
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`:
|
|
30
34
|
|
|
31
35
|
```sh
|
|
32
36
|
npm run load-test --workspace @maxiaochao/pi-herdr-subagents
|
|
@@ -38,7 +42,7 @@ Run the real Herdr smoke test from a Herdr session:
|
|
|
38
42
|
npm run test:real-herdr --workspace @maxiaochao/pi-herdr-subagents
|
|
39
43
|
```
|
|
40
44
|
|
|
41
|
-
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.
|
|
45
|
+
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.
|
|
42
46
|
|
|
43
47
|
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.
|
|
44
48
|
|
|
@@ -42,7 +42,7 @@ export function normalizeHerdrSubagentsArguments(args: unknown): HerdrSubagentsR
|
|
|
42
42
|
tasks: [{
|
|
43
43
|
name: label || "task",
|
|
44
44
|
prompt: legacy.prompt,
|
|
45
|
-
|
|
45
|
+
agent: legacy.agent as string,
|
|
46
46
|
...(typeof legacy.model === "string" ? { model: legacy.model } : {}),
|
|
47
47
|
}],
|
|
48
48
|
...(label ? { label } : {}),
|
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "0.7.0",
|
|
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"
|
|
@@ -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
|
-
|
|
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)}`);
|
|
@@ -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 };
|
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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
|
+
}
|
|
@@ -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),
|
|
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,18 +230,25 @@ 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 });
|
|
190
|
-
writeFileSync(join(lifecycleRoot, ".pi", "herdr-subagents.json"), JSON.stringify({ exposure: "model-only" }));
|
|
239
|
+
writeFileSync(join(lifecycleRoot, ".pi", "herdr-subagents.json"), JSON.stringify({ exposure: "model-only", defaultExtensions: [] }));
|
|
191
240
|
const configuredTools = new Map();
|
|
241
|
+
const configuredCommands = new Map();
|
|
192
242
|
registerHerdrSubagents({
|
|
193
243
|
on() {},
|
|
194
244
|
registerTool(tool) { configuredTools.set(tool.name, tool); },
|
|
195
|
-
registerCommand() {},
|
|
245
|
+
registerCommand(name, command) { configuredCommands.set(name, command); },
|
|
196
246
|
}, { herdr: {}, isEditor: () => false, configurationCwd: lifecycleRoot, configurationPaths });
|
|
197
247
|
assert.equal(configuredTools.get("herdr_subagents")?.exposure, "model-only", "Project configuration can exclude Subagent execution from Codemode");
|
|
198
248
|
assert.equal(configuredTools.get("herdr_subagents_control")?.exposure, "model-only", "Project configuration applies consistently to both Subagent tools");
|
|
249
|
+
const notifications = [];
|
|
250
|
+
await configuredCommands.get("herdr-subagents").handler("settings", { cwd: lifecycleRoot, ui: { notify(message) { notifications.push(message); } } });
|
|
251
|
+
assert.match(notifications[0], /defaultExtensions=\[\] \(project\)/, "Settings must show empty defaults and their source");
|
|
199
252
|
}
|
|
200
253
|
|
|
201
254
|
{
|
|
@@ -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
|
|
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
|
|
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: {
|
|
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);
|