@davidorex/pi-workflows 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/CHANGELOG.md +7 -4
  2. package/README.md +12 -2
  3. package/agents/architecture-designer.agent.yaml +58 -0
  4. package/agents/architecture-inferrer.agent.yaml +60 -0
  5. package/agents/gap-identifier.agent.yaml +66 -0
  6. package/agents/handoff-writer.agent.yaml +64 -0
  7. package/agents/plan-creator.agent.yaml +63 -0
  8. package/agents/project-definer.agent.yaml +53 -0
  9. package/agents/project-inferrer.agent.yaml +63 -0
  10. package/agents/requirements-gatherer.agent.yaml +58 -0
  11. package/dist/agent-spec.d.ts +27 -0
  12. package/dist/agent-spec.d.ts.map +1 -0
  13. package/dist/agent-spec.js +112 -0
  14. package/dist/agent-spec.js.map +1 -0
  15. package/dist/checkpoint.d.ts +50 -0
  16. package/dist/checkpoint.d.ts.map +1 -0
  17. package/dist/checkpoint.js +109 -0
  18. package/dist/checkpoint.js.map +1 -0
  19. package/dist/completion.d.ts +18 -0
  20. package/dist/completion.d.ts.map +1 -0
  21. package/dist/completion.js +58 -0
  22. package/dist/completion.js.map +1 -0
  23. package/dist/dag.d.ts +56 -0
  24. package/dist/dag.d.ts.map +1 -0
  25. package/dist/dag.js +172 -0
  26. package/dist/dag.js.map +1 -0
  27. package/dist/dispatch.d.ts +35 -0
  28. package/dist/dispatch.d.ts.map +1 -0
  29. package/dist/dispatch.js +311 -0
  30. package/dist/dispatch.js.map +1 -0
  31. package/dist/expression.d.ts +64 -0
  32. package/dist/expression.d.ts.map +1 -0
  33. package/dist/expression.js +294 -0
  34. package/dist/expression.js.map +1 -0
  35. package/dist/format.d.ts +19 -0
  36. package/dist/format.d.ts.map +1 -0
  37. package/dist/format.js +42 -0
  38. package/dist/format.js.map +1 -0
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +675 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/output.d.ts +12 -0
  44. package/dist/output.d.ts.map +1 -0
  45. package/dist/output.js +59 -0
  46. package/dist/output.js.map +1 -0
  47. package/dist/state.d.ts +66 -0
  48. package/dist/state.d.ts.map +1 -0
  49. package/dist/state.js +227 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/step-agent.d.ts +35 -0
  52. package/dist/step-agent.d.ts.map +1 -0
  53. package/dist/step-agent.js +156 -0
  54. package/dist/step-agent.js.map +1 -0
  55. package/dist/step-command.d.ts +21 -0
  56. package/dist/step-command.d.ts.map +1 -0
  57. package/dist/step-command.js +150 -0
  58. package/dist/step-command.js.map +1 -0
  59. package/dist/step-foreach.d.ts +33 -0
  60. package/dist/step-foreach.d.ts.map +1 -0
  61. package/dist/step-foreach.js +112 -0
  62. package/dist/step-foreach.js.map +1 -0
  63. package/dist/step-gate.d.ts +18 -0
  64. package/dist/step-gate.d.ts.map +1 -0
  65. package/dist/step-gate.js +137 -0
  66. package/dist/step-gate.js.map +1 -0
  67. package/dist/step-loop.d.ts +32 -0
  68. package/dist/step-loop.d.ts.map +1 -0
  69. package/dist/step-loop.js +278 -0
  70. package/dist/step-loop.js.map +1 -0
  71. package/dist/step-monitor.d.ts +56 -0
  72. package/dist/step-monitor.d.ts.map +1 -0
  73. package/dist/step-monitor.js +317 -0
  74. package/dist/step-monitor.js.map +1 -0
  75. package/dist/step-parallel.d.ts +43 -0
  76. package/dist/step-parallel.d.ts.map +1 -0
  77. package/dist/step-parallel.js +122 -0
  78. package/dist/step-parallel.js.map +1 -0
  79. package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
  80. package/dist/step-pause.d.ts.map +1 -0
  81. package/dist/step-pause.js +22 -0
  82. package/dist/step-pause.js.map +1 -0
  83. package/dist/step-shared.d.ts +57 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +125 -0
  86. package/dist/step-shared.js.map +1 -0
  87. package/dist/step-transform.d.ts +11 -0
  88. package/dist/step-transform.d.ts.map +1 -0
  89. package/dist/step-transform.js +41 -0
  90. package/dist/step-transform.js.map +1 -0
  91. package/dist/template.d.ts +51 -0
  92. package/dist/template.d.ts.map +1 -0
  93. package/{src/template.ts → dist/template.js} +32 -38
  94. package/dist/template.js.map +1 -0
  95. package/dist/test-helpers.d.ts +17 -0
  96. package/dist/test-helpers.d.ts.map +1 -0
  97. package/dist/test-helpers.js +46 -0
  98. package/dist/test-helpers.js.map +1 -0
  99. package/dist/tui.d.ts +47 -0
  100. package/dist/tui.d.ts.map +1 -0
  101. package/dist/tui.js +143 -0
  102. package/dist/tui.js.map +1 -0
  103. package/dist/types.d.ts +164 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +3 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/workflow-discovery.d.ts +23 -0
  108. package/dist/workflow-discovery.d.ts.map +1 -0
  109. package/dist/workflow-discovery.js +103 -0
  110. package/dist/workflow-discovery.js.map +1 -0
  111. package/dist/workflow-executor.d.ts +50 -0
  112. package/dist/workflow-executor.d.ts.map +1 -0
  113. package/dist/workflow-executor.js +726 -0
  114. package/dist/workflow-executor.js.map +1 -0
  115. package/dist/workflow-sdk.d.ts +44 -0
  116. package/dist/workflow-sdk.d.ts.map +1 -0
  117. package/dist/workflow-sdk.js +438 -0
  118. package/dist/workflow-sdk.js.map +1 -0
  119. package/dist/workflow-spec.d.ts +33 -0
  120. package/dist/workflow-spec.d.ts.map +1 -0
  121. package/dist/workflow-spec.js +464 -0
  122. package/dist/workflow-spec.js.map +1 -0
  123. package/dist/workflows-dir.d.ts +3 -0
  124. package/dist/workflows-dir.d.ts.map +1 -0
  125. package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
  126. package/dist/workflows-dir.js.map +1 -0
  127. package/package.json +53 -38
  128. package/schemas/decomposition-specs.schema.json +48 -48
  129. package/schemas/execution-results.schema.json +48 -48
  130. package/schemas/investigation-findings.schema.json +43 -43
  131. package/schemas/pattern-analysis.schema.json +41 -41
  132. package/schemas/phase.schema.json +48 -0
  133. package/schemas/plan-breakdown.schema.json +20 -20
  134. package/schemas/quality-analysis.schema.json +33 -33
  135. package/schemas/research-findings.schema.json +42 -42
  136. package/schemas/structure-analysis.schema.json +40 -40
  137. package/schemas/synthesis.schema.json +31 -31
  138. package/schemas/verifier-output.schema.json +94 -94
  139. package/skill-narrative.md +84 -0
  140. package/skills/pi-workflows/SKILL.md +247 -0
  141. package/templates/architecture-designer/task.md +117 -0
  142. package/templates/architecture-inferrer/task.md +61 -0
  143. package/templates/gap-identifier/task.md +103 -0
  144. package/templates/handoff-writer/task.md +91 -0
  145. package/templates/plan-creator/task.md +143 -0
  146. package/templates/project-definer/task.md +67 -0
  147. package/templates/project-inferrer/task.md +56 -0
  148. package/templates/requirements-gatherer/task.md +90 -0
  149. package/workflows/analyze-existing-project.workflow.yaml +74 -0
  150. package/workflows/create-handoff.workflow.yaml +58 -0
  151. package/workflows/create-phase.workflow.yaml +1 -1
  152. package/workflows/do-gap.workflow.yaml +4 -4
  153. package/workflows/fix-audit.workflow.yaml +1 -1
  154. package/workflows/gap-to-phase.workflow.yaml +1 -1
  155. package/workflows/init-new-project.workflow.yaml +55 -0
  156. package/workflows/plan-from-requirements.workflow.yaml +51 -0
  157. package/workflows/self-implement.workflow.yaml +3 -3
  158. package/workflows/typed-analysis.workflow.yaml +4 -4
  159. package/src/agent-spec.test.ts +0 -289
  160. package/src/agent-spec.ts +0 -122
  161. package/src/block-validation.test.ts +0 -350
  162. package/src/checkpoint.test.ts +0 -237
  163. package/src/checkpoint.ts +0 -140
  164. package/src/completion.test.ts +0 -143
  165. package/src/completion.ts +0 -68
  166. package/src/dag.test.ts +0 -350
  167. package/src/dag.ts +0 -219
  168. package/src/dispatch.test.ts +0 -475
  169. package/src/dispatch.ts +0 -353
  170. package/src/expression.test.ts +0 -353
  171. package/src/expression.ts +0 -332
  172. package/src/format.test.ts +0 -80
  173. package/src/format.ts +0 -41
  174. package/src/graduated-failure.test.ts +0 -556
  175. package/src/index.test.ts +0 -114
  176. package/src/index.ts +0 -516
  177. package/src/loop.test.ts +0 -551
  178. package/src/output.test.ts +0 -213
  179. package/src/output.ts +0 -70
  180. package/src/parallel-integration.test.ts +0 -175
  181. package/src/resume.test.ts +0 -192
  182. package/src/state.test.ts +0 -192
  183. package/src/state.ts +0 -254
  184. package/src/step-agent.test.ts +0 -327
  185. package/src/step-agent.ts +0 -178
  186. package/src/step-command.test.ts +0 -330
  187. package/src/step-command.ts +0 -132
  188. package/src/step-foreach.test.ts +0 -647
  189. package/src/step-foreach.ts +0 -148
  190. package/src/step-gate.test.ts +0 -185
  191. package/src/step-gate.ts +0 -116
  192. package/src/step-loop.test.ts +0 -626
  193. package/src/step-loop.ts +0 -323
  194. package/src/step-parallel.test.ts +0 -475
  195. package/src/step-parallel.ts +0 -168
  196. package/src/step-pause.test.ts +0 -30
  197. package/src/step-shared.test.ts +0 -355
  198. package/src/step-shared.ts +0 -157
  199. package/src/step-transform.test.ts +0 -155
  200. package/src/step-transform.ts +0 -47
  201. package/src/template-integration.test.ts +0 -72
  202. package/src/template.test.ts +0 -162
  203. package/src/test-helpers.ts +0 -51
  204. package/src/tui.test.ts +0 -355
  205. package/src/tui.ts +0 -182
  206. package/src/types.ts +0 -195
  207. package/src/verifier-schema.test.ts +0 -226
  208. package/src/workflow-discovery.test.ts +0 -105
  209. package/src/workflow-discovery.ts +0 -114
  210. package/src/workflow-executor.test.ts +0 -1532
  211. package/src/workflow-executor.ts +0 -810
  212. package/src/workflow-sdk.test.ts +0 -238
  213. package/src/workflow-sdk.ts +0 -262
  214. package/src/workflow-spec.test.ts +0 -576
  215. package/src/workflow-spec.ts +0 -471
@@ -0,0 +1,726 @@
1
+ /**
2
+ * Workflow executor — orchestrates step execution with DAG-based layering,
3
+ * parallel dispatch, timeout enforcement, state persistence, and TUI updates.
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { readBlock, writeBlock } from "@davidorex/pi-project/src/block-api.js";
8
+ import { rollbackBlockFiles, snapshotBlockFiles, validateChangedBlocks, } from "@davidorex/pi-project/src/block-validation.js";
9
+ import { PROJECT_DIR } from "@davidorex/pi-project/src/project-dir.js";
10
+ import { validate, validateFromFile } from "@davidorex/pi-project/src/schema-validator.js";
11
+ import { truncateTail } from "@mariozechner/pi-coding-agent";
12
+ import { resolveCompletion } from "./completion.js";
13
+ import { buildPlanFromDeps, extractDependencies } from "./dag.js";
14
+ import { dispatch } from "./dispatch.js";
15
+ import { evaluateCondition, resolveExpressions } from "./expression.js";
16
+ import { buildResult, formatResult, generateRunId, getWorkflowDir, initRunDir, writeMetrics, writeState, } from "./state.js";
17
+ import { executeAgentStep } from "./step-agent.js";
18
+ import { executeCommand } from "./step-command.js";
19
+ import { executeForEach } from "./step-foreach.js";
20
+ import { executeGate } from "./step-gate.js";
21
+ import { executeLoop } from "./step-loop.js";
22
+ import { executeMonitor } from "./step-monitor.js";
23
+ import { executeParallelLayer, executeParallelStep } from "./step-parallel.js";
24
+ import { executePause } from "./step-pause.js";
25
+ import { persistStep, resolveSchemaPath, SIGKILL_GRACE_MS, WIDGET_ID, zeroUsage } from "./step-shared.js";
26
+ import { executeTransform } from "./step-transform.js";
27
+ import { createTemplateEnv } from "./template.js";
28
+ import { createProgressWidget } from "./tui.js";
29
+ // Re-export SIGKILL_GRACE_MS so tests that grep this file still find it
30
+ export { SIGKILL_GRACE_MS };
31
+ /** Module-level flag set by the Ctrl+H keybinding handler. */
32
+ let pauseRequested = false;
33
+ /** Set by the extension keybinding to request a pause after the current step. */
34
+ export function requestPause() {
35
+ pauseRequested = true;
36
+ }
37
+ /** Clear the pause flag (called at the start of each workflow execution). */
38
+ function clearPauseRequest() {
39
+ pauseRequested = false;
40
+ }
41
+ /**
42
+ * Build a conservative execution plan that preserves declaration-order
43
+ * sequencing for steps without explicit `${{ steps.X }}` dependencies.
44
+ *
45
+ * Steps with no explicit dependencies implicitly depend on the previous
46
+ * step in declaration order. This ensures backward compatibility with
47
+ * workflows written for sequential execution while still allowing
48
+ * DAG-inferred parallelism for steps that DO have explicit dependencies
49
+ * (e.g., diamond patterns where two steps both depend on an earlier step).
50
+ */
51
+ function buildConservativePlan(spec) {
52
+ const deps = extractDependencies(spec);
53
+ const allSteps = Object.keys(spec.steps);
54
+ // Add implicit declaration-order dependency for steps with no explicit deps.
55
+ // If a step has no ${{ steps.X }} references at all, it depends on the
56
+ // immediately preceding step in YAML order.
57
+ for (let i = 1; i < allSteps.length; i++) {
58
+ const stepDeps = deps.get(allSteps[i]);
59
+ if (stepDeps.size === 0) {
60
+ stepDeps.add(allSteps[i - 1]);
61
+ }
62
+ }
63
+ return buildPlanFromDeps(allSteps, deps);
64
+ }
65
+ /**
66
+ * Determine if a step type supports retry.
67
+ * Agent, forEach, and loop steps are retryable.
68
+ * Command, gate, and transform are deterministic and not retryable.
69
+ */
70
+ function isRetryableStepType(stepSpec) {
71
+ if (stepSpec.command || stepSpec.gate || stepSpec.transform || stepSpec.monitor)
72
+ return false;
73
+ return true;
74
+ }
75
+ /**
76
+ * Execute a single step (agent, gate, transform, loop, or parallel).
77
+ *
78
+ * This is the central step type dispatcher. It delegates to the appropriate
79
+ * step executor module based on the step spec type.
80
+ * Returns true if the workflow should continue, false if it should stop
81
+ * (due to failure, break, or cancellation).
82
+ *
83
+ * Supports per-step retry: if step.retry is configured and the step type
84
+ * is retryable, failures (including block validation) trigger rollback
85
+ * and re-execution with error context injected into the prompt.
86
+ */
87
+ async function executeSingleStep(stepName, stepSpec, state, options) {
88
+ const { ctx, signal, loadAgent, runDir, spec, widgetState } = options;
89
+ // Check cancellation
90
+ if (signal?.aborted) {
91
+ state.steps[stepName] = {
92
+ step: stepName,
93
+ agent: stepSpec.agent ?? "",
94
+ status: "failed",
95
+ usage: zeroUsage(),
96
+ durationMs: 0,
97
+ error: "Workflow cancelled",
98
+ };
99
+ state.status = "failed";
100
+ return false;
101
+ }
102
+ // Build expression scope
103
+ const scope = { input: state.input, steps: state.steps };
104
+ // Expose forEach bindings (as name + forEach metadata) if present on the state
105
+ const stateAny = state;
106
+ if (stateAny.forEach !== undefined) {
107
+ scope.forEach = stateAny.forEach;
108
+ }
109
+ // Expose any custom bindings (e.g. from forEach as)
110
+ for (const key of Object.keys(stateAny)) {
111
+ if (key !== "input" &&
112
+ key !== "steps" &&
113
+ key !== "status" &&
114
+ key !== "loop" &&
115
+ key !== "workflowName" &&
116
+ key !== "specVersion" &&
117
+ key !== "startedAt" &&
118
+ key !== "updatedAt" &&
119
+ key !== "forEach") {
120
+ scope[key] = stateAny[key];
121
+ }
122
+ }
123
+ // Evaluate `when` conditional
124
+ if (stepSpec.when) {
125
+ const conditionExpr = stepSpec.when.replace(/^\$\{\{\s*/, "").replace(/\s*\}\}$/, "");
126
+ const shouldRun = evaluateCondition(conditionExpr, scope);
127
+ if (!shouldRun) {
128
+ persistStep(state, stepName, {
129
+ step: stepName,
130
+ agent: stepSpec.agent ?? "skipped",
131
+ status: "skipped",
132
+ usage: zeroUsage(),
133
+ durationMs: 0,
134
+ }, runDir, widgetState, ctx);
135
+ return true;
136
+ }
137
+ }
138
+ // Determine retry config
139
+ const retryConfig = stepSpec.retry;
140
+ const maxAttempts = retryConfig?.maxAttempts ?? 1;
141
+ const canRetry = isRetryableStepType(stepSpec) && maxAttempts > 1;
142
+ const totalAttempts = canRetry ? maxAttempts : 1;
143
+ const priorErrors = [];
144
+ for (let attempt = 1; attempt <= totalAttempts; attempt++) {
145
+ // Check cancellation between retry attempts
146
+ if (attempt > 1 && signal?.aborted) {
147
+ state.steps[stepName] = {
148
+ step: stepName,
149
+ agent: stepSpec.agent ?? "",
150
+ status: "failed",
151
+ usage: zeroUsage(),
152
+ durationMs: 0,
153
+ error: "Workflow cancelled",
154
+ };
155
+ state.status = "failed";
156
+ return false;
157
+ }
158
+ // Snapshot project block files before step execution for post-step validation
159
+ const blockSnapshot = snapshotBlockFiles(ctx.cwd);
160
+ // Update widget: mark this step as current
161
+ widgetState.currentStep = stepName;
162
+ if (ctx.hasUI) {
163
+ ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
164
+ }
165
+ // Build retry context for this attempt (if retrying)
166
+ const retryContext = attempt > 1
167
+ ? {
168
+ attempt,
169
+ priorErrors: [...priorErrors],
170
+ steeringMessage: retryConfig?.steeringMessage,
171
+ }
172
+ : undefined;
173
+ // Execute the step, then validate any changed block files
174
+ // Reset failed status before re-attempt
175
+ if (attempt > 1) {
176
+ delete state.steps[stepName];
177
+ state.status = "running";
178
+ }
179
+ const continueWorkflow = await executeStepByType(stepName, stepSpec, state, scope, options, retryContext);
180
+ // Post-step block validation: if the step succeeded, validate changed project block files
181
+ let blockValidationFailed = false;
182
+ if (continueWorkflow && state.steps[stepName]?.status === "completed") {
183
+ try {
184
+ validateChangedBlocks(ctx.cwd, blockSnapshot);
185
+ }
186
+ catch (err) {
187
+ const msg = err instanceof Error ? err.message : String(err);
188
+ blockValidationFailed = true;
189
+ priorErrors.push(msg);
190
+ // Always rollback — invalid data must not persist
191
+ rollbackBlockFiles(ctx.cwd, blockSnapshot);
192
+ if (attempt < totalAttempts) {
193
+ // Retry
194
+ continue;
195
+ }
196
+ // Last attempt — mark failed
197
+ state.steps[stepName].status = "failed";
198
+ state.steps[stepName].error = msg;
199
+ state.steps[stepName].attempt = attempt;
200
+ state.steps[stepName].totalAttempts = attempt;
201
+ state.steps[stepName].priorErrors = [...priorErrors];
202
+ state.status = "failed";
203
+ persistStep(state, stepName, state.steps[stepName], runDir, widgetState, ctx);
204
+ // Check onExhausted
205
+ if (retryConfig?.onExhausted === "skip") {
206
+ state.steps[stepName].status = "skipped";
207
+ state.steps[stepName].warnings = [
208
+ ...(state.steps[stepName].warnings ?? []),
209
+ `Step skipped after ${attempt} failed attempts (onExhausted: skip)`,
210
+ ];
211
+ state.status = "running";
212
+ persistStep(state, stepName, state.steps[stepName], runDir, widgetState, ctx);
213
+ return true;
214
+ }
215
+ return false;
216
+ }
217
+ }
218
+ if (!continueWorkflow || state.steps[stepName]?.status === "failed") {
219
+ const errorMsg = state.steps[stepName]?.error ?? "Step failed";
220
+ priorErrors.push(errorMsg);
221
+ if (attempt < totalAttempts && canRetry) {
222
+ // Rollback block files and retry
223
+ rollbackBlockFiles(ctx.cwd, blockSnapshot);
224
+ continue;
225
+ }
226
+ // Last attempt or not retryable — annotate and return
227
+ if (state.steps[stepName]) {
228
+ state.steps[stepName].attempt = attempt;
229
+ state.steps[stepName].totalAttempts = attempt;
230
+ state.steps[stepName].priorErrors = attempt > 1 ? [...priorErrors] : undefined;
231
+ persistStep(state, stepName, state.steps[stepName], runDir, widgetState, ctx);
232
+ }
233
+ // Check onExhausted
234
+ if (canRetry && retryConfig?.onExhausted === "skip") {
235
+ if (state.steps[stepName]) {
236
+ state.steps[stepName].status = "skipped";
237
+ state.steps[stepName].warnings = [
238
+ ...(state.steps[stepName].warnings ?? []),
239
+ `Step skipped after ${attempt} failed attempts (onExhausted: skip)`,
240
+ ];
241
+ state.status = "running";
242
+ persistStep(state, stepName, state.steps[stepName], runDir, widgetState, ctx);
243
+ }
244
+ return true;
245
+ }
246
+ return false;
247
+ }
248
+ // Success — annotate with attempt tracking
249
+ if (state.steps[stepName]) {
250
+ state.steps[stepName].attempt = attempt;
251
+ state.steps[stepName].totalAttempts = attempt;
252
+ state.steps[stepName].priorErrors = attempt > 1 ? [...priorErrors] : undefined;
253
+ }
254
+ // Parse step output into a summary for TUI display
255
+ const stepOutput = state.steps[stepName]?.output;
256
+ if (stepOutput && typeof stepOutput === "object") {
257
+ const out = stepOutput;
258
+ const summary = {};
259
+ if (Array.isArray(out.tasks)) {
260
+ summary.tasks = out.tasks
261
+ .filter((t) => t && typeof t === "object")
262
+ .map((t) => ({
263
+ name: String(t.name ?? "?"),
264
+ status: String(t.status ?? "?"),
265
+ files: Array.isArray(t.files_modified) ? t.files_modified.map(String) : undefined,
266
+ }));
267
+ }
268
+ if (typeof out.test_count === "number") {
269
+ summary.testCount = out.test_count;
270
+ }
271
+ if (typeof out.notes === "string") {
272
+ summary.note = out.notes;
273
+ }
274
+ if (summary.tasks || summary.testCount || summary.note) {
275
+ widgetState.outputSummaries.set(stepName, summary);
276
+ if (ctx.hasUI) {
277
+ ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
278
+ }
279
+ }
280
+ }
281
+ return continueWorkflow;
282
+ }
283
+ // Should not reach here, but safety net
284
+ return false;
285
+ }
286
+ /**
287
+ * Execute a step based on its type (agent, gate, transform, etc.).
288
+ * Factored out of executeSingleStep to allow post-step validation.
289
+ */
290
+ async function executeStepByType(stepName, stepSpec, state, scope, options, retryContext) {
291
+ const { ctx, signal, loadAgent, runDir, spec, widgetState } = options;
292
+ // ── ForEach step (wraps any step type) ──
293
+ if (stepSpec.forEach) {
294
+ const asName = stepSpec.as ?? "item";
295
+ const forEachResult = await executeForEach(stepName, stepSpec, state, stepSpec.forEach, asName, executeSingleStep, options);
296
+ persistStep(state, stepName, forEachResult, runDir, widgetState, ctx);
297
+ if (forEachResult.status === "failed") {
298
+ state.status = "failed";
299
+ return false;
300
+ }
301
+ return true;
302
+ }
303
+ // ── Gate step ──
304
+ if (stepSpec.gate) {
305
+ const resolvedCheck = String(resolveExpressions(stepSpec.gate.check, scope));
306
+ const resolvedGate = { ...stepSpec.gate, check: resolvedCheck };
307
+ const resolvedGateOutputPath = stepSpec.output?.path
308
+ ? String(resolveExpressions(stepSpec.output.path, scope))
309
+ : undefined;
310
+ const gateResult = await executeGate(resolvedGate, stepName, {
311
+ cwd: ctx.cwd,
312
+ signal,
313
+ timeoutMs: stepSpec.timeout ? stepSpec.timeout.seconds * 1000 : undefined,
314
+ runDir,
315
+ outputPath: resolvedGateOutputPath,
316
+ });
317
+ const gateOutput = gateResult.output;
318
+ if (gateOutput.passed) {
319
+ const onPass = stepSpec.gate.onPass ?? "continue";
320
+ persistStep(state, stepName, gateResult, runDir, widgetState, ctx);
321
+ return onPass !== "break";
322
+ }
323
+ else {
324
+ const onFail = stepSpec.gate.onFail ?? "fail";
325
+ if (onFail === "fail") {
326
+ gateResult.status = "failed";
327
+ gateResult.error = `Gate check failed (exit ${gateOutput.exitCode}): ${gateOutput.output}`;
328
+ persistStep(state, stepName, gateResult, runDir, widgetState, ctx);
329
+ state.status = "failed";
330
+ return false;
331
+ }
332
+ else if (onFail === "continue") {
333
+ persistStep(state, stepName, gateResult, runDir, widgetState, ctx);
334
+ return true;
335
+ }
336
+ else if (onFail === "break") {
337
+ persistStep(state, stepName, gateResult, runDir, widgetState, ctx);
338
+ return false;
339
+ }
340
+ }
341
+ }
342
+ // ── Command step ──
343
+ if (stepSpec.command) {
344
+ const resolvedCommand = String(resolveExpressions(stepSpec.command, scope));
345
+ const resolvedCommandOutputPath = stepSpec.output?.path
346
+ ? String(resolveExpressions(stepSpec.output.path, scope))
347
+ : undefined;
348
+ const commandResult = await executeCommand(resolvedCommand, stepName, {
349
+ cwd: ctx.cwd,
350
+ signal,
351
+ timeoutMs: stepSpec.timeout ? stepSpec.timeout.seconds * 1000 : undefined,
352
+ runDir,
353
+ outputPath: resolvedCommandOutputPath,
354
+ }, stepSpec.output?.format);
355
+ persistStep(state, stepName, commandResult, runDir, widgetState, ctx);
356
+ if (commandResult.status === "failed") {
357
+ state.status = "failed";
358
+ return false;
359
+ }
360
+ return true;
361
+ }
362
+ // ── Monitor step ──
363
+ if (stepSpec.monitor) {
364
+ const resolvedInput = stepSpec.input ? resolveExpressions(stepSpec.input, scope) : {};
365
+ const resolvedMonitorOutputPath = stepSpec.output?.path
366
+ ? String(resolveExpressions(stepSpec.output.path, scope))
367
+ : undefined;
368
+ const monitorResult = await executeMonitor(stepSpec.monitor, stepName, resolvedInput, {
369
+ cwd: ctx.cwd,
370
+ ctx,
371
+ signal,
372
+ runDir,
373
+ outputPath: resolvedMonitorOutputPath,
374
+ });
375
+ persistStep(state, stepName, monitorResult, runDir, widgetState, ctx);
376
+ if (monitorResult.status === "failed") {
377
+ state.status = "failed";
378
+ return false;
379
+ }
380
+ return true;
381
+ }
382
+ // ── Transform step ──
383
+ if (stepSpec.transform) {
384
+ const resolvedTransformOutputPath = stepSpec.output?.path
385
+ ? String(resolveExpressions(stepSpec.output.path, scope))
386
+ : undefined;
387
+ const transformResult = executeTransform(stepSpec.transform, stepName, scope, runDir, resolvedTransformOutputPath);
388
+ persistStep(state, stepName, transformResult, runDir, widgetState, ctx);
389
+ if (transformResult.status === "failed") {
390
+ state.status = "failed";
391
+ return false;
392
+ }
393
+ return true;
394
+ }
395
+ // ── Loop step ──
396
+ if (stepSpec.loop) {
397
+ const resolvedLoopOutputPath = stepSpec.output?.path
398
+ ? String(resolveExpressions(stepSpec.output.path, scope))
399
+ : undefined;
400
+ const loopResult = await executeLoop(stepSpec.loop, stepName, state, {
401
+ ctx,
402
+ pi: options.pi,
403
+ signal,
404
+ loadAgent,
405
+ runDir,
406
+ spec,
407
+ dispatchAgent: (s, a, p, o) => (options.dispatchFn ?? dispatch)(s, a, p, { ...o, modelConfig: options.modelConfig }),
408
+ templateEnv: options.templateEnv,
409
+ outputPath: resolvedLoopOutputPath,
410
+ });
411
+ persistStep(state, stepName, loopResult, runDir, widgetState, ctx);
412
+ if (loopResult.status === "failed") {
413
+ state.status = "failed";
414
+ return false;
415
+ }
416
+ return true;
417
+ }
418
+ // ── Parallel step ──
419
+ if (stepSpec.parallel) {
420
+ const parallelResult = await executeParallelStep(stepSpec.parallel, stepName, state, executeSingleStep, options);
421
+ persistStep(state, stepName, parallelResult, runDir, widgetState, ctx);
422
+ if (parallelResult.status === "failed") {
423
+ state.status = "failed";
424
+ return false;
425
+ }
426
+ return true;
427
+ }
428
+ // ── Pause step ──
429
+ if (stepSpec.pause !== undefined) {
430
+ const message = typeof stepSpec.pause === "string" ? stepSpec.pause : undefined;
431
+ const pauseResult = executePause(stepName, message);
432
+ persistStep(state, stepName, pauseResult, runDir, widgetState, ctx);
433
+ state.status = "paused";
434
+ if (ctx.hasUI) {
435
+ ctx.ui.notify(message || "Workflow paused. Use /workflow resume or Ctrl+J to continue.", "info");
436
+ }
437
+ return false;
438
+ }
439
+ // ── Agent step (default) ──
440
+ const agentResult = await executeAgentStep(stepName, stepSpec, state, {
441
+ ctx,
442
+ signal,
443
+ loadAgent,
444
+ runDir,
445
+ specFilePath: spec.filePath,
446
+ widgetState,
447
+ templateEnv: options.templateEnv,
448
+ dispatchFn: options.dispatchFn,
449
+ modelConfig: options.modelConfig,
450
+ retryContext,
451
+ onStepActivity: (activity) => {
452
+ if (!widgetState.activities)
453
+ widgetState.activities = new Map();
454
+ const existing = widgetState.activities.get(stepName) || [];
455
+ if (existing.length >= 5)
456
+ existing.shift();
457
+ existing.push(activity);
458
+ widgetState.activities.set(stepName, existing);
459
+ if (ctx.hasUI) {
460
+ ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
461
+ }
462
+ },
463
+ });
464
+ persistStep(state, stepName, agentResult, runDir, widgetState, ctx);
465
+ if (agentResult.status === "failed") {
466
+ state.status = "failed";
467
+ return false;
468
+ }
469
+ return true;
470
+ }
471
+ /**
472
+ * Execute a workflow from a parsed spec and validated input.
473
+ *
474
+ * Runs steps sequentially (phase 1), resolving ${{ }} expressions,
475
+ * dispatching subprocesses, validating outputs, persisting state,
476
+ * updating TUI, and injecting the result into the conversation.
477
+ *
478
+ * Supports step types: agent (default), gate, transform, loop.
479
+ * Supports `when` conditionals for skipping steps.
480
+ *
481
+ * Returns the WorkflowResult (also injected into conversation via sendMessage).
482
+ */
483
+ export async function executeWorkflow(spec, input, options) {
484
+ const { ctx, pi, signal, loadAgent } = options;
485
+ clearPauseRequest();
486
+ // 1. Validate input against workflow input schema (if defined)
487
+ // Skip on resume — input was validated on first run.
488
+ if (!options.resume && spec.input) {
489
+ validate(spec.input, input, `workflow input for '${spec.name}'`);
490
+ }
491
+ // 2. Initialize or resume run
492
+ let runId;
493
+ let runDir;
494
+ let state;
495
+ if (options.resume) {
496
+ runId = options.resume.runId;
497
+ runDir = options.resume.runDir;
498
+ state = options.resume.state;
499
+ // Reset failed steps so they re-run
500
+ for (const [name, result] of Object.entries(state.steps)) {
501
+ if (result.status === "failed") {
502
+ delete state.steps[name];
503
+ }
504
+ }
505
+ state.status = "running";
506
+ input = state.input; // use original input
507
+ }
508
+ else {
509
+ runId = generateRunId(spec.name);
510
+ runDir = initRunDir(ctx.cwd, spec.name, runId);
511
+ state = {
512
+ input,
513
+ steps: {},
514
+ status: "running",
515
+ workflowName: spec.name,
516
+ specVersion: spec.version,
517
+ startedAt: new Date().toISOString(),
518
+ };
519
+ }
520
+ // 3. Show TUI progress widget
521
+ const widgetState = {
522
+ spec,
523
+ state,
524
+ startTime: Date.now(),
525
+ activities: new Map(),
526
+ outputSummaries: new Map(),
527
+ };
528
+ if (options.resume) {
529
+ widgetState.resumedSteps = Object.keys(state.steps).filter((s) => state.steps[s].status === "completed" || state.steps[s].status === "skipped").length;
530
+ }
531
+ if (ctx.hasUI) {
532
+ ctx.ui.setWidget(WIDGET_ID, createProgressWidget(widgetState));
533
+ }
534
+ // 4. Set working message
535
+ if (ctx.hasUI) {
536
+ ctx.ui.setWorkingMessage(`Running ${spec.name} workflow...`);
537
+ }
538
+ // 5. Load model config (project-level model assignments by role)
539
+ let modelConfig = options.modelConfig;
540
+ if (!modelConfig) {
541
+ try {
542
+ modelConfig = readBlock(ctx.cwd, "model-config");
543
+ }
544
+ catch {
545
+ /* no config file — models come from agent specs or step specs */
546
+ }
547
+ }
548
+ // 6. Build execution plan and execute layers
549
+ const plan = buildConservativePlan(spec);
550
+ const templateEnv = createTemplateEnv(ctx.cwd);
551
+ const stepOpts = {
552
+ ctx,
553
+ pi,
554
+ signal,
555
+ loadAgent,
556
+ runDir,
557
+ spec,
558
+ widgetState,
559
+ templateEnv,
560
+ dispatchFn: options.dispatchFn,
561
+ modelConfig,
562
+ };
563
+ for (const layer of plan) {
564
+ if (signal?.aborted) {
565
+ // Mark first unprocessed step as cancelled
566
+ for (const sn of layer.steps) {
567
+ if (!state.steps[sn]) {
568
+ state.steps[sn] = {
569
+ step: sn,
570
+ agent: spec.steps[sn].agent ?? "",
571
+ status: "failed",
572
+ usage: zeroUsage(),
573
+ durationMs: 0,
574
+ error: "Workflow cancelled",
575
+ };
576
+ }
577
+ }
578
+ state.status = "failed";
579
+ break;
580
+ }
581
+ // Filter to only pending steps in this layer (resume support)
582
+ const pendingSteps = layer.steps.filter((s) => !state.steps[s] || state.steps[s].status === "failed");
583
+ if (pendingSteps.length === 0)
584
+ continue; // entire layer already done
585
+ if (pendingSteps.length === 1) {
586
+ const stepName = pendingSteps[0];
587
+ const stepSpec = spec.steps[stepName];
588
+ const cont = await executeSingleStep(stepName, stepSpec, state, stepOpts);
589
+ if (!cont)
590
+ break;
591
+ }
592
+ else {
593
+ // Run only the pending steps concurrently
594
+ const pendingLayer = { steps: pendingSteps };
595
+ await executeParallelLayer(pendingLayer, spec, state, executeSingleStep, stepOpts);
596
+ if (state.status === "failed" || state.status === "paused")
597
+ break;
598
+ }
599
+ // Check keybinding-initiated pause (between layers/steps)
600
+ if (pauseRequested) {
601
+ pauseRequested = false;
602
+ state.status = "paused";
603
+ try {
604
+ writeState(runDir, state);
605
+ }
606
+ catch (err) {
607
+ const msg = err instanceof Error ? err.message : String(err);
608
+ if (ctx.hasUI) {
609
+ ctx.ui.notify(`Warning: state write failed after pause — resume may not work: ${msg}`, "error");
610
+ }
611
+ }
612
+ if (ctx.hasUI) {
613
+ ctx.ui.notify("Workflow paused. Use /workflow resume or Ctrl+J to continue.", "info");
614
+ }
615
+ break;
616
+ }
617
+ }
618
+ // 5. Finalize
619
+ if (state.status === "running") {
620
+ state.status = "completed";
621
+ }
622
+ try {
623
+ writeState(runDir, state);
624
+ }
625
+ catch (err) {
626
+ const msg = err instanceof Error ? err.message : String(err);
627
+ if (ctx.hasUI) {
628
+ ctx.ui.notify(`Warning: final state write failed — run history may be incomplete: ${msg}`, "error");
629
+ }
630
+ }
631
+ writeMetrics(runDir, state.steps);
632
+ // 6. Process artifacts
633
+ const writtenArtifacts = {};
634
+ if (spec.artifacts) {
635
+ const workflowDir = getWorkflowDir(ctx.cwd, spec.name);
636
+ const artifactScope = {
637
+ input: state.input,
638
+ steps: state.steps,
639
+ runId,
640
+ runDir,
641
+ };
642
+ for (const [name, artifactSpec] of Object.entries(spec.artifacts)) {
643
+ try {
644
+ // Resolve the output path (may contain expressions)
645
+ // Relative paths resolve against the workflow's output directory
646
+ const resolvedPath = String(resolveExpressions(artifactSpec.path, artifactScope));
647
+ const absolutePath = path.isAbsolute(resolvedPath) ? resolvedPath : path.resolve(workflowDir, resolvedPath);
648
+ // Resolve the data source — wrap `from` as ${{ from }} for expression resolution
649
+ const fromExpr = artifactSpec.from.startsWith("${{") ? artifactSpec.from : `\${{ ${artifactSpec.from} }}`;
650
+ const data = resolveExpressions(fromExpr, artifactScope);
651
+ // Validate against schema if specified
652
+ if (artifactSpec.schema) {
653
+ const schemaPath = resolveSchemaPath(artifactSpec.schema, spec.filePath);
654
+ validateFromFile(schemaPath, data, `artifact '${name}'`);
655
+ }
656
+ // Route project block JSON targets through block-api for block-schema validation
657
+ const workflowPrefix = path.join(ctx.cwd, PROJECT_DIR) + path.sep;
658
+ if (absolutePath.startsWith(workflowPrefix) && absolutePath.endsWith(".json")) {
659
+ const blockName = path.basename(absolutePath, ".json");
660
+ writeBlock(ctx.cwd, blockName, data);
661
+ writtenArtifacts[name] = absolutePath;
662
+ }
663
+ else {
664
+ // Write non-block artifacts directly
665
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
666
+ if (typeof data === "string") {
667
+ fs.writeFileSync(absolutePath, data);
668
+ }
669
+ else {
670
+ fs.writeFileSync(absolutePath, JSON.stringify(data, null, 2));
671
+ }
672
+ writtenArtifacts[name] = absolutePath;
673
+ }
674
+ }
675
+ catch (err) {
676
+ // Artifact write failure is non-fatal — log warning, don't fail the workflow
677
+ const msg = err instanceof Error ? err.message : String(err);
678
+ if (ctx.hasUI) {
679
+ ctx.ui.notify(`Artifact '${name}' failed: ${msg}`, "warning");
680
+ }
681
+ }
682
+ }
683
+ }
684
+ // 7. Clean up TUI
685
+ if (ctx.hasUI) {
686
+ ctx.ui.setWidget(WIDGET_ID, undefined);
687
+ ctx.ui.setWorkingMessage(undefined);
688
+ }
689
+ // 8. Build and inject result
690
+ const result = buildResult(spec, runId, runDir, state, state.status);
691
+ // Attach written artifact paths to the result
692
+ if (Object.keys(writtenArtifacts).length > 0) {
693
+ result.artifacts = writtenArtifacts;
694
+ }
695
+ const triggerTurn = spec.triggerTurn !== false;
696
+ if (state.status === "paused") {
697
+ const completedCount = Object.values(state.steps).filter((s) => s.status === "completed").length;
698
+ const totalCount = Object.keys(spec.steps).length;
699
+ const pauseContent = `Workflow '${spec.name}' paused (${completedCount}/${totalCount} steps completed). Use /workflow resume ${spec.name} or Ctrl+J to continue.`;
700
+ pi.sendMessage({ customType: "workflow-result", content: pauseContent, display: "verbose" }, { triggerTurn: false });
701
+ }
702
+ else {
703
+ let content;
704
+ if (spec.completion) {
705
+ try {
706
+ content = resolveCompletion(spec.completion, result, input);
707
+ }
708
+ catch (err) {
709
+ const msg = err instanceof Error ? err.message : String(err);
710
+ content = formatResult(result) + `\n\nCompletion template error: ${msg}`;
711
+ }
712
+ }
713
+ else {
714
+ content = formatResult(result);
715
+ }
716
+ // P7: Truncate completion message to avoid context overflow (50KB / 2000 lines)
717
+ const truncated = truncateTail(content, { maxLines: 2000, maxBytes: 50 * 1024 });
718
+ content = truncated.content;
719
+ if (truncated.truncated) {
720
+ content += `\n\n[Truncated: output exceeded ${truncated.truncatedBy === "bytes" ? "50KB" : "2000 lines"}. Full output in run dir: ${runDir}]`;
721
+ }
722
+ pi.sendMessage({ customType: "workflow-result", content, display: "verbose" }, { triggerTurn });
723
+ }
724
+ return result;
725
+ }
726
+ //# sourceMappingURL=workflow-executor.js.map