@henryqw/pi-subagent 16.1.0 → 17.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
@@ -25,7 +25,7 @@ Provide validated built-in and user Roles, shared task-model Pi launch policy, g
25
25
  - Every Role launch installs the shared tool policy. On a continuing token crossing, or the continuing penultimate `maxTurns` turn, the policy waits for `turn_end`, disables every tool, and steers one structured final report. This covers `delegate_task` and library Role launches made with the public launch API. With `maxTurns` set to 1, tools are disabled during `session_start`, and the sole provider turn is the response-only handoff. The fixed decision packet is the default, but exact task or Role output takes precedence and is returned alone. Terminal boundary responses get no handoff. Before the final boundary, the policy steers the fixed convergence warning once at each 80% threshold for completed turns, aggregate tokens when configured, and maximum runtime. It combines thresholds first due together and starts no timer or extra warning turn. Timeout, provider, or child-process failures can prevent a handoff. After direct Pi exits, inherited stdout/stderr drain until EOF unless an escaped descendant holds them past short inactivity or a one-second hard deadline. Configurable in `~/.pi/agent/config/pi-subagent/config.json` (`maxTurns` defaults to 50; `maxTokens` defaults to unlimited; `timeout.idleMinutes`/`maxMinutes` default to 10/30).
26
26
  - Up to five active ephemeral `delegate_task` children run per Main by default, configurable via `maxSubagents` in `~/.pi/agent/config/pi-subagent/config.json` or the `PI_SUBAGENT_MAX_SUBAGENTS` environment variable; excess calls wait FIFO. Queued calls do not start a child or consume child timeout.
27
27
  - Ambient child extensions and Skills stay disabled. Every Role requires `tools`, `extensions`, and `skills` YAML arrays, and every launch installs the Role tool policy. Optional `mcps` defaults to an empty deny-all list. Non-empty `mcps` loads `pi-mcp-adapter` with an isolated in-memory config containing only those exact configured servers; every tool that those servers expose is active, unknown names fail before the first model turn, and direct adapter loading is rejected. `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.
28
- - 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.
28
+ - Role Skill names resolve through Main's effective Pi Skill registry. `prepareRoleLaunch` and `resolveConfiguredRoleLaunch` reject a nonempty `missingSkills` list before returning a prepared launch, so `delegate_task` does not start an under-capable child. Callers that bypass preparation must reject missing Skills before launch. 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.
29
29
  - Route precedence is explicit call-level `modelClass` > Role `modelClass` > configured Model Task assignment or declared default. pi-subagent's local `pi-subagent/delegateTask` declaration defaults to `fast`. A direct model replaces only the selected route model and must honor its exact thinking level. Library callers select a Role plus their own Model Task declaration.
30
30
  - The selected profile resolves primary then fallback only before launch when a route, model, or thinking level is unavailable. A direct model never changes the route level and fails before launch if it cannot honor it. A missing local JSON config uses defaults quietly. Missing shared task-model config warns once per session because delegation needs a route. If neither route is usable, launch rejects with `Run /task-models`; a started child is never retried by this package.
31
31
  - User Role Markdown files and Subagent JSON config (`config/pi-subagent/config.json`) live only in the user `config/pi-subagent` directory; model routes live in shared `config/pi-task-models/config.json`. Package-shipped built-in Roles (`implementer`, `reviewer`, `scout`) resolve from the package's own `examples/roles/` Markdown through the same parser and intentionally leave `modelClass` unset; same-named user files override built-ins for `delegate_task` and public API callers. The `synthesizer` Markdown remains the only optional inert sample.
package/README.md CHANGED
@@ -175,6 +175,8 @@ A Role's `modelClass` is a default. A call-level class wins.
175
175
 
176
176
  An unreadable or invalid Role fails loading fast. Duplicate Role names are rejected. A same-named user file overrides a built-in Role.
177
177
 
178
+ A missing named Skill rejects `prepareRoleLaunch` and `resolveConfiguredRoleLaunch`. `delegate_task` returns a Role-specific workflow error and does not start a child.
179
+
178
180
  The package always provides these built-in Roles. Their files leave `modelClass` unset, so they use the configured `pi-subagent/delegateTask` assignment or declared default unless a call overrides it:
179
181
 
180
182
  | Role | Purpose | Isolation/use |
@@ -190,9 +192,12 @@ The package root includes these main exports:
190
192
  | Surface | Type | Purpose |
191
193
  | --- | --- | --- |
192
194
  | `loadRoles` | function | Loads built-in and user Role definitions. |
195
+ | `RoleName` / `parseRoleName` | type/function | Normalizes arbitrary Role names and rejects empty or C0/C1 control-character values. |
193
196
  | `resolveRoleSkills` | function | Resolves a Role's named Skills from Pi's effective registry. |
194
197
  | `resolveRoleLaunch` | function | Resolves a Role, route, and launch resources. |
198
+ | `resolveConfiguredRoleLaunch` | function | Resolves a configured Role and its package resources with an explicit model class. Rejects missing Role Skills. |
195
199
  | `createRoleLaunch` | function | Builds launch arguments from a resolved route. |
200
+ | `prepareRoleLaunch` / `finalizeRoleLaunch` | functions | Separates the stable Role prompt, exposes its immutable tool policy, and rejects missing Role Skills. |
196
201
  | `createEphemeralSubagentExecutor` | function | Creates the bounded child-process executor. |
197
202
  | Worktree helpers | functions | Create, inspect, finalize, and report child worktrees. |
198
203
  | `prepareExactReviewEvidence` | function | Create a bounded private base-to-tip patch with exact Git identity for caller-owned review. |
@@ -201,7 +206,7 @@ The executor works only inside the active Pi process. It does not discover or st
201
206
 
202
207
  `finalizeChildWorktree` returns the breaking `WorktreePayload` lifecycle union. `pruned` proves zero commits, a clean tree, and removed worktree and branch. `retained` contains measured `commits` and `dirty` values. `recovery` has an actionable `note` and only completed measurements. An omitted recovery measurement is unknown.
203
208
 
204
- See the [public Role and executor API](./docs/orchestration.md#public-role-and-executor-api) for contracts and a `prepare` example. Pass `modelClass` to `resolveRoleLaunch` to override a Role default.
209
+ See the [public Role and executor API](./docs/orchestration.md#public-role-and-executor-api) for contracts and a `prepare` example. Pass `modelClass` to `resolveRoleLaunch` to override a Role default. `resolveConfiguredRoleLaunch` requires a model class and does not use Role or task defaults.
205
210
 
206
211
  ## Limits and recovery
207
212
 
@@ -44,6 +44,7 @@ export interface EphemeralSubagentRunInput {
44
44
  interface EphemeralSubagentResultBase {
45
45
  exitCode: number;
46
46
  output: string;
47
+ outputTruncated: boolean;
47
48
  stderr: string;
48
49
  stopReason?: string;
49
50
  errorMessage?: string;
package/dist/ephemeral.js CHANGED
@@ -416,8 +416,13 @@ async function runPi(prepared, input, budget, invocation) {
416
416
  let lineEventType;
417
417
  let ignoreLine = false;
418
418
  let output = "";
419
+ let outputTruncated = false;
419
420
  const stderr = { prefix: "", totalBytes: 0 };
420
421
  const partial = { prefix: "", totalBytes: 0 };
422
+ const updateOutput = (nextOutput) => {
423
+ output = nextOutput;
424
+ outputTruncated = partial.totalBytes > MAX_OUTPUT_BYTES;
425
+ };
421
426
  let hasPartialText = false;
422
427
  let stopReason;
423
428
  let errorMessage;
@@ -532,6 +537,7 @@ async function runPi(prepared, input, budget, invocation) {
532
537
  outcome,
533
538
  exitCode,
534
539
  output,
540
+ outputTruncated,
535
541
  stderr: boundedText(stderr),
536
542
  stopReason,
537
543
  errorMessage,
@@ -657,14 +663,16 @@ async function runPi(prepared, input, budget, invocation) {
657
663
  const update = record.assistantMessageEvent;
658
664
  if (update && typeof update === "object" && !Array.isArray(update)) {
659
665
  const assistantEvent = update;
660
- if (assistantEvent.type === "text_start" && hasPartialText)
666
+ if (assistantEvent.type === "text_start" && hasPartialText) {
661
667
  appendBounded(partial, "\n");
668
+ updateOutput(boundedText(partial));
669
+ }
662
670
  if (assistantEvent.type === "text_start")
663
671
  hasPartialText = true;
664
672
  if (assistantEvent.type === "text_delta" && typeof assistantEvent.delta === "string") {
665
673
  hasPartialText = true;
666
674
  appendBounded(partial, assistantEvent.delta);
667
- output = boundedText(partial);
675
+ updateOutput(boundedText(partial));
668
676
  invokeCallback("onUpdate", input.onUpdate, output);
669
677
  }
670
678
  }
@@ -697,7 +705,10 @@ async function runPi(prepared, input, budget, invocation) {
697
705
  return;
698
706
  const text = assistantText(record.message);
699
707
  if (text !== undefined) {
700
- output = capEphemeralSubagentOutput(text);
708
+ partial.prefix = "";
709
+ partial.totalBytes = 0;
710
+ appendBounded(partial, text);
711
+ updateOutput(capEphemeralSubagentOutput(text));
701
712
  invokeCallback("onUpdate", input.onUpdate, output);
702
713
  }
703
714
  if (record.message && typeof record.message === "object" && !Array.isArray(record.message)) {
package/dist/index.d.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
3
3
  export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.ts";
4
- export { fingerprintRoleMcpConfig, parseRoleMcpAllowlist, roleMcpFlagValue, selectRoleMcpConfig, type RoleMcpConfig } from "./mcp-role.ts";
4
+ export { parseRoleMcpAllowlist, roleMcpFlagValue, selectRoleMcpConfig, type RoleMcpConfig } from "./mcp-role.ts";
5
5
  export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutionBudget, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
6
6
  export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
7
7
  export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
8
8
  export declare const PI_ORCHESTRATOR_PROCESS_LEASE = "PI_ORCHESTRATOR_PROCESS_LEASE";
9
- export declare const ROLE_MCP_CONFIG_SHA256_FLAG = "pi-subagent-role-mcp-config-sha256";
10
9
  export declare const ROLE_MCP_POLICY_FLAG = "pi-subagent-role-mcps";
11
10
  export declare const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
12
11
  export declare const CHILD_EXCLUDED_TOOL_NAMES: readonly ["delegate_task", "ask_question", "orchestrate_execute", "orchestrate_status", "orchestrate_resume", "orchestrate_abort"];
@@ -17,8 +16,9 @@ export declare const DELEGATE_TASK: {
17
16
  readonly purpose: "Launch an isolated Pi subagent.";
18
17
  readonly defaultProfile: "fast";
19
18
  };
19
+ export type RoleName = string;
20
20
  export interface Role {
21
- name: string;
21
+ name: RoleName;
22
22
  description: string;
23
23
  modelClass?: ProfileName;
24
24
  tools: string[];
@@ -37,6 +37,13 @@ export interface ResolvedRoleLaunch extends PiLaunch {
37
37
  thinkingLevel: ThinkingLevel;
38
38
  missingSkills: string[];
39
39
  }
40
+ export interface PreparedRoleLaunch extends ResolvedRoleLaunch {
41
+ role: RoleName;
42
+ isolation?: "worktree";
43
+ tools: readonly string[];
44
+ systemPrompt: string;
45
+ promptArgIndex: number;
46
+ }
40
47
  export interface CreateRoleLaunchInput {
41
48
  role: Role;
42
49
  route: ResolvedTaskRoute;
@@ -49,10 +56,16 @@ export interface ResolveRoleLaunchInput extends Omit<CreateRoleLaunchInput, "rou
49
56
  modelClass?: ProfileName;
50
57
  agentDir?: string;
51
58
  }
59
+ export interface ResolveConfiguredRoleLaunchInput {
60
+ role: string;
61
+ modelClass: ProfileName;
62
+ }
52
63
  export interface ResolvedRoleSkills {
53
64
  paths: string[];
54
65
  missing: string[];
55
66
  }
67
+ /** Normalize one arbitrary Role name using the Role configuration contract. */
68
+ export declare function parseRoleName(value: unknown, source?: string): RoleName;
56
69
  declare const BUILTIN_ROLE_NAMES: readonly ["implementer", "reviewer", "scout"];
57
70
  export type BuiltinRoleName = (typeof BUILTIN_ROLE_NAMES)[number];
58
71
  export declare function loadBuiltinRole(name: BuiltinRoleName): Role;
@@ -64,5 +77,19 @@ export declare function loadBuiltinRole(name: BuiltinRoleName): Role;
64
77
  export declare function loadRoles(agentDir?: string): Role[];
65
78
  export declare function resolveTaskRoute(ctx: ExtensionContext, profileName: ProfileName, agentDir?: string): ResolvedTaskRoute;
66
79
  export declare function resolveRoleSkills(pi: Pick<ExtensionAPI, "getCommands">, role: Role): ResolvedRoleSkills;
80
+ /** Resolve all enabled package resources selected by one Role's extension sources. */
81
+ export declare function resolveRolePackageResources(role: Role, ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">): Promise<{
82
+ extensions: string[];
83
+ skills: string[];
84
+ prompts: string[];
85
+ themes: string[];
86
+ }>;
67
87
  export declare function createRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: Pick<ExtensionContext, "isProjectTrusted">, input: CreateRoleLaunchInput): ResolvedRoleLaunch;
68
88
  export declare function resolveRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: ResolveRoleLaunchInput): ResolvedRoleLaunch;
89
+ /** Prepare a resolved or resolvable Role launch while keeping its system prompt out of argv. */
90
+ export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: ResolveRoleLaunchInput): PreparedRoleLaunch;
91
+ export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: CreateRoleLaunchInput): PreparedRoleLaunch;
92
+ /** Resolve and prepare a configured Role with its package-owned resources. */
93
+ export declare function resolveConfiguredRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: ResolveConfiguredRoleLaunchInput): Promise<PreparedRoleLaunch>;
94
+ /** Restore the system prompt pair after a caller has prepared its launch argv. */
95
+ export declare function finalizeRoleLaunch(prepared: PreparedRoleLaunch): ResolvedRoleLaunch;
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { readFileSync, readdirSync } from "node:fs";
2
2
  import { isAbsolute, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
4
+ import { DefaultPackageManager, getAgentDir, parseFrontmatter, SettingsManager, } from "@earendil-works/pi-coding-agent";
5
5
  import { extensionConfigDir } from "@henryqw/pi-config-store";
6
6
  import { hasDisplayControlCharacters } from "./display-text.js";
7
+ import { selectRoleMcpConfig } from "./mcp-role.js";
7
8
  import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, PROFILE_NAMES, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
8
9
  export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
9
- export { fingerprintRoleMcpConfig, parseRoleMcpAllowlist, roleMcpFlagValue, selectRoleMcpConfig } from "./mcp-role.js";
10
+ export { parseRoleMcpAllowlist, roleMcpFlagValue, selectRoleMcpConfig } from "./mcp-role.js";
10
11
  export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
11
12
  export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, } from "./worktree.js";
12
13
  export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, } from "./review-evidence.js";
@@ -15,7 +16,6 @@ const MULTI_CODEX_EXTENSION = fileURLToPath(import.meta.resolve("@henryqw/pi-mul
15
16
  const ROLE_MCP_EXTENSION = fileURLToPath(new URL("../extensions/role-mcp.ts", import.meta.url));
16
17
  const ROLE_TOOLS_EXTENSION = fileURLToPath(new URL("../extensions/role-tools.ts", import.meta.url));
17
18
  export const PI_ORCHESTRATOR_PROCESS_LEASE = "PI_ORCHESTRATOR_PROCESS_LEASE";
18
- export const ROLE_MCP_CONFIG_SHA256_FLAG = "pi-subagent-role-mcp-config-sha256";
19
19
  export const ROLE_MCP_POLICY_FLAG = "pi-subagent-role-mcps";
20
20
  export const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
21
21
  export const CHILD_EXCLUDED_TOOL_NAMES = [
@@ -27,6 +27,7 @@ export const CHILD_EXCLUDED_TOOL_NAMES = [
27
27
  "orchestrate_abort",
28
28
  ];
29
29
  export const CHILD_EXCLUDED_TOOLS = CHILD_EXCLUDED_TOOL_NAMES.join(",");
30
+ const SYSTEM_PROMPT_FLAG = "--append-system-prompt";
30
31
  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.";
31
32
  export const DELEGATE_TASK = {
32
33
  id: "pi-subagent/delegateTask",
@@ -46,6 +47,10 @@ const cleanDisplayText = (value, field, source) => {
46
47
  }
47
48
  return cleanText(value, field, source);
48
49
  };
50
+ /** Normalize one arbitrary Role name using the Role configuration contract. */
51
+ export function parseRoleName(value, source = "Role") {
52
+ return cleanDisplayText(value, "name", source);
53
+ }
49
54
  const stringList = (value, field, source) => {
50
55
  if (value === undefined)
51
56
  throw new Error(`${source}: ${field} is required.`);
@@ -72,9 +77,19 @@ function mcpList(value, source) {
72
77
  throw new Error(`${source}: mcps contains duplicate MCP server names.`);
73
78
  return names;
74
79
  }
80
+ function roleToolPolicy(role, additionalTools = []) {
81
+ return [...new Set([...role.tools, ...additionalTools].map((tool) => cleanText(tool, "tool", `Role ${role.name}`)))];
82
+ }
75
83
  function namesMcpAdapter(extension) {
76
84
  return extension.toLowerCase().split(/[\\/:@]+/).some((component) => component === "pi-mcp-adapter" || component.startsWith("pi-mcp-adapter."));
77
85
  }
86
+ const FORBIDDEN_ROLE_PACKAGE_SOURCE_NAMES = ["pi-orchestrator", "pi-mcp-adapter"];
87
+ function rejectForbiddenRolePackageSource(value, role) {
88
+ const components = value.toLowerCase().split(/[\\/:@]+/);
89
+ if (FORBIDDEN_ROLE_PACKAGE_SOURCE_NAMES.some((name) => components.some((component) => component === name || component.startsWith(`${name}.`)))) {
90
+ throw new Error(`Role ${role.name} extension explicitly names the forbidden ${FORBIDDEN_ROLE_PACKAGE_SOURCE_NAMES.join("/")} source: ${value}`);
91
+ }
92
+ }
78
93
  function roleModelClass(value, source) {
79
94
  if (value === undefined)
80
95
  return;
@@ -83,6 +98,14 @@ function roleModelClass(value, source) {
83
98
  }
84
99
  return value;
85
100
  }
101
+ function roleIsolation(value, source) {
102
+ if (value === undefined)
103
+ return;
104
+ if (cleanText(value, "isolation", source) !== "worktree") {
105
+ throw new Error(`${source}: isolation must be "worktree".`);
106
+ }
107
+ return "worktree";
108
+ }
86
109
  // Built-in Roles resolved from the package-shipped Markdown relative to this module.
87
110
  const BUILTIN_ROLE_NAMES = ["implementer", "reviewer", "scout"];
88
111
  /** Single-file Role parser shared by built-in and user roles. */
@@ -95,12 +118,10 @@ function parseRoleFile(file, raw) {
95
118
  throw new Error(`${file}: ${error instanceof Error ? error.message : String(error)}`);
96
119
  }
97
120
  const frontmatter = parsed.frontmatter;
98
- const isolation = frontmatter.isolation === undefined ? undefined : cleanText(frontmatter.isolation, "isolation", file);
99
- if (isolation !== undefined && isolation !== "worktree")
100
- throw new Error(`${file}: isolation must be "worktree".`);
121
+ const isolation = roleIsolation(frontmatter.isolation, file);
101
122
  const modelClass = roleModelClass(frontmatter.modelClass, file);
102
123
  return {
103
- name: cleanDisplayText(frontmatter.name, "name", file),
124
+ name: parseRoleName(frontmatter.name, file),
104
125
  description: cleanDisplayText(frontmatter.description, "description", file),
105
126
  ...(modelClass === undefined ? {} : { modelClass }),
106
127
  tools: stringList(frontmatter.tools, "tools", file),
@@ -192,11 +213,39 @@ export function resolveRoleSkills(pi, role) {
192
213
  }
193
214
  return { paths, missing };
194
215
  }
216
+ function packageManager(ctx) {
217
+ const agentDir = getAgentDir();
218
+ const settingsManager = SettingsManager.create(ctx.cwd, agentDir, { projectTrusted: ctx.isProjectTrusted() });
219
+ return new DefaultPackageManager({ cwd: ctx.cwd, agentDir, settingsManager });
220
+ }
221
+ /** Resolve all enabled package resources selected by one Role's extension sources. */
222
+ export async function resolveRolePackageResources(role, ctx) {
223
+ const sources = role.extensions;
224
+ for (const source of sources)
225
+ rejectForbiddenRolePackageSource(source, role);
226
+ if (!sources.length)
227
+ return { extensions: [], skills: [], prompts: [], themes: [] };
228
+ const resolved = await packageManager(ctx).resolveExtensionSources([...sources]);
229
+ const resourceGroups = [resolved.extensions, resolved.skills, resolved.prompts, resolved.themes]
230
+ .map((resources) => resources.filter((resource) => resource.enabled));
231
+ const resolvedSources = new Set(resourceGroups.flat().map((resource) => resource.metadata.source));
232
+ const missing = sources.filter((source) => !resolvedSources.has(source));
233
+ if (missing.length)
234
+ throw new Error(`Role extension sources resolved no resources: ${missing.join(", ")}.`);
235
+ return {
236
+ extensions: resourceGroups[0].map((resource) => resource.path),
237
+ skills: resourceGroups[1].map((resource) => resource.path),
238
+ prompts: resourceGroups[2].map((resource) => resource.path),
239
+ themes: resourceGroups[3].map((resource) => resource.path),
240
+ };
241
+ }
195
242
  export function createRoleLaunch(pi, ctx, input) {
243
+ return createRoleLaunchFromSkills(ctx, input, resolveRoleSkills(pi, input.role));
244
+ }
245
+ function createRoleLaunchFromSkills(ctx, input, skills) {
196
246
  const role = input.role;
197
- const skills = resolveRoleSkills(pi, role);
198
247
  const mcps = mcpList(role.mcps, `Role ${role.name}`);
199
- const tools = [...new Set([...role.tools, ...(input.tools ?? [])].map((tool) => cleanText(tool, "tool", `Role ${role.name}`)))];
248
+ const tools = roleToolPolicy(role, input.tools);
200
249
  const selectedExtensions = [...role.extensions, ...(input.extensions ?? [])]
201
250
  .map((extension) => validateExtension(extension, `Role ${role.name}`));
202
251
  if (selectedExtensions.some(namesMcpAdapter)) {
@@ -227,7 +276,7 @@ export function createRoleLaunch(pi, ctx, input) {
227
276
  if (input.route.thinkingLevel)
228
277
  args.push("--thinking", input.route.thinkingLevel);
229
278
  args.push(ctx.isProjectTrusted() ? "--approve" : "--no-approve");
230
- args.push("--append-system-prompt", `${CHILD_IDENTITY_POLICY}\n\n${cleanText(role.systemPrompt, "system prompt", `Role ${role.name}`)}`);
279
+ args.push(SYSTEM_PROMPT_FLAG, `${CHILD_IDENTITY_POLICY}\n\n${cleanText(role.systemPrompt, "system prompt", `Role ${role.name}`)}`);
231
280
  return {
232
281
  env,
233
282
  args,
@@ -246,3 +295,97 @@ export function resolveRoleLaunch(pi, ctx, input) {
246
295
  : resolveTaskRoute(ctx, selectedClass, agentDir),
247
296
  });
248
297
  }
298
+ function stripRoleSystemPrompt(rawArgs) {
299
+ const indexes = rawArgs.flatMap((arg, index) => arg === SYSTEM_PROMPT_FLAG ? [index] : []);
300
+ if (indexes.length !== 1)
301
+ throw new Error(`Role launch must contain exactly one ${SYSTEM_PROMPT_FLAG} pair.`);
302
+ const promptArgIndex = indexes[0];
303
+ const systemPrompt = rawArgs[promptArgIndex + 1];
304
+ if (typeof systemPrompt !== "string" || !systemPrompt.includes("\n") || !systemPrompt.trim() || systemPrompt.includes("\0")) {
305
+ throw new Error(`Role ${SYSTEM_PROMPT_FLAG} value must be the exact multiline Role prompt.`);
306
+ }
307
+ const args = [...rawArgs.slice(0, promptArgIndex), ...rawArgs.slice(promptArgIndex + 2)];
308
+ if (args.includes(SYSTEM_PROMPT_FLAG) || args.includes(systemPrompt)) {
309
+ throw new Error("Sanitized Role argv must contain no prompt flag or raw Role prompt.");
310
+ }
311
+ return { args, systemPrompt, promptArgIndex };
312
+ }
313
+ function prepareResolvedRoleLaunch(roleDefinition, launch, additionalTools = []) {
314
+ const role = parseRoleName(roleDefinition.name);
315
+ const isolation = roleIsolation(roleDefinition.isolation, `Role ${role}`);
316
+ const tools = Object.freeze(roleToolPolicy(roleDefinition, additionalTools));
317
+ const { args, systemPrompt, promptArgIndex } = stripRoleSystemPrompt(launch.args);
318
+ return { ...launch, args, role, isolation, tools, systemPrompt, promptArgIndex };
319
+ }
320
+ function assertNoMissingRoleSkills(role, launch) {
321
+ if (!launch.missingSkills.length)
322
+ return;
323
+ throw new Error(`Role ${parseRoleName(role.name)} requires missing Skills: ${launch.missingSkills.join(", ")}.`);
324
+ }
325
+ export function prepareRoleLaunch(pi, ctx, input) {
326
+ const launch = "route" in input
327
+ ? createRoleLaunch(pi, ctx, input)
328
+ : resolveRoleLaunch(pi, ctx, input);
329
+ assertNoMissingRoleSkills(input.role, launch);
330
+ return prepareResolvedRoleLaunch(input.role, launch, input.tools);
331
+ }
332
+ /** Resolve and prepare a configured Role with its package-owned resources. */
333
+ export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
334
+ const roleName = parseRoleName(input.role);
335
+ if (input.modelClass === undefined)
336
+ throw new Error("Configured Role launch requires an explicit modelClass.");
337
+ const matches = loadRoles().filter((role) => role.name === roleName);
338
+ if (matches.length !== 1)
339
+ throw new Error(`Required configured Role ${roleName} is missing or ambiguous.`);
340
+ const role = matches[0];
341
+ if (role.mcps?.length) {
342
+ const { loadMcpConfig } = await import("pi-mcp-adapter/config");
343
+ selectRoleMcpConfig(loadMcpConfig(join(getAgentDir(), "mcp.json"), ctx.cwd), role.mcps);
344
+ }
345
+ const resources = await resolveRolePackageResources(role, ctx);
346
+ const effectiveRole = { ...role, extensions: resources.extensions };
347
+ const namedSkills = resolveRoleSkills(pi, effectiveRole);
348
+ const skills = {
349
+ ...namedSkills,
350
+ paths: [...new Set([...namedSkills.paths, ...resources.skills])],
351
+ };
352
+ const launch = createRoleLaunchFromSkills(ctx, {
353
+ role: effectiveRole,
354
+ route: resolveTaskRoute(ctx, input.modelClass),
355
+ }, skills);
356
+ assertNoMissingRoleSkills(effectiveRole, launch);
357
+ const additions = [
358
+ "--no-prompt-templates",
359
+ "--no-themes",
360
+ ...resources.prompts.flatMap((path) => ["--prompt-template", path]),
361
+ ...resources.themes.flatMap((path) => ["--theme", path]),
362
+ ];
363
+ const promptArgIndex = launch.args.indexOf(SYSTEM_PROMPT_FLAG);
364
+ if (promptArgIndex < 0)
365
+ throw new Error(`Resolved Role launch has no ${SYSTEM_PROMPT_FLAG}.`);
366
+ const args = [...launch.args];
367
+ args.splice(promptArgIndex, 0, ...additions);
368
+ return prepareResolvedRoleLaunch(effectiveRole, { ...launch, args });
369
+ }
370
+ /** Restore the system prompt pair after a caller has prepared its launch argv. */
371
+ export function finalizeRoleLaunch(prepared) {
372
+ if (prepared.args.includes(SYSTEM_PROMPT_FLAG)) {
373
+ throw new Error(`Prepared Role launch already contains ${SYSTEM_PROMPT_FLAG}.`);
374
+ }
375
+ if (!Number.isSafeInteger(prepared.promptArgIndex)
376
+ || prepared.promptArgIndex < 0 || prepared.promptArgIndex > prepared.args.length) {
377
+ throw new Error("Prepared Role launch has an invalid system prompt insertion index.");
378
+ }
379
+ if (typeof prepared.systemPrompt !== "string" || !prepared.systemPrompt.trim() || prepared.systemPrompt.includes("\0")) {
380
+ throw new Error("Prepared Role launch has an invalid system prompt.");
381
+ }
382
+ const args = [...prepared.args];
383
+ args.splice(prepared.promptArgIndex, 0, SYSTEM_PROMPT_FLAG, prepared.systemPrompt);
384
+ return {
385
+ env: prepared.env,
386
+ args,
387
+ model: prepared.model,
388
+ thinkingLevel: prepared.thinkingLevel,
389
+ missingSkills: prepared.missingSkills,
390
+ };
391
+ }
@@ -4,5 +4,4 @@ export interface RoleMcpConfig {
4
4
  }
5
5
  export declare function parseRoleMcpAllowlist(value: unknown): string[];
6
6
  export declare function roleMcpFlagValue(args: readonly string[], flag: string): string | undefined;
7
- export declare function fingerprintRoleMcpConfig(config: RoleMcpConfig): string;
8
7
  export declare function selectRoleMcpConfig(config: RoleMcpConfig, allowlist: readonly string[]): RoleMcpConfig;
package/dist/mcp-role.js CHANGED
@@ -1,4 +1,3 @@
1
- import { createHash } from "node:crypto";
2
1
  export function parseRoleMcpAllowlist(value) {
3
2
  if (typeof value !== "string")
4
3
  throw new Error("The Role MCP policy flag must contain a JSON array of MCP server names.");
@@ -29,9 +28,6 @@ export function roleMcpFlagValue(args, flag) {
29
28
  throw new Error(`${flag} requires a value.`);
30
29
  return value;
31
30
  }
32
- export function fingerprintRoleMcpConfig(config) {
33
- return createHash("sha256").update(JSON.stringify(config)).digest("hex");
34
- }
35
31
  export function selectRoleMcpConfig(config, allowlist) {
36
32
  const missing = allowlist.filter((name) => !Object.hasOwn(config.mcpServers, name));
37
33
  if (missing.length)
@@ -8,8 +8,11 @@ Generic callers compose workflows with JavaScript. Fan-out and fan-in use promis
8
8
 
9
9
  Resource Policy is split at launch preparation:
10
10
 
11
- - Role owns base tools, extensions, and Skill names.
11
+ - Pi Subagent alone parses and resolves effective Roles.
12
+ - Role owns base tools, extension capability bundles, Skill names, and an exact MCP server allowlist.
12
13
  - Caller may add explicit tools, extensions, and environment through `createRoleLaunch`.
14
+ - `resolveConfiguredRoleLaunch` reloads the current Role, package resources, Skill registry, model route, and MCP policy for each launch.
15
+ - The prepared launch keeps the Role system prompt separate from prompt-free argv until the caller's final launch boundary.
13
16
  - The executor receives the resulting Pi Launch and does not discover resources.
14
17
 
15
18
  Built-in `implementer`, `reviewer`, and `scout` Roles ship as Markdown in `examples/roles/` and use the same parser as user Roles. For generic delegation, a same-named user Role explicitly overrides a built-in. The package does not install, copy, or write user configuration.
@@ -112,7 +112,7 @@ Every launch installs the Role tool policy. At launch, a package caller may add
112
112
 
113
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.
114
114
 
115
- Role Skill names resolve through Main's effective Pi Skill registry at launch. Missing names are returned in `ResolvedRoleLaunch.missingSkills`; `delegate_task` warns and skips them. Library callers must surface that warning themselves. Missing Skills do not block launch.
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.
116
116
 
117
117
  ### Role final-turn handoff
118
118
 
@@ -133,9 +133,12 @@ The package root exports the following mechanism-level APIs:
133
133
  | API | Responsibility |
134
134
  | --- | --- |
135
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. |
136
137
  | `resolveRoleSkills(pi, role)` | Resolve Role Skill names from Pi's effective registry. |
137
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. |
138
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. |
139
142
  | `createEphemeralSubagentExecutor(options)` | Queue and run one prepared no-session child per `run`. |
140
143
  | `createChildWorktree` / `finalizeChildWorktree` | Optional caller-managed worktree lifecycle; `createChildWorktree` can prepare exact metadata before allocation. |
141
144
  | `prepareExactReviewEvidence` | Validate Git identity and create a bounded private base-to-tip patch with exact `{base, tip, patchPath}` evidence. |
@@ -148,7 +151,7 @@ The package root exports the following mechanism-level APIs:
148
151
  | `retained` | `path`, `branch`, `commits`, `dirty` | Work was preserved. Both measurements are known. |
149
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. |
150
153
 
151
- 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`. 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`.
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.
152
155
 
153
156
  `createEphemeralSubagentExecutor` requires:
154
157
 
@@ -174,7 +177,8 @@ This JavaScript runs inside a Pi extension. `pi` is that extension's `ExtensionA
174
177
  ```js
175
178
  import {
176
179
  createEphemeralSubagentExecutor,
177
- resolveRoleLaunch,
180
+ finalizeRoleLaunch,
181
+ prepareRoleLaunch,
178
182
  } from "@henryqw/pi-subagent";
179
183
  import { registerModelTask } from "@henryqw/pi-task-models";
180
184
 
@@ -223,7 +227,7 @@ export function createRunRole(pi) {
223
227
  prepare: async () => {
224
228
  // prepare runs only after this delegation owns a FIFO permit.
225
229
  const ctx = latestContext();
226
- const launch = resolveRoleLaunch(pi, ctx, {
230
+ const prepared = prepareRoleLaunch(pi, ctx, {
227
231
  role,
228
232
  task: MODEL_TASK,
229
233
  modelClass,
@@ -231,13 +235,7 @@ export function createRunRole(pi) {
231
235
  tools,
232
236
  env,
233
237
  });
234
- if (launch.missingSkills.length && ctx.hasUI) {
235
- ctx.ui.notify(
236
- `Skipped unavailable Skills: ${launch.missingSkills.join(", ")}`,
237
- "warning",
238
- );
239
- }
240
- return { launch, task, cwd: cwd ?? ctx.cwd };
238
+ return { launch: finalizeRoleLaunch(prepared), task, cwd: cwd ?? ctx.cwd };
241
239
  },
242
240
  });
243
241
  }
@@ -3,9 +3,7 @@ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
3
3
  import { createMcpAdapter } from "pi-mcp-adapter";
4
4
  import { loadMcpConfig } from "pi-mcp-adapter/config";
5
5
  import {
6
- fingerprintRoleMcpConfig,
7
6
  parseRoleMcpAllowlist,
8
- ROLE_MCP_CONFIG_SHA256_FLAG,
9
7
  ROLE_MCP_POLICY_FLAG,
10
8
  roleMcpFlagValue,
11
9
  selectRoleMcpConfig,
@@ -16,16 +14,7 @@ export default function roleMcp(pi: ExtensionAPI): void {
16
14
  description: "Internal Pi Subagent Role MCP policy",
17
15
  type: "string",
18
16
  });
19
- pi.registerFlag(ROLE_MCP_CONFIG_SHA256_FLAG, {
20
- description: "Internal Pi Subagent Role MCP config fingerprint",
21
- type: "string",
22
- });
23
17
  const allowlist = parseRoleMcpAllowlist(roleMcpFlagValue(process.argv, `--${ROLE_MCP_POLICY_FLAG}`));
24
18
  const config = selectRoleMcpConfig(loadMcpConfig(join(getAgentDir(), "mcp.json"), process.cwd()), allowlist);
25
- const expectedFingerprint = roleMcpFlagValue(process.argv, `--${ROLE_MCP_CONFIG_SHA256_FLAG}`);
26
- if (expectedFingerprint !== undefined
27
- && (typeof expectedFingerprint !== "string" || fingerprintRoleMcpConfig(config) !== expectedFingerprint)) {
28
- throw new Error("Role MCP config fingerprint drifted before launch.");
29
- }
30
19
  createMcpAdapter({ config })(pi);
31
20
  }
@@ -16,13 +16,13 @@ import {
16
16
  createChildWorktree,
17
17
  createEphemeralSubagentExecutor,
18
18
  DELEGATE_TASK,
19
- createRoleLaunch,
20
19
  DEFAULT_MAX_TURNS,
21
20
  EphemeralSubagentError,
22
21
  finalizeChildWorktree,
22
+ finalizeRoleLaunch,
23
23
  formatDuration,
24
24
  loadRoles,
25
- resolveRoleLaunch,
25
+ prepareRoleLaunch,
26
26
  WorktreeSetupError,
27
27
  worktreeContextNote,
28
28
  type EphemeralSubagentActivityEvent,
@@ -620,28 +620,19 @@ export default function subagentExtension(
620
620
 
621
621
  // Resolve against the latest known session context after each FIFO permit.
622
622
  const launchCtx = () => latestCtx ?? ctx;
623
- const resolveLaunch = (role: Role, delegation: Delegation) => {
623
+ const prepareLaunch = (role: Role, delegation: Delegation) => {
624
624
  const context = launchCtx();
625
- const launch = resolveRoleLaunch(pi, context, {
625
+ const launch = prepareRoleLaunch(pi, context, {
626
626
  role,
627
627
  task: DELEGATE_TASK,
628
628
  ...(delegation.modelClass === undefined ? {} : { modelClass: delegation.modelClass }),
629
629
  });
630
630
  if (delegation.model === undefined) return launch;
631
- return createRoleLaunch(pi, context, {
631
+ return prepareRoleLaunch(pi, context, {
632
632
  role,
633
633
  route: replaceRouteModel(context, delegation.model, launch),
634
634
  });
635
635
  };
636
- const notifyMissingSkills = (role: Role, launch: ReturnType<typeof resolveLaunch>) => {
637
- if (launch.missingSkills.length) {
638
- ctx.ui.notify(
639
- `Subagent role ${role.name} skipped unavailable Pi skills: ${launch.missingSkills.join(", ")}.`,
640
- "warning",
641
- );
642
- }
643
- };
644
-
645
636
  const foregroundWorkflow: ParsedWorkflow = { ...workflow, background: false };
646
637
  const entries = identifyWorkflowEntries(toolCallId, foregroundWorkflow);
647
638
  const states = new Map<string, WorkflowTransportEntry>(entries.map((entry) => [entry.id, {
@@ -706,21 +697,18 @@ export default function subagentExtension(
706
697
  // Route and effective Role resources resolve only after this entry's
707
698
  // shared executor permit, before isolated state is created.
708
699
  const role = reloadRole(entry.delegation.role);
709
- const launch = resolveLaunch(role, entry.delegation);
710
- notifyMissingSkills(role, launch);
711
- model = modelReference(launch.model);
712
- thinkingLevel = launch.thinkingLevel;
713
- if (role.isolation === "worktree") {
700
+ const preparedLaunch = prepareLaunch(role, entry.delegation);
701
+ model = modelReference(preparedLaunch.model);
702
+ thinkingLevel = preparedLaunch.thinkingLevel;
703
+ if (preparedLaunch.isolation === "worktree") {
714
704
  worktree = await createChildWorktree(ctx.cwd, entry.id, undefined, workflowSignal);
715
705
  }
716
- startWidgetItem(entry.id, entry.id, role.name, launch.model.id, launch.thinkingLevel, entry.delegation.name, ctx);
706
+ startWidgetItem(entry.id, entry.id, preparedLaunch.role, preparedLaunch.model.id, preparedLaunch.thinkingLevel, entry.delegation.name, ctx);
717
707
  setState("running", "");
718
708
  emitUpdate(emitToolUpdates);
719
- return {
720
- launch,
721
- task: worktree ? `${entry.delegation.task}${worktreeContextNote(worktree)}` : entry.delegation.task,
722
- cwd: worktree?.cwd ?? ctx.cwd,
723
- };
709
+ const task = worktree ? `${entry.delegation.task}${worktreeContextNote(worktree)}` : entry.delegation.task;
710
+ const cwd = worktree?.cwd ?? ctx.cwd;
711
+ return { launch: finalizeRoleLaunch(preparedLaunch), task, cwd };
724
712
  },
725
713
  });
726
714
  if (child.outcome === "failure") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "16.1.0",
3
+ "version": "17.0.0",
4
4
  "description": "Delegate bounded single, parallel, or chained tasks to isolated Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",