@bastani/atomic 0.8.21 → 0.8.22-0

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 (235) hide show
  1. package/CHANGELOG.md +40 -9
  2. package/dist/builtin/intercom/broker/broker.ts +3 -3
  3. package/dist/builtin/intercom/config.ts +3 -3
  4. package/dist/builtin/intercom/index.ts +1 -1
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/intercom/ui/compose.ts +2 -2
  7. package/dist/builtin/mcp/host-html-template.ts +0 -3
  8. package/dist/builtin/mcp/package.json +1 -1
  9. package/dist/builtin/subagents/CHANGELOG.md +13 -4
  10. package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
  11. package/dist/builtin/subagents/agents/debugger.md +6 -6
  12. package/dist/builtin/subagents/package.json +1 -1
  13. package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
  14. package/dist/builtin/subagents/skills/browser-use/SKILL.md +234 -0
  15. package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +76 -0
  16. package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +92 -0
  17. package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
  18. package/dist/builtin/subagents/src/agents/skills.ts +19 -1
  19. package/dist/builtin/subagents/src/extension/index.ts +24 -22
  20. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +7 -1
  21. package/dist/builtin/subagents/src/runs/background/async-execution.ts +23 -7
  22. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +98 -3
  23. package/dist/builtin/subagents/src/runs/background/async-status.ts +3 -1
  24. package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -1
  25. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +3 -0
  26. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +37 -12
  27. package/dist/builtin/subagents/src/runs/foreground/chain-clarify.ts +15 -15
  28. package/dist/builtin/subagents/src/runs/foreground/execution.ts +26 -2
  29. package/dist/builtin/subagents/src/runs/shared/nested-render.ts +1 -1
  30. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +7 -0
  31. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +28 -1
  32. package/dist/builtin/subagents/src/shared/fast-mode.ts +80 -0
  33. package/dist/builtin/subagents/src/shared/formatters.ts +4 -2
  34. package/dist/builtin/subagents/src/shared/types.ts +4 -2
  35. package/dist/builtin/subagents/src/shared/utils.ts +3 -61
  36. package/dist/builtin/subagents/src/tui/render.ts +303 -157
  37. package/dist/builtin/web-access/package.json +1 -1
  38. package/dist/builtin/workflows/CHANGELOG.md +95 -35
  39. package/dist/builtin/workflows/README.md +228 -41
  40. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +535 -541
  41. package/dist/builtin/workflows/builtin/goal.ts +39 -25
  42. package/dist/builtin/workflows/builtin/open-claude-design.ts +66 -69
  43. package/dist/builtin/workflows/builtin/ralph.ts +21 -21
  44. package/dist/builtin/workflows/package.json +6 -5
  45. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  46. package/dist/builtin/workflows/src/extension/background-ui-adapter.ts +2 -2
  47. package/dist/builtin/workflows/src/extension/discovery.ts +25 -146
  48. package/dist/builtin/workflows/src/extension/dispatcher.ts +72 -24
  49. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +363 -0
  50. package/dist/builtin/workflows/src/extension/index.ts +690 -352
  51. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +99 -62
  52. package/dist/builtin/workflows/src/extension/render-call.ts +2 -1
  53. package/dist/builtin/workflows/src/extension/render-result.ts +9 -3
  54. package/dist/builtin/workflows/src/extension/renderers.ts +5 -3
  55. package/dist/builtin/workflows/src/extension/runtime.ts +68 -33
  56. package/dist/builtin/workflows/src/extension/status-writer.ts +1 -1
  57. package/dist/builtin/workflows/src/extension/wiring.ts +34 -13
  58. package/dist/builtin/workflows/src/extension/workflow-module-loader.ts +142 -0
  59. package/dist/builtin/workflows/src/extension/workflow-schema.ts +4 -4
  60. package/dist/builtin/workflows/src/index.ts +2 -0
  61. package/dist/builtin/workflows/src/intercom/result-intercom.ts +1 -1
  62. package/dist/builtin/workflows/src/runs/background/runner.ts +6 -4
  63. package/dist/builtin/workflows/src/runs/background/status.ts +45 -21
  64. package/dist/builtin/workflows/src/runs/foreground/executor.ts +624 -52
  65. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +1 -1
  66. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +80 -24
  67. package/dist/builtin/workflows/src/runs/shared/validate-inputs.ts +61 -24
  68. package/dist/builtin/workflows/src/runs/shared/workflow-runner.ts +32 -10
  69. package/dist/builtin/workflows/src/sdk-surface.ts +6 -0
  70. package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +178 -0
  71. package/dist/builtin/workflows/src/shared/persistence-restore.ts +92 -12
  72. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +21 -3
  73. package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +1 -2
  74. package/dist/builtin/workflows/src/shared/run-visibility.ts +9 -0
  75. package/dist/builtin/workflows/src/shared/schema-introspection.ts +121 -0
  76. package/dist/builtin/workflows/src/shared/serializable.ts +132 -0
  77. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +91 -9
  78. package/dist/builtin/workflows/src/shared/store-types.ts +31 -3
  79. package/dist/builtin/workflows/src/shared/store.ts +58 -14
  80. package/dist/builtin/workflows/src/shared/types.ts +105 -40
  81. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +129 -13
  82. package/dist/builtin/workflows/src/tui/chat-surface.ts +6 -1
  83. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +3 -2
  84. package/dist/builtin/workflows/src/tui/graph-canvas.ts +1 -1
  85. package/dist/builtin/workflows/src/tui/graph-view.ts +91 -65
  86. package/dist/builtin/workflows/src/tui/inline-form-card.ts +1 -1
  87. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +3 -2
  88. package/dist/builtin/workflows/src/tui/inputs-overlay.ts +3 -2
  89. package/dist/builtin/workflows/src/tui/inputs-picker.ts +8 -7
  90. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +2 -0
  91. package/dist/builtin/workflows/src/tui/node-card.ts +34 -8
  92. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -11
  93. package/dist/builtin/workflows/src/tui/prompt-card.ts +98 -50
  94. package/dist/builtin/workflows/src/tui/session-list.ts +7 -2
  95. package/dist/builtin/workflows/src/tui/session-picker.ts +2 -0
  96. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +226 -55
  97. package/dist/builtin/workflows/src/tui/status-helpers.ts +2 -0
  98. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +37 -158
  99. package/dist/builtin/workflows/src/tui/toast.ts +2 -2
  100. package/dist/builtin/workflows/src/tui/widget.ts +53 -12
  101. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +270 -19
  102. package/dist/builtin/workflows/src/tui/workflow-notice-card.ts +184 -0
  103. package/dist/builtin/workflows/src/workflows/define-workflow.ts +138 -43
  104. package/dist/config.d.ts +9 -0
  105. package/dist/config.d.ts.map +1 -1
  106. package/dist/config.js +45 -0
  107. package/dist/config.js.map +1 -1
  108. package/dist/core/agent-session.d.ts +27 -9
  109. package/dist/core/agent-session.d.ts.map +1 -1
  110. package/dist/core/agent-session.js +196 -17
  111. package/dist/core/agent-session.js.map +1 -1
  112. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  113. package/dist/core/atomic-guide-command.js +2 -2
  114. package/dist/core/atomic-guide-command.js.map +1 -1
  115. package/dist/core/codex-fast-mode.d.ts +36 -0
  116. package/dist/core/codex-fast-mode.d.ts.map +1 -0
  117. package/dist/core/codex-fast-mode.js +117 -0
  118. package/dist/core/codex-fast-mode.js.map +1 -0
  119. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  120. package/dist/core/compaction/branch-summarization.js +1 -1
  121. package/dist/core/compaction/branch-summarization.js.map +1 -1
  122. package/dist/core/compaction/compaction.d.ts.map +1 -1
  123. package/dist/core/compaction/compaction.js +1 -1
  124. package/dist/core/compaction/compaction.js.map +1 -1
  125. package/dist/core/extensions/index.d.ts +4 -1
  126. package/dist/core/extensions/index.d.ts.map +1 -1
  127. package/dist/core/extensions/index.js +1 -0
  128. package/dist/core/extensions/index.js.map +1 -1
  129. package/dist/core/extensions/loader.d.ts +7 -2
  130. package/dist/core/extensions/loader.d.ts.map +1 -1
  131. package/dist/core/extensions/loader.js +23 -8
  132. package/dist/core/extensions/loader.js.map +1 -1
  133. package/dist/core/extensions/reactive-widget.d.ts +58 -0
  134. package/dist/core/extensions/reactive-widget.d.ts.map +1 -0
  135. package/dist/core/extensions/reactive-widget.js +182 -0
  136. package/dist/core/extensions/reactive-widget.js.map +1 -0
  137. package/dist/core/extensions/runner.d.ts.map +1 -1
  138. package/dist/core/extensions/runner.js +1 -0
  139. package/dist/core/extensions/runner.js.map +1 -1
  140. package/dist/core/extensions/types.d.ts +26 -12
  141. package/dist/core/extensions/types.d.ts.map +1 -1
  142. package/dist/core/extensions/types.js.map +1 -1
  143. package/dist/core/messages.d.ts +1 -1
  144. package/dist/core/messages.d.ts.map +1 -1
  145. package/dist/core/messages.js +8 -2
  146. package/dist/core/messages.js.map +1 -1
  147. package/dist/core/model-registry.d.ts +4 -0
  148. package/dist/core/model-registry.d.ts.map +1 -1
  149. package/dist/core/model-registry.js +11 -0
  150. package/dist/core/model-registry.js.map +1 -1
  151. package/dist/core/resource-loader.d.ts +9 -1
  152. package/dist/core/resource-loader.d.ts.map +1 -1
  153. package/dist/core/resource-loader.js +49 -21
  154. package/dist/core/resource-loader.js.map +1 -1
  155. package/dist/core/sdk.d.ts.map +1 -1
  156. package/dist/core/sdk.js +22 -13
  157. package/dist/core/sdk.js.map +1 -1
  158. package/dist/core/session-manager.d.ts +7 -5
  159. package/dist/core/session-manager.d.ts.map +1 -1
  160. package/dist/core/session-manager.js +5 -3
  161. package/dist/core/session-manager.js.map +1 -1
  162. package/dist/core/settings-manager.d.ts +16 -0
  163. package/dist/core/settings-manager.d.ts.map +1 -1
  164. package/dist/core/settings-manager.js +64 -5
  165. package/dist/core/settings-manager.js.map +1 -1
  166. package/dist/core/slash-commands.d.ts.map +1 -1
  167. package/dist/core/slash-commands.js +1 -0
  168. package/dist/core/slash-commands.js.map +1 -1
  169. package/dist/core/system-prompt.d.ts.map +1 -1
  170. package/dist/core/system-prompt.js +7 -4
  171. package/dist/core/system-prompt.js.map +1 -1
  172. package/dist/core/tools/ask-user-question/ask-user-question.d.ts.map +1 -1
  173. package/dist/core/tools/ask-user-question/ask-user-question.js +2 -2
  174. package/dist/core/tools/ask-user-question/ask-user-question.js.map +1 -1
  175. package/dist/index.d.ts +4 -3
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +3 -2
  178. package/dist/index.js.map +1 -1
  179. package/dist/main.d.ts +3 -0
  180. package/dist/main.d.ts.map +1 -1
  181. package/dist/main.js +12 -0
  182. package/dist/main.js.map +1 -1
  183. package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -1
  184. package/dist/modes/interactive/chat-input-actions.js.map +1 -1
  185. package/dist/modes/interactive/components/diff.d.ts.map +1 -1
  186. package/dist/modes/interactive/components/diff.js +0 -1
  187. package/dist/modes/interactive/components/diff.js.map +1 -1
  188. package/dist/modes/interactive/components/fast-mode-selector.d.ts +27 -0
  189. package/dist/modes/interactive/components/fast-mode-selector.d.ts.map +1 -0
  190. package/dist/modes/interactive/components/fast-mode-selector.js +105 -0
  191. package/dist/modes/interactive/components/fast-mode-selector.js.map +1 -0
  192. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  193. package/dist/modes/interactive/components/footer.js +7 -12
  194. package/dist/modes/interactive/components/footer.js.map +1 -1
  195. package/dist/modes/interactive/components/index.d.ts +1 -0
  196. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  197. package/dist/modes/interactive/components/index.js +1 -0
  198. package/dist/modes/interactive/components/index.js.map +1 -1
  199. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  200. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  201. package/dist/modes/interactive/interactive-mode.js +132 -30
  202. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  203. package/dist/modes/print-mode.d.ts.map +1 -1
  204. package/dist/modes/print-mode.js +53 -6
  205. package/dist/modes/print-mode.js.map +1 -1
  206. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  207. package/dist/modes/rpc/rpc-mode.js +3 -0
  208. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  209. package/docs/compaction.md +1 -1
  210. package/docs/custom-provider.md +2 -2
  211. package/docs/development.md +2 -2
  212. package/docs/docs.json +2 -2
  213. package/docs/extensions.md +18 -13
  214. package/docs/providers.md +5 -1
  215. package/docs/quickstart.md +5 -3
  216. package/docs/rpc.md +5 -5
  217. package/docs/sdk.md +12 -12
  218. package/docs/settings.md +18 -0
  219. package/docs/themes.md +6 -6
  220. package/docs/tui.md +20 -18
  221. package/docs/usage.md +2 -0
  222. package/docs/workflows.md +403 -39
  223. package/examples/extensions/qna.ts +2 -2
  224. package/package.json +4 -4
  225. package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +0 -392
  226. package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +0 -23
  227. package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +0 -39
  228. package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +0 -87
  229. package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +0 -241
  230. package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +0 -225
  231. package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
  232. package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +0 -275
  233. package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +0 -134
  234. package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +0 -139
  235. package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +0 -143
@@ -276,7 +276,7 @@ export function createStageControlRegistry(): StageControlRegistry {
276
276
  _byRun.clear();
277
277
  for (const handle of handles) {
278
278
  void Promise.resolve(handle.dispose?.()).catch((err: unknown) => {
279
- console.warn("pi-workflows: stage handle dispose failed", err);
279
+ console.warn("atomic-workflows: stage handle dispose failed", err);
280
280
  });
281
281
  }
282
282
  },
@@ -9,7 +9,13 @@
9
9
 
10
10
  import { mkdir, writeFile } from "node:fs/promises";
11
11
  import { dirname, isAbsolute, resolve } from "node:path";
12
- import { SessionManager, type AgentSession, type CreateAgentSessionOptions, type PromptOptions } from "@bastani/atomic";
12
+ import {
13
+ shouldApplyCodexFastModeForScope,
14
+ SessionManager,
15
+ type AgentSession,
16
+ type CreateAgentSessionOptions,
17
+ type PromptOptions,
18
+ } from "@bastani/atomic";
13
19
  import type {
14
20
  CompleteStageOpts,
15
21
  StageContext,
@@ -19,6 +25,7 @@ import type {
19
25
  StagePromptOptions,
20
26
  WorkflowMaxOutput,
21
27
  WorkflowModelAttempt,
28
+ WorkflowExecutionMode,
22
29
  WorkflowModelCatalogPort,
23
30
  } from "../../shared/types.js";
24
31
  import {
@@ -47,6 +54,8 @@ export interface StageSessionRuntime {
47
54
  readonly isStreaming: AgentSession["isStreaming"];
48
55
  /** Number of SDK-level queued steering/follow-up messages, when supported. */
49
56
  readonly pendingMessageCount?: number;
57
+ /** Settings manager supplied by the Atomic SDK when the adapter did not pre-create one. */
58
+ readonly settingsManager?: WorkflowFastModeSettingsManager;
50
59
  navigateTree: AgentSession["navigateTree"];
51
60
  compact: AgentSession["compact"];
52
61
  abortCompaction(): void;
@@ -57,12 +66,27 @@ export interface StageSessionRuntime {
57
66
 
58
67
  export type StageSessionCreateOptions = CreateAgentSessionOptions & Pick<StageOptions, "mcp" | "fallbackModels">;
59
68
 
69
+ type WorkflowFastModeSettings = {
70
+ readonly chat: boolean;
71
+ readonly workflow: boolean;
72
+ };
73
+
74
+ type WorkflowFastModeSettingsManager = {
75
+ getCodexFastModeSettings(): WorkflowFastModeSettings;
76
+ };
77
+
78
+ export interface StageSessionCreateResult {
79
+ readonly session: StageSessionRuntime;
80
+ readonly settingsManager?: WorkflowFastModeSettingsManager;
81
+ }
82
+
60
83
  export interface AgentSessionAdapter {
61
- create(options: StageSessionCreateOptions, meta?: StageExecutionMeta): Promise<StageSessionRuntime>;
84
+ create(options: StageSessionCreateOptions, meta?: StageExecutionMeta): Promise<StageSessionRuntime | StageSessionCreateResult>;
62
85
  }
63
86
 
64
87
  export interface StageModelFallbackMeta {
65
88
  readonly model?: string;
89
+ readonly fastMode?: boolean;
66
90
  readonly attemptedModels?: readonly string[];
67
91
  readonly modelAttempts?: readonly WorkflowModelAttempt[];
68
92
  readonly warnings?: readonly string[];
@@ -94,6 +118,10 @@ export interface StageRunnerOpts {
94
118
  signal?: AbortSignal;
95
119
  /** Optional model catalog used for fallback validation/resolution. */
96
120
  models?: WorkflowModelCatalogPort;
121
+ /** Runtime execution mode forwarded to stage session adapters. */
122
+ executionMode?: WorkflowExecutionMode;
123
+ /** Internal: notifies the executor when an in-flight fallback changes model/fast metadata. */
124
+ onModelFallbackMetaChange?: (meta: StageModelFallbackMeta) => void;
97
125
  }
98
126
 
99
127
  export interface InternalStageContext extends StageContext {
@@ -164,17 +192,17 @@ type AgentSessionConsumer = "prompt" | "complete";
164
192
  function missingAdapter(consumer: AgentSessionConsumer): never {
165
193
  if (consumer === "complete") {
166
194
  throw new Error(
167
- "pi-workflows: ctx.complete requires either RunOpts.adapters.complete or RunOpts.adapters.agentSession",
195
+ "atomic-workflows: ctx.complete requires either RunOpts.adapters.complete or RunOpts.adapters.agentSession",
168
196
  );
169
197
  }
170
198
  throw new Error(
171
- "pi-workflows: prompt adapter not configured — provide an AgentSessionAdapter via RunOpts.adapters.agentSession",
199
+ "atomic-workflows: prompt adapter not configured — provide an AgentSessionAdapter via RunOpts.adapters.agentSession",
172
200
  );
173
201
  }
174
202
 
175
203
  function unavailableSync(property: string): never {
176
204
  throw new Error(
177
- `pi-workflows: stage AgentSession property "${property}" is unavailable until the SDK session has been created`,
205
+ `atomic-workflows: stage AgentSession property "${property}" is unavailable until the SDK session has been created`,
178
206
  );
179
207
  }
180
208
 
@@ -400,7 +428,7 @@ function splitPromptOptions(options: StagePromptOptions | undefined): {
400
428
  function validatePromptOutputOptions(outputOptions: StageOutputOptions): void {
401
429
  if (outputOptions.outputMode === "file-only" && (typeof outputOptions.output !== "string" || outputOptions.output.length === 0)) {
402
430
  throw new Error(
403
- "pi-workflows: prompt sets outputMode: \"file-only\" but does not configure an output file. Set output to a path or use outputMode: \"inline\".",
431
+ "atomic-workflows: prompt sets outputMode: \"file-only\" but does not configure an output file. Set output to a path or use outputMode: \"inline\".",
404
432
  );
405
433
  }
406
434
  }
@@ -430,8 +458,8 @@ async function finalizePromptOutput(
430
458
  }
431
459
 
432
460
  export function createStageContext(opts: StageRunnerOpts): InternalStageContext {
433
- const { stageId, stageName, adapters, runId, signal, stageOptions } = opts;
434
- const meta: StageExecutionMeta = { runId, stageId, stageName, signal, stageOptions };
461
+ const { stageId, stageName, adapters, runId, signal, stageOptions, executionMode } = opts;
462
+ const meta: StageExecutionMeta = { runId, stageId, stageName, signal, stageOptions, executionMode };
435
463
  let session: StageSessionRuntime | undefined;
436
464
  let sessionPromise: Promise<StageSessionRuntime> | undefined;
437
465
  let lastAssistantText: string | undefined;
@@ -532,19 +560,52 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
532
560
  return { ...(stageOptions ?? {}), model: candidate.value, fallbackModels: undefined };
533
561
  }
534
562
 
535
- function attachSession(created: StageSessionRuntime): StageSessionRuntime {
536
- session = created;
563
+ let sessionSettingsManager: WorkflowFastModeSettingsManager | undefined;
564
+
565
+ function isWorkflowFastModeEnabled(): boolean | undefined {
566
+ const model = session?.model;
567
+ const settingsManager = sessionSettingsManager ?? stageOptions?.settingsManager;
568
+ if (model === undefined || settingsManager === undefined) return undefined;
569
+ return shouldApplyCodexFastModeForScope(model, settingsManager.getCodexFastModeSettings(), "workflow");
570
+ }
571
+
572
+ function currentModelFallbackMeta(): StageModelFallbackMeta {
573
+ const attemptedModels = modelAttempts.map((attempt) => attempt.model);
574
+ const model = selectedModel ?? workflowModelId(session?.model);
575
+ const fastMode = isWorkflowFastModeEnabled();
576
+ return {
577
+ ...(model !== undefined ? { model } : {}),
578
+ ...(fastMode !== undefined ? { fastMode } : {}),
579
+ ...(attemptedModels.length > 0 ? { attemptedModels } : {}),
580
+ ...(modelAttempts.length > 0 ? { modelAttempts: [...modelAttempts] } : {}),
581
+ ...(modelWarnings.length > 0 ? { warnings: [...modelWarnings] } : {}),
582
+ };
583
+ }
584
+
585
+ function notifyModelFallbackMetaChange(): void {
586
+ opts.onModelFallbackMetaChange?.(currentModelFallbackMeta());
587
+ }
588
+
589
+ function normalizeSessionCreateResult(created: StageSessionRuntime | StageSessionCreateResult): StageSessionCreateResult {
590
+ if ("session" in created) return created;
591
+ return { session: created };
592
+ }
593
+
594
+ function attachSession(created: StageSessionRuntime | StageSessionCreateResult): StageSessionRuntime {
595
+ const result = normalizeSessionCreateResult(created);
596
+ session = result.session;
597
+ sessionSettingsManager = result.settingsManager ?? result.session.settingsManager;
537
598
  if (pendingThinkingLevel !== undefined) {
538
- created.setThinkingLevel(pendingThinkingLevel);
599
+ result.session.setThinkingLevel(pendingThinkingLevel);
539
600
  }
540
601
  for (const listener of pendingListeners) {
541
- listenerUnsubscribes.set(listener, created.subscribe(listener));
602
+ listenerUnsubscribes.set(listener, result.session.subscribe(listener));
542
603
  }
543
604
  // Track terminating tool calls for this session so the stage result text is
544
605
  // derived deterministically from a tool that actually ended the turn.
545
606
  unsubscribeTerminateWatcher?.();
546
- unsubscribeTerminateWatcher = created.subscribe((event) => recordTerminatingToolCall(event));
547
- return created;
607
+ unsubscribeTerminateWatcher = result.session.subscribe((event) => recordTerminatingToolCall(event));
608
+ return result.session;
548
609
  }
549
610
 
550
611
  async function createSession(
@@ -561,7 +622,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
561
622
  }
562
623
 
563
624
  async function ensureSession(consumer: AgentSessionConsumer = "prompt"): Promise<StageSessionRuntime> {
564
- if (disposed) throw new Error(`pi-workflows: stage "${stageName}" session has been disposed`);
625
+ if (disposed) throw new Error(`atomic-workflows: stage "${stageName}" session has been disposed`);
565
626
  if (!sessionPromise) {
566
627
  sessionPromise = (async () => {
567
628
  if (!hasExplicitModelFallbackConfig) return createSession(undefined, consumer);
@@ -580,6 +641,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
580
641
  const current = session;
581
642
  session = undefined;
582
643
  sessionPromise = undefined;
644
+ sessionSettingsManager = undefined;
583
645
  for (const unsubscribe of listenerUnsubscribes.values()) unsubscribe();
584
646
  listenerUnsubscribes.clear();
585
647
  unsubscribeTerminateWatcher?.();
@@ -651,6 +713,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
651
713
  : await createSession(candidate, consumer);
652
714
  activeCandidateIndex = index;
653
715
  selectedModel = candidate.id;
716
+ notifyModelFallbackMetaChange();
654
717
  try {
655
718
  await promptWithPauseResume(activeSession, text, sdkOptions);
656
719
  modelAttempts.push({ model: candidate.id, success: true });
@@ -705,7 +768,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
705
768
  completeOpts?.fallbackModels !== undefined
706
769
  ) {
707
770
  throw new Error(
708
- "pi-workflows: complete options require a CompleteAdapter via RunOpts.adapters.complete",
771
+ "atomic-workflows: complete options require a CompleteAdapter via RunOpts.adapters.complete",
709
772
  );
710
773
  }
711
774
  // Intentional fallback: when a CompleteAdapter is not configured,
@@ -835,14 +898,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
835
898
  },
836
899
 
837
900
  __modelFallbackMeta() {
838
- const attemptedModels = modelAttempts.map((attempt) => attempt.model);
839
- const model = selectedModel ?? workflowModelId(session?.model);
840
- return {
841
- ...(model !== undefined ? { model } : {}),
842
- ...(attemptedModels.length > 0 ? { attemptedModels } : {}),
843
- ...(modelAttempts.length > 0 ? { modelAttempts: [...modelAttempts] } : {}),
844
- ...(modelWarnings.length > 0 ? { warnings: [...modelWarnings] } : {}),
845
- };
901
+ return currentModelFallbackMeta();
846
902
  },
847
903
 
848
904
  async __requestPause() {
@@ -1,20 +1,35 @@
1
1
  /**
2
2
  * validateInputs — check a parsed input bag against a workflow's declared
3
- * input schema. Used by slash-command and programmatic SDK dispatch paths to
4
- * reject malformed input payloads before dispatch.
3
+ * TypeBox input schema. Used by slash-command and programmatic SDK dispatch
4
+ * paths to reject malformed input payloads before dispatch.
5
5
  *
6
6
  * Reports:
7
7
  * - unknown input keys (catches typos like "propmt")
8
- * - wrong-typed values (number/boolean/string/text/select)
9
- * - select values not in the declared choices
8
+ * - wrong-typed values (number/boolean/string/select-union/integer)
9
+ * - select values not in the declared literal union
10
10
  * - missing required inputs
11
+ * - non-JSON-serializable values
11
12
  *
12
13
  * Does NOT coerce: "true" is not a boolean, "3" is not a number. JSON parsing
13
14
  * upstream already preserves types — string-typed values reaching this point
14
15
  * are user mistakes worth surfacing.
16
+ *
17
+ * The legacy `{ type }` descriptor is gone; the field kind, choices, and
18
+ * required-ness are derived from the TypeBox schema via schema-introspection,
19
+ * keeping the historical error wording byte-for-byte stable.
15
20
  */
16
21
 
17
- import type { WorkflowInputSchema } from "../../shared/types.js";
22
+ import { Value } from "typebox/value";
23
+ import {
24
+ workflowSerializableValidationError,
25
+ workflowSerializableTypeName,
26
+ } from "../../shared/serializable.js";
27
+ import {
28
+ schemaChoices,
29
+ schemaFieldKind,
30
+ schemaIsRequired,
31
+ } from "../../shared/schema-introspection.js";
32
+ import type { TSchema, WorkflowInputValues } from "../../shared/types.js";
18
33
 
19
34
  export interface ValidationError {
20
35
  key: string;
@@ -22,8 +37,8 @@ export interface ValidationError {
22
37
  }
23
38
 
24
39
  export function validateInputs(
25
- schema: Readonly<Record<string, WorkflowInputSchema>>,
26
- inputs: Record<string, unknown>,
40
+ schema: Readonly<Record<string, TSchema>>,
41
+ inputs: WorkflowInputValues,
27
42
  ): ValidationError[] {
28
43
  const errors: ValidationError[] = [];
29
44
 
@@ -35,49 +50,71 @@ export function validateInputs(
35
50
 
36
51
  for (const [key, def] of Object.entries(schema)) {
37
52
  const value = inputs[key];
53
+ let hasTypeError = false;
38
54
 
39
55
  if (value === undefined) {
40
- if (def.required === true) {
56
+ if (schemaIsRequired(def)) {
41
57
  errors.push({ key, reason: "required input is missing" });
42
58
  }
43
59
  continue;
44
60
  }
45
61
 
46
- switch (def.type) {
62
+ const kind = schemaFieldKind(def);
63
+ switch (kind) {
47
64
  case "text":
48
- case "string":
49
65
  if (typeof value !== "string") {
50
- errors.push({ key, reason: `expected string, got ${typeName(value)}` });
66
+ errors.push({ key, reason: `expected string, got ${workflowSerializableTypeName(value)}` });
67
+ hasTypeError = true;
51
68
  }
52
69
  break;
53
70
  case "number":
54
- if (typeof value !== "number" || Number.isNaN(value)) {
55
- errors.push({ key, reason: `expected number, got ${typeName(value)}` });
71
+ if (typeof value !== "number" || !Number.isFinite(value)) {
72
+ errors.push({ key, reason: `expected finite number, got ${workflowSerializableTypeName(value)}` });
73
+ hasTypeError = true;
74
+ }
75
+ break;
76
+ case "integer":
77
+ if (typeof value !== "number" || !Number.isInteger(value)) {
78
+ errors.push({ key, reason: `expected integer, got ${workflowSerializableTypeName(value)}` });
79
+ hasTypeError = true;
56
80
  }
57
81
  break;
58
82
  case "boolean":
59
83
  if (typeof value !== "boolean") {
60
- errors.push({ key, reason: `expected boolean, got ${typeName(value)}` });
84
+ errors.push({ key, reason: `expected boolean, got ${workflowSerializableTypeName(value)}` });
85
+ hasTypeError = true;
61
86
  }
62
87
  break;
63
88
  case "select": {
64
- const allowed = def.choices.join(", ");
89
+ const choices = schemaChoices(def) ?? [];
90
+ const allowed = choices.join(", ");
65
91
  if (typeof value !== "string") {
66
- errors.push({ key, reason: `expected one of [${allowed}], got ${typeName(value)}` });
67
- } else if (!def.choices.includes(value)) {
92
+ errors.push({ key, reason: `expected one of [${allowed}], got ${workflowSerializableTypeName(value)}` });
93
+ hasTypeError = true;
94
+ } else if (!choices.includes(value)) {
68
95
  errors.push({ key, reason: `must be one of [${allowed}]` });
69
96
  }
70
97
  break;
71
98
  }
99
+ default:
100
+ // object / array / unknown: defer to the schema's own checker so a
101
+ // precise declared shape (Type.Object({ ... }), Type.Array(...)) is
102
+ // still enforced, while loose schemas accept any serializable value.
103
+ if (!Value.Check(def, value)) {
104
+ const first = [...Value.Errors(def, value)][0];
105
+ errors.push({ key, reason: first === undefined ? `does not match ${kind} schema` : first.message });
106
+ hasTypeError = true;
107
+ }
108
+ break;
109
+ }
110
+
111
+ const serializableError = hasTypeError
112
+ ? undefined
113
+ : workflowSerializableValidationError(value, `input "${key}"`);
114
+ if (serializableError !== undefined) {
115
+ errors.push({ key, reason: serializableError.replace(/^input "[^"]+" /, "") });
72
116
  }
73
117
  }
74
118
 
75
119
  return errors;
76
120
  }
77
-
78
- function typeName(value: unknown): string {
79
- if (value === null) return "null";
80
- if (Array.isArray(value)) return "array";
81
- if (typeof value === "number" && Number.isNaN(value)) return "NaN";
82
- return typeof value;
83
- }
@@ -11,6 +11,7 @@ import { buildRuntimeAdapters, type RuntimeAdapterBuildOptions, type RuntimeWiri
11
11
  import { discoverWorkflows } from "../../extension/discovery.js";
12
12
  import { createStore } from "../../shared/store.js";
13
13
  import { renderInputsSchema } from "../../shared/render-inputs-schema.js";
14
+ import { deriveInputFields } from "../../shared/schema-introspection.js";
14
15
  import { validateInputs, type ValidationError } from "./validate-inputs.js";
15
16
  import type { CreateAgentSessionOptions } from "@bastani/atomic";
16
17
  import type { StageSessionRuntime } from "../foreground/stage-runner.js";
@@ -22,6 +23,7 @@ import type {
22
23
  WorkflowDirectOptions,
23
24
  WorkflowDirectTaskItem,
24
25
  WorkflowInputSchema,
26
+ WorkflowInputValues,
25
27
  WorkflowMaxOutput,
26
28
  WorkflowOutputMode,
27
29
  } from "../../shared/types.js";
@@ -29,7 +31,7 @@ import type {
29
31
  export interface WorkflowDefinition extends StageOptions {
30
32
  mode?: "workflow" | "named" | "single" | "parallel" | "chain";
31
33
  workflow?: string;
32
- inputs?: Record<string, unknown>;
34
+ inputs?: WorkflowInputValues;
33
35
  /** Direct single-task mode, or root task text for direct chain/parallel execution. */
34
36
  task?: WorkflowDirectTaskItem | string;
35
37
  /** Direct top-level parallel mode. */
@@ -221,12 +223,21 @@ async function runNamedWorkflow(
221
223
  const available = discovery.registry.names();
222
224
  throw new Error(`Workflow not found: "${workflowName}". Available: ${available.length > 0 ? available.join(", ") : "(none)"}`);
223
225
  }
224
- const inputs = definition.inputs ?? {};
226
+ // Apply schema defaults before validating so an input declared with both
227
+ // `required: true` and a `default` is not rejected as "missing" when omitted,
228
+ // mirroring the resolve-then-validate order every other dispatch path uses.
229
+ // validateInputs (rather than resolveInputs) still surfaces the full set of
230
+ // input problems through formatWorkflowValidationFailure; run() re-resolves
231
+ // internally, which is idempotent on already-defaulted inputs.
232
+ const inputs = withResolvedDefaults(workflow.inputs, definition.inputs ?? {});
225
233
  const errors = validateInputs(workflow.inputs, inputs);
226
234
  if (errors.length > 0) {
227
235
  throw new Error(formatWorkflowValidationFailure(workflow.name, workflow.inputs, errors));
228
236
  }
229
- const result = await run(workflow, inputs, runOptions);
237
+ const result = await run(workflow, inputs, {
238
+ ...runOptions,
239
+ registry: discovery.registry,
240
+ });
230
241
  return {
231
242
  action: "run",
232
243
  mode: "named",
@@ -241,18 +252,29 @@ async function runNamedWorkflow(
241
252
  };
242
253
  }
243
254
 
255
+ function withResolvedDefaults(
256
+ schema: Readonly<Record<string, WorkflowInputSchema>>,
257
+ provided: WorkflowInputValues,
258
+ ): WorkflowInputValues {
259
+ const resolved: Record<string, WorkflowInputValues[string]> = {};
260
+ for (const [key, value] of Object.entries(provided)) {
261
+ if (value !== undefined) resolved[key] = value;
262
+ }
263
+ for (const [key, def] of Object.entries(schema)) {
264
+ const declaredDefault = (def as { default?: unknown }).default;
265
+ if (resolved[key] === undefined && declaredDefault !== undefined) {
266
+ resolved[key] = declaredDefault as WorkflowInputValues[string];
267
+ }
268
+ }
269
+ return resolved;
270
+ }
271
+
244
272
  function formatWorkflowValidationFailure(
245
273
  workflowName: string,
246
274
  schema: Readonly<Record<string, WorkflowInputSchema>>,
247
275
  errors: ValidationError[],
248
276
  ): string {
249
- const entries = Object.entries(schema).map(([name, definition]) => ({
250
- name,
251
- type: definition.type,
252
- description: definition.description,
253
- required: definition.required,
254
- default: "default" in definition ? definition.default : undefined,
255
- }));
277
+ const entries = deriveInputFields(schema);
256
278
  const lines = errors.map((error) => ` - ${error.key}: ${error.reason}`);
257
279
  return `Invalid inputs for "${workflowName}":\n${lines.join("\n")}\n\n${renderInputsSchema(workflowName, entries)}`;
258
280
  }
@@ -7,9 +7,15 @@
7
7
  */
8
8
 
9
9
  export { defineWorkflow } from "./workflows/define-workflow.js";
10
+
11
+ // TypeBox authoring surface so jiti-loaded workflows can `import { Type } from
12
+ // "@bastani/workflows"` (the virtual module is built from this file).
13
+ export { Type } from "typebox";
14
+ export type { Static, TSchema } from "typebox";
10
15
  export { createRegistry } from "./workflows/registry.js";
11
16
  export { normalizeWorkflowName, workflowNamesEqual } from "./workflows/identity.js";
12
17
  export type * from "./shared/types.js";
18
+ export { INTERACTIVE_WORKFLOW_POLICY, NON_INTERACTIVE_WORKFLOW_POLICY } from "./shared/types.js";
13
19
  export type { WorkflowBuilder, CompletedWorkflowBuilder } from "./workflows/define-workflow.js";
14
20
  export type { WorkflowRegistry } from "./workflows/registry.js";
15
21
 
@@ -0,0 +1,178 @@
1
+ import type {
2
+ RunSnapshot,
3
+ StageSnapshot,
4
+ StoreSnapshot,
5
+ } from "./store-types.js";
6
+
7
+ export interface ExpandedWorkflowStageTarget {
8
+ readonly runId: string;
9
+ readonly stageId: string;
10
+ readonly runName: string;
11
+ readonly depth: number;
12
+ }
13
+
14
+ export interface ExpandedWorkflowStage extends StageSnapshot {
15
+ readonly workflowGraphTarget: ExpandedWorkflowStageTarget;
16
+ }
17
+
18
+ export interface ExpandedWorkflowGraph {
19
+ readonly stages: readonly ExpandedWorkflowStage[];
20
+ readonly targets: ReadonlyMap<string, ExpandedWorkflowStageTarget>;
21
+ }
22
+
23
+ interface ExpandedRunResult {
24
+ readonly stages: ExpandedWorkflowStage[];
25
+ readonly terminalIds: readonly string[];
26
+ }
27
+
28
+ function virtualStageId(runId: string, stageId: string, isRootRun: boolean): string {
29
+ return isRootRun ? stageId : `${runId}:${stageId}`;
30
+ }
31
+
32
+ function childRunIdFor(stage: StageSnapshot): string | undefined {
33
+ return stage.workflowChildRun?.runId ?? stage.workflowChild?.runId;
34
+ }
35
+
36
+ function childAliasFor(stage: StageSnapshot): string | undefined {
37
+ return stage.workflowChildRun?.alias ?? stage.workflowChild?.alias;
38
+ }
39
+
40
+ function localTerminalStageIds(stages: readonly StageSnapshot[]): readonly string[] {
41
+ const parentIds = new Set<string>();
42
+ for (const stage of stages) {
43
+ for (const parentId of stage.parentIds) parentIds.add(parentId);
44
+ }
45
+ const terminals = stages
46
+ .filter((stage) => !parentIds.has(stage.id))
47
+ .map((stage) => stage.id);
48
+ return terminals.length > 0 ? terminals : stages.map((stage) => stage.id);
49
+ }
50
+
51
+ /**
52
+ * Build a view-only expanded graph for a run and any nested child workflow
53
+ * runs it references. Child stages are cloned with virtual ids so their local
54
+ * parent ids do not collide with parent-run stage ids; each virtual node keeps
55
+ * a target mapping back to the actual `{ runId, stageId }` for attach/control.
56
+ *
57
+ * Imported workflows are flattened: a boundary stage that wraps a child run
58
+ * with its own stages is NOT emitted as its own node. Instead the child's
59
+ * stages stand in for it, so a nested workflow reads as a flat layout with no
60
+ * extra "information" node marking the import boundary. The boundary's incoming
61
+ * parents become the child roots' parents, and stages downstream of the
62
+ * boundary rewire to the child's terminal stages. A boundary whose child run
63
+ * produced no stages of its own is kept as a single node so the import stays
64
+ * visible. Boundary stages are non-attachable, so dropping them loses no
65
+ * interactive capability.
66
+ */
67
+ export function expandWorkflowGraph(
68
+ snapshot: StoreSnapshot,
69
+ rootRunId: string,
70
+ ): ExpandedWorkflowGraph {
71
+ const runById = new Map(snapshot.runs.map((run) => [run.id, run]));
72
+ const root = runById.get(rootRunId);
73
+ if (!root) return { stages: [], targets: new Map() };
74
+
75
+ const targets = new Map<string, ExpandedWorkflowStageTarget>();
76
+ // Cycle guard only. This relies on the store invariant that each child run is
77
+ // referenced by exactly one parent stage (runIds are unique and a child run
78
+ // has a single boundary stage), so removing a run from `visiting` on exit
79
+ // cannot double-expand a shared child into duplicate virtual stage ids. If
80
+ // that invariant is ever relaxed, dedupe expanded stages by virtual id here.
81
+ const visiting = new Set<string>();
82
+
83
+ const expandRun = (
84
+ run: RunSnapshot,
85
+ depth: number,
86
+ incomingParentIds: readonly string[],
87
+ ): ExpandedRunResult => {
88
+ if (visiting.has(run.id)) return { stages: [], terminalIds: [] };
89
+ visiting.add(run.id);
90
+
91
+ const isRootRun = run.id === rootRunId;
92
+ const expandedStages: ExpandedWorkflowStage[] = [];
93
+ const replacementTerminals = new Map<string, readonly string[]>();
94
+
95
+ for (const stage of run.stages) {
96
+ const id = virtualStageId(run.id, stage.id, isRootRun);
97
+ const resolvedParentIds = stage.parentIds.length === 0
98
+ ? [...incomingParentIds]
99
+ : stage.parentIds.flatMap((parentId) =>
100
+ replacementTerminals.get(parentId) ?? [virtualStageId(run.id, parentId, isRootRun)],
101
+ );
102
+
103
+ const childRunId = childRunIdFor(stage);
104
+ const childRun = childRunId === undefined ? undefined : runById.get(childRunId);
105
+
106
+ // Flatten the imported workflow in place: when the boundary wraps a child
107
+ // run that has its own stages, splice those stages in instead of emitting
108
+ // the boundary node. Child roots inherit the boundary's incoming parents,
109
+ // and downstream stages rewire to the child's terminals below.
110
+ if (childRun !== undefined && childRun.stages.length > 0) {
111
+ const childExpanded = expandRun(childRun, depth + 1, resolvedParentIds);
112
+ expandedStages.push(...childExpanded.stages);
113
+ replacementTerminals.set(
114
+ stage.id,
115
+ childExpanded.terminalIds.length > 0 ? childExpanded.terminalIds : resolvedParentIds,
116
+ );
117
+ continue;
118
+ }
119
+
120
+ // Regular stage, or a boundary whose child produced no stages of its own:
121
+ // emit it so the import still appears as exactly one node.
122
+ const target: ExpandedWorkflowStageTarget = {
123
+ runId: run.id,
124
+ stageId: stage.id,
125
+ runName: run.name,
126
+ depth,
127
+ };
128
+ targets.set(id, target);
129
+ expandedStages.push({
130
+ ...stage,
131
+ id,
132
+ parentIds: Object.freeze(resolvedParentIds),
133
+ workflowGraphTarget: target,
134
+ });
135
+ }
136
+
137
+ const terminalIds = localTerminalStageIds(run.stages).flatMap((stageId) =>
138
+ replacementTerminals.get(stageId) ?? [virtualStageId(run.id, stageId, isRootRun)],
139
+ );
140
+
141
+ visiting.delete(run.id);
142
+ return { stages: expandedStages, terminalIds };
143
+ };
144
+
145
+ const expanded = expandRun(root, 0, []);
146
+ return { stages: expanded.stages, targets };
147
+ }
148
+
149
+ export function expandedStageTarget(
150
+ graph: ExpandedWorkflowGraph,
151
+ virtualStageIdValue: string,
152
+ ): ExpandedWorkflowStageTarget | undefined {
153
+ return graph.targets.get(virtualStageIdValue);
154
+ }
155
+
156
+ export function stageMatchesExpandedIdentifier(
157
+ stage: ExpandedWorkflowStage,
158
+ target: string,
159
+ ): boolean {
160
+ return (
161
+ stage.id === target ||
162
+ stage.name === target ||
163
+ stage.id.startsWith(target) ||
164
+ stage.workflowGraphTarget.stageId === target ||
165
+ stage.workflowGraphTarget.stageId.startsWith(target) ||
166
+ stage.workflowGraphTarget.runId === target ||
167
+ stage.workflowGraphTarget.runId.startsWith(target)
168
+ );
169
+ }
170
+
171
+ export function expandedStageLabel(stage: ExpandedWorkflowStage): string {
172
+ const runPrefix = stage.workflowGraphTarget.runId.slice(0, 8);
173
+ const stagePrefix = stage.workflowGraphTarget.stageId.slice(0, 8);
174
+ const depthPrefix = stage.workflowGraphTarget.depth > 0
175
+ ? `${childAliasFor(stage) ?? stage.workflowGraphTarget.runName}:`
176
+ : "";
177
+ return `${depthPrefix}${stage.name} (${runPrefix}/${stagePrefix})`;
178
+ }