@selesai/code 0.5.29 → 0.6.1

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 (83) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/dist/core/system-prompt.d.ts.map +1 -1
  4. package/dist/core/system-prompt.js +18 -0
  5. package/dist/core/system-prompt.js.map +1 -1
  6. package/dist/core/system-prompt.test.d.ts +2 -0
  7. package/dist/core/system-prompt.test.d.ts.map +1 -0
  8. package/dist/core/system-prompt.test.js +89 -0
  9. package/dist/core/system-prompt.test.js.map +1 -0
  10. package/dist/defaults/models.json +13 -45
  11. package/dist/defaults/settings.json +1 -2
  12. package/dist/extensions/copy-turn.test.ts +131 -0
  13. package/dist/extensions/copy-turn.ts +6 -1
  14. package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  15. package/dist/extensions/package.json +0 -1
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +3 -0
  17. package/dist/extensions/pi-subagents/README.md +27 -32
  18. package/dist/extensions/pi-subagents/agents/architect.md +4 -4
  19. package/dist/extensions/pi-subagents/agents/builder.md +5 -4
  20. package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
  21. package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
  22. package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
  23. package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
  24. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -0
  25. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +10 -9
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +12 -11
  27. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +10 -11
  28. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +56 -9
  29. package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
  30. package/dist/extensions/pi-subagents/src/api/preflight.ts +1 -1
  31. package/dist/extensions/pi-subagents/src/extension/index.ts +5 -1
  32. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  33. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +24 -7
  34. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +23 -5
  35. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +27 -1
  36. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +64 -6
  37. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +16 -1
  38. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +72 -18
  39. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +19 -5
  40. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +127 -31
  41. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +4 -6
  42. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
  43. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -0
  44. package/dist/extensions/pi-subagents/src/shared/types.ts +41 -2
  45. package/dist/extensions/pi-subagents/src/shared/utils.ts +29 -1
  46. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -1
  47. package/dist/extensions/pi-subagents/src/tui/render.ts +28 -6
  48. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +111 -6
  49. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +74 -43
  50. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +36 -21
  51. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +5 -3
  52. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -8
  53. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +14 -7
  54. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +81 -5
  55. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +49 -10
  56. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
  57. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
  58. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -6
  59. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
  60. package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
  61. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +34 -0
  62. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +24 -0
  63. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +6 -1
  64. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +29 -0
  65. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +2 -0
  66. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +12 -0
  67. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
  68. package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
  69. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +23 -1
  70. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +60 -9
  71. package/dist/skills/ponytail/SKILL.md +1 -3
  72. package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
  73. package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
  74. package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
  75. package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
  76. package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
  77. package/package.json +2 -2
  78. package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
  79. package/dist/extensions/caveman/index.js +0 -118
  80. package/dist/extensions/caveman/package.json +0 -8
  81. package/dist/extensions/caveman/test/extension.test.js +0 -203
  82. package/dist/extensions/caveman/test/helpers.test.js +0 -58
  83. package/dist/skills/caveman/SKILL.md +0 -50
@@ -28,13 +28,14 @@ import { discoverAvailableSkills, resolveSkills } from "./skills.ts";
28
28
  import {
29
29
  buildProactiveSkillSubagentRecommendationLines,
30
30
  } from "./proactive-skills.ts";
31
+ import { formatTaskAwareAgentRecommendation, recommendTaskAwareAgent } from "./task-aware-routing.ts";
31
32
  import { parseFrontmatter } from "./frontmatter.ts";
32
33
  import { toModelInfo } from "../shared/model-info.ts";
33
34
  import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/model-fallback.ts";
34
35
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
35
36
  import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
36
37
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
37
- import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
38
+ import type { AcceptanceInput, CatalogAgentMetadata, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
38
39
  import { getProjectConfigDir } from "../shared/utils.ts";
39
40
  import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
40
41
 
@@ -48,10 +49,12 @@ interface ManagementParams {
48
49
  chainName?: string;
49
50
  agentScope?: string;
50
51
  config?: unknown;
52
+ /** Optional advisory intent for action:'list' only; never launches work. */
53
+ task?: string;
51
54
  }
52
55
 
53
- function result(text: string, isError = false): AgentToolResult<Details> {
54
- return { content: [{ type: "text", text }], isError, details: { mode: "management", results: [] } };
56
+ function result(text: string, isError = false, extraDetails: Partial<Details> = {}): AgentToolResult<Details> {
57
+ return { content: [{ type: "text", text }], isError, details: { mode: "management", results: [], ...extraDetails } };
55
58
  }
56
59
 
57
60
  function parseCsv(value: string): string[] {
@@ -585,8 +588,13 @@ function renamePath(
585
588
  return { filePath };
586
589
  }
587
590
 
591
+ /** Effective declared tools: normal tools plus mcp:-prefixed direct MCP tools. */
592
+ function effectiveAgentTools(agent: AgentConfig): string[] {
593
+ return [...(agent.tools ?? []), ...(agent.mcpDirectTools ?? []).map((t) => `mcp:${t}`)];
594
+ }
595
+
588
596
  function formatAgentDetail(agent: AgentConfig): string {
589
- const tools = [...(agent.tools ?? []), ...(agent.mcpDirectTools ?? []).map((t) => `mcp:${t}`)];
597
+ const tools = effectiveAgentTools(agent);
590
598
  const lines: string[] = [`Agent: ${agent.name} (${agent.source})`, `Path: ${agent.filePath}`, `Description: ${agent.description}`];
591
599
  if (agent.packageName) {
592
600
  lines.push(`Local name: ${frontmatterNameForConfig(agent)}`);
@@ -672,6 +680,33 @@ function formatChainDetail(chain: ChainConfig): string {
672
680
  return lines.join("\n");
673
681
  }
674
682
 
683
+ function formatCatalogAgentLine(agent: AgentConfig): string {
684
+ const parts: string[] = [agent.source, `context: ${agent.defaultContext ?? "fresh"}`];
685
+ if (agent.acceptanceRole) parts.push(`role: ${agent.acceptanceRole}`);
686
+ if (agent.aliases?.length) parts.push(`aliases: ${agent.aliases.join(", ")}`);
687
+ const tools = effectiveAgentTools(agent);
688
+ if (tools.length) parts.push(`tools: ${tools.join(", ")}`);
689
+ return `- ${agent.name} (${parts.join(", ")}): ${agent.description}`;
690
+ }
691
+
692
+ function catalogAgentMetadata(
693
+ agent: AgentConfig,
694
+ executable: boolean,
695
+ restrictionSources: string[] | undefined,
696
+ ): CatalogAgentMetadata {
697
+ return {
698
+ name: agent.name,
699
+ source: agent.source,
700
+ description: agent.description,
701
+ executable,
702
+ ...(restrictionSources?.length ? { restrictionSources } : {}),
703
+ ...(agent.aliases?.length ? { aliases: [...agent.aliases] } : {}),
704
+ defaultContext: agent.defaultContext ?? "fresh",
705
+ ...(agent.acceptanceRole ? { acceptanceRole: agent.acceptanceRole } : {}),
706
+ tools: effectiveAgentTools(agent),
707
+ };
708
+ }
709
+
675
710
  export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
676
711
  const scope = normalizeListScope(params.agentScope) ?? "both";
677
712
  const d = discoverAgentsAll(ctx.cwd);
@@ -690,23 +725,35 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
690
725
  config: ctx.config?.proactiveSkillSubagents,
691
726
  discoverAvailableSkills: () => discoverAvailableSkills(ctx.cwd),
692
727
  });
728
+ const taskAdvice = params.task?.trim()
729
+ ? formatTaskAwareAgentRecommendation(recommendTaskAwareAgent({ task: params.task, agents, capabilityCeiling }))
730
+ : [];
693
731
  const lines = [
694
732
  "Executable agents:",
695
- ...(agents.length
696
- ? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`)
697
- : ["- (none)"]),
733
+ ...(agents.length ? agents.map(formatCatalogAgentLine) : ["- (none)"]),
698
734
  ...(restrictedAgents.length ? [
699
735
  "",
700
736
  `Restricted agents (not executable in this session${restrictedSources?.length ? `; capability ceiling: ${restrictedSources.join(", ")}` : ""}):`,
701
- ...restrictedAgents.map((a) => `- ${a.name} (${a.source}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`),
737
+ ...restrictedAgents.map(formatCatalogAgentLine),
702
738
  ] : []),
703
739
  "",
704
740
  "Chains:",
705
741
  ...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
706
742
  ...(proactiveSuggestions.length ? ["", ...proactiveSuggestions] : []),
743
+ ...(taskAdvice.length ? ["", ...taskAdvice] : []),
707
744
  ...(diagnostics.length ? ["", "Chain diagnostics:", ...diagnostics.map((entry) => `- ${entry.filePath}: ${entry.error}`)] : []),
708
745
  ];
709
- return result(lines.join("\n"));
746
+ return result(lines.join("\n"), false, {
747
+ catalog: {
748
+ version: 1,
749
+ agents: [
750
+ ...agents.map((a) => catalogAgentMetadata(a, true, undefined)),
751
+ ...restrictedAgents.map((a) => catalogAgentMetadata(a, false, restrictedSources)),
752
+ ],
753
+ chains: chains.map((c) => ({ name: c.name, source: c.source, description: c.description })),
754
+ ...(restrictedSources?.length ? { capabilityCeilingSources: restrictedSources } : {}),
755
+ },
756
+ });
710
757
  }
711
758
 
712
759
  function formatModelSource(agent: AgentConfig, currentModel: ParentModel | undefined): string {
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Pure task-aware advisory agent recommender.
3
+ *
4
+ * Given an already-discovered, effective agent set and the current capability
5
+ * ceiling, deterministically selects at most one canonical agent to recommend
6
+ * for a task, or returns recovery guidance when intent is unknown or no safe
7
+ * candidate exists. This module is deliberately pure: it performs no
8
+ * filesystem/discovery access, no alias/chains/executor/RPC/preflight/settings
9
+ * access, no mutation, launch, scheduling, or persistence, and never writes to
10
+ * params. Launching stays explicit: the caller must make a separate execution
11
+ * call with the recommended canonical `agent.name`.
12
+ */
13
+
14
+ import type { AgentConfig, AgentSource } from "./agents.ts";
15
+ import { agentHasWriteTools } from "./agent-memory.ts";
16
+ import { classifyTaskMutationIntent, resolveAgentRoutingRole } from "../runs/shared/task-intent.ts";
17
+ import { isAgentAllowedByCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
18
+
19
+ /** Core tools that make an implementation agent able to write (matches agent-memory). */
20
+ const WRITER_TOOLS = new Set(["edit", "write", "bash"]);
21
+
22
+ /** Source precedence for deterministic ordering: project > user > package > builtin. */
23
+ const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3 };
24
+
25
+ export type TaskAwareAgentIntent = "implementation" | "read-only" | "unknown";
26
+
27
+ export interface TaskAwareAgentRecommendationAgent {
28
+ /** Canonical runtime agent name accepted by execution; never an alias. */
29
+ name: string;
30
+ source: AgentSource;
31
+ role: "writer" | "read-only";
32
+ roleBasis: "declared" | "inferred";
33
+ reason: string;
34
+ }
35
+
36
+ export interface TaskAwareAgentRecommendation {
37
+ intent: TaskAwareAgentIntent;
38
+ agent?: TaskAwareAgentRecommendationAgent;
39
+ /** Recovery guidance when no safe recommendation exists (unknown intent or no candidate). */
40
+ next?: string;
41
+ }
42
+
43
+ /**
44
+ * Recommend a canonical agent for a trimmed non-empty task, or `undefined` for
45
+ * an empty/whitespace task (no-op). Never recommends disabled or
46
+ * capability-disallowed agents, never guesses for unknown intent, and never
47
+ * outputs an alias or a role-incompatible agent.
48
+ */
49
+ export function recommendTaskAwareAgent(input: {
50
+ task: string;
51
+ agents: AgentConfig[];
52
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
53
+ }): TaskAwareAgentRecommendation | undefined {
54
+ const task = input.task.trim();
55
+ if (!task) return undefined;
56
+
57
+ const intent = classifyTaskMutationIntent("builder", task).kind;
58
+ if (intent === "unknown") {
59
+ return {
60
+ intent,
61
+ next: "Clarify whether the task is read-only analysis/review or implementation allowed to edit files.",
62
+ };
63
+ }
64
+
65
+ const candidates: TaskAwareAgentRecommendationAgent[] = [];
66
+ for (const agent of input.agents) {
67
+ if (agent.disabled) continue;
68
+ if (!isAgentAllowedByCapabilityCeiling(agent.name, input.capabilityCeiling)) continue;
69
+ const role = resolveAgentRoutingRole(agent.name, agent.acceptanceRole);
70
+ if (!role) continue;
71
+ const hasWriteTools = agentHasWriteTools(agent);
72
+ if (intent === "implementation") {
73
+ if (role !== "writer" || !hasWriteTools) continue;
74
+ const allowedTools = input.capabilityCeiling?.allowedTools;
75
+ if (allowedTools !== undefined && !allowedTools.some((tool) => WRITER_TOOLS.has(tool))) continue;
76
+ } else {
77
+ if (role !== "read-only" || hasWriteTools) continue;
78
+ }
79
+ const roleBasis = agent.acceptanceRole !== undefined ? "declared" : "inferred";
80
+ candidates.push({
81
+ name: agent.name,
82
+ source: agent.source,
83
+ role,
84
+ roleBasis,
85
+ reason: intent === "implementation"
86
+ ? `${roleBasis} writer role with write tools`
87
+ : `${roleBasis} read-only role without known write tools`,
88
+ });
89
+ }
90
+
91
+ candidates.sort((a, b) => {
92
+ const sourceOrder = AGENT_SOURCE_PRECEDENCE[b.source] - AGENT_SOURCE_PRECEDENCE[a.source];
93
+ if (sourceOrder !== 0) return sourceOrder;
94
+ const declaredOrder = (a.roleBasis === "declared" ? 0 : 1) - (b.roleBasis === "declared" ? 0 : 1);
95
+ if (declaredOrder !== 0) return declaredOrder;
96
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
97
+ });
98
+
99
+ const agent = candidates[0];
100
+ if (!agent) {
101
+ return {
102
+ intent,
103
+ next: intent === "implementation"
104
+ ? "No executable agent has a writer role with write tools for this task. Check the executable/restricted sections above or adjust the capability ceiling."
105
+ : "No executable agent has a read-only role without known write tools for this task. Check the executable/restricted sections above.",
106
+ };
107
+ }
108
+ return { intent, agent };
109
+ }
110
+
111
+ /** Text-only advisory lines; `undefined` (empty-task no-op) renders nothing. */
112
+ export function formatTaskAwareAgentRecommendation(recommendation: TaskAwareAgentRecommendation | undefined): string[] {
113
+ if (!recommendation) return [];
114
+ const lines = ["Task-aware advisory routing:", `- Intent: ${recommendation.intent}`];
115
+ if (recommendation.agent) {
116
+ lines.push(`- Recommended: ${recommendation.agent.name} (${recommendation.agent.source})`);
117
+ lines.push(`- Reason: ${recommendation.agent.reason}`);
118
+ lines.push("- Advisory only: no subagent was launched. To proceed, explicitly call subagent with this canonical agent name and the task.");
119
+ } else {
120
+ lines.push("- Recommendation: none");
121
+ lines.push(`- Next: ${recommendation.next ?? "Refine the task wording and retry."}`);
122
+ lines.push("- Advisory only: no subagent was launched.");
123
+ }
124
+ return lines;
125
+ }
@@ -284,7 +284,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
284
284
  diagnostics.push({ code: "denied_required_tool", severity: "error", message });
285
285
  return { ok: false, code: "denied_required_tool", message, diagnostics };
286
286
  }
287
- const artifactsEnabled = input.artifacts !== false;
287
+ const artifactsEnabled = input.artifacts === true;
288
288
  const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir ?? "project") : undefined;
289
289
  const artifactPaths = artifactsDir ? getArtifactPaths(artifactsDir, runId, agent.name, 0) : undefined;
290
290
  const outputPath = resolveSingleOutputPath(behavior.output, effectiveCwd, effectiveCwd, artifactsDir ? path.join(artifactsDir, "outputs", runId) : undefined);
@@ -51,7 +51,7 @@ import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/
51
51
  import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
52
52
  import { formatDuration, shortenPath } from "../shared/formatters.ts";
53
53
  import { loadConfig } from "./config.ts";
54
- import { buildSubagentToolDescription } from "./tool-description.ts";
54
+ import { buildSubagentToolDescription, SUBAGENT_PARENT_ROUTING_GUIDANCE } from "./tool-description.ts";
55
55
  import {
56
56
  type Details,
57
57
  type SubagentState,
@@ -405,6 +405,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
405
405
  name: "subagent",
406
406
  label: "Subagent",
407
407
  description: buildSubagentToolDescription(config),
408
+ // Always-visible active-tool path: rendered into the main system prompt by
409
+ // buildSystemPrompt (default and custom-prompt branches). Parent-only; the
410
+ // child-safe fanout registration must not receive this guidance.
411
+ promptGuidelines: [SUBAGENT_PARENT_ROUTING_GUIDANCE],
408
412
  parameters: SubagentParams,
409
413
 
410
414
  prepareArguments(args) {
@@ -262,7 +262,7 @@ const ControlOverrides = Type.Object({
262
262
 
263
263
  const SubagentParamsSchema = Type.Object({
264
264
  agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
265
- task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
265
+ task: Type.Optional(Type.String({ description: "Task for SINGLE-mode execution, or an optional advisory intent for action:'list' (list only appends a task-aware recommendation and never launches work; explicitly call subagent with the recommended canonical agent name and the task to execute)" })),
266
266
  // Management action (when present, tool operates in management mode)
267
267
  action: Type.Optional(Type.String({
268
268
  description: "Optional management/control action. Omit this field entirely for execution/delegation ({agent, task}, {tasks}, or {chain}); use it only for management/control actions."
@@ -321,7 +321,7 @@ const SubagentParamsSchema = Type.Object({
321
321
  usageBudget: Type.Optional(UsageBudgetOverride),
322
322
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
323
323
  cwd: Type.Optional(Type.String()),
324
- artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
324
+ artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: false)" })),
325
325
  includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
326
326
  share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist for sharing (default: false)" })),
327
327
  sessionDir: Type.Optional(
@@ -6,13 +6,27 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
6
6
  const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
7
7
  const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
8
8
 
9
+ /**
10
+ * Generic parent-only delegation routing contract. Deliberately contains no
11
+ * bundled agent names: agents are selected from runtime catalog metadata only.
12
+ * Attached to the parent `subagent` tool as promptGuidelines and embedded in
13
+ * the mandatory safety guidance so custom descriptions cannot remove it.
14
+ */
15
+ export const SUBAGENT_PARENT_ROUTING_GUIDANCE = `PARENT-ONLY SUBAGENT ROUTING:
16
+ • Before any execution, call { action: "list" } and select only an executable entry using its current role, context, and tool metadata.
17
+ • Keep tiny targeted reads and simple answers local with the parent.
18
+ • Send broad local investigation, external research, and mutation/implementation work to a capable delegated agent.
19
+ • The parent remains the decision-maker and normally the sole writer.`;
20
+
9
21
  export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
10
22
  • Use { action: "list" } before execution and only run executable/non-disabled agents or chains.
11
23
  • Keep execution and management separate: omit action for SINGLE/PARALLEL/CHAIN execution; use action only for list/get/models/create/update/delete/status/grant-spawn-budget/interrupt/stop/resume/steer/append-step/approve-checkpoint/reject-checkpoint/doctor.
12
24
  • Async/background runs: launch with async:true only when work can proceed independently. Do not sleep or poll status just to wait. In an interactive session, normally return control and let Pi wake you; do not call subagent_wait merely to wait. Override that default and call subagent_wait when the current request is run-to-completion — for example, the user asked you to report results back before continuing or a skill must finish in one turn. Headless sessions auto-drain current-session work at agent_end; use subagent_wait when this turn must receive results before it ends.
13
25
  • Child-safety boundary: ordinary child subagents are not orchestrators and must not run subagents. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
14
26
  • Writing/review safety: keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers/validators for independent review, then have the parent synthesize and apply fixes as the sole writer unless an isolated worktree was intentionally requested.
15
- • Artifacts/status essentials: chain outputs live under {chain_dir}; async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Include output paths and residual risks when reporting results.`;
27
+ • Artifacts/status essentials: chain outputs live under {chain_dir}; async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Normal delegated results are reference-first: completion returns saved-output references plus lifecycle/status information, not child prose — inspect full output through the saved output path, async status/transcript, or resume. Include output paths and residual risks when reporting results.
28
+
29
+ ${SUBAGENT_PARENT_ROUTING_GUIDANCE}`;
16
30
 
17
31
  export const FULL_SUBAGENT_TOOL_DESCRIPTION = `To delegate work, call with { agent, task }, { tasks }, or { chain }; omit action. Use action only for management/control actions listed below.
18
32
 
@@ -37,8 +51,9 @@ CHAIN EXAMPLES (quick reference for the nested schema):
37
51
  • Parallel fan-out: { chain: [{parallel: [{agent:"agent-a", task:"Check part of {task}", count: 3}]}] }
38
52
  • Mixed: { chain: [{agent:"agent-a", task:"Research {task}"}, {checkpoint:"review", message:"Approve implementation?"}, {parallel: [{agent:"agent-b", task:"Review {previous}", count: 2}]}, {agent:"agent-c", task:"Summarize {previous}"}] }
39
53
 
40
- MANAGEMENT (use action field, omit agent/task/chain/tasks):
54
+ MANAGEMENT (use action field; omit agent/chain/tasks — the sole field exception is list's optional advisory task):
41
55
  • { action: "list" } - discover executable agents/chains
56
+ • { action: "list", task: "..." } - same, plus optional task-aware advisory routing: recommends one canonical agent (implementation or read-only) for the task, or explains why none is safe. It only recommends and never launches; to proceed, explicitly call subagent with the recommended canonical agent name and the task.
42
57
  • { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
43
58
  • { action: "models", agent?: "name" } - show the runtime-loaded builtin subagent model mapping, optionally filtered to one builtin
44
59
  • { action: "watchdog.status" | "watchdog.check" | "watchdog.recommend-model" } - inspect the opt-in subagent watchdog and its strong complementary model recommendation
@@ -47,10 +62,10 @@ MANAGEMENT (use action field, omit agent/task/chain/tasks):
47
62
  • acceptanceRole affects inferred acceptance only, never tool access. Explicit task mutation/no-edit intent wins; omission preserves name heuristics. Update with false or an empty string to clear it.
48
63
  • { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
49
64
  • { action: "delete", agent: "code-analysis.custom-agent" }
50
- • { action: "eject", agent: "commentator", agentScope?: "user" | "project" } - copy a bundled/package agent to user/project scope as an editable custom file that shadows the original (default scope: user)
51
- • { action: "disable", agent: "commentator", agentScope?: "user" | "project" } - hide any agent from runtime discovery via a reversible settings override (default scope: user)
52
- • { action: "enable", agent: "commentator", agentScope?: "user" | "project" } - remove a disabled override and restore discovery
53
- • { action: "reset", agent: "commentator", agentScope?: "user" | "project" } - delete the scope's custom agent file and/or settings override, restoring the bundled default
65
+ • { action: "eject", agent: "agent-name", agentScope?: "user" | "project" } - copy a bundled/package agent to user/project scope as an editable custom file that shadows the original (default scope: user)
66
+ • { action: "disable", agent: "agent-name", agentScope?: "user" | "project" } - hide any agent from runtime discovery via a reversible settings override (default scope: user)
67
+ • { action: "enable", agent: "agent-name", agentScope?: "user" | "project" } - remove a disabled override and restore discovery
68
+ • { action: "reset", agent: "agent-name", agentScope?: "user" | "project" } - delete the scope's custom agent file and/or settings override, restoring the bundled default
54
69
  • { action: "grant-spawn-budget", additional: 10 } - add bounded capacity from the root interactive parent after native user confirmation; grants are rejected while children are active and cumulative grants cannot exceed the original configured cap
55
70
  • Use chainName for chain operations; packaged chains also use dotted runtime names
56
71
 
@@ -88,7 +103,8 @@ EXECUTE:
88
103
  • If list shows proactive skill subagent suggestions, use a small fresh-context fanout only when the task is broad enough.
89
104
 
90
105
  MANAGE / CONTROL:
91
- • Use action without execution fields: list, get, models, create, update, delete, eject, disable, enable, reset, grant-spawn-budget, doctor, watchdog.status, watchdog.check, watchdog.recommend-model, watchdog.configure.
106
+ • Use action without execution fields (list may add an optional advisory task): list, get, models, create, update, delete, eject, disable, enable, reset, grant-spawn-budget, doctor, watchdog.status, watchdog.check, watchdog.recommend-model, watchdog.configure.
107
+ • { action: "list", task: "..." } optionally appends task-aware advisory routing that recommends a canonical agent but never launches; execute the recommended agent explicitly in a separate call.
92
108
  • Agent acceptanceRole (read-only or writer) affects inferred acceptance only, never tools. Explicit task intent wins; omission keeps name heuristics. Update with false or an empty string to clear it.
93
109
  • Async control actions: status, interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint. Use stop with an id for current-session top-level async runs. Use status view:"fleet" for active-run overview, view:"transcript" to tail child output, steer for acknowledged top-level live async guidance, and resume for paused/completed/failed revival or a routed nested follow-up. Stopped runs are non-resumable. Steering delivery means Pi accepted the correlated user input, not model compliance; use index for a specific child.
94
110
  • Opt-in schedule actions: schedule, schedule-list, schedule-status, schedule-cancel. Schedule only explicit delayed runs the user asked for.
@@ -98,6 +114,7 @@ ASYNC / WAIT:
98
114
  • Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, session files, and { action:"status", id:"..." }.
99
115
 
100
116
  SAFETY:
117
+ • Parent-only routing: call { action: "list" } before any execution and select only an executable entry using its current role, context, and tool metadata. Keep tiny targeted reads and simple answers local; send broad local investigation, external research, and mutation/implementation work to a capable delegated agent. The parent remains the decision-maker and normally the sole writer.
101
118
  • Ordinary child subagents are not orchestrators and must not run subagents. Only explicit fanout children may use child-safe subagent, still bounded by depth/session limits.
102
119
  • Keep one writer per cwd/worktree. Use fresh read-only review/validation fanout, then synthesize and apply fixes from the parent unless isolated worktrees were intentionally requested.`;
103
120
 
@@ -686,9 +686,19 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
686
686
  const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
687
687
  if (behavior.progress) progressInstructionCreated = true;
688
688
  const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
689
- const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd, outputBaseDir);
689
+ const explicitOutput = typeof behavior.output === "string" ? behavior.output : undefined;
690
+ // Omitted output generates a collision-safe per-child durable path
691
+ // `<asyncDir>/outputs/<flat-index>-<agent>.md`; omitted outputMode resolves
692
+ // to file-only whenever an output path is active. Explicit paths,
693
+ // `output: false`, and explicit `outputMode: "inline"` are preserved.
694
+ const outputPath = explicitOutput
695
+ ? resolveSingleOutputPath(explicitOutput, ctx.cwd, instructionCwd, outputBaseDir)
696
+ : behavior.output === false && s.output !== false && s.output !== "false" && flatIndex !== undefined
697
+ ? path.join(asyncDir, "outputs", `${flatIndex}-${s.agent}.md`)
698
+ : undefined;
690
699
  if (!namespaceOutputPath) systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, a);
691
- const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
700
+ const effectiveOutputMode = s.outputMode ?? (outputPath ? "file-only" : "inline");
701
+ const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Async step (${s.agent})`);
692
702
  if (validationError) throw new AsyncStartValidationError(validationError);
693
703
  let taskTemplate = s.task ?? "{previous}";
694
704
  taskTemplate = taskTemplate.replace(/\{task\}/g, originalTask ?? "");
@@ -751,7 +761,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
751
761
  skills: resolvedSkills.map((r) => r.name),
752
762
  outputPath,
753
763
  ...(namespaceOutputPath ? { namespaceOutputPath: true } : {}),
754
- outputMode: behavior.outputMode,
764
+ outputMode: effectiveOutputMode,
755
765
  sessionFile,
756
766
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
757
767
  waitToolEnabled: params.waitToolEnabled,
@@ -1228,9 +1238,17 @@ export function executeAsyncSingle(
1228
1238
  }
1229
1239
 
1230
1240
  const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
1231
- const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
1241
+ // Omitted output resolves to a generated per-run durable path
1242
+ // `<asyncDir>/outputs/0-<agent>.md`; omitted outputMode resolves to file-only
1243
+ // whenever an output path is active. Explicit paths, `output: false`, and
1244
+ // explicit `outputMode: "inline"` are preserved exactly.
1245
+ const outputPath = typeof effectiveOutput === "string"
1246
+ ? resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined))
1247
+ : effectiveOutput === false
1248
+ ? undefined
1249
+ : path.join(asyncDir, "outputs", `0-${agent}.md`);
1232
1250
  systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
1233
- const outputMode = params.outputMode ?? "inline";
1251
+ const outputMode = params.outputMode ?? (outputPath ? "file-only" : "inline");
1234
1252
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
1235
1253
  if (validationError) return formatAsyncStartError("single", validationError);
1236
1254
  const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath, agentConfig);
@@ -192,6 +192,32 @@ function completionBatchKey(result: CompletionNotification): string {
192
192
  return cwd ? `cwd:${cwd}` : "unknown";
193
193
  }
194
194
 
195
+ /**
196
+ * Reference-first notification preview: compact child summaries built by the
197
+ * result watcher from each child's authoritative output path/log. Never uses
198
+ * `results[].output` or the run-level `summary` as parent-facing text; the run
199
+ * summary remains only a defensive fallback for legacy payloads without results.
200
+ */
201
+ function buildReferenceFirstPreview(result: CompletionNotification): string {
202
+ const children = Array.isArray(result.results) ? result.results : [];
203
+ if (children.length === 0) {
204
+ return typeof result.summary === "string" ? result.summary : "";
205
+ }
206
+ if (children.length === 1) {
207
+ const childSummary = typeof children[0]?.summary === "string" && children[0].summary.trim()
208
+ ? children[0].summary
209
+ : "(no output)";
210
+ return childSummary;
211
+ }
212
+ return children.map((child, index) => {
213
+ const agentLabel = typeof child?.agent === "string" ? child.agent : `step-${index + 1}`;
214
+ const childSummary = typeof child?.summary === "string" && child.summary.trim()
215
+ ? child.summary
216
+ : "(no output)";
217
+ return `${index + 1}. ${agentLabel}\n${childSummary}`;
218
+ }).join("\n\n");
219
+ }
220
+
195
221
  export function buildCompletionDetails(result: CompletionNotification): SubagentNotifyDetails {
196
222
  const agent = result.agent ?? "unknown";
197
223
  const summary = typeof result.summary === "string" ? result.summary : "";
@@ -229,7 +255,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
229
255
  status,
230
256
  ...(result.source ? { source: result.source } : {}),
231
257
  ...(taskInfo ? { taskInfo } : {}),
232
- resultPreview: summary,
258
+ resultPreview: buildReferenceFirstPreview(result),
233
259
  ...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
234
260
  ...(handoffPath ? { handoffPath } : {}),
235
261
  ...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
@@ -20,6 +20,7 @@ import {
20
20
  } from "../../intercom/result-intercom.ts";
21
21
  import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-events.ts";
22
22
  import { resolveWatchPath } from "../../shared/utils.ts";
23
+ import { formatBoundedPersistenceFallback, formatSavedOutputReference } from "../shared/single-output.ts";
23
24
  import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
24
25
 
25
26
  const WATCHER_RESTART_DELAY_MS = 3000;
@@ -46,10 +47,14 @@ type ResultWatcherDeps = {
46
47
  type ResultFileChild = {
47
48
  agent?: string;
48
49
  output?: string;
50
+ /** Authoritative durable output location: explicit/generated saved path or async output log. */
51
+ outputPath?: string;
52
+ savedOutputPath?: string;
49
53
  outputState?: SubagentOutputState;
50
54
  error?: string;
51
55
  success?: boolean;
52
56
  state?: string;
57
+ exitCode?: number | null;
53
58
  interrupted?: boolean;
54
59
  timedOut?: boolean;
55
60
  stopped?: boolean;
@@ -92,6 +97,57 @@ function isNotFound(error: unknown): boolean {
92
97
  return errorCode(error) === "ENOENT";
93
98
  }
94
99
 
100
+ /**
101
+ * Resolve the authoritative output location for one async child: explicit/generated
102
+ * saved output path first, else the persisted per-child output log
103
+ * (`asyncDir/output-<flat-index>.log`). Legacy result files without saved-path
104
+ * fields fall back to the output log when the run directory is known.
105
+ */
106
+ function childOutputPath(data: ResultFileData, result: ResultFileChild | undefined, index: number): string | undefined {
107
+ if (typeof result?.outputPath === "string" && result.outputPath.trim()) return result.outputPath;
108
+ if (typeof result?.savedOutputPath === "string" && result.savedOutputPath.trim()) return result.savedOutputPath;
109
+ if (typeof data.asyncDir === "string" && data.asyncDir.trim()) return path.join(data.asyncDir, `output-${index}.log`);
110
+ return undefined;
111
+ }
112
+
113
+ /**
114
+ * Reference-only child summary for async delivery: the saved-output reference
115
+ * (or output-log reference) plus process error/status for failed children. Never
116
+ * emits `results[].output` or the run `summary` as parent-facing text; when the
117
+ * output location is absent or unreadable, a bounded fallback is used instead.
118
+ */
119
+ function buildChildReferenceSummary(fsApi: ResultWatcherFs, input: {
120
+ outputPath: string | undefined;
121
+ outputText: string;
122
+ exitCode: number;
123
+ error?: string;
124
+ }): string {
125
+ const statusError = input.exitCode !== 0 && input.error ? input.error : undefined;
126
+ if (input.outputPath) {
127
+ try {
128
+ const content = fsApi.readFileSync(input.outputPath, "utf-8");
129
+ const ref = formatSavedOutputReference(input.outputPath, content);
130
+ return statusError ? `${statusError}\n\n${ref.message}` : ref.message;
131
+ } catch (error) {
132
+ const fallback = formatBoundedPersistenceFallback({
133
+ error: `Failed to read saved output: ${error instanceof Error ? error.message : String(error)}`,
134
+ outputPath: input.outputPath,
135
+ fullOutput: input.outputText,
136
+ exitCode: input.exitCode,
137
+ processError: input.error,
138
+ });
139
+ return statusError ? `${statusError}\n\n${fallback}` : fallback;
140
+ }
141
+ }
142
+ const fallback = formatBoundedPersistenceFallback({
143
+ error: "No saved output path is available for this child.",
144
+ fullOutput: input.outputText,
145
+ exitCode: input.exitCode,
146
+ processError: input.error,
147
+ });
148
+ return statusError ? `${statusError}\n\n${fallback}` : fallback;
149
+ }
150
+
95
151
  function shouldPoll(error: unknown): boolean {
96
152
  const code = errorCode(error);
97
153
  return code === "EMFILE" || code === "ENOSPC";
@@ -180,14 +236,16 @@ export function createResultWatcher(
180
236
  const hasResultChildren = Array.isArray(data.results) && data.results.length > 0;
181
237
  const resultChildren: ResultFileChild[] = hasResultChildren
182
238
  ? data.results!
183
- : [{ agent: data.agent ?? undefined, output: data.summary, outputState: "unknown", success: data.success }];
239
+ : [{ agent: data.agent ?? undefined, outputPath: childOutputPath(data, undefined, 0), outputState: "unknown", success: data.success }];
184
240
  const normalizedChildren = attachNestedChildrenToResultChildren(runId, resultChildren.map((result = {}, index): SubagentResultIntercomChild => {
185
241
  const baseOutput = hasResultChildren ? result.output : result.output ?? data.summary;
186
- const hasRealOutput = typeof baseOutput === "string" && baseOutput.trim().length > 0;
187
- const output = hasRealOutput ? baseOutput : "(no output)";
188
- const summary = result.success === false && result.error
189
- ? `${result.error}${hasRealOutput ? `\n\nOutput:\n${baseOutput}` : ""}`
190
- : output;
242
+ const outputText = typeof baseOutput === "string" ? baseOutput : "";
243
+ const summary = buildChildReferenceSummary(fsApi, {
244
+ outputPath: childOutputPath(data, result, index),
245
+ outputText,
246
+ exitCode: result.success === false ? 1 : result.success === true ? 0 : typeof result.exitCode === "number" ? result.exitCode : data.success === false ? 1 : 0,
247
+ error: result.error ?? (result.success === false ? (typeof data.error === "string" ? data.error : undefined) : undefined),
248
+ });
191
249
  const sessionPath = result.sessionFile ?? (resultChildren.length === 1 ? data.sessionFile : undefined);
192
250
  const childNestedChildren = sanitizeNestedResultChildren(result.children, resultPath, `results[${index}].children`);
193
251
  const childState = result.state === "paused" || result.state === "stopped"
@@ -1476,7 +1476,11 @@ async function runSingleStep(
1476
1476
 
1477
1477
  const rawOutput = finalResult?.finalOutput ?? "";
1478
1478
  const outputForPersistence = stripAcceptanceReport(rawOutput);
1479
- const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
1479
+ // Persist whenever an output path is configured and meaningful output exists,
1480
+ // not only for successful runs: failed children keep a readable result file
1481
+ // and a saved-output reference instead of raw inline output. Synthetic
1482
+ // failures without output stay "absent" and are not promoted to a reference.
1483
+ const resolvedOutput = step.outputPath && (outputForPersistence.trim() || (finalResult?.exitCode ?? 1) === 0)
1480
1484
  ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
1481
1485
  : { fullOutput: outputForPersistence };
1482
1486
  const output = stripAcceptanceReport(resolvedOutput.fullOutput);
@@ -1515,6 +1519,9 @@ async function runSingleStep(
1515
1519
  savedPath: resolvedOutput.savedPath,
1516
1520
  outputReference,
1517
1521
  saveError: resolvedOutput.saveError,
1522
+ error: finalResult?.error
1523
+ ?? (finalResult?.stopped ? ctx.stopMessage ?? "Subagent stopped by user." : undefined)
1524
+ ?? (finalResult?.timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : undefined),
1518
1525
  });
1519
1526
  outputForSummary = finalizedOutput.displayOutput;
1520
1527
  const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
@@ -1596,6 +1603,10 @@ async function runSingleStep(
1596
1603
  context: step.context,
1597
1604
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1598
1605
  launchContractDigest: actualLaunchContractDigest,
1606
+ // Authoritative durable output location for parent-facing delivery: the
1607
+ // explicit/generated saved output path when present, otherwise the async
1608
+ // per-child output log (`asyncDir/output-<flat-index>.log`).
1609
+ outputPath: step.outputPath ?? ctx.outputFile,
1599
1610
  output: outputForSummary,
1600
1611
  outputState,
1601
1612
  exitCode: effectiveFinalExitCode,
@@ -3388,6 +3399,7 @@ async function runSubagent(
3388
3399
  launchResolvedExtensions: pr.launchResolvedExtensions,
3389
3400
  runtimeAcknowledgedExtensions: pr.runtimeAcknowledgedExtensions,
3390
3401
  output: pr.output,
3402
+ outputPath: pr.outputPath,
3391
3403
  outputState: pr.outputState,
3392
3404
  error: pr.error,
3393
3405
  protocolError: pr.protocolError,
@@ -3797,6 +3809,7 @@ async function runSubagent(
3797
3809
  launchContractDigest: pr.launchContractDigest,
3798
3810
  launchResolvedExtensions: pr.launchResolvedExtensions,
3799
3811
  output: pr.output,
3812
+ outputPath: pr.outputPath,
3800
3813
  outputState: pr.outputState,
3801
3814
  error: pr.error,
3802
3815
  protocolError: pr.protocolError,
@@ -3984,6 +3997,7 @@ async function runSubagent(
3984
3997
  launchResolvedExtensions: singleResult.launchResolvedExtensions,
3985
3998
  runtimeAcknowledgedExtensions: singleResult.runtimeAcknowledgedExtensions,
3986
3999
  output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
4000
+ outputPath: singleResult.outputPath,
3987
4001
  outputState: singleResult.outputState,
3988
4002
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
3989
4003
  protocolError: singleResult.protocolError,
@@ -4311,6 +4325,7 @@ async function runSubagent(
4311
4325
  results: results.map((r) => ({
4312
4326
  agent: r.agent,
4313
4327
  context: r.context,
4328
+ outputPath: r.outputPath,
4314
4329
  output: r.output,
4315
4330
  outputState: r.outputState,
4316
4331
  error: r.error,