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.
- package/README.md +80 -23
- package/agents/elsabro-executor.md +32 -0
- package/agents/elsabro-orchestrator.md +39 -0
- package/agents/elsabro-qa.md +37 -0
- package/agents/elsabro-verifier.md +37 -0
- package/bin/install.js +71 -0
- package/commands/elsabro/debug.md +54 -16
- package/commands/elsabro/execute.md +294 -7
- package/commands/elsabro/quick.md +32 -9
- package/flow-engine/src/graph.js +16 -4
- package/flow-engine/src/index.js +10 -0
- package/flow-engine/src/template.js +6 -5
- package/flow-engine/tests/cli.test.js +3 -2
- package/flow-engine/tests/execute-dispatcher.test.js +2 -1
- package/flow-engine/tests/graph.test.js +27 -26
- package/flow-engine/tests/integration.test.js +30 -34
- package/flows/development-flow.json +109 -12
- package/hooks/auto-sync-check.sh +238 -0
- package/hooks/check-review-skills.sh +45 -0
- package/hooks/hooks-config-updated.json +68 -10
- package/hooks/review-gate.sh +90 -0
- package/hooks/skill-gate.sh +107 -0
- package/package.json +1 -1
- package/references/enforcement-rules.md +43 -16
|
@@ -52,10 +52,11 @@ function makeMockCallbacks() {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
describe('Integration: Graph loading', () => {
|
|
55
|
-
it('loads all
|
|
55
|
+
it('loads all nodes from development-flow.json', () => {
|
|
56
56
|
const engine = new FlowEngine();
|
|
57
57
|
engine.loadFlow(flow);
|
|
58
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
337
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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('
|
|
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('
|
|
842
|
+
it('deprecated nodes are unreachable via normal flow traversal', () => {
|
|
840
843
|
const engine = new FlowEngine();
|
|
841
844
|
engine.loadFlow(flow);
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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('
|
|
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
|
-
|
|
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":
|
|
18
|
-
"implemented":
|
|
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": "
|
|
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": {
|
|
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": {
|
|
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": {
|
|
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
|