@bastani/atomic 0.8.24-alpha.2 → 0.8.24-alpha.4

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 (193) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -1
  3. package/dist/builtin/intercom/CHANGELOG.md +12 -0
  4. package/dist/builtin/intercom/package.json +1 -1
  5. package/dist/builtin/mcp/CHANGELOG.md +12 -0
  6. package/dist/builtin/mcp/package.json +1 -1
  7. package/dist/builtin/subagents/CHANGELOG.md +16 -0
  8. package/dist/builtin/subagents/README.md +132 -21
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/subagents/prompts/parallel-context-build.md +4 -2
  11. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +3 -1
  12. package/dist/builtin/subagents/skills/subagent/SKILL.md +49 -11
  13. package/dist/builtin/subagents/src/agents/agent-management.ts +79 -16
  14. package/dist/builtin/subagents/src/agents/agents.ts +47 -16
  15. package/dist/builtin/subagents/src/agents/chain-serializer.ts +114 -0
  16. package/dist/builtin/subagents/src/extension/schemas.ts +139 -3
  17. package/dist/builtin/subagents/src/runs/background/async-execution.ts +92 -6
  18. package/dist/builtin/subagents/src/runs/background/async-status.ts +11 -1
  19. package/dist/builtin/subagents/src/runs/background/run-status.ts +4 -1
  20. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +529 -32
  21. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +361 -118
  22. package/dist/builtin/subagents/src/runs/foreground/execution.ts +75 -7
  23. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +33 -0
  24. package/dist/builtin/subagents/src/runs/shared/acceptance.ts +611 -0
  25. package/dist/builtin/subagents/src/runs/shared/chain-outputs.ts +101 -0
  26. package/dist/builtin/subagents/src/runs/shared/dynamic-fanout.ts +293 -0
  27. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +29 -1
  28. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +11 -0
  29. package/dist/builtin/subagents/src/runs/shared/structured-output.ts +79 -0
  30. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +52 -2
  31. package/dist/builtin/subagents/src/runs/shared/workflow-graph.ts +206 -0
  32. package/dist/builtin/subagents/src/shared/formatters.ts +2 -2
  33. package/dist/builtin/subagents/src/shared/settings.ts +53 -4
  34. package/dist/builtin/subagents/src/shared/types.ts +226 -0
  35. package/dist/builtin/subagents/src/shared/utils.ts +2 -1
  36. package/dist/builtin/subagents/src/slash/slash-commands.ts +41 -3
  37. package/dist/builtin/subagents/src/tui/render.ts +152 -34
  38. package/dist/builtin/web-access/CHANGELOG.md +12 -0
  39. package/dist/builtin/web-access/package.json +1 -1
  40. package/dist/builtin/workflows/CHANGELOG.md +12 -0
  41. package/dist/builtin/workflows/package.json +1 -1
  42. package/dist/builtin/workflows/skills/create-spec/SKILL.md +1 -1
  43. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +0 -1
  44. package/dist/core/slash-commands.d.ts.map +1 -1
  45. package/dist/core/slash-commands.js +1 -0
  46. package/dist/core/slash-commands.js.map +1 -1
  47. package/dist/core/system-prompt.d.ts.map +1 -1
  48. package/dist/core/system-prompt.js +4 -3
  49. package/dist/core/system-prompt.js.map +1 -1
  50. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  51. package/dist/modes/interactive/interactive-mode.js +1 -1
  52. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  53. package/docs/usage.md +1 -0
  54. package/docs/workflows.md +173 -0
  55. package/node_modules/@earendil-works/pi-tui/README.md +779 -0
  56. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.d.ts +54 -0
  57. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.d.ts.map +1 -0
  58. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js +632 -0
  59. package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js.map +1 -0
  60. package/node_modules/@earendil-works/pi-tui/dist/components/box.d.ts +22 -0
  61. package/node_modules/@earendil-works/pi-tui/dist/components/box.d.ts.map +1 -0
  62. package/node_modules/@earendil-works/pi-tui/dist/components/box.js +104 -0
  63. package/node_modules/@earendil-works/pi-tui/dist/components/box.js.map +1 -0
  64. package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.d.ts +22 -0
  65. package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.d.ts.map +1 -0
  66. package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.js +35 -0
  67. package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.js.map +1 -0
  68. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts +249 -0
  69. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -0
  70. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +1857 -0
  71. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -0
  72. package/node_modules/@earendil-works/pi-tui/dist/components/image.d.ts +28 -0
  73. package/node_modules/@earendil-works/pi-tui/dist/components/image.d.ts.map +1 -0
  74. package/node_modules/@earendil-works/pi-tui/dist/components/image.js +89 -0
  75. package/node_modules/@earendil-works/pi-tui/dist/components/image.js.map +1 -0
  76. package/node_modules/@earendil-works/pi-tui/dist/components/input.d.ts +37 -0
  77. package/node_modules/@earendil-works/pi-tui/dist/components/input.d.ts.map +1 -0
  78. package/node_modules/@earendil-works/pi-tui/dist/components/input.js +378 -0
  79. package/node_modules/@earendil-works/pi-tui/dist/components/input.js.map +1 -0
  80. package/node_modules/@earendil-works/pi-tui/dist/components/loader.d.ts +31 -0
  81. package/node_modules/@earendil-works/pi-tui/dist/components/loader.d.ts.map +1 -0
  82. package/node_modules/@earendil-works/pi-tui/dist/components/loader.js +69 -0
  83. package/node_modules/@earendil-works/pi-tui/dist/components/loader.js.map +1 -0
  84. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts +96 -0
  85. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts.map +1 -0
  86. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +644 -0
  87. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js.map +1 -0
  88. package/node_modules/@earendil-works/pi-tui/dist/components/select-list.d.ts +50 -0
  89. package/node_modules/@earendil-works/pi-tui/dist/components/select-list.d.ts.map +1 -0
  90. package/node_modules/@earendil-works/pi-tui/dist/components/select-list.js +159 -0
  91. package/node_modules/@earendil-works/pi-tui/dist/components/select-list.js.map +1 -0
  92. package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.d.ts +50 -0
  93. package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.d.ts.map +1 -0
  94. package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.js +185 -0
  95. package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.js.map +1 -0
  96. package/node_modules/@earendil-works/pi-tui/dist/components/spacer.d.ts +12 -0
  97. package/node_modules/@earendil-works/pi-tui/dist/components/spacer.d.ts.map +1 -0
  98. package/node_modules/@earendil-works/pi-tui/dist/components/spacer.js +23 -0
  99. package/node_modules/@earendil-works/pi-tui/dist/components/spacer.js.map +1 -0
  100. package/node_modules/@earendil-works/pi-tui/dist/components/text.d.ts +19 -0
  101. package/node_modules/@earendil-works/pi-tui/dist/components/text.d.ts.map +1 -0
  102. package/node_modules/@earendil-works/pi-tui/dist/components/text.js +89 -0
  103. package/node_modules/@earendil-works/pi-tui/dist/components/text.js.map +1 -0
  104. package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.d.ts +13 -0
  105. package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.d.ts.map +1 -0
  106. package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.js +51 -0
  107. package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.js.map +1 -0
  108. package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts +39 -0
  109. package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts.map +1 -0
  110. package/node_modules/@earendil-works/pi-tui/dist/editor-component.js +2 -0
  111. package/node_modules/@earendil-works/pi-tui/dist/editor-component.js.map +1 -0
  112. package/node_modules/@earendil-works/pi-tui/dist/fuzzy.d.ts +16 -0
  113. package/node_modules/@earendil-works/pi-tui/dist/fuzzy.d.ts.map +1 -0
  114. package/node_modules/@earendil-works/pi-tui/dist/fuzzy.js +110 -0
  115. package/node_modules/@earendil-works/pi-tui/dist/fuzzy.js.map +1 -0
  116. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +23 -0
  117. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +1 -0
  118. package/node_modules/@earendil-works/pi-tui/dist/index.js +32 -0
  119. package/node_modules/@earendil-works/pi-tui/dist/index.js.map +1 -0
  120. package/node_modules/@earendil-works/pi-tui/dist/keybindings.d.ts +193 -0
  121. package/node_modules/@earendil-works/pi-tui/dist/keybindings.d.ts.map +1 -0
  122. package/node_modules/@earendil-works/pi-tui/dist/keybindings.js +174 -0
  123. package/node_modules/@earendil-works/pi-tui/dist/keybindings.js.map +1 -0
  124. package/node_modules/@earendil-works/pi-tui/dist/keys.d.ts +184 -0
  125. package/node_modules/@earendil-works/pi-tui/dist/keys.d.ts.map +1 -0
  126. package/node_modules/@earendil-works/pi-tui/dist/keys.js +1173 -0
  127. package/node_modules/@earendil-works/pi-tui/dist/keys.js.map +1 -0
  128. package/node_modules/@earendil-works/pi-tui/dist/kill-ring.d.ts +28 -0
  129. package/node_modules/@earendil-works/pi-tui/dist/kill-ring.d.ts.map +1 -0
  130. package/node_modules/@earendil-works/pi-tui/dist/kill-ring.js +44 -0
  131. package/node_modules/@earendil-works/pi-tui/dist/kill-ring.js.map +1 -0
  132. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts +3 -0
  133. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts.map +1 -0
  134. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js +53 -0
  135. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js.map +1 -0
  136. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.d.ts +50 -0
  137. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.d.ts.map +1 -0
  138. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js +361 -0
  139. package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js.map +1 -0
  140. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts +90 -0
  141. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts.map +1 -0
  142. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +366 -0
  143. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js.map +1 -0
  144. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +113 -0
  145. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -0
  146. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +472 -0
  147. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -0
  148. package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts +227 -0
  149. package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts.map +1 -0
  150. package/node_modules/@earendil-works/pi-tui/dist/tui.js +1106 -0
  151. package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -0
  152. package/node_modules/@earendil-works/pi-tui/dist/undo-stack.d.ts +17 -0
  153. package/node_modules/@earendil-works/pi-tui/dist/undo-stack.d.ts.map +1 -0
  154. package/node_modules/@earendil-works/pi-tui/dist/undo-stack.js +25 -0
  155. package/node_modules/@earendil-works/pi-tui/dist/undo-stack.js.map +1 -0
  156. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts +84 -0
  157. package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts.map +1 -0
  158. package/node_modules/@earendil-works/pi-tui/dist/utils.js +1029 -0
  159. package/node_modules/@earendil-works/pi-tui/dist/utils.js.map +1 -0
  160. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.d.ts +25 -0
  161. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.d.ts.map +1 -0
  162. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js +96 -0
  163. package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js.map +1 -0
  164. package/node_modules/@earendil-works/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  165. package/node_modules/@earendil-works/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  166. package/node_modules/@earendil-works/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  167. package/node_modules/@earendil-works/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  168. package/node_modules/@earendil-works/pi-tui/package.json +47 -0
  169. package/node_modules/get-east-asian-width/index.d.ts +60 -0
  170. package/node_modules/get-east-asian-width/index.js +30 -0
  171. package/node_modules/get-east-asian-width/license +9 -0
  172. package/node_modules/get-east-asian-width/lookup-data.js +21 -0
  173. package/node_modules/get-east-asian-width/lookup.js +138 -0
  174. package/node_modules/get-east-asian-width/package.json +71 -0
  175. package/node_modules/get-east-asian-width/readme.md +65 -0
  176. package/node_modules/get-east-asian-width/utilities.js +24 -0
  177. package/node_modules/marked/LICENSE.md +44 -0
  178. package/node_modules/marked/README.md +106 -0
  179. package/node_modules/marked/bin/main.js +282 -0
  180. package/node_modules/marked/bin/marked.js +15 -0
  181. package/node_modules/marked/lib/marked.cjs +2211 -0
  182. package/node_modules/marked/lib/marked.cjs.map +7 -0
  183. package/node_modules/marked/lib/marked.d.cts +728 -0
  184. package/node_modules/marked/lib/marked.d.ts +728 -0
  185. package/node_modules/marked/lib/marked.esm.js +2189 -0
  186. package/node_modules/marked/lib/marked.esm.js.map +7 -0
  187. package/node_modules/marked/lib/marked.umd.js +2213 -0
  188. package/node_modules/marked/lib/marked.umd.js.map +7 -0
  189. package/node_modules/marked/man/marked.1 +111 -0
  190. package/node_modules/marked/man/marked.1.md +92 -0
  191. package/node_modules/marked/marked.min.js +69 -0
  192. package/node_modules/marked/package.json +111 -0
  193. package/package.json +9 -1
@@ -17,7 +17,7 @@ import {
17
17
  parsePackageName,
18
18
  } from "./agents.ts";
19
19
  import { serializeAgent } from "./agent-serializer.ts";
20
- import { serializeChain } from "./chain-serializer.ts";
20
+ import { serializeChain, serializeJsonChain } from "./chain-serializer.ts";
21
21
  import { discoverAvailableSkills } from "./skills.ts";
22
22
  import type { SubagentToolResult } from "../shared/types.ts";
23
23
 
@@ -116,10 +116,31 @@ function nameExistsInScope(cwd: string, scope: ManagementScope, name: string, ex
116
116
  return false;
117
117
  }
118
118
 
119
+ function chainStepAgentNames(step: ChainStepConfig): string[] {
120
+ const names: string[] = [];
121
+ if (typeof step.agent === "string") names.push(step.agent);
122
+ const parallel = step.parallel;
123
+ if (Array.isArray(parallel)) {
124
+ for (const item of parallel) {
125
+ if (item && typeof item === "object") {
126
+ const agent = (item as { agent?: unknown }).agent;
127
+ if (typeof agent === "string") names.push(agent);
128
+ }
129
+ }
130
+ } else if (parallel && typeof parallel === "object") {
131
+ const agent = (parallel as { agent?: unknown }).agent;
132
+ if (typeof agent === "string") names.push(agent);
133
+ }
134
+ return names;
135
+ }
136
+
119
137
  function unknownChainAgents(cwd: string, steps: ChainStepConfig[]): string[] {
120
138
  const d = discoverAgentsAll(cwd);
121
139
  const known = new Set(allAgents(d).map((a) => a.name));
122
- return [...new Set(steps.map((s) => s.agent).filter((a) => !known.has(a)))].sort((a, b) => a.localeCompare(b));
140
+ const unknown = steps
141
+ .flatMap((step) => chainStepAgentNames(step))
142
+ .filter((agent) => !known.has(agent));
143
+ return [...new Set(unknown)].sort((a, b) => a.localeCompare(b));
123
144
  }
124
145
 
125
146
  function chainStepWarnings(ctx: ManagementContext, steps: ChainStepConfig[]): string[] {
@@ -169,6 +190,22 @@ function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: strin
169
190
  const s = item as Record<string, unknown>;
170
191
  if (typeof s.agent !== "string" || !s.agent.trim()) return { error: `config.steps[${i}].agent must be a non-empty string.` };
171
192
  const step: ChainStepConfig = { agent: s.agent.trim(), task: typeof s.task === "string" ? s.task : "" };
193
+ if (hasKey(s, "phase")) {
194
+ if (typeof s.phase === "string") step.phase = s.phase;
195
+ else return { error: `config.steps[${i}].phase must be a string.` };
196
+ }
197
+ if (hasKey(s, "label")) {
198
+ if (typeof s.label === "string") step.label = s.label;
199
+ else return { error: `config.steps[${i}].label must be a string.` };
200
+ }
201
+ if (hasKey(s, "as")) {
202
+ if (typeof s.as === "string") step.as = s.as;
203
+ else return { error: `config.steps[${i}].as must be a string.` };
204
+ }
205
+ if (hasKey(s, "outputSchema")) {
206
+ if (typeof s.outputSchema === "string") step.outputSchema = s.outputSchema;
207
+ else return { error: `config.steps[${i}].outputSchema must be a schema file path string for saved chains.` };
208
+ }
172
209
  if (hasKey(s, "output")) {
173
210
  if (s.output === false) step.output = false;
174
211
  else if (typeof s.output === "string") step.output = s.output;
@@ -345,7 +382,7 @@ function renamePath(
345
382
  cwd: string,
346
383
  ): { filePath?: string; error?: string } {
347
384
  if (nameExistsInScope(cwd, scope, newName, currentPath)) return { error: `Name '${newName}' already exists in ${scope} scope.` };
348
- const ext = kind === "agent" ? ".md" : ".chain.md";
385
+ const ext = kind === "agent" ? ".md" : currentPath.endsWith(".chain.json") ? ".chain.json" : ".chain.md";
349
386
  const filePath = path.join(path.dirname(currentPath), `${newName}${ext}`);
350
387
  if (fs.existsSync(filePath) && filePath !== currentPath) {
351
388
  return { error: `File already exists at ${filePath} but is not a valid ${kind} definition. Remove or rename it first.` };
@@ -381,6 +418,41 @@ function formatAgentDetail(agent: AgentConfig): string {
381
418
  return lines.join("\n");
382
419
  }
383
420
 
421
+ function formatChainStepDetail(step: ChainStepConfig, index: number): string[] {
422
+ const lines: string[] = [];
423
+ if (step.expand || step.collect) {
424
+ const parallel = step.parallel && !Array.isArray(step.parallel) && typeof step.parallel === "object" ? step.parallel as { agent?: unknown; task?: unknown; label?: unknown; outputSchema?: unknown } : undefined;
425
+ const expand = step.expand && typeof step.expand === "object" ? step.expand as { from?: { output?: unknown; path?: unknown }; item?: unknown; key?: unknown; maxItems?: unknown; onEmpty?: unknown } : undefined;
426
+ const collect = step.collect && typeof step.collect === "object" ? step.collect as { as?: unknown; outputSchema?: unknown } : undefined;
427
+ lines.push(`${index + 1}. Dynamic fanout${typeof collect?.as === "string" ? ` -> ${collect.as}` : ""}`);
428
+ if (expand?.from) lines.push(` Expand: ${String(expand.from.output ?? "?")}${String(expand.from.path ?? "")}`);
429
+ if (typeof expand?.item === "string") lines.push(` Item variable: ${expand.item}`);
430
+ if (typeof expand?.key === "string") lines.push(` Key: ${expand.key}`);
431
+ if (typeof expand?.maxItems === "number") lines.push(` Max items: ${expand.maxItems}`);
432
+ if (typeof expand?.onEmpty === "string") lines.push(` On empty: ${expand.onEmpty}`);
433
+ if (parallel?.agent) lines.push(` Agent: ${String(parallel.agent)}`);
434
+ if (typeof parallel?.label === "string") lines.push(` Label: ${parallel.label}`);
435
+ if (typeof parallel?.task === "string" && parallel.task.trim()) lines.push(` Task: ${parallel.task}`);
436
+ if (parallel?.outputSchema) lines.push(" Structured output: true");
437
+ if (collect?.outputSchema) lines.push(" Collect schema: true");
438
+ if (step.concurrency !== undefined) lines.push(` Concurrency: ${step.concurrency}`);
439
+ if (step.failFast !== undefined) lines.push(` Fail fast: ${step.failFast ? "true" : "false"}`);
440
+ return lines;
441
+ }
442
+ lines.push(`${index + 1}. ${step.agent}`);
443
+ if (step.task?.trim()) lines.push(` Task: ${step.task}`);
444
+ if (step.output === false) lines.push(" Output: false");
445
+ else if (step.output) lines.push(` Output: ${step.output}`);
446
+ if (step.outputMode) lines.push(` Output mode: ${step.outputMode}`);
447
+ if (step.reads === false) lines.push(" Reads: false");
448
+ else if (Array.isArray(step.reads) && step.reads.length > 0) lines.push(` Reads: ${step.reads.join(", ")}`);
449
+ if (step.model) lines.push(` Model: ${step.model}`);
450
+ if (step.skills === false) lines.push(" Skills: false");
451
+ else if (Array.isArray(step.skills) && step.skills.length > 0) lines.push(` Skills: ${step.skills.join(", ")}`);
452
+ if (step.progress !== undefined) lines.push(` Progress: ${step.progress ? "true" : "false"}`);
453
+ return lines;
454
+ }
455
+
384
456
  function formatChainDetail(chain: ChainConfig): string {
385
457
  const lines: string[] = [`Chain: ${chain.name} (${chain.source})`, `Path: ${chain.filePath}`, `Description: ${chain.description}`];
386
458
  if (chain.packageName) {
@@ -389,18 +461,7 @@ function formatChainDetail(chain: ChainConfig): string {
389
461
  }
390
462
  lines.push("", "Steps:");
391
463
  for (let i = 0; i < chain.steps.length; i++) {
392
- const s = chain.steps[i]!;
393
- lines.push(`${i + 1}. ${s.agent}`);
394
- if (s.task.trim()) lines.push(` Task: ${s.task}`);
395
- if (s.output === false) lines.push(" Output: false");
396
- else if (s.output) lines.push(` Output: ${s.output}`);
397
- if (s.outputMode) lines.push(` Output mode: ${s.outputMode}`);
398
- if (s.reads === false) lines.push(" Reads: false");
399
- else if (Array.isArray(s.reads) && s.reads.length > 0) lines.push(` Reads: ${s.reads.join(", ")}`);
400
- if (s.model) lines.push(` Model: ${s.model}`);
401
- if (s.skills === false) lines.push(" Skills: false");
402
- else if (Array.isArray(s.skills) && s.skills.length > 0) lines.push(` Skills: ${s.skills.join(", ")}`);
403
- if (s.progress !== undefined) lines.push(` Progress: ${s.progress ? "true" : "false"}`);
464
+ lines.push(...formatChainStepDetail(chain.steps[i]!, i));
404
465
  }
405
466
  return lines.join("\n");
406
467
  }
@@ -411,6 +472,7 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Su
411
472
  const scopedAgents = allAgents(d).filter((a) => scope === "both" || a.source === "builtin" || a.source === scope).sort((a, b) => a.name.localeCompare(b.name));
412
473
  const agents = scopedAgents.filter((a) => !a.disabled);
413
474
  const chains = d.chains.filter((c) => scope === "both" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
475
+ const diagnostics = d.chainDiagnostics.filter((entry) => scope === "both" || entry.source === scope);
414
476
  const lines = [
415
477
  "Executable agents:",
416
478
  ...(agents.length
@@ -419,6 +481,7 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Su
419
481
  "",
420
482
  "Chains:",
421
483
  ...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
484
+ ...(diagnostics.length ? ["", "Chain diagnostics:", ...diagnostics.map((entry) => `- ${entry.filePath}: ${entry.error}`)] : []),
422
485
  ];
423
486
  return result(lines.join("\n"));
424
487
  }
@@ -614,7 +677,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
614
677
  if (renamed.error) return result(renamed.error, true);
615
678
  updated.filePath = renamed.filePath!;
616
679
  }
617
- fs.writeFileSync(updated.filePath, serializeChain(updated), "utf-8");
680
+ fs.writeFileSync(updated.filePath, updated.filePath.endsWith(".chain.json") ? serializeJsonChain(updated) : serializeChain(updated), "utf-8");
618
681
  const headline = updated.name === oldName
619
682
  ? `Updated chain '${updated.name}' at ${updated.filePath}.`
620
683
  : `Updated chain '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
@@ -7,9 +7,9 @@ import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { CONFIG_DIR_NAME, getAgentConfigPaths, getEnvValue, getProjectConfigDirs } from "@bastani/atomic";
10
- import type { OutputMode } from "../shared/types.ts";
10
+ import type { AcceptanceInput, OutputMode } from "../shared/types.ts";
11
11
  import { KNOWN_FIELDS } from "./agent-serializer.ts";
12
- import { parseChain } from "./chain-serializer.ts";
12
+ import { parseChain, parseJsonChain } from "./chain-serializer.ts";
13
13
  import { mergeAgentsForScope } from "./agent-selection.ts";
14
14
  import { parseFrontmatter } from "./frontmatter.ts";
15
15
  import { buildRuntimeName, parsePackageName } from "./identity.ts";
@@ -111,14 +111,25 @@ interface SubagentSettings {
111
111
  const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
112
112
 
113
113
  export interface ChainStepConfig {
114
- agent: string;
115
- task: string;
114
+ agent?: string;
115
+ task?: string;
116
+ phase?: string;
117
+ label?: string;
118
+ as?: string;
119
+ outputSchema?: string | Record<string, unknown>;
116
120
  output?: string | false;
117
121
  outputMode?: OutputMode;
118
122
  reads?: string[] | false;
119
123
  model?: string;
120
124
  skills?: string[] | false;
121
125
  progress?: boolean;
126
+ parallel?: unknown;
127
+ expand?: unknown;
128
+ collect?: unknown;
129
+ concurrency?: number;
130
+ failFast?: boolean;
131
+ worktree?: boolean;
132
+ acceptance?: AcceptanceInput;
122
133
  }
123
134
 
124
135
  export interface ChainConfig {
@@ -132,6 +143,12 @@ export interface ChainConfig {
132
143
  extraFields?: Record<string, string>;
133
144
  }
134
145
 
146
+ export interface ChainDiscoveryDiagnostic {
147
+ source: "user" | "project";
148
+ filePath: string;
149
+ error: string;
150
+ }
151
+
135
152
  interface AgentDiscoveryResult {
136
153
  agents: AgentConfig[];
137
154
  projectAgentsDir: string | null;
@@ -582,7 +599,7 @@ export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "us
582
599
  return filePath;
583
600
  }
584
601
 
585
- function listMarkdownFilesRecursive(dir: string, predicate: (fileName: string) => boolean): string[] {
602
+ function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean): string[] {
586
603
  const files: string[] = [];
587
604
  if (!fs.existsSync(dir)) return files;
588
605
 
@@ -596,7 +613,7 @@ function listMarkdownFilesRecursive(dir: string, predicate: (fileName: string) =
596
613
  for (const entry of entries) {
597
614
  const filePath = path.join(dir, entry.name);
598
615
  if (entry.isDirectory()) {
599
- files.push(...listMarkdownFilesRecursive(filePath, predicate));
616
+ files.push(...listFilesRecursive(filePath, predicate));
600
617
  continue;
601
618
  }
602
619
  if (!entry.isFile() && !entry.isSymbolicLink()) continue;
@@ -609,7 +626,7 @@ function listMarkdownFilesRecursive(dir: string, predicate: (fileName: string) =
609
626
  function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
610
627
  const agents: AgentConfig[] = [];
611
628
 
612
- for (const filePath of listMarkdownFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
629
+ for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
613
630
  let content: string;
614
631
  try {
615
632
  content = fs.readFileSync(filePath, "utf-8");
@@ -741,10 +758,11 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
741
758
  return agents;
742
759
  }
743
760
 
744
- function loadChainsFromDir(dir: string, source: "user" | "project"): ChainConfig[] {
745
- const chains: ChainConfig[] = [];
761
+ function loadChainsFromDir(dir: string, source: "user" | "project"): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
762
+ const chains = new Map<string, ChainConfig>();
763
+ const diagnostics: ChainDiscoveryDiagnostic[] = [];
746
764
 
747
- for (const filePath of listMarkdownFilesRecursive(dir, (fileName) => fileName.endsWith(".chain.md"))) {
765
+ for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".chain.md") || fileName.endsWith(".chain.json"))) {
748
766
  let content: string;
749
767
  try {
750
768
  content = fs.readFileSync(filePath, "utf-8");
@@ -753,13 +771,17 @@ function loadChainsFromDir(dir: string, source: "user" | "project"): ChainConfig
753
771
  }
754
772
 
755
773
  try {
756
- chains.push(parseChain(content, source, filePath));
757
- } catch {
774
+ const chain = filePath.endsWith(".chain.json") ? parseJsonChain(content, source, filePath) : parseChain(content, source, filePath);
775
+ const existing = chains.get(chain.name);
776
+ if (existing && existing.filePath.endsWith(".chain.json") && filePath.endsWith(".chain.md")) continue;
777
+ chains.set(chain.name, chain);
778
+ } catch (error) {
779
+ diagnostics.push({ source, filePath, error: error instanceof Error ? error.message : String(error) });
758
780
  continue;
759
781
  }
760
782
  }
761
783
 
762
- return chains;
784
+ return { chains: Array.from(chains.values()), diagnostics };
763
785
  }
764
786
 
765
787
  function isDirectory(p: string): boolean {
@@ -836,6 +858,7 @@ export function discoverAgentsAll(cwd: string): {
836
858
  user: AgentConfig[];
837
859
  project: AgentConfig[];
838
860
  chains: ChainConfig[];
861
+ chainDiagnostics: ChainDiscoveryDiagnostic[];
839
862
  userDir: string;
840
863
  projectDir: string | null;
841
864
  userChainDir: string;
@@ -875,19 +898,27 @@ export function discoverAgentsAll(cwd: string): {
875
898
  const project = Array.from(projectMap.values());
876
899
 
877
900
  const chainMap = new Map<string, ChainConfig>();
901
+ const projectChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
878
902
  for (const dir of projectChainDirs) {
879
- for (const chain of loadChainsFromDir(dir, "project")) {
903
+ const loaded = loadChainsFromDir(dir, "project");
904
+ projectChainDiagnostics.push(...loaded.diagnostics);
905
+ for (const chain of loaded.chains) {
880
906
  chainMap.set(chain.name, chain);
881
907
  }
882
908
  }
909
+ const userChainLoads = getUserChainDirs().map((dir) => loadChainsFromDir(dir, "user"));
883
910
  const chains = [
884
- ...getUserChainDirs().flatMap((dir) => loadChainsFromDir(dir, "user")),
911
+ ...userChainLoads.flatMap((loaded) => loaded.chains),
885
912
  ...Array.from(chainMap.values()),
886
913
  ];
914
+ const chainDiagnostics = [
915
+ ...userChainLoads.flatMap((loaded) => loaded.diagnostics),
916
+ ...projectChainDiagnostics,
917
+ ];
887
918
 
888
919
  const legacyUserAgentDir = userDirOld[0]!;
889
920
  // ATOMIC_CODING_AGENT_DIR is already applied by getUserAgentDirs(); prefer that resolved path over ~/.agents.
890
921
  const userDir = getEnvValue("ATOMIC_CODING_AGENT_DIR") ? legacyUserAgentDir : fs.existsSync(userDirNew) ? userDirNew : legacyUserAgentDir;
891
922
 
892
- return { builtin, user, project, chains, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
923
+ return { builtin, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
893
924
  }
@@ -1,6 +1,9 @@
1
1
  import type { ChainConfig, ChainStepConfig } from "./agents.ts";
2
2
  import { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
3
3
  import { parseFrontmatter } from "./frontmatter.ts";
4
+ import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
5
+ import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
6
+ import type { ChainStep } from "../shared/settings.ts";
4
7
 
5
8
  function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
6
9
  const lines = sectionBody.split("\n");
@@ -20,6 +23,25 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
20
23
  else if (rawValue) step.output = rawValue;
21
24
  continue;
22
25
  }
26
+ if (key === "phase") {
27
+ if (rawValue) step.phase = rawValue;
28
+ continue;
29
+ }
30
+ if (key === "label") {
31
+ if (rawValue) step.label = rawValue;
32
+ continue;
33
+ }
34
+ if (key === "as") {
35
+ if (rawValue) step.as = rawValue;
36
+ continue;
37
+ }
38
+ if (key === "outputschema") {
39
+ if (rawValue.startsWith("{") || rawValue.startsWith("[")) {
40
+ throw new Error("Inline outputSchema values are not supported in .chain.md files; use a schema file path.");
41
+ }
42
+ if (rawValue) step.outputSchema = rawValue;
43
+ continue;
44
+ }
23
45
  if (key === "outputmode") {
24
46
  if (rawValue === "inline" || rawValue === "file-only") step.outputMode = rawValue;
25
47
  continue;
@@ -102,6 +124,94 @@ export function parseChain(content: string, source: "user" | "project", filePath
102
124
  };
103
125
  }
104
126
 
127
+ export function parseJsonChain(content: string, source: "user" | "project", filePath: string): ChainConfig {
128
+ let parsed: unknown;
129
+ try {
130
+ parsed = JSON.parse(content);
131
+ } catch (error) {
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ throw new Error(`Invalid JSON chain '${filePath}': ${message}`);
134
+ }
135
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
136
+ throw new Error(`JSON chain '${filePath}' must contain an object root.`);
137
+ }
138
+ const input = parsed as Record<string, unknown>;
139
+ if (typeof input.name !== "string" || !input.name.trim()) {
140
+ throw new Error(`JSON chain '${filePath}' must include string name.`);
141
+ }
142
+ if (typeof input.description !== "string" || !input.description.trim()) {
143
+ throw new Error(`JSON chain '${filePath}' must include string description.`);
144
+ }
145
+ if (!Array.isArray(input.chain)) {
146
+ throw new Error(`JSON chain '${filePath}' must include array chain.`);
147
+ }
148
+ for (let i = 0; i < input.chain.length; i++) {
149
+ const step = input.chain[i];
150
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
151
+ throw new Error(`JSON chain '${filePath}' step ${i + 1} must be an object.`);
152
+ }
153
+ const stepRecord = step as Record<string, unknown>;
154
+ const acceptanceErrors = validateAcceptanceInput(stepRecord.acceptance, `step ${i + 1} acceptance`);
155
+ if (acceptanceErrors.length > 0) {
156
+ throw new Error(`Invalid JSON chain '${filePath}': ${acceptanceErrors.join(" ")}`);
157
+ }
158
+ const parallel = stepRecord.parallel;
159
+ if (Array.isArray(parallel)) {
160
+ for (let taskIndex = 0; taskIndex < parallel.length; taskIndex++) {
161
+ const task = parallel[taskIndex];
162
+ if (!task || typeof task !== "object" || Array.isArray(task)) continue;
163
+ const taskErrors = validateAcceptanceInput((task as Record<string, unknown>).acceptance, `step ${i + 1} parallel task ${taskIndex + 1} acceptance`);
164
+ if (taskErrors.length > 0) {
165
+ throw new Error(`Invalid JSON chain '${filePath}': ${taskErrors.join(" ")}`);
166
+ }
167
+ }
168
+ } else if (parallel && typeof parallel === "object") {
169
+ const templateErrors = validateAcceptanceInput((parallel as Record<string, unknown>).acceptance, `step ${i + 1} dynamic template acceptance`);
170
+ if (templateErrors.length > 0) {
171
+ throw new Error(`Invalid JSON chain '${filePath}': ${templateErrors.join(" ")}`);
172
+ }
173
+ }
174
+ }
175
+ try {
176
+ validateChainOutputBindings(input.chain as ChainStep[], { maxItems: Number.MAX_SAFE_INTEGER });
177
+ } catch (error) {
178
+ if (error instanceof ChainOutputValidationError) throw new Error(`Invalid JSON chain '${filePath}': ${error.message}`);
179
+ throw error;
180
+ }
181
+ const parsedPackage = parsePackageName(typeof input.package === "string" ? input.package : undefined, `Chain '${input.name}' package`);
182
+ if (parsedPackage.error) throw new Error(parsedPackage.error);
183
+ const extraFields: Record<string, string> = {};
184
+ for (const [key, value] of Object.entries(input)) {
185
+ if (key === "name" || key === "package" || key === "description" || key === "chain") continue;
186
+ if (typeof value === "string") extraFields[key] = value;
187
+ }
188
+ return {
189
+ name: buildRuntimeName(input.name.trim(), parsedPackage.packageName),
190
+ localName: input.name.trim(),
191
+ packageName: parsedPackage.packageName,
192
+ description: input.description.trim(),
193
+ source,
194
+ filePath,
195
+ steps: input.chain as ChainStepConfig[],
196
+ extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
197
+ };
198
+ }
199
+
200
+ export function serializeJsonChain(config: ChainConfig): string {
201
+ const root: Record<string, unknown> = {
202
+ name: frontmatterNameForConfig(config),
203
+ description: config.description,
204
+ chain: config.steps,
205
+ };
206
+ if (config.packageName) root.package = config.packageName;
207
+ if (config.extraFields) {
208
+ for (const [key, value] of Object.entries(config.extraFields)) {
209
+ if (key !== "name" && key !== "description" && key !== "package" && key !== "chain") root[key] = value;
210
+ }
211
+ }
212
+ return `${JSON.stringify(root, null, 2)}\n`;
213
+ }
214
+
105
215
  export function serializeChain(config: ChainConfig): string {
106
216
  const lines: string[] = [];
107
217
  lines.push("---");
@@ -121,6 +231,10 @@ export function serializeChain(config: ChainConfig): string {
121
231
  lines.push(`## ${step.agent}`);
122
232
  if (step.output === false) lines.push("output: false");
123
233
  else if (step.output) lines.push(`output: ${step.output}`);
234
+ if (step.phase) lines.push(`phase: ${step.phase}`);
235
+ if (step.label) lines.push(`label: ${step.label}`);
236
+ if (step.as) lines.push(`as: ${step.as}`);
237
+ if (step.outputSchema) lines.push(`outputSchema: ${step.outputSchema}`);
124
238
  if (step.outputMode) lines.push(`outputMode: ${step.outputMode}`);
125
239
  if (step.reads === false) lines.push("reads: false");
126
240
  else if (Array.isArray(step.reads) && step.reads.length > 0) lines.push(`reads: ${step.reads.join(", ")}`);
@@ -35,6 +35,82 @@ const ReadsOverride = Type.Unsafe({
35
35
  description: "Files to read before running (array of filenames), or false to disable",
36
36
  });
37
37
 
38
+ const JsonSchemaObject = Type.Unsafe({
39
+ type: "object",
40
+ additionalProperties: true,
41
+ description: "JSON Schema object for strict structured output. Non-object roots are rejected.",
42
+ });
43
+
44
+ const AcceptanceEvidenceKind = Type.String({
45
+ enum: [
46
+ "changed-files",
47
+ "tests-added",
48
+ "commands-run",
49
+ "validation-output",
50
+ "residual-risks",
51
+ "no-staged-files",
52
+ "diff-summary",
53
+ "review-findings",
54
+ "manual-notes",
55
+ ],
56
+ });
57
+
58
+ const AcceptanceGateSchema = Type.Object({
59
+ id: Type.String(),
60
+ must: Type.String(),
61
+ evidence: Type.Optional(Type.Array(AcceptanceEvidenceKind)),
62
+ severity: Type.Optional(Type.String({ enum: ["required", "recommended"] })),
63
+ }, { additionalProperties: false });
64
+
65
+ const AcceptanceVerifyCommandSchema = Type.Object({
66
+ id: Type.String(),
67
+ command: Type.String(),
68
+ timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
69
+ cwd: Type.Optional(Type.String()),
70
+ env: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: { type: "string" } })),
71
+ allowFailure: Type.Optional(Type.Boolean()),
72
+ }, { additionalProperties: false });
73
+
74
+ const AcceptanceReviewGateSchema = Type.Object({
75
+ agent: Type.Optional(Type.String()),
76
+ focus: Type.Optional(Type.String()),
77
+ required: Type.Optional(Type.Boolean()),
78
+ }, { additionalProperties: false });
79
+
80
+ const AcceptanceOverride = Type.Unsafe({
81
+ anyOf: [
82
+ { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
83
+ { const: false },
84
+ {
85
+ type: "object",
86
+ properties: {
87
+ level: { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
88
+ criteria: {
89
+ type: "array",
90
+ items: {
91
+ anyOf: [
92
+ { type: "string" },
93
+ AcceptanceGateSchema,
94
+ ],
95
+ },
96
+ },
97
+ evidence: { type: "array", items: AcceptanceEvidenceKind },
98
+ verify: { type: "array", items: AcceptanceVerifyCommandSchema },
99
+ review: {
100
+ anyOf: [
101
+ { const: false },
102
+ AcceptanceReviewGateSchema,
103
+ ],
104
+ },
105
+ stopRules: { type: "array", items: { type: "string" } },
106
+ reason: { type: "string" },
107
+ },
108
+ additionalProperties: false,
109
+ },
110
+ ],
111
+ description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
112
+ });
113
+
38
114
  const TaskItem = Type.Object({
39
115
  agent: Type.String(),
40
116
  task: Type.String(),
@@ -46,12 +122,17 @@ const TaskItem = Type.Object({
46
122
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
47
123
  model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
48
124
  skill: Type.Optional(SkillOverride),
125
+ acceptance: Type.Optional(AcceptanceOverride),
49
126
  });
50
127
 
51
128
  // Parallel task item (within a parallel step)
52
129
  const ParallelTaskSchema = Type.Object({
53
130
  agent: Type.String(),
54
131
  task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })),
132
+ phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
133
+ label: Type.Optional(Type.String({ description: "Optional user-facing label for this parallel task." })),
134
+ as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
135
+ outputSchema: Type.Optional(JsonSchemaObject),
55
136
  cwd: Type.Optional(Type.String()),
56
137
  count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
57
138
  output: Type.Optional(OutputOverride),
@@ -60,14 +141,51 @@ const ParallelTaskSchema = Type.Object({
60
141
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
61
142
  skill: Type.Optional(SkillOverride),
62
143
  model: Type.Optional(Type.String({ description: "Override model for this task" })),
144
+ acceptance: Type.Optional(AcceptanceOverride),
63
145
  });
64
146
 
147
+ const DynamicExpandSchema = Type.Object({
148
+ from: Type.Object({
149
+ output: Type.String({ description: "Prior named structured output to expand from." }),
150
+ path: Type.String({ description: "JSON Pointer into the structured output, e.g. /items." }),
151
+ }, { additionalProperties: false }),
152
+ item: Type.Optional(Type.String({ description: "Template variable name for each item. Defaults to item." })),
153
+ key: Type.Optional(Type.String({ description: "JSON Pointer relative to each item for stable child ids." })),
154
+ maxItems: Type.Optional(Type.Integer({ minimum: 0, description: "Required fanout bound unless configured globally." })),
155
+ onEmpty: Type.Optional(Type.String({ enum: ["skip", "fail"], description: "Empty input behavior. Defaults to skip." })),
156
+ }, { additionalProperties: false });
157
+
158
+ const DynamicParallelTemplateSchema = Type.Object({
159
+ agent: Type.String(),
160
+ task: Type.Optional(Type.String({ description: "Task template with {item}, {item.path}, {task}, {previous}, {chain_dir}, and {outputs.name} variables." })),
161
+ phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
162
+ label: Type.Optional(Type.String({ description: "Optional user-facing label; item templates are supported." })),
163
+ outputSchema: Type.Optional(JsonSchemaObject),
164
+ cwd: Type.Optional(Type.String()),
165
+ output: Type.Optional(OutputOverride),
166
+ outputMode: Type.Optional(OutputModeOverride),
167
+ reads: Type.Optional(ReadsOverride),
168
+ progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
169
+ skill: Type.Optional(SkillOverride),
170
+ model: Type.Optional(Type.String({ description: "Override model for this task" })),
171
+ acceptance: Type.Optional(AcceptanceOverride),
172
+ }, { additionalProperties: false });
173
+
174
+ const DynamicCollectSchema = Type.Object({
175
+ as: Type.String({ description: "Safe output name for the ordered collected result array." }),
176
+ outputSchema: Type.Optional(JsonSchemaObject),
177
+ }, { additionalProperties: false });
178
+
65
179
  // Flattened so chain steps do not need an object-shape anyOf/oneOf union.
66
180
  const ChainItem = Type.Object({
67
181
  agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
68
182
  task: Type.Optional(Type.String({
69
- description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
183
+ description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder, {outputs.name}=prior named output. Required for first step, defaults to '{previous}' for subsequent steps."
70
184
  })),
185
+ phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
186
+ label: Type.Optional(Type.String({ description: "Optional user-facing label for this chain step." })),
187
+ as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
188
+ outputSchema: Type.Optional(JsonSchemaObject),
71
189
  cwd: Type.Optional(Type.String()),
72
190
  output: Type.Optional(OutputOverride),
73
191
  outputMode: Type.Optional(OutputModeOverride),
@@ -75,13 +193,30 @@ const ChainItem = Type.Object({
75
193
  progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
76
194
  skill: Type.Optional(SkillOverride),
77
195
  model: Type.Optional(Type.String({ description: "Override model for this step" })),
78
- parallel: Type.Optional(Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" })),
196
+ acceptance: Type.Optional(AcceptanceOverride),
197
+ parallel: Type.Optional(Type.Unsafe({
198
+ anyOf: [
199
+ Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
200
+ DynamicParallelTemplateSchema,
201
+ ],
202
+ description: "Static parallel tasks array, or a single dynamic fanout child template when expand/collect are present.",
203
+ })),
204
+ expand: Type.Optional(DynamicExpandSchema),
205
+ collect: Type.Optional(DynamicCollectSchema),
79
206
  concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
80
207
  failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
81
208
  worktree: Type.Optional(Type.Boolean({
82
209
  description: "Create isolated git worktrees for each parallel task."
83
210
  })),
84
- }, { description: "Chain step: use {agent, task?, ...} for sequential or {parallel: [...]} for concurrent execution" });
211
+ }, {
212
+ description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
213
+ additionalProperties: false,
214
+ allOf: [
215
+ { if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
216
+ { if: { required: ["collect"] }, then: { required: ["expand", "parallel"], properties: { parallel: { type: "object" } } } },
217
+ { not: { required: ["expand"], properties: { parallel: { type: "array", items: {} } } } },
218
+ ],
219
+ });
85
220
 
86
221
  const ControlOverrides = Type.Object({
87
222
  enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
@@ -165,4 +300,5 @@ export const SubagentParams = Type.Object({
165
300
  outputMode: Type.Optional(OutputModeOverride),
166
301
  skill: Type.Optional(SkillOverride),
167
302
  model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
303
+ acceptance: Type.Optional(AcceptanceOverride),
168
304
  });