@herbertgao/pi-subagents 0.19.0 → 0.22.0-local.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.0-local.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#287](https://github.com/HerbertGao/pi-extensions/pull/287) [`3a0deb9`](https://github.com/HerbertGao/pi-extensions/commit/3a0deb96297aa6da94fe40724a3945ef16c34497) Thanks [@HerbertGao](https://github.com/HerbertGao)! - Stop bundling resume-from and lower the Node engines floor to `>=22.19.0`, matching Pi. Node 24 was copied from resume-from's engines field; these packages do not use a Node 24-only API. Pi's bundled Node 22 can install the current release instead of silently selecting an older one.
8
+
9
+ - [#288](https://github.com/HerbertGao/pi-extensions/pull/288) [`ec336b0`](https://github.com/HerbertGao/pi-extensions/commit/ec336b0ec89b9bd46dfc321703de432d22811615) Thanks [@HerbertGao](https://github.com/HerbertGao)! - Replay pi-cc-extensions onto upstream 1.0.4 and pi-subagents onto upstream 0.22.0, keeping local patches. Advance companion pins through ponytail 5.2.0, ask-user-question 2.13.8, fast-mode 0.1.14, pi-lens 4.4.3, pi-provider-qoder 0.5.1, and pi-web-access 0.40.0.
10
+
3
11
  ## 0.19.0
4
12
 
5
13
  ### Minor Changes
@@ -0,0 +1,211 @@
1
+ # Shipping subagents in a pi package
2
+
3
+ A pi package can declare its own subagents and workflow scripts. Installing the package is then the whole setup — no copying `.md` files into every project's `.pi/agents/`, and no sync script running on container start ([#109](https://github.com/tintinweb/pi-subagents/issues/109)).
4
+
5
+ This guide is for both sides of that: authoring a package that ships agents, and controlling which packages a machine accepts them from.
6
+
7
+ For where locally-authored agents live and what their frontmatter accepts, see [`README.md`](../README.md#custom-agents).
8
+
9
+ ## The idea
10
+
11
+ Pi already ships four resource types inside packages — `extensions`, `skills`, `prompts`, `themes` — declared under a `pi` key in `package.json` and installed with `pi install`. Subagents are this extension's concept, so pi knows nothing about them, and until now a package author's only option was to tell users to copy files.
12
+
13
+ The mechanism here is the same shape as pi's, with one deliberate difference: **a package must declare its agents by name.** Pi falls back to scanning conventional directories (`skills/`, `prompts/`, …) when a package has no `pi` key at all. This extension never does. A package that happens to carry an `agents/` folder for some other tool contributes nothing until it says so — that is the author's half of the agreement, and installing the package is the user's.
14
+
15
+ ## Authoring
16
+
17
+ ### 1. Declare the paths
18
+
19
+ ```json
20
+ {
21
+ "name": "my-subagents",
22
+ "version": "1.0.0",
23
+ "keywords": ["pi-package"],
24
+ "pi": {
25
+ "extensions": ["./src/index.ts"],
26
+ "subagents": {
27
+ "agents": ["./agents"],
28
+ "workflows": ["./workflows"]
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ `keywords: ["pi-package"]` is pi's discoverability convention, not a requirement. The `extensions` entry is only there to show the keys coexisting — a package can ship agents and nothing else.
35
+
36
+ Adding `pi.subagents` is safe for pi itself: its manifest reader looks at `extensions`, `skills`, `prompts` and `themes` and ignores every other key.
37
+
38
+ ### 2. Put the files there
39
+
40
+ ```
41
+ my-subagents/
42
+ ├── package.json
43
+ ├── agents/
44
+ │ ├── researcher.md
45
+ │ └── reviewer.md
46
+ └── workflows/
47
+ └── audit.js
48
+ ```
49
+
50
+ An agent file is an ordinary agent `.md` — the same frontmatter a `.pi/agents/` file takes, documented in [`README.md`](../README.md#custom-agents). A workflow file is an ordinary saved workflow, and still has to carry its `export const meta = …` declaration to count as one ([`workflows.md`](workflows.md)).
51
+
52
+ ```markdown
53
+ ---
54
+ name: pkg-researcher
55
+ description: Research with sources, for the my-subagents toolchain
56
+ tools: read, grep, ext:my-mcp/search
57
+ model: claude-haiku-4-5
58
+ ---
59
+
60
+ You research things. Cite what you read.
61
+ ```
62
+
63
+ ### 3. Ship it
64
+
65
+ Include the directories in the published tarball — npm's `files` field, or an `.npmignore` that does not exclude them. Then:
66
+
67
+ ```bash
68
+ pi install npm:my-subagents # published
69
+ pi install git:github.com/me/repo # straight from git
70
+ pi install /path/to/my-subagents # local checkout, for development
71
+ pi install ./my-subagents -l # project scope, into .pi/settings.json
72
+ ```
73
+
74
+ A local-path install is not copied, so an edit to the agent file is live on the next `Agent` call — the fastest authoring loop.
75
+
76
+ ### Entry syntax
77
+
78
+ Entries are paths relative to the package root.
79
+
80
+ | Entry | Meaning |
81
+ | -------------------- | --------------------------------------------------------------------------------------------------------------- |
82
+ | `"./agents"` | A directory. Scanned for `*.md` (`*.js` for workflows), non-recursively. |
83
+ | `"./agents/one.md"` | A single file, taken as-is. |
84
+ | `"!./agents/wip.md"` | An exclusion. Applies to a file inside an included directory, which is the only form that does anything useful. |
85
+
86
+ The exclusion may name a path the package does not ship yet, so a file added in a later version is still skipped rather than silently appearing.
87
+
88
+ Nothing outside the package root is read. The containment check runs on canonical paths, so `../../.ssh` is refused and a symlinked `agents/` pointing at `/etc` does not get around it either.
89
+
90
+ Glob patterns are deliberately **not** supported. Pi expands globs because it resolves four resource types across arbitrary layouts; here the entries are one or two directories, and answering a glob would mean walking a third-party tree.
91
+
92
+ ### Accepted spellings
93
+
94
+ All four mean the same thing. The top-level `pi-subagents` key is the one [`nicobailon/pi-subagents`](https://github.com/nicobailon/pi-subagents) reads, so a single manifest works with either extension.
95
+
96
+ ```jsonc
97
+ "pi": { "subagents": { "agents": ["./agents"], "workflows": ["./flows"] } }
98
+ "pi": { "subagents": ["./agents"] } // shorthand for { "agents": [...] }
99
+ "pi-subagents": { "agents": ["./agents"] }
100
+ "pi-subagents": ["./agents"] // shorthand for { "agents": [...] }
101
+ ```
102
+
103
+ `pi.subagents` wins if a package carries both.
104
+
105
+ ### Naming
106
+
107
+ A package agent registers under its frontmatter `name:`, with no namespace prefix. There is no `mypkg:researcher` form (yet — the `:` character stays reserved for it, so agent names may not contain one).
108
+
109
+ That makes a name a shared space, so prefix anything that could collide: `acme-researcher` rather than `researcher`. Two packages claiming one name is last-load-wins with a warning naming the file that took over, and _any_ local file claiming it takes it from both.
110
+
111
+ ## Operating
112
+
113
+ ### Which packages are read
114
+
115
+ Only what pi itself has installed — the `packages[]` array in pi's `settings.json`, global (`~/.pi/agent/settings.json`) and project (`.pi/settings.json`), resolved through pi's own package manager so npm, git and local sources all land on the right install root.
116
+
117
+ `node_modules` is never scanned. Being a transitive dependency of your project means nothing here, exactly as it means nothing to pi.
118
+
119
+ A package listed only by a project's settings stays invisible until you trust the project, matching pi — an untrusted `.pi/settings.json` is not read at all. This is the shape a team distributes agents in: commit the package to `.pi/settings.json`, and everyone who trusts the repo gets them.
120
+
121
+ One wrinkle on the _very first_ session in a repo, before pi has saved a trust decision. The `Agent` tool's description — the list of agent types the model is told about — is built when the extension loads, which is before the trust prompt is answered. A project-scoped package's agents are registered and can be spawned by name that session, but the model is not told they exist until the next one. Once the decision is saved (or `defaultProjectTrust` is `"always"`), every later session is correct from the first turn.
122
+
123
+ ### Precedence
124
+
125
+ Lowest to highest:
126
+
127
+ | | Source |
128
+ | ----------- | ------------------------------------------------------ |
129
+ | 1 (lowest) | Built-in agents (`general-purpose`, `Explore`, `Plan`) |
130
+ | 2 | Installed pi packages |
131
+ | 3 | `$PI_CODING_AGENT_DIR/agents/*.md` (global) |
132
+ | 4 | `.agents/agents/*.md` (project workspace) |
133
+ | 5 (highest) | `.pi/agents/*.md` (project) |
134
+
135
+ A package can offer a `reviewer`; it can never take the name from a `reviewer` of yours. This mirrors pi's own precedence for package-provided skills and Claude Code's for plugin agents.
136
+
137
+ Saved workflows resolve first-hit-wins over the same ordering, so package directories are searched last.
138
+
139
+ One consequence worth knowing: a package agent _does_ outrank a built-in. A package could ship a `general-purpose` — which is also the default `fallbackSubagent` — and take over unspecified delegation. Narrow the gate below if that matters for your setup.
140
+
141
+ ### In `/agents`
142
+
143
+ A package agent is listed with a `▪` badge and is read-only: its file lives under pi's install root, where an edit is lost on the next `pi update` and a delete is undone by the next `pi install`.
144
+
145
+ The badge says a package won, not which one. The declaring package's name — the full `@scope/name`, which is also what you can write into a [`packageAgents` allowlist](#turning-it-off) — is prefixed to the description line under the highlighted row, and titles the agent's action menu.
146
+
147
+ - **Eject** copies it into your project or personal agent directory as an editable file that shadows the original. The copy carries the agent's full configuration, including `ext:` tool selectors and `persist_session`.
148
+ - **Disable** writes an `enabled: false` stub at project or personal scope, which shadows the package definition without touching it. Deleting that stub restores the package agent.
149
+
150
+ An agent the _package author_ shipped with `enabled: false` shows as disabled and offers no Enable — only the package can take that back. Ejecting it is the way out: the copy is written without an `enabled:` line, so the ejected agent is on.
151
+
152
+ ### Turning it off
153
+
154
+ `packageAgents` and `packageWorkflows` in [`subagents.json`](../README.md#persistent-settings) both default to `true`:
155
+
156
+ ```json
157
+ {
158
+ "packageAgents": ["my-subagents"],
159
+ "packageWorkflows": false
160
+ }
161
+ ```
162
+
163
+ | Value | Effect |
164
+ | ------------------ | ------------------------------------ |
165
+ | `true` (or absent) | Every declaring package contributes. |
166
+ | `false` | None do. |
167
+ | `["a", "b"]` | Only those packages. |
168
+
169
+ A list entry matches the unscoped short name (`@acme/tools` → `tools`), the full package name, or the settings source string, case-insensitively. A bare string is read as a one-entry list, so a `"my-pkg"` typo narrows the gate rather than silently widening it.
170
+
171
+ The two are separate on purpose: an agent is markdown handed to a model, a workflow is JavaScript this extension executes, so package agents can be accepted while package workflow scripts are refused.
172
+
173
+ Both appear in `/agents → Settings` as on/off rows. An allowlist is hand-written in `subagents.json`; toggling the row replaces it, and the toast says what it replaced.
174
+
175
+ ### Why on by default
176
+
177
+ Installing a package is already the trust decision. Pi executes an installed package's `extensions/` and puts its `skills/` descriptions in the system prompt, with no per-resource prompt — [pi's own docs](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) say so plainly: _"Pi packages run with full system access."_
178
+
179
+ A declared `.md` agent is strictly less privileged than what the same package already runs, so it rides the boundary the user crossed at `pi install` rather than inventing a second one. The gate exists for narrowing that anyway, not as a second install step.
180
+
181
+ ## Troubleshooting
182
+
183
+ **The agent does not appear.**
184
+
185
+ 1. `pi list` — is the package in pi's settings at all? Discovery reads that array, not `node_modules`, and not a `pi -e` source: a `--extension` install is resolved for that one run and never written to `packages[]`, so its agents stay invisible even though its extensions and skills load.
186
+ 2. Does `package.json` carry `pi.subagents` (or `pi-subagents`)? A `pi` key that declares only `extensions` contributes no agents; the conventional `agents/` directory is not scanned.
187
+ 3. Is the file `*.md` directly inside a declared directory? The scan is not recursive.
188
+ 4. Does the frontmatter parse, and does it have a `name:`? A broken file is skipped with a `[pi-subagents] Skipping agent file …` warning on stderr.
189
+ 5. Is `packageAgents` set to `false` or to a list that does not include it? Check `/agents → Settings → Package agents`.
190
+ 6. Is the package configured only in an untrusted project's `.pi/settings.json`? Trust the project, or install it globally.
191
+
192
+ **It appears but a different one runs.** A local file of the same name outranks it. `/agents` shows the badge of whichever won — `•` project, `◦` global, `▪` package.
193
+
194
+ **Edits to the package file do nothing.** Agents reload on every `Agent` call, so a local-path install picks up edits immediately. An npm or git install is a copy under `~/.pi/agent/{npm,git}/`; edit the source and reinstall, or install the local path during development.
195
+
196
+ **A file over 1 MB is skipped.** Package files are third-party content re-read on every spawn, so there is a per-file ceiling, matching Claude Code's for plugin agents. Local files are unbounded.
197
+
198
+ **`strictAgentFiles` and a broken package file.** Strict mode fails startup over an unparseable file in _your_ `.pi/agents/`, which is the point of it. It deliberately does not extend to files inside installed packages — those are not yours to fix, and one bad `.md` in a dependency should not stop pi from starting.
199
+
200
+ ## Reference
201
+
202
+ | | |
203
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
204
+ | Manifest keys | `pi.subagents.agents`, `pi.subagents.workflows`, `pi-subagents.agents`, `pi-subagents.workflows`, plus the bare-array shorthand for each |
205
+ | Settings | `packageAgents`, `packageWorkflows` — `boolean \| string[]`, default `true` |
206
+ | Agent file layout | `<declared dir>/*.md`, non-recursive |
207
+ | Workflow file layout | `<declared dir>/*.js`, must carry `export const meta = …` |
208
+ | Per-file size ceiling | 1 MiB |
209
+ | Discovery source | pi's `settings.json → packages[]`, global and project |
210
+ | Precedence | Above built-ins, below every local agent |
211
+ | Implementation | [`src/package-resources.ts`](../src/package-resources.ts) |
package/docs/rpc.md CHANGED
@@ -8,27 +8,27 @@ For the channel list, the reply envelope, the per-channel snippets and the event
8
8
 
9
9
  ## Spawn options
10
10
 
11
- `subagents:rpc:spawn` forwards `options` to `AgentManager.spawn` — but not verbatim. The manager's `spawn` behind the RPC is `spawnTopLevel` (`src/index.ts:698-721`), which deletes internal-only fields first, and then `spawnResolved` (`src/index.ts:666-696`) overwrites the activity-tracker callbacks with its own. The full interface is `SpawnOptions` at `src/agent-manager.ts:177-313`; what a bus caller actually gets is three different things.
11
+ `subagents:rpc:spawn` forwards `options` to `AgentManager.spawn` — but not verbatim. The manager's `spawn` behind the RPC is `spawnTopLevel` in `src/index.ts`, which deletes internal-only fields first, and then `spawnResolved` overwrites the activity-tracker callbacks with its own. The full interface is `SpawnOptions` in `src/agent-manager.ts`; what a bus caller actually gets is three different things.
12
12
 
13
13
  **Honoured** — set these and they take effect:
14
14
 
15
- | Field | Type | Notes |
16
- | --------------------------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
- | `description` | string | What the agent is doing. Shown in the widget, FleetView and the completion notification |
18
- | `name` | string | A memorable second handle (`@auth-audit`). Slugged, never validated — anything unusable degrades rather than failing the spawn |
19
- | `model` | `Model` **or** `"provider/modelId"` | Strings are resolved at the RPC boundary against `ctx.modelRegistry`. `null` means inherit, not override. Resolution is fuzzy — see [Model Scope](../README.md#model-scope) |
20
- | `maxTurns` | number | Turn ceiling for the run |
21
- | `isolated` | boolean | Strips extensions, skills and nested tools. **Not** a git worktree — see the trap table below |
22
- | `inheritContext` | boolean | Fork the parent conversation into the child |
23
- | `thinkingLevel` | ThinkingLevel | Clamped to what the resolved model supports |
24
- | `isBackground` | boolean | Occupies a `maxConcurrent` slot and queues behind them. Every RPC spawn runs detached regardless; this is what decides whether it is _pooled_ |
25
- | `bypassQueue` | boolean | Starts immediately even when the concurrency limit would queue it. The slot is still counted once running |
26
- | `structuredOutput` | CompiledSchema | Makes the child report through a `StructuredOutput` tool |
27
- | `isolation` | `"worktree"` | Temp git worktree, committed to a `pi-agent-*` branch on completion |
28
- | `cwd` | absolute path | The agent's tools operate here; `.pi` config still loads from the parent session's project |
29
- | `invocation` | AgentInvocation | Resolved snapshot used for UI display |
30
- | `signal` | AbortSignal | Aborting it stops the subagent |
31
- | `onSpawned` / `onQueued` / `onCompaction` / `onBeforeWorktreeCleanup` | functions | Fire as documented on `SpawnOptions` |
15
+ | Field | Type | Notes |
16
+ | --------------------------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `description` | string | What the agent is doing. Shown in the widget, FleetView and the completion notification |
18
+ | `name` | string | A memorable second handle (`@auth-audit`). Slugged, never validated — anything unusable degrades rather than failing the spawn |
19
+ | `model` | `Model` **or** `"provider/modelId"` | Strings are resolved at the RPC boundary against `ctx.modelRegistry`. `null` means inherit, not override. Resolution is fuzzy — see [Model Scope](../README.md#model-scope) |
20
+ | `maxTurns` | number | Turn ceiling for the run |
21
+ | `isolated` | boolean | Strips extensions, skills and nested tools. Applies only when the agent file leaves `isolated:` unset — frontmatter is authoritative. **Not** a git worktree — see the trap table below |
22
+ | `inheritContext` | boolean | Fork the parent conversation into the child. Only this option decides — an agent file's `inherit_context:` does not apply to RPC spawns |
23
+ | `thinkingLevel` | ThinkingLevel | Clamped to what the resolved model supports |
24
+ | `isBackground` | boolean | Occupies a `maxConcurrent` slot and queues behind them. Every RPC spawn runs detached regardless; this is what decides whether it is _pooled_ |
25
+ | `bypassQueue` | boolean | Starts immediately even when the concurrency limit would queue it. The slot is still counted once running |
26
+ | `structuredOutput` | CompiledSchema | Makes the child report through a `StructuredOutput` tool |
27
+ | `isolation` | `"worktree"` | Temp git worktree, committed to a `pi-agent-*` branch on completion. The agent file's `isolation:` wins: `worktree` applies even when this is omitted, and `off` refuses it |
28
+ | `cwd` | absolute path | The agent's tools operate here; `.pi` config still loads from the parent session's project, and the child reports this directory as untrusted (`ctx.isProjectTrusted()` is `false`) |
29
+ | `invocation` | AgentInvocation | Resolved snapshot used for UI display |
30
+ | `signal` | AbortSignal | Aborting it stops the subagent |
31
+ | `onSpawned` / `onQueued` / `onCompaction` / `onBeforeWorktreeCleanup` | functions | Fire as documented on `SpawnOptions` |
32
32
 
33
33
  **Silently stripped** — set these and nothing happens, with no error and no note. Each deletion is a deliberate guard, and the reasons are worth knowing because they say what the surface refuses to let a caller forge:
34
34
 
@@ -43,13 +43,13 @@ For the channel list, the reply envelope, the per-channel snippets and the event
43
43
  | `reclaim` | Bypasses handle allocation, so a forged value would duplicate a live agent's name and make `@handle` ambiguous |
44
44
  | `blocking` | Every spawn through here is detached. A forged `blocking` would charge it to the foreground pool and defer it behind a queue whose gate nobody is holding |
45
45
 
46
- **Silently overwritten** — `onToolActivity`, `onTextDelta`, `onTurnEnd`, `onSessionCreated` and `onAssistantUsage` are replaced by the activity tracker's own (`src/index.ts:693`). Every programmatic spawn passes through one funnel so none can supply half-wired callbacks; a half-wired tracker renders worse than none, which is the bug behind a row that reads `thinking…` for an agent's whole life ([#181](https://github.com/tintinweb/pi-subagents/pull/181)).
46
+ **Silently overwritten** — `onToolActivity`, `onTextDelta`, `onTurnEnd`, `onSessionCreated` and `onAssistantUsage` are replaced by the activity tracker's own (`spawnResolved`, `src/index.ts`). Every programmatic spawn passes through one funnel so none can supply half-wired callbacks; a half-wired tracker renders worse than none, which is the bug behind a row that reads `thinking…` for an agent's whole life ([#181](https://github.com/tintinweb/pi-subagents/pull/181)).
47
47
 
48
48
  Four things that are not obvious from the tables:
49
49
 
50
- - **Nothing is required at runtime.** `description` is non-optional in TypeScript and never validated. A spawn with no `options` at all is legal and is what `test/cross-extension-rpc.test.ts:81-94` pins.
50
+ - **Nothing is required at runtime.** `description` is non-optional in TypeScript and never validated. A spawn with no `options` at all is legal and is what "returns agent id on success" in `test/cross-extension-rpc.test.ts` pins.
51
51
  - **`bypassQueue` is not stripped.** Its own doc comment scopes it to the scheduler and the `/agents` generator, but a bus caller can set it and skip the `maxConcurrent` check.
52
- - **`structuredOutput` is documented "set only by the workflow host"** (`src/agent-manager.ts:239-242`) and is also not stripped.
52
+ - **`structuredOutput` is documented "set only by the workflow host"** (its field comment on `SpawnOptions`) and is also not stripped.
53
53
  - **`signal` and the `on*` callbacks are function values.** They work only because the bus is in-process. A caller that genuinely serializes its payload cannot use them, and they arrive as `undefined` rather than failing.
54
54
 
55
55
  ### Names that look right and are not
@@ -69,44 +69,42 @@ One of these already shipped as a bug in this project's own README example, so i
69
69
 
70
70
  ## Errors
71
71
 
72
- Every failure reaches the caller as `{ success: false, error }`, where `error` is `err?.message ?? String(err)` (`src/cross-extension-rpc.ts:87`) — so these strings are what you will actually see.
73
-
74
- | Error | Source |
75
- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
76
- | `No active session` | `src/cross-extension-rpc.ts:107` — called before the first bound `session_start`, or in a session that excludes pi-subagents |
77
- | `Model override "<label>" provided but ctx.modelRegistry is unavailable` | `src/cross-extension-rpc.ts:126` |
78
- | `Model not found: "<input>".` + available models | `src/model-resolver.ts:117` |
79
- | `Model not in scope: "<input>".` + allowed models | `src/model-scope.ts:62` — only with `scopeModels` on, and checked against the _resolved_ model |
80
- | `Unknown or disabled agent type: "<raw>". Available: <list>.` | `src/agent-types.ts:187` — only under `fallbackSubagent: none` |
81
- | `No agent type given. Available: <list>.` | `src/agent-types.ts:187-194` — same condition |
82
- | `<reason> The configured fallbackSubagent "<x>" is itself unknown or disabled. Available: <list>.` | `src/agent-types.ts:205-207` |
83
- | `SpawnOptions.cwd must be an absolute path: "<value>"` | `src/agent-manager.ts:93` |
84
- | `SpawnOptions.cwd does not exist: "<cwd>"` | `src/agent-manager.ts:99` |
85
- | `SpawnOptions.cwd is not a directory: "<cwd>"` | `src/agent-manager.ts:102` |
86
- | `Cannot run with isolation: "worktree" — not a git repo, no commits yet, or 'git worktree add' failed.` | `src/agent-manager.ts:724-727`, surfaced through `awaitStartup` |
87
- | `Worktree cleanup failed: <reason>` + retained recovery path | `src/agent-manager.ts:76-81`; the agent becomes an error instead of losing the worktree |
88
- | git plumbing failures | `src/worktree.ts:78` |
89
- | `Agent not found` | stop — `src/cross-extension-rpc.ts:170` |
90
- | `Agent is owned by another agent or workflow` | stop — `:178` |
91
- | `Agent is not running` | stop — `:182`. The record exists, so it has already settled |
92
- | `Agent not found or still running` | consume — `:193` |
72
+ Every failure reaches the caller as `{ success: false, error }`, where `error` is `err?.message ?? String(err)` (`handleRpc` in `src/cross-extension-rpc.ts`) — so these strings are what you will actually see.
73
+
74
+ | Error | Source |
75
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
76
+ | `No active session` | spawn handler, `src/cross-extension-rpc.ts` — called before the first bound `session_start`, or in a session that excludes pi-subagents |
77
+ | `Model override "<label>" provided but ctx.modelRegistry is unavailable` | spawn handler, `src/cross-extension-rpc.ts` |
78
+ | `Model not found: "<input>".` + available models | `resolveModel`, `src/model-resolver.ts` |
79
+ | `Model not in scope: "<input>".` + allowed models | `checkModelScope`, `src/model-scope.ts` — only with `scopeModels` on, and checked against the _resolved_ model |
80
+ | `Unknown or disabled agent type: "<raw>". Available: <list>.` | `resolveSpawnTypeIn`, `src/agent-types.ts` — only under `fallbackSubagent: none` |
81
+ | `No agent type given. Available: <list>.` | `resolveSpawnTypeIn` — same condition |
82
+ | `<reason> The configured fallbackSubagent "<x>" is itself unknown or disabled. Available: <list>.` | `resolveSpawnTypeIn` |
83
+ | `SpawnOptions.cwd must be an absolute path: "<value>"` | `assertValidSpawnCwd`, `src/agent-manager.ts` |
84
+ | `SpawnOptions.cwd does not exist: "<cwd>"` | `assertValidSpawnCwd` |
85
+ | `SpawnOptions.cwd is not a directory: "<cwd>"` | `assertValidSpawnCwd` |
86
+ | ``Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. Initialize git and commit at least once, or omit `isolation`.`` | `AgentManager.startAgent`, surfaced through `awaitStartup`. Every git failure ends here: `createWorktree` (`src/worktree.ts`) catches git's own error and returns nothing |
87
+ | `Agent not found` | stop handler, `src/cross-extension-rpc.ts` |
88
+ | `Agent is owned by another agent or workflow` | stop handler |
89
+ | `Agent is not running` | stop handler. The record exists, so it has already settled |
90
+ | `Agent not found or still running` | consume handler |
93
91
 
94
92
  Three things the table cannot show:
95
93
 
96
- - **The failure that is not an error.** With `worktreeIsolation` off project-wide, `isolation: "worktree"` is dropped at `src/agent-manager.ts:720` with no error, no note on the record, and a success envelope on the wire. Your agent runs in the main tree. If you asked for isolation because two agents were going to write the same files, they now collide and nothing told you.
94
+ - **The failure that is not an error.** With `worktreeIsolation` off project-wide, `isolation: "worktree"` is dropped by the `isWorktreeIsolationEnabled()` check in `AgentManager.startAgent` with no error, no note on the record, and a success envelope on the wire. Your agent runs in the main tree. If you asked for isolation because two agents were going to write the same files, they now collide and nothing told you.
97
95
  - **`data` is omitted** when a handler returns nothing, so a successful stop or consume reply is a bare `{ success: true }` and `reply.data.anything` throws.
98
96
  - **`requestId` is not validated.** It is interpolated straight into the reply channel, so a caller that omits it gets its reply on the literal channel `subagents:rpc:spawn:reply:undefined` — where every other caller that omitted it is also listening. Send one, and send a unique one.
99
97
 
100
98
  ## Ownership
101
99
 
102
- `isTopLevelAgent(record)` is `parentAgentId === undefined && workflowId === undefined` (`src/agent-manager.ts:130-134`). `subagents:rpc:stop` enforces it (`src/cross-extension-rpc.ts:178`): a nested child or a workflow's agent is owned by something that is _waiting on it_, and aborting it out from under that owner turns another extension's stop into a failed step. It is defence in depth rather than a live hole — no RPC hands out agent ids, so a caller has no ordinary way to name one it does not own.
100
+ `isTopLevelAgent(record)` is `parentAgentId === undefined && workflowId === undefined` (`src/agent-manager.ts`). `subagents:rpc:stop` enforces it in its handler (`src/cross-extension-rpc.ts`): a nested child or a workflow's agent is owned by something that is _waiting on it_, and aborting it out from under that owner turns another extension's stop into a failed step. It is defence in depth rather than a live hole — no RPC hands out agent ids, so a caller has no ordinary way to name one it does not own.
103
101
 
104
102
  Two asymmetries to know about, stated as they are:
105
103
 
106
- - **Stop takes an id only** (`src/index.ts:806`). Consume takes an id _or_ an `@handle`, through `resolveAgentRef` (`src/index.ts:816` → `:731-736`).
107
- - **Consume checks `parentAgentId` but not `workflowId`** (`src/index.ts:816`). A workflow-owned agent's result can be marked consumed over the bus even though the same agent cannot be stopped.
104
+ - **Stop takes an id only**: its `getRecord` is the manager's plain lookup. Consume takes an id _or_ an `@handle`: its `consumeResult` (both wired in the `session_start` handler, `src/index.ts`) goes through `resolveAgentRef`.
105
+ - **Consume checks `parentAgentId` but not `workflowId`** (`consumeResult`, `src/index.ts`). A workflow-owned agent's result can be marked consumed over the bus even though the same agent cannot be stopped.
108
106
 
109
- The same predicate silently scopes the events. **Every lifecycle event is top-level only** — `subagents:started`, `:completed`, `:failed` and `:compacted` all return early for nested and workflow-owned agents (`src/index.ts:573`, `:615`, `:631`). A workflow's children are invisible on the bus: you will see the workflow's own agents come and go without a single event.
107
+ The same predicate silently scopes the events. **Every lifecycle event is top-level only** — `subagents:started`, `:completed`, `:failed` and `:compacted` all return early for nested and workflow-owned agents (the `isTopLevelAgent` checks in the `AgentManager` callbacks, `src/index.ts`). A workflow's children are invisible on the bus: you will see the workflow's own agents come and go without a single event.
110
108
 
111
109
  ## The notification race
112
110
 
@@ -114,25 +112,25 @@ When a background agent finishes, pi-subagents sends the user a completion notif
114
112
 
115
113
  **When you send it decides whether it works.** The timeline:
116
114
 
117
- 1. The agent settles and `subagents:completed` is emitted — `src/index.ts:581`.
118
- 2. Eleven lines later, at `src/index.ts:592`, the code checks `record.resultConsumed` and decides whether to notify at all.
115
+ 1. The agent settles, and the manager's completion callback in `src/index.ts` emits `subagents:completed`.
116
+ 2. Next, in the same synchronous callback, it checks `record.resultConsumed` and decides whether to notify at all.
119
117
  3. `pi.events` dispatch is synchronous and in-process, so a handler that emits `subagents:rpc:consume` **without awaiting anything** has already set that flag before step 2 evaluates.
120
118
 
121
- | When you consume | What happens |
122
- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
123
- | Synchronously, inside your `subagents:completed` handler | The notification is never scheduled. This is the clean path |
124
- | After an `await`, within 200 ms | Still suppressed. The nudge is held for `NUDGE_HOLD_MS` (`src/index.ts:451`), `consume` cancels the pending timer (`:819`), and there is a re-check at send time (`:474`) |
125
- | After 200 ms | Too late. The follow-up has fired with `triggerTurn: true` and cost the parent a turn |
119
+ | When you consume | What happens |
120
+ | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
121
+ | Synchronously, inside your `subagents:completed` handler | The notification is never scheduled. This is the clean path |
122
+ | After an `await`, before the notification is sent | Still suppressed. The notification waits in the outbox (`src/completion-outbox.ts`), and consuming the result cancels it there. It is sent about 200 ms after the completion when the parent is idle (`NOTIFICATION_HOLD_MS`), and at the end of the parent's current tool batch when it is busy |
123
+ | After it was sent | Too late. pi has queued it with `triggerTurn: true`, and it costs the parent a model call |
126
124
 
127
- Fire-and-forget is the intended use: the reply carries nothing to act on, and the channel sits outside the `subagents:rpc:ping` version handshake on purpose (`src/cross-extension-rpc.ts:190`), so you can send it unconditionally and an older pi-subagents with no handler simply keeps notifying.
125
+ Fire-and-forget is the intended use: the reply carries nothing to act on, and the channel sits outside the `subagents:rpc:ping` version handshake on purpose (the comment on the consume handler, `src/cross-extension-rpc.ts`), so you can send it unconditionally and an older pi-subagents with no handler simply keeps notifying.
128
126
 
129
- Consumption is not terminal. An `@handle` steer un-consumes the record (`src/index.ts:920`) because the agent's reply to that message still needs relaying, and so does a background resume (`src/agent-manager.ts:1148`) because the record is starting a new run.
127
+ Consumption is not terminal. An `@handle` steer un-consumes the record (the mention handler, `src/index.ts`) because the agent's reply to that message still needs relaying. A background resume (`AgentManager.resume`) does too, because the record is starting a new run, and so does handing a foreground agent to the background (`AgentManager.sendToBackground`), because its result now has to reach the parent as a notification.
130
128
 
131
- One related thing that lives nowhere else: on every top-level settle, pi-subagents writes a session entry — not an event — via `pi.appendEntry("subagents:record", …)` (`src/index.ts:585`), carrying `id`, `type`, `description`, `status`, `result`, `error`, `startedAt` and `completedAt`. It exists for cross-extension history reconstruction. It is append-only history, not something to react to.
129
+ One related thing that lives nowhere else: on every top-level settle, pi-subagents writes a session entry — not an event — via `pi.appendEntry("subagents:record", …)` (the manager's completion callback, `src/index.ts`), carrying `id`, `type`, `description`, `status`, `result`, `error`, `startedAt` and `completedAt`. It exists for cross-extension history reconstruction. It is append-only history, not something to react to.
132
130
 
133
131
  ## The manager registry
134
132
 
135
- `globalThis[Symbol.for("pi-subagents:manager")]` (`src/index.ts:649-659`) is a second integration surface — the standard Node cross-package singleton pattern, no bus involved:
133
+ `globalThis[Symbol.for("pi-subagents:manager")]` (`MANAGER_KEY` and `registryEntry`, `src/index.ts`) is a second integration surface — the standard Node cross-package singleton pattern, no bus involved:
136
134
 
137
135
  | Member | Signature | Notes |
138
136
  | --------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
@@ -141,13 +139,13 @@ One related thing that lives nowhere else: on every top-level settle, pi-subagen
141
139
  | `spawn(pi, ctx, type, prompt, options)` | `=> string` | **Is** `spawnTopLevel`, so the strip list above applies identically |
142
140
  | `getRecord(id)` | `=> AgentRecord \| undefined` | Filtered through `isTopLevelAgent`, so someone else's child comes back `undefined` rather than leaking |
143
141
 
144
- The slot is claimed by the first activation only; subagent sessions re-activate this extension in the same process, and unconditionally overwriting would point the registry at a short-lived child manager whose shutdown would then delete the root session's entry ([#128](https://github.com/tintinweb/pi-subagents/pull/128)). Child activations leave it alone, and shutdown releases it only if this activation claimed it (`src/index.ts:747-750`, `:1105-1107`).
142
+ The slot is claimed by the first activation only; subagent sessions re-activate this extension in the same process, and unconditionally overwriting would point the registry at a short-lived child manager whose shutdown would then delete the root session's entry ([#128](https://github.com/tintinweb/pi-subagents/pull/128)). Child activations leave it alone, and shutdown releases it only if this activation claimed it (`ownsManagerRegistry`, checked in the `session_shutdown` handler).
145
143
 
146
144
  Prefer the bus. The registry has no reply envelope, no version, and no availability event — `globalThis[Symbol.for("pi-subagents:manager")] === undefined` is the only probe you get, and it is also `undefined` in a session that filtered pi-subagents out. Reach for it for the two things the bus has no verb for — _is anything still running_, and _give me a settled record back_ — or for a headless host that wants to block on `waitForAll()` before exiting.
147
145
 
148
146
  ## Protocol versions
149
147
 
150
- `subagents:rpc:ping` replies `{ version: PROTOCOL_VERSION }`, currently `2` (`src/cross-extension-rpc.ts:33`). The constant was introduced already equal to `2` in 0.5.0; "v1" is a retroactive name for the pre-envelope contract, where spawn replied with a bare `{ id }` or `{ error }`, stop replied `{ success: boolean }` with no message, and each handler caught its own errors.
148
+ `subagents:rpc:ping` replies `{ version: PROTOCOL_VERSION }`, currently `2` (`src/cross-extension-rpc.ts`). The constant was introduced already equal to `2` in 0.5.0; "v1" is a retroactive name for the pre-envelope contract, where spawn replied with a bare `{ id }` or `{ error }`, stop replied `{ success: boolean }` with no message, and each handler caught its own errors.
151
149
 
152
150
  Everything added since shipped **without a bump**, because all of it is additive: stop's ownership refusal, string-`model` resolution ([#59](https://github.com/tintinweb/pi-subagents/pull/59)/[#60](https://github.com/tintinweb/pi-subagents/issues/60)), `scopeModels` enforcement ([#240](https://github.com/tintinweb/pi-subagents/issues/240)), and the whole `consume` channel.
153
151
 
@@ -157,11 +155,11 @@ So: send `consume` unconditionally and ignore the outcome — an older build has
157
155
 
158
156
  ## Availability
159
157
 
160
- `subagents:ready` is the discovery signal, and both the RPC handlers and the event itself are wired on the first bound `session_start` (`src/index.ts:789`, `:799`, `:827`) — deliberately not at factory time. pi runs every extension factory _before_ applying an agent's `extensions:` filter and only delivers lifecycle events to the survivors, so a factory-time broadcast made a filtered-out session advertise a spawn service it could never provide: `ping` succeeded and every `spawn` answered `No active session` ([#142](https://github.com/tintinweb/pi-subagents/issues/142)).
158
+ `subagents:ready` is the discovery signal, and both the RPC handlers and the event itself are wired on the first bound `session_start` (its handler in `src/index.ts`) — deliberately not at factory time. pi runs every extension factory _before_ applying an agent's `extensions:` filter and only delivers lifecycle events to the survivors, so a factory-time broadcast made a filtered-out session advertise a spawn service it could never provide: `ping` succeeded and every `spawn` answered `No active session` ([#142](https://github.com/tintinweb/pi-subagents/issues/142)).
161
159
 
162
- The consequence is worth stating plainly: **a session that excludes pi-subagents is indistinguishable from pi-subagents not being installed.** It emits no `subagents:ready` and answers nothing. Give discovery a timeout and treat expiry as "not available here" rather than waiting indefinitely. The payload is `{}` — read nothing off it. Handlers are torn down and the flag reset on `session_shutdown` (`src/index.ts:1100-1103`), so a later `session_start` re-registers and re-emits.
160
+ The consequence is worth stating plainly: **a session that excludes pi-subagents is indistinguishable from pi-subagents not being installed.** It emits no `subagents:ready` and answers nothing. Give discovery a timeout and treat expiry as "not available here" rather than waiting indefinitely. The payload is `{}` — read nothing off it. Handlers are torn down and `rpcHandle` reset in the `session_shutdown` handler, so a later `session_start` re-registers and re-emits.
163
161
 
164
- One more trap on the way in: an RPC-spawned agent emits **no `subagents:created`**. The only two emit sites are the `Agent` tool's background branch (`src/index.ts:2104`) and detached resume (`:1350`). Your first event for your own agent is `subagents:started` (`:625`), so key your bookkeeping off the id that `spawn` handed you, not off `subagents:created`.
162
+ One more trap on the way in: an RPC-spawned agent emits **no `subagents:created`**. The only three emit sites, all in `src/index.ts`, are the `Agent` tool's `run_in_background` branch, a detached resume (`startBackgroundResume`), and a foreground `Agent` call handed to the background with ctrl+b or the viewer's `b` (`sendForegroundToBackground`) — that one fires after the agent's `subagents:started`, as resume does. Your first event for your own agent is `subagents:started` (the manager's start callback), so key your bookkeeping off the id that `spawn` handed you, not off `subagents:created`.
165
163
 
166
164
  ## What the tests pin
167
165
 
package/docs/workflows.md CHANGED
@@ -263,17 +263,17 @@ Spawns one subagent and resolves to its final text — or, with `schema`, to a v
263
263
 
264
264
  **Returns `null` if the agent failed terminally _or_ if you skipped it from the inspector**, indistinguishably. Filter with `.filter(Boolean)` when a `null` would break a later stage, and be careful with in-script retry loops: retrying on `null` will re-run something you deliberately skipped.
265
265
 
266
- | Option | Type | Notes |
267
- | ----------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
268
- | `label` | string | Display name in the progress tree. Also the handle `resume` addresses |
269
- | `phase` | string | Put this agent in a named group, overriding the ambient `phase()`. **Use it inside `pipeline`/`parallel` stages**, where the ambient phase races |
270
- | `agentType` | string | Which agent definition to use. Defaults to `general-purpose`; built-ins are `general-purpose`, `Explore`, `Plan`, plus your custom agents |
271
- | `model` | string | `provider/modelId`, or fuzzy like `haiku` |
272
- | `effort` | string | `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Omitted, the agent definition's own `thinking` decides, then the parent's |
273
- | `isolation` | `"worktree"` | Run in a throwaway git worktree. Only when agents write files in parallel and would collide — it costs setup time and disk per agent |
274
- | `gate` | string | A shell command run after the agent finishes; a non-zero exit fails the agent and its output becomes the error |
275
- | `resume` | string | Continue the child that ran under that label instead of starting fresh |
276
- | `schema` | object | A JSON Schema with an object root. Resolves to the validated object instead of text |
266
+ | Option | Type | Notes |
267
+ | ----------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
268
+ | `label` | string | Display name in the progress tree. Also the handle `resume` addresses |
269
+ | `phase` | string | Put this agent in a named group, overriding the ambient `phase()`. **Use it inside `pipeline`/`parallel` stages**, where the ambient phase races |
270
+ | `agentType` | string | Which agent definition to use. Defaults to `general-purpose`; built-ins are `general-purpose`, `Explore`, `Plan`, plus your custom agents |
271
+ | `model` | string | `provider/modelId`, or fuzzy like `haiku` |
272
+ | `effort` | string | `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Omitted, the agent definition's own `thinking` decides, then the parent's |
273
+ | `isolation` | `"worktree"` | Run in a throwaway git worktree. Only when agents write files in parallel and would collide — it costs setup time and disk per agent. Only this option decides, as in Claude Code — an agent file's `isolation:` does not apply to workflow children |
274
+ | `gate` | string | A shell command run after the agent finishes; a non-zero exit fails the agent and its output becomes the error |
275
+ | `resume` | string | Continue the child that ran under that label instead of starting fresh |
276
+ | `schema` | object | A JSON Schema with an object root. Resolves to the validated object instead of text |
277
277
 
278
278
  Any other key is rejected **by name** at the call. Note that this checks option _keys_, not option _values_ — an `agentType` that names no known agent falls back to `general-purpose` silently.
279
279
 
@@ -309,14 +309,22 @@ The child runs in the _same_ worker and vm context under its own globals, so it
309
309
 
310
310
  ### Where files live
311
311
 
312
- | What | Where |
313
- | ------------------------ | ---------------------------------------------------------------------------------- |
314
- | An inline script, as run | `<tmp>/pi-subagents-<uid>/<encoded-cwd>/<session>/tasks/<run id>.workflow.js` |
315
- | The resume journal | the same directory, `<run id>.workflow.jsonl` |
316
- | Saved workflows | `.pi/workflows/` → `.agents/workflows/` → `<agent dir>/workflows/`, first hit wins |
312
+ | What | Where |
313
+ | ------------------------ | ---------------------------------------------------------------------------------------------------------- |
314
+ | An inline script, as run | `<tmp>/pi-subagents-<uid>/<encoded-cwd>/<session>/tasks/<run id>.workflow.js` |
315
+ | The resume journal | the same directory, `<run id>.workflow.jsonl` |
316
+ | Saved workflows | `.pi/workflows/` → `.agents/workflows/` → `<agent dir>/workflows/` → installed pi packages, first hit wins |
317
317
 
318
318
  The first two are scratch: temp storage, wiped by a reboot or a temp sweep. Only the third is durable, and copying a script there is a manual step.
319
319
 
320
+ A pi package can ship workflow scripts too, by declaring them in its `package.json`:
321
+
322
+ ```json
323
+ { "pi": { "subagents": { "workflows": ["./workflows"] } } }
324
+ ```
325
+
326
+ Package directories are searched **last**, so a script of yours always wins the name, and only packages pi has actually installed are read — see [Shipping agents in a pi package](../README.md#shipping-agents-in-a-pi-package) for the full manifest and how discovery works. A package script still has to carry the `export const meta = …` declaration to count as a workflow. Set `"packageWorkflows": false` in `subagents.json` (or `/agents → Settings → Package workflows`) to leave package scripts out; the setting is separate from `packageAgents` because a workflow is JavaScript this extension executes.
327
+
320
328
  ### Limits and caps
321
329
 
322
330
  | Limit | Value |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-subagents",
3
- "version": "0.19.0",
3
+ "version": "0.22.0-local.1",
4
4
  "description": "Claude Code-style autonomous subagents and workflow orchestration for Pi, with HerbertGao-maintained UI extensions.",
5
5
  "keywords": [
6
6
  "agent",
@@ -58,7 +58,7 @@
58
58
  "typebox": "*"
59
59
  },
60
60
  "engines": {
61
- "node": ">=24"
61
+ "node": ">=22.19.0"
62
62
  },
63
63
  "pi": {
64
64
  "extensions": [
@@ -69,9 +69,10 @@
69
69
  },
70
70
  "x-upstream": {
71
71
  "package": "@tintinweb/pi-subagents",
72
- "version": "0.20.0",
73
- "reviewedVersion": "0.20.0",
72
+ "version": "0.22.0",
73
+ "reviewedVersion": "0.22.0",
74
74
  "repository": "https://github.com/tintinweb/pi-subagents",
75
- "commit": "13106ab6f608b3acc54185da5299ee7fbaabb6b9"
75
+ "commit": "85a704cb8f35c96c0cabfa665b572bab296942e5",
76
+ "trackVersion": true
76
77
  }
77
78
  }