@henryqw/pi-subagent 23.0.2 → 24.0.1

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
@@ -76,7 +76,7 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
76
76
  - Recognized Pi JSON events renew idle timeout; raw bytes do not. Child maximum runtime always terminates ephemeral children.
77
77
  - Output, stderr, protocol events, callback draining, and inherited descendant streams are bounded.
78
78
  - Low-level worktree finalization returns explicit `pruned`, `retained`, or `recovery` evidence and never force-deletes uncertain work.
79
- - Exact review and working-change helpers produce bounded private evidence; callers must preserve identity guards and verdict validation.
79
+ - Exact review helpers produce bounded private evidence; callers must preserve identity guards and verdict validation.
80
80
 
81
81
  ## Owned storage
82
82
 
package/README.md CHANGED
@@ -30,7 +30,7 @@ pi install npm:pi-mcp-adapter
30
30
 
31
31
  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
32
 
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.
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. 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
34
 
35
35
  | Surface | Type | Purpose |
36
36
  | --- | --- | --- |
@@ -199,7 +199,7 @@ Children disable ambient extension and Skill discovery. Only declared resources
199
199
  | `prepareRoleLaunch`, `finalizeRoleLaunch` | functions | Prepare Role prompts, arguments, and tool policy. |
200
200
  | `createEphemeralSubagentExecutor` | function | Run bounded no-session Pi children through a FIFO pool. |
201
201
  | `createChildWorktree`, `finalizeChildWorktree` | functions | Create and conservatively finalize child worktrees. |
202
- | `prepareExactReviewEvidence`, `prepareWorkingChangeEvidence` | functions | Prepare private exact review or working-change evidence. |
202
+ | `prepareExactReviewEvidence` | function | Prepare private exact base-to-tip review evidence. |
203
203
  | `ExecuteRequestSchema`, `StageRequestSchema`, `IntegrationActionSchema` | schemas | Validate checked request and integration inputs. |
204
204
  | `IsolatedRunner`, `FileRunStore` | classes | Run checked work and persist durable request state. |
205
205
  | `RoleLaunchRuntime`, `HerdrHostRuntime`, `CheckedGitRuntime` | classes | Supply launch, host, and Git behavior to the runner. |
package/dist/index.d.ts CHANGED
@@ -1,17 +1,16 @@
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, roleMcpFlagValue, selectRoleMcpConfig, type RoleMcpConfig } from "./mcp-role.ts";
4
+ export { parseRoleMcpAllowlist, 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
- export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
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";
8
- export { captureWorkingCheckoutBaseline, captureWorkingCheckoutState, prepareWorkingChangeEvidence, sameWorkingCheckoutState, sameWorkingSnapshot, type PreparedWorkingChangeEvidence, type WorkingCheckoutBaseline, type WorkingCheckoutState, type WorkingSnapshotIdentity, } from "./working-evidence.ts";
9
8
  export declare const PI_SUBAGENT_PROCESS_LEASE = "PI_SUBAGENT_PROCESS_LEASE";
10
9
  export declare const ROLE_MCP_POLICY_FLAG = "pi-subagent-role-mcps";
10
+ /** Read child MCP policy before Pi binds registered extension flag values. */
11
+ export declare function roleMcpAllowlistFromArgv(args: readonly string[]): string[];
11
12
  export declare const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
12
- export declare const EXECUTION_BUDGET_FLAG = "pi-subagent-execution-budget";
13
13
  export declare const CHILD_EXCLUDED_TOOL_NAMES: readonly ["delegate_task", "ask_question", "subagent_status", "subagent_resume", "subagent_stage", "subagent_abort"];
14
- export declare const CHILD_EXCLUDED_TOOLS: string;
15
14
  export declare const DELEGATE_TASK: {
16
15
  readonly id: "pi-subagent/delegateTask";
17
16
  readonly label: "Subagent delegation";
@@ -91,7 +90,7 @@ export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">,
91
90
  export declare function prepareRoleLaunch(pi: Pick<ExtensionAPI, "getCommands">, ctx: ExtensionContext, input: CreateRoleLaunchInput): PreparedRoleLaunch;
92
91
  /** Resolve and prepare a configured Role with its package-owned resources. */
93
92
  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. */
93
+ /** Insert the system prompt pair after a caller has prepared its launch argv. */
95
94
  export declare function finalizeRoleLaunch(prepared: PreparedRoleLaunch): ResolvedRoleLaunch;
96
95
  export * from "./composition.ts";
97
96
  export * from "./git-runtime.ts";
package/dist/index.js CHANGED
@@ -4,22 +4,28 @@ 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 { selectRoleMcpConfig } from "./mcp-role.js";
7
+ import { parseRoleMcpAllowlist, selectRoleMcpConfig } 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, roleMcpFlagValue, selectRoleMcpConfig } from "./mcp-role.js";
10
+ export { parseRoleMcpAllowlist, selectRoleMcpConfig } from "./mcp-role.js";
11
11
  export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
12
- export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, } from "./worktree.js";
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";
14
- export { captureWorkingCheckoutBaseline, captureWorkingCheckoutState, prepareWorkingChangeEvidence, sameWorkingCheckoutState, sameWorkingSnapshot, } from "./working-evidence.js";
15
14
  const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
16
15
  const MULTI_CODEX_EXTENSION = fileURLToPath(import.meta.resolve("@henryqw/pi-multi-codex/extensions/multi-codex.ts"));
17
16
  const ROLE_MCP_EXTENSION = fileURLToPath(new URL("../extensions/role-mcp.ts", import.meta.url));
18
17
  const ROLE_TOOLS_EXTENSION = fileURLToPath(new URL("../extensions/role-tools.ts", import.meta.url));
19
18
  export const PI_SUBAGENT_PROCESS_LEASE = "PI_SUBAGENT_PROCESS_LEASE";
20
19
  export const ROLE_MCP_POLICY_FLAG = "pi-subagent-role-mcps";
20
+ /** Read child MCP policy before Pi binds registered extension flag values. */
21
+ export function roleMcpAllowlistFromArgv(args) {
22
+ const flag = `--${ROLE_MCP_POLICY_FLAG}`;
23
+ const indexes = args.flatMap((arg, index) => arg === flag ? [index] : []);
24
+ if (indexes.length !== 1)
25
+ throw new Error(`${flag} must appear exactly once.`);
26
+ return parseRoleMcpAllowlist(args[indexes[0] + 1]);
27
+ }
21
28
  export const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
22
- export const EXECUTION_BUDGET_FLAG = "pi-subagent-execution-budget";
23
29
  export const CHILD_EXCLUDED_TOOL_NAMES = [
24
30
  "delegate_task",
25
31
  "ask_question",
@@ -28,7 +34,6 @@ export const CHILD_EXCLUDED_TOOL_NAMES = [
28
34
  "subagent_stage",
29
35
  "subagent_abort",
30
36
  ];
31
- export const CHILD_EXCLUDED_TOOLS = CHILD_EXCLUDED_TOOL_NAMES.join(",");
32
37
  const SYSTEM_PROMPT_FLAG = "--append-system-prompt";
33
38
  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.";
34
39
  export const DELEGATE_TASK = {
@@ -238,9 +243,10 @@ export async function resolveRolePackageResources(role, ctx) {
238
243
  };
239
244
  }
240
245
  export function createRoleLaunch(pi, ctx, input) {
241
- return createRoleLaunchFromSkills(ctx, input, resolveRoleSkills(pi, input.role));
246
+ return finalizeRoleLaunch(prepareRoleLaunchFromSkills(ctx, input, resolveRoleSkills(pi, input.role)));
242
247
  }
243
- function createRoleLaunchFromSkills(ctx, input, skills) {
248
+ /** Build the prompt-free argv and tool policy; the Role prompt stays separate until `finalizeRoleLaunch`. */
249
+ function prepareRoleLaunchFromSkills(ctx, input, skills) {
244
250
  const role = input.role;
245
251
  const mcps = mcpList(role.mcps, `Role ${role.name}`);
246
252
  const tools = roleToolPolicy(role, input.tools);
@@ -262,7 +268,7 @@ function createRoleLaunchFromSkills(ctx, input, skills) {
262
268
  throw new Error(`Invalid launch environment value: ${key}`);
263
269
  return [key, value];
264
270
  }));
265
- const args = ["--no-session", "--no-extensions", "--no-skills", "--exclude-tools", CHILD_EXCLUDED_TOOLS];
271
+ const args = ["--no-session", "--no-extensions", "--no-skills", "--exclude-tools", CHILD_EXCLUDED_TOOL_NAMES.join(",")];
266
272
  if (mcps.length)
267
273
  args.push(`--${ROLE_MCP_POLICY_FLAG}`, JSON.stringify(mcps));
268
274
  for (const extension of new Set(extensions))
@@ -274,46 +280,30 @@ function createRoleLaunchFromSkills(ctx, input, skills) {
274
280
  if (input.route.thinkingLevel)
275
281
  args.push("--thinking", input.route.thinkingLevel);
276
282
  args.push(ctx.isProjectTrusted() ? "--approve" : "--no-approve");
277
- args.push(SYSTEM_PROMPT_FLAG, `${CHILD_IDENTITY_POLICY}\n\n${cleanText(role.systemPrompt, "system prompt", `Role ${role.name}`)}`);
278
283
  return {
279
284
  env,
280
285
  args,
281
286
  model: input.route.model,
282
287
  thinkingLevel: input.route.thinkingLevel,
283
288
  missingSkills: skills.missing,
289
+ role: role.name,
290
+ tools: Object.freeze(tools),
291
+ systemPrompt: `${CHILD_IDENTITY_POLICY}\n\n${cleanText(role.systemPrompt, "system prompt", `Role ${role.name}`)}`,
292
+ promptArgIndex: args.length,
284
293
  };
285
294
  }
286
- export function resolveRoleLaunch(pi, ctx, input) {
295
+ function prepareResolvedRoleLaunch(pi, ctx, input) {
287
296
  const { task, modelClass, agentDir, ...launchInput } = input;
288
297
  const selectedClass = modelClass ?? input.role.modelClass;
289
- return createRoleLaunch(pi, ctx, {
298
+ return prepareRoleLaunchFromSkills(ctx, {
290
299
  ...launchInput,
291
300
  route: selectedClass === undefined
292
301
  ? resolveConfiguredTaskRoute(ctx, task, agentDir)
293
302
  : resolveTaskRoute(ctx, selectedClass, agentDir),
294
- });
303
+ }, resolveRoleSkills(pi, input.role));
295
304
  }
296
- function stripRoleSystemPrompt(rawArgs) {
297
- const indexes = rawArgs.flatMap((arg, index) => arg === SYSTEM_PROMPT_FLAG ? [index] : []);
298
- if (indexes.length !== 1)
299
- throw new Error(`Role launch must contain exactly one ${SYSTEM_PROMPT_FLAG} pair.`);
300
- const promptArgIndex = indexes[0];
301
- const systemPrompt = rawArgs[promptArgIndex + 1];
302
- if (typeof systemPrompt !== "string" || !systemPrompt.includes("\n") || !systemPrompt.trim() || systemPrompt.includes("\0")) {
303
- throw new Error(`Role ${SYSTEM_PROMPT_FLAG} value must be the exact multiline Role prompt.`);
304
- }
305
- const args = [...rawArgs.slice(0, promptArgIndex), ...rawArgs.slice(promptArgIndex + 2)];
306
- if (args.includes(SYSTEM_PROMPT_FLAG) || args.includes(systemPrompt)) {
307
- throw new Error("Sanitized Role argv must contain no prompt flag or raw Role prompt.");
308
- }
309
- return { args, systemPrompt, promptArgIndex };
310
- }
311
- function prepareResolvedRoleLaunch(roleDefinition, launch, additionalTools = []) {
312
- const role = parseRoleName(roleDefinition.name);
313
- rejectRetiredRoleIsolation(roleDefinition.isolation, `Role ${role}`);
314
- const tools = Object.freeze(roleToolPolicy(roleDefinition, additionalTools));
315
- const { args, systemPrompt, promptArgIndex } = stripRoleSystemPrompt(launch.args);
316
- return { ...launch, args, role, tools, systemPrompt, promptArgIndex };
305
+ export function resolveRoleLaunch(pi, ctx, input) {
306
+ return finalizeRoleLaunch(prepareResolvedRoleLaunch(pi, ctx, input));
317
307
  }
318
308
  function assertNoMissingRoleSkills(role, launch) {
319
309
  if (!launch.missingSkills.length)
@@ -321,11 +311,13 @@ function assertNoMissingRoleSkills(role, launch) {
321
311
  throw new Error(`Role ${parseRoleName(role.name)} requires missing Skills: ${launch.missingSkills.join(", ")}.`);
322
312
  }
323
313
  export function prepareRoleLaunch(pi, ctx, input) {
324
- const launch = "route" in input
325
- ? createRoleLaunch(pi, ctx, input)
326
- : resolveRoleLaunch(pi, ctx, input);
327
- assertNoMissingRoleSkills(input.role, launch);
328
- return prepareResolvedRoleLaunch(input.role, launch, input.tools);
314
+ const role = parseRoleName(input.role.name);
315
+ rejectRetiredRoleIsolation(input.role.isolation, `Role ${role}`);
316
+ const prepared = "route" in input
317
+ ? prepareRoleLaunchFromSkills(ctx, input, resolveRoleSkills(pi, input.role))
318
+ : prepareResolvedRoleLaunch(pi, ctx, input);
319
+ assertNoMissingRoleSkills(input.role, prepared);
320
+ return { ...prepared, role };
329
321
  }
330
322
  /** Resolve and prepare a configured Role with its package-owned resources. */
331
323
  export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
@@ -347,36 +339,22 @@ export async function resolveConfiguredRoleLaunch(pi, ctx, input) {
347
339
  ...namedSkills,
348
340
  paths: [...new Set([...namedSkills.paths, ...resources.skills])],
349
341
  };
350
- const launch = createRoleLaunchFromSkills(ctx, {
342
+ const prepared = prepareRoleLaunchFromSkills(ctx, {
351
343
  role: effectiveRole,
352
344
  route: resolveTaskRoute(ctx, input.modelClass),
353
345
  }, skills);
354
- assertNoMissingRoleSkills(effectiveRole, launch);
355
- const additions = [
346
+ assertNoMissingRoleSkills(effectiveRole, prepared);
347
+ const args = [
348
+ ...prepared.args,
356
349
  "--no-prompt-templates",
357
350
  "--no-themes",
358
351
  ...resources.prompts.flatMap((path) => ["--prompt-template", path]),
359
352
  ...resources.themes.flatMap((path) => ["--theme", path]),
360
353
  ];
361
- const promptArgIndex = launch.args.indexOf(SYSTEM_PROMPT_FLAG);
362
- if (promptArgIndex < 0)
363
- throw new Error(`Resolved Role launch has no ${SYSTEM_PROMPT_FLAG}.`);
364
- const args = [...launch.args];
365
- args.splice(promptArgIndex, 0, ...additions);
366
- return prepareResolvedRoleLaunch(effectiveRole, { ...launch, args });
354
+ return { ...prepared, args, promptArgIndex: args.length };
367
355
  }
368
- /** Restore the system prompt pair after a caller has prepared its launch argv. */
356
+ /** Insert the system prompt pair after a caller has prepared its launch argv. */
369
357
  export function finalizeRoleLaunch(prepared) {
370
- if (prepared.args.includes(SYSTEM_PROMPT_FLAG)) {
371
- throw new Error(`Prepared Role launch already contains ${SYSTEM_PROMPT_FLAG}.`);
372
- }
373
- if (!Number.isSafeInteger(prepared.promptArgIndex)
374
- || prepared.promptArgIndex < 0 || prepared.promptArgIndex > prepared.args.length) {
375
- throw new Error("Prepared Role launch has an invalid system prompt insertion index.");
376
- }
377
- if (typeof prepared.systemPrompt !== "string" || !prepared.systemPrompt.trim() || prepared.systemPrompt.includes("\0")) {
378
- throw new Error("Prepared Role launch has an invalid system prompt.");
379
- }
380
358
  const args = [...prepared.args];
381
359
  args.splice(prepared.promptArgIndex, 0, SYSTEM_PROMPT_FLAG, prepared.systemPrompt);
382
360
  return {
@@ -1,5 +1,4 @@
1
1
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { EphemeralSubagentExecutionBudget } from "./ephemeral.ts";
3
2
  import type { CoordinatorRuntime, OperationContext, TransientLaunchHandle, VerifiedLaunch } from "./runner.ts";
4
3
  import { type ExecuteRequest, type ModelClass, type WorkspaceIdentity } from "./schema.ts";
5
4
  export declare const ISOLATED_MODEL_TASK: {
@@ -21,7 +20,6 @@ export interface LaunchRuntimeOptions {
21
20
  inspectMainBase(input: {
22
21
  root: string;
23
22
  }, context: OperationContext): Promise<WorkspaceIdentity>;
24
- executionBudget?: () => Omit<EphemeralSubagentExecutionBudget, "startedAt">;
25
23
  now?: () => number;
26
24
  randomToken?: () => string;
27
25
  }
@@ -3,7 +3,7 @@ import { constants } from "node:fs";
3
3
  import { chmod, lstat, mkdtemp, open, realpath, rmdir, unlink } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { isAbsolute, join, normalize, relative, sep } from "node:path";
6
- import { EXECUTION_BUDGET_FLAG, resolveConfiguredRoleLaunch } from "./index.js";
6
+ import { resolveConfiguredRoleLaunch } from "./index.js";
7
7
  import { registerModelTask } from "@henryqw/pi-task-models";
8
8
  const PROMPT_FLAG = "--append-system-prompt";
9
9
  const DIRECTORY_MODE = 0o700;
@@ -130,20 +130,13 @@ export class RoleLaunchRuntime {
130
130
  if (prepared.missingSkills.length) {
131
131
  throw new Error(`Role ${role} requires missing Skills: ${prepared.missingSkills.join(", ")}.`);
132
132
  }
133
- const executionBudget = this.options.executionBudget?.();
134
133
  return Object.freeze({
135
134
  launch: Object.freeze({
136
135
  role: prepared.role,
137
136
  modelClass,
138
137
  model: `${prepared.model.provider}/${prepared.model.id}`,
139
138
  thinkingLevel: prepared.thinkingLevel,
140
- args: Object.freeze([
141
- ...prepared.args,
142
- ...(executionBudget === undefined ? [] : [
143
- `--${EXECUTION_BUDGET_FLAG}`,
144
- JSON.stringify({ ...executionBudget, startedAt: this.now() }),
145
- ]),
146
- ]),
139
+ args: Object.freeze([...prepared.args]),
147
140
  env: Object.freeze({ ...prepared.env }),
148
141
  tools: Object.freeze([...prepared.tools]),
149
142
  }),
@@ -3,5 +3,4 @@ export interface RoleMcpConfig {
3
3
  settings?: Record<string, unknown>;
4
4
  }
5
5
  export declare function parseRoleMcpAllowlist(value: unknown): string[];
6
- export declare function roleMcpFlagValue(args: readonly string[], flag: string): string | undefined;
7
6
  export declare function selectRoleMcpConfig(config: RoleMcpConfig, allowlist: readonly string[]): RoleMcpConfig;
package/dist/mcp-role.js CHANGED
@@ -17,17 +17,6 @@ export function parseRoleMcpAllowlist(value) {
17
17
  throw new Error("The Role MCP policy flag contains duplicate MCP server names.");
18
18
  return names;
19
19
  }
20
- export function roleMcpFlagValue(args, flag) {
21
- const indexes = args.flatMap((arg, index) => arg === flag ? [index] : []);
22
- if (indexes.length > 1)
23
- throw new Error(`${flag} must appear at most once.`);
24
- if (!indexes.length)
25
- return;
26
- const value = args[indexes[0] + 1];
27
- if (value === undefined)
28
- throw new Error(`${flag} requires a value.`);
29
- return value;
30
- }
31
20
  export function selectRoleMcpConfig(config, allowlist) {
32
21
  const missing = allowlist.filter((name) => !Object.hasOwn(config.mcpServers, name));
33
22
  if (missing.length)
package/dist/runner.js CHANGED
@@ -69,7 +69,7 @@ class DurableRunStopped extends Error {
69
69
  }
70
70
  class ProductiveScope {
71
71
  signal;
72
- constructor(_timeoutMs, _now, outerSignal) {
72
+ constructor(outerSignal) {
73
73
  this.signal = outerSignal ?? new AbortController().signal;
74
74
  }
75
75
  async call(operation) {
@@ -150,10 +150,6 @@ class FollowupControl {
150
150
  this.sealed = true;
151
151
  return undefined;
152
152
  }
153
- invalidate() {
154
- this.sealed = true;
155
- this.queue.length = 0;
156
- }
157
153
  close() {
158
154
  this.sealed = true;
159
155
  this.queue.length = 0;
@@ -662,7 +658,7 @@ export class IsolatedRunner {
662
658
  async execute(value, cwd, outerSignal) {
663
659
  const request = parseExecuteRequest(value);
664
660
  const policy = Object.freeze({ ...this.currentPolicy() });
665
- const scope = new ProductiveScope(policy.childMaxMs, () => this.coordinatorRuntime.now(), outerSignal);
661
+ const scope = new ProductiveScope(outerSignal);
666
662
  const canonicalCwd = realpathSync.native(cwd);
667
663
  const prepared = await scope.call(async (context) => await this.coordinatorRuntime.preflight({ request, cwd: canonicalCwd }, context));
668
664
  const root = realpathSync.native(prepared.root);
@@ -748,8 +744,7 @@ export class IsolatedRunner {
748
744
  return loaded;
749
745
  }, { productiveRunLease: lifecycle.lease });
750
746
  const state = handle.state;
751
- const current = this.currentPolicy();
752
- const scope = new ProductiveScope(Math.min(state.policy.childMaxMs, current.childMaxMs), () => this.coordinatorRuntime.now(), outerSignal);
747
+ const scope = new ProductiveScope(outerSignal);
753
748
  try {
754
749
  if (request.action === "finalize")
755
750
  return await this.finalize(handle, scope);
@@ -782,8 +777,6 @@ export class IsolatedRunner {
782
777
  throw error;
783
778
  }
784
779
  finally {
785
- if (scope instanceof DeadlineScope)
786
- scope.close();
787
780
  this.closeRequestControls(root, request.id);
788
781
  if (!lifecycle.stopped) {
789
782
  delete state.recovery;
@@ -820,7 +813,7 @@ export class IsolatedRunner {
820
813
  ? generation.combinedTip ?? generation.stages.at(-1)?.tip ?? generation.integrationBase : state.main;
821
814
  if (!sameIdentity(action.expectedTip, expected))
822
815
  throw new Error("Rejection recovery has a stale integration tip.");
823
- const scope = new ProductiveScope(Math.min(state.policy.childMaxMs, this.currentPolicy().childMaxMs), () => this.coordinatorRuntime.now(), outerSignal);
816
+ const scope = new ProductiveScope(outerSignal);
824
817
  const termination = attempt.termination;
825
818
  const result = await scope.call((context) => this.hostRuntime.reconcileWorkerTermination({
826
819
  task: changesetTaskRequest(state, task.taskId), attempt, workerId: termination.workerId,
@@ -841,7 +834,7 @@ export class IsolatedRunner {
841
834
  || !sameIdentity(attempt.readiness.candidate, action.candidate)) {
842
835
  throw new Error("Stage action refers to a stale or unowned candidate.");
843
836
  }
844
- const scope = new ProductiveScope(Math.min(state.policy.childMaxMs, this.currentPolicy().childMaxMs), () => this.coordinatorRuntime.now(), outerSignal);
837
+ const scope = new ProductiveScope(outerSignal);
845
838
  const generations = state.integration.generations;
846
839
  let generation = generations.at(-1);
847
840
  const nextNumber = generation?.status === "superseded" ? generation.number + 1 : generation?.number ?? 1;
@@ -1100,7 +1093,7 @@ export class IsolatedRunner {
1100
1093
  const request = parseExecuteRequest(state.request);
1101
1094
  const generation = state.integration.generations.at(-1);
1102
1095
  if (action.action === "release") {
1103
- const scope = new ProductiveScope(Math.min(state.policy.childMaxMs, this.currentPolicy().childMaxMs), () => this.coordinatorRuntime.now(), outerSignal);
1096
+ const scope = new ProductiveScope(outerSignal);
1104
1097
  if (action.taskId !== undefined || action.attempt !== undefined) {
1105
1098
  if (!action.taskId || !action.attempt || action.generation !== (generation?.number ?? 1)) {
1106
1099
  throw new Error("Candidate release requires the latest generation and exact task attempt.");
@@ -1149,7 +1142,7 @@ export class IsolatedRunner {
1149
1142
  if (state.integration.generations.filter((item) => item.worktree && !item.cleanup?.every((step) => step.status === "completed")).length >= MAX_RETAINED_INTEGRATION_GENERATIONS) {
1150
1143
  throw new Error("Retained integration worktree limit is exhausted.");
1151
1144
  }
1152
- const scope = new ProductiveScope(Math.min(state.policy.childMaxMs, this.currentPolicy().childMaxMs), () => this.coordinatorRuntime.now(), outerSignal);
1145
+ const scope = new ProductiveScope(outerSignal);
1153
1146
  const actualMain = await scope.call((context) => this.gitRuntime.inspectMain({ root }, context));
1154
1147
  if (!isCleanCommitted(action.newMain) || !sameIdentity(actualMain, action.newMain)) {
1155
1148
  throw new Error("Refresh requires the exact new clean Main identity.");
@@ -1233,7 +1226,7 @@ export class IsolatedRunner {
1233
1226
  || (terminal(state) && !(state.status === "completed" && action.action === "cleanup"))
1234
1227
  || (state.status !== "needs_attention" && !(state.status === "completed" && action.action === "cleanup")))
1235
1228
  throw new Error("Integration action has a stale generation or combined tip.");
1236
- const scope = new ProductiveScope(Math.min(state.policy.childMaxMs, this.currentPolicy().childMaxMs), () => this.coordinatorRuntime.now(), outerSignal);
1229
+ const scope = new ProductiveScope(outerSignal);
1237
1230
  const receipts = this.stageReceipts(generation);
1238
1231
  if (action.action === "advance") {
1239
1232
  if (generation.status !== "staging" || generation.stages.some((stage) => stage.status !== "staged")
@@ -1603,7 +1596,7 @@ export class IsolatedRunner {
1603
1596
  }
1604
1597
  const activeControls = this.activeControls(root, id);
1605
1598
  for (const { control } of activeControls)
1606
- control.invalidate();
1599
+ control.close();
1607
1600
  if (!activeControls.length
1608
1601
  && !lifecycle.productiveRunLeaseActive
1609
1602
  && this.recoverInterrupted(state)) {
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>;
@@ -139,14 +144,7 @@ type ExecuteRequestInput = Static<typeof ExecuteRequestSchema>;
139
144
  export type ExecuteRequest = Omit<ExecuteRequestInput, "finalChecks"> & {
140
145
  finalChecks: CheckCommand[];
141
146
  };
142
- export interface ExecutionPolicySnapshot {
143
- maxSubagents: number;
144
- maxTurns: number;
145
- maxTokens?: number;
146
- childIdleMs: number;
147
- childMaxMs: number;
148
- maxCorrections: number;
149
- }
147
+ export type ExecutionPolicySnapshot = Static<typeof ExecutionPolicySnapshotSchema>;
150
148
  export type ResumeRequest = Static<typeof ResumeRequestSchema>;
151
149
  export type WorkspaceIdentity = Static<typeof WorkspaceSchema>;
152
150
  declare const WorktreeRecordSchema: Type.TObject<{
@@ -376,6 +374,31 @@ export declare const IntegrationActionSchema: Type.TUnion<[Type.TObject<{
376
374
  taskId: Type.TOptional<Type.TString>;
377
375
  attempt: Type.TOptional<Type.TInteger>;
378
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
+ }>;
379
402
  export type IntegrationAction = Static<typeof IntegrationActionSchema>;
380
403
  export declare function parseIntegrationAction(value: unknown): IntegrationAction;
381
404
  export declare function parseStageRequest(value: unknown): StageRequest;
@@ -1083,6 +1106,14 @@ declare const WaveStateSchema: Type.TObject<{
1083
1106
  taskIds: Type.TArray<Type.TString>;
1084
1107
  status: Type.TUnion<[Type.TLiteral<"dispatching">, Type.TLiteral<"integrating">, Type.TLiteral<"completed">, Type.TLiteral<"needs_attention">]>;
1085
1108
  }>;
1109
+ declare const ExecutionPolicySnapshotSchema: Type.TObject<{
1110
+ maxSubagents: Type.TInteger;
1111
+ maxTurns: Type.TInteger;
1112
+ maxTokens: Type.TOptional<Type.TInteger>;
1113
+ childIdleMs: Type.TNumber;
1114
+ childMaxMs: Type.TNumber;
1115
+ maxCorrections: Type.TInteger;
1116
+ }>;
1086
1117
  declare const IntegrationCandidateSchema: Type.TObject<{
1087
1118
  taskId: Type.TString;
1088
1119
  attempt: Type.TInteger;
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));
@@ -57,5 +57,3 @@ export declare function inspectWorktreeDirty(cwd: string, run?: GitRunner, inclu
57
57
  * recorded; any uncertainty preserves work and omits unknown measurements.
58
58
  */
59
59
  export declare function finalizeChildWorktree(info: WorktreeInfo, run?: GitRunner): Promise<WorktreePayload>;
60
- /** Context block telling the child to work inside its isolated worktree. */
61
- export declare function worktreeContextNote(info: WorktreeInfo): string;
package/dist/worktree.js CHANGED
@@ -269,7 +269,3 @@ export async function finalizeChildWorktree(info, run = runGit) {
269
269
  }
270
270
  return { outcome: "pruned", path: info.path, branch: info.branch };
271
271
  }
272
- /** Context block telling the child to work inside its isolated worktree. */
273
- export function worktreeContextNote(info) {
274
- return `\n\n[WORKTREE ISOLATION] Work only in ${info.path} on ${info.branch}; do not use main checkout. Commit changes to this branch for parent review.`;
275
- }
@@ -14,10 +14,6 @@ export function roleIsReadOnlyScout(role: Role): boolean {
14
14
  return !roleCanWrite(role) && role.extensions.length === 0 && !role.mcps?.length;
15
15
  }
16
16
 
17
- export interface CheckoutAdmission {
18
- register(pi: ExtensionAPI, directCanWrite: (input: unknown) => boolean): void;
19
- }
20
-
21
17
  async function checkoutKey(pi: ExtensionAPI, ctx: ExtensionContext): Promise<string> {
22
18
  const cwd = await realpath(ctx.cwd);
23
19
  const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd, timeout: 5_000 });
@@ -32,7 +28,7 @@ async function checkoutKey(pi: ExtensionAPI, ctx: ExtensionContext): Promise<str
32
28
  * not an OS sandbox: external processes and trusted extension lifecycle code
33
29
  * remain outside interception.
34
30
  */
35
- export function createCheckoutAdmission(): CheckoutAdmission {
31
+ export function registerCheckoutAdmission(pi: ExtensionAPI, directCanWrite: (input: unknown) => boolean): void {
36
32
  const ownerByCheckout = new Map<string, string>();
37
33
  const checkoutByCall = new Map<string, string>();
38
34
  const release = (toolCallId: string) => {
@@ -41,30 +37,26 @@ export function createCheckoutAdmission(): CheckoutAdmission {
41
37
  checkoutByCall.delete(toolCallId);
42
38
  if (ownerByCheckout.get(key) === toolCallId) ownerByCheckout.delete(key);
43
39
  };
44
- return {
45
- register(pi, directCanWrite) {
46
- pi.on("tool_call", async (event, ctx) => {
47
- const potentiallyWriting = event.toolName === "delegate_task"
48
- ? directCanWrite(event.input)
49
- : !READ_ONLY_TOOLS.has(event.toolName);
50
- if (!potentiallyWriting) return;
51
- const key = await checkoutKey(pi, ctx);
52
- const owner = ownerByCheckout.get(key);
53
- if (owner && owner !== event.toolCallId) {
54
- return {
55
- block: true,
56
- reason: `Checkout ${key} already has an admitted Pi writer (${owner}); retry after it settles.`,
57
- };
58
- }
59
- ownerByCheckout.set(key, event.toolCallId);
60
- checkoutByCall.set(event.toolCallId, key);
61
- });
62
- pi.on("tool_result", (event) => release(event.toolCallId));
63
- pi.on("tool_execution_end", (event) => release(event.toolCallId));
64
- pi.on("session_shutdown", () => {
65
- ownerByCheckout.clear();
66
- checkoutByCall.clear();
67
- });
68
- },
69
- };
40
+ pi.on("tool_call", async (event, ctx) => {
41
+ const potentiallyWriting = event.toolName === "delegate_task"
42
+ ? directCanWrite(event.input)
43
+ : !READ_ONLY_TOOLS.has(event.toolName);
44
+ if (!potentiallyWriting) return;
45
+ const key = await checkoutKey(pi, ctx);
46
+ const owner = ownerByCheckout.get(key);
47
+ if (owner && owner !== event.toolCallId) {
48
+ return {
49
+ block: true,
50
+ reason: `Checkout ${key} already has an admitted Pi writer (${owner}); retry after it settles.`,
51
+ };
52
+ }
53
+ ownerByCheckout.set(key, event.toolCallId);
54
+ checkoutByCall.set(event.toolCallId, key);
55
+ });
56
+ pi.on("tool_result", (event) => release(event.toolCallId));
57
+ pi.on("tool_execution_end", (event) => release(event.toolCallId));
58
+ pi.on("session_shutdown", () => {
59
+ ownerByCheckout.clear();
60
+ checkoutByCall.clear();
61
+ });
70
62
  }