elsabro 6.0.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { describe, it } = require('node:test');
4
4
  const assert = require('node:assert/strict');
5
- const { FlowEngine, NotImplementedError } = require('../src/index');
5
+ const { FlowEngine, NotImplementedError, DeprecatedNodeError } = require('../src/index');
6
6
  const { resolveTemplate } = require('../src/template');
7
7
 
8
8
  // Load the REAL flow
@@ -69,30 +69,31 @@ describe('Integration: Graph loading', () => {
69
69
  engine.loadFlow(flow);
70
70
  const meta = engine.getFlowMetadata();
71
71
  assert.equal(meta.sync_metadata.audit_result.total_nodes, 42);
72
- assert.equal(meta.sync_metadata.audit_result.implemented, 19);
73
- assert.equal(meta.sync_metadata.audit_result.partial, 3);
74
- assert.equal(meta.sync_metadata.audit_result.not_implemented, 20);
72
+ assert.equal(meta.sync_metadata.audit_result.implemented, 40);
73
+ assert.equal(meta.sync_metadata.audit_result.partial, 0);
74
+ assert.equal(meta.sync_metadata.audit_result.not_implemented, 0);
75
+ assert.equal(meta.sync_metadata.audit_result.deprecated, 2);
75
76
  });
76
77
 
77
78
  it('counts implemented nodes correctly', () => {
78
79
  const engine = new FlowEngine();
79
80
  engine.loadFlow(flow);
80
81
  const implemented = engine.getNodesWhere(n => n.runtime_status === 'implemented');
81
- assert.equal(implemented.length, 19);
82
+ assert.equal(implemented.length, 40);
82
83
  });
83
84
 
84
85
  it('counts not_implemented nodes correctly', () => {
85
86
  const engine = new FlowEngine();
86
87
  engine.loadFlow(flow);
87
88
  const notImpl = engine.getNodesWhere(n => n.runtime_status === 'not_implemented');
88
- assert.equal(notImpl.length, 20);
89
+ assert.equal(notImpl.length, 0);
89
90
  });
90
91
 
91
92
  it('counts partial nodes correctly', () => {
92
93
  const engine = new FlowEngine();
93
94
  engine.loadFlow(flow);
94
95
  const partial = engine.getNodesWhere(n => n.runtime_status === 'partial');
95
- assert.equal(partial.length, 3);
96
+ assert.equal(partial.length, 0);
96
97
  });
97
98
 
98
99
  it('validates graph has no orphaned references', () => {
@@ -147,73 +148,284 @@ describe('Integration: Template resolution with real flow data', () => {
147
148
  });
148
149
  });
149
150
 
150
- describe('Integration: Default profile path (stops at interview_default)', () => {
151
- it('traverses start skill_discovery load_context → profile_router → interview_default (NOT_IMPLEMENTED)', async () => {
151
+ describe('Integration: Default profile path (traverses past interview_default)', () => {
152
+ it('traverses interview_default and continues to standard_analyze', async () => {
152
153
  const engine = new FlowEngine();
153
154
  engine.loadFlow(flow);
154
- const { log } = makeMockCallbacks();
155
155
  const callbacks = makeMockCallbacks();
156
156
 
157
- await assert.rejects(
158
- engine.run(
157
+ // Default path goes deep: interview_default → standard_analyze → merge_analysis → ...
158
+ // Eventually hits not_implemented nodes downstream or completes
159
+ // We just verify the interview node is traversed successfully
160
+ try {
161
+ await engine.run(
159
162
  { task: 'test integration', profile: 'default', complexity: 'medium' },
160
163
  callbacks
161
- ),
162
- (err) => {
163
- assert.equal(err.name, 'NotImplementedError');
164
- assert.ok(err.message.includes('interview_default'));
165
- assert.ok(err.message.includes('not yet implemented'));
166
- return true;
167
- }
168
- );
164
+ );
165
+ } catch (err) {
166
+ // May stop at a downstream not_implemented node — that's OK
167
+ assert.ok(!err.message.includes('interview_default'), 'Should NOT stop at interview_default');
168
+ }
169
169
 
170
- // Verify the traversal path via callbacks log
171
170
  const nodeStarts = callbacks.log.filter(e => e.type === 'node_start').map(e => e.id);
172
- assert.deepEqual(nodeStarts, ['start', 'skill_discovery', 'load_context', 'profile_router', 'interview_default']);
171
+ assert.ok(nodeStarts.includes('interview_default'), 'interview_default was visited');
172
+ assert.ok(nodeStarts.includes('standard_analyze'), 'standard_analyze was reached after interview');
173
+ });
174
+
175
+ it('interview_default calls onAgent with correct inputs', async () => {
176
+ const engine = new FlowEngine();
177
+ engine.loadFlow(flow);
178
+ const callbacks = makeMockCallbacks();
179
+
180
+ try {
181
+ await engine.run({ task: 'add auth', profile: 'default', complexity: 'medium' }, callbacks);
182
+ } catch { /* may stop downstream */ }
183
+
184
+ const agentCalls = callbacks.log.filter(e => e.type === 'agent' && e.id === 'interview_default');
185
+ assert.equal(agentCalls.length, 1, 'onAgent called once for interview_default');
186
+ assert.equal(agentCalls[0].agent, 'elsabro-analyst');
173
187
  });
174
188
  });
175
189
 
176
- describe('Integration: Yolo profile path (stops at interview_yolo)', () => {
177
- it('routes to yolo interview via profile_router', async () => {
190
+ describe('Integration: Yolo profile path (traverses past P2 nodes)', () => {
191
+ it('traverses yolo_execute and quick_verify after interview_yolo', async () => {
178
192
  const engine = new FlowEngine();
179
193
  engine.loadFlow(flow);
180
194
  const callbacks = makeMockCallbacks();
181
195
 
182
- await assert.rejects(
183
- engine.run(
196
+ try {
197
+ await engine.run(
184
198
  { task: 'quick fix', profile: 'yolo', complexity: 'low' },
185
199
  callbacks
186
- ),
187
- (err) => {
188
- assert.ok(err.message.includes('interview_yolo'));
189
- return true;
190
- }
191
- );
200
+ );
201
+ } catch (err) {
202
+ // May hit max traversals in review loop or end successfully — OK
203
+ assert.ok(!err.message.includes('yolo_execute'), 'Should NOT stop at yolo_execute');
204
+ assert.ok(!err.message.includes('quick_verify'), 'Should NOT stop at quick_verify');
205
+ }
192
206
 
193
207
  const nodeStarts = callbacks.log.filter(e => e.type === 'node_start').map(e => e.id);
194
- assert.deepEqual(nodeStarts, ['start', 'skill_discovery', 'load_context', 'profile_router', 'interview_yolo']);
208
+ assert.ok(nodeStarts.includes('interview_yolo'), 'interview_yolo was visited');
209
+ assert.ok(nodeStarts.includes('yolo_execute'), 'yolo_execute was visited');
210
+ assert.ok(nodeStarts.includes('quick_verify'), 'quick_verify was visited');
211
+ assert.ok(nodeStarts.includes('verify_check'), 'verify_check was reached after quick_verify');
212
+ });
213
+
214
+ it('yolo_execute calls onAgent with elsabro-yolo-dev', async () => {
215
+ const engine = new FlowEngine();
216
+ engine.loadFlow(flow);
217
+ const callbacks = makeMockCallbacks();
218
+
219
+ try {
220
+ await engine.run({ task: 'quick fix', profile: 'yolo', complexity: 'low' }, callbacks);
221
+ } catch { /* may stop downstream */ }
222
+
223
+ const agentCalls = callbacks.log.filter(e => e.type === 'agent' && e.id === 'yolo_execute');
224
+ assert.equal(agentCalls.length, 1, 'onAgent called once for yolo_execute');
225
+ assert.equal(agentCalls[0].agent, 'elsabro-yolo-dev');
226
+ });
227
+
228
+ it('quick_verify calls onAgent with elsabro-verifier', async () => {
229
+ const engine = new FlowEngine();
230
+ engine.loadFlow(flow);
231
+ const callbacks = makeMockCallbacks();
232
+
233
+ try {
234
+ await engine.run({ task: 'quick fix', profile: 'yolo', complexity: 'low' }, callbacks);
235
+ } catch { /* may stop downstream */ }
236
+
237
+ const agentCalls = callbacks.log.filter(e => e.type === 'agent' && e.id === 'quick_verify');
238
+ assert.equal(agentCalls.length, 1, 'onAgent called once for quick_verify');
239
+ assert.equal(agentCalls[0].agent, 'elsabro-verifier');
240
+ });
241
+
242
+ it('interview_yolo uses haiku model config', () => {
243
+ const engine = new FlowEngine();
244
+ engine.loadFlow(flow);
245
+ const node = engine.getNode('interview_yolo');
246
+ assert.equal(node.config.model, 'haiku');
247
+ });
248
+ });
249
+
250
+ describe('Integration: BMAD profile path (traverses past bmad_interview_analysis)', () => {
251
+ it('traverses bmad_analyze sequence with both agent steps (Bug A fix)', async () => {
252
+ const engine = new FlowEngine();
253
+ engine.loadFlow(flow);
254
+ const node = engine.getNode('bmad_analyze');
255
+ assert.equal(node.type, 'sequence');
256
+ assert.equal(node.runtime_status, 'implemented');
257
+ assert.equal(node.steps.length, 2);
258
+ assert.equal(node.steps[0].action, 'agent', 'prd step has action: agent');
259
+ assert.equal(node.steps[1].action, 'agent', 'architecture step has action: agent');
260
+ assert.equal(node.steps[0].as, 'prd');
261
+ assert.equal(node.steps[1].as, 'architecture');
262
+ });
263
+
264
+ it('bmad_interview_analysis uses analysis phase', async () => {
265
+ const engine = new FlowEngine();
266
+ engine.loadFlow(flow);
267
+ const node = engine.getNode('bmad_interview_analysis');
268
+ assert.equal(node.inputs.phase, 'analysis');
269
+ assert.ok(node.inputs.topics.includes('usuarios objetivo'));
270
+ });
271
+ });
272
+
273
+ describe('Integration: Careful profile path (traverses past P2 nodes)', () => {
274
+ it('traverses careful_analyze and interrupt_plan_approval after interview_careful', async () => {
275
+ const engine = new FlowEngine();
276
+ engine.loadFlow(flow);
277
+ const callbacks = makeMockCallbacks();
278
+
279
+ try {
280
+ await engine.run(
281
+ { task: 'secure refactor', profile: 'careful', complexity: 'high' },
282
+ callbacks
283
+ );
284
+ } catch (err) {
285
+ // May stop downstream — OK
286
+ assert.ok(!err.message.includes('careful_analyze'), 'Should NOT stop at careful_analyze');
287
+ assert.ok(!err.message.includes('interrupt_plan_approval'), 'Should NOT stop at interrupt_plan_approval');
288
+ }
289
+
290
+ const nodeStarts = callbacks.log.filter(e => e.type === 'node_start').map(e => e.id);
291
+ assert.ok(nodeStarts.includes('interview_careful'), 'interview_careful was visited');
292
+ assert.ok(nodeStarts.includes('careful_analyze'), 'careful_analyze was visited');
293
+ assert.ok(nodeStarts.includes('interrupt_plan_approval'), 'interrupt_plan_approval was visited');
294
+ assert.ok(nodeStarts.includes('parallel_implementation'), 'parallel_implementation reached after approval');
295
+ });
296
+
297
+ it('careful_analyze steps call onAgent for each step', async () => {
298
+ const engine = new FlowEngine();
299
+ engine.loadFlow(flow);
300
+ const stepAgentCalls = [];
301
+ const callbacks = {
302
+ ...makeMockCallbacks(),
303
+ onAgent: async (config) => {
304
+ stepAgentCalls.push({ agent: config.agent, as: config.as, id: config.id });
305
+ return { analysis: 'mock analysis result' };
306
+ }
307
+ };
308
+
309
+ try {
310
+ await engine.run({ task: 'secure refactor', profile: 'careful', complexity: 'high' }, callbacks);
311
+ } catch { /* may stop downstream */ }
312
+
313
+ // Sequence executor calls onAgent with { agent, model, inputs, as } (no id field)
314
+ const sequenceSteps = stepAgentCalls.filter(c => c.as && !c.id);
315
+ const stepNames = sequenceSteps.map(c => c.as);
316
+ assert.ok(stepNames.includes('requirements'), 'requirements step was executed');
317
+ assert.ok(stepNames.includes('risks'), 'risks step was executed');
318
+ assert.ok(stepNames.includes('detailed_plan'), 'detailed_plan step was executed');
319
+ });
320
+
321
+ it('interview_careful uses careful topics', () => {
322
+ const engine = new FlowEngine();
323
+ engine.loadFlow(flow);
324
+ const node = engine.getNode('interview_careful');
325
+ assert.ok(node.inputs.topics.includes('requisitos funcionales'));
326
+ assert.ok(node.inputs.topics.includes('acceptance criteria'));
195
327
  });
196
328
  });
197
329
 
198
- describe('Integration: BMAD profile path (stops at bmad_interview_analysis)', () => {
199
- it('routes to BMAD interview via profile_router', async () => {
330
+ describe('Integration: Teams profile path (traverses past interview_teams)', () => {
331
+ it('traverses interview_teams then stops at teams_spawn (deprecated)', async () => {
200
332
  const engine = new FlowEngine();
201
333
  engine.loadFlow(flow);
202
334
  const callbacks = makeMockCallbacks();
203
335
 
204
336
  await assert.rejects(
205
337
  engine.run(
206
- { task: 'build app', profile: 'bmad', complexity: 'high' },
338
+ { task: 'build dashboard', profile: 'teams', complexity: 'high' },
207
339
  callbacks
208
340
  ),
209
341
  (err) => {
210
- assert.ok(err.message.includes('bmad_interview_analysis'));
342
+ assert.equal(err.name, 'DeprecatedNodeError', 'Should throw DeprecatedNodeError');
343
+ assert.ok(err.message.includes('teams_spawn'), 'Should stop at teams_spawn');
344
+ assert.ok(err.message.includes('deprecated'), 'Should mention deprecated');
211
345
  return true;
212
346
  }
213
347
  );
214
348
 
215
349
  const nodeStarts = callbacks.log.filter(e => e.type === 'node_start').map(e => e.id);
216
- assert.deepEqual(nodeStarts, ['start', 'skill_discovery', 'load_context', 'profile_router', 'bmad_interview_analysis']);
350
+ assert.ok(nodeStarts.includes('interview_teams'), 'interview_teams was visited');
351
+ assert.ok(nodeStarts.includes('teams_spawn'), 'teams_spawn was reached after interview');
352
+ });
353
+
354
+ it('interview_teams uses teams topics', async () => {
355
+ const engine = new FlowEngine();
356
+ engine.loadFlow(flow);
357
+ const node = engine.getNode('interview_teams');
358
+ assert.ok(node.inputs.topics.includes('prioridades del equipo'));
359
+ assert.ok(node.inputs.topics.includes('integraciones'));
360
+ });
361
+ });
362
+
363
+ describe('Integration: Interview agent output storage', () => {
364
+ it('interview node output is stored at context.nodes[id].outputs.output', async () => {
365
+ const engine = new FlowEngine();
366
+ engine.loadFlow(flow);
367
+ const mockInterviewResult = { summary: 'User wants auth with JWT', decisions: ['use bcrypt'] };
368
+ let capturedContext = null;
369
+
370
+ const callbacks = {
371
+ ...makeMockCallbacks(),
372
+ onAgent: async (config) => {
373
+ if (config.id === 'interview_yolo') {
374
+ return mockInterviewResult;
375
+ }
376
+ return { result: 'other agent' };
377
+ },
378
+ onCheckpoint: async (data) => {
379
+ // Capture context after interview_yolo completes
380
+ if (data.currentNode === 'interview_yolo') {
381
+ capturedContext = JSON.parse(JSON.stringify(data.context));
382
+ }
383
+ }
384
+ };
385
+
386
+ try {
387
+ await engine.run({ task: 'test output', profile: 'yolo', complexity: 'low' }, callbacks);
388
+ } catch { /* may stop at downstream not_implemented node */ }
389
+
390
+ assert.ok(capturedContext, 'Checkpoint captured after interview_yolo');
391
+ assert.deepEqual(
392
+ capturedContext.nodes.interview_yolo.outputs.output,
393
+ mockInterviewResult,
394
+ 'Interview result stored at outputs.output'
395
+ );
396
+ });
397
+
398
+ it('all 5 interview nodes are marked as implemented', () => {
399
+ const engine = new FlowEngine();
400
+ engine.loadFlow(flow);
401
+ const interviewNodes = ['interview_default', 'interview_yolo', 'interview_careful', 'interview_teams', 'bmad_interview_analysis'];
402
+ for (const id of interviewNodes) {
403
+ const node = engine.getNode(id);
404
+ assert.equal(node.runtime_status, 'implemented', `${id} should be implemented`);
405
+ assert.equal(node.implemented_in, 'agents/elsabro-analyst.md#[INTERVIEW]', `${id} should reference analyst [INTERVIEW]`);
406
+ assert.deepEqual(node.gaps, [], `${id} should have no gaps`);
407
+ }
408
+ });
409
+
410
+ it('all 5 interview nodes use elsabro-analyst agent', () => {
411
+ const engine = new FlowEngine();
412
+ engine.loadFlow(flow);
413
+ const interviewNodes = ['interview_default', 'interview_yolo', 'interview_careful', 'interview_teams', 'bmad_interview_analysis'];
414
+ for (const id of interviewNodes) {
415
+ const node = engine.getNode(id);
416
+ assert.equal(node.agent, 'elsabro-analyst', `${id} agent should be elsabro-analyst`);
417
+ assert.equal(node.inputs.command, '[INTERVIEW]', `${id} command should be [INTERVIEW]`);
418
+ }
419
+ });
420
+
421
+ it('interview nodes do not have custom outputs (use executor default)', () => {
422
+ const engine = new FlowEngine();
423
+ engine.loadFlow(flow);
424
+ const interviewNodes = ['interview_default', 'interview_yolo', 'interview_careful', 'interview_teams', 'bmad_interview_analysis'];
425
+ for (const id of interviewNodes) {
426
+ const node = engine.getNode(id);
427
+ assert.equal(node.outputs, undefined, `${id} should not have custom outputs field`);
428
+ }
217
429
  });
218
430
  });
219
431
 
@@ -249,3 +461,420 @@ describe('Integration: Node type coverage', () => {
249
461
  assert.equal(types.size, 9);
250
462
  });
251
463
  });
464
+
465
+ describe('Integration: careful_analyze output mapping (Bug 2 fix)', () => {
466
+ it('careful_analyze step outputs are accessible at node outputs via outputs field', async () => {
467
+ const engine = new FlowEngine();
468
+ engine.loadFlow(flow);
469
+ let capturedContext = null;
470
+
471
+ const callbacks = {
472
+ ...makeMockCallbacks(),
473
+ onAgent: async (config) => {
474
+ if (config.as === 'requirements') return { detail: 'requirements analysis' };
475
+ if (config.as === 'risks') return { detail: 'risk analysis' };
476
+ if (config.as === 'detailed_plan') return { detail: 'implementation plan' };
477
+ return { result: 'other agent' };
478
+ },
479
+ onCheckpoint: async (data) => {
480
+ if (data.currentNode === 'careful_analyze') {
481
+ capturedContext = JSON.parse(JSON.stringify(data.context));
482
+ }
483
+ }
484
+ };
485
+
486
+ try {
487
+ await engine.run({ task: 'test outputs', profile: 'careful', complexity: 'high' }, callbacks);
488
+ } catch { /* may stop downstream */ }
489
+
490
+ assert.ok(capturedContext, 'Checkpoint captured after careful_analyze');
491
+ assert.ok(capturedContext.nodes.careful_analyze.outputs.requirements, 'requirements output exists');
492
+ assert.ok(capturedContext.nodes.careful_analyze.outputs.risks, 'risks output exists');
493
+ assert.ok(capturedContext.nodes.careful_analyze.outputs.detailed_plan, 'detailed_plan output exists');
494
+ });
495
+ });
496
+
497
+ describe('Integration: interrupt_plan_approval routing', () => {
498
+ it('routes to parallel_implementation on approve', async () => {
499
+ const engine = new FlowEngine();
500
+ engine.loadFlow(flow);
501
+ const callbacks = {
502
+ ...makeMockCallbacks(),
503
+ onInterrupt: async (display) => {
504
+ if (display.nodeId === 'interrupt_plan_approval') return 'approve';
505
+ const options = display.display.options || [];
506
+ return options.length > 0 ? options[0].id : 'approve';
507
+ }
508
+ };
509
+
510
+ try {
511
+ await engine.run({ task: 'test approval', profile: 'careful', complexity: 'high' }, callbacks);
512
+ } catch { /* may stop downstream */ }
513
+
514
+ const nodeStarts = callbacks.log.filter(e => e.type === 'node_start').map(e => e.id);
515
+ assert.ok(nodeStarts.includes('parallel_implementation'), 'approve routes to parallel_implementation');
516
+ });
517
+
518
+ it('routes to careful_analyze on modify (loop)', async () => {
519
+ const engine = new FlowEngine();
520
+ engine.loadFlow(flow);
521
+ let interruptCount = 0;
522
+ const callbacks = {
523
+ ...makeMockCallbacks(),
524
+ onInterrupt: async (display) => {
525
+ if (display.nodeId === 'interrupt_plan_approval') {
526
+ interruptCount++;
527
+ return interruptCount === 1 ? 'modify' : 'approve';
528
+ }
529
+ const options = display.display.options || [];
530
+ return options.length > 0 ? options[0].id : 'approve';
531
+ }
532
+ };
533
+
534
+ try {
535
+ await engine.run({ task: 'test modify loop', profile: 'careful', complexity: 'high' }, callbacks);
536
+ } catch { /* may stop downstream */ }
537
+
538
+ const nodeStarts = callbacks.log.filter(e => e.type === 'node_start').map(e => e.id);
539
+ const carefulCount = nodeStarts.filter(id => id === 'careful_analyze').length;
540
+ assert.ok(carefulCount >= 2, `careful_analyze visited ${carefulCount} times (expected >= 2)`);
541
+ });
542
+
543
+ it('routes to end_cancelled on reject', async () => {
544
+ const engine = new FlowEngine();
545
+ engine.loadFlow(flow);
546
+ const callbacks = {
547
+ ...makeMockCallbacks(),
548
+ onInterrupt: async (display) => {
549
+ if (display.nodeId === 'interrupt_plan_approval') return 'reject';
550
+ const options = display.display.options || [];
551
+ return options.length > 0 ? options[0].id : 'approve';
552
+ }
553
+ };
554
+
555
+ const result = await engine.run(
556
+ { task: 'test reject', profile: 'careful', complexity: 'high' },
557
+ callbacks
558
+ );
559
+
560
+ const nodeStarts = callbacks.log.filter(e => e.type === 'node_start').map(e => e.id);
561
+ assert.ok(nodeStarts.includes('end_cancelled'), 'reject routes to end_cancelled');
562
+ });
563
+ });
564
+
565
+ describe('Integration: Partial nodes upgraded to implemented (P2)', () => {
566
+ it('profile_router is now implemented', () => {
567
+ const engine = new FlowEngine();
568
+ engine.loadFlow(flow);
569
+ const node = engine.getNode('profile_router');
570
+ assert.equal(node.runtime_status, 'implemented');
571
+ assert.deepEqual(node.gaps, []);
572
+ });
573
+
574
+ it('interrupt_manual_fix is now implemented', () => {
575
+ const engine = new FlowEngine();
576
+ engine.loadFlow(flow);
577
+ const node = engine.getNode('interrupt_manual_fix');
578
+ assert.equal(node.runtime_status, 'implemented');
579
+ assert.deepEqual(node.gaps, []);
580
+ });
581
+
582
+ it('wait_manual_fix is now implemented', () => {
583
+ const engine = new FlowEngine();
584
+ engine.loadFlow(flow);
585
+ const node = engine.getNode('wait_manual_fix');
586
+ assert.equal(node.runtime_status, 'implemented');
587
+ assert.deepEqual(node.gaps, []);
588
+ });
589
+
590
+ it('no partial nodes remain in the flow', () => {
591
+ const engine = new FlowEngine();
592
+ engine.loadFlow(flow);
593
+ const partial = engine.getNodesWhere(n => n.runtime_status === 'partial');
594
+ assert.equal(partial.length, 0, 'All partial nodes should be upgraded');
595
+ });
596
+ });
597
+
598
+ describe('Integration: P2 node metadata', () => {
599
+ it('all 4 new implemented nodes have correct metadata', () => {
600
+ const engine = new FlowEngine();
601
+ engine.loadFlow(flow);
602
+ const p2Nodes = ['yolo_execute', 'quick_verify', 'careful_analyze', 'interrupt_plan_approval'];
603
+ for (const id of p2Nodes) {
604
+ const node = engine.getNode(id);
605
+ assert.equal(node.runtime_status, 'implemented', `${id} should be implemented`);
606
+ assert.ok(node.implemented_in, `${id} should have implemented_in`);
607
+ assert.deepEqual(node.gaps, [], `${id} should have no gaps`);
608
+ }
609
+ });
610
+
611
+ it('careful_analyze steps all have action: agent (Bug 1 fix)', () => {
612
+ const engine = new FlowEngine();
613
+ engine.loadFlow(flow);
614
+ const node = engine.getNode('careful_analyze');
615
+ for (const step of node.steps) {
616
+ assert.equal(step.action, 'agent', `Step "${step.as}" should have action: "agent"`);
617
+ }
618
+ });
619
+
620
+ it('careful_analyze has outputs mapping (Bug 2 fix)', () => {
621
+ const engine = new FlowEngine();
622
+ engine.loadFlow(flow);
623
+ const node = engine.getNode('careful_analyze');
624
+ assert.ok(node.outputs, 'careful_analyze should have outputs field');
625
+ assert.ok(node.outputs.requirements, 'should have requirements output mapping');
626
+ assert.ok(node.outputs.risks, 'should have risks output mapping');
627
+ assert.ok(node.outputs.detailed_plan, 'should have detailed_plan output mapping');
628
+ });
629
+
630
+ it('verify_check condition uses .output.passed path (Bug 3 fix)', () => {
631
+ const engine = new FlowEngine();
632
+ engine.loadFlow(flow);
633
+ const node = engine.getNode('verify_check');
634
+ assert.ok(node.condition.includes('.output.passed'), 'condition should use .output.passed path');
635
+ assert.ok(!node.condition.match(/outputs\.passed[^.]/), 'condition should NOT use .outputs.passed (without .output)');
636
+ });
637
+ });
638
+
639
+ // ==========================================================
640
+ // P4: BMAD Full Path Integration Tests
641
+ // ==========================================================
642
+
643
+ describe('Integration: BMAD Full Path (P4 — 8 nodes implemented)', () => {
644
+ it('all 8 BMAD nodes are marked implemented', () => {
645
+ const engine = new FlowEngine();
646
+ engine.loadFlow(flow);
647
+ const bmadNodes = [
648
+ 'bmad_analyze', 'interrupt_prd_approval', 'bmad_interview_planning',
649
+ 'bmad_plan', 'interrupt_manifest', 'bmad_interview_solution',
650
+ 'bmad_solution', 'interrupt_solution_approval'
651
+ ];
652
+ for (const id of bmadNodes) {
653
+ const node = engine.getNode(id);
654
+ assert.equal(node.runtime_status, 'implemented', `${id} should be implemented`);
655
+ assert.deepStrictEqual(node.gaps, [], `${id} should have no gaps`);
656
+ }
657
+ });
658
+
659
+ it('bmad_interview_planning has no custom outputs field (Bug B fix)', () => {
660
+ const engine = new FlowEngine();
661
+ engine.loadFlow(flow);
662
+ const node = engine.getNode('bmad_interview_planning');
663
+ assert.equal(node.outputs, undefined, 'should NOT have custom outputs field');
664
+ assert.equal(node.type, 'agent');
665
+ });
666
+
667
+ it('bmad_interview_solution has no custom outputs field (Bug D fix)', () => {
668
+ const engine = new FlowEngine();
669
+ engine.loadFlow(flow);
670
+ const node = engine.getNode('bmad_interview_solution');
671
+ assert.equal(node.outputs, undefined, 'should NOT have custom outputs field');
672
+ assert.equal(node.type, 'agent');
673
+ });
674
+
675
+ it('bmad_solution has no custom outputs field (Bug E fix)', () => {
676
+ const engine = new FlowEngine();
677
+ engine.loadFlow(flow);
678
+ const node = engine.getNode('bmad_solution');
679
+ assert.equal(node.outputs, undefined, 'should NOT have custom outputs field');
680
+ assert.equal(node.type, 'agent');
681
+ });
682
+
683
+ it('bmad_plan step has action: agent (Bug C fix)', () => {
684
+ const engine = new FlowEngine();
685
+ engine.loadFlow(flow);
686
+ const node = engine.getNode('bmad_plan');
687
+ assert.equal(node.steps[0].action, 'agent', 'stories step should have action: agent');
688
+ assert.equal(node.steps[0].as, 'stories');
689
+ });
690
+
691
+ it('bmad_plan references .outputs.output for interview context (F1 fix)', () => {
692
+ const engine = new FlowEngine();
693
+ engine.loadFlow(flow);
694
+ const node = engine.getNode('bmad_plan');
695
+ const interviewRef = node.steps[0].inputs.interview_context;
696
+ assert.ok(interviewRef.includes('.outputs.output'), 'should reference .outputs.output');
697
+ assert.ok(!interviewRef.includes('.outputs.interview_summary'), 'should NOT reference .outputs.interview_summary');
698
+ });
699
+
700
+ it('bmad_solution references .outputs.output for interview context (F2 fix)', () => {
701
+ const engine = new FlowEngine();
702
+ engine.loadFlow(flow);
703
+ const node = engine.getNode('bmad_solution');
704
+ const interviewRef = node.inputs.interview_context;
705
+ assert.ok(interviewRef.includes('.outputs.output'), 'should reference .outputs.output');
706
+ assert.ok(!interviewRef.includes('.outputs.interview_summary'), 'should NOT reference .outputs.interview_summary');
707
+ });
708
+
709
+ it('parallel_implementation references .outputs.output for bmad_solution (F3/F4 fix)', () => {
710
+ const engine = new FlowEngine();
711
+ engine.loadFlow(flow);
712
+ const node = engine.getNode('parallel_implementation');
713
+ const branchInputs = JSON.stringify(node.branches);
714
+ assert.ok(branchInputs.includes('bmad_solution.outputs.output'), 'should reference .outputs.output');
715
+ assert.ok(!branchInputs.includes('bmad_solution.outputs.solution'), 'should NOT reference .outputs.solution');
716
+ });
717
+
718
+ it('interrupt_prd_approval routes correctly: approve/modify/reject', () => {
719
+ const engine = new FlowEngine();
720
+ engine.loadFlow(flow);
721
+ const node = engine.getNode('interrupt_prd_approval');
722
+ assert.equal(node.routes.approve, 'bmad_interview_planning');
723
+ assert.equal(node.routes.modify, 'bmad_analyze');
724
+ assert.equal(node.routes.reject, 'end_cancelled');
725
+ });
726
+
727
+ it('interrupt_solution_approval routes correctly: approve/modify/reject', () => {
728
+ const engine = new FlowEngine();
729
+ engine.loadFlow(flow);
730
+ const node = engine.getNode('interrupt_solution_approval');
731
+ assert.equal(node.routes.approve, 'parallel_implementation');
732
+ assert.equal(node.routes.modify, 'bmad_solution');
733
+ assert.equal(node.routes.reject, 'end_cancelled');
734
+ });
735
+
736
+ it('BMAD path traverses all 8 nodes and reaches shared pipeline', async () => {
737
+ const engine = new FlowEngine();
738
+ engine.loadFlow(flow);
739
+ const callbacks = makeMockCallbacks();
740
+
741
+ // BMAD flow will loop in review cycle (mock callbacks don't satisfy conditions)
742
+ // or succeed through post_mortem → end_success. We verify BMAD nodes were visited.
743
+ try {
744
+ await engine.run(
745
+ { task: 'build saas app', profile: 'bmad', complexity: 'high' },
746
+ callbacks
747
+ );
748
+ } catch (err) {
749
+ // Expected: max traversals in review loop (mock callbacks cause infinite loop)
750
+ assert.ok(
751
+ err.message.includes('maximum node traversals'),
752
+ `Expected max traversals, got: ${err.message}`
753
+ );
754
+ }
755
+
756
+ const nodeStarts = callbacks.log.filter(e => e.type === 'node_start').map(e => e.id);
757
+
758
+ // Verify all 8 BMAD nodes were traversed
759
+ const bmadNodes = [
760
+ 'bmad_interview_analysis', 'bmad_analyze', 'interrupt_prd_approval',
761
+ 'bmad_interview_planning', 'bmad_plan', 'interrupt_manifest',
762
+ 'bmad_interview_solution', 'bmad_solution', 'interrupt_solution_approval'
763
+ ];
764
+ for (const id of bmadNodes) {
765
+ assert.ok(nodeStarts.includes(id), `visited ${id}`);
766
+ }
767
+
768
+ // Verify it joined the shared pipeline
769
+ assert.ok(nodeStarts.includes('parallel_implementation'), 'reached parallel_implementation');
770
+ });
771
+
772
+ it('only 3 nodes remain not_implemented after P4 (now 0 after P5)', () => {
773
+ const engine = new FlowEngine();
774
+ engine.loadFlow(flow);
775
+ const notImpl = engine.getNodesWhere(n => n.runtime_status === 'not_implemented');
776
+ assert.equal(notImpl.length, 0, 'P5 resolved all not_implemented nodes');
777
+ });
778
+ });
779
+
780
+ // ============================================================
781
+ // P5: Cleanup & Deprecation (3 nodes: deprecate 2, implement 1)
782
+ // ============================================================
783
+
784
+ describe('Integration: P5 Cleanup & Deprecation', () => {
785
+ it('teams_spawn has runtime_status: deprecated', () => {
786
+ const engine = new FlowEngine();
787
+ engine.loadFlow(flow);
788
+ const node = engine.getNode('teams_spawn');
789
+ assert.equal(node.runtime_status, 'deprecated');
790
+ });
791
+
792
+ it('teams_spawn has deprecated_reason field', () => {
793
+ const engine = new FlowEngine();
794
+ engine.loadFlow(flow);
795
+ const node = engine.getNode('teams_spawn');
796
+ assert.ok(node.deprecated_reason, 'should have deprecated_reason');
797
+ assert.ok(node.deprecated_reason.includes('IMPERATIVO_AGENT_TEAMS'), 'should mention replacement');
798
+ });
799
+
800
+ it('interrupt_teams_failed has runtime_status: deprecated', () => {
801
+ const engine = new FlowEngine();
802
+ engine.loadFlow(flow);
803
+ const node = engine.getNode('interrupt_teams_failed');
804
+ assert.equal(node.runtime_status, 'deprecated');
805
+ });
806
+
807
+ it('interrupt_teams_failed has deprecated_reason field', () => {
808
+ const engine = new FlowEngine();
809
+ engine.loadFlow(flow);
810
+ const node = engine.getNode('interrupt_teams_failed');
811
+ assert.ok(node.deprecated_reason, 'should have deprecated_reason');
812
+ assert.ok(node.deprecated_reason.includes('teams_spawn'), 'should reference teams_spawn');
813
+ });
814
+
815
+ it('post_mortem has runtime_status: implemented', () => {
816
+ const engine = new FlowEngine();
817
+ engine.loadFlow(flow);
818
+ const node = engine.getNode('post_mortem');
819
+ assert.equal(node.runtime_status, 'implemented');
820
+ });
821
+
822
+ it('post_mortem step uses agent action with elsabro-tech-writer', () => {
823
+ const engine = new FlowEngine();
824
+ engine.loadFlow(flow);
825
+ const node = engine.getNode('post_mortem');
826
+ assert.equal(node.steps.length, 1, 'should have 1 step');
827
+ assert.equal(node.steps[0].action, 'agent', 'step action should be agent');
828
+ assert.equal(node.steps[0].agent, 'elsabro-tech-writer', 'agent should be tech-writer');
829
+ assert.equal(node.steps[0].as, 'learnings', 'step should store as learnings');
830
+ });
831
+
832
+ it('post_mortem routes to end_success', () => {
833
+ const engine = new FlowEngine();
834
+ engine.loadFlow(flow);
835
+ const node = engine.getNode('post_mortem');
836
+ assert.equal(node.next, 'end_success');
837
+ });
838
+
839
+ it('DeprecatedNodeError is thrown for deprecated nodes', async () => {
840
+ const engine = new FlowEngine();
841
+ engine.loadFlow(flow);
842
+ const callbacks = makeMockCallbacks();
843
+
844
+ await assert.rejects(
845
+ engine.run(
846
+ { task: 'build dashboard', profile: 'teams', complexity: 'high' },
847
+ callbacks
848
+ ),
849
+ (err) => {
850
+ assert.equal(err.name, 'DeprecatedNodeError');
851
+ assert.equal(err.nodeId, 'teams_spawn');
852
+ assert.ok(err.reason.includes('IMPERATIVO_AGENT_TEAMS'));
853
+ return true;
854
+ }
855
+ );
856
+ });
857
+
858
+ it('0 not_implemented nodes remain after P5', () => {
859
+ const engine = new FlowEngine();
860
+ engine.loadFlow(flow);
861
+ const notImpl = engine.getNodesWhere(n => n.runtime_status === 'not_implemented');
862
+ assert.equal(notImpl.length, 0);
863
+ });
864
+
865
+ it('exactly 2 deprecated nodes exist', () => {
866
+ const engine = new FlowEngine();
867
+ engine.loadFlow(flow);
868
+ const deprecated = engine.getNodesWhere(n => n.runtime_status === 'deprecated');
869
+ const ids = deprecated.map(n => n.id).sort();
870
+ assert.equal(deprecated.length, 2);
871
+ assert.deepStrictEqual(ids, ['interrupt_teams_failed', 'teams_spawn']);
872
+ });
873
+
874
+ it('40 implemented nodes exist', () => {
875
+ const engine = new FlowEngine();
876
+ engine.loadFlow(flow);
877
+ const implemented = engine.getNodesWhere(n => n.runtime_status === 'implemented');
878
+ assert.equal(implemented.length, 40);
879
+ });
880
+ });