@maxiaochao/pi-toolkit 0.9.6 → 0.10.0

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 (53) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/package.json +5 -5
  4. package/packages/herdr-subagents/CHANGELOG.md +41 -0
  5. package/packages/herdr-subagents/README.md +155 -0
  6. package/packages/herdr-subagents/TESTING.md +24 -0
  7. package/packages/herdr-subagents/agents/explorer.md +8 -0
  8. package/packages/herdr-subagents/agents/reviewer.md +8 -0
  9. package/packages/{tiny-subagent → herdr-subagents}/agents/worker.md +1 -0
  10. package/packages/herdr-subagents/docs/demo.gif +0 -0
  11. package/packages/herdr-subagents/docs/screenshot.png +0 -0
  12. package/packages/herdr-subagents/extensions/herdr-subagents.ts +258 -0
  13. package/packages/herdr-subagents/extensions/subagent-report.ts +65 -0
  14. package/packages/{tiny-subagent → herdr-subagents}/package.json +7 -6
  15. package/packages/herdr-subagents/src/batch-runner.ts +219 -0
  16. package/packages/herdr-subagents/src/blocking.ts +175 -0
  17. package/packages/herdr-subagents/src/config.ts +125 -0
  18. package/packages/herdr-subagents/src/continue-run.ts +87 -0
  19. package/packages/herdr-subagents/src/dispatcher.ts +73 -0
  20. package/packages/herdr-subagents/src/errors.ts +3 -0
  21. package/packages/herdr-subagents/src/fleet-controller.ts +82 -0
  22. package/packages/herdr-subagents/src/fleet.ts +56 -0
  23. package/packages/herdr-subagents/src/herdr.ts +224 -0
  24. package/packages/herdr-subagents/src/history.ts +210 -0
  25. package/packages/herdr-subagents/src/live-agent.ts +87 -0
  26. package/packages/herdr-subagents/src/monitor.ts +18 -0
  27. package/packages/herdr-subagents/src/ownership.ts +17 -0
  28. package/packages/herdr-subagents/src/parameters.ts +21 -0
  29. package/packages/{tiny-subagent → herdr-subagents}/src/personas.ts +13 -2
  30. package/packages/herdr-subagents/src/protocol.ts +23 -0
  31. package/packages/herdr-subagents/src/queued-runs.ts +70 -0
  32. package/packages/herdr-subagents/src/reconcile.ts +167 -0
  33. package/packages/herdr-subagents/src/records.ts +213 -0
  34. package/packages/herdr-subagents/src/result-path.ts +7 -0
  35. package/packages/herdr-subagents/src/run-locator.ts +19 -0
  36. package/packages/herdr-subagents/src/run-pane-allocator.ts +78 -0
  37. package/packages/herdr-subagents/src/run-status.ts +23 -0
  38. package/packages/herdr-subagents/src/runner.ts +250 -0
  39. package/packages/herdr-subagents/src/state.ts +11 -0
  40. package/packages/herdr-subagents/src/tab-cleanup.ts +68 -0
  41. package/packages/herdr-subagents/src/tool.ts +217 -0
  42. package/packages/herdr-subagents/src/validation.ts +38 -0
  43. package/packages/herdr-subagents/tests/extension-contract.ts +225 -0
  44. package/packages/herdr-subagents/tests/run-tests.mjs +889 -0
  45. package/packages/todo/extensions/todo.ts +1 -1
  46. package/packages/tiny-subagent/CHANGELOG.md +0 -12
  47. package/packages/tiny-subagent/README.md +0 -128
  48. package/packages/tiny-subagent/TESTING.md +0 -20
  49. package/packages/tiny-subagent/docs/demo.gif +0 -0
  50. package/packages/tiny-subagent/docs/screenshot.png +0 -0
  51. package/packages/tiny-subagent/extensions/tiny-subagent.ts +0 -228
  52. package/packages/tiny-subagent/src/runner.ts +0 -319
  53. package/packages/tiny-subagent/tests/run-tests.mjs +0 -111
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 2026-10-08 - v0.10.0
2
+
3
+ - bundle herdr-subagents v0.2.0: replace Tiny Subagent with visible interactive Herdr agents and durable batch workflows
4
+ - replace Tiny Subagent's hidden print/shell execution with Herdr Subagents: visible interactive Pi Agents, one exclusive on-demand pane per task, Fleet keyboard navigation, durable blocked/history continuation, parent-session cancellation, and focus-safe batch tab cleanup
5
+ - fix Herdr Subagents Fleet state after blocked responses, preserve the active task widget while later background runs queue, and share ownership, pane-allocation, and status-reduction logic across recovery paths
6
+
1
7
  ## 2026-10-08 - v0.9.6
2
8
 
3
9
  - bundle codex-edit v0.1.9: install the pinned Pi runtime for clean-runner publication tests
package/README.md CHANGED
@@ -25,7 +25,7 @@ The root package embeds the child-package source snapshots listed below. Install
25
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` |
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
- | [pi-tiny-subagent](packages/tiny-subagent/README.md) | One focused task in a fresh child Pi context | `pi install npm:@maxiaochao/pi-tiny-subagent` | `pi update npm:@maxiaochao/pi-tiny-subagent` |
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` |
29
29
  | [pi-worktree](packages/worktree/README.md) | Worktree CLI, completion, installer, and agent skill | `pi install npm:@maxiaochao/pi-worktree` | `pi update npm:@maxiaochao/pi-worktree` |
30
30
 
31
31
  Each child README owns its feature description, motivation or upstream alignment, examples, screenshots/demos, configuration, and test documentation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.9.6",
3
+ "version": "0.10.0",
4
4
  "description": "Personal pi coding agent toolkit: /todos, /btw, /cache_export and apply_patch extensions, browser automation, and bundled theme/skills.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -15,7 +15,7 @@
15
15
  "packages/todo",
16
16
  "packages/cache-export",
17
17
  "packages/codex-edit",
18
- "packages/tiny-subagent",
18
+ "packages/herdr-subagents",
19
19
  "packages/worktree",
20
20
  "themes",
21
21
  "README.md",
@@ -34,7 +34,7 @@
34
34
  "./packages/cache-export/extensions/cache-export.ts",
35
35
  "./extensions/btw/index.ts",
36
36
  "./packages/codex-edit/extensions/codex-edit.ts",
37
- "./packages/tiny-subagent/extensions/tiny-subagent.ts"
37
+ "./packages/herdr-subagents/extensions/herdr-subagents.ts"
38
38
  ],
39
39
  "skills": [
40
40
  "./skills",
@@ -57,8 +57,8 @@
57
57
  "ws": "^8.19.0"
58
58
  },
59
59
  "scripts": {
60
- "test": "node tests/package-layout.mjs && npm --prefix packages/todo test && npm --prefix packages/cache-export test && npm --prefix packages/codex-edit test && npm --prefix packages/tiny-subagent test && npm --prefix packages/worktree test && node --experimental-strip-types extensions/btw/tests/history.mjs",
61
- "load-test": "pi -ne -e ./packages/todo/extensions/todo.ts && pi -ne -e ./packages/cache-export/extensions/cache-export.ts && pi -ne -e ./extensions/btw/index.ts && pi -ne -e ./packages/codex-edit/extensions/codex-edit.ts && pi -ne -e ./packages/tiny-subagent/extensions/tiny-subagent.ts",
60
+ "test": "node tests/package-layout.mjs && npm --prefix packages/todo test && npm --prefix packages/cache-export test && npm --prefix packages/codex-edit test && npm --prefix packages/herdr-subagents test && npm --prefix packages/worktree test && node --experimental-strip-types extensions/btw/tests/history.mjs",
61
+ "load-test": "pi -ne -e ./packages/todo/extensions/todo.ts && pi -ne -e ./packages/cache-export/extensions/cache-export.ts && pi -ne -e ./extensions/btw/index.ts && pi -ne -e ./packages/codex-edit/extensions/codex-edit.ts && pi -ne -e ./packages/herdr-subagents/extensions/herdr-subagents.ts",
62
62
  "eval:agents": "node evaluations/agents-md/run.mjs",
63
63
  "eval:agents:test": "node evaluations/agents-md/tests/run-tests.mjs",
64
64
  "postinstall": "node scripts/install-agents.mjs"
@@ -0,0 +1,41 @@
1
+ ## 2026-10-08 - v0.2.0
2
+
3
+ - replace Tiny Subagent with visible interactive Herdr agents and durable batch workflows
4
+ - Skip tool and command registration outside a Herdr workspace so unavailable Subagent capabilities do not enter the model context.
5
+ - Preserve newly allocated panes when blocked runs resume, stop blindly retrying Herdr resource-creating commands after uncertain failures, and prevent concurrent historical resumes from writing the same Pi session.
6
+ - Isolate malformed queue/history entries so healthy active runs and durable history remain recoverable, and share durable attempt settlement plus task projection contracts across execution modes.
7
+ - Retain blocked Fleet and dispatcher ownership after stateless response errors, and include public test entrypoints in the published package.
8
+ - Preserve every pending stale-tab cleanup across repeated pane migrations, retry all of them during startup reconciliation, and surface allocator cleanup failures through batch updates.
9
+ - Keep persisted reports authoritative even when Herdr transport fails after submission, persist failure reports for historical follow-ups, reject resume attempts against active or cleanup-pending runs, recover terminal deferred cleanup on startup, preserve cancelled ownership on shutdown, keep recovered Agent attempts aligned with durable turns, archive malformed turns, remove rejected background queue entries, support Pi enhanced keyboard sequences and authoritative status rendering, centralize lifecycle metadata, surface control-plane and cleanup failures, merge deferred cleanup against current durable state, and force-close focused tabs when parent shutdown is already active.
10
+ - Share strict report parsing across live and recovered paths, validate durable task ordering, surface unreadable queue/history records, reconcile prior sessions even within one Pi process, keep settled Fleet rows navigable, preserve surviving sibling panes during dead-pane recovery, and retain cleanup diagnostics.
11
+ - Keep queued and blocked siblings alive when one blocked response fails, centralize authoritative run-status transitions, and bind focus-deferred tab cleanup to parent-session shutdown.
12
+ - Retain dispatcher and Fleet ownership while a batch is blocked, centralize task/turn transitions, surface transient pane lookup failures, and notify deferred single-task cleanup failures.
13
+ - Fail reconciliation when task records are unreadable, archive malformed child reports before replacing them with valid failure records, and keep durable history and saved-session follow-up usable.
14
+ - Keep startup reconciliation moving past truncated child reports, resume dispatch after recovery errors, clear stale blocked fields after authoritative follow-ups, and surface deferred cleanup failures.
15
+ - Stream Fleet row and pane updates while a blocked answer resumes work and releases queued siblings.
16
+ - Keep the active Fleet widget stable when another background run is queued, refresh rows and pane navigation after blocked responses, and cover the public extension UI/input wiring with injectable dependencies.
17
+ - Centralize parent ownership, run-status aggregation, and serialized pane allocation across initial, continued, recovered, and historical runs.
18
+ - Run every initial task, blocked response, recovered task, and historical follow-up as a visible interactive Pi Agent through Herdr Agent start/prompt; remove print mode, pane shell injection, completion markers, and marker polling.
19
+ - Give every started task a new on-demand pane, retain completed panes until the batch settles, and then close the whole tab with focus-safe cleanup.
20
+ - Add a flat fully described tool schema with runtime mode validation and a Fleet-style task list with direct keyboard pane navigation.
21
+ - Continue historical sessions as durable turns of the same logical task, and cancel/archive unfinished work when the parent Pi session closes.
22
+ - Persist and reconcile queued/running work across parent shutdown, avoid retrying uncertain startup operations, preserve post-submit failures, warn without timing out stalled children, and atomically replace state snapshots.
23
+ - Add compact project-local history, saved-session follow-ups in new Herdr tabs, explicit per-run cleanup, unlimited retention, and generated-state Git exclusion.
24
+ - Support structured `needs-input` reports, preserve blocked panes and sessions, and resume the original child with `action: respond`.
25
+ - Queue background batches behind one active run, return stable IDs immediately, keep progress visible, and send one compact run-level parent notification.
26
+ - Show active run counts in a persistent widget, expose compact `/herdr-subagents active` and pane focus navigation, update progress labels, and defer cleanup while a completed tab is focused.
27
+ - Execute ordered read-only batches with FIFO bounded concurrency, stable result order, and sibling failure isolation; force write-capable runs to serial execution.
28
+ - Add built-in worker, explorer, and reviewer personas with project/global/package discovery precedence.
29
+ - Add merged global and project model, thinking, skill, and concurrency settings plus `/herdr-subagents` discovery commands.
30
+ - Run one synchronous subagent in a dedicated, unfocused Herdr tab and close it after completion.
31
+ - Require a structured child report and return only its summary and document paths to the parent.
32
+ - Persist run/task metadata, the full result, and the child Pi session under `.pi/herdr-subagents/runs`.
33
+
34
+ ## 2026-09-30 - v0.1.1
35
+
36
+ - Add standalone testing documentation and production-runner screenshot/demo media.
37
+ - Align the package manifest and root bundle with the shared workspace source.
38
+
39
+ ## 2026-09-01 - v0.1.0
40
+
41
+ - initial release
@@ -0,0 +1,155 @@
1
+ # @maxiaochao/pi-herdr-subagents
2
+
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
+
5
+ ![Herdr Subagents production runner screenshot](docs/screenshot.png)
6
+
7
+ ## Demo
8
+
9
+ ![Herdr Subagents demo](docs/demo.gif)
10
+
11
+ ## Requirements and install
12
+
13
+ Run Pi inside Herdr, then install the package:
14
+
15
+ ```bash
16
+ pi install npm:@maxiaochao/pi-herdr-subagents
17
+ ```
18
+
19
+ Update an existing standalone installation with:
20
+
21
+ ```bash
22
+ pi update npm:@maxiaochao/pi-herdr-subagents
23
+ ```
24
+
25
+ The full `@maxiaochao/pi-toolkit` package also bundles this extension. Install the full toolkit or this standalone child package, not both.
26
+
27
+ 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.
28
+
29
+ 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.
30
+
31
+ ## Usage
32
+
33
+ Discover the effective personas first:
34
+
35
+ ```text
36
+ herdr_subagents({ action: "list" })
37
+ ```
38
+
39
+ Then run one self-contained task:
40
+
41
+ ```text
42
+ herdr_subagents({
43
+ agent: "worker",
44
+ label: "auth-review",
45
+ prompt: "Inspect the authentication flow and summarize the risks"
46
+ })
47
+ ```
48
+
49
+ Or submit an ordered foreground batch:
50
+
51
+ ```text
52
+ herdr_subagents({
53
+ label: "security-review",
54
+ concurrency: 3,
55
+ tasks: [
56
+ { name: "auth", agent: "reviewer", prompt: "Review authentication" },
57
+ { name: "sessions", agent: "explorer", prompt: "Trace session storage" }
58
+ ]
59
+ })
60
+ ```
61
+
62
+ 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.
63
+
64
+ 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:
65
+
66
+ ```text
67
+ herdr_subagents({ action: "respond", runId: "<run-id>", answer: "Use the main branch." })
68
+ ```
69
+
70
+ 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.
71
+
72
+ ## History
73
+
74
+ 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.
75
+
76
+ 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.
77
+
78
+ 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.
79
+
80
+ 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.
81
+
82
+ 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.
83
+
84
+ ## Reports and records
85
+
86
+ The child must finish through the package's structured `subagent_report` tool. The parent receives only:
87
+
88
+ - completion or failure status
89
+ - a concise summary
90
+ - paths to useful documents
91
+ - the task-record location
92
+
93
+ The complete result is not copied into the parent model context. Each run is retained under:
94
+
95
+ ```text
96
+ .pi/herdr-subagents/runs/<run>/tasks/<task>/
97
+ ```
98
+
99
+ The task directory contains the full result, structured report, task metadata, system prompt, and persistent Pi session. Run-level metadata is stored in the parent run directory.
100
+
101
+ ## Personas
102
+
103
+ The package includes `worker` (write access), `explorer` (read access), and `reviewer` (read access). Persona definitions are discovered in this order, with the first available higher-precedence definition replacing the lower one:
104
+
105
+ 1. `.pi/herdr-subagents/agents/*.md` in the project
106
+ 2. `~/.pi/agent/herdr-subagents/agents/*.md` globally
107
+ 3. this package's `agents/*.md`
108
+
109
+ A custom persona without `access` is treated as write-capable. Definitions may set `access`, `model`, `thinking`, `tools`, and `skills` in frontmatter.
110
+
111
+ The default `agents/worker.md` uses this format:
112
+
113
+ ```md
114
+ ---
115
+ name: worker
116
+ description: General-purpose worker for one focused task
117
+ tools: read, bash, edit, write, grep, find, ls
118
+ ---
119
+
120
+ You are a focused worker subagent.
121
+ ```
122
+
123
+ Supported frontmatter:
124
+
125
+ - `name` and `description` are required.
126
+ - `model` optionally selects a child model. A call-level `model` overrides it; otherwise the current session model is inherited.
127
+ - `tools` optionally restricts built-in tools. `subagent_report` is always added so the child can settle the task.
128
+ - `access` is `read` or `write`; omitted custom values default to `write`.
129
+ - `thinking` and `skills` provide persona defaults.
130
+
131
+ Global settings live at `~/.pi/agent/herdr-subagents.json`; project overrides live at `.pi/herdr-subagents.json`. Both support `defaultConcurrency`, `maxConcurrency`, `stalledWarningSeconds`, `defaultModel`, and a `personas` object whose entries can override `model`, `thinking`, and `skills`.
132
+
133
+ ```json
134
+ {
135
+ "defaultModel": "provider/model",
136
+ "maxConcurrency": 3,
137
+ "personas": {
138
+ "reviewer": { "thinking": "high", "skills": ["code-review"] }
139
+ }
140
+ }
141
+ ```
142
+
143
+ Project values override global values recursively. Model resolution is: task override, project persona setting, global persona setting, persona frontmatter, global/project default model, then parent model. Run `/herdr-subagents agents`, `/herdr-subagents models`, or `/herdr-subagents settings` to inspect effective values and their sources.
144
+
145
+ ## Recovery and failures
146
+
147
+ Queued background submissions are persisted before dispatch. Closing the parent Pi session cancels queued, running, and blocked work, records `cancelled`, and closes the owned run tab. On the next Pi start, stale active records and queued submissions from a prior parent session are reconciled to a terminal state, including when Pi reused the same process, and leaked tabs are cleaned up when possible. Missing, truncated, or structurally invalid child reports become explicit failures or cancellation diagnostics; recovery errors are reported without leaving new submissions paused.
148
+
149
+ Tab, pane, and child startup commands are not automatically retried: an error may arrive after Herdr already created the resource. A failure after prompt submission is preserved and never automatically rerun. Historical follow-ups exclude concurrent resumes of the same run with an atomic `.resume.lock` directory; if Pi crashes without releasing it, confirm no Agent is still using that run before removing the lock manually. `stalledWarningSeconds` emits a warning without killing long-running work. Run/task/report snapshots use atomic replacement so interruption cannot expose half-written JSON.
150
+
151
+ The child does not inherit parent extensions, parent conversation, or unconfigured skills, but normal project context files still apply. It uses the same Pi installation, provider configuration, model catalog, and credentials as the parent.
152
+
153
+ ## Development and tests
154
+
155
+ See [TESTING.md](TESTING.md) for deterministic tests, extension loading, packaging, and real Herdr smoke requirements.
@@ -0,0 +1,24 @@
1
+ # Herdr Subagents test strategy
2
+
3
+ ## Flow
4
+
5
+ 1. `npm --prefix packages/herdr-subagents test` runs deterministic scheduler, recovery, and persona tests.
6
+ 2. `pi -ne -ns --no-session -e ./packages/herdr-subagents/extensions/herdr-subagents.ts` verifies extension loading.
7
+ 3. `npm pack ./packages/herdr-subagents --dry-run` verifies package contents.
8
+ 4. Verify the README example and the production-runner media `docs/screenshot.png` and `docs/demo.gif` are present in the tarball.
9
+
10
+ ## Cases
11
+
12
+ - Resolve package, global, and project personas and validate frontmatter/configuration.
13
+ - Run foreground tasks and FIFO bounded-concurrency batches through an injectable Herdr boundary.
14
+ - Verify all paths use interactive Agent start/prompt without print mode, shell injection, markers, or pane reuse.
15
+ - Force write-capable batches to serial execution while allowing bounded read-only parallelism.
16
+ - Preserve compact reports, full results, Pi sessions, blocked questions, and historical follow-ups.
17
+ - Recover queued/running work after interruption without retrying post-prompt side effects, including truncated reports and recovery errors.
18
+ - 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 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.
20
+ - Verify resumed batches retain newly allocated pane IDs and historical follow-ups exclude simultaneous session writers.
21
+
22
+ ## Latest baseline
23
+
24
+ All 133 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.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: explorer
3
+ description: Read-only explorer for fast codebase investigation
4
+ access: read
5
+ tools: read, bash, grep, find, ls
6
+ ---
7
+
8
+ Investigate the assigned question without modifying files. Report concrete evidence and useful paths.
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: reviewer
3
+ description: Read-only reviewer for correctness and maintainability risks
4
+ access: read
5
+ tools: read, bash, grep, find, ls
6
+ ---
7
+
8
+ Review the assigned work without modifying files. Report actionable findings with concrete evidence.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: worker
3
3
  description: General-purpose worker for one focused task
4
+ access: write
4
5
  tools: read, bash, edit, write, grep, find, ls
5
6
  ---
6
7
 
@@ -0,0 +1,258 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { AgentToolResult, ExtensionAPI, ExtensionCommandContext, Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
4
+ import { Container, Editor, Spacer, Text } from "@earendil-works/pi-tui";
5
+ import { loadSubagentConfiguration } from "../src/config.ts";
6
+ import { CliHerdrAutomation, type HerdrAutomation } from "../src/herdr.ts";
7
+ import { activeRunText } from "../src/monitor.ts";
8
+ import { RunDispatcher } from "../src/dispatcher.ts";
9
+ import { historyText, listSubagentHistory } from "../src/history.ts";
10
+ import { cancelQueuedRuns, loadQueuedRuns, persistQueuedRun, removeQueuedRun } from "../src/queued-runs.ts";
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";
14
+ import { HERDR_ACTIONS } from "../src/validation.ts";
15
+ import { FleetController } from "../src/fleet-controller.ts";
16
+ import { isSettledRunStatus } from "../src/records.ts";
17
+
18
+ const packageAgentsDir = fileURLToPath(new URL("../agents/", import.meta.url));
19
+
20
+ function statusVisual(status: string): { color: "success" | "warning" | "error" | "accent"; glyph: string } {
21
+ if (status === "completed") return { color: "success", glyph: "✓" };
22
+ if (status === "blocked") return { color: "warning", glyph: "?" };
23
+ if (status === "cancelled" || status === "partial") return { color: "warning", glyph: "−" };
24
+ if (status === "starting" || status === "queued" || status === "running") return { color: "accent", glyph: "·" };
25
+ return { color: "error", glyph: "✗" };
26
+ }
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);
40
+ }
41
+
42
+ function renderToolResult(result: AgentToolResult<unknown>, _options: ToolRenderResultOptions, theme: Theme): Container | Text {
43
+ const batch = result.details as HerdrSubagentsBatchDetails | undefined;
44
+ if (batch?.tasks && "effectiveConcurrency" in batch) {
45
+ const container = new Container();
46
+ const visual = statusVisual(batch.status);
47
+ container.addChild(new Text(`${theme.fg("toolTitle", theme.bold(batch.label))} ${theme.fg(visual.color, batch.status)} · ${batch.tasks.length} settled · concurrency ${batch.effectiveConcurrency}`, 0, 0));
48
+ for (const task of batch.tasks) container.addChild(new Text(`${statusVisual(task.status).glyph} ${task.name}: ${task.summary}`, 0, 0));
49
+ return container;
50
+ }
51
+ 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));
62
+ }
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;
66
+ }
67
+
68
+ async function handleHerdrCommand(args: string, ctx: ExtensionCommandContext, fleet: FleetController): Promise<void> {
69
+ const section = args.trim() || "active";
70
+ if (section === "active") {
71
+ ctx.ui.notify(fleet.active ? activeRunText(fleet.active) : "No active Subagent run.", "info");
72
+ return;
73
+ }
74
+ if (section === "history") {
75
+ ctx.ui.notify(historyText(await listSubagentHistory(ctx.cwd)), "info");
76
+ return;
77
+ }
78
+ if (section.startsWith("focus ")) {
79
+ if (!await fleet.focus(Number(section.slice(6).trim()))) ctx.ui.notify("That task is not active in a Herdr pane.", "warning");
80
+ return;
81
+ }
82
+ if (section === "models") {
83
+ await ctx.modelRegistry.refresh();
84
+ const lines = ctx.modelRegistry.getAvailable().map((model) => `${model.provider}/${model.id}`).sort();
85
+ ctx.ui.notify(lines.length ? `Available Pi models:\n${lines.join("\n")}` : "No authenticated Pi models are available.", "info");
86
+ return;
87
+ }
88
+ const configuration = loadSubagentConfiguration(ctx.cwd, packageAgentsDir, ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
89
+ if (section === "agents") {
90
+ ctx.ui.notify(configuration.personas.map((persona) => `${persona.name}: ${persona.source}, ${persona.access}, model=${persona.model ?? "unresolved"} (${persona.modelSource}), thinking=${persona.thinking ?? "default"}, tools=${persona.tools?.join(",") ?? "default"}, skills=${persona.skills.join(",") || "none"}`).join("\n"), "info");
91
+ return;
92
+ }
93
+ if (section === "settings") {
94
+ const settings = configuration.settings;
95
+ const values: Record<string, string | number | undefined> = { defaultConcurrency: settings.defaultConcurrency, maxConcurrency: settings.maxConcurrency, stalledWarningSeconds: settings.stalledWarningSeconds, defaultModel: settings.defaultModel };
96
+ const lines = Object.entries(values).map(([key, value]) => `${key}=${String(value ?? "unset")} (${configuration.settingSources[key] ?? "built-in"})`);
97
+ if (configuration.diagnostics.length) lines.push("Diagnostics:", ...configuration.diagnostics);
98
+ ctx.ui.notify(lines.join("\n"), "info");
99
+ return;
100
+ }
101
+ ctx.ui.notify("Usage: /herdr-subagents active|history|focus <task-number>|agents|models|settings", "warning");
102
+ }
103
+
104
+ export interface HerdrSubagentsExtensionDependencies {
105
+ herdr?: HerdrAutomation;
106
+ execute?: typeof executeHerdrSubagents;
107
+ isEditor?: (value: unknown) => boolean;
108
+ createFleetWidget?: (lines: string[]) => Container;
109
+ }
110
+
111
+ async function recoverParentSession(cwd: string, herdr: HerdrAutomation, owner: { sessionId?: string; processId: number }, notify: (message: string) => void): Promise<void> {
112
+ try {
113
+ const queued = await loadQueuedRuns(cwd, notify);
114
+ await cancelQueuedRuns(cwd, queued.map((run) => run.runId), { reason: "Owning parent Pi session is no longer active.", owner, onlyOrphaned: true, onError: notify });
115
+ }
116
+ catch (error) { notify(`Subagent queue recovery failed: ${error instanceof Error ? error.message : String(error)}`); }
117
+ try {
118
+ const reconciliation = await reconcileSubagentRuns(cwd, herdr, owner);
119
+ for (const message of reconciliation.cleanupErrors) notify(message);
120
+ }
121
+ catch (error) { notify(`Subagent recovery could not inspect active runs: ${error instanceof Error ? error.message : String(error)}`); }
122
+ }
123
+
124
+ async function shutdownParentSession(options: { cwd: string; herdr: HerdrAutomation; owner: { sessionId?: string; processId: number }; dispatcher: RunDispatcher; fleet: FleetController; sessionController: AbortController; runControllers: Map<string, AbortController>; inFlight: Set<Promise<unknown>>; notify: (message: string) => void }): Promise<void> {
125
+ options.dispatcher.pause();
126
+ options.sessionController.abort(new Error("Parent Pi session closed."));
127
+ const queuedRunIds = options.dispatcher.cancelQueued(new Error("Parent Pi session closed."));
128
+ await cancelQueuedRuns(options.cwd, queuedRunIds, { reason: "Parent Pi session closed.", owner: options.owner });
129
+ for (const controller of options.runControllers.values()) controller.abort(new Error("Parent Pi session closed."));
130
+ options.fleet.shutdown();
131
+ await Promise.allSettled([...options.inFlight]);
132
+ const cancellation = await cancelActiveSubagentRuns(options.cwd, options.herdr, { reason: "Parent Pi session closed.", owner: options.owner });
133
+ const retainedRunId = options.dispatcher.snapshot().activeRunId;
134
+ if (retainedRunId) options.dispatcher.release(retainedRunId);
135
+ for (const message of cancellation.cleanupErrors) options.notify(message);
136
+ }
137
+
138
+ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSubagentsExtensionDependencies = {}) {
139
+ if (process.env.PI_HERDR_SUBAGENTS_CHILD === "1" || process.env.HERDR_ENV !== "1" || !process.env.HERDR_WORKSPACE_ID) return;
140
+ const herdr = dependencies.herdr ?? new CliHerdrAutomation();
141
+ const execute = dependencies.execute ?? executeHerdrSubagents;
142
+ const isEditorComponent = dependencies.isEditor ?? ((value: unknown) => value instanceof Editor);
143
+ const dispatcher = new RunDispatcher();
144
+ const fleet = new FleetController(herdr, isEditorComponent, dependencies.createFleetWidget);
145
+ const runControllers = new Map<string, AbortController>();
146
+ const inFlight = new Set<Promise<unknown>>();
147
+ let sessionController = new AbortController();
148
+ let ownerSessionId: string | undefined;
149
+ let sessionGeneration = 0;
150
+ const notifyRun = (runId: string, result: { details: unknown; content?: Array<{ type: string; text?: string }> }) => {
151
+ const details = result.details as Partial<HerdrSubagentsBatchDetails> & { errorMessage?: string };
152
+ if (!details.tasks || !details.documents || !details.status) {
153
+ const error = details.errorMessage ?? result.content?.find((item) => item.type === "text")?.text ?? "Unknown Subagent failure.";
154
+ pi.sendMessage({ customType: "herdr-subagents-run", content: `Subagent run ${runId} failed: ${error}`, display: true, details: { runId, status: "failed" } }, { triggerTurn: true, deliverAs: "followUp" });
155
+ return;
156
+ }
157
+ const lines = [`Subagent run ${runId} ${details.status}: ${details.summary}`, ...details.tasks.map((task) => `- ${task.name}: ${task.summary}`)];
158
+ if (details.documents.length) lines.push("Documents:", ...details.documents.map((document) => `- ${document.description}: ${document.path}`));
159
+ pi.sendMessage({ customType: "herdr-subagents-run", content: lines.join("\n"), display: true, details: { runId, status: details.status } }, { triggerTurn: true, deliverAs: "followUp" });
160
+ };
161
+
162
+ pi.on("session_shutdown", async (_event, ctx) => {
163
+ sessionGeneration += 1;
164
+ await shutdownParentSession({ cwd: ctx.cwd, herdr, owner: { sessionId: ownerSessionId, processId: process.pid }, dispatcher, fleet, sessionController, runControllers, inFlight, notify: (message) => ctx.ui.notify(message, "error") });
165
+ });
166
+ pi.on("session_start", async (_event, ctx) => {
167
+ sessionGeneration += 1;
168
+ sessionController.abort(new Error("Parent Pi session changed."));
169
+ sessionController = new AbortController();
170
+ ownerSessionId = `${ctx.sessionManager.getSessionId()}:${randomUUID()}`;
171
+ fleet.bind(ctx.ui, ctx.mode === "tui");
172
+ dispatcher.pause();
173
+ try { await recoverParentSession(ctx.cwd, herdr, { sessionId: ownerSessionId, processId: process.pid }, (message) => ctx.ui.notify(message, "error")); }
174
+ finally { dispatcher.resume(); }
175
+ });
176
+
177
+ pi.registerTool({
178
+ name: "herdr_subagents",
179
+ 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.",
181
+ 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."],
183
+ parameters: HerdrSubagentsParams,
184
+
185
+ async execute(_toolCallId, params: HerdrSubagentsToolParams, signal, onUpdate, ctx) {
186
+ const generation = sessionGeneration;
187
+ const runId = params.tasks ? randomUUID() : undefined;
188
+ const executionId = runId ?? params.runId ?? randomUUID();
189
+ const controller = new AbortController();
190
+ runControllers.set(executionId, controller);
191
+ const runSignal = AbortSignal.any([controller.signal, sessionController.signal, ...(!params.background && signal ? [signal] : [])]);
192
+ const executeRun = async () => {
193
+ const result = await execute(params, runSignal, (update) => {
194
+ const details = update.details as HerdrSubagentsBatchDetails | undefined;
195
+ if (details?.activity) fleet.sync(details);
196
+ const updateText = update.content.find((item) => item.type === "text")?.text;
197
+ const updateDetails = update.details as { warning?: string };
198
+ if (updateDetails.warning || updateText?.includes("cleanup failed") || (params.background && updateText?.includes("appears stalled"))) ctx.ui.notify(updateDetails.warning ?? updateText ?? "Subagent warning", "warning");
199
+ if (!params.background) onUpdate?.(update);
200
+ }, ctx, { agentsDirectory: packageAgentsDir, owner: { sessionId: ownerSessionId, processId: process.pid }, runId });
201
+ fleet.sync(result.details);
202
+ return result;
203
+ };
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
+ if (params.background) await persistQueuedRun(ctx.cwd, runId, params, { sessionId: ownerSessionId, processId: process.pid });
220
+ const promise = dispatcher.submit(runId, async () => {
221
+ const result = await executeRun().finally(async () => { if (params.background) await removeQueuedRun(ctx.cwd, runId); });
222
+ const details = result.details as HerdrSubagentsBatchDetails;
223
+ fleet.setActive(details.status === "blocked" ? details : undefined);
224
+ if (fleet.active) {
225
+ dispatcher.retain(runId);
226
+ }
227
+ return result;
228
+ });
229
+ inFlight.add(promise);
230
+ void promise.finally(() => { inFlight.delete(promise); runControllers.delete(executionId); }).catch(() => undefined);
231
+ if (params.background) {
232
+ void promise.then((result) => {
233
+ if (generation !== sessionGeneration) return;
234
+ notifyRun(runId, result);
235
+ if (!fleet.active && !dispatcher.snapshot().activeRunId && dispatcher.snapshot().queuedRunIds.length === 0) fleet.render();
236
+ }).catch((error) => {
237
+ if (generation !== sessionGeneration) return;
238
+ pi.sendMessage({ customType: "herdr-subagents-run", content: `Subagent run ${runId} failed: ${error instanceof Error ? error.message : String(error)}`, display: true, details: { runId, status: "failed" } }, { triggerTurn: true, deliverAs: "followUp" });
239
+ });
240
+ return { content: [{ type: "text" as const, text: `Subagent run queued: ${runId}` }], details: { runId, status: "queued", background: true } };
241
+ }
242
+ const result = await promise;
243
+ if (!fleet.active && !dispatcher.snapshot().activeRunId && dispatcher.snapshot().queuedRunIds.length === 0) fleet.resetSelection();
244
+ return result;
245
+ },
246
+
247
+ renderCall: renderToolCall,
248
+ renderResult: renderToolResult,
249
+ });
250
+
251
+ pi.registerCommand("herdr-subagents", {
252
+ description: "Inspect effective subagent agents, models, and settings",
253
+ getArgumentCompletions: (prefix) => ["active", "history", "agents", "models", "settings", "focus "].filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value })),
254
+ handler: (args, ctx) => handleHerdrCommand(args, ctx, fleet),
255
+ });
256
+ }
257
+
258
+ export default registerHerdrSubagents;