elsabro 7.3.2 → 7.5.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.
@@ -34,6 +34,78 @@ vive en el engine y en `flows/development-flow.json`. Este archivo solo define e
34
34
  Leer `.elsabro/state.json` siguiendo el protocolo de @references/state-sync.md.
35
35
  Verificar flujo en progreso. Actualizar phase a "stepping".
36
36
 
37
+ ### Skill Discovery Gate (OBLIGATORIO)
38
+
39
+ Antes de cualquier otra accion, verificar que skill discovery fue ejecutado:
40
+
41
+ ```javascript
42
+ // GATE: Verificar skill discovery antes de ejecutar
43
+ const skillGateResult = Bash('bash hooks/skill-gate.sh status');
44
+ const skillGate = JSON.parse(skillGateResult);
45
+
46
+ if (skillGate.elsabro_active && !skillGate.done) {
47
+ // Skill discovery no ejecutado — ejecutar ahora
48
+ output("Ejecutando skill discovery antes de continuar...");
49
+ state.context = state.context || {};
50
+ try {
51
+ const discoveryResult = Bash(`bash ./hooks/skill-discovery.sh "${inputs.task || args}" "medium"`, { timeout: 30000 });
52
+ try {
53
+ state.context.available_skills = JSON.parse(discoveryResult).recommended || [];
54
+ } catch (e) {
55
+ output("Warning: skill-discovery devolvio JSON invalido, continuando sin skills");
56
+ state.context.available_skills = [];
57
+ }
58
+ } catch (e) {
59
+ output("Warning: skill-discovery fallo, continuando sin skills");
60
+ state.context.available_skills = [];
61
+ }
62
+ Write(".elsabro/state.json", JSON.stringify(state, null, 2));
63
+ Bash('bash hooks/skill-gate.sh set "execute"');
64
+ }
65
+ ```
66
+
67
+ ### Plan Verification (OBLIGATORIO)
68
+
69
+ Antes de ejecutar CUALQUIER codigo, verificar que existe un plan aprobado:
70
+
71
+ ```javascript
72
+ // GATE: No ejecutar sin plan previo
73
+ state.context = state.context || {};
74
+ const planFile = state.context.plan_file;
75
+ const hasPlan = planFile && fs.existsSync(planFile);
76
+ const hasPlanInPlanning = fs.readdirSync('.planning/').some(f => f.endsWith('-PLAN.md'));
77
+
78
+ if (!hasPlan && !hasPlanInPlanning) {
79
+ // No hay plan — forzar planificacion primero
80
+ output("⚠ No se encontro plan de implementacion.");
81
+ output("→ Ejecuta /elsabro:plan primero, o usa EnterPlanMode para crear un plan.");
82
+ output("→ ELSABRO requiere un plan aprobado antes de ejecutar codigo.");
83
+
84
+ // Ofrecer opciones al usuario
85
+ const answer = AskUserQuestion({
86
+ questions: [{
87
+ question: "No hay plan de implementacion. ¿Que deseas hacer?",
88
+ header: "Plan Check",
89
+ options: [
90
+ { label: "Crear plan ahora (Recomendado)", description: "Entra a plan mode para disenar la implementacion" },
91
+ { label: "Continuar sin plan", description: "Solo para tareas triviales - se registra como excepcion" }
92
+ ],
93
+ multiSelect: false
94
+ }]
95
+ });
96
+
97
+ if (answer.includes("Crear plan")) {
98
+ EnterPlanMode();
99
+ return; // Exit execute — se reanuda despues de aprobar plan
100
+ }
101
+
102
+ // Usuario eligio continuar sin plan — registrar excepcion
103
+ state.context.plan_skipped = true;
104
+ state.context.plan_skip_reason = "user_override";
105
+ Write(".elsabro/state.json", JSON.stringify(state, null, 2));
106
+ }
107
+ ```
108
+
37
109
  ```bash
38
110
  FLOW="flows/development-flow.json"
39
111
  TASK="[descripcion de la tarea del usuario]"
@@ -311,10 +383,39 @@ for (const member of instruction.team.members) {
311
383
  })
312
384
  }
313
385
 
314
- // Al completar todos:
386
+ // Al completar todos - shutdown con verificacion:
315
387
  for (const member of instruction.team.members) {
316
- SendMessage({ type: "shutdown_request", recipient: member.name, content: "Done" })
388
+ SendMessage({ type: "shutdown_request", recipient: member.name, content: "Task complete" })
389
+ }
390
+
391
+ // Verificar que todos los teammates terminaron (max 30s timeout)
392
+ // Nota: sleep() es pseudocodigo. Implementar como: const sleep = ms => new Promise(r => setTimeout(r, ms));
393
+ const TEAM_SHUTDOWN_TIMEOUT = 30000;
394
+ const POLL_INTERVAL = 3000;
395
+ let elapsed = 0;
396
+ let allShutdown = false;
397
+
398
+ while (!allShutdown && elapsed < TEAM_SHUTDOWN_TIMEOUT) {
399
+ await sleep(POLL_INTERVAL); // ver nota arriba
400
+ elapsed += POLL_INTERVAL;
401
+ // Check if all members responded to shutdown
402
+ const teamStatus = TaskList(); // Check remaining active tasks
403
+ allShutdown = instruction.team.members.every(m =>
404
+ !teamStatus.some(t => t.owner === m.name && t.status === 'in_progress')
405
+ );
406
+ }
407
+
408
+ if (!allShutdown) {
409
+ // Emit telemetry for team lifecycle anomaly
410
+ emit_telemetry({
411
+ signal: "team_lifecycle_anomaly",
412
+ teamName: instruction.team.name,
413
+ timeout: TEAM_SHUTDOWN_TIMEOUT,
414
+ activeMembers: instruction.team.members.filter(m => /* still active */).map(m => m.name)
415
+ });
416
+ log_warn(`Team ${instruction.team.name}: ${elapsed}ms timeout - forcing cleanup`);
317
417
  }
418
+
318
419
  TeamDelete()
319
420
  // result: team outputs aggregated
320
421
  ```
@@ -452,6 +553,41 @@ for (const step of instruction.steps) {
452
553
  // result: stepOutputs with all step execution results
453
554
  ```
454
555
 
556
+ ## 3.1. Skill Invocation Protocol
557
+
558
+ Cuando un agente recibe `availableSkills` en sus inputs, DEBE seguir este protocolo:
559
+
560
+ ### Prioridad de Uso
561
+ 1. **Skills instalados relevantes** — Usar primero los que ya estan instalados y matchean la tarea
562
+ 2. **Skills recomendados** — Si `recommendedSkills` incluye skills no instalados pero criticos, sugerir instalacion
563
+ 3. **Implementacion manual** — Solo si no hay skill apropiado disponible
564
+
565
+ ### Reglas
566
+ - **Top 5**: Solo considerar los 5 skills mas relevantes para la tarea actual
567
+ - **Silencioso**: No mostrar al usuario la consulta de skills - solo usarlos
568
+ - **Fallback**: Si un skill falla o no aplica, continuar con implementacion manual sin bloquear
569
+ - **Context7**: SIEMPRE consultar Context7 silenciosamente antes de tocar codigo que use librerias externas
570
+ - **Reportar**: Al final, incluir lista de skills usados en el output del agente
571
+
572
+ ### Context7 Integration
573
+ Antes de implementar codigo que use APIs externas:
574
+ 1. Resolver library ID: `mcp__context7__resolve-library-id` con el nombre de la libreria
575
+ 2. Consultar docs: `mcp__context7__query-docs` con la query relevante
576
+ 3. Aplicar patterns encontrados — si difieren de lo que se conoce, ALERTAR
577
+ 4. Todo esto es silencioso — el usuario no ve estas consultas
578
+
579
+ ### Telemetria
580
+ Emitir signal despues de cada agente que use skills:
581
+ ```json
582
+ {
583
+ "signal": "skill_usage",
584
+ "nodeId": "{{instruction.nodeId}}",
585
+ "skills_available": ["..."],
586
+ "skills_used": ["..."],
587
+ "skills_failed": ["..."]
588
+ }
589
+ ```
590
+
455
591
  ## 4. Observabilidad (4 Senales)
456
592
 
457
593
  Despues de cada `step` y `complete`, emitir al log:
@@ -484,15 +620,166 @@ Cuando el loop retorna `{ finished: true }`:
484
620
  - Establecer `suggested_next: "verify-work"`
485
621
  2. Actualizar `.elsabro/context.md` con resumen legible
486
622
 
623
+ <code_review_gate>
624
+ ## 5.1. Code Review Gate (OBLIGATORIO - NO NEGOCIABLE)
625
+
626
+ **ANTES de mostrar resultado o ofrecer commit, VERIFICAR:**
627
+
628
+ ```
629
+ ¿Se escribió/modificó código durante la ejecución?
630
+
631
+ ├─ SÍ → ¿Se ejecutó code review (parallel_review node)?
632
+ │ │
633
+ │ ├─ NO → EJECUTAR AHORA con Agent Teams FULL (5 agentes):
634
+ │ │ 1. bash hooks/review-gate.sh set (si no esta activo)
635
+ │ │ 2. TeamCreate("elsabro-review") + 5 teammates especializados
636
+ │ │ 3. Consolidar hallazgos de los 5 reviewers
637
+ │ │ 4. Si issues criticos > 0: fix y re-review (max 3 iteraciones)
638
+ │ │ 5. Solo cuando issues criticos == 0: bash hooks/review-gate.sh clear
639
+ │ │ 6. SendMessage(shutdown_request) x5 + TeamDelete()
640
+ │ │ 7. Continuar
641
+ │ │
642
+ │ └─ SÍ, issues == 0, gate cleared → Continuar
643
+
644
+ └─ NO → Continuar (no aplica)
645
+ ```
646
+
647
+ ```javascript
648
+ // Verificacion programatica del review gate
649
+ const gateStatus = Bash("bash hooks/review-gate.sh status");
650
+ const gate = JSON.parse(gateStatus);
651
+
652
+ state.current_flow = state.current_flow || {};
653
+
654
+ if (gate.pending) {
655
+ // Hay codigo modificado sin review — EJECUTAR REVIEW con Agent Teams FULL
656
+ state.current_flow.code_written = true;
657
+ output("⚠ Review gate activo: " + gate.count + " archivo(s) sin revisar");
658
+ output("→ Lanzando Agent Teams FULL review (5 agentes especializados)...");
659
+
660
+ // 1. Crear team de review
661
+ TeamCreate({ team_name: "elsabro-review", description: "Code review team - 5 agentes especializados" })
662
+
663
+ // 2. Lanzar 5 teammates en paralelo (UN SOLO MENSAJE con multiples Task calls)
664
+ Task({
665
+ subagent_type: "pr-review-toolkit:code-reviewer",
666
+ team_name: "elsabro-review",
667
+ name: "quality-reviewer",
668
+ model: "sonnet",
669
+ prompt: "Review code quality: naming conventions, design patterns, DRY, SOLID principles. Focus on recently modified files. DO NOT ask questions. Report only high-confidence issues."
670
+ })
671
+ Task({
672
+ subagent_type: "feature-dev:code-reviewer",
673
+ team_name: "elsabro-review",
674
+ name: "security-reviewer",
675
+ model: "sonnet",
676
+ prompt: "Review security: OWASP top 10, injection vulnerabilities, XSS, secrets exposure, token handling, input validation. Focus on recently modified files. DO NOT ask questions. Report only high-confidence issues."
677
+ })
678
+ Task({
679
+ subagent_type: "pr-review-toolkit:pr-test-analyzer",
680
+ team_name: "elsabro-review",
681
+ name: "test-analyzer",
682
+ model: "sonnet",
683
+ prompt: "Analyze test coverage: missing tests, edge cases, happy/error paths, test quality. Focus on recently modified files. DO NOT ask questions. Report only critical gaps."
684
+ })
685
+ Task({
686
+ subagent_type: "pr-review-toolkit:silent-failure-hunter",
687
+ team_name: "elsabro-review",
688
+ name: "performance-reviewer",
689
+ model: "sonnet",
690
+ prompt: "Review for silent failures, N+1 queries, memory leaks, inadequate error handling, dangerous fallback behavior. Focus on recently modified files. DO NOT ask questions. Report only high-confidence issues."
691
+ })
692
+ Task({
693
+ subagent_type: "pr-review-toolkit:type-design-analyzer",
694
+ team_name: "elsabro-review",
695
+ name: "type-analyzer",
696
+ model: "sonnet",
697
+ prompt: "Analyze type design: encapsulation, invariant expression, null safety, interface correctness. Focus on recently modified files. DO NOT ask questions. Report only high-confidence issues."
698
+ })
699
+
700
+ // 3. Consolidar resultados — esperar que todos terminen
701
+ // Agregar hallazgos de cada reviewer al reporte consolidado
702
+ // Si ANY reviewer reporta issues criticos: fix y re-review (max 3 iteraciones)
703
+
704
+ // 4. Shutdown team
705
+ for (const name of ["quality-reviewer", "security-reviewer", "test-analyzer", "performance-reviewer", "type-analyzer"]) {
706
+ SendMessage({ type: "shutdown_request", recipient: name, content: "Review complete" })
707
+ }
708
+ TeamDelete()
709
+
710
+ // 5. Si review pasa sin issues criticos, limpiar gate y setear flag
711
+ Bash("bash hooks/review-gate.sh clear");
712
+ state.current_flow.code_review_passed = true;
713
+ Write(".elsabro/state.json", JSON.stringify(state, null, 2));
714
+ }
715
+ ```
716
+
717
+ **Donde se setean los flags:**
718
+ - `state.current_flow.code_written = true` — se setea aqui (5.1) cuando review-gate.sh detecta archivos pendientes, y tambien por el PostToolUse hook al hacer Write/Edit
719
+ - `state.current_flow.code_review_passed = true` — se setea aqui (5.1) despues de que los 5 reviewers pasan sin issues criticos y el gate se limpia
720
+
721
+ **Agent Teams FULL — 5 Reviewers:**
722
+ | Teammate | Plugin | Foco |
723
+ |----------|--------|------|
724
+ | quality-reviewer | pr-review-toolkit:code-reviewer | naming, patterns, DRY, SOLID |
725
+ | security-reviewer | feature-dev:code-reviewer | OWASP, injection, secrets |
726
+ | test-analyzer | pr-review-toolkit:pr-test-analyzer | coverage, edge cases |
727
+ | performance-reviewer | pr-review-toolkit:silent-failure-hunter | silent failures, N+1, memory |
728
+ | type-analyzer | pr-review-toolkit:type-design-analyzer | types, encapsulation, null safety |
729
+
730
+ **VIOLACIÓN CRÍTICA**: Reportar resultado o ofrecer commit sin code review = ABORTAR OPERACIÓN
731
+ </code_review_gate>
732
+
733
+ <siguiente_paso>
487
734
  ## 6. Siguiente Paso
488
735
 
489
- **Gate checks** (por referencia a @references/enforcement-rules.md):
490
- - Rule 7: Si se escribio codigo -> code review debe haber pasado (el flow lo garantiza via parallel_review)
491
- - Rule 8: Si se usaron 2+ agentes -> Agent Teams debe haberse usado (callbacks.js lo garantiza)
736
+ Al completar la ejecucion, verificar gates programaticamente antes de sugerir siguiente paso:
737
+
738
+ ```javascript
739
+ // GATE CHECK: Enforcement estricto antes de cerrar
740
+ state.current_flow = state.current_flow || {};
741
+
742
+ // Cross-check: review-gate.sh es la fuente de verdad para codigo pendiente
743
+ const gateStatus = Bash("bash hooks/review-gate.sh status");
744
+ const gate = JSON.parse(gateStatus);
745
+
746
+ // Si review-gate tiene archivos pendientes, code_written DEBE estar seteado
747
+ if (gate.pending && !state.current_flow.code_written) {
748
+ state.current_flow.code_written = true; // Auto-fix inconsistencia
749
+ }
750
+
751
+ // Flags verificados
752
+ const codeWritten = state.current_flow.code_written === true;
753
+ const codeReviewPassed = state.current_flow.code_review_passed === true;
754
+
755
+ // Rule 7: Si se escribio codigo, code review DEBE haber pasado
756
+ if (codeWritten && !codeReviewPassed) {
757
+ // VIOLACIÓN CRÍTICA — NO proceder. Volver a code_review_gate.
758
+ output("⛔ CRITICAL: Code written without review. Execute section 5.1 first.");
759
+ output("→ Volver a Code Review Gate (seccion 5.1) antes de continuar.");
760
+ return; // Bloquea — no se puede llegar a suggested_next sin review
761
+ }
762
+
763
+ // Rule 7b: Double-check con review-gate.sh (fuente de verdad del sistema)
764
+ if (gate.pending) {
765
+ output("⛔ REVIEW GATE PENDING: " + gate.count + " file(s) sin revisar.");
766
+ output("→ Ejecutar code review y bash hooks/review-gate.sh clear primero.");
767
+ return; // Bloquea — gate aun activo
768
+ }
769
+
770
+ // Rule 8: Si se usaron 2+ agentes, Agent Teams debe haberse usado
771
+ // (callbacks.js lo garantiza, pero verificamos por seguridad)
772
+
773
+ // Todo limpio — establecer siguiente paso
774
+ state.suggested_next = "verify-work";
775
+ Write(".elsabro/state.json", JSON.stringify(state, null, 2));
776
+ ```
492
777
 
778
+ Mostrar al usuario:
493
779
  ```
494
780
  Siguiente Paso
495
781
 
496
- -> /elsabro:verify-work -- verificar el trabajo completado
497
- -> /elsabro:progress -- ver el progreso general del proyecto
782
+ /elsabro:verify-work verificar el trabajo completado
783
+ /elsabro:progress ver el progreso general del proyecto
498
784
  ```
785
+ </siguiente_paso>
@@ -1,6 +1,18 @@
1
1
  ---
2
2
  name: quick
3
3
  description: Modo de ejecución rápida para tareas simples - mínima ceremonia, máxima velocidad
4
+ allowed-tools:
5
+ - Read
6
+ - Write
7
+ - Edit
8
+ - Bash
9
+ - Glob
10
+ - Grep
11
+ - Task
12
+ - TeamCreate
13
+ - TeamDelete
14
+ - SendMessage
15
+ - AskUserQuestion
4
16
  sync:
5
17
  reads: [".elsabro/state.json"]
6
18
  writes: [".elsabro/state.json", ".elsabro/context.md"]
@@ -126,9 +138,11 @@ ELSABRO: [Ejecuta en <30 segundos]
126
138
  4. Verify (10 seg)
127
139
  └─ Correr tests afectados
128
140
 
129
- 5. Code Review (OBLIGATORIO)
130
- └─ Task(pr-review-toolkit:code-reviewer) sobre archivos modificados
131
- └─ Si hay issues: fix antes de reportar
141
+ 5. Code Review (OBLIGATORIO - Agent Teams LITE)
142
+ └─ TeamCreate("elsabro-review-lite") + 3 teammates especializados
143
+ └─ quality-reviewer + failure-hunter + test-analyzer (en paralelo)
144
+ └─ Consolidar hallazgos. Si issues criticos: fix y re-review
145
+ └─ SendMessage(shutdown_request) x3 + TeamDelete()
132
146
  └─ Si issues == 0: state.current_flow.code_review_passed = true ← MARCAR
133
147
 
134
148
  6. Report
@@ -141,7 +155,7 @@ ELSABRO: [Ejecuta en <30 segundos]
141
155
  1. **Max 2 preguntas** - Si necesitas más info, usa `/elsabro:plan`
142
156
  2. **Max 3 archivos** - Si afecta más, usa flujo normal
143
157
  3. **Auto-test** - Siempre corre tests relacionados
144
- 4. **Auto-review** - SIEMPRE ejecutar code review con `Task(pr-review-toolkit:code-reviewer)` después de escribir código
158
+ 4. **Auto-review** - SIEMPRE ejecutar code review con Agent Teams LITE (3 agentes) después de escribir código
145
159
  5. **No docs** - Skip documentación para velocidad
146
160
  6. **Offer commit** - Siempre pregunta si commitear
147
161
 
@@ -177,18 +191,27 @@ Task({
177
191
  ```
178
192
  ¿Se escribió/modificó código?
179
193
 
180
- ├─ SÍ → ¿Se ejecutó code review (Paso 5)?
194
+ ├─ SÍ → ¿Se ejecutó code review con Agent Teams LITE (Paso 5)?
181
195
  │ │
182
196
  │ ├─ NO → EJECUTAR AHORA:
183
- │ │ Task(pr-review-toolkit:code-reviewer)
184
- │ │ Si issues > 0: fix y re-review
185
- │ │ Solo cuando issues == 0: continuar
197
+ │ │ TeamCreate("elsabro-review-lite")
198
+ │ │ + 3 teammates: quality-reviewer, failure-hunter, test-analyzer
199
+ │ │ Consolidar hallazgos
200
+ │ │ Si issues criticos > 0: fix y re-review
201
+ │ │ Solo cuando issues == 0: shutdown + TeamDelete + continuar
186
202
  │ │
187
- │ └─ SÍ, issues == 0 → Continuar
203
+ │ └─ SÍ, issues == 0, team deleted → Continuar
188
204
 
189
205
  └─ NO → Continuar (no aplica)
190
206
  ```
191
207
 
208
+ **Agent Teams LITE — 3 Reviewers:**
209
+ | Teammate | Plugin | Foco |
210
+ |----------|--------|------|
211
+ | quality-reviewer | pr-review-toolkit:code-reviewer | bugs, patterns, naming |
212
+ | failure-hunter | pr-review-toolkit:silent-failure-hunter | error handling, fallbacks |
213
+ | test-analyzer | pr-review-toolkit:pr-test-analyzer | coverage, edge cases |
214
+
192
215
  **VIOLACIÓN CRÍTICA**: Reportar resultado sin code review = ABORTAR OPERACIÓN
193
216
  </code_review_gate>
194
217
 
@@ -66,11 +66,16 @@ function buildGraph(flowDefinition) {
66
66
  * Validate that all node references (next, routes, true, false, onMaxIterations,
67
67
  * onError) point to existing nodes, and detect orphaned nodes.
68
68
  *
69
+ * Returns errors (dangling references — fatal) and warnings (orphaned nodes —
70
+ * informational). Deprecated and standalone nodes are reported as warnings,
71
+ * not errors, since they are intentionally disconnected.
72
+ *
69
73
  * @param {{ nodes: Map<string, object>, entryNode: string }} graph
70
- * @returns {{ valid: boolean, errors: string[] }}
74
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
71
75
  */
72
76
  function validateGraph(graph) {
73
77
  const errors = [];
78
+ const warnings = [];
74
79
  const nodeIds = new Set(graph.nodes.keys());
75
80
 
76
81
  // 1. Check for dangling references (invalid routes)
@@ -103,14 +108,21 @@ function validateGraph(graph) {
103
108
  }
104
109
  }
105
110
 
106
- // Find orphaned nodes
111
+ // Classify orphaned nodes: deprecated/standalone → warning, others → warning too
112
+ // (orphaned nodes are informational, not fatal — dangling refs are fatal)
107
113
  for (const nodeId of nodeIds) {
108
114
  if (!reachable.has(nodeId)) {
109
- errors.push(`Orphaned node "${nodeId}" is unreachable from entry point`);
115
+ const node = graph.nodes.get(nodeId);
116
+ const status = node?.runtime_status || '';
117
+ if (status === 'deprecated') {
118
+ warnings.push(`Deprecated node "${nodeId}" is unreachable from entry point`);
119
+ } else {
120
+ warnings.push(`Orphaned node "${nodeId}" is unreachable from entry point`);
121
+ }
110
122
  }
111
123
  }
112
124
 
113
- return { valid: errors.length === 0, errors };
125
+ return { valid: errors.length === 0, errors, warnings };
114
126
  }
115
127
 
116
128
  /**
@@ -50,9 +50,19 @@ class FlowEngine {
50
50
  );
51
51
  }
52
52
 
53
+ this._validationWarnings = validation.warnings || [];
53
54
  return this;
54
55
  }
55
56
 
57
+ /**
58
+ * Get validation warnings from the last loadFlow() call.
59
+ * Warnings are informational (e.g. orphaned nodes) and do not prevent flow execution.
60
+ * @returns {string[]}
61
+ */
62
+ getValidationWarnings() {
63
+ return this._validationWarnings || [];
64
+ }
65
+
56
66
  /**
57
67
  * Get a node by ID.
58
68
  * @param {string} id
@@ -73,17 +73,18 @@ registerFunction('collectErrors', (context) => {
73
73
  registerFunction('hasCriticalIssues', (context, obj) => {
74
74
  if (!obj) return false;
75
75
  const json = typeof obj === 'string' ? obj : JSON.stringify(obj);
76
+ const jsonLower = json.toLowerCase();
76
77
 
77
- // Enhanced pattern detection for critical issues
78
+ // Case-insensitive pattern detection for critical issues
78
79
  const criticalPatterns = [
79
80
  '"critical"',
80
81
  '"blocking"',
81
- '"P0"',
82
- '"MUST_FIX"',
83
- '"URGENT"'
82
+ '"p0"',
83
+ '"must_fix"',
84
+ '"urgent"'
84
85
  ];
85
86
 
86
- return criticalPatterns.some(pattern => json.includes(pattern));
87
+ return criticalPatterns.some(pattern => jsonLower.includes(pattern));
87
88
  });
88
89
 
89
90
  registerFunction('generateSummary', (context) => {
@@ -48,10 +48,11 @@ describe('CLI: helpers', () => {
48
48
  // ---------- validate ----------
49
49
 
50
50
  describe('CLI: validate', () => {
51
- it('reports valid flow with 44 nodes', async () => {
51
+ it('reports valid flow with correct node count', async () => {
52
52
  const result = await main(['node', 'cli.js', 'validate', '--flow', FLOW_PATH]);
53
+ const flow = require('../../flows/development-flow.json');
53
54
  assert.equal(result.valid, true);
54
- assert.equal(result.nodeCount, 44);
55
+ assert.equal(result.nodeCount, flow.nodes.length);
55
56
  assert.ok(result.parallelNodes.length >= 4);
56
57
  });
57
58
 
@@ -385,7 +385,8 @@ describe('Execute Dispatcher: Error Handling', () => {
385
385
 
386
386
  it('condition node throws when branch is missing', async () => {
387
387
  const executor = getExecutor('condition');
388
- const ctx = makeContext({ inputs: { val: true } });
388
+ // Condition evaluates to false, but no "false" branch is defined
389
+ const ctx = makeContext({ inputs: { val: false } });
389
390
  await assert.rejects(
390
391
  executor(
391
392
  { id: 'cond', type: 'condition', condition: '{{inputs.val}}', true: 'next' },
@@ -160,8 +160,8 @@ describe('flow validation enhancements', () => {
160
160
  ]
161
161
  });
162
162
  const result = validateGraph(graph);
163
- assert.equal(result.valid, false);
164
- assert.ok(result.errors.some(e => e.includes('orphan') && e.includes('unreachable')));
163
+ assert.equal(result.valid, true); // orphans are warnings, not errors
164
+ assert.ok(result.warnings.some(e => e.includes('orphan') && e.includes('unreachable')));
165
165
  });
166
166
 
167
167
  it('detects orphaned nodes in complex branching', () => {
@@ -175,9 +175,9 @@ describe('flow validation enhancements', () => {
175
175
  ]
176
176
  });
177
177
  const result = validateGraph(graph);
178
- assert.equal(result.valid, false);
179
- assert.ok(result.errors.some(e => e.includes('isolated_branch') && e.includes('unreachable')));
180
- assert.ok(result.errors.some(e => e.includes('isolated_end') && e.includes('unreachable')));
178
+ assert.equal(result.valid, true); // orphans are warnings, not errors
179
+ assert.ok(result.warnings.some(e => e.includes('isolated_branch') && e.includes('unreachable')));
180
+ assert.ok(result.warnings.some(e => e.includes('isolated_end') && e.includes('unreachable')));
181
181
  });
182
182
 
183
183
  it('validates onMaxIterations handler references', () => {
@@ -229,11 +229,11 @@ describe('flow validation enhancements', () => {
229
229
  ]
230
230
  });
231
231
  const result = validateGraph(graph);
232
- assert.equal(result.valid, false);
232
+ assert.equal(result.valid, false); // dangling ref is a real error
233
233
  // Should have dangling reference error (missing_next)
234
234
  assert.ok(result.errors.some(e => e.includes('missing_next')));
235
- // Should have orphaned node errors
236
- assert.ok(result.errors.some(e => e.includes('orphan')));
235
+ // Orphaned nodes are warnings, not errors
236
+ assert.ok(result.warnings.some(e => e.includes('orphan')));
237
237
  });
238
238
 
239
239
  it('passes validation for graph with all error handlers', () => {
@@ -276,9 +276,9 @@ describe('flow validation enhancements', () => {
276
276
  ]
277
277
  });
278
278
  const result = validateGraph(graph);
279
- assert.equal(result.valid, false);
280
- assert.ok(result.errors.some(e => e.includes('circular_a') && e.includes('unreachable')));
281
- assert.ok(result.errors.some(e => e.includes('circular_b') && e.includes('unreachable')));
279
+ assert.equal(result.valid, true); // orphans are warnings, not errors
280
+ assert.ok(result.warnings.some(e => e.includes('circular_a') && e.includes('unreachable')));
281
+ assert.ok(result.warnings.some(e => e.includes('circular_b') && e.includes('unreachable')));
282
282
  });
283
283
 
284
284
  it('validates condition node with both branches pointing to same target', () => {
@@ -316,12 +316,15 @@ describe('flow validation enhancements', () => {
316
316
  ]
317
317
  });
318
318
  const result = validateGraph(graph);
319
- assert.equal(result.valid, false);
320
- // Check error message format includes node IDs and issue type
319
+ assert.equal(result.valid, false); // dangling refs are real errors
320
+ // Check error message format for dangling references
321
+ assert.ok(result.errors.length > 0);
321
322
  result.errors.forEach(err => {
322
323
  assert.ok(typeof err === 'string');
323
324
  assert.ok(err.length > 0);
324
325
  });
326
+ // Orphaned node reported as warning
327
+ assert.ok(result.warnings.some(w => w.includes('orphan')));
325
328
  });
326
329
  });
327
330
 
@@ -329,7 +332,7 @@ describe('real flow loading', () => {
329
332
  it('loads the full development-flow.json', () => {
330
333
  const flow = require('../../flows/development-flow.json');
331
334
  const graph = buildGraph(flow);
332
- assert.equal(graph.nodes.size, 44);
335
+ assert.equal(graph.nodes.size, flow.nodes.length);
333
336
  assert.equal(graph.entryNode, 'start');
334
337
  assert.equal(graph.meta.version, '5.3.0');
335
338
  });
@@ -338,17 +341,15 @@ describe('real flow loading', () => {
338
341
  const flow = require('../../flows/development-flow.json');
339
342
  const graph = buildGraph(flow);
340
343
  const result = validateGraph(graph);
341
- // Development flow has known orphaned nodes (P0.3 - teams mode deprecated nodes)
342
- // Expected orphaned nodes: teams_spawn, interrupt_teams_failed, design_ui, interrupt_design_complete
343
- if (!result.valid) {
344
- const orphanedNodes = result.errors.filter(e => e.includes('unreachable'));
345
- assert.ok(orphanedNodes.length > 0, 'Should detect orphaned nodes');
346
- // Verify these are the known teams mode orphaned nodes
347
- const hasTeamsNodes = orphanedNodes.some(e =>
348
- e.includes('teams_spawn') || e.includes('interrupt_teams') ||
349
- e.includes('design_ui') || e.includes('interrupt_design')
350
- );
351
- assert.ok(hasTeamsNodes, 'Orphaned nodes should include known teams mode nodes');
352
- }
344
+ // Flow should be valid (no dangling references)
345
+ assert.equal(result.valid, true, 'Flow should have no dangling reference errors');
346
+ assert.equal(result.errors.length, 0);
347
+ // Known orphaned nodes reported as warnings (deprecated + standalone subflows)
348
+ assert.ok(result.warnings.length > 0, 'Should have warnings for orphaned nodes');
349
+ const hasKnownOrphans = result.warnings.some(w =>
350
+ w.includes('teams_spawn') || w.includes('interrupt_teams') ||
351
+ w.includes('design_ui') || w.includes('interrupt_design')
352
+ );
353
+ assert.ok(hasKnownOrphans, 'Warnings should include known orphaned nodes');
353
354
  });
354
355
  });