@maxiaochao/pi-toolkit 0.10.1 → 0.10.3

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/global/AGENTS.md +33 -50
  4. package/package.json +2 -2
  5. package/packages/herdr-subagents/CHANGELOG.md +14 -0
  6. package/packages/herdr-subagents/README.md +20 -18
  7. package/packages/herdr-subagents/TESTING.md +6 -4
  8. package/packages/herdr-subagents/docs/demo.webm +0 -0
  9. package/packages/herdr-subagents/docs/screenshot.png +0 -0
  10. package/packages/herdr-subagents/extensions/herdr-subagents.ts +91 -51
  11. package/packages/herdr-subagents/package.json +1 -1
  12. package/packages/herdr-subagents/src/batch-runner.ts +5 -5
  13. package/packages/herdr-subagents/src/blocking.ts +4 -4
  14. package/packages/herdr-subagents/src/fleet-controller.ts +164 -16
  15. package/packages/herdr-subagents/src/fleet.ts +119 -8
  16. package/packages/herdr-subagents/src/history.ts +11 -5
  17. package/packages/herdr-subagents/src/parameters.ts +16 -13
  18. package/packages/herdr-subagents/src/queued-runs.ts +3 -3
  19. package/packages/herdr-subagents/src/records.ts +53 -1
  20. package/packages/herdr-subagents/src/tool.ts +40 -6
  21. package/packages/herdr-subagents/src/validation.ts +16 -29
  22. package/packages/herdr-subagents/tests/extension-contract.ts +27 -9
  23. package/packages/herdr-subagents/tests/run-tests.mjs +153 -18
  24. package/packages/todo/CHANGELOG.md +4 -0
  25. package/packages/todo/README.md +19 -9
  26. package/packages/todo/TESTING.md +9 -7
  27. package/packages/todo/docs/demo.gif +0 -0
  28. package/packages/todo/docs/screenshot.png +0 -0
  29. package/packages/todo/extensions/todo.ts +120 -90
  30. package/packages/todo/package.json +2 -2
  31. package/packages/todo/src/todo-state.ts +48 -49
  32. package/packages/todo/tests/todo-extension.mjs +134 -0
  33. package/packages/todo/tests/todo-state.mjs +80 -70
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 2026-10-08 - v0.10.3
2
+
3
+ - bundle herdr-subagents v0.4.0: add the live compact Fleet footer with authoritative metrics and refreshed media
4
+ - Add the responsive below-editor Herdr Fleet monitor with real personas, task states, cumulative active durations, and authoritative child Pi token totals, including event-driven live updates and safe partial-JSONL handling.
5
+ - Color running Fleet rows yellow and completed rows green, compact the shared metrics column instead of pushing it to the terminal edge, and update the published media with focus restoration after capture.
6
+
7
+ ## 2026-10-08 - v0.10.2
8
+
9
+ - bundle herdr-subagents v0.3.0: split execution and control tools and unify task runs
10
+ - Bundle the Herdr Subagents execution/control tool split and unified non-empty `tasks` input for single and multi-task runs.
11
+
1
12
  ## 2026-10-08 - v0.10.1
2
13
 
3
14
  - bundle herdr-subagents v0.2.1: add default thinking configuration and inline Herdr workflow video
@@ -178,6 +189,7 @@
178
189
 
179
190
  ## Unreleased
180
191
 
192
+ - redesign bundled Todo as a branch-local execution plan with lifecycle-only model actions, expected-current guards, serialized calls, preserved branch history, and a live current-plan widget
181
193
  - let `/cache_export` switch between all history and individual compaction-defined Context segments, defaulting to the latest segment; cache-hit cumulative lines reset at compaction or model boundaries
182
194
  - add the independent `@maxiaochao/pi-codex-edit` package with model-aware Codex-style `apply_patch`
183
195
  - include the benchmark summary and interactive HTML architecture explainer
package/README.md CHANGED
@@ -22,7 +22,7 @@ The root package embeds the child-package source snapshots listed below. Install
22
22
 
23
23
  | Package | Capability | Install | Update |
24
24
  |---|---|---|---|
25
- | [pi-todo](packages/todo/README.md) | Session plans, automatic task advancement, `/todos` | `pi install npm:@maxiaochao/pi-todo` | `pi update npm:@maxiaochao/pi-todo` |
25
+ | [pi-todo](packages/todo/README.md) | Branch-local execution plans, guarded advancement, `/todos` history | `pi install npm:@maxiaochao/pi-todo` | `pi update npm:@maxiaochao/pi-todo` |
26
26
  | [pi-cache-export](packages/cache-export/README.md) | Tau-aligned interactive cache dashboard for Pi sessions | `pi install npm:@maxiaochao/pi-cache-export` | `pi update npm:@maxiaochao/pi-cache-export` |
27
27
  | [pi-codex-edit](packages/codex-edit/README.md) | Codex-harness `apply_patch` routing for selected GPT models | `pi install npm:@maxiaochao/pi-codex-edit` | `pi update npm:@maxiaochao/pi-codex-edit` |
28
28
  | [pi-herdr-subagents](packages/herdr-subagents/README.md) | Interactive Herdr Pi Agents with bounded concurrency, direct navigation, and recovery | `pi install npm:@maxiaochao/pi-herdr-subagents` | `pi update npm:@maxiaochao/pi-herdr-subagents` |
package/global/AGENTS.md CHANGED
@@ -4,71 +4,54 @@ Behavioral guidelines to reduce common LLM coding mistakes. Project-specific ins
4
4
 
5
5
  ## 1. Think Before Coding
6
6
 
7
- **Surface tradeoffs. Name confusion instead of resolving it silently.**
7
+ - Surface material assumptions, interpretations, and tradeoffs.
8
+ - If unresolved ambiguity would materially change the result, ask. When no human is reachable, choose a defensible interpretation, state it, and proceed with verifiable criteria.
9
+ - Prefer the simpler approach. Name conflicting requirements rather than silently compromising between them.
8
10
 
9
- - State your material assumptions explicitly.
10
- - If multiple interpretations exist, present them rather than picking one silently.
11
- - If a simpler approach exists, say so. Push back when warranted.
12
- - If requirements conflict (durability vs latency, correctness vs speed), name the conflict explicitly. Never average conflicting requirements into something that satisfies neither.
13
- - When unresolved ambiguity would materially change the result, ask. When no human is reachable (headless, async, subagent), choose the interpretation you can defend, state it as an assumption, and proceed under Rule 4.
11
+ ## 2. Minimal Solution
14
12
 
15
- ## 2. Simplicity First
13
+ Understand the problem and relevant code flow before choosing the first rung that works:
16
14
 
17
- **YAGNI cuts what you don't need; KISS keeps what remains simple.**
15
+ 1. Skip functionality that is speculative or was not requested.
16
+ 2. Reuse an existing project abstraction when it encodes the required semantics.
17
+ 3. Otherwise use the standard library or a native platform feature.
18
+ 4. Otherwise use a direct dependency already declared by the project.
19
+ 5. Only then write the minimum custom code that works.
18
20
 
19
- - Build what was asked, for the callers that exist today.
20
- - Introduce an abstraction after repeated use reveals a stable concept.
21
+ - Introduce abstractions only after repeated use reveals a stable concept.
22
+ - Prefer boring, focused code; review long or deeply nested functions for splitting.
23
+ - Never simplify away explicit requirements, trust-boundary validation, data-loss protection, security, or accessibility.
21
24
  - For states excluded by enforced invariants, fail visibly instead of inventing fallback behavior.
22
- - Each function you write does one thing. If it runs past a couple of screens or nests past three levels, review whether splitting it improves clarity.
25
+ - State the ceiling and upgrade trigger of any deliberate simplification that cuts a real corner.
23
26
 
24
- ## 3. Surgical Changes
27
+ ## 3. Skill Boundaries
25
28
 
26
- **Touch only what you must. Clean up only your own mess.**
29
+ After loading a skill, treat its documented interface as sufficient for normal use. Do not inspect the skill's implementation, installed executable, or package source unless the user asks to review, debug, or modify it; its documented behavior conflicts with observed behavior; or required usage details are missing from the skill.
27
30
 
28
- The test: every changed line traces to the user's request, or to a defect that request reveals.
31
+ ## 4. Read, Then Change Surgically
29
32
 
30
- - Match existing style, even if you'd do it differently.
31
- - Remove imports, variables, and functions that YOUR changes made unused. Leave pre-existing dead code in place and mention it.
33
+ Before editing, read the affected public API and its immediate callers. For a bug, find every statically discoverable caller and relevant implementation.
32
34
 
33
- When fixing a bug, fix the root cause, not the symptom. Find where the defect actually lives:
34
- - Find every statically discoverable caller and relevant implementation; a report names a symptom, and patching only the path the ticket mentions leaves sibling callers still broken.
35
- - If the shared function is wrong, fix it once - do not scatter per-caller guards. Name the sibling callers the fix now affects.
36
- - If one caller is wrong, fix that caller - do not add defensive handling to a correct shared function.
35
+ Every changed line traces to the user's request or to a defect that request reveals.
37
36
 
38
- ## 4. Goal-Driven Execution
37
+ - Match existing style.
38
+ - Remove imports, variables, and functions that your changes make unused. Leave pre-existing dead code in place and mention it.
39
+ - Fix a faulty shared function once and name affected sibling callers; if only one caller is wrong, fix that caller instead of guarding a correct shared function.
39
40
 
40
- **Define success criteria. Loop until verified.**
41
+ ## 5. Goal-Driven Execution
41
42
 
42
- Transform tasks into verifiable goals:
43
- - "Add validation" -> "Write tests for invalid inputs, then make them pass"
44
- - "Fix the bug" -> "Write a test that reproduces it, then make it pass"
45
- - "Refactor X" -> "Ensure tests pass before and after"
46
- - "Make X fast" -> "Write and test a simple reference implementation, then optimize against its results"
43
+ - Turn work into checkable outcomes: reproduce bugs before fixing them, preserve behavior during refactors, and optimize against a tested reference implementation.
44
+ - For multi-step or materially ambiguous work, state a brief plan and give every step its own verification.
45
+ - Leave the smallest relevant runnable check for new or changed non-trivial logic. Do not introduce a test framework solely for that check.
47
46
 
48
- For multi-step or materially ambiguous tasks, state a brief plan where every step carries its own check:
49
- 1. [Step] -> verify: [check]
47
+ ## 6. Fail Visibly
50
48
 
51
- Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
52
-
53
- ## 5. Read Before You Write
54
-
55
- **Understand adjacent code before changing it. "Looks orthogonal" is a dangerous assumption.**
56
-
57
- Before changing a file:
58
- - Read the file's exports and public API.
59
- - Read the immediate callers of the function you're modifying.
60
-
61
- ## 6. Fail Visibly, Not Silently
62
-
63
- **Silent skips are failures.**
64
-
65
- - "Completed" is wrong if anything was silently skipped.
49
+ - "Completed" is wrong if anything required was silently skipped.
66
50
  - "Tests pass" is wrong if any required test was skipped or not run.
67
- - If a required input, file, or tool is missing (or a step is impossible under the stated constraints), do NOT fabricate it to make a check pass. Report what is missing and what you need.
51
+ - If a required input, file, or tool is missing, or a step is impossible under the stated constraints, report what is missing and what you need. Never fabricate it to make a check pass.
52
+
53
+ ## 7. Worktree Isolation
68
54
 
69
- ## 7. Reuse Before Writing
55
+ For a Git-repository task that coordinates substantial changes across multiple files or modules, or implements a feature intended for release, ask before editing whether to isolate it in a worktree. Skip the question when already in a linked worktree.
70
56
 
71
- **Before writing new code, check in order:**
72
- 1. Reuse an existing project abstraction when it encodes project semantics. (If it is itself the bug, fix it once - see Rule 3.)
73
- 2. Otherwise use the standard library or a direct dependency already declared by the project.
74
- 3. Only then write it yourself.
57
+ If the user agrees, read the `pi-worktree` skill and establish the worktree before changing code. When no human is reachable, create a worktree only when the task explicitly requests one.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.10.1",
4
- "description": "Personal pi coding agent toolkit: /todos, /btw, /cache_export and apply_patch extensions, browser automation, and bundled theme/skills.",
3
+ "version": "0.10.3",
4
+ "description": "Personal pi coding agent toolkit: execution plans, /btw, /cache_export and apply_patch extensions, browser automation, and bundled theme/skills.",
5
5
  "type": "module",
6
6
  "workspaces": [
7
7
  "packages/*"
@@ -1,3 +1,17 @@
1
+ ## 2026-10-08 - v0.4.0
2
+
3
+ - add the live compact Fleet footer with authoritative metrics and refreshed media
4
+ - Replace the below-editor task list with a compact responsive Fleet monitor showing authoritative persona, task status, cumulative active time, and true child Pi usage from assistant, tool-result, standalone usage, compaction, and branch-summary entries.
5
+ - Update Fleet metrics from child-session file events, repaint elapsed time only while work runs, safely ignore malformed or partial JSONL tails, and dispose every watcher and timer with the widget.
6
+ - Preserve exact-pane keyboard focus while extending blocked-response and historical-resume activity projections with durable timing and session metadata.
7
+ - Use warning/yellow indicators for running Fleet rows and success/green indicators for completed rows, compact the shared metrics column instead of pushing it to the terminal edge, and publish media that demonstrates the live parent footer while restoring focus after capture.
8
+
9
+ ## 2026-10-08 - v0.3.0
10
+
11
+ - split execution and control tools and unify task runs
12
+ - Split execution and control into `herdr_subagents` and `herdr_subagents_control`; require a non-empty `tasks` array for new runs so one-item runs and batches share background and concurrency semantics.
13
+ - Normalize legacy top-level single-task prompts into one-item task arrays before schema validation.
14
+
1
15
  ## 2026-10-08 - v0.2.1
2
16
 
3
17
  - add default thinking configuration and inline Herdr workflow video
@@ -2,16 +2,15 @@
2
2
 
3
3
  A Pi extension that runs focused foreground or background work as normal interactive Pi Agents in visible Herdr panes. Full answers and sessions stay in project-local records while the parent receives only concise summaries and document paths.
4
4
 
5
- ![A parent Pi conversation running three visible Herdr Subagents panes](docs/screenshot.png)
5
+ ![A parent Pi conversation showing the live Herdr Subagents Fleet footer](docs/screenshot.png)
6
6
 
7
7
  ## Demo
8
8
 
9
- The recording shows a real Pi conversation invoking a three-task read-only batch, Herdr opening one interactive Agent pane per task, and the compact results returning to the parent.
9
+ The recording shows a real Pi conversation invoking a three-task read-only batch, the active Fleet footer in the parent, Herdr opening one interactive Agent pane per task, and the compact results returning to the parent.
10
10
 
11
11
  https://github.com/user-attachments/assets/8ddf3ae6-a11a-492b-9cae-d9496ba65b67
12
12
 
13
- The video starts a three-agent batch, focuses each live child pane in turn, then returns
14
- to the parent summary. The reproducible source recording is retained as [`docs/demo.webm`](docs/demo.webm).
13
+ The video starts a three-agent batch, focuses each live child pane in turn, captures the parent Fleet footer with yellow running indicators and true usage, then returns to the parent summary. Completed Fleet rows use green indicators. The reproducible source recording is retained as [`docs/demo.webm`](docs/demo.webm).
15
14
 
16
15
  ## Requirements and install
17
16
 
@@ -29,7 +28,7 @@ pi update npm:@maxiaochao/pi-herdr-subagents
29
28
 
30
29
  The full `@maxiaochao/pi-toolkit` package also bundles this extension. Install the full toolkit or this standalone child package, not both.
31
30
 
32
- Inside a Herdr workspace, the package registers `herdr_subagents` and `/herdr-subagents`. Outside Herdr (or without a workspace ID), the extension registers neither, so the tool is absent from the model context. Start Pi inside Herdr or reload the extension there to enable them.
31
+ Inside a Herdr workspace, the package registers `herdr_subagents`, `herdr_subagents_control`, and `/herdr-subagents`. Outside Herdr (or without a workspace ID), the extension registers none of them, so these capabilities are absent from the model context. Start Pi inside Herdr or reload the extension there to enable them.
33
32
 
34
33
  The design replaces hidden print-mode subprocesses with ordinary interactive Pi Agents managed through Herdr, so delegated work stays visible, inspectable, resumable, and durably recorded.
35
34
 
@@ -38,16 +37,17 @@ The design replaces hidden print-mode subprocesses with ordinary interactive Pi
38
37
  Discover the effective personas first:
39
38
 
40
39
  ```text
41
- herdr_subagents({ action: "list" })
40
+ herdr_subagents_control({ action: "list" })
42
41
  ```
43
42
 
44
43
  Then run one self-contained task:
45
44
 
46
45
  ```text
47
46
  herdr_subagents({
48
- agent: "worker",
49
47
  label: "auth-review",
50
- prompt: "Inspect the authentication flow and summarize the risks"
48
+ tasks: [
49
+ { name: "auth", agent: "worker", prompt: "Inspect the authentication flow and summarize the risks" }
50
+ ]
51
51
  })
52
52
  ```
53
53
 
@@ -64,25 +64,27 @@ herdr_subagents({
64
64
  })
65
65
  ```
66
66
 
67
- Set `background: true` to return a stable run ID immediately while the queue continues. One run is dispatched at a time by an extension instance; later runs remain queued and start FIFO. Each background run sends one compact parent notification only when the whole run completes, partially fails, fails, or blocks. Individual successful tasks do not wake the parent model.
67
+ `tasks` is always a non-empty array: one item is a single task and multiple items form a batch. Set `background: true` for either form to return a stable run ID immediately while the queue continues. One run is dispatched at a time by an extension instance; later runs remain queued and start FIFO. Each background run sends one compact parent notification only when the whole run completes, partially fails, fails, or blocks. Individual successful tasks do not wake the parent model.
68
68
 
69
- A child missing required information reports `needs-input` with an exact question. The run becomes blocked, keeps its Herdr tab, pane, partial report, and Pi session, and returns control to the parent. Answer through the same tool to continue the original session:
69
+ A child missing required information reports `needs-input` with an exact question. The run becomes blocked, keeps its Herdr tab, pane, partial report, and Pi session, and returns control to the parent. Answer through the control tool to continue the original session:
70
70
 
71
71
  ```text
72
- herdr_subagents({ action: "respond", runId: "<run-id>", answer: "Use the main branch." })
72
+ herdr_subagents_control({ action: "respond", runId: "<run-id>", answer: "Use the main branch." })
73
73
  ```
74
74
 
75
- While a batch is active, its task rows stay below the editor. With an empty editor, press `↓` to enter the list, `↑`/`↓` to choose a task, `Enter` to focus its exact pane, and `Esc` to return. `/herdr-subagents focus <task-number>` remains available as a fallback.
75
+ While a batch is active, a compact Fleet monitor stays below the editor. Its divider/title row summarizes the run, and each task row shows a hollow or filled status circle, the real persona and task name, status, live active duration, and cumulative child Pi token usage in one compact shared metrics column after the longest row rather than at the terminal edge. Running work uses Pi's warning/yellow semantic color and completed work uses success/green. Token totals use the child session's persisted assistant, tool-result, standalone usage, compaction, and branch-summary usage; they are never estimated. Queued tasks show neither fabricated time nor tokens. The monitor follows Pi theme semantics, truncates by visible terminal columns, and updates session usage from file events while one lightweight repaint timer advances running durations.
76
+
77
+ With an empty editor, press `↓` to enter the list, `↑`/`↓` to choose a task, `Enter` to focus its exact pane, and `Esc` to return. `/herdr-subagents focus <task-number>` remains available as a fallback.
76
78
 
77
79
  ## History
78
80
 
79
- Run `/herdr-subagents history` or `herdr_subagents({ action: "history" })` to list compact project-local run and task reports after Herdr tabs close. Full results, described documents, artifacts, and persistent Pi sessions remain separate on disk and are not loaded into parent context.
81
+ Run `/herdr-subagents history` or `herdr_subagents_control({ action: "history" })` to list compact project-local run and task reports after Herdr tabs close. Full results, described documents, artifacts, and persistent Pi sessions remain separate on disk and are not loaded into parent context.
80
82
 
81
- Continue a saved conversation in a new Herdr tab with `herdr_subagents({ action: "resume", runId: "<run-id>", task: 1, prompt: "Follow up..." })`. Remove only a selected archive with `herdr_subagents({ action: "cleanup", runId: "<run-id>" })`. History has no automatic count-based eviction. The extension adds the runtime path to local Git excludes when available (and otherwise uses `.pi/.gitignore`) without overwriting unrelated rules.
83
+ Continue a saved conversation in a new Herdr tab with `herdr_subagents_control({ action: "resume", runId: "<run-id>", task: 1, prompt: "Follow up..." })`. Remove only a selected archive with `herdr_subagents_control({ action: "cleanup", runId: "<run-id>" })`. History has no automatic count-based eviction. The extension adds the runtime path to local Git excludes when available (and otherwise uses `.pi/.gitignore`) without overwriting unrelated rules.
82
84
 
83
85
  Read-only tasks start FIFO up to the configured concurrency limit. A pane is created only when its task starts, belongs exclusively to that task, and is never reused. Completed panes remain inspectable while another task in the batch is unsettled. Results remain in input order, and one failed task does not cancel its siblings. Any batch containing a write-capable persona is forced to concurrency one.
84
86
 
85
- The widget shows one row per queued, running, blocked, failed, or completed task. Answering a blocked task refreshes rows and pane IDs as that task resumes and queued siblings start. Completed rows remain selectable until the whole batch settles. `/herdr-subagents active` shows the same compact state. Native labels stay short: `SA · <batch>` for the tab and `<order> · <task>` for each pane. When all tasks settle, the tab closes immediately if unfocused or after the user leaves it; a deferred close failure is persisted and shown as a warning.
87
+ The widget shows one row per queued, running, blocked, failed, or completed task. Answering a blocked task or resuming saved history refreshes authoritative persona, timing, session, status, and pane projections as work advances. Active duration accumulates only while the task is running, excluding time spent queued or waiting for an answer. Completed rows remain selectable until the whole batch settles. `/herdr-subagents active` shows the same compact state. Native labels stay short: `SA · <batch>` for the tab and `<order> · <task>` for each pane. When all tasks settle, the tab closes immediately if unfocused or after the user leaves it; a deferred close failure is persisted and shown as a warning.
86
88
 
87
89
  Foreground calls block until all children report completion, failure, or a request for input. Every child is started with Herdr's Agent lifecycle as a normal Pi TUI and prompted with wait semantics; no print-mode command or completion marker is injected into the pane.
88
90
 
@@ -250,9 +252,9 @@ Override the model for one task:
250
252
 
251
253
  ```text
252
254
  herdr_subagents({
253
- agent: "reviewer",
254
- model: "anthropic/claude-opus-4-1:high",
255
- prompt: "Review the authorization boundary"
255
+ tasks: [
256
+ { name: "authorization", agent: "reviewer", model: "anthropic/claude-opus-4-1:high", prompt: "Review the authorization boundary" }
257
+ ]
256
258
  })
257
259
  ```
258
260
 
@@ -4,9 +4,9 @@
4
4
 
5
5
  1. `npm --prefix packages/herdr-subagents test` runs deterministic scheduler, recovery, and persona tests.
6
6
  2. `pi -ne -ns --no-session -e ./packages/herdr-subagents/extensions/herdr-subagents.ts` verifies extension loading.
7
- 3. From a visible X11 Herdr client with the GStreamer X11 and VP8 plugins installed, run `HERDR_MEDIA_WINDOW_ID=<window-id> node scripts/capture-package-media.mjs`. The Herdr capture must show a parent Pi conversation, a three-task run tab, each of the three real interactive Agent panes receiving focus in turn, and the final compact parent response.
7
+ 3. From a dedicated visible X11 Herdr client/window with the GStreamer X11 and VP8 plugins installed, run `HERDR_MEDIA_WINDOW_ID=<window-id> node scripts/capture-package-media.mjs`. Capture intentionally cycles through the live child panes, so avoid using that client/window for other work while recording; the script restores the previously focused pane afterward when it still exists. The Herdr capture must show a parent Pi conversation, a three-task run tab, each of the three real interactive Agent panes receiving focus in turn, the active parent Fleet footer with yellow running indicators and real metrics, and the final compact parent response. The published screenshot must be the parent Fleet view, while the full WebM preserves the parent → three focused children → Fleet → parent sequence.
8
8
  4. `npm pack ./packages/herdr-subagents --dry-run` verifies package contents.
9
- 5. Verify the README renders the GitHub attachment video directly and live Herdr UI media `docs/screenshot.png` and `docs/demo.webm` are present in the tarball; `docs/demo.gif` must be absent. Inspect the WebM metadata and play it through once to confirm the parent → three focused children → parent sequence.
9
+ 5. Verify the README renders the GitHub attachment video directly and live Herdr UI media `docs/screenshot.png` and `docs/demo.webm` are present in the tarball; `docs/demo.gif` must be absent. Inspect the WebM metadata and play it through once to confirm the parent → three focused children → Fleet → parent sequence.
10
10
 
11
11
  ## Cases
12
12
 
@@ -16,10 +16,12 @@
16
16
  - Force write-capable batches to serial execution while allowing bounded read-only parallelism.
17
17
  - Preserve compact reports, full results, Pi sessions, blocked questions, and historical follow-ups.
18
18
  - Recover queued/running work after interruption without retrying post-prompt side effects, including truncated reports and recovery errors.
19
- - Verify the flat public contract and injectable extension wiring, including conditional registration only inside Herdr, Fleet rendering, editor focus ownership, terminal-input passthrough, exact-pane navigation, and stable display while another background run queues.
19
+ - Verify the split public contracts (`herdr_subagents` with required `tasks`, plus `herdr_subagents_control` with required `action`) and injectable extension wiring, including conditional registration only inside Herdr, Fleet rendering, editor focus ownership, terminal-input passthrough, exact-pane navigation, and stable display while another background run queues.
20
+ - Verify Fleet divider/row formatting, warning/yellow running indicators, success/green completed indicators, persona and task projections, cumulative active duration across blocking/resume, exact Pi usage aggregation from assistant messages, tool results, standalone usage, compaction, and branch summaries, malformed/truncated JSONL tolerance, queued metric omission, and visible-column bounds at narrow widths.
21
+ - Verify live token updates are file-event driven, elapsed repainting exists only while work runs, and widget disposal closes watchers and timers.
20
22
  - Verify blocked-response live Fleet updates, authoritative stale-field replacement, parent-shutdown cancellation, cleanup notifications, atomic state replacement, non-retried uncertain startup operations, and stalled warnings.
21
23
  - Verify resumed batches retain newly allocated pane IDs and historical follow-ups exclude simultaneous session writers.
22
24
 
23
25
  ## Latest baseline
24
26
 
25
- All 136 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, and persona cases must pass. Release validation also requires extension loading and a real Herdr read-only batch smoke with distinct task panes and final tab cleanup. The latest smoke completed two concurrent tasks, retained its focused run tab through settlement, and closed it automatically after focus moved away.
27
+ All 159 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, metrics, responsive-formatting, and persona cases must pass. Release validation also requires extension loading and a real Herdr read-only batch smoke with distinct task panes and final tab cleanup. The latest smoke completed two concurrent tasks, retained its focused run tab through settlement, and closed it automatically after focus moved away.
@@ -9,10 +9,10 @@ import { RunDispatcher } from "../src/dispatcher.ts";
9
9
  import { historyText, listSubagentHistory } from "../src/history.ts";
10
10
  import { cancelQueuedRuns, loadQueuedRuns, persistQueuedRun, removeQueuedRun } from "../src/queued-runs.ts";
11
11
  import { cancelActiveSubagentRuns, reconcileSubagentRuns } from "../src/reconcile.ts";
12
- import { executeHerdrSubagents, type HerdrSubagentsBatchDetails, type HerdrSubagentsDetails, type HerdrSubagentsToolParams } from "../src/tool.ts";
13
- import { HerdrSubagentsParams } from "../src/parameters.ts";
12
+ import { executeHerdrSubagents, executeHerdrSubagentsControl, type HerdrSubagentsBatchDetails, type HerdrSubagentsControlToolParams, type HerdrSubagentsDetails, type HerdrSubagentsRunParams } from "../src/tool.ts";
13
+ import { HerdrSubagentsControlParams, HerdrSubagentsParams } from "../src/parameters.ts";
14
14
  import { HERDR_ACTIONS } from "../src/validation.ts";
15
- import { FleetController } from "../src/fleet-controller.ts";
15
+ import { FleetController, type FleetWidgetRuntime } from "../src/fleet-controller.ts";
16
16
  import { isSettledRunStatus } from "../src/records.ts";
17
17
 
18
18
  const packageAgentsDir = fileURLToPath(new URL("../agents/", import.meta.url));
@@ -25,18 +25,32 @@ function statusVisual(status: string): { color: "success" | "warning" | "error"
25
25
  return { color: "error", glyph: "✗" };
26
26
  }
27
27
 
28
- function renderToolCall(args: HerdrSubagentsToolParams, theme: Theme): Text {
29
- if (args.action) {
30
- const action = HERDR_ACTIONS[args.action];
31
- const runId = action.includeRunId ? ` ${theme.fg("muted", String(args.runId ?? ""))}` : "";
32
- return new Text(`${theme.fg("toolTitle", theme.bold(`herdr_subagents ${args.action}`))}${runId}`, 0, 0);
33
- }
34
- if (args.tasks) return new Text(`${theme.fg("toolTitle", theme.bold("herdr_subagents"))} ${theme.fg("accent", `${args.tasks.length} tasks`)}${theme.fg("muted", ` · concurrency ${args.concurrency ?? "default"}`)}`, 0, 0);
35
- const agent = args.agent || "worker";
36
- const label = args.label ? ` · ${String(args.label)}` : "";
37
- const prompt = args.prompt ? String(args.prompt).replace(/\s+/g, " ") : "...";
38
- const preview = prompt.length > 80 ? `${prompt.slice(0, 80)}...` : prompt;
39
- return new Text(`${theme.fg("toolTitle", theme.bold("herdr_subagents"))} ${theme.fg("accent", agent)}${theme.fg("muted", label)}\n ${theme.fg("dim", preview)}`, 0, 0);
28
+ function renderToolCall(args: HerdrSubagentsRunParams, theme: Theme): Text {
29
+ return new Text(`${theme.fg("toolTitle", theme.bold("herdr_subagents"))} ${theme.fg("accent", `${args.tasks.length} task${args.tasks.length === 1 ? "" : "s"}`)}${theme.fg("muted", ` · concurrency ${args.concurrency ?? "default"}`)}`, 0, 0);
30
+ }
31
+
32
+ function renderControlToolCall(args: HerdrSubagentsControlToolParams, theme: Theme): Text {
33
+ const action = HERDR_ACTIONS[args.action];
34
+ const runId = action.includeRunId ? ` ${theme.fg("muted", String(args.runId ?? ""))}` : "";
35
+ return new Text(`${theme.fg("toolTitle", theme.bold(`herdr_subagents_control ${args.action}`))}${runId}`, 0, 0);
36
+ }
37
+
38
+ export function normalizeHerdrSubagentsArguments(args: unknown): HerdrSubagentsRunParams {
39
+ if (!args || typeof args !== "object") return args as HerdrSubagentsRunParams;
40
+ const legacy = args as Record<string, unknown>;
41
+ if (Array.isArray(legacy.tasks) || typeof legacy.prompt !== "string" || legacy.action !== undefined) return args as HerdrSubagentsRunParams;
42
+ if (Object.keys(legacy).some((key) => !["prompt", "agent", "label", "model", "background"].includes(key))) return args as HerdrSubagentsRunParams;
43
+ const label = typeof legacy.label === "string" ? legacy.label : undefined;
44
+ return {
45
+ tasks: [{
46
+ name: label || "task",
47
+ prompt: legacy.prompt,
48
+ ...(typeof legacy.agent === "string" ? { agent: legacy.agent } : {}),
49
+ ...(typeof legacy.model === "string" ? { model: legacy.model } : {}),
50
+ }],
51
+ ...(label ? { label } : {}),
52
+ ...(typeof legacy.background === "boolean" ? { background: legacy.background } : {}),
53
+ };
40
54
  }
41
55
 
42
56
  function renderToolResult(result: AgentToolResult<unknown>, _options: ToolRenderResultOptions, theme: Theme): Container | Text {
@@ -49,20 +63,20 @@ function renderToolResult(result: AgentToolResult<unknown>, _options: ToolRender
49
63
  return container;
50
64
  }
51
65
  const details = result.details as HerdrSubagentsDetails | undefined;
52
- if (!details?.agent) {
53
- const content = result.content[0];
54
- return new Text(content?.type === "text" ? content.text : "(no output)", 0, 0);
55
- }
56
- const container = new Container();
57
- container.addChild(new Text(`${theme.fg("toolTitle", theme.bold(details.agent))} ${theme.fg(statusVisual(details.status).color, details.status)}`, 0, 0));
58
- container.addChild(new Text(details.summary, 0, 0));
59
- if (details.documents.length > 0) {
60
- container.addChild(new Spacer(1));
61
- for (const document of details.documents) container.addChild(new Text(theme.fg("muted", `${document.description}: ${document.path}`), 0, 0));
66
+ if (details?.agent) {
67
+ const container = new Container();
68
+ container.addChild(new Text(`${theme.fg("toolTitle", theme.bold(details.agent))} ${theme.fg(statusVisual(details.status).color, details.status)}`, 0, 0));
69
+ container.addChild(new Text(details.summary, 0, 0));
70
+ if (details.documents.length > 0) {
71
+ container.addChild(new Spacer(1));
72
+ for (const document of details.documents) container.addChild(new Text(theme.fg("muted", `${document.description}: ${document.path}`), 0, 0));
73
+ }
74
+ if (details.recordDirectory) container.addChild(new Text(theme.fg("dim", `Record: ${details.recordDirectory}`), 0, 0));
75
+ if (details.errorMessage) container.addChild(new Text(theme.fg("error", `Error: ${details.errorMessage}`), 0, 0));
76
+ return container;
62
77
  }
63
- if (details.recordDirectory) container.addChild(new Text(theme.fg("dim", `Record: ${details.recordDirectory}`), 0, 0));
64
- if (details.errorMessage) container.addChild(new Text(theme.fg("error", `Error: ${details.errorMessage}`), 0, 0));
65
- return container;
78
+ const content = result.content[0];
79
+ return new Text(content?.type === "text" ? content.text : "(no output)", 0, 0);
66
80
  }
67
81
 
68
82
  async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fleet: FleetController): Promise<void> {
@@ -104,8 +118,10 @@ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fl
104
118
  export interface HerdrSubagentsExtensionDependencies {
105
119
  herdr?: HerdrAutomation;
106
120
  execute?: typeof executeHerdrSubagents;
121
+ executeControl?: typeof executeHerdrSubagentsControl;
107
122
  isEditor?: (value: unknown) => boolean;
108
123
  createFleetWidget?: (lines: string[]) => Container;
124
+ fleetWidgetRuntime?: FleetWidgetRuntime;
109
125
  }
110
126
 
111
127
  async function recoverParentSession(cwd: string, herdr: HerdrAutomation, owner: { sessionId?: string; processId: number }, notify: (message: string) => void): Promise<void> {
@@ -139,9 +155,10 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
139
155
  if (process.env.PI_HERDR_SUBAGENTS_CHILD === "1" || process.env.HERDR_ENV !== "1" || !process.env.HERDR_WORKSPACE_ID) return;
140
156
  const herdr = dependencies.herdr ?? new CliHerdrAutomation();
141
157
  const execute = dependencies.execute ?? executeHerdrSubagents;
158
+ const executeControl = dependencies.executeControl ?? executeHerdrSubagentsControl;
142
159
  const isEditorComponent = dependencies.isEditor ?? ((value: unknown) => value instanceof Editor);
143
160
  const dispatcher = new RunDispatcher();
144
- const fleet = new FleetController(herdr, isEditorComponent, dependencies.createFleetWidget);
161
+ const fleet = new FleetController(herdr, isEditorComponent, dependencies.createFleetWidget, dependencies.fleetWidgetRuntime);
145
162
  const runControllers = new Map<string, AbortController>();
146
163
  const inFlight = new Set<Promise<unknown>>();
147
164
  let sessionController = new AbortController();
@@ -177,17 +194,17 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
177
194
  pi.registerTool({
178
195
  name: "herdr_subagents",
179
196
  label: "Herdr Subagents",
180
- description: "Run one focused task or an ordered bounded-concurrency batch in visible interactive Herdr Pi Agents. Foreground calls block while workers run concurrently; background batches return a run ID immediately and publish completion later. Complete results and Pi sessions stay in project-local records.",
197
+ description: "Run one or more ordered tasks in visible interactive Herdr Pi Agents. Use one tasks item for a single task. Independent read-only tasks may run concurrently; write-capable runs are serialized. Foreground calls block, while background calls return a run ID immediately. Complete results and Pi sessions stay in project-local records.",
181
198
  promptSnippet: "Run focused work in visible Herdr subagents",
182
- promptGuidelines: ["Call action=list once before the first execution. Submit one prompt or an ordered tasks list; use concurrency only for independent read-only tasks. Foreground calls block until the run settles; background calls return a run ID immediately."],
199
+ promptGuidelines: ["Call herdr_subagents_control with action=list once before the first execution. Submit an ordered tasks list; use one item for a single task and concurrency only for independent read-only tasks. Foreground calls block until the run settles; background calls return a run ID immediately."],
183
200
  parameters: HerdrSubagentsParams,
201
+ prepareArguments: normalizeHerdrSubagentsArguments,
184
202
 
185
- async execute(_toolCallId, params: HerdrSubagentsToolParams, signal, onUpdate, ctx) {
203
+ async execute(_toolCallId, params: HerdrSubagentsRunParams, signal, onUpdate, ctx) {
186
204
  const generation = sessionGeneration;
187
- const runId = params.tasks ? randomUUID() : undefined;
188
- const executionId = runId ?? params.runId ?? randomUUID();
205
+ const runId = randomUUID();
189
206
  const controller = new AbortController();
190
- runControllers.set(executionId, controller);
207
+ runControllers.set(runId, controller);
191
208
  const runSignal = AbortSignal.any([controller.signal, sessionController.signal, ...(!params.background && signal ? [signal] : [])]);
192
209
  const executeRun = async () => {
193
210
  const result = await execute(params, runSignal, (update) => {
@@ -201,21 +218,6 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
201
218
  fleet.sync(result.details);
202
219
  return result;
203
220
  };
204
- if (!params.tasks) {
205
- const promise = executeRun();
206
- inFlight.add(promise);
207
- const result = await promise.finally(() => { inFlight.delete(promise); runControllers.delete(executionId); });
208
- if (params.action === "respond") {
209
- const details = result.details as { runId?: string; status?: string };
210
- if (details.status === "blocked") fleet.sync(result.details);
211
- else if (isSettledRunStatus(details.status)) {
212
- fleet.clearRun(params.runId);
213
- if (params.runId) dispatcher.release(params.runId);
214
- }
215
- }
216
- return result;
217
- }
218
- if (!runId) throw new Error("Batch run ID was not allocated.");
219
221
  if (params.background) await persistQueuedRun(ctx.cwd, runId, params, { sessionId: ownerSessionId, processId: process.pid });
220
222
  const promise = dispatcher.submit(runId, async () => {
221
223
  const result = await executeRun().finally(async () => { if (params.background) await removeQueuedRun(ctx.cwd, runId); });
@@ -227,7 +229,7 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
227
229
  return result;
228
230
  });
229
231
  inFlight.add(promise);
230
- void promise.finally(() => { inFlight.delete(promise); runControllers.delete(executionId); }).catch(() => undefined);
232
+ void promise.finally(() => { inFlight.delete(promise); runControllers.delete(runId); }).catch(() => undefined);
231
233
  if (params.background) {
232
234
  void promise.then((result) => {
233
235
  if (generation !== sessionGeneration) return;
@@ -248,6 +250,44 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
248
250
  renderResult: renderToolResult,
249
251
  });
250
252
 
253
+ pi.registerTool({
254
+ name: "herdr_subagents_control",
255
+ label: "Herdr Subagents Control",
256
+ description: "Inspect Herdr personas and run history, answer a blocked task, resume a saved task, or remove a selected archived run. Use herdr_subagents to start new work.",
257
+ promptSnippet: "Inspect and control Herdr subagent runs",
258
+ parameters: HerdrSubagentsControlParams,
259
+
260
+ async execute(_toolCallId, params: HerdrSubagentsControlToolParams, signal, onUpdate, ctx) {
261
+ const executionId = params.runId ?? randomUUID();
262
+ const controller = new AbortController();
263
+ runControllers.set(executionId, controller);
264
+ const runSignal = AbortSignal.any([controller.signal, sessionController.signal, ...(signal ? [signal] : [])]);
265
+ const promise = executeControl(params, runSignal, (update) => {
266
+ const details = update.details as HerdrSubagentsBatchDetails | undefined;
267
+ if (details?.activity) fleet.sync(details);
268
+ const updateText = update.content.find((item) => item.type === "text")?.text;
269
+ const updateDetails = update.details as { warning?: string };
270
+ if (updateDetails.warning || updateText?.includes("cleanup failed")) ctx.ui.notify(updateDetails.warning ?? updateText ?? "Subagent warning", "warning");
271
+ onUpdate?.(update);
272
+ }, ctx, { agentsDirectory: packageAgentsDir, owner: { sessionId: ownerSessionId, processId: process.pid } });
273
+ inFlight.add(promise);
274
+ const result = await promise.finally(() => { inFlight.delete(promise); runControllers.delete(executionId); });
275
+ fleet.sync(result.details);
276
+ if (params.action === "respond") {
277
+ const details = result.details as { status?: string };
278
+ if (details.status === "blocked") fleet.sync(result.details);
279
+ else if (isSettledRunStatus(details.status)) {
280
+ fleet.clearRun(params.runId);
281
+ if (params.runId) dispatcher.release(params.runId);
282
+ }
283
+ }
284
+ return result;
285
+ },
286
+
287
+ renderCall: renderControlToolCall,
288
+ renderResult: renderToolResult,
289
+ });
290
+
251
291
  pi.registerCommand("herdr-subagents", {
252
292
  description: "Inspect effective subagent agents, models, and settings",
253
293
  getArgumentCompletions: (prefix) => ["active", "history", "agents", "models", "settings", "focus "].filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-herdr-subagents",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Run inspectable interactive Pi subagent batches through Herdr.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -10,7 +10,7 @@ import { writeJsonAtomic } from "./state.ts";
10
10
  import type { OwnerIdentity } from "./ownership.ts";
11
11
  import { ownerRecord } from "./ownership.ts";
12
12
  import { RunPaneAllocator } from "./run-pane-allocator.ts";
13
- import { applyReport, applyTaskStatus, settleAttemptFailure, type PersistedReport, type RunRecord, type TaskRecord, type TaskStatus } from "./records.ts";
13
+ import { applyReport, applyTaskStatus, projectTaskActivity, settleAttemptFailure, type PersistedReport, type RunRecord, type TaskActivity, type TaskRecord, type TaskStatus } from "./records.ts";
14
14
  import { reportProtocolPrompt } from "./protocol.ts";
15
15
  import { cleanupRunTab } from "./tab-cleanup.ts";
16
16
  import { applyRunStatus } from "./run-status.ts";
@@ -63,7 +63,7 @@ export interface BatchResult {
63
63
  requestedConcurrency: number;
64
64
  effectiveConcurrency: number;
65
65
  tasks: BatchTaskResult[];
66
- activity: Array<{ index: number; name: string; status: BatchTaskStatus; paneId?: string }>;
66
+ activity: TaskActivity[];
67
67
  summary: string;
68
68
  documents: HerdrSubagentsDocument[];
69
69
  recordDirectory: string;
@@ -114,7 +114,7 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
114
114
  await writeJsonAtomic(runFile, runRecord);
115
115
  const herdr = options.herdr ?? new CliHerdrAutomation();
116
116
  const results = new Array<BatchTaskResult>(tasks.length);
117
- const activity: BatchResult["activity"] = tasks.map((task, index) => ({ index, name: task.name, status: "queued" }));
117
+ const activity: BatchResult["activity"] = contexts.map((context, index) => projectTaskActivity(context.taskRecord, index));
118
118
  let tabId: string | undefined;
119
119
  const paneAllocator = new RunPaneAllocator({ herdr, run: runRecord, runFile, signal: options.signal, onCleanupError: options.onCleanupError });
120
120
  const emit = (status: BatchResult["status"], summary: string) => options.onUpdate?.({
@@ -146,7 +146,7 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
146
146
  applyTaskStatus(context.taskRecord, "running");
147
147
  Object.assign(context.taskRecord, { tabId, paneId, paneLabel, agentName, startedAt: timestamp() });
148
148
  await writeJsonAtomic(context.taskFile, context.taskRecord);
149
- activity[index] = { index, name: task.name, status: "running", paneId };
149
+ activity[index] = projectTaskActivity(context.taskRecord, index);
150
150
  emit("running", `${settled}/${tasks.length} tasks settled.`);
151
151
  const prompted = await promptLiveAgent({ herdr, task: { ...task, id: context.taskId, sessionFile: context.sessionFile }, paneId, prompt: task.prompt, systemPromptFile: context.systemPromptFile, agentName, signal: options.signal, stalledWarningMs: options.stalledWarningSeconds ? options.stalledWarningSeconds * 1000 : undefined, onStalled: () => emit("running", `${task.name} appears stalled; it is still running.`) });
152
152
  agentName = prompted.agentName;
@@ -164,7 +164,7 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
164
164
  Object.assign(context.taskRecord, { paneId, agentName });
165
165
  await writeJsonAtomic(context.taskFile, context.taskRecord);
166
166
  results[index] = { index, name: task.name, status, summary: report.summary, documents: report.documents, ...(report.error ? { error: report.error } : {}), ...(report.question ? { question: report.question } : {}), paneId, recordDirectory: context.taskDirectory, sessionFile: context.sessionFile };
167
- activity[index] = { index, name: task.name, status, ...(paneId ? { paneId } : {}) };
167
+ activity[index] = projectTaskActivity(context.taskRecord, index);
168
168
  settled += 1;
169
169
  emit("running", `${settled}/${tasks.length} tasks settled.`);
170
170
  };