pi-subagents 0.40.0 → 0.41.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -8,7 +8,7 @@ import { parse as parseYaml } from "yaml";
8
8
  import * as os from "node:os";
9
9
  import * as path from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
- import type { AcceptanceInput, AcceptanceRole, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
11
+ import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
12
12
  import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
13
13
  import { KNOWN_FIELDS } from "./agent-serializer.ts";
14
14
  import { parseChain, parseJsonChain } from "./chain-serializer.ts";
@@ -20,6 +20,7 @@ export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./
20
20
  import { parseMemoryFrontmatter } from "./agent-memory.ts";
21
21
  import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
22
22
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
23
+ import { validatePermissionRules, type PermissionRules } from "../runs/shared/permissions.ts";
23
24
 
24
25
  export type AgentScope = "user" | "project" | "both";
25
26
 
@@ -115,6 +116,7 @@ export interface AgentModelSourceInfo {
115
116
 
116
117
  export interface AgentConfig {
117
118
  name: string;
119
+ runner?: AgentRunnerConfig;
118
120
  localName?: string;
119
121
  packageName?: string;
120
122
  description: string;
@@ -148,6 +150,7 @@ export interface AgentConfig {
148
150
  maxSubagentDepth?: number;
149
151
  completionGuard?: boolean;
150
152
  toolBudget?: ToolBudgetConfig;
153
+ permissions?: PermissionRules;
151
154
  memory?: AgentMemoryConfig;
152
155
  disabled?: boolean;
153
156
  extraFields?: Record<string, string>;
@@ -327,6 +330,21 @@ function getGlobalNpmRoot(): string | null {
327
330
  const offline = process.env.PI_OFFLINE?.toLowerCase();
328
331
  if (offline === "1" || offline === "true" || offline === "yes") return null;
329
332
  if (cachedGlobalNpmRoot !== null) return cachedGlobalNpmRoot;
333
+
334
+ const windowsGlobalRoot = process.platform === "win32" && process.env.APPDATA
335
+ ? path.join(process.env.APPDATA, "npm", "node_modules")
336
+ : undefined;
337
+ if (windowsGlobalRoot) {
338
+ try {
339
+ if (fs.statSync(windowsGlobalRoot).isDirectory()) {
340
+ cachedGlobalNpmRoot = fs.realpathSync(windowsGlobalRoot);
341
+ return cachedGlobalNpmRoot;
342
+ }
343
+ } catch {
344
+ // Fall through if the directory disappears while resolving it.
345
+ }
346
+ }
347
+
330
348
  try {
331
349
  cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim());
332
350
  return cachedGlobalNpmRoot;
@@ -486,7 +504,8 @@ function effectiveAgentMatch(matches: AgentConfig[]): { agent?: AgentConfig; err
486
504
  const distinctNames = [...new Set(matches.map((agent) => agent.name))];
487
505
  if (distinctNames.length === 1) {
488
506
  const sourceRank = new Map<AgentConfig["source"], number>([["builtin", 0], ["package", 1], ["user", 2], ["project", 3]]);
489
- return { agent: [...matches].sort((a, b) => (sourceRank.get(b.source) ?? 0) - (sourceRank.get(a.source) ?? 0))[0] };
507
+ const agent = [...matches].sort((a, b) => (sourceRank.get(b.source) ?? 0) - (sourceRank.get(a.source) ?? 0))[0];
508
+ return agent ? { agent } : {};
490
509
  }
491
510
  return {};
492
511
  }
@@ -494,7 +513,7 @@ function effectiveAgentMatch(matches: AgentConfig[]): { agent?: AgentConfig; err
494
513
  export function resolveAgentName(name: string, agents: AgentConfig[]): { agent?: AgentConfig; error?: string } {
495
514
  const raw = name.trim();
496
515
  const exact = agents.filter((agent) => agent.name === raw || agent.localName === raw);
497
- if (exact.length === 1) return { agent: exact[0] };
516
+ if (exact.length === 1) return exact[0] ? { agent: exact[0] } : {};
498
517
  if (exact.length > 1) {
499
518
  const effective = effectiveAgentMatch(exact);
500
519
  if (effective.agent) return effective;
@@ -502,7 +521,7 @@ export function resolveAgentName(name: string, agents: AgentConfig[]): { agent?:
502
521
  }
503
522
 
504
523
  const aliases = agents.filter((agent) => agent.aliases?.includes(raw));
505
- if (aliases.length === 1) return { agent: aliases[0] };
524
+ if (aliases.length === 1) return aliases[0] ? { agent: aliases[0] } : {};
506
525
  if (aliases.length > 1) {
507
526
  const effective = effectiveAgentMatch(aliases);
508
527
  if (effective.agent) return effective;
@@ -548,24 +567,24 @@ function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean
548
567
  function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
549
568
  return {
550
569
  description: agent.description,
551
- model: agent.model,
552
- fallbackModels: agent.fallbackModels ? [...agent.fallbackModels] : undefined,
553
- thinking: agent.thinking,
570
+ ...(agent.model !== undefined ? { model: agent.model } : {}),
571
+ ...(agent.fallbackModels ? { fallbackModels: [...agent.fallbackModels] } : {}),
572
+ ...(agent.thinking !== undefined ? { thinking: agent.thinking } : {}),
554
573
  systemPromptMode: agent.systemPromptMode,
555
574
  inheritProjectContext: agent.inheritProjectContext,
556
575
  inheritSkills: agent.inheritSkills,
557
- defaultContext: agent.defaultContext,
558
- acceptanceRole: agent.acceptanceRole,
559
- disabled: agent.disabled,
576
+ ...(agent.defaultContext !== undefined ? { defaultContext: agent.defaultContext } : {}),
577
+ ...(agent.acceptanceRole !== undefined ? { acceptanceRole: agent.acceptanceRole } : {}),
578
+ ...(agent.disabled !== undefined ? { disabled: agent.disabled } : {}),
560
579
  systemPrompt: agent.systemPrompt,
561
- skills: agent.skills ? [...agent.skills] : undefined,
562
- skillPath: agent.skillPath ? [...agent.skillPath] : undefined,
563
- tools: agent.tools ? [...agent.tools] : undefined,
564
- mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
565
- extensions: agent.extensionsFromDefault ? undefined : agent.extensions ? [...agent.extensions] : undefined,
566
- subagentOnlyExtensions: agent.subagentOnlyExtensions ? [...agent.subagentOnlyExtensions] : undefined,
567
- completionGuard: agent.completionGuard,
568
- toolBudget: agent.toolBudget,
580
+ ...(agent.skills ? { skills: [...agent.skills] } : {}),
581
+ ...(agent.skillPath ? { skillPath: [...agent.skillPath] } : {}),
582
+ ...(agent.tools ? { tools: [...agent.tools] } : {}),
583
+ ...(agent.mcpDirectTools ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
584
+ ...(!agent.extensionsFromDefault && agent.extensions ? { extensions: [...agent.extensions] } : {}),
585
+ ...(agent.subagentOnlyExtensions ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
586
+ ...(agent.completionGuard !== undefined ? { completionGuard: agent.completionGuard } : {}),
587
+ ...(agent.toolBudget !== undefined ? { toolBudget: agent.toolBudget } : {}),
569
588
  };
570
589
  }
571
590
 
@@ -882,14 +901,23 @@ function readSubagentSettings(filePath: string | null): SubagentSettings {
882
901
 
883
902
  const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
884
903
  const agentOverrides = subagentsObject.agentOverrides;
904
+ const parsedSettings: SubagentSettings = {
905
+ overrides: parsed,
906
+ ...(defaultModel !== undefined ? { defaultModel } : {}),
907
+ ...(defaultThinking !== undefined ? { defaultThinking } : {}),
908
+ ...(defaultExtensions !== undefined ? { defaultExtensions } : {}),
909
+ ...(disableBuiltins !== undefined ? { disableBuiltins } : {}),
910
+ ...(disableThinking !== undefined ? { disableThinking } : {}),
911
+ ...(modelScope !== undefined ? { modelScope } : {}),
912
+ };
885
913
  if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) {
886
- return { overrides: parsed, defaultModel, defaultThinking, defaultExtensions, disableBuiltins, disableThinking, modelScope };
914
+ return parsedSettings;
887
915
  }
888
916
  for (const [name, value] of Object.entries(agentOverrides)) {
889
917
  const override = parseBuiltinOverrideEntry(name, value, filePath);
890
918
  if (override) parsed[name] = override;
891
919
  }
892
- return { overrides: parsed, defaultModel, defaultThinking, defaultExtensions, disableBuiltins, disableThinking, modelScope };
920
+ return parsedSettings;
893
921
  }
894
922
 
895
923
  function resolveSubagentDefaultModel(
@@ -980,41 +1008,34 @@ function applyBuiltinOverride(
980
1008
  };
981
1009
 
982
1010
  if (override.description !== undefined) next.description = override.description;
983
- if (override.model !== undefined) next.model = override.model === false ? undefined : override.model;
984
- if (override.fallbackModels !== undefined) {
985
- next.fallbackModels = override.fallbackModels === false ? undefined : [...override.fallbackModels];
986
- }
987
- if (override.thinking !== undefined) next.thinking = override.thinking === false ? undefined : override.thinking;
1011
+ if (override.model !== undefined) { if (override.model === false) delete next.model; else next.model = override.model; }
1012
+ if (override.fallbackModels !== undefined) { if (override.fallbackModels === false) delete next.fallbackModels; else next.fallbackModels = [...override.fallbackModels]; }
1013
+ if (override.thinking !== undefined) { if (override.thinking === false) delete next.thinking; else next.thinking = override.thinking; }
988
1014
  if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
989
1015
  if (override.inheritProjectContext !== undefined) next.inheritProjectContext = override.inheritProjectContext;
990
1016
  if (override.inheritSkills !== undefined) next.inheritSkills = override.inheritSkills;
991
- if (override.defaultContext !== undefined) next.defaultContext = override.defaultContext === false ? undefined : override.defaultContext;
992
- if (override.acceptanceRole !== undefined) next.acceptanceRole = override.acceptanceRole === false ? undefined : override.acceptanceRole;
1017
+ if (override.defaultContext !== undefined) { if (override.defaultContext === false) delete next.defaultContext; else next.defaultContext = override.defaultContext; }
1018
+ if (override.acceptanceRole !== undefined) { if (override.acceptanceRole === false) delete next.acceptanceRole; else next.acceptanceRole = override.acceptanceRole; }
993
1019
  if (override.disabled !== undefined) next.disabled = override.disabled;
994
1020
  if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
995
- if (override.skills !== undefined) next.skills = override.skills === false ? undefined : [...override.skills];
1021
+ if (override.skills !== undefined) { if (override.skills === false) delete next.skills; else next.skills = [...override.skills]; }
996
1022
  if (override.tools !== undefined) {
997
1023
  const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools);
998
- next.tools = tools;
999
- next.mcpDirectTools = mcpDirectTools;
1000
- }
1001
- if (override.extensions !== undefined) next.extensions = override.extensions === false ? undefined : [...override.extensions];
1002
- if (override.subagentOnlyExtensions !== undefined) {
1003
- next.subagentOnlyExtensions = override.subagentOnlyExtensions === false ? undefined : [...override.subagentOnlyExtensions];
1024
+ if (tools === undefined) delete next.tools; else next.tools = tools;
1025
+ if (mcpDirectTools === undefined) delete next.mcpDirectTools; else next.mcpDirectTools = mcpDirectTools;
1004
1026
  }
1027
+ if (override.extensions !== undefined) { if (override.extensions === false) delete next.extensions; else next.extensions = [...override.extensions]; }
1028
+ if (override.subagentOnlyExtensions !== undefined) { if (override.subagentOnlyExtensions === false) delete next.subagentOnlyExtensions; else next.subagentOnlyExtensions = [...override.subagentOnlyExtensions]; }
1005
1029
  if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
1006
- if (override.toolBudget !== undefined) next.toolBudget = override.toolBudget === false ? undefined : override.toolBudget;
1030
+ if (override.toolBudget !== undefined) { if (override.toolBudget === false) delete next.toolBudget; else next.toolBudget = override.toolBudget; }
1007
1031
 
1008
1032
  return next;
1009
1033
  }
1010
1034
 
1011
1035
  function clearBuiltinThinking(agent: AgentConfig, meta: { scope: "user" | "project"; path: string }): AgentConfig {
1012
1036
  if (agent.thinking === undefined) return agent;
1013
- return {
1014
- ...agent,
1015
- thinking: undefined,
1016
- override: agent.override ?? { ...meta, base: cloneOverrideBase(agent) },
1017
- };
1037
+ const { thinking: _thinking, ...next } = agent;
1038
+ return { ...next, override: agent.override ?? { ...meta, base: cloneOverrideBase(agent) } };
1018
1039
  }
1019
1040
 
1020
1041
  function applyBuiltinOverrides(
@@ -1096,7 +1117,8 @@ function applyCustomAgentOverride(
1096
1117
  value: AgentConfig[K],
1097
1118
  ): void => {
1098
1119
  if (agentHasFrontmatterField(agent, ...frontmatterFields)) return;
1099
- mutable()[field] = value;
1120
+ const target = mutable();
1121
+ if (value === undefined) delete target[field]; else target[field] = value;
1100
1122
  anyFilled = true;
1101
1123
  };
1102
1124
 
@@ -1142,8 +1164,8 @@ function applyCustomAgentOverride(
1142
1164
  if (override.tools !== undefined && !agentHasFrontmatterField(agent, "tools")) {
1143
1165
  const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools);
1144
1166
  const target = mutable();
1145
- target.tools = tools;
1146
- target.mcpDirectTools = mcpDirectTools;
1167
+ if (tools === undefined) delete target.tools; else target.tools = tools;
1168
+ if (mcpDirectTools === undefined) delete target.mcpDirectTools; else target.mcpDirectTools = mcpDirectTools;
1147
1169
  anyFilled = true;
1148
1170
  }
1149
1171
  if (override.extensions !== undefined) {
@@ -1397,6 +1419,55 @@ function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
1397
1419
  return parts.some((part, index) => part === ".agents" && parts[index + 1] === "skills");
1398
1420
  }
1399
1421
 
1422
+ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string): AgentRunnerConfig | undefined {
1423
+ if (raw === undefined || !raw.trim()) return undefined;
1424
+ let parsed: unknown;
1425
+ try {
1426
+ parsed = parseYaml(raw);
1427
+ } catch (error) {
1428
+ throw new Error(`Agent '${agentName}' has invalid runner frontmatter: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1429
+ }
1430
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1431
+ throw new Error(`Agent '${agentName}' has invalid runner frontmatter; expected an object.`);
1432
+ }
1433
+ const runner = parsed as Record<string, unknown>;
1434
+ if (runner.type === "pi") {
1435
+ if (Object.keys(runner).some((key) => key !== "type")) throw new Error(`Agent '${agentName}' has invalid Pi runner frontmatter; only 'type' is supported.`);
1436
+ return { type: "pi" };
1437
+ }
1438
+ if (runner.type !== "external-cli") {
1439
+ throw new Error(`Agent '${agentName}' has invalid runner.type; expected 'pi' or 'external-cli'.`);
1440
+ }
1441
+ if (typeof runner.command !== "string" || !runner.command.trim()) {
1442
+ throw new Error(`Agent '${agentName}' external-cli runner requires a non-empty command string.`);
1443
+ }
1444
+ if (runner.args !== undefined && (!Array.isArray(runner.args) || runner.args.some((arg) => typeof arg !== "string"))) {
1445
+ throw new Error(`Agent '${agentName}' external-cli runner args must be an array of strings.`);
1446
+ }
1447
+ if (runner.promptDelivery !== undefined && runner.promptDelivery !== "stdin") {
1448
+ throw new Error(`Agent '${agentName}' external-cli runner promptDelivery must be 'stdin'.`);
1449
+ }
1450
+ const supported = new Set(["type", "command", "args", "promptDelivery"]);
1451
+ const unknown = Object.keys(runner).filter((key) => !supported.has(key));
1452
+ if (unknown.length > 0) throw new Error(`Agent '${agentName}' external-cli runner has unsupported fields: ${unknown.join(", ")}.`);
1453
+ const runnerArgs = Array.isArray(runner.args) ? runner.args.filter((arg): arg is string => typeof arg === "string") : undefined;
1454
+ return {
1455
+ type: "external-cli",
1456
+ command: runner.command.trim(),
1457
+ ...(runnerArgs?.length ? { args: runnerArgs } : {}),
1458
+ ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}),
1459
+ };
1460
+ }
1461
+
1462
+ function validateExternalRunnerProfile(frontmatter: Record<string, string>, agentName: string, runner: AgentRunnerConfig | undefined): void {
1463
+ if (runner?.type !== "external-cli") return;
1464
+ const unsupported = ["tools", "model", "fallbackModels", "thinking", "extensions", "subagentOnlyExtensions", "maxSubagentDepth", "completionGuard", "skills", "skill", "skillPath", "toolBudget", "permission", "permissions"]
1465
+ .filter((field) => frontmatter[field] !== undefined);
1466
+ if (unsupported.length > 0) {
1467
+ throw new Error(`Agent '${agentName}' uses runner.type='external-cli' and declares unsupported Pi-only fields: ${unsupported.join(", ")}.`);
1468
+ }
1469
+ }
1470
+
1400
1471
  function parseAgentAcceptanceFrontmatter(raw: string | undefined, agentName: string): AcceptanceInput | undefined {
1401
1472
  if (raw === undefined || !raw.trim()) return undefined;
1402
1473
  let parsed: unknown;
@@ -1437,6 +1508,8 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1437
1508
  const packageName = parsedPackage.packageName;
1438
1509
  const runtimeName = buildRuntimeName(localName, packageName);
1439
1510
 
1511
+ const runner = parseAgentRunnerFrontmatter(frontmatter.runner, localName);
1512
+ validateExternalRunnerProfile(frontmatter, localName, runner);
1440
1513
  const rawTools = parseFrontmatterList(frontmatter.tools);
1441
1514
  const parsedTools = splitToolList(rawTools);
1442
1515
  const tools = parsedTools.tools ?? [];
@@ -1504,6 +1577,13 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1504
1577
  }
1505
1578
 
1506
1579
  const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
1580
+ if (frontmatter.permission !== undefined && frontmatter.permissions !== undefined) {
1581
+ throw new Error(`Agent '${localName}' cannot declare both permission and permissions frontmatter.`);
1582
+ }
1583
+ const permissionSource = frontmatter.permissions ?? frontmatter.permission;
1584
+ const permissions = permissionSource?.trim()
1585
+ ? validatePermissionRules(parseYaml(permissionSource), `Agent '${localName}' permissions`)
1586
+ : undefined;
1507
1587
  let toolBudget: ToolBudgetConfig | undefined;
1508
1588
  if (frontmatter.toolBudget !== undefined && frontmatter.toolBudget.trim()) {
1509
1589
  const parsed = JSON.parse(frontmatter.toolBudget) as unknown;
@@ -1518,45 +1598,48 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1518
1598
  ? true
1519
1599
  : undefined;
1520
1600
 
1601
+ const maxSubagentDepth = Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0
1602
+ ? parsedMaxSubagentDepth
1603
+ : undefined;
1604
+ const memory = parseMemoryFrontmatter(frontmatter.memory);
1521
1605
  const agent: AgentConfig = {
1522
1606
  name: runtimeName,
1607
+ ...(runner !== undefined ? { runner } : {}),
1523
1608
  localName,
1524
- packageName,
1609
+ ...(packageName !== undefined ? { packageName } : {}),
1525
1610
  description: frontmatter.description,
1526
- aliases,
1527
- tools: rawTools !== undefined ? tools : undefined,
1528
- mcpDirectTools: mcpDirectTools.length > 0 ? mcpDirectTools : undefined,
1529
- model: frontmatter.model,
1530
- fallbackModels: fallbackModels && fallbackModels.length > 0 ? fallbackModels : undefined,
1531
- thinking: frontmatter.thinking === "false" ? false : frontmatter.thinking,
1611
+ ...(aliases !== undefined ? { aliases } : {}),
1612
+ ...(rawTools !== undefined ? { tools } : {}),
1613
+ ...(mcpDirectTools.length > 0 ? { mcpDirectTools } : {}),
1614
+ ...(frontmatter.model !== undefined ? { model: frontmatter.model } : {}),
1615
+ ...(fallbackModels?.length ? { fallbackModels } : {}),
1616
+ ...(frontmatter.thinking !== undefined ? { thinking: frontmatter.thinking === "false" ? false : frontmatter.thinking } : {}),
1532
1617
  systemPromptMode,
1533
1618
  inheritProjectContext,
1534
1619
  inheritSkills,
1535
- defaultContext,
1536
- defaultAsync,
1537
- defaultTimeoutMs,
1538
- defaultTurnBudget,
1539
- defaultAcceptance,
1540
- acceptanceRole,
1620
+ ...(defaultContext !== undefined ? { defaultContext } : {}),
1621
+ ...(defaultAsync !== undefined ? { defaultAsync } : {}),
1622
+ ...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
1623
+ ...(defaultTurnBudget !== undefined ? { defaultTurnBudget } : {}),
1624
+ ...(defaultAcceptance !== undefined ? { defaultAcceptance } : {}),
1625
+ ...(acceptanceRole !== undefined ? { acceptanceRole } : {}),
1541
1626
  systemPrompt: body,
1542
1627
  source,
1543
1628
  filePath,
1544
- skills: skills && skills.length > 0 ? skills : undefined,
1545
- skillPath: skillPath && skillPath.length > 0 ? skillPath : undefined,
1546
- extensions,
1547
- subagentOnlyExtensions,
1548
- output: frontmatter.output,
1549
- defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,
1629
+ ...(skills?.length ? { skills } : {}),
1630
+ ...(skillPath?.length ? { skillPath } : {}),
1631
+ ...(extensions !== undefined ? { extensions } : {}),
1632
+ ...(subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions } : {}),
1633
+ ...(frontmatter.output !== undefined ? { output: frontmatter.output } : {}),
1634
+ ...(defaultReads?.length ? { defaultReads } : {}),
1550
1635
  defaultProgress: frontmatter.defaultProgress === "true",
1551
1636
  interactive: frontmatter.interactive === "true",
1552
- maxSubagentDepth:
1553
- Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0
1554
- ? parsedMaxSubagentDepth
1555
- : undefined,
1556
- completionGuard,
1557
- toolBudget,
1558
- memory: parseMemoryFrontmatter(frontmatter.memory),
1559
- extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
1637
+ ...(maxSubagentDepth !== undefined ? { maxSubagentDepth } : {}),
1638
+ ...(completionGuard !== undefined ? { completionGuard } : {}),
1639
+ ...(toolBudget !== undefined ? { toolBudget } : {}),
1640
+ ...(permissions !== undefined ? { permissions } : {}),
1641
+ ...(memory !== undefined ? { memory } : {}),
1642
+ ...(Object.keys(extraFields).length > 0 ? { extraFields } : {}),
1560
1643
  };
1561
1644
  agentFrontmatterFields.set(agent, new Set(Object.keys(frontmatter)));
1562
1645
  agents.push(agent);
@@ -1696,7 +1779,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
1696
1779
  const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
1697
1780
  .filter((agent) => agent.disabled !== true);
1698
1781
 
1699
- return { agents, projectAgentsDir, modelScope };
1782
+ return { agents, projectAgentsDir, ...(modelScope !== undefined ? { modelScope } : {}) };
1700
1783
  }
1701
1784
 
1702
1785
  export function discoverAgentsAll(cwd: string): {
@@ -17,8 +17,10 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
17
17
  for (const line of configLines) {
18
18
  const match = line.match(/^([\w-]+):\s*(.*)$/);
19
19
  if (!match) continue;
20
- const key = match[1].trim().toLowerCase();
21
- const rawValue = match[2].trim();
20
+ const [keyValue, rawValueValue] = match.slice(1);
21
+ if (keyValue === undefined || rawValueValue === undefined) continue;
22
+ const key = keyValue.trim().toLowerCase();
23
+ const rawValue = rawValueValue.trim();
22
24
 
23
25
  if (key === "output") {
24
26
  if (rawValue === "false") step.output = false;
@@ -91,7 +93,8 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
91
93
  }
92
94
  const validation = validateToolBudgetConfig(parsed, `toolBudget for step '${agent}'`);
93
95
  if (validation.error) throw new Error(validation.error);
94
- step.toolBudget = parsed as ChainStepConfig["toolBudget"];
96
+ const toolBudget = parsed as ChainStepConfig["toolBudget"];
97
+ if (toolBudget !== undefined) step.toolBudget = toolBudget;
95
98
  }
96
99
  }
97
100
 
@@ -130,12 +133,12 @@ export function parseChain(content: string, source: AgentSource, filePath: strin
130
133
  return {
131
134
  name: buildRuntimeName(localName, packageName),
132
135
  localName,
133
- packageName,
136
+ ...(packageName !== undefined ? { packageName } : {}),
134
137
  description: frontmatter.description,
135
138
  source,
136
139
  filePath,
137
140
  steps,
138
- extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
141
+ ...(Object.keys(extraFields).length > 0 ? { extraFields } : {}),
139
142
  };
140
143
  }
141
144
 
@@ -213,12 +216,12 @@ export function parseJsonChain(content: string, source: AgentSource, filePath: s
213
216
  return {
214
217
  name: buildRuntimeName(input.name.trim(), parsedPackage.packageName),
215
218
  localName: input.name.trim(),
216
- packageName: parsedPackage.packageName,
219
+ ...(parsedPackage.packageName !== undefined ? { packageName: parsedPackage.packageName } : {}),
217
220
  description: input.description.trim(),
218
221
  source,
219
222
  filePath,
220
223
  steps: input.chain as ChainStepConfig[],
221
- extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
224
+ ...(Object.keys(extraFields).length > 0 ? { extraFields } : {}),
222
225
  };
223
226
  }
224
227
 
@@ -113,20 +113,22 @@ export function parseFrontmatter(content: string): { frontmatter: Record<string,
113
113
 
114
114
  const match = line.match(/^([\w-]+):\s*(.*)$/);
115
115
  if (match) {
116
- const rawValue = match[2].trim();
116
+ const [key, rawValueValue] = match.slice(1);
117
+ if (key === undefined || rawValueValue === undefined) continue;
118
+ const rawValue = rawValueValue.trim();
117
119
  const isQuoted = (rawValue.startsWith('"') && rawValue.endsWith('"')) || (rawValue.startsWith("'") && rawValue.endsWith("'"));
118
120
  const value = isQuoted ? rawValue.slice(1, -1) : rawValue;
119
121
  const isFolded = !isQuoted && (rawValue === ">" || rawValue === ">-");
120
122
 
121
123
  if (value === "" || isFolded) {
122
124
  // Key with empty value or folded block indicator — defer storing until we see indent
123
- currentKey = match[1];
125
+ currentKey = key;
124
126
  currentBlockLines = [];
125
127
  currentIndent = indent;
126
128
  currentFolded = isFolded;
127
129
  } else {
128
130
  // Simple key: value
129
- frontmatter[match[1]] = value;
131
+ frontmatter[key] = value;
130
132
  }
131
133
  }
132
134
  // Lines that don't match a key pattern (e.g., comments, empty lines) are ignored
@@ -9,7 +9,7 @@ function normalizePackageName(value: string | undefined): string | undefined {
9
9
  }
10
10
 
11
11
  export function parsePackageName(value: unknown, label = "package"): { packageName?: string; error?: string } {
12
- if (value === undefined || value === false || value === "") return { packageName: undefined };
12
+ if (value === undefined || value === false || value === "") return {};
13
13
  if (typeof value !== "string") return { error: `${label} must be a string or false when provided.` };
14
14
  const packageName = normalizePackageName(value);
15
15
  if (!packageName || !IDENTIFIER_PATTERN.test(packageName)) return { error: `${label} is invalid after sanitization.` };
@@ -141,14 +141,17 @@ export function recommendProactiveSkillSubagents(input: {
141
141
 
142
142
  return [...counts.entries()]
143
143
  .filter(([skill, sources]) => sources.size >= config.minReferences && (!availableByName || availableByName.has(skill)))
144
- .map(([skill, sources]) => ({
145
- skill,
146
- agent,
147
- references: sources.size,
148
- sources: [...sources].sort((a, b) => a.localeCompare(b)),
149
- description: availableByName?.get(skill)?.description,
150
- reason: `referenced by ${sources.size} configured agents/chains`,
151
- }))
144
+ .map(([skill, sources]) => {
145
+ const description = availableByName?.get(skill)?.description;
146
+ return {
147
+ skill,
148
+ agent,
149
+ references: sources.size,
150
+ sources: [...sources].sort((a, b) => a.localeCompare(b)),
151
+ ...(description !== undefined ? { description } : {}),
152
+ reason: `referenced by ${sources.size} configured agents/chains`,
153
+ };
154
+ })
152
155
  .sort((a, b) => b.references - a.references || a.skill.localeCompare(b.skill))
153
156
  .slice(0, config.maxRecommendations);
154
157
  }
@@ -184,8 +187,8 @@ export function buildProactiveSkillSubagentRecommendationLines(input: {
184
187
  }
185
188
  return formatProactiveSkillSubagentRecommendations(recommendProactiveSkillSubagents({
186
189
  agents: input.agents,
187
- chains: input.chains,
190
+ ...(input.chains !== undefined ? { chains: input.chains } : {}),
188
191
  availableSkills,
189
- config: input.config,
192
+ ...(input.config !== undefined ? { config: input.config } : {}),
190
193
  }));
191
194
  }
@@ -125,8 +125,23 @@ function getGlobalNpmRoot(): string | null {
125
125
  const offline = process.env.PI_OFFLINE?.toLowerCase();
126
126
  if (offline === "1" || offline === "true" || offline === "yes") return null;
127
127
  if (cachedGlobalNpmRoot !== null) return cachedGlobalNpmRoot;
128
+
129
+ const windowsGlobalRoot = process.platform === "win32" && process.env.APPDATA
130
+ ? path.join(process.env.APPDATA, "npm", "node_modules")
131
+ : undefined;
132
+ if (windowsGlobalRoot) {
133
+ try {
134
+ if (fs.statSync(windowsGlobalRoot).isDirectory()) {
135
+ cachedGlobalNpmRoot = fs.realpathSync(windowsGlobalRoot);
136
+ return cachedGlobalNpmRoot;
137
+ }
138
+ } catch {
139
+ // Fall through if the directory disappears while resolving it.
140
+ }
141
+ }
142
+
128
143
  try {
129
- cachedGlobalNpmRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
144
+ cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim());
130
145
  return cachedGlobalNpmRoot;
131
146
  } catch {
132
147
  // Global npm root is optional in constrained environments.
@@ -410,15 +425,17 @@ function collectFilesystemSkills(cwd: string, agentDir: string, skillPaths: Skil
410
425
  const resolvedFile = path.resolve(filePath);
411
426
  if (!fs.existsSync(resolvedFile)) return;
412
427
  const source = inferSkillSource(resolvedFile, cwd, agentDir, sourceHint);
428
+ const description = maybeReadSkillDescription(resolvedFile);
413
429
  const existingIndex = seen.get(resolvedFile);
414
430
  if (existingIndex !== undefined) {
415
431
  const existing = entries[existingIndex];
416
432
  if (existing && (SOURCE_PRIORITY[source] ?? 0) > (SOURCE_PRIORITY[existing.source] ?? 0)) {
433
+ const { description: _description, ...existingWithoutDescription } = existing;
417
434
  entries[existingIndex] = {
418
- ...existing,
435
+ ...existingWithoutDescription,
419
436
  name,
420
437
  source,
421
- description: maybeReadSkillDescription(resolvedFile),
438
+ ...(description !== undefined ? { description } : {}),
422
439
  };
423
440
  }
424
441
  return;
@@ -428,7 +445,7 @@ function collectFilesystemSkills(cwd: string, agentDir: string, skillPaths: Skil
428
445
  name,
429
446
  filePath: resolvedFile,
430
447
  source,
431
- description: maybeReadSkillDescription(resolvedFile),
448
+ ...(description !== undefined ? { description } : {}),
432
449
  order: order++,
433
450
  });
434
451
  };
@@ -592,7 +609,7 @@ function readSkill(
592
609
  name: skillName,
593
610
  path: skillPath,
594
611
  content,
595
- description,
612
+ ...(description !== undefined ? { description } : {}),
596
613
  source,
597
614
  };
598
615
 
@@ -733,7 +750,7 @@ export function discoverAvailableSkills(cwd: string): Array<{
733
750
  .map((s) => ({
734
751
  name: s.name,
735
752
  source: s.source,
736
- description: s.description,
753
+ ...(s.description !== undefined ? { description: s.description } : {}),
737
754
  }))
738
755
  .sort((a, b) => a.name.localeCompare(b.name));
739
756
  }
@@ -0,0 +1,4 @@
1
+ export {
2
+ requestAsyncStop,
3
+ type StopRequest,
4
+ } from "../runs/background/control-channel.ts";