@henryqw/pi-subagent 24.0.0 → 25.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
@@ -62,7 +62,7 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
62
62
  ### Resource and launch policy
63
63
 
64
64
  - Ambient child extensions and Skills stay disabled. Only Role/caller resources plus required internal adapters load.
65
- - Every Role requires `tools`, `extensions`, and `skills` arrays. Omitted or empty `mcps` denies MCP access. Direct `pi-mcp-adapter` loading is rejected because it bypasses the allowlist.
65
+ - Every Role requires `tools`, `extensions`, and `skills` arrays. `extensions` may name Pi built-in extensions as `builtin:<name>`. `mcps` names servers from the global `~/.pi/agent/mcp.json`; their tools load in the child with direct exposure. Omitted or empty `mcps` denies MCP access. Direct `pi-mcp-adapter` loading is rejected because it bypasses the allowlist.
66
66
  - Selected extensions are trusted executable bundles, not a sandbox. All tools and lifecycle behavior they register load together.
67
67
  - Role Skill names resolve through Main's effective Pi registry. Missing Roles, Skills, tools, MCP servers, routes, models, or thinking levels fail before the first productive turn.
68
68
  - Route precedence is call model class, then Role default, then registered Model Task assignment/default. A direct model replaces only the route model and must support its thinking level.
package/README.md CHANGED
@@ -13,24 +13,17 @@ pi install npm:@henryqw/pi-subagent
13
13
 
14
14
  Run `/task-models` and configure the `fast`, `balanced`, `frontier`, and `fav` routes you use.
15
15
 
16
- Install `pi-mcp-adapter` only when a Role declares an MCP server allowlist:
17
-
18
- ```bash
19
- pi install npm:pi-mcp-adapter
20
- ```
21
-
22
16
  ## Works with
23
17
 
24
18
  | Package | Relationship | Purpose |
25
19
  | --- | --- | --- |
26
20
  | [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) | Required | Provides the configured model routes for delegated Roles. |
27
- | [`pi-mcp-adapter`](https://github.com/nicobailon/pi-mcp-adapter) | Required | Loads the MCP servers named in a Role's `mcps` allowlist, only for Roles that declare one. |
28
21
 
29
22
  ## Use
30
23
 
31
24
  Ask Main to delegate a bounded task, such as “Have a scout trace sign-in without editing files.” Main uses `delegate_task`; you receive progress and a result or an actionable failure while Main remains available.
32
25
 
33
- Commands are for you; tools and the packaged skill are for Main. You do not need to call agent tools or manage candidate identities yourself.
26
+ Commands are for you; tools and the packaged skill are for Main. You do not need to call agent tools or manage candidate identities yourself. Delegation and continuation tools use plain-object parameter schemas so providers that omit root-union tools can expose them; each mode and action still requires its exact arguments.
34
27
 
35
28
  | Surface | Type | Purpose |
36
29
  | --- | --- | --- |
@@ -181,14 +174,14 @@ Role Markdown lives in `~/.pi/agent/config/pi-subagent/` and requires frontmatte
181
174
  | `name`, `description` | Required non-empty text without terminal control characters |
182
175
  | `modelClass` | Optional `fast`, `balanced`, `frontier`, or `fav` default |
183
176
  | `tools` | Required array of base tool names; `[]` selects none |
184
- | `extensions` | Required array of trusted absolute paths or supported package sources |
177
+ | `extensions` | Required array of trusted absolute paths, supported package sources, or Pi built-in extensions such as `builtin:codemode` |
185
178
  | `skills` | Required array of effective Pi Skill names |
186
- | `mcps` | Optional exact MCP server names; omitted or `[]` denies MCP access |
179
+ | `mcps` | Optional exact server names from `~/.pi/agent/mcp.json` in Pi's native `mcpServers` format; omitted or `[]` denies MCP access |
187
180
  | body | Required system instructions |
188
181
 
189
182
  Roles describe responsibility and capabilities. They do not choose isolation; each request does. A same-named user Role overrides a built-in Role. The package ships `implementer`, `reviewer`, and `scout`.
190
183
 
191
- Children disable ambient extension and Skill discovery. Only declared resources and required internal policy adapters load. Missing Skills, tools, MCP servers, Roles, or routes fail before productive work starts. Main-only delegation and recovery tools plus `ask_question` are excluded from children.
184
+ Children run with `--no-extensions`, so a Role must list the extension that registers its route model's provider, and only declared resources plus required internal policy adapters load. `builtin:<name>` entries load Pi built-in extensions (`builtin:codemode`, `builtin:tool-search`, `builtin:llama.cpp`) explicitly; `builtin:mcp` is rejected because it would connect every configured server and bypass `mcps`. The servers named in `mcps` come from the global `~/.pi/agent/mcp.json` only; their tools load in the child with direct exposure and are declared to the model as `mcp__<server>__<tool>` once the server connects. Missing Skills, tools, MCP servers, Roles, or routes fail before productive work starts. Main-only delegation and recovery tools plus `ask_question` are excluded from children.
192
185
 
193
186
  ## API
194
187
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
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 { parseRoleMcpAllowlist, selectRoleMcpConfig, type RoleMcpConfig } from "./mcp-role.ts";
4
+ export { loadRoleMcpConfig, parseRoleMcpAllowlist } 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, 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";
package/dist/index.js CHANGED
@@ -4,10 +4,10 @@ import { fileURLToPath } from "node:url";
4
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 { parseRoleMcpAllowlist, selectRoleMcpConfig } from "./mcp-role.js";
7
+ import { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
8
8
  import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, PROFILE_NAMES, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
9
9
  export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
10
- export { parseRoleMcpAllowlist, selectRoleMcpConfig } from "./mcp-role.js";
10
+ export { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
11
11
  export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
12
12
  export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, } from "./worktree.js";
13
13
  export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, } from "./review-evidence.js";
@@ -66,12 +66,25 @@ const stringList = (value, field, source) => {
66
66
  }
67
67
  return value.map((item) => item.trim());
68
68
  };
69
+ // Pi 0.99 built-in extensions (`builtin:<name>`, see Pi's settings docs); Pi exports no list of them.
70
+ // `builtin:mcp` is excluded: it connects every server in mcp.json and would bypass the Role `mcps` allowlist.
71
+ const BUILTIN_EXTENSION_NAMES = ["codemode", "tool-search", "llama.cpp"];
72
+ const BUILTIN_EXTENSION_PREFIX = "builtin:";
69
73
  function validateExtension(extension, source) {
70
74
  const value = cleanText(extension, "extension", source);
75
+ if (value.startsWith(BUILTIN_EXTENSION_PREFIX)) {
76
+ if (value === `${BUILTIN_EXTENSION_PREFIX}mcp`) {
77
+ throw new Error(`${source} must select MCP servers with mcps instead of loading builtin:mcp directly.`);
78
+ }
79
+ if (!BUILTIN_EXTENSION_NAMES.includes(value.slice(BUILTIN_EXTENSION_PREFIX.length))) {
80
+ throw new Error(`${source}: unknown built-in extension ${value}; use ${BUILTIN_EXTENSION_NAMES.map((name) => `${BUILTIN_EXTENSION_PREFIX}${name}`).join(", ")}.`);
81
+ }
82
+ return value;
83
+ }
71
84
  const packageSource = /^(?:npm|git|github|https?|ssh):/.test(value);
72
85
  const userPath = isAbsolute(value) || value.startsWith("~/") || value.startsWith("~\\") || value.startsWith("file://");
73
86
  if (!packageSource && !userPath) {
74
- throw new Error(`${source}: extensions entries must be absolute paths or package sources.`);
87
+ throw new Error(`${source}: extensions entries must be absolute paths, package sources, or builtin:<name>.`);
75
88
  }
76
89
  return value;
77
90
  }
@@ -231,7 +244,9 @@ export async function resolveRolePackageResources(role, ctx) {
231
244
  const resolved = await packageManager(ctx).resolveExtensionSources([...sources]);
232
245
  const resourceGroups = [resolved.extensions, resolved.skills, resolved.prompts, resolved.themes]
233
246
  .map((resources) => resources.filter((resource) => resource.enabled));
234
- const resolvedSources = new Set(resourceGroups.flat().map((resource) => resource.metadata.source));
247
+ // Built-in extensions resolve to their `builtin:<name>` path with the bare source `builtin`.
248
+ const resolvedSources = new Set(resourceGroups.flat()
249
+ .map((resource) => resource.metadata.source === "builtin" ? resource.path : resource.metadata.source));
235
250
  const missing = sources.filter((source) => !resolvedSources.has(source));
236
251
  if (missing.length)
237
252
  throw new Error(`Role extension sources resolved no resources: ${missing.join(", ")}.`);
@@ -317,6 +332,9 @@ export function prepareRoleLaunch(pi, ctx, input) {
317
332
  ? prepareRoleLaunchFromSkills(ctx, input, resolveRoleSkills(pi, input.role))
318
333
  : prepareResolvedRoleLaunch(pi, ctx, input);
319
334
  assertNoMissingRoleSkills(input.role, prepared);
335
+ // Validate the mcp.json the child reads: its environment inherits Main's, with launch overrides on top.
336
+ if (input.role.mcps?.length)
337
+ loadRoleMcpConfig(prepared.env.PI_CODING_AGENT_DIR ?? getAgentDir(), input.role.mcps);
320
338
  return { ...prepared, role };
321
339
  }
322
340
  /** Resolve and prepare a configured Role with its package-owned resources. */
@@ -328,10 +346,8 @@ export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
328
346
  if (matches.length !== 1)
329
347
  throw new Error(`Required configured Role ${roleName} is missing or ambiguous.`);
330
348
  const role = matches[0];
331
- if (role.mcps?.length) {
332
- const { loadMcpConfig } = await import("pi-mcp-adapter/config");
333
- selectRoleMcpConfig(loadMcpConfig(join(getAgentDir(), "mcp.json"), ctx.cwd), role.mcps);
334
- }
349
+ if (role.mcps?.length)
350
+ loadRoleMcpConfig(getAgentDir(), role.mcps);
335
351
  const resources = await resolveRolePackageResources(role, ctx);
336
352
  const effectiveRole = { ...role, extensions: resources.extensions };
337
353
  const namedSkills = resolveRoleSkills(pi, effectiveRole);
@@ -1,6 +1,8 @@
1
- export interface RoleMcpConfig {
2
- mcpServers: Record<string, unknown>;
3
- settings?: Record<string, unknown>;
4
- }
1
+ import type { LoadedMcpConfig } from "@earendil-works/pi-coding-agent";
5
2
  export declare function parseRoleMcpAllowlist(value: unknown): string[];
6
- export declare function selectRoleMcpConfig(config: RoleMcpConfig, allowlist: readonly string[]): RoleMcpConfig;
3
+ /**
4
+ * Select the Role's allowlisted servers from the global `mcp.json` in Pi's native `mcpServers`
5
+ * shape; the project `.pi/mcp.json` is ignored. A `--no-extensions` child has no codemode tool, so
6
+ * every selected server is forced to direct exposure and codemode is never activated.
7
+ */
8
+ export declare function loadRoleMcpConfig(agentDir: string, allowlist: readonly string[]): LoadedMcpConfig;
package/dist/mcp-role.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  export function parseRoleMcpAllowlist(value) {
2
4
  if (typeof value !== "string")
3
5
  throw new Error("The Role MCP policy flag must contain a JSON array of MCP server names.");
@@ -17,14 +19,60 @@ export function parseRoleMcpAllowlist(value) {
17
19
  throw new Error("The Role MCP policy flag contains duplicate MCP server names.");
18
20
  return names;
19
21
  }
20
- export function selectRoleMcpConfig(config, allowlist) {
21
- const missing = allowlist.filter((name) => !Object.hasOwn(config.mcpServers, name));
22
+ function isRecord(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+ const isStringRecord = (value) => isRecord(value) && Object.values(value).every((item) => typeof item === "string");
26
+ /** Documented optional fields of an `mcpServers` entry and their shapes (see Pi's docs/mcp.md). */
27
+ const FIELD_CHECKS = {
28
+ args: [(value) => Array.isArray(value) && value.every((item) => typeof item === "string"), "an array of strings"],
29
+ env: [isStringRecord, "an object of strings"],
30
+ headers: [isStringRecord, "an object of strings"],
31
+ cwd: [(value) => typeof value === "string", "a string"],
32
+ timeout: [(value) => typeof value === "number" && Number.isFinite(value) && value > 0, "a positive number"],
33
+ enabled: [(value) => typeof value === "boolean", "a boolean"],
34
+ oauth: [isRecord, "an object"],
35
+ };
36
+ /**
37
+ * Select the Role's allowlisted servers from the global `mcp.json` in Pi's native `mcpServers`
38
+ * shape; the project `.pi/mcp.json` is ignored. A `--no-extensions` child has no codemode tool, so
39
+ * every selected server is forced to direct exposure and codemode is never activated.
40
+ */
41
+ export function loadRoleMcpConfig(agentDir, allowlist) {
42
+ const path = join(agentDir, "mcp.json");
43
+ let parsed = {};
44
+ try {
45
+ parsed = JSON.parse(readFileSync(path, "utf8"));
46
+ }
47
+ catch (error) {
48
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
49
+ throw new Error(`${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
50
+ }
51
+ }
52
+ if (!isRecord(parsed) || (parsed.mcpServers !== undefined && !isRecord(parsed.mcpServers))) {
53
+ throw new Error(`${path}: expected an object with an "mcpServers" object.`);
54
+ }
55
+ const configured = parsed.mcpServers ?? {};
56
+ const missing = allowlist.filter((name) => !Object.hasOwn(configured, name));
22
57
  if (missing.length)
23
58
  throw new Error(`Role MCP servers are not configured: ${missing.join(", ")}.`);
24
- const mcpServers = Object.fromEntries(allowlist.map((name) => [name, config.mcpServers[name]]));
25
- const { agentPluginPaths: _agentPluginPaths, hostConfigDiscovery: _hostConfigDiscovery, ...settings } = config.settings ?? {};
26
- return {
27
- mcpServers,
28
- ...(Object.keys(settings).length ? { settings } : {}),
29
- };
59
+ const servers = allowlist.map((name) => {
60
+ const value = configured[name];
61
+ if (!isRecord(value))
62
+ throw new Error(`${path}: MCP server "${name}" must be an object.`);
63
+ if (value.enabled === false)
64
+ throw new Error(`${path}: Role MCP server "${name}" is disabled.`);
65
+ const stdio = typeof value.command === "string" && value.command.trim() !== "" && (value.type === undefined || value.type === "stdio");
66
+ const http = typeof value.url === "string" && value.url.trim() !== "" && (value.type === undefined || value.type === "http" || value.type === "streamable-http");
67
+ if (stdio === http) {
68
+ throw new Error(`${path}: MCP server "${name}" needs either a "command" (stdio) or a "url" (http or streamable-http).`);
69
+ }
70
+ for (const [field, [valid, shape]] of Object.entries(FIELD_CHECKS)) {
71
+ if (value[field] !== undefined && !valid(value[field]))
72
+ throw new Error(`${path}: MCP server "${name}" field "${field}" must be ${shape}.`);
73
+ }
74
+ const { toolExposure: _toolExposure, ...config } = value;
75
+ return { name, config: { ...config, exposure: "direct" }, source: path, scope: "global" };
76
+ });
77
+ return { servers, autoEnableCodemode: false, errors: [] };
30
78
  }
package/dist/schema.d.ts CHANGED
@@ -129,6 +129,11 @@ export declare const ResumeRequestSchema: Type.TUnion<[Type.TObject<{
129
129
  id: Type.TString;
130
130
  action: Type.TLiteral<"finalize">;
131
131
  }>]>;
132
+ export declare const ResumeRequestParameters: Type.TObject<{
133
+ id: Type.TString;
134
+ action: Type.TUnion<[Type.TLiteral<"retry">, Type.TLiteral<"verify">, Type.TLiteral<"finalize">]>;
135
+ taskId: Type.TOptional<Type.TString>;
136
+ }>;
132
137
  export type IdOnly = Static<typeof IdOnlySchema>;
133
138
  export type CheckCommand = Static<typeof CheckCommandSchema>;
134
139
  export type Judgment = Static<typeof JudgmentSchema>;
@@ -369,6 +374,31 @@ export declare const IntegrationActionSchema: Type.TUnion<[Type.TObject<{
369
374
  taskId: Type.TOptional<Type.TString>;
370
375
  attempt: Type.TOptional<Type.TInteger>;
371
376
  }>]>;
377
+ export declare const IntegrationActionParameters: Type.TObject<{
378
+ id: Type.TString;
379
+ generation: Type.TInteger;
380
+ action: Type.TUnion<[Type.TLiteral<"refresh">, Type.TUnion<[Type.TLiteral<"advance">, Type.TLiteral<"validate">, Type.TLiteral<"correct">, Type.TLiteral<"promote">, Type.TLiteral<"reconcile">, Type.TLiteral<"cleanup">]>, Type.TLiteral<"release">]>;
381
+ expectedTip: Type.TObject<{
382
+ branch: Type.TString;
383
+ head: Type.TString;
384
+ index: Type.TString;
385
+ tree: Type.TString;
386
+ }>;
387
+ expectedMain: Type.TOptional<Type.TObject<{
388
+ branch: Type.TString;
389
+ head: Type.TString;
390
+ index: Type.TString;
391
+ tree: Type.TString;
392
+ }>>;
393
+ newMain: Type.TOptional<Type.TObject<{
394
+ branch: Type.TString;
395
+ head: Type.TString;
396
+ index: Type.TString;
397
+ tree: Type.TString;
398
+ }>>;
399
+ taskId: Type.TOptional<Type.TString>;
400
+ attempt: Type.TOptional<Type.TInteger>;
401
+ }>;
372
402
  export type IntegrationAction = Static<typeof IntegrationActionSchema>;
373
403
  export declare function parseIntegrationAction(value: unknown): IntegrationAction;
374
404
  export declare function parseStageRequest(value: unknown): StageRequest;
package/dist/schema.js CHANGED
@@ -65,7 +65,17 @@ export const ResumeRequestSchema = Type.Union([
65
65
  Type.Object({ id: IdSchema, action: Type.Literal("retry"), taskId: IdSchema }, { additionalProperties: false }),
66
66
  Type.Object({ id: IdSchema, action: Type.Literal("verify"), taskId: IdSchema }, { additionalProperties: false }),
67
67
  Type.Object({ id: IdSchema, action: Type.Literal("finalize") }, { additionalProperties: false }),
68
- ], { type: "object" });
68
+ ]);
69
+ // Tool parameters need a plain object root; the action union remains the validation boundary.
70
+ export const ResumeRequestParameters = Type.Object({
71
+ id: IdSchema,
72
+ action: Type.Union([
73
+ ResumeRequestSchema.anyOf[0].properties.action,
74
+ ResumeRequestSchema.anyOf[1].properties.action,
75
+ ResumeRequestSchema.anyOf[2].properties.action,
76
+ ]),
77
+ taskId: Type.Optional(IdSchema),
78
+ }, { additionalProperties: false });
69
79
  const AllocationLifecycleFields = {
70
80
  generation: Type.Integer({ minimum: 1, maximum: 2 }),
71
81
  token: Type.String({ pattern: TOKEN_PATTERN }),
@@ -167,7 +177,21 @@ export const IntegrationActionSchema = Type.Union([
167
177
  taskId: Type.Optional(IdSchema),
168
178
  attempt: Type.Optional(Type.Integer({ minimum: 1, maximum: 2 })),
169
179
  }, { additionalProperties: false }),
170
- ], { type: "object" });
180
+ ]);
181
+ export const IntegrationActionParameters = Type.Object({
182
+ id: IntegrationActionSchema.anyOf[0].properties.id,
183
+ generation: IntegrationActionSchema.anyOf[0].properties.generation,
184
+ action: Type.Union([
185
+ IntegrationActionSchema.anyOf[0].properties.action,
186
+ IntegrationActionSchema.anyOf[1].properties.action,
187
+ IntegrationActionSchema.anyOf[2].properties.action,
188
+ ]),
189
+ expectedTip: IntegrationActionSchema.anyOf[0].properties.expectedTip,
190
+ expectedMain: Type.Optional(IntegrationActionSchema.anyOf[0].properties.expectedMain),
191
+ newMain: Type.Optional(IntegrationActionSchema.anyOf[0].properties.newMain),
192
+ taskId: IntegrationActionSchema.anyOf[2].properties.taskId,
193
+ attempt: IntegrationActionSchema.anyOf[2].properties.attempt,
194
+ }, { additionalProperties: false });
171
195
  export function parseIntegrationAction(value) {
172
196
  if (!Check(IntegrationActionSchema, value))
173
197
  throw schemaValidationError("subagent_integrate requires an exact generation and tip", Errors(IntegrationActionSchema, value));
@@ -139,7 +139,7 @@ Every child launch:
139
139
  5. excludes Main-only delegation/recovery tools and `ask_question`; and
140
140
  6. prepends the delegated-subagent identity contract.
141
141
 
142
- An empty `tools` list adds no base tools, though selected extension tools can still activate. Omitted or empty `mcps` denies MCP access. Loading `pi-mcp-adapter` directly is rejected because it bypasses the allowlist.
142
+ An empty `tools` list adds no base tools, though selected extension tools can still activate. Children run with `--no-extensions`, so a Role must list the extension that registers its route model's provider; `builtin:<name>` entries load Pi built-in extensions. `mcps` names servers from the global `~/.pi/agent/mcp.json` in Pi's native format, and their tools load in the child with direct exposure. Omitted or empty `mcps` denies MCP access. Loading `pi-mcp-adapter` directly is rejected because it bypasses the allowlist.
143
143
 
144
144
  Isolated Herdr Role launches receive the same turn, token, and child-runtime budget metadata as ephemeral launches. Retained workers therefore keep one non-refilling session budget across follow-ups rather than receiving a new request budget.
145
145
 
@@ -17,9 +17,9 @@ import { RoleLaunchRuntime, type LaunchRuntimeOptions } from "../dist/launch-run
17
17
  import { IsolatedRunner, type OperationContext, type RunResponse } from "../dist/runner.js";
18
18
  import {
19
19
  IdOnlySchema,
20
- ResumeRequestSchema,
20
+ ResumeRequestParameters,
21
21
  StageRequestSchema,
22
- IntegrationActionSchema,
22
+ IntegrationActionParameters,
23
23
  parseIntegrationAction,
24
24
  parseStageRequest,
25
25
  parseExecuteRequest,
@@ -614,7 +614,7 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
614
614
  name: "subagent_resume",
615
615
  label: "Subagent resume",
616
616
  description: "Resume one unfinished isolated request without resetting its recorded policy or correction count.",
617
- parameters: ResumeRequestSchema,
617
+ parameters: ResumeRequestParameters,
618
618
  prepareArguments: parseResumeRequest,
619
619
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
620
620
  latestCtx = ctx;
@@ -641,7 +641,7 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
641
641
  name: "subagent_integrate",
642
642
  label: "Subagent integrate",
643
643
  description: "Main advances staged dependents, refreshes after clean Main drift, validates, corrects, promotes, reconciles an interrupted promotion, cleans up promoted resources, or explicitly releases rejected/superseded owned resources. Never replays an uncertain mutation.",
644
- parameters: IntegrationActionSchema,
644
+ parameters: IntegrationActionParameters,
645
645
  prepareArguments: parseIntegrationAction,
646
646
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
647
647
  latestCtx = ctx;
@@ -1,16 +1,12 @@
1
- import { join } from "node:path";
2
- import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { createMcpAdapter } from "pi-mcp-adapter";
4
- import { loadMcpConfig } from "pi-mcp-adapter/config";
5
- import { roleMcpAllowlistFromArgv, ROLE_MCP_POLICY_FLAG, selectRoleMcpConfig } from "@henryqw/pi-subagent";
1
+ import { createMcpExtension, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadRoleMcpConfig, roleMcpAllowlistFromArgv, ROLE_MCP_POLICY_FLAG } from "@henryqw/pi-subagent";
6
3
 
7
- export default function roleMcp(pi: ExtensionAPI): void {
4
+ export default function roleMcp(pi: ExtensionAPI): void | Promise<void> {
8
5
  pi.registerFlag(ROLE_MCP_POLICY_FLAG, {
9
6
  description: "Internal Pi Subagent Role MCP policy",
10
7
  type: "string",
11
8
  });
12
- // Pi binds extension flag values after factories load.
13
- const allowlist = roleMcpAllowlistFromArgv(process.argv);
14
- const config = selectRoleMcpConfig(loadMcpConfig(join(getAgentDir(), "mcp.json"), process.cwd()), allowlist);
15
- createMcpAdapter({ config })(pi);
9
+ // Pi binds extension flag values after factories load. Throwing here exits the child non-zero.
10
+ const config = loadRoleMcpConfig(getAgentDir(), roleMcpAllowlistFromArgv(process.argv));
11
+ return createMcpExtension({ loadConfig: () => config })(pi);
16
12
  }
@@ -9,7 +9,7 @@ import { registerIsolatedExtension } from "./isolated.ts";
9
9
  import { registerSubagentCommand, type DirectTask } from "./subagent-command.ts";
10
10
  import { MODEL_CLASS_GUIDANCE } from "./model-class-policy.ts";
11
11
  import { ENTRY_STATUS_PRESENTATION, formatWorkflowResult, type BackgroundWorkflowTransportDetails, type WorkflowTransportEntry } from "./result-transport.ts";
12
- import { DelegateTaskSchema, identifyWorkflowEntries, parseDelegateTask, runForegroundWorkflow, type Delegation, type ParsedWorkflow, type WorkflowEntry } from "./workflow.ts";
12
+ import { DelegateTaskParameters, identifyWorkflowEntries, parseDelegateTask, runForegroundWorkflow, type Delegation, type ParsedWorkflow, type WorkflowEntry } from "./workflow.ts";
13
13
  import { createDirectHerdr, type DirectHandle, type DirectTab } from "../dist/direct-herdr.js";
14
14
  import { materializeTransientLaunch } from "../dist/launch-runtime.js";
15
15
  const WIDGET_KEY = "subagent-status";
@@ -460,7 +460,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
460
460
  "Isolation uses typed tasks and dependencies. Keep tightly coupled changes with one owner; do not split by file count. Failures, ambiguity, limits, and conflicts retain work and never waive checks or identity guards.",
461
461
  `For delegate_task, ${MODEL_CLASS_GUIDANCE} A direct model replaces only the selected route's model; its thinking level stays unchanged.`,
462
462
  ],
463
- parameters: DelegateTaskSchema,
463
+ parameters: DelegateTaskParameters,
464
464
  prepareArguments(args) {
465
465
  try {
466
466
  const parsed = parseDelegateTask(args);
@@ -42,7 +42,21 @@ export const DirectWorkflowSchema = Type.Object({
42
42
  chain: Type.Optional(Type.Array(DelegationSchema, { minItems: 1, maxItems: MAX_WORKFLOW_ENTRIES })),
43
43
  }, { additionalProperties: false, description: "Direct mode: exactly one compact single, tasks, or chain workflow" });
44
44
 
45
- export const DelegateTaskSchema = Type.Union([DirectWorkflowSchema, ExecuteRequestSchema], { type: "object" });
45
+ export const DelegateTaskSchema = Type.Union([DirectWorkflowSchema, ExecuteRequestSchema]);
46
+ // Keep the strict mode union for parsing; expose a plain object root to tool providers.
47
+ export const DelegateTaskParameters = Type.Object({
48
+ ...DirectWorkflowSchema.properties,
49
+ mode: Type.Union([DirectWorkflowSchema.properties.mode, ExecuteRequestSchema.properties.mode]),
50
+ id: Type.Optional(ExecuteRequestSchema.properties.id),
51
+ goal: Type.Optional(ExecuteRequestSchema.properties.goal),
52
+ tasks: Type.Optional(Type.Union([
53
+ Type.Array(DelegationSchema, { minItems: 1, maxItems: MAX_WORKFLOW_ENTRIES }),
54
+ ExecuteRequestSchema.properties.tasks,
55
+ ])),
56
+ finalChecks: ExecuteRequestSchema.properties.finalChecks,
57
+ finalJudgment: ExecuteRequestSchema.properties.finalJudgment,
58
+ }, { additionalProperties: false });
59
+ export const WorkflowSchema = DirectWorkflowSchema;
46
60
 
47
61
  export type Delegation = Static<typeof DelegationSchema> & { kind: "text" };
48
62
  export type WorkflowMode = "single" | "parallel" | "chain";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "24.0.0",
3
+ "version": "25.0.0",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -40,9 +40,9 @@
40
40
  "pack:check": "npm pack --dry-run"
41
41
  },
42
42
  "peerDependencies": {
43
- "@earendil-works/pi-ai": ">=0.86.0 <0.88.0",
44
- "@earendil-works/pi-coding-agent": ">=0.86.0 <0.88.0",
45
- "@earendil-works/pi-tui": ">=0.86.0 <0.88.0",
43
+ "@earendil-works/pi-ai": ">=0.99.1 <0.100.0",
44
+ "@earendil-works/pi-coding-agent": ">=0.99.1 <0.100.0",
45
+ "@earendil-works/pi-tui": ">=0.99.1 <0.100.0",
46
46
  "typebox": "^1.3.15"
47
47
  },
48
48
  "repository": {
@@ -74,9 +74,9 @@
74
74
  "proper-lockfile": "^4.1.2"
75
75
  },
76
76
  "devDependencies": {
77
- "@earendil-works/pi-ai": "0.87.0",
78
- "@earendil-works/pi-coding-agent": "0.87.0",
79
- "@earendil-works/pi-tui": "0.87.0",
77
+ "@earendil-works/pi-ai": "0.99.1",
78
+ "@earendil-works/pi-coding-agent": "0.99.1",
79
+ "@earendil-works/pi-tui": "0.99.1",
80
80
  "@types/proper-lockfile": "^4.1.4"
81
81
  }
82
82
  }