pi-ui-extend 1.0.42 → 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 (233) hide show
  1. package/README.md +41 -12
  2. package/dist/app/app.d.ts +10 -0
  3. package/dist/app/app.js +132 -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 +3 -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 +51 -34
  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 +14 -0
  17. package/dist/app/commands/reload-context-inventory.js +12 -1
  18. package/dist/app/input/autocomplete-controller.js +61 -34
  19. package/dist/app/input/input-action-controller.d.ts +2 -0
  20. package/dist/app/input/input-action-controller.js +8 -1
  21. package/dist/app/input/input-controller.d.ts +2 -1
  22. package/dist/app/input/input-controller.js +7 -2
  23. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  24. package/dist/app/input/voice-controller.d.ts +41 -45
  25. package/dist/app/input/voice-controller.js +351 -419
  26. package/dist/app/model/model-usage-controller.js +17 -7
  27. package/dist/app/model/model-usage-status.d.ts +4 -1
  28. package/dist/app/model/model-usage-status.js +270 -46
  29. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  30. package/dist/app/popup/menu-items-controller.js +37 -21
  31. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  32. package/dist/app/popup/popup-action-controller.js +77 -24
  33. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  34. package/dist/app/popup/popup-menu-controller.js +239 -69
  35. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  36. package/dist/app/rendering/dcp-stats.js +40 -425
  37. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  38. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  39. package/dist/app/rendering/render-controller.js +23 -2
  40. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  41. package/dist/app/rendering/status-line-renderer.js +41 -5
  42. package/dist/app/rendering/tab-line-renderer.js +26 -20
  43. package/dist/app/runtime.d.ts +12 -1
  44. package/dist/app/runtime.js +120 -12
  45. package/dist/app/screen/mouse-controller.d.ts +2 -0
  46. package/dist/app/screen/mouse-controller.js +17 -7
  47. package/dist/app/screen/status-controller.d.ts +4 -0
  48. package/dist/app/screen/status-controller.js +5 -0
  49. package/dist/app/session/lazy-session-manager.js +34 -0
  50. package/dist/app/session/session-event-controller.d.ts +1 -0
  51. package/dist/app/session/session-event-controller.js +10 -1
  52. package/dist/app/session/session-history.d.ts +1 -0
  53. package/dist/app/session/session-history.js +12 -1
  54. package/dist/app/session/session-lifecycle-controller.d.ts +6 -1
  55. package/dist/app/session/session-lifecycle-controller.js +10 -1
  56. package/dist/app/session/tabs-controller.d.ts +25 -1
  57. package/dist/app/session/tabs-controller.js +442 -153
  58. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  59. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  60. package/dist/app/types.d.ts +11 -1
  61. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  62. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  63. package/dist/app/workspace/workspace-undo.js +1 -0
  64. package/dist/bundled-extensions/question/index.js +9 -1
  65. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  66. package/dist/bundled-extensions/question/remote.js +33 -0
  67. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  68. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  69. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  70. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  71. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  72. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  73. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  74. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  75. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  76. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  77. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  78. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  79. package/dist/config.d.ts +21 -2
  80. package/dist/config.js +159 -32
  81. package/dist/default-pix-config.js +20 -10
  82. package/dist/schemas/pi-tools-suite-schema.d.ts +8 -4
  83. package/dist/schemas/pi-tools-suite-schema.js +11 -5
  84. package/dist/schemas/pix-schema.d.ts +12 -2
  85. package/dist/schemas/pix-schema.js +37 -9
  86. package/dist/schemas/tasks-schema.js +1 -1
  87. package/docs/concurrency.md +3 -1
  88. package/docs/desktop-mvp.md +10 -5
  89. package/docs/desktop-task-manager.md +81 -28
  90. package/docs/release.md +30 -6
  91. package/external/pi-tools-suite/README.md +165 -39
  92. package/external/pi-tools-suite/docs/session-recovery.md +7 -0
  93. package/external/pi-tools-suite/docs/subagent-model-pools.md +12 -5
  94. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  95. package/external/pi-tools-suite/package.json +6 -1
  96. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  97. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  98. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  99. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  100. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  101. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  102. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  103. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  104. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  105. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  106. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  107. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  108. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  109. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  110. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  111. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  112. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  113. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  114. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  115. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  116. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  117. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  119. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  127. package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
  128. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  129. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +0 -2
  130. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  131. package/external/pi-tools-suite/src/async-subagents/core/config.ts +40 -17
  132. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  133. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  134. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +7 -4
  135. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +34 -24
  136. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  137. package/external/pi-tools-suite/src/async-subagents/lib.ts +9 -1
  138. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +0 -1
  139. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +2 -2
  140. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  141. package/external/pi-tools-suite/src/config.ts +14 -0
  142. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  143. package/external/pi-tools-suite/src/context-gateway/config.ts +66 -1
  144. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  145. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +123 -3
  146. package/external/pi-tools-suite/src/context-gateway/index.ts +137 -4
  147. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +5 -5
  148. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +2 -2
  149. package/external/pi-tools-suite/src/context-gateway/types.ts +8 -1
  150. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  151. package/external/pi-tools-suite/src/dcp/auto-compress.ts +41 -14
  152. package/external/pi-tools-suite/src/dcp/commands.ts +14 -156
  153. package/external/pi-tools-suite/src/dcp/compress-tool.ts +8 -2
  154. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  155. package/external/pi-tools-suite/src/dcp/config.ts +58 -4
  156. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  157. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  158. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  159. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  160. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  161. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  162. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +92 -2
  163. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  164. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  165. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  166. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  167. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  168. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  169. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +68 -16
  170. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  171. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  172. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  173. package/external/pi-tools-suite/src/resource-registry/index.ts +282 -21
  174. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  175. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  176. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  177. package/external/pi-tools-suite/src/tool-descriptions.ts +44 -15
  178. package/package.json +7 -8
  179. package/schemas/pi-tools-suite.json +44 -12
  180. package/schemas/pix.json +109 -15
  181. package/schemas/tasks.json +1 -2
  182. package/skills/simplify/SKILL.md +33 -5
  183. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -196
  184. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  185. package/skills/playwright-cli/SKILL.md +0 -420
  186. package/skills/playwright-cli/references/element-attributes.md +0 -23
  187. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  188. package/skills/playwright-cli/references/request-mocking.md +0 -87
  189. package/skills/playwright-cli/references/running-code.md +0 -241
  190. package/skills/playwright-cli/references/session-management.md +0 -273
  191. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  192. package/skills/playwright-cli/references/storage-state.md +0 -290
  193. package/skills/playwright-cli/references/test-generation.md +0 -142
  194. package/skills/playwright-cli/references/tracing.md +0 -154
  195. package/skills/playwright-cli/references/video-recording.md +0 -147
  196. package/skills/spec-wiki/SKILL.md +0 -55
  197. package/skills/spec-wiki/evals/evals.json +0 -132
  198. package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +0 -14
  199. package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +0 -4
  200. package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +0 -9
  201. package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +0 -17
  202. package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +0 -12
  203. package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +0 -11
  204. package/skills/spec-wiki/evals/files/focused/src/session.ts +0 -5
  205. package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +0 -17
  206. package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +0 -2
  207. package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +0 -3
  208. package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +0 -18
  209. package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +0 -5
  210. package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +0 -24
  211. package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +0 -7
  212. package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +0 -7
  213. package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +0 -17
  214. package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +0 -3
  215. package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +0 -3
  216. package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +0 -4
  217. package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +0 -9
  218. package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +0 -15
  219. package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +0 -6
  220. package/skills/spec-wiki/evals/maintenance_regression.py +0 -311
  221. package/skills/spec-wiki/evals/retrieval-eval.md +0 -37
  222. package/skills/spec-wiki/evals/retrieval-evals.json +0 -37
  223. package/skills/spec-wiki/evals/trigger-evals.json +0 -94
  224. package/skills/spec-wiki/references/bootstrap.md +0 -100
  225. package/skills/spec-wiki/references/focused-spec.md +0 -117
  226. package/skills/spec-wiki/references/index-schema.md +0 -330
  227. package/skills/spec-wiki/references/maintenance.md +0 -134
  228. package/skills/spec-wiki/references/query.md +0 -74
  229. package/skills/spec-wiki/scripts/spec_wiki.py +0 -2076
  230. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  231. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  232. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -3,13 +3,14 @@ import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { parse as parseJsonc } from "jsonc-parser";
5
5
  import { projectAgentsDir, readAgentDefinitionsFromDir, readProjectAgentDefinitions, type AgentDefinition } from "./agents-dir.js";
6
+ import { LEGACY_BROWSER_QA_TYPE, UI_QA_TYPE } from "./browser-qa.js";
6
7
  import type { AgentTask, RetryConfig } from "./types.js";
7
8
 
8
9
  export interface ModelByParentEntry {
9
10
  /** Model ref to use when the parent model matches the entry's pattern. */
10
11
  model: string;
11
12
  /** Ordered fallbacks used when this entry's model hits quota/rate limits; replaces the normal fallback chain. */
12
- fallbackModels?: string[];
13
+ fallbackModels: string[];
13
14
  }
14
15
 
15
16
  export interface SubagentTypeConfig {
@@ -29,7 +30,8 @@ export interface SubagentTypeConfig {
29
30
  /**
30
31
  * Parent-model-aware model selection. Keys are glob model refs (e.g. "zai/*")
31
32
  * matched against the current parent model; the first matching key wins.
32
- * 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.
33
35
  * Ordinary roles: explicit task/forced model and preset models take priority.
34
36
  * Oracle alone keeps parent-aware selection ahead of presets.
35
37
  */
@@ -46,8 +48,6 @@ export interface SubagentTypeConfig {
46
48
  notForParentModels?: string[];
47
49
  thinking?: string;
48
50
  tools?: string[];
49
- /** Explicit skill files loaded after disabling normal skill discovery. */
50
- isolatedSkills?: string[];
51
51
  extraArgs?: string[];
52
52
  /** Extra prompt text appended after the generated or overridden prompt. */
53
53
  promptAppend?: string;
@@ -136,8 +136,6 @@ export interface SubagentConfig {
136
136
  export interface ResolvedAgentTaskConfig {
137
137
  task: AgentTask;
138
138
  extraArgs: string[];
139
- /** Explicit skill files loaded with normal skill discovery disabled. */
140
- isolatedSkills: string[];
141
139
  /** Ordered model fallbacks for the resolved model. Current-process exhausted models are skipped before spawning. */
142
140
  fallbackModels: string[];
143
141
  profile?: SubagentTypeConfig;
@@ -243,6 +241,11 @@ export function getProjectSubagentPresetsPath(cwd: string): string {
243
241
  /** Normalize `.pi/agents/*.md` definitions through the shared type-profile path. */
244
242
  function projectAgentTypes(cwd: string): Partial<SubagentConfig> {
245
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
+ }
246
249
  const types = normalizeAgentDefinitions(definitions);
247
250
  if (Object.keys(types).length === 0) return {};
248
251
  return { types };
@@ -336,7 +339,6 @@ export function resolveAgentTaskConfig(
336
339
  return {
337
340
  profile,
338
341
  extraArgs,
339
- isolatedSkills: arrayOfStrings(profile?.isolatedSkills) ?? [],
340
342
  fallbackModels,
341
343
  retry: resolveRetryConfig(config.retry, profile?.retry),
342
344
  maxResultBytes: profile?.maxResultBytes ?? config.maxResultBytes,
@@ -359,7 +361,7 @@ export function resolveSubagentRoutingConfig(config: SubagentConfig): ResolvedSu
359
361
  }
360
362
 
361
363
  export function defaultSubagentType(config: SubagentConfig): string | undefined {
362
- const configured = trimString(config.defaultType);
364
+ const configured = normalizeSubagentType(config.defaultType, config);
363
365
  if (configured) return Object.prototype.hasOwnProperty.call(config.types, configured) ? configured : undefined;
364
366
  return Object.keys(config.types).find((name) => trimString(name));
365
367
  }
@@ -429,11 +431,20 @@ export function filterSubagentConfigForParentModel(
429
431
  }
430
432
 
431
433
  export function selectSubagentType(task: AgentTask, config: SubagentConfig): string | undefined {
432
- const explicit = trimString(task.subagentType);
434
+ const explicit = normalizeSubagentType(task.subagentType, config);
433
435
  if (explicit) return explicit;
434
436
  return defaultSubagentType(config);
435
437
  }
436
438
 
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;
446
+ }
447
+
437
448
  function readPresetConfigFile(file: string): Partial<SubagentConfig> {
438
449
  const raw = fs.readFileSync(file, "utf-8");
439
450
  const parsed = parseJsonc(raw) as unknown;
@@ -467,18 +478,19 @@ export function normalizeSubagentTypeProfile(
467
478
  file: string,
468
479
  ): SubagentTypeConfig {
469
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;
470
483
  return {
471
484
  description: trimString(rawProfile.description),
472
485
  icon: trimString(rawProfile.icon),
473
486
  models,
474
- model: models === undefined ? trimString(rawProfile.model) : undefined,
475
- fallbackModels: models === undefined ? modelList(rawProfile.fallbackModels, rawProfile.fallbackModel) : undefined,
476
- 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,
477
490
  forParentModels: normalizeParentModelPatterns(rawProfile.forParentModels, "forParentModels", name, file),
478
491
  notForParentModels: normalizeParentModelPatterns(rawProfile.notForParentModels, "notForParentModels", name, file),
479
492
  thinking: trimString(rawProfile.thinking),
480
493
  tools: arrayOfStrings(rawProfile.tools),
481
- isolatedSkills: arrayOfStrings(rawProfile.isolatedSkills),
482
494
  extraArgs: arrayOfStrings(rawProfile.extraArgs),
483
495
  promptAppend: textBlock(rawProfile.promptAppend),
484
496
  promptOverride: textBlock(rawProfile.promptOverride),
@@ -545,7 +557,6 @@ function compactProfile(profile: SubagentTypeConfig): SubagentTypeConfig {
545
557
  if (profile.notForParentModels !== undefined) compact.notForParentModels = profile.notForParentModels;
546
558
  if (profile.thinking) compact.thinking = profile.thinking;
547
559
  if (profile.tools && profile.tools.length > 0) compact.tools = profile.tools;
548
- if (profile.isolatedSkills && profile.isolatedSkills.length > 0) compact.isolatedSkills = profile.isolatedSkills;
549
560
  if (profile.extraArgs && profile.extraArgs.length > 0) compact.extraArgs = profile.extraArgs;
550
561
  if (profile.promptAppend) compact.promptAppend = profile.promptAppend;
551
562
  if (profile.promptOverride) compact.promptOverride = profile.promptOverride;
@@ -589,7 +600,12 @@ function normalizeParentModelPatterns(
589
600
  return [...new Set(value.map((pattern: string) => pattern.trim()))];
590
601
  }
591
602
 
592
- function normalizeModelByParent(value: unknown, typeName: string, file: string): Record<string, ModelByParentEntry> | undefined {
603
+ function normalizeModelByParent(
604
+ value: unknown,
605
+ typeName: string,
606
+ file: string,
607
+ defaultFallbackModels: string[] = [],
608
+ ): Record<string, ModelByParentEntry> | undefined {
593
609
  if (value === undefined || value === null) return undefined;
594
610
  if (!isRecord(value)) throw new Error(`Subagent type "${typeName}" modelByParent must be an object: ${file}`);
595
611
  const out: Record<string, ModelByParentEntry> = {};
@@ -598,13 +614,20 @@ function normalizeModelByParent(value: unknown, typeName: string, file: string):
598
614
  if (!pat) continue;
599
615
  if (typeof raw === "string") {
600
616
  const model = trimString(raw);
601
- if (model) out[pat] = { model };
617
+ if (model) out[pat] = { model, fallbackModels: [...defaultFallbackModels] };
602
618
  continue;
603
619
  }
604
620
  if (isRecord(raw)) {
605
621
  const model = trimString(raw.model);
606
622
  if (!model) throw new Error(`Subagent type "${typeName}" modelByParent["${pat}"].model must be a non-empty string: ${file}`);
607
- 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
+ };
608
631
  continue;
609
632
  }
610
633
  throw new Error(`Subagent type "${typeName}" modelByParent["${pat}"] must be a string or object: ${file}`);
@@ -1,5 +1,6 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import { isBlindModelRef, SubagentModelSelectionError, type ResolvedAgentTaskConfig, type SubagentConfig } from "./config.js";
3
+ import { isUiQaType } from "./browser-qa.js";
3
4
  import { isSessionModelUnavailable } from "./model-fallback.js";
4
5
 
5
6
  export interface SubagentModelRegistry {
@@ -17,7 +18,7 @@ export async function selectAvailableAgentModels(
17
18
  signal?: AbortSignal,
18
19
  ): Promise<ResolvedAgentTaskConfig> {
19
20
  if (signal?.aborted) throw new Error("Aborted");
20
- const needsImages = Boolean(resolved.task.imagePaths?.length) || resolved.task.subagentType === "browser-qa";
21
+ const needsImages = Boolean(resolved.task.imagePaths?.length) || isUiQaType(resolved.task.subagentType);
21
22
  if (!registry && !needsImages) return resolved; // Narrow offline/test callers.
22
23
  const candidates = [resolved.task.model, ...resolved.fallbackModels].filter((ref): ref is string => Boolean(ref));
23
24
  let registeredAvailable: Set<string> | undefined;
@@ -4,6 +4,15 @@ import { getRunRoot, hasAgentPrompt, hasLaunchedAgentPrompt, isDir, resolveRunDi
4
4
 
5
5
  export const SUBAGENT_REGISTRY_FILE = "registry.json";
6
6
 
7
+ const REGISTRY_LOCK_RETRY_MS = 10;
8
+ const REGISTRY_LOCK_TIMEOUT_MS = 2_000;
9
+ const REGISTRY_LOCK_STALE_MS = 30_000;
10
+
11
+ interface RegistryLockOwner {
12
+ pid: number;
13
+ token: string;
14
+ }
15
+
7
16
  export interface SubagentRegistryRun {
8
17
  runId: string;
9
18
  runDir: string;
@@ -62,45 +71,44 @@ export function loadSubagentRegistry(cwd: string): SubagentRegistry {
62
71
 
63
72
  export function saveSubagentRegistry(cwd: string, registry: SubagentRegistry): void {
64
73
  const registryPath = getSubagentRegistryPath(cwd);
65
- fs.mkdirSync(path.dirname(registryPath), { recursive: true });
66
- fs.writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, "utf-8");
74
+ withRegistryLock(registryPath, () => writeRegistryAtomically(registryPath, registry));
67
75
  }
68
76
 
69
77
  export function recordSubagentRun(cwd: string, runDir: string, agentIds: string[]): SubagentRegistry {
70
- const registry = loadSubagentRegistry(cwd);
71
- const resolvedRunDir = path.resolve(runDir);
72
- const runId = path.basename(resolvedRunDir);
73
- const now = new Date().toISOString();
74
- const uniqueAgentIds = [...new Set(agentIds.filter((id) => id.trim().length > 0))];
75
- const previous = registry.runs[runId];
76
- registry.runs[runId] = {
77
- runId,
78
- runDir: resolvedRunDir,
79
- agentIds: uniqueAgentIds,
80
- createdAt: previous?.createdAt ?? now,
81
- updatedAt: now,
82
- };
83
- registry.latestRunId = runId;
84
- registry.latestRunDir = resolvedRunDir;
85
- for (const agentId of uniqueAgentIds) {
86
- registry.agents[agentId] = { agentId, runId, runDir: resolvedRunDir, updatedAt: now };
87
- }
88
- saveSubagentRegistry(cwd, registry);
89
- return registry;
78
+ return updateSubagentRegistry(cwd, (registry) => {
79
+ const resolvedRunDir = path.resolve(runDir);
80
+ const runId = path.basename(resolvedRunDir);
81
+ const now = new Date().toISOString();
82
+ const uniqueAgentIds = [...new Set(agentIds.filter((id) => id.trim().length > 0))];
83
+ const previous = registry.runs[runId];
84
+ registry.runs[runId] = {
85
+ runId,
86
+ runDir: resolvedRunDir,
87
+ agentIds: uniqueAgentIds,
88
+ createdAt: previous?.createdAt ?? now,
89
+ updatedAt: now,
90
+ };
91
+ registry.latestRunId = runId;
92
+ registry.latestRunDir = resolvedRunDir;
93
+ for (const agentId of uniqueAgentIds) {
94
+ registry.agents[agentId] = { agentId, runId, runDir: resolvedRunDir, updatedAt: now };
95
+ }
96
+ return registry;
97
+ });
90
98
  }
91
99
 
92
100
  export function removeSubagentRunsFromRegistry(cwd: string, runDirs: string[]): SubagentRegistry {
93
- const registry = loadSubagentRegistry(cwd);
94
- const removed = new Set(runDirs.map((runDir) => normalizePath(runDir)));
95
- for (const [runId, run] of Object.entries(registry.runs)) {
96
- if (removed.has(normalizePath(run.runDir))) delete registry.runs[runId];
97
- }
98
- for (const [agentId, agent] of Object.entries(registry.agents)) {
99
- if (removed.has(normalizePath(agent.runDir))) delete registry.agents[agentId];
100
- }
101
- refreshLatestRun(registry);
102
- saveSubagentRegistry(cwd, registry);
103
- return registry;
101
+ return updateSubagentRegistry(cwd, (registry) => {
102
+ const removed = new Set(runDirs.map((runDir) => normalizePath(runDir)));
103
+ for (const [runId, run] of Object.entries(registry.runs)) {
104
+ if (removed.has(normalizePath(run.runDir))) delete registry.runs[runId];
105
+ }
106
+ for (const [agentId, agent] of Object.entries(registry.agents)) {
107
+ if (removed.has(normalizePath(agent.runDir))) delete registry.agents[agentId];
108
+ }
109
+ refreshLatestRun(registry);
110
+ return registry;
111
+ });
104
112
  }
105
113
 
106
114
  export function resolveSubagentRunDir(cwd: string, runDir?: string): string {
@@ -157,6 +165,123 @@ function refreshLatestRun(registry: SubagentRegistry): void {
157
165
  delete registry.latestRunDir;
158
166
  }
159
167
 
168
+ function updateSubagentRegistry(cwd: string, update: (registry: SubagentRegistry) => SubagentRegistry): SubagentRegistry {
169
+ const registryPath = getSubagentRegistryPath(cwd);
170
+ return withRegistryLock(registryPath, () => {
171
+ const registry = loadSubagentRegistry(cwd);
172
+ const updated = update(registry);
173
+ writeRegistryAtomically(registryPath, updated);
174
+ return updated;
175
+ });
176
+ }
177
+
178
+ function withRegistryLock<T>(registryPath: string, operation: () => T): T {
179
+ fs.mkdirSync(path.dirname(registryPath), { recursive: true });
180
+ const lockPath = `${registryPath}.lock`;
181
+ const owner = registryLockOwner();
182
+ const deadline = Date.now() + REGISTRY_LOCK_TIMEOUT_MS;
183
+ while (!createRegistryLock(lockPath, owner)) {
184
+ recoverStaleRegistryLock(lockPath, owner);
185
+ if (Date.now() >= deadline) {
186
+ throw new Error(`Timed out waiting for sub-agent registry lock at ${lockPath}.`);
187
+ }
188
+ sleep(REGISTRY_LOCK_RETRY_MS);
189
+ }
190
+ try {
191
+ return operation();
192
+ } finally {
193
+ releaseRegistryLock(lockPath, owner);
194
+ }
195
+ }
196
+
197
+ function writeRegistryAtomically(registryPath: string, registry: SubagentRegistry): void {
198
+ const temporaryPath = `${registryPath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
199
+ try {
200
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(registry, null, 2)}\n`, { encoding: "utf-8", mode: 0o600, flag: "wx" });
201
+ fs.renameSync(temporaryPath, registryPath);
202
+ } finally {
203
+ fs.rmSync(temporaryPath, { force: true });
204
+ }
205
+ }
206
+
207
+ function registryLockOwner(): RegistryLockOwner {
208
+ return { pid: process.pid, token: `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}` };
209
+ }
210
+
211
+ function createRegistryLock(lockPath: string, owner: RegistryLockOwner): boolean {
212
+ try {
213
+ fs.writeFileSync(lockPath, JSON.stringify(owner), { encoding: "utf-8", mode: 0o600, flag: "wx" });
214
+ return true;
215
+ } catch (error) {
216
+ if (errorCode(error) === "EEXIST") return false;
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ function recoverStaleRegistryLock(lockPath: string, owner: RegistryLockOwner): void {
222
+ let lockStat: fs.Stats;
223
+ try {
224
+ lockStat = fs.statSync(lockPath);
225
+ } catch (error) {
226
+ if (errorCode(error) === "ENOENT") return;
227
+ throw error;
228
+ }
229
+ const existingOwner = readRegistryLockOwner(lockPath);
230
+ const lockAgeMs = Date.now() - lockStat.mtimeMs;
231
+ if (existingOwner && isProcessAlive(existingOwner.pid)) return;
232
+ if (!existingOwner && lockAgeMs <= REGISTRY_LOCK_STALE_MS) return;
233
+
234
+ const quarantinePath = `${lockPath}.stale-${owner.token}`;
235
+ try {
236
+ fs.renameSync(lockPath, quarantinePath);
237
+ } catch (error) {
238
+ if (errorCode(error) === "ENOENT") return;
239
+ throw error;
240
+ }
241
+ try {
242
+ const movedStat = fs.statSync(quarantinePath);
243
+ if (movedStat.dev !== lockStat.dev || movedStat.ino !== lockStat.ino) return;
244
+ fs.rmSync(quarantinePath, { force: true });
245
+ } finally {
246
+ if (fs.existsSync(quarantinePath) && !fs.existsSync(lockPath)) {
247
+ fs.renameSync(quarantinePath, lockPath);
248
+ }
249
+ }
250
+ }
251
+
252
+ function releaseRegistryLock(lockPath: string, owner: RegistryLockOwner): void {
253
+ if (readRegistryLockOwner(lockPath)?.token === owner.token) fs.rmSync(lockPath, { force: true });
254
+ }
255
+
256
+ function readRegistryLockOwner(lockPath: string): RegistryLockOwner | undefined {
257
+ try {
258
+ const parsed: unknown = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
259
+ if (!isRecord(parsed) || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.token !== "string") return undefined;
260
+ return { pid: parsed.pid, token: parsed.token };
261
+ } catch {
262
+ return undefined;
263
+ }
264
+ }
265
+
266
+ function isProcessAlive(pid: number): boolean {
267
+ try {
268
+ process.kill(pid, 0);
269
+ return true;
270
+ } catch (error) {
271
+ return errorCode(error) !== "ESRCH";
272
+ }
273
+ }
274
+
275
+ function errorCode(error: unknown): string | undefined {
276
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
277
+ ? error.code
278
+ : undefined;
279
+ }
280
+
281
+ function sleep(ms: number): void {
282
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
283
+ }
284
+
160
285
  function looksLikeRunDir(runDir: string): boolean {
161
286
  if (isDir(path.join(runDir, "prompts"))) return true;
162
287
  try {
@@ -5,6 +5,7 @@ import {
5
5
  currentModelRef,
6
6
  defaultSubagentType,
7
7
  filterSubagentConfigForParentModel,
8
+ normalizeSubagentType,
8
9
  resolveSubagentRoutingConfig,
9
10
  type ResolvedSubagentRoutingConfig,
10
11
  type SubagentConfig,
@@ -46,7 +47,7 @@ export class SubagentRoutingError extends Error {
46
47
  const ROUTER_SYSTEM_PROMPT = [
47
48
  "You route Pi async sub-agent tasks to the best configured subagentType.",
48
49
  "Choose exactly one allowed type for each task. Use the allowed type descriptions as the source of truth.",
49
- "Prefer a matching project specialist. Otherwise use research for reading/evidence or focused review questions, implement for code/docs/tests/UI changes, verify for running checks, browser-qa for real-browser testing, and frontier-review for an independent post-implementation code review when that type is allowed. Oracle is a deliberate strong second opinion, not routine code review or the default for difficult work.",
50
+ "Prefer a matching project specialist. Otherwise use research for reading/evidence or focused review questions, implement for code/docs/tests/UI changes, verify for running checks, ui-qa for real user-interface testing across browsers, TUIs, and desktop GUIs, and frontier-review for an independent post-implementation code review when that type is allowed. Oracle is a deliberate strong second opinion, not routine code review or the default for difficult work.",
50
51
  "Return only strict JSON with this shape: {\"routes\":[{\"id\":\"task-id\",\"subagentType\":\"type\"}]}",
51
52
  "Do not include markdown, comments, explanations, or unknown types.",
52
53
  ].join("\n");
@@ -59,9 +60,11 @@ export async function routeSubagentTasks(
59
60
  ): Promise<RoutedSubagentTasks> {
60
61
  if (signal?.aborted) throw new Error("Aborted");
61
62
  // Validate even the fast path: an explicit typo must not bypass profiles.
62
- tasks = tasks.map((task) => hasText(task.subagentType) && task.subagentType !== task.subagentType.trim()
63
- ? { ...task, subagentType: task.subagentType.trim() }
64
- : task);
63
+ tasks = tasks.map((task) => {
64
+ if (!hasText(task.subagentType)) return task;
65
+ const subagentType = normalizeSubagentType(task.subagentType, config);
66
+ return subagentType !== task.subagentType ? { ...task, subagentType } : task;
67
+ });
65
68
  const parentModel = currentModelRef(ctx.model);
66
69
  const effectiveConfig = filterSubagentConfigForParentModel(config, parentModel);
67
70
  const invalidTasks = tasks.filter((task) => hasText(task.subagentType)
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { selectSuitableToolsForModel } from "../../lib/tool-args.js";
6
- import { BROWSER_QA_RUNNER_ENV, getBrowserQaRunnerPath } from "./browser-qa.js";
6
+ import { BROWSER_QA_RUNNER_ENV, getBrowserQaRunnerPath, getUiQaRunnerPath, isUiQaType, UI_QA_RUNNER_ENV } from "./browser-qa.js";
7
7
  import { validateBasename } from "./paths.js";
8
8
  import { getPiInvocation } from "./pi-invocation.js";
9
9
  import { writePromptFile } from "./prompt.js";
@@ -20,12 +20,11 @@ export interface SpawnAgentOptions {
20
20
  parentSession?: string;
21
21
  timeoutMs?: number;
22
22
  maxResultBytes?: number;
23
- /** Explicit skills to load after disabling normal skill discovery. */
24
- isolatedSkills?: string[];
25
23
  }
26
24
 
27
25
  export const DEFAULT_AGENT_TIMEOUT_MS = 30 * 60 * 1000;
28
26
  const BROWSER_QA_WORKSPACE_DIR = "browser-qa";
27
+ const UI_QA_WORKSPACE_DIR = "ui-qa";
29
28
  const SUBAGENT_AGENT_DIR_ENV = "PI_SUBAGENT_AGENT_DIR";
30
29
  const AGENT_TIMEOUT_EXIT_CODE = 124;
31
30
  const AGENT_TIMEOUT_KILL_GRACE_MS = 5_000;
@@ -50,7 +49,7 @@ export function spawnAgent(
50
49
  validateBasename(task.id, "task.id");
51
50
  const agentDir = path.join(runDir, task.id);
52
51
  fs.mkdirSync(agentDir, { recursive: true });
53
- prepareBrowserQaWorkspace(agentDir, task.subagentType);
52
+ prepareUiQaWorkspace(agentDir, task.subagentType);
54
53
 
55
54
  // Clean previous state when reusing a run directory/agent id.
56
55
  for (const f of [
@@ -89,10 +88,9 @@ export function spawnAgent(
89
88
  // detached subprocesses do not depend on persisted local pi settings.
90
89
  const persistSessions = shouldPersistSubagentSessions();
91
90
  const sessionDir = persistSessions ? getAgentSessionDir(agentDir) : undefined;
92
- // QA instructions now live in the agent prompt, but normal skill discovery
93
- // must stay disabled even when no additional skill is configured.
94
- const isolateSkills = task.subagentType === "browser-qa" || Boolean(options.isolatedSkills?.length);
95
- const forwardedExtraArgs = isolateSkills ? withoutSkillArgs(extraArgs) : extraArgs;
91
+ // Sub-agent roles are self-contained. Never inherit/discover skills, and do
92
+ // not allow profile/user extra args to re-enable or inject them.
93
+ const forwardedExtraArgs = withoutSkillArgs(extraArgs);
96
94
  if (sessionDir) fs.mkdirSync(sessionDir, { recursive: true });
97
95
  const piArgs: string[] = ["--mode", "rpc"];
98
96
  if (sessionDir) piArgs.push("--session-dir", sessionDir);
@@ -101,16 +99,15 @@ export function spawnAgent(
101
99
  piArgs.push("--extension", getModelToolsExtensionPath());
102
100
  // Preserve `--no-extensions` unless this invocation explicitly selects an
103
101
  // Antigravity model. Environment/default models do not opt the provider in.
104
- if (usesAntigravityModel(task.model, forwardedExtraArgs)) {
102
+ const configuredModel = task.model || getEnvModel();
103
+ const selectedModel = resolveSelectedModel(configuredModel, forwardedExtraArgs);
104
+ const explicitModel = resolveSelectedModel(task.model, forwardedExtraArgs);
105
+ if (explicitModel?.startsWith("antigravity/") && explicitModel.length > "antigravity/".length) {
105
106
  piArgs.push("--extension", getAntigravityAuthExtensionPath());
106
107
  }
107
- if (isolateSkills) {
108
- piArgs.push("--no-skills");
109
- for (const skillPath of options.isolatedSkills ?? []) piArgs.push("--skill", skillPath);
110
- }
111
- const envModel = task.model || getEnvModel();
112
- if (envModel) piArgs.push("--model", envModel);
113
- const selectedTools = task.tools ? filterSubagentTools(selectSuitableToolsForModel(envModel, task.tools)) : undefined;
108
+ piArgs.push("--no-skills");
109
+ if (configuredModel) piArgs.push("--model", configuredModel);
110
+ const selectedTools = task.tools ? filterSubagentTools(selectSuitableToolsForModel(selectedModel, task.tools)) : undefined;
114
111
  if (selectedTools) {
115
112
  if (selectedTools.length > 0) piArgs.push("--tools", selectedTools.join(","));
116
113
  else piArgs.push("--no-tools");
@@ -119,6 +116,10 @@ export function spawnAgent(
119
116
 
120
117
  // User-supplied extra args (e.g. --thinking high)
121
118
  piArgs.push(...forwardedExtraArgs);
119
+ // The child always runs one explicitly selected model. Override persisted
120
+ // enabledModels (and any extra --models value) so isolated children do not
121
+ // resolve unrelated provider patterns before their limited extensions load.
122
+ if (selectedModel) piArgs.push("--models", selectedModel);
122
123
  // Keep recursive/interactive parent-only tools disabled even if explicit
123
124
  // sub-agent extraArgs load additional extensions or override --tools.
124
125
  piArgs.push("--extension", getSubagentToolGuardExtensionPath());
@@ -151,7 +152,7 @@ export function spawnAgent(
151
152
 
152
153
  const proc = spawn(invocation.command, invocation.args, {
153
154
  cwd,
154
- env: subagentEnvironment(process.env, task.subagentType === "browser-qa" ? agentDir : undefined),
155
+ env: subagentEnvironment(process.env, isUiQaType(task.subagentType) ? agentDir : undefined),
155
156
  stdio: ["pipe", "pipe", "pipe"],
156
157
  detached: process.platform !== "win32",
157
158
  });
@@ -477,8 +478,8 @@ function withoutSkillArgs(args: string[]): string[] {
477
478
  return filtered;
478
479
  }
479
480
 
480
- function usesAntigravityModel(taskModel: string | undefined, extraArgs: string[]): boolean {
481
- let model = taskModel?.trim() || undefined;
481
+ function resolveSelectedModel(configuredModel: string | undefined, extraArgs: string[]): string | undefined {
482
+ let model = configuredModel?.trim() || undefined;
482
483
  for (let index = 0; index < extraArgs.length; index += 1) {
483
484
  const arg = extraArgs[index];
484
485
  if (arg === "--model" || arg === "-m") {
@@ -490,7 +491,7 @@ function usesAntigravityModel(taskModel: string | undefined, extraArgs: string[]
490
491
  model = arg.slice("--model=".length).trim() || undefined;
491
492
  }
492
493
  }
493
- return Boolean(model?.startsWith("antigravity/") && model.length > "antigravity/".length);
494
+ return model;
494
495
  }
495
496
 
496
497
  function getModelToolsExtensionPath(): string {
@@ -821,21 +822,30 @@ function subagentEnvironment(env: NodeJS.ProcessEnv, agentDir?: string): NodeJS.
821
822
  };
822
823
  delete result[SUBAGENT_AGENT_DIR_ENV];
823
824
  delete result[BROWSER_QA_RUNNER_ENV];
825
+ delete result[UI_QA_RUNNER_ENV];
824
826
  if (agentDir) {
825
827
  result[SUBAGENT_AGENT_DIR_ENV] = fs.realpathSync(agentDir);
826
828
  result[BROWSER_QA_RUNNER_ENV] = getBrowserQaRunnerPath();
829
+ result[UI_QA_RUNNER_ENV] = getUiQaRunnerPath();
827
830
  }
828
831
  return result;
829
832
  }
830
833
 
831
- function prepareBrowserQaWorkspace(agentDir: string, subagentType: string | undefined): void {
832
- const workspace = path.join(agentDir, BROWSER_QA_WORKSPACE_DIR);
834
+ function prepareUiQaWorkspace(agentDir: string, subagentType: string | undefined): void {
835
+ const workspace = path.join(agentDir, UI_QA_WORKSPACE_DIR);
836
+ const browserWorkspace = path.join(agentDir, BROWSER_QA_WORKSPACE_DIR);
833
837
  fs.rmSync(workspace, { recursive: true, force: true });
834
- if (subagentType !== "browser-qa") return;
835
- const flows = path.join(workspace, "flows");
838
+ fs.rmSync(browserWorkspace, { recursive: true, force: true });
839
+ if (!isUiQaType(subagentType)) return;
840
+ const flows = path.join(browserWorkspace, "flows");
841
+ const uiFlows = path.join(workspace, "flows");
842
+ fs.mkdirSync(workspace, { recursive: true, mode: 0o700 });
843
+ fs.mkdirSync(uiFlows, { recursive: true, mode: 0o700 });
836
844
  fs.mkdirSync(flows, { recursive: true, mode: 0o700 });
837
845
  if (process.platform !== "win32") {
838
846
  fs.chmodSync(workspace, 0o700);
847
+ fs.chmodSync(uiFlows, 0o700);
848
+ fs.chmodSync(browserWorkspace, 0o700);
839
849
  fs.chmodSync(flows, 0o700);
840
850
  }
841
851
  }