@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
@@ -0,0 +1,206 @@
1
+ import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
2
+ import type { SingleResult, SubagentRunMode, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowNodeStatus } from "../../shared/types.ts";
3
+
4
+ export interface WorkflowGraphBuildInput {
5
+ runId: string;
6
+ mode?: SubagentRunMode;
7
+ steps: ChainStep[];
8
+ results?: Array<Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "error" | "acceptance">>;
9
+ currentFlatIndex?: number;
10
+ currentStepIndex?: number;
11
+ stepStatuses?: Array<{ status?: string; error?: string }>;
12
+ dynamicChildren?: Record<number, Array<{ agent: string; label?: string; flatIndex: number; itemKey: string; outputName?: string; structured?: boolean; error?: string }>>;
13
+ dynamicGroupStatuses?: Record<number, { status: WorkflowNodeStatus; error?: string; acceptance?: SingleResult["acceptance"] }>;
14
+ }
15
+
16
+ function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undefined {
17
+ switch (status) {
18
+ case "complete":
19
+ case "completed":
20
+ return "completed";
21
+ case "running":
22
+ return "running";
23
+ case "failed":
24
+ return "failed";
25
+ case "paused":
26
+ return "paused";
27
+ case "detached":
28
+ return "detached";
29
+ case "pending":
30
+ return "pending";
31
+ default:
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ function resultStatus(result: Pick<SingleResult, "exitCode" | "detached" | "interrupted"> | undefined): WorkflowNodeStatus | undefined {
37
+ if (!result) return undefined;
38
+ if (result.detached) return "detached";
39
+ if (result.interrupted) return "paused";
40
+ return result.exitCode === 0 ? "completed" : "failed";
41
+ }
42
+
43
+ function nodeStatus(input: WorkflowGraphBuildInput, flatIndex: number): WorkflowNodeStatus {
44
+ return normalizeStatus(input.stepStatuses?.[flatIndex]?.status)
45
+ ?? resultStatus(input.results?.[flatIndex])
46
+ ?? (input.currentFlatIndex === flatIndex ? "running" : "pending");
47
+ }
48
+
49
+ function pushPhase(phases: WorkflowGraphSnapshot["phases"], phase: string | undefined, nodeId: string): void {
50
+ if (!phase) return;
51
+ let group = phases.find((candidate) => candidate.title === phase);
52
+ if (!group) {
53
+ group = { title: phase, nodeIds: [] };
54
+ phases.push(group);
55
+ }
56
+ group.nodeIds.push(nodeId);
57
+ }
58
+
59
+ function seqLabel(step: SequentialStep, stepIndex: number): string {
60
+ return step.label?.trim() || step.agent || `Step ${stepIndex + 1}`;
61
+ }
62
+
63
+ function summarizeParallelStatuses(statuses: WorkflowNodeStatus[]): WorkflowNodeStatus {
64
+ if (statuses.some((status) => status === "running")) return "running";
65
+ if (statuses.some((status) => status === "failed")) return "failed";
66
+ if (statuses.some((status) => status === "paused")) return "paused";
67
+ if (statuses.some((status) => status === "detached")) return "detached";
68
+ if (statuses.length > 0 && statuses.every((status) => status === "completed")) return "completed";
69
+ if (statuses.some((status) => status === "completed")) return "running";
70
+ return "pending";
71
+ }
72
+
73
+ export function buildWorkflowGraphSnapshot(input: WorkflowGraphBuildInput): WorkflowGraphSnapshot {
74
+ const nodes: WorkflowGraphNode[] = [];
75
+ const phases: WorkflowGraphSnapshot["phases"] = [];
76
+ let flatIndex = 0;
77
+ let currentNodeId: string | undefined;
78
+
79
+ for (let stepIndex = 0; stepIndex < input.steps.length; stepIndex++) {
80
+ const step = input.steps[stepIndex]!;
81
+ if (isParallelStep(step)) {
82
+ const groupId = `step-${stepIndex}`;
83
+ const children: WorkflowGraphNode[] = [];
84
+ const childStatuses: WorkflowNodeStatus[] = [];
85
+ for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
86
+ const task = step.parallel[taskIndex]!;
87
+ const status = nodeStatus(input, flatIndex);
88
+ childStatuses.push(status);
89
+ const childId = `step-${stepIndex}-agent-${taskIndex}`;
90
+ const child: WorkflowGraphNode = {
91
+ id: childId,
92
+ kind: "agent",
93
+ agent: task.agent,
94
+ phase: task.phase,
95
+ label: task.label?.trim() || task.agent || `Agent ${taskIndex + 1}`,
96
+ status,
97
+ flatIndex,
98
+ stepIndex,
99
+ outputName: task.as,
100
+ structured: Boolean(task.outputSchema),
101
+ acceptanceStatus: input.results?.[flatIndex]?.acceptance?.status,
102
+ error: input.stepStatuses?.[flatIndex]?.error ?? input.results?.[flatIndex]?.error,
103
+ };
104
+ children.push(child);
105
+ pushPhase(phases, task.phase, childId);
106
+ if (status === "running" || input.currentFlatIndex === flatIndex) currentNodeId = childId;
107
+ flatIndex++;
108
+ }
109
+ const groupStatus = summarizeParallelStatuses(childStatuses);
110
+ if (input.currentStepIndex === stepIndex && !currentNodeId) currentNodeId = groupId;
111
+ nodes.push({
112
+ id: groupId,
113
+ kind: "parallel-group",
114
+ label: step.parallel.length === 1 ? "Parallel task" : `Parallel group (${step.parallel.length})`,
115
+ status: groupStatus,
116
+ stepIndex,
117
+ children,
118
+ });
119
+ continue;
120
+ }
121
+
122
+ if (isDynamicParallelStep(step)) {
123
+ const groupId = `step-${stepIndex}`;
124
+ const materialized = input.dynamicChildren?.[stepIndex] ?? [];
125
+ const groupOverride = input.dynamicGroupStatuses?.[stepIndex];
126
+ const children: WorkflowGraphNode[] = [];
127
+ const childStatuses: WorkflowNodeStatus[] = [];
128
+ for (let taskIndex = 0; taskIndex < materialized.length; taskIndex++) {
129
+ const task = materialized[taskIndex]!;
130
+ const status = nodeStatus(input, task.flatIndex);
131
+ childStatuses.push(status);
132
+ const childId = `step-${stepIndex}-item-${task.itemKey.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
133
+ const child: WorkflowGraphNode = {
134
+ id: childId,
135
+ kind: "agent",
136
+ agent: task.agent,
137
+ phase: step.parallel.phase ?? step.phase,
138
+ label: task.label?.trim() || step.parallel.label?.trim() || `${task.agent} ${task.itemKey}`,
139
+ status,
140
+ flatIndex: task.flatIndex,
141
+ stepIndex,
142
+ itemKey: task.itemKey,
143
+ outputName: task.outputName,
144
+ structured: task.structured,
145
+ acceptanceStatus: input.results?.[task.flatIndex]?.acceptance?.status,
146
+ error: input.stepStatuses?.[task.flatIndex]?.error ?? input.results?.[task.flatIndex]?.error ?? task.error,
147
+ };
148
+ children.push(child);
149
+ pushPhase(phases, child.phase, childId);
150
+ if (status === "running" || input.currentFlatIndex === task.flatIndex) currentNodeId = childId;
151
+ }
152
+ const groupStatus = groupOverride?.status ?? (children.length > 0 ? summarizeParallelStatuses(childStatuses) : (input.currentStepIndex === stepIndex ? "running" : "pending"));
153
+ if (input.currentStepIndex === stepIndex && !currentNodeId) currentNodeId = groupId;
154
+ nodes.push({
155
+ id: groupId,
156
+ kind: "dynamic-parallel-group",
157
+ label: step.label?.trim() || step.parallel.label?.trim() || `Dynamic fanout (${step.collect.as})`,
158
+ status: groupStatus,
159
+ stepIndex,
160
+ outputName: step.collect.as,
161
+ structured: Boolean(step.collect.outputSchema),
162
+ acceptanceStatus: groupOverride?.acceptance?.status,
163
+ error: groupOverride?.error,
164
+ dynamic: {
165
+ sourceOutput: step.expand.from.output,
166
+ sourcePath: step.expand.from.path,
167
+ itemName: step.expand.item ?? "item",
168
+ maxItems: step.expand.maxItems,
169
+ collectAs: step.collect.as,
170
+ },
171
+ children,
172
+ });
173
+ if (materialized.length > 0) flatIndex = Math.max(flatIndex, ...materialized.map((child) => child.flatIndex + 1));
174
+ continue;
175
+ }
176
+
177
+ const seq = step as SequentialStep;
178
+ const status = nodeStatus(input, flatIndex);
179
+ const id = `step-${stepIndex}`;
180
+ nodes.push({
181
+ id,
182
+ kind: "step",
183
+ agent: seq.agent,
184
+ phase: seq.phase,
185
+ label: seqLabel(seq, stepIndex),
186
+ status,
187
+ flatIndex,
188
+ stepIndex,
189
+ outputName: seq.as,
190
+ structured: Boolean(seq.outputSchema),
191
+ acceptanceStatus: input.results?.[flatIndex]?.acceptance?.status,
192
+ error: input.stepStatuses?.[flatIndex]?.error ?? input.results?.[flatIndex]?.error,
193
+ });
194
+ pushPhase(phases, seq.phase, id);
195
+ if (status === "running" || input.currentFlatIndex === flatIndex || input.currentStepIndex === stepIndex) currentNodeId = id;
196
+ flatIndex++;
197
+ }
198
+
199
+ return {
200
+ runId: input.runId,
201
+ mode: input.mode ?? "chain",
202
+ phases,
203
+ nodes,
204
+ currentNodeId,
205
+ };
206
+ }
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { Usage, SingleResult } from "./types.ts";
8
8
  import type { ChainStep } from "./settings.ts";
9
- import { isParallelStep } from "./settings.ts";
9
+ import { isDynamicParallelStep, isParallelStep } from "./settings.ts";
10
10
  import { splitKnownThinkingSuffix, THINKING_LEVELS } from "./model-info.ts";
11
11
 
12
12
  /**
@@ -70,7 +70,7 @@ export function buildChainSummary(
70
70
  failedStep?: { index: number; error: string },
71
71
  ): string {
72
72
  const stepNames = steps
73
- .map((step) => (isParallelStep(step) ? `parallel[${step.parallel.length}]` : step.agent))
73
+ .map((step) => (isParallelStep(step) ? `parallel[${step.parallel.length}]` : isDynamicParallelStep(step) ? `expand:${step.parallel.agent}` : step.agent))
74
74
  .join(" → ");
75
75
 
76
76
  const totalDuration = results.reduce((sum, r) => sum + (r.progress?.durationMs || 0), 0);
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { AgentConfig } from "../agents/agents.ts";
8
8
  import { normalizeSkillInput } from "../agents/skills.ts";
9
- import { CHAIN_RUNS_DIR, type OutputMode } from "./types.ts";
9
+ import { CHAIN_RUNS_DIR, type AcceptanceInput, type JsonSchemaObject, type OutputMode } from "./types.ts";
10
10
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
11
11
  const INITIAL_PROGRESS_CONTENT = "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n";
12
12
 
@@ -44,6 +44,10 @@ function normalizeOutputOverride(output: string | false | undefined): string | f
44
44
  export interface SequentialStep {
45
45
  agent: string;
46
46
  task?: string;
47
+ phase?: string;
48
+ label?: string;
49
+ as?: string;
50
+ outputSchema?: JsonSchemaObject;
47
51
  cwd?: string;
48
52
  output?: string | false;
49
53
  outputMode?: OutputMode;
@@ -51,12 +55,17 @@ export interface SequentialStep {
51
55
  progress?: boolean;
52
56
  skill?: string | string[] | false;
53
57
  model?: string;
58
+ acceptance?: AcceptanceInput;
54
59
  }
55
60
 
56
61
  /** Parallel task item within a parallel step */
57
- interface ParallelTaskItem {
62
+ export interface ParallelTaskItem {
58
63
  agent: string;
59
64
  task?: string;
65
+ phase?: string;
66
+ label?: string;
67
+ as?: string;
68
+ outputSchema?: JsonSchemaObject;
60
69
  cwd?: string;
61
70
  count?: number;
62
71
  output?: string | false;
@@ -65,10 +74,40 @@ interface ParallelTaskItem {
65
74
  progress?: boolean;
66
75
  skill?: string | string[] | false;
67
76
  model?: string;
77
+ acceptance?: AcceptanceInput;
78
+ }
79
+
80
+ export interface DynamicExpandSpec {
81
+ from: {
82
+ output: string;
83
+ path: string;
84
+ };
85
+ item?: string;
86
+ key?: string;
87
+ maxItems?: number;
88
+ onEmpty?: "skip" | "fail";
89
+ }
90
+
91
+ export type DynamicParallelTemplate = Omit<ParallelTaskItem, "as" | "count">;
92
+
93
+ export interface DynamicCollectSpec {
94
+ as: string;
95
+ outputSchema?: JsonSchemaObject;
96
+ }
97
+
98
+ export interface DynamicParallelStep {
99
+ expand: DynamicExpandSpec;
100
+ parallel: DynamicParallelTemplate;
101
+ collect: DynamicCollectSpec;
102
+ concurrency?: number;
103
+ failFast?: boolean;
104
+ phase?: string;
105
+ label?: string;
106
+ acceptance?: AcceptanceInput;
68
107
  }
69
108
 
70
109
  /** Parallel step: multiple agents running concurrently */
71
- interface ParallelStep {
110
+ export interface ParallelStep {
72
111
  parallel: ParallelTaskItem[];
73
112
  cwd?: string;
74
113
  concurrency?: number;
@@ -77,7 +116,7 @@ interface ParallelStep {
77
116
  }
78
117
 
79
118
  /** Union type for chain steps */
80
- export type ChainStep = SequentialStep | ParallelStep;
119
+ export type ChainStep = SequentialStep | ParallelStep | DynamicParallelStep;
81
120
 
82
121
  // =============================================================================
83
122
  // Type Guards
@@ -87,11 +126,18 @@ export function isParallelStep(step: ChainStep): step is ParallelStep {
87
126
  return "parallel" in step && Array.isArray((step as ParallelStep).parallel);
88
127
  }
89
128
 
129
+ export function isDynamicParallelStep(step: ChainStep): step is DynamicParallelStep {
130
+ return "expand" in step && "collect" in step && "parallel" in step && !Array.isArray((step as { parallel?: unknown }).parallel);
131
+ }
132
+
90
133
  /** Get all agent names in a step (single for sequential, multiple for parallel) */
91
134
  export function getStepAgents(step: ChainStep): string[] {
92
135
  if (isParallelStep(step)) {
93
136
  return step.parallel.map((t) => t.agent);
94
137
  }
138
+ if (isDynamicParallelStep(step)) {
139
+ return [step.parallel.agent];
140
+ }
95
141
  return [step.agent];
96
142
  }
97
143
 
@@ -161,6 +207,9 @@ export function resolveChainTemplates(
161
207
  return "{previous}";
162
208
  });
163
209
  }
210
+ if (isDynamicParallelStep(step)) {
211
+ return step.parallel.task ?? "{previous}";
212
+ }
164
213
  // Sequential step: existing logic
165
214
  const seq = step as SequentialStep;
166
215
  if (seq.task) return seq.task;
@@ -26,6 +26,51 @@ export interface MaxOutputConfig {
26
26
 
27
27
  export type OutputMode = "inline" | "file-only";
28
28
 
29
+ export type JsonSchemaObject = Record<string, unknown>;
30
+
31
+ export interface ChainOutputMapEntry {
32
+ text: string;
33
+ structured?: unknown;
34
+ agent: string;
35
+ stepIndex: number;
36
+ }
37
+
38
+ export type ChainOutputMap = Record<string, ChainOutputMapEntry>;
39
+
40
+ export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed" | "paused" | "detached";
41
+
42
+ export interface WorkflowGraphNode {
43
+ id: string;
44
+ kind: "step" | "parallel-group" | "dynamic-parallel-group" | "agent";
45
+ agent?: string;
46
+ phase?: string;
47
+ label: string;
48
+ status: WorkflowNodeStatus;
49
+ flatIndex?: number;
50
+ stepIndex?: number;
51
+ children?: WorkflowGraphNode[];
52
+ dynamic?: {
53
+ sourceOutput: string;
54
+ sourcePath: string;
55
+ itemName: string;
56
+ maxItems?: number;
57
+ collectAs?: string;
58
+ };
59
+ itemKey?: string;
60
+ outputName?: string;
61
+ structured?: boolean;
62
+ acceptanceStatus?: AcceptanceLedgerStatus;
63
+ error?: string;
64
+ }
65
+
66
+ export interface WorkflowGraphSnapshot {
67
+ runId: string;
68
+ mode: "chain" | "parallel" | "single";
69
+ phases: Array<{ title: string; nodeIds: string[] }>;
70
+ nodes: WorkflowGraphNode[];
71
+ currentNodeId?: string;
72
+ }
73
+
29
74
  export interface SavedOutputReference {
30
75
  path: string;
31
76
  bytes: number;
@@ -202,6 +247,151 @@ export interface ModelAttempt {
202
247
  usage?: Usage;
203
248
  }
204
249
 
250
+ export type AcceptanceLevel = "auto" | "none" | "attested" | "checked" | "verified" | "reviewed";
251
+
252
+ export type AcceptanceEvidenceKind =
253
+ | "changed-files"
254
+ | "tests-added"
255
+ | "commands-run"
256
+ | "validation-output"
257
+ | "residual-risks"
258
+ | "no-staged-files"
259
+ | "diff-summary"
260
+ | "review-findings"
261
+ | "manual-notes";
262
+
263
+ export interface AcceptanceGate {
264
+ id: string;
265
+ must: string;
266
+ evidence?: AcceptanceEvidenceKind[];
267
+ severity?: "required" | "recommended";
268
+ }
269
+
270
+ export interface AcceptanceVerifyCommand {
271
+ id: string;
272
+ command: string;
273
+ timeoutMs?: number;
274
+ cwd?: string;
275
+ env?: Record<string, string>;
276
+ allowFailure?: boolean;
277
+ }
278
+
279
+ export interface AcceptanceReviewGate {
280
+ agent?: string;
281
+ focus?: string;
282
+ required?: boolean;
283
+ }
284
+
285
+ export interface AcceptanceConfig {
286
+ level?: AcceptanceLevel;
287
+ criteria?: Array<string | AcceptanceGate>;
288
+ evidence?: AcceptanceEvidenceKind[];
289
+ verify?: AcceptanceVerifyCommand[];
290
+ review?: AcceptanceReviewGate | false;
291
+ stopRules?: string[];
292
+ reason?: string;
293
+ }
294
+
295
+ export type AcceptanceInput = AcceptanceLevel | false | AcceptanceConfig;
296
+
297
+ export interface ResolvedAcceptanceGate extends AcceptanceGate {
298
+ id: string;
299
+ must: string;
300
+ evidence: AcceptanceEvidenceKind[];
301
+ severity: "required" | "recommended";
302
+ }
303
+
304
+ export interface ResolvedAcceptanceConfig {
305
+ level: Exclude<AcceptanceLevel, "auto">;
306
+ explicit: boolean;
307
+ inferredReason: string[];
308
+ criteria: ResolvedAcceptanceGate[];
309
+ evidence: AcceptanceEvidenceKind[];
310
+ verify: AcceptanceVerifyCommand[];
311
+ review?: AcceptanceReviewGate | false;
312
+ stopRules: string[];
313
+ reason?: string;
314
+ }
315
+
316
+ export interface AcceptanceReport {
317
+ criteriaSatisfied?: Array<{
318
+ id?: string;
319
+ status: "satisfied" | "not-satisfied" | "not-applicable";
320
+ evidence: string;
321
+ }>;
322
+ changedFiles?: string[];
323
+ testsAddedOrUpdated?: string[];
324
+ commandsRun?: Array<{
325
+ command: string;
326
+ result: "passed" | "failed" | "not-run";
327
+ summary: string;
328
+ }>;
329
+ validationOutput?: string[];
330
+ residualRisks?: string[];
331
+ noStagedFiles?: boolean;
332
+ diffSummary?: string;
333
+ reviewFindings?: string[];
334
+ manualNotes?: string;
335
+ notes?: string;
336
+ }
337
+
338
+ export type AcceptanceRuntimeCheckStatus = "passed" | "failed" | "not-applicable";
339
+
340
+ export interface AcceptanceRuntimeCheck {
341
+ id: string;
342
+ status: AcceptanceRuntimeCheckStatus;
343
+ message: string;
344
+ }
345
+
346
+ export interface AcceptanceVerifyResult {
347
+ id: string;
348
+ command: string;
349
+ cwd?: string;
350
+ exitCode: number | null;
351
+ status: "passed" | "failed" | "timed-out" | "allowed-failure";
352
+ stdout?: string;
353
+ stderr?: string;
354
+ durationMs: number;
355
+ }
356
+
357
+ export interface AcceptanceReviewResult {
358
+ status: "no-blockers" | "blockers" | "needs-parent-decision";
359
+ findings: Array<{
360
+ severity: "blocker" | "non-blocking";
361
+ file?: string;
362
+ issue: string;
363
+ rationale: string;
364
+ }>;
365
+ }
366
+
367
+ export type AcceptanceLedgerStatus =
368
+ | "not-required"
369
+ | "claimed"
370
+ | "attested"
371
+ | "checked"
372
+ | "verified"
373
+ | "reviewed"
374
+ | "accepted"
375
+ | "rejected";
376
+
377
+ export interface AcceptanceLedger {
378
+ status: AcceptanceLedgerStatus;
379
+ explicit: boolean;
380
+ effectiveAcceptance: ResolvedAcceptanceConfig;
381
+ inferredReason: string[];
382
+ criteria: ResolvedAcceptanceGate[];
383
+ childReport?: AcceptanceReport;
384
+ childReportParseError?: string;
385
+ runtimeChecks: AcceptanceRuntimeCheck[];
386
+ verifyRuns: AcceptanceVerifyResult[];
387
+ reviewResult?: AcceptanceReviewResult;
388
+ parentDecision?: {
389
+ status: "accepted" | "rejected";
390
+ at: string;
391
+ reason?: string;
392
+ };
393
+ }
394
+
205
395
  export interface SingleResult {
206
396
  agent: string;
207
397
  task: string;
@@ -230,6 +420,10 @@ export interface SingleResult {
230
420
  savedOutputPath?: string;
231
421
  outputReference?: SavedOutputReference;
232
422
  outputSaveError?: string;
423
+ structuredOutput?: unknown;
424
+ structuredOutputPath?: string;
425
+ structuredOutputSchemaPath?: string;
426
+ acceptance?: AcceptanceLedger;
233
427
  }
234
428
 
235
429
  export interface Details {
@@ -256,6 +450,8 @@ export interface Details {
256
450
  chainAgents?: string[]; // Agent names in order, e.g., ["scout", "planner"]
257
451
  totalSteps?: number; // Total steps in chain
258
452
  currentStepIndex?: number; // 0-indexed current step (for running chains)
453
+ workflowGraph?: WorkflowGraphSnapshot;
454
+ outputs?: ChainOutputMap;
259
455
  }
260
456
 
261
457
  // Upstream AgentToolResult omits the runtime isError flag that subagent tool results still emit/read.
@@ -372,6 +568,7 @@ export interface AsyncStartedEvent {
372
568
  chain?: string[];
373
569
  chainStepCount?: number;
374
570
  parallelGroups?: AsyncParallelGroupStatus[];
571
+ workflowGraph?: WorkflowGraphSnapshot;
375
572
  nestedRoute?: NestedRouteInfo;
376
573
  }
377
574
 
@@ -395,8 +592,13 @@ export interface AsyncStatus {
395
592
  currentStep?: number;
396
593
  chainStepCount?: number;
397
594
  parallelGroups?: AsyncParallelGroupStatus[];
595
+ workflowGraph?: WorkflowGraphSnapshot;
398
596
  steps?: Array<{
399
597
  agent: string;
598
+ phase?: string;
599
+ label?: string;
600
+ outputName?: string;
601
+ structured?: boolean;
400
602
  status: "pending" | "running" | "complete" | "completed" | "failed" | "paused";
401
603
  children?: NestedRunSummary[];
402
604
  sessionFile?: string;
@@ -422,11 +624,16 @@ export interface AsyncStatus {
422
624
  attemptedModels?: string[];
423
625
  modelAttempts?: ModelAttempt[];
424
626
  error?: string;
627
+ structuredOutput?: unknown;
628
+ structuredOutputPath?: string;
629
+ structuredOutputSchemaPath?: string;
630
+ acceptance?: AcceptanceLedger;
425
631
  }>;
426
632
  sessionDir?: string;
427
633
  outputFile?: string;
428
634
  totalTokens?: TokenUsage;
429
635
  sessionFile?: string;
636
+ outputs?: ChainOutputMap;
430
637
  }
431
638
 
432
639
  export type AsyncJobStep = NonNullable<AsyncStatus["steps"]>[number] & {
@@ -592,6 +799,18 @@ export interface RunSyncOptions {
592
799
  currentModel?: string;
593
800
  /** Skills to inject (overrides agent default if provided) */
594
801
  skills?: string[];
802
+ structuredOutput?: {
803
+ schema: JsonSchemaObject;
804
+ schemaPath: string;
805
+ outputPath: string;
806
+ };
807
+ acceptance?: AcceptanceInput;
808
+ acceptanceContext?: {
809
+ mode?: SubagentRunMode;
810
+ async?: boolean;
811
+ dynamic?: boolean;
812
+ dynamicGroup?: boolean;
813
+ };
595
814
  }
596
815
 
597
816
  export type IntercomBridgeMode = "off" | "fork-only" | "always";
@@ -606,6 +825,12 @@ interface TopLevelParallelConfig {
606
825
  concurrency?: number;
607
826
  }
608
827
 
828
+ interface ExtensionChainConfig {
829
+ dynamicFanout?: {
830
+ maxItems?: number;
831
+ };
832
+ }
833
+
609
834
  export interface ExtensionConfig {
610
835
  asyncByDefault?: boolean;
611
836
  forceTopLevelAsync?: boolean;
@@ -613,6 +838,7 @@ export interface ExtensionConfig {
613
838
  maxSubagentDepth?: number;
614
839
  control?: ControlConfig;
615
840
  parallel?: TopLevelParallelConfig;
841
+ chain?: ExtensionChainConfig;
616
842
  worktreeSetupHook?: string;
617
843
  worktreeSetupHookTimeoutMs?: number;
618
844
  intercomBridge?: IntercomBridgeConfig;
@@ -132,7 +132,8 @@ export function getFinalOutput(messages: Message[]): string {
132
132
  const hasAssistantError = ("errorMessage" in msg && typeof msg.errorMessage === "string" && msg.errorMessage.length > 0)
133
133
  || ("stopReason" in msg && msg.stopReason === "error");
134
134
  if (hasAssistantError) continue;
135
- for (const part of msg.content) {
135
+ for (let j = msg.content.length - 1; j >= 0; j--) {
136
+ const part = msg.content[j];
136
137
  if (part.type === "text" && part.text.trim().length > 0) return part.text;
137
138
  }
138
139
  }