@s_s/harmonia 1.2.0 → 1.4.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 (180) hide show
  1. package/README.md +140 -392
  2. package/build/cli/setup.d.ts +4 -2
  3. package/build/cli/setup.js +44 -18
  4. package/build/cli/setup.js.map +1 -1
  5. package/build/core/action-registry.d.ts +36 -0
  6. package/build/core/action-registry.js +53 -0
  7. package/build/core/action-registry.js.map +1 -0
  8. package/build/core/artifacts.d.ts +66 -0
  9. package/build/core/artifacts.js +178 -0
  10. package/build/core/artifacts.js.map +1 -0
  11. package/build/core/dispatch.d.ts +18 -11
  12. package/build/core/dispatch.js +43 -33
  13. package/build/core/dispatch.js.map +1 -1
  14. package/build/core/issues.d.ts +37 -0
  15. package/build/core/issues.js +100 -0
  16. package/build/core/issues.js.map +1 -0
  17. package/build/core/overrides.d.ts +19 -26
  18. package/build/core/overrides.js +32 -98
  19. package/build/core/overrides.js.map +1 -1
  20. package/build/core/plugin.d.ts +86 -0
  21. package/build/core/plugin.js +332 -0
  22. package/build/core/plugin.js.map +1 -0
  23. package/build/core/registry.d.ts +36 -3
  24. package/build/core/registry.js +63 -5
  25. package/build/core/registry.js.map +1 -1
  26. package/build/core/reviews.d.ts +13 -13
  27. package/build/core/reviews.js +31 -32
  28. package/build/core/reviews.js.map +1 -1
  29. package/build/core/schema.d.ts +43 -15
  30. package/build/core/schema.js +124 -20
  31. package/build/core/schema.js.map +1 -1
  32. package/build/core/state.d.ts +29 -22
  33. package/build/core/state.js +49 -81
  34. package/build/core/state.js.map +1 -1
  35. package/build/core/steps.d.ts +15 -15
  36. package/build/core/steps.js +32 -33
  37. package/build/core/steps.js.map +1 -1
  38. package/build/core/tree-utils.d.ts +52 -0
  39. package/build/core/tree-utils.js +226 -0
  40. package/build/core/tree-utils.js.map +1 -0
  41. package/build/core/types.d.ts +417 -117
  42. package/build/core/types.js +15 -1
  43. package/build/core/types.js.map +1 -1
  44. package/build/core/workflow-engine.d.ts +68 -0
  45. package/build/core/workflow-engine.js +821 -0
  46. package/build/core/workflow-engine.js.map +1 -0
  47. package/build/core/workflow-validator.d.ts +22 -0
  48. package/build/core/workflow-validator.js +489 -0
  49. package/build/core/workflow-validator.js.map +1 -0
  50. package/build/index.js +28 -25
  51. package/build/index.js.map +1 -1
  52. package/build/setup/inject.d.ts +4 -4
  53. package/build/setup/inject.js +6 -6
  54. package/build/setup/inject.js.map +1 -1
  55. package/build/setup/templates.d.ts +9 -7
  56. package/build/setup/templates.js +68 -103
  57. package/build/setup/templates.js.map +1 -1
  58. package/build/tools/artifact-approve.d.ts +8 -0
  59. package/build/tools/artifact-approve.js +94 -0
  60. package/build/tools/artifact-approve.js.map +1 -0
  61. package/build/tools/artifact-schema.d.ts +12 -0
  62. package/build/tools/artifact-schema.js +148 -0
  63. package/build/tools/artifact-schema.js.map +1 -0
  64. package/build/tools/artifact-tools.d.ts +18 -0
  65. package/build/tools/artifact-tools.js +465 -0
  66. package/build/tools/artifact-tools.js.map +1 -0
  67. package/build/tools/{report-dispatch.d.ts → dispatch-report.d.ts} +7 -3
  68. package/build/tools/dispatch-report.js +261 -0
  69. package/build/tools/dispatch-report.js.map +1 -0
  70. package/build/tools/engine-helpers.d.ts +41 -0
  71. package/build/tools/engine-helpers.js +182 -0
  72. package/build/tools/engine-helpers.js.map +1 -0
  73. package/build/tools/get-project-status.d.ts +6 -4
  74. package/build/tools/get-project-status.js +308 -246
  75. package/build/tools/get-project-status.js.map +1 -1
  76. package/build/tools/get-role-prompt.d.ts +1 -1
  77. package/build/tools/get-role-prompt.js +7 -41
  78. package/build/tools/get-role-prompt.js.map +1 -1
  79. package/build/tools/issue-tools.d.ts +10 -0
  80. package/build/tools/issue-tools.js +169 -0
  81. package/build/tools/issue-tools.js.map +1 -0
  82. package/build/tools/iteration-start.d.ts +7 -4
  83. package/build/tools/iteration-start.js +51 -20
  84. package/build/tools/iteration-start.js.map +1 -1
  85. package/build/tools/loop-done.d.ts +11 -0
  86. package/build/tools/loop-done.js +109 -0
  87. package/build/tools/loop-done.js.map +1 -0
  88. package/build/tools/patch-start.d.ts +16 -0
  89. package/build/tools/patch-start.js +122 -0
  90. package/build/tools/patch-start.js.map +1 -0
  91. package/build/tools/project-init.d.ts +5 -5
  92. package/build/tools/project-init.js +47 -18
  93. package/build/tools/project-init.js.map +1 -1
  94. package/build/tools/role-dispatch.d.ts +55 -0
  95. package/build/tools/role-dispatch.js +508 -0
  96. package/build/tools/role-dispatch.js.map +1 -0
  97. package/build/tools/utils.d.ts +40 -0
  98. package/build/tools/utils.js +97 -0
  99. package/build/tools/utils.js.map +1 -0
  100. package/package.json +1 -1
  101. package/{build/hooks/claude-code.js → workflows/dev/hooks/claude.js} +34 -23
  102. package/{build → workflows/dev}/hooks/content.js +27 -18
  103. package/workflows/dev/hooks/index.js +52 -0
  104. package/{build → workflows/dev}/hooks/openclaw.js +31 -20
  105. package/{build → workflows/dev}/hooks/opencode.js +31 -20
  106. package/workflows/dev/roles/architect.md +68 -28
  107. package/workflows/dev/roles/coordinator.md +103 -0
  108. package/workflows/dev/roles/developer.md +5 -5
  109. package/workflows/dev/roles/tester.md +19 -19
  110. package/workflows/dev/schemas/api-contract.json +42 -0
  111. package/workflows/dev/schemas/api-design.json +30 -13
  112. package/workflows/dev/schemas/data-model.json +20 -7
  113. package/workflows/dev/schemas/prd.completeness-check.json +6 -5
  114. package/workflows/dev/schemas/prd.draft.json +13 -5
  115. package/workflows/dev/schemas/prd.final.json +34 -11
  116. package/workflows/dev/schemas/prd.json +29 -11
  117. package/workflows/dev/schemas/prd.requirements.json +6 -5
  118. package/workflows/dev/schemas/prototype.json +6 -2
  119. package/workflows/dev/schemas/task-breakdown.coarse.json +4 -3
  120. package/workflows/dev/schemas/task-breakdown.dependencies.json +5 -4
  121. package/workflows/dev/schemas/task-breakdown.detailed.json +8 -3
  122. package/workflows/dev/schemas/task-breakdown.final.json +8 -3
  123. package/workflows/dev/schemas/task-breakdown.json +8 -3
  124. package/workflows/dev/schemas/tech-design.analysis.json +6 -5
  125. package/workflows/dev/schemas/tech-design.draft.json +14 -5
  126. package/workflows/dev/schemas/tech-design.final.json +39 -13
  127. package/workflows/dev/schemas/tech-design.json +34 -13
  128. package/workflows/dev/schemas/tech-design.research.json +21 -0
  129. package/workflows/dev/schemas/test-plan.json +17 -7
  130. package/workflows/dev/schemas/test-report.json +26 -9
  131. package/workflows/dev/schemas/user-stories.json +7 -3
  132. package/workflows/dev/tools/index.js +23 -0
  133. package/workflows/dev/workflow.json +234 -101
  134. package/build/core/docs.d.ts +0 -32
  135. package/build/core/docs.js +0 -91
  136. package/build/core/docs.js.map +0 -1
  137. package/build/core/workflow.d.ts +0 -33
  138. package/build/core/workflow.js +0 -140
  139. package/build/core/workflow.js.map +0 -1
  140. package/build/hooks/claude-code.d.ts +0 -20
  141. package/build/hooks/claude-code.js.map +0 -1
  142. package/build/hooks/content.d.ts +0 -43
  143. package/build/hooks/content.js.map +0 -1
  144. package/build/hooks/install.d.ts +0 -40
  145. package/build/hooks/install.js +0 -63
  146. package/build/hooks/install.js.map +0 -1
  147. package/build/hooks/openclaw.d.ts +0 -24
  148. package/build/hooks/openclaw.js.map +0 -1
  149. package/build/hooks/opencode.d.ts +0 -29
  150. package/build/hooks/opencode.js.map +0 -1
  151. package/build/tools/approve-doc.d.ts +0 -6
  152. package/build/tools/approve-doc.js +0 -108
  153. package/build/tools/approve-doc.js.map +0 -1
  154. package/build/tools/dispatch-role.d.ts +0 -16
  155. package/build/tools/dispatch-role.js +0 -277
  156. package/build/tools/dispatch-role.js.map +0 -1
  157. package/build/tools/doc-tools.d.ts +0 -16
  158. package/build/tools/doc-tools.js +0 -389
  159. package/build/tools/doc-tools.js.map +0 -1
  160. package/build/tools/override-tools.d.ts +0 -6
  161. package/build/tools/override-tools.js +0 -129
  162. package/build/tools/override-tools.js.map +0 -1
  163. package/build/tools/report-dispatch.js +0 -194
  164. package/build/tools/report-dispatch.js.map +0 -1
  165. package/build/tools/set-scale.d.ts +0 -6
  166. package/build/tools/set-scale.js +0 -107
  167. package/build/tools/set-scale.js.map +0 -1
  168. package/build/tools/setup-project.d.ts +0 -8
  169. package/build/tools/setup-project.js +0 -116
  170. package/build/tools/setup-project.js.map +0 -1
  171. package/build/tools/update-phase.d.ts +0 -12
  172. package/build/tools/update-phase.js +0 -159
  173. package/build/tools/update-phase.js.map +0 -1
  174. package/workflows/dev/roles/pm.md +0 -99
  175. package/workflows/dev/schemas/deploy.json +0 -20
  176. package/workflows/dev/schemas/fsd.json +0 -25
  177. package/workflows/dev/schemas/project-plan.json +0 -20
  178. package/workflows/dev/schemas/retrospective.json +0 -20
  179. package/workflows/dev/schemas/risk-assessment.json +0 -15
  180. package/workflows/dev/schemas/tech-design.api-contract.json +0 -20
@@ -0,0 +1,821 @@
1
+ /**
2
+ * Workflow engine — core state machine for node-based workflow execution.
3
+ *
4
+ * This module handles:
5
+ * - Node state initialization from a WorkflowDefinition
6
+ * - State transitions based on WorkflowEvents
7
+ * - NextAction computation (telling the coordinator what to do)
8
+ * - Gate evaluation
9
+ * - Goto execution (state reset + retry tracking)
10
+ * - Failure handling (onFailed, bubbling, onExhausted)
11
+ *
12
+ * Key insight: Core is passive (MCP server). Every interaction is
13
+ * coordinator-driven. Engine computes state changes and returns nextAction,
14
+ * but never proactively pushes the workflow forward.
15
+ */
16
+ import { findNodeInTree, collectAllNodeIds, findParent, collectSubsequentNodeIds, } from './tree-utils.js';
17
+ // ─── Node State Initialization ───
18
+ /**
19
+ * Initialize node states for all nodes in a workflow definition.
20
+ * All nodes start as 'pending', except the first actionable node
21
+ * which will be activated when the workflow begins.
22
+ */
23
+ export function initNodeStates(definition) {
24
+ const states = {};
25
+ collectNodeStates(definition.root, states);
26
+ if (definition.floatingNodes) {
27
+ for (const fn of definition.floatingNodes) {
28
+ states[fn.id] = createNodeState(fn.id);
29
+ }
30
+ }
31
+ return states;
32
+ }
33
+ function collectNodeStates(node, states) {
34
+ states[node.id] = createNodeState(node.id);
35
+ switch (node.type) {
36
+ case 'sequence':
37
+ case 'parallel':
38
+ for (const child of node.children) {
39
+ collectNodeStates(child, states);
40
+ }
41
+ break;
42
+ case 'gate':
43
+ collectNodeStates(node.pass, states);
44
+ if ('type' in node.fail) {
45
+ collectNodeStates(node.fail, states);
46
+ }
47
+ break;
48
+ case 'loop':
49
+ collectNodeStates(node.body, states);
50
+ break;
51
+ case 'task':
52
+ break;
53
+ }
54
+ }
55
+ function createNodeState(id) {
56
+ return {
57
+ id,
58
+ status: 'pending',
59
+ retryCount: 0,
60
+ };
61
+ }
62
+ // ─── Start Workflow ───
63
+ /**
64
+ * Start the workflow — activate the root node and return the first nextAction.
65
+ */
66
+ export function startWorkflow(definition, state, context) {
67
+ const now = new Date().toISOString();
68
+ const newState = { ...state, nodes: { ...state.nodes }, updatedAt: now };
69
+ return activateNode(definition.root, definition, newState, context);
70
+ }
71
+ // ─── Compute Next Action ───
72
+ /**
73
+ * Process a workflow event and compute the next action.
74
+ * This is the main entry point for all state transitions.
75
+ */
76
+ export function computeNextAction(definition, state, event, context) {
77
+ const now = new Date().toISOString();
78
+ const newState = { ...state, nodes: { ...state.nodes }, updatedAt: now };
79
+ switch (event.type) {
80
+ case 'node_completed':
81
+ return completeNode(newState, definition, event.nodeId, context, event.result);
82
+ case 'node_failed':
83
+ return failNode(newState, definition, event.nodeId, event.error, context);
84
+ case 'artifact_written':
85
+ case 'artifact_approved':
86
+ // These events might trigger gate re-evaluation
87
+ return reevaluateGates(newState, definition, context);
88
+ case 'dispatch_requested':
89
+ return handleDispatchRequest(newState, definition, event.nodeId, context);
90
+ case 'loop_done': {
91
+ // Mark the loop as done — it will terminate after current iteration completes
92
+ const loopState = newState.nodes[event.nodeId];
93
+ if (!loopState || loopState.status !== 'active') {
94
+ return {
95
+ state: newState,
96
+ nextAction: {
97
+ type: 'wait',
98
+ instructions: `Loop node "${event.nodeId}" is not active (current: ${loopState?.status ?? 'unknown'})`,
99
+ },
100
+ };
101
+ }
102
+ loopState.done = true;
103
+ newState.nodes[event.nodeId] = loopState;
104
+ return {
105
+ state: newState,
106
+ nextAction: {
107
+ type: 'wait',
108
+ nodeId: event.nodeId,
109
+ instructions: `Loop "${event.nodeId}" marked for termination. Current iteration (${loopState.currentIteration}) will complete, then the loop will end.`,
110
+ },
111
+ };
112
+ }
113
+ case 'query_status':
114
+ return { state: newState, nextAction: computeStatusAction(newState, definition, context) };
115
+ }
116
+ }
117
+ // ─── Node Activation ───
118
+ /**
119
+ * Activate a node — set it to 'active' and determine what action to take.
120
+ */
121
+ function activateNode(node, definition, state, context, gateResults) {
122
+ const now = new Date().toISOString();
123
+ state.nodes[node.id] = {
124
+ ...state.nodes[node.id],
125
+ status: 'active',
126
+ startedAt: now,
127
+ };
128
+ switch (node.type) {
129
+ case 'task':
130
+ return activateTask(node, state, context, gateResults);
131
+ case 'sequence':
132
+ return activateSequence(node, definition, state, context);
133
+ case 'parallel':
134
+ return activateParallel(node, definition, state, context);
135
+ case 'gate':
136
+ return activateGate(node, definition, state, context);
137
+ case 'loop':
138
+ return activateLoop(node, definition, state, context);
139
+ }
140
+ }
141
+ function activateTask(node, state, context, gateResults) {
142
+ state.activeNodeId = node.id;
143
+ const rolePrompt = context.getRolePrompt ? context.getRolePrompt(node.role, node.id, gateResults) : undefined;
144
+ return {
145
+ state,
146
+ nextAction: {
147
+ type: 'dispatch',
148
+ nodeId: node.id,
149
+ role: node.role,
150
+ instructions: `Dispatch role "${node.role}" for task "${node.id}"`,
151
+ rolePrompt,
152
+ gateResults,
153
+ },
154
+ };
155
+ }
156
+ function activateSequence(node, definition, state, context) {
157
+ if (node.children.length === 0) {
158
+ // Empty sequence — immediately complete
159
+ return markCompleted(node, definition, state, context);
160
+ }
161
+ // Activate the first child
162
+ return activateNode(node.children[0], definition, state, context);
163
+ }
164
+ function activateParallel(node, definition, state, context) {
165
+ if (node.children.length === 0) {
166
+ return markCompleted(node, definition, state, context);
167
+ }
168
+ // Activate all children simultaneously
169
+ for (const child of node.children) {
170
+ const result = activateNode(child, definition, state, context);
171
+ state = result.state;
172
+ }
173
+ // Build parallel dispatch list with each child's nodeId and role
174
+ const dispatchActions = node.children
175
+ .filter((c) => c.type === 'task')
176
+ .map((c) => ({
177
+ nodeId: c.id,
178
+ role: c.role,
179
+ }));
180
+ return {
181
+ state,
182
+ nextAction: {
183
+ type: 'dispatch',
184
+ nodeId: node.id,
185
+ instructions: `Parallel execution: dispatch ${dispatchActions.length} tasks simultaneously: ${dispatchActions.map((d) => `${d.role}(${d.nodeId})`).join(', ')}`,
186
+ role: dispatchActions[0]?.role,
187
+ parallelDispatch: dispatchActions,
188
+ },
189
+ };
190
+ }
191
+ function activateGate(node, definition, state, context) {
192
+ const evaluation = evaluateGate(node, context.gate);
193
+ if (evaluation.passed) {
194
+ // Gate passed — activate pass branch
195
+ state.nodes[node.id] = {
196
+ ...state.nodes[node.id],
197
+ status: 'completed',
198
+ completedAt: new Date().toISOString(),
199
+ };
200
+ return activateNode(node.pass, definition, state, context);
201
+ }
202
+ // Gate failed
203
+ if ('goto' in node.fail && !('type' in node.fail)) {
204
+ // fail is a GotoTarget
205
+ const gotoTarget = node.fail;
206
+ return handleGoto(gotoTarget, node.id, definition, state, context, evaluation);
207
+ }
208
+ else {
209
+ // fail is an inline WorkflowNode
210
+ state.nodes[node.id] = {
211
+ ...state.nodes[node.id],
212
+ status: 'completed',
213
+ completedAt: new Date().toISOString(),
214
+ };
215
+ return activateNode(node.fail, definition, state, context, evaluation);
216
+ }
217
+ }
218
+ function activateLoop(node, definition, state, context) {
219
+ // Initialize loop state with iteration tracking
220
+ const loopState = {
221
+ ...state.nodes[node.id],
222
+ status: 'active',
223
+ startedAt: new Date().toISOString(),
224
+ currentIteration: 0,
225
+ done: false,
226
+ };
227
+ state.nodes[node.id] = loopState;
228
+ // Activate the body node
229
+ return activateNode(node.body, definition, state, context);
230
+ }
231
+ // ─── Node Completion ───
232
+ /**
233
+ * Handle node completion: mark as completed, advance to next node.
234
+ */
235
+ export function completeNode(state, definition, nodeId, context, result) {
236
+ const now = new Date().toISOString();
237
+ state.nodes[nodeId] = {
238
+ ...state.nodes[nodeId],
239
+ status: 'completed',
240
+ completedAt: now,
241
+ };
242
+ // Find the parent node to determine what happens next
243
+ const parentInfo = findParent(definition.root, nodeId);
244
+ if (!parentInfo) {
245
+ // This is the root node completing — workflow is done
246
+ return {
247
+ state,
248
+ nextAction: {
249
+ type: 'completed',
250
+ instructions: 'Workflow completed successfully',
251
+ },
252
+ };
253
+ }
254
+ const { parent } = parentInfo;
255
+ switch (parent.type) {
256
+ case 'sequence':
257
+ return handleSequenceChildComplete(parent, nodeId, definition, state, context);
258
+ case 'parallel':
259
+ return handleParallelChildComplete(parent, definition, state, context);
260
+ case 'gate':
261
+ // Pass/fail branch task completed — gate's work is done
262
+ // Mark gate as completed and check parent
263
+ return markCompleted(parent, definition, state, context);
264
+ case 'loop':
265
+ return handleLoopBodyComplete(parent, definition, state, context);
266
+ default:
267
+ return {
268
+ state,
269
+ nextAction: {
270
+ type: 'wait',
271
+ instructions: `Node "${nodeId}" completed, but could not determine next step`,
272
+ },
273
+ };
274
+ }
275
+ }
276
+ function handleSequenceChildComplete(sequence, completedChildId, definition, state, context) {
277
+ const childIndex = sequence.children.findIndex((c) => c.id === completedChildId);
278
+ if (childIndex < 0) {
279
+ return {
280
+ state,
281
+ nextAction: {
282
+ type: 'wait',
283
+ instructions: `Child "${completedChildId}" not found in sequence "${sequence.id}"`,
284
+ },
285
+ };
286
+ }
287
+ // Check if there's a next child
288
+ const nextIndex = childIndex + 1;
289
+ if (nextIndex < sequence.children.length) {
290
+ // Activate next child
291
+ return activateNode(sequence.children[nextIndex], definition, state, context);
292
+ }
293
+ // All children completed — mark sequence as completed
294
+ return markCompleted(sequence, definition, state, context);
295
+ }
296
+ function handleParallelChildComplete(parallel, definition, state, context) {
297
+ // Check if all children are done
298
+ const allDone = parallel.children.every((c) => {
299
+ const childState = state.nodes[c.id];
300
+ return childState?.status === 'completed' || childState?.status === 'failed';
301
+ });
302
+ if (!allDone) {
303
+ return {
304
+ state,
305
+ nextAction: {
306
+ type: 'wait',
307
+ nodeId: parallel.id,
308
+ instructions: `Parallel node "${parallel.id}": waiting for remaining tasks to complete`,
309
+ },
310
+ };
311
+ }
312
+ // Check for failures
313
+ const failures = parallel.children.filter((c) => state.nodes[c.id]?.status === 'failed');
314
+ if (failures.length > 0) {
315
+ // Parallel has failures — treat as failed
316
+ return failNode(state, definition, parallel.id, `${failures.length} child node(s) failed: ${failures.map((f) => f.id).join(', ')}`, context);
317
+ }
318
+ // All succeeded
319
+ return markCompleted(parallel, definition, state, context);
320
+ }
321
+ function handleLoopBodyComplete(loop, definition, state, context) {
322
+ const loopState = state.nodes[loop.id];
323
+ // Check if loop_done has been called — terminate loop
324
+ if (loopState.done) {
325
+ return markCompleted(loop, definition, state, context);
326
+ }
327
+ // Check if maxIterations reached — safety cap, mark as failed
328
+ const nextIteration = loopState.currentIteration + 1;
329
+ if (nextIteration >= loop.maxIterations) {
330
+ return failNode(state, definition, loop.id, `Loop "${loop.id}" reached maxIterations (${loop.maxIterations}) without loop_done being called`, context);
331
+ }
332
+ // Continue: increment iteration, reset body, re-activate
333
+ loopState.currentIteration = nextIteration;
334
+ state.nodes[loop.id] = loopState;
335
+ // Reset all body nodes to pending
336
+ resetLoopBody(state, loop);
337
+ // Re-activate body
338
+ return activateNode(loop.body, definition, state, context);
339
+ }
340
+ /**
341
+ * Reset all nodes within a loop's body subtree to pending state.
342
+ * Used between loop iterations.
343
+ */
344
+ function resetLoopBody(state, loopNode) {
345
+ const bodyIds = new Set();
346
+ collectAllNodeIds(loopNode.body, bodyIds);
347
+ bodyIds.forEach((id) => {
348
+ const existing = state.nodes[id];
349
+ if (existing) {
350
+ state.nodes[id] = {
351
+ id,
352
+ status: 'pending',
353
+ retryCount: 0,
354
+ };
355
+ }
356
+ });
357
+ }
358
+ /**
359
+ * Mark a node as completed and propagate up.
360
+ */
361
+ function markCompleted(node, definition, state, context) {
362
+ const now = new Date().toISOString();
363
+ state.nodes[node.id] = {
364
+ ...state.nodes[node.id],
365
+ status: 'completed',
366
+ completedAt: now,
367
+ };
368
+ // Propagate up — find parent and handle completion there
369
+ const parentInfo = findParent(definition.root, node.id);
370
+ if (!parentInfo) {
371
+ // Root completed
372
+ return {
373
+ state,
374
+ nextAction: {
375
+ type: 'completed',
376
+ instructions: 'Workflow completed successfully',
377
+ },
378
+ };
379
+ }
380
+ return completeNode(state, definition, node.id, context);
381
+ }
382
+ // ─── Node Failure Handling ───
383
+ /**
384
+ * Handle node failure: check onFailed handler, process goto or bubble up.
385
+ *
386
+ * Failure flow:
387
+ * 1. If node has onFailed with goto → try goto (with retry tracking)
388
+ * 2. If goto maxRetries exhausted → jump to onExhausted floating node
389
+ * 3. If no onFailed → bubble failure up to parent
390
+ */
391
+ function failNode(state, definition, nodeId, error, context) {
392
+ const now = new Date().toISOString();
393
+ state.nodes[nodeId] = {
394
+ ...state.nodes[nodeId],
395
+ status: 'failed',
396
+ completedAt: now,
397
+ error,
398
+ };
399
+ // Find the node to check for onFailed
400
+ const node = findNode(definition.root, nodeId, definition.floatingNodes);
401
+ if (!node) {
402
+ return {
403
+ state,
404
+ nextAction: {
405
+ type: 'wait',
406
+ instructions: `Node "${nodeId}" failed but could not be found in definition: ${error}`,
407
+ },
408
+ };
409
+ }
410
+ // Check if the node has onFailed handler
411
+ const failureHandler = getFailureHandler(node);
412
+ if (failureHandler) {
413
+ return handleFailureHandler(failureHandler, nodeId, definition, state, context, error);
414
+ }
415
+ // No onFailed — bubble failure up to parent
416
+ return bubbleFailure(nodeId, error, definition, state, context);
417
+ }
418
+ /**
419
+ * Get the failure handler from a node (only task and parallel have onFailed).
420
+ */
421
+ function getFailureHandler(node) {
422
+ if (node.type === 'task' || node.type === 'parallel' || node.type === 'loop') {
423
+ return node.onFailed;
424
+ }
425
+ return undefined;
426
+ }
427
+ /**
428
+ * Process a FailureHandler (goto with retry tracking).
429
+ */
430
+ function handleFailureHandler(handler, failedNodeId, definition, state, context, error) {
431
+ // Check retry count for the goto target
432
+ const targetState = state.nodes[handler.goto];
433
+ const currentRetries = targetState?.retryCount ?? 0;
434
+ if (handler.maxRetries !== undefined && currentRetries >= handler.maxRetries) {
435
+ // Retries exhausted
436
+ if (handler.onExhausted) {
437
+ return activateFloatingNode(handler.onExhausted, definition, state, context, error);
438
+ }
439
+ // No onExhausted — bubble up
440
+ return bubbleFailure(failedNodeId, error, definition, state, context);
441
+ }
442
+ // Execute goto
443
+ return handleGoto({ goto: handler.goto, maxRetries: handler.maxRetries, onExhausted: handler.onExhausted }, failedNodeId, definition, state, context);
444
+ }
445
+ /**
446
+ * Bubble a failure up to the parent node.
447
+ */
448
+ function bubbleFailure(failedNodeId, error, definition, state, context) {
449
+ const parentInfo = findParent(definition.root, failedNodeId);
450
+ if (!parentInfo) {
451
+ // Root node failed — workflow failed
452
+ return {
453
+ state,
454
+ nextAction: {
455
+ type: 'failed',
456
+ instructions: `Workflow failed: ${error}`,
457
+ },
458
+ };
459
+ }
460
+ const { parent } = parentInfo;
461
+ if (parent.type === 'parallel') {
462
+ if (parent.failStrategy === 'fail-fast') {
463
+ // Cancel remaining active children
464
+ for (const child of parent.children) {
465
+ const childState = state.nodes[child.id];
466
+ if (childState && childState.status === 'active') {
467
+ state.nodes[child.id] = {
468
+ ...childState,
469
+ status: 'cancelled',
470
+ completedAt: new Date().toISOString(),
471
+ };
472
+ }
473
+ }
474
+ // Fail the parallel node itself
475
+ return failNode(state, definition, parent.id, error, context);
476
+ }
477
+ // wait-all: check if all children are done
478
+ const allDone = parent.children.every((c) => {
479
+ const cs = state.nodes[c.id];
480
+ return cs?.status === 'completed' || cs?.status === 'failed' || cs?.status === 'cancelled';
481
+ });
482
+ if (allDone) {
483
+ return failNode(state, definition, parent.id, error, context);
484
+ }
485
+ // Still waiting for others
486
+ return {
487
+ state,
488
+ nextAction: {
489
+ type: 'wait',
490
+ nodeId: parent.id,
491
+ instructions: `Parallel node "${parent.id}": child "${failedNodeId}" failed, waiting for remaining tasks (wait-all strategy)`,
492
+ },
493
+ };
494
+ }
495
+ // For sequence parents, failure in a child means the sequence fails
496
+ return failNode(state, definition, parent.id, error, context);
497
+ }
498
+ /**
499
+ * Activate a floating node (used by onExhausted).
500
+ */
501
+ function activateFloatingNode(floatingNodeId, definition, state, context, error) {
502
+ const floatingNode = definition.floatingNodes?.find((fn) => fn.id === floatingNodeId);
503
+ if (!floatingNode) {
504
+ return {
505
+ state,
506
+ nextAction: {
507
+ type: 'wait',
508
+ instructions: `Floating node "${floatingNodeId}" not found. Retries exhausted. Original error: ${error}`,
509
+ },
510
+ };
511
+ }
512
+ return activateNode(floatingNode, definition, state, context);
513
+ }
514
+ // ─── Goto Handling ───
515
+ /**
516
+ * Execute a goto: reset the target node and all subsequent nodes to pending,
517
+ * increment retry count on the target, then re-activate the target.
518
+ */
519
+ function handleGoto(gotoTarget, sourceNodeId, definition, state, context, gateResults) {
520
+ const targetId = gotoTarget.goto;
521
+ // Check retry limit before proceeding
522
+ const targetState = state.nodes[targetId];
523
+ const currentRetries = targetState?.retryCount ?? 0;
524
+ if (gotoTarget.maxRetries !== undefined && currentRetries >= gotoTarget.maxRetries) {
525
+ // Retries exhausted
526
+ if (gotoTarget.onExhausted) {
527
+ return activateFloatingNode(gotoTarget.onExhausted, definition, state, context, `Goto target "${targetId}" exhausted retries (${currentRetries}/${gotoTarget.maxRetries})`);
528
+ }
529
+ // No onExhausted — report as failed
530
+ return {
531
+ state,
532
+ nextAction: {
533
+ type: 'failed',
534
+ instructions: `Goto target "${targetId}" exhausted retries (${currentRetries}/${gotoTarget.maxRetries}). No onExhausted handler configured.`,
535
+ },
536
+ };
537
+ }
538
+ // Reset target and subsequent nodes
539
+ state = executeGoto(state, definition, targetId);
540
+ // Find the target node and activate it
541
+ const targetNode = findNode(definition.root, targetId, definition.floatingNodes);
542
+ if (!targetNode) {
543
+ return {
544
+ state,
545
+ nextAction: {
546
+ type: 'wait',
547
+ instructions: `Goto target node "${targetId}" not found in definition`,
548
+ },
549
+ };
550
+ }
551
+ return activateNode(targetNode, definition, state, context, gateResults);
552
+ }
553
+ /**
554
+ * Execute goto state reset: reset target node and all subsequent nodes to pending.
555
+ * Increment retry count on the target node.
556
+ */
557
+ function executeGoto(state, definition, targetId) {
558
+ // Collect all node IDs that need to be reset:
559
+ // the target node itself + all nodes that come after it in execution order
560
+ const resetIds = collectSubsequentNodeIds(definition.root, targetId);
561
+ resetIds.add(targetId);
562
+ const now = new Date().toISOString();
563
+ resetIds.forEach((id) => {
564
+ const existing = state.nodes[id];
565
+ if (existing) {
566
+ const isTarget = id === targetId;
567
+ state.nodes[id] = {
568
+ id,
569
+ status: 'pending',
570
+ retryCount: isTarget ? existing.retryCount + 1 : 0,
571
+ // Clear timing fields
572
+ };
573
+ }
574
+ });
575
+ state.updatedAt = now;
576
+ return state;
577
+ }
578
+ /**
579
+ // ─── Gate Evaluation ───
580
+
581
+ /**
582
+ * Evaluate all conditions for a gate node.
583
+ * All conditions must pass for the gate to pass (AND logic).
584
+ */
585
+ export function evaluateGate(gate, gateCtx) {
586
+ const results = gate.conditions.map((condition) => evaluateCondition(condition, gateCtx));
587
+ return {
588
+ passed: results.every((r) => r.met),
589
+ conditions: results,
590
+ };
591
+ }
592
+ /**
593
+ * Evaluate a single gate condition.
594
+ */
595
+ function evaluateCondition(condition, ctx) {
596
+ switch (condition.type) {
597
+ case 'artifact_exists': {
598
+ const exists = ctx.artifactExists(condition.artifact);
599
+ return { condition, met: exists };
600
+ }
601
+ case 'artifact_approved': {
602
+ const approved = ctx.artifactApproved(condition.artifact);
603
+ return { condition, met: approved };
604
+ }
605
+ case 'artifact_field': {
606
+ const value = ctx.artifactField(condition.artifact, condition.field);
607
+ const met = evaluateFieldCondition(value, condition.operator, condition.value);
608
+ return { condition, met, actualValue: value };
609
+ }
610
+ }
611
+ }
612
+ /**
613
+ * Evaluate a field comparison operation.
614
+ */
615
+ function evaluateFieldCondition(actualValue, operator, expectedValue) {
616
+ switch (operator) {
617
+ case 'eq':
618
+ return actualValue === expectedValue;
619
+ case 'neq':
620
+ return actualValue !== expectedValue;
621
+ case 'gt':
622
+ return typeof actualValue === 'number' && typeof expectedValue === 'number' && actualValue > expectedValue;
623
+ case 'lt':
624
+ return typeof actualValue === 'number' && typeof expectedValue === 'number' && actualValue < expectedValue;
625
+ case 'gte':
626
+ return typeof actualValue === 'number' && typeof expectedValue === 'number' && actualValue >= expectedValue;
627
+ case 'lte':
628
+ return typeof actualValue === 'number' && typeof expectedValue === 'number' && actualValue <= expectedValue;
629
+ case 'contains':
630
+ if (typeof actualValue === 'string' && typeof expectedValue === 'string') {
631
+ return actualValue.includes(expectedValue);
632
+ }
633
+ if (Array.isArray(actualValue)) {
634
+ return actualValue.includes(expectedValue);
635
+ }
636
+ return false;
637
+ case 'in':
638
+ if (Array.isArray(expectedValue)) {
639
+ return expectedValue.includes(actualValue);
640
+ }
641
+ return false;
642
+ default:
643
+ return false;
644
+ }
645
+ }
646
+ // ─── Gate Re-evaluation ───
647
+ /**
648
+ * Re-evaluate all active gate nodes after an artifact event.
649
+ * If a gate that was previously waiting (status='active') now passes,
650
+ * activate its pass branch.
651
+ */
652
+ function reevaluateGates(state, definition, context) {
653
+ // Find all active gate nodes
654
+ const activeGates = findActiveGates(definition.root, state);
655
+ for (const gate of activeGates) {
656
+ const evaluation = evaluateGate(gate, context.gate);
657
+ if (evaluation.passed) {
658
+ // Gate now passes — mark it completed and activate pass branch
659
+ state.nodes[gate.id] = {
660
+ ...state.nodes[gate.id],
661
+ status: 'completed',
662
+ completedAt: new Date().toISOString(),
663
+ };
664
+ return activateNode(gate.pass, definition, state, context);
665
+ }
666
+ }
667
+ // No gate changed — return wait
668
+ return {
669
+ state,
670
+ nextAction: {
671
+ type: 'wait',
672
+ instructions: 'Artifact event processed. No gate conditions newly satisfied.',
673
+ },
674
+ };
675
+ }
676
+ /**
677
+ * Find all gate nodes that are currently in 'active' state.
678
+ */
679
+ function findActiveGates(node, state) {
680
+ const gates = [];
681
+ if (node.type === 'gate' && state.nodes[node.id]?.status === 'active') {
682
+ gates.push(node);
683
+ }
684
+ switch (node.type) {
685
+ case 'sequence':
686
+ case 'parallel':
687
+ for (const child of node.children) {
688
+ gates.push(...findActiveGates(child, state));
689
+ }
690
+ break;
691
+ case 'gate':
692
+ gates.push(...findActiveGates(node.pass, state));
693
+ if ('type' in node.fail) {
694
+ gates.push(...findActiveGates(node.fail, state));
695
+ }
696
+ break;
697
+ case 'loop':
698
+ if (state.nodes[node.id]?.status === 'active') {
699
+ gates.push(...findActiveGates(node.body, state));
700
+ }
701
+ break;
702
+ }
703
+ return gates;
704
+ }
705
+ // ─── Dispatch Request Handling ───
706
+ /**
707
+ * Handle an explicit dispatch request for a specific node.
708
+ * Used when the coordinator explicitly requests to dispatch a node
709
+ * (e.g., for parallel tasks that need individual dispatching).
710
+ */
711
+ function handleDispatchRequest(state, definition, nodeId, context) {
712
+ const node = findNode(definition.root, nodeId, definition.floatingNodes);
713
+ if (!node) {
714
+ return {
715
+ state,
716
+ nextAction: {
717
+ type: 'wait',
718
+ instructions: `Node "${nodeId}" not found in workflow definition`,
719
+ },
720
+ };
721
+ }
722
+ const nodeState = state.nodes[nodeId];
723
+ if (!nodeState || nodeState.status !== 'active') {
724
+ return {
725
+ state,
726
+ nextAction: {
727
+ type: 'wait',
728
+ instructions: `Node "${nodeId}" is not in 'active' state (current: ${nodeState?.status ?? 'unknown'})`,
729
+ },
730
+ };
731
+ }
732
+ if (node.type !== 'task') {
733
+ return {
734
+ state,
735
+ nextAction: {
736
+ type: 'wait',
737
+ instructions: `Node "${nodeId}" is a ${node.type} node, not a task. Only task nodes can be dispatched.`,
738
+ },
739
+ };
740
+ }
741
+ // Return dispatch action for this task
742
+ return activateTask(node, state, context);
743
+ }
744
+ // ─── Status Query ───
745
+ /**
746
+ * Compute the current status nextAction without modifying state.
747
+ * Used for query_status events — tells the coordinator what to do next
748
+ * based on the current workflow state.
749
+ */
750
+ function computeStatusAction(state, definition, context) {
751
+ // Collect all node statuses for a summary
752
+ const activeNodes = [];
753
+ const pendingNodes = [];
754
+ const failedNodes = [];
755
+ const completedNodes = [];
756
+ for (const [id, nodeState] of Object.entries(state.nodes)) {
757
+ switch (nodeState.status) {
758
+ case 'active':
759
+ activeNodes.push(id);
760
+ break;
761
+ case 'pending':
762
+ pendingNodes.push(id);
763
+ break;
764
+ case 'failed':
765
+ failedNodes.push(id);
766
+ break;
767
+ case 'completed':
768
+ completedNodes.push(id);
769
+ break;
770
+ }
771
+ }
772
+ // Check if workflow is completed (root node completed)
773
+ const rootState = state.nodes[definition.root.id];
774
+ if (rootState?.status === 'completed') {
775
+ return {
776
+ type: 'completed',
777
+ instructions: 'Workflow completed successfully',
778
+ };
779
+ }
780
+ if (rootState?.status === 'failed') {
781
+ return {
782
+ type: 'failed',
783
+ instructions: `Workflow failed. Failed nodes: ${failedNodes.join(', ')}`,
784
+ };
785
+ }
786
+ // If there's an active task node, suggest dispatching it
787
+ if (state.activeNodeId) {
788
+ const activeNode = findNode(definition.root, state.activeNodeId, definition.floatingNodes);
789
+ if (activeNode?.type === 'task') {
790
+ return {
791
+ type: 'dispatch',
792
+ nodeId: state.activeNodeId,
793
+ role: activeNode.role,
794
+ instructions: `Current active task: "${state.activeNodeId}" (role: ${activeNode.role}). ${activeNodes.length} active, ${pendingNodes.length} pending, ${completedNodes.length} completed, ${failedNodes.length} failed.`,
795
+ };
796
+ }
797
+ }
798
+ return {
799
+ type: 'wait',
800
+ instructions: `${activeNodes.length} active, ${pendingNodes.length} pending, ${completedNodes.length} completed, ${failedNodes.length} failed. Active nodes: ${activeNodes.join(', ') || 'none'}`,
801
+ };
802
+ }
803
+ // ─── Helper Functions ───
804
+ /**
805
+ * Find a node by ID anywhere in the workflow tree (including floating nodes).
806
+ * This stays in engine because it accesses floatingNodes — engine-specific logic.
807
+ */
808
+ function findNode(root, targetId, floatingNodes) {
809
+ // Check the tree
810
+ const found = findNodeInTree(root, targetId);
811
+ if (found)
812
+ return found;
813
+ // Check floating nodes
814
+ if (floatingNodes) {
815
+ const floating = floatingNodes.find((fn) => fn.id === targetId);
816
+ if (floating)
817
+ return floating;
818
+ }
819
+ return null;
820
+ }
821
+ //# sourceMappingURL=workflow-engine.js.map