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.
@@ -52,10 +52,11 @@ function makeMockCallbacks() {
52
52
  }
53
53
 
54
54
  describe('Integration: Graph loading', () => {
55
- it('loads all 44 nodes from development-flow.json', () => {
55
+ it('loads all nodes from development-flow.json', () => {
56
56
  const engine = new FlowEngine();
57
57
  engine.loadFlow(flow);
58
- assert.equal(engine.getNodeCount(), 44);
58
+ const expectedCount = Object.keys(flow.nodes).length;
59
+ assert.equal(engine.getNodeCount(), expectedCount);
59
60
  });
60
61
 
61
62
  it('finds the entry node at "start"', () => {
@@ -68,8 +69,10 @@ describe('Integration: Graph loading', () => {
68
69
  const engine = new FlowEngine();
69
70
  engine.loadFlow(flow);
70
71
  const meta = engine.getFlowMetadata();
71
- assert.equal(meta.sync_metadata.audit_result.total_nodes, 44);
72
- assert.equal(meta.sync_metadata.audit_result.implemented, 42);
72
+ const expectedTotal = Object.keys(flow.nodes).length;
73
+ const expectedImpl = flow.nodes.filter(n => n.runtime_status === 'implemented').length;
74
+ assert.equal(meta.sync_metadata.audit_result.total_nodes, expectedTotal);
75
+ assert.equal(meta.sync_metadata.audit_result.implemented, expectedImpl);
73
76
  assert.equal(meta.sync_metadata.audit_result.partial, 0);
74
77
  assert.equal(meta.sync_metadata.audit_result.not_implemented, 0);
75
78
  assert.equal(meta.sync_metadata.audit_result.deprecated, 2);
@@ -79,7 +82,8 @@ describe('Integration: Graph loading', () => {
79
82
  const engine = new FlowEngine();
80
83
  engine.loadFlow(flow);
81
84
  const implemented = engine.getNodesWhere(n => n.runtime_status === 'implemented');
82
- assert.equal(implemented.length, 42);
85
+ const expectedImpl = flow.nodes.filter(n => n.runtime_status === 'implemented').length;
86
+ assert.equal(implemented.length, expectedImpl);
83
87
  });
84
88
 
85
89
  it('counts not_implemented nodes correctly', () => {
@@ -328,27 +332,26 @@ describe('Integration: Careful profile path (traverses past P2 nodes)', () => {
328
332
  });
329
333
 
330
334
  describe('Integration: Teams profile path (traverses past interview_teams)', () => {
331
- it('traverses interview_teams then stops at teams_spawn (deprecated)', async () => {
335
+ it('traverses interview_teams then continues to standard_analyze (teams_spawn bypassed)', async () => {
332
336
  const engine = new FlowEngine();
333
337
  engine.loadFlow(flow);
334
338
  const callbacks = makeMockCallbacks();
335
339
 
336
- await assert.rejects(
337
- engine.run(
340
+ // teams profile now bypasses deprecated teams_spawn, going to standard_analyze
341
+ try {
342
+ await engine.run(
338
343
  { task: 'build dashboard', profile: 'teams', complexity: 'high' },
339
344
  callbacks
340
- ),
341
- (err) => {
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');
345
- return true;
346
- }
347
- );
345
+ );
346
+ } catch (err) {
347
+ // May hit max traversals in review loop (mock callbacks cause loops) — OK
348
+ assert.ok(!err.message.includes('interview_teams'), 'Should NOT stop at interview_teams');
349
+ }
348
350
 
349
351
  const nodeStarts = callbacks.log.filter(e => e.type === 'node_start').map(e => e.id);
350
352
  assert.ok(nodeStarts.includes('interview_teams'), 'interview_teams was visited');
351
- assert.ok(nodeStarts.includes('teams_spawn'), 'teams_spawn was reached after interview');
353
+ assert.ok(nodeStarts.includes('standard_analyze'), 'standard_analyze was reached (bypassing deprecated teams_spawn)');
354
+ assert.ok(!nodeStarts.includes('teams_spawn'), 'teams_spawn should NOT be reached');
352
355
  });
353
356
 
354
357
  it('interview_teams uses teams topics', async () => {
@@ -836,23 +839,15 @@ describe('Integration: P5 Cleanup & Deprecation', () => {
836
839
  assert.equal(node.next, 'end_success');
837
840
  });
838
841
 
839
- it('DeprecatedNodeError is thrown for deprecated nodes', async () => {
842
+ it('deprecated nodes are unreachable via normal flow traversal', () => {
840
843
  const engine = new FlowEngine();
841
844
  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
- );
845
+ // Deprecated nodes exist in the graph but are orphaned (unreachable)
846
+ const deprecated = engine.getNodesWhere(n => n.runtime_status === 'deprecated');
847
+ assert.equal(deprecated.length, 2);
848
+ // Verify they are reported as warnings during validation (via public getter)
849
+ assert.ok(engine.getValidationWarnings().some(w => w.includes('teams_spawn')));
850
+ assert.ok(engine.getValidationWarnings().some(w => w.includes('interrupt_teams_failed')));
856
851
  });
857
852
 
858
853
  it('0 not_implemented nodes remain after P5', () => {
@@ -871,10 +866,11 @@ describe('Integration: P5 Cleanup & Deprecation', () => {
871
866
  assert.deepStrictEqual(ids, ['interrupt_teams_failed', 'teams_spawn']);
872
867
  });
873
868
 
874
- it('42 implemented nodes exist', () => {
869
+ it('all implemented nodes exist', () => {
875
870
  const engine = new FlowEngine();
876
871
  engine.loadFlow(flow);
877
872
  const implemented = engine.getNodesWhere(n => n.runtime_status === 'implemented');
878
- assert.equal(implemented.length, 42);
873
+ const expectedImpl = flow.nodes.filter(n => n.runtime_status === 'implemented').length;
874
+ assert.equal(implemented.length, expectedImpl);
879
875
  });
880
876
  });
@@ -14,8 +14,8 @@
14
14
  "sync_metadata": {
15
15
  "last_audit": "2026-02-09",
16
16
  "audit_result": {
17
- "total_nodes": 44,
18
- "implemented": 42,
17
+ "total_nodes": 47,
18
+ "implemented": 45,
19
19
  "partial": 0,
20
20
  "not_implemented": 0,
21
21
  "deprecated": 2,
@@ -69,6 +69,62 @@
69
69
  "skillRecommendations": "{{steps.discovery.output.recommended.skills || []}}",
70
70
  "installCommands": "{{steps.discovery.output.recommended.install_commands || []}}"
71
71
  },
72
+ "next": "interrupt_skill_install"
73
+ },
74
+
75
+ {
76
+ "id": "interrupt_skill_install",
77
+ "type": "interrupt",
78
+ "description": "Ofrecer instalacion de skills recomendados (top 5 mas relevantes)",
79
+ "runtime_status": "implemented",
80
+ "implemented_in": "commands/elsabro/execute.md#3-dispatch (type: interrupt)",
81
+ "reason": "Skill discovery found recommended skills to install",
82
+ "display": {
83
+ "title": "Skills Recomendados",
84
+ "content": "Se encontraron skills relevantes para esta tarea. Top 5 mas relevantes:",
85
+ "skills": "{{nodes.skill_discovery.outputs.skillRecommendations.slice(0, 5)}}",
86
+ "install_commands": "{{nodes.skill_discovery.outputs.installCommands.slice(0, 5)}}",
87
+ "options": [
88
+ { "id": "install", "label": "Instalar skills seleccionados" },
89
+ { "id": "skip", "label": "Continuar sin instalar" }
90
+ ]
91
+ },
92
+ "routes": {
93
+ "install": "install_skills",
94
+ "skip": "load_context"
95
+ },
96
+ "skipCondition": "{{!nodes.skill_discovery.outputs.skillRecommendations || nodes.skill_discovery.outputs.skillRecommendations.length === 0}}",
97
+ "skipRoute": "load_context"
98
+ },
99
+
100
+ {
101
+ "id": "install_skills",
102
+ "type": "sequence",
103
+ "description": "Instalar skills recomendados en batch paralelo",
104
+ "runtime_status": "implemented",
105
+ "implemented_in": "commands/elsabro/execute.md#3-dispatch (type: sequence)",
106
+ "errorPolicy": "continue",
107
+ "steps": [
108
+ {
109
+ "action": "bash",
110
+ "command": "{{nodes.skill_discovery.outputs.installCommands.slice(0, 5).map(c => c.command.replace(/[;&|$`]/g, '')).join(' & ')}} && wait",
111
+ "as": "batch_install",
112
+ "timeout": 60000,
113
+ "captureExitCode": true
114
+ },
115
+ {
116
+ "action": "bash",
117
+ "command": "npx skills list --json 2>/dev/null || echo '{\"skills\":[]}'",
118
+ "as": "verify_install",
119
+ "timeout": 15000,
120
+ "captureExitCode": true
121
+ }
122
+ ],
123
+ "outputs": {
124
+ "installResult": "{{steps.batch_install.output}}",
125
+ "installedSkills": "{{steps.verify_install.output}}",
126
+ "installExitCode": "{{steps.batch_install.exitCode}}"
127
+ },
72
128
  "next": "load_context"
73
129
  },
74
130
 
@@ -643,7 +699,9 @@
643
699
  "task": "{{inputs.task}}",
644
700
  "plan": "{{nodes.merge_analysis.outputs.plan || nodes.careful_analyze.outputs.detailed_plan || nodes.bmad_solution.outputs.output}}",
645
701
  "patterns": "{{state.patterns}}",
646
- "mistakes": "{{state.mistakes}}"
702
+ "mistakes": "{{state.mistakes}}",
703
+ "availableSkills": "{{state.availableSkills.installed.skills || []}}",
704
+ "recommendedSkills": "{{state.skillRecommendations || []}}"
647
705
  }
648
706
  },
649
707
  {
@@ -652,7 +710,9 @@
652
710
  "config": { "model": "opus", "timeout": 600000 },
653
711
  "inputs": {
654
712
  "task": "Crear tests para: {{inputs.task}}",
655
- "plan": "{{nodes.merge_analysis.outputs.plan || nodes.careful_analyze.outputs.detailed_plan || nodes.bmad_solution.outputs.output}}"
713
+ "plan": "{{nodes.merge_analysis.outputs.plan || nodes.careful_analyze.outputs.detailed_plan || nodes.bmad_solution.outputs.output}}",
714
+ "availableSkills": "{{state.availableSkills.installed.skills || []}}",
715
+ "recommendedSkills": "{{state.skillRecommendations || []}}"
656
716
  }
657
717
  }
658
718
  ],
@@ -701,10 +761,34 @@
701
761
  "runtime_status": "implemented",
702
762
  "implemented_in": "flow-engine/src/cli.js (condition auto-resolve)",
703
763
  "condition": "{{nodes.quality_gate.outputs.tests.exitCode === 0 && nodes.quality_gate.outputs.typescript.exitCode === 0 && nodes.quality_gate.outputs.lint.exitCode === 0}}",
704
- "true": "parallel_review",
764
+ "true": "check_review_skills",
705
765
  "false": "fix_issues"
706
766
  },
707
767
 
768
+ {
769
+ "id": "check_review_skills",
770
+ "type": "sequence",
771
+ "description": "Verificar que skills de review estan disponibles antes de parallel_review. Advertir si faltan pero continuar.",
772
+ "runtime_status": "implemented",
773
+ "implemented_in": "commands/elsabro/execute.md#3-dispatch (type: sequence)",
774
+ "errorPolicy": "continue",
775
+ "steps": [
776
+ {
777
+ "action": "bash",
778
+ "command": "bash ./hooks/check-review-skills.sh",
779
+ "as": "skill_check",
780
+ "timeout": 10000,
781
+ "captureExitCode": true
782
+ }
783
+ ],
784
+ "outputs": {
785
+ "reviewSkillsStatus": "{{steps.skill_check.output}}",
786
+ "hasAllSkills": "{{steps.skill_check.output.status === 'ok'}}"
787
+ },
788
+ "next": "parallel_review",
789
+ "warningOnMissing": "Review skills no instalados. El code review continuara con agentes disponibles pero puede ser menos exhaustivo. Considere instalar: npx skills add pr-review-toolkit -g"
790
+ },
791
+
708
792
  {
709
793
  "id": "fix_issues",
710
794
  "type": "parallel",
@@ -716,19 +800,28 @@
716
800
  "id": "debugger",
717
801
  "agent": "elsabro-debugger",
718
802
  "config": { "model": "opus" },
719
- "inputs": { "errors": "{{nodes.quality_gate.outputs}}" }
803
+ "inputs": {
804
+ "errors": "{{nodes.quality_gate.outputs}}",
805
+ "availableSkills": "{{state.availableSkills.installed.skills || []}}"
806
+ }
720
807
  },
721
808
  {
722
809
  "id": "error_detective",
723
810
  "agent": "error-detective",
724
811
  "config": { "model": "opus" },
725
- "inputs": { "errors": "{{nodes.quality_gate.outputs}}" }
812
+ "inputs": {
813
+ "errors": "{{nodes.quality_gate.outputs}}",
814
+ "availableSkills": "{{state.availableSkills.installed.skills || []}}"
815
+ }
726
816
  },
727
817
  {
728
818
  "id": "refactor",
729
819
  "agent": "refactoring-specialist",
730
820
  "config": { "model": "opus" },
731
- "inputs": { "issues": "{{nodes.quality_gate.outputs.lint}}" }
821
+ "inputs": {
822
+ "issues": "{{nodes.quality_gate.outputs.lint}}",
823
+ "availableSkills": "{{state.availableSkills.installed.skills || []}}"
824
+ }
732
825
  }
733
826
  ],
734
827
  "joinType": "all",
@@ -793,7 +886,8 @@
793
886
  "config": { "model": "opus" },
794
887
  "inputs": {
795
888
  "changes": "{{collectOutputs('filesModified')}}",
796
- "focus": "code quality, patterns, maintainability"
889
+ "focus": "code quality, patterns, maintainability",
890
+ "availableSkills": "{{state.availableSkills.installed.skills || []}}"
797
891
  }
798
892
  },
799
893
  {
@@ -802,7 +896,8 @@
802
896
  "config": { "model": "opus" },
803
897
  "inputs": {
804
898
  "changes": "{{collectOutputs('filesModified')}}",
805
- "focus": "error handling, edge cases, silent failures"
899
+ "focus": "error handling, edge cases, silent failures",
900
+ "availableSkills": "{{state.availableSkills.installed.skills || []}}"
806
901
  }
807
902
  },
808
903
  {
@@ -811,7 +906,8 @@
811
906
  "config": { "model": "opus" },
812
907
  "inputs": {
813
908
  "changes": "{{collectOutputs('filesModified')}}",
814
- "focus": "Staff Engineer review: architecture, scalability, security"
909
+ "focus": "Staff Engineer review: architecture, scalability, security",
910
+ "availableSkills": "{{state.availableSkills.installed.skills || []}}"
815
911
  }
816
912
  }
817
913
  ],
@@ -843,7 +939,8 @@
843
939
  "task": "Corregir TODOS los issues del code review. No dejar ninguno pendiente.",
844
940
  "issues": "{{nodes.parallel_review.outputs}}",
845
941
  "iteration": "{{state.reviewIteration || 1}}",
846
- "maxIterations": 5
942
+ "maxIterations": 5,
943
+ "availableSkills": "{{state.availableSkills.installed.skills || []}}"
847
944
  },
848
945
  "next": "parallel_review",
849
946
  "maxIterations": 5,
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env bash
2
+ # auto-sync-check.sh - ELSABRO Auto-Sync Validation Hook (v1.0.0)
3
+ #
4
+ # Valida que archivos criticos esten sincronizados despues de cada comando ELSABRO.
5
+ # Detecta desyncs de version, metadata obsoleta, y archivos no actualizados.
6
+ #
7
+ # Uso: bash ./hooks/auto-sync-check.sh [--fix]
8
+ # Output: JSON en stdout con warnings/errors
9
+ # Con --fix: intenta corregir desyncs simples automaticamente
10
+ #
11
+ # Archivos verificados:
12
+ # 1. package.json → version (source of truth)
13
+ # 2. .elsabro/state.json → version, updated_at
14
+ # 3. .elsabro/context.md → version mention
15
+ # 4. CHANGELOG.md → entry for current version
16
+ # 5. development-flow.json → sync_metadata (node counts)
17
+ # 6. README.md → version mentions (if any)
18
+ #
19
+ # Requiere: bash 4+, jq
20
+
21
+ set -euo pipefail
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
25
+ FIX_MODE="${1:-}"
26
+
27
+ # Colores para stderr
28
+ RED='\033[0;31m'
29
+ GREEN='\033[0;32m'
30
+ YELLOW='\033[1;33m'
31
+ BLUE='\033[0;34m'
32
+ NC='\033[0m'
33
+ PREFIX="[ELSABRO:sync]"
34
+
35
+ log_info() { echo -e "${BLUE}${PREFIX}${NC} $*" >&2; }
36
+ log_ok() { echo -e "${GREEN}${PREFIX}${NC} $*" >&2; }
37
+ log_warn() { echo -e "${YELLOW}${PREFIX}${NC} $*" >&2; }
38
+ log_error() { echo -e "${RED}${PREFIX}${NC} $*" >&2; }
39
+
40
+ # ============================================================================
41
+ # DEPENDENCY CHECK
42
+ # ============================================================================
43
+
44
+ if ! command -v jq &>/dev/null; then
45
+ echo '{"status":"error","message":"jq required but not installed"}'
46
+ exit 1
47
+ fi
48
+
49
+ # ============================================================================
50
+ # COLLECT DATA
51
+ # ============================================================================
52
+
53
+ ERRORS=()
54
+ WARNINGS=()
55
+ FIXES=()
56
+
57
+ # 1. package.json version (SOURCE OF TRUTH)
58
+ PKG_FILE="${PROJECT_ROOT}/package.json"
59
+ if [[ -f "$PKG_FILE" ]]; then
60
+ PKG_VERSION=$(jq -r '.version // "unknown"' "$PKG_FILE" 2>/dev/null || echo "unknown")
61
+ log_info "package.json version: $PKG_VERSION"
62
+ else
63
+ PKG_VERSION="unknown"
64
+ ERRORS+=("package.json not found at $PKG_FILE")
65
+ fi
66
+
67
+ # 2. .elsabro/state.json version
68
+ STATE_FILE="${PROJECT_ROOT}/.elsabro/state.json"
69
+ if [[ -f "$STATE_FILE" ]]; then
70
+ STATE_VERSION=$(jq -r '.version // "unknown"' "$STATE_FILE" 2>/dev/null || echo "unknown")
71
+ STATE_UPDATED=$(jq -r '.updated_at // "unknown"' "$STATE_FILE" 2>/dev/null || echo "unknown")
72
+ if [[ "$STATE_VERSION" != "$PKG_VERSION" && "$PKG_VERSION" != "unknown" ]]; then
73
+ ERRORS+=("state.json version ($STATE_VERSION) != package.json ($PKG_VERSION)")
74
+ if [[ "$FIX_MODE" == "--fix" ]]; then
75
+ jq --arg v "$PKG_VERSION" '.version = $v | .updated_at = (now | strftime("%Y-%m-%dT%H:%M:%SZ"))' \
76
+ "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
77
+ FIXES+=("state.json version updated to $PKG_VERSION")
78
+ fi
79
+ else
80
+ log_ok "state.json version: $STATE_VERSION (in sync)"
81
+ fi
82
+ else
83
+ WARNINGS+=("state.json not found (may not be initialized)")
84
+ fi
85
+
86
+ # 3. .elsabro/context.md version mention
87
+ CONTEXT_FILE="${PROJECT_ROOT}/.elsabro/context.md"
88
+ if [[ -f "$CONTEXT_FILE" ]]; then
89
+ if grep -qF "$PKG_VERSION" "$CONTEXT_FILE" 2>/dev/null; then
90
+ log_ok "context.md mentions version $PKG_VERSION"
91
+ else
92
+ WARNINGS+=("context.md does not mention current version $PKG_VERSION")
93
+ fi
94
+ else
95
+ WARNINGS+=("context.md not found (may not be initialized)")
96
+ fi
97
+
98
+ # 4. CHANGELOG.md entry for current version
99
+ CHANGELOG_FILE="${PROJECT_ROOT}/CHANGELOG.md"
100
+ if [[ -f "$CHANGELOG_FILE" ]]; then
101
+ if grep -qF "[$PKG_VERSION]" "$CHANGELOG_FILE" 2>/dev/null; then
102
+ log_ok "CHANGELOG.md has entry for [$PKG_VERSION]"
103
+ else
104
+ ERRORS+=("CHANGELOG.md missing entry for version [$PKG_VERSION]")
105
+ fi
106
+ else
107
+ WARNINGS+=("CHANGELOG.md not found")
108
+ fi
109
+
110
+ # 5. development-flow.json sync_metadata
111
+ FLOW_FILE="${PROJECT_ROOT}/flows/development-flow.json"
112
+ if [[ -f "$FLOW_FILE" ]]; then
113
+ # Count actual nodes
114
+ ACTUAL_NODES=$(jq '.nodes | length' "$FLOW_FILE" 2>/dev/null || echo "0")
115
+ # Read metadata count
116
+ META_NODES=$(jq '.sync_metadata.audit_result.total_nodes // 0' "$FLOW_FILE" 2>/dev/null || echo "0")
117
+
118
+ if [[ "$ACTUAL_NODES" != "$META_NODES" && "$META_NODES" != "0" ]]; then
119
+ ERRORS+=("flow sync_metadata.total_nodes ($META_NODES) != actual node count ($ACTUAL_NODES)")
120
+ if [[ "$FIX_MODE" == "--fix" ]]; then
121
+ jq --argjson n "$ACTUAL_NODES" '.sync_metadata.audit_result.total_nodes = $n' \
122
+ "$FLOW_FILE" > "${FLOW_FILE}.tmp" && mv "${FLOW_FILE}.tmp" "$FLOW_FILE"
123
+ FIXES+=("flow sync_metadata.total_nodes updated to $ACTUAL_NODES")
124
+ fi
125
+ else
126
+ log_ok "flow sync_metadata: $ACTUAL_NODES nodes (in sync)"
127
+ fi
128
+
129
+ # Check implemented count
130
+ ACTUAL_IMPL=$(jq '[.nodes[] | select(.runtime_status == "implemented")] | length' "$FLOW_FILE" 2>/dev/null || echo "0")
131
+ META_IMPL=$(jq '.sync_metadata.audit_result.implemented // 0' "$FLOW_FILE" 2>/dev/null || echo "0")
132
+ if [[ "$ACTUAL_IMPL" != "$META_IMPL" && "$META_IMPL" != "0" ]]; then
133
+ WARNINGS+=("flow sync_metadata.implemented ($META_IMPL) != actual ($ACTUAL_IMPL)")
134
+ if [[ "$FIX_MODE" == "--fix" ]]; then
135
+ jq --argjson n "$ACTUAL_IMPL" '.sync_metadata.audit_result.implemented = $n' \
136
+ "$FLOW_FILE" > "${FLOW_FILE}.tmp" && mv "${FLOW_FILE}.tmp" "$FLOW_FILE"
137
+ FIXES+=("flow sync_metadata.implemented updated to $ACTUAL_IMPL")
138
+ fi
139
+ fi
140
+
141
+ # Check deprecated count
142
+ ACTUAL_DEPR=$(jq '[.nodes[] | select(.runtime_status == "deprecated")] | length' "$FLOW_FILE" 2>/dev/null || echo "0")
143
+ META_DEPR=$(jq '.sync_metadata.audit_result.deprecated // 0' "$FLOW_FILE" 2>/dev/null || echo "0")
144
+ if [[ "$ACTUAL_DEPR" != "$META_DEPR" && "$META_DEPR" != "0" ]]; then
145
+ WARNINGS+=("flow sync_metadata.deprecated ($META_DEPR) != actual ($ACTUAL_DEPR)")
146
+ fi
147
+ else
148
+ WARNINGS+=("development-flow.json not found")
149
+ fi
150
+
151
+ # 6. README.md version (soft check - may not always mention version)
152
+ README_FILE="${PROJECT_ROOT}/README.md"
153
+ if [[ -f "$README_FILE" ]]; then
154
+ # Only check if README explicitly has a version badge or version line
155
+ if grep -qE 'v[0-9]+\.[0-9]+\.[0-9]+|version.*[0-9]+\.[0-9]+\.[0-9]+' "$README_FILE" 2>/dev/null; then
156
+ if grep -qF "$PKG_VERSION" "$README_FILE" 2>/dev/null; then
157
+ log_ok "README.md mentions version $PKG_VERSION"
158
+ else
159
+ WARNINGS+=("README.md has version references but not $PKG_VERSION")
160
+ fi
161
+ fi
162
+ fi
163
+
164
+ # ============================================================================
165
+ # GENERATE OUTPUT
166
+ # ============================================================================
167
+
168
+ STATUS="ok"
169
+ if [[ ${#ERRORS[@]} -gt 0 ]]; then
170
+ # If --fix mode applied fixes for ALL errors, re-check if errors remain unfixed
171
+ if [[ "$FIX_MODE" == "--fix" && ${#FIXES[@]} -ge ${#ERRORS[@]} ]]; then
172
+ STATUS="fixed"
173
+ else
174
+ STATUS="desync"
175
+ fi
176
+ fi
177
+
178
+ # Build JSON output
179
+ ERRORS_JSON="[]"
180
+ WARNINGS_JSON="[]"
181
+ FIXES_JSON="[]"
182
+
183
+ if [[ ${#ERRORS[@]} -gt 0 ]]; then
184
+ ERRORS_JSON=$(printf '%s\n' "${ERRORS[@]}" | jq -R '.' | jq -s '.')
185
+ fi
186
+ if [[ ${#WARNINGS[@]} -gt 0 ]]; then
187
+ WARNINGS_JSON=$(printf '%s\n' "${WARNINGS[@]}" | jq -R '.' | jq -s '.')
188
+ fi
189
+ if [[ ${#FIXES[@]} -gt 0 ]]; then
190
+ FIXES_JSON=$(printf '%s\n' "${FIXES[@]}" | jq -R '.' | jq -s '.')
191
+ fi
192
+
193
+ jq -n \
194
+ --arg status "$STATUS" \
195
+ --arg version "$PKG_VERSION" \
196
+ --argjson errors "$ERRORS_JSON" \
197
+ --argjson warnings "$WARNINGS_JSON" \
198
+ --argjson fixes "$FIXES_JSON" \
199
+ '{
200
+ status: $status,
201
+ version: $version,
202
+ timestamp: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
203
+ errors: {
204
+ count: ($errors | length),
205
+ items: $errors
206
+ },
207
+ warnings: {
208
+ count: ($warnings | length),
209
+ items: $warnings
210
+ },
211
+ fixes_applied: {
212
+ count: ($fixes | length),
213
+ items: $fixes
214
+ },
215
+ files_checked: [
216
+ "package.json",
217
+ ".elsabro/state.json",
218
+ ".elsabro/context.md",
219
+ "CHANGELOG.md",
220
+ "flows/development-flow.json",
221
+ "README.md"
222
+ ]
223
+ }'
224
+
225
+ # Summary to stderr
226
+ if [[ "$STATUS" == "ok" ]]; then
227
+ log_ok "All files in sync (v$PKG_VERSION)"
228
+ exit 0
229
+ elif [[ "$STATUS" == "fixed" ]]; then
230
+ log_ok "All errors auto-fixed (${#FIXES[@]} fixes applied)"
231
+ exit 0
232
+ else
233
+ log_error "DESYNC DETECTED: ${#ERRORS[@]} errors, ${#WARNINGS[@]} warnings"
234
+ if [[ ${#FIXES[@]} -gt 0 ]]; then
235
+ log_warn "Applied ${#FIXES[@]} fixes, but errors remain"
236
+ fi
237
+ exit 1
238
+ fi
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # check-review-skills.sh - Verify review skills are installed
3
+ #
4
+ # Checks if required code review skills (pr-review-toolkit, code-reviewer)
5
+ # are available. Outputs JSON status. Always exits 0 (informational only).
6
+ #
7
+ # Usage: bash ./hooks/check-review-skills.sh
8
+ # Output: JSON to stdout, warnings to stderr
9
+
10
+ set -euo pipefail
11
+
12
+ SKILLS_DIR="${HOME}/.claude/skills"
13
+ REQUIRED_SKILLS=("pr-review-toolkit" "code-reviewer")
14
+ MISSING=()
15
+
16
+ YELLOW='\033[1;33m'
17
+ GREEN='\033[0;32m'
18
+ NC='\033[0m'
19
+ PREFIX="[ELSABRO:skills]"
20
+
21
+ for skill in "${REQUIRED_SKILLS[@]}"; do
22
+ found=false
23
+ if [[ -d "$SKILLS_DIR" ]]; then
24
+ # Case-insensitive search in skills directory
25
+ if ls "$SKILLS_DIR" 2>/dev/null | grep -qi "$skill"; then
26
+ found=true
27
+ fi
28
+ fi
29
+ if [[ "$found" == "false" ]]; then
30
+ MISSING+=("$skill")
31
+ fi
32
+ done
33
+
34
+ if [[ ${#MISSING[@]} -eq 0 ]]; then
35
+ echo '{"status":"ok","missing":[]}'
36
+ echo -e "${GREEN}${PREFIX}${NC} All review skills installed" >&2
37
+ else
38
+ # Build JSON array safely with jq
39
+ MISSING_JSON=$(printf '%s\n' "${MISSING[@]}" | jq -R '.' | jq -s '.')
40
+ jq -n --argjson missing "$MISSING_JSON" '{"status":"warning","missing":$missing}'
41
+ echo -e "${YELLOW}${PREFIX}${NC} Missing review skills: ${MISSING[*]}" >&2
42
+ echo -e "${YELLOW}${PREFIX}${NC} Install with: npx skills add pr-review-toolkit -g" >&2
43
+ fi
44
+
45
+ exit 0