@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.
- package/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- 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
|
+
};
|