@henryqw/pi-subagent 11.0.1 → 11.0.4

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/README.md CHANGED
@@ -1,19 +1,18 @@
1
1
  # `@henryqw/pi-subagent`
2
2
 
3
- Main is the parent Pi session. It can delegate bounded single, parallel, and chained tasks, plus package-owned Git Flow work, to isolated Pi child roles.
3
+ Main, the parent Pi session, can delegate bounded single, parallel, and chained tasks to isolated child Roles. It also provides a fixed Git Flow for independent implementation work.
4
4
 
5
5
  ![Pi showing six delegated tasks running in parallel](./example.png)
6
6
 
7
7
  ## Why
8
8
 
9
9
  - **Created for**: Delegate bounded work to isolated child Pi processes without losing Main's context.
10
- - **Advantage**: Generic bounded delegation and a deterministic package-owned Git Flow use reusable Role launch policies for package authors.
10
+ - **Advantage**: Generic delegation and Git Flow use the same Role settings.
11
11
 
12
12
  ## Install
13
13
 
14
14
  ```bash
15
15
  pi install npm:@henryqw/pi-task-models
16
- pi install npm:@henryqw/pi-multi-codex
17
16
  pi install npm:@henryqw/pi-subagent
18
17
  ```
19
18
 
@@ -21,228 +20,131 @@ pi install npm:@henryqw/pi-subagent
21
20
 
22
21
  | Package | Why |
23
22
  | --- | --- |
24
- | `@henryqw/pi-multi-codex` | Required. Children can use Main's active Codex slot. |
25
- | `@henryqw/pi-task-models` | Required. Shared `fast` / `balanced` / `frontier` / `fav` routes. |
23
+ | `@henryqw/pi-task-models` | Required. Supplies `fast`, `balanced`, `frontier`, and `fav` model routes. |
26
24
 
27
- Model routing is not configured here. Children resolve routes through shared `@henryqw/pi-task-models` config at `~/.pi/agent/config/pi-task-models/config.json`, which stores only explicit task overrides. The local `pi-subagent/delegateTask` declaration supplies the omitted-class default.
28
-
29
- The local config is optional and uses defaults quietly when missing. If shared task-model config is missing, pi-subagent warns once at session start because delegation needs a route.
25
+ Routes come from `~/.pi/agent/config/pi-task-models/config.json`. It stores explicit task overrides. An omitted class uses pi-subagent's `pi-subagent/delegateTask` default, `fast`. Missing shared model config warns once because delegation needs a route.
30
26
 
31
27
  ## Use
32
28
 
33
- | Surface | Type | Purpose |
34
- | --- | --- | --- |
35
- | `delegate_task` | tool | Generic bounded delegation in one single, parallel, or chain mode. |
36
- | `delegate_flow` | tool | Package-owned parallel implementation and declared-order Git integration for 1–8 independent units. |
37
- | `delegate_flow_continue` | tool | Repair the blocked Flow unit once in its existing worktree. |
38
-
39
- All three delegation tools use Pi's built-in renderer. Its tool-execution block shows live updates and Pi's configured expansion hint; expanding shows the call and result content.
40
-
41
- ### `delegate_task`
29
+ | Tool | Purpose |
30
+ | --- | --- |
31
+ | `delegate_task` | Run one bounded task, independent tasks in parallel, or dependent tasks in a chain. |
32
+ | `delegate_flow` | Implement and integrate 1–8 independent Git units. |
33
+ | `delegate_flow_continue` | Repair the one blocked Flow unit once. |
42
34
 
43
- Select exactly one shape:
35
+ Pi's built-in tool block shows each call and result. Select exactly one `delegate_task` shape:
44
36
 
45
37
  ```text
46
38
  // Single
47
39
  { role, name, task, model?, modelClass?, background? }
48
40
 
49
- // Parallel: 1–8 independent delegations
41
+ // Parallel: 1–8 independent tasks
50
42
  { tasks: [{ role, name, task, model?, modelClass? }], background? }
51
43
 
52
- // Chain: 1–8 dependent delegations
44
+ // Chain: 1–8 dependent tasks
53
45
  { chain: [{ role, name, task, model?, modelClass? }], background? }
54
46
  ```
55
47
 
56
- Main supplies every delegation's required `name`: a short description of about five words and fewer than 30 characters. Names must not contain C0/C1 control characters, including newlines and terminal escape characters.
57
-
58
- #### Model routing
48
+ Main supplies each `name`. It must be a short description, about five words and fewer than 30 characters, with no C0/C1 control characters such as newlines or terminal escapes. `role` and an explicit `model` also reject those controls.
59
49
 
60
- 1. `modelClass` selects a route. It is `fast`, `balanced`, `frontier`, or `fav`.
61
- 2. An omitted class uses pi-subagent's `pi-subagent/delegateTask` declaration. Its default is `fast`.
62
- 3. The route supplies the model and exact thinking level.
63
- 4. An explicit `model` is `provider/modelId`. It replaces only the route model and must support its thinking level.
50
+ `modelClass` selects `fast`, `balanced`, `frontier`, or `fav`. The route sets the model and exact thinking level. An explicit `model` (`provider/modelId`) replaces only the route model and must support that thinking level. `background` applies to the whole selected mode, never one entry.
64
51
 
65
- `background` applies to the whole selected mode. It is never a per-delegation field.
52
+ Parallel tasks start together, settle together, and report in input order. Chains are sequential and fail at the first failure. `{previous}` passes only the immediately preceding successful assistant output. Foreground failures throw after keeping bounded sibling and recovery evidence.
66
53
 
67
- The transient status widget renders one line per child with: status glyph, bracketed uppercase role initial (`[I]` for `implementer`, `[R]` for `reviewer`, and likewise for custom Roles), Main-supplied short name, activity (thinking… or active tool with elapsed time and path basename), and metrics (completed turns, started tools, compact `model·thinking` pair, tokens, total duration). Rows are ordered active-first (working items first, stable insertion order for the rest). A hard six-physical-line maximum applies: when total items are six or fewer, all child rows render; above six, five child rows plus one status-aware overflow line render (`… N more · X working · Y complete · Z failed · W stopped`). Terminal rows clear on the next real user input; active rows persist until the child settles. It is separate from Pi's built-in tool-execution block.
54
+ One call has one aggregate 50 KiB cap for Main-visible text. Background work belongs to its launching session; shutdown or reload aborts it and may leave only recoverable-work evidence or no follow-up message.
68
55
 
69
- #### Modes, limits, and isolation
56
+ Each entry resolves its own Role, resources, route, and optional isolation. A Role with `isolation: worktree` gets a separate deterministic worktree when available. Non-Git and unborn-`HEAD` contexts can use Main's directory. Other setup failures, including unsafe submodule layouts, reject instead of falling back. Siblings and chain steps never share a created worktree.
70
57
 
71
- - Parallel mode starts entries concurrently, waits for every entry, and reports them in input order.
72
- - Chain mode is sequential and fail-fast. Each literal `{previous}` receives only the immediately preceding successful assistant output.
73
- - Foreground failures throw after retaining bounded sibling and recovery evidence.
74
- - One tool call has one aggregate 50 KiB Main-visible transport cap. It is not 50 KiB per child.
75
- - Background workflows are session-scoped. Session shutdown or reload aborts them and may deliver only recoverable-work evidence or no follow-up message.
76
- - Each delegation resolves its own Role, resources, route, and optional worktree request.
77
- - When available, `isolation: worktree` gives each entry a deterministic separate worktree. Non-Git or unborn-`HEAD` contexts may use Main's cwd.
78
- - Siblings and chain steps never implicitly share one created worktree.
58
+ See the [orchestration guide](./docs/orchestration.md) for full delegation, transport, isolation, and UI behavior.
79
59
 
80
- See [Orchestration, isolation, and the public API](./docs/orchestration.md) for generic delegation, Flow behavior, and JavaScript composition examples.
60
+ ### Flow
81
61
 
82
- ### `delegate_flow`
83
-
84
- Use Flow only for independent, commuting Git changes. Commuting changes can integrate in any order.
85
-
86
- Flow accepts 1–8 uniquely identified units. Each unit has a required Main-supplied short `name`, bounded task, optional `modelClass`, direct command/argument validation gate, and optional non-empty `review` judgment criterion. Names must not contain C0/C1 control characters, including newlines and terminal escape characters.
62
+ Flow requires a clean Main worktree on an attached branch with a committed `HEAD`. Use it only for independent Git changes that can merge in any order. Do not split units that overlap files, APIs, schemas, generated output, package metadata, lockfiles, or invariants.
87
63
 
88
64
  ```text
89
65
  delegate_flow({ units: [{ id, name, task, modelClass?, validation: [{ command, args }], review? }] })
90
66
  delegate_flow_continue({ guidance, modelClass? })
91
67
  ```
92
68
 
93
- ![Delegate Flow lifecycle](./docs/delegate-flow.svg)
94
-
95
- Objective verification is authoritative. Flow always inspects committed Git state and runs declared validation.
96
-
97
- A unit without `review` skips review evidence and Reviewer launch. It fast-forwards its exact validated tip through the existing guarded `git merge --ff-only` path.
98
-
99
- Add `review` only for judgment that automation cannot establish. That unit keeps the exact `{base, tip, patchPath}` protocol and requires exact `PASS` before the same integration path.
69
+ A Flow has 1–8 units with unique non-empty IDs. It is memory-only and allows one active Flow. It freezes the effective Implementer at start and freezes the Reviewer only when a unit requests `review`.
100
70
 
101
- Only one memory-only Flow may be active. At start, it resolves and freezes the effective `implementer` Role, including a same-named user override. It resolves and freezes the effective `reviewer` only when at least one unit requests `review`.
71
+ - Each unit gets one worktree. Implementers run in parallel. Flow handles units in declared order.
72
+ - Flow runs each declared command with its arguments. That validation is authoritative for objective checks.
73
+ - Without `review`, Flow fast-forwards the exact validated tip.
74
+ - With `review`, the Reviewer receives the exact `{base, tip, patchPath}` packet and must return exactly `PASS` before the same integration path. Use `review` only for stated judgment that validation cannot decide.
102
75
 
103
- An omitted `modelClass` uses the local `pi-subagent/delegateTask` declaration, which defaults to `fast`. A selected class resolves through its shared profile model-and-thinking route for the unit's Implementer and, when applicable, Reviewer.
76
+ One `delegate_flow_continue` can repair an Implementer, validation, or review block in the same worktree. Omitting its class keeps the unit's class; supplying one changes that repair only. A second block is terminal. Rebase and infrastructure failures are terminal. Flow has no dependency graph, saved recovery, automatic retry, aggregate review, or post-merge validation.
104
77
 
105
- Flow creates one Unit Worktree per unit before it launches Implementers. It runs Implementers in parallel, then processes settled results in declared order.
106
-
107
- After integration, it removes the worktree and branch non-forcibly. A refusal is a completion warning with the retained worktree path and/or branch.
108
-
109
- A rebase that drops all unit commits is a no-op. Flow validates it, skips Reviewer and merge, then cleans up ordinarily.
110
-
111
- Implementer, validation, or review blocks can be repaired once through `delegate_flow_continue` in the same worktree. An omitted continuation `modelClass` retains the blocked unit's current class. A supplied class replaces it for that one repair.
112
-
113
- Rebase and infrastructure failures are terminal. A reported fast-forward failure completes with its diagnostic as a warning only when Git left Main clean at the exact integrated tip.
114
-
115
- Otherwise it is terminal and retains the affected worktree. Flow has no graph, saved recovery, automatic retry, aggregate review, or post-merge gate.
116
-
117
- `delegate_task` keeps its generic isolation behavior. Flow uses the package-shipped Implementer by default and the package-shipped Reviewer only when a unit requests review; the built-in Scout is not part of Flow. Same-named user Roles remain supported overrides.
118
-
119
- ### Delegate UI summary
120
-
121
- The transient status widget shows status glyph, role, status label, task summary, activity, and metrics for each child. Activity is `thinking…` or the active tool with elapsed time and path basename. Metrics are completed turns, started tools, model, thinking level, tokens, and total duration.
122
-
123
- | Aspect | Behavior |
124
- | --- | --- |
125
- | Tool-execution block | Pi's built-in renderer: live updates and expandable call/result content |
126
- | Widget rows | One line per child: glyph, `[role initial]`, short name, activity, metrics |
127
- | Ordering | Active-first stable (working first, then insertion order) |
128
- | Line cap | 6 physical lines max (≤6 items: all child rows; >6 items: 5 rows + 1 status-aware overflow) |
129
- | Terminal retention | Active rows persist; terminal rows clear on next user input |
130
-
131
- - Rows are active-first: working items first, then stable insertion order.
132
- - The hard six-physical-line maximum shows all child rows for six or fewer items.
133
- - Above six, it shows five child rows and one status-aware overflow line: `… N more · X working · Y complete · Z failed · W stopped`.
134
- - Terminal rows clear on the next real user input. Active rows persist until the child settles.
135
- - The final `delegate_task` block is deliberately minimal. It has bounded final summaries and role attribution for parallel and chain work. It shows only retained-worktree recovery paths.
78
+ Flow never force-deletes recoverable work. Failed or uncertain units, and cleanup refusals after integration, retain their worktree path or branch for recovery. See [Flow mechanics and recovery](./docs/orchestration.md#delegate_flow).
136
79
 
137
80
  ## Config
138
81
 
139
- pi-subagent owns the extension-named config directory `~/.pi/agent/config/pi-subagent/`. It holds two kinds of user-owned configuration.
140
-
141
- One Markdown file belongs to each Role (see [Roles](#roles)). The other is its own optional JSON file below.
82
+ pi-subagent owns `~/.pi/agent/config/pi-subagent/config.json`. It is optional. A missing file uses these defaults without a warning.
142
83
 
143
- `~/.pi/agent/config/pi-subagent/config.json` controls the child pool and execution limits. All fields are optional. A missing file uses defaults without a warning.
144
-
145
- | Field | Required | Possible values | Default |
146
- | --- | --- | --- | --- |
147
- | `maxSubagents` | No | Safe integer 1 | `5` |
148
- | `maxTurns` | No | Safe integer 1 | `50` |
149
- | `timeout.idleMinutes` | No | Positive number of minutes where minutes × 60 000 ms ≤ 2,147,483,647 | `10` |
150
- | `timeout.maxMinutes` | No | Positive number within the same ms cap that must be greater than `timeout.idleMinutes`, otherwise the whole `timeout` object falls back to defaults | `30` |
151
-
152
- - Excess children wait FIFO without consuming child timeout.
153
- - A terminal response on turn 50 succeeds.
154
- - An attempted continuation starts no model work and rejects with `turn_limit`.
155
- - Before a continuing turn, the child gets one warning when completed turns reach 80%.
156
- - It gets another warning when elapsed time reaches 80% of the maximum runtime.
157
- - If both thresholds are first reached together, the child gets one combined warning.
158
- - `PI_SUBAGENT_MAX_SUBAGENTS` overrides `maxSubagents` for the session. It must be a positive integer. An invalid value prevents the extension from loading and leaves `delegate_task` unavailable.
159
-
160
- This JSON is read leniently. Malformed JSON, a non-object root, unknown keys, or invalid values are collected into one warning.
161
-
162
- The affected settings fall back to defaults. The file is never rewritten.
163
-
164
- ### Role frontmatter
84
+ | Field | Valid value | Default |
85
+ | --- | --- | --- |
86
+ | `maxSubagents` | Safe integer 1 | `5` |
87
+ | `maxTurns` | Safe integer 1 | `50` |
88
+ | `timeout.idleMinutes` | Positive minutes; minutes × 60,000 2,147,483,647 ms | `10` |
89
+ | `timeout.maxMinutes` | Positive minutes greater than `idleMinutes`; minutes × 60,000 ≤ 2,147,483,647 ms | `30` |
165
90
 
166
- Each Role `.md` file in the same directory accepts these frontmatter fields:
91
+ Excess children wait FIFO without using a child timeout. A terminal response on turn 50 succeeds; an attempted continuation rejects with `turn_limit`.
167
92
 
168
- | Field | Required | Possible values | Default |
169
- | --- | --- | --- | --- |
170
- | `name` | Yes | Non-empty text; unique across roles | — |
171
- | `description` | Yes | Non-empty text | — |
172
- | `tools` | Yes | YAML array of non-empty tool names | `[]` activates no base built-ins; trusted extension tools and caller additions still activate |
173
- | `isolation` | No | `worktree` | None |
174
- | `extensions` | Yes | YAML array of absolute paths, `~/…`, `file://`, or package sources (`npm:`, `git:`, `github:`, `https?:`, `ssh:`) | `[]` selects no Role extension bundle |
175
- | `skills` | Yes | YAML array of non-empty Skill names | `[]` selects no separately named Role Skills; trusted extension Skills still load |
176
- | body | Yes | System-prompt Markdown after the frontmatter | — |
93
+ Malformed or unreadable JSON, a non-object root, unknown keys, and invalid values produce one warning. Invalid settings use defaults while valid settings still apply. If the effective maximum is not greater than the idle timeout, both timeout settings use defaults. The file is never rewritten.
177
94
 
178
- An unreadable or invalid Role file fails role loading fast. Duplicate role names are rejected.
95
+ `PI_SUBAGENT_MAX_SUBAGENTS` overrides `maxSubagents` for the session. It must be a positive integer. An invalid value prevents the extension from loading, so `delegate_task` is unavailable.
179
96
 
180
97
  ## Roles
181
98
 
182
- The package ships three working built-in Roles. They are always available without configuration.
99
+ Role Markdown files live beside the config file. They require frontmatter and a Markdown system prompt body.
183
100
 
184
- - `implementer`: focused edits requesting worktree isolation; commits completed scoped changes locally and never pushes or opens PRs without authorization
185
- - `reviewer`: read-only correctness review of supplied plans or files, or—only when a Flow unit declares `review`—of Flow's exact `{base, tip, patchPath}` packet in its Unit Worktree; never edits or commits
186
- - `scout`: read-only code and evidence mapping for one bounded task; never changes files
101
+ | Field | Requirement |
102
+ | --- | --- |
103
+ | `name` | Required unique, non-empty text without C0/C1 controls. |
104
+ | `description` | Required non-empty text without C0/C1 controls. |
105
+ | `tools` | Required YAML array of non-empty tool names. `[]` selects no base built-ins. |
106
+ | `isolation` | Optional; only `worktree`. |
107
+ | `extensions` | Required YAML array. Entries are absolute paths, `~/…`, `file://`, or `npm:`, `git:`, `github:`, `https?:`, or `ssh:` sources. |
108
+ | `skills` | Required YAML array of non-empty Skill names. |
109
+ | body | Required Markdown system prompt after the frontmatter. |
110
+
111
+ An unreadable or invalid Role fails loading fast. Duplicate Role names are rejected. A same-named user file overrides a built-in Role.
187
112
 
188
- A same-named Markdown file in `~/.pi/agent/config/pi-subagent/` explicitly overrides the built-in default. The built-in `scout` is available to generic `delegate_task`; `delegate_flow` remains limited to its fixed Implementer/Reviewer protocol.
113
+ The package always provides these built-in Roles:
189
114
 
190
- The repository also includes one optional inert sample:
115
+ | Role | Purpose | Isolation/use |
116
+ | --- | --- | --- |
117
+ | `implementer` | Make and validate one focused change. | Requests a worktree; commits scoped work locally. Never pushes or opens a PR without permission. |
118
+ | `reviewer` | Review supplied plans or files for correctness. | Read-only. For Flow review, reads the exact packet in the Unit Worktree; never edits or commits. |
119
+ | `scout` | Map code and evidence for one bounded task. | Read-only and generic `delegate_task` only; never changes files. |
191
120
 
192
- - [`synthesizer`](./examples/roles/synthesizer.md): reconcile supplied reports
121
+ Flow uses the effective Implementer and, only when requested, Reviewer. The Scout is not part of Flow.
193
122
 
194
- Copy it manually from your installed `@henryqw/pi-subagent` package if you want a starting point. npm installs ship the `examples/roles/` directory.
123
+ [`synthesizer`](./examples/roles/synthesizer.md) is an optional sample for reconciling supplied reports. It does nothing until you copy it:
195
124
 
196
125
  ```bash
197
126
  mkdir -p ~/.pi/agent/config/pi-subagent
198
127
  cp <package-install-dir>/examples/roles/synthesizer.md ~/.pi/agent/config/pi-subagent/
199
128
  ```
200
129
 
201
- Locate the install directory with `npm root` inside your project, or through Pi's package installation path.
202
-
203
- The package never installs or writes Role configuration. After copying, edit or replace `synthesizer.md` as your own Role.
204
-
205
- ## Skill
206
-
207
- The bundled [`pi-subagent-delegated-development`](./skills/pi-subagent-delegated-development/SKILL.md) Skill is Main-side planner and orchestrator policy only. `delegate_flow` owns its fixed Git mechanics and validation authority.
208
-
209
- The Skill adds no runtime code, configuration, or Role installation. Generic orchestration remains outside the executor under [ADR 001](./docs/adr/001-composable-ephemeral-execution.md).
210
-
211
- ## Role, Skill, and resource trust
212
-
213
- ### Role resources
214
-
215
- A Role explicitly owns base tools, extensions, named Skills, instructions, and optional `isolation: worktree`. Every launch installs its Role tool policy.
130
+ Use `npm root` in a project to find the package install directory. The package never installs or changes this file; after copying, it is yours.
216
131
 
217
- Named Skills resolve through Main's effective Pi Skill registry. Ambient extension and Skill discovery is disabled in children.
132
+ ## Role resources and trust
218
133
 
219
- ### Trusted extensions are not sandboxing
134
+ A Role selects base tools, extensions, named Skills, instructions, and optional worktree isolation. Named Skills resolve from Main's effective Pi registry; unavailable ones warn and skip.
220
135
 
221
- Selecting an extension explicitly selects a trusted atomic capability bundle, not just a provider path. Every tool it registers and every Skill supplied through its Pi package metadata or dynamic `resources_discover` loads alongside separately named Role Skills.
136
+ Children disable ambient extension and Skill discovery. `tools: []` adds no base tools, but selected extension tools and caller tools still activate. `extensions: []` adds no Role extension bundle. `skills: []` adds no separately named Role Skills, but selected extension Skills still load.
222
137
 
223
- This is intentional. An extension may depend on its tools, Skills, lifecycle, and prompt behavior. Loading it permits that executable behavior and is not sandboxing.
138
+ An explicitly selected extension is trusted, not sandboxed. Its tools, Skills, and executable behavior load together. Select fewer trusted extensions to reduce scope. pi-subagent does not guess or remove undocumented dependencies.
224
139
 
225
- Scope a child by selecting fewer trusted extensions. Finer-grained selection requires separate extension entry points or configuration, or an upstream split. pi-subagent does not infer or externally narrow undocumented dependencies.
140
+ Parent-only delegation tools and `ask_question` are always excluded. Requested Role or caller tool names are checked after provider loading; unavailable tools fail before the first model turn.
226
141
 
227
- ### Tool filtering
142
+ ## Skill
228
143
 
229
- - `tools: []` activates no base built-ins. Trusted selected extension tools and explicit caller tool additions still activate.
230
- - `skills: []` selects no separately named Role Skills. Trusted selected extension Skills still load.
231
- - `extensions: []` selects no Role extension bundle.
232
- - Parent-only recursive orchestration tools and interactive `ask_question` are excluded from children.
233
- - Explicit Role or caller tool names are verified against the child’s final filtered active registry after provider extensions finish `session_start`.
234
- - Unavailable tool names fail before the first model turn with provider-extension guidance. Unavailable named Skills warn and skip.
144
+ The bundled [`pi-subagent-delegated-development`](./skills/pi-subagent-delegated-development/SKILL.md) Skill guides Main's planning and orchestration. It adds no runtime code, config, or Role installation. `delegate_flow` owns its Git mechanics and validation authority.
235
145
 
236
146
  ## Library API
237
147
 
238
- The package root exports Role loading and launch resolution, `createEphemeralSubagentExecutor`, and worktree helpers.
239
-
240
- The executor is for code already running inside active Pi. It does not provide standalone Node.js Pi discovery or launch support.
241
-
242
- It defaults to a hard limit of 50 turns. An attempted continuation rejects with `turn_limit` while preserving accumulated usage and bounded output.
243
-
244
- After Pi exits, the executor drains inherited stdout and stderr normally. It destroys streams held by escaped descendants after a short inactivity deadline or one-second hard deadline, so they cannot retain a pool permit.
245
-
246
- Use [`docs/orchestration.md`](./docs/orchestration.md#public-role-and-executor-api) for exact API behavior.
148
+ The package root exports `loadRoles`, `resolveRoleSkills`, `resolveRoleLaunch`, `createRoleLaunch`, `createEphemeralSubagentExecutor`, and worktree helpers. The executor works only inside the active Pi process; it does not discover or start a standalone Node.js Pi installation.
247
149
 
248
- It includes a post-permit `prepare` example. The example uses `resolveRoleLaunch` with a caller-owned Model Task declaration against the latest Pi context.
150
+ See the [public Role and executor API](./docs/orchestration.md#public-role-and-executor-api) for contracts and a `prepare` example.
@@ -0,0 +1,5 @@
1
+ export declare const DISPLAY_TEXT_CONTRACT: {
2
+ readonly controlRanges: "\\u0000-\\u001F\\u007F-\\u009F";
3
+ readonly pattern: "^(?![\\s\\S]*[\\u0000-\\u001F\\u007F-\\u009F])[\\s\\S]+$";
4
+ };
5
+ export declare function hasDisplayControlCharacters(value: string): boolean;
@@ -0,0 +1,9 @@
1
+ const C0_C1_CONTROL_RANGES = "\\u0000-\\u001F\\u007F-\\u009F";
2
+ export const DISPLAY_TEXT_CONTRACT = {
3
+ controlRanges: C0_C1_CONTROL_RANGES,
4
+ pattern: `^(?![\\s\\S]*[${C0_C1_CONTROL_RANGES}])[\\s\\S]+$`,
5
+ };
6
+ const DISPLAY_TEXT_CONTROL_PATTERN = new RegExp(`[${DISPLAY_TEXT_CONTRACT.controlRanges}]`, "u");
7
+ export function hasDisplayControlCharacters(value) {
8
+ return DISPLAY_TEXT_CONTROL_PATTERN.test(value);
9
+ }
package/dist/ephemeral.js CHANGED
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { basename } from "node:path";
4
4
  import { StringDecoder } from "node:string_decoder";
5
+ import { hasDisplayControlCharacters } from "./display-text.js";
5
6
  const MAX_OUTPUT_BYTES = 50 * 1024;
6
7
  const MAX_JSON_EVENT_BYTES = 1024 * 1024;
7
8
  export const DEFAULT_MAX_TURNS = 50;
@@ -249,10 +250,7 @@ function activityTooLong(value) {
249
250
  return typeof value === "string" && Buffer.byteLength(value, "utf8") > MAX_ACTIVITY_TEXT_BYTES;
250
251
  }
251
252
  function hasTerminalControlChars(text) {
252
- return Array.from(text).some((character) => {
253
- const code = character.codePointAt(0);
254
- return code <= 0x1f || code >= 0x7f && code <= 0x9f || code === 0x2028 || code === 0x2029;
255
- });
253
+ return hasDisplayControlCharacters(text) || /[\u2028\u2029]/u.test(text);
256
254
  }
257
255
  function activityText(value) {
258
256
  return typeof value === "string" && value.trim().length > 0 && !activityTooLong(value) && !hasTerminalControlChars(value);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
3
+ export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.ts";
3
4
  export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
4
5
  export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
5
6
  export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
package/dist/index.js CHANGED
@@ -3,7 +3,9 @@ import { isAbsolute, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
5
5
  import { extensionConfigDir } from "@henryqw/pi-config-store";
6
+ import { hasDisplayControlCharacters } from "./display-text.js";
6
7
  import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
8
+ export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
7
9
  export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
8
10
  export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, } from "./worktree.js";
9
11
  export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, } from "./review-evidence.js";
@@ -26,6 +28,12 @@ const cleanText = (value, field, source) => {
26
28
  }
27
29
  return value.trim();
28
30
  };
31
+ const cleanDisplayText = (value, field, source) => {
32
+ if (typeof value === "string" && hasDisplayControlCharacters(value)) {
33
+ throw new Error(`${source}: ${field} must not contain C0/C1 control characters.`);
34
+ }
35
+ return cleanText(value, field, source);
36
+ };
29
37
  const stringList = (value, field, source) => {
30
38
  if (value === undefined)
31
39
  throw new Error(`${source}: ${field} is required.`);
@@ -62,8 +70,8 @@ function parseRoleFile(file, raw) {
62
70
  if (isolation !== undefined && isolation !== "worktree")
63
71
  throw new Error(`${file}: isolation must be "worktree".`);
64
72
  return {
65
- name: cleanText(frontmatter.name, "name", file),
66
- description: cleanText(frontmatter.description, "description", file),
73
+ name: cleanDisplayText(frontmatter.name, "name", file),
74
+ description: cleanDisplayText(frontmatter.description, "description", file),
67
75
  tools: stringList(frontmatter.tools, "tools", file),
68
76
  isolation,
69
77
  extensions: extensionList(frontmatter.extensions, file),
@@ -19,7 +19,6 @@ import {
19
19
  } from "@henryqw/pi-subagent";
20
20
  import { Type, type Static } from "typebox";
21
21
  import { Check } from "typebox/value";
22
- import { runDelegation } from "./delegation.ts";
23
22
  import { MODEL_CLASS_GUIDANCE } from "./model-class-policy.ts";
24
23
  import { TASK_NAME_CONTRACT, TaskNameSchema, normalizeTaskName } from "./task-name.ts";
25
24
 
@@ -356,7 +355,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
356
355
  let started = false;
357
356
  try {
358
357
  assertCurrent(flow);
359
- const result = await runDelegation(runtime.executor, {
358
+ const result = await runtime.executor.run({
360
359
  signal,
361
360
  onTokens: (tokens) => runtime.updateWidgetTokens(widgetId, tokens),
362
361
  onActivity: (event) => runtime.updateWidgetActivity(widgetId, event),
@@ -34,7 +34,6 @@ import {
34
34
  } from "@henryqw/pi-subagent";
35
35
  import { DEFAULT_TIMEOUT_CONFIG, readSubagentConfig, type SubagentTimeoutConfig } from "./config.ts";
36
36
  import { registerDelegateFlow } from "./delegate-flow.ts";
37
- import { runDelegation } from "./delegation.ts";
38
37
  import { MODEL_CLASS_GUIDANCE } from "./model-class-policy.ts";
39
38
  import {
40
39
  formatBackgroundWorkflowResult,
@@ -641,7 +640,7 @@ export default function subagentExtension(
641
640
  : { ...base, status: nextStatus, assistantOutput: nextText });
642
641
  };
643
642
  try {
644
- child = await runDelegation(executor, {
643
+ child = await executor.run({
645
644
  signal: workflowSignal,
646
645
  onUpdate: (output) => {
647
646
  setState("running", output);
@@ -1,7 +1,7 @@
1
+ import { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "@henryqw/pi-subagent";
1
2
  import { Type, type Static } from "typebox";
2
3
 
3
4
  const TASK_NAME_MAX_LENGTH = 29 as const;
4
- const TASK_NAME_CONTROL_RANGES = "\\u0000-\\u001F\\u007F-\\u009F" as const;
5
5
  const TASK_NAME_LIMIT_WORDING = `about five words and fewer than ${TASK_NAME_MAX_LENGTH + 1} characters`;
6
6
  const TASK_NAME_WORDING = `short descriptive name of ${TASK_NAME_LIMIT_WORDING}`;
7
7
 
@@ -10,8 +10,8 @@ export const TASK_NAME_CONTRACT = {
10
10
  maxLength: TASK_NAME_MAX_LENGTH,
11
11
  description: `Short descriptive task name, ${TASK_NAME_LIMIT_WORDING}; C0/C1 control characters are rejected.`,
12
12
  promptGuidance: `Use a ${TASK_NAME_WORDING} without C0/C1 control characters.`,
13
- controlRanges: TASK_NAME_CONTROL_RANGES,
14
- pattern: `^(?![\\s\\S]*[${TASK_NAME_CONTROL_RANGES}])[\\s\\S]+$`,
13
+ controlRanges: DISPLAY_TEXT_CONTRACT.controlRanges,
14
+ pattern: DISPLAY_TEXT_CONTRACT.pattern,
15
15
  } as const;
16
16
 
17
17
  export const TaskNameSchema = Type.String({
@@ -23,10 +23,8 @@ export const TaskNameSchema = Type.String({
23
23
 
24
24
  export type TaskName = Static<typeof TaskNameSchema>;
25
25
 
26
- const TASK_NAME_CONTROL_PATTERN = new RegExp(`[${TASK_NAME_CONTRACT.controlRanges}]`, "u");
27
-
28
26
  export function normalizeTaskName(value: TaskName, path: string): TaskName {
29
- if (TASK_NAME_CONTROL_PATTERN.test(value)) throw new Error(`${path} must not contain C0/C1 control characters.`);
27
+ if (hasDisplayControlCharacters(value)) throw new Error(`${path} must not contain C0/C1 control characters.`);
30
28
  const normalized = value.trim();
31
29
  if (!normalized) throw new Error(`${path} must be non-empty text.`);
32
30
  return normalized;
@@ -1,4 +1,5 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
+ import { DISPLAY_TEXT_CONTRACT } from "@henryqw/pi-subagent";
2
3
  import { PROFILE_NAMES } from "@henryqw/pi-task-models";
3
4
  import { Type, type Static } from "typebox";
4
5
  import { Check } from "typebox/value";
@@ -6,9 +7,9 @@ import { TaskNameSchema, normalizeTaskName } from "./task-name.ts";
6
7
 
7
8
  export const MAX_WORKFLOW_ENTRIES = 8;
8
9
 
9
- const RoleSchema = Type.String({ minLength: 1, description: "Configured Subagent role name" });
10
+ const RoleSchema = Type.String({ minLength: 1, pattern: DISPLAY_TEXT_CONTRACT.pattern, description: "Configured Subagent role name" });
10
11
  const TaskSchema = Type.String({ minLength: 1, description: "Bounded task packet" });
11
- const ModelSchema = Type.String({ minLength: 1, description: "Designated model as provider/modelId; replaces the selected route model" });
12
+ const ModelSchema = Type.String({ minLength: 1, pattern: DISPLAY_TEXT_CONTRACT.pattern, description: "Designated model as provider/modelId; replaces the selected route model" });
12
13
  const ModelClassSchema = StringEnum(PROFILE_NAMES, { description: "Task model profile" });
13
14
 
14
15
  export const DelegationSchema = Type.Object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "11.0.1",
3
+ "version": "11.0.4",
4
4
  "description": "Delegate bounded single, parallel, or chained tasks to isolated Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1,9 +0,0 @@
1
- import type { EphemeralSubagentExecutor, EphemeralSubagentResult, EphemeralSubagentRunInput } from "@henryqw/pi-subagent";
2
-
3
- /** Runs one caller-prepared child launch without making lifecycle decisions. */
4
- export function runDelegation(
5
- executor: EphemeralSubagentExecutor,
6
- input: EphemeralSubagentRunInput,
7
- ): Promise<EphemeralSubagentResult> {
8
- return executor.run(input);
9
- }