@maxiaochao/pi-toolkit 0.11.19 → 0.11.22
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 +15 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/packages/herdr-subagents/ARCHITECTURE.md +4 -0
- package/packages/herdr-subagents/CHANGELOG.md +10 -0
- package/packages/herdr-subagents/README.md +232 -6
- package/packages/herdr-subagents/TESTING.md +10 -1
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +11 -4
- package/packages/herdr-subagents/package.json +2 -2
- package/packages/herdr-subagents/src/config.ts +7 -0
- package/packages/herdr-subagents/tests/core-state-machine.mjs +17 -1
- package/packages/herdr-subagents/tests/real-herdr-config.mjs +14 -0
- package/packages/herdr-subagents/tests/real-herdr-config.test.mjs +80 -0
- package/packages/herdr-subagents/tests/real-herdr-smoke.mjs +7 -5
- package/skills/agent-team/SKILL.md +19 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 2026-10-15 - v0.11.22
|
|
2
|
+
|
|
3
|
+
- herdr-subagents v0.5.4: preserve configured model and authentication extensions in isolated real-Herdr smoke tests
|
|
4
|
+
- Herdr Subagents: preserve configured models and explicit authentication extensions in isolated real-Herdr E2E tests instead of losing account-pool setup.
|
|
5
|
+
|
|
6
|
+
## 2026-10-15 - v0.11.21
|
|
7
|
+
|
|
8
|
+
- simplify Agent Team to config-driven orchestration with persistent investigation records and optional extra review
|
|
9
|
+
|
|
10
|
+
## 2026-10-14 - v0.11.20
|
|
11
|
+
|
|
12
|
+
- herdr-subagents v0.5.3: allow optional Pi-native tool exposure while inheriting Pi defaults when unset
|
|
13
|
+
|
|
1
14
|
## 2026-10-14 - v0.11.19
|
|
2
15
|
|
|
3
16
|
- herdr-subagents v0.5.2: stop Pi startup from scanning historical runs or closing recorded Herdr tabs
|
|
@@ -36,6 +49,8 @@
|
|
|
36
49
|
|
|
37
50
|
## Unreleased
|
|
38
51
|
|
|
52
|
+
- Agent Team: make the main agent responsible for config-driven dispatch, result collection, and investigation records; use configured role charters as the single source of truth and make role rotation, extra replication, and open attack rounds optional.
|
|
53
|
+
- Keep release-routing test fixtures independent of changes since the latest root release tag.
|
|
39
54
|
- OpenAI Account Pool: restore `●本会话` after successful requests and failover across fresh command contexts; status and preference commands no longer reinstall the session provider.
|
|
40
55
|
- Scope child and root release checks to their owned code, and run Herdr tests or the real Agent smoke only when Herdr is the target or changed since the previous root release.
|
|
41
56
|
- OpenAI Account Pool: keep deterministic account priority and show it as `★首选`, separately from the session-local `●本会话` account; automatic mode uses account order rather than random selection.
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Restart Pi after updating. `pi update --extensions` has the same effect when upd
|
|
|
39
39
|
|
|
40
40
|
Each child README owns its feature description, motivation or upstream alignment, examples, screenshots/demos, configuration, and test documentation.
|
|
41
41
|
|
|
42
|
-
Herdr Subagents personas can opt into local extension files or npm/git Pi packages through `extensions`. This uses native Pi package semantics: package-provided skills, prompt templates, and themes may also load, and `--no-skills` does not exclude explicitly selected package skills. See the [extension-source contract](packages/herdr-subagents/README.md#explicit-extension-sources-and-pi-packages) for precedence, isolation, and examples.
|
|
42
|
+
Herdr Subagents personas can opt into local extension files or npm/git Pi packages through `extensions`. This uses native Pi package semantics: package-provided skills, prompt templates, and themes may also load, and `--no-skills` does not exclude explicitly selected package skills. See the [extension-source contract](packages/herdr-subagents/README.md#explicit-extension-sources-and-pi-packages) for precedence, isolation, and examples. The [real Herdr smoke test](packages/herdr-subagents/TESTING.md) preserves the configured analyst model and explicit authentication extensions while isolating unrelated settings.
|
|
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
|
|
|
@@ -76,7 +76,7 @@ bash scripts/install.sh
|
|
|
76
76
|
| `html-artifact` | Build self-contained HTML explainers and diagrams |
|
|
77
77
|
| `pdlog` | Decompress and inspect Pudu `.pdlog` files |
|
|
78
78
|
| `herdr` | Control Herdr workspaces, tabs, panes, and agents |
|
|
79
|
-
| `agent-team` |
|
|
79
|
+
| `agent-team` | Let the main agent dispatch concurrent analysis using configured models and roles, collect results, and maintain investigation records; run extra review rounds as needed |
|
|
80
80
|
| `tapd` | Investigate and operate on TAPD items with workspace, evidence, and safe-download guardrails |
|
|
81
81
|
|
|
82
82
|
Package-specific skills, such as `pi-worktree`, are documented by their child package.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-toolkit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.22",
|
|
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": [
|
|
@@ -110,3 +110,7 @@ Writing the same report again is idempotent. A conflicting replacement is reject
|
|
|
110
110
|
## Cancellation and shutdown
|
|
111
111
|
|
|
112
112
|
Caller or parent-session cancellation aborts active waits, marks nonterminal tasks cancelled, and closes the Herdr tab owned by that session. A new Pi session never scans, mutates, resumes, or closes historical runs during startup. If Pi is terminated before its shutdown handlers run, its visible Subagent tabs remain for explicit user inspection or cleanup.
|
|
113
|
+
|
|
114
|
+
## Tool exposure
|
|
115
|
+
|
|
116
|
+
The extension reads optional `exposure` once before registering its two tools. When omitted, the extension also omits the registration field and delegates to Pi's default. Global or project configuration can select Pi's `model-only` or `direct` exposure explicitly. Changing this registration-time setting requires `/reload` or a Pi restart; the extension does not re-register tools during a session.
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 2026-10-15 - v0.5.4
|
|
2
|
+
|
|
3
|
+
- preserve configured model and authentication extensions in isolated real-Herdr smoke tests
|
|
4
|
+
- Preserve the configured analyst model, thinking level, and explicit authentication extensions in isolated real-Herdr smoke tests; append the concurrent package probe without replacing those extensions.
|
|
5
|
+
- Add deterministic smoke-configuration coverage and isolate exposure test fixtures from personal settings.
|
|
6
|
+
|
|
7
|
+
## 2026-10-14 - v0.5.3
|
|
8
|
+
|
|
9
|
+
- allow optional Pi-native tool exposure while inheriting Pi defaults when unset
|
|
10
|
+
|
|
1
11
|
## 2026-10-14 - v0.5.2
|
|
2
12
|
|
|
3
13
|
- stop Pi startup from scanning historical runs or closing recorded Herdr tabs
|
|
@@ -12,6 +12,18 @@ https://github.com/user-attachments/assets/8ddf3ae6-a11a-492b-9cae-d9496ba65b67
|
|
|
12
12
|
|
|
13
13
|
The reproducible source recording is retained as [`docs/demo.webm`](docs/demo.webm).
|
|
14
14
|
|
|
15
|
+
## Requirements and install
|
|
16
|
+
|
|
17
|
+
Run Pi inside a Herdr workspace, then install the standalone package:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:@maxiaochao/pi-herdr-subagents
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Update it with `pi update npm:@maxiaochao/pi-herdr-subagents`. The full `@maxiaochao/pi-toolkit` package already bundles this extension, so install the toolkit or this standalone package, not both.
|
|
24
|
+
|
|
25
|
+
The tools and command are registered only when Pi is running inside Herdr with a workspace ID. If they are unavailable after entering Herdr, use `/reload` or restart Pi there.
|
|
26
|
+
|
|
15
27
|
## Execution contract
|
|
16
28
|
|
|
17
29
|
`herdr_subagents` accepts an ordered, non-empty `tasks` array.
|
|
@@ -85,14 +97,228 @@ Records are stored under:
|
|
|
85
97
|
|
|
86
98
|
The orchestrator is the only writer of `run.json`, `task.json`, and promoted `report.json`. The child only writes its attempt-scoped result and report.
|
|
87
99
|
|
|
88
|
-
##
|
|
100
|
+
## Tools and UI
|
|
101
|
+
|
|
102
|
+
### `herdr_subagents`
|
|
103
|
+
|
|
104
|
+
Starts one ordered batch and waits until every task reaches a terminal state. Its parameters are:
|
|
105
|
+
|
|
106
|
+
| Field | Required | Default | Purpose |
|
|
107
|
+
| --- | --- | --- | --- |
|
|
108
|
+
| `tasks` | Yes | — | Non-empty ordered task array. |
|
|
109
|
+
| `label` | No | `"batch"` | Short Herdr run-tab label, at most 48 characters. |
|
|
110
|
+
| `concurrency` | No | `defaultConcurrency` | Maximum number of simultaneously active read-only tasks. It is capped by `maxConcurrency`. |
|
|
111
|
+
| `tasks[].name` | Yes | — | Short pane/task name, at most 48 characters. |
|
|
112
|
+
| `tasks[].prompt` | Yes | — | Self-contained task instructions. |
|
|
113
|
+
| `tasks[].agent` | No | `"worker"` | Effective Persona name. |
|
|
114
|
+
| `tasks[].model` | No | Persona/default model | Model override for this task. |
|
|
115
|
+
|
|
116
|
+
A batch containing a write-capable Persona must explicitly or effectively use `concurrency: 1`; an unsafe parallel request is rejected.
|
|
117
|
+
|
|
118
|
+
### `herdr_subagents_control`
|
|
119
|
+
|
|
120
|
+
| Action | `runId` | Purpose |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `list` | Not accepted | List effective Personas, resolved options, and diagnostics. |
|
|
123
|
+
| `status` | Required | Show one run and its task states. |
|
|
124
|
+
| `history` | Not accepted | List durable project-local run history. |
|
|
125
|
+
| `cancel` | Required | Cancel an active run. |
|
|
126
|
+
| `cleanup` | Required | Remove one archived run. |
|
|
127
|
+
|
|
128
|
+
Settled tasks are immutable. Follow-up work starts a new task or run; there is no resume/respond lifecycle.
|
|
129
|
+
|
|
130
|
+
### Slash command
|
|
131
|
+
|
|
132
|
+
`/herdr-subagents` supports:
|
|
133
|
+
|
|
134
|
+
| Command | Purpose |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `active` | Show the current run. This is also the default with no argument. |
|
|
137
|
+
| `history` | Show project-local history. |
|
|
138
|
+
| `focus <task-number>` | Focus a running task pane. |
|
|
139
|
+
| `agents` | Show effective Persona source, access, model, thinking, tools, skills, and extensions. |
|
|
140
|
+
| `models` | List authenticated Pi models. |
|
|
141
|
+
| `settings` | Show effective settings, their sources, and configuration diagnostics. |
|
|
142
|
+
|
|
143
|
+
## Configuration and Personas
|
|
144
|
+
|
|
145
|
+
Settings and Persona definitions are separate:
|
|
146
|
+
|
|
147
|
+
| Scope | Settings file | Persona directory |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| Project | `<project>/.pi/herdr-subagents.json` | `<project>/.pi/herdr-subagents/agents/*.md` |
|
|
150
|
+
| Global | `~/.pi/agent/herdr-subagents.json` | `~/.pi/agent/herdr-subagents/agents/*.md` |
|
|
151
|
+
| Package | Built-in defaults | Installed package `agents/*.md` |
|
|
152
|
+
|
|
153
|
+
For JSON settings, project fields override global fields, which override built-in defaults. `personas` entries merge by Persona name and then by field; array values replace lower-precedence arrays rather than appending to them.
|
|
154
|
+
|
|
155
|
+
Persona Markdown uses whole-definition replacement instead of field merging. A same-name definition is selected in this order:
|
|
156
|
+
|
|
157
|
+
1. project Persona
|
|
158
|
+
2. global Persona
|
|
159
|
+
3. package Persona
|
|
160
|
+
|
|
161
|
+
### Complete settings reference
|
|
162
|
+
|
|
163
|
+
These are all supported top-level fields in `herdr-subagents.json`:
|
|
164
|
+
|
|
165
|
+
| Option | Accepted value | Built-in default | Purpose |
|
|
166
|
+
| --- | --- | --- | --- |
|
|
167
|
+
| `exposure` | `"model-only"` or `"direct"` | Unset (inherit Pi; currently `direct`) | Controls whether the two tools may be called only by the top-level model or also by nested tools such as Codemode. |
|
|
168
|
+
| `defaultConcurrency` | Positive integer | `1` | Concurrency used when a call omits `concurrency`; capped to `maxConcurrency`. |
|
|
169
|
+
| `maxConcurrency` | Positive integer | `4` | Hard ceiling for requested/default read-only concurrency. It does not make write-capable batches safe. |
|
|
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
|
+
| `defaultModel` | Non-empty Pi model ID, normally `provider/model` | Unset | Fallback child model. If unset, the parent model is inherited. |
|
|
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
|
+
| `personas` | Object keyed by an existing Persona name | `{}` | Per-Persona overrides. It cannot create a Persona without a matching Markdown definition. |
|
|
174
|
+
|
|
175
|
+
Each `personas.<name>` object supports:
|
|
176
|
+
|
|
177
|
+
| Option | Accepted value | Purpose |
|
|
178
|
+
| --- | --- | --- |
|
|
179
|
+
| `model` | Non-empty Pi model ID | Override the model from Persona Markdown and `defaultModel`. |
|
|
180
|
+
| `thinking` | Non-empty Pi thinking value | Override Persona Markdown and `defaultThinking`. |
|
|
181
|
+
| `skills` | Array of non-empty strings | Replace the lower-precedence explicit skill list. Use `[]` to clear it. |
|
|
182
|
+
| `extensions` | Array of non-empty Pi source strings | Replace the lower-precedence extension-source list. Use `[]` to clear it. |
|
|
183
|
+
|
|
184
|
+
JSON overrides cannot change a Persona's `name`, `description`, `access`, `tools`, or system prompt; those belong in its Markdown definition.
|
|
185
|
+
|
|
186
|
+
Invalid known values are ignored and reported by `/herdr-subagents settings` or the control tool's `list` action. If `defaultConcurrency` exceeds `maxConcurrency`, the effective default is reduced to the maximum. Model IDs, thinking compatibility, skills, and extension sources are ultimately resolved by child Pi.
|
|
187
|
+
|
|
188
|
+
### Exposure
|
|
189
|
+
|
|
190
|
+
Use the restrictive mode when only direct model calls should be allowed:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"exposure": "model-only"
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- `model-only`: the top-level model may call the tools, but Codemode scripts and other tools cannot invoke them as nested tools.
|
|
199
|
+
- `direct`: allow both top-level and nested calls explicitly.
|
|
200
|
+
- omitted: do not set an exposure during registration; inherit Pi's default behavior.
|
|
201
|
+
|
|
202
|
+
`exposure` is read once when the extension registers. After changing it, use `/reload` or restart Pi. Other settings and Persona files are re-read at the start of each new tool call; they do not modify an already running task.
|
|
203
|
+
|
|
204
|
+
### Complete global example
|
|
205
|
+
|
|
206
|
+
Save this as `~/.pi/agent/herdr-subagents.json`. Model IDs, skills, and extension sources are illustrative and must exist in your Pi environment.
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"exposure": "model-only",
|
|
211
|
+
"defaultConcurrency": 2,
|
|
212
|
+
"maxConcurrency": 4,
|
|
213
|
+
"stalledWarningSeconds": 300,
|
|
214
|
+
"defaultModel": "anthropic/claude-sonnet-4-5",
|
|
215
|
+
"defaultThinking": "medium",
|
|
216
|
+
"personas": {
|
|
217
|
+
"explorer": {
|
|
218
|
+
"model": "anthropic/claude-haiku-4-5",
|
|
219
|
+
"thinking": "low",
|
|
220
|
+
"skills": ["code-search"],
|
|
221
|
+
"extensions": ["~/.pi/agent/extensions/provider-routing.ts"]
|
|
222
|
+
},
|
|
223
|
+
"reviewer": {
|
|
224
|
+
"thinking": "high",
|
|
225
|
+
"skills": ["code-review"],
|
|
226
|
+
"extensions": []
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
A project can override only what differs in `.pi/herdr-subagents.json`:
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"defaultConcurrency": 3,
|
|
237
|
+
"stalledWarningSeconds": 600,
|
|
238
|
+
"personas": {
|
|
239
|
+
"explorer": {
|
|
240
|
+
"model": "openai/gpt-5.2",
|
|
241
|
+
"skills": []
|
|
242
|
+
},
|
|
243
|
+
"reviewer": {
|
|
244
|
+
"extensions": ["npm:@example/pi-review-tools@1.0.0"]
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
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
|
+
|
|
252
|
+
### Built-in Personas
|
|
253
|
+
|
|
254
|
+
None of the bundled Personas pins a model, thinking level, skill, or optional extension. Those values come from configuration or parent-session inheritance.
|
|
255
|
+
|
|
256
|
+
| Persona | Access | Built-in tools | Intended use |
|
|
257
|
+
| --- | --- | --- | --- |
|
|
258
|
+
| `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. |
|
|
262
|
+
|
|
263
|
+
`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
|
+
|
|
265
|
+
### Custom Persona example
|
|
266
|
+
|
|
267
|
+
Save the following as `.pi/herdr-subagents/agents/security-reviewer.md` (or in the global Persona directory):
|
|
268
|
+
|
|
269
|
+
```md
|
|
270
|
+
---
|
|
271
|
+
name: security-reviewer
|
|
272
|
+
description: Read-only reviewer for security risks
|
|
273
|
+
access: read
|
|
274
|
+
model: anthropic/claude-sonnet-4-5
|
|
275
|
+
thinking: high
|
|
276
|
+
tools: read, bash, grep, find, ls, codemode
|
|
277
|
+
skills: security-review, code-review
|
|
278
|
+
extensions: ~/.pi/agent/extensions/provider-routing.ts
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
Review the assigned scope without modifying files. Separate confirmed vulnerabilities from hypotheses and cite concrete evidence.
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The Markdown body is the child system prompt. Frontmatter is intentionally a small single-line format, not full YAML:
|
|
285
|
+
|
|
286
|
+
| Field | Accepted value | Required/default | Purpose |
|
|
287
|
+
| --- | --- | --- | --- |
|
|
288
|
+
| `name` | Non-empty string | Required | Identifier used by `tasks[].agent`; unique within one directory. |
|
|
289
|
+
| `description` | Non-empty string | Required | Description shown by Persona listings. |
|
|
290
|
+
| `access` | `read` or `write` | `write` | Scheduling safety declaration. |
|
|
291
|
+
| `model` | Non-empty Pi model ID | Unset | Persona model default. |
|
|
292
|
+
| `thinking` | Non-empty Pi thinking value | Unset | Persona thinking default. |
|
|
293
|
+
| `tools` | Non-empty comma-separated list, optionally `[bracketed]` | Pi child defaults | Built-in-tool allowlist. Supported names: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`, `powershell`, `codemode`. `subagent_report` is always added. |
|
|
294
|
+
| `skills` | Comma-separated list, optionally `[bracketed]` | None | Explicit child skills; normal automatic skill discovery is disabled. |
|
|
295
|
+
| `extensions` | One source or a comma-separated list; `[]` clears | None | Additional Pi extension/package sources loaded into the child. |
|
|
296
|
+
|
|
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.
|
|
298
|
+
|
|
299
|
+
### Effective option precedence
|
|
300
|
+
|
|
301
|
+
The child model is selected in this order:
|
|
302
|
+
|
|
303
|
+
1. `tasks[].model`
|
|
304
|
+
2. project `personas.<name>.model`
|
|
305
|
+
3. global `personas.<name>.model`
|
|
306
|
+
4. effective Persona Markdown `model`
|
|
307
|
+
5. effective `defaultModel` (project, then global)
|
|
308
|
+
6. parent Pi model
|
|
309
|
+
|
|
310
|
+
Thinking is selected in this order:
|
|
89
311
|
|
|
90
|
-
|
|
312
|
+
1. project `personas.<name>.thinking`
|
|
313
|
+
2. global `personas.<name>.thinking`
|
|
314
|
+
3. effective Persona Markdown `thinking`
|
|
315
|
+
4. effective `defaultThinking` (project, then global)
|
|
316
|
+
5. parent thinking when the child also inherits the parent model
|
|
91
317
|
|
|
92
|
-
|
|
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.
|
|
93
319
|
|
|
94
|
-
|
|
320
|
+
Use `/herdr-subagents agents`, `/herdr-subagents models`, and `/herdr-subagents settings` to inspect the result before starting work.
|
|
95
321
|
|
|
96
|
-
|
|
322
|
+
## Development and tests
|
|
97
323
|
|
|
98
|
-
|
|
324
|
+
See [TESTING.md](TESTING.md) for deterministic tests, extension loading, packaging, and real Herdr smoke requirements. The real smoke preserves your configured `analyst` model, thinking level, and explicit extensions (including authentication providers) inside its isolated fixture; unrelated Persona settings remain isolated.
|
|
@@ -17,9 +17,12 @@ It verifies:
|
|
|
17
17
|
- explicit failure when the process exits without a report;
|
|
18
18
|
- parent cancellation of running and queued tasks;
|
|
19
19
|
- startup isolation: a new Pi session neither mutates historical records nor closes their recorded Herdr tabs;
|
|
20
|
+
- omitted exposure delegates both tools to Pi's default, while project configuration can consistently select model-only exposure;
|
|
20
21
|
- immutable terminal task records;
|
|
21
22
|
- idempotent duplicate reports and rejection of conflicting replacements;
|
|
22
|
-
- keeping a settled child alive for focus-aware cleanup instead of force-exiting its TUI
|
|
23
|
+
- keeping a settled child alive for focus-aware cleanup instead of force-exiting its TUI;
|
|
24
|
+
- preserving the configured smoke model, thinking level, and authentication extensions while isolating unrelated settings;
|
|
25
|
+
- project-over-global smoke configuration, explicit extension clearing, model overrides, and local-source paths after moving to a temporary directory.
|
|
23
26
|
|
|
24
27
|
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.
|
|
25
28
|
|
|
@@ -35,4 +38,10 @@ Run the real Herdr smoke test from a Herdr session:
|
|
|
35
38
|
npm run test:real-herdr --workspace @maxiaochao/pi-herdr-subagents
|
|
36
39
|
```
|
|
37
40
|
|
|
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.
|
|
42
|
+
|
|
43
|
+
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
|
+
|
|
45
|
+
`HERDR_SUBAGENTS_E2E_MODEL=provider/model` overrides only the smoke model; configured extensions remain loaded. Set `HERDR_SUBAGENTS_E2E_FOCUS_CLEANUP=1` to also verify deferred cleanup while focused and restoration of the original workspace/tab focus.
|
|
46
|
+
|
|
38
47
|
For real smoke testing, verify that each child appears in a visible pane and can be focused and inspected. An early-finishing child must remain visible while sibling tasks continue. After the whole run settles, a focused child must remain at its restored Pi UI until focus leaves the run tab; the tab must then close automatically. Verify that the parent returns only after all requested tasks settle. Also verify cancellation closes the run tab and does not overwrite a report already atomically published.
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import type { AgentToolResult, ExtensionAPI, ExtensionCommandContext, Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Container, Editor, Text } from "@earendil-works/pi-tui";
|
|
5
|
-
import { loadSubagentConfiguration } from "../src/config.ts";
|
|
5
|
+
import { loadSubagentConfiguration, type ConfigurationPaths } from "../src/config.ts";
|
|
6
6
|
import { CliHerdrAutomation, type HerdrAutomation } from "../src/herdr.ts";
|
|
7
7
|
import { activeRunText } from "../src/monitor.ts";
|
|
8
8
|
import { historyText, listSubagentHistory } from "../src/history.ts";
|
|
@@ -62,7 +62,7 @@ function renderToolResult(result: AgentToolResult<unknown>, _options: ToolRender
|
|
|
62
62
|
return new Text(content?.type === "text" ? content.text : "(no output)", 0, 0);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fleet: FleetController): Promise<void> {
|
|
65
|
+
async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fleet: FleetController, registeredExposure: { value?: "direct" | "model-only"; source: string }): Promise<void> {
|
|
66
66
|
const section = args.trim() || "active";
|
|
67
67
|
if (section === "active") {
|
|
68
68
|
ctx.ui.notify(fleet.active ? activeRunText(fleet.active) : "No active Subagent run.", "info");
|
|
@@ -90,7 +90,7 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
|
|
|
90
90
|
if (section === "settings") {
|
|
91
91
|
const settings = configuration.settings;
|
|
92
92
|
const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, defaultModel: settings.defaultModel, defaultThinking: settings.defaultThinking };
|
|
93
|
-
const lines = Object.entries(values).map(([key, value]) => `${key}=${String(value ?? "unset")} (${configuration.settingSources[key] ?? "built-in"})`);
|
|
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");
|
|
96
96
|
return;
|
|
@@ -100,6 +100,8 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
|
|
|
100
100
|
|
|
101
101
|
export interface HerdrSubagentsExtensionDependencies {
|
|
102
102
|
herdr?: HerdrAutomation;
|
|
103
|
+
configurationCwd?: string;
|
|
104
|
+
configurationPaths?: ConfigurationPaths;
|
|
103
105
|
execute?: typeof executeHerdrSubagents;
|
|
104
106
|
executeControl?: typeof executeHerdrSubagentsControl;
|
|
105
107
|
isEditor?: (value: unknown) => boolean;
|
|
@@ -121,6 +123,9 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
121
123
|
const herdr = dependencies.herdr ?? new CliHerdrAutomation();
|
|
122
124
|
const execute = dependencies.execute ?? executeHerdrSubagents;
|
|
123
125
|
const executeControl = dependencies.executeControl ?? executeHerdrSubagentsControl;
|
|
126
|
+
const registrationConfiguration = loadSubagentConfiguration(dependencies.configurationCwd ?? process.cwd(), packageAgentsDir, undefined, dependencies.configurationPaths);
|
|
127
|
+
const exposure = registrationConfiguration.settings.exposure;
|
|
128
|
+
const registeredExposure = { value: exposure, source: registrationConfiguration.settingSources.exposure ?? "Pi" };
|
|
124
129
|
const isEditorComponent = dependencies.isEditor ?? ((value: unknown) => value instanceof Editor);
|
|
125
130
|
const fleet = new FleetController(herdr, isEditorComponent, dependencies.createFleetWidget, dependencies.fleetWidgetRuntime);
|
|
126
131
|
const runControllers = new Map<string, AbortController>();
|
|
@@ -141,6 +146,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
141
146
|
|
|
142
147
|
pi.registerTool({
|
|
143
148
|
name: "herdr_subagents",
|
|
149
|
+
...(exposure ? { exposure } : {}),
|
|
144
150
|
label: "Herdr Subagents",
|
|
145
151
|
description: "Run one or more ordered tasks in visible, focusable Herdr Pi panes. concurrency 1 is strict serial execution; higher values form a bounded pool for read-only tasks. Write-capable tasks require concurrency 1. The call returns only after every task has one terminal completed, failed, or cancelled result. Complete results and Pi sessions stay in project-local records.",
|
|
146
152
|
promptSnippet: "Run focused work in visible Herdr subagents",
|
|
@@ -181,6 +187,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
181
187
|
|
|
182
188
|
pi.registerTool({
|
|
183
189
|
name: "herdr_subagents_control",
|
|
190
|
+
...(exposure ? { exposure } : {}),
|
|
184
191
|
label: "Herdr Subagents Control",
|
|
185
192
|
description: "Inspect Herdr personas, query one run by ID, list run history, cancel active work, or remove an archived run. Use herdr_subagents to start new work.",
|
|
186
193
|
parameters: HerdrSubagentsControlParams,
|
|
@@ -231,7 +238,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
231
238
|
pi.registerCommand("herdr-subagents", {
|
|
232
239
|
description: "Inspect effective subagent agents, models, and settings",
|
|
233
240
|
getArgumentCompletions: (prefix) => ["active", "history", "agents", "models", "settings", "focus "].filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
|
|
234
|
-
handler: (args, ctx) => handleHerdrCommand(args, ctx, fleet),
|
|
241
|
+
handler: (args, ctx) => handleHerdrCommand(args, ctx, fleet, registeredExposure),
|
|
235
242
|
});
|
|
236
243
|
}
|
|
237
244
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-herdr-subagents",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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",
|
|
40
|
+
"test": "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"
|
|
@@ -4,8 +4,10 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { discoverPackageAgents, type AgentDefinition } from "./personas.ts";
|
|
5
5
|
|
|
6
6
|
export type PersonaAccess = "read" | "write";
|
|
7
|
+
export type SubagentToolExposure = "direct" | "model-only";
|
|
7
8
|
export interface PersonaConfig { model?: string; thinking?: string; skills?: string[]; extensions?: string[]; }
|
|
8
9
|
export interface SubagentConfig {
|
|
10
|
+
exposure?: SubagentToolExposure;
|
|
9
11
|
defaultConcurrency: number;
|
|
10
12
|
maxConcurrency: number;
|
|
11
13
|
stalledWarningSeconds: number;
|
|
@@ -32,6 +34,7 @@ export interface SubagentConfiguration {
|
|
|
32
34
|
diagnostics: string[];
|
|
33
35
|
}
|
|
34
36
|
interface ConfigFile {
|
|
37
|
+
exposure?: string;
|
|
35
38
|
defaultConcurrency?: number;
|
|
36
39
|
maxConcurrency?: number;
|
|
37
40
|
stalledWarningSeconds?: number;
|
|
@@ -61,6 +64,10 @@ function readConfig(path: string, source: string, diagnostics: string[]): Config
|
|
|
61
64
|
|
|
62
65
|
function mergeConfig(base: SubagentConfig, input: ConfigFile, source: string, sources: Record<string, string>, diagnostics: string[]): SubagentConfig {
|
|
63
66
|
const result: SubagentConfig = { ...base, personas: { ...base.personas } };
|
|
67
|
+
if (input.exposure !== undefined) {
|
|
68
|
+
if (input.exposure === "direct" || input.exposure === "model-only") { result.exposure = input.exposure; sources.exposure = source; }
|
|
69
|
+
else diagnostics.push(`${source} exposure must be 'direct' or 'model-only'`);
|
|
70
|
+
}
|
|
64
71
|
for (const key of ["defaultConcurrency", "maxConcurrency", "stalledWarningSeconds"] as const) {
|
|
65
72
|
const value = input[key];
|
|
66
73
|
if (value === undefined) continue;
|
|
@@ -159,11 +159,13 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
|
|
|
159
159
|
const historicalRun = { id: "historical-run", label: "historical", status: "completed", ownerSessionId: "old-session", ownerProcessId: 99999999, tabId: "w1:t-reused", taskIds: ["historical-task"], completedAt: "2026-10-01T00:00:00.000Z" };
|
|
160
160
|
writeFileSync(join(runDirectory, "run.json"), JSON.stringify(historicalRun));
|
|
161
161
|
writeFileSync(join(taskDirectory, "task.json"), JSON.stringify({ id: "historical-task", runId: "historical-run", order: 1, status: "completed", completedAt: historicalRun.completedAt }));
|
|
162
|
+
const configurationPaths = { globalConfig: join(lifecycleRoot, "global-config.json") };
|
|
162
163
|
const events = new Map();
|
|
164
|
+
const registeredTools = new Map();
|
|
163
165
|
const herdrCalls = [];
|
|
164
166
|
registerHerdrSubagents({
|
|
165
167
|
on(name, handler) { events.set(name, handler); },
|
|
166
|
-
registerTool() {},
|
|
168
|
+
registerTool(tool) { registeredTools.set(tool.name, tool); },
|
|
167
169
|
registerCommand() {},
|
|
168
170
|
}, {
|
|
169
171
|
herdr: {
|
|
@@ -171,6 +173,8 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
|
|
|
171
173
|
async closeTab(tabId) { herdrCalls.push(["closeTab", tabId]); },
|
|
172
174
|
},
|
|
173
175
|
isEditor: () => false,
|
|
176
|
+
configurationCwd: lifecycleRoot,
|
|
177
|
+
configurationPaths,
|
|
174
178
|
});
|
|
175
179
|
await events.get("session_start")?.({}, {
|
|
176
180
|
cwd: lifecycleRoot,
|
|
@@ -180,6 +184,18 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
|
|
|
180
184
|
});
|
|
181
185
|
assert.deepEqual(herdrCalls, [], "Pi startup must not inspect or close historical Subagent tabs");
|
|
182
186
|
assert.deepEqual(JSON.parse(readFileSync(join(runDirectory, "run.json"), "utf8")), historicalRun, "Pi startup must not mutate historical Subagent records");
|
|
187
|
+
assert.equal(registeredTools.get("herdr_subagents")?.exposure, undefined, "Unconfigured Subagent execution must inherit Pi's exposure default");
|
|
188
|
+
assert.equal(registeredTools.get("herdr_subagents_control")?.exposure, undefined, "Unconfigured Subagent control must inherit Pi's exposure default");
|
|
189
|
+
mkdirSync(join(lifecycleRoot, ".pi"), { recursive: true });
|
|
190
|
+
writeFileSync(join(lifecycleRoot, ".pi", "herdr-subagents.json"), JSON.stringify({ exposure: "model-only" }));
|
|
191
|
+
const configuredTools = new Map();
|
|
192
|
+
registerHerdrSubagents({
|
|
193
|
+
on() {},
|
|
194
|
+
registerTool(tool) { configuredTools.set(tool.name, tool); },
|
|
195
|
+
registerCommand() {},
|
|
196
|
+
}, { herdr: {}, isEditor: () => false, configurationCwd: lifecycleRoot, configurationPaths });
|
|
197
|
+
assert.equal(configuredTools.get("herdr_subagents")?.exposure, "model-only", "Project configuration can exclude Subagent execution from Codemode");
|
|
198
|
+
assert.equal(configuredTools.get("herdr_subagents_control")?.exposure, "model-only", "Project configuration applies consistently to both Subagent tools");
|
|
183
199
|
}
|
|
184
200
|
|
|
185
201
|
{
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { loadSubagentConfiguration } from "../src/config.ts";
|
|
3
|
+
|
|
4
|
+
export function smokePersona(cwd, agentsDirectory, model, paths) {
|
|
5
|
+
const discovery = loadSubagentConfiguration(cwd, agentsDirectory, undefined, paths);
|
|
6
|
+
const analyst = discovery.personas.find(({ name }) => name === "analyst");
|
|
7
|
+
if (!analyst) throw new Error("Real Herdr smoke requires an analyst Persona.");
|
|
8
|
+
return {
|
|
9
|
+
model: model?.trim() || analyst.model,
|
|
10
|
+
thinking: analyst.thinking,
|
|
11
|
+
// Local sources must still resolve from the caller's project after moving to the fixture cwd.
|
|
12
|
+
extensions: analyst.extensions.map((source) => /^(?:[a-z][a-z\d+.-]*:|~(?:\/|$)|git@)/i.test(source) ? source : resolve(cwd, source)),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { loadSubagentConfiguration } from "../src/config.ts";
|
|
7
|
+
import { childPiArgs } from "../src/live-agent.ts";
|
|
8
|
+
import { smokePersona } from "./real-herdr-config.mjs";
|
|
9
|
+
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "herdr-smoke-config-"));
|
|
11
|
+
const agents = fileURLToPath(new URL("../agents", import.meta.url));
|
|
12
|
+
const paths = {
|
|
13
|
+
globalConfig: join(root, "global.json"),
|
|
14
|
+
projectConfig: join(root, "project.json"),
|
|
15
|
+
globalAgents: join(root, "global-agents"),
|
|
16
|
+
projectAgents: join(root, "project-agents"),
|
|
17
|
+
};
|
|
18
|
+
const write = (path, value) => writeFileSync(path, JSON.stringify(value));
|
|
19
|
+
try {
|
|
20
|
+
write(paths.globalConfig, {
|
|
21
|
+
maxConcurrency: 1,
|
|
22
|
+
defaultModel: "openai-codex/test-model",
|
|
23
|
+
defaultThinking: "high",
|
|
24
|
+
personas: { analyst: {
|
|
25
|
+
extensions: ["npm:@maxiaochao/pi-openai-account-pool"],
|
|
26
|
+
skills: ["unrelated-skill"],
|
|
27
|
+
} },
|
|
28
|
+
});
|
|
29
|
+
const before = readFileSync(paths.globalConfig, "utf8");
|
|
30
|
+
const persona = smokePersona(root, agents, undefined, paths);
|
|
31
|
+
assert.deepEqual(persona, {
|
|
32
|
+
model: "openai-codex/test-model",
|
|
33
|
+
thinking: "high",
|
|
34
|
+
extensions: ["npm:@maxiaochao/pi-openai-account-pool"],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const fixture = join(root, "fixture.json");
|
|
38
|
+
const probe = join(root, "local-package");
|
|
39
|
+
write(fixture, { personas: { analyst: { ...persona, extensions: [...persona.extensions, probe] } } });
|
|
40
|
+
const isolated = loadSubagentConfiguration(root, agents, undefined, { ...paths, globalConfig: fixture });
|
|
41
|
+
const analyst = isolated.personas.find(({ name }) => name === "analyst");
|
|
42
|
+
assert.equal(isolated.settings.maxConcurrency, 4);
|
|
43
|
+
assert.equal(analyst.access, "read");
|
|
44
|
+
assert.deepEqual(analyst.skills, []);
|
|
45
|
+
assert.deepEqual(analyst.extensions, ["npm:@maxiaochao/pi-openai-account-pool", probe]);
|
|
46
|
+
const args = childPiArgs({ ...analyst, sessionFile: join(root, "session.jsonl") }, join(root, "system.md"));
|
|
47
|
+
assert.equal(args[args.indexOf("--model") + 1], "openai-codex/test-model");
|
|
48
|
+
assert.ok(args.includes("--no-extensions"));
|
|
49
|
+
assert.ok(args.includes("npm:@maxiaochao/pi-openai-account-pool"));
|
|
50
|
+
assert.ok(args.includes(probe));
|
|
51
|
+
assert.equal(readFileSync(paths.globalConfig, "utf8"), before);
|
|
52
|
+
console.log("PASS smoke preserves configured model and authentication extensions without copying unrelated settings");
|
|
53
|
+
|
|
54
|
+
write(paths.projectConfig, { personas: { analyst: {
|
|
55
|
+
model: "custom/project-model",
|
|
56
|
+
thinking: "low",
|
|
57
|
+
extensions: ["./provider with spaces.ts", "../provider-package", "~/provider.ts", "npm:provider@1.0.0", "git:github.com/example/provider@v1", "builtin:codemode", "https://example.test/provider.git"],
|
|
58
|
+
} } });
|
|
59
|
+
assert.deepEqual(smokePersona(root, agents, undefined, paths), {
|
|
60
|
+
model: "custom/project-model",
|
|
61
|
+
thinking: "low",
|
|
62
|
+
extensions: [join(root, "provider with spaces.ts"), join(root, "..", "provider-package"), "~/provider.ts", "npm:provider@1.0.0", "git:github.com/example/provider@v1", "builtin:codemode", "https://example.test/provider.git"],
|
|
63
|
+
});
|
|
64
|
+
console.log("PASS smoke honors project precedence and keeps local and native package sources valid in a temporary cwd");
|
|
65
|
+
|
|
66
|
+
write(paths.projectConfig, { personas: { analyst: { extensions: [] } } });
|
|
67
|
+
assert.deepEqual(smokePersona(root, agents, " custom/override-model ", paths), {
|
|
68
|
+
model: "custom/override-model",
|
|
69
|
+
thinking: "high",
|
|
70
|
+
extensions: [],
|
|
71
|
+
});
|
|
72
|
+
mkdirSync(paths.globalAgents, { recursive: true });
|
|
73
|
+
writeFileSync(join(paths.globalAgents, "analyst.md"), "---\nname: analyst\ndescription: Custom user analyst\naccess: write\ntools: write\n---\nUnrelated user prompt.\n");
|
|
74
|
+
const selected = smokePersona(root, agents, undefined, paths);
|
|
75
|
+
assert.deepEqual(Object.keys(selected).sort(), ["extensions", "model", "thinking"]);
|
|
76
|
+
assert.equal(selected.model, "openai-codex/test-model");
|
|
77
|
+
console.log("PASS smoke supports model overrides and explicit extension clearing without copying Persona tools or prompts");
|
|
78
|
+
} finally {
|
|
79
|
+
rmSync(root, { recursive: true, force: true });
|
|
80
|
+
}
|
|
@@ -7,12 +7,14 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { executeHerdrSubagents } from "../src/tool.ts";
|
|
8
8
|
import { CliHerdrAutomation } from "../src/herdr.ts";
|
|
9
9
|
import { waitFor } from "./wait-for.mjs";
|
|
10
|
+
import { smokePersona } from "./real-herdr-config.mjs";
|
|
10
11
|
|
|
11
12
|
if (process.env.HERDR_ENV !== "1" || !process.env.HERDR_WORKSPACE_ID) {
|
|
12
13
|
throw new Error("The real Herdr smoke test must run from a Pi session inside Herdr.");
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const packageDirectory = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
17
|
+
const runtimePersona = smokePersona(process.env.INIT_CWD || process.cwd(), join(packageDirectory, "agents"), process.env.HERDR_SUBAGENTS_E2E_MODEL);
|
|
16
18
|
const root = mkdtempSync(join(tmpdir(), "pi-herdr-subagents-real-"));
|
|
17
19
|
const herdr = new CliHerdrAutomation();
|
|
18
20
|
const tabIds = new Set();
|
|
@@ -47,7 +49,6 @@ for (const method of ["createTab", "splitPane", "startAgent", "isTabFocused", "w
|
|
|
47
49
|
}
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
|
-
const model = process.env.HERDR_SUBAGENTS_E2E_MODEL?.trim();
|
|
51
52
|
const configurationPaths = {
|
|
52
53
|
globalConfig: join(root, "global-config.json"),
|
|
53
54
|
projectConfig: join(root, "project-config.json"),
|
|
@@ -94,7 +95,9 @@ async function waitForCleanup(result) {
|
|
|
94
95
|
}, `cleanup of ${result.tabId}`, 5_000);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
async function runCase(label, names, concurrency,
|
|
98
|
+
async function runCase(label, names, concurrency, extraExtensions = []) {
|
|
99
|
+
const extensions = [...runtimePersona.extensions, ...extraExtensions];
|
|
100
|
+
writeFileSync(configurationPaths.globalConfig, JSON.stringify({ personas: { analyst: { ...runtimePersona, extensions } } }));
|
|
98
101
|
if (concurrency > 1 && process.env.HERDR_SUBAGENTS_E2E_FOCUS_CLEANUP === "1") {
|
|
99
102
|
const workspaces = spawnSync(process.env.PI_HERDR_SUBAGENTS_HERDR_BINARY?.trim() || "herdr", ["workspace", "list"], { encoding: "utf8", timeout: 10_000 });
|
|
100
103
|
assert.equal(workspaces.status, 0, workspaces.stderr);
|
|
@@ -108,7 +111,6 @@ async function runCase(label, names, concurrency, extensions = []) {
|
|
|
108
111
|
tasks: names.map((name) => ({
|
|
109
112
|
name,
|
|
110
113
|
agent: "analyst",
|
|
111
|
-
...(model ? { model } : {}),
|
|
112
114
|
prompt: `Do not inspect files. Call subagent_report with status completed, summary '${name} completed', and result 'hello'.`,
|
|
113
115
|
})),
|
|
114
116
|
}, timeout, (update) => {
|
|
@@ -145,7 +147,8 @@ async function runCase(label, names, concurrency, extensions = []) {
|
|
|
145
147
|
const report = JSON.parse(readFileSync(join(taskResult.recordDirectory, "report.json"), "utf8"));
|
|
146
148
|
assert.equal(task.status, "completed");
|
|
147
149
|
assert.deepEqual(task.extensions, extensions);
|
|
148
|
-
if (
|
|
150
|
+
if (runtimePersona.model) assert.equal(task.model, runtimePersona.model);
|
|
151
|
+
if (extraExtensions.length) {
|
|
149
152
|
const commands = JSON.parse(readFileSync(join(taskResult.recordDirectory, "extension-loaded.json"), "utf8"));
|
|
150
153
|
assert.ok(commands.includes("skill:smoke-package-skill"), "Explicit package skill did not load in the real child");
|
|
151
154
|
}
|
|
@@ -168,7 +171,6 @@ try {
|
|
|
168
171
|
export default function (pi) {
|
|
169
172
|
pi.on("session_start", () => writeFileSync(join(process.env.PI_HERDR_SUBAGENTS_TASK_DIR, "extension-loaded.json"), JSON.stringify(pi.getCommands().map((command) => command.name))));
|
|
170
173
|
}`);
|
|
171
|
-
writeFileSync(configurationPaths.globalConfig, JSON.stringify({ personas: { analyst: { extensions: [packageSource] } } }));
|
|
172
174
|
await runCase("real-concurrent", ["hello-one", "hello-two"], 2, [packageSource]);
|
|
173
175
|
} catch (error) {
|
|
174
176
|
failed = true;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-team
|
|
3
|
-
description:
|
|
3
|
+
description: 多模型协作分析:主 Agent 按配置的模型和角色调度独立子代理,汇总结果并维护调查档案。用于用户要求多模型分析或对抗评审,或高代价决策需要独立复核时。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
当前会话是**主 Agent**,负责核心调度:按配置中的模型和角色启动独立子代理,分派用户问题,收集结果并维护调查档案。主 Agent 根据结果决定直接回答、补充核验或继续下一轮。子代理不需要常驻;跨轮连续性来自持久调查档案和逻辑成员身份,而不是聊天记忆。
|
|
7
7
|
|
|
8
8
|
## 何时值得开
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
用户明确要求多模型分析或对抗评审时开启;主 Agent 也可在结论会驱动高代价动作(改内核、下线设备、向客户定责)时主动开启。查一个事实、读一份文件用不上。
|
|
11
11
|
|
|
12
12
|
## 1. 建立调查档案
|
|
13
13
|
|
|
@@ -19,46 +19,42 @@ description: 以对抗评审的方式让不同厂商的模型交叉验证同一
|
|
|
19
19
|
├── FINDINGS-stageN.md # 当前唯一事实源;新阶段整体取代旧阶段
|
|
20
20
|
├── LEDGER.md # 只增不改的 N/X 记录
|
|
21
21
|
└── rounds/
|
|
22
|
-
└── N.md #
|
|
22
|
+
└── N.md # 本轮成员、模型、角色和分派问题
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
`BRIEF.md` 只记录稳定背景。`FINDINGS-stageN.md`
|
|
25
|
+
`BRIEF.md` 只记录稳定背景。`FINDINGS-stageN.md` 记录当前问题、编号发现及证据、已推翻结论和待处理项。`LEDGER.md` 中发现按 `N1, N2…` 追加,被推翻的结论按 `X1, X2…` 追加并写明推翻者。只有主 Agent 能更新这三个权威文件;子代理只提交结果和候选证据。
|
|
26
26
|
|
|
27
27
|
## 2. 配置成员与角色
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
读取本 skill 同目录的 [models.json](models.json):`members` 是成员与模型的唯一配置来源,`roles` 是角色职责的唯一配置来源。默认将两个数组按顺序一一对应;配置为空或数量不匹配时,报告配置问题后再启动。
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
按配置成员数并发启动子代理,显式传入各自的 `model`,并将对应角色的 `charter` 放入任务。成员使用相同的中性只读执行约束。启动后核对并在本轮记录中写下每个任务实际生效的模型;无法确认或与配置不符时,该成员结果无效并报告原因。
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
- `replicator`:绕开管理者的脚本和口径,从原始数据独立复算。
|
|
35
|
-
- `adversary`:攻击前提和方法学,查样本敏感性、一对多计数和阴性对照。
|
|
36
|
-
|
|
37
|
-
模型与角色不得永久绑定。首轮可以按 `models.json` 顺序分配三个角色;后续根据争议轮换或交叉复核,使模型差异和角色差异能够被区分。每轮在 `rounds/N.md` 写明成员、模型、角色、输入文件和攻击点。
|
|
33
|
+
每轮在 `rounds/N.md` 写明成员、模型、角色、输入文件和分派问题。默认沿用角色映射;需要交叉复核时可轮换,并记录新的映射。
|
|
38
34
|
|
|
39
35
|
## 3. 交底与通信
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
拓扑是星型,主 Agent 是唯一消息路由。成员在同一轮中独立作答,在提交前不得看到其他成员本轮答案。需要逻辑成员 A 复核逻辑成员 B 的上一轮结论时,由主 Agent 在新任务中引用 B 的原话和证据位置;不能假定新实例记得上一轮聊天。
|
|
42
38
|
|
|
43
|
-
|
|
39
|
+
下行给用户问题、角色职责、具体任务和文件指针:每个成员阅读 `BRIEF.md`、最新 `FINDINGS-stageN.md`、`LEDGER.md` 和本轮文件,使用档案恢复背景。上行结果必须能归属到轮次和成员,并包含结论、证据位置和未解决问题;长结果写入持久文件,不能依赖截断的终端文本。
|
|
44
40
|
|
|
45
41
|
## 4. 回合
|
|
46
42
|
|
|
47
|
-
一轮 = 记录受保护文件和工作树初始状态 → 写本轮任务 →
|
|
43
|
+
一轮 = 记录受保护文件和工作树初始状态 → 写本轮任务 → 配置成员并发独立分析 → 主 Agent 收齐结果或失败报告 → 检查子代理未修改项目或权威文件 → 汇总并核验关键证据 → 更新 FINDINGS 和 LEDGER → 决定收尾或继续。
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
每个成员收到范围明确、可检查结果的任务。例如“查 `adbconnection.cc`,确认该错误日志与被销毁的 fd 是否 1:1”。只有存在需要补证、复核或继续处理的问题时才开下一轮,并写明目标;子代理结束后即可释放。
|
|
50
46
|
|
|
51
47
|
## 5. 纪律
|
|
52
48
|
|
|
53
49
|
- **独立性**:同轮答案不得互相可见;成员之间不直接通信。
|
|
54
50
|
- **只读完整性**:只读是协议约束,不假定执行环境构成沙箱。每轮结束后比较初始状态;发现子代理修改项目或权威文件时,该成员结果无效并显式报告污染范围。
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
- **grading**:每条结论标注 `源码读取 / 实测 / 推断` 和置信度;推断不能与实测并列陈述。
|
|
59
|
-
- **裁决**:成员冲突时管理者必须判定谁对并说明证据,不能多数投票或折中平均。
|
|
51
|
+
- **更正**:结论被推翻时,明确记录更正及依据,并追加对应 X。
|
|
52
|
+
- **证据**:区分直接证据、实测、推断和未知;结论附证据位置。
|
|
53
|
+
- **分歧**:根据证据说明支持、推翻或未决;未决项写明缺失证据,不以多数投票或折中平均替代核验。
|
|
60
54
|
- **缺席视角**:成员失败或额度耗尽时,明确记录缺失的模型和方法角色,不能把不完整轮次当成完整评审。
|
|
61
55
|
|
|
62
|
-
## 6.
|
|
56
|
+
## 6. 按需核验与收尾
|
|
57
|
+
|
|
58
|
+
主 Agent 根据问题需要选择角色轮换、跨模型独立复算、阴性对照或开放攻击。开放攻击时,由各成员自行选择其认为最强的反证方向。这些是补充核验手段,不是每次分析的必经步骤;任务仍须遵循所分配角色的 `charter`。
|
|
63
59
|
|
|
64
|
-
|
|
60
|
+
当用户问题已有足够证据支持的回答,或剩余问题受缺失输入、工具或权限阻塞时,可以收尾。最终回答汇总结论及依据,单列未解决问题、失败或缺席成员,以及需要人或设备批准的动作;受阻停止时明确说明限制,不宣称已经收敛。
|