@selesai/code 0.5.29 → 0.6.2

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 (94) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/dist/config.d.ts +16 -3
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +106 -5
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/system-prompt.d.ts.map +1 -1
  8. package/dist/core/system-prompt.js +18 -0
  9. package/dist/core/system-prompt.js.map +1 -1
  10. package/dist/core/system-prompt.test.d.ts +2 -0
  11. package/dist/core/system-prompt.test.d.ts.map +1 -0
  12. package/dist/core/system-prompt.test.js +89 -0
  13. package/dist/core/system-prompt.test.js.map +1 -0
  14. package/dist/defaults/models.json +13 -45
  15. package/dist/defaults/settings.json +5 -7
  16. package/dist/extensions/copy-turn.test.ts +131 -0
  17. package/dist/extensions/copy-turn.ts +6 -1
  18. package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  19. package/dist/extensions/package.json +0 -1
  20. package/dist/extensions/pi-subagents/CHANGELOG.md +3 -0
  21. package/dist/extensions/pi-subagents/README.md +27 -32
  22. package/dist/extensions/pi-subagents/agents/architect.md +4 -4
  23. package/dist/extensions/pi-subagents/agents/builder.md +5 -4
  24. package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
  25. package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
  26. package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
  27. package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
  28. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -0
  29. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +10 -9
  30. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +12 -11
  31. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +10 -11
  32. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +56 -9
  33. package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
  34. package/dist/extensions/pi-subagents/src/api/preflight.ts +1 -1
  35. package/dist/extensions/pi-subagents/src/extension/index.ts +5 -1
  36. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  37. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +24 -7
  38. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +23 -5
  39. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +27 -1
  40. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +64 -6
  41. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +16 -1
  42. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +72 -18
  43. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +19 -5
  44. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +127 -31
  45. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +4 -6
  46. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
  47. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -0
  48. package/dist/extensions/pi-subagents/src/shared/types.ts +41 -2
  49. package/dist/extensions/pi-subagents/src/shared/utils.ts +29 -1
  50. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -1
  51. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -6
  52. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +111 -6
  53. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +74 -43
  54. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +36 -21
  55. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +5 -3
  56. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -8
  57. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +14 -7
  58. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +227 -0
  59. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +81 -5
  60. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +49 -10
  61. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
  62. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
  63. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -6
  64. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
  65. package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
  66. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +34 -0
  67. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +24 -0
  68. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +6 -1
  69. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +29 -0
  70. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +2 -0
  71. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +12 -0
  72. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
  73. package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
  74. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +23 -1
  75. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +60 -9
  76. package/dist/extensions/pi-web-agent/package.json +1 -1
  77. package/dist/skills/pi-subagents/SKILL.md +43 -0
  78. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
  79. package/dist/skills/pi-subagents/references/execution-controls.md +431 -0
  80. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
  81. package/dist/skills/pi-subagents/references/prompting-and-roles.md +281 -0
  82. package/dist/skills/ponytail/SKILL.md +1 -3
  83. package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
  84. package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
  85. package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
  86. package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
  87. package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
  88. package/package.json +2 -2
  89. package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
  90. package/dist/extensions/caveman/index.js +0 -118
  91. package/dist/extensions/caveman/package.json +0 -8
  92. package/dist/extensions/caveman/test/extension.test.js +0 -203
  93. package/dist/extensions/caveman/test/helpers.test.js +0 -58
  94. package/dist/skills/caveman/SKILL.md +0 -50
@@ -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,
@@ -91,6 +91,34 @@ import { usageBudgetExceededMessage, usageBudgetState } from "../shared/usage-bu
91
91
  import type { ContextMode } from "../shared/context-mode.ts";
92
92
  import type { ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
93
93
 
94
+ /**
95
+ * Resolve a chain step's output path and effective output mode.
96
+ *
97
+ * Explicit `output` paths pass through unchanged (absolute as-is, relative under
98
+ * the chain dir). Omitted output generates a collision-safe per-child durable
99
+ * path `<chainDir>/outputs/<flat-index>-<agent>.md` unless the caller explicitly
100
+ * used `output: false`. Omitted `outputMode` resolves to `file-only` when an
101
+ * output path is active; explicit `outputMode: "inline"` is preserved exactly.
102
+ */
103
+ function resolveChainStepOutput(input: {
104
+ behaviorOutput: string | false;
105
+ rawOutput: string | false | undefined;
106
+ rawOutputMode: "inline" | "file-only" | undefined;
107
+ chainDir: string;
108
+ flatIndex: number;
109
+ agent: string;
110
+ }): { outputPath?: string; outputMode: "inline" | "file-only" } {
111
+ const explicit = typeof input.behaviorOutput === "string" ? input.behaviorOutput : undefined;
112
+ let outputPath: string | undefined;
113
+ if (explicit) {
114
+ outputPath = path.isAbsolute(explicit) ? explicit : path.join(input.chainDir, explicit);
115
+ } else if (input.behaviorOutput === false && input.rawOutput !== false && input.rawOutput !== "false") {
116
+ outputPath = path.join(input.chainDir, "outputs", `${input.flatIndex}-${input.agent}.md`);
117
+ }
118
+ return { outputPath, outputMode: input.rawOutputMode ?? (outputPath ? "file-only" : "inline") };
119
+ }
120
+
121
+
94
122
  interface ChainExecutionDetailsInput {
95
123
  results: SingleResult[];
96
124
  includeProgress?: boolean;
@@ -353,9 +381,16 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
353
381
  ? input.worktreeSetup.worktrees[taskIndex]!.agentCwd
354
382
  : resolveChildCwd(input.cwd ?? input.ctx.cwd, task.cwd);
355
383
 
356
- const outputPath = typeof behavior.output === "string"
357
- ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(input.chainDir, behavior.output))
358
- : undefined;
384
+ const resolvedTaskOutput = resolveChainStepOutput({
385
+ behaviorOutput: behavior.output,
386
+ rawOutput: task.output,
387
+ rawOutputMode: task.outputMode,
388
+ chainDir: input.chainDir,
389
+ flatIndex: childIndex,
390
+ agent: task.agent,
391
+ });
392
+ const outputPath = resolvedTaskOutput.outputPath;
393
+ const effectiveOutputMode = resolvedTaskOutput.outputMode;
359
394
  taskStr = injectSingleOutputInstruction(taskStr, outputPath, taskAgentConfig);
360
395
  const interruptController = new AbortController();
361
396
  if (input.foregroundControl) {
@@ -399,7 +434,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
399
434
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
400
435
  artifactConfig: input.artifactConfig,
401
436
  outputPath,
402
- outputMode: behavior.outputMode,
437
+ outputMode: effectiveOutputMode,
403
438
  maxSubagentDepth,
404
439
  controlConfig: input.controlConfig,
405
440
  onControlEvent: input.onControlEvent,
@@ -793,10 +828,16 @@ ${step.message}` : ""}` }],
793
828
  .map((behavior, taskIndex) => suppressProgressForReadOnlyTask(behavior, parallelTemplates[taskIndex] ?? step.parallel[taskIndex]?.task, originalTask));
794
829
  for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
795
830
  const behavior = parallelBehaviors[taskIndex]!;
796
- const outputPath = typeof behavior.output === "string"
797
- ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
798
- : undefined;
799
- const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${step.parallel[taskIndex]!.agent})`);
831
+ const task = step.parallel[taskIndex]!;
832
+ const resolvedOutput = resolveChainStepOutput({
833
+ behaviorOutput: behavior.output,
834
+ rawOutput: task.output,
835
+ rawOutputMode: task.outputMode,
836
+ chainDir,
837
+ flatIndex: globalTaskIndex + taskIndex,
838
+ agent: task.agent,
839
+ });
840
+ const validationError = validateFileOnlyOutputMode(resolvedOutput.outputMode, resolvedOutput.outputPath, `Parallel chain step ${stepIndex + 1} task ${taskIndex + 1} (${task.agent})`);
800
841
  if (validationError) return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
801
842
  }
802
843
  progressCreated = ensureParallelProgressFile(chainDir, progressCreated, parallelBehaviors);
@@ -1049,10 +1090,16 @@ ${step.message}` : ""}` }],
1049
1090
 
1050
1091
  for (let taskIndex = 0; taskIndex < dynamicParallelStep.parallel.length; taskIndex++) {
1051
1092
  const behavior = parallelBehaviors[taskIndex]!;
1052
- const outputPath = typeof behavior.output === "string"
1053
- ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
1054
- : undefined;
1055
- const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Dynamic chain step ${stepIndex + 1} item ${taskIndex + 1} (${dynamicParallelStep.parallel[taskIndex]!.agent})`);
1093
+ const task = dynamicParallelStep.parallel[taskIndex]!;
1094
+ const resolvedOutput = resolveChainStepOutput({
1095
+ behaviorOutput: behavior.output,
1096
+ rawOutput: task.output,
1097
+ rawOutputMode: task.outputMode,
1098
+ chainDir,
1099
+ flatIndex: globalTaskIndex + taskIndex,
1100
+ agent: task.agent,
1101
+ });
1102
+ const validationError = validateFileOnlyOutputMode(resolvedOutput.outputMode, resolvedOutput.outputPath, `Dynamic chain step ${stepIndex + 1} item ${taskIndex + 1} (${task.agent})`);
1056
1103
  if (validationError) {
1057
1104
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: validationError };
1058
1105
  return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex + taskIndex }));
@@ -1289,11 +1336,18 @@ ${step.message}` : ""}` }],
1289
1336
  { scope: modelScope },
1290
1337
  );
1291
1338
 
1292
- const outputPath = typeof behavior.output === "string"
1293
- ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
1294
- : undefined;
1339
+ const resolvedStepOutput = resolveChainStepOutput({
1340
+ behaviorOutput: behavior.output,
1341
+ rawOutput: stepOverride.output,
1342
+ rawOutputMode: seqStep.outputMode,
1343
+ chainDir,
1344
+ flatIndex: globalTaskIndex,
1345
+ agent: seqStep.agent,
1346
+ });
1347
+ const outputPath = resolvedStepOutput.outputPath;
1348
+ const effectiveOutputMode = resolvedStepOutput.outputMode;
1295
1349
  stepTask = injectSingleOutputInstruction(stepTask, outputPath, agentConfig);
1296
- const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
1350
+ const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Chain step ${stepIndex + 1} (${seqStep.agent})`);
1297
1351
  if (validationError) {
1298
1352
  return buildChainExecutionErrorResult(validationError, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
1299
1353
  }
@@ -1339,7 +1393,7 @@ ${step.message}` : ""}` }],
1339
1393
  dynamicChildren,
1340
1394
  dynamicGroupStatuses,
1341
1395
  });
1342
- r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1396
+ r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
1343
1397
  parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
1344
1398
  capabilityCeiling: params.capabilityCeiling,
1345
1399
  context: params.contextForAgent?.(seqStep.agent),
@@ -1358,7 +1412,7 @@ ${step.message}` : ""}` }],
1358
1412
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1359
1413
  artifactConfig,
1360
1414
  outputPath,
1361
- outputMode: behavior.outputMode,
1415
+ outputMode: effectiveOutputMode,
1362
1416
  maxSubagentDepth,
1363
1417
  controlConfig,
1364
1418
  onControlEvent,
@@ -62,7 +62,7 @@ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
62
62
  import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
63
63
  import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
64
64
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
65
- import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
65
+ import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatBoundedPersistenceFallback, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
66
66
  import {
67
67
  buildModelCandidates,
68
68
  formatModelAttemptNote,
@@ -1282,7 +1282,12 @@ async function runSingleAttempt(
1282
1282
  reason: "completion_guard",
1283
1283
  }));
1284
1284
  }
1285
- if (options.outputPath && result.exitCode === 0) {
1285
+ // Persist whenever an output path is configured and meaningful child output
1286
+ // exists, not only for successful runs: failed children with output keep a
1287
+ // readable result file and a saved-output reference instead of raw inline
1288
+ // output. Synthetic failures without output (startup errors, empty responses)
1289
+ // stay outputState "absent" and are not promoted to a bogus saved reference.
1290
+ if (options.outputPath && (fullOutput.trim() || result.exitCode === 0)) {
1286
1291
  const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
1287
1292
  fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
1288
1293
  result.savedOutputPath = resolvedOutput.savedPath;
@@ -1295,9 +1300,18 @@ async function runSingleAttempt(
1295
1300
  artifactOutputByResult.set(result, fullOutput);
1296
1301
  acceptanceOutputByResult.set(result, acceptanceOutput);
1297
1302
  result.outputMode = options.outputMode ?? "inline";
1298
- result.finalOutput = options.outputMode === "file-only" && result.savedOutputPath && result.outputReference
1299
- ? result.outputReference.message
1300
- : fullOutput;
1303
+ const hasSavedReference = result.savedOutputPath && result.outputReference ? true : false;
1304
+ result.finalOutput = hasSavedReference && (options.outputMode === "file-only" || result.exitCode !== 0)
1305
+ ? result.outputReference!.message
1306
+ : result.outputSaveError && options.outputPath
1307
+ ? formatBoundedPersistenceFallback({
1308
+ error: result.outputSaveError,
1309
+ outputPath: options.outputPath,
1310
+ fullOutput,
1311
+ exitCode: result.exitCode,
1312
+ processError: result.error,
1313
+ })
1314
+ : fullOutput;
1301
1315
  result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
1302
1316
  if (options.onUpdate) {
1303
1317
  const finalText = result.finalOutput || result.error || "(no output)";