@davidorex/pi-workflows 0.2.0 → 0.3.1

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 (216) 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 +178 -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 +724 -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 +171 -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 +63 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +136 -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 +165 -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 +738 -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 +457 -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 +476 -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 +107 -0
  140. package/skills/pi-workflows/SKILL.md +185 -0
  141. package/skills/pi-workflows/references/bundled-resources.md +87 -0
  142. package/templates/architecture-designer/task.md +117 -0
  143. package/templates/architecture-inferrer/task.md +61 -0
  144. package/templates/gap-identifier/task.md +103 -0
  145. package/templates/handoff-writer/task.md +91 -0
  146. package/templates/plan-creator/task.md +143 -0
  147. package/templates/project-definer/task.md +67 -0
  148. package/templates/project-inferrer/task.md +56 -0
  149. package/templates/requirements-gatherer/task.md +90 -0
  150. package/workflows/analyze-existing-project.workflow.yaml +94 -0
  151. package/workflows/create-handoff.workflow.yaml +66 -0
  152. package/workflows/create-phase.workflow.yaml +1 -1
  153. package/workflows/do-gap.workflow.yaml +4 -4
  154. package/workflows/fix-audit.workflow.yaml +1 -1
  155. package/workflows/gap-to-phase.workflow.yaml +1 -1
  156. package/workflows/init-new-project.workflow.yaml +81 -0
  157. package/workflows/plan-from-requirements.workflow.yaml +59 -0
  158. package/workflows/self-implement.workflow.yaml +3 -3
  159. package/workflows/typed-analysis.workflow.yaml +4 -4
  160. package/src/agent-spec.test.ts +0 -289
  161. package/src/agent-spec.ts +0 -122
  162. package/src/block-validation.test.ts +0 -350
  163. package/src/checkpoint.test.ts +0 -237
  164. package/src/checkpoint.ts +0 -140
  165. package/src/completion.test.ts +0 -143
  166. package/src/completion.ts +0 -68
  167. package/src/dag.test.ts +0 -350
  168. package/src/dag.ts +0 -219
  169. package/src/dispatch.test.ts +0 -475
  170. package/src/dispatch.ts +0 -353
  171. package/src/expression.test.ts +0 -353
  172. package/src/expression.ts +0 -332
  173. package/src/format.test.ts +0 -80
  174. package/src/format.ts +0 -41
  175. package/src/graduated-failure.test.ts +0 -556
  176. package/src/index.test.ts +0 -114
  177. package/src/index.ts +0 -516
  178. package/src/loop.test.ts +0 -551
  179. package/src/output.test.ts +0 -213
  180. package/src/output.ts +0 -70
  181. package/src/parallel-integration.test.ts +0 -175
  182. package/src/resume.test.ts +0 -192
  183. package/src/state.test.ts +0 -192
  184. package/src/state.ts +0 -254
  185. package/src/step-agent.test.ts +0 -327
  186. package/src/step-agent.ts +0 -178
  187. package/src/step-command.test.ts +0 -330
  188. package/src/step-command.ts +0 -132
  189. package/src/step-foreach.test.ts +0 -647
  190. package/src/step-foreach.ts +0 -148
  191. package/src/step-gate.test.ts +0 -185
  192. package/src/step-gate.ts +0 -116
  193. package/src/step-loop.test.ts +0 -626
  194. package/src/step-loop.ts +0 -323
  195. package/src/step-parallel.test.ts +0 -475
  196. package/src/step-parallel.ts +0 -168
  197. package/src/step-pause.test.ts +0 -30
  198. package/src/step-shared.test.ts +0 -355
  199. package/src/step-shared.ts +0 -157
  200. package/src/step-transform.test.ts +0 -155
  201. package/src/step-transform.ts +0 -47
  202. package/src/template-integration.test.ts +0 -72
  203. package/src/template.test.ts +0 -162
  204. package/src/test-helpers.ts +0 -51
  205. package/src/tui.test.ts +0 -355
  206. package/src/tui.ts +0 -182
  207. package/src/types.ts +0 -195
  208. package/src/verifier-schema.test.ts +0 -226
  209. package/src/workflow-discovery.test.ts +0 -105
  210. package/src/workflow-discovery.ts +0 -114
  211. package/src/workflow-executor.test.ts +0 -1532
  212. package/src/workflow-executor.ts +0 -810
  213. package/src/workflow-sdk.test.ts +0 -238
  214. package/src/workflow-sdk.ts +0 -262
  215. package/src/workflow-spec.test.ts +0 -576
  216. package/src/workflow-spec.ts +0 -471
@@ -1,471 +0,0 @@
1
- import { parse as parseYaml } from "yaml";
2
- import type { WorkflowSpec, StepSpec, StepOutputSpec, CompletionSpec, ArtifactSpec, LoopSpec, GateSpec, TransformSpec, RetryConfig } from "./types.ts";
3
-
4
- /** Descriptor for a workflow step type — used by SDK and spec validation. */
5
- export interface StepTypeDescriptor {
6
- name: string;
7
- field: string;
8
- retryable: boolean;
9
- supportsInput: boolean;
10
- supportsOutput: boolean;
11
- }
12
-
13
- /** Step type registry — add a step type here, parsing and SDK both see it automatically. */
14
- export const STEP_TYPES: StepTypeDescriptor[] = [
15
- { name: "agent", field: "agent", retryable: true, supportsInput: true, supportsOutput: true },
16
- { name: "gate", field: "gate", retryable: false, supportsInput: false, supportsOutput: false },
17
- { name: "transform", field: "transform", retryable: false, supportsInput: false, supportsOutput: false },
18
- { name: "loop", field: "loop", retryable: true, supportsInput: false, supportsOutput: false },
19
- { name: "parallel", field: "parallel", retryable: true, supportsInput: false, supportsOutput: false },
20
- { name: "pause", field: "pause", retryable: false, supportsInput: false, supportsOutput: false },
21
- { name: "command", field: "command", retryable: false, supportsInput: true, supportsOutput: true },
22
- ];
23
-
24
- /** Set of valid step type field names — derived from STEP_TYPES. */
25
- export const STEP_TYPE_FIELDS = new Set(STEP_TYPES.map(t => t.field));
26
-
27
- /**
28
- * Error class for spec parsing failures.
29
- */
30
- export class WorkflowSpecError extends Error {
31
- readonly filePath: string;
32
- readonly reason: string;
33
-
34
- constructor(filePath: string, reason: string) {
35
- super(`Invalid workflow spec (${filePath}): ${reason}`);
36
- this.name = "WorkflowSpecError";
37
- this.filePath = filePath;
38
- this.reason = reason;
39
- }
40
- }
41
-
42
- /**
43
- * Parse a YAML string into a WorkflowSpec.
44
- * Validates structure (required fields, types).
45
- * Does NOT validate JSON Schemas or resolve agent references — that happens at execution time.
46
- *
47
- * @param content - raw YAML string
48
- * @param filePath - absolute path to the file (stored on the spec, used in error messages)
49
- * @param source - "user" or "project"
50
- * @throws WorkflowSpecError on invalid structure
51
- */
52
- export function parseWorkflowSpec(content: string, filePath: string, source: "user" | "project"): WorkflowSpec {
53
- let doc: unknown;
54
- try {
55
- doc = parseYaml(content);
56
- } catch (err) {
57
- const msg = err instanceof Error ? err.message : String(err);
58
- throw new WorkflowSpecError(filePath, `invalid YAML: ${msg}`);
59
- }
60
-
61
- if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
62
- throw new WorkflowSpecError(filePath, "'name' is required");
63
- }
64
-
65
- const raw = doc as Record<string, unknown>;
66
-
67
- // Validate name
68
- if (!("name" in raw) || raw.name === undefined || raw.name === null) {
69
- throw new WorkflowSpecError(filePath, "'name' is required");
70
- }
71
- if (typeof raw.name !== "string") {
72
- throw new WorkflowSpecError(filePath, "'name' is required");
73
- }
74
-
75
- // Validate steps
76
- if (!("steps" in raw) || raw.steps === undefined || raw.steps === null) {
77
- throw new WorkflowSpecError(filePath, "'steps' must be a non-empty object");
78
- }
79
- if (typeof raw.steps !== "object" || Array.isArray(raw.steps)) {
80
- throw new WorkflowSpecError(filePath, "'steps' must be a non-empty object");
81
- }
82
- const rawSteps = raw.steps as Record<string, unknown>;
83
- if (Object.keys(rawSteps).length === 0) {
84
- throw new WorkflowSpecError(filePath, "'steps' must be a non-empty object");
85
- }
86
-
87
- // Validate each step
88
- const steps: Record<string, StepSpec> = {};
89
- for (const [stepName, stepValue] of Object.entries(rawSteps)) {
90
- steps[stepName] = validateStep(stepValue, stepName, filePath);
91
- }
92
-
93
- // Build the spec with defaults
94
- const spec: WorkflowSpec = {
95
- name: raw.name as string,
96
- description: typeof raw.description === "string" ? raw.description : "",
97
- steps,
98
- source,
99
- filePath,
100
- };
101
-
102
- if (raw.version !== undefined) spec.version = raw.version as string;
103
- if (raw.input !== undefined) spec.input = raw.input as Record<string, unknown>;
104
- if (raw.output !== undefined) spec.output = raw.output as Record<string, unknown>;
105
-
106
- // triggerTurn defaults to true
107
- if (typeof raw.triggerTurn === "boolean") {
108
- spec.triggerTurn = raw.triggerTurn;
109
- } else {
110
- spec.triggerTurn = true;
111
- }
112
-
113
- // completion (optional)
114
- if ("completion" in raw && raw.completion !== undefined) {
115
- if (typeof raw.completion !== "object" || raw.completion === null || Array.isArray(raw.completion)) {
116
- throw new WorkflowSpecError(filePath, "'completion' must be an object");
117
- }
118
- const rawComp = raw.completion as Record<string, unknown>;
119
-
120
- // Mutual exclusivity: template and message cannot coexist
121
- if ("template" in rawComp && "message" in rawComp) {
122
- throw new WorkflowSpecError(filePath, "'completion' cannot have both 'template' and 'message'");
123
- }
124
-
125
- // Must have at least one
126
- if (!("template" in rawComp) && !("message" in rawComp)) {
127
- throw new WorkflowSpecError(filePath, "'completion' must have either 'template' or 'message'");
128
- }
129
-
130
- const completion: CompletionSpec = {};
131
-
132
- if (typeof rawComp.template === "string") {
133
- completion.template = rawComp.template;
134
- } else if ("template" in rawComp) {
135
- throw new WorkflowSpecError(filePath, "'completion.template' must be a string");
136
- }
137
-
138
- if (typeof rawComp.message === "string") {
139
- completion.message = rawComp.message;
140
- } else if ("message" in rawComp) {
141
- throw new WorkflowSpecError(filePath, "'completion.message' must be a string");
142
- }
143
-
144
- if ("include" in rawComp) {
145
- if (!Array.isArray(rawComp.include)) {
146
- throw new WorkflowSpecError(filePath, "'completion.include' must be an array of strings");
147
- }
148
- for (const item of rawComp.include) {
149
- if (typeof item !== "string") {
150
- throw new WorkflowSpecError(filePath, "'completion.include' must be an array of strings");
151
- }
152
- }
153
- completion.include = rawComp.include as string[];
154
- }
155
-
156
- spec.completion = completion;
157
- }
158
-
159
- // artifacts (optional)
160
- if ("artifacts" in raw && raw.artifacts !== undefined) {
161
- if (typeof raw.artifacts !== "object" || raw.artifacts === null || Array.isArray(raw.artifacts)) {
162
- throw new WorkflowSpecError(filePath, "'artifacts' must be an object");
163
- }
164
- const rawArtifacts = raw.artifacts as Record<string, unknown>;
165
- const artifacts: Record<string, ArtifactSpec> = {};
166
- for (const [artName, artValue] of Object.entries(rawArtifacts)) {
167
- if (typeof artValue !== "object" || artValue === null || Array.isArray(artValue)) {
168
- throw new WorkflowSpecError(filePath, `artifact '${artName}' must be an object`);
169
- }
170
- const rawArt = artValue as Record<string, unknown>;
171
- if (typeof rawArt.path !== "string") {
172
- throw new WorkflowSpecError(filePath, `artifact '${artName}' must have a 'path' string`);
173
- }
174
- if (typeof rawArt.from !== "string") {
175
- throw new WorkflowSpecError(filePath, `artifact '${artName}' must have a 'from' string`);
176
- }
177
- const artifact: ArtifactSpec = {
178
- path: rawArt.path,
179
- from: rawArt.from,
180
- };
181
- if (rawArt.schema !== undefined) {
182
- if (typeof rawArt.schema !== "string") {
183
- throw new WorkflowSpecError(filePath, `artifact '${artName}' schema must be a string`);
184
- }
185
- artifact.schema = rawArt.schema;
186
- }
187
- artifacts[artName] = artifact;
188
- }
189
- spec.artifacts = artifacts;
190
- }
191
-
192
- return spec;
193
- }
194
-
195
- /**
196
- * Validate and parse a single step from raw YAML data.
197
- * Enforces that exactly one of agent, gate, transform, or loop is set.
198
- * Rejects steps with `workflow` (not yet supported).
199
- * Recursively validates sub-steps within loops.
200
- */
201
- function validateStep(stepValue: unknown, stepName: string, filePath: string): StepSpec {
202
- if (typeof stepValue !== "object" || stepValue === null || Array.isArray(stepValue)) {
203
- throw new WorkflowSpecError(filePath, `step '${stepName}' must be an object`);
204
- }
205
- const rawStep = stepValue as Record<string, unknown>;
206
-
207
- // Reject workflow (not yet supported)
208
- if ("workflow" in rawStep && rawStep.workflow !== undefined) {
209
- throw new WorkflowSpecError(filePath, `step '${stepName}': nested workflows ('workflow') are not yet supported`);
210
- }
211
-
212
- // Count step types (derived from STEP_TYPES registry)
213
- const presentTypes = STEP_TYPES.filter(t => t.field in rawStep && rawStep[t.field] !== undefined);
214
- const stepTypeList = STEP_TYPES.map(t => t.field).join(", ");
215
-
216
- if (presentTypes.length === 0) {
217
- throw new WorkflowSpecError(filePath, `step '${stepName}' must have exactly one of: ${stepTypeList}`);
218
- }
219
- if (presentTypes.length > 1) {
220
- throw new WorkflowSpecError(filePath, `step '${stepName}' must have exactly one of: ${stepTypeList}`);
221
- }
222
-
223
- // Individual flags for downstream parsing (derived from presentTypes check above)
224
- const hasAgent = presentTypes[0].field === "agent";
225
- const hasGate = presentTypes[0].field === "gate";
226
- const hasTransform = presentTypes[0].field === "transform";
227
- const hasLoop = presentTypes[0].field === "loop";
228
- const hasParallel = presentTypes[0].field === "parallel";
229
- const hasPause = presentTypes[0].field === "pause";
230
- const hasCommand = presentTypes[0].field === "command";
231
-
232
- const step: StepSpec = {};
233
-
234
- // Common optional fields
235
- if (rawStep.when !== undefined) step.when = rawStep.when as string;
236
- if (rawStep.timeout !== undefined) step.timeout = rawStep.timeout as { seconds: number };
237
-
238
- // Retry config (optional)
239
- if ("retry" in rawStep && rawStep.retry !== undefined) {
240
- if (typeof rawStep.retry !== "object" || rawStep.retry === null || Array.isArray(rawStep.retry)) {
241
- throw new WorkflowSpecError(filePath, `step '${stepName}' retry must be an object`);
242
- }
243
- const rawRetry = rawStep.retry as Record<string, unknown>;
244
- const retry: RetryConfig = {};
245
- if ("maxAttempts" in rawRetry) {
246
- if (typeof rawRetry.maxAttempts !== "number" || !Number.isInteger(rawRetry.maxAttempts) || rawRetry.maxAttempts < 1) {
247
- throw new WorkflowSpecError(filePath, `step '${stepName}' retry.maxAttempts must be a positive integer`);
248
- }
249
- retry.maxAttempts = rawRetry.maxAttempts;
250
- }
251
- if ("onExhausted" in rawRetry) {
252
- if (rawRetry.onExhausted !== "fail" && rawRetry.onExhausted !== "skip") {
253
- throw new WorkflowSpecError(filePath, `step '${stepName}' retry.onExhausted must be 'fail' or 'skip'`);
254
- }
255
- retry.onExhausted = rawRetry.onExhausted;
256
- }
257
- if ("steeringMessage" in rawRetry) {
258
- if (typeof rawRetry.steeringMessage !== "string") {
259
- throw new WorkflowSpecError(filePath, `step '${stepName}' retry.steeringMessage must be a string`);
260
- }
261
- retry.steeringMessage = rawRetry.steeringMessage;
262
- }
263
- step.retry = retry;
264
- }
265
-
266
- // forEach and as (can combine with any step type)
267
- if ("forEach" in rawStep && rawStep.forEach !== undefined) {
268
- if (typeof rawStep.forEach !== "string") {
269
- throw new WorkflowSpecError(filePath, `step '${stepName}' forEach must be a string`);
270
- }
271
- step.forEach = rawStep.forEach;
272
- }
273
- if ("as" in rawStep && rawStep.as !== undefined) {
274
- if (typeof rawStep.as !== "string") {
275
- throw new WorkflowSpecError(filePath, `step '${stepName}' as must be a string`);
276
- }
277
- step.as = rawStep.as;
278
- }
279
-
280
- // Pause step
281
- if (hasPause) {
282
- if (typeof rawStep.pause !== "string" && rawStep.pause !== true) {
283
- throw new WorkflowSpecError(filePath, `step '${stepName}' pause must be a string or true`);
284
- }
285
- step.pause = rawStep.pause as string | boolean;
286
- return step;
287
- }
288
-
289
- // Command step
290
- if (hasCommand) {
291
- if (typeof rawStep.command !== "string") {
292
- throw new WorkflowSpecError(filePath, `step '${stepName}' command must be a string`);
293
- }
294
- step.command = rawStep.command;
295
-
296
- // output spec (optional, same as agent)
297
- if ("output" in rawStep && rawStep.output !== undefined) {
298
- if (typeof rawStep.output !== "object" || rawStep.output === null || Array.isArray(rawStep.output)) {
299
- throw new WorkflowSpecError(filePath, `step '${stepName}' output must be an object`);
300
- }
301
- const rawOutput = rawStep.output as Record<string, unknown>;
302
- const output: StepOutputSpec = {};
303
- if ("format" in rawOutput) {
304
- output.format = rawOutput.format as StepOutputSpec["format"];
305
- }
306
- if ("path" in rawOutput) {
307
- if (typeof rawOutput.path !== "string") {
308
- throw new WorkflowSpecError(filePath, `step '${stepName}' output.path must be a string`);
309
- }
310
- output.path = rawOutput.path;
311
- }
312
- step.output = output;
313
- }
314
-
315
- // input (optional, for expression resolution context)
316
- if ("input" in rawStep && rawStep.input !== undefined) {
317
- if (typeof rawStep.input !== "object" || rawStep.input === null || Array.isArray(rawStep.input)) {
318
- throw new WorkflowSpecError(filePath, `step '${stepName}' input must be an object`);
319
- }
320
- step.input = rawStep.input as Record<string, unknown>;
321
- }
322
-
323
- return step;
324
- }
325
-
326
- // Agent step
327
- if (hasAgent) {
328
- if (typeof rawStep.agent !== "string") {
329
- throw new WorkflowSpecError(filePath, `step '${stepName}' agent must be a string`);
330
- }
331
- step.agent = rawStep.agent;
332
-
333
- // model must be a string if present
334
- if ("model" in rawStep && rawStep.model !== undefined) {
335
- if (typeof rawStep.model !== "string") {
336
- throw new WorkflowSpecError(filePath, `step '${stepName}' model must be a string`);
337
- }
338
- step.model = rawStep.model;
339
- }
340
-
341
- // input must be an object if present
342
- if ("input" in rawStep && rawStep.input !== undefined) {
343
- if (typeof rawStep.input !== "object" || rawStep.input === null || Array.isArray(rawStep.input)) {
344
- throw new WorkflowSpecError(filePath, `step '${stepName}' input must be an object`);
345
- }
346
- step.input = rawStep.input as Record<string, unknown>;
347
- }
348
-
349
- // output must be an object if present
350
- if ("output" in rawStep && rawStep.output !== undefined) {
351
- if (typeof rawStep.output !== "object" || rawStep.output === null || Array.isArray(rawStep.output)) {
352
- throw new WorkflowSpecError(filePath, `step '${stepName}' output must be an object`);
353
- }
354
- const rawOutput = rawStep.output as Record<string, unknown>;
355
- const output: StepOutputSpec = {};
356
- if ("format" in rawOutput) {
357
- output.format = rawOutput.format as StepOutputSpec["format"];
358
- }
359
- if ("schema" in rawOutput) {
360
- if (typeof rawOutput.schema !== "string") {
361
- throw new WorkflowSpecError(filePath, `step '${stepName}' output.schema must be a string`);
362
- }
363
- output.schema = rawOutput.schema;
364
- }
365
- if ("path" in rawOutput) {
366
- if (typeof rawOutput.path !== "string") {
367
- throw new WorkflowSpecError(filePath, `step '${stepName}' output.path must be a string`);
368
- }
369
- output.path = rawOutput.path;
370
- }
371
- step.output = output;
372
- }
373
- }
374
-
375
- // Gate step
376
- if (hasGate) {
377
- if (typeof rawStep.gate !== "object" || rawStep.gate === null || Array.isArray(rawStep.gate)) {
378
- throw new WorkflowSpecError(filePath, `step '${stepName}' gate must be an object`);
379
- }
380
- const rawGate = rawStep.gate as Record<string, unknown>;
381
- if (typeof rawGate.check !== "string") {
382
- throw new WorkflowSpecError(filePath, `step '${stepName}' gate must have a 'check' string`);
383
- }
384
- const gate: GateSpec = { check: rawGate.check };
385
- if (rawGate.onPass !== undefined) gate.onPass = rawGate.onPass as GateSpec["onPass"];
386
- if (rawGate.onFail !== undefined) gate.onFail = rawGate.onFail as GateSpec["onFail"];
387
- step.gate = gate;
388
- }
389
-
390
- // Transform step
391
- if (hasTransform) {
392
- if (typeof rawStep.transform !== "object" || rawStep.transform === null || Array.isArray(rawStep.transform)) {
393
- throw new WorkflowSpecError(filePath, `step '${stepName}' transform must be an object`);
394
- }
395
- const rawTransform = rawStep.transform as Record<string, unknown>;
396
- if (typeof rawTransform.mapping !== "object" || rawTransform.mapping === null || Array.isArray(rawTransform.mapping)) {
397
- throw new WorkflowSpecError(filePath, `step '${stepName}' transform must have a 'mapping' object`);
398
- }
399
- step.transform = { mapping: rawTransform.mapping as Record<string, unknown> };
400
- }
401
-
402
- // Loop step
403
- if (hasLoop) {
404
- if (typeof rawStep.loop !== "object" || rawStep.loop === null || Array.isArray(rawStep.loop)) {
405
- throw new WorkflowSpecError(filePath, `step '${stepName}' loop must be an object`);
406
- }
407
- const rawLoop = rawStep.loop as Record<string, unknown>;
408
-
409
- // Must have maxAttempts (number or expression string) or attempts (string expression)
410
- const maxAttemptsRaw = rawLoop.maxAttempts;
411
- const hasMaxAttemptsNum = typeof maxAttemptsRaw === "number";
412
- const hasMaxAttemptsExpr = typeof maxAttemptsRaw === "string";
413
- const hasAttempts = "attempts" in rawLoop && typeof rawLoop.attempts === "string";
414
- if (!hasMaxAttemptsNum && !hasMaxAttemptsExpr && !hasAttempts) {
415
- throw new WorkflowSpecError(filePath, `step '${stepName}' loop must have 'maxAttempts' (number or expression) or 'attempts' (expression)`);
416
- }
417
-
418
- // Must have non-empty steps
419
- if (!("steps" in rawLoop) || typeof rawLoop.steps !== "object" || rawLoop.steps === null || Array.isArray(rawLoop.steps)) {
420
- throw new WorkflowSpecError(filePath, `step '${stepName}' loop must have a non-empty 'steps' object`);
421
- }
422
- const rawLoopSteps = rawLoop.steps as Record<string, unknown>;
423
- if (Object.keys(rawLoopSteps).length === 0) {
424
- throw new WorkflowSpecError(filePath, `step '${stepName}' loop must have a non-empty 'steps' object`);
425
- }
426
-
427
- // Recursively validate sub-steps
428
- const loopSteps: Record<string, StepSpec> = {};
429
- for (const [subName, subValue] of Object.entries(rawLoopSteps)) {
430
- loopSteps[subName] = validateStep(subValue, `${stepName}.loop.${subName}`, filePath);
431
- }
432
-
433
- const loop: LoopSpec = {
434
- maxAttempts: hasMaxAttemptsNum ? maxAttemptsRaw as number : undefined,
435
- steps: loopSteps,
436
- };
437
- // If maxAttempts is an expression string, treat it as the `attempts` field
438
- if (hasMaxAttemptsExpr) {
439
- loop.attempts = maxAttemptsRaw as string;
440
- }
441
- if (hasAttempts) loop.attempts = rawLoop.attempts as string;
442
- if (hasMaxAttemptsNum) loop.maxAttempts = maxAttemptsRaw as number;
443
-
444
- // onExhausted is an optional step
445
- if ("onExhausted" in rawLoop && rawLoop.onExhausted !== undefined) {
446
- loop.onExhausted = validateStep(rawLoop.onExhausted, `${stepName}.loop.onExhausted`, filePath);
447
- }
448
-
449
- step.loop = loop;
450
- }
451
-
452
- // Parallel step
453
- if (hasParallel) {
454
- if (typeof rawStep.parallel !== "object" || rawStep.parallel === null || Array.isArray(rawStep.parallel)) {
455
- throw new WorkflowSpecError(filePath, `step '${stepName}' parallel must be a non-empty object`);
456
- }
457
- const rawParallel = rawStep.parallel as Record<string, unknown>;
458
- if (Object.keys(rawParallel).length === 0) {
459
- throw new WorkflowSpecError(filePath, `step '${stepName}' parallel must be a non-empty object`);
460
- }
461
-
462
- // Recursively validate sub-steps
463
- const parallelSteps: Record<string, StepSpec> = {};
464
- for (const [subName, subValue] of Object.entries(rawParallel)) {
465
- parallelSteps[subName] = validateStep(subValue, `${stepName}.parallel.${subName}`, filePath);
466
- }
467
- step.parallel = parallelSteps;
468
- }
469
-
470
- return step;
471
- }