@henryqw/pi-subagent 24.0.1 → 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,18 +13,11 @@ 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
 
@@ -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
  }
@@ -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
 
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "24.0.1",
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
  }