pi-ui-extend 1.0.41 → 1.0.42

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 (124) hide show
  1. package/README.md +10 -2
  2. package/dist/app/app.d.ts +3 -0
  3. package/dist/app/app.js +26 -0
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.js +6 -1
  6. package/dist/app/commands/command-registry.js +1 -1
  7. package/dist/app/commands/command-session-actions.js +9 -3
  8. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  9. package/dist/app/commands/reload-context-inventory.js +59 -0
  10. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  11. package/dist/app/extensions/extension-actions-controller.js +6 -0
  12. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  13. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  14. package/dist/app/rendering/editor-panels.js +10 -4
  15. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  16. package/dist/app/session/session-lifecycle-controller.js +6 -0
  17. package/dist/app/session/tabs-controller.d.ts +1 -0
  18. package/dist/app/session/tabs-controller.js +19 -1
  19. package/dist/app/subagents/subagents-files.js +60 -1
  20. package/dist/app/subagents/subagents-model.d.ts +1 -0
  21. package/dist/app/subagents/subagents-model.js +18 -2
  22. package/dist/app/types.d.ts +5 -0
  23. package/dist/default-pix-config.js +10 -0
  24. package/dist/schemas/index.d.ts +1 -0
  25. package/dist/schemas/index.js +1 -0
  26. package/dist/schemas/pi-tools-suite-schema.d.ts +80 -58
  27. package/dist/schemas/pi-tools-suite-schema.js +43 -78
  28. package/dist/schemas/pix-schema.d.ts +7 -0
  29. package/dist/schemas/pix-schema.js +14 -0
  30. package/dist/schemas/tasks-schema.d.ts +18 -0
  31. package/dist/schemas/tasks-schema.js +41 -0
  32. package/docs/concurrency.md +6 -0
  33. package/docs/desktop-mvp.md +12 -1
  34. package/docs/desktop-task-manager.md +93 -82
  35. package/external/pi-tools-suite/README.md +37 -50
  36. package/external/pi-tools-suite/docs/browser-qa-subagent.md +19 -0
  37. package/external/pi-tools-suite/docs/evals.md +1 -1
  38. package/external/pi-tools-suite/docs/session-recovery.md +40 -14
  39. package/external/pi-tools-suite/docs/subagent-model-pools.md +38 -27
  40. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  42. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +14 -70
  44. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  46. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  47. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -3
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +122 -312
  49. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +15 -2
  50. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +6 -2
  51. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  52. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  53. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  54. package/external/pi-tools-suite/src/async-subagents/lib.ts +7 -8
  55. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -4
  56. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  57. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  58. package/external/pi-tools-suite/src/config.ts +41 -1
  59. package/external/pi-tools-suite/src/context-gateway/config.ts +11 -2
  60. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +102 -0
  61. package/external/pi-tools-suite/src/context-gateway/index.ts +73 -19
  62. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +3 -3
  63. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  64. package/external/pi-tools-suite/src/context-gateway/types.ts +9 -3
  65. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  66. package/external/pi-tools-suite/src/dcp/auto-compress.ts +96 -44
  67. package/external/pi-tools-suite/src/dcp/commands.ts +18 -0
  68. package/external/pi-tools-suite/src/dcp/compress-tool.ts +170 -45
  69. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  70. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  71. package/external/pi-tools-suite/src/dcp/config.ts +56 -13
  72. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  73. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  74. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +1 -1
  75. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  76. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  77. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +13 -9
  78. package/external/pi-tools-suite/src/index.ts +7 -1
  79. package/external/pi-tools-suite/src/resource-registry/index.ts +2290 -0
  80. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  81. package/external/pi-tools-suite/src/tool-descriptions.ts +3 -4
  82. package/package.json +4 -2
  83. package/schemas/pi-tools-suite.json +427 -280
  84. package/schemas/pix.json +24 -0
  85. package/schemas/tasks.json +132 -0
  86. package/skills/spec-wiki/SKILL.md +55 -0
  87. package/skills/spec-wiki/evals/evals.json +132 -0
  88. package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +14 -0
  89. package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +4 -0
  90. package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +9 -0
  91. package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +17 -0
  92. package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +12 -0
  93. package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +11 -0
  94. package/skills/spec-wiki/evals/files/focused/src/session.ts +5 -0
  95. package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +17 -0
  96. package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +2 -0
  97. package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +3 -0
  98. package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +18 -0
  99. package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +5 -0
  100. package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +24 -0
  101. package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +7 -0
  102. package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +7 -0
  103. package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +17 -0
  104. package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +3 -0
  105. package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +3 -0
  106. package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +4 -0
  107. package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +9 -0
  108. package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +15 -0
  109. package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +6 -0
  110. package/skills/spec-wiki/evals/maintenance_regression.py +311 -0
  111. package/skills/spec-wiki/evals/retrieval-eval.md +37 -0
  112. package/skills/spec-wiki/evals/retrieval-evals.json +37 -0
  113. package/skills/spec-wiki/evals/trigger-evals.json +94 -0
  114. package/skills/spec-wiki/references/bootstrap.md +100 -0
  115. package/skills/spec-wiki/references/focused-spec.md +117 -0
  116. package/skills/spec-wiki/references/index-schema.md +330 -0
  117. package/skills/spec-wiki/references/maintenance.md +134 -0
  118. package/skills/spec-wiki/references/query.md +74 -0
  119. package/skills/spec-wiki/scripts/spec_wiki.py +2076 -0
  120. package/docs/desktop-markdown-media.md +0 -77
  121. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  122. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  123. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  124. package/skills/spec-lite/SKILL.md +0 -140
@@ -1,10 +1,8 @@
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";
8
6
  import type { AgentTask, RetryConfig } from "./types.js";
9
7
 
10
8
  export interface ModelByParentEntry {
@@ -36,6 +34,16 @@ export interface SubagentTypeConfig {
36
34
  * Oracle alone keeps parent-aware selection ahead of presets.
37
35
  */
38
36
  modelByParent?: Record<string, ModelByParentEntry>;
37
+ /**
38
+ * Optional parent-model allow-list. When present, this role is exposed only
39
+ * when the current parent model matches at least one pattern.
40
+ */
41
+ forParentModels?: string[];
42
+ /**
43
+ * Optional parent-model deny-list. A match hides the role even when
44
+ * forParentModels also matches.
45
+ */
46
+ notForParentModels?: string[];
39
47
  thinking?: string;
40
48
  tools?: string[];
41
49
  /** Explicit skill files loaded after disabling normal skill discovery. */
@@ -125,13 +133,6 @@ 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[];
@@ -192,82 +193,51 @@ export const DEFAULT_ROUTING_CONFIG: ResolvedSubagentRoutingConfig = {
192
193
  fallbackModels: ["openai-codex/gpt-5.6-luna"],
193
194
  maxTaskChars: 1200,
194
195
  maxTokens: 512,
195
- maxRetries: 3,
196
- timeoutMs: 10_000,
196
+ maxRetries: 1,
197
+ timeoutMs: 12_000,
197
198
  debug: false,
198
199
  };
199
200
 
200
201
  const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "agents");
202
+ const BUILTIN_PRESETS_FILE = path.join(BUILTIN_AGENTS_DIR, "presets.jsonc");
203
+ const DEFAULT_BLIND_MODEL_PATTERNS = [
204
+ "zai/glm-4.5*", "glm-4.5*", "*/glm-4.5*",
205
+ "zai/glm-5-turbo*", "glm-5-turbo*", "*/glm-5-turbo*",
206
+ "zai/glm-5.3", "glm-5.3", "*/glm-5.3",
207
+ ];
201
208
 
202
209
  const BUILTIN_CONFIG: SubagentConfig = {
203
210
  defaultType: "research",
204
211
  maxConcurrent: DEFAULT_MAX_CONCURRENT,
212
+ maxResultBytes: 100_000,
205
213
  routing: { ...DEFAULT_ROUTING_CONFIG },
214
+ vision: { blindModelPatterns: DEFAULT_BLIND_MODEL_PATTERNS },
215
+ presets: readPresetConfigFile(BUILTIN_PRESETS_FILE).presets,
206
216
  types: normalizeAgentDefinitions(readAgentDefinitionsFromDir(BUILTIN_AGENTS_DIR)),
207
217
  };
208
218
 
209
219
  export function loadSubagentConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): SubagentConfig {
210
220
  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
- }
221
+ mergeConfig(config, projectPresetConfig(cwd));
222
+ // Project-local agent definitions (.pi/agents/*.md) are the only project
223
+ // source of role/profile configuration and are loaded fresh on every call.
224
+ mergeConfig(config, projectAgentTypes(cwd));
223
225
  applyEnvModelOverrides(config, env);
224
226
  applyEnvRoutingOverrides(config, env);
227
+ applyEnvRuntimeOverrides(config, env);
225
228
  return config;
226
229
  }
227
230
 
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
231
  export function getBuiltinSubagentDefinitionsDir(): string {
252
232
  return BUILTIN_AGENTS_DIR;
253
233
  }
254
234
 
255
- export function getSubagentConfigInitTargetPath(cwd: string, env: NodeJS.ProcessEnv = process.env): string {
256
- return explicitSubagentConfigPath(cwd, env) ?? getDefaultSubagentConfigPath();
235
+ export function getBuiltinSubagentPresetsPath(): string {
236
+ return BUILTIN_PRESETS_FILE;
257
237
  }
258
238
 
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);
263
- }
264
-
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));
239
+ export function getProjectSubagentPresetsPath(cwd: string): string {
240
+ return path.join(projectAgentsDir(cwd) ?? path.join(path.resolve(cwd), ".pi", "agents"), "presets.jsonc");
271
241
  }
272
242
 
273
243
  /** Normalize `.pi/agents/*.md` definitions through the shared type-profile path. */
@@ -278,6 +248,13 @@ function projectAgentTypes(cwd: string): Partial<SubagentConfig> {
278
248
  return { types };
279
249
  }
280
250
 
251
+ function projectPresetConfig(cwd: string): Partial<SubagentConfig> {
252
+ const dir = projectAgentsDir(cwd);
253
+ if (!dir) return {};
254
+ const file = path.join(dir, "presets.jsonc");
255
+ return fs.existsSync(file) ? readPresetConfigFile(file) : {};
256
+ }
257
+
281
258
  function normalizeAgentDefinitions(definitions: Record<string, AgentDefinition>): Record<string, SubagentTypeConfig> {
282
259
  const types: Record<string, SubagentTypeConfig> = {};
283
260
  for (const [name, definition] of Object.entries(definitions)) {
@@ -286,42 +263,6 @@ function normalizeAgentDefinitions(definitions: Record<string, AgentDefinition>)
286
263
  return types;
287
264
  }
288
265
 
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
266
  export function resolveAgentTaskConfig(
326
267
  task: AgentTask,
327
268
  config: SubagentConfig,
@@ -445,10 +386,11 @@ export function shouldForceCurrentSubagentModel(env: NodeJS.ProcessEnv = process
445
386
  }
446
387
 
447
388
  export function currentModelRef(model: unknown): string | undefined {
389
+ if (typeof model === "string") return trimString(model);
448
390
  if (!isRecord(model)) return undefined;
449
- const id = trimString(model.id);
391
+ const id = trimString(model.modelId) || trimString(model.id) || trimString(model.model) || trimString(model.name);
450
392
  if (!id) return undefined;
451
- const provider = trimString(model.provider);
393
+ const provider = trimString(model.provider) || trimString(model.providerId);
452
394
  return provider && !id.includes("/") ? `${provider}/${id}` : id;
453
395
  }
454
396
 
@@ -457,93 +399,67 @@ export function isBlindModelRef(modelRef: string | undefined, config: SubagentCo
457
399
  return matchesAnyModelPattern(modelRef, config.vision?.blindModelPatterns ?? []);
458
400
  }
459
401
 
460
- export function selectSubagentType(task: AgentTask, config: SubagentConfig): string | undefined {
461
- const explicit = trimString(task.subagentType);
462
- if (explicit) return explicit;
463
- return defaultSubagentType(config);
402
+ /** Whether one role is available to the current parent model. Deny wins. */
403
+ export function isSubagentTypeAvailableForParent(
404
+ profile: SubagentTypeConfig,
405
+ parentModelRef: string | undefined,
406
+ ): boolean {
407
+ const included = profile.forParentModels;
408
+ if (included !== undefined) {
409
+ if (!parentModelRef || !matchesAnyModelPattern(parentModelRef, included)) return false;
410
+ }
411
+ const excluded = profile.notForParentModels;
412
+ if (parentModelRef && excluded !== undefined && matchesAnyModelPattern(parentModelRef, excluded)) return false;
413
+ return true;
464
414
  }
465
415
 
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);
416
+ /**
417
+ * Return the effective config visible to a particular parent model. Only the
418
+ * role catalog is filtered; presets, routing policy, retry defaults, etc. are
419
+ * preserved unchanged.
420
+ */
421
+ export function filterSubagentConfigForParentModel(
422
+ config: SubagentConfig,
423
+ parentModelRef: string | undefined,
424
+ ): SubagentConfig {
425
+ const types = Object.fromEntries(
426
+ Object.entries(config.types).filter(([, profile]) => isSubagentTypeAvailableForParent(profile, parentModelRef)),
427
+ );
428
+ return Object.keys(types).length === Object.keys(config.types).length ? config : { ...config, types };
471
429
  }
472
430
 
473
- function readPiToolsSuiteSubagentConfig(file: string): Partial<SubagentConfig> {
474
- const raw = fs.readFileSync(file, "utf-8");
475
- 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) : {};
431
+ export function selectSubagentType(task: AgentTask, config: SubagentConfig): string | undefined {
432
+ const explicit = trimString(task.subagentType);
433
+ if (explicit) return explicit;
434
+ return defaultSubagentType(config);
479
435
  }
480
436
 
481
- function hasPiToolsSuiteSubagentConfig(file: string): boolean {
437
+ function readPresetConfigFile(file: string): Partial<SubagentConfig> {
482
438
  const raw = fs.readFileSync(file, "utf-8");
483
439
  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);
440
+ if (!isRecord(parsed)) throw new Error(`Subagent presets file must contain an object: ${file}`);
441
+ return { presets: normalizePresetMap(parsed, file, "preset file") };
442
+ }
443
+
444
+ function normalizePresetMap(value: unknown, file: string, owner: string): Record<string, SubagentPreset> {
445
+ if (!isRecord(value)) throw new Error(`Subagent ${owner} must be an object: ${file}`);
446
+ const presets: Record<string, SubagentPreset> = {};
447
+ for (const [name, rawPreset] of Object.entries(value)) {
448
+ if (!isRecord(rawPreset)) throw new Error(`Subagent preset "${name}" must be an object: ${file}`);
449
+ const models = normalizeModels(rawPreset.models, `preset "${name}"`, file);
450
+ if (models === undefined) throw new Error(`Subagent preset "${name}" must define models: ${file}`);
451
+ presets[name] = {
452
+ description: trimString(rawPreset.description),
453
+ models,
454
+ };
538
455
  }
539
- output.types = types;
540
- return output;
456
+ return presets;
541
457
  }
542
458
 
543
459
  /**
544
460
  * 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.
461
+ * an agent Markdown definition. Bundled and project files share the same
462
+ * validation and trimming path.
547
463
  */
548
464
  export function normalizeSubagentTypeProfile(
549
465
  rawProfile: Record<string, unknown>,
@@ -558,6 +474,8 @@ export function normalizeSubagentTypeProfile(
558
474
  model: models === undefined ? trimString(rawProfile.model) : undefined,
559
475
  fallbackModels: models === undefined ? modelList(rawProfile.fallbackModels, rawProfile.fallbackModel) : undefined,
560
476
  modelByParent: models === undefined ? normalizeModelByParent(rawProfile.modelByParent, name, file) : undefined,
477
+ forParentModels: normalizeParentModelPatterns(rawProfile.forParentModels, "forParentModels", name, file),
478
+ notForParentModels: normalizeParentModelPatterns(rawProfile.notForParentModels, "notForParentModels", name, file),
561
479
  thinking: trimString(rawProfile.thinking),
562
480
  tools: arrayOfStrings(rawProfile.tools),
563
481
  isolatedSkills: arrayOfStrings(rawProfile.isolatedSkills),
@@ -571,13 +489,6 @@ export function normalizeSubagentTypeProfile(
571
489
  }
572
490
 
573
491
  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
492
  for (const [name, profile] of Object.entries(source.types ?? {})) {
582
493
  target.types[name] = mergeTypeProfile(target.types[name] ?? {}, profile);
583
494
  }
@@ -585,17 +496,12 @@ function mergeConfig(target: SubagentConfig, source: Partial<SubagentConfig>): v
585
496
  target.presets = target.presets ?? {};
586
497
  const previous = target.presets[name] ?? {};
587
498
  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
- }
499
+ delete merged.model;
500
+ delete merged.fallbackModels;
501
+ delete merged.types;
502
+ delete merged.thinking;
503
+ delete merged.extraArgs;
504
+ delete merged.timeoutMs;
599
505
  target.presets[name] = merged;
600
506
  }
601
507
  }
@@ -618,36 +524,6 @@ function mergeTypeProfile(base: SubagentTypeConfig, source: SubagentTypeConfig):
618
524
  return merged;
619
525
  }
620
526
 
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
527
  function matchesAnyModelPattern(modelRef: string, patterns: string[]): boolean {
652
528
  return patterns.some((pattern) => modelPatternRegExp(pattern).test(modelRef));
653
529
  }
@@ -665,6 +541,8 @@ function compactProfile(profile: SubagentTypeConfig): SubagentTypeConfig {
665
541
  if (profile.model) compact.model = profile.model;
666
542
  if (profile.fallbackModels) compact.fallbackModels = profile.fallbackModels;
667
543
  if (profile.modelByParent) compact.modelByParent = profile.modelByParent;
544
+ if (profile.forParentModels !== undefined) compact.forParentModels = profile.forParentModels;
545
+ if (profile.notForParentModels !== undefined) compact.notForParentModels = profile.notForParentModels;
668
546
  if (profile.thinking) compact.thinking = profile.thinking;
669
547
  if (profile.tools && profile.tools.length > 0) compact.tools = profile.tools;
670
548
  if (profile.isolatedSkills && profile.isolatedSkills.length > 0) compact.isolatedSkills = profile.isolatedSkills;
@@ -690,34 +568,6 @@ function compactPreset(preset: SubagentPreset): SubagentPreset {
690
568
  return compact;
691
569
  }
692
570
 
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
571
  function normalizeModels(value: unknown, owner: string, file: string): string[] | undefined {
722
572
  if (value === undefined) return undefined;
723
573
  if (!Array.isArray(value) || value.some((ref) => typeof ref !== "string" || !/^[^\s/*]+\/[^\s*]+$/.test(ref.trim()))) {
@@ -726,6 +576,19 @@ function normalizeModels(value: unknown, owner: string, file: string): string[]
726
576
  return [...new Set(value.map((ref: string) => ref.trim()))];
727
577
  }
728
578
 
579
+ function normalizeParentModelPatterns(
580
+ value: unknown,
581
+ field: "forParentModels" | "notForParentModels",
582
+ typeName: string,
583
+ file: string,
584
+ ): string[] | undefined {
585
+ if (value === undefined) return undefined;
586
+ if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string" || !pattern.trim())) {
587
+ throw new Error(`Subagent type "${typeName}" ${field} must be an array of non-empty model patterns: ${file}`);
588
+ }
589
+ return [...new Set(value.map((pattern: string) => pattern.trim()))];
590
+ }
591
+
729
592
  function normalizeModelByParent(value: unknown, typeName: string, file: string): Record<string, ModelByParentEntry> | undefined {
730
593
  if (value === undefined || value === null) return undefined;
731
594
  if (!isRecord(value)) throw new Error(`Subagent type "${typeName}" modelByParent must be an object: ${file}`);
@@ -806,55 +669,23 @@ function applyEnvRoutingOverrides(config: SubagentConfig, env: NodeJS.ProcessEnv
806
669
  config.routing = routing;
807
670
  }
808
671
 
809
- function typeEnvKey(typeName: string): string {
810
- return typeName.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
672
+ function applyEnvRuntimeOverrides(config: SubagentConfig, env: NodeJS.ProcessEnv): void {
673
+ const maxConcurrent = finiteEnvNumber(env.PI_SUBAGENTS_MAX_CONCURRENT || env.ASYNC_SUBAGENTS_MAX_CONCURRENT);
674
+ if (maxConcurrent !== undefined) config.maxConcurrent = Math.max(0, Math.round(maxConcurrent));
675
+ const maxResultBytes = finiteEnvNumber(env.PI_SUBAGENTS_MAX_RESULT_BYTES || env.ASYNC_SUBAGENTS_MAX_RESULT_BYTES);
676
+ if (maxResultBytes !== undefined) config.maxResultBytes = Math.max(0, Math.round(maxResultBytes));
677
+ const timeoutMs = finiteEnvNumber(env.PI_SUBAGENTS_TIMEOUT_MS || env.ASYNC_SUBAGENTS_TIMEOUT_MS);
678
+ if (timeoutMs !== undefined) config.timeoutMs = Math.max(1, Math.round(timeoutMs));
811
679
  }
812
680
 
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");
681
+ function typeEnvKey(typeName: string): string {
682
+ return typeName.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
848
683
  }
849
684
 
850
685
  function cloneConfig(config: SubagentConfig): SubagentConfig {
851
686
  return JSON.parse(JSON.stringify(config)) as SubagentConfig;
852
687
  }
853
688
 
854
- function expandHome(value: string): string {
855
- return value === "~" || value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
856
- }
857
-
858
689
  function trimString(value: unknown): string | undefined {
859
690
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
860
691
  }
@@ -874,10 +705,6 @@ function finiteEnvNumber(value: unknown): number | undefined {
874
705
  return Number.isFinite(parsed) ? parsed : undefined;
875
706
  }
876
707
 
877
- function isNodeError(error: unknown): error is NodeJS.ErrnoException {
878
- return error instanceof Error && "code" in error;
879
- }
880
-
881
708
  function arrayOfStrings(value: unknown): string[] | undefined {
882
709
  if (!Array.isArray(value)) return undefined;
883
710
  const items = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
@@ -899,23 +726,6 @@ function modelList(...values: unknown[]): string[] | undefined {
899
726
  return models.length > 0 || values.some(Array.isArray) ? models : undefined;
900
727
  }
901
728
 
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
729
  function textBlock(value: unknown): string | undefined {
920
730
  if (typeof value === "string") return trimString(value);
921
731
  const lines = arrayOfStrings(value);
@@ -4,6 +4,7 @@ import type { AgentTask } from "./types.js";
4
4
  import {
5
5
  currentModelRef,
6
6
  defaultSubagentType,
7
+ filterSubagentConfigForParentModel,
7
8
  resolveSubagentRoutingConfig,
8
9
  type ResolvedSubagentRoutingConfig,
9
10
  type SubagentConfig,
@@ -45,7 +46,7 @@ export class SubagentRoutingError extends Error {
45
46
  const ROUTER_SYSTEM_PROMPT = [
46
47
  "You route Pi async sub-agent tasks to the best configured subagentType.",
47
48
  "Choose exactly one allowed type for each task. Use the allowed type descriptions as the source of truth.",
48
- "Prefer a matching project specialist. Otherwise use research for reading/review and evidence, implement for code/docs/tests/UI changes, verify for running checks, browser-qa for real-browser testing. Oracle is a deliberate strong second opinion, not the default for difficult work.",
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.",
49
50
  "Return only strict JSON with this shape: {\"routes\":[{\"id\":\"task-id\",\"subagentType\":\"type\"}]}",
50
51
  "Do not include markdown, comments, explanations, or unknown types.",
51
52
  ].join("\n");
@@ -61,11 +62,23 @@ export async function routeSubagentTasks(
61
62
  tasks = tasks.map((task) => hasText(task.subagentType) && task.subagentType !== task.subagentType.trim()
62
63
  ? { ...task, subagentType: task.subagentType.trim() }
63
64
  : task);
65
+ const parentModel = currentModelRef(ctx.model);
66
+ const effectiveConfig = filterSubagentConfigForParentModel(config, parentModel);
64
67
  const invalidTasks = tasks.filter((task) => hasText(task.subagentType)
65
68
  && !Object.prototype.hasOwnProperty.call(config.types, task.subagentType));
66
69
  if (invalidTasks.length > 0) {
67
- throw routingError(`Unknown subagentType: ${invalidTasks.map((task) => `${task.id}=${JSON.stringify(task.subagentType)}`).join(", ")}.`, invalidTasks, config);
70
+ throw routingError(`Unknown subagentType: ${invalidTasks.map((task) => `${task.id}=${JSON.stringify(task.subagentType)}`).join(", ")}.`, invalidTasks, effectiveConfig);
71
+ }
72
+ const unavailableTasks = tasks.filter((task) => hasText(task.subagentType)
73
+ && !Object.prototype.hasOwnProperty.call(effectiveConfig.types, task.subagentType));
74
+ if (unavailableTasks.length > 0) {
75
+ throw routingError(
76
+ `subagentType unavailable for parent model ${parentModel ?? "(unknown)"}: ${unavailableTasks.map((task) => `${task.id}=${JSON.stringify(task.subagentType)}`).join(", ")}.`,
77
+ unavailableTasks,
78
+ effectiveConfig,
79
+ );
68
80
  }
81
+ config = effectiveConfig;
69
82
  const autoTasks = tasks.filter((task) => !hasText(task.subagentType));
70
83
  if (autoTasks.length === 0) return { tasks, usedLlm: false, routes: {}, warnings: [] };
71
84
 
@@ -696,10 +696,14 @@ function compactRpcEventForProgress(event: RpcEventRecord): Record<string, unkno
696
696
  toolName: typeof event.toolName === "string" ? event.toolName : undefined,
697
697
  });
698
698
  }
699
- if (event.type === "message_end") {
699
+ if (event.type === "message_start" || event.type === "message_end") {
700
700
  return stripUndefined({
701
701
  type: event.type,
702
- role: isRecord(event.message) && typeof event.message.role === "string" ? event.message.role : undefined,
702
+ role: typeof event.role === "string"
703
+ ? event.role
704
+ : isRecord(event.message) && typeof event.message.role === "string"
705
+ ? event.message.role
706
+ : undefined,
703
707
  stopReason: isRecord(event.message) && typeof event.message.stopReason === "string" ? event.message.stopReason : undefined,
704
708
  });
705
709
  }