@girardmedia/bootspring 1.2.0 → 2.0.3

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 (253) hide show
  1. package/README.md +107 -14
  2. package/bin/bootspring.js +166 -27
  3. package/cli/agent.js +189 -17
  4. package/cli/analyze.js +499 -0
  5. package/cli/audit.js +557 -0
  6. package/cli/auth.js +495 -38
  7. package/cli/billing.js +302 -0
  8. package/cli/build.js +695 -0
  9. package/cli/business.js +109 -26
  10. package/cli/checkpoint-utils.js +168 -0
  11. package/cli/checkpoint.js +639 -0
  12. package/cli/cloud-sync.js +447 -0
  13. package/cli/content.js +198 -0
  14. package/cli/context.js +1 -1
  15. package/cli/deploy.js +543 -0
  16. package/cli/fundraise.js +112 -50
  17. package/cli/github-cmd.js +435 -0
  18. package/cli/health.js +477 -0
  19. package/cli/init.js +84 -13
  20. package/cli/legal.js +107 -95
  21. package/cli/log.js +2 -2
  22. package/cli/loop.js +976 -73
  23. package/cli/manager.js +711 -0
  24. package/cli/metrics.js +480 -0
  25. package/cli/monitor.js +812 -0
  26. package/cli/onboard.js +521 -0
  27. package/cli/orchestrator.js +12 -24
  28. package/cli/prd.js +594 -0
  29. package/cli/preseed-start.js +1483 -0
  30. package/cli/preseed.js +2302 -0
  31. package/cli/project.js +436 -0
  32. package/cli/quality.js +233 -0
  33. package/cli/security.js +913 -0
  34. package/cli/seed.js +1441 -5
  35. package/cli/skill.js +273 -211
  36. package/cli/suggest.js +989 -0
  37. package/cli/switch.js +453 -0
  38. package/cli/visualize.js +527 -0
  39. package/cli/watch.js +769 -0
  40. package/cli/workspace.js +607 -0
  41. package/core/analyze-workflow.js +1134 -0
  42. package/core/api-client.js +535 -22
  43. package/core/audit-workflow.js +1350 -0
  44. package/core/build-orchestrator.js +480 -0
  45. package/core/build-state.js +577 -0
  46. package/core/checkpoint-engine.js +408 -0
  47. package/core/config.js +1109 -26
  48. package/core/context-loader.js +21 -1
  49. package/core/deploy-workflow.js +836 -0
  50. package/core/entitlements.js +93 -22
  51. package/core/github-sync.js +610 -0
  52. package/core/index.js +8 -1
  53. package/core/ingest.js +1111 -0
  54. package/core/metrics-engine.js +768 -0
  55. package/core/onboard-workflow.js +1007 -0
  56. package/core/preseed-workflow.js +934 -0
  57. package/core/preseed.js +1617 -0
  58. package/core/project-context.js +325 -0
  59. package/core/project-state.js +694 -0
  60. package/core/r2-sync.js +583 -0
  61. package/core/scaffold.js +525 -7
  62. package/core/session.js +258 -0
  63. package/core/task-extractor.js +758 -0
  64. package/core/telemetry.js +28 -6
  65. package/core/tier-enforcement.js +737 -0
  66. package/core/utils.js +38 -14
  67. package/generators/questionnaire.js +15 -12
  68. package/generators/sections/ai.js +7 -7
  69. package/generators/sections/content.js +300 -0
  70. package/generators/sections/index.js +3 -0
  71. package/generators/sections/plugins.js +7 -6
  72. package/generators/templates/build-planning.template.js +596 -0
  73. package/generators/templates/content.template.js +819 -0
  74. package/generators/templates/index.js +2 -1
  75. package/hooks/git-autopilot.js +1250 -0
  76. package/hooks/index.js +9 -0
  77. package/intelligence/agent-collab.js +2057 -0
  78. package/intelligence/auto-suggest.js +634 -0
  79. package/intelligence/content-gen.js +1589 -0
  80. package/intelligence/cross-project.js +1647 -0
  81. package/intelligence/index.js +184 -0
  82. package/intelligence/learning/insights.json +517 -7
  83. package/intelligence/learning/pattern-learner.js +1008 -14
  84. package/intelligence/memory/decision-tracker.js +1431 -31
  85. package/intelligence/memory/decisions.jsonl +0 -0
  86. package/intelligence/orchestrator.js +2896 -1
  87. package/intelligence/prd.js +92 -1
  88. package/intelligence/recommendation-weights.json +14 -2
  89. package/intelligence/recommendations.js +463 -9
  90. package/intelligence/workflow-composer.js +1451 -0
  91. package/marketplace/index.d.ts +324 -0
  92. package/marketplace/index.js +1921 -0
  93. package/mcp/contracts/mcp-contract.v1.json +342 -4
  94. package/mcp/registry.js +680 -3
  95. package/mcp/response-formatter.js +23 -0
  96. package/mcp/tools/assist-tool.js +78 -4
  97. package/mcp/tools/autopilot-tool.js +408 -0
  98. package/mcp/tools/content-tool.js +571 -0
  99. package/mcp/tools/dashboard-tool.js +251 -5
  100. package/mcp/tools/mvp-tool.js +344 -0
  101. package/mcp/tools/plugin-tool.js +23 -1
  102. package/mcp/tools/prd-tool.js +579 -0
  103. package/mcp/tools/seed-tool.js +447 -0
  104. package/mcp/tools/skill-tool.js +43 -14
  105. package/mcp/tools/suggest-tool.js +147 -0
  106. package/package.json +15 -6
  107. package/agents/README.md +0 -93
  108. package/agents/ai-integration-expert/context.md +0 -386
  109. package/agents/api-expert/context.md +0 -416
  110. package/agents/architecture-expert/context.md +0 -454
  111. package/agents/auth-expert/context.md +0 -399
  112. package/agents/backend-expert/context.md +0 -483
  113. package/agents/business-strategy-expert/context.md +0 -180
  114. package/agents/code-review-expert/context.md +0 -365
  115. package/agents/competitive-analysis-expert/context.md +0 -239
  116. package/agents/data-modeling-expert/context.md +0 -352
  117. package/agents/database-expert/context.md +0 -250
  118. package/agents/devops-expert/context.md +0 -446
  119. package/agents/email-expert/context.md +0 -379
  120. package/agents/financial-expert/context.md +0 -213
  121. package/agents/frontend-expert/context.md +0 -364
  122. package/agents/fundraising-expert/context.md +0 -257
  123. package/agents/growth-expert/context.md +0 -249
  124. package/agents/index.js +0 -140
  125. package/agents/investor-relations-expert/context.md +0 -266
  126. package/agents/legal-expert/context.md +0 -284
  127. package/agents/marketing-expert/context.md +0 -236
  128. package/agents/monitoring-expert/context.md +0 -362
  129. package/agents/operations-expert/context.md +0 -279
  130. package/agents/partnerships-expert/context.md +0 -286
  131. package/agents/payment-expert/context.md +0 -340
  132. package/agents/performance-expert/context.md +0 -377
  133. package/agents/private-equity-expert/context.md +0 -246
  134. package/agents/railway-expert/context.md +0 -284
  135. package/agents/research-expert/context.md +0 -245
  136. package/agents/sales-expert/context.md +0 -241
  137. package/agents/security-expert/context.md +0 -343
  138. package/agents/testing-expert/context.md +0 -414
  139. package/agents/ui-ux-expert/context.md +0 -448
  140. package/agents/vercel-expert/context.md +0 -426
  141. package/skills/index.js +0 -787
  142. package/skills/patterns/README.md +0 -163
  143. package/skills/patterns/ai/agents.md +0 -281
  144. package/skills/patterns/ai/claude.md +0 -138
  145. package/skills/patterns/ai/embeddings.md +0 -150
  146. package/skills/patterns/ai/rag.md +0 -266
  147. package/skills/patterns/ai/streaming.md +0 -170
  148. package/skills/patterns/ai/structured-output.md +0 -162
  149. package/skills/patterns/ai/tools.md +0 -154
  150. package/skills/patterns/analytics/tracking.md +0 -220
  151. package/skills/patterns/api/errors.md +0 -296
  152. package/skills/patterns/api/graphql.md +0 -440
  153. package/skills/patterns/api/middleware.md +0 -279
  154. package/skills/patterns/api/openapi.md +0 -285
  155. package/skills/patterns/api/rate-limiting.md +0 -231
  156. package/skills/patterns/api/route-handler.md +0 -217
  157. package/skills/patterns/api/server-action.md +0 -249
  158. package/skills/patterns/api/versioning.md +0 -443
  159. package/skills/patterns/api/webhooks.md +0 -247
  160. package/skills/patterns/auth/clerk.md +0 -132
  161. package/skills/patterns/auth/mfa.md +0 -313
  162. package/skills/patterns/auth/nextauth.md +0 -140
  163. package/skills/patterns/auth/oauth.md +0 -237
  164. package/skills/patterns/auth/rbac.md +0 -152
  165. package/skills/patterns/auth/session-management.md +0 -367
  166. package/skills/patterns/auth/session.md +0 -120
  167. package/skills/patterns/database/audit.md +0 -177
  168. package/skills/patterns/database/migrations.md +0 -177
  169. package/skills/patterns/database/pagination.md +0 -230
  170. package/skills/patterns/database/pooling.md +0 -357
  171. package/skills/patterns/database/prisma.md +0 -180
  172. package/skills/patterns/database/relations.md +0 -187
  173. package/skills/patterns/database/seeding.md +0 -246
  174. package/skills/patterns/database/soft-delete.md +0 -153
  175. package/skills/patterns/database/transactions.md +0 -162
  176. package/skills/patterns/deployment/ci-cd.md +0 -231
  177. package/skills/patterns/deployment/docker.md +0 -188
  178. package/skills/patterns/deployment/monitoring.md +0 -387
  179. package/skills/patterns/deployment/vercel.md +0 -160
  180. package/skills/patterns/email/resend.md +0 -143
  181. package/skills/patterns/email/templates.md +0 -245
  182. package/skills/patterns/email/transactional.md +0 -503
  183. package/skills/patterns/email/verification.md +0 -176
  184. package/skills/patterns/files/download.md +0 -243
  185. package/skills/patterns/files/upload.md +0 -239
  186. package/skills/patterns/i18n/nextintl.md +0 -188
  187. package/skills/patterns/logging/structured.md +0 -292
  188. package/skills/patterns/notifications/email-queue.md +0 -248
  189. package/skills/patterns/notifications/push.md +0 -279
  190. package/skills/patterns/payments/checkout.md +0 -303
  191. package/skills/patterns/payments/invoices.md +0 -287
  192. package/skills/patterns/payments/portal.md +0 -245
  193. package/skills/patterns/payments/stripe.md +0 -272
  194. package/skills/patterns/payments/subscriptions.md +0 -300
  195. package/skills/patterns/payments/usage.md +0 -279
  196. package/skills/patterns/performance/caching.md +0 -276
  197. package/skills/patterns/performance/code-splitting.md +0 -233
  198. package/skills/patterns/performance/edge.md +0 -254
  199. package/skills/patterns/performance/isr.md +0 -266
  200. package/skills/patterns/performance/lazy-loading.md +0 -281
  201. package/skills/patterns/realtime/sse.md +0 -327
  202. package/skills/patterns/realtime/websockets.md +0 -336
  203. package/skills/patterns/search/filtering.md +0 -329
  204. package/skills/patterns/search/fulltext.md +0 -260
  205. package/skills/patterns/security/audit-logging.md +0 -444
  206. package/skills/patterns/security/csrf.md +0 -234
  207. package/skills/patterns/security/headers.md +0 -252
  208. package/skills/patterns/security/sanitization.md +0 -258
  209. package/skills/patterns/security/secrets.md +0 -261
  210. package/skills/patterns/security/validation.md +0 -268
  211. package/skills/patterns/security/xss.md +0 -229
  212. package/skills/patterns/seo/metadata.md +0 -252
  213. package/skills/patterns/state/context.md +0 -349
  214. package/skills/patterns/state/react-query.md +0 -313
  215. package/skills/patterns/state/url-state.md +0 -482
  216. package/skills/patterns/state/zustand.md +0 -262
  217. package/skills/patterns/testing/api.md +0 -259
  218. package/skills/patterns/testing/component.md +0 -233
  219. package/skills/patterns/testing/coverage.md +0 -207
  220. package/skills/patterns/testing/fixtures.md +0 -225
  221. package/skills/patterns/testing/integration.md +0 -436
  222. package/skills/patterns/testing/mocking.md +0 -177
  223. package/skills/patterns/testing/playwright.md +0 -162
  224. package/skills/patterns/testing/snapshot.md +0 -175
  225. package/skills/patterns/testing/vitest.md +0 -307
  226. package/skills/patterns/ui/accordions.md +0 -395
  227. package/skills/patterns/ui/cards.md +0 -299
  228. package/skills/patterns/ui/dropdowns.md +0 -476
  229. package/skills/patterns/ui/empty-states.md +0 -320
  230. package/skills/patterns/ui/forms.md +0 -405
  231. package/skills/patterns/ui/inputs.md +0 -319
  232. package/skills/patterns/ui/layouts.md +0 -282
  233. package/skills/patterns/ui/loading.md +0 -291
  234. package/skills/patterns/ui/modals.md +0 -338
  235. package/skills/patterns/ui/navigation.md +0 -374
  236. package/skills/patterns/ui/tables.md +0 -407
  237. package/skills/patterns/ui/toasts.md +0 -300
  238. package/skills/patterns/ui/tooltips.md +0 -396
  239. package/skills/patterns/utils/dates.md +0 -435
  240. package/skills/patterns/utils/errors.md +0 -451
  241. package/skills/patterns/utils/formatting.md +0 -345
  242. package/skills/patterns/utils/validation.md +0 -434
  243. package/templates/bootspring.config.js +0 -83
  244. package/templates/business/business-model-canvas.md +0 -246
  245. package/templates/business/business-plan.md +0 -266
  246. package/templates/business/competitive-analysis.md +0 -312
  247. package/templates/fundraising/data-room-checklist.md +0 -300
  248. package/templates/fundraising/investor-research.md +0 -243
  249. package/templates/fundraising/pitch-deck-outline.md +0 -253
  250. package/templates/legal/gdpr-checklist.md +0 -339
  251. package/templates/legal/privacy-policy.md +0 -285
  252. package/templates/legal/terms-of-service.md +0 -222
  253. package/templates/mcp.json +0 -9
@@ -0,0 +1,1451 @@
1
+ /**
2
+ * Bootspring Workflow Composition Engine
3
+ *
4
+ * Enables chaining multiple workflows together into composed sequences.
5
+ * Supports sequential, parallel, and DAG-based workflow compositions.
6
+ *
7
+ * Features:
8
+ * - Sequential composition: feature-dev → testing → launch-prep
9
+ * - Parallel composition: Run security-audit and performance-optimization concurrently
10
+ * - Dependency graphs: Define complex workflow relationships (DAG)
11
+ * - Data passing: Share context and outputs between workflows
12
+ * - Orchestrator integration: Hooks into workflow completion signals
13
+ *
14
+ * @package bootspring
15
+ * @module intelligence/workflow-composer
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const orchestrator = require('./orchestrator');
21
+ const telemetry = require('../core/telemetry');
22
+
23
+ /**
24
+ * Composition state file path
25
+ */
26
+ function getCompositionPath() {
27
+ const projectRoot = process.cwd();
28
+ return path.join(projectRoot, '.bootspring', 'composition-state.json');
29
+ }
30
+
31
+ /**
32
+ * Load composition state
33
+ */
34
+ function loadCompositionState() {
35
+ const statePath = getCompositionPath();
36
+ if (fs.existsSync(statePath)) {
37
+ try {
38
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
39
+ } catch {
40
+ return createDefaultCompositionState();
41
+ }
42
+ }
43
+ return createDefaultCompositionState();
44
+ }
45
+
46
+ /**
47
+ * Create default composition state
48
+ */
49
+ function createDefaultCompositionState() {
50
+ return {
51
+ activeComposition: null,
52
+ compositions: {},
53
+ parallelGroups: {},
54
+ dagCompositions: {},
55
+ sharedContext: {},
56
+ history: []
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Get project root for telemetry
62
+ */
63
+ function getProjectRoot() {
64
+ return process.cwd();
65
+ }
66
+
67
+ /**
68
+ * Emit composition telemetry event
69
+ */
70
+ function emitCompositionTelemetry(event, payload = {}) {
71
+ try {
72
+ telemetry.emitEvent(event, payload, { projectRoot: getProjectRoot() });
73
+ } catch {
74
+ // Telemetry should not block composition execution
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Save composition state
80
+ */
81
+ function saveCompositionState(state) {
82
+ const statePath = getCompositionPath();
83
+ const dir = path.dirname(statePath);
84
+ if (!fs.existsSync(dir)) {
85
+ fs.mkdirSync(dir, { recursive: true });
86
+ }
87
+ state.lastUpdated = new Date().toISOString();
88
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
89
+ }
90
+
91
+ /**
92
+ * Create a new workflow composition
93
+ * @param {string} name - Composition name
94
+ * @param {Array<string>} workflowSequence - Array of workflow names in order
95
+ * @param {object} options - Options for the composition
96
+ */
97
+ function createComposition(name, workflowSequence, options = {}) {
98
+ // Validate all workflows exist
99
+ const invalidWorkflows = workflowSequence.filter(w => !orchestrator.getWorkflow(w));
100
+ if (invalidWorkflows.length > 0) {
101
+ return {
102
+ success: false,
103
+ error: `Unknown workflows: ${invalidWorkflows.join(', ')}`
104
+ };
105
+ }
106
+
107
+ const state = loadCompositionState();
108
+
109
+ const composition = {
110
+ name,
111
+ workflows: workflowSequence,
112
+ currentIndex: 0,
113
+ status: 'pending',
114
+ createdAt: new Date().toISOString(),
115
+ options: {
116
+ autoAdvance: options.autoAdvance !== false, // Auto-start next workflow when one completes
117
+ pauseBetween: options.pauseBetween || false, // Pause between workflows for review
118
+ ...options
119
+ },
120
+ progress: workflowSequence.map(w => ({
121
+ workflow: w,
122
+ status: 'pending',
123
+ startedAt: null,
124
+ completedAt: null
125
+ }))
126
+ };
127
+
128
+ state.compositions[name] = composition;
129
+
130
+ // Initialize shared context for data passing
131
+ state.sharedContext[name] = {
132
+ inputs: options.initialContext || {},
133
+ outputs: {},
134
+ workflowResults: {}
135
+ };
136
+
137
+ saveCompositionState(state);
138
+
139
+ emitCompositionTelemetry('composition_created', {
140
+ composition: name,
141
+ workflowCount: workflowSequence.length,
142
+ workflows: workflowSequence
143
+ });
144
+
145
+ return {
146
+ success: true,
147
+ composition: name,
148
+ workflows: workflowSequence,
149
+ message: `Created composition "${name}" with ${workflowSequence.length} workflows`
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Set context data for a composition
155
+ * This data will be passed to all workflows in the composition
156
+ * @param {string} compositionName - Composition name
157
+ * @param {string} key - Context key
158
+ * @param {any} value - Context value
159
+ */
160
+ function setCompositionContext(compositionName, key, value) {
161
+ const state = loadCompositionState();
162
+
163
+ if (!state.sharedContext[compositionName]) {
164
+ state.sharedContext[compositionName] = { inputs: {}, outputs: {}, workflowResults: {} };
165
+ }
166
+
167
+ state.sharedContext[compositionName].inputs[key] = value;
168
+ saveCompositionState(state);
169
+
170
+ return {
171
+ success: true,
172
+ composition: compositionName,
173
+ key,
174
+ message: `Context "${key}" set for composition "${compositionName}"`
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Get context data for a composition
180
+ * @param {string} compositionName - Composition name
181
+ * @param {string} key - Optional specific key to retrieve
182
+ */
183
+ function getCompositionContext(compositionName, key = null) {
184
+ const state = loadCompositionState();
185
+ const context = state.sharedContext[compositionName];
186
+
187
+ if (!context) {
188
+ return null;
189
+ }
190
+
191
+ if (key) {
192
+ return context.inputs[key] || context.outputs[key] || context.workflowResults[key];
193
+ }
194
+
195
+ return {
196
+ inputs: context.inputs,
197
+ outputs: context.outputs,
198
+ workflowResults: context.workflowResults
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Record workflow output data for passing to subsequent workflows
204
+ * @param {string} compositionName - Composition name
205
+ * @param {string} workflowName - Source workflow name
206
+ * @param {object} outputData - Output data from the workflow
207
+ */
208
+ function recordWorkflowOutput(compositionName, workflowName, outputData) {
209
+ const state = loadCompositionState();
210
+
211
+ if (!state.sharedContext[compositionName]) {
212
+ return { success: false, error: 'Composition context not found' };
213
+ }
214
+
215
+ state.sharedContext[compositionName].workflowResults[workflowName] = {
216
+ data: outputData,
217
+ recordedAt: new Date().toISOString()
218
+ };
219
+
220
+ // Also merge into outputs for easy access
221
+ if (typeof outputData === 'object' && outputData !== null) {
222
+ Object.assign(state.sharedContext[compositionName].outputs, outputData);
223
+ }
224
+
225
+ saveCompositionState(state);
226
+
227
+ emitCompositionTelemetry('workflow_output_recorded', {
228
+ composition: compositionName,
229
+ workflow: workflowName,
230
+ outputKeys: Object.keys(outputData || {})
231
+ });
232
+
233
+ return {
234
+ success: true,
235
+ composition: compositionName,
236
+ workflow: workflowName,
237
+ message: `Output recorded for workflow "${workflowName}"`
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Start a composition
243
+ */
244
+ function startComposition(compositionName) {
245
+ const state = loadCompositionState();
246
+ const composition = state.compositions[compositionName];
247
+
248
+ if (!composition) {
249
+ return { success: false, error: `Composition not found: ${compositionName}` };
250
+ }
251
+
252
+ if (state.activeComposition) {
253
+ return {
254
+ success: false,
255
+ error: `Another composition is active: ${state.activeComposition}`
256
+ };
257
+ }
258
+
259
+ // Start the first workflow
260
+ const firstWorkflow = composition.workflows[0];
261
+ const startResult = orchestrator.startWorkflow(firstWorkflow);
262
+
263
+ if (!startResult.success) {
264
+ return startResult;
265
+ }
266
+
267
+ composition.status = 'active';
268
+ composition.currentIndex = 0;
269
+ composition.progress[0].status = 'active';
270
+ composition.progress[0].startedAt = new Date().toISOString();
271
+ state.activeComposition = compositionName;
272
+
273
+ state.history.push({
274
+ action: 'composition_started',
275
+ composition: compositionName,
276
+ timestamp: new Date().toISOString()
277
+ });
278
+
279
+ saveCompositionState(state);
280
+
281
+ return {
282
+ success: true,
283
+ composition: compositionName,
284
+ currentWorkflow: firstWorkflow,
285
+ currentIndex: 0,
286
+ totalWorkflows: composition.workflows.length,
287
+ message: `Started composition "${compositionName}" with workflow "${firstWorkflow}"`
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Advance to the next workflow in the composition
293
+ */
294
+ function advanceComposition() {
295
+ const state = loadCompositionState();
296
+
297
+ if (!state.activeComposition) {
298
+ return { success: false, error: 'No active composition' };
299
+ }
300
+
301
+ const composition = state.compositions[state.activeComposition];
302
+ const currentIndex = composition.currentIndex;
303
+
304
+ // Mark current workflow as complete
305
+ composition.progress[currentIndex].status = 'complete';
306
+ composition.progress[currentIndex].completedAt = new Date().toISOString();
307
+
308
+ // Check if composition is complete
309
+ if (currentIndex >= composition.workflows.length - 1) {
310
+ composition.status = 'complete';
311
+ state.history.push({
312
+ action: 'composition_completed',
313
+ composition: state.activeComposition,
314
+ timestamp: new Date().toISOString()
315
+ });
316
+ state.activeComposition = null;
317
+ saveCompositionState(state);
318
+
319
+ return {
320
+ success: true,
321
+ complete: true,
322
+ composition: composition.name,
323
+ message: `Composition "${composition.name}" completed!`
324
+ };
325
+ }
326
+
327
+ // Check if we should pause
328
+ if (composition.options.pauseBetween) {
329
+ composition.status = 'paused';
330
+ saveCompositionState(state);
331
+
332
+ return {
333
+ success: true,
334
+ paused: true,
335
+ composition: composition.name,
336
+ currentIndex,
337
+ nextWorkflow: composition.workflows[currentIndex + 1],
338
+ message: 'Composition paused between workflows. Use resumeComposition() to continue.'
339
+ };
340
+ }
341
+
342
+ // Advance to next workflow
343
+ const nextIndex = currentIndex + 1;
344
+ const nextWorkflow = composition.workflows[nextIndex];
345
+
346
+ const startResult = orchestrator.startWorkflow(nextWorkflow);
347
+ if (!startResult.success) {
348
+ return startResult;
349
+ }
350
+
351
+ composition.currentIndex = nextIndex;
352
+ composition.progress[nextIndex].status = 'active';
353
+ composition.progress[nextIndex].startedAt = new Date().toISOString();
354
+
355
+ state.history.push({
356
+ action: 'composition_workflow_advanced',
357
+ composition: composition.name,
358
+ fromWorkflow: composition.workflows[currentIndex],
359
+ toWorkflow: nextWorkflow,
360
+ timestamp: new Date().toISOString()
361
+ });
362
+
363
+ saveCompositionState(state);
364
+
365
+ return {
366
+ success: true,
367
+ composition: composition.name,
368
+ currentWorkflow: nextWorkflow,
369
+ currentIndex: nextIndex,
370
+ totalWorkflows: composition.workflows.length,
371
+ message: `Advanced to workflow "${nextWorkflow}"`
372
+ };
373
+ }
374
+
375
+ /**
376
+ * Resume a paused composition
377
+ */
378
+ function resumeComposition() {
379
+ const state = loadCompositionState();
380
+
381
+ if (!state.activeComposition) {
382
+ return { success: false, error: 'No active composition' };
383
+ }
384
+
385
+ const composition = state.compositions[state.activeComposition];
386
+
387
+ if (composition.status !== 'paused') {
388
+ return { success: false, error: 'Composition is not paused' };
389
+ }
390
+
391
+ composition.status = 'active';
392
+ saveCompositionState(state);
393
+
394
+ // Continue by advancing to next workflow
395
+ return advanceComposition();
396
+ }
397
+
398
+ /**
399
+ * Get current composition status
400
+ */
401
+ function getCompositionStatus(compositionName) {
402
+ const state = loadCompositionState();
403
+ const name = compositionName || state.activeComposition;
404
+
405
+ if (!name) {
406
+ return null;
407
+ }
408
+
409
+ const composition = state.compositions[name];
410
+ if (!composition) {
411
+ return null;
412
+ }
413
+
414
+ const completedCount = composition.progress.filter(p => p.status === 'complete').length;
415
+
416
+ return {
417
+ name: composition.name,
418
+ status: composition.status,
419
+ workflows: composition.workflows,
420
+ currentIndex: composition.currentIndex,
421
+ currentWorkflow: composition.workflows[composition.currentIndex],
422
+ progress: composition.progress,
423
+ summary: {
424
+ completed: completedCount,
425
+ total: composition.workflows.length,
426
+ percent: Math.round((completedCount / composition.workflows.length) * 100)
427
+ },
428
+ options: composition.options
429
+ };
430
+ }
431
+
432
+ /**
433
+ * List all compositions
434
+ */
435
+ function listCompositions() {
436
+ const state = loadCompositionState();
437
+
438
+ return Object.entries(state.compositions).map(([name, comp]) => ({
439
+ name,
440
+ status: comp.status,
441
+ workflows: comp.workflows,
442
+ workflowCount: comp.workflows.length,
443
+ isActive: state.activeComposition === name
444
+ }));
445
+ }
446
+
447
+ /**
448
+ * Cancel active composition
449
+ */
450
+ function cancelComposition() {
451
+ const state = loadCompositionState();
452
+
453
+ if (!state.activeComposition) {
454
+ return { success: false, error: 'No active composition' };
455
+ }
456
+
457
+ const composition = state.compositions[state.activeComposition];
458
+ composition.status = 'cancelled';
459
+
460
+ state.history.push({
461
+ action: 'composition_cancelled',
462
+ composition: state.activeComposition,
463
+ timestamp: new Date().toISOString()
464
+ });
465
+
466
+ state.activeComposition = null;
467
+ saveCompositionState(state);
468
+
469
+ return {
470
+ success: true,
471
+ composition: composition.name,
472
+ message: `Cancelled composition "${composition.name}"`
473
+ };
474
+ }
475
+
476
+ // ============================================================================
477
+ // PARALLEL COMPOSITION
478
+ // ============================================================================
479
+
480
+ /**
481
+ * Create a parallel workflow group
482
+ * All workflows in the group run concurrently
483
+ * @param {string} groupName - Group name
484
+ * @param {Array<string>} workflows - Array of workflow names to run in parallel
485
+ * @param {object} options - Options for the parallel group
486
+ */
487
+ function createParallelGroup(groupName, workflows, options = {}) {
488
+ // Validate all workflows exist
489
+ const invalidWorkflows = workflows.filter(w => !orchestrator.getWorkflow(w));
490
+ if (invalidWorkflows.length > 0) {
491
+ return {
492
+ success: false,
493
+ error: `Unknown workflows: ${invalidWorkflows.join(', ')}`
494
+ };
495
+ }
496
+
497
+ const state = loadCompositionState();
498
+
499
+ const group = {
500
+ name: groupName,
501
+ workflows,
502
+ status: 'pending',
503
+ createdAt: new Date().toISOString(),
504
+ options: {
505
+ failFast: options.failFast !== false, // Stop all if one fails
506
+ continueOnFailure: options.continueOnFailure || false,
507
+ ...options
508
+ },
509
+ workflowStates: workflows.reduce((acc, w) => {
510
+ acc[w] = {
511
+ status: 'pending',
512
+ startedAt: null,
513
+ completedAt: null,
514
+ result: null,
515
+ error: null
516
+ };
517
+ return acc;
518
+ }, {})
519
+ };
520
+
521
+ state.parallelGroups[groupName] = group;
522
+ saveCompositionState(state);
523
+
524
+ emitCompositionTelemetry('parallel_group_created', {
525
+ group: groupName,
526
+ workflowCount: workflows.length,
527
+ workflows
528
+ });
529
+
530
+ return {
531
+ success: true,
532
+ group: groupName,
533
+ workflows,
534
+ message: `Created parallel group "${groupName}" with ${workflows.length} workflows`
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Start a parallel workflow group
540
+ * Initiates all workflows in the group concurrently
541
+ * @param {string} groupName - Group name to start
542
+ */
543
+ function startParallelGroup(groupName) {
544
+ const state = loadCompositionState();
545
+ const group = state.parallelGroups[groupName];
546
+
547
+ if (!group) {
548
+ return { success: false, error: `Parallel group not found: ${groupName}` };
549
+ }
550
+
551
+ if (group.status !== 'pending') {
552
+ return { success: false, error: `Group already ${group.status}` };
553
+ }
554
+
555
+ const startResults = [];
556
+ const errors = [];
557
+
558
+ // Start all workflows
559
+ for (const workflowName of group.workflows) {
560
+ // We use orchestrator's defineWorkflowWithParallel for tracking
561
+ // but for parallel groups, we track independently
562
+ group.workflowStates[workflowName].status = 'active';
563
+ group.workflowStates[workflowName].startedAt = new Date().toISOString();
564
+ startResults.push({ workflow: workflowName, started: true });
565
+ }
566
+
567
+ group.status = 'active';
568
+ group.startedAt = new Date().toISOString();
569
+
570
+ state.history.push({
571
+ action: 'parallel_group_started',
572
+ group: groupName,
573
+ workflows: group.workflows,
574
+ timestamp: new Date().toISOString()
575
+ });
576
+
577
+ saveCompositionState(state);
578
+
579
+ emitCompositionTelemetry('parallel_group_started', {
580
+ group: groupName,
581
+ workflowCount: group.workflows.length
582
+ });
583
+
584
+ return {
585
+ success: true,
586
+ group: groupName,
587
+ startResults,
588
+ errors,
589
+ message: `Started ${startResults.length} workflows in parallel`
590
+ };
591
+ }
592
+
593
+ /**
594
+ * Complete a workflow in a parallel group
595
+ * @param {string} groupName - Group name
596
+ * @param {string} workflowName - Workflow that completed
597
+ * @param {object} result - Result data from the workflow
598
+ */
599
+ function completeParallelWorkflow(groupName, workflowName, result = {}) {
600
+ const state = loadCompositionState();
601
+ const group = state.parallelGroups[groupName];
602
+
603
+ if (!group) {
604
+ return { success: false, error: `Parallel group not found: ${groupName}` };
605
+ }
606
+
607
+ if (!group.workflowStates[workflowName]) {
608
+ return { success: false, error: `Workflow "${workflowName}" not in group "${groupName}"` };
609
+ }
610
+
611
+ const wfState = group.workflowStates[workflowName];
612
+ wfState.status = 'complete';
613
+ wfState.completedAt = new Date().toISOString();
614
+ wfState.result = result;
615
+
616
+ // Store output in shared context
617
+ if (!state.sharedContext[groupName]) {
618
+ state.sharedContext[groupName] = { inputs: {}, outputs: {}, workflowResults: {} };
619
+ }
620
+ state.sharedContext[groupName].workflowResults[workflowName] = result;
621
+
622
+ state.history.push({
623
+ action: 'parallel_workflow_completed',
624
+ group: groupName,
625
+ workflow: workflowName,
626
+ timestamp: new Date().toISOString()
627
+ });
628
+
629
+ // Check if all workflows are complete
630
+ const allComplete = Object.values(group.workflowStates).every(
631
+ ws => ws.status === 'complete' || ws.status === 'failed'
632
+ );
633
+
634
+ if (allComplete) {
635
+ group.status = 'complete';
636
+ group.completedAt = new Date().toISOString();
637
+
638
+ emitCompositionTelemetry('parallel_group_completed', {
639
+ group: groupName,
640
+ workflowCount: group.workflows.length,
641
+ allSucceeded: Object.values(group.workflowStates).every(ws => ws.status === 'complete')
642
+ });
643
+ }
644
+
645
+ saveCompositionState(state);
646
+
647
+ return {
648
+ success: true,
649
+ group: groupName,
650
+ workflow: workflowName,
651
+ allComplete,
652
+ progress: {
653
+ completed: Object.values(group.workflowStates).filter(ws => ws.status === 'complete').length,
654
+ failed: Object.values(group.workflowStates).filter(ws => ws.status === 'failed').length,
655
+ total: group.workflows.length
656
+ },
657
+ message: allComplete
658
+ ? `All workflows in group "${groupName}" complete`
659
+ : `Workflow "${workflowName}" completed in group "${groupName}"`
660
+ };
661
+ }
662
+
663
+ /**
664
+ * Mark a workflow as failed in a parallel group
665
+ * @param {string} groupName - Group name
666
+ * @param {string} workflowName - Workflow that failed
667
+ * @param {string} error - Error message
668
+ */
669
+ function failParallelWorkflow(groupName, workflowName, error) {
670
+ const state = loadCompositionState();
671
+ const group = state.parallelGroups[groupName];
672
+
673
+ if (!group) {
674
+ return { success: false, error: `Parallel group not found: ${groupName}` };
675
+ }
676
+
677
+ const wfState = group.workflowStates[workflowName];
678
+ if (!wfState) {
679
+ return { success: false, error: `Workflow "${workflowName}" not in group "${groupName}"` };
680
+ }
681
+
682
+ wfState.status = 'failed';
683
+ wfState.completedAt = new Date().toISOString();
684
+ wfState.error = error;
685
+
686
+ state.history.push({
687
+ action: 'parallel_workflow_failed',
688
+ group: groupName,
689
+ workflow: workflowName,
690
+ error,
691
+ timestamp: new Date().toISOString()
692
+ });
693
+
694
+ // If failFast, mark group as failed
695
+ if (group.options.failFast) {
696
+ group.status = 'failed';
697
+ group.failedAt = new Date().toISOString();
698
+ group.failureReason = `Workflow "${workflowName}" failed: ${error}`;
699
+ }
700
+
701
+ saveCompositionState(state);
702
+
703
+ return {
704
+ success: true,
705
+ group: groupName,
706
+ workflow: workflowName,
707
+ groupFailed: group.status === 'failed',
708
+ message: group.status === 'failed'
709
+ ? `Group "${groupName}" failed due to workflow failure`
710
+ : `Workflow "${workflowName}" failed but group continues`
711
+ };
712
+ }
713
+
714
+ /**
715
+ * Get parallel group status
716
+ * @param {string} groupName - Group name
717
+ */
718
+ function getParallelGroupStatus(groupName) {
719
+ const state = loadCompositionState();
720
+ const group = state.parallelGroups[groupName];
721
+
722
+ if (!group) {
723
+ return null;
724
+ }
725
+
726
+ const completed = Object.values(group.workflowStates).filter(ws => ws.status === 'complete').length;
727
+ const failed = Object.values(group.workflowStates).filter(ws => ws.status === 'failed').length;
728
+ const active = Object.values(group.workflowStates).filter(ws => ws.status === 'active').length;
729
+ const pending = Object.values(group.workflowStates).filter(ws => ws.status === 'pending').length;
730
+
731
+ return {
732
+ name: group.name,
733
+ status: group.status,
734
+ workflows: group.workflows,
735
+ workflowStates: group.workflowStates,
736
+ progress: {
737
+ completed,
738
+ failed,
739
+ active,
740
+ pending,
741
+ total: group.workflows.length,
742
+ percent: Math.round(((completed + failed) / group.workflows.length) * 100)
743
+ },
744
+ options: group.options
745
+ };
746
+ }
747
+
748
+ /**
749
+ * List all parallel groups
750
+ */
751
+ function listParallelGroups() {
752
+ const state = loadCompositionState();
753
+
754
+ return Object.entries(state.parallelGroups).map(([name, group]) => ({
755
+ name,
756
+ status: group.status,
757
+ workflowCount: group.workflows.length,
758
+ createdAt: group.createdAt
759
+ }));
760
+ }
761
+
762
+ // ============================================================================
763
+ // DAG-BASED COMPOSITION (Dependencies)
764
+ // ============================================================================
765
+
766
+ /**
767
+ * Create a DAG (Directed Acyclic Graph) composition
768
+ * Allows defining complex workflow dependencies
769
+ * @param {string} name - Composition name
770
+ * @param {object} workflowGraph - Workflow dependency graph
771
+ * @param {object} options - Options
772
+ *
773
+ * @example
774
+ * createDAGComposition('complex-launch', {
775
+ * 'feature-development': { dependsOn: [] },
776
+ * 'security-audit': { dependsOn: ['feature-development'] },
777
+ * 'performance-optimization': { dependsOn: ['feature-development'] },
778
+ * 'launch-preparation': { dependsOn: ['security-audit', 'performance-optimization'] }
779
+ * })
780
+ */
781
+ function createDAGComposition(name, workflowGraph, options = {}) {
782
+ // Validate all workflows exist
783
+ const workflows = Object.keys(workflowGraph);
784
+ const invalidWorkflows = workflows.filter(w => !orchestrator.getWorkflow(w));
785
+ if (invalidWorkflows.length > 0) {
786
+ return {
787
+ success: false,
788
+ error: `Unknown workflows: ${invalidWorkflows.join(', ')}`
789
+ };
790
+ }
791
+
792
+ // Validate dependencies exist in graph
793
+ for (const [workflow, config] of Object.entries(workflowGraph)) {
794
+ const deps = config.dependsOn || [];
795
+ const invalidDeps = deps.filter(d => !workflows.includes(d));
796
+ if (invalidDeps.length > 0) {
797
+ return {
798
+ success: false,
799
+ error: `Workflow "${workflow}" has unknown dependencies: ${invalidDeps.join(', ')}`
800
+ };
801
+ }
802
+ }
803
+
804
+ // Check for cycles
805
+ const cycleCheck = detectCycles(workflowGraph);
806
+ if (cycleCheck.hasCycle) {
807
+ return {
808
+ success: false,
809
+ error: `Dependency cycle detected: ${cycleCheck.cycle.join(' → ')}`
810
+ };
811
+ }
812
+
813
+ const state = loadCompositionState();
814
+
815
+ const dagComposition = {
816
+ name,
817
+ graph: workflowGraph,
818
+ workflows,
819
+ status: 'pending',
820
+ createdAt: new Date().toISOString(),
821
+ options: {
822
+ failFast: options.failFast !== false,
823
+ maxParallel: options.maxParallel || 0, // 0 = no limit
824
+ ...options
825
+ },
826
+ workflowStates: workflows.reduce((acc, w) => {
827
+ acc[w] = {
828
+ status: 'pending',
829
+ dependsOn: workflowGraph[w].dependsOn || [],
830
+ dependencyMet: (workflowGraph[w].dependsOn || []).length === 0,
831
+ startedAt: null,
832
+ completedAt: null,
833
+ result: null,
834
+ error: null
835
+ };
836
+ return acc;
837
+ }, {}),
838
+ executionOrder: [],
839
+ completedCount: 0
840
+ };
841
+
842
+ state.dagCompositions[name] = dagComposition;
843
+
844
+ // Initialize shared context
845
+ state.sharedContext[name] = {
846
+ inputs: options.initialContext || {},
847
+ outputs: {},
848
+ workflowResults: {}
849
+ };
850
+
851
+ saveCompositionState(state);
852
+
853
+ emitCompositionTelemetry('dag_composition_created', {
854
+ composition: name,
855
+ workflowCount: workflows.length,
856
+ workflows
857
+ });
858
+
859
+ return {
860
+ success: true,
861
+ composition: name,
862
+ workflows,
863
+ graph: workflowGraph,
864
+ readyToStart: getReadyWorkflows(dagComposition),
865
+ message: `Created DAG composition "${name}" with ${workflows.length} workflows`
866
+ };
867
+ }
868
+
869
+ /**
870
+ * Detect cycles in workflow dependency graph
871
+ * Uses DFS with coloring (white=unvisited, gray=visiting, black=visited)
872
+ */
873
+ function detectCycles(graph) {
874
+ const WHITE = 0, GRAY = 1, BLACK = 2;
875
+ const colors = {};
876
+ const parent = {};
877
+
878
+ for (const node of Object.keys(graph)) {
879
+ colors[node] = WHITE;
880
+ parent[node] = null;
881
+ }
882
+
883
+ function dfs(node, path) {
884
+ colors[node] = GRAY;
885
+ path.push(node);
886
+
887
+ const deps = graph[node]?.dependsOn || [];
888
+ for (const dep of deps) {
889
+ if (colors[dep] === GRAY) {
890
+ // Found cycle - extract cycle path
891
+ const cycleStart = path.indexOf(dep);
892
+ const cycle = path.slice(cycleStart);
893
+ cycle.push(dep); // Close the cycle
894
+ return { hasCycle: true, cycle };
895
+ }
896
+ if (colors[dep] === WHITE) {
897
+ const result = dfs(dep, [...path]);
898
+ if (result.hasCycle) return result;
899
+ }
900
+ }
901
+
902
+ colors[node] = BLACK;
903
+ return { hasCycle: false };
904
+ }
905
+
906
+ for (const node of Object.keys(graph)) {
907
+ if (colors[node] === WHITE) {
908
+ const result = dfs(node, []);
909
+ if (result.hasCycle) return result;
910
+ }
911
+ }
912
+
913
+ return { hasCycle: false };
914
+ }
915
+
916
+ /**
917
+ * Get workflows that are ready to execute (all dependencies met)
918
+ */
919
+ function getReadyWorkflows(dagComposition) {
920
+ const ready = [];
921
+
922
+ for (const [workflow, wfState] of Object.entries(dagComposition.workflowStates)) {
923
+ if (wfState.status !== 'pending') continue;
924
+
925
+ // Check if all dependencies are complete
926
+ const allDepsMet = wfState.dependsOn.every(dep => {
927
+ const depState = dagComposition.workflowStates[dep];
928
+ return depState && depState.status === 'complete';
929
+ });
930
+
931
+ if (allDepsMet) {
932
+ ready.push(workflow);
933
+ }
934
+ }
935
+
936
+ return ready;
937
+ }
938
+
939
+ /**
940
+ * Start a DAG composition
941
+ * Initiates all workflows that have no dependencies
942
+ * @param {string} name - DAG composition name
943
+ */
944
+ function startDAGComposition(name) {
945
+ const state = loadCompositionState();
946
+ const dag = state.dagCompositions[name];
947
+
948
+ if (!dag) {
949
+ return { success: false, error: `DAG composition not found: ${name}` };
950
+ }
951
+
952
+ if (dag.status !== 'pending') {
953
+ return { success: false, error: `DAG composition already ${dag.status}` };
954
+ }
955
+
956
+ dag.status = 'active';
957
+ dag.startedAt = new Date().toISOString();
958
+
959
+ // Find workflows with no dependencies and start them
960
+ const readyWorkflows = getReadyWorkflows(dag);
961
+
962
+ for (const workflow of readyWorkflows) {
963
+ dag.workflowStates[workflow].status = 'active';
964
+ dag.workflowStates[workflow].startedAt = new Date().toISOString();
965
+ dag.workflowStates[workflow].dependencyMet = true;
966
+ dag.executionOrder.push(workflow);
967
+ }
968
+
969
+ state.history.push({
970
+ action: 'dag_composition_started',
971
+ composition: name,
972
+ initialWorkflows: readyWorkflows,
973
+ timestamp: new Date().toISOString()
974
+ });
975
+
976
+ saveCompositionState(state);
977
+
978
+ emitCompositionTelemetry('dag_composition_started', {
979
+ composition: name,
980
+ initialWorkflows: readyWorkflows
981
+ });
982
+
983
+ return {
984
+ success: true,
985
+ composition: name,
986
+ startedWorkflows: readyWorkflows,
987
+ message: `Started DAG composition "${name}" with ${readyWorkflows.length} initial workflows`
988
+ };
989
+ }
990
+
991
+ /**
992
+ * Complete a workflow in a DAG composition
993
+ * Automatically starts dependent workflows whose dependencies are now met
994
+ * @param {string} name - DAG composition name
995
+ * @param {string} workflowName - Completed workflow name
996
+ * @param {object} result - Result data to pass to dependent workflows
997
+ */
998
+ function completeDAGWorkflow(name, workflowName, result = {}) {
999
+ const state = loadCompositionState();
1000
+ const dag = state.dagCompositions[name];
1001
+
1002
+ if (!dag) {
1003
+ return { success: false, error: `DAG composition not found: ${name}` };
1004
+ }
1005
+
1006
+ const wfState = dag.workflowStates[workflowName];
1007
+ if (!wfState) {
1008
+ return { success: false, error: `Workflow "${workflowName}" not in DAG "${name}"` };
1009
+ }
1010
+
1011
+ if (wfState.status !== 'active') {
1012
+ return { success: false, error: `Workflow "${workflowName}" is not active` };
1013
+ }
1014
+
1015
+ // Mark workflow as complete
1016
+ wfState.status = 'complete';
1017
+ wfState.completedAt = new Date().toISOString();
1018
+ wfState.result = result;
1019
+ dag.completedCount++;
1020
+
1021
+ // Store in shared context for dependent workflows
1022
+ if (!state.sharedContext[name]) {
1023
+ state.sharedContext[name] = { inputs: {}, outputs: {}, workflowResults: {} };
1024
+ }
1025
+ state.sharedContext[name].workflowResults[workflowName] = result;
1026
+ if (typeof result === 'object' && result !== null) {
1027
+ Object.assign(state.sharedContext[name].outputs, result);
1028
+ }
1029
+
1030
+ state.history.push({
1031
+ action: 'dag_workflow_completed',
1032
+ composition: name,
1033
+ workflow: workflowName,
1034
+ timestamp: new Date().toISOString()
1035
+ });
1036
+
1037
+ // Find newly ready workflows (dependencies now met)
1038
+ const newlyReady = getReadyWorkflows(dag);
1039
+
1040
+ // Start newly ready workflows
1041
+ for (const workflow of newlyReady) {
1042
+ if (dag.workflowStates[workflow].status === 'pending') {
1043
+ dag.workflowStates[workflow].status = 'active';
1044
+ dag.workflowStates[workflow].startedAt = new Date().toISOString();
1045
+ dag.workflowStates[workflow].dependencyMet = true;
1046
+ dag.executionOrder.push(workflow);
1047
+ }
1048
+ }
1049
+
1050
+ // Check if DAG is complete
1051
+ const allComplete = Object.values(dag.workflowStates).every(
1052
+ ws => ws.status === 'complete' || ws.status === 'failed'
1053
+ );
1054
+
1055
+ if (allComplete) {
1056
+ dag.status = 'complete';
1057
+ dag.completedAt = new Date().toISOString();
1058
+
1059
+ emitCompositionTelemetry('dag_composition_completed', {
1060
+ composition: name,
1061
+ workflowCount: dag.workflows.length,
1062
+ allSucceeded: Object.values(dag.workflowStates).every(ws => ws.status === 'complete')
1063
+ });
1064
+ }
1065
+
1066
+ saveCompositionState(state);
1067
+
1068
+ return {
1069
+ success: true,
1070
+ composition: name,
1071
+ workflow: workflowName,
1072
+ newlyStarted: newlyReady.filter(w => w !== workflowName),
1073
+ allComplete,
1074
+ progress: {
1075
+ completed: dag.completedCount,
1076
+ total: dag.workflows.length,
1077
+ percent: Math.round((dag.completedCount / dag.workflows.length) * 100)
1078
+ },
1079
+ message: allComplete
1080
+ ? `DAG composition "${name}" complete!`
1081
+ : `Workflow "${workflowName}" completed. ${newlyReady.length} workflows now ready.`
1082
+ };
1083
+ }
1084
+
1085
+ /**
1086
+ * Fail a workflow in a DAG composition
1087
+ * @param {string} name - DAG composition name
1088
+ * @param {string} workflowName - Failed workflow name
1089
+ * @param {string} error - Error message
1090
+ */
1091
+ function failDAGWorkflow(name, workflowName, error) {
1092
+ const state = loadCompositionState();
1093
+ const dag = state.dagCompositions[name];
1094
+
1095
+ if (!dag) {
1096
+ return { success: false, error: `DAG composition not found: ${name}` };
1097
+ }
1098
+
1099
+ const wfState = dag.workflowStates[workflowName];
1100
+ if (!wfState) {
1101
+ return { success: false, error: `Workflow "${workflowName}" not in DAG "${name}"` };
1102
+ }
1103
+
1104
+ wfState.status = 'failed';
1105
+ wfState.completedAt = new Date().toISOString();
1106
+ wfState.error = error;
1107
+
1108
+ state.history.push({
1109
+ action: 'dag_workflow_failed',
1110
+ composition: name,
1111
+ workflow: workflowName,
1112
+ error,
1113
+ timestamp: new Date().toISOString()
1114
+ });
1115
+
1116
+ // Find dependent workflows that can no longer complete
1117
+ const blockedWorkflows = [];
1118
+ for (const [wfName, ws] of Object.entries(dag.workflowStates)) {
1119
+ if (ws.dependsOn.includes(workflowName) && ws.status === 'pending') {
1120
+ blockedWorkflows.push(wfName);
1121
+ }
1122
+ }
1123
+
1124
+ // If failFast, mark entire DAG as failed
1125
+ if (dag.options.failFast) {
1126
+ dag.status = 'failed';
1127
+ dag.failedAt = new Date().toISOString();
1128
+ dag.failureReason = `Workflow "${workflowName}" failed: ${error}`;
1129
+ }
1130
+
1131
+ saveCompositionState(state);
1132
+
1133
+ return {
1134
+ success: true,
1135
+ composition: name,
1136
+ workflow: workflowName,
1137
+ blockedWorkflows,
1138
+ dagFailed: dag.status === 'failed',
1139
+ message: dag.status === 'failed'
1140
+ ? `DAG "${name}" failed due to workflow failure`
1141
+ : `Workflow "${workflowName}" failed. ${blockedWorkflows.length} dependent workflows blocked.`
1142
+ };
1143
+ }
1144
+
1145
+ /**
1146
+ * Get DAG composition status
1147
+ * @param {string} name - DAG composition name
1148
+ */
1149
+ function getDAGStatus(name) {
1150
+ const state = loadCompositionState();
1151
+ const dag = state.dagCompositions[name];
1152
+
1153
+ if (!dag) {
1154
+ return null;
1155
+ }
1156
+
1157
+ const completed = Object.values(dag.workflowStates).filter(ws => ws.status === 'complete').length;
1158
+ const failed = Object.values(dag.workflowStates).filter(ws => ws.status === 'failed').length;
1159
+ const active = Object.values(dag.workflowStates).filter(ws => ws.status === 'active').length;
1160
+ const pending = Object.values(dag.workflowStates).filter(ws => ws.status === 'pending').length;
1161
+ const ready = getReadyWorkflows(dag);
1162
+
1163
+ return {
1164
+ name: dag.name,
1165
+ status: dag.status,
1166
+ graph: dag.graph,
1167
+ workflowStates: dag.workflowStates,
1168
+ executionOrder: dag.executionOrder,
1169
+ readyToStart: ready,
1170
+ progress: {
1171
+ completed,
1172
+ failed,
1173
+ active,
1174
+ pending,
1175
+ total: dag.workflows.length,
1176
+ percent: Math.round(((completed + failed) / dag.workflows.length) * 100)
1177
+ },
1178
+ context: state.sharedContext[name] || {},
1179
+ options: dag.options
1180
+ };
1181
+ }
1182
+
1183
+ /**
1184
+ * List all DAG compositions
1185
+ */
1186
+ function listDAGCompositions() {
1187
+ const state = loadCompositionState();
1188
+
1189
+ return Object.entries(state.dagCompositions).map(([name, dag]) => ({
1190
+ name,
1191
+ status: dag.status,
1192
+ workflowCount: dag.workflows.length,
1193
+ completedCount: dag.completedCount,
1194
+ createdAt: dag.createdAt
1195
+ }));
1196
+ }
1197
+
1198
+ // ============================================================================
1199
+ // COMPOSITION TEMPLATES
1200
+ // ============================================================================
1201
+
1202
+ /**
1203
+ * Pre-defined composition templates
1204
+ */
1205
+ const COMPOSITION_TEMPLATES = {
1206
+ 'full-feature': {
1207
+ name: 'Full Feature Development',
1208
+ description: 'Complete feature lifecycle from design to launch',
1209
+ type: 'sequential',
1210
+ workflows: ['feature-development', 'security-audit', 'launch-preparation']
1211
+ },
1212
+ 'quick-ship': {
1213
+ name: 'Quick Ship',
1214
+ description: 'Fast track for well-tested features',
1215
+ type: 'sequential',
1216
+ workflows: ['feature-development', 'launch-preparation']
1217
+ },
1218
+ 'hardening': {
1219
+ name: 'Production Hardening',
1220
+ description: 'Security and performance optimization (parallel)',
1221
+ type: 'parallel',
1222
+ workflows: ['security-audit', 'performance-optimization']
1223
+ },
1224
+ 'api-launch': {
1225
+ name: 'API Launch',
1226
+ description: 'API development through to launch',
1227
+ type: 'sequential',
1228
+ workflows: ['api-development', 'security-audit', 'launch-preparation']
1229
+ },
1230
+ 'comprehensive-feature': {
1231
+ name: 'Comprehensive Feature Launch',
1232
+ description: 'Feature development with parallel audits then launch',
1233
+ type: 'dag',
1234
+ graph: {
1235
+ 'feature-development': { dependsOn: [] },
1236
+ 'security-audit': { dependsOn: ['feature-development'] },
1237
+ 'performance-optimization': { dependsOn: ['feature-development'] },
1238
+ 'launch-preparation': { dependsOn: ['security-audit', 'performance-optimization'] }
1239
+ }
1240
+ },
1241
+ 'full-stack-parallel': {
1242
+ name: 'Full Stack Parallel',
1243
+ description: 'Parallel frontend/backend development then integration',
1244
+ type: 'dag',
1245
+ graph: {
1246
+ 'api-development': { dependsOn: [] },
1247
+ 'feature-development': { dependsOn: [] },
1248
+ 'security-audit': { dependsOn: ['api-development', 'feature-development'] },
1249
+ 'launch-preparation': { dependsOn: ['security-audit'] }
1250
+ }
1251
+ }
1252
+ };
1253
+
1254
+ /**
1255
+ * Create composition from template
1256
+ * Supports sequential, parallel, and DAG templates
1257
+ */
1258
+ function createFromTemplate(templateName, compositionName, options = {}) {
1259
+ const template = COMPOSITION_TEMPLATES[templateName];
1260
+ if (!template) {
1261
+ return {
1262
+ success: false,
1263
+ error: `Unknown template: ${templateName}`,
1264
+ availableTemplates: Object.keys(COMPOSITION_TEMPLATES)
1265
+ };
1266
+ }
1267
+
1268
+ const name = compositionName || `${templateName}-${Date.now()}`;
1269
+
1270
+ switch (template.type) {
1271
+ case 'parallel':
1272
+ return createParallelGroup(name, template.workflows, options);
1273
+
1274
+ case 'dag':
1275
+ return createDAGComposition(name, template.graph, options);
1276
+
1277
+ case 'sequential':
1278
+ default:
1279
+ return createComposition(name, template.workflows, options);
1280
+ }
1281
+ }
1282
+
1283
+ /**
1284
+ * List all active compositions across all types
1285
+ */
1286
+ function listAllCompositions() {
1287
+ const sequential = listCompositions();
1288
+ const parallel = listParallelGroups();
1289
+ const dag = listDAGCompositions();
1290
+
1291
+ return {
1292
+ sequential: sequential.map(c => ({ ...c, type: 'sequential' })),
1293
+ parallel: parallel.map(c => ({ ...c, type: 'parallel' })),
1294
+ dag: dag.map(c => ({ ...c, type: 'dag' })),
1295
+ total: sequential.length + parallel.length + dag.length
1296
+ };
1297
+ }
1298
+
1299
+ /**
1300
+ * Get status of any composition type by name
1301
+ * @param {string} name - Composition name
1302
+ */
1303
+ function getAnyCompositionStatus(name) {
1304
+ // Check sequential
1305
+ const seqStatus = getCompositionStatus(name);
1306
+ if (seqStatus) {
1307
+ return { type: 'sequential', ...seqStatus };
1308
+ }
1309
+
1310
+ // Check parallel
1311
+ const parStatus = getParallelGroupStatus(name);
1312
+ if (parStatus) {
1313
+ return { type: 'parallel', ...parStatus };
1314
+ }
1315
+
1316
+ // Check DAG
1317
+ const dagStatus = getDAGStatus(name);
1318
+ if (dagStatus) {
1319
+ return { type: 'dag', ...dagStatus };
1320
+ }
1321
+
1322
+ return null;
1323
+ }
1324
+
1325
+ /**
1326
+ * Hook into orchestrator workflow completion
1327
+ * Call this when a workflow completes to auto-advance compositions
1328
+ * @param {string} workflowName - Name of completed workflow
1329
+ * @param {object} result - Workflow result data
1330
+ */
1331
+ function onWorkflowComplete(workflowName, result = {}) {
1332
+ const state = loadCompositionState();
1333
+ const updates = [];
1334
+
1335
+ // Check if any DAG compositions are waiting on this workflow
1336
+ for (const [dagName, dag] of Object.entries(state.dagCompositions)) {
1337
+ if (dag.status === 'active') {
1338
+ const wfState = dag.workflowStates[workflowName];
1339
+ if (wfState && wfState.status === 'active') {
1340
+ const dagResult = completeDAGWorkflow(dagName, workflowName, result);
1341
+ updates.push({ type: 'dag', name: dagName, result: dagResult });
1342
+ }
1343
+ }
1344
+ }
1345
+
1346
+ // Check if any parallel groups are waiting on this workflow
1347
+ for (const [groupName, group] of Object.entries(state.parallelGroups)) {
1348
+ if (group.status === 'active') {
1349
+ const wfState = group.workflowStates[workflowName];
1350
+ if (wfState && wfState.status === 'active') {
1351
+ const parResult = completeParallelWorkflow(groupName, workflowName, result);
1352
+ updates.push({ type: 'parallel', name: groupName, result: parResult });
1353
+ }
1354
+ }
1355
+ }
1356
+
1357
+ return {
1358
+ workflow: workflowName,
1359
+ updates,
1360
+ updateCount: updates.length
1361
+ };
1362
+ }
1363
+
1364
+ /**
1365
+ * Create a complex composition with mixed sequential and parallel sections
1366
+ * @param {string} name - Composition name
1367
+ * @param {Array} phases - Array of phases, each being either a workflow name or array of parallel workflows
1368
+ * @param {object} options - Options
1369
+ *
1370
+ * @example
1371
+ * createMixedComposition('my-flow', [
1372
+ * 'feature-development', // Sequential
1373
+ * ['security-audit', 'performance-optimization'], // Parallel
1374
+ * 'launch-preparation' // Sequential
1375
+ * ])
1376
+ */
1377
+ function createMixedComposition(name, phases, options = {}) {
1378
+ // Convert to DAG representation
1379
+ const graph = {};
1380
+ let previousPhaseWorkflows = [];
1381
+
1382
+ for (let i = 0; i < phases.length; i++) {
1383
+ const phase = phases[i];
1384
+ const currentPhaseWorkflows = Array.isArray(phase) ? phase : [phase];
1385
+
1386
+ // Validate workflows
1387
+ const invalidWorkflows = currentPhaseWorkflows.filter(w => !orchestrator.getWorkflow(w));
1388
+ if (invalidWorkflows.length > 0) {
1389
+ return {
1390
+ success: false,
1391
+ error: `Unknown workflows: ${invalidWorkflows.join(', ')}`
1392
+ };
1393
+ }
1394
+
1395
+ // Each workflow in current phase depends on all workflows from previous phase
1396
+ for (const workflow of currentPhaseWorkflows) {
1397
+ graph[workflow] = { dependsOn: [...previousPhaseWorkflows] };
1398
+ }
1399
+
1400
+ previousPhaseWorkflows = currentPhaseWorkflows;
1401
+ }
1402
+
1403
+ return createDAGComposition(name, graph, options);
1404
+ }
1405
+
1406
+ module.exports = {
1407
+ // Sequential composition
1408
+ createComposition,
1409
+ startComposition,
1410
+ advanceComposition,
1411
+ resumeComposition,
1412
+ getCompositionStatus,
1413
+ listCompositions,
1414
+ cancelComposition,
1415
+
1416
+ // Parallel composition
1417
+ createParallelGroup,
1418
+ startParallelGroup,
1419
+ completeParallelWorkflow,
1420
+ failParallelWorkflow,
1421
+ getParallelGroupStatus,
1422
+ listParallelGroups,
1423
+
1424
+ // DAG composition
1425
+ createDAGComposition,
1426
+ startDAGComposition,
1427
+ completeDAGWorkflow,
1428
+ failDAGWorkflow,
1429
+ getDAGStatus,
1430
+ listDAGCompositions,
1431
+
1432
+ // Mixed composition
1433
+ createMixedComposition,
1434
+
1435
+ // Context/data passing
1436
+ setCompositionContext,
1437
+ getCompositionContext,
1438
+ recordWorkflowOutput,
1439
+
1440
+ // Templates
1441
+ createFromTemplate,
1442
+ COMPOSITION_TEMPLATES,
1443
+
1444
+ // Utilities
1445
+ loadCompositionState,
1446
+ listAllCompositions,
1447
+ getAnyCompositionStatus,
1448
+ onWorkflowComplete,
1449
+ detectCycles,
1450
+ getReadyWorkflows
1451
+ };