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.
Files changed (90) hide show
  1. package/adapter-common.sh +192 -0
  2. package/adapters/cpp.sh +76 -0
  3. package/adapters/dart.sh +41 -0
  4. package/adapters/flutter.sh +41 -0
  5. package/adapters/go.sh +59 -0
  6. package/adapters/iac.sh +189 -0
  7. package/adapters/java.sh +191 -0
  8. package/adapters/kotlin.sh +77 -0
  9. package/adapters/objectivec.sh +38 -0
  10. package/adapters/powershell.sh +138 -0
  11. package/adapters/python.sh +104 -0
  12. package/adapters/shell.sh +55 -0
  13. package/adapters/swift.sh +44 -0
  14. package/adapters/typescript.sh +61 -0
  15. package/bin/xp-gate.js +157 -0
  16. package/hooks/adapter-common.sh +192 -0
  17. package/hooks/pre-commit +1667 -0
  18. package/hooks/pre-push +395 -0
  19. package/lib/__tests__/detect-deps.test.js +209 -0
  20. package/lib/__tests__/doctor.test.js +448 -0
  21. package/lib/__tests__/download-skill.test.js +281 -0
  22. package/lib/__tests__/init.test.js +327 -0
  23. package/lib/__tests__/install-skill.test.js +326 -0
  24. package/lib/__tests__/migrate.test.js +212 -0
  25. package/lib/__tests__/rollback.test.js +183 -0
  26. package/lib/__tests__/ui-detector.test.ts +200 -0
  27. package/lib/__tests__/uninstall-skill.test.js +189 -0
  28. package/lib/__tests__/uninstall.test.js +589 -0
  29. package/lib/__tests__/update-skill.test.js +276 -0
  30. package/lib/detect-deps.js +157 -0
  31. package/lib/doctor.js +370 -0
  32. package/lib/download-skill.js +96 -0
  33. package/lib/init.js +367 -0
  34. package/lib/install-skill.js +184 -0
  35. package/lib/migrate.js +120 -0
  36. package/lib/rollback.js +78 -0
  37. package/lib/ui-detector.ts +99 -0
  38. package/lib/uninstall-skill.js +69 -0
  39. package/lib/uninstall.js +401 -0
  40. package/lib/update-skill.js +90 -0
  41. package/package.json +39 -0
  42. package/plugins/claude-code/.claude-plugin/plugin.json +21 -0
  43. package/plugins/claude-code/bin/delphi-review-guard.sh +68 -0
  44. package/plugins/claude-code/bin/xp-gate-check +47 -0
  45. package/plugins/claude-code/hooks/hooks.json +37 -0
  46. package/skills/delphi-review/.delphi-config.json.example +45 -0
  47. package/skills/delphi-review/AGENTS.md +54 -0
  48. package/skills/delphi-review/INSTALL.md +152 -0
  49. package/skills/delphi-review/SKILL.md +371 -0
  50. package/skills/delphi-review/evals/evals.json +82 -0
  51. package/skills/delphi-review/opencode.json.delphi.example +56 -0
  52. package/skills/delphi-review/references/code-walkthrough.md +486 -0
  53. package/skills/ralph-loop/SKILL.md +330 -0
  54. package/skills/ralph-loop/evals/evals.json +311 -0
  55. package/skills/ralph-loop/evolution-history.json +59 -0
  56. package/skills/ralph-loop/evolution-log.md +16 -0
  57. package/skills/ralph-loop/references/components/memory.md +55 -0
  58. package/skills/ralph-loop/references/components/middleware.md +54 -0
  59. package/skills/ralph-loop/references/components/skill-invocations.md +39 -0
  60. package/skills/ralph-loop/references/components/system-prompt.md +24 -0
  61. package/skills/ralph-loop/references/components/tool-descriptions.md +32 -0
  62. package/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
  63. package/skills/ralph-loop/templates/progress-log.md +36 -0
  64. package/skills/sprint-flow/SKILL.md +600 -0
  65. package/skills/sprint-flow/evals/evals.json +78 -0
  66. package/skills/sprint-flow/evolution-history.json +39 -0
  67. package/skills/sprint-flow/evolution-log.md +23 -0
  68. package/skills/sprint-flow/references/components/memory.md +87 -0
  69. package/skills/sprint-flow/references/components/middleware.md +72 -0
  70. package/skills/sprint-flow/references/components/skill-invocations.md +104 -0
  71. package/skills/sprint-flow/references/components/system-prompt.md +27 -0
  72. package/skills/sprint-flow/references/components/tool-descriptions.md +96 -0
  73. package/skills/sprint-flow/references/phase-0-think.md +115 -0
  74. package/skills/sprint-flow/references/phase-1-plan.md +178 -0
  75. package/skills/sprint-flow/references/phase-2-build.md +198 -0
  76. package/skills/sprint-flow/references/phase-3-review.md +213 -0
  77. package/skills/sprint-flow/references/phase-4-uat.md +125 -0
  78. package/skills/sprint-flow/references/phase-5-feedback.md +100 -0
  79. package/skills/sprint-flow/references/phase-6-ship.md +193 -0
  80. package/skills/sprint-flow/references/phase-7-land.md +140 -0
  81. package/skills/sprint-flow/references/phase-8-cleanup.md +192 -0
  82. package/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
  83. package/skills/sprint-flow/templates/pain-document-template.md +115 -0
  84. package/skills/sprint-flow/templates/sprint-summary-template.md +120 -0
  85. package/skills/test-specification-alignment/AGENTS.md +59 -0
  86. package/skills/test-specification-alignment/SKILL.md +605 -0
  87. package/skills/test-specification-alignment/evals/evals.json +75 -0
  88. package/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
  89. package/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
  90. 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
+ });