pi-ui-extend 1.0.41 → 1.0.45

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 (236) hide show
  1. package/README.md +50 -13
  2. package/dist/app/app.d.ts +13 -0
  3. package/dist/app/app.js +158 -8
  4. package/dist/app/cli/install.js +2 -2
  5. package/dist/app/commands/command-controller.d.ts +4 -0
  6. package/dist/app/commands/command-controller.js +11 -0
  7. package/dist/app/commands/command-git-actions.d.ts +25 -0
  8. package/dist/app/commands/command-git-actions.js +381 -0
  9. package/dist/app/commands/command-host.d.ts +4 -0
  10. package/dist/app/commands/command-host.js +5 -2
  11. package/dist/app/commands/command-model-actions.d.ts +1 -0
  12. package/dist/app/commands/command-model-actions.js +57 -35
  13. package/dist/app/commands/command-navigation-actions.d.ts +1 -0
  14. package/dist/app/commands/command-navigation-actions.js +16 -3
  15. package/dist/app/commands/command-registry.d.ts +2 -0
  16. package/dist/app/commands/command-registry.js +15 -1
  17. package/dist/app/commands/command-session-actions.js +9 -3
  18. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  19. package/dist/app/commands/reload-context-inventory.js +70 -0
  20. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  21. package/dist/app/extensions/extension-actions-controller.js +6 -0
  22. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  23. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  24. package/dist/app/input/autocomplete-controller.js +61 -34
  25. package/dist/app/input/input-action-controller.d.ts +2 -0
  26. package/dist/app/input/input-action-controller.js +8 -1
  27. package/dist/app/input/input-controller.d.ts +2 -1
  28. package/dist/app/input/input-controller.js +7 -2
  29. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  30. package/dist/app/input/voice-controller.d.ts +41 -45
  31. package/dist/app/input/voice-controller.js +351 -419
  32. package/dist/app/model/model-usage-controller.js +17 -7
  33. package/dist/app/model/model-usage-status.d.ts +4 -1
  34. package/dist/app/model/model-usage-status.js +270 -46
  35. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  36. package/dist/app/popup/menu-items-controller.js +37 -21
  37. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  38. package/dist/app/popup/popup-action-controller.js +77 -24
  39. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  40. package/dist/app/popup/popup-menu-controller.js +239 -69
  41. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  42. package/dist/app/rendering/dcp-stats.js +40 -425
  43. package/dist/app/rendering/editor-panels.js +10 -4
  44. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  45. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  46. package/dist/app/rendering/render-controller.js +23 -2
  47. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  48. package/dist/app/rendering/status-line-renderer.js +41 -5
  49. package/dist/app/rendering/tab-line-renderer.js +26 -20
  50. package/dist/app/runtime.d.ts +12 -1
  51. package/dist/app/runtime.js +120 -12
  52. package/dist/app/screen/mouse-controller.d.ts +2 -0
  53. package/dist/app/screen/mouse-controller.js +17 -7
  54. package/dist/app/screen/status-controller.d.ts +4 -0
  55. package/dist/app/screen/status-controller.js +5 -0
  56. package/dist/app/session/lazy-session-manager.js +34 -0
  57. package/dist/app/session/session-event-controller.d.ts +1 -0
  58. package/dist/app/session/session-event-controller.js +10 -1
  59. package/dist/app/session/session-history.d.ts +1 -0
  60. package/dist/app/session/session-history.js +12 -1
  61. package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
  62. package/dist/app/session/session-lifecycle-controller.js +16 -1
  63. package/dist/app/session/tabs-controller.d.ts +26 -1
  64. package/dist/app/session/tabs-controller.js +454 -147
  65. package/dist/app/subagents/subagents-files.js +60 -1
  66. package/dist/app/subagents/subagents-model.d.ts +1 -0
  67. package/dist/app/subagents/subagents-model.js +18 -2
  68. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  69. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  70. package/dist/app/types.d.ts +16 -1
  71. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  72. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  73. package/dist/app/workspace/workspace-undo.js +1 -0
  74. package/dist/bundled-extensions/question/index.js +9 -1
  75. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  76. package/dist/bundled-extensions/question/remote.js +33 -0
  77. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  78. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  79. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  80. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  81. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  82. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  83. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  84. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  85. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  86. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  87. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  88. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  89. package/dist/config.d.ts +21 -2
  90. package/dist/config.js +159 -32
  91. package/dist/default-pix-config.js +25 -5
  92. package/dist/schemas/index.d.ts +1 -0
  93. package/dist/schemas/index.js +1 -0
  94. package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
  95. package/dist/schemas/pi-tools-suite-schema.js +54 -83
  96. package/dist/schemas/pix-schema.d.ts +19 -2
  97. package/dist/schemas/pix-schema.js +47 -5
  98. package/dist/schemas/tasks-schema.d.ts +18 -0
  99. package/dist/schemas/tasks-schema.js +41 -0
  100. package/docs/concurrency.md +9 -1
  101. package/docs/desktop-mvp.md +22 -6
  102. package/docs/desktop-task-manager.md +148 -84
  103. package/docs/release.md +30 -6
  104. package/external/pi-tools-suite/README.md +197 -84
  105. package/external/pi-tools-suite/docs/evals.md +1 -1
  106. package/external/pi-tools-suite/docs/session-recovery.md +47 -14
  107. package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
  108. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  109. package/external/pi-tools-suite/package.json +6 -1
  110. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  111. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  112. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  113. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  114. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  115. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  116. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  117. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  119. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  127. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  128. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  129. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  130. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  131. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  132. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  133. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  134. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  135. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  136. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  137. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  138. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  139. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  140. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  141. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  142. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  143. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  144. package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
  145. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  146. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  147. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  148. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
  149. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  150. package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
  151. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  152. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  153. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
  154. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
  155. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  156. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  157. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  158. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  159. package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
  160. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
  161. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
  162. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  163. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  164. package/external/pi-tools-suite/src/config.ts +55 -1
  165. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  166. package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
  167. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  168. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
  169. package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
  170. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
  171. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  172. package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
  173. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  174. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  175. package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
  176. package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
  177. package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
  178. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  179. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  180. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  181. package/external/pi-tools-suite/src/dcp/config.ts +114 -17
  182. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  183. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  184. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  185. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  186. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  187. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  188. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  189. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  190. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
  191. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  192. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  193. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  194. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  195. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  196. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  197. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  198. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  199. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
  200. package/external/pi-tools-suite/src/index.ts +7 -1
  201. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  202. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  203. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  204. package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
  205. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  206. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  207. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  208. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  209. package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
  210. package/package.json +10 -9
  211. package/schemas/pi-tools-suite.json +466 -287
  212. package/schemas/pix.json +129 -11
  213. package/schemas/tasks.json +131 -0
  214. package/skills/simplify/SKILL.md +33 -5
  215. package/docs/desktop-markdown-media.md +0 -77
  216. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
  217. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  218. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  219. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  220. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  221. package/skills/playwright-cli/SKILL.md +0 -420
  222. package/skills/playwright-cli/references/element-attributes.md +0 -23
  223. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  224. package/skills/playwright-cli/references/request-mocking.md +0 -87
  225. package/skills/playwright-cli/references/running-code.md +0 -241
  226. package/skills/playwright-cli/references/session-management.md +0 -273
  227. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  228. package/skills/playwright-cli/references/storage-state.md +0 -290
  229. package/skills/playwright-cli/references/test-generation.md +0 -142
  230. package/skills/playwright-cli/references/tracing.md +0 -154
  231. package/skills/playwright-cli/references/video-recording.md +0 -147
  232. package/skills/spec-lite/SKILL.md +0 -140
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  234. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  235. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  236. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -1,17 +1,16 @@
1
1
  import * as fs from "node:fs";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
4
3
  import { fileURLToPath } from "node:url";
5
- import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
6
- import { readAgentDefinitionsFromDir, readProjectAgentDefinitions, type AgentDefinition } from "./agents-dir.js";
7
- import { ensurePiToolsSuiteUserConfig, getPiToolsSuiteUserConfigPath } from "../../config.js";
4
+ import { parse as parseJsonc } from "jsonc-parser";
5
+ import { projectAgentsDir, readAgentDefinitionsFromDir, readProjectAgentDefinitions, type AgentDefinition } from "./agents-dir.js";
6
+ import { LEGACY_BROWSER_QA_TYPE, UI_QA_TYPE } from "./browser-qa.js";
8
7
  import type { AgentTask, RetryConfig } from "./types.js";
9
8
 
10
9
  export interface ModelByParentEntry {
11
10
  /** Model ref to use when the parent model matches the entry's pattern. */
12
11
  model: string;
13
12
  /** Ordered fallbacks used when this entry's model hits quota/rate limits; replaces the normal fallback chain. */
14
- fallbackModels?: string[];
13
+ fallbackModels: string[];
15
14
  }
16
15
 
17
16
  export interface SubagentTypeConfig {
@@ -31,15 +30,24 @@ export interface SubagentTypeConfig {
31
30
  /**
32
31
  * Parent-model-aware model selection. Keys are glob model refs (e.g. "zai/*")
33
32
  * matched against the current parent model; the first matching key wins.
34
- * Values may be a model ref string or { model, fallbackModels? }.
33
+ * Raw values may be a model ref string or { model, fallbackModels? };
34
+ * normalized entries always carry an explicit fallbackModels array.
35
35
  * Ordinary roles: explicit task/forced model and preset models take priority.
36
36
  * Oracle alone keeps parent-aware selection ahead of presets.
37
37
  */
38
38
  modelByParent?: Record<string, ModelByParentEntry>;
39
+ /**
40
+ * Optional parent-model allow-list. When present, this role is exposed only
41
+ * when the current parent model matches at least one pattern.
42
+ */
43
+ forParentModels?: string[];
44
+ /**
45
+ * Optional parent-model deny-list. A match hides the role even when
46
+ * forParentModels also matches.
47
+ */
48
+ notForParentModels?: string[];
39
49
  thinking?: string;
40
50
  tools?: string[];
41
- /** Explicit skill files loaded after disabling normal skill discovery. */
42
- isolatedSkills?: string[];
43
51
  extraArgs?: string[];
44
52
  /** Extra prompt text appended after the generated or overridden prompt. */
45
53
  promptAppend?: string;
@@ -125,18 +133,9 @@ export interface SubagentConfig {
125
133
  timeoutMs?: number;
126
134
  }
127
135
 
128
- export interface CopySubagentConfigSampleResult {
129
- copied: boolean;
130
- targetPath: string;
131
- samplePath: string;
132
- existingFiles: string[];
133
- }
134
-
135
136
  export interface ResolvedAgentTaskConfig {
136
137
  task: AgentTask;
137
138
  extraArgs: string[];
138
- /** Explicit skill files loaded with normal skill discovery disabled. */
139
- isolatedSkills: string[];
140
139
  /** Ordered model fallbacks for the resolved model. Current-process exhausted models are skipped before spawning. */
141
140
  fallbackModels: string[];
142
141
  profile?: SubagentTypeConfig;
@@ -192,92 +191,73 @@ export const DEFAULT_ROUTING_CONFIG: ResolvedSubagentRoutingConfig = {
192
191
  fallbackModels: ["openai-codex/gpt-5.6-luna"],
193
192
  maxTaskChars: 1200,
194
193
  maxTokens: 512,
195
- maxRetries: 3,
196
- timeoutMs: 10_000,
194
+ maxRetries: 1,
195
+ timeoutMs: 12_000,
197
196
  debug: false,
198
197
  };
199
198
 
200
199
  const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "agents");
200
+ const BUILTIN_PRESETS_FILE = path.join(BUILTIN_AGENTS_DIR, "presets.jsonc");
201
+ const DEFAULT_BLIND_MODEL_PATTERNS = [
202
+ "zai/glm-4.5*", "glm-4.5*", "*/glm-4.5*",
203
+ "zai/glm-5-turbo*", "glm-5-turbo*", "*/glm-5-turbo*",
204
+ "zai/glm-5.3", "glm-5.3", "*/glm-5.3",
205
+ ];
201
206
 
202
207
  const BUILTIN_CONFIG: SubagentConfig = {
203
208
  defaultType: "research",
204
209
  maxConcurrent: DEFAULT_MAX_CONCURRENT,
210
+ maxResultBytes: 100_000,
205
211
  routing: { ...DEFAULT_ROUTING_CONFIG },
212
+ vision: { blindModelPatterns: DEFAULT_BLIND_MODEL_PATTERNS },
213
+ presets: readPresetConfigFile(BUILTIN_PRESETS_FILE).presets,
206
214
  types: normalizeAgentDefinitions(readAgentDefinitionsFromDir(BUILTIN_AGENTS_DIR)),
207
215
  };
208
216
 
209
217
  export function loadSubagentConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): SubagentConfig {
210
218
  const config = cloneConfig(BUILTIN_CONFIG);
211
- if (!explicitSubagentConfigPath(cwd, env)) {
212
- for (const file of piToolsSuiteConfigFiles(cwd, env)) {
213
- mergeConfig(config, readPiToolsSuiteSubagentConfig(file));
214
- }
215
- // Project-local agent definitions (.pi/agents/*.md) win name collisions
216
- // over user/project JSONC config; loaded fresh on every call so edits and
217
- // /reload are respected without a restart.
218
- mergeConfig(config, projectAgentTypes(cwd));
219
- }
220
- for (const file of configFiles(cwd, env)) {
221
- mergeConfig(config, readConfigFile(file));
222
- }
219
+ mergeConfig(config, projectPresetConfig(cwd));
220
+ // Project-local agent definitions (.pi/agents/*.md) are the only project
221
+ // source of role/profile configuration and are loaded fresh on every call.
222
+ mergeConfig(config, projectAgentTypes(cwd));
223
223
  applyEnvModelOverrides(config, env);
224
224
  applyEnvRoutingOverrides(config, env);
225
+ applyEnvRuntimeOverrides(config, env);
225
226
  return config;
226
227
  }
227
228
 
228
- export function configFiles(cwd: string, env: NodeJS.ProcessEnv = process.env): string[] {
229
- const explicit = explicitSubagentConfigPath(cwd, env);
230
- if (explicit) {
231
- if (!fs.existsSync(explicit)) throw new Error(`Subagent config not found: ${explicit}`);
232
- return [explicit];
233
- }
234
-
235
- return existingSubagentConfigFiles(cwd, env);
236
- }
237
-
238
- function explicitSubagentConfigPath(cwd: string, env: NodeJS.ProcessEnv): string | undefined {
239
- const explicit = trimString(env.ASYNC_SUBAGENTS_CONFIG || env.PI_SUBAGENTS_CONFIG);
240
- return explicit ? path.resolve(cwd, expandHome(explicit)) : undefined;
241
- }
242
-
243
- export function getDefaultSubagentConfigPath(): string {
244
- return getPiToolsSuiteUserConfigPath();
245
- }
246
-
247
- export function getSubagentConfigSamplePath(): string {
248
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "async-subagents.sample.jsonc");
249
- }
250
-
251
229
  export function getBuiltinSubagentDefinitionsDir(): string {
252
230
  return BUILTIN_AGENTS_DIR;
253
231
  }
254
232
 
255
- export function getSubagentConfigInitTargetPath(cwd: string, env: NodeJS.ProcessEnv = process.env): string {
256
- return explicitSubagentConfigPath(cwd, env) ?? getDefaultSubagentConfigPath();
257
- }
258
-
259
- export function existingSubagentConfigFiles(cwd: string, env: NodeJS.ProcessEnv = process.env): string[] {
260
- const explicit = explicitSubagentConfigPath(cwd, env);
261
- if (explicit) return fs.existsSync(explicit) ? [explicit] : [];
262
- return piToolsSuiteConfigFiles(cwd, env).filter(hasPiToolsSuiteSubagentConfig);
233
+ export function getBuiltinSubagentPresetsPath(): string {
234
+ return BUILTIN_PRESETS_FILE;
263
235
  }
264
236
 
265
- function piToolsSuiteConfigFiles(cwd: string, env: NodeJS.ProcessEnv): string[] {
266
- return [
267
- getPiToolsSuiteUserConfigPath(),
268
- env.PI_CONFIG_DIR ? path.join(env.PI_CONFIG_DIR, "pi-tools-suite.jsonc") : undefined,
269
- findProjectPiToolsSuiteConfig(cwd),
270
- ].filter((file): file is string => typeof file === "string" && fs.existsSync(file));
237
+ export function getProjectSubagentPresetsPath(cwd: string): string {
238
+ return path.join(projectAgentsDir(cwd) ?? path.join(path.resolve(cwd), ".pi", "agents"), "presets.jsonc");
271
239
  }
272
240
 
273
241
  /** Normalize `.pi/agents/*.md` definitions through the shared type-profile path. */
274
242
  function projectAgentTypes(cwd: string): Partial<SubagentConfig> {
275
243
  const definitions = readProjectAgentDefinitions(cwd);
244
+ const legacyUiQa = definitions[LEGACY_BROWSER_QA_TYPE];
245
+ if (legacyUiQa) {
246
+ if (!definitions[UI_QA_TYPE]) definitions[UI_QA_TYPE] = legacyUiQa;
247
+ delete definitions[LEGACY_BROWSER_QA_TYPE];
248
+ }
276
249
  const types = normalizeAgentDefinitions(definitions);
277
250
  if (Object.keys(types).length === 0) return {};
278
251
  return { types };
279
252
  }
280
253
 
254
+ function projectPresetConfig(cwd: string): Partial<SubagentConfig> {
255
+ const dir = projectAgentsDir(cwd);
256
+ if (!dir) return {};
257
+ const file = path.join(dir, "presets.jsonc");
258
+ return fs.existsSync(file) ? readPresetConfigFile(file) : {};
259
+ }
260
+
281
261
  function normalizeAgentDefinitions(definitions: Record<string, AgentDefinition>): Record<string, SubagentTypeConfig> {
282
262
  const types: Record<string, SubagentTypeConfig> = {};
283
263
  for (const [name, definition] of Object.entries(definitions)) {
@@ -286,42 +266,6 @@ function normalizeAgentDefinitions(definitions: Record<string, AgentDefinition>)
286
266
  return types;
287
267
  }
288
268
 
289
- function findProjectPiToolsSuiteConfig(startDir: string): string | undefined {
290
- let dir = path.resolve(startDir);
291
- const root = path.parse(dir).root;
292
- while (true) {
293
- const candidate = path.join(dir, ".pi", "pi-tools-suite.jsonc");
294
- if (fs.existsSync(candidate)) return candidate;
295
- if (dir === root) return undefined;
296
- const parent = path.dirname(dir);
297
- if (parent === dir) return undefined;
298
- dir = parent;
299
- }
300
- }
301
-
302
- export function copySubagentConfigSample(cwd: string, env: NodeJS.ProcessEnv = process.env): CopySubagentConfigSampleResult {
303
- const samplePath = getSubagentConfigSamplePath();
304
- const existingFiles = existingSubagentConfigFiles(cwd, env);
305
- if (existingFiles.length > 0) return { copied: false, targetPath: existingFiles[0], samplePath, existingFiles };
306
-
307
- const explicit = explicitSubagentConfigPath(cwd, env);
308
- const targetPath = getSubagentConfigInitTargetPath(cwd, env);
309
- if (explicit) {
310
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
311
- try {
312
- fs.copyFileSync(samplePath, targetPath, fs.constants.COPYFILE_EXCL);
313
- } catch (error) {
314
- if (isNodeError(error) && error.code === "EEXIST") {
315
- return { copied: false, targetPath, samplePath, existingFiles: [targetPath] };
316
- }
317
- throw error;
318
- }
319
- } else {
320
- writePiToolsSuiteSubagentConfig(targetPath, readConfigFile(samplePath));
321
- }
322
- return { copied: true, targetPath, samplePath, existingFiles: [] };
323
- }
324
-
325
269
  export function resolveAgentTaskConfig(
326
270
  task: AgentTask,
327
271
  config: SubagentConfig,
@@ -395,7 +339,6 @@ export function resolveAgentTaskConfig(
395
339
  return {
396
340
  profile,
397
341
  extraArgs,
398
- isolatedSkills: arrayOfStrings(profile?.isolatedSkills) ?? [],
399
342
  fallbackModels,
400
343
  retry: resolveRetryConfig(config.retry, profile?.retry),
401
344
  maxResultBytes: profile?.maxResultBytes ?? config.maxResultBytes,
@@ -418,7 +361,7 @@ export function resolveSubagentRoutingConfig(config: SubagentConfig): ResolvedSu
418
361
  }
419
362
 
420
363
  export function defaultSubagentType(config: SubagentConfig): string | undefined {
421
- const configured = trimString(config.defaultType);
364
+ const configured = normalizeSubagentType(config.defaultType, config);
422
365
  if (configured) return Object.prototype.hasOwnProperty.call(config.types, configured) ? configured : undefined;
423
366
  return Object.keys(config.types).find((name) => trimString(name));
424
367
  }
@@ -445,10 +388,11 @@ export function shouldForceCurrentSubagentModel(env: NodeJS.ProcessEnv = process
445
388
  }
446
389
 
447
390
  export function currentModelRef(model: unknown): string | undefined {
391
+ if (typeof model === "string") return trimString(model);
448
392
  if (!isRecord(model)) return undefined;
449
- const id = trimString(model.id);
393
+ const id = trimString(model.modelId) || trimString(model.id) || trimString(model.model) || trimString(model.name);
450
394
  if (!id) return undefined;
451
- const provider = trimString(model.provider);
395
+ const provider = trimString(model.provider) || trimString(model.providerId);
452
396
  return provider && !id.includes("/") ? `${provider}/${id}` : id;
453
397
  }
454
398
 
@@ -457,93 +401,76 @@ export function isBlindModelRef(modelRef: string | undefined, config: SubagentCo
457
401
  return matchesAnyModelPattern(modelRef, config.vision?.blindModelPatterns ?? []);
458
402
  }
459
403
 
404
+ /** Whether one role is available to the current parent model. Deny wins. */
405
+ export function isSubagentTypeAvailableForParent(
406
+ profile: SubagentTypeConfig,
407
+ parentModelRef: string | undefined,
408
+ ): boolean {
409
+ const included = profile.forParentModels;
410
+ if (included !== undefined) {
411
+ if (!parentModelRef || !matchesAnyModelPattern(parentModelRef, included)) return false;
412
+ }
413
+ const excluded = profile.notForParentModels;
414
+ if (parentModelRef && excluded !== undefined && matchesAnyModelPattern(parentModelRef, excluded)) return false;
415
+ return true;
416
+ }
417
+
418
+ /**
419
+ * Return the effective config visible to a particular parent model. Only the
420
+ * role catalog is filtered; presets, routing policy, retry defaults, etc. are
421
+ * preserved unchanged.
422
+ */
423
+ export function filterSubagentConfigForParentModel(
424
+ config: SubagentConfig,
425
+ parentModelRef: string | undefined,
426
+ ): SubagentConfig {
427
+ const types = Object.fromEntries(
428
+ Object.entries(config.types).filter(([, profile]) => isSubagentTypeAvailableForParent(profile, parentModelRef)),
429
+ );
430
+ return Object.keys(types).length === Object.keys(config.types).length ? config : { ...config, types };
431
+ }
432
+
460
433
  export function selectSubagentType(task: AgentTask, config: SubagentConfig): string | undefined {
461
- const explicit = trimString(task.subagentType);
434
+ const explicit = normalizeSubagentType(task.subagentType, config);
462
435
  if (explicit) return explicit;
463
436
  return defaultSubagentType(config);
464
437
  }
465
438
 
466
- function readConfigFile(file: string): Partial<SubagentConfig> {
467
- const raw = fs.readFileSync(file, "utf-8");
468
- const parsed = JSON.parse(stripJsonComments(raw)) as unknown;
469
- if (!isRecord(parsed)) throw new Error(`Subagent config must be an object: ${file}`);
470
- return normalizeConfig(parsed, file);
439
+ /** Preserve explicit legacy browser-qa calls after the built-in role was broadened to ui-qa. */
440
+ export function normalizeSubagentType(value: string | undefined, config: SubagentConfig): string | undefined {
441
+ const requested = trimString(value);
442
+ if (!requested) return undefined;
443
+ if (Object.prototype.hasOwnProperty.call(config.types, requested)) return requested;
444
+ if (requested === LEGACY_BROWSER_QA_TYPE && Object.prototype.hasOwnProperty.call(config.types, UI_QA_TYPE)) return UI_QA_TYPE;
445
+ return requested;
471
446
  }
472
447
 
473
- function readPiToolsSuiteSubagentConfig(file: string): Partial<SubagentConfig> {
448
+ function readPresetConfigFile(file: string): Partial<SubagentConfig> {
474
449
  const raw = fs.readFileSync(file, "utf-8");
475
450
  const parsed = parseJsonc(raw) as unknown;
476
- if (!isRecord(parsed)) return {};
477
- const section = parsed.asyncSubagents ?? parsed["async-subagents"] ?? parsed.subagents;
478
- return isRecord(section) ? normalizeConfig(section, file) : {};
479
- }
480
-
481
- function hasPiToolsSuiteSubagentConfig(file: string): boolean {
482
- const raw = fs.readFileSync(file, "utf-8");
483
- const parsed = parseJsonc(raw) as unknown;
484
- if (!isRecord(parsed)) return false;
485
- return isRecord(parsed.asyncSubagents ?? parsed["async-subagents"] ?? parsed.subagents);
486
- }
487
-
488
- function writePiToolsSuiteSubagentConfig(file: string, config: Partial<SubagentConfig>): void {
489
- ensurePiToolsSuiteUserConfig();
490
- const original = fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "{}\n";
491
- const edits = modify(original, ["asyncSubagents"], config, {
492
- formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" },
493
- });
494
- const updated = applyEdits(original, edits);
495
- fs.mkdirSync(path.dirname(file), { recursive: true });
496
- fs.writeFileSync(file, updated.endsWith("\n") ? updated : `${updated}\n`, "utf-8");
497
- }
498
-
499
- function normalizeConfig(value: Record<string, unknown>, file: string): Partial<SubagentConfig> {
500
- const output: Partial<SubagentConfig> = {};
501
- if (typeof value.defaultType === "string") output.defaultType = value.defaultType.trim();
502
- if (typeof value.routing === "boolean") output.routing = { enabled: value.routing };
503
- else if (isRecord(value.routing)) output.routing = normalizeRoutingConfig(value.routing);
504
- const vision = normalizeVisionConfig(value);
505
- if (vision) output.vision = vision;
506
- const maxConcurrent = finiteNumber(value.maxConcurrent);
507
- if (maxConcurrent !== undefined) output.maxConcurrent = Math.max(0, Math.round(maxConcurrent));
508
- const maxResultBytes = finiteNumber(value.maxResultBytes);
509
- if (maxResultBytes !== undefined) output.maxResultBytes = Math.max(0, Math.round(maxResultBytes));
510
- const timeoutMs = positiveMilliseconds(value.timeoutMs);
511
- if (timeoutMs !== undefined) output.timeoutMs = timeoutMs;
512
- if (isRecord(value.retry)) output.retry = normalizeRetryConfig(value.retry);
513
- if (value.presets !== undefined) {
514
- if (!isRecord(value.presets)) throw new Error(`Subagent config "presets" must be an object: ${file}`);
515
- const presets: Record<string, SubagentPreset> = {};
516
- for (const [name, rawPreset] of Object.entries(value.presets)) {
517
- if (!isRecord(rawPreset)) throw new Error(`Subagent preset "${name}" must be an object: ${file}`);
518
- presets[name] = {
519
- description: trimString(rawPreset.description),
520
- models: normalizeModels(rawPreset.models, `preset "${name}"`, file),
521
- model: trimString(rawPreset.model),
522
- fallbackModels: modelList(rawPreset.fallbackModels, rawPreset.fallbackModel),
523
- thinking: trimString(rawPreset.thinking),
524
- extraArgs: arrayOfStrings(rawPreset.extraArgs),
525
- timeoutMs: positiveMilliseconds(rawPreset.timeoutMs),
526
- types: normalizePresetTypeOverrides(rawPreset.types, file, name),
527
- };
528
- }
529
- output.presets = presets;
530
- }
531
- if (value.types === undefined) return output;
532
- if (!isRecord(value.types)) throw new Error(`Subagent config "types" must be an object: ${file}`);
533
-
534
- const types: Record<string, SubagentTypeConfig> = {};
535
- for (const [name, rawProfile] of Object.entries(value.types)) {
536
- if (!isRecord(rawProfile)) throw new Error(`Subagent type "${name}" must be an object: ${file}`);
537
- types[name] = normalizeSubagentTypeProfile(rawProfile, name, file);
451
+ if (!isRecord(parsed)) throw new Error(`Subagent presets file must contain an object: ${file}`);
452
+ return { presets: normalizePresetMap(parsed, file, "preset file") };
453
+ }
454
+
455
+ function normalizePresetMap(value: unknown, file: string, owner: string): Record<string, SubagentPreset> {
456
+ if (!isRecord(value)) throw new Error(`Subagent ${owner} must be an object: ${file}`);
457
+ const presets: Record<string, SubagentPreset> = {};
458
+ for (const [name, rawPreset] of Object.entries(value)) {
459
+ if (!isRecord(rawPreset)) throw new Error(`Subagent preset "${name}" must be an object: ${file}`);
460
+ const models = normalizeModels(rawPreset.models, `preset "${name}"`, file);
461
+ if (models === undefined) throw new Error(`Subagent preset "${name}" must define models: ${file}`);
462
+ presets[name] = {
463
+ description: trimString(rawPreset.description),
464
+ models,
465
+ };
538
466
  }
539
- output.types = types;
540
- return output;
467
+ return presets;
541
468
  }
542
469
 
543
470
  /**
544
471
  * Normalize one raw sub-agent type profile (`SubagentTypeConfig` shape) from
545
- * any source (JSONC config files or project agent definitions). Shared so all
546
- * sources apply identical field validation and trimming.
472
+ * an agent Markdown definition. Bundled and project files share the same
473
+ * validation and trimming path.
547
474
  */
548
475
  export function normalizeSubagentTypeProfile(
549
476
  rawProfile: Record<string, unknown>,
@@ -551,16 +478,19 @@ export function normalizeSubagentTypeProfile(
551
478
  file: string,
552
479
  ): SubagentTypeConfig {
553
480
  const models = normalizeModels(rawProfile.models, `type "${name}"`, file);
481
+ const model = models === undefined ? trimString(rawProfile.model) : undefined;
482
+ const fallbackModels = models === undefined ? modelList(rawProfile.fallbackModels, rawProfile.fallbackModel) : undefined;
554
483
  return {
555
484
  description: trimString(rawProfile.description),
556
485
  icon: trimString(rawProfile.icon),
557
486
  models,
558
- model: models === undefined ? trimString(rawProfile.model) : undefined,
559
- fallbackModels: models === undefined ? modelList(rawProfile.fallbackModels, rawProfile.fallbackModel) : undefined,
560
- modelByParent: models === undefined ? normalizeModelByParent(rawProfile.modelByParent, name, file) : undefined,
487
+ model,
488
+ fallbackModels: models === undefined && (model || fallbackModels !== undefined) ? fallbackModels ?? [] : undefined,
489
+ modelByParent: models === undefined ? normalizeModelByParent(rawProfile.modelByParent, name, file, fallbackModels ?? []) : undefined,
490
+ forParentModels: normalizeParentModelPatterns(rawProfile.forParentModels, "forParentModels", name, file),
491
+ notForParentModels: normalizeParentModelPatterns(rawProfile.notForParentModels, "notForParentModels", name, file),
561
492
  thinking: trimString(rawProfile.thinking),
562
493
  tools: arrayOfStrings(rawProfile.tools),
563
- isolatedSkills: arrayOfStrings(rawProfile.isolatedSkills),
564
494
  extraArgs: arrayOfStrings(rawProfile.extraArgs),
565
495
  promptAppend: textBlock(rawProfile.promptAppend),
566
496
  promptOverride: textBlock(rawProfile.promptOverride),
@@ -571,13 +501,6 @@ export function normalizeSubagentTypeProfile(
571
501
  }
572
502
 
573
503
  function mergeConfig(target: SubagentConfig, source: Partial<SubagentConfig>): void {
574
- if (source.defaultType) target.defaultType = source.defaultType;
575
- if (source.routing) target.routing = { ...(target.routing ?? {}), ...source.routing };
576
- if (source.vision) target.vision = { ...(target.vision ?? {}), ...compactVisionConfig(source.vision) };
577
- if (source.maxConcurrent !== undefined) target.maxConcurrent = source.maxConcurrent;
578
- if (source.maxResultBytes !== undefined) target.maxResultBytes = source.maxResultBytes;
579
- if (source.timeoutMs !== undefined) target.timeoutMs = source.timeoutMs;
580
- if (source.retry) target.retry = { ...(target.retry ?? {}), ...source.retry };
581
504
  for (const [name, profile] of Object.entries(source.types ?? {})) {
582
505
  target.types[name] = mergeTypeProfile(target.types[name] ?? {}, profile);
583
506
  }
@@ -585,17 +508,12 @@ function mergeConfig(target: SubagentConfig, source: Partial<SubagentConfig>): v
585
508
  target.presets = target.presets ?? {};
586
509
  const previous = target.presets[name] ?? {};
587
510
  const merged = { ...previous, ...compactPreset(preset) };
588
- // The explicit selector form in the later config wins in both directions.
589
- if (preset.models !== undefined) {
590
- delete merged.model;
591
- delete merged.fallbackModels;
592
- delete merged.types;
593
- delete merged.thinking;
594
- delete merged.extraArgs;
595
- delete merged.timeoutMs;
596
- } else if (preset.model || preset.fallbackModels !== undefined || preset.types) {
597
- delete merged.models;
598
- }
511
+ delete merged.model;
512
+ delete merged.fallbackModels;
513
+ delete merged.types;
514
+ delete merged.thinking;
515
+ delete merged.extraArgs;
516
+ delete merged.timeoutMs;
599
517
  target.presets[name] = merged;
600
518
  }
601
519
  }
@@ -618,36 +536,6 @@ function mergeTypeProfile(base: SubagentTypeConfig, source: SubagentTypeConfig):
618
536
  return merged;
619
537
  }
620
538
 
621
- function normalizeRoutingConfig(value: Record<string, unknown>): SubagentRoutingConfig {
622
- const routing: SubagentRoutingConfig = {};
623
- if (typeof value.enabled === "boolean") routing.enabled = value.enabled;
624
- if (typeof value.model === "string" && value.model.trim()) routing.model = value.model.trim();
625
- const fallbackModels = modelList(value.fallbackModels, value.fallbackModel);
626
- if (fallbackModels) routing.fallbackModels = fallbackModels;
627
- if (typeof value.debug === "boolean") routing.debug = value.debug;
628
- const maxTaskChars = finiteNumber(value.maxTaskChars);
629
- if (maxTaskChars !== undefined) routing.maxTaskChars = Math.max(100, Math.round(maxTaskChars));
630
- const maxTokens = finiteNumber(value.maxTokens);
631
- if (maxTokens !== undefined) routing.maxTokens = Math.max(8, Math.round(maxTokens));
632
- const maxRetries = finiteNumber(value.maxRetries);
633
- if (maxRetries !== undefined) routing.maxRetries = Math.max(0, Math.round(maxRetries));
634
- const timeoutMs = finiteNumber(value.timeoutMs);
635
- if (timeoutMs !== undefined) routing.timeoutMs = Math.max(1000, Math.round(timeoutMs));
636
- return routing;
637
- }
638
-
639
- function normalizeVisionConfig(value: Record<string, unknown>): SubagentVisionConfig | undefined {
640
- const rawVision = value.vision;
641
- const output: SubagentVisionConfig = {};
642
- if (isRecord(rawVision)) {
643
- const patterns = patternList(rawVision.blindModelPatterns, rawVision.blindModelPattern, rawVision.blindModels, rawVision.blindModelMasks, rawVision.blindModelMask);
644
- if (patterns) output.blindModelPatterns = patterns;
645
- }
646
- const topLevelPatterns = patternList(value.blindModelPatterns, value.blindModelPattern, value.blindModels, value.blindModelMasks, value.blindModelMask);
647
- if (topLevelPatterns) output.blindModelPatterns = topLevelPatterns;
648
- return output.blindModelPatterns !== undefined ? output : undefined;
649
- }
650
-
651
539
  function matchesAnyModelPattern(modelRef: string, patterns: string[]): boolean {
652
540
  return patterns.some((pattern) => modelPatternRegExp(pattern).test(modelRef));
653
541
  }
@@ -665,9 +553,10 @@ function compactProfile(profile: SubagentTypeConfig): SubagentTypeConfig {
665
553
  if (profile.model) compact.model = profile.model;
666
554
  if (profile.fallbackModels) compact.fallbackModels = profile.fallbackModels;
667
555
  if (profile.modelByParent) compact.modelByParent = profile.modelByParent;
556
+ if (profile.forParentModels !== undefined) compact.forParentModels = profile.forParentModels;
557
+ if (profile.notForParentModels !== undefined) compact.notForParentModels = profile.notForParentModels;
668
558
  if (profile.thinking) compact.thinking = profile.thinking;
669
559
  if (profile.tools && profile.tools.length > 0) compact.tools = profile.tools;
670
- if (profile.isolatedSkills && profile.isolatedSkills.length > 0) compact.isolatedSkills = profile.isolatedSkills;
671
560
  if (profile.extraArgs && profile.extraArgs.length > 0) compact.extraArgs = profile.extraArgs;
672
561
  if (profile.promptAppend) compact.promptAppend = profile.promptAppend;
673
562
  if (profile.promptOverride) compact.promptOverride = profile.promptOverride;
@@ -690,34 +579,6 @@ function compactPreset(preset: SubagentPreset): SubagentPreset {
690
579
  return compact;
691
580
  }
692
581
 
693
- function compactVisionConfig(vision: SubagentVisionConfig): SubagentVisionConfig {
694
- const compact: SubagentVisionConfig = {};
695
- if (vision.blindModelPatterns) compact.blindModelPatterns = vision.blindModelPatterns;
696
- return compact;
697
- }
698
-
699
- function normalizePresetTypeOverrides(value: unknown, file: string, presetName: string): Record<string, SubagentPresetTypeOverride> | undefined {
700
- if (value === undefined) return undefined;
701
- if (!isRecord(value)) throw new Error(`Subagent preset "${presetName}" types must be an object: ${file}`);
702
- const types: Record<string, SubagentPresetTypeOverride> = {};
703
- for (const [name, rawOverride] of Object.entries(value)) {
704
- if (!isRecord(rawOverride)) throw new Error(`Subagent preset "${presetName}" type override "${name}" must be an object: ${file}`);
705
- const override: SubagentPresetTypeOverride = {};
706
- const model = trimString(rawOverride.model);
707
- const fallbackModels = modelList(rawOverride.fallbackModels, rawOverride.fallbackModel);
708
- const thinking = trimString(rawOverride.thinking);
709
- const extraArgs = arrayOfStrings(rawOverride.extraArgs);
710
- const timeoutMs = positiveMilliseconds(rawOverride.timeoutMs);
711
- if (model) override.model = model;
712
- if (fallbackModels) override.fallbackModels = fallbackModels;
713
- if (thinking) override.thinking = thinking;
714
- if (extraArgs && extraArgs.length > 0) override.extraArgs = extraArgs;
715
- if (timeoutMs !== undefined) override.timeoutMs = timeoutMs;
716
- if (override.model || override.fallbackModels || override.thinking || override.extraArgs || override.timeoutMs !== undefined) types[name] = override;
717
- }
718
- return Object.keys(types).length > 0 ? types : undefined;
719
- }
720
-
721
582
  function normalizeModels(value: unknown, owner: string, file: string): string[] | undefined {
722
583
  if (value === undefined) return undefined;
723
584
  if (!Array.isArray(value) || value.some((ref) => typeof ref !== "string" || !/^[^\s/*]+\/[^\s*]+$/.test(ref.trim()))) {
@@ -726,7 +587,25 @@ function normalizeModels(value: unknown, owner: string, file: string): string[]
726
587
  return [...new Set(value.map((ref: string) => ref.trim()))];
727
588
  }
728
589
 
729
- function normalizeModelByParent(value: unknown, typeName: string, file: string): Record<string, ModelByParentEntry> | undefined {
590
+ function normalizeParentModelPatterns(
591
+ value: unknown,
592
+ field: "forParentModels" | "notForParentModels",
593
+ typeName: string,
594
+ file: string,
595
+ ): string[] | undefined {
596
+ if (value === undefined) return undefined;
597
+ if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string" || !pattern.trim())) {
598
+ throw new Error(`Subagent type "${typeName}" ${field} must be an array of non-empty model patterns: ${file}`);
599
+ }
600
+ return [...new Set(value.map((pattern: string) => pattern.trim()))];
601
+ }
602
+
603
+ function normalizeModelByParent(
604
+ value: unknown,
605
+ typeName: string,
606
+ file: string,
607
+ defaultFallbackModels: string[] = [],
608
+ ): Record<string, ModelByParentEntry> | undefined {
730
609
  if (value === undefined || value === null) return undefined;
731
610
  if (!isRecord(value)) throw new Error(`Subagent type "${typeName}" modelByParent must be an object: ${file}`);
732
611
  const out: Record<string, ModelByParentEntry> = {};
@@ -735,13 +614,20 @@ function normalizeModelByParent(value: unknown, typeName: string, file: string):
735
614
  if (!pat) continue;
736
615
  if (typeof raw === "string") {
737
616
  const model = trimString(raw);
738
- if (model) out[pat] = { model };
617
+ if (model) out[pat] = { model, fallbackModels: [...defaultFallbackModels] };
739
618
  continue;
740
619
  }
741
620
  if (isRecord(raw)) {
742
621
  const model = trimString(raw.model);
743
622
  if (!model) throw new Error(`Subagent type "${typeName}" modelByParent["${pat}"].model must be a non-empty string: ${file}`);
744
- out[pat] = { model, fallbackModels: modelList(raw.fallbackModels, raw.fallbackModel) };
623
+ const hasFallbackOverride = Object.prototype.hasOwnProperty.call(raw, "fallbackModels")
624
+ || Object.prototype.hasOwnProperty.call(raw, "fallbackModel");
625
+ out[pat] = {
626
+ model,
627
+ fallbackModels: hasFallbackOverride
628
+ ? modelList(raw.fallbackModels, raw.fallbackModel) ?? []
629
+ : [...defaultFallbackModels],
630
+ };
745
631
  continue;
746
632
  }
747
633
  throw new Error(`Subagent type "${typeName}" modelByParent["${pat}"] must be a string or object: ${file}`);
@@ -806,55 +692,23 @@ function applyEnvRoutingOverrides(config: SubagentConfig, env: NodeJS.ProcessEnv
806
692
  config.routing = routing;
807
693
  }
808
694
 
809
- function typeEnvKey(typeName: string): string {
810
- return typeName.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
695
+ function applyEnvRuntimeOverrides(config: SubagentConfig, env: NodeJS.ProcessEnv): void {
696
+ const maxConcurrent = finiteEnvNumber(env.PI_SUBAGENTS_MAX_CONCURRENT || env.ASYNC_SUBAGENTS_MAX_CONCURRENT);
697
+ if (maxConcurrent !== undefined) config.maxConcurrent = Math.max(0, Math.round(maxConcurrent));
698
+ const maxResultBytes = finiteEnvNumber(env.PI_SUBAGENTS_MAX_RESULT_BYTES || env.ASYNC_SUBAGENTS_MAX_RESULT_BYTES);
699
+ if (maxResultBytes !== undefined) config.maxResultBytes = Math.max(0, Math.round(maxResultBytes));
700
+ const timeoutMs = finiteEnvNumber(env.PI_SUBAGENTS_TIMEOUT_MS || env.ASYNC_SUBAGENTS_TIMEOUT_MS);
701
+ if (timeoutMs !== undefined) config.timeoutMs = Math.max(1, Math.round(timeoutMs));
811
702
  }
812
703
 
813
- function stripJsonComments(input: string): string {
814
- let output = "";
815
- let inString = false;
816
- let quote = "";
817
- let escaped = false;
818
- for (let i = 0; i < input.length; i++) {
819
- const ch = input[i];
820
- const next = input[i + 1];
821
- if (inString) {
822
- output += ch;
823
- if (escaped) escaped = false;
824
- else if (ch === "\\") escaped = true;
825
- else if (ch === quote) inString = false;
826
- continue;
827
- }
828
- if (ch === '"' || ch === "'") {
829
- inString = true;
830
- quote = ch;
831
- output += ch;
832
- continue;
833
- }
834
- if (ch === "/" && next === "/") {
835
- while (i < input.length && input[i] !== "\n") i++;
836
- output += "\n";
837
- continue;
838
- }
839
- if (ch === "/" && next === "*") {
840
- i += 2;
841
- while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
842
- i++;
843
- continue;
844
- }
845
- output += ch;
846
- }
847
- return output.replace(/,\s*([}\]])/g, "$1");
704
+ function typeEnvKey(typeName: string): string {
705
+ return typeName.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
848
706
  }
849
707
 
850
708
  function cloneConfig(config: SubagentConfig): SubagentConfig {
851
709
  return JSON.parse(JSON.stringify(config)) as SubagentConfig;
852
710
  }
853
711
 
854
- function expandHome(value: string): string {
855
- return value === "~" || value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
856
- }
857
-
858
712
  function trimString(value: unknown): string | undefined {
859
713
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
860
714
  }
@@ -874,10 +728,6 @@ function finiteEnvNumber(value: unknown): number | undefined {
874
728
  return Number.isFinite(parsed) ? parsed : undefined;
875
729
  }
876
730
 
877
- function isNodeError(error: unknown): error is NodeJS.ErrnoException {
878
- return error instanceof Error && "code" in error;
879
- }
880
-
881
731
  function arrayOfStrings(value: unknown): string[] | undefined {
882
732
  if (!Array.isArray(value)) return undefined;
883
733
  const items = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
@@ -899,23 +749,6 @@ function modelList(...values: unknown[]): string[] | undefined {
899
749
  return models.length > 0 || values.some(Array.isArray) ? models : undefined;
900
750
  }
901
751
 
902
- function patternList(...values: unknown[]): string[] | undefined {
903
- let sawArray = false;
904
- const seen = new Set<string>();
905
- const patterns: string[] = [];
906
- for (const value of values) {
907
- if (Array.isArray(value)) sawArray = true;
908
- const items = Array.isArray(value) ? value : [value];
909
- for (const item of items) {
910
- const pattern = trimString(item);
911
- if (!pattern || seen.has(pattern)) continue;
912
- seen.add(pattern);
913
- patterns.push(pattern);
914
- }
915
- }
916
- return sawArray || patterns.length > 0 ? patterns : undefined;
917
- }
918
-
919
752
  function textBlock(value: unknown): string | undefined {
920
753
  if (typeof value === "string") return trimString(value);
921
754
  const lines = arrayOfStrings(value);