@bastani/atomic 0.8.21 → 0.8.22

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 +66 -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 +20 -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 +113 -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
@@ -6,10 +6,11 @@ import type {
6
6
  WorkflowToolResult,
7
7
  } from "./render-result.js";
8
8
  import { renderInputsSchema } from "../shared/render-inputs-schema.js";
9
+ import { deriveInputFields, schemaIsRequired, schemaChoices, schemaFieldKind, schemaDescription } from "../shared/schema-introspection.js";
9
10
  import { WorkflowParametersSchema } from "./workflow-schema.js";
10
11
  import { renderRunBanner, renderRunSummary } from "./renderers.js";
11
12
  import type { RunEndPayload, RunStartPayload } from "./renderers.js";
12
- import type { StageSnapshot, StageStatus, ToolEvent } from "../shared/store-types.js";
13
+ import type { RunStatus, StageSnapshot, StageStatus, ToolEvent } from "../shared/store-types.js";
13
14
  import { store } from "../shared/store.js";
14
15
  import { stageUiBroker } from "../shared/stage-ui-broker.js";
15
16
  import {
@@ -29,6 +30,7 @@ import {
29
30
  interruptRun,
30
31
  interruptAllRuns,
31
32
  inspectRun,
33
+ type RunDetail,
32
34
  } from "../runs/background/status.js";
33
35
  import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
34
36
  import { stageControlRegistry } from "../runs/foreground/stage-control-registry.js";
@@ -45,6 +47,7 @@ import type { OverlayPiSurface } from "../tui/overlay-adapter.js";
45
47
  import type { GraphOverlayPort } from "../tui/overlay-adapter.js";
46
48
  import { renderSessionList } from "../tui/session-list.js";
47
49
  import { selectRunsForPicker } from "../tui/session-picker.js";
50
+ import { renderRunDetail } from "../tui/run-detail.js";
48
51
 
49
52
  import { openSessionPicker, openKillConfirm } from "../tui/session-overlays.js";
50
53
  import {
@@ -77,8 +80,15 @@ import {
77
80
  resetWorkflowLifecycleNotificationState,
78
81
  seedWorkflowLifecycleNotificationState,
79
82
  withWorkflowLifecycleNotificationsSuppressed,
83
+ withWorkflowLifecycleNotificationsSuppressedAsync,
80
84
  } from "./lifecycle-notifications.js";
81
85
  import type { WorkflowLifecycleNotificationConfig } from "./lifecycle-notifications.js";
86
+ import {
87
+ createWorkflowHilAnswerNotificationState,
88
+ installWorkflowHilAnswerNotifications,
89
+ registerHilAnswerNoticeRenderer,
90
+ resetWorkflowHilAnswerNotificationState,
91
+ } from "./hil-answer-notifications.js";
82
92
  import type { ConfigLoadResult } from "./config-loader.js";
83
93
  import type {
84
94
  WorkflowPersistencePort,
@@ -91,7 +101,11 @@ import type {
91
101
  WorkflowModelCatalogPort,
92
102
  WorkflowModelInfo,
93
103
  StageOptions,
104
+ WorkflowExecutionPolicy,
105
+ WorkflowInputValues,
106
+ WorkflowSerializableValue,
94
107
  } from "../shared/types.js";
108
+ import { INTERACTIVE_WORKFLOW_POLICY, NON_INTERACTIVE_WORKFLOW_POLICY } from "../shared/types.js";
95
109
  import { buildRuntimeAdapters } from "./wiring.js";
96
110
  import type { PiUISurface } from "./wiring.js";
97
111
  import { createStatusWriter } from "./status-writer.js";
@@ -99,8 +113,22 @@ import type { StatusWriter } from "./status-writer.js";
99
113
  import { setMcpScope, clearMcpScope } from "./mcp.js";
100
114
  import type { PiMcpExtensionAPI, PiEventBus } from "./mcp.js";
101
115
  import type { StageSessionRuntime } from "../runs/foreground/stage-runner.js";
116
+ import {
117
+ expandWorkflowGraph,
118
+ expandedStageLabel,
119
+ stageMatchesExpandedIdentifier,
120
+ } from "../shared/expanded-workflow-graph.js";
121
+ import { topLevelWorkflowRuns } from "../shared/run-visibility.js";
102
122
  import { WORKFLOW_STAGE_SUBAGENT_GUARD_ENV, getEnvValue, type CreateAgentSessionOptions } from "@bastani/atomic";
103
123
 
124
+ export const WORKFLOW_TOOL_DESCRIPTION =
125
+ "Run named workflows or direct one-off task/tasks/chain workflows; " +
126
+ "discover with list/get/inputs, inspect status/stages/stage details, " +
127
+ "send prompt answers or steering, pause/resume/interrupt/kill runs, and reload workflow resources. " +
128
+ "For transcripts, prefer status/stages/stage to get sessionFile/transcriptPath, " +
129
+ "quote the exact path without rewriting separators (Windows backslashes are valid), " +
130
+ "search it with rg/grep, and read small ranges; transcript defaults to at most 5 recent entries and explicit tail/limit overrides that preview.";
131
+
104
132
  // ---------------------------------------------------------------------------
105
133
  // Minimal ExtensionAPI structural types
106
134
  // No `any`; all optional fields use explicit union with undefined.
@@ -133,12 +161,17 @@ export interface PiMessageRenderComponent {
133
161
  invalidate?: () => void;
134
162
  }
135
163
 
136
- export type PiMessageRendererResult = string | PiMessageRenderComponent | undefined;
137
-
138
- function textRenderComponent(text: string): PiRenderComponent {
139
- return dynamicTextRenderComponent(() => text);
164
+ export interface PiMessageRenderOptions {
165
+ expanded: boolean;
140
166
  }
141
167
 
168
+ export type PiMessageRendererResult = string | PiMessageRenderComponent | undefined;
169
+ export type PiMessageRenderer = (
170
+ payload: unknown,
171
+ options?: PiMessageRenderOptions,
172
+ theme?: unknown,
173
+ ) => PiMessageRendererResult;
174
+
142
175
  function dynamicTextRenderComponent(renderText: (width: number) => string): PiRenderComponent {
143
176
  return {
144
177
  render(width: number): string[] {
@@ -300,7 +333,7 @@ export interface ExtensionAPI {
300
333
  registerCommand?: (name: string, options: PiCommandOptions) => void;
301
334
  registerMessageRenderer?: (
302
335
  event: string,
303
- renderer: (payload: unknown) => PiMessageRendererResult,
336
+ renderer: PiMessageRenderer,
304
337
  ) => void;
305
338
  /**
306
339
  * Inject a custom message into chat history. Used by inline workflow surfaces
@@ -316,12 +349,16 @@ export interface ExtensionAPI {
316
349
  },
317
350
  options?: {
318
351
  triggerTurn?: boolean;
319
- deliverAs?: "steer" | "followUp" | "nextTurn";
352
+ deliverAs?: "steer" | "followUp" | "nextTurn" | "interrupt";
353
+ excludeFromContext?: boolean;
354
+ interruptAbortMessage?: string;
320
355
  },
321
356
  ) => void | Promise<void>;
322
357
  registerFlag?: (name: string, opts: PiFlagNamedOpts) => void;
323
358
  /** Return package-provided workflow files discovered by Atomic's package loader. */
324
359
  getWorkflowResources?: () => readonly WorkflowResourceInfo[];
360
+ /** Refresh package-provided workflow files before rediscovery, when supported by host. */
361
+ refreshWorkflowResources?: () => Promise<readonly WorkflowResourceInfo[]>;
325
362
  /**
326
363
  * Register a keyboard shortcut.
327
364
  * Present on pi >= 1.x; absent on older runtimes.
@@ -339,9 +376,8 @@ export interface ExtensionAPI {
339
376
  */
340
377
  getActiveTools?: () => string[];
341
378
  /**
342
- * Replace the model's active tool set by name. Used to drop the `workflow`
343
- * tool in non-interactive sessions. Present on pi's ExtensionAPI; absent on
344
- * older runtimes.
379
+ * Replace the model's active tool set by name. Present on pi's ExtensionAPI;
380
+ * absent on older runtimes.
345
381
  */
346
382
  setActiveTools?: (toolNames: string[]) => void;
347
383
  /**
@@ -432,7 +468,7 @@ export interface ExtensionAPI {
432
468
  export interface WorkflowToolArgs extends StageOptions {
433
469
  /** Canonical named workflow identifier. */
434
470
  workflow?: string;
435
- inputs?: Record<string, unknown>;
471
+ inputs?: WorkflowInputValues;
436
472
  action?:
437
473
  | "run"
438
474
  | "list"
@@ -601,6 +637,11 @@ function renderTranscriptToolContent(
601
637
  ];
602
638
  if (result.sessionId) lines.push(`sessionId: ${result.sessionId}`);
603
639
  if (result.sessionFile) lines.push(`sessionFile: ${result.sessionFile}`);
640
+ if (result.sessionFile) lines.push(`sessionFileJson: ${JSON.stringify(result.sessionFile)}`);
641
+ if (result.transcriptPath) lines.push(`transcriptPath: ${result.transcriptPath}`);
642
+ if (result.transcriptPath) lines.push(`transcriptPathJson: ${JSON.stringify(result.transcriptPath)}`);
643
+ if (result.entryCount !== undefined) lines.push(`availableEntries: ${result.entryCount}`);
644
+ if (result.entryLimit !== undefined) lines.push(`entryLimit: ${result.entryLimit}`);
604
645
  if (result.entries.length === 0) {
605
646
  lines.push("entries: none");
606
647
  return lines.join("\n");
@@ -644,6 +685,9 @@ function renderStagesToolContent(
644
685
  lines.push(`[${index + 1}] ${stage.name} (${stage.id}) ${stage.status}`);
645
686
  if (stage.sessionId) lines.push(`sessionId: ${stage.sessionId}`);
646
687
  if (stage.sessionFile) lines.push(`sessionFile: ${stage.sessionFile}`);
688
+ if (stage.sessionFile) lines.push(`sessionFileJson: ${JSON.stringify(stage.sessionFile)}`);
689
+ if (stage.transcriptPath) lines.push(`transcriptPath: ${stage.transcriptPath}`);
690
+ if (stage.transcriptPath) lines.push(`transcriptPathJson: ${JSON.stringify(stage.transcriptPath)}`);
647
691
  if (stage.error) lines.push(`error: ${stage.error}`);
648
692
  if (stage.awaitingInputSince !== undefined) {
649
693
  lines.push(`awaitingInputSince: ${stage.awaitingInputSince}`);
@@ -670,6 +714,10 @@ function renderStageToolContent(
670
714
  }
671
715
  lines.push("stage:");
672
716
  lines.push(JSON.stringify(result.stage, null, 2));
717
+ if (result.stage.sessionFile) {
718
+ lines.push(`transcriptPath: ${result.stage.sessionFile}`);
719
+ lines.push(`transcriptPathJson: ${JSON.stringify(result.stage.sessionFile)}`);
720
+ }
673
721
  return lines.join("\n");
674
722
  }
675
723
 
@@ -716,14 +764,7 @@ function workflowGetResult(
716
764
  error: `Workflow not found: "${workflow}"`,
717
765
  };
718
766
  }
719
- const inputs = Object.entries(def.inputs).map(([name, schema]) => ({
720
- name,
721
- type: schema.type,
722
- description: schema.description,
723
- required: schema.required,
724
- default: "default" in schema ? schema.default : undefined,
725
- choices: schema.type === "select" ? schema.choices : undefined,
726
- }));
767
+ const inputs = deriveInputFields(def.inputs);
727
768
  return {
728
769
  action: "get",
729
770
  workflow: def.normalizedName,
@@ -735,7 +776,7 @@ function workflowGetResult(
735
776
  workflow: def.normalizedName,
736
777
  name: def.name,
737
778
  description: def.description,
738
- inputs,
779
+ inputs: inputs as unknown as WorkflowSerializableValue[],
739
780
  },
740
781
  progress: { completed: 0, total: 0 },
741
782
  },
@@ -752,6 +793,7 @@ type WorkflowStageSummary = {
752
793
  status: StageStatus;
753
794
  sessionId?: string;
754
795
  sessionFile?: string;
796
+ transcriptPath?: string;
755
797
  error?: string;
756
798
  awaitingInputSince?: number;
757
799
  pendingPrompt?: StageSnapshot["pendingPrompt"];
@@ -776,8 +818,10 @@ type MessageLike = {
776
818
  readonly createdAt?: number;
777
819
  };
778
820
 
779
- function cloneStage(stage: StageSnapshot): StageSnapshot {
780
- return structuredClone(stage);
821
+ function cloneStage(stage: StageSnapshot): StageSnapshot & { transcriptPath?: string } {
822
+ const cloned = structuredClone(stage) as StageSnapshot & { transcriptPath?: string };
823
+ if (cloned.sessionFile !== undefined) cloned.transcriptPath = cloned.sessionFile;
824
+ return cloned;
781
825
  }
782
826
 
783
827
  function summarizeStage(stage: StageSnapshot): WorkflowStageSummary {
@@ -787,6 +831,7 @@ function summarizeStage(stage: StageSnapshot): WorkflowStageSummary {
787
831
  status: stage.status,
788
832
  sessionId: stage.sessionId,
789
833
  sessionFile: stage.sessionFile,
834
+ transcriptPath: stage.sessionFile,
790
835
  error: stage.error,
791
836
  awaitingInputSince: stage.awaitingInputSince,
792
837
  pendingPrompt: stage.pendingPrompt === undefined
@@ -798,27 +843,50 @@ function summarizeStage(stage: StageSnapshot): WorkflowStageSummary {
798
843
  };
799
844
  }
800
845
 
801
- const DEFAULT_TRANSCRIPT_LIMIT = 50;
846
+ const DEFAULT_TRANSCRIPT_LIMIT = 5;
847
+
848
+ type TranscriptEntrySelection = {
849
+ entries: WorkflowTranscriptEntry[];
850
+ truncated: boolean;
851
+ entryCount: number;
852
+ entryLimit?: number;
853
+ };
802
854
 
803
- function boundedCount(args: WorkflowToolArgs): number {
855
+ function requestedTranscriptEntryLimit(args: WorkflowToolArgs): number {
804
856
  const raw = args.tail ?? args.limit;
805
857
  if (raw === undefined) return DEFAULT_TRANSCRIPT_LIMIT;
806
858
  if (!Number.isFinite(raw) || raw <= 0) return 0;
807
859
  return Math.floor(raw);
808
860
  }
809
861
 
810
- function applyEntryLimit<T>(
811
- entries: readonly T[],
862
+ function selectTranscriptEntries(
863
+ entries: readonly WorkflowTranscriptEntry[],
812
864
  args: WorkflowToolArgs,
813
- ): { entries: T[]; truncated: boolean } {
814
- const count = boundedCount(args);
865
+ ): TranscriptEntrySelection {
866
+ const count = requestedTranscriptEntryLimit(args);
867
+ const entryCount = entries.length;
815
868
  if (count === 0) {
816
- return { entries: [], truncated: false };
869
+ return {
870
+ entries: [],
871
+ truncated: false,
872
+ entryCount,
873
+ entryLimit: count,
874
+ };
817
875
  }
818
876
  if (entries.length <= count) {
819
- return { entries: [...entries], truncated: false };
877
+ return {
878
+ entries: [...entries],
879
+ truncated: false,
880
+ entryCount,
881
+ entryLimit: count,
882
+ };
820
883
  }
821
- return { entries: entries.slice(entries.length - count), truncated: true };
884
+ return {
885
+ entries: entries.slice(entries.length - count),
886
+ truncated: true,
887
+ entryCount,
888
+ entryLimit: count,
889
+ };
822
890
  }
823
891
 
824
892
  function messageText(content: MessageLike["content"]): string | undefined {
@@ -981,7 +1049,15 @@ function stageFailureMessage(
981
1049
  }
982
1050
 
983
1051
  function inFlightRunCount(): number {
984
- return store.runs().filter((run) => run.endedAt === undefined).length;
1052
+ return topLevelWorkflowRuns(store.runs()).filter((run) => run.endedAt === undefined).length;
1053
+ }
1054
+
1055
+ function topLevelExpandedSnapshots() {
1056
+ const snapshot = store.snapshot();
1057
+ return topLevelWorkflowRuns(snapshot.runs).map((run) => ({
1058
+ ...structuredClone(run),
1059
+ stages: expandWorkflowGraph(snapshot, run.id).stages.map((stage) => structuredClone(stage)),
1060
+ }));
985
1061
  }
986
1062
 
987
1063
  function reloadBlockedMessage(count = inFlightRunCount()): string {
@@ -1012,39 +1088,152 @@ function isWorkflowStageToolContext(ctx: PiExecuteContext): boolean {
1012
1088
  return hasWorkflowStageSubagentGuardEnv() || ctx.orchestrationContext?.kind === "workflow-stage";
1013
1089
  }
1014
1090
 
1015
- /** Tool name registered for workflow execution; shared by the de-advertise guard. */
1016
- const WORKFLOW_TOOL_NAME = "workflow";
1017
-
1018
1091
  /**
1019
- * User-facing message shown when the `/workflow` command is invoked in a
1020
- * non-interactive (`-p` / `--print` / `--mode json`) session. Such sessions
1021
- * bind a no-op UI surface (`ctx.hasUI === false`) and cannot drive workflow
1022
- * pickers, the graph overlay, or human-in-the-loop prompts.
1023
- *
1024
- * The `workflow` tool is removed from the model's tool set in these sessions
1025
- * (see `deAdvertiseWorkflowToolWhenHeadless`). The slash command has no
1026
- * advertise layer and is still reachable via `atomic -p "/workflow …"` (print
1027
- * mode dispatches leading-slash commands through `session.prompt`), so it is
1028
- * refused here instead.
1092
+ * Legacy message retained for consumers that imported the old refusal string.
1093
+ * Non-interactive sessions now keep the workflow tool and `/workflow` command
1094
+ * available; policy gates interactive pickers and runtime human-input APIs.
1029
1095
  */
1030
1096
  export const WORKFLOW_NON_INTERACTIVE_MESSAGE =
1031
- "Workflows are disabled in non-interactive (-p) mode; run Atomic interactively to use workflows.";
1097
+ "Workflows are policy-gated in non-interactive (-p) mode; deterministic workflows can run headlessly while runtime human input remains unavailable.";
1032
1098
 
1033
- /**
1034
- * Remove the `workflow` tool from the model's active tool set in non-interactive
1035
- * (`-p` / `--mode json`) sessions, which bind a no-op UI (`ctx.hasUI === false`)
1036
- * and cannot drive workflow prompts or the graph overlay. Invoked from
1037
- * `session_start`, which the host awaits before the first prompt, so the tool is
1038
- * gone before the model's first turn. Interactive and RPC modes bind a real UI
1039
- * context (`hasUI: true`) and keep the tool. No-ops on hosts that predate the
1040
- * `getActiveTools`/`setActiveTools` extension API.
1041
- */
1042
- function deAdvertiseWorkflowToolWhenHeadless(pi: ExtensionAPI, hasUI: boolean | undefined): void {
1099
+ export function workflowPolicyFromContext(ctx?: { readonly hasUI?: boolean }): WorkflowExecutionPolicy {
1100
+ if (ctx?.hasUI === false) {
1101
+ return NON_INTERACTIVE_WORKFLOW_POLICY;
1102
+ }
1103
+ return INTERACTIVE_WORKFLOW_POLICY;
1104
+ }
1105
+
1106
+ function isRunStatus(value: string): value is RunStatus {
1107
+ switch (value) {
1108
+ case "pending":
1109
+ case "running":
1110
+ case "paused":
1111
+ case "completed":
1112
+ case "failed":
1113
+ case "killed":
1114
+ return true;
1115
+ default:
1116
+ return false;
1117
+ }
1118
+ }
1119
+
1120
+ function fallbackRunDetailFromResult(
1121
+ workflowName: string,
1122
+ inputs: Readonly<WorkflowInputValues>,
1123
+ result: Extract<WorkflowToolResult, { action: "run"; runId: string }>,
1124
+ ): RunDetail {
1125
+ const now = Date.now();
1126
+ const stages = result.stages?.map((stage) => structuredClone(stage)) ?? [];
1127
+ // This path is a degraded last-resort view used only when the retained run
1128
+ // snapshot has disappeared before output rendering. Timestamps are synthetic,
1129
+ // so prefer a conservative failed status over fabricating success if the tool
1130
+ // result status is not one of the known run states.
1131
+ return {
1132
+ runId: result.runId,
1133
+ name: result.name ?? workflowName,
1134
+ status: isRunStatus(result.status) ? result.status : "failed",
1135
+ mode: stages.length > 1 ? "chain" : "single",
1136
+ startedAt: now,
1137
+ endedAt: now,
1138
+ durationMs: 0,
1139
+ inputs,
1140
+ stages,
1141
+ result: result.result,
1142
+ error: result.error,
1143
+ };
1144
+ }
1145
+
1146
+ function emitTerminalRunDetailSurface(
1147
+ pi: ExtensionAPI,
1148
+ workflowName: string,
1149
+ inputs: Readonly<WorkflowInputValues>,
1150
+ result: Extract<WorkflowToolResult, { action: "run"; runId: string }>,
1151
+ ): void {
1152
+ const inspected = inspectRun(result.runId);
1153
+ const detail = inspected.ok
1154
+ ? inspected.detail
1155
+ : fallbackRunDetailFromResult(workflowName, inputs, result);
1156
+ emitChatSurface(
1157
+ pi,
1158
+ { kind: "detail", detail },
1159
+ { content: renderRunDetail(detail, { width: 100 }) },
1160
+ );
1161
+ }
1162
+
1163
+ export const WORKFLOW_COMMAND_OUTPUT_CUSTOM_TYPE = "workflows:command-output";
1164
+
1165
+ interface WorkflowCommandOutputDetails {
1166
+ readonly command: string;
1167
+ readonly workflowName?: string;
1168
+ }
1169
+
1170
+ function emitWorkflowCommandOutput(
1171
+ pi: ExtensionAPI,
1172
+ content: string,
1173
+ details: WorkflowCommandOutputDetails,
1174
+ ): void {
1175
+ if (typeof pi.sendMessage !== "function") return;
1176
+ void pi.sendMessage<WorkflowCommandOutputDetails>({
1177
+ customType: WORKFLOW_COMMAND_OUTPUT_CUSTOM_TYPE,
1178
+ content,
1179
+ display: true,
1180
+ details,
1181
+ });
1182
+ }
1183
+
1184
+ interface WorkflowCommandReporter {
1185
+ info(message: string): void;
1186
+ error(message: string): void;
1187
+ }
1188
+
1189
+ function formatAvailableWorkflowNames(names: readonly string[]): string {
1190
+ return names.length > 0 ? names.join(", ") : "(none)";
1191
+ }
1192
+
1193
+ const ASK_USER_QUESTION_TOOL_NAME = "ask_user_question";
1194
+
1195
+ function deAdvertiseAskUserQuestionWhenHeadless(
1196
+ pi: ExtensionAPI,
1197
+ hasUI: boolean | undefined,
1198
+ ): void {
1043
1199
  if (hasUI !== false) return;
1044
1200
  if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
1045
- const active = pi.getActiveTools();
1046
- if (!active.includes(WORKFLOW_TOOL_NAME)) return;
1047
- pi.setActiveTools(active.filter((name) => name !== WORKFLOW_TOOL_NAME));
1201
+
1202
+ const activeTools = pi.getActiveTools();
1203
+ if (!activeTools.includes(ASK_USER_QUESTION_TOOL_NAME)) return;
1204
+
1205
+ pi.setActiveTools(activeTools.filter((toolName) => toolName !== ASK_USER_QUESTION_TOOL_NAME));
1206
+ }
1207
+
1208
+ class WorkflowHeadlessCommandError extends Error {
1209
+ constructor(message: string) {
1210
+ super(message);
1211
+ this.name = "WorkflowHeadlessCommandError";
1212
+ }
1213
+ }
1214
+
1215
+ function createWorkflowCommandReporter(
1216
+ ctx: PiCommandContext,
1217
+ policy: WorkflowExecutionPolicy = workflowPolicyFromContext(ctx),
1218
+ pi?: ExtensionAPI,
1219
+ ): WorkflowCommandReporter {
1220
+ return {
1221
+ info(message: string): void {
1222
+ if (policy.mode === "non_interactive") {
1223
+ if (pi) {
1224
+ emitWorkflowCommandOutput(pi, message, { command: "message" });
1225
+ }
1226
+ return;
1227
+ }
1228
+ ctx.ui.notify(message, "info");
1229
+ },
1230
+ error(message: string): void {
1231
+ if (policy.mode === "non_interactive") {
1232
+ throw new WorkflowHeadlessCommandError(message);
1233
+ }
1234
+ ctx.ui.notify(message, "error");
1235
+ },
1236
+ };
1048
1237
  }
1049
1238
 
1050
1239
  // ---------------------------------------------------------------------------
@@ -1078,6 +1267,7 @@ export function makeExecuteWorkflowTool(
1078
1267
  }
1079
1268
  const activeRuntime =
1080
1269
  typeof runtime === "function" ? runtime(ctx) : runtime;
1270
+ const policy = workflowPolicyFromContext(ctx);
1081
1271
 
1082
1272
  switch (action) {
1083
1273
  case "get":
@@ -1096,12 +1286,13 @@ export function makeExecuteWorkflowTool(
1096
1286
  }
1097
1287
  const details = await activeRuntime.runDirect(
1098
1288
  withForkParentSession(args, ctx),
1289
+ { policy },
1099
1290
  );
1100
1291
  return workflowRunResultFromDetails(details);
1101
1292
  }
1102
1293
  // Delegate to registry-backed dispatcher.
1103
1294
  // Real errors propagate — no broad catch.
1104
- return activeRuntime.dispatch(args);
1295
+ return activeRuntime.dispatch(args, { policy });
1105
1296
 
1106
1297
  case "status": {
1107
1298
  // Detail mode — single-run lookup via id.
@@ -1123,10 +1314,9 @@ export function makeExecuteWorkflowTool(
1123
1314
  }
1124
1315
  // List mode — emit all retained snapshots; the renderer produces the
1125
1316
  // canonical band + card surface.
1126
- const snapshots = store.runs();
1127
1317
  return {
1128
1318
  action: "status",
1129
- snapshots: snapshots.map((snapshot) => structuredClone(snapshot)),
1319
+ snapshots: topLevelExpandedSnapshots(),
1130
1320
  };
1131
1321
  }
1132
1322
 
@@ -1200,14 +1390,15 @@ export function makeExecuteWorkflowTool(
1200
1390
  : stage.message,
1201
1391
  };
1202
1392
  }
1203
- const run = store.runs().find((r) => r.id === target.runId);
1393
+ const stageRunId = stage.runId ?? target.runId;
1394
+ const run = store.runs().find((r) => r.id === stageRunId);
1204
1395
  const snapshot = run?.stages.find((s) => s.id === stage.stageId);
1205
1396
  return snapshot
1206
- ? { action: "stage", runId: target.runId, stage: cloneStage(snapshot) }
1397
+ ? { action: "stage", runId: stageRunId, stage: cloneStage(snapshot) }
1207
1398
  : {
1208
1399
  action: "stage",
1209
- runId: target.runId,
1210
- error: `Stage not found in run ${target.runId.slice(0, 8)}: ${stage.stageId}`,
1400
+ runId: stageRunId,
1401
+ error: `Stage not found in run ${stageRunId.slice(0, 8)}: ${stage.stageId}`,
1211
1402
  };
1212
1403
  }
1213
1404
 
@@ -1263,36 +1454,39 @@ export function makeExecuteWorkflowTool(
1263
1454
  truncated: false,
1264
1455
  };
1265
1456
  }
1266
- const run = store.runs().find((r) => r.id === target.runId);
1457
+ const stageRunId = stage.runId ?? target.runId;
1458
+ const run = store.runs().find((r) => r.id === stageRunId);
1267
1459
  const snapshot = run?.stages.find((s) => s.id === stage.stageId);
1268
- const liveHandle = stageControlRegistry.get(target.runId, stage.stageId);
1460
+ const liveHandle = stageControlRegistry.get(stageRunId, stage.stageId);
1269
1461
  if (liveHandle !== undefined) {
1270
- const limited = applyEntryLimit(
1462
+ const sessionFile = liveHandle.sessionFile ?? snapshot?.sessionFile;
1463
+ const sessionId = liveHandle.sessionId ?? snapshot?.sessionId;
1464
+ const limited = selectTranscriptEntries(
1271
1465
  liveHandle.messages.map((m) => transcriptEntryFromMessage(m as MessageLike)),
1272
1466
  args,
1273
1467
  );
1274
1468
  return {
1275
1469
  action: "transcript",
1276
- runId: target.runId,
1470
+ runId: stageRunId,
1277
1471
  stageId: stage.stageId,
1278
1472
  source: "live",
1279
- entries: limited.entries,
1280
- truncated: limited.truncated,
1281
- sessionId: liveHandle.sessionId,
1282
- sessionFile: liveHandle.sessionFile,
1473
+ ...limited,
1474
+ sessionId,
1475
+ sessionFile,
1476
+ transcriptPath: sessionFile,
1283
1477
  };
1284
1478
  }
1285
1479
  const fallback = snapshotTranscriptEntries(snapshot, args.includeToolOutput === true);
1286
- const limited = applyEntryLimit(fallback, args);
1480
+ const limited = selectTranscriptEntries(fallback, args);
1287
1481
  return {
1288
1482
  action: "transcript",
1289
- runId: target.runId,
1483
+ runId: stageRunId,
1290
1484
  stageId: stage.stageId,
1291
1485
  source: "snapshot",
1292
- entries: limited.entries,
1293
- truncated: limited.truncated,
1486
+ ...limited,
1294
1487
  sessionId: snapshot?.sessionId,
1295
1488
  sessionFile: snapshot?.sessionFile,
1489
+ transcriptPath: snapshot?.sessionFile,
1296
1490
  };
1297
1491
  }
1298
1492
 
@@ -1318,13 +1512,14 @@ export function makeExecuteWorkflowTool(
1318
1512
  stage.ok ? "Stage id, prefix, or name is required." : stage.message,
1319
1513
  );
1320
1514
  }
1321
- const run = store.runs().find((r) => r.id === target.runId);
1515
+ const stageRunId = stage.runId ?? target.runId;
1516
+ const run = store.runs().find((r) => r.id === stageRunId);
1322
1517
  const snapshot = run?.stages.find((s) => s.id === stage.stageId);
1323
1518
  // Brokered structured prompts (in-stage ask_user_question / readiness
1324
1519
  // gate) resolve through StageUiBroker rather than store.pendingPrompt.
1325
1520
  // Answer those first when one is pending and the promptId (if any) lines
1326
1521
  // up — otherwise fall through to the store-prompt / live-handle paths.
1327
- const brokerPrompt = stageUiBroker.peekStagePrompt(target.runId, stage.stageId);
1522
+ const brokerPrompt = stageUiBroker.peekStagePrompt(stageRunId, stage.stageId);
1328
1523
  const targetsBrokerPrompt =
1329
1524
  brokerPrompt !== undefined &&
1330
1525
  (args.promptId === undefined || args.promptId === brokerPrompt.id) &&
@@ -1333,11 +1528,13 @@ export function makeExecuteWorkflowTool(
1333
1528
  requestedDelivery === "auto");
1334
1529
  if (targetsBrokerPrompt && brokerPrompt !== undefined) {
1335
1530
  if (!hasPayloadProperty(args)) {
1336
- return workflowSendResult(target.runId, stage.stageId, "answer", "noop", "Send requires text, response, or message.");
1531
+ return workflowSendResult(stageRunId, stage.stageId, "answer", "noop", "Send requires text, response, or message.");
1337
1532
  }
1338
- const ok = stageUiBroker.answerStagePrompt(target.runId, stage.stageId, brokerAnswerFromArgs(args));
1533
+ const ok = stageUiBroker.answerStagePrompt(stageRunId, stage.stageId, brokerAnswerFromArgs(args), {
1534
+ answerSource: "workflow_tool",
1535
+ });
1339
1536
  return workflowSendResult(
1340
- target.runId,
1537
+ stageRunId,
1341
1538
  stage.stageId,
1342
1539
  "answer",
1343
1540
  ok ? "ok" : "noop",
@@ -1351,14 +1548,25 @@ export function makeExecuteWorkflowTool(
1351
1548
  if (targetsPrompt) {
1352
1549
  const promptId = args.promptId ?? snapshot?.pendingPrompt?.id;
1353
1550
  if (promptId === undefined) {
1354
- return workflowSendResult(target.runId, stage.stageId, "answer", "noop", "No pending prompt to answer.");
1551
+ return workflowSendResult(stageRunId, stage.stageId, "answer", "noop", "No pending prompt to answer.");
1355
1552
  }
1356
1553
  if (!hasPayloadProperty(args)) {
1357
- return workflowSendResult(target.runId, stage.stageId, "answer", "noop", "Send requires text, response, or message.");
1554
+ return workflowSendResult(stageRunId, stage.stageId, "answer", "noop", "Send requires text, response, or message.");
1555
+ }
1556
+ if (stageUiBroker.wasStagePromptResolved(stageRunId, stage.stageId, promptId)) {
1557
+ return workflowSendResult(
1558
+ stageRunId,
1559
+ stage.stageId,
1560
+ "answer",
1561
+ "ok",
1562
+ `Input request ${promptId} was already answered.`,
1563
+ );
1358
1564
  }
1359
- const ok = store.resolveStagePendingPrompt(target.runId, stage.stageId, promptId, promptPayloadFromArgs(args));
1565
+ const ok = store.resolveStagePendingPrompt(stageRunId, stage.stageId, promptId, promptPayloadFromArgs(args), {
1566
+ answerSource: "workflow_tool",
1567
+ });
1360
1568
  return workflowSendResult(
1361
- target.runId,
1569
+ stageRunId,
1362
1570
  stage.stageId,
1363
1571
  "answer",
1364
1572
  ok ? "ok" : "noop",
@@ -1367,26 +1575,26 @@ export function makeExecuteWorkflowTool(
1367
1575
  }
1368
1576
  const text = textPayloadFromArgs(args);
1369
1577
  if (text === undefined) {
1370
- return workflowSendResult(target.runId, stage.stageId, requestedDelivery, "noop", "Send requires text, response, or message.");
1578
+ return workflowSendResult(stageRunId, stage.stageId, requestedDelivery, "noop", "Send requires text, response, or message.");
1371
1579
  }
1372
- const handle = stageControlRegistry.get(target.runId, stage.stageId);
1580
+ const handle = stageControlRegistry.get(stageRunId, stage.stageId);
1373
1581
  if (handle === undefined) {
1374
- return workflowSendResult(target.runId, stage.stageId, requestedDelivery, "noop", "No live handle for stage.");
1582
+ return workflowSendResult(stageRunId, stage.stageId, requestedDelivery, "noop", "No live handle for stage.");
1375
1583
  }
1376
1584
  if (requestedDelivery === "resume" || (requestedDelivery === "auto" && handle.status === "paused")) {
1377
1585
  await handle.resume(text);
1378
- return workflowSendResult(target.runId, stage.stageId, "resume", "ok", "Resumed stage with message.");
1586
+ return workflowSendResult(stageRunId, stage.stageId, "resume", "ok", "Resumed stage with message.");
1379
1587
  }
1380
1588
  if (requestedDelivery === "steer" || (requestedDelivery === "auto" && handle.isStreaming)) {
1381
1589
  await handle.steer(text);
1382
- return workflowSendResult(target.runId, stage.stageId, "steer", "ok", "Steered live stage.");
1590
+ return workflowSendResult(stageRunId, stage.stageId, "steer", "ok", "Steered live stage.");
1383
1591
  }
1384
1592
  if (requestedDelivery === "prompt") {
1385
1593
  await handle.prompt(text);
1386
- return workflowSendResult(target.runId, stage.stageId, "prompt", "ok", "Prompt sent to stage.");
1594
+ return workflowSendResult(stageRunId, stage.stageId, "prompt", "ok", "Prompt sent to stage.");
1387
1595
  }
1388
1596
  await handle.followUp(text);
1389
- return workflowSendResult(target.runId, stage.stageId, "followUp", "ok", "Follow-up queued for stage.");
1597
+ return workflowSendResult(stageRunId, stage.stageId, "followUp", "ok", "Follow-up queued for stage.");
1390
1598
  }
1391
1599
 
1392
1600
  case "pause": {
@@ -1415,14 +1623,15 @@ export function makeExecuteWorkflowTool(
1415
1623
  if (target.kind === "not_found") return { action, runId: target.target, status: "noop", message: target.message };
1416
1624
  const stage = resolveToolStageTarget(target.runId, args.stageId);
1417
1625
  if (!stage.ok) return { action, runId: target.runId, status: "noop", message: stage.message };
1418
- const result = pauseRun(target.runId, { stageId: stage.stageId });
1626
+ const stageRunId = stage.runId ?? target.runId;
1627
+ const result = pauseRun(stageRunId, { stageId: stage.stageId });
1419
1628
  return result.ok
1420
1629
  ? { action, runId: result.runId, status: "paused", message: `Paused ${result.paused.length} stage(s) on run ${result.runId.slice(0, 8)}.` }
1421
1630
  : {
1422
1631
  action,
1423
- runId: target.runId,
1632
+ runId: stageRunId,
1424
1633
  status: "noop",
1425
- message: stageFailureMessage(target.runId, result.reason, "pause"),
1634
+ message: stageFailureMessage(stageRunId, result.reason, "pause"),
1426
1635
  };
1427
1636
  }
1428
1637
 
@@ -1544,7 +1753,8 @@ export function makeExecuteWorkflowTool(
1544
1753
  if (!stage.ok) {
1545
1754
  return { action, runId: target.runId, status: "noop", message: stage.message };
1546
1755
  }
1547
- const result = interruptRun(target.runId, { stageId: stage.stageId });
1756
+ const stageRunId = stage.runId ?? target.runId;
1757
+ const result = interruptRun(stageRunId, { stageId: stage.stageId });
1548
1758
  if (result.ok) {
1549
1759
  return {
1550
1760
  action,
@@ -1557,9 +1767,9 @@ export function makeExecuteWorkflowTool(
1557
1767
  }
1558
1768
  return {
1559
1769
  action,
1560
- runId: target.runId,
1770
+ runId: stageRunId,
1561
1771
  status: "noop",
1562
- message: stageFailureMessage(target.runId, result.reason, "interrupt"),
1772
+ message: stageFailureMessage(stageRunId, result.reason, "interrupt"),
1563
1773
  };
1564
1774
  }
1565
1775
 
@@ -1578,20 +1788,21 @@ export function makeExecuteWorkflowTool(
1578
1788
  if (!stage.ok) {
1579
1789
  return { action: "resume", runId: target.runId, status: "noop", message: stage.message };
1580
1790
  }
1581
- const run = store.runs().find((r) => r.id === target.runId);
1791
+ const stageRunId = stage.runId ?? target.runId;
1792
+ const run = store.runs().find((r) => r.id === stageRunId);
1582
1793
  const isPaused =
1583
1794
  run?.status === "paused" ||
1584
1795
  (run?.stages.some((s) => s.status === "paused") ?? false);
1585
1796
  if (!isPaused && run?.status === "failed" && run.endedAt !== undefined && run.resumable !== false) {
1586
- const continuation = activeRuntime.resumeFailedRun(target.runId, stage.stageId);
1797
+ const continuation = activeRuntime.resumeFailedRun(stageRunId, stage.stageId, { policy });
1587
1798
  return {
1588
1799
  action: "resume",
1589
- runId: continuation.ok ? continuation.runId : target.runId,
1800
+ runId: continuation.ok ? continuation.runId : stageRunId,
1590
1801
  status: continuation.ok ? "running" : "noop",
1591
1802
  message: continuation.message,
1592
1803
  };
1593
1804
  }
1594
- const result = resumeRun(target.runId, { stageId: stage.stageId, message: args.message });
1805
+ const result = resumeRun(stageRunId, { stageId: stage.stageId, message: args.message });
1595
1806
  if (result.ok) {
1596
1807
  const message = result.message ?? (isPaused
1597
1808
  ? result.resumed.length === 0
@@ -1607,9 +1818,9 @@ export function makeExecuteWorkflowTool(
1607
1818
  }
1608
1819
  return {
1609
1820
  action: "resume",
1610
- runId: target.runId,
1821
+ runId: stageRunId,
1611
1822
  status: "noop",
1612
- message: `Run not found: ${target.runId}`,
1823
+ message: `Run not found: ${stageRunId}`,
1613
1824
  };
1614
1825
  }
1615
1826
 
@@ -1638,6 +1849,26 @@ export function makeExecuteWorkflowTool(
1638
1849
  */
1639
1850
  type WorkflowCommandHandler = PiCommandOptions["handler"];
1640
1851
 
1852
+ interface ParsedWorkflowSlashCommand {
1853
+ name: string;
1854
+ args: string;
1855
+ }
1856
+
1857
+ function parseWorkflowSlashCommand(text: string): ParsedWorkflowSlashCommand | undefined {
1858
+ const trimmed = text.trim();
1859
+ if (!trimmed.startsWith("/")) return undefined;
1860
+
1861
+ // First token (after `/`) is the command name. Whitespace splits
1862
+ // command from args; quote handling lives inside the command
1863
+ // handler itself (`tokenizeWorkflowArgs`).
1864
+ const firstSpace = trimmed.indexOf(" ");
1865
+ const name =
1866
+ firstSpace === -1 ? trimmed.slice(1) : trimmed.slice(1, firstSpace);
1867
+ const args = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1);
1868
+
1869
+ return { name, args };
1870
+ }
1871
+
1641
1872
  /**
1642
1873
  * Register a slash command with the host AND remember the handler so
1643
1874
  * the input interceptor can dispatch directly.
@@ -1707,26 +1938,24 @@ function installInputInterceptor(
1707
1938
  pi.on("input", async (event, ctx) => {
1708
1939
  const text = (event as { text?: unknown } | undefined)?.text;
1709
1940
  if (typeof text !== "string") return undefined;
1710
- const trimmed = text.trim();
1711
- if (!trimmed.startsWith("/")) return undefined;
1712
-
1713
- // First token (after `/`) is the command name. Whitespace splits
1714
- // command from args; quote handling lives inside the command
1715
- // handler itself (`tokenizeWorkflowArgs`).
1716
- const firstSpace = trimmed.indexOf(" ");
1717
- const name =
1718
- firstSpace === -1 ? trimmed.slice(1) : trimmed.slice(1, firstSpace);
1941
+ const parsedCommand = parseWorkflowSlashCommand(text);
1942
+ if (!parsedCommand) return undefined;
1943
+
1944
+ const { name, args } = parsedCommand;
1719
1945
  const handler = commands.get(name);
1720
1946
  if (!handler) return undefined; // not ours — let host run its normal flow.
1721
-
1722
- const args = firstSpace === -1 ? "" : trimmed.slice(firstSpace + 1);
1723
1947
  const commandCtx = ctx as PiCommandContext;
1724
1948
  try {
1725
1949
  await handler(args, commandCtx);
1726
1950
  } catch (err) {
1727
- // Match the host command runner: swallow handler exceptions so a
1728
- // throw never bubbles out and crashes the editor submit pipeline.
1729
- // Surface the failure via `ctx.ui.notify` so the user sees it.
1951
+ if (commandCtx.hasUI === false) {
1952
+ throw err;
1953
+ }
1954
+ // Match the host command runner for interactive contexts: swallow
1955
+ // handler exceptions so a throw never bubbles out and crashes the
1956
+ // editor submit pipeline. Surface the failure via `ctx.ui.notify` so
1957
+ // the user sees it. Headless contexts rethrow above because notify is
1958
+ // a no-op in print mode and would otherwise hide command failures.
1730
1959
  const message = err instanceof Error ? err.message : String(err);
1731
1960
  commandCtx.ui.notify(`/${name} failed: ${message}`, "error");
1732
1961
  }
@@ -1805,33 +2034,45 @@ function resolveToolRunTarget(
1805
2034
  }
1806
2035
 
1807
2036
  type ToolStageTarget =
1808
- | { ok: true; stageId?: string }
2037
+ | { ok: true; runId?: string; stageId?: string }
1809
2038
  | { ok: false; message: string };
1810
2039
 
1811
- function stageMatchesIdentifier(stage: { readonly id: string; readonly name: string }, target: string): boolean {
1812
- return stage.id === target || stage.name === target || stage.id.startsWith(target);
1813
- }
1814
-
1815
- function stageMatchLabel(stage: { readonly id: string; readonly name: string }): string {
1816
- return `${stage.name} (${stage.id.slice(0, 12)})`;
1817
- }
1818
-
1819
2040
  function resolveStageTarget(runId: string, stageTarget?: string): ToolStageTarget {
1820
2041
  const target = stageTarget?.trim();
1821
- if (!target) return { ok: true };
2042
+ if (!target) return { ok: true, runId };
1822
2043
 
1823
- const run = store.runs().find((r) => r.id === runId);
1824
- const exactId = run?.stages.find((stage) => stage.id === target);
1825
- if (exactId !== undefined) return { ok: true, stageId: exactId.id };
2044
+ const graph = expandWorkflowGraph(store.snapshot(), runId);
2045
+ const exactId = graph.stages.find(
2046
+ (stage) => stage.id === target || stage.workflowGraphTarget.stageId === target,
2047
+ );
2048
+ if (exactId !== undefined) {
2049
+ return {
2050
+ ok: true,
2051
+ runId: exactId.workflowGraphTarget.runId,
2052
+ stageId: exactId.workflowGraphTarget.stageId,
2053
+ };
2054
+ }
1826
2055
 
1827
- const exactNames = run?.stages.filter((stage) => stage.name === target) ?? [];
1828
- if (exactNames.length === 1) return { ok: true, stageId: exactNames[0]!.id };
1829
- if (exactNames.length > 1) return { ok: false, message: `Ambiguous stage identifier "${target}" matches: ${exactNames.map(stageMatchLabel).join(", ")}` };
2056
+ const exactNames = graph.stages.filter((stage) => stage.name === target);
2057
+ if (exactNames.length === 1) {
2058
+ const stage = exactNames[0]!;
2059
+ return {
2060
+ ok: true,
2061
+ runId: stage.workflowGraphTarget.runId,
2062
+ stageId: stage.workflowGraphTarget.stageId,
2063
+ };
2064
+ }
2065
+ if (exactNames.length > 1) return { ok: false, message: `Ambiguous stage identifier "${target}" matches: ${exactNames.map(expandedStageLabel).join(", ")}` };
1830
2066
 
1831
- const matches = run?.stages.filter((stage) => stageMatchesIdentifier(stage, target)) ?? [];
2067
+ const matches = graph.stages.filter((stage) => stageMatchesExpandedIdentifier(stage, target));
1832
2068
  if (matches.length === 0) return { ok: false, message: `Stage not found in run ${runId.slice(0, 8)}: ${target}` };
1833
- if (matches.length > 1) return { ok: false, message: `Ambiguous stage identifier "${target}" matches: ${matches.map(stageMatchLabel).join(", ")}` };
1834
- return { ok: true, stageId: matches[0]!.id };
2069
+ if (matches.length > 1) return { ok: false, message: `Ambiguous stage identifier "${target}" matches: ${matches.map(expandedStageLabel).join(", ")}` };
2070
+ const stage = matches[0]!;
2071
+ return {
2072
+ ok: true,
2073
+ runId: stage.workflowGraphTarget.runId,
2074
+ stageId: stage.workflowGraphTarget.stageId,
2075
+ };
1835
2076
  }
1836
2077
 
1837
2078
  function resolveToolStageTarget(runId: string, stageTarget?: string): ToolStageTarget {
@@ -1920,8 +2161,8 @@ export function tokenizeWorkflowArgs(args: string): string[] {
1920
2161
  * Tokens that are standalone valid JSON objects/arrays are merged in.
1921
2162
  * All other tokens are ignored (non-kv positional args not supported).
1922
2163
  */
1923
- export function parseWorkflowArgs(tokens: string[]): Record<string, unknown> {
1924
- const result: Record<string, unknown> = {};
2164
+ export function parseWorkflowArgs(tokens: string[]): WorkflowInputValues {
2165
+ const result: Record<string, WorkflowSerializableValue> = {};
1925
2166
  for (const token of tokens) {
1926
2167
  // Try JSON object/array merge
1927
2168
  if (
@@ -1935,7 +2176,7 @@ export function parseWorkflowArgs(tokens: string[]): Record<string, unknown> {
1935
2176
  typeof parsed === "object" &&
1936
2177
  !Array.isArray(parsed)
1937
2178
  ) {
1938
- Object.assign(result, parsed as Record<string, unknown>);
2179
+ Object.assign(result, parsed as WorkflowInputValues);
1939
2180
  }
1940
2181
  continue;
1941
2182
  } catch {
@@ -1948,9 +2189,9 @@ export function parseWorkflowArgs(tokens: string[]): Record<string, unknown> {
1948
2189
  const key = token.slice(0, eqIdx);
1949
2190
  const raw = token.slice(eqIdx + 1);
1950
2191
  // Try to parse value as JSON for typed values (numbers, booleans, objects)
1951
- let value: unknown = raw;
2192
+ let value: WorkflowSerializableValue = raw;
1952
2193
  try {
1953
- value = JSON.parse(raw) as unknown;
2194
+ value = JSON.parse(raw) as WorkflowSerializableValue;
1954
2195
  } catch {
1955
2196
  // keep as string
1956
2197
  }
@@ -2101,16 +2342,29 @@ function factory(pi: ExtensionAPI): void {
2101
2342
  );
2102
2343
  let lifecycleNotificationsUnsubscribe: (() => void) | null = null;
2103
2344
  let lifecycleNotificationsActive = false;
2345
+ let hilAnswerNotificationsUnsubscribe: (() => void) | null = null;
2346
+ let hilAnswerNotificationsActive = false;
2104
2347
  const lifecycleNotificationState = createWorkflowLifecycleNotificationState();
2348
+ const hilAnswerNotificationState = createWorkflowHilAnswerNotificationState();
2105
2349
  const lifecycleNotificationConfigRef: { current: WorkflowLifecycleNotificationConfig } = {
2106
2350
  current: WORKFLOW_CONFIG_DEFAULTS.workflowNotifications,
2107
2351
  };
2352
+ const registerMessageRenderer: ExtensionAPI["registerMessageRenderer"] | undefined =
2353
+ typeof pi.registerMessageRenderer === "function"
2354
+ ? (event, renderer) => pi.registerMessageRenderer!(event, renderer)
2355
+ : undefined;
2108
2356
  registerLifecycleNoticeRenderer({
2109
2357
  rendererHost: pi,
2110
- registerMessageRenderer: pi.registerMessageRenderer
2111
- ? (event, renderer) => pi.registerMessageRenderer?.(event, renderer)
2112
- : undefined,
2358
+ registerMessageRenderer,
2113
2359
  });
2360
+ registerHilAnswerNoticeRenderer({
2361
+ rendererHost: pi,
2362
+ registerMessageRenderer,
2363
+ });
2364
+ const sendWorkflowNotificationMessage: ExtensionAPI["sendMessage"] | undefined =
2365
+ typeof pi.sendMessage === "function"
2366
+ ? (message, options) => pi.sendMessage!(message, options)
2367
+ : undefined;
2114
2368
  const reinstallLifecycleNotifications = (): void => {
2115
2369
  lifecycleNotificationsUnsubscribe?.();
2116
2370
  lifecycleNotificationsUnsubscribe = null;
@@ -2120,11 +2374,33 @@ function factory(pi: ExtensionAPI): void {
2120
2374
  config: lifecycleNotificationConfigRef.current,
2121
2375
  state: lifecycleNotificationState,
2122
2376
  seedExisting: true,
2123
- sendMessage: pi.sendMessage
2124
- ? (message, options) => pi.sendMessage?.(message, options)
2125
- : undefined,
2377
+ sendMessage: sendWorkflowNotificationMessage,
2126
2378
  });
2127
2379
  };
2380
+ const reinstallHilAnswerNotifications = (): void => {
2381
+ hilAnswerNotificationsUnsubscribe?.();
2382
+ hilAnswerNotificationsUnsubscribe = null;
2383
+ if (!hilAnswerNotificationsActive) return;
2384
+ hilAnswerNotificationsUnsubscribe = installWorkflowHilAnswerNotifications({
2385
+ store,
2386
+ stageUiBroker,
2387
+ state: hilAnswerNotificationState,
2388
+ sendMessage: sendWorkflowNotificationMessage,
2389
+ });
2390
+ };
2391
+
2392
+ async function runWithLifecycleSuppressedForPolicy<T>(
2393
+ policy: WorkflowExecutionPolicy,
2394
+ fn: () => Promise<T>,
2395
+ ): Promise<T> {
2396
+ if (policy.mode !== "non_interactive" || policy.awaitTerminalRun !== true) {
2397
+ return fn();
2398
+ }
2399
+ return withWorkflowLifecycleNotificationsSuppressedAsync(
2400
+ lifecycleNotificationState,
2401
+ fn,
2402
+ );
2403
+ }
2128
2404
  let intercomParentSession: string | null = null;
2129
2405
  const intercomPort = {
2130
2406
  emit:
@@ -2135,9 +2411,11 @@ function factory(pi: ExtensionAPI): void {
2135
2411
  parentSession: () => intercomParentSession ?? undefined,
2136
2412
  };
2137
2413
 
2414
+ const startupDiscovery = discoverStartupWorkflowsSync();
2138
2415
  const runtimeRef: { current: ExtensionRuntime } = {
2139
2416
  current: createExtensionRuntime({
2140
- registry: discoverStartupWorkflowsSync().registry,
2417
+ registry: startupDiscovery.registry,
2418
+ cwd: process.cwd(),
2141
2419
  adapters,
2142
2420
  cancellation: cancellationRegistry,
2143
2421
  persistence: persistenceRef.current,
@@ -2154,14 +2432,14 @@ function factory(pi: ExtensionAPI): void {
2154
2432
  get registry() {
2155
2433
  return runtimeRef.current.registry;
2156
2434
  },
2157
- dispatch(args) {
2158
- return runtimeRef.current.dispatch(args);
2435
+ dispatch(args, options) {
2436
+ return runtimeRef.current.dispatch(args, options);
2159
2437
  },
2160
- runDirect(args) {
2161
- return runtimeRef.current.runDirect(args);
2438
+ runDirect(args, options) {
2439
+ return runtimeRef.current.runDirect(args, options);
2162
2440
  },
2163
- resumeFailedRun(sourceRunId, stageId) {
2164
- return runtimeRef.current.resumeFailedRun(sourceRunId, stageId);
2441
+ resumeFailedRun(sourceRunId, stageId, options) {
2442
+ return runtimeRef.current.resumeFailedRun(sourceRunId, stageId, options);
2165
2443
  },
2166
2444
  };
2167
2445
 
@@ -2203,6 +2481,7 @@ function factory(pi: ExtensionAPI): void {
2203
2481
  if (models === undefined) return runtimeProxy;
2204
2482
  return createExtensionRuntime({
2205
2483
  registry: runtimeRef.current.registry,
2484
+ cwd: process.cwd(),
2206
2485
  adapters,
2207
2486
  cancellation: cancellationRegistry,
2208
2487
  persistence: persistenceRef.current,
@@ -2230,6 +2509,16 @@ function factory(pi: ExtensionAPI): void {
2230
2509
  await reload;
2231
2510
  }
2232
2511
 
2512
+ async function loadPackageWorkflowPaths(): Promise<string[]> {
2513
+ const packageResources =
2514
+ (await pi.refreshWorkflowResources?.()) ??
2515
+ pi.getWorkflowResources?.() ??
2516
+ [];
2517
+ return packageResources
2518
+ .filter((resource) => resource.enabled !== false)
2519
+ .map((resource) => resource.path);
2520
+ }
2521
+
2233
2522
  async function reloadWorkflowResourcesNow(options?: { allowInFlight?: boolean }): Promise<void> {
2234
2523
  const activeRuns = inFlightRunCount();
2235
2524
  if (options?.allowInFlight !== true) {
@@ -2260,9 +2549,7 @@ function factory(pi: ExtensionAPI): void {
2260
2549
  )
2261
2550
  : undefined;
2262
2551
 
2263
- const packageWorkflowPaths = (pi.getWorkflowResources?.() ?? [])
2264
- .filter((resource) => resource.enabled !== false)
2265
- .map((resource) => resource.path);
2552
+ const packageWorkflowPaths = await loadPackageWorkflowPaths();
2266
2553
  const result = await discoverWorkflows({ config: discoveryConfig, packageWorkflowPaths });
2267
2554
  discoveryRef.current = result;
2268
2555
 
@@ -2289,6 +2576,7 @@ function factory(pi: ExtensionAPI): void {
2289
2576
  );
2290
2577
  runtimeRef.current = createExtensionRuntime({
2291
2578
  registry: result.registry,
2579
+ cwd: process.cwd(),
2292
2580
  adapters,
2293
2581
  cancellation: cancellationRegistry,
2294
2582
  persistence: persistenceRef.current,
@@ -2325,13 +2613,19 @@ function factory(pi: ExtensionAPI): void {
2325
2613
  pi.registerTool<WorkflowToolArgs, WorkflowToolResult>({
2326
2614
  name: "workflow",
2327
2615
  label: "workflow",
2328
- description: "Run a defined multi-stage workflow by name.",
2616
+ description: WORKFLOW_TOOL_DESCRIPTION,
2329
2617
  parameters: workflowParameters,
2330
2618
  renderShell: "self",
2331
2619
  execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
2332
2620
  // Overlay is opt-in via F2 / ctrl+h; do not auto-open from a
2333
- // tool-call dispatch path.
2334
- const details = await executeWorkflowTool(params, ctx);
2621
+ // tool-call dispatch path. Awaited non-interactive runs suppress
2622
+ // lifecycle steer notices until the terminal tool result is ready.
2623
+ const policy = workflowPolicyFromContext(ctx);
2624
+ const details = (params.action ?? "run") === "run"
2625
+ ? await runWithLifecycleSuppressedForPolicy(policy, () =>
2626
+ executeWorkflowTool(params, ctx),
2627
+ )
2628
+ : await executeWorkflowTool(params, ctx);
2335
2629
  return {
2336
2630
  content: [{ type: "text", text: renderWorkflowToolContent(details, params) }],
2337
2631
  details,
@@ -2376,18 +2670,41 @@ function factory(pi: ExtensionAPI): void {
2376
2670
  action: "connect" | "interrupt" | "kill" | "attach" | "pause" | "resume",
2377
2671
  rest: string[],
2378
2672
  ctx: PiCommandContext,
2673
+ reporter: WorkflowCommandReporter = createWorkflowCommandReporter(ctx),
2379
2674
  ): Promise<boolean> {
2380
- const print = (msg: string): void => ctx.ui.notify(msg, "info");
2675
+ const policy = workflowPolicyFromContext(ctx);
2676
+ const print = (msg: string): void => reporter.info(msg);
2677
+ const fail = (msg: string): void => reporter.error(msg);
2678
+ const canOpenPicker = (ui: PiCommandContext["ui"] | undefined): boolean =>
2679
+ policy.allowInputPicker && typeof ui?.custom === "function";
2680
+ const confirmationPrompt = policy.allowHumanInput && typeof ctx.ui?.confirm === "function"
2681
+ ? ctx.ui.confirm.bind(ctx.ui)
2682
+ : undefined;
2381
2683
  const theme = deriveGraphTheme({});
2684
+ const failHeadlessAttachCommand = (
2685
+ targetAction: "connect" | "attach",
2686
+ runId: string,
2687
+ stageId?: string,
2688
+ ): boolean => {
2689
+ if (policy.allowInputPicker) return false;
2690
+ const displayTarget = stageId
2691
+ ? `${runId.slice(0, 8)} stage ${stageId.slice(0, 8)}`
2692
+ : runId.slice(0, 8);
2693
+ fail(
2694
+ `/workflow ${targetAction} requires an interactive UI surface and cannot attach in non-interactive mode. ` +
2695
+ `Target: ${displayTarget}. Use /workflow status ${runId.slice(0, 8)} or the workflow tool's status/stages/transcript actions for non-interactive inspection.`,
2696
+ );
2697
+ return true;
2698
+ };
2382
2699
 
2383
2700
  if (action === "connect") {
2384
2701
  const target = rest.find((t) => !t.startsWith("--"));
2385
2702
  if (!target) {
2386
2703
  // Picker mode — mount the overlay and route the resolved action.
2387
2704
  const ui = ctx.ui;
2388
- if (!ui || typeof ui.custom !== "function") {
2389
- print(
2390
- `${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires a UI surface. Pass a runId: /workflow connect <id>`,
2705
+ if (!canOpenPicker(ui)) {
2706
+ fail(
2707
+ `${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires an interactive UI surface. Pass a runId: /workflow connect <id>`,
2391
2708
  );
2392
2709
  return true;
2393
2710
  }
@@ -2400,7 +2717,7 @@ function factory(pi: ExtensionAPI): void {
2400
2717
  if (result.kind === "kill") {
2401
2718
  const run = store.runs().find((r) => r.id === result.runId);
2402
2719
  if (!run) {
2403
- print(`Run not found: ${result.runId}`);
2720
+ fail(`Run not found: ${result.runId}`);
2404
2721
  return true;
2405
2722
  }
2406
2723
  if (run.endedAt !== undefined) {
@@ -2424,34 +2741,37 @@ function factory(pi: ExtensionAPI): void {
2424
2741
  run,
2425
2742
  previousStatus: killed.previousStatus,
2426
2743
  });
2744
+ print(`Run ${killed.runId.slice(0, 8)} killed and retained for inspection.`);
2745
+ } else if (killed.reason === "already_ended") {
2746
+ print(formatAlreadyEndedRetainedMessage(killed.runId));
2747
+ } else {
2748
+ fail(`Run not found: ${result.runId.slice(0, 8)}.`);
2427
2749
  }
2428
- print(
2429
- killed.ok
2430
- ? `Run ${killed.runId.slice(0, 8)} killed and retained for inspection.`
2431
- : killed.reason === "already_ended"
2432
- ? formatAlreadyEndedRetainedMessage(killed.runId)
2433
- : `Run not found: ${result.runId.slice(0, 8)}.`,
2434
- );
2435
2750
  return true;
2436
2751
  }
2437
2752
  return true;
2438
2753
  }
2439
2754
  const resolved = resolveRunIdPrefix(target);
2440
2755
  if (resolved.kind === "not_found") {
2441
- print(
2756
+ fail(
2442
2757
  `Run not found: ${target}\n\n${renderSessionList(store.runs(), { theme, includeAll: true })}`,
2443
2758
  );
2444
2759
  return true;
2445
2760
  }
2446
2761
  if (resolved.kind === "ambiguous") {
2447
- print(
2762
+ fail(
2448
2763
  `Ambiguous run prefix "${target}" matches: ${resolved.matches
2449
2764
  .map((id) => id.slice(0, 12))
2450
2765
  .join(", ")}`,
2451
2766
  );
2452
2767
  return true;
2453
2768
  }
2454
- overlay.open(resolved.runId, overlaySurfaceFromContext(ctx));
2769
+ if (failHeadlessAttachCommand("connect", resolved.runId)) {
2770
+ return true;
2771
+ }
2772
+ if (policy.allowInputPicker) {
2773
+ overlay.open(resolved.runId, overlaySurfaceFromContext(ctx));
2774
+ }
2455
2775
  print(
2456
2776
  `Attached to ${resolved.runId.slice(0, 8)}. h/ctrl+d hide · q kill · esc close.`,
2457
2777
  );
@@ -2465,18 +2785,18 @@ function factory(pi: ExtensionAPI): void {
2465
2785
  if (!target && !wantsAll) {
2466
2786
  target = store.activeRunId() ?? undefined;
2467
2787
  if (!target) {
2468
- print("No in-flight runs to interrupt.");
2788
+ fail("No in-flight runs to interrupt.");
2469
2789
  return true;
2470
2790
  }
2471
2791
  }
2472
2792
  if (wantsAll) {
2473
- const inFlight = store.runs().filter((r) => r.endedAt === undefined);
2793
+ const inFlight = topLevelWorkflowRuns(store.runs()).filter((r) => r.endedAt === undefined);
2474
2794
  if (inFlight.length === 0) {
2475
- print("No in-flight runs to interrupt.");
2795
+ fail("No in-flight runs to interrupt.");
2476
2796
  return true;
2477
2797
  }
2478
- if (!yes && ctx.ui && typeof ctx.ui.confirm === "function") {
2479
- const ok = await ctx.ui.confirm(
2798
+ if (!yes && confirmationPrompt) {
2799
+ const ok = await confirmationPrompt(
2480
2800
  `Interrupt all ${inFlight.length} in-flight workflow runs?`,
2481
2801
  `Pauses: ${inFlight.map((r) => `${r.name} (${r.id.slice(0, 8)})`).join(", ")}`,
2482
2802
  );
@@ -2487,20 +2807,20 @@ function factory(pi: ExtensionAPI): void {
2487
2807
  }
2488
2808
  const results = interruptAllRuns();
2489
2809
  const interrupted = results.filter((r) => r.ok).length;
2490
- print(
2491
- interrupted > 0
2492
- ? `Interrupted ${interrupted} run(s).`
2493
- : "No in-flight runs to interrupt.",
2494
- );
2810
+ if (interrupted > 0) {
2811
+ print(`Interrupted ${interrupted} run(s).`);
2812
+ } else {
2813
+ fail("No in-flight runs to interrupt.");
2814
+ }
2495
2815
  return true;
2496
2816
  }
2497
2817
  const resolved = resolveRunIdPrefix(target!);
2498
2818
  if (resolved.kind === "not_found") {
2499
- print(`Run not found: ${target}`);
2819
+ fail(`Run not found: ${target}`);
2500
2820
  return true;
2501
2821
  }
2502
2822
  if (resolved.kind === "ambiguous") {
2503
- print(
2823
+ fail(
2504
2824
  `Ambiguous run prefix "${target}" matches multiple runs: ${resolved.matches
2505
2825
  .map((id) => id.slice(0, 12))
2506
2826
  .join(", ")}`,
@@ -2508,8 +2828,8 @@ function factory(pi: ExtensionAPI): void {
2508
2828
  return true;
2509
2829
  }
2510
2830
  const run = store.runs().find((r) => r.id === resolved.runId);
2511
- if (!yes && run && run.endedAt === undefined && typeof ctx.ui.confirm === "function") {
2512
- const confirmed = await ctx.ui.confirm(
2831
+ if (!yes && run && run.endedAt === undefined && confirmationPrompt) {
2832
+ const confirmed = await confirmationPrompt(
2513
2833
  `Interrupt workflow run ${run.name} (${run.id.slice(0, 8)})?`,
2514
2834
  "Pauses live work so it can be resumed later.",
2515
2835
  );
@@ -2526,7 +2846,7 @@ function factory(pi: ExtensionAPI): void {
2526
2846
  `Run ${result.runId.slice(0, 8)} interrupted and can be resumed.`,
2527
2847
  );
2528
2848
  } else {
2529
- print(
2849
+ fail(
2530
2850
  result.reason === "not_found"
2531
2851
  ? `Run not found: ${target}`
2532
2852
  : result.reason === "already_ended"
@@ -2546,18 +2866,18 @@ function factory(pi: ExtensionAPI): void {
2546
2866
  if (!target && !wantsAll) {
2547
2867
  target = store.activeRunId() ?? undefined;
2548
2868
  if (!target) {
2549
- print("No in-flight runs to kill.");
2869
+ fail("No in-flight runs to kill.");
2550
2870
  return true;
2551
2871
  }
2552
2872
  }
2553
2873
  if (wantsAll) {
2554
- const inFlight = store.runs().filter((r) => r.endedAt === undefined);
2874
+ const inFlight = topLevelWorkflowRuns(store.runs()).filter((r) => r.endedAt === undefined);
2555
2875
  if (inFlight.length === 0) {
2556
- print("No in-flight runs to kill.");
2876
+ fail("No in-flight runs to kill.");
2557
2877
  return true;
2558
2878
  }
2559
- if (!yes && ctx.ui && typeof ctx.ui.confirm === "function") {
2560
- const ok = await ctx.ui.confirm(
2879
+ if (!yes && confirmationPrompt) {
2880
+ const ok = await confirmationPrompt(
2561
2881
  `Kill ${inFlight.length} in-flight workflow runs? Killed runs are retained for inspection.`,
2562
2882
  `Aborts: ${inFlight.map((r) => `${r.name} (${r.id.slice(0, 8)})`).join(", ")}`,
2563
2883
  );
@@ -2571,20 +2891,20 @@ function factory(pi: ExtensionAPI): void {
2571
2891
  persistence: persistenceRef.current,
2572
2892
  });
2573
2893
  const killed = results.filter((r) => r.ok).length;
2574
- print(
2575
- killed > 0
2576
- ? `Killed and retained ${killed} run(s) for inspection.`
2577
- : "No in-flight runs to kill.",
2578
- );
2894
+ if (killed > 0) {
2895
+ print(`Killed and retained ${killed} run(s) for inspection.`);
2896
+ } else {
2897
+ fail("No in-flight runs to kill.");
2898
+ }
2579
2899
  return true;
2580
2900
  }
2581
2901
  const resolved = resolveRunIdPrefix(target!);
2582
2902
  if (resolved.kind === "not_found") {
2583
- print(`Run not found: ${target}`);
2903
+ fail(`Run not found: ${target}`);
2584
2904
  return true;
2585
2905
  }
2586
2906
  if (resolved.kind === "ambiguous") {
2587
- print(
2907
+ fail(
2588
2908
  `Ambiguous run prefix "${target}" matches multiple runs: ${resolved.matches
2589
2909
  .map((id) => id.slice(0, 12))
2590
2910
  .join(", ")}`,
@@ -2596,7 +2916,7 @@ function factory(pi: ExtensionAPI): void {
2596
2916
  print(formatAlreadyEndedRetainedMessage(resolved.runId));
2597
2917
  return true;
2598
2918
  }
2599
- if (!yes && run && ctx.ui) {
2919
+ if (!yes && run && confirmationPrompt) {
2600
2920
  const confirmed = await openKillConfirm(ctx.ui, run, theme);
2601
2921
  if (!confirmed) {
2602
2922
  print(
@@ -2620,12 +2940,10 @@ function factory(pi: ExtensionAPI): void {
2620
2940
  print(
2621
2941
  `Run ${result.runId.slice(0, 8)} killed and retained for inspection (was ${result.previousStatus}).`,
2622
2942
  );
2943
+ } else if (result.reason === "already_ended") {
2944
+ print(formatAlreadyEndedRetainedMessage(result.runId));
2623
2945
  } else {
2624
- print(
2625
- result.reason === "already_ended"
2626
- ? formatAlreadyEndedRetainedMessage(result.runId)
2627
- : `Run not found: ${target}`,
2628
- );
2946
+ fail(`Run not found: ${target}`);
2629
2947
  }
2630
2948
  return true;
2631
2949
  }
@@ -2636,9 +2954,9 @@ function factory(pi: ExtensionAPI): void {
2636
2954
  let runId: string;
2637
2955
  if (!target) {
2638
2956
  const ui = ctx.ui;
2639
- if (!ui || typeof ui.custom !== "function") {
2640
- print(
2641
- `${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires a UI surface. Pass a runId: /workflow attach <id> [stageId]`,
2957
+ if (!canOpenPicker(ui)) {
2958
+ fail(
2959
+ `${renderSessionList(store.runs(), { theme, includeAll: true })}\n\nPicker requires an interactive UI surface. Pass a runId: /workflow attach <id> [stageId]`,
2642
2960
  );
2643
2961
  return true;
2644
2962
  }
@@ -2652,6 +2970,7 @@ function factory(pi: ExtensionAPI): void {
2652
2970
  "kill",
2653
2971
  [picked.runId, "-y"],
2654
2972
  ctx,
2973
+ reporter,
2655
2974
  );
2656
2975
  }
2657
2976
  return true;
@@ -2660,11 +2979,11 @@ function factory(pi: ExtensionAPI): void {
2660
2979
  } else {
2661
2980
  const resolved = resolveRunIdPrefix(target);
2662
2981
  if (resolved.kind === "not_found") {
2663
- print(`Run not found: ${target}`);
2982
+ fail(`Run not found: ${target}`);
2664
2983
  return true;
2665
2984
  }
2666
2985
  if (resolved.kind === "ambiguous") {
2667
- print(
2986
+ fail(
2668
2987
  `Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`,
2669
2988
  );
2670
2989
  return true;
@@ -2679,12 +2998,17 @@ function factory(pi: ExtensionAPI): void {
2679
2998
  exact ?? run.stages.find((s) => s.id.startsWith(stageTarget));
2680
2999
  const byName = prefix ?? run.stages.find((s) => s.name === stageTarget);
2681
3000
  if (!byName) {
2682
- print(`Stage not found in run ${runId.slice(0, 8)}: ${stageTarget}`);
3001
+ fail(`Stage not found in run ${runId.slice(0, 8)}: ${stageTarget}`);
2683
3002
  return true;
2684
3003
  }
2685
3004
  stageId = byName.id;
2686
3005
  }
2687
- overlay.open(runId, overlaySurfaceFromContext(ctx), stageId);
3006
+ if (failHeadlessAttachCommand("attach", runId, stageId)) {
3007
+ return true;
3008
+ }
3009
+ if (policy.allowInputPicker) {
3010
+ overlay.open(runId, overlaySurfaceFromContext(ctx), stageId);
3011
+ }
2688
3012
  const attachedStage = stageId ? run?.stages.find((s) => s.id === stageId) : undefined;
2689
3013
  print(
2690
3014
  stageId
@@ -2702,14 +3026,14 @@ function factory(pi: ExtensionAPI): void {
2702
3026
  let runId: string;
2703
3027
  if (!target) {
2704
3028
  const ui = ctx.ui;
2705
- if (!ui || typeof ui.custom !== "function") {
2706
- const active = store.runs().filter((r) => r.endedAt === undefined);
3029
+ if (!canOpenPicker(ui)) {
3030
+ const active = topLevelWorkflowRuns(store.runs()).filter((r) => r.endedAt === undefined);
2707
3031
  if (active.length === 0) {
2708
- print("No active runs to pause.");
3032
+ fail("No active runs to pause.");
2709
3033
  return true;
2710
3034
  }
2711
- print(
2712
- `Picker requires a UI surface. Active runs:\n${active.map((r) => ` ${r.id.slice(0, 8)} ${r.name}`).join("\n")}\n\nUsage: /workflow pause <runId> [stageId]`,
3035
+ fail(
3036
+ `Picker requires an interactive UI surface. Active runs:\n${active.map((r) => ` ${r.id.slice(0, 8)} ${r.name}`).join("\n")}\n\nUsage: /workflow pause <runId> [stageId]`,
2713
3037
  );
2714
3038
  return true;
2715
3039
  }
@@ -2719,11 +3043,11 @@ function factory(pi: ExtensionAPI): void {
2719
3043
  } else {
2720
3044
  const resolved = resolveRunIdPrefix(target);
2721
3045
  if (resolved.kind === "not_found") {
2722
- print(`Run not found: ${target}`);
3046
+ fail(`Run not found: ${target}`);
2723
3047
  return true;
2724
3048
  }
2725
3049
  if (resolved.kind === "ambiguous") {
2726
- print(
3050
+ fail(
2727
3051
  `Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`,
2728
3052
  );
2729
3053
  return true;
@@ -2731,31 +3055,27 @@ function factory(pi: ExtensionAPI): void {
2731
3055
  runId = resolved.runId;
2732
3056
  }
2733
3057
  let stageId: string | undefined;
3058
+ let stageRunId = runId;
2734
3059
  if (stageTarget) {
2735
- const run = store.runs().find((r) => r.id === runId);
2736
- const stage = run?.stages.find(
2737
- (s) =>
2738
- s.id === stageTarget ||
2739
- s.id.startsWith(stageTarget) ||
2740
- s.name === stageTarget,
2741
- );
2742
- if (!stage) {
2743
- print(`Stage not found in run ${runId.slice(0, 8)}: ${stageTarget}`);
3060
+ const resolvedStage = resolveStageTarget(runId, stageTarget);
3061
+ if (!resolvedStage.ok) {
3062
+ fail(resolvedStage.message);
2744
3063
  return true;
2745
3064
  }
2746
- stageId = stage.id;
3065
+ stageId = resolvedStage.stageId;
3066
+ stageRunId = resolvedStage.runId ?? runId;
2747
3067
  }
2748
- const result = pauseRun(runId, { stageId });
3068
+ const result = pauseRun(stageRunId, { stageId });
2749
3069
  if (!result.ok) {
2750
3070
  const why =
2751
3071
  result.reason === "not_found"
2752
- ? `Run not found: ${runId.slice(0, 8)}`
3072
+ ? `Run not found: ${stageRunId.slice(0, 8)}`
2753
3073
  : result.reason === "already_ended"
2754
- ? `Run ${runId.slice(0, 8)} already ended.`
3074
+ ? `Run ${stageRunId.slice(0, 8)} already ended.`
2755
3075
  : result.reason === "no_active_stages"
2756
- ? `No pausable stages on run ${runId.slice(0, 8)}.`
3076
+ ? `No pausable stages on run ${stageRunId.slice(0, 8)}.`
2757
3077
  : `Stage not found: ${stageTarget ?? "(unknown)"}`;
2758
- print(why);
3078
+ fail(why);
2759
3079
  return true;
2760
3080
  }
2761
3081
  // Open the orchestrator overlay (graph for run-level pause, stage
@@ -2763,13 +3083,13 @@ function factory(pi: ExtensionAPI): void {
2763
3083
  // the full-screen overlay hides Pi's "Working… (esc to interrupt)"
2764
3084
  // spinner, which otherwise stays visible because the host session
2765
3085
  // is still streaming whatever was happening before the pause hit.
2766
- if (typeof ctx.ui?.custom === "function") {
3086
+ if (policy.allowInputPicker) {
2767
3087
  overlay.open(runId, overlaySurfaceFromContext(ctx), stageId);
2768
3088
  }
2769
3089
  print(
2770
3090
  result.paused.length === 0
2771
- ? `No stages were paused on run ${runId.slice(0, 8)}.`
2772
- : `Paused ${result.paused.length} stage(s) on run ${runId.slice(0, 8)}: ${result.paused.map((s) => s.name).join(", ")}`,
3091
+ ? `No stages were paused on run ${stageRunId.slice(0, 8)}.`
3092
+ : `Paused ${result.paused.length} stage(s) on run ${stageRunId.slice(0, 8)}: ${result.paused.map((s) => s.name).join(", ")}`,
2773
3093
  );
2774
3094
  return true;
2775
3095
  }
@@ -2781,8 +3101,8 @@ function factory(pi: ExtensionAPI): void {
2781
3101
  let runId: string;
2782
3102
  if (!target) {
2783
3103
  const ui = ctx.ui;
2784
- if (!ui || typeof ui.custom !== "function") {
2785
- print(`Usage: /workflow resume <runId> [stageId] [message…]`);
3104
+ if (!canOpenPicker(ui)) {
3105
+ fail(`Usage: /workflow resume <runId> [stageId] [message…]`);
2786
3106
  return true;
2787
3107
  }
2788
3108
  const picked = await openSessionPicker(ui, store, theme, "resume");
@@ -2791,11 +3111,11 @@ function factory(pi: ExtensionAPI): void {
2791
3111
  } else {
2792
3112
  const resolved = resolveRunIdPrefix(target);
2793
3113
  if (resolved.kind === "not_found") {
2794
- print(`Run not found: ${target}`);
3114
+ fail(`Run not found: ${target}`);
2795
3115
  return true;
2796
3116
  }
2797
3117
  if (resolved.kind === "ambiguous") {
2798
- print(
3118
+ fail(
2799
3119
  `Ambiguous run prefix "${target}" matches: ${resolved.matches.map((id) => id.slice(0, 12)).join(", ")}`,
2800
3120
  );
2801
3121
  return true;
@@ -2803,29 +3123,36 @@ function factory(pi: ExtensionAPI): void {
2803
3123
  runId = resolved.runId;
2804
3124
  }
2805
3125
  let stageId: string | undefined;
2806
- const run = store.runs().find((r) => r.id === runId);
2807
3126
  const resolvedStage = resolveStageTarget(runId, stageTarget);
2808
3127
  if (!resolvedStage.ok) {
2809
- print(resolvedStage.message);
3128
+ fail(resolvedStage.message);
2810
3129
  return true;
2811
3130
  }
2812
3131
  stageId = resolvedStage.stageId;
3132
+ const stageRunId = resolvedStage.runId ?? runId;
3133
+ const run = store.runs().find((r) => r.id === stageRunId);
2813
3134
  const isPaused =
2814
3135
  run?.status === "paused" ||
2815
3136
  (run?.stages.some((s) => s.status === "paused") ?? false);
2816
3137
  if (!isPaused && run?.status === "failed" && run.endedAt !== undefined && run.resumable !== false) {
2817
- const continuation = runtimeForContext(ctx).resumeFailedRun(runId, stageId);
2818
- print(continuation.message);
3138
+ const continuation = runtimeForContext(ctx).resumeFailedRun(stageRunId, stageId, { policy });
3139
+ if (continuation.ok) {
3140
+ print(continuation.message);
3141
+ } else {
3142
+ fail(continuation.message);
3143
+ }
2819
3144
  return true;
2820
3145
  }
2821
- const result = resumeRun(runId, { stageId, message });
3146
+ const result = resumeRun(stageRunId, { stageId, message });
2822
3147
  if (!result.ok) {
2823
- print(`Run not found: ${runId.slice(0, 8)}`);
3148
+ fail(`Run not found: ${stageRunId.slice(0, 8)}`);
2824
3149
  return true;
2825
3150
  }
2826
3151
  if (!isPaused) {
2827
- // Non-paused fallback: reopen the orchestrator overlay as before.
2828
- overlay.open(result.runId, overlaySurfaceFromContext(ctx));
3152
+ // Non-paused fallback: reopen the orchestrator overlay as before when interactive.
3153
+ if (policy.allowInputPicker) {
3154
+ overlay.open(result.runId, overlaySurfaceFromContext(ctx));
3155
+ }
2829
3156
  print(
2830
3157
  result.message ?? `Snapshot available: run ${result.runId} (${result.snapshot.name}) \u2014 status: ${result.snapshot.status}, stages: ${result.snapshot.stages.length}`,
2831
3158
  );
@@ -2834,14 +3161,14 @@ function factory(pi: ExtensionAPI): void {
2834
3161
  // Paused live resume: when no message was provided and the picker
2835
3162
  // is available, open the attached chat so the user can talk to
2836
3163
  // the freshly-resumed stage.
2837
- if (!message && stageId && ctx.ui?.custom) {
3164
+ if (!message && stageId && policy.allowInputPicker) {
2838
3165
  overlay.open(runId, overlaySurfaceFromContext(ctx), stageId);
2839
3166
  }
2840
- print(
2841
- result.resumed.length === 0
2842
- ? `No paused stages on run ${runId.slice(0, 8)}.`
2843
- : `Resumed ${result.resumed.length} stage(s) on run ${runId.slice(0, 8)}${message ? ` with message: "${message}"` : ""}.`,
2844
- );
3167
+ if (result.resumed.length === 0) {
3168
+ fail(`No paused stages on run ${stageRunId.slice(0, 8)}.`);
3169
+ } else {
3170
+ print(`Resumed ${result.resumed.length} stage(s) on run ${stageRunId.slice(0, 8)}${message ? ` with message: "${message}"` : ""}.`);
3171
+ }
2845
3172
  return true;
2846
3173
  }
2847
3174
 
@@ -2853,16 +3180,48 @@ function factory(pi: ExtensionAPI): void {
2853
3180
  "workflow",
2854
3181
  {
2855
3182
  description:
2856
- "Run or inspect pi workflows. Usage: /workflow <name> [key=value…] | /workflow [list|status|connect|attach|interrupt|kill|pause|resume|inputs|reload] [args]",
3183
+ "Run or inspect Atomic workflows. Usage: /workflow <name> [key=value…] | /workflow [list|status|connect|attach|interrupt|kill|pause|resume|inputs|reload] [args]",
2857
3184
  handler: async (args: string, ctx: PiCommandContext) => {
2858
- // Print/JSON (`-p`) sessions cannot drive workflow pickers, the graph
2859
- // overlay, or human-in-the-loop prompts. Refuse before parsing so the
2860
- // command surface matches the disabled `workflow` tool.
2861
- if (ctx.hasUI === false) {
2862
- ctx.ui.notify(WORKFLOW_NON_INTERACTIVE_MESSAGE, "warning");
2863
- return;
2864
- }
2865
- const print = (msg: string): void => ctx.ui.notify(msg, "info");
3185
+ const policy = workflowPolicyFromContext(ctx);
3186
+ const reporter = createWorkflowCommandReporter(ctx, policy, pi);
3187
+ const print = (msg: string): void => reporter.info(msg);
3188
+ const fail = (msg: string): void => reporter.error(msg);
3189
+ const withImplicitYesFlag = (tokens: string[]): string[] =>
3190
+ tokens.some((t) => t === "--yes" || t === "-y") ? tokens : [...tokens, "-y"];
3191
+ const showWorkflowInputs = async (
3192
+ workflowName: string,
3193
+ command: WorkflowCommandOutputDetails["command"] = "inputs",
3194
+ ): Promise<void> => {
3195
+ const result = await runtimeForContext(ctx).dispatch({
3196
+ workflow: workflowName,
3197
+ inputs: {},
3198
+ action: "inputs",
3199
+ }, { policy });
3200
+ if (result.action === "inputs" && "inputs" in result) {
3201
+ const r = result as Extract<
3202
+ WorkflowToolResult,
3203
+ { action: "inputs" }
3204
+ >;
3205
+ if (r.error) {
3206
+ const available = runtimeProxy.registry.names();
3207
+ fail(
3208
+ `${r.error}\nAvailable: ${formatAvailableWorkflowNames(available)}`,
3209
+ );
3210
+ } else {
3211
+ const schemaText = renderInputsSchema(workflowName, r.inputs, {
3212
+ theme: deriveGraphTheme({}),
3213
+ });
3214
+ if (policy.mode === "non_interactive") {
3215
+ emitWorkflowCommandOutput(pi, schemaText, {
3216
+ command,
3217
+ workflowName,
3218
+ });
3219
+ } else {
3220
+ print(schemaText);
3221
+ }
3222
+ }
3223
+ }
3224
+ };
2866
3225
  // Quote-aware split so `prompt="map the codebase"` stays a single
2867
3226
  // token. Plain `.split(/\s+/)` would mangle quoted multi-word values
2868
3227
  // into `prompt="map`, `the`, `codebase"` — the dispatch confirm then
@@ -2876,15 +3235,15 @@ function factory(pi: ExtensionAPI): void {
2876
3235
  // pause — pause a run or specific stage.
2877
3236
  // -----------------------------------------------------------------------
2878
3237
  if (subcommand === "connect") {
2879
- await handleRunControlCommand("connect", parts.slice(1), ctx);
3238
+ await handleRunControlCommand("connect", parts.slice(1), ctx, reporter);
2880
3239
  return;
2881
3240
  }
2882
3241
  if (subcommand === "attach") {
2883
- await handleRunControlCommand("attach", parts.slice(1), ctx);
3242
+ await handleRunControlCommand("attach", parts.slice(1), ctx, reporter);
2884
3243
  return;
2885
3244
  }
2886
3245
  if (subcommand === "pause") {
2887
- await handleRunControlCommand("pause", parts.slice(1), ctx);
3246
+ await handleRunControlCommand("pause", parts.slice(1), ctx, reporter);
2888
3247
  return;
2889
3248
  }
2890
3249
 
@@ -2898,7 +3257,7 @@ function factory(pi: ExtensionAPI): void {
2898
3257
  description: def.description,
2899
3258
  inputs: Object.entries(def.inputs).map(([iname, schema]) => ({
2900
3259
  name: iname,
2901
- required: schema.required === true,
3260
+ required: schemaIsRequired(schema),
2902
3261
  })),
2903
3262
  }));
2904
3263
  emitChatSurface(pi, { kind: "list", entries: items });
@@ -2915,11 +3274,11 @@ function factory(pi: ExtensionAPI): void {
2915
3274
  if (target && !target.startsWith("--")) {
2916
3275
  const resolved = resolveRunIdPrefix(target);
2917
3276
  if (resolved.kind === "not_found") {
2918
- print(`Run not found: ${target}`);
3277
+ fail(`Run not found: ${target}`);
2919
3278
  return;
2920
3279
  }
2921
3280
  if (resolved.kind === "ambiguous") {
2922
- print(
3281
+ fail(
2923
3282
  `Ambiguous run prefix "${target}" matches: ${resolved.matches
2924
3283
  .map((id) => id.slice(0, 12))
2925
3284
  .join(", ")}`,
@@ -2928,7 +3287,7 @@ function factory(pi: ExtensionAPI): void {
2928
3287
  }
2929
3288
  const inspected = inspectRun(resolved.runId);
2930
3289
  if (!inspected.ok) {
2931
- print(`Run not found: ${target}`);
3290
+ fail(`Run not found: ${target}`);
2932
3291
  return;
2933
3292
  }
2934
3293
  emitChatSurface(pi, { kind: "detail", detail: inspected.detail });
@@ -2954,14 +3313,14 @@ function factory(pi: ExtensionAPI): void {
2954
3313
  if (subcommand === "reload") {
2955
3314
  const activeRuns = inFlightRunCount();
2956
3315
  if (activeRuns > 0) {
2957
- print(reloadBlockedMessage(activeRuns));
3316
+ fail(reloadBlockedMessage(activeRuns));
2958
3317
  return;
2959
3318
  }
2960
3319
  try {
2961
3320
  await reloadWorkflowResources();
2962
3321
  print("Reloaded workflow resources.");
2963
3322
  } catch (error) {
2964
- print(reloadFailureMessage(error));
3323
+ fail(reloadFailureMessage(error));
2965
3324
  }
2966
3325
  return;
2967
3326
  }
@@ -2975,11 +3334,11 @@ function factory(pi: ExtensionAPI): void {
2975
3334
  // command should pause immediately, even when a confirm surface is
2976
3335
  // unavailable or would steal focus from the running workflow.
2977
3336
  const interruptArgs = parts.slice(1);
2978
- const hasYes = interruptArgs.some((t) => t === "--yes" || t === "-y");
2979
3337
  await handleRunControlCommand(
2980
3338
  "interrupt",
2981
- hasYes ? interruptArgs : [...interruptArgs, "-y"],
3339
+ withImplicitYesFlag(interruptArgs),
2982
3340
  ctx,
3341
+ reporter,
2983
3342
  );
2984
3343
  return;
2985
3344
  }
@@ -2989,11 +3348,11 @@ function factory(pi: ExtensionAPI): void {
2989
3348
  // -----------------------------------------------------------------------
2990
3349
  if (subcommand === "kill") {
2991
3350
  const killArgs = parts.slice(1);
2992
- const hasYes = killArgs.some((t) => t === "--yes" || t === "-y");
2993
3351
  await handleRunControlCommand(
2994
3352
  "kill",
2995
- hasYes ? killArgs : [...killArgs, "-y"],
3353
+ withImplicitYesFlag(killArgs),
2996
3354
  ctx,
3355
+ reporter,
2997
3356
  );
2998
3357
  return;
2999
3358
  }
@@ -3003,7 +3362,7 @@ function factory(pi: ExtensionAPI): void {
3003
3362
  // behaviour); paused runs resume live work through the registry.
3004
3363
  // -----------------------------------------------------------------------
3005
3364
  if (subcommand === "resume") {
3006
- await handleRunControlCommand("resume", parts.slice(1), ctx);
3365
+ await handleRunControlCommand("resume", parts.slice(1), ctx, reporter);
3007
3366
  return;
3008
3367
  }
3009
3368
 
@@ -3013,32 +3372,10 @@ function factory(pi: ExtensionAPI): void {
3013
3372
  if (subcommand === "inputs") {
3014
3373
  const workflowName = parts[1] ?? "";
3015
3374
  if (!workflowName) {
3016
- print("Usage: /workflow inputs <name>");
3375
+ fail("Usage: /workflow inputs <name>");
3017
3376
  return;
3018
3377
  }
3019
- const result = await runtimeForContext(ctx).dispatch({
3020
- workflow: workflowName,
3021
- inputs: {},
3022
- action: "inputs",
3023
- });
3024
- if (result.action === "inputs" && "inputs" in result) {
3025
- const r = result as Extract<
3026
- WorkflowToolResult,
3027
- { action: "inputs" }
3028
- >;
3029
- if (r.error) {
3030
- const available = runtimeProxy.registry.names();
3031
- print(
3032
- `${r.error}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
3033
- );
3034
- } else {
3035
- print(
3036
- renderInputsSchema(workflowName, r.inputs, {
3037
- theme: deriveGraphTheme({}),
3038
- }),
3039
- );
3040
- }
3041
- }
3378
+ await showWorkflowInputs(workflowName);
3042
3379
  return;
3043
3380
  }
3044
3381
 
@@ -3051,29 +3388,7 @@ function factory(pi: ExtensionAPI): void {
3051
3388
  const inputTokens = parts.slice(1);
3052
3389
 
3053
3390
  if (inputTokens.includes("--help")) {
3054
- const helpResult = await runtimeForContext(ctx).dispatch({
3055
- workflow: workflowName,
3056
- inputs: {},
3057
- action: "inputs",
3058
- });
3059
- if (helpResult.action === "inputs" && "inputs" in helpResult) {
3060
- const r = helpResult as Extract<
3061
- WorkflowToolResult,
3062
- { action: "inputs" }
3063
- >;
3064
- if (r.error) {
3065
- const available = runtimeProxy.registry.names();
3066
- print(
3067
- `${r.error}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
3068
- );
3069
- } else {
3070
- print(
3071
- renderInputsSchema(workflowName, r.inputs, {
3072
- theme: deriveGraphTheme({}),
3073
- }),
3074
- );
3075
- }
3076
- }
3391
+ await showWorkflowInputs(workflowName, "help");
3077
3392
  return;
3078
3393
  }
3079
3394
 
@@ -3107,6 +3422,7 @@ function factory(pi: ExtensionAPI): void {
3107
3422
  // back to the supported overlay picker rather than surfacing the host
3108
3423
  // exception as a workflow command error.
3109
3424
  const canOpenPicker =
3425
+ policy.allowInputPicker &&
3110
3426
  !wantsPickerSkip &&
3111
3427
  (typeof ctx.ui?.setEditorComponent === "function" ||
3112
3428
  typeof ctx.ui?.custom === "function");
@@ -3115,7 +3431,7 @@ function factory(pi: ExtensionAPI): void {
3115
3431
  workflow: workflowName,
3116
3432
  inputs: {},
3117
3433
  action: "inputs",
3118
- });
3434
+ }, { policy });
3119
3435
  const schema =
3120
3436
  schemaResult.action === "inputs" && "inputs" in schemaResult
3121
3437
  ? (schemaResult as Extract<
@@ -3165,26 +3481,38 @@ function factory(pi: ExtensionAPI): void {
3165
3481
  }
3166
3482
  }
3167
3483
 
3168
- const result = await runtimeForContext(ctx).dispatch({
3169
- workflow: workflowName,
3170
- inputs: mergedInputs,
3171
- action: "run",
3172
- });
3484
+ const result = await runWithLifecycleSuppressedForPolicy(policy, () =>
3485
+ runtimeForContext(ctx).dispatch({
3486
+ workflow: workflowName,
3487
+ inputs: mergedInputs,
3488
+ action: "run",
3489
+ }, { policy }),
3490
+ );
3173
3491
  if (result.action === "run" && "runId" in result) {
3174
3492
  const r = result as Extract<
3175
3493
  WorkflowToolResult,
3176
3494
  { action: "run"; runId: string }
3177
3495
  >;
3178
3496
  if (r.status === "failed" && r.runId === "") {
3179
- const available = runtimeProxy.registry.names();
3180
- print(
3181
- `Workflow not found: ${workflowName}\nAvailable: ${available.length > 0 ? available.join(", ") : "(none)"}`,
3182
- );
3497
+ if (r.error?.toLowerCase().includes("not found")) {
3498
+ const available = runtimeProxy.registry.names();
3499
+ fail(
3500
+ `Workflow not found: ${workflowName}\nAvailable: ${formatAvailableWorkflowNames(available)}`,
3501
+ );
3502
+ } else {
3503
+ fail(
3504
+ `Workflow "${workflowName}" failed: ${r.error ?? "unknown error"}`,
3505
+ );
3506
+ }
3183
3507
  } else if (r.status === "failed") {
3184
- print(
3508
+ fail(
3185
3509
  `Workflow "${workflowName}" failed: ${r.error ?? "unknown error"}`,
3186
3510
  );
3187
3511
  } else {
3512
+ if (policy.mode === "non_interactive") {
3513
+ emitTerminalRunDetailSurface(pi, workflowName, mergedInputs, r);
3514
+ return;
3515
+ }
3188
3516
  // Always-background — the run is alive, the chat is free.
3189
3517
  // Route via emitChatSurface so the band+card chrome receives the
3190
3518
  // real chat content width via pi-tui's Component contract
@@ -3239,7 +3567,7 @@ function factory(pi: ExtensionAPI): void {
3239
3567
  }));
3240
3568
 
3241
3569
  const runIdItems = (): PiArgumentCompletion[] =>
3242
- store.runs().map((run) => ({
3570
+ topLevelWorkflowRuns(store.runs()).map((run) => ({
3243
3571
  value: `${run.id} `,
3244
3572
  label: run.id.slice(0, 8),
3245
3573
  description: `${run.name} — ${run.status}`,
@@ -3368,17 +3696,19 @@ function factory(pi: ExtensionAPI): void {
3368
3696
  if (equalsIndex > 0) {
3369
3697
  const inputName = token.slice(0, equalsIndex);
3370
3698
  const schema = workflow.inputs[inputName];
3371
- if (schema?.type === "select") {
3699
+ const schemaChoiceValues = schema === undefined ? undefined : schemaChoices(schema);
3700
+ const schemaKind = schema === undefined ? undefined : schemaFieldKind(schema);
3701
+ if (schemaChoiceValues !== undefined) {
3372
3702
  return completeToken(
3373
3703
  partial,
3374
- schema.choices.map((choice) => ({
3704
+ schemaChoiceValues.map((choice) => ({
3375
3705
  value: `${inputName}=${choice} `,
3376
3706
  label: choice,
3377
3707
  description: inputName,
3378
3708
  })),
3379
3709
  );
3380
3710
  }
3381
- if (schema?.type === "boolean") {
3711
+ if (schemaKind === "boolean") {
3382
3712
  return completeToken(partial, [
3383
3713
  {
3384
3714
  value: `${inputName}=true `,
@@ -3400,7 +3730,7 @@ function factory(pi: ExtensionAPI): void {
3400
3730
  ).map(([name, schema]) => ({
3401
3731
  value: `${name}=`,
3402
3732
  label: name,
3403
- description: schema.description,
3733
+ description: schemaDescription(schema),
3404
3734
  }));
3405
3735
  return completeToken(partial, [
3406
3736
  {
@@ -3488,11 +3818,12 @@ function factory(pi: ExtensionAPI): void {
3488
3818
  });
3489
3819
 
3490
3820
  pi.on("session_start", async (_event, ctx) => {
3491
- // Non-interactive (`-p` / `--mode json`) sessions cannot drive workflow
3492
- // prompts or the graph overlay; drop the `workflow` tool from the model's
3493
- // tool set before the first turn. The host awaits this handler during
3494
- // bindExtensions, ahead of the initial prompt.
3495
- deAdvertiseWorkflowToolWhenHeadless(pi, ctx?.hasUI);
3821
+ // Non-interactive (`-p` / `--mode json`) sessions keep the workflow tool
3822
+ // available for deterministic automation. Policy gates disable pickers
3823
+ // and make runtime human-input APIs unavailable.
3824
+ // Defense-in-depth for older/nonstandard hosts: remove only the
3825
+ // unavailable human-input tool from the active tool set.
3826
+ deAdvertiseAskUserQuestionWhenHeadless(pi, ctx?.hasUI);
3496
3827
 
3497
3828
  // Workflow lifecycle is scoped to the originating chat session.
3498
3829
  // A new session inherits a clean store; any leftover live runs from a
@@ -3506,6 +3837,7 @@ function factory(pi: ExtensionAPI): void {
3506
3837
  });
3507
3838
  store.clear();
3508
3839
  resetWorkflowLifecycleNotificationState(lifecycleNotificationState);
3840
+ resetWorkflowHilAnswerNotificationState(hilAnswerNotificationState);
3509
3841
  stageControlRegistry.clear();
3510
3842
 
3511
3843
  // pi-intercom session naming lives here so we don't trip the
@@ -3517,7 +3849,9 @@ function factory(pi: ExtensionAPI): void {
3517
3849
  // tunables must be resolved first.
3518
3850
  await discoveryPromise;
3519
3851
  lifecycleNotificationsActive = true;
3852
+ hilAnswerNotificationsActive = true;
3520
3853
  reinstallLifecycleNotifications();
3854
+ reinstallHilAnswerNotifications();
3521
3855
  if (ctx?.ui) {
3522
3856
  const diagnostics = formatStartupDiagnostics(configLoadRef.current, discoveryRef.current);
3523
3857
  if (diagnostics !== null) {
@@ -3569,12 +3903,16 @@ function factory(pi: ExtensionAPI): void {
3569
3903
  cancellation: cancellationRegistry,
3570
3904
  persistence: persistenceRef.current,
3571
3905
  });
3906
+ stageControlRegistry.clear();
3572
3907
  }
3573
3908
  storeWidgetUnsubscribe?.();
3574
3909
  storeWidgetUnsubscribe = null;
3575
3910
  lifecycleNotificationsActive = false;
3911
+ hilAnswerNotificationsActive = false;
3576
3912
  lifecycleNotificationsUnsubscribe?.();
3577
3913
  lifecycleNotificationsUnsubscribe = null;
3914
+ hilAnswerNotificationsUnsubscribe?.();
3915
+ hilAnswerNotificationsUnsubscribe = null;
3578
3916
  });
3579
3917
  }
3580
3918