@henryqw/pi-subagent 18.0.1 → 22.0.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.
Files changed (47) hide show
  1. package/CONTEXT.md +76 -24
  2. package/README.md +116 -142
  3. package/dist/composition.d.ts +11 -0
  4. package/dist/composition.js +107 -0
  5. package/dist/direct-herdr.d.ts +17 -0
  6. package/dist/direct-herdr.js +219 -0
  7. package/dist/ephemeral.d.ts +2 -2
  8. package/dist/ephemeral.js +7 -7
  9. package/dist/git-runtime.d.ts +110 -0
  10. package/dist/git-runtime.js +472 -0
  11. package/dist/herdr-runtime.d.ts +137 -0
  12. package/dist/herdr-runtime.js +1228 -0
  13. package/dist/index.d.ts +11 -5
  14. package/dist/index.js +20 -16
  15. package/dist/integration-git.d.ts +59 -0
  16. package/dist/integration-git.js +484 -0
  17. package/dist/launch-runtime.d.ts +54 -0
  18. package/dist/launch-runtime.js +195 -0
  19. package/dist/process.d.ts +9 -0
  20. package/dist/process.js +11 -0
  21. package/dist/runner.d.ts +356 -0
  22. package/dist/runner.js +2710 -0
  23. package/dist/schema.d.ts +2234 -0
  24. package/dist/schema.js +1236 -0
  25. package/dist/store.d.ts +50 -0
  26. package/dist/store.js +288 -0
  27. package/dist/working-evidence.d.ts +25 -0
  28. package/dist/working-evidence.js +261 -0
  29. package/docs/adr/001-composable-ephemeral-execution.md +3 -3
  30. package/docs/delegation-routing.html +89 -0
  31. package/docs/delegation-routing.svg +78 -0
  32. package/docs/orchestration-architecture.html +238 -0
  33. package/docs/orchestration-architecture.svg +189 -0
  34. package/docs/orchestration.md +122 -331
  35. package/example.png +0 -0
  36. package/examples/roles/implementer.md +2 -3
  37. package/extensions/admission.ts +66 -0
  38. package/extensions/config.ts +51 -37
  39. package/extensions/isolated.ts +638 -0
  40. package/extensions/result-transport.ts +13 -103
  41. package/extensions/role-tools.ts +11 -11
  42. package/extensions/subagent-command.ts +167 -0
  43. package/extensions/subagent.ts +264 -540
  44. package/extensions/task-name.ts +1 -2
  45. package/extensions/workflow.ts +60 -96
  46. package/package.json +14 -6
  47. package/skills/pi-subagent/SKILL.md +35 -0
@@ -1,406 +1,197 @@
1
1
  # Orchestration and package-author API
2
2
 
3
- `pi-subagent` separates generic delegation from its execution mechanism:
3
+ `pi-subagent` owns both delegation paths behind `delegate_task`:
4
4
 
5
5
  ```text
6
- Role (built-in or user override) + latest Pi registries ── resolveRoleLaunch ──> PiLaunch
7
- │
8
- caller-owned Model Task declaration, cwd, signal ────────┤
9
- v
10
- active-Pi ephemeral executor
6
+ ┌─ direct ─── read-only Herdr tab in Main's workspace
7
+ request + Role + route ──┤
8
+ └─ isolated ─ Herdr worktree, checked candidate, durable state
11
9
  ```
12
10
 
13
- Main plans and orchestrates. `delegate_task` remains lightweight generic delegation with flat single, parallel, and chain modes. The public executor runs one prepared delegation. Downstream packages compose their own workflows with ordinary JavaScript and own semantic protocols, shared workspace and state, checks, integration, retry decisions, and bounds. There is no recursive workflow AST.
11
+ Both paths resolve Roles and model routes from Pi's effective registries. They share the package's global concurrency, turn/token, timeout, and correction policy. A request cannot supply or refill execution limits.
14
12
 
15
- ## Frozen `delegate_task` contract
13
+ ## Direct contract
16
14
 
17
- A call selects exactly one of these shapes. Unknown properties and nested modes are rejected.
18
-
19
- ### Single
15
+ Direct mode selects one single, parallel, or chain workflow:
20
16
 
21
17
  ```json
22
18
  {
23
- "role": "user-configured-role",
24
- "name": "Inspect authentication flow",
25
- "task": "One bounded task packet",
26
- "modelClass": "balanced",
27
- "background": false
19
+ "mode": "direct",
20
+ "role": "scout",
21
+ "name": "Inspect auth flow",
22
+ "task": "Return the relevant files and risks.",
23
+ "kind": "text",
24
+ "modelClass": "fast"
28
25
  }
29
26
  ```
30
27
 
31
- Single mode puts one delegation's fields at the top level.
32
-
33
- ### Parallel
34
-
35
28
  ```json
36
29
  {
30
+ "mode": "direct",
37
31
  "tasks": [
38
- { "role": "user-role-a", "name": "Inspect subsystem A", "task": "Inspect subsystem A", "modelClass": "fast" },
39
- { "role": "user-role-b", "name": "Inspect subsystem B", "task": "Inspect subsystem B", "model": "provider/model-id" }
40
- ],
41
- "background": false
32
+ { "role": "scout", "name": "Inspect service", "task": "Inspect the service boundary." },
33
+ { "role": "reviewer", "name": "Inspect tests", "task": "Inspect the test contract." }
34
+ ]
42
35
  }
43
36
  ```
44
37
 
45
- `tasks` contains 1–8 independent delegations. All entries start concurrently subject to the shared executor cap, all settle even when a sibling fails, and outcomes remain in input order rather than completion order.
46
-
47
- ### Chain
48
-
49
38
  ```json
50
39
  {
40
+ "mode": "direct",
51
41
  "chain": [
52
- { "role": "user-role-a", "name": "Collect evidence", "task": "Collect evidence" },
53
- { "role": "user-role-b", "name": "Review collected evidence", "task": "Review this evidence:\n{previous}" },
54
- { "role": "user-role-c", "name": "Summarize evidence review", "task": "Summarize this review:\n{previous}" }
55
- ],
56
- "background": false
42
+ { "role": "scout", "name": "Collect evidence", "task": "Collect exact evidence." },
43
+ { "role": "reviewer", "name": "Review evidence", "task": "Review this report:\n{previous}" }
44
+ ]
57
45
  }
58
46
  ```
59
47
 
60
- `chain` contains 1–8 sequential delegations. Before an entry starts, every literal `{previous}` in its task is replaced with the immediately preceding successful assistant output; the first entry receives an empty string. Replacement is literal and non-recursive. The chain stops at its first child or infrastructure failure.
48
+ Unknown properties and mixed shapes are rejected.
61
49
 
62
50
  ### Delegation fields
63
51
 
52
+ Each entry has:
53
+
64
54
  | Field | Required | Contract |
65
55
  | --- | --- | --- |
66
- | `role` | yes | Name of a Role in the user's effective `config/pi-subagent` directory or a package-shipped built-in (`implementer`, `reviewer`, `scout`); a same-named user file overrides the built-in. |
67
- | `name` | yes | Main-supplied short task name: about five words and fewer than 30 characters; C0/C1 control characters are rejected. |
68
- | `task` | yes | Non-empty bounded task packet. |
69
- | `model` | no | Designated `provider/modelId`; replaces only the selected route model and must support that route's thinking level. |
70
- | `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav`; selects the route. Main prioritizes `fast` for straightforward work and `balanced` for complex work. It reserves `frontier` for exceptionally complex or tricky work. Omission uses the selected Role default, then pi-subagent's local Model Task assignment or declared default. |
71
-
72
- Those five fields are the complete delegation object. An explicit call `modelClass` wins over the selected Role's optional default. If neither is present, the local Model Task assignment or declared default selects the route. Routes set exact thinking levels. A direct model never changes that level and fails before launch if it cannot support it. `tasks`, `chain`, and `background` cannot be nested. Route fallback occurs only before launch; a started child is never retried by this package.
73
-
74
- ### Background, failures, and transport
75
-
76
- `background` is top-level policy for the whole selected mode. Foreground blocks until that mode settles. Background returns one acknowledgement and, while its launching session remains active, later delivers the settled workflow outcome; callers cannot background only one entry in an array. Session shutdown or reload aborts unfinished background work, which may deliver only recoverable isolated-work evidence or no follow-up message.
77
-
78
- Any foreground entry failure makes the tool call throw. Parallel mode first settles every sibling, while chain mode stops immediately. The thrown failure retains available successful sibling output, failed/rejected entry evidence, and worktree recovery reports so useful work remains locatable.
79
-
80
- All Main-visible text for one tool call shares one aggregate 50 KiB UTF-8 transport cap, including child output, sibling failures, and worktree/recovery evidence. Parallel execution does not multiply the cap by its entry count. Truncation is explicit; internal bookkeeping is not made visible by bypassing the cap.
81
-
82
- ## Per-delegation resources and isolation
83
-
84
- `delegate_task` first preflights every requested Role name, so an initially unknown Role starts no sibling. Then, after receiving an executor permit, every single entry, parallel sibling, and chain step independently:
85
-
86
- 1. reloads its effective Role by requested name;
87
- 2. resolves its route and named Skills from the latest effective Pi context;
88
- 3. creates its Role launch policy; and
89
- 4. when the reloaded Role requests `isolation: worktree`, creates a worktree identified by the tool call, mode, and input index.
90
-
91
- Separate deterministic identities produce separate worktree paths and branches. Parallel siblings cannot collide, and a chain does not base one step's worktree on the preceding step's branch. `{previous}` passes text only. There is no implicit shared worktree or hidden workflow state.
92
-
93
- A worktree starts from Main's current `HEAD`. Clean worktrees with no child commits are pruned; committed, dirty, switched, unmeasurable, or otherwise recoverable work is preserved and reported. For `delegate_task`, non-git directories and repositories with an unborn `HEAD` use Main's working directory. Git submodules reject worktree isolation, and setup failure in a real repository throws rather than silently sharing Main's checkout.
94
-
95
- If steps must share files, make that an explicit caller decision: use an intentionally shared workspace, merge preserved child commits, or pass state through a caller-owned store. Do not rely on chain order to imply filesystem sharing.
96
-
97
- ## Resource Policy
98
-
99
- Every Role launch centrally prepends this child identity contract to its system instructions before the Role prompt: the child is a delegated Pi Subagent, not Main; it executes its assigned Role and task directly; Main-only delegation rules do not apply; recursive delegation is unavailable and it must not seek or invoke delegation tools.
100
-
101
- A Role file requires:
56
+ | `role` | yes | Effective configured Role name |
57
+ | `name` | yes | Short display name without terminal controls |
58
+ | `task` | yes | Non-empty task packet |
59
+ | `kind` | no | `text` only (default); direct `changeset` is rejected |
60
+ | `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav` route |
61
+ | `model` | no | `provider/modelId`; replaces only the route model |
102
62
 
103
- - base tools: a YAML array; `tools: []` activates no base built-ins, while trusted selected extension tools still activate;
104
- - explicit extension paths or package sources: a YAML array; `extensions: []` selects no Role extension bundle;
105
- - additional effective Pi Skill names: a YAML array; `skills: []` selects no separately named Role Skills, while trusted selected extension Skills still load;
106
- - optional exact MCP server names: `mcps: []` or an omitted field denies MCP access, while allowed servers expose all their tools through `pi-mcp-adapter`;
107
- - system instructions;
108
- - optional `modelClass` default (`fast`, `balanced`, `frontier`, or `fav`); and
109
- - optional `isolation: worktree` for the tool layer.
63
+ An explicit model must support the route's thinking level. A launched child is never retried automatically after provider or process failure.
110
64
 
111
- Every launch installs the Role tool policy. At launch, a package caller may add `tools`, `extensions`, and `env`; caller tools are unioned into the Role base list and loaded extension tools activate in every case. Caller `env` adds to or overrides the active Pi process environment for the child.
65
+ Only Roles with known read-only tools and no extensions or MCP servers may run direct. A direct `changeset` or write-capable Role is rejected; use isolated mode for implementation. Parallel reads run concurrently and report in request order. A chain substitutes only the preceding successful output and stops at its first failure.
112
66
 
113
- Children start with ambient extension and Skill discovery disabled. Only explicit Role or caller extensions, resolved Skill paths, extension package resources, and required internal tool-policy, MCP, or Codex adapters load. A non-empty `mcps` list requires an installed `pi-mcp-adapter`. The MCP adapter receives an isolated in-memory config containing only the named servers. Unknown server names fail before the first model turn, and direct adapter loading through `extensions` is rejected. Loaded extension tools activate even when the Role base list is empty. Child-inappropriate parent tools are always excluded: `delegate_task`, `orchestrate_execute`, `orchestrate_status`, `orchestrate_resume`, `orchestrate_abort`, and `ask_question`. Explicit Role or caller tool names are verified against the final filtered registry after each provider extension completes `session_start`. Unavailable names fail before the first model turn and identify the missing names with provider guidance.
67
+ The tool returns a handle after the first exact Herdr tab is launched. The remaining tabs are launched asynchronously; their identities are recorded on the session branch. A session-owned observer awaits each exact agent's lifecycle and retrieves its final answer from its Pi session file, not its possibly truncated screen. Idle, blocked, unknown, malformed, or oversized answers fail with recoverable tab/session identity; no whole-run deadline is imposed. `/subagent` shows exact recorded tabs on this session branch, distinguishing locally observed work from unobserved records. A session switch stops observation and carries tab identity into the new branch.
114
68
 
115
- Role Skill names resolve through Main's effective Pi Skill registry at launch. Missing names are returned in `ResolvedRoleLaunch.missingSkills`. `prepareRoleLaunch` and `resolveConfiguredRoleLaunch` reject a nonempty list with `Role <name> requires missing Skills: ...` before returning a finalizable prepared launch. `delegate_task` reports that failure as a workflow error and does not start a child. Raw launch callers must reject `missingSkills` before launching.
69
+ The result is delivered to Main with `triggerTurn: true, deliverAs: "followUp"`. Busy Main finishes its current turn before consuming the result; idle Main starts a turn. Herdr reports worker lifecycle but never prompts Main directly. Read-only workers can observe a concurrently changing checkout; consumers must account for drift rather than treating a tab as file isolation.
116
70
 
117
- ### Role final-turn handoff
71
+ ## Isolated contract
118
72
 
119
- Role launches made by `createRoleLaunch` reserve a response-only handoff at either boundary. They use the penultimate `maxTurns` turn or a continuing `maxTokens` crossing. This includes `delegate_task` and library Role launches made with the public launch API.
73
+ An isolated call is a checked directed graph:
120
74
 
121
- With `maxTurns` set to 1, Pi disables tools during `session_start`. The sole provider turn is the response-only handoff.
75
+ ```text
76
+ {
77
+ mode: "isolated",
78
+ id,
79
+ goal,
80
+ tasks: [1..8],
81
+ finalChecks?,
82
+ finalJudgment?
83
+ }
84
+ ```
122
85
 
123
- After a continuing boundary turn, Pi completes its tools. It then disables every active tool and queues one structured final handoff. A terminal boundary response succeeds without a handoff. If the child continues after the token handoff, the executor rejects with `token_limit`.
86
+ Every task declares `id`, `kind`, `role`, `modelClass`, `requirements`, `deliverable`, `dependsOn`, and `contextFrom`.
124
87
 
125
- The handoff requests a fixed Markdown decision packet with Status (`completed`, `blocked`, or `incomplete`), one-sentence Outcome, up to three concrete Evidence facts, Blocker, one material Risk, and one Suggested next action. It is the default. Exact output required by the assigned task or Role takes precedence, and the child replies only with it. This preserves caller-required exact structured output. The handoff remains inside `maxTurns`, but it is the one permitted response after crossing `maxTokens`. A timeout, provider failure, or child-process failure can end a Role launch before handoff. Structured executor and retained-worktree facts remain authoritative; the model handoff supplies semantic context and a suggested next action.
88
+ - A `text` task runs in a disposable child worktree and must leave it unchanged.
89
+ - A `changeset` task requires focused checks and owns one worktree, Herdr workspace, worker tab, and retained worker.
90
+ - `dependsOn` controls ready waves; dependent tasks can run from a selected staged snapshot only after Main explicitly advances it.
91
+ - `contextFrom` may reference completed text tasks only and preserves declaration order.
92
+ - A graph containing changesets requires final checks. The repository root must have a `package.json` test script; admission prepends canonical `pnpm test` if absent from final checks. Combined validation rejects a changed root test script.
126
93
 
127
- A raw `createEphemeralSubagentExecutor` launch enforces the token extra-turn window. It does not guarantee disabled tools or the handoff message. Only Role launches install that policy.
94
+ The runner executes ready worker tasks concurrently, inspects clean committed candidates, runs preliminary task checks and optional preliminary judgment, then records ready candidates for Main. **It does not automatically integrate them.** Main selects exact candidates via `subagent_stage` into an owned integration checkout in explicit order. Each stage is a committed merge of the candidate, not a mutation of Main. Conflicts retain the checkout for Main to resolve; `resolve` verifies the exact two-parent merge instead of replaying an uncertain merge. Main can explicitly run dependent tasks from an exact staged tip with `subagent_integrate advance` and stage their results afterward. Independent candidates may be staged without re-running workers.
128
95
 
129
- ## Public Role and executor API
96
+ Once all selected work is staged, Main calls `subagent_integrate validate` on the exact clean combined tip. This runs every final check, including `pnpm test`, and optional final judgment in the integration checkout before any Main mutation. A failed check/review can be followed by one Main-authored committed correction in that checkout, recorded with `correct`, then another `validate`. `promote` requires exact passing combined evidence and unchanged Main and uses a guarded update to Main. Only proven promotion leads to selected-worker termination and verified cleanup. A changed candidate, integration tip, or Main identity invalidates prior evidence or blocks promotion; review accepts only exact `PASS`.
130
97
 
131
- The package root exports the following mechanism-level APIs:
98
+ ### Readiness and Main actions
132
99
 
133
- | API | Responsibility |
134
- | --- | --- |
135
- | `loadRoles(agentDir?)` | Validate and load package-shipped built-in and user Role Markdown. |
136
- | `parseRoleName(value, source?)` / `RoleName` | Normalize one arbitrary Role name. Empty names and C0/C1 controls fail. |
137
- | `resolveRoleSkills(pi, role)` | Resolve Role Skill names from Pi's effective registry. |
138
- | `resolveRoleLaunch(pi, ctx, input)` | Resolve a caller-owned Model Task route, applying call-level then Role `modelClass` precedence, and produce `ResolvedRoleLaunch`. |
139
- | `resolveConfiguredRoleLaunch(pi, ctx, { role, modelClass })` | Reload one configured Role and package resources with a required explicit Model Class; reject missing Role Skills. |
140
- | `createRoleLaunch(pi, ctx, input)` | Produce the same launch from a caller-supplied resolved route. |
141
- | `prepareRoleLaunch` / `finalizeRoleLaunch` | Separate the stable Role prompt from argv, expose its immutable tool policy, and reject unavailable Role Skills. |
142
- | `createEphemeralSubagentExecutor(options)` | Queue and run one prepared no-session child per `run`. |
143
- | `createChildWorktree` / `finalizeChildWorktree` | Optional caller-managed worktree lifecycle; `createChildWorktree` can prepare exact metadata before allocation. |
144
- | `prepareExactReviewEvidence` | Validate Git identity and create a bounded private base-to-tip patch with exact `{base, tip, patchPath}` evidence. |
145
-
146
- `finalizeChildWorktree` returns the breaking `WorktreePayload` lifecycle union:
147
-
148
- | `outcome` | Fields | Contract |
149
- | --- | --- | --- |
150
- | `pruned` | `path`, `branch` | Zero commits and a clean tree were proved. Worktree and branch cleanup completed. |
151
- | `retained` | `path`, `branch`, `commits`, `dirty` | Work was preserved. Both measurements are known. |
152
- | `recovery` | `path`, `branch`, `note`, optional `commits`, `dirty` | Recovery needs action. The note tells Main what to inspect. Present measurements completed; omitted values are unknown. |
100
+ The user's request can authorize the declared local graph, checks, review, and Main's later explicit integration actions. After each successful preliminary check batch, the runner drains any follow-up already queued for the same live worker. Each revision must produce a new clean commit and rerun preliminary checks. When the queue is empty, the runner atomically seals the candidate and persists exact readiness evidence without waiting for input.
153
101
 
154
- `parseRoleName` returns a trimmed `RoleName`. It accepts arbitrary names, not only the built-in Role catalog. A loaded `Role` contains `name`, `description`, required normalized `tools`, `extensions`, and `skills` arrays, a normalized `mcps` array, optional `modelClass` and `isolation`, and `systemPrompt`. `resolveRoleLaunch` accepts `role`, a caller-owned `task` Model Task declaration, optional call-level `modelClass`, and optional caller `agentDir`, `extensions`, `tools`, and `env`. `resolveConfiguredRoleLaunch` accepts a Role name and required `modelClass`. It does not use Role or task defaults. Its named and package Skill paths load once in that order. At extension load, callers invoke `registerModelTask(pi, task)` from `@henryqw/pi-task-models` once to expose that declaration in the shared control plane. Its result is a `PiLaunch` (`{ env, args }`) plus the selected `model`, `thinkingLevel`, and `missingSkills`. A prepared launch exposes an immutable, ordered, de-duplicated `tools` policy. It includes explicit caller tool additions. `prepareRoleLaunch` and `resolveConfiguredRoleLaunch` reject missing Skills before returning a prepared launch; `finalizeRoleLaunch` restores that prompt after callers add variable state.
102
+ `/subagent` opens a native picker for direct recovery and isolated inspection; locally owned active changesets may queue an optional same-worker revision or drain all still-pending instructions into the native editor and submit one replacement. Cancel after draining leaves them withdrawn; claimed instructions are unaffected. Pi's global Option+Up is unchanged. There is no guaranteed post-completion editing window; late follow-ups fail visibly. After readiness, use `subagent_stage revise` with an exact candidate, generation, current `expectedTip`, and instruction when a same-worker correction remains allowed. Material scope growth, missing authorization, user-owned conflicts, or consequential external actions still require a new decision. pi-subagent never pushes, publishes, deploys, or opens a pull request.
155
103
 
156
- `createEphemeralSubagentExecutor` requires:
104
+ `subagent_status` reports ready candidates with `taskId`, `attempt`, and `tip`, plus each generation's number, stage lineage, combined tip, checks, promotion, and retained worktree. Use those exact identities (including branch, HEAD, index, and tree) rather than guessed commits:
157
105
 
158
- ```js
159
- const executorOptions = {
160
- maxConcurrency: 4,
161
- maxTurns: 50,
162
- maxTokens: 100_000, // optional; omitted means unlimited
163
- timeout: { idleMs: 10 * 60_000, maxMs: 30 * 60_000 },
164
- };
165
- ```
166
-
167
- Concurrency is FIFO. `run` accepts optional `signal`, `onUpdate(text)`, `onTokens(number)`, and `onActivity(event)` callbacks plus required `prepare()`. A queued run receives its permit before `prepare` executes, so resource and route resolution can use the latest Pi state. Queued time does not consume child timeout. `maxConcurrency`, `maxTurns`, and configured `maxTokens` must be safe integers >= 1. `idleMs` and `maxMs` must be positive. `maxMs` must exceed `idleMs`. Omitted `maxTurns` defaults to 50. Omitted `maxTokens` is unlimited.
168
-
169
- One executor applies `maxTokens` independently to every `run`; it is not a shared pool. `run` has no token override. The built-in extension reads its global value only from `config/pi-subagent/config.json`.
106
+ - `subagent_stage`: `{ id, action: "stage" | "resolve" | "reject" | "revise", generation, taskId, attempt, candidate, expectedTip, instruction? }`. `candidate` is the ready candidate's `tip`; `expectedTip` is Main's recorded identity for the first stage or the previous stage's exact tip. `resolve` reconciles a pending/conflicted merge; `reject` and `revise` arbitrate a candidate without changing Main.
107
+ - `subagent_integrate`: `{ id, action: "advance" | "validate" | "correct" | "promote" | "reconcile" | "cleanup", generation, expectedTip }`. Use the generation's exact `combinedTip`. `advance` dispatches ready dependents on that snapshot, `correct` records the single clean committed correction after a definitive failed check/review, `reconcile` inspects an interrupted validation or promotion without replaying an uncertain promotion, and `cleanup` completes proved post-promotion cleanup.
108
+ - `subagent_integrate refresh`: `{ id, action: "refresh", generation, expectedTip, expectedMain, newMain }`. If Main advanced to a clean same-branch descendant, this freezes the old generation and allocates a new integration checkout from `newMain`. Restage selected immutable candidates in explicit order, resolve fresh conflicts, then rerun combined validation. Dirty, divergent, or uncertain Main cannot refresh.
109
+ - `subagent_integrate release`: `{ id, action: "release", generation, expectedTip, taskId?, attempt? }`. With no task, release an explicitly superseded checkout using that generation's last staged tip (or integration base if empty). With both `taskId` and `attempt`, release a rejected, terminated worker using its exact candidate tip and the latest generation number (or 1 when none exists); release every generation that staged it first. Cleanup steps are persisted separately, and a retry reconciles already-removed resources by proving absence. Release refuses dirty/conflicted work, changed refs, unproved termination, and still-needed checkouts.
170
110
 
171
- The executor is **active-Pi-only**. It reuses the currently running Pi invocation and does not locate or support a standalone Node.js Pi installation. Once direct Pi exits, stdout/stderr drain normally until EOF; an escaped descendant retaining either stream is cut off after short output inactivity or a one-second hard deadline so it cannot retain the FIFO permit.
111
+ A rejection or revision of a staged candidate supersedes that generation, leaves its checkout read-only, and requires explicit restaging of chosen candidates into a new generation. At most two unreleased owned integration checkouts can coexist per request. Superseded integration checkouts and rejected candidate resources are **retained until Main explicitly releases them**. Inspect exact status first; dirty or unresolved conflicts require manual recovery without force deletion. Rejection invalidates dependent attempts; if the required fresh dependent work cannot run within the bounded attempt/lineage constraints, start a new request. `subagent_abort` cannot discard retained resources, but can finish after all candidates are rejected and all owned checkouts are released.
172
112
 
173
- ### Prepare after the permit
113
+ ### Recovery and state
174
114
 
175
- This JavaScript runs inside a Pi extension. `pi` is that extension's `ExtensionAPI`; it is not a standalone Node entry point.
115
+ State version 5 is stored privately under:
176
116
 
177
- ```js
178
- import {
179
- createEphemeralSubagentExecutor,
180
- finalizeRoleLaunch,
181
- prepareRoleLaunch,
182
- } from "@henryqw/pi-subagent";
183
- import { registerModelTask } from "@henryqw/pi-task-models";
184
-
185
- const MODEL_TASK = {
186
- id: "your-package/delegate",
187
- label: "Package delegation",
188
- purpose: "Run one package-owned delegated task.",
189
- defaultProfile: "fast",
190
- };
191
-
192
- export function createRunRole(pi) {
193
- // Register the Model Task at extension load so /task-models discovery works.
194
- registerModelTask(pi, MODEL_TASK);
195
-
196
- const executor = createEphemeralSubagentExecutor({
197
- maxConcurrency: 4,
198
- maxTurns: 50,
199
- maxTokens: 100_000,
200
- timeout: { idleMs: 10 * 60_000, maxMs: 30 * 60_000 },
201
- });
202
-
203
- let latestCtx;
204
- pi.on("session_start", (_event, ctx) => { latestCtx = ctx; });
205
- pi.on("model_select", (event, ctx) => {
206
- latestCtx = { ...ctx, model: event.model };
207
- });
208
- pi.on("agent_settled", (_event, ctx) => { latestCtx = ctx; });
209
-
210
- function latestContext() {
211
- if (!latestCtx) throw new Error("Pi session has not started.");
212
- return latestCtx;
213
- }
214
-
215
- async function runRole(role, task, options = {}) {
216
- const {
217
- signal,
218
- cwd,
219
- modelClass,
220
- extensions = [],
221
- tools,
222
- env = {},
223
- } = options;
224
-
225
- return executor.run({
226
- signal,
227
- prepare: async () => {
228
- // prepare runs only after this delegation owns a FIFO permit.
229
- const ctx = latestContext();
230
- const prepared = prepareRoleLaunch(pi, ctx, {
231
- role,
232
- task: MODEL_TASK,
233
- modelClass,
234
- extensions,
235
- tools,
236
- env,
237
- });
238
- return { launch: finalizeRoleLaunch(prepared), task, cwd: cwd ?? ctx.cwd };
239
- },
240
- });
241
- }
242
-
243
- return { runRole };
244
- }
117
+ ```text
118
+ <agent-dir>/config/pi-subagent/state/<repository-hash>/<request-id>.json
245
119
  ```
246
120
 
247
- `run` resolves to `EphemeralSubagentResult`. Both outcome variants contain `exitCode`, `output`, `stderr`, and optional `stopReason`, `errorMessage`, and `usage`. A launched child/model failure is a typed `{ outcome: "failure", ... }` result. Abort, timeout, turn-limit, token-limit, spawn, protocol, preparation, and callback failures reject with `EphemeralSubagentError` and a stable `code`. A terminal response at `maxTurns` succeeds; an attempted continuation rejects with `turn_limit`, accumulated `usage`, and bounded `output`.
121
+ Strict parsing rejects unknown properties, malformed evidence, invalid lineage, and old v4 state without migration; preserve old files and recover their work manually. Writes are atomic and repository productive work uses a durable process lease. `delegate_task` and `subagent_resume` acknowledge only after durable state is saved, then continue productive work asynchronously. Completion or attention is sent as a Pi follow-up to the launching session. `subagent_status` is read-only and remains the recovery authority if a follow-up is missed. `subagent_resume` accepts only the continuation reported by state. `subagent_abort` performs exact worker termination and teardown.
248
122
 
249
- Token accounting adds each completed assistant response's `Usage.totalTokens` once. It uses the same aggregate `usage` returned by the executor. A terminal response crossing `maxTokens` succeeds. A continuing crossing response and its tools finish before one final response turn. That turn may overshoot the configured value. Further continuation rejects with `token_limit`, aggregate `usage`, and bounded last assistant `output`.
123
+ Productive execution has no whole-run wall-clock deadline. Long productive work and later resumes remain valid. The request retains its original policy snapshot and correction count; a resume cannot refill them. Current configuration may tighten the correction allowance. Child idle/hard runtime, subprocess I/O, status inspection, termination, cleanup, and outer abort remain bounded independently.
250
124
 
251
- The executor passes its turn, token, and runtime budget through its internal child protocol. On a Role launch, its tool policy sends one convergence warning at each 80% threshold. It combines thresholds first due together and uses no timer or extra warning turn. Assistant `output` and `stderr` are bounded, and `usage` contains aggregate child usage when Pi supplies it.
125
+ Ambiguous prompt submission or merge/promotion is never replayed automatically. Unknown allocation, failed checks, review findings, Main drift, conflicts, interrupted promotion, unproved termination, or cleanup failure requires Main attention and preserves exact evidence. A definitive failed combined check retains its generation for one correction. Proven promotion is never rolled back; recovery finishes termination and cleanup. Only selected workers and the promoted integration checkout are automatically cleaned after proven promotion; rejected workers and superseded generations require the explicit `release` action.
252
126
 
253
- ### Activity callbacks
127
+ ## Role and resource policy
254
128
 
255
- The optional `onActivity` callback receives structured activity events serially in child JSON-event order. This ordering applies only to `onActivity`; `onUpdate` and `onTokens` remain independent. A thrown or rejected activity callback fails the run with an `EphemeralSubagentError` whose code is `callback`.
129
+ A Role declares tools, trusted extension sources, Skill names, optional MCP names, instructions, and an optional model-class default. Roles do not declare isolation.
256
130
 
257
- | Event type | Fields |
258
- | --- | --- |
259
- | `tool_execution_start` | `toolCallId: string`, `toolName: string`, `path?: string` |
260
- | `tool_execution_end` | `toolCallId: string`, `toolName: string` |
261
- | `message_end` | none |
131
+ Every child launch:
262
132
 
263
- Activity text is limited to 4 KiB per field. An invalid `toolCallId` or `toolName`, or an oversized `path`, drops the event. A blank path or one containing C0/C1 terminal controls or Unicode line/paragraph separators is omitted from an otherwise valid start event.
133
+ 1. disables ambient extension and Skill discovery;
134
+ 2. loads only declared resources plus required internal adapters;
135
+ 3. resolves Skill and model names from Pi's effective registries;
136
+ 4. installs an exact tool policy;
137
+ 5. excludes Main-only delegation/recovery tools and `ask_question`; and
138
+ 6. prepends the delegated-subagent identity contract.
264
139
 
265
- The low-level executor does not interpret `Role.isolation`, discover resources, compose modes, create shared state, or promote child failure outcomes to tool errors. A direct caller that wants worktrees must call `createChildWorktree` after the permit, choose the returned `cwd`, call `finalizeChildWorktree` on every exit path, and preserve its recovery payload.
140
+ An empty `tools` list adds no base tools, though selected extension tools can still activate. Omitted or empty `mcps` denies MCP access. Loading `pi-mcp-adapter` directly is rejected because it bypasses the allowlist.
266
141
 
267
- ## JavaScript composition
142
+ Isolated Herdr Role launches receive the same turn, token, and child-runtime budget metadata as ephemeral launches. Retained workers therefore keep one non-refilling session budget across follow-ups rather than receiving a new request budget.
268
143
 
269
- The examples below use caller-selected `Role` objects and the `runRole` function returned by the package's initializer. A consuming Pi extension calls the initializer once at startup:
144
+ ## Ephemeral executor API
270
145
 
271
146
  ```js
272
- import { createRunRole } from "your-package";
273
-
274
- export default function yourExtension(pi) {
275
- const { runRole } = createRunRole(pi);
276
- }
277
- ```
278
-
279
- Variable names such as `reviewRole` are local bindings, not reserved Role names. The executor's `maxConcurrency` bounds launches; callers must also bound collections and loops.
280
-
281
- A small caller-owned failure policy keeps the examples readable:
282
-
283
- ```js
284
- async function successfulOutput(run) {
285
- const result = await run;
286
- if (result.outcome === "failure") {
287
- const error = new Error(
288
- result.errorMessage || result.stderr || result.output ||
289
- `Child exited ${result.exitCode}`,
290
- );
291
- error.result = result;
292
- throw error;
293
- }
294
- return result.output;
295
- }
296
- ```
297
-
298
- ### Single
147
+ const executor = createEphemeralSubagentExecutor({
148
+ maxConcurrency: 4,
149
+ maxTurns: 50,
150
+ maxTokens: 100_000, // optional
151
+ timeout: {
152
+ idleMs: 10 * 60_000,
153
+ maxMs: 30 * 60_000,
154
+ },
155
+ });
299
156
 
300
- ```js
301
- const result = await runRole(selectedRole, boundedTask, {
157
+ const result = await executor.run({
302
158
  signal,
303
- cwd: sharedWorkspace,
159
+ onUpdate(text) {},
160
+ onTokens(total) {},
161
+ onActivity(event) {},
162
+ prepare: async () => ({ launch, task, cwd }),
304
163
  });
305
164
  ```
306
165
 
307
- ### Bounded parallel
308
-
309
- ```js
310
- const settled = await Promise.allSettled(
311
- taskPackets.map((task) =>
312
- successfulOutput(runRole(selectedRole, task, { signal, cwd: sharedWorkspace }))),
313
- );
314
- // Promise.allSettled preserves taskPackets order; child failure outcomes reject here,
315
- // and the executor caps active children.
316
- ```
317
-
318
- Unlike the tool's fixed maximum of eight entries, a library caller owns its collection bound. Do not pass an unbounded producer merely because active execution is capped.
319
-
320
- ### Chain
321
-
322
- ```js
323
- let previous = "";
324
- for (const step of steps) {
325
- const task = step.task.replaceAll("{previous}", () => previous);
326
- previous = await successfulOutput(
327
- runRole(step.role, task, { signal, cwd: sharedWorkspace }),
328
- );
329
- }
330
- ```
331
-
332
- The loop is fail-fast, and only the immediate successful output becomes `previous`.
333
-
334
- ### Fan-out / fan-in
335
-
336
- ```js
337
- const reports = await Promise.all(
338
- partitions.map((packet) =>
339
- successfulOutput(runRole(analysisRole, packet, { signal, cwd: sharedWorkspace }))),
340
- );
341
-
342
- const synthesis = await successfulOutput(runRole(
343
- synthesisRole,
344
- `Reconcile these caller-bounded reports:\n${JSON.stringify(reports)}`,
345
- { signal, cwd: sharedWorkspace },
346
- ));
347
- ```
166
+ The FIFO permit is acquired before `prepare`, so queued work can resolve fresh Role and route state without consuming timeout. Each child gets independent turn/token and idle/hard-runtime limits. Output, stderr, protocol events, callback draining, and descendant stream draining are bounded.
348
167
 
349
- The caller chooses report bounds and the fan-in protocol; the executor supplies no hidden aggregation state.
168
+ A result contains `outcome`, `exitCode`, bounded `output` and `stderr`, truncation state, optional stop reason/error, and aggregate usage. Abort, timeout, turn/token limit, spawn, protocol, preparation, and callback failures reject with typed `EphemeralSubagentError` codes.
350
169
 
351
- ### Bounded review loop
170
+ Token accounting adds each completed assistant response once. A terminal response that crosses the configured token value succeeds. A continuing crossing turn completes tools and receives one response-only handoff; further continuation fails. Role tools provide a convergence warning near configured turn/token/runtime limits.
352
171
 
353
- ```js
354
- function parseReviewVerdict(text) {
355
- const value = JSON.parse(text);
356
- if (
357
- !value || typeof value !== "object" ||
358
- typeof value.approved !== "boolean" ||
359
- !Array.isArray(value.findings) ||
360
- value.findings.some((finding) => typeof finding !== "string")
361
- ) {
362
- throw new Error("Invalid review verdict.");
363
- }
364
- return value;
365
- }
172
+ ## Role launch API
366
173
 
367
- const maxReviewRounds = 3;
368
- let findings = [];
369
- let approved = false;
370
-
371
- for (let round = 1; round <= maxReviewRounds; round += 1) {
372
- await successfulOutput(runRole(
373
- changeRole,
374
- `Apply round ${round}. Address: ${JSON.stringify(findings)}`,
375
- { signal, cwd: sharedWorkspace },
376
- ));
377
-
378
- const review = await successfulOutput(runRole(
379
- reviewRole,
380
- 'Return JSON only: {"approved":boolean,"findings":string[]}',
381
- { signal, cwd: sharedWorkspace },
382
- ));
383
- ({ approved, findings } = parseReviewVerdict(review));
384
- if (approved) break;
385
- }
174
+ The package exports:
386
175
 
387
- if (!approved) throw new Error(`Review did not pass after ${maxReviewRounds} rounds.`);
388
- ```
389
-
390
- The verdict schema, parser, round state, shared workspace, and terminal decision all belong to the caller. Add a richer protocol only when the workflow requires one; do not encode it as a recursive package workflow definition.
391
-
392
- ## Built-in Roles
393
-
394
- The package ships three working built-in Roles, validated by the same parser as user roles and always present even with no `config/pi-subagent` directory. Their files leave `modelClass` unset, so they use the local Model Task route unless a caller overrides it:
395
-
396
- | Built-in | Behavior |
176
+ | API | Responsibility |
397
177
  | --- | --- |
398
- | `implementer` | Focused implementation requesting `isolation: worktree`; commits scoped changes locally, never pushes or opens PRs without authorization. Non-Git or unborn-`HEAD` contexts may use Main's cwd. |
399
- | `reviewer` | Read-only correctness review of supplied plans, files, or caller-prepared exact evidence; never edits or commits. |
400
- | `scout` | Read-only code and evidence mapping for one bounded task; never changes files. |
401
-
402
- A same-named Markdown file in `config/pi-subagent/` explicitly overrides the built-in default.
403
-
404
- `delegate_task` remains the generic flat single, parallel, and chain mechanism. Library callers own any richer protocol.
405
-
406
- See [ADR 001](./adr/001-composable-ephemeral-execution.md) for the executor boundary.
178
+ | `loadRoles` | Load built-in and user Role Markdown with strict validation |
179
+ | `parseRoleName` | Normalize arbitrary Role names and reject display controls |
180
+ | `resolveRoleSkills` | Resolve named Skills from Pi's effective registry |
181
+ | `resolveRoleLaunch` | Resolve Role, caller-owned Model Task, resources, and route |
182
+ | `resolveConfiguredRoleLaunch` | Resolve a named Role with an explicit model class |
183
+ | `prepareRoleLaunch` / `finalizeRoleLaunch` | Separate stable prompt data from final argv and tool policy |
184
+ | `createEphemeralSubagentExecutor` | Run bounded no-session Pi children through one FIFO pool |
185
+ | `createChildWorktree` / `finalizeChildWorktree` | Create and conservatively finalize optional child worktrees |
186
+ | `prepareExactReviewEvidence` | Produce bounded private base-to-tip evidence |
187
+ | checked runner exports | Compose strict schemas, state, Git, Herdr, and launch runtimes |
188
+
189
+ Package callers must register their Model Task once with `@henryqw/pi-task-models`. `resolveRoleLaunch` applies call-level model class, then Role default, then the registered task route. `resolveConfiguredRoleLaunch` requires the class explicitly. Missing Skills reject before launch.
190
+
191
+ Worktree finalization returns one of:
192
+
193
+ - `pruned`: no commits and clean files were proved, then branch/worktree removal completed;
194
+ - `retained`: exact commit and dirty measurements are known and work remains; or
195
+ - `recovery`: cleanup or measurement needs deliberate action and reports only proved fields.
196
+
197
+ No cleanup path uses `git clean`, force deletion, hidden fallback to Main, or deletion of unproved work.
package/example.png ADDED
Binary file
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: implementer
3
- description: Implements and validates one bounded change, requesting worktree isolation
3
+ description: Implements and validates one bounded change in the checkout selected by Main
4
4
  tools:
5
5
  - read
6
6
  - bash
@@ -12,7 +12,6 @@ tools:
12
12
  extensions: []
13
13
  skills: []
14
14
  mcps: []
15
- isolation: worktree
16
15
  ---
17
16
 
18
17
  Implement the bounded outcome, not a preassigned file list. Work in the assigned cwd. Read applicable repository instructions and domain context; inspect the relevant flow, callers, and tests before editing. Preserve unrelated work. Fix the root cause with the smallest complete diff using existing patterns and dependencies. Add no speculative work. Stop when complete or blocked.
@@ -21,6 +20,6 @@ Run focused checks required by the task. Before reporting, remove only task-crea
21
20
 
22
21
  Access credentials or the network, create extra artifacts, or broaden scope only when the task requires it. Never invoke external LLM APIs, SDKs, agent harnesses, or model CLIs.
23
22
 
24
- Commit completed scoped changes unless the task says otherwise. Do not create or manage another worktree. Leave the assigned worktree and branch intact. Never push or open a pull request without explicit authorization.
23
+ Follow the task's commit policy. Direct delegation leaves changes uncommitted; isolated delegation commits completed scoped changes. Do not create or manage another worktree. Leave the assigned checkout and branch intact. Never push or open a pull request without explicit authorization.
25
24
 
26
25
  Report briefly: outcome, commit, checks, and remaining risks.