musubi-sdd 3.0.1 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/musubi-change.js +623 -10
- package/bin/musubi-orchestrate.js +456 -0
- package/bin/musubi-trace.js +393 -0
- package/package.json +3 -2
- package/src/analyzers/impact-analyzer.js +682 -0
- package/src/integrations/cicd.js +782 -0
- package/src/integrations/documentation.js +740 -0
- package/src/integrations/examples.js +789 -0
- package/src/integrations/index.js +23 -0
- package/src/integrations/platforms.js +929 -0
- package/src/managers/delta-spec.js +484 -0
- package/src/monitoring/incident-manager.js +890 -0
- package/src/monitoring/index.js +633 -0
- package/src/monitoring/observability.js +938 -0
- package/src/monitoring/release-manager.js +622 -0
- package/src/orchestration/index.js +168 -0
- package/src/orchestration/orchestration-engine.js +409 -0
- package/src/orchestration/pattern-registry.js +319 -0
- package/src/orchestration/patterns/auto.js +386 -0
- package/src/orchestration/patterns/group-chat.js +395 -0
- package/src/orchestration/patterns/human-in-loop.js +506 -0
- package/src/orchestration/patterns/nested.js +322 -0
- package/src/orchestration/patterns/sequential.js +278 -0
- package/src/orchestration/patterns/swarm.js +395 -0
- package/src/orchestration/workflow-orchestrator.js +738 -0
- package/src/reporters/coverage-report.js +452 -0
- package/src/reporters/traceability-matrix-report.js +684 -0
- package/src/steering/advanced-validation.js +812 -0
- package/src/steering/auto-updater.js +670 -0
- package/src/steering/index.js +119 -0
- package/src/steering/quality-metrics.js +650 -0
- package/src/steering/template-constraints.js +789 -0
- package/src/templates/agents/claude-code/skills/agent-assistant/SKILL.md +22 -0
- package/src/templates/agents/claude-code/skills/issue-resolver/SKILL.md +21 -0
- package/src/templates/agents/claude-code/skills/orchestrator/SKILL.md +90 -28
- package/src/templates/agents/claude-code/skills/project-manager/SKILL.md +32 -0
- package/src/templates/agents/claude-code/skills/site-reliability-engineer/SKILL.md +27 -0
- package/src/templates/agents/claude-code/skills/steering/SKILL.md +30 -0
- package/src/templates/agents/claude-code/skills/test-engineer/SKILL.md +21 -0
- package/src/templates/agents/claude-code/skills/ui-ux-designer/SKILL.md +27 -0
- package/src/templates/agents/codex/AGENTS.md +36 -1
- package/src/templates/agents/cursor/AGENTS.md +36 -1
- package/src/templates/agents/gemini-cli/GEMINI.md +36 -1
- package/src/templates/agents/github-copilot/AGENTS.md +65 -1
- package/src/templates/agents/qwen-code/QWEN.md +36 -1
- package/src/templates/agents/windsurf/AGENTS.md +36 -1
- package/src/templates/shared/delta-spec-template.md +246 -0
- package/src/validators/delta-format.js +474 -0
- package/src/validators/traceability-validator.js +561 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HumanInLoopPattern - Validation gates with human interaction
|
|
3
|
+
*
|
|
4
|
+
* Enables human validation at key points in the workflow.
|
|
5
|
+
* Supports approval gates, feedback collection, and decision points.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { BasePattern } = require('../pattern-registry');
|
|
9
|
+
const { PatternType, ExecutionContext, ExecutionStatus } = require('../orchestration-engine');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Gate type
|
|
13
|
+
*/
|
|
14
|
+
const GateType = {
|
|
15
|
+
APPROVAL: 'approval', // Yes/No approval
|
|
16
|
+
REVIEW: 'review', // Review with feedback
|
|
17
|
+
DECISION: 'decision', // Multiple choice decision
|
|
18
|
+
CONFIRMATION: 'confirmation' // Simple confirmation
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Gate result
|
|
23
|
+
*/
|
|
24
|
+
const GateResult = {
|
|
25
|
+
APPROVED: 'approved',
|
|
26
|
+
REJECTED: 'rejected',
|
|
27
|
+
NEEDS_CHANGES: 'needs-changes',
|
|
28
|
+
TIMEOUT: 'timeout',
|
|
29
|
+
SKIPPED: 'skipped'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* HumanInLoopPattern - Human validation gates
|
|
34
|
+
*/
|
|
35
|
+
class HumanInLoopPattern extends BasePattern {
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
super({
|
|
38
|
+
name: PatternType.HUMAN_IN_LOOP,
|
|
39
|
+
type: PatternType.HUMAN_IN_LOOP,
|
|
40
|
+
description: 'Enable human validation gates at key workflow points',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
tags: ['validation', 'human', 'approval', 'gate'],
|
|
43
|
+
useCases: [
|
|
44
|
+
'Quality gates',
|
|
45
|
+
'Approval workflows',
|
|
46
|
+
'Decision points',
|
|
47
|
+
'Review processes'
|
|
48
|
+
],
|
|
49
|
+
complexity: 'medium',
|
|
50
|
+
supportsParallel: false,
|
|
51
|
+
requiresHuman: true
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this.options = {
|
|
55
|
+
timeout: options.timeout || 300000, // 5 minutes
|
|
56
|
+
autoApproveOnTimeout: options.autoApproveOnTimeout || false,
|
|
57
|
+
collectFeedback: options.collectFeedback || true,
|
|
58
|
+
notifyOnGate: options.notifyOnGate || true,
|
|
59
|
+
...options
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate the execution context
|
|
65
|
+
* @param {ExecutionContext} context - Execution context
|
|
66
|
+
* @param {OrchestrationEngine} engine - Orchestration engine
|
|
67
|
+
* @returns {object} Validation result
|
|
68
|
+
*/
|
|
69
|
+
validate(context, engine) {
|
|
70
|
+
const errors = [];
|
|
71
|
+
const input = context.input;
|
|
72
|
+
|
|
73
|
+
// Check for workflow definition
|
|
74
|
+
if (!input.workflow || !Array.isArray(input.workflow)) {
|
|
75
|
+
errors.push('HumanInLoop pattern requires input.workflow array');
|
|
76
|
+
} else if (input.workflow.length === 0) {
|
|
77
|
+
errors.push('HumanInLoop pattern requires at least one workflow step');
|
|
78
|
+
} else {
|
|
79
|
+
// Validate each step
|
|
80
|
+
for (let i = 0; i < input.workflow.length; i++) {
|
|
81
|
+
const step = input.workflow[i];
|
|
82
|
+
|
|
83
|
+
if (!step.skill && !step.gate) {
|
|
84
|
+
errors.push(`Workflow step ${i + 1} requires either skill or gate`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (step.skill && !engine.getSkill(step.skill)) {
|
|
88
|
+
errors.push(`Unknown skill in workflow: ${step.skill}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (step.gate && !Object.values(GateType).includes(step.gate)) {
|
|
92
|
+
errors.push(`Invalid gate type: ${step.gate}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
valid: errors.length === 0,
|
|
99
|
+
errors
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Execute human-in-loop pattern
|
|
105
|
+
* @param {ExecutionContext} context - Execution context
|
|
106
|
+
* @param {OrchestrationEngine} engine - Orchestration engine
|
|
107
|
+
* @returns {Promise<object>} Execution result
|
|
108
|
+
*/
|
|
109
|
+
async execute(context, engine) {
|
|
110
|
+
const validation = this.validate(context, engine);
|
|
111
|
+
if (!validation.valid) {
|
|
112
|
+
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { workflow, initialInput = {} } = context.input;
|
|
116
|
+
const results = [];
|
|
117
|
+
let currentInput = { ...initialInput };
|
|
118
|
+
let aborted = false;
|
|
119
|
+
|
|
120
|
+
engine.emit('humanInLoopStarted', {
|
|
121
|
+
context,
|
|
122
|
+
workflow: workflow.map(s => s.skill || s.gate),
|
|
123
|
+
totalSteps: workflow.length
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < workflow.length && !aborted; i++) {
|
|
127
|
+
const step = workflow[i];
|
|
128
|
+
const stepNumber = i + 1;
|
|
129
|
+
|
|
130
|
+
engine.emit('humanInLoopStepStarted', {
|
|
131
|
+
context,
|
|
132
|
+
step,
|
|
133
|
+
stepNumber,
|
|
134
|
+
totalSteps: workflow.length
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
let stepResult;
|
|
139
|
+
|
|
140
|
+
if (step.skill) {
|
|
141
|
+
// Execute skill
|
|
142
|
+
stepResult = await this._executeSkillStep(
|
|
143
|
+
step,
|
|
144
|
+
currentInput,
|
|
145
|
+
context,
|
|
146
|
+
engine,
|
|
147
|
+
stepNumber
|
|
148
|
+
);
|
|
149
|
+
} else if (step.gate) {
|
|
150
|
+
// Execute gate
|
|
151
|
+
stepResult = await this._executeGateStep(
|
|
152
|
+
step,
|
|
153
|
+
currentInput,
|
|
154
|
+
context,
|
|
155
|
+
engine,
|
|
156
|
+
stepNumber
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
results.push({
|
|
161
|
+
step: stepNumber,
|
|
162
|
+
type: step.skill ? 'skill' : 'gate',
|
|
163
|
+
name: step.skill || step.gate,
|
|
164
|
+
status: stepResult.status,
|
|
165
|
+
result: stepResult
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Check if gate rejected
|
|
169
|
+
if (step.gate && stepResult.result === GateResult.REJECTED) {
|
|
170
|
+
aborted = true;
|
|
171
|
+
engine.emit('humanInLoopAborted', {
|
|
172
|
+
context,
|
|
173
|
+
step,
|
|
174
|
+
stepNumber,
|
|
175
|
+
reason: 'Gate rejected'
|
|
176
|
+
});
|
|
177
|
+
} else if (step.gate && stepResult.result === GateResult.NEEDS_CHANGES) {
|
|
178
|
+
// Could implement retry logic here
|
|
179
|
+
if (step.retryOnNeededChanges) {
|
|
180
|
+
// Go back to previous skill step
|
|
181
|
+
const prevSkillIndex = this._findPreviousSkillIndex(workflow, i);
|
|
182
|
+
if (prevSkillIndex >= 0) {
|
|
183
|
+
i = prevSkillIndex - 1; // Will increment in loop
|
|
184
|
+
currentInput = {
|
|
185
|
+
...currentInput,
|
|
186
|
+
feedback: stepResult.feedback,
|
|
187
|
+
needsChanges: true
|
|
188
|
+
};
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update input for next step
|
|
195
|
+
if (stepResult.output) {
|
|
196
|
+
currentInput = { ...currentInput, ...stepResult.output };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
engine.emit('humanInLoopStepCompleted', {
|
|
200
|
+
context,
|
|
201
|
+
step,
|
|
202
|
+
stepNumber,
|
|
203
|
+
result: stepResult
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
} catch (error) {
|
|
207
|
+
results.push({
|
|
208
|
+
step: stepNumber,
|
|
209
|
+
type: step.skill ? 'skill' : 'gate',
|
|
210
|
+
name: step.skill || step.gate,
|
|
211
|
+
status: ExecutionStatus.FAILED,
|
|
212
|
+
error: error.message
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
engine.emit('humanInLoopStepFailed', {
|
|
216
|
+
context,
|
|
217
|
+
step,
|
|
218
|
+
stepNumber,
|
|
219
|
+
error
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!step.continueOnError) {
|
|
223
|
+
aborted = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const summary = this._createSummary(results, aborted);
|
|
229
|
+
|
|
230
|
+
engine.emit('humanInLoopCompleted', {
|
|
231
|
+
context,
|
|
232
|
+
results,
|
|
233
|
+
summary,
|
|
234
|
+
aborted
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
results,
|
|
239
|
+
summary,
|
|
240
|
+
aborted,
|
|
241
|
+
finalOutput: currentInput
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Execute a skill step
|
|
247
|
+
* @private
|
|
248
|
+
*/
|
|
249
|
+
async _executeSkillStep(step, input, parentContext, engine, stepNumber) {
|
|
250
|
+
const stepContext = new ExecutionContext({
|
|
251
|
+
task: `HIL Step ${stepNumber}: ${step.skill}`,
|
|
252
|
+
skill: step.skill,
|
|
253
|
+
input: { ...input, ...step.input },
|
|
254
|
+
parentId: parentContext.id,
|
|
255
|
+
metadata: {
|
|
256
|
+
pattern: PatternType.HUMAN_IN_LOOP,
|
|
257
|
+
stepNumber,
|
|
258
|
+
stepType: 'skill'
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
parentContext.children.push(stepContext);
|
|
263
|
+
stepContext.start();
|
|
264
|
+
|
|
265
|
+
const output = await engine.executeSkill(step.skill, stepContext.input, parentContext);
|
|
266
|
+
stepContext.complete(output);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
status: ExecutionStatus.COMPLETED,
|
|
270
|
+
output
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Execute a gate step
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
async _executeGateStep(step, input, parentContext, engine, stepNumber) {
|
|
279
|
+
const stepContext = new ExecutionContext({
|
|
280
|
+
task: `HIL Gate ${stepNumber}: ${step.gate}`,
|
|
281
|
+
skill: null,
|
|
282
|
+
input,
|
|
283
|
+
parentId: parentContext.id,
|
|
284
|
+
metadata: {
|
|
285
|
+
pattern: PatternType.HUMAN_IN_LOOP,
|
|
286
|
+
stepNumber,
|
|
287
|
+
stepType: 'gate',
|
|
288
|
+
gateType: step.gate
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
parentContext.children.push(stepContext);
|
|
293
|
+
stepContext.start();
|
|
294
|
+
stepContext.waitForHuman();
|
|
295
|
+
|
|
296
|
+
// Emit gate notification
|
|
297
|
+
if (this.options.notifyOnGate) {
|
|
298
|
+
engine.emit('humanInLoopGateReached', {
|
|
299
|
+
context: parentContext,
|
|
300
|
+
gate: step,
|
|
301
|
+
stepNumber,
|
|
302
|
+
input
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Build gate question
|
|
307
|
+
const question = this._buildGateQuestion(step, input);
|
|
308
|
+
|
|
309
|
+
// Request human validation
|
|
310
|
+
let response;
|
|
311
|
+
try {
|
|
312
|
+
response = await this._requestWithTimeout(
|
|
313
|
+
() => engine.requestHumanValidation(stepContext, question),
|
|
314
|
+
this.options.timeout
|
|
315
|
+
);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error.message.includes('timeout')) {
|
|
318
|
+
response = this.options.autoApproveOnTimeout
|
|
319
|
+
? { approved: true, feedback: 'Auto-approved on timeout' }
|
|
320
|
+
: { approved: false, feedback: 'Timeout waiting for human response' };
|
|
321
|
+
|
|
322
|
+
stepContext.complete({
|
|
323
|
+
result: this.options.autoApproveOnTimeout
|
|
324
|
+
? GateResult.APPROVED
|
|
325
|
+
: GateResult.TIMEOUT,
|
|
326
|
+
feedback: response.feedback
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
status: ExecutionStatus.COMPLETED,
|
|
331
|
+
result: this.options.autoApproveOnTimeout
|
|
332
|
+
? GateResult.APPROVED
|
|
333
|
+
: GateResult.TIMEOUT,
|
|
334
|
+
feedback: response.feedback
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Process response
|
|
341
|
+
const gateResult = this._processGateResponse(step.gate, response);
|
|
342
|
+
|
|
343
|
+
stepContext.complete({
|
|
344
|
+
result: gateResult,
|
|
345
|
+
feedback: response.feedback,
|
|
346
|
+
response
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
status: ExecutionStatus.COMPLETED,
|
|
351
|
+
result: gateResult,
|
|
352
|
+
feedback: response.feedback,
|
|
353
|
+
output: response.output
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Request with timeout
|
|
359
|
+
* @private
|
|
360
|
+
*/
|
|
361
|
+
_requestWithTimeout(fn, timeout) {
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
const timer = setTimeout(() => {
|
|
364
|
+
reject(new Error(`Gate timeout after ${timeout}ms`));
|
|
365
|
+
}, timeout);
|
|
366
|
+
|
|
367
|
+
Promise.resolve(fn())
|
|
368
|
+
.then(result => {
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
resolve(result);
|
|
371
|
+
})
|
|
372
|
+
.catch(error => {
|
|
373
|
+
clearTimeout(timer);
|
|
374
|
+
reject(error);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Build gate question
|
|
381
|
+
* @private
|
|
382
|
+
*/
|
|
383
|
+
_buildGateQuestion(step, input) {
|
|
384
|
+
const context = step.context || '';
|
|
385
|
+
const prompt = step.prompt || this._getDefaultPrompt(step.gate);
|
|
386
|
+
|
|
387
|
+
let question = prompt;
|
|
388
|
+
if (context) {
|
|
389
|
+
question = `${context}\n\n${prompt}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Include relevant input data
|
|
393
|
+
if (step.showInput && input) {
|
|
394
|
+
question += `\n\nContext:\n${JSON.stringify(input, null, 2)}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return question;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get default prompt for gate type
|
|
402
|
+
* @private
|
|
403
|
+
*/
|
|
404
|
+
_getDefaultPrompt(gateType) {
|
|
405
|
+
switch (gateType) {
|
|
406
|
+
case GateType.APPROVAL:
|
|
407
|
+
return 'Do you approve this to proceed? (yes/no)';
|
|
408
|
+
case GateType.REVIEW:
|
|
409
|
+
return 'Please review the above and provide feedback.';
|
|
410
|
+
case GateType.DECISION:
|
|
411
|
+
return 'Please make a decision.';
|
|
412
|
+
case GateType.CONFIRMATION:
|
|
413
|
+
return 'Please confirm to continue.';
|
|
414
|
+
default:
|
|
415
|
+
return 'Please respond.';
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Process gate response
|
|
421
|
+
* @private
|
|
422
|
+
*/
|
|
423
|
+
_processGateResponse(gateType, response) {
|
|
424
|
+
if (response.approved === true) {
|
|
425
|
+
return GateResult.APPROVED;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (response.approved === false) {
|
|
429
|
+
if (response.needsChanges) {
|
|
430
|
+
return GateResult.NEEDS_CHANGES;
|
|
431
|
+
}
|
|
432
|
+
return GateResult.REJECTED;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (response.skipped) {
|
|
436
|
+
return GateResult.SKIPPED;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Default based on gate type
|
|
440
|
+
switch (gateType) {
|
|
441
|
+
case GateType.CONFIRMATION:
|
|
442
|
+
return response.confirmed ? GateResult.APPROVED : GateResult.REJECTED;
|
|
443
|
+
default:
|
|
444
|
+
return GateResult.APPROVED; // Default to approved for review gates
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Find previous skill index
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
_findPreviousSkillIndex(workflow, currentIndex) {
|
|
453
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
454
|
+
if (workflow[i].skill) {
|
|
455
|
+
return i;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return -1;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Create execution summary
|
|
463
|
+
* @private
|
|
464
|
+
*/
|
|
465
|
+
_createSummary(results, aborted) {
|
|
466
|
+
const skillSteps = results.filter(r => r.type === 'skill');
|
|
467
|
+
const gateSteps = results.filter(r => r.type === 'gate');
|
|
468
|
+
|
|
469
|
+
const approvedGates = gateSteps.filter(r =>
|
|
470
|
+
r.result?.result === GateResult.APPROVED
|
|
471
|
+
).length;
|
|
472
|
+
|
|
473
|
+
const rejectedGates = gateSteps.filter(r =>
|
|
474
|
+
r.result?.result === GateResult.REJECTED
|
|
475
|
+
).length;
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
totalSteps: results.length,
|
|
479
|
+
skillSteps: skillSteps.length,
|
|
480
|
+
gateSteps: gateSteps.length,
|
|
481
|
+
approvedGates,
|
|
482
|
+
rejectedGates,
|
|
483
|
+
aborted,
|
|
484
|
+
completed: !aborted,
|
|
485
|
+
gateApprovalRate: gateSteps.length > 0
|
|
486
|
+
? (approvedGates / gateSteps.length * 100).toFixed(1) + '%'
|
|
487
|
+
: 'N/A'
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Create a human-in-loop pattern with custom options
|
|
494
|
+
* @param {object} options - Pattern options
|
|
495
|
+
* @returns {HumanInLoopPattern} HumanInLoop pattern instance
|
|
496
|
+
*/
|
|
497
|
+
function createHumanInLoopPattern(options = {}) {
|
|
498
|
+
return new HumanInLoopPattern(options);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
module.exports = {
|
|
502
|
+
HumanInLoopPattern,
|
|
503
|
+
GateType,
|
|
504
|
+
GateResult,
|
|
505
|
+
createHumanInLoopPattern
|
|
506
|
+
};
|