@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 +1 -1
- package/README.md +6 -1
- package/dist/ephemeral.d.ts +1 -0
- package/dist/ephemeral.js +14 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +153 -10
- package/dist/mcp-role.d.ts +0 -1
- package/dist/mcp-role.js +0 -4
- package/docs/adr/001-composable-ephemeral-execution.md +4 -1
- package/docs/orchestration.md +9 -11
- package/extensions/role-mcp.ts +0 -11
- package/extensions/subagent.ts +13 -25
- package/package.json +1 -1
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
|
|
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
|
|
package/dist/ephemeral.d.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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 {
|
|
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 =
|
|
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:
|
|
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 =
|
|
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(
|
|
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
|
+
}
|
package/dist/mcp-role.d.ts
CHANGED
|
@@ -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
|
-
-
|
|
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.
|
package/docs/orchestration.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/extensions/role-mcp.ts
CHANGED
|
@@ -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
|
}
|
package/extensions/subagent.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
623
|
+
const prepareLaunch = (role: Role, delegation: Delegation) => {
|
|
624
624
|
const context = launchCtx();
|
|
625
|
-
const launch =
|
|
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
|
|
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
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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") {
|