pi-ui-extend 1.0.39 → 1.0.41

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 (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -3,6 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
6
+ import { readAgentDefinitionsFromDir, readProjectAgentDefinitions, type AgentDefinition } from "./agents-dir.js";
6
7
  import { ensurePiToolsSuiteUserConfig, getPiToolsSuiteUserConfigPath } from "../../config.js";
7
8
  import type { AgentTask, RetryConfig } from "./types.js";
8
9
 
@@ -15,14 +16,24 @@ export interface ModelByParentEntry {
15
16
 
16
17
  export interface SubagentTypeConfig {
17
18
  description?: string;
19
+ /**
20
+ * Agent icon name resolved by UIs (pix TUI icon themes, Pix Desktop lucide
21
+ * icons). Opaque pass-through here: unknown names render as the neutral
22
+ * default agent icon.
23
+ */
24
+ icon?: string;
25
+ /** Ranked candidates. The preset filters availability, never changes this order. */
26
+ models?: string[];
27
+ /** Legacy primary candidate; new profiles use models. */
18
28
  model?: string;
19
- /** Ordered model fallbacks used when the selected model hits quota/rate limits. */
29
+ /** Legacy candidates after model; new profiles use one ordered models list. */
20
30
  fallbackModels?: string[];
21
31
  /**
22
32
  * Parent-model-aware model selection. Keys are glob model refs (e.g. "zai/*")
23
33
  * matched against the current parent model; the first matching key wins.
24
34
  * Values may be a model ref string or { model, fallbackModels? }.
25
- * Resolved after explicit task.model / forcedModel, before preset/static model.
35
+ * Ordinary roles: explicit task/forced model and preset models take priority.
36
+ * Oracle alone keeps parent-aware selection ahead of presets.
26
37
  */
27
38
  modelByParent?: Record<string, ModelByParentEntry>;
28
39
  thinking?: string;
@@ -70,6 +81,9 @@ export interface SubagentVisionConfig {
70
81
 
71
82
  export interface SubagentPreset {
72
83
  description?: string;
84
+ /** Available model pool. Agent candidate order wins; [] allows no models. */
85
+ models?: string[];
86
+ /** Legacy default model; prefer models for new presets. */
73
87
  model?: string;
74
88
  /** Ordered global model fallbacks used when this preset's selected model hits quota/rate limits. */
75
89
  fallbackModels?: string[];
@@ -92,9 +106,10 @@ export interface SubagentPresetTypeOverride {
92
106
  }
93
107
 
94
108
  export interface SubagentConfig {
109
+ /** Ambiguous-task router hint and legacy resolver default; never a spawn error fallback. */
95
110
  defaultType?: string;
96
111
  types: Record<string, SubagentTypeConfig>;
97
- /** LLM-based role routing for tasks that omit subagentType. */
112
+ /** Fallback LLM role selection for omitted types; explicit valid types bypass it. */
98
113
  routing?: SubagentRoutingConfig;
99
114
  /** Vision capability overrides for parent-model guidance. */
100
115
  vision?: SubagentVisionConfig;
@@ -133,6 +148,13 @@ export interface ResolvedAgentTaskConfig {
133
148
  timeoutMs?: number;
134
149
  }
135
150
 
151
+ export class SubagentModelSelectionError extends Error {
152
+ constructor(taskId: string, message: string) {
153
+ super(`Task ${taskId}: ${message} No agents were launched. Configure a compatible model pool/candidate list or provide an explicit model override.`);
154
+ this.name = "SubagentModelSelectionError";
155
+ }
156
+ }
157
+
136
158
  export interface ResolveAgentTaskOptions {
137
159
  /** Default model for spawned sub-agents when task/profile do not specify one. */
138
160
  model?: string;
@@ -166,7 +188,7 @@ export const DEFAULT_MAX_CONCURRENT = 5;
166
188
  /** Default lightweight LLM router used when subagentType is omitted. */
167
189
  export const DEFAULT_ROUTING_CONFIG: ResolvedSubagentRoutingConfig = {
168
190
  enabled: true,
169
- model: "zai/glm-4.5-air",
191
+ model: "zai/glm-5-turbo",
170
192
  fallbackModels: ["openai-codex/gpt-5.6-luna"],
171
193
  maxTaskChars: 1200,
172
194
  maxTokens: 512,
@@ -175,98 +197,13 @@ export const DEFAULT_ROUTING_CONFIG: ResolvedSubagentRoutingConfig = {
175
197
  debug: false,
176
198
  };
177
199
 
200
+ const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "agents");
201
+
178
202
  const BUILTIN_CONFIG: SubagentConfig = {
203
+ defaultType: "research",
179
204
  maxConcurrent: DEFAULT_MAX_CONCURRENT,
180
205
  routing: { ...DEFAULT_ROUTING_CONFIG },
181
- types: {
182
- quick: {
183
- description: "Use for tiny cheap tasks: answer a simple question, inspect one known file, or verify one fact. Not for broad repo search.",
184
- model: "openai-codex/gpt-5.6-luna",
185
- fallbackModels: ["zai/glm-4.5-air"],
186
- thinking: "off",
187
- },
188
- scan: {
189
- description: "Use for finding files, symbols, text, or inventory across a repo. Return paths/facts; do not judge code quality.",
190
- model: "openai-codex/gpt-5.6-luna",
191
- fallbackModels: ["zai/glm-4.5-air"],
192
- thinking: "off",
193
- },
194
- research: {
195
- description: "Use for multi-file codebase research: read several files and explain how something works. No edits.",
196
- model: "openai-codex/gpt-5.6-terra",
197
- fallbackModels: ["zai/glm-5-turbo"],
198
- thinking: "low",
199
- },
200
- docs: {
201
- description: "Use for documentation work: README/API docs review, docs gaps, changelog, migration notes, examples.",
202
- model: "openai-codex/gpt-5.6-luna",
203
- fallbackModels: ["zai/glm-4.5-air"],
204
- thinking: "low",
205
- },
206
- frontend: {
207
- description: "Use for frontend UI/UX visual work: styling, layout, typography, animation, responsive states, component polish, and accessibility. Avoid backend/business logic unless needed for UI behavior.",
208
- thinking: "medium",
209
- promptAppend: [
210
- "Act as a frontend UI/UX engineer for visual and product-facing work.",
211
- "Prioritize layout, typography, spacing, color, motion, responsive states, accessibility, and consistency with the existing design system.",
212
- "Before editing, inspect nearby components/styles and infer the project's design language. Avoid backend/business-logic changes unless required for UI behavior.",
213
- "When no mockup exists, choose a clear aesthetic direction and explain it briefly. Verify with targeted build/lint/tests or screenshot-relevant checks when possible.",
214
- ].join("\n"),
215
- },
216
- "browser-qa": {
217
- description: "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
218
- model: "zai/glm-5.3-flash",
219
- fallbackModels: ["openai-codex/gpt-5.6-luna"],
220
- thinking: "low",
221
- timeoutMs: 300_000,
222
- tools: ["read", "grep", "bash"],
223
- isolatedSkills: [getBrowserQaSkillPath()],
224
- },
225
- implement: {
226
- description: "Use when the sub-agent should make or plan code changes for a feature, bug fix, or refactor.",
227
- modelByParent: {
228
- "openai-codex/gpt-5.6-luna*": { model: "openai-codex/gpt-5.6-terra", fallbackModels: ["zai/glm-5.3"] },
229
- "openai-codex/gpt-5.6-sol*": { model: "openai-codex/gpt-5.6-terra", fallbackModels: ["zai/glm-5.3"] },
230
- },
231
- thinking: "high",
232
- },
233
- tests: {
234
- description: "Use for tests: locate coverage, find gaps, run/check targeted test commands, diagnose failing tests.",
235
- model: "openai-codex/gpt-5.6-terra",
236
- fallbackModels: ["zai/glm-5-turbo"],
237
- thinking: "medium",
238
- },
239
- review: {
240
- description: "Use for review/audit of existing code or changes: correctness, security, performance, maintainability, API risks, quality. Do not implement new code.",
241
- modelByParent: {
242
- "openai-codex/gpt-5.6-luna*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
243
- "openai-codex/gpt-5.6-terra*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
244
- },
245
- thinking: "high",
246
- },
247
- deep: {
248
- description: "Use for broad hard reasoning: architecture, root-cause analysis, cross-module impact, complex debugging or tradeoffs.",
249
- modelByParent: {
250
- "openai-codex/gpt-5.6-luna*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
251
- "openai-codex/gpt-5.6-terra*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
252
- },
253
- thinking: "high",
254
- },
255
- oracle: {
256
- description: "Oracle: cross-provider flagship second opinion for hard or high-stakes uncertainty. Use sparingly to pressure-test architecture, plans, root-cause hypotheses, risk/security calls, or final recommendations when independent disagreement is valuable. Read-only; advise, do not edit.",
257
- model: "openai-codex/gpt-5.6-sol",
258
- fallbackModels: ["zai/glm-5.3"],
259
- modelByParent: {
260
- "zai/*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
261
- "openai-codex/*": { model: "zai/glm-5.3", fallbackModels: ["openai-codex/gpt-5.6-sol"] },
262
- "antigravity/*": { model: "zai/glm-5.3", fallbackModels: ["openai-codex/gpt-5.6-sol"] },
263
- "anthropic/*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
264
- },
265
- thinking: "max",
266
- tools: ["read", "grep", "bash"],
267
- promptAppend: "You are an oracle: a flagship model from a different provider giving a second opinion to the parent agent. Give a concise, decisive recommendation with key tradeoffs and risks. Disagree when warranted; do not rubber-stamp. Do not edit unless explicitly asked.",
268
- },
269
- },
206
+ types: normalizeAgentDefinitions(readAgentDefinitionsFromDir(BUILTIN_AGENTS_DIR)),
270
207
  };
271
208
 
272
209
  export function loadSubagentConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): SubagentConfig {
@@ -275,6 +212,10 @@ export function loadSubagentConfig(cwd: string, env: NodeJS.ProcessEnv = process
275
212
  for (const file of piToolsSuiteConfigFiles(cwd, env)) {
276
213
  mergeConfig(config, readPiToolsSuiteSubagentConfig(file));
277
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));
278
219
  }
279
220
  for (const file of configFiles(cwd, env)) {
280
221
  mergeConfig(config, readConfigFile(file));
@@ -307,8 +248,8 @@ export function getSubagentConfigSamplePath(): string {
307
248
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "async-subagents.sample.jsonc");
308
249
  }
309
250
 
310
- export function getBrowserQaSkillPath(): string {
311
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "private-skills", "browser-qa", "SKILL.md");
251
+ export function getBuiltinSubagentDefinitionsDir(): string {
252
+ return BUILTIN_AGENTS_DIR;
312
253
  }
313
254
 
314
255
  export function getSubagentConfigInitTargetPath(cwd: string, env: NodeJS.ProcessEnv = process.env): string {
@@ -329,6 +270,22 @@ function piToolsSuiteConfigFiles(cwd: string, env: NodeJS.ProcessEnv): string[]
329
270
  ].filter((file): file is string => typeof file === "string" && fs.existsSync(file));
330
271
  }
331
272
 
273
+ /** Normalize `.pi/agents/*.md` definitions through the shared type-profile path. */
274
+ function projectAgentTypes(cwd: string): Partial<SubagentConfig> {
275
+ const definitions = readProjectAgentDefinitions(cwd);
276
+ const types = normalizeAgentDefinitions(definitions);
277
+ if (Object.keys(types).length === 0) return {};
278
+ return { types };
279
+ }
280
+
281
+ function normalizeAgentDefinitions(definitions: Record<string, AgentDefinition>): Record<string, SubagentTypeConfig> {
282
+ const types: Record<string, SubagentTypeConfig> = {};
283
+ for (const [name, definition] of Object.entries(definitions)) {
284
+ types[name] = normalizeSubagentTypeProfile(definition.raw, name, definition.file);
285
+ }
286
+ return types;
287
+ }
288
+
332
289
  function findProjectPiToolsSuiteConfig(startDir: string): string | undefined {
333
290
  let dir = path.resolve(startDir);
334
291
  const root = path.parse(dir).root;
@@ -373,12 +330,19 @@ export function resolveAgentTaskConfig(
373
330
  const selectedType = selectSubagentType(task, config);
374
331
  const profile = selectedType ? config.types[selectedType] : undefined;
375
332
  const preset = globalOptions.preset;
376
- const presetType = selectedType ? preset?.types?.[selectedType] : undefined;
333
+ // A new pool preset has no per-role policy. Ignore any inherited legacy
334
+ // matrix/defaults so changing the pool cannot resurrect an expensive model.
335
+ const legacyPreset = preset?.models === undefined ? preset : undefined;
377
336
  const explicitType = trimString(task.subagentType);
337
+ const requestedType = explicitType || trimString(config.defaultType);
338
+ // Legacy preset matrices still apply to an explicitly configured type with
339
+ // the same name. There are no implicit aliases between type names.
340
+ const presetType = (requestedType ? legacyPreset?.types?.[requestedType] : undefined)
341
+ ?? (selectedType ? legacyPreset?.types?.[selectedType] : undefined);
378
342
  const taskExtraArgs = arrayOfStrings(task.extraArgs) ?? [];
379
343
  const profileExtraArgs = arrayOfStrings(profile?.extraArgs) ?? [];
380
344
  const presetTypeExtraArgs = arrayOfStrings(presetType?.extraArgs) ?? [];
381
- const presetExtraArgs = arrayOfStrings(preset?.extraArgs) ?? [];
345
+ const presetExtraArgs = arrayOfStrings(legacyPreset?.extraArgs) ?? [];
382
346
  const globalExtraArgs = arrayOfStrings(globalOptions.extraArgs) ?? [];
383
347
  const promptAppend = joinTextBlocks(profile?.promptAppend, task.promptAppend);
384
348
  const forcedModel = trimString(globalOptions.forcedModel);
@@ -387,33 +351,60 @@ export function resolveAgentTaskConfig(
387
351
  const parentMatchModel = trimString(parentMatch?.model);
388
352
  const presetTypeModel = trimString(presetType?.model);
389
353
  const globalModel = trimString(globalOptions.model);
390
- const presetModel = trimString(preset?.model);
391
- const profileModel = trimString(profile?.model);
392
- const model = forcedModel || taskModel || parentMatchModel || presetTypeModel || globalModel || presetModel || profileModel;
393
- const usedParentMatch = Boolean(parentMatchModel) && model === parentMatchModel;
394
- const fallbackModels = forcedModel || taskModel
354
+ const presetModel = trimString(legacyPreset?.model);
355
+ const profileModels = profile?.models ?? modelList(profile?.model, profile?.fallbackModels) ?? [];
356
+ const profileModel = profileModels[0];
357
+ const usedParentMatch = Boolean(parentMatchModel) && !forcedModel && !taskModel
358
+ && (selectedType === "oracle" || !(presetTypeModel || globalModel || presetModel));
359
+ const primaryModel = forcedModel || taskModel || (usedParentMatch ? parentMatchModel : undefined)
360
+ || presetTypeModel || globalModel || presetModel || profileModel;
361
+ const configuredFallbacks = forcedModel || taskModel
395
362
  ? []
396
- : usedParentMatch && parentMatch?.fallbackModels && parentMatch.fallbackModels.length > 0
363
+ : usedParentMatch && parentMatch?.fallbackModels !== undefined
397
364
  ? parentMatch.fallbackModels
398
- : resolveFallbackModels({ model, presetType, preset, profile });
365
+ : resolveFallbackModels({ model: primaryModel, presetType, preset: legacyPreset, profileModels, profile });
399
366
  const extraArgs = forcedModel
400
367
  ? stripModelArgs([...profileExtraArgs, ...presetTypeExtraArgs, ...taskExtraArgs, ...presetExtraArgs, ...globalExtraArgs])
401
368
  : [...profileExtraArgs, ...presetTypeExtraArgs, ...taskExtraArgs, ...presetExtraArgs, ...globalExtraArgs];
402
- const timeoutMs = task.timeoutMs ?? globalOptions.timeoutMs ?? presetType?.timeoutMs ?? preset?.timeoutMs ?? profile?.timeoutMs ?? config.timeoutMs;
369
+ const cliModel = modelFromArgs(extraArgs);
370
+ const explicitModel = forcedModel || cliModel || taskModel;
371
+ let candidates = modelList(explicitModel || primaryModel, explicitModel ? [] : configuredFallbacks) ?? [];
372
+ if (!explicitModel) {
373
+ if (selectedType === "oracle" && globalOptions.parentModel && !usedParentMatch
374
+ && !presetTypeModel && !presetModel && !globalModel && !profile?.model) {
375
+ const parentProvider = globalOptions.parentModel.split("/")[0];
376
+ candidates = [
377
+ ...candidates.filter((ref) => ref.split("/")[0] !== parentProvider),
378
+ ...candidates.filter((ref) => ref.split("/")[0] === parentProvider),
379
+ ];
380
+ }
381
+ if (preset?.models !== undefined) {
382
+ const available = new Set(preset.models);
383
+ candidates = candidates.filter((ref) => available.has(ref));
384
+ if (candidates.length === 0) {
385
+ throw new SubagentModelSelectionError(task.id, "No ranked candidate is in the active preset's models pool.");
386
+ }
387
+ }
388
+ }
389
+ if (candidates.length === 0) {
390
+ throw new SubagentModelSelectionError(task.id, "No model candidates are configured; set models on the agent profile.");
391
+ }
392
+ const [model, ...fallbackModels] = candidates;
393
+ const timeoutMs = task.timeoutMs ?? globalOptions.timeoutMs ?? presetType?.timeoutMs ?? legacyPreset?.timeoutMs ?? profile?.timeoutMs ?? config.timeoutMs;
403
394
 
404
395
  return {
405
396
  profile,
406
397
  extraArgs,
407
- isolatedSkills: resolveIsolatedSkills(selectedType, profile),
398
+ isolatedSkills: arrayOfStrings(profile?.isolatedSkills) ?? [],
408
399
  fallbackModels,
409
400
  retry: resolveRetryConfig(config.retry, profile?.retry),
410
401
  maxResultBytes: profile?.maxResultBytes ?? config.maxResultBytes,
411
402
  timeoutMs,
412
403
  task: {
413
404
  ...task,
414
- subagentType: explicitType || selectedType,
405
+ subagentType: selectedType,
415
406
  model,
416
- thinking: trimString(globalOptions.thinking) || trimString(task.thinking) || trimString(presetType?.thinking) || trimString(globalOptions.defaultThinking) || trimString(preset?.thinking) || trimString(profile?.thinking),
407
+ thinking: trimString(globalOptions.thinking) || trimString(task.thinking) || trimString(presetType?.thinking) || trimString(globalOptions.defaultThinking) || trimString(legacyPreset?.thinking) || trimString(profile?.thinking),
417
408
  promptAppend,
418
409
  promptOverride: trimString(task.promptOverride) || trimString(profile?.promptOverride),
419
410
  tools: task.tools && task.tools.length > 0 ? task.tools : arrayOfStrings(profile?.tools),
@@ -422,18 +413,14 @@ export function resolveAgentTaskConfig(
422
413
  };
423
414
  }
424
415
 
425
- function resolveIsolatedSkills(selectedType: string | undefined, profile: SubagentTypeConfig | undefined): string[] {
426
- const configured = arrayOfStrings(profile?.isolatedSkills) ?? [];
427
- if (selectedType !== "browser-qa") return configured;
428
- return [...new Set([getBrowserQaSkillPath(), ...configured])];
429
- }
430
-
431
416
  export function resolveSubagentRoutingConfig(config: SubagentConfig): ResolvedSubagentRoutingConfig {
432
417
  return { ...DEFAULT_ROUTING_CONFIG, ...(config.routing ?? {}) };
433
418
  }
434
419
 
435
420
  export function defaultSubagentType(config: SubagentConfig): string | undefined {
436
- return trimString(config.defaultType) || Object.keys(config.types).find((name) => trimString(name));
421
+ const configured = trimString(config.defaultType);
422
+ if (configured) return Object.prototype.hasOwnProperty.call(config.types, configured) ? configured : undefined;
423
+ return Object.keys(config.types).find((name) => trimString(name));
437
424
  }
438
425
 
439
426
  /** Merge global and per-type retry partials into a fully resolved RetryConfig. Per-type wins. */
@@ -530,6 +517,7 @@ function normalizeConfig(value: Record<string, unknown>, file: string): Partial<
530
517
  if (!isRecord(rawPreset)) throw new Error(`Subagent preset "${name}" must be an object: ${file}`);
531
518
  presets[name] = {
532
519
  description: trimString(rawPreset.description),
520
+ models: normalizeModels(rawPreset.models, `preset "${name}"`, file),
533
521
  model: trimString(rawPreset.model),
534
522
  fallbackModels: modelList(rawPreset.fallbackModels, rawPreset.fallbackModel),
535
523
  thinking: trimString(rawPreset.thinking),
@@ -546,26 +534,42 @@ function normalizeConfig(value: Record<string, unknown>, file: string): Partial<
546
534
  const types: Record<string, SubagentTypeConfig> = {};
547
535
  for (const [name, rawProfile] of Object.entries(value.types)) {
548
536
  if (!isRecord(rawProfile)) throw new Error(`Subagent type "${name}" must be an object: ${file}`);
549
- types[name] = {
550
- description: trimString(rawProfile.description),
551
- model: trimString(rawProfile.model),
552
- fallbackModels: modelList(rawProfile.fallbackModels, rawProfile.fallbackModel),
553
- modelByParent: normalizeModelByParent(rawProfile.modelByParent, name, file),
554
- thinking: trimString(rawProfile.thinking),
555
- tools: arrayOfStrings(rawProfile.tools),
556
- isolatedSkills: arrayOfStrings(rawProfile.isolatedSkills),
557
- extraArgs: arrayOfStrings(rawProfile.extraArgs),
558
- promptAppend: textBlock(rawProfile.promptAppend),
559
- promptOverride: textBlock(rawProfile.promptOverride),
560
- retry: isRecord(rawProfile.retry) ? normalizeRetryConfig(rawProfile.retry) : undefined,
561
- maxResultBytes: finiteNumber(rawProfile.maxResultBytes) !== undefined ? Math.max(0, Math.round(finiteNumber(rawProfile.maxResultBytes)!)) : undefined,
562
- timeoutMs: positiveMilliseconds(rawProfile.timeoutMs),
563
- };
537
+ types[name] = normalizeSubagentTypeProfile(rawProfile, name, file);
564
538
  }
565
539
  output.types = types;
566
540
  return output;
567
541
  }
568
542
 
543
+ /**
544
+ * 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.
547
+ */
548
+ export function normalizeSubagentTypeProfile(
549
+ rawProfile: Record<string, unknown>,
550
+ name: string,
551
+ file: string,
552
+ ): SubagentTypeConfig {
553
+ const models = normalizeModels(rawProfile.models, `type "${name}"`, file);
554
+ return {
555
+ description: trimString(rawProfile.description),
556
+ icon: trimString(rawProfile.icon),
557
+ 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,
561
+ thinking: trimString(rawProfile.thinking),
562
+ tools: arrayOfStrings(rawProfile.tools),
563
+ isolatedSkills: arrayOfStrings(rawProfile.isolatedSkills),
564
+ extraArgs: arrayOfStrings(rawProfile.extraArgs),
565
+ promptAppend: textBlock(rawProfile.promptAppend),
566
+ promptOverride: textBlock(rawProfile.promptOverride),
567
+ retry: isRecord(rawProfile.retry) ? normalizeRetryConfig(rawProfile.retry) : undefined,
568
+ maxResultBytes: finiteNumber(rawProfile.maxResultBytes) !== undefined ? Math.max(0, Math.round(finiteNumber(rawProfile.maxResultBytes)!)) : undefined,
569
+ timeoutMs: positiveMilliseconds(rawProfile.timeoutMs),
570
+ };
571
+ }
572
+
569
573
  function mergeConfig(target: SubagentConfig, source: Partial<SubagentConfig>): void {
570
574
  if (source.defaultType) target.defaultType = source.defaultType;
571
575
  if (source.routing) target.routing = { ...(target.routing ?? {}), ...source.routing };
@@ -575,14 +579,45 @@ function mergeConfig(target: SubagentConfig, source: Partial<SubagentConfig>): v
575
579
  if (source.timeoutMs !== undefined) target.timeoutMs = source.timeoutMs;
576
580
  if (source.retry) target.retry = { ...(target.retry ?? {}), ...source.retry };
577
581
  for (const [name, profile] of Object.entries(source.types ?? {})) {
578
- target.types[name] = { ...(target.types[name] ?? {}), ...compactProfile(profile) };
582
+ target.types[name] = mergeTypeProfile(target.types[name] ?? {}, profile);
579
583
  }
580
584
  for (const [name, preset] of Object.entries(source.presets ?? {})) {
581
585
  target.presets = target.presets ?? {};
582
- target.presets[name] = { ...(target.presets[name] ?? {}), ...compactPreset(preset) };
586
+ const previous = target.presets[name] ?? {};
587
+ 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
+ }
599
+ target.presets[name] = merged;
583
600
  }
584
601
  }
585
602
 
603
+ /** Preserve legacy field overrides without allowing inherited primary models
604
+ * to win over a newly supplied candidate list (or the reverse). */
605
+ function mergeTypeProfile(base: SubagentTypeConfig, source: SubagentTypeConfig): SubagentTypeConfig {
606
+ const merged = { ...compactProfile(base), ...compactProfile(source) };
607
+ if (source.models !== undefined) {
608
+ merged.models = [...source.models];
609
+ delete merged.model;
610
+ delete merged.fallbackModels;
611
+ delete merged.modelByParent;
612
+ } else if (source.model || source.fallbackModels !== undefined) {
613
+ const previous = base.models ?? modelList(base.model, base.fallbackModels) ?? [];
614
+ delete merged.models;
615
+ merged.model = source.model ?? previous[0];
616
+ merged.fallbackModels = source.fallbackModels ?? base.fallbackModels ?? previous.slice(1);
617
+ }
618
+ return merged;
619
+ }
620
+
586
621
  function normalizeRoutingConfig(value: Record<string, unknown>): SubagentRoutingConfig {
587
622
  const routing: SubagentRoutingConfig = {};
588
623
  if (typeof value.enabled === "boolean") routing.enabled = value.enabled;
@@ -625,8 +660,10 @@ function modelPatternRegExp(pattern: string): RegExp {
625
660
  function compactProfile(profile: SubagentTypeConfig): SubagentTypeConfig {
626
661
  const compact: SubagentTypeConfig = {};
627
662
  if (profile.description) compact.description = profile.description;
663
+ if (profile.icon) compact.icon = profile.icon;
664
+ if (profile.models !== undefined) compact.models = profile.models;
628
665
  if (profile.model) compact.model = profile.model;
629
- if (profile.fallbackModels && profile.fallbackModels.length > 0) compact.fallbackModels = profile.fallbackModels;
666
+ if (profile.fallbackModels) compact.fallbackModels = profile.fallbackModels;
630
667
  if (profile.modelByParent) compact.modelByParent = profile.modelByParent;
631
668
  if (profile.thinking) compact.thinking = profile.thinking;
632
669
  if (profile.tools && profile.tools.length > 0) compact.tools = profile.tools;
@@ -643,8 +680,9 @@ function compactProfile(profile: SubagentTypeConfig): SubagentTypeConfig {
643
680
  function compactPreset(preset: SubagentPreset): SubagentPreset {
644
681
  const compact: SubagentPreset = {};
645
682
  if (preset.description) compact.description = preset.description;
683
+ if (preset.models !== undefined) compact.models = preset.models;
646
684
  if (preset.model) compact.model = preset.model;
647
- if (preset.fallbackModels && preset.fallbackModels.length > 0) compact.fallbackModels = preset.fallbackModels;
685
+ if (preset.fallbackModels) compact.fallbackModels = preset.fallbackModels;
648
686
  if (preset.thinking) compact.thinking = preset.thinking;
649
687
  if (preset.extraArgs && preset.extraArgs.length > 0) compact.extraArgs = preset.extraArgs;
650
688
  if (preset.timeoutMs !== undefined) compact.timeoutMs = preset.timeoutMs;
@@ -671,7 +709,7 @@ function normalizePresetTypeOverrides(value: unknown, file: string, presetName:
671
709
  const extraArgs = arrayOfStrings(rawOverride.extraArgs);
672
710
  const timeoutMs = positiveMilliseconds(rawOverride.timeoutMs);
673
711
  if (model) override.model = model;
674
- if (fallbackModels && fallbackModels.length > 0) override.fallbackModels = fallbackModels;
712
+ if (fallbackModels) override.fallbackModels = fallbackModels;
675
713
  if (thinking) override.thinking = thinking;
676
714
  if (extraArgs && extraArgs.length > 0) override.extraArgs = extraArgs;
677
715
  if (timeoutMs !== undefined) override.timeoutMs = timeoutMs;
@@ -680,6 +718,14 @@ function normalizePresetTypeOverrides(value: unknown, file: string, presetName:
680
718
  return Object.keys(types).length > 0 ? types : undefined;
681
719
  }
682
720
 
721
+ function normalizeModels(value: unknown, owner: string, file: string): string[] | undefined {
722
+ if (value === undefined) return undefined;
723
+ if (!Array.isArray(value) || value.some((ref) => typeof ref !== "string" || !/^[^\s/*]+\/[^\s*]+$/.test(ref.trim()))) {
724
+ throw new Error(`Subagent ${owner} models must be an array of provider/model references: ${file}`);
725
+ }
726
+ return [...new Set(value.map((ref: string) => ref.trim()))];
727
+ }
728
+
683
729
  function normalizeModelByParent(value: unknown, typeName: string, file: string): Record<string, ModelByParentEntry> | undefined {
684
730
  if (value === undefined || value === null) return undefined;
685
731
  if (!isRecord(value)) throw new Error(`Subagent type "${typeName}" modelByParent must be an object: ${file}`);
@@ -716,13 +762,12 @@ function resolveFallbackModels(options: {
716
762
  model?: string;
717
763
  presetType?: SubagentPresetTypeOverride;
718
764
  preset?: SubagentPreset;
765
+ profileModels: string[];
719
766
  profile?: SubagentTypeConfig;
720
767
  }): string[] {
721
- const fallbacks = [
722
- ...(options.presetType?.fallbackModels ?? []),
723
- ...(options.preset?.fallbackModels ?? []),
724
- ...(options.profile?.fallbackModels ?? []),
725
- ];
768
+ // A selected budget's fallback list is authoritative, including [] (none).
769
+ const fallbacks = options.presetType?.fallbackModels
770
+ ?? options.preset?.fallbackModels ?? options.profile?.fallbackModels ?? options.profileModels.slice(1);
726
771
  const seen = new Set<string>();
727
772
  if (options.model) seen.add(options.model);
728
773
  const result: string[] = [];
@@ -736,10 +781,12 @@ function resolveFallbackModels(options: {
736
781
  }
737
782
 
738
783
  function applyEnvModelOverrides(config: SubagentConfig, env: NodeJS.ProcessEnv): void {
739
- for (const [name, profile] of Object.entries(config.types)) {
784
+ for (const name of Object.keys(config.types)) {
740
785
  const key = typeEnvKey(name);
741
786
  const model = trimString(env[`ASYNC_SUBAGENTS_${key}_MODEL`] || env[`PI_SUBAGENTS_${key}_MODEL`]);
742
- if (model) profile.model = model;
787
+ if (model) {
788
+ config.types[name] = mergeTypeProfile(config.types[name] ?? {}, { model });
789
+ }
743
790
  }
744
791
  }
745
792
 
@@ -849,7 +896,7 @@ function modelList(...values: unknown[]): string[] | undefined {
849
896
  models.push(model);
850
897
  }
851
898
  }
852
- return models.length > 0 ? models : undefined;
899
+ return models.length > 0 || values.some(Array.isArray) ? models : undefined;
853
900
  }
854
901
 
855
902
  function patternList(...values: unknown[]): string[] | undefined {
@@ -917,6 +964,16 @@ function stripModelArgs(args: string[]): string[] {
917
964
  return output;
918
965
  }
919
966
 
967
+ /** CLI overrides are explicit, too; retain the final flag's actual model. */
968
+ function modelFromArgs(args: string[]): string | undefined {
969
+ let model: string | undefined;
970
+ for (let i = 0; i < args.length; i++) {
971
+ if (args[i] === "--model" || args[i] === "-m") model = trimString(args[++i]);
972
+ else if (args[i].startsWith("--model=")) model = trimString(args[i].slice("--model=".length));
973
+ }
974
+ return model;
975
+ }
976
+
920
977
  function isRecord(value: unknown): value is Record<string, unknown> {
921
978
  return typeof value === "object" && value !== null;
922
979
  }
@@ -104,7 +104,7 @@ function firstAvailableFallback(failedModel: string, fallbackModels: string[]):
104
104
  return candidates.find((candidate) => modelProvider(candidate) !== failedProvider && !isSessionModelUnavailable(candidate));
105
105
  }
106
106
 
107
- function isSessionModelUnavailable(model: string): boolean {
107
+ export function isSessionModelUnavailable(model: string): boolean {
108
108
  return exhaustedModels.has(model) || exhaustedProviders.has(modelProvider(model));
109
109
  }
110
110
 
@@ -0,0 +1,54 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { isBlindModelRef, SubagentModelSelectionError, type ResolvedAgentTaskConfig, type SubagentConfig } from "./config.js";
3
+ import { isSessionModelUnavailable } from "./model-fallback.js";
4
+
5
+ export interface SubagentModelRegistry {
6
+ find(provider: string, modelId: string): Model<Api> | undefined;
7
+ /** SDK snapshot excludes providers which are registered but not configured. */
8
+ getAvailable?(): Model<Api>[];
9
+ getApiKeyAndHeaders?(model: Model<Api>): Promise<{ ok?: boolean }>;
10
+ }
11
+
12
+ /** Rank was already determined by the profile/preset intersection. No LLM call. */
13
+ export async function selectAvailableAgentModels(
14
+ resolved: ResolvedAgentTaskConfig,
15
+ config: SubagentConfig,
16
+ registry?: SubagentModelRegistry,
17
+ signal?: AbortSignal,
18
+ ): Promise<ResolvedAgentTaskConfig> {
19
+ if (signal?.aborted) throw new Error("Aborted");
20
+ const needsImages = Boolean(resolved.task.imagePaths?.length) || resolved.task.subagentType === "browser-qa";
21
+ if (!registry && !needsImages) return resolved; // Narrow offline/test callers.
22
+ const candidates = [resolved.task.model, ...resolved.fallbackModels].filter((ref): ref is string => Boolean(ref));
23
+ let registeredAvailable: Set<string> | undefined;
24
+ try {
25
+ if (registry?.getAvailable) {
26
+ registeredAvailable = new Set(registry.getAvailable().map((model) => `${model.provider}/${model.id}`));
27
+ }
28
+ } catch {
29
+ throw new SubagentModelSelectionError(resolved.task.id, "Model runtime availability could not be read.");
30
+ }
31
+ const available: string[] = [];
32
+ for (const ref of candidates) {
33
+ if (signal?.aborted) throw new Error("Aborted");
34
+ if (registeredAvailable && !registeredAvailable.has(ref)) continue;
35
+ if (isSessionModelUnavailable(ref)) continue;
36
+ const slash = ref.indexOf("/");
37
+ if (slash <= 0) continue;
38
+ try {
39
+ const model = registry?.find(ref.slice(0, slash), ref.slice(slash + 1));
40
+ if (!model) continue;
41
+ if (needsImages && (isBlindModelRef(ref, config) || !model.input?.includes("image"))) continue;
42
+ if (registry?.getApiKeyAndHeaders && (await registry.getApiKeyAndHeaders(model)).ok === false) continue;
43
+ } catch {
44
+ // Auth errors may contain sensitive provider details; never echo them.
45
+ continue;
46
+ }
47
+ available.push(ref);
48
+ }
49
+ if (signal?.aborted) throw new Error("Aborted");
50
+ const [model, ...fallbackModels] = available;
51
+ if (!model) throw new SubagentModelSelectionError(resolved.task.id,
52
+ `No configured candidate is available${needsImages ? " with confirmed image support" : ""} in the model runtime.`);
53
+ return { ...resolved, task: { ...resolved.task, model }, fallbackModels };
54
+ }
@@ -23,18 +23,19 @@ Scope and constraints:
23
23
  - Do not spawn other agents or background jobs unless explicitly allowed.
24
24
  ${scopeLine}${imageLine}- Follow the parent task's constraints and repository instructions.
25
25
  - Keep output compact by default.
26
+ - Stay within the assigned scope and preserve unrelated work. Do not re-plan the project.
27
+ - Report concrete blockers and gathered evidence rather than widening the task or escalating models yourself.
28
+ - Keep noisy intermediate output in artifacts; return only decision-relevant evidence to the parent.
26
29
  - If the task explicitly asks for verbatim/raw file contents, command output, logs, or exact text, output only that content exactly and do not summarize it.
27
30
  - If requested raw content is very large, include the requested relevant portion and clearly say what was omitted.
28
31
 
29
32
  Output format:
30
33
  - If the task explicitly requests a raw/verbatim/exact output format, follow that request instead and do not add the standard summary sections below.
31
34
  - Otherwise use:
32
- 1. Summary, max 5 bullets
33
- 2. Evidence / files inspected, paths first
34
- 3. Recommended changes or patch plan
35
- 4. Files changed, if any
36
- 5. Tests/commands run, with short results
37
- 6. Risks / open questions
35
+ 1. Result: answer or completion status, briefly.
36
+ 2. Evidence: relevant file:line references, changed files, check results and artifact paths.
37
+ 3. Limits: blockers, unverified assumptions, risks or necessary next action; omit when empty.
38
+ - Do not repeat the task or paste full logs/diffs unless requested. Preserve required QA artifact links.
38
39
  `;
39
40
 
40
41
  return task.promptAppend