xp-gate 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapter-common.sh +192 -0
- package/adapters/cpp.sh +76 -0
- package/adapters/dart.sh +41 -0
- package/adapters/flutter.sh +41 -0
- package/adapters/go.sh +59 -0
- package/adapters/iac.sh +189 -0
- package/adapters/java.sh +191 -0
- package/adapters/kotlin.sh +77 -0
- package/adapters/objectivec.sh +38 -0
- package/adapters/powershell.sh +138 -0
- package/adapters/python.sh +104 -0
- package/adapters/shell.sh +55 -0
- package/adapters/swift.sh +44 -0
- package/adapters/typescript.sh +61 -0
- package/bin/xp-gate.js +157 -0
- package/hooks/adapter-common.sh +192 -0
- package/hooks/pre-commit +1667 -0
- package/hooks/pre-push +395 -0
- package/lib/__tests__/detect-deps.test.js +209 -0
- package/lib/__tests__/doctor.test.js +448 -0
- package/lib/__tests__/download-skill.test.js +281 -0
- package/lib/__tests__/init.test.js +327 -0
- package/lib/__tests__/install-skill.test.js +326 -0
- package/lib/__tests__/migrate.test.js +212 -0
- package/lib/__tests__/rollback.test.js +183 -0
- package/lib/__tests__/ui-detector.test.ts +200 -0
- package/lib/__tests__/uninstall-skill.test.js +189 -0
- package/lib/__tests__/uninstall.test.js +589 -0
- package/lib/__tests__/update-skill.test.js +276 -0
- package/lib/detect-deps.js +157 -0
- package/lib/doctor.js +370 -0
- package/lib/download-skill.js +96 -0
- package/lib/init.js +367 -0
- package/lib/install-skill.js +184 -0
- package/lib/migrate.js +120 -0
- package/lib/rollback.js +78 -0
- package/lib/ui-detector.ts +99 -0
- package/lib/uninstall-skill.js +69 -0
- package/lib/uninstall.js +401 -0
- package/lib/update-skill.js +90 -0
- package/package.json +39 -0
- package/plugins/claude-code/.claude-plugin/plugin.json +21 -0
- package/plugins/claude-code/bin/delphi-review-guard.sh +68 -0
- package/plugins/claude-code/bin/xp-gate-check +47 -0
- package/plugins/claude-code/hooks/hooks.json +37 -0
- package/skills/delphi-review/.delphi-config.json.example +45 -0
- package/skills/delphi-review/AGENTS.md +54 -0
- package/skills/delphi-review/INSTALL.md +152 -0
- package/skills/delphi-review/SKILL.md +371 -0
- package/skills/delphi-review/evals/evals.json +82 -0
- package/skills/delphi-review/opencode.json.delphi.example +56 -0
- package/skills/delphi-review/references/code-walkthrough.md +486 -0
- package/skills/ralph-loop/SKILL.md +330 -0
- package/skills/ralph-loop/evals/evals.json +311 -0
- package/skills/ralph-loop/evolution-history.json +59 -0
- package/skills/ralph-loop/evolution-log.md +16 -0
- package/skills/ralph-loop/references/components/memory.md +55 -0
- package/skills/ralph-loop/references/components/middleware.md +54 -0
- package/skills/ralph-loop/references/components/skill-invocations.md +39 -0
- package/skills/ralph-loop/references/components/system-prompt.md +24 -0
- package/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
- package/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
- package/skills/ralph-loop/templates/progress-log.md +36 -0
- package/skills/sprint-flow/SKILL.md +600 -0
- package/skills/sprint-flow/evals/evals.json +78 -0
- package/skills/sprint-flow/evolution-history.json +39 -0
- package/skills/sprint-flow/evolution-log.md +23 -0
- package/skills/sprint-flow/references/components/memory.md +87 -0
- package/skills/sprint-flow/references/components/middleware.md +72 -0
- package/skills/sprint-flow/references/components/skill-invocations.md +104 -0
- package/skills/sprint-flow/references/components/system-prompt.md +27 -0
- package/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
- package/skills/sprint-flow/references/phase-0-think.md +115 -0
- package/skills/sprint-flow/references/phase-1-plan.md +178 -0
- package/skills/sprint-flow/references/phase-2-build.md +198 -0
- package/skills/sprint-flow/references/phase-3-review.md +213 -0
- package/skills/sprint-flow/references/phase-4-uat.md +125 -0
- package/skills/sprint-flow/references/phase-5-feedback.md +100 -0
- package/skills/sprint-flow/references/phase-6-ship.md +193 -0
- package/skills/sprint-flow/references/phase-7-land.md +140 -0
- package/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
- package/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
- package/skills/sprint-flow/templates/pain-document-template.md +115 -0
- package/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
- package/skills/test-specification-alignment/AGENTS.md +59 -0
- package/skills/test-specification-alignment/SKILL.md +605 -0
- package/skills/test-specification-alignment/evals/evals.json +75 -0
- package/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
- package/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
- package/skills/test-specification-alignment/references/specification-format.md +348 -0
package/hooks/pre-push
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pre-push Hook - Code Walkthrough Result Validator
|
|
3
|
+
#
|
|
4
|
+
# DESIGN: Hook validates result file, Skill executes review
|
|
5
|
+
# No CLI skill invocation - avoids OpenCode architecture mismatch
|
|
6
|
+
#
|
|
7
|
+
# See: docs/plans/delphi-review --mode code-walkthrough-pre-push-design-v2.md
|
|
8
|
+
#
|
|
9
|
+
# Install: cp this-file .git/hooks/pre-push && chmod +x .git/hooks/pre-push
|
|
10
|
+
|
|
11
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
12
|
+
echo " CODE WALKTHROUGH - PRE-PUSH CHECK"
|
|
13
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
14
|
+
|
|
15
|
+
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
16
|
+
REMOTE="$1"
|
|
17
|
+
URL="$2"
|
|
18
|
+
|
|
19
|
+
# Get files being pushed in this push operation
|
|
20
|
+
# pre-push receives stdin with ref information
|
|
21
|
+
PUSHED_FILES=""
|
|
22
|
+
TS_FILES=""
|
|
23
|
+
while read local_ref local_sha remote_ref remote_sha; do
|
|
24
|
+
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
25
|
+
# New branch - compare against empty tree
|
|
26
|
+
PUSHED_FILES=$(git diff-tree -r --name-only HEAD)
|
|
27
|
+
TS_FILES=$(git diff-tree -r --name-only HEAD | grep '\.ts$' || true)
|
|
28
|
+
else
|
|
29
|
+
# Existing branch - compare against what we're pushing from
|
|
30
|
+
PUSHED_FILES=$(git diff-tree -r --name-only "$remote_sha" "$local_sha")
|
|
31
|
+
TS_FILES=$(git diff-tree -r --name-only "$remote_sha" "$local_sha" | grep '\.ts$' || true)
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
if [ -z "$PUSHED_FILES" ]; then
|
|
36
|
+
echo "📚 No files changed in push. Skipping walkthrough."
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Determine if pushed files contain ONLY documentation/non-source files
|
|
41
|
+
DOC_ONLY=true
|
|
42
|
+
SOURCE_EXTENSIONS="\.py$|\.js$|\.ts$|\.tsx$|\.java$|\.go$|\.rs$|\.cpp$|\.c$|\.swift$|\.kt$|\.sh$|\.dart$|\.ps1$|\.m$|\.mm$|\.h$|\.hpp$"
|
|
43
|
+
|
|
44
|
+
for file in $PUSHED_FILES; do
|
|
45
|
+
if echo "$file" | grep -qE "$SOURCE_EXTENSIONS"; then
|
|
46
|
+
DOC_ONLY=false
|
|
47
|
+
break
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
# Documentation-only changes (no source code) → skip walkthrough
|
|
52
|
+
if [ "$DOC_ONLY" = "true" ]; then
|
|
53
|
+
echo "📚 Documentation-only push (no source code files)."
|
|
54
|
+
echo " Files: $(echo "$PUSHED_FILES" | tr '\n' ', ' | sed 's/,$//')"
|
|
55
|
+
echo " ✅ No code review required. Proceeding with push..."
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Size limits check — REMOVED: AI workflows generate large cumulative pushes; size is not a quality signal
|
|
60
|
+
DIFF_STATS=$(git diff origin/main...HEAD --stat 2>/dev/null || git diff origin/master...HEAD --stat 2>/dev/null)
|
|
61
|
+
FILES_CHANGED=$(echo "$DIFF_STATS" | tail -1 | grep -oE '[0-9]+ file' | grep -oE '[0-9]+' || echo "0")
|
|
62
|
+
LINES_ADDED=$(echo "$DIFF_STATS" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0")
|
|
63
|
+
LINES_DELETED=$(echo "$DIFF_STATS" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0")
|
|
64
|
+
|
|
65
|
+
echo ""
|
|
66
|
+
echo "Branch: $CURRENT_BRANCH"
|
|
67
|
+
echo "Files changed: $FILES_CHANGED"
|
|
68
|
+
echo "Lines: +$LINES_ADDED -$LINES_DELETED"
|
|
69
|
+
echo ""
|
|
70
|
+
|
|
71
|
+
# ============================================================================
|
|
72
|
+
# GATE M: MUTATION TESTING
|
|
73
|
+
# ============================================================================
|
|
74
|
+
echo ""
|
|
75
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
76
|
+
echo " GATE M: MUTATION TESTING"
|
|
77
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
78
|
+
|
|
79
|
+
# Source adapter-common.sh — 3-tier resolution (global → project → script dir)
|
|
80
|
+
ADAPTER_COMMON=""
|
|
81
|
+
GLOBAL_ADAPTER_DIR="$HOME/.config/xp-gate/adapters"
|
|
82
|
+
PROJECT_GITHOOKS="$(git rev-parse --show-toplevel 2>/dev/null)/githooks"
|
|
83
|
+
|
|
84
|
+
if [[ -f "$GLOBAL_ADAPTER_DIR/adapter-common.sh" ]]; then
|
|
85
|
+
ADAPTER_COMMON="$GLOBAL_ADAPTER_DIR/adapter-common.sh"
|
|
86
|
+
elif [[ -f "$PROJECT_GITHOOKS/adapter-common.sh" ]]; then
|
|
87
|
+
ADAPTER_COMMON="$PROJECT_GITHOOKS/adapter-common.sh"
|
|
88
|
+
elif [[ -f "$(dirname "$0")/adapter-common.sh" ]]; then
|
|
89
|
+
ADAPTER_COMMON="$(dirname "$0")/adapter-common.sh"
|
|
90
|
+
fi
|
|
91
|
+
# shellcheck source=githooks/adapter-common.sh
|
|
92
|
+
source "$ADAPTER_COMMON" 2>/dev/null || {
|
|
93
|
+
echo "⚠️ Could not source adapter-common.sh. SKIP — Gate M."
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Only run for TypeScript projects
|
|
97
|
+
if [[ ! -f "package.json" ]] || [[ ! -f "tsconfig.json" ]]; then
|
|
98
|
+
echo "📚 Not a TypeScript project. SKIP — Gate M."
|
|
99
|
+
else
|
|
100
|
+
# Check if mutation testing is configured
|
|
101
|
+
if ! detect_mutation_testable 2>/dev/null; then
|
|
102
|
+
echo "⚠️ Stryker config not found or @stryker-mutator not installed."
|
|
103
|
+
echo " SKIP — Gate M mutation testing."
|
|
104
|
+
else
|
|
105
|
+
# Filter to source files (exclude tests)
|
|
106
|
+
CHANGED_SOURCE_FILES=$(echo "$TS_FILES" | grep -v '__tests__' | grep -v '\.test\.' | grep -v '\.spec\.' | grep -v '\.d\.ts$' || true)
|
|
107
|
+
|
|
108
|
+
if [ -z "$CHANGED_SOURCE_FILES" ]; then
|
|
109
|
+
echo "📚 No changed TypeScript source files. SKIP — Gate M."
|
|
110
|
+
else
|
|
111
|
+
echo "🧬 Running mutation tests on changed files..."
|
|
112
|
+
echo "$CHANGED_SOURCE_FILES"
|
|
113
|
+
|
|
114
|
+
# Check if mutation gate script exists
|
|
115
|
+
if [[ ! -f "src/mutation/gate-m.ts" ]]; then
|
|
116
|
+
echo "⚠️ Gate M script (src/mutation/gate-m.ts) not found. SKIP — Gate M."
|
|
117
|
+
else
|
|
118
|
+
# Run mutation gate with 120s timeout
|
|
119
|
+
MUTATION_OUTPUT=$(mktemp)
|
|
120
|
+
timeout 120s npx tsx src/mutation/gate-m.ts --changed-files "$CHANGED_SOURCE_FILES" > "$MUTATION_OUTPUT" 2>&1
|
|
121
|
+
MUTATION_EXIT=$?
|
|
122
|
+
|
|
123
|
+
case $MUTATION_EXIT in
|
|
124
|
+
0)
|
|
125
|
+
echo "✅ Gate M: PASS"
|
|
126
|
+
# Update baseline after successful mutation test
|
|
127
|
+
if [ -f ".stryker-baseline.json" ]; then
|
|
128
|
+
echo "📝 Updating mutation baseline..."
|
|
129
|
+
cp ".stryker-baseline.json" ".stryker-baseline.json.prev" 2>/dev/null || true
|
|
130
|
+
fi
|
|
131
|
+
;;
|
|
132
|
+
1)
|
|
133
|
+
echo ""
|
|
134
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
135
|
+
echo " ❌ GATE M FAILED - PUSH BLOCKED"
|
|
136
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
137
|
+
echo ""
|
|
138
|
+
cat "$MUTATION_OUTPUT"
|
|
139
|
+
echo ""
|
|
140
|
+
rm -f "$MUTATION_OUTPUT"
|
|
141
|
+
exit 1
|
|
142
|
+
;;
|
|
143
|
+
124)
|
|
144
|
+
echo "⏱️ Gate M: TIMEOUT (120s). Mutation testing incomplete."
|
|
145
|
+
echo " Allowing push with warning — review mutation coverage manually."
|
|
146
|
+
;;
|
|
147
|
+
*)
|
|
148
|
+
echo "⚠️ Gate M: Unexpected exit code $MUTATION_EXIT"
|
|
149
|
+
cat "$MUTATION_OUTPUT"
|
|
150
|
+
echo " Allowing push with warning."
|
|
151
|
+
;;
|
|
152
|
+
esac
|
|
153
|
+
|
|
154
|
+
rm -f "$MUTATION_OUTPUT"
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# ============================================================================
|
|
161
|
+
# GATE M2: MOCK DENSITY CHECK (BLOCK at 50%, ADVISORY at 30%)
|
|
162
|
+
# ============================================================================
|
|
163
|
+
echo ""
|
|
164
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
165
|
+
echo " GATE M2: MOCK DENSITY CHECK"
|
|
166
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
167
|
+
|
|
168
|
+
# Collect TS and Python test files from pushed files
|
|
169
|
+
ALL_PUSHED_TEST_FILES=""
|
|
170
|
+
TS_TEST_FILES=$(echo "$TS_FILES" | grep -E '\.(test|spec)\.(ts|tsx)$' || true)
|
|
171
|
+
PY_TEST_FILES=$(echo "$PUSHED_FILES" | grep -E '(_test\.py|test_.*\.py)$' || true)
|
|
172
|
+
|
|
173
|
+
if [ -n "$TS_TEST_FILES" ]; then
|
|
174
|
+
ALL_PUSHED_TEST_FILES="$TS_TEST_FILES"
|
|
175
|
+
fi
|
|
176
|
+
if [ -n "$PY_TEST_FILES" ]; then
|
|
177
|
+
ALL_PUSHED_TEST_FILES="$ALL_PUSHED_TEST_FILES $PY_TEST_FILES"
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
if [ -z "$ALL_PUSHED_TEST_FILES" ]; then
|
|
181
|
+
echo "✅ No test files in push. SKIP — Mock density check."
|
|
182
|
+
else
|
|
183
|
+
MOCK_BLOCKED=false
|
|
184
|
+
|
|
185
|
+
for test_file in $ALL_PUSHED_TEST_FILES; do
|
|
186
|
+
if [ -f "$test_file" ]; then
|
|
187
|
+
# Count mock keyword references (precise patterns only)
|
|
188
|
+
MOCK_COUNT=0
|
|
189
|
+
for kw in 'jest\.mock' 'vi\.mock' 'jest\.spyOn' 'vi\.spyOn' 'jest\.fn' 'vi\.fn' \
|
|
190
|
+
'mockResolvedValue' 'mockRejectedValue' 'mockReturnValue' 'mockImplementation' \
|
|
191
|
+
'createMock' 'mockReset' 'mockClear' 'mockRestore' 'MagicMock' 'unittest\.mock' \
|
|
192
|
+
'\.patch(' 'gomock' 'mockgen' '.EXPECT()'; do
|
|
193
|
+
c=$(grep -o -c "$kw" "$test_file" 2>/dev/null || echo "0")
|
|
194
|
+
MOCK_COUNT=$((MOCK_COUNT + c))
|
|
195
|
+
done
|
|
196
|
+
|
|
197
|
+
# Count total non-empty, non-comment lines for density denominator
|
|
198
|
+
TOTAL_LINES=$(grep -v '^\s*$' "$test_file" | grep -v '^\s*//' | grep -v '^\s*\*' | grep -v '^\s*#' | wc -l | awk '{print $1}')
|
|
199
|
+
|
|
200
|
+
if [ "$TOTAL_LINES" -gt 0 ] 2>/dev/null; then
|
|
201
|
+
MOCK_DENSITY=$(awk "BEGIN {printf \"%.1f\", ($MOCK_COUNT / $TOTAL_LINES) * 100}")
|
|
202
|
+
else
|
|
203
|
+
MOCK_DENSITY="0"
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
THRESHOLD_50=$(awk "BEGIN {print ($MOCK_DENSITY > 50) ? 1 : 0}")
|
|
207
|
+
THRESHOLD_30=$(awk "BEGIN {print ($MOCK_DENSITY > 30) ? 1 : 0}")
|
|
208
|
+
|
|
209
|
+
# Check for @mock-justified annotation with reason text (min 10 chars)
|
|
210
|
+
HAS_JUSTIFIED=$(grep -qE '@mock-justified\s*:\s*.{10,}' "$test_file" 2>/dev/null && echo "true" || echo "false")
|
|
211
|
+
|
|
212
|
+
if [ "$THRESHOLD_50" = "1" ]; then
|
|
213
|
+
if [ "$HAS_JUSTIFIED" = "false" ]; then
|
|
214
|
+
echo "❌ BLOCKED: $test_file — Mock density ${MOCK_DENSITY}% exceeds 50% threshold"
|
|
215
|
+
echo " Must: Reduce mocks OR add '// @mock-justified: <reason>' (min 10 char explanation)"
|
|
216
|
+
MOCK_BLOCKED=true
|
|
217
|
+
else
|
|
218
|
+
echo "⚠️ WARNING: $test_file — Mock density ${MOCK_DENSITY}% (justified by annotation)"
|
|
219
|
+
fi
|
|
220
|
+
elif [ "$THRESHOLD_30" = "1" ]; then
|
|
221
|
+
echo "ℹ️ ADVISORY: $test_file — Mock density ${MOCK_DENSITY}% (consider integration tests)"
|
|
222
|
+
else
|
|
223
|
+
echo "✅ $test_file — Mock density ${MOCK_DENSITY}% (acceptable)"
|
|
224
|
+
fi
|
|
225
|
+
fi
|
|
226
|
+
done
|
|
227
|
+
|
|
228
|
+
if [ "$MOCK_BLOCKED" = true ]; then
|
|
229
|
+
echo ""
|
|
230
|
+
echo "❌ PUSH BLOCKED — Mock density too high without justification"
|
|
231
|
+
exit 1
|
|
232
|
+
fi
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# ============================================================================
|
|
236
|
+
# VALIDATE CODE WALKTHROUGH RESULT FILE
|
|
237
|
+
# ============================================================================
|
|
238
|
+
RESULT_FILE=".code-walkthrough-result.json"
|
|
239
|
+
|
|
240
|
+
# Skip Delphi walkthrough for main/master (Gate M already ran above)
|
|
241
|
+
if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
|
|
242
|
+
echo "⚠️ Pushing to main/master - Delphi walkthrough skipped"
|
|
243
|
+
else
|
|
244
|
+
|
|
245
|
+
if [ ! -f "$RESULT_FILE" ]; then
|
|
246
|
+
echo ""
|
|
247
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
248
|
+
echo " ❌ CODE WALKTHROUGH REQUIRED - PUSH BLOCKED"
|
|
249
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
250
|
+
echo ""
|
|
251
|
+
echo "No code walkthrough result found."
|
|
252
|
+
echo ""
|
|
253
|
+
echo "Before pushing, run code walkthrough in your Agent session:"
|
|
254
|
+
echo ""
|
|
255
|
+
echo " /delphi-review --mode code-walkthrough"
|
|
256
|
+
echo ""
|
|
257
|
+
echo "After APPROVED verdict, retry this push."
|
|
258
|
+
echo ""
|
|
259
|
+
exit 1
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
# Check jq availability (MANDATORY - zero degradation)
|
|
263
|
+
if ! command -v jq &> /dev/null; then
|
|
264
|
+
echo ""
|
|
265
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
266
|
+
echo " ❌ ENVIRONMENT ERROR - PUSH BLOCKED"
|
|
267
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
268
|
+
echo ""
|
|
269
|
+
echo "jq is NOT installed. Required for JSON validation."
|
|
270
|
+
echo ""
|
|
271
|
+
echo "Install jq:"
|
|
272
|
+
echo " apt-get install jq (Debian/Ubuntu)"
|
|
273
|
+
echo " brew install jq (macOS)"
|
|
274
|
+
echo " choco install jq (Windows)"
|
|
275
|
+
echo ""
|
|
276
|
+
echo "Zero degradation principle: Cannot proceed without jq."
|
|
277
|
+
echo ""
|
|
278
|
+
exit 1
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# Validate result file content
|
|
282
|
+
CURRENT_COMMIT=$(git rev-parse HEAD)
|
|
283
|
+
|
|
284
|
+
# Check JSON validity
|
|
285
|
+
if ! jq empty "$RESULT_FILE" 2>/dev/null; then
|
|
286
|
+
echo ""
|
|
287
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
288
|
+
echo " ❌ RESULT FILE INVALID - PUSH BLOCKED"
|
|
289
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
290
|
+
echo ""
|
|
291
|
+
echo "$RESULT_FILE is not valid JSON."
|
|
292
|
+
echo ""
|
|
293
|
+
echo "Re-run: /delphi-review --mode code-walkthrough"
|
|
294
|
+
echo ""
|
|
295
|
+
exit 1
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
RESULT_COMMIT=$(jq -r '.commit' "$RESULT_FILE")
|
|
299
|
+
RESULT_VERDICT=$(jq -r '.verdict' "$RESULT_FILE")
|
|
300
|
+
RESULT_EXPIRES=$(jq -r '.expires' "$RESULT_FILE")
|
|
301
|
+
RESULT_BRANCH=$(jq -r '.branch' "$RESULT_FILE")
|
|
302
|
+
CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
303
|
+
|
|
304
|
+
# Branch match check (optional but recommended)
|
|
305
|
+
if [ "$RESULT_BRANCH" != "$CURRENT_BRANCH" ]; then
|
|
306
|
+
echo ""
|
|
307
|
+
echo "⚠️ WARNING: Result file is for different branch"
|
|
308
|
+
echo " Expected: $CURRENT_BRANCH"
|
|
309
|
+
echo " Found: $RESULT_BRANCH"
|
|
310
|
+
echo ""
|
|
311
|
+
echo "Proceeding with commit verification..."
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# Commit match check
|
|
315
|
+
if [ "$RESULT_COMMIT" != "$CURRENT_COMMIT" ]; then
|
|
316
|
+
echo ""
|
|
317
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
318
|
+
echo " ❌ RESULT FILE OUTDATED - PUSH BLOCKED"
|
|
319
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
320
|
+
echo ""
|
|
321
|
+
echo "Result file is for a different commit:"
|
|
322
|
+
echo " Expected: $CURRENT_COMMIT"
|
|
323
|
+
echo " Found: $RESULT_COMMIT"
|
|
324
|
+
echo ""
|
|
325
|
+
echo "Re-run: /delphi-review --mode code-walkthrough"
|
|
326
|
+
echo ""
|
|
327
|
+
exit 1
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
# Verdict check
|
|
331
|
+
if [ "$RESULT_VERDICT" != "APPROVED" ]; then
|
|
332
|
+
echo ""
|
|
333
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
334
|
+
echo " ❌ CODE WALKTHROUGH NOT APPROVED - PUSH BLOCKED"
|
|
335
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
336
|
+
echo ""
|
|
337
|
+
echo "Verdict: $RESULT_VERDICT"
|
|
338
|
+
echo ""
|
|
339
|
+
|
|
340
|
+
# Show issues if available
|
|
341
|
+
ISSUES=$(jq -r '.issues[] | "- [\(.severity)] \(.description)"' "$RESULT_FILE" 2>/dev/null)
|
|
342
|
+
if [ -n "$ISSUES" ]; then
|
|
343
|
+
echo "Issues found:"
|
|
344
|
+
echo "$ISSUES"
|
|
345
|
+
echo ""
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
echo "Fix issues and re-run: /delphi-review --mode code-walkthrough"
|
|
349
|
+
echo ""
|
|
350
|
+
exit 1
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
# Expiration check
|
|
354
|
+
if [[ "$CURRENT_TIME" > "$RESULT_EXPIRES" ]]; then
|
|
355
|
+
echo ""
|
|
356
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
357
|
+
echo " ❌ RESULT FILE EXPIRED - PUSH BLOCKED"
|
|
358
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
359
|
+
echo ""
|
|
360
|
+
echo "Code walkthrough result has expired:"
|
|
361
|
+
echo " Expired: $RESULT_EXPIRES"
|
|
362
|
+
echo " Current: $CURRENT_TIME"
|
|
363
|
+
echo ""
|
|
364
|
+
echo "Re-run: /delphi-review --mode code-walkthrough"
|
|
365
|
+
echo ""
|
|
366
|
+
exit 1
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
# ============================================================================
|
|
370
|
+
# ALL CHECKS PASSED
|
|
371
|
+
# ============================================================================
|
|
372
|
+
echo ""
|
|
373
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
374
|
+
echo " ✅ CODE WALKTHROUGH VERIFIED"
|
|
375
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
376
|
+
echo ""
|
|
377
|
+
echo "Branch: $CURRENT_BRANCH"
|
|
378
|
+
echo "Files changed: $FILES_CHANGED"
|
|
379
|
+
echo "Lines: +$LINES_ADDED -$LINES_DELETED"
|
|
380
|
+
echo ""
|
|
381
|
+
echo "Code walkthrough result:"
|
|
382
|
+
echo " Commit: $RESULT_COMMIT"
|
|
383
|
+
echo " Verdict: APPROVED"
|
|
384
|
+
echo " Expires: $RESULT_EXPIRES"
|
|
385
|
+
|
|
386
|
+
# Show confidence if available
|
|
387
|
+
CONFIDENCE=$(jq -r '.confidence' "$RESULT_FILE" 2>/dev/null)
|
|
388
|
+
if [ -n "$CONFIDENCE" ] && [ "$CONFIDENCE" != "null" ]; then
|
|
389
|
+
echo " Confidence: $CONFIDENCE/10"
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
echo ""
|
|
393
|
+
echo "Proceeding with push..."
|
|
394
|
+
fi
|
|
395
|
+
exit 0
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test detect-deps
|
|
3
|
+
* @intent Verify checkDeps() correctly detects missing/present/outdated dependencies
|
|
4
|
+
*/
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
describe('detect-deps', () => {
|
|
10
|
+
let tmpHome;
|
|
11
|
+
let originalHome;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
originalHome = process.env.HOME;
|
|
15
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-detect-'));
|
|
16
|
+
process.env.HOME = tmpHome;
|
|
17
|
+
vi.resetModules();
|
|
18
|
+
delete require.cache[require.resolve('../detect-deps')];
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
process.env.HOME = originalHome;
|
|
23
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function makeSkillDir(skillName, contents = {}) {
|
|
28
|
+
const dir = path.join(tmpHome, '.config', 'opencode', 'skills', skillName);
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
if (contents.packageJson) {
|
|
31
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(contents.packageJson));
|
|
32
|
+
}
|
|
33
|
+
if (contents.skillMd) {
|
|
34
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), contents.skillMd);
|
|
35
|
+
}
|
|
36
|
+
return dir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeOpencodeDir(skillName, contents = {}) {
|
|
40
|
+
const dir = path.join(tmpHome, '.config', 'opencode', skillName);
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
+
if (contents.packageJson) {
|
|
43
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(contents.packageJson));
|
|
44
|
+
}
|
|
45
|
+
if (contents.skillMd) {
|
|
46
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), contents.skillMd);
|
|
47
|
+
}
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it('returns ok:false missing:superpowers when no deps exist', async () => {
|
|
52
|
+
const { checkDeps } = require('../detect-deps');
|
|
53
|
+
const result = await checkDeps();
|
|
54
|
+
expect(result.ok).toBe(false);
|
|
55
|
+
expect(result.missing).toBe('superpowers');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns ok:false missing:gstack when superpowers exists but gstack missing', async () => {
|
|
59
|
+
makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } });
|
|
60
|
+
const { checkDeps } = require('../detect-deps');
|
|
61
|
+
const result = await checkDeps();
|
|
62
|
+
expect(result.ok).toBe(false);
|
|
63
|
+
expect(result.missing).toBe('gstack');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns ok:true when both deps exist via package.json with adequate version', async () => {
|
|
67
|
+
makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } });
|
|
68
|
+
makeSkillDir('gstack', { packageJson: { version: '1.5.0' } });
|
|
69
|
+
const { checkDeps } = require('../detect-deps');
|
|
70
|
+
const result = await checkDeps();
|
|
71
|
+
expect(result.ok).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns ok:true when deps exist in fallback OPENCODE_DIR path', async () => {
|
|
75
|
+
makeOpencodeDir('superpowers', { packageJson: { version: '1.0.0' } });
|
|
76
|
+
makeOpencodeDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
77
|
+
const { checkDeps } = require('../detect-deps');
|
|
78
|
+
const result = await checkDeps();
|
|
79
|
+
expect(result.ok).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns ok:true when version read from SKILL.md frontmatter', async () => {
|
|
83
|
+
makeSkillDir('superpowers', { skillMd: 'version: "2.1.0"\n---\nSkill content' });
|
|
84
|
+
makeSkillDir('gstack', { skillMd: 'version: "1.2.3"\n---\nSkill content' });
|
|
85
|
+
const { checkDeps } = require('../detect-deps');
|
|
86
|
+
const result = await checkDeps();
|
|
87
|
+
expect(result.ok).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('reads version from SKILL.md without quotes', async () => {
|
|
91
|
+
makeSkillDir('superpowers', { skillMd: 'version: 2.1.0\n' });
|
|
92
|
+
makeSkillDir('gstack', { skillMd: 'version: 1.0.0\n' });
|
|
93
|
+
const { checkDeps } = require('../detect-deps');
|
|
94
|
+
const result = await checkDeps();
|
|
95
|
+
expect(result.ok).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns versionMismatch when superpowers version < minVersion', async () => {
|
|
99
|
+
makeSkillDir('superpowers', { packageJson: { version: '0.0.1' } });
|
|
100
|
+
makeSkillDir('gstack', { packageJson: { version: '2.0.0' } });
|
|
101
|
+
const { checkDeps } = require('../detect-deps');
|
|
102
|
+
const result = await checkDeps();
|
|
103
|
+
expect(result.ok).toBe(false);
|
|
104
|
+
expect(result.versionMismatch).toEqual({
|
|
105
|
+
name: 'superpowers',
|
|
106
|
+
required: '1.0.0',
|
|
107
|
+
found: '0.0.1',
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns versionMismatch when gstack version < minVersion', async () => {
|
|
112
|
+
makeSkillDir('superpowers', { packageJson: { version: '2.0.0' } });
|
|
113
|
+
makeSkillDir('gstack', { packageJson: { version: '0.5.0' } });
|
|
114
|
+
const { checkDeps } = require('../detect-deps');
|
|
115
|
+
const result = await checkDeps();
|
|
116
|
+
expect(result.ok).toBe(false);
|
|
117
|
+
expect(result.versionMismatch.name).toBe('gstack');
|
|
118
|
+
expect(result.versionMismatch.found).toBe('0.5.0');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('passes when version is exactly equal to minVersion', async () => {
|
|
122
|
+
makeSkillDir('superpowers', { packageJson: { version: '1.0.0' } });
|
|
123
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
124
|
+
const { checkDeps } = require('../detect-deps');
|
|
125
|
+
const result = await checkDeps();
|
|
126
|
+
expect(result.ok).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('returns ok:true (no version check) when package.json has no version and no SKILL.md', async () => {
|
|
130
|
+
// getSkillVersion returns null → skips version check → ok
|
|
131
|
+
makeSkillDir('superpowers', { packageJson: {} });
|
|
132
|
+
makeSkillDir('gstack', { packageJson: {} });
|
|
133
|
+
const { checkDeps } = require('../detect-deps');
|
|
134
|
+
const result = await checkDeps();
|
|
135
|
+
expect(result.ok).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('handles malformed package.json gracefully (falls through to SKILL.md or null)', async () => {
|
|
139
|
+
const supDir = path.join(tmpHome, '.config', 'opencode', 'skills', 'superpowers');
|
|
140
|
+
fs.mkdirSync(supDir, { recursive: true });
|
|
141
|
+
fs.writeFileSync(path.join(supDir, 'package.json'), '{invalid json');
|
|
142
|
+
fs.writeFileSync(path.join(supDir, 'SKILL.md'), 'version: "2.0.0"\n');
|
|
143
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
144
|
+
const { checkDeps } = require('../detect-deps');
|
|
145
|
+
const result = await checkDeps();
|
|
146
|
+
expect(result.ok).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns null version when neither package.json nor SKILL.md exist (skips version check)', async () => {
|
|
150
|
+
// Create empty dirs (no metadata files)
|
|
151
|
+
fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', 'superpowers'), {
|
|
152
|
+
recursive: true,
|
|
153
|
+
});
|
|
154
|
+
fs.mkdirSync(path.join(tmpHome, '.config', 'opencode', 'skills', 'gstack'), {
|
|
155
|
+
recursive: true,
|
|
156
|
+
});
|
|
157
|
+
const { checkDeps } = require('../detect-deps');
|
|
158
|
+
const result = await checkDeps();
|
|
159
|
+
expect(result.ok).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('returns null when SKILL.md exists but has no version line', async () => {
|
|
163
|
+
makeSkillDir('superpowers', { skillMd: 'no version here\n' });
|
|
164
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
165
|
+
const { checkDeps } = require('../detect-deps');
|
|
166
|
+
const result = await checkDeps();
|
|
167
|
+
// superpowers version=null → skips version check → ok
|
|
168
|
+
expect(result.ok).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('compareVersions handles partial versions (e.g. 1.0 treated as 1.0.0)', async () => {
|
|
172
|
+
// version "1.0" in SKILL.md regex requires X.Y.Z so won't match; use package.json
|
|
173
|
+
makeSkillDir('superpowers', { packageJson: { version: '1' } });
|
|
174
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
175
|
+
const { checkDeps } = require('../detect-deps');
|
|
176
|
+
const result = await checkDeps();
|
|
177
|
+
// '1' parsed as [1] vs [1,0,0] → equal at index 0; index 1: 0 vs 0; index 2: 0 vs 0 → equal → passes
|
|
178
|
+
expect(result.ok).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('compareVersions: greater version passes', async () => {
|
|
182
|
+
makeSkillDir('superpowers', { packageJson: { version: '10.0.0' } });
|
|
183
|
+
makeSkillDir('gstack', { packageJson: { version: '5.5.5' } });
|
|
184
|
+
const { checkDeps } = require('../detect-deps');
|
|
185
|
+
const result = await checkDeps();
|
|
186
|
+
expect(result.ok).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('compareVersions: minor version less fails', async () => {
|
|
190
|
+
makeSkillDir('superpowers', { packageJson: { version: '0.9.99' } });
|
|
191
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
192
|
+
const { checkDeps } = require('../detect-deps');
|
|
193
|
+
const result = await checkDeps();
|
|
194
|
+
expect(result.ok).toBe(false);
|
|
195
|
+
expect(result.versionMismatch.found).toBe('0.9.99');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('prefers SKILLS_DIR over OPENCODE_DIR when both exist', async () => {
|
|
199
|
+
// Put low version in SKILLS_DIR, high in OPENCODE_DIR
|
|
200
|
+
makeSkillDir('superpowers', { packageJson: { version: '0.0.1' } });
|
|
201
|
+
makeOpencodeDir('superpowers', { packageJson: { version: '2.0.0' } });
|
|
202
|
+
makeSkillDir('gstack', { packageJson: { version: '1.0.0' } });
|
|
203
|
+
const { checkDeps } = require('../detect-deps');
|
|
204
|
+
const result = await checkDeps();
|
|
205
|
+
// Should use SKILLS_DIR (first in possiblePaths) → 0.0.1 fails
|
|
206
|
+
expect(result.ok).toBe(false);
|
|
207
|
+
expect(result.versionMismatch.found).toBe('0.0.1');
|
|
208
|
+
});
|
|
209
|
+
});
|