@henryqw/pi-subagent 6.1.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTEXT.md CHANGED
@@ -28,10 +28,10 @@ Provide validated built-in and user Roles, shared task-model Pi launch policy, g
28
28
  - Up to five active ephemeral `delegate_task` children run per Main by default, configurable via `maxSubagents` in `~/.pi/agent/config/pi-subagent/pi-subagent.json` or the `PI_SUBAGENT_MAX_SUBAGENTS` environment variable; excess calls wait FIFO. Queued calls do not start a child or consume child timeout. Managed Herdr workers are unaffected.
29
29
  - Ambient child extensions and Skills stay disabled. Every Role requires `tools`, `extensions`, and `skills` YAML arrays, and every launch installs the Role tool policy. `tools: []` activates no base built-ins but does activate all tools from explicitly selected trusted extension bundles and explicit caller tool additions; `skills: []` selects no separately named Role Skills but trusted selected extension Skills still load; `extensions: []` selects no Role extension bundle. A Role/caller explicitly selected extension is a trusted atomic capability bundle: all tools it registers and all Skills supplied through its Pi package metadata or dynamic `resources_discover` load alongside separately named Role Skills. This intentionally includes the extension's executable lifecycle/prompt behavior; pi-subagent does not infer or externally narrow undocumented dependencies, and loading an extension is not sandboxing. Scope children by selecting fewer trusted extensions; finer granularity requires separate entry points/configuration or an upstream split. Explicit Role/caller tool names still verify against the final filtered registry, while parent-only recursive orchestration tools remain excluded.
30
30
  - Role Skill names resolve through Main's effective Pi Skill registry; unavailable names warn and skip without blocking delegation. Explicit Role/caller tool names verify against the final filtered child registry after explicit provider `session_start` handlers, and unavailable names fail before the first turn.
31
- - Main policy populates direct `model` and `thinking` only for explicit user overrides; otherwise it selects only `modelClass` (`fast` normally, `balanced` upfront for obvious complexity). This has no provenance tracking or runtime enforcement. An omitted class uses the shared `pi-subagent/delegateTask` assignment; library callers select a Role plus their own shared task ID.
31
+ - Main policy populates direct `model` and `thinking` only for explicit user overrides; otherwise it selects only `modelClass` (`fast` normally, `balanced` upfront for obvious complexity). This has no provenance tracking or runtime enforcement. An omitted class uses pi-subagent's local `pi-subagent/delegateTask` Model Task declaration (default `fast`); library callers select a Role plus their own Model Task declaration.
32
32
  - The selected profile resolves primary then fallback only before launch when a route, model, or thinking level is unavailable. If neither route is usable, launch rejects with `Run /task-models`; a started child is never retried by this package.
33
33
  - User Role Markdown files and Subagent JSON config live only in the user `config/pi-subagent` directory; model routes live in shared `config/pi-task-models.json`. Package-shipped built-in Roles (`implementer`, `reviewer`) resolve from the package's own `examples/roles/` Markdown through the same parser; a same-named user file explicitly overrides a built-in for `delegate_task` and `delegate_flow`.
34
- - `delegate_flow` accepts 1–8 independent units with direct validation commands, optional `modelClass`, and optional non-empty `review` judgment text. Omitted classes use the shared `pi-subagent/delegateTask` assignment; each unit's current class resolves through its shared profile route for its Implementer and, when `review` exists, Reviewer. At Flow start it always resolves/freezes the effective Implementer and resolves/freezes a Reviewer only when at least one requested unit has `review`. One active Flow creates every Unit Worktree before launching Implementers in parallel, then processes settled units in declared order. For each unit Flow rebases in place when earlier units advanced Main, inspects committed Git state, and runs declared validation. Validation is authoritative for objective verification: units without `review` fast-forward their exact validated tip; units with `review` send the exact Review Packet to the Reviewer in the same worktree and require exact `PASS` before the same guarded `git merge --ff-only` path.
34
+ - `delegate_flow` accepts 1–8 independent units with direct validation commands, optional `modelClass`, and optional non-empty `review` judgment text. Omitted classes use pi-subagent's local `pi-subagent/delegateTask` declaration (default `fast`); each unit's current class resolves through its shared profile route for its Implementer and, when `review` exists, Reviewer. At Flow start it always resolves/freezes the effective Implementer and resolves/freezes a Reviewer only when at least one requested unit has `review`. One active Flow creates every Unit Worktree before launching Implementers in parallel, then processes settled units in declared order. For each unit Flow rebases in place when earlier units advanced Main, inspects committed Git state, and runs declared validation. Validation is authoritative for objective verification: units without `review` fast-forward their exact validated tip; units with `review` send the exact Review Packet to the Reviewer in the same worktree and require exact `PASS` before the same guarded `git merge --ff-only` path.
35
35
  - Flow is memory-only. Only a post-rebase commit drop produces a no-op (`base === tip`); it validates, skips Reviewer and merge, then cleans up ordinarily. Initial zero-commit implementations block. Implementer, validation, or reviewer blocks allow one `delegate_flow_continue({ guidance, modelClass? })` repair in the same worktree; omission retains the Unit's current class and presence replaces it for that repair. A second block is terminal. Rebase and evidence/Reviewer/infrastructure failures retain worktrees. A reported fast-forward failure retains its worktree unless Main is clean at the exact integrated tip, which completes with the merge diagnostic as a warning. Cleanup uses non-forced worktree removal and branch deletion; cleanup refusal is a completion warning.
36
36
  - Flow has no graph, saved recovery, automatic retry, aggregate review, or post-merge validation. It never changes generic `delegate_task` Role resolution, isolation, non-Git fallback, or ordinary direct plan/file review.
37
37
  - Numbered Codex routes prefer Main's active account slot and explicitly load the multi-Codex child extension.
package/README.md CHANGED
@@ -30,7 +30,7 @@ pi install npm:@henryqw/pi-subagent
30
30
  | `delegate_flow` | tool | Package-owned parallel implementation and declared-order Git integration for 1–8 independent units. |
31
31
  | `delegate_flow_continue` | tool | Repair the blocked Flow unit once in its existing worktree. |
32
32
 
33
- All three delegation tool blocks use a compact self-rendered shell and never exceed five physical lines, including Pi's leading spacer: one call line plus at most three result lines in either collapsed or expanded view.
33
+ All three delegation tool blocks use Pi's default boxed shell and background. Their compact custom content is an immutable call label, foreground aggregate partial-result status, and bounded final summaries—no expanded view.
34
34
 
35
35
  ### `delegate_task`
36
36
 
@@ -47,13 +47,13 @@ Select exactly one shape:
47
47
  { chain: [{ role, task, model?, modelClass?, thinking? }], background? }
48
48
  ```
49
49
 
50
- `model` is `provider/modelId` and overrides `modelClass`. Main populates `model` and `thinking` only for an explicit user override; otherwise it chooses only `modelClass`—`fast` normally, or `balanced` upfront for obviously complex work. This is Main policy only: the runtime records no provenance and does not enforce it. `modelClass` is `fast`, `balanced`, `frontier`, or `fav`; omission uses the shared `pi-subagent/delegateTask` assignment. `background` applies to the entire selected mode and is never a per-delegation field.
50
+ `model` is `provider/modelId` and overrides `modelClass`. Main populates `model` and `thinking` only for an explicit user override; otherwise it chooses only `modelClass`—`fast` normally, or `balanced` upfront for obviously complex work. This is Main policy only: the runtime records no provenance and does not enforce it. `modelClass` is `fast`, `balanced`, `frontier`, or `fav`; omission uses pi-subagent's local `pi-subagent/delegateTask` Model Task declaration (default `fast`), which shared config can explicitly override. `background` applies to the entire selected mode and is never a per-delegation field.
51
51
 
52
52
  Parallel mode starts entries concurrently, waits for every entry, and reports them in input order. Chain mode is sequential and fail-fast; every literal `{previous}` receives only the immediately preceding successful assistant output. Foreground failures throw after retaining bounded sibling and recovery evidence. One tool call has one aggregate 50 KiB Main-visible transport cap, not 50 KiB per child.
53
53
 
54
54
  Background workflows are session-scoped. Session shutdown or reload aborts them and may deliver only recoverable-work evidence or no follow-up message.
55
55
 
56
- The transient two-line status widget owns deterministic live progress: status and task summary above thinking or the active tool (with elapsed time and path basename), completed turns, started tools, model, thinking level, tokens, and total duration; terminal activity is Done, Failed, or Stopped. At capacity, it evicts the oldest terminal row so new active work remains visible, and terminal rows otherwise clear on the next real user input. The final `delegate_task` block is deliberately minimal: bounded final summaries, role attribution for parallel/chain, and only retained-worktree recovery paths. It has no expanded view.
56
+ The transient status widget renders one line per child with: status glyph, role, status label, task summary, activity (thinking or active tool with elapsed time and path basename), and metrics (completed turns, started tools, model, thinking level, 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. The final `delegate_task` block is deliberately minimal: bounded final summaries with role attribution for parallel/chain, and only retained-worktree recovery paths. It has no expanded view.
57
57
 
58
58
  Each delegation resolves its own Role, resources, route, and optional worktree request. When available, `isolation: worktree` gives each entry a deterministic separate worktree; non-Git or unborn-`HEAD` contexts may use Main's cwd. Siblings and chain steps never implicitly share one created worktree.
59
59
 
@@ -70,15 +70,27 @@ delegate_flow_continue({ guidance, modelClass? })
70
70
 
71
71
  Objective verification is authoritative. Flow always inspects committed Git state and runs declared validation. A unit without `review` skips review evidence and Reviewer launch, then fast-forwards its exact validated tip through the existing guarded `git merge --ff-only` path. Add `review` only for an explicit judgment that automation cannot establish; that unit retains the exact `{base, tip, patchPath}` protocol and requires exact `PASS` before the same integration path.
72
72
 
73
- One memory-only Flow may be active. At start it resolves/freezes the effective `implementer` Role, including a same-named user override, and resolves/freezes the effective `reviewer` only if at least one requested unit has `review`. Omitted `modelClass` uses the shared `pi-subagent/delegateTask` assignment; a selected class resolves through its shared profile model-and-thinking route for the unit's Implementer and, when applicable, Reviewer. It creates one Unit Worktree per unit, runs Implementers in parallel, then processes settled results in declared order. It removes the worktree and branch non-forcibly after integration; a refusal is a completion warning with the retained worktree path and/or branch.
73
+ One memory-only Flow may be active. At start it resolves/freezes the effective `implementer` Role, including a same-named user override, and resolves/freezes the effective `reviewer` only if at least one requested unit has `review`. Omitted `modelClass` uses pi-subagent's local `pi-subagent/delegateTask` declaration (default `fast`); a selected class resolves through its shared profile model-and-thinking route for the unit's Implementer and, when applicable, Reviewer. It creates one Unit Worktree per unit, runs Implementers in parallel, then processes settled results in declared order. It removes the worktree and branch non-forcibly after integration; a refusal is a completion warning with the retained worktree path and/or branch.
74
74
 
75
75
  A rebase that drops all unit commits is a no-op: Flow validates it, skips Reviewer and merge, then cleans up ordinarily. Implementer, validation, or review blocks can be repaired once through `delegate_flow_continue` in the same worktree. Omitted continuation `modelClass` retains the blocked unit's current class; a supplied class replaces it for that one repair. 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; otherwise it is terminal and retains the affected worktree. Flow has no graph, saved recovery, automatic retry, aggregate review, or post-merge gate.
76
76
 
77
77
  `delegate_task` remains generic with its ordinary isolation behavior. Flow uses the package-shipped Implementer by default and the package-shipped Reviewer only when a unit requests review; same-named user Roles remain supported overrides.
78
78
 
79
+ ### Delegate UI summary
80
+
81
+ | Aspect | Behavior |
82
+ | --- | --- |
83
+ | Call label | `delegate_task · single/parallel/chain · N task(s)`; `delegate_flow · parallel→serial · N unit(s)`; `delegate_flow_continue · repair continuation` |
84
+ | Partial progress | `delegate_task`: aggregate counts (running/pending/complete/failed/skipped); `delegate_flow`: phase transitions (setup → implement → verify/integrate → review → repair) |
85
+ | Widget rows | One line per child: glyph, role, status, task, activity, metrics |
86
+ | Ordering | Active-first stable (working first, then insertion order) |
87
+ | Line cap | 6 physical lines max (≤6 items: all child rows; >6 items: 5 rows + 1 status-aware overflow) |
88
+ | Terminal retention | Active rows persist; terminal rows clear on next user input |
89
+ | Final result | Bounded summaries with recovery paths; no expanded view |
90
+
79
91
  ## Config
80
92
 
81
- pi-subagent owns the extension-named config directory `~/.pi/agent/config/pi-subagent/`, which holds two kinds of user-owned configuration: one Markdown file per Role (see [Roles](#roles)) and its own optional JSON file below. Model routing is *not* configured here; children resolve routes through the shared `@henryqw/pi-task-models` config at `~/.pi/agent/config/pi-task-models.json`.
93
+ pi-subagent owns the extension-named config directory `~/.pi/agent/config/pi-subagent/`, which holds two kinds of user-owned configuration: one Markdown file per Role (see [Roles](#roles)) and its own optional JSON file below. Model routing is *not* configured here; children resolve routes through the shared `@henryqw/pi-task-models` config at `~/.pi/agent/config/pi-task-models.json`, which stores only explicit task overrides. The local `pi-subagent/delegateTask` declaration supplies the omitted-class default.
82
94
 
83
95
  `~/.pi/agent/config/pi-subagent/pi-subagent.json` controls the ephemeral child pool and timeouts. All fields are optional; a missing file uses defaults.
84
96
 
@@ -143,4 +155,4 @@ A Role explicitly owns base tools, extensions, named Skills, instructions, and o
143
155
 
144
156
  The package root exports Role loading and launch resolution, `createEphemeralSubagentExecutor`, worktree helpers, and generic managed Herdr lifecycle helpers. The ephemeral executor is for code already running inside active Pi; it does not provide standalone Node.js Pi discovery or launch support. After Pi itself exits, it drains inherited stdout/stderr normally but destroys streams still held by escaped descendants after a short inactivity deadline or one-second hard deadline, so they cannot retain a pool permit.
145
157
 
146
- Use [`docs/orchestration.md`](./docs/orchestration.md#public-role-and-executor-api) for exact API behavior and a post-permit `prepare` example using `resolveRoleLaunch` against the latest Pi context.
158
+ Use [`docs/orchestration.md`](./docs/orchestration.md#public-role-and-executor-api) for exact API behavior and a post-permit `prepare` example using `resolveRoleLaunch` with a caller-owned Model Task declaration against the latest Pi context.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { type HerdrExecutor } from "@henryqw/pi-herdr";
3
- import { type AvailableModel, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
3
+ import { type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
4
4
  export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, EphemeralSubagentError, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
5
5
  export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
6
6
  export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
7
7
  export declare const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
8
8
  export declare const CHILD_EXCLUDED_TOOL_NAMES: readonly ["delegate_task", "delegate_flow", "delegate_flow_continue", "ask_question"];
9
9
  export declare const CHILD_EXCLUDED_TOOLS: string;
10
+ export declare const DELEGATE_TASK: {
11
+ readonly id: "pi-subagent/delegateTask";
12
+ readonly label: "Subagent delegation";
13
+ readonly purpose: "Launch an isolated Pi subagent.";
14
+ readonly defaultProfile: "fast";
15
+ };
10
16
  export interface Role {
11
17
  name: string;
12
18
  description: string;
@@ -33,7 +39,7 @@ export interface CreateRoleLaunchInput {
33
39
  env?: Readonly<Record<string, string>>;
34
40
  }
35
41
  export interface ResolveRoleLaunchInput extends Omit<CreateRoleLaunchInput, "route"> {
36
- taskId: string;
42
+ task: ModelTask;
37
43
  agentDir?: string;
38
44
  }
39
45
  export interface ResolvedRoleSkills {
package/dist/index.js CHANGED
@@ -15,6 +15,12 @@ export const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
15
15
  export const CHILD_EXCLUDED_TOOL_NAMES = ["delegate_task", "delegate_flow", "delegate_flow_continue", "ask_question"];
16
16
  export const CHILD_EXCLUDED_TOOLS = CHILD_EXCLUDED_TOOL_NAMES.join(",");
17
17
  const CHILD_IDENTITY_POLICY = "You are a delegated Pi Subagent, not Main. Execute the assigned Role and task directly. Main-only delegation rules do not apply. Recursive delegation is unavailable; do not seek or invoke delegation tools.";
18
+ export const DELEGATE_TASK = {
19
+ id: "pi-subagent/delegateTask",
20
+ label: "Subagent delegation",
21
+ purpose: "Launch an isolated Pi subagent.",
22
+ defaultProfile: "fast",
23
+ };
18
24
  const cleanText = (value, field, source) => {
19
25
  if (typeof value !== "string" || !value.trim() || value.includes("\0")) {
20
26
  throw new Error(`${source}: ${field} must be non-empty text.`);
@@ -186,10 +192,9 @@ export function createRoleLaunch(pi, ctx, input) {
186
192
  };
187
193
  }
188
194
  export function resolveRoleLaunch(pi, ctx, input) {
189
- const taskId = cleanText(input.taskId, "task ID", "Role launch");
190
195
  return createRoleLaunch(pi, ctx, {
191
196
  ...input,
192
- route: resolveConfiguredTaskRoute(ctx, taskId, input.agentDir),
197
+ route: resolveConfiguredTaskRoute(ctx, input.task, input.agentDir),
193
198
  });
194
199
  }
195
200
  function launchEnvironmentArgs(launch) {
@@ -5,7 +5,7 @@
5
5
  ```text
6
6
  Role (built-in or user override) + latest Pi registries ── resolveRoleLaunch ──> PiLaunch
7
7
 
8
- caller-owned task, cwd, signal ─────────────────────────┤
8
+ caller-owned Model Task declaration, cwd, signal ────────┤
9
9
  v
10
10
  active-Pi ephemeral executor
11
11
  ```
@@ -68,7 +68,7 @@ Single mode puts one delegation's fields at the top level.
68
68
  | `role` | yes | Name of a Role in the user's effective `config/pi-subagent` directory or a package-shipped built-in (`implementer`, `reviewer`); a same-named user file overrides the built-in. |
69
69
  | `task` | yes | Non-empty bounded task packet. |
70
70
  | `model` | no | Designated `provider/modelId`; takes precedence over `modelClass`, and Main supplies it only for an explicit user override. |
71
- | `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav`; Main normally chooses `fast`, may choose `balanced` upfront for obvious complexity, and omission uses shared task assignment. |
71
+ | `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav`; Main normally chooses `fast`, may choose `balanced` upfront for obvious complexity, and omission uses pi-subagent's local Model Task declaration. |
72
72
  | `thinking` | no | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; Main supplies it only for an explicit user override. Route selection skips models that cannot honor it. |
73
73
 
74
74
  Those five fields are the complete delegation object. The direct-model/thinking rule is Main-facing policy only: the runtime adds no provenance tracking or enforcement. `tasks`, `chain`, and `background` cannot be nested. Route fallback occurs only before launch; a started child is never retried by this package.
@@ -85,7 +85,7 @@ All Main-visible text for one tool call shares one aggregate 50 KiB UTF-8 transp
85
85
 
86
86
  `delegate_flow({ units })` accepts 1–8 units with unique non-empty `id` and `task`, one or more direct `{command, args}` validation commands, optional `modelClass`, and optional non-empty `review` text. `delegate_flow_continue({ guidance, modelClass? })` is available only for the one blocked unit of the active Flow.
87
87
 
88
- A Flow is memory-only and permits one active Flow. At start it always resolves/freezes the effective `implementer` Role, including a same-named user override. It resolves/freezes the effective `reviewer` only if at least one requested unit declares `review`. Omitted unit classes use the shared `pi-subagent/delegateTask` assignment; a selected class resolves through its existing `pi-task-models` profile model-and-thinking route for the unit's Implementer and, when applicable, Reviewer. It requires clean committed Git Main and creates every Unit Worktree before launching work; setup failure launches no Implementer. Each unit gets exactly one worktree and one Implementer. Implementers run in parallel and all settle. Flow then processes units in declared order:
88
+ A Flow is memory-only and permits one active Flow. At start it always resolves/freezes the effective `implementer` Role, including a same-named user override. It resolves/freezes the effective `reviewer` only if at least one requested unit declares `review`. Omitted unit classes use pi-subagent's local `pi-subagent/delegateTask` declaration (default `fast`); a selected class resolves through its existing `pi-task-models` profile model-and-thinking route for the unit's Implementer and, when applicable, Reviewer. It requires clean committed Git Main and creates every Unit Worktree before launching work; setup failure launches no Implementer. Each unit gets exactly one worktree and one Implementer. Implementers run in parallel and all settle. Flow then processes units in declared order:
89
89
 
90
90
  ```text
91
91
  Implementers (parallel, one Unit Worktree each)
@@ -149,12 +149,12 @@ The package root exports the following mechanism-level APIs:
149
149
  | --- | --- |
150
150
  | `loadRoles(agentDir?)` | Validate and load package-shipped built-in and user Role Markdown. |
151
151
  | `resolveRoleSkills(pi, role)` | Resolve Role Skill names from Pi's effective registry. |
152
- | `resolveRoleLaunch(pi, ctx, input)` | Resolve a shared task route and produce `ResolvedRoleLaunch`. |
152
+ | `resolveRoleLaunch(pi, ctx, input)` | Resolve a caller-owned Model Task route and produce `ResolvedRoleLaunch`. |
153
153
  | `createRoleLaunch(pi, ctx, input)` | Produce the same launch from a caller-supplied resolved route. |
154
154
  | `createEphemeralSubagentExecutor(options)` | Queue and run one prepared no-session child per `run`. |
155
155
  | `createChildWorktree` / `finalizeChildWorktree` | Optional caller-managed worktree lifecycle. |
156
156
 
157
- A loaded `Role` contains `name`, `description`, required normalized `tools`, `extensions`, and `skills` arrays, optional `isolation`, and `systemPrompt`. `resolveRoleLaunch` accepts `role`, `taskId`, and optional caller `agentDir`, `extensions`, `tools`, and `env`. Its result is a `PiLaunch` (`{ env, args }`) plus the selected `model`, `thinkingLevel`, and `missingSkills`.
157
+ A loaded `Role` contains `name`, `description`, required normalized `tools`, `extensions`, and `skills` arrays, optional `isolation`, and `systemPrompt`. `resolveRoleLaunch` accepts `role`, a caller-owned `task` Model Task declaration, and optional caller `agentDir`, `extensions`, `tools`, and `env`. 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`.
158
158
 
159
159
  `createEphemeralSubagentExecutor` requires:
160
160
 
@@ -178,54 +178,69 @@ import {
178
178
  createEphemeralSubagentExecutor,
179
179
  resolveRoleLaunch,
180
180
  } from "@henryqw/pi-subagent";
181
+ import { registerModelTask } from "@henryqw/pi-task-models";
181
182
 
182
- const executor = createEphemeralSubagentExecutor({
183
- maxConcurrency: 4,
184
- timeout: { idleMs: 10 * 60_000, maxMs: 30 * 60_000 },
185
- });
183
+ const MODEL_TASK = {
184
+ id: "your-package/delegate",
185
+ label: "Package delegation",
186
+ purpose: "Run one package-owned delegated task.",
187
+ defaultProfile: "fast",
188
+ };
186
189
 
187
- let latestCtx;
188
- pi.on("session_start", (_event, ctx) => { latestCtx = ctx; });
189
- pi.on("model_select", (event, ctx) => {
190
- latestCtx = { ...ctx, model: event.model };
191
- });
192
- pi.on("agent_settled", (_event, ctx) => { latestCtx = ctx; });
190
+ export function createRunRole(pi) {
191
+ // Register the Model Task at extension load so /task-models discovery works.
192
+ registerModelTask(pi, MODEL_TASK);
193
193
 
194
- function latestContext() {
195
- if (!latestCtx) throw new Error("Pi session has not started.");
196
- return latestCtx;
197
- }
194
+ const executor = createEphemeralSubagentExecutor({
195
+ maxConcurrency: 4,
196
+ timeout: { idleMs: 10 * 60_000, maxMs: 30 * 60_000 },
197
+ });
198
198
 
199
- async function runRole(role, task, options = {}) {
200
- const {
201
- signal,
202
- cwd,
203
- extensions = [],
204
- tools,
205
- env = {},
206
- } = options;
207
-
208
- return executor.run({
209
- signal,
210
- prepare: async () => {
211
- // prepare runs only after this delegation owns a FIFO permit.
212
- const ctx = latestContext();
213
- const launch = resolveRoleLaunch(pi, ctx, {
214
- role,
215
- taskId: "your-package/delegate",
216
- extensions,
217
- tools,
218
- env,
219
- });
220
- if (launch.missingSkills.length && ctx.hasUI) {
221
- ctx.ui.notify(
222
- `Skipped unavailable Skills: ${launch.missingSkills.join(", ")}`,
223
- "warning",
224
- );
225
- }
226
- return { launch, task, cwd: cwd ?? ctx.cwd };
227
- },
199
+ let latestCtx;
200
+ pi.on("session_start", (_event, ctx) => { latestCtx = ctx; });
201
+ pi.on("model_select", (event, ctx) => {
202
+ latestCtx = { ...ctx, model: event.model };
228
203
  });
204
+ pi.on("agent_settled", (_event, ctx) => { latestCtx = ctx; });
205
+
206
+ function latestContext() {
207
+ if (!latestCtx) throw new Error("Pi session has not started.");
208
+ return latestCtx;
209
+ }
210
+
211
+ async function runRole(role, task, options = {}) {
212
+ const {
213
+ signal,
214
+ cwd,
215
+ extensions = [],
216
+ tools,
217
+ env = {},
218
+ } = options;
219
+
220
+ return executor.run({
221
+ signal,
222
+ prepare: async () => {
223
+ // prepare runs only after this delegation owns a FIFO permit.
224
+ const ctx = latestContext();
225
+ const launch = resolveRoleLaunch(pi, ctx, {
226
+ role,
227
+ task: MODEL_TASK,
228
+ extensions,
229
+ tools,
230
+ env,
231
+ });
232
+ if (launch.missingSkills.length && ctx.hasUI) {
233
+ ctx.ui.notify(
234
+ `Skipped unavailable Skills: ${launch.missingSkills.join(", ")}`,
235
+ "warning",
236
+ );
237
+ }
238
+ return { launch, task, cwd: cwd ?? ctx.cwd };
239
+ },
240
+ });
241
+ }
242
+
243
+ return { runRole };
229
244
  }
230
245
  ```
231
246
 
@@ -249,7 +264,17 @@ Generic managed Herdr exports (`managedSubagentWorkspaceId`, reconciliation help
249
264
 
250
265
  ## JavaScript composition
251
266
 
252
- The examples below use caller-selected `Role` objects and the `runRole` helper above. 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.
267
+ 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:
268
+
269
+ ```js
270
+ import { createRunRole } from "your-package";
271
+
272
+ export default function yourExtension(pi) {
273
+ const { runRole } = createRunRole(pi);
274
+ }
275
+ ```
276
+
277
+ 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.
253
278
 
254
279
  A small caller-owned failure policy keeps the examples readable:
255
280
 
@@ -108,6 +108,7 @@ type FlowState = {
108
108
  };
109
109
 
110
110
  type UsageMeter = { usage?: Usage };
111
+ type FlowProgress = { line: string };
111
112
 
112
113
  type CommandResult = {
113
114
  stdout: string;
@@ -176,9 +177,18 @@ export function parseDelegateFlowContinue(value: unknown): Static<typeof Delegat
176
177
  };
177
178
  }
178
179
 
180
+ function unitCount(count: number): string {
181
+ return `${count} unit${count === 1 ? "" : "s"}`;
182
+ }
183
+
179
184
  function flowCallLabel(args: { units?: unknown }): string {
180
185
  const count = Array.isArray(args.units) ? args.units.length : 0;
181
- return `delegate_flow · working: ${count} unit${count === 1 ? "" : "s"}`;
186
+ return `delegate_flow · parallel→serial · ${unitCount(count)}`;
187
+ }
188
+
189
+ function isFlowProgress(value: unknown): value is FlowProgress {
190
+ return typeof value === "object" && value !== null && !Array.isArray(value)
191
+ && typeof (value as { line?: unknown }).line === "string";
182
192
  }
183
193
 
184
194
  function flowResultLines(text: string): string[] {
@@ -609,6 +619,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
609
619
  signal: AbortSignal | undefined,
610
620
  ctx: ExtensionContext,
611
621
  meter: UsageMeter,
622
+ emitProgress: (line: string) => void,
612
623
  ) => {
613
624
  assertCurrent(flow);
614
625
  const main = flow.main!;
@@ -624,6 +635,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
624
635
 
625
636
  const implementationFailure = settlementFailure(unit.implementation!);
626
637
  if (implementationFailure) return block(flow, unit, "implementer", implementationFailure, meter);
638
+ emitProgress(`verify/integrate · unit ${flow.index + 1}/${flow.units.length}`);
627
639
 
628
640
  let inspected = await inspectUnit(unit, false, signal);
629
641
  assertCurrent(flow);
@@ -690,6 +702,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
690
702
  if (reviewCriterion !== undefined) {
691
703
  const reviewer = flow.reviewer;
692
704
  if (!reviewer) return terminal(flow, "infrastructure", "Flow Reviewer was not resolved for a unit that requires review.", meter);
705
+ emitProgress(`review · unit ${flow.index + 1}/${flow.units.length}`);
693
706
  let evidence;
694
707
  try {
695
708
  evidence = await prepareExactReviewEvidence({ base: main.expectedHead, tip, worktree: unit.worktree.path }, signal);
@@ -787,16 +800,16 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
787
800
  "If a Flow blocks, inspect its classification and call delegate_flow_continue once with explicit repair guidance; modelClass may replace that one repair's current class.",
788
801
  ],
789
802
  parameters: DelegateFlowSchema,
790
- renderShell: "self",
791
803
  renderCall(args, theme, _context) {
792
804
  return renderToolLines([theme.fg("toolTitle", flowCallLabel(args))], theme);
793
805
  },
794
- renderResult(result, _options, theme, _context) {
806
+ renderResult(result, { isPartial }, theme, _context) {
807
+ if (isPartial) return renderToolLines(isFlowProgress(result.details) ? [theme.fg("muted", result.details.line)] : [], theme);
795
808
  const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
796
809
  return renderToolLines(flowResultLines(text), theme);
797
810
  },
798
811
  prepareArguments: parseDelegateFlow,
799
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
812
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
800
813
  const request = parseDelegateFlow(params);
801
814
  if (active) throw new Error("delegate_flow rejected because another Flow is active.");
802
815
  const roles = runtime.loadRoles();
@@ -805,6 +818,10 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
805
818
  const reviewer = needsReviewer ? roles.find(({ name }) => name === "reviewer") : undefined;
806
819
  if (!implementer) throw new Error("delegate_flow requires an implementer Role.");
807
820
  if (needsReviewer && !reviewer) throw new Error("delegate_flow requires a reviewer Role when a unit declares review.");
821
+ const emitProgress = (line: string) => {
822
+ const progress: FlowProgress = { line };
823
+ onUpdate?.({ content: [{ type: "text", text: progress.line }], details: progress });
824
+ };
808
825
  const flow: FlowState = {
809
826
  phase: "running",
810
827
  generation: runtime.getSessionGeneration(),
@@ -818,6 +835,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
818
835
  warnings: [],
819
836
  };
820
837
  active = flow;
838
+ emitProgress(`setup · ${unitCount(request.units.length)}`);
821
839
  const operationSignal = bindSignal(flow, signal);
822
840
  const meter: UsageMeter = {};
823
841
  let setupComplete = false;
@@ -855,6 +873,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
855
873
  await checkMain(flow.main, operationSignal);
856
874
  assertCurrent(flow);
857
875
  setupComplete = true;
876
+ let completedImplementers = 0;
858
877
  const settlements = await Promise.all(flow.units.map((unit, index) => runChild(
859
878
  flow,
860
879
  flow.implementer,
@@ -866,11 +885,15 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
866
885
  operationSignal,
867
886
  ctx,
868
887
  meter,
869
- )));
888
+ ).then((settlement) => {
889
+ completedImplementers += 1;
890
+ emitProgress(`implement · ${completedImplementers}/${flow.units.length} complete`);
891
+ return settlement;
892
+ })));
870
893
  for (const [index, settlement] of settlements.entries()) flow.units[index]!.implementation = settlement;
871
894
  assertCurrent(flow);
872
895
  if (operationSignal.aborted) operationSignal.throwIfAborted();
873
- return await processFlow(flow, toolCallId, operationSignal, ctx, meter);
896
+ return await processFlow(flow, toolCallId, operationSignal, ctx, meter, emitProgress);
874
897
  } catch (error) {
875
898
  if (!setupComplete) {
876
899
  for (const unit of [...flow.units].reverse()) {
@@ -892,16 +915,16 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
892
915
  promptSnippet: "Repair and continue the blocked deterministic Flow",
893
916
  promptGuidelines: ["Call delegate_flow_continue only after delegate_flow reports a repairable block, with explicit guidance addressing that block."],
894
917
  parameters: DelegateFlowContinueSchema,
895
- renderShell: "self",
896
918
  renderCall(_args, theme, _context) {
897
- return renderToolLines([theme.fg("toolTitle", "delegate_flow_continue · working: repair continuation")], theme);
919
+ return renderToolLines([theme.fg("toolTitle", "delegate_flow_continue · repair continuation")], theme);
898
920
  },
899
- renderResult(result, _options, theme, _context) {
921
+ renderResult(result, { isPartial }, theme, _context) {
922
+ if (isPartial) return renderToolLines(isFlowProgress(result.details) ? [theme.fg("muted", result.details.line)] : [], theme);
900
923
  const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
901
924
  return renderToolLines(flowResultLines(text), theme);
902
925
  },
903
926
  prepareArguments: parseDelegateFlowContinue,
904
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
927
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
905
928
  const { guidance, modelClass } = parseDelegateFlowContinue(params);
906
929
  const flow = active;
907
930
  if (!flow) throw new Error("delegate_flow_continue requires an active blocked Flow.");
@@ -916,7 +939,12 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
916
939
  if (modelClass !== undefined) unit.modelClass = modelClass;
917
940
  const operationSignal = bindSignal(flow, signal);
918
941
  const meter: UsageMeter = {};
942
+ const emitProgress = (line: string) => {
943
+ const progress: FlowProgress = { line };
944
+ onUpdate?.({ content: [{ type: "text", text: progress.line }], details: progress });
945
+ };
919
946
  try {
947
+ emitProgress(`repair · unit ${flow.index + 1}/${flow.units.length}`);
920
948
  unit.implementation = await runChild(
921
949
  flow,
922
950
  flow.implementer,
@@ -931,7 +959,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
931
959
  );
932
960
  assertCurrent(flow);
933
961
  if (operationSignal.aborted) operationSignal.throwIfAborted();
934
- return await processFlow(flow, toolCallId, operationSignal, ctx, meter);
962
+ return await processFlow(flow, toolCallId, operationSignal, ctx, meter, emitProgress);
935
963
  } catch (error) {
936
964
  return terminal(flow, "infrastructure", errorText(error), meter);
937
965
  }
@@ -6,6 +6,7 @@ import {
6
6
  availableTaskModels,
7
7
  type ThinkingLevel,
8
8
  modelReference,
9
+ registerModelTask,
9
10
  resolveAvailableModel,
10
11
  resolveConfiguredTaskRoute,
11
12
  type ResolvedTaskRoute,
@@ -15,6 +16,7 @@ import {
15
16
  capEphemeralSubagentOutput as capOutput,
16
17
  createChildWorktree,
17
18
  createEphemeralSubagentExecutor,
19
+ DELEGATE_TASK,
18
20
  createRoleLaunch,
19
21
  EphemeralSubagentError,
20
22
  finalizeChildWorktree,
@@ -54,10 +56,10 @@ import {
54
56
  type WorkflowEntry,
55
57
  } from "./workflow.ts";
56
58
 
57
- const SUBAGENT_TASK = "pi-subagent/delegateTask";
58
59
  const WIDGET_KEY = "subagent-status";
59
60
  const WIDGET_INTERVAL_MS = 80;
60
- const MAX_WIDGET_ROWS = 8;
61
+ const MAX_WIDGET_ITEMS = 8;
62
+ const MAX_WIDGET_LINES = 6;
61
63
  export const MAX_WIDGET_ACTIVE_TOOLS = 8;
62
64
  const DEFAULT_TIMEOUT_POLICY = {
63
65
  idleMs: DEFAULT_TIMEOUT_CONFIG.idleMinutes * 60_000,
@@ -178,9 +180,30 @@ function isWorkflowTransportDetails(value: unknown): value is WorkflowTransportD
178
180
  }
179
181
 
180
182
  function workflowCallLabel(args: { tasks?: unknown; chain?: unknown }): string {
181
- if (Array.isArray(args.chain)) return `delegate_task · working: chain · ${args.chain.length} task${args.chain.length === 1 ? "" : "s"}`;
182
- if (Array.isArray(args.tasks)) return `delegate_task · working: parallel · ${args.tasks.length} task${args.tasks.length === 1 ? "" : "s"}`;
183
- return "delegate_task · working: single · 1 task";
183
+ if (Array.isArray(args.chain)) return `delegate_task · chain · ${args.chain.length} task${args.chain.length === 1 ? "" : "s"}`;
184
+ if (Array.isArray(args.tasks)) return `delegate_task · parallel · ${args.tasks.length} task${args.tasks.length === 1 ? "" : "s"}`;
185
+ return "delegate_task · single · 1 task";
186
+ }
187
+
188
+ function workflowProgressLine(details: WorkflowTransportDetails): string {
189
+ let running = 0;
190
+ let pending = 0;
191
+ let complete = 0;
192
+ let failed = 0;
193
+ let skipped = 0;
194
+ for (const { status } of details.entries) {
195
+ switch (status) {
196
+ case "running": running += 1; break;
197
+ case "pending": pending += 1; break;
198
+ case "succeeded": complete += 1; break;
199
+ case "failed":
200
+ case "rejected": failed += 1; break;
201
+ case "skipped": skipped += 1; break;
202
+ }
203
+ }
204
+ return [[running, "running"], [pending, "pending"], [complete, "complete"], [failed, "failed"], [skipped, "skipped"]]
205
+ .flatMap(([count, label]) => count ? [`${count} ${label}`] : [])
206
+ .join(" · ");
184
207
  }
185
208
 
186
209
  function workflowResultLines(details: WorkflowTransportDetails, theme: Theme): string[] {
@@ -215,18 +238,23 @@ function renderWidgetRows(
215
238
  spinnerIndex: number,
216
239
  theme: Theme,
217
240
  ): string[] {
218
- const visible = items.slice(0, MAX_WIDGET_ROWS);
241
+ const ordered = [...items.filter(({ status }) => status === "working"), ...items.filter(({ status }) => status !== "working")];
242
+ const visible = ordered.slice(0, ordered.length > MAX_WIDGET_LINES ? MAX_WIDGET_LINES - 1 : MAX_WIDGET_LINES);
219
243
  if (!visible.length) return [];
220
- const indent = " ".repeat(Math.min(2, Math.max(0, width - 1)));
221
- const contentWidth = Math.max(0, width - indent.length);
222
- const lines = visible.flatMap((item) => [
223
- truncateToWidth(
224
- `${statusGlyph(item.status, spinnerIndex, theme)} ${theme.fg("accent", item.role)} · ${statusLabel(item.status)} · ${theme.fg("text", item.task)}`,
225
- width,
226
- ),
227
- `${indent}${truncateToWidth(`${theme.fg("text", activityLabel(item, now))} · ${theme.fg("muted", activityMetrics(item, now))}`, contentWidth)}`,
228
- ]);
229
- if (items.length > visible.length) lines.push(truncateToWidth(theme.fg("muted", `… ${items.length - visible.length} more`), width));
244
+ const hidden = ordered.slice(visible.length);
245
+ const lines = visible.map((item) => truncateToWidth(
246
+ `${statusGlyph(item.status, spinnerIndex, theme)} ${theme.fg("accent", item.role)} · ${statusLabel(item.status)} · ${theme.fg("text", item.task)} · ${theme.fg("text", activityLabel(item, now))} · ${theme.fg("muted", activityMetrics(item, now))}`,
247
+ width,
248
+ ));
249
+ if (hidden.length) {
250
+ const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0, aborted: 0 };
251
+ for (const { status } of hidden) counts[status] += 1;
252
+ lines.push(truncateToWidth(theme.fg("muted", [
253
+ `… ${hidden.length} more`,
254
+ ...(["working", "success", "failure", "aborted"] as const).flatMap((status) =>
255
+ counts[status] ? [`${counts[status]} ${statusLabel(status)}`] : []),
256
+ ].join(" · ")), width));
257
+ }
230
258
  return lines;
231
259
  }
232
260
 
@@ -277,6 +305,7 @@ export default function subagentExtension(
277
305
  pi: ExtensionAPI,
278
306
  overrideTimeoutPolicy?: TimeoutPolicy,
279
307
  ): void {
308
+ registerModelTask(pi, DELEGATE_TASK);
280
309
  const widgetItems = new Map<string, WidgetItem>();
281
310
  // Each child is a full Pi process issuing its own model calls; cap parallel
282
311
  // spend. Precedence: PI_SUBAGENT_MAX_SUBAGENTS env > config/pi-subagent/pi-subagent.json
@@ -357,11 +386,11 @@ export default function subagentExtension(
357
386
  ) => {
358
387
  if (!ctx.hasUI) return;
359
388
  ensureWidget(ctx);
360
- if (!widgetItems.has(id) && widgetItems.size >= MAX_WIDGET_ROWS) {
389
+ if (!widgetItems.has(id) && widgetItems.size >= MAX_WIDGET_ITEMS) {
361
390
  for (const [oldestId, item] of widgetItems) {
362
391
  if (item.status === "working") continue;
363
392
  widgetItems.delete(oldestId);
364
- if (widgetItems.size < MAX_WIDGET_ROWS) break;
393
+ if (widgetItems.size < MAX_WIDGET_ITEMS) break;
365
394
  }
366
395
  }
367
396
  widgetItems.set(id, {
@@ -557,7 +586,7 @@ export default function subagentExtension(
557
586
  return createRoleLaunch(pi, launchCtx, {
558
587
  role,
559
588
  route: modelClass === undefined
560
- ? resolveConfiguredTaskRoute(launchCtx, SUBAGENT_TASK)
589
+ ? resolveConfiguredTaskRoute(launchCtx, DELEGATE_TASK)
561
590
  : resolveTaskRoute(launchCtx, modelClass),
562
591
  });
563
592
  },
@@ -580,13 +609,14 @@ export default function subagentExtension(
580
609
  "delegate_task background applies to the whole selected workflow and returns before results exist; use it only when the user explicitly asks for non-blocking work.",
581
610
  ],
582
611
  parameters: WorkflowSchema,
583
- renderShell: "self",
584
612
  renderCall(args, theme, _context) {
585
613
  return renderToolLines([theme.fg("toolTitle", workflowCallLabel(args))], theme);
586
614
  },
587
615
  renderResult(result, { isPartial }, theme, _context) {
588
- if (isPartial) return renderToolLines([], theme);
589
616
  const details = result.details;
617
+ if (isPartial) return renderToolLines(isWorkflowTransportDetails(details)
618
+ ? [theme.fg("muted", workflowProgressLine(details))]
619
+ : [], theme);
590
620
  if (isWorkflowTransportDetails(details)) return renderToolLines(workflowResultLines(details, theme), theme);
591
621
  if (typeof details === "object" && details !== null && (details as { background?: unknown }).background === true) {
592
622
  return renderToolLines([theme.fg("muted", "Background workflow accepted.")], theme);
@@ -633,7 +663,7 @@ export default function subagentExtension(
633
663
  route: delegation.model !== undefined
634
664
  ? resolveDesignatedRoute(launchCtx(), delegation.model, delegation.thinking)
635
665
  : delegation.modelClass === undefined
636
- ? resolveConfiguredTaskRoute(launchCtx(), SUBAGENT_TASK, undefined, delegation.thinking)
666
+ ? resolveConfiguredTaskRoute(launchCtx(), DELEGATE_TASK, undefined, delegation.thinking)
637
667
  : resolveTaskRoute(launchCtx(), delegation.modelClass, undefined, delegation.thinking),
638
668
  });
639
669
  const notifyMissingSkills = (role: Role, launch: ReturnType<typeof resolveLaunch>) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "6.1.0",
3
+ "version": "7.0.0",
4
4
  "description": "Delegate bounded single, parallel, or chained tasks to isolated Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -66,6 +66,6 @@
66
66
  "dependencies": {
67
67
  "@henryqw/pi-herdr": "^0.4.0",
68
68
  "@henryqw/pi-multi-codex": "^0.3.8",
69
- "@henryqw/pi-task-models": "^2.0.0"
69
+ "@henryqw/pi-task-models": "^3.0.0"
70
70
  }
71
71
  }
@@ -9,6 +9,8 @@ You are Main, the planner/orchestrator: slice work and call `delegate_flow`. Do
9
9
 
10
10
  ## Slice
11
11
 
12
+ Before slicing, identify applicable repository prohibitions. If the request or plan conflicts with them, stop and resolve the conflict before delegation. Copy them into every affected task and into `review` when automated validation cannot establish compliance; never replace repository policy with generic preservation or migration assumptions. When compatibility is disallowed, require deletion of replaced paths and forbid legacy readers, aliases, adapters, dual schemas, deprecation paths, and compatibility fallbacks.
13
+
12
14
  Use the fewest cohesive units. `delegate_flow` is for independent units expected to commute; combine or sequence work that overlaps files, APIs, schemas, generated output, package metadata, lockfiles, or invariants. Dependent work remains outside Flow, in one task or ordinary caller-controlled sequencing.
13
15
 
14
16
  Give every unit a bounded objective, owned scope and exclusions, and its direct validation command/argument array. Do not pass the parent request unchanged. Use `modelClass: "fast"` normally; use `"balanced"` upfront only for obviously complex work. Add non-empty `review` only for an explicit judgment that automated validation cannot establish. Call `delegate_flow` with 1–8 units; the runtime always supplies the effective Implementer and supplies the Reviewer only when a unit needs review.