@maxiaochao/pi-toolkit 0.12.0 → 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 +5 -0
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/package.json +3 -2
- package/packages/herdr-subagents/CHANGELOG.md +5 -0
- package/packages/herdr-subagents/README.md +21 -5
- package/packages/herdr-subagents/TESTING.md +4 -2
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +1 -1
- package/packages/herdr-subagents/package.json +2 -2
- package/packages/herdr-subagents/src/config.ts +11 -2
- package/packages/herdr-subagents/tests/config.mjs +103 -0
- package/packages/herdr-subagents/tests/core-state-machine.mjs +6 -2
- 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,8 @@
|
|
|
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
|
+
|
|
1
6
|
## 2026-10-15 - v0.12.0
|
|
2
7
|
|
|
3
8
|
- 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. 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.
|
|
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,8 @@
|
|
|
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
|
+
|
|
1
6
|
## 2026-10-15 - v0.6.0
|
|
2
7
|
|
|
3
8
|
- 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 `
|
|
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,6 +8,8 @@ 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;
|
|
@@ -28,7 +30,7 @@ It verifies:
|
|
|
28
30
|
|
|
29
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.
|
|
30
32
|
|
|
31
|
-
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`:
|
|
32
34
|
|
|
33
35
|
```sh
|
|
34
36
|
npm run load-test --workspace @maxiaochao/pi-herdr-subagents
|
|
@@ -40,7 +42,7 @@ Run the real Herdr smoke test from a Herdr session:
|
|
|
40
42
|
npm run test:real-herdr --workspace @maxiaochao/pi-herdr-subagents
|
|
41
43
|
```
|
|
42
44
|
|
|
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.
|
|
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.
|
|
44
46
|
|
|
45
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.
|
|
46
48
|
|
|
@@ -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");
|
|
@@ -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"
|
|
@@ -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
|
+
}
|
|
@@ -236,15 +236,19 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
|
|
|
236
236
|
assert.equal(registeredTools.get("herdr_subagents")?.exposure, undefined, "Unconfigured Subagent execution must inherit Pi's exposure default");
|
|
237
237
|
assert.equal(registeredTools.get("herdr_subagents_control")?.exposure, undefined, "Unconfigured Subagent control must inherit Pi's exposure default");
|
|
238
238
|
mkdirSync(join(lifecycleRoot, ".pi"), { recursive: true });
|
|
239
|
-
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: [] }));
|
|
240
240
|
const configuredTools = new Map();
|
|
241
|
+
const configuredCommands = new Map();
|
|
241
242
|
registerHerdrSubagents({
|
|
242
243
|
on() {},
|
|
243
244
|
registerTool(tool) { configuredTools.set(tool.name, tool); },
|
|
244
|
-
registerCommand() {},
|
|
245
|
+
registerCommand(name, command) { configuredCommands.set(name, command); },
|
|
245
246
|
}, { herdr: {}, isEditor: () => false, configurationCwd: lifecycleRoot, configurationPaths });
|
|
246
247
|
assert.equal(configuredTools.get("herdr_subagents")?.exposure, "model-only", "Project configuration can exclude Subagent execution from Codemode");
|
|
247
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");
|
|
248
252
|
}
|
|
249
253
|
|
|
250
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);
|