forgecraft-mcp 1.2.0 → 1.3.2
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 +525 -525
- package/dist/cli/help.js +44 -44
- package/dist/registry/renderer-skeletons.js +92 -92
- package/dist/shared/gs-score-logger.js +6 -6
- package/dist/tools/add-module.js +123 -123
- package/dist/tools/advice-registry.js +18 -18
- package/dist/tools/check-cascade-report.js +64 -64
- package/dist/tools/configure-mcp.d.ts +3 -0
- package/dist/tools/configure-mcp.d.ts.map +1 -1
- package/dist/tools/configure-mcp.js +10 -0
- package/dist/tools/configure-mcp.js.map +1 -1
- package/dist/tools/forgecraft-dispatch.d.ts.map +1 -1
- package/dist/tools/forgecraft-dispatch.js +3 -0
- package/dist/tools/forgecraft-dispatch.js.map +1 -1
- package/dist/tools/forgecraft-schema-params.d.ts +9 -0
- package/dist/tools/forgecraft-schema-params.d.ts.map +1 -1
- package/dist/tools/forgecraft-schema-params.js +21 -0
- package/dist/tools/forgecraft-schema-params.js.map +1 -1
- package/dist/tools/forgecraft-schema.d.ts +9 -0
- package/dist/tools/forgecraft-schema.d.ts.map +1 -1
- package/dist/tools/refresh-output.js +14 -14
- package/dist/tools/scaffold-spec-stubs.js +115 -115
- package/dist/tools/scaffold-templates.js +62 -62
- package/dist/tools/setup-artifact-writers.d.ts +30 -0
- package/dist/tools/setup-artifact-writers.d.ts.map +1 -1
- package/dist/tools/setup-artifact-writers.js +120 -8
- package/dist/tools/setup-artifact-writers.js.map +1 -1
- package/dist/tools/setup-phase1.d.ts +3 -0
- package/dist/tools/setup-phase1.d.ts.map +1 -1
- package/dist/tools/setup-phase1.js +79 -35
- package/dist/tools/setup-phase1.js.map +1 -1
- package/dist/tools/setup-phase2.d.ts +2 -0
- package/dist/tools/setup-phase2.d.ts.map +1 -1
- package/dist/tools/setup-phase2.js +10 -1
- package/dist/tools/setup-phase2.js.map +1 -1
- package/dist/tools/setup-project.d.ts +18 -0
- package/dist/tools/setup-project.d.ts.map +1 -1
- package/dist/tools/setup-project.js +77 -1
- package/dist/tools/setup-project.js.map +1 -1
- package/dist/tools/spec-parser-tags.d.ts +9 -0
- package/dist/tools/spec-parser-tags.d.ts.map +1 -1
- package/dist/tools/spec-parser-tags.js +92 -0
- package/dist/tools/spec-parser-tags.js.map +1 -1
- package/package.json +89 -86
- package/templates/analytics/instructions.yaml +37 -37
- package/templates/analytics/mcp-servers.yaml +11 -11
- package/templates/analytics/structure.yaml +25 -25
- package/templates/api/instructions.yaml +231 -231
- package/templates/api/mcp-servers.yaml +22 -13
- package/templates/api/nfr.yaml +23 -23
- package/templates/api/review.yaml +103 -103
- package/templates/api/structure.yaml +34 -34
- package/templates/api/verification.yaml +132 -132
- package/templates/cli/instructions.yaml +31 -31
- package/templates/cli/mcp-servers.yaml +11 -11
- package/templates/cli/review.yaml +53 -53
- package/templates/cli/structure.yaml +16 -16
- package/templates/data-lineage/instructions.yaml +28 -28
- package/templates/data-lineage/mcp-servers.yaml +22 -22
- package/templates/data-pipeline/instructions.yaml +84 -84
- package/templates/data-pipeline/mcp-servers.yaml +13 -13
- package/templates/data-pipeline/nfr.yaml +39 -39
- package/templates/data-pipeline/structure.yaml +23 -23
- package/templates/fintech/hooks.yaml +55 -55
- package/templates/fintech/instructions.yaml +112 -112
- package/templates/fintech/mcp-servers.yaml +13 -13
- package/templates/fintech/nfr.yaml +46 -46
- package/templates/fintech/playbook.yaml +210 -210
- package/templates/fintech/verification.yaml +239 -239
- package/templates/game/instructions.yaml +289 -289
- package/templates/game/mcp-servers.yaml +38 -38
- package/templates/game/nfr.yaml +64 -64
- package/templates/game/playbook.yaml +214 -214
- package/templates/game/review.yaml +97 -97
- package/templates/game/structure.yaml +67 -67
- package/templates/game/verification.yaml +174 -174
- package/templates/healthcare/instructions.yaml +42 -42
- package/templates/healthcare/mcp-servers.yaml +13 -13
- package/templates/healthcare/nfr.yaml +47 -47
- package/templates/hipaa/instructions.yaml +41 -41
- package/templates/hipaa/mcp-servers.yaml +13 -13
- package/templates/infra/instructions.yaml +104 -104
- package/templates/infra/mcp-servers.yaml +20 -20
- package/templates/infra/nfr.yaml +46 -46
- package/templates/infra/review.yaml +65 -65
- package/templates/infra/structure.yaml +25 -25
- package/templates/library/instructions.yaml +36 -36
- package/templates/library/mcp-servers.yaml +20 -20
- package/templates/library/review.yaml +56 -56
- package/templates/library/structure.yaml +19 -19
- package/templates/medallion-architecture/instructions.yaml +41 -41
- package/templates/medallion-architecture/mcp-servers.yaml +22 -22
- package/templates/ml/instructions.yaml +85 -85
- package/templates/ml/mcp-servers.yaml +11 -11
- package/templates/ml/nfr.yaml +39 -39
- package/templates/ml/structure.yaml +25 -25
- package/templates/ml/verification.yaml +156 -156
- package/templates/mobile/instructions.yaml +44 -44
- package/templates/mobile/mcp-servers.yaml +11 -11
- package/templates/mobile/nfr.yaml +49 -49
- package/templates/mobile/structure.yaml +27 -27
- package/templates/mobile/verification.yaml +121 -121
- package/templates/observability-xray/instructions.yaml +40 -40
- package/templates/observability-xray/mcp-servers.yaml +15 -15
- package/templates/realtime/instructions.yaml +42 -42
- package/templates/realtime/mcp-servers.yaml +13 -13
- package/templates/soc2/instructions.yaml +41 -41
- package/templates/soc2/mcp-servers.yaml +24 -24
- package/templates/social/instructions.yaml +43 -43
- package/templates/social/mcp-servers.yaml +24 -24
- package/templates/state-machine/instructions.yaml +42 -42
- package/templates/state-machine/mcp-servers.yaml +11 -11
- package/templates/tools-registry.yaml +164 -164
- package/templates/universal/hooks.yaml +531 -531
- package/templates/universal/instructions.yaml +1692 -1692
- package/templates/universal/mcp-servers.yaml +50 -50
- package/templates/universal/nfr.yaml +197 -197
- package/templates/universal/reference.yaml +326 -326
- package/templates/universal/review.yaml +204 -204
- package/templates/universal/skills.yaml +262 -262
- package/templates/universal/structure.yaml +67 -67
- package/templates/universal/verification.yaml +416 -416
- package/templates/web-react/hooks.yaml +44 -44
- package/templates/web-react/instructions.yaml +207 -207
- package/templates/web-react/mcp-servers.yaml +20 -20
- package/templates/web-react/nfr.yaml +27 -27
- package/templates/web-react/review.yaml +94 -94
- package/templates/web-react/structure.yaml +46 -46
- package/templates/web-react/verification.yaml +126 -126
- package/templates/web-static/instructions.yaml +115 -115
- package/templates/web-static/mcp-servers.yaml +20 -20
- package/templates/web3/instructions.yaml +44 -44
- package/templates/web3/mcp-servers.yaml +11 -11
- package/templates/web3/verification.yaml +159 -159
- package/templates/zero-trust/instructions.yaml +41 -41
- package/templates/zero-trust/mcp-servers.yaml +15 -15
|
@@ -1,531 +1,531 @@
|
|
|
1
|
-
tag: UNIVERSAL
|
|
2
|
-
section: hooks
|
|
3
|
-
hooks:
|
|
4
|
-
- name: commit-msg
|
|
5
|
-
trigger: commit-msg
|
|
6
|
-
description: "Enforce conventional commit format: <type>(<scope>): <description>"
|
|
7
|
-
filename: commit-msg.sh
|
|
8
|
-
script: |
|
|
9
|
-
#!/usr/bin/env bash
|
|
10
|
-
# commit-msg: enforce conventional commit format
|
|
11
|
-
# ForgeCraft — generated hook
|
|
12
|
-
|
|
13
|
-
COMMIT_MSG_FILE="$1"
|
|
14
|
-
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
|
15
|
-
|
|
16
|
-
# Skip merge commits and fixup commits
|
|
17
|
-
if echo "$COMMIT_MSG" | grep -qE "^(Merge|Revert|fixup!|squash!)"; then
|
|
18
|
-
exit 0
|
|
19
|
-
fi
|
|
20
|
-
|
|
21
|
-
# Skip empty messages and comments-only
|
|
22
|
-
STRIPPED=$(echo "$COMMIT_MSG" | sed '/^#/d' | sed '/^$/d')
|
|
23
|
-
if [ -z "$STRIPPED" ]; then
|
|
24
|
-
exit 0
|
|
25
|
-
fi
|
|
26
|
-
|
|
27
|
-
PATTERN="^(feat|fix|refactor|docs|test|chore|perf|ci|build|revert)(\([a-z0-9/_-]+\))?(!)?: .{1,72}"
|
|
28
|
-
|
|
29
|
-
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
|
|
30
|
-
echo ""
|
|
31
|
-
echo " ✗ Commit message does not follow conventional commit format."
|
|
32
|
-
echo ""
|
|
33
|
-
echo " Required format: <type>(<scope>): <description>"
|
|
34
|
-
echo " Types: feat | fix | refactor | docs | test | chore | perf | ci | build | revert"
|
|
35
|
-
echo " Examples:"
|
|
36
|
-
echo " feat(auth): add JWT refresh token support"
|
|
37
|
-
echo " fix(api): handle null response from payment gateway"
|
|
38
|
-
echo " docs: update README with setup instructions"
|
|
39
|
-
echo ""
|
|
40
|
-
echo " Your message: $COMMIT_MSG"
|
|
41
|
-
echo ""
|
|
42
|
-
exit 1
|
|
43
|
-
fi
|
|
44
|
-
|
|
45
|
-
exit 0
|
|
46
|
-
|
|
47
|
-
- name: branch-protection
|
|
48
|
-
trigger: pre-commit
|
|
49
|
-
description: "Block direct commits to main/master branches"
|
|
50
|
-
filename: pre-commit-branch-check.sh
|
|
51
|
-
script: |
|
|
52
|
-
#!/bin/bash
|
|
53
|
-
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
54
|
-
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
55
|
-
echo "❌ Direct commits to $BRANCH are blocked. Create a feature branch."
|
|
56
|
-
exit 1
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
- name: dangerous-commands
|
|
60
|
-
trigger: pre-exec
|
|
61
|
-
description: "Block destructive commands (rm -rf /, DROP DATABASE, force push)"
|
|
62
|
-
filename: pre-exec-safety.sh
|
|
63
|
-
script: |
|
|
64
|
-
#!/bin/bash
|
|
65
|
-
DANGEROUS_PATTERNS=(
|
|
66
|
-
"rm -rf /"
|
|
67
|
-
"DROP DATABASE"
|
|
68
|
-
"DROP TABLE"
|
|
69
|
-
"TRUNCATE"
|
|
70
|
-
"force push"
|
|
71
|
-
"git push.*--force"
|
|
72
|
-
"kubectl delete namespace"
|
|
73
|
-
)
|
|
74
|
-
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
|
75
|
-
if echo "$1" | grep -iqE "$pattern"; then
|
|
76
|
-
echo "❌ Blocked dangerous command matching: $pattern"
|
|
77
|
-
exit 1
|
|
78
|
-
fi
|
|
79
|
-
done
|
|
80
|
-
|
|
81
|
-
- name: auto-format
|
|
82
|
-
trigger: pre-commit
|
|
83
|
-
description: "Run language-appropriate formatter on staged files"
|
|
84
|
-
filename: pre-commit-format.sh
|
|
85
|
-
script: |
|
|
86
|
-
#!/bin/bash
|
|
87
|
-
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
88
|
-
# Python
|
|
89
|
-
echo "$STAGED" | grep '\.py$' | xargs -r black --quiet 2>/dev/null
|
|
90
|
-
echo "$STAGED" | grep '\.py$' | xargs -r isort --quiet 2>/dev/null
|
|
91
|
-
# TypeScript/JavaScript
|
|
92
|
-
echo "$STAGED" | grep '\.\(ts\|tsx\|js\|jsx\)$' | xargs -r npx prettier --write 2>/dev/null
|
|
93
|
-
# Re-stage formatted files
|
|
94
|
-
echo "$STAGED" | xargs -r git add
|
|
95
|
-
|
|
96
|
-
- name: secrets-scanner
|
|
97
|
-
trigger: pre-commit
|
|
98
|
-
description: "Scan for accidentally committed secrets (AWS keys, API keys, passwords)"
|
|
99
|
-
filename: pre-commit-secrets.sh
|
|
100
|
-
script: |
|
|
101
|
-
#!/bin/bash
|
|
102
|
-
PATTERNS=(
|
|
103
|
-
'AKIA[0-9A-Z]{16}'
|
|
104
|
-
'password\s*=\s*["\x27][^"\x27]+'
|
|
105
|
-
'BEGIN RSA PRIVATE KEY'
|
|
106
|
-
'sk-[a-zA-Z0-9]{48}'
|
|
107
|
-
'ghp_[a-zA-Z0-9]{36}'
|
|
108
|
-
)
|
|
109
|
-
STAGED=$(git diff --cached --name-only)
|
|
110
|
-
for file in $STAGED; do
|
|
111
|
-
for pattern in "${PATTERNS[@]}"; do
|
|
112
|
-
if grep -qE "$pattern" "$file" 2>/dev/null; then
|
|
113
|
-
echo "❌ Potential secret found in $file matching pattern"
|
|
114
|
-
exit 1
|
|
115
|
-
fi
|
|
116
|
-
done
|
|
117
|
-
done
|
|
118
|
-
|
|
119
|
-
- name: compile-check
|
|
120
|
-
trigger: pre-commit
|
|
121
|
-
description: "Detect project type and run appropriate compile/build check"
|
|
122
|
-
filename: pre-commit-compile.sh
|
|
123
|
-
script: |
|
|
124
|
-
#!/bin/bash
|
|
125
|
-
echo "🔨 Running build check..."
|
|
126
|
-
if [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ]; then
|
|
127
|
-
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
|
|
128
|
-
if [ -n "$STAGED_PY" ]; then
|
|
129
|
-
for file in $STAGED_PY; do
|
|
130
|
-
python -m py_compile "$file" 2>&1
|
|
131
|
-
if [ $? -ne 0 ]; then
|
|
132
|
-
echo "❌ Syntax error in $file"
|
|
133
|
-
exit 1
|
|
134
|
-
fi
|
|
135
|
-
done
|
|
136
|
-
echo " ✅ Python syntax OK"
|
|
137
|
-
fi
|
|
138
|
-
fi
|
|
139
|
-
if [ -f "tsconfig.json" ]; then
|
|
140
|
-
npx tsc --noEmit 2>&1
|
|
141
|
-
if [ $? -ne 0 ]; then
|
|
142
|
-
echo "❌ TypeScript compilation failed."
|
|
143
|
-
exit 1
|
|
144
|
-
fi
|
|
145
|
-
echo " ✅ TypeScript compilation OK"
|
|
146
|
-
fi
|
|
147
|
-
echo "🔨 Build check passed"
|
|
148
|
-
|
|
149
|
-
- name: tdd-phase-gate
|
|
150
|
-
trigger: pre-commit
|
|
151
|
-
description: "RED gate: test-only commits must have failing tests; warn on implementation without tests"
|
|
152
|
-
filename: pre-commit-tdd-check.sh
|
|
153
|
-
script: |
|
|
154
|
-
#!/bin/bash
|
|
155
|
-
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
156
|
-
if [ -z "$STAGED" ]; then exit 0; fi
|
|
157
|
-
SRC_PATTERNS='^(src|lib|app|server|client|pkg|internal|cmd)/'
|
|
158
|
-
TEST_PATTERNS='^(tests?|spec|__tests__)/'
|
|
159
|
-
TEST_FILE_PATTERNS='\.(test|spec)\.(ts|tsx|js|jsx|mjs|py|go|rb|java|kt)$'
|
|
160
|
-
SRC_FILES=()
|
|
161
|
-
TEST_FILES=()
|
|
162
|
-
while IFS= read -r file; do
|
|
163
|
-
if echo "$file" | grep -qE "$SRC_PATTERNS"; then
|
|
164
|
-
SRC_FILES+=("$file")
|
|
165
|
-
elif echo "$file" | grep -qE "$TEST_PATTERNS"; then
|
|
166
|
-
TEST_FILES+=("$file")
|
|
167
|
-
elif echo "$file" | grep -qE "$TEST_FILE_PATTERNS"; then
|
|
168
|
-
TEST_FILES+=("$file")
|
|
169
|
-
fi
|
|
170
|
-
done <<< "$STAGED"
|
|
171
|
-
SRC_COUNT=${#SRC_FILES[@]}
|
|
172
|
-
TEST_COUNT=${#TEST_FILES[@]}
|
|
173
|
-
# RED gate: test-only commit must have at least one failing test
|
|
174
|
-
if [ "$TEST_COUNT" -gt 0 ] && [ "$SRC_COUNT" -eq 0 ]; then
|
|
175
|
-
echo "🔴 TDD gate: test-only commit — running staged tests..."
|
|
176
|
-
RUN_CMD=""
|
|
177
|
-
if [ -f "package.json" ]; then
|
|
178
|
-
if grep -q '"vitest"' package.json 2>/dev/null; then
|
|
179
|
-
RUN_CMD="npx vitest run"
|
|
180
|
-
elif grep -q '"jest"' package.json 2>/dev/null; then
|
|
181
|
-
RUN_CMD="npx jest --passWithNoTests"
|
|
182
|
-
fi
|
|
183
|
-
elif [ -f "pytest.ini" ] || [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
|
|
184
|
-
RUN_CMD="python -m pytest"
|
|
185
|
-
fi
|
|
186
|
-
if [ -n "$RUN_CMD" ]; then
|
|
187
|
-
$RUN_CMD "${TEST_FILES[@]}" > /tmp/tdd-check-output.txt 2>&1
|
|
188
|
-
if [ $? -eq 0 ]; then
|
|
189
|
-
echo "❌ TDD RED gate violation: all staged tests PASS"
|
|
190
|
-
echo " A test-only commit must contain at least one failing test."
|
|
191
|
-
echo " Either tests were written after implementation, or assertions are vacuous."
|
|
192
|
-
cat /tmp/tdd-check-output.txt | head -30
|
|
193
|
-
exit 1
|
|
194
|
-
fi
|
|
195
|
-
echo " ✅ RED gate satisfied — staged tests fail as expected"
|
|
196
|
-
else
|
|
197
|
-
echo " ⚠️ Cannot detect test runner — skipping RED gate check"
|
|
198
|
-
fi
|
|
199
|
-
fi
|
|
200
|
-
# Source gate: warn on implementation without tests
|
|
201
|
-
if [ "$SRC_COUNT" -gt 0 ] && [ "$TEST_COUNT" -eq 0 ]; then
|
|
202
|
-
echo "⚠️ TDD warning: implementation committed without test changes"
|
|
203
|
-
echo " Verify a preceding test(scope): [RED] commit exists in this branch."
|
|
204
|
-
fi
|
|
205
|
-
exit 0
|
|
206
|
-
|
|
207
|
-
- name: test-coverage
|
|
208
|
-
trigger: pre-commit
|
|
209
|
-
description: "Run tests for test-only commits; skip for docs/config-only commits; defer to coverage-gate when src/ staged"
|
|
210
|
-
filename: pre-commit-test.sh
|
|
211
|
-
script: |
|
|
212
|
-
#!/bin/bash
|
|
213
|
-
COVERAGE_MIN={{coverage_minimum | default: 80}}
|
|
214
|
-
|
|
215
|
-
# Collect staged files once.
|
|
216
|
-
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
217
|
-
|
|
218
|
-
# Determine what kinds of files are staged.
|
|
219
|
-
SRC_STAGED=0
|
|
220
|
-
CODE_STAGED=0
|
|
221
|
-
while IFS= read -r file; do
|
|
222
|
-
if echo "$file" | grep -qE '^src/'; then
|
|
223
|
-
SRC_STAGED=1
|
|
224
|
-
CODE_STAGED=1
|
|
225
|
-
elif echo "$file" | grep -qE '^tests?/'; then
|
|
226
|
-
CODE_STAGED=1
|
|
227
|
-
fi
|
|
228
|
-
done <<< "$STAGED"
|
|
229
|
-
|
|
230
|
-
# If src/ is staged the coverage-gate hook runs the full test + coverage pass.
|
|
231
|
-
# Skip the bare run here to avoid running the full suite twice.
|
|
232
|
-
if [ "$SRC_STAGED" -eq 1 ]; then
|
|
233
|
-
echo "🧪 src/ files staged — tests will run via coverage gate, skipping bare run."
|
|
234
|
-
exit 0
|
|
235
|
-
fi
|
|
236
|
-
|
|
237
|
-
# If no code files at all are staged (docs-only, config-only, etc.) skip the run.
|
|
238
|
-
if [ "$CODE_STAGED" -eq 0 ]; then
|
|
239
|
-
echo "🧪 No code files staged — skipping test run."
|
|
240
|
-
exit 0
|
|
241
|
-
fi
|
|
242
|
-
|
|
243
|
-
echo "🧪 Running tests..."
|
|
244
|
-
if [ -f "package.json" ]; then
|
|
245
|
-
if grep -q '"vitest"' package.json 2>/dev/null; then
|
|
246
|
-
npx vitest run --reporter=verbose 2>&1
|
|
247
|
-
if [ $? -ne 0 ]; then
|
|
248
|
-
echo "❌ Tests failed."
|
|
249
|
-
exit 1
|
|
250
|
-
fi
|
|
251
|
-
echo " ✅ Tests passed"
|
|
252
|
-
elif grep -q '"jest"' package.json 2>/dev/null; then
|
|
253
|
-
npx jest --passWithNoTests --coverage \
|
|
254
|
-
--coverageThreshold="{\"global\":{\"lines\":$COVERAGE_MIN}}" \
|
|
255
|
-
--silent 2>&1
|
|
256
|
-
if [ $? -ne 0 ]; then
|
|
257
|
-
echo "❌ Jest tests failed or coverage below ${COVERAGE_MIN}%."
|
|
258
|
-
exit 1
|
|
259
|
-
fi
|
|
260
|
-
echo " ✅ Jest tests passed"
|
|
261
|
-
fi
|
|
262
|
-
fi
|
|
263
|
-
if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
|
|
264
|
-
if command -v pytest &> /dev/null; then
|
|
265
|
-
pytest --tb=short --quiet --cov=src --cov-fail-under=$COVERAGE_MIN 2>&1
|
|
266
|
-
if [ $? -ne 0 ]; then
|
|
267
|
-
echo "❌ Tests failed or coverage below ${COVERAGE_MIN}%."
|
|
268
|
-
exit 1
|
|
269
|
-
fi
|
|
270
|
-
echo " ✅ Python tests passed"
|
|
271
|
-
fi
|
|
272
|
-
fi
|
|
273
|
-
echo "🧪 All tests passed"
|
|
274
|
-
|
|
275
|
-
- name: coverage-gate
|
|
276
|
-
trigger: pre-commit
|
|
277
|
-
description: "Enforce minimum coverage thresholds when src/ files are staged"
|
|
278
|
-
filename: pre-commit-coverage.sh
|
|
279
|
-
script: |
|
|
280
|
-
#!/bin/bash
|
|
281
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
282
|
-
# Pre-Commit Hook: Coverage Gate
|
|
283
|
-
#
|
|
284
|
-
# Enforces minimum coverage thresholds before a commit lands.
|
|
285
|
-
# Thresholds are read from vitest.config.ts (via --coverage).
|
|
286
|
-
# Only runs when files under src/ are staged — skips for
|
|
287
|
-
# docs-only, config-only, or test-only commits.
|
|
288
|
-
#
|
|
289
|
-
# Current thresholds (vitest.config.ts):
|
|
290
|
-
# Lines / Statements / Functions: {{coverage_minimum | default: 80}}%
|
|
291
|
-
# Branches: 70%
|
|
292
|
-
#
|
|
293
|
-
# Trigger: git pre-commit (via scripts/setup-hooks.sh)
|
|
294
|
-
# Exit: 1 blocks commit, 0 allows
|
|
295
|
-
# ──────────────────────────────────────────────────────────────────────
|
|
296
|
-
|
|
297
|
-
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
298
|
-
|
|
299
|
-
if [ -z "$STAGED" ]; then
|
|
300
|
-
exit 0
|
|
301
|
-
fi
|
|
302
|
-
|
|
303
|
-
# ── Check if any src/ file is staged ─────────────────────────────────
|
|
304
|
-
SRC_STAGED=0
|
|
305
|
-
while IFS= read -r file; do
|
|
306
|
-
if echo "$file" | grep -qE '^src/'; then
|
|
307
|
-
SRC_STAGED=1
|
|
308
|
-
break
|
|
309
|
-
fi
|
|
310
|
-
done <<< "$STAGED"
|
|
311
|
-
|
|
312
|
-
if [ "$SRC_STAGED" -eq 0 ]; then
|
|
313
|
-
echo "📊 Coverage gate: no src/ files staged, skipping."
|
|
314
|
-
exit 0
|
|
315
|
-
fi
|
|
316
|
-
|
|
317
|
-
# ── Run coverage ───────────────────────────────────────────────────────
|
|
318
|
-
echo "📊 Running coverage gate (src/ files staged)..."
|
|
319
|
-
|
|
320
|
-
if [ ! -f "package.json" ]; then
|
|
321
|
-
echo " ⚠️ No package.json found — skipping coverage check."
|
|
322
|
-
exit 0
|
|
323
|
-
fi
|
|
324
|
-
|
|
325
|
-
if grep -q '"vitest"' package.json 2>/dev/null; then
|
|
326
|
-
OUTPUT=$(npx vitest run --coverage --reporter=verbose 2>&1)
|
|
327
|
-
EXIT_CODE=$?
|
|
328
|
-
|
|
329
|
-
if [ $EXIT_CODE -ne 0 ]; then
|
|
330
|
-
echo "$OUTPUT" | grep -E "ERROR|Coverage|does not meet|%|FAIL|passed|failed" | head -40
|
|
331
|
-
echo ""
|
|
332
|
-
echo "❌ Coverage gate failed — thresholds not met."
|
|
333
|
-
echo " Run 'npx vitest run --coverage' locally to see the full report."
|
|
334
|
-
echo " Add tests until coverage meets the configured minimums."
|
|
335
|
-
exit 1
|
|
336
|
-
fi
|
|
337
|
-
echo " ✅ Coverage gate passed"
|
|
338
|
-
exit 0
|
|
339
|
-
fi
|
|
340
|
-
|
|
341
|
-
if grep -q '"jest"' package.json 2>/dev/null; then
|
|
342
|
-
COVERAGE_MIN={{coverage_minimum | default: 80}}
|
|
343
|
-
npx jest --passWithNoTests --coverage \
|
|
344
|
-
--coverageThreshold="{\"global\":{\"lines\":$COVERAGE_MIN,\"statements\":$COVERAGE_MIN,\"functions\":$COVERAGE_MIN,\"branches\":70}}" \
|
|
345
|
-
--silent 2>&1
|
|
346
|
-
if [ $? -ne 0 ]; then
|
|
347
|
-
echo "❌ Coverage gate failed — thresholds not met."
|
|
348
|
-
exit 1
|
|
349
|
-
fi
|
|
350
|
-
echo " ✅ Coverage gate passed"
|
|
351
|
-
exit 0
|
|
352
|
-
fi
|
|
353
|
-
|
|
354
|
-
if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
|
|
355
|
-
if command -v pytest &> /dev/null; then
|
|
356
|
-
pytest --tb=no --quiet --cov=src --cov-fail-under={{coverage_minimum | default: 80}} 2>&1
|
|
357
|
-
if [ $? -ne 0 ]; then
|
|
358
|
-
echo "❌ Coverage gate failed — below {{coverage_minimum | default: 80}}%."
|
|
359
|
-
exit 1
|
|
360
|
-
fi
|
|
361
|
-
echo " ✅ Coverage gate passed"
|
|
362
|
-
fi
|
|
363
|
-
fi
|
|
364
|
-
|
|
365
|
-
exit 0
|
|
366
|
-
|
|
367
|
-
- name: anti-pattern-detector
|
|
368
|
-
trigger: pre-commit
|
|
369
|
-
description: "Scan source files for production code anti-patterns. Respects .forgecraft/exceptions.json for recorded false positives."
|
|
370
|
-
filename: pre-commit-prod-quality.sh
|
|
371
|
-
checks:
|
|
372
|
-
- Hardcoded URLs/hosts (localhost, 127.0.0.1)
|
|
373
|
-
- Mock/stub data in production code
|
|
374
|
-
- Direct DB calls in route/controller layer (layer violation)
|
|
375
|
-
- Bare Error throws (custom hierarchy recommended)
|
|
376
|
-
- .env vs .env.example drift (warns on missing variables)
|
|
377
|
-
script: |
|
|
378
|
-
#!/bin/bash
|
|
379
|
-
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
380
|
-
SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(py|ts|tsx|js|jsx)$' | grep -vE '(test_|\.test\.|\.spec\.|__tests__|tests/|fixtures/|mock|conftest)')
|
|
381
|
-
if [ -z "$SOURCE_FILES" ]; then exit 0; fi
|
|
382
|
-
VIOLATIONS=0
|
|
383
|
-
WARNINGS=0
|
|
384
|
-
# Check if a file is covered by a hook exception in .forgecraft/exceptions.json
|
|
385
|
-
# Usage: is_excepted "layer-boundary" "src/migrations/001.ts"
|
|
386
|
-
# Add entries to .forgecraft/exceptions.json to record known false positives.
|
|
387
|
-
is_excepted() {
|
|
388
|
-
local hook_name="$1"
|
|
389
|
-
local file_path="$2"
|
|
390
|
-
if [ ! -f ".forgecraft/exceptions.json" ]; then return 1; fi
|
|
391
|
-
node -e "
|
|
392
|
-
const fs = require('fs');
|
|
393
|
-
const data = JSON.parse(fs.readFileSync('.forgecraft/exceptions.json', 'utf-8'));
|
|
394
|
-
const exc = (data.exceptions || []).find(e => {
|
|
395
|
-
if (e.hook !== '$hook_name') return false;
|
|
396
|
-
const pat = e.pattern.replace(/\\/g, '/').replace(/\./g, '\\\\.').replace(/\*\*/g, '<<<D>>>').replace(/\*/g, '[^/]*').replace(/<<<D>>>/g, '.*');
|
|
397
|
-
return new RegExp('^' + pat + '$').test('$file_path'.replace(/\\\\/g, '/'));
|
|
398
|
-
});
|
|
399
|
-
if (exc) { console.log('EXCEPTED: ' + exc.reason); process.exit(0); }
|
|
400
|
-
process.exit(1);
|
|
401
|
-
" 2>/dev/null
|
|
402
|
-
}
|
|
403
|
-
echo "🔍 Scanning for production code anti-patterns..."
|
|
404
|
-
for file in $SOURCE_FILES; do
|
|
405
|
-
if echo "$file" | grep -vqE '(config|settings|\.env)'; then
|
|
406
|
-
if grep -nE '(localhost|127\.0\.0\.1|0\.0\.0\.0)' "$file" | grep -vE '(#|//|""")' > /tmp/violations 2>/dev/null; then
|
|
407
|
-
if [ -s /tmp/violations ]; then
|
|
408
|
-
echo " ❌ $file — hardcoded URL/host"
|
|
409
|
-
VIOLATIONS=$((VIOLATIONS + 1))
|
|
410
|
-
fi
|
|
411
|
-
fi
|
|
412
|
-
fi
|
|
413
|
-
if ! is_excepted "anti-pattern/mock-data" "$file"; then
|
|
414
|
-
if grep -nEi '\b(mock_data|fake_data|dummy_data|stub_response)' "$file" > /tmp/violations 2>/dev/null; then
|
|
415
|
-
if [ -s /tmp/violations ]; then
|
|
416
|
-
echo " ❌ $file — mock/stub data in production code"
|
|
417
|
-
VIOLATIONS=$((VIOLATIONS + 1))
|
|
418
|
-
fi
|
|
419
|
-
fi
|
|
420
|
-
fi
|
|
421
|
-
# Layer boundary: no direct DB/ORM imports from route handlers / controllers
|
|
422
|
-
if echo "$file" | grep -qE '(routes|controllers|handlers|endpoints)'; then
|
|
423
|
-
if ! is_excepted "layer-boundary" "$file"; then
|
|
424
|
-
if grep -nE '\b(prisma\.|knex\(|mongoose\.|sequelize\.|db\.query|pool\.query)' "$file" > /tmp/violations 2>/dev/null; then
|
|
425
|
-
if [ -s /tmp/violations ]; then
|
|
426
|
-
echo " ❌ $file — direct DB call in route/controller (layer violation)"
|
|
427
|
-
VIOLATIONS=$((VIOLATIONS + 1))
|
|
428
|
-
fi
|
|
429
|
-
fi
|
|
430
|
-
fi
|
|
431
|
-
fi
|
|
432
|
-
# Bare Error throws in business logic (not test files)
|
|
433
|
-
if ! is_excepted "error-hierarchy" "$file"; then
|
|
434
|
-
if grep -nE 'throw new Error\(' "$file" > /tmp/violations 2>/dev/null; then
|
|
435
|
-
if [ -s /tmp/violations ]; then
|
|
436
|
-
echo " ⚠️ $file — bare 'throw new Error()' found — use custom error hierarchy"
|
|
437
|
-
WARNINGS=$((WARNINGS + 1))
|
|
438
|
-
fi
|
|
439
|
-
fi
|
|
440
|
-
fi
|
|
441
|
-
LINE_COUNT=$(wc -l < "$file")
|
|
442
|
-
if [ "$LINE_COUNT" -gt {{max_file_length | default: 300}} ]; then
|
|
443
|
-
echo " ⚠️ $file — $LINE_COUNT lines (max {{max_file_length | default: 300}})"
|
|
444
|
-
WARNINGS=$((WARNINGS + 1))
|
|
445
|
-
fi
|
|
446
|
-
done
|
|
447
|
-
rm -f /tmp/violations
|
|
448
|
-
if [ $VIOLATIONS -gt 0 ]; then
|
|
449
|
-
echo "❌ $VIOLATIONS violation(s) found — commit blocked."
|
|
450
|
-
exit 1
|
|
451
|
-
fi
|
|
452
|
-
if [ $WARNINGS -gt 0 ]; then
|
|
453
|
-
echo "⚠️ $WARNINGS warning(s) found — review recommended."
|
|
454
|
-
fi
|
|
455
|
-
echo "🔍 Production quality scan passed"
|
|
456
|
-
|
|
457
|
-
- name: function-length
|
|
458
|
-
trigger: pre-commit
|
|
459
|
-
description: "Warn when staged functions exceed the configured maximum line count"
|
|
460
|
-
filename: pre-commit-function-length.sh
|
|
461
|
-
script: |
|
|
462
|
-
#!/bin/bash
|
|
463
|
-
MAX_LENGTH={{max_function_length | default: 50}}
|
|
464
|
-
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
465
|
-
SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(ts|tsx|js|jsx)$' | grep -vE '(\.test\.|\.spec\.|__tests__|tests/)')
|
|
466
|
-
if [ -z "$SOURCE_FILES" ]; then exit 0; fi
|
|
467
|
-
WARNINGS=0
|
|
468
|
-
for file in $SOURCE_FILES; do
|
|
469
|
-
# Heuristic: find function/method declarations and count lines to next declaration or closing brace
|
|
470
|
-
awk -v max="$MAX_LENGTH" -v fname="$file" '
|
|
471
|
-
/^[[:space:]]*(export )?(async )?(function |const [a-zA-Z]+ = (async )?\(|[a-zA-Z]+\(.*\) \{|[a-zA-Z]+\(.*\): )/ {
|
|
472
|
-
if (start > 0 && NR - start > max) {
|
|
473
|
-
printf " ⚠️ %s:%d — function starting here is %d lines (max %d)\n", fname, start, NR - start, max
|
|
474
|
-
warnings++
|
|
475
|
-
}
|
|
476
|
-
start = NR
|
|
477
|
-
}
|
|
478
|
-
END {
|
|
479
|
-
if (start > 0 && NR - start > max) {
|
|
480
|
-
printf " ⚠️ %s:%d — function starting here is %d lines (max %d)\n", fname, start, NR - start, max
|
|
481
|
-
warnings++
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
' "$file"
|
|
485
|
-
WARNINGS=$((WARNINGS + $?))
|
|
486
|
-
done
|
|
487
|
-
# Warning only — does not block commit since bash heuristics aren't perfect
|
|
488
|
-
exit 0
|
|
489
|
-
|
|
490
|
-
- name: import-cycle-detector
|
|
491
|
-
trigger: pre-commit
|
|
492
|
-
description: "Detect circular import dependencies in TypeScript and Python projects"
|
|
493
|
-
filename: pre-commit-import-cycles.sh
|
|
494
|
-
script: |
|
|
495
|
-
#!/bin/bash
|
|
496
|
-
echo "🔄 Checking for circular imports..."
|
|
497
|
-
if [ -f "tsconfig.json" ]; then
|
|
498
|
-
if command -v npx &> /dev/null; then
|
|
499
|
-
RESULT=$(npx --yes madge --circular --extensions ts src/ 2>&1)
|
|
500
|
-
if echo "$RESULT" | grep -q "Found.*circular"; then
|
|
501
|
-
echo "❌ Circular imports detected in TypeScript:"
|
|
502
|
-
echo "$RESULT"
|
|
503
|
-
exit 1
|
|
504
|
-
fi
|
|
505
|
-
echo " ✅ No circular imports (TypeScript)"
|
|
506
|
-
fi
|
|
507
|
-
fi
|
|
508
|
-
if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
|
|
509
|
-
if command -v lint-imports &> /dev/null; then
|
|
510
|
-
lint-imports 2>&1
|
|
511
|
-
if [ $? -ne 0 ]; then
|
|
512
|
-
echo "❌ Circular imports detected in Python"
|
|
513
|
-
exit 1
|
|
514
|
-
fi
|
|
515
|
-
echo " ✅ No circular imports (Python)"
|
|
516
|
-
fi
|
|
517
|
-
fi
|
|
518
|
-
echo "🔄 Import cycle check passed"
|
|
519
|
-
|
|
520
|
-
- name: code-review
|
|
521
|
-
trigger: pre-commit
|
|
522
|
-
description: "AI assistant reviews diff against project standards"
|
|
523
|
-
filename: pre-commit-review.sh
|
|
524
|
-
script: |
|
|
525
|
-
#!/bin/bash
|
|
526
|
-
DIFF=$(git diff --cached)
|
|
527
|
-
if [ -z "$DIFF" ]; then exit 0; fi
|
|
528
|
-
echo "📝 Staged changes ready for review"
|
|
529
|
-
# Full auto-review requires claude CLI integration
|
|
530
|
-
# This hook validates the diff is non-empty and staged
|
|
531
|
-
exit 0
|
|
1
|
+
tag: UNIVERSAL
|
|
2
|
+
section: hooks
|
|
3
|
+
hooks:
|
|
4
|
+
- name: commit-msg
|
|
5
|
+
trigger: commit-msg
|
|
6
|
+
description: "Enforce conventional commit format: <type>(<scope>): <description>"
|
|
7
|
+
filename: commit-msg.sh
|
|
8
|
+
script: |
|
|
9
|
+
#!/usr/bin/env bash
|
|
10
|
+
# commit-msg: enforce conventional commit format
|
|
11
|
+
# ForgeCraft — generated hook
|
|
12
|
+
|
|
13
|
+
COMMIT_MSG_FILE="$1"
|
|
14
|
+
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
|
15
|
+
|
|
16
|
+
# Skip merge commits and fixup commits
|
|
17
|
+
if echo "$COMMIT_MSG" | grep -qE "^(Merge|Revert|fixup!|squash!)"; then
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Skip empty messages and comments-only
|
|
22
|
+
STRIPPED=$(echo "$COMMIT_MSG" | sed '/^#/d' | sed '/^$/d')
|
|
23
|
+
if [ -z "$STRIPPED" ]; then
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
PATTERN="^(feat|fix|refactor|docs|test|chore|perf|ci|build|revert)(\([a-z0-9/_-]+\))?(!)?: .{1,72}"
|
|
28
|
+
|
|
29
|
+
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
|
|
30
|
+
echo ""
|
|
31
|
+
echo " ✗ Commit message does not follow conventional commit format."
|
|
32
|
+
echo ""
|
|
33
|
+
echo " Required format: <type>(<scope>): <description>"
|
|
34
|
+
echo " Types: feat | fix | refactor | docs | test | chore | perf | ci | build | revert"
|
|
35
|
+
echo " Examples:"
|
|
36
|
+
echo " feat(auth): add JWT refresh token support"
|
|
37
|
+
echo " fix(api): handle null response from payment gateway"
|
|
38
|
+
echo " docs: update README with setup instructions"
|
|
39
|
+
echo ""
|
|
40
|
+
echo " Your message: $COMMIT_MSG"
|
|
41
|
+
echo ""
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
exit 0
|
|
46
|
+
|
|
47
|
+
- name: branch-protection
|
|
48
|
+
trigger: pre-commit
|
|
49
|
+
description: "Block direct commits to main/master branches"
|
|
50
|
+
filename: pre-commit-branch-check.sh
|
|
51
|
+
script: |
|
|
52
|
+
#!/bin/bash
|
|
53
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
54
|
+
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
55
|
+
echo "❌ Direct commits to $BRANCH are blocked. Create a feature branch."
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
- name: dangerous-commands
|
|
60
|
+
trigger: pre-exec
|
|
61
|
+
description: "Block destructive commands (rm -rf /, DROP DATABASE, force push)"
|
|
62
|
+
filename: pre-exec-safety.sh
|
|
63
|
+
script: |
|
|
64
|
+
#!/bin/bash
|
|
65
|
+
DANGEROUS_PATTERNS=(
|
|
66
|
+
"rm -rf /"
|
|
67
|
+
"DROP DATABASE"
|
|
68
|
+
"DROP TABLE"
|
|
69
|
+
"TRUNCATE"
|
|
70
|
+
"force push"
|
|
71
|
+
"git push.*--force"
|
|
72
|
+
"kubectl delete namespace"
|
|
73
|
+
)
|
|
74
|
+
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
|
75
|
+
if echo "$1" | grep -iqE "$pattern"; then
|
|
76
|
+
echo "❌ Blocked dangerous command matching: $pattern"
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
- name: auto-format
|
|
82
|
+
trigger: pre-commit
|
|
83
|
+
description: "Run language-appropriate formatter on staged files"
|
|
84
|
+
filename: pre-commit-format.sh
|
|
85
|
+
script: |
|
|
86
|
+
#!/bin/bash
|
|
87
|
+
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
88
|
+
# Python
|
|
89
|
+
echo "$STAGED" | grep '\.py$' | xargs -r black --quiet 2>/dev/null
|
|
90
|
+
echo "$STAGED" | grep '\.py$' | xargs -r isort --quiet 2>/dev/null
|
|
91
|
+
# TypeScript/JavaScript
|
|
92
|
+
echo "$STAGED" | grep '\.\(ts\|tsx\|js\|jsx\)$' | xargs -r npx prettier --write 2>/dev/null
|
|
93
|
+
# Re-stage formatted files
|
|
94
|
+
echo "$STAGED" | xargs -r git add
|
|
95
|
+
|
|
96
|
+
- name: secrets-scanner
|
|
97
|
+
trigger: pre-commit
|
|
98
|
+
description: "Scan for accidentally committed secrets (AWS keys, API keys, passwords)"
|
|
99
|
+
filename: pre-commit-secrets.sh
|
|
100
|
+
script: |
|
|
101
|
+
#!/bin/bash
|
|
102
|
+
PATTERNS=(
|
|
103
|
+
'AKIA[0-9A-Z]{16}'
|
|
104
|
+
'password\s*=\s*["\x27][^"\x27]+'
|
|
105
|
+
'BEGIN RSA PRIVATE KEY'
|
|
106
|
+
'sk-[a-zA-Z0-9]{48}'
|
|
107
|
+
'ghp_[a-zA-Z0-9]{36}'
|
|
108
|
+
)
|
|
109
|
+
STAGED=$(git diff --cached --name-only)
|
|
110
|
+
for file in $STAGED; do
|
|
111
|
+
for pattern in "${PATTERNS[@]}"; do
|
|
112
|
+
if grep -qE "$pattern" "$file" 2>/dev/null; then
|
|
113
|
+
echo "❌ Potential secret found in $file matching pattern"
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
117
|
+
done
|
|
118
|
+
|
|
119
|
+
- name: compile-check
|
|
120
|
+
trigger: pre-commit
|
|
121
|
+
description: "Detect project type and run appropriate compile/build check"
|
|
122
|
+
filename: pre-commit-compile.sh
|
|
123
|
+
script: |
|
|
124
|
+
#!/bin/bash
|
|
125
|
+
echo "🔨 Running build check..."
|
|
126
|
+
if [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ]; then
|
|
127
|
+
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
|
|
128
|
+
if [ -n "$STAGED_PY" ]; then
|
|
129
|
+
for file in $STAGED_PY; do
|
|
130
|
+
python -m py_compile "$file" 2>&1
|
|
131
|
+
if [ $? -ne 0 ]; then
|
|
132
|
+
echo "❌ Syntax error in $file"
|
|
133
|
+
exit 1
|
|
134
|
+
fi
|
|
135
|
+
done
|
|
136
|
+
echo " ✅ Python syntax OK"
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
if [ -f "tsconfig.json" ]; then
|
|
140
|
+
npx tsc --noEmit 2>&1
|
|
141
|
+
if [ $? -ne 0 ]; then
|
|
142
|
+
echo "❌ TypeScript compilation failed."
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
echo " ✅ TypeScript compilation OK"
|
|
146
|
+
fi
|
|
147
|
+
echo "🔨 Build check passed"
|
|
148
|
+
|
|
149
|
+
- name: tdd-phase-gate
|
|
150
|
+
trigger: pre-commit
|
|
151
|
+
description: "RED gate: test-only commits must have failing tests; warn on implementation without tests"
|
|
152
|
+
filename: pre-commit-tdd-check.sh
|
|
153
|
+
script: |
|
|
154
|
+
#!/bin/bash
|
|
155
|
+
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
156
|
+
if [ -z "$STAGED" ]; then exit 0; fi
|
|
157
|
+
SRC_PATTERNS='^(src|lib|app|server|client|pkg|internal|cmd)/'
|
|
158
|
+
TEST_PATTERNS='^(tests?|spec|__tests__)/'
|
|
159
|
+
TEST_FILE_PATTERNS='\.(test|spec)\.(ts|tsx|js|jsx|mjs|py|go|rb|java|kt)$'
|
|
160
|
+
SRC_FILES=()
|
|
161
|
+
TEST_FILES=()
|
|
162
|
+
while IFS= read -r file; do
|
|
163
|
+
if echo "$file" | grep -qE "$SRC_PATTERNS"; then
|
|
164
|
+
SRC_FILES+=("$file")
|
|
165
|
+
elif echo "$file" | grep -qE "$TEST_PATTERNS"; then
|
|
166
|
+
TEST_FILES+=("$file")
|
|
167
|
+
elif echo "$file" | grep -qE "$TEST_FILE_PATTERNS"; then
|
|
168
|
+
TEST_FILES+=("$file")
|
|
169
|
+
fi
|
|
170
|
+
done <<< "$STAGED"
|
|
171
|
+
SRC_COUNT=${#SRC_FILES[@]}
|
|
172
|
+
TEST_COUNT=${#TEST_FILES[@]}
|
|
173
|
+
# RED gate: test-only commit must have at least one failing test
|
|
174
|
+
if [ "$TEST_COUNT" -gt 0 ] && [ "$SRC_COUNT" -eq 0 ]; then
|
|
175
|
+
echo "🔴 TDD gate: test-only commit — running staged tests..."
|
|
176
|
+
RUN_CMD=""
|
|
177
|
+
if [ -f "package.json" ]; then
|
|
178
|
+
if grep -q '"vitest"' package.json 2>/dev/null; then
|
|
179
|
+
RUN_CMD="npx vitest run"
|
|
180
|
+
elif grep -q '"jest"' package.json 2>/dev/null; then
|
|
181
|
+
RUN_CMD="npx jest --passWithNoTests"
|
|
182
|
+
fi
|
|
183
|
+
elif [ -f "pytest.ini" ] || [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
|
|
184
|
+
RUN_CMD="python -m pytest"
|
|
185
|
+
fi
|
|
186
|
+
if [ -n "$RUN_CMD" ]; then
|
|
187
|
+
$RUN_CMD "${TEST_FILES[@]}" > /tmp/tdd-check-output.txt 2>&1
|
|
188
|
+
if [ $? -eq 0 ]; then
|
|
189
|
+
echo "❌ TDD RED gate violation: all staged tests PASS"
|
|
190
|
+
echo " A test-only commit must contain at least one failing test."
|
|
191
|
+
echo " Either tests were written after implementation, or assertions are vacuous."
|
|
192
|
+
cat /tmp/tdd-check-output.txt | head -30
|
|
193
|
+
exit 1
|
|
194
|
+
fi
|
|
195
|
+
echo " ✅ RED gate satisfied — staged tests fail as expected"
|
|
196
|
+
else
|
|
197
|
+
echo " ⚠️ Cannot detect test runner — skipping RED gate check"
|
|
198
|
+
fi
|
|
199
|
+
fi
|
|
200
|
+
# Source gate: warn on implementation without tests
|
|
201
|
+
if [ "$SRC_COUNT" -gt 0 ] && [ "$TEST_COUNT" -eq 0 ]; then
|
|
202
|
+
echo "⚠️ TDD warning: implementation committed without test changes"
|
|
203
|
+
echo " Verify a preceding test(scope): [RED] commit exists in this branch."
|
|
204
|
+
fi
|
|
205
|
+
exit 0
|
|
206
|
+
|
|
207
|
+
- name: test-coverage
|
|
208
|
+
trigger: pre-commit
|
|
209
|
+
description: "Run tests for test-only commits; skip for docs/config-only commits; defer to coverage-gate when src/ staged"
|
|
210
|
+
filename: pre-commit-test.sh
|
|
211
|
+
script: |
|
|
212
|
+
#!/bin/bash
|
|
213
|
+
COVERAGE_MIN={{coverage_minimum | default: 80}}
|
|
214
|
+
|
|
215
|
+
# Collect staged files once.
|
|
216
|
+
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
217
|
+
|
|
218
|
+
# Determine what kinds of files are staged.
|
|
219
|
+
SRC_STAGED=0
|
|
220
|
+
CODE_STAGED=0
|
|
221
|
+
while IFS= read -r file; do
|
|
222
|
+
if echo "$file" | grep -qE '^src/'; then
|
|
223
|
+
SRC_STAGED=1
|
|
224
|
+
CODE_STAGED=1
|
|
225
|
+
elif echo "$file" | grep -qE '^tests?/'; then
|
|
226
|
+
CODE_STAGED=1
|
|
227
|
+
fi
|
|
228
|
+
done <<< "$STAGED"
|
|
229
|
+
|
|
230
|
+
# If src/ is staged the coverage-gate hook runs the full test + coverage pass.
|
|
231
|
+
# Skip the bare run here to avoid running the full suite twice.
|
|
232
|
+
if [ "$SRC_STAGED" -eq 1 ]; then
|
|
233
|
+
echo "🧪 src/ files staged — tests will run via coverage gate, skipping bare run."
|
|
234
|
+
exit 0
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# If no code files at all are staged (docs-only, config-only, etc.) skip the run.
|
|
238
|
+
if [ "$CODE_STAGED" -eq 0 ]; then
|
|
239
|
+
echo "🧪 No code files staged — skipping test run."
|
|
240
|
+
exit 0
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
echo "🧪 Running tests..."
|
|
244
|
+
if [ -f "package.json" ]; then
|
|
245
|
+
if grep -q '"vitest"' package.json 2>/dev/null; then
|
|
246
|
+
npx vitest run --reporter=verbose 2>&1
|
|
247
|
+
if [ $? -ne 0 ]; then
|
|
248
|
+
echo "❌ Tests failed."
|
|
249
|
+
exit 1
|
|
250
|
+
fi
|
|
251
|
+
echo " ✅ Tests passed"
|
|
252
|
+
elif grep -q '"jest"' package.json 2>/dev/null; then
|
|
253
|
+
npx jest --passWithNoTests --coverage \
|
|
254
|
+
--coverageThreshold="{\"global\":{\"lines\":$COVERAGE_MIN}}" \
|
|
255
|
+
--silent 2>&1
|
|
256
|
+
if [ $? -ne 0 ]; then
|
|
257
|
+
echo "❌ Jest tests failed or coverage below ${COVERAGE_MIN}%."
|
|
258
|
+
exit 1
|
|
259
|
+
fi
|
|
260
|
+
echo " ✅ Jest tests passed"
|
|
261
|
+
fi
|
|
262
|
+
fi
|
|
263
|
+
if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
|
|
264
|
+
if command -v pytest &> /dev/null; then
|
|
265
|
+
pytest --tb=short --quiet --cov=src --cov-fail-under=$COVERAGE_MIN 2>&1
|
|
266
|
+
if [ $? -ne 0 ]; then
|
|
267
|
+
echo "❌ Tests failed or coverage below ${COVERAGE_MIN}%."
|
|
268
|
+
exit 1
|
|
269
|
+
fi
|
|
270
|
+
echo " ✅ Python tests passed"
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
echo "🧪 All tests passed"
|
|
274
|
+
|
|
275
|
+
- name: coverage-gate
|
|
276
|
+
trigger: pre-commit
|
|
277
|
+
description: "Enforce minimum coverage thresholds when src/ files are staged"
|
|
278
|
+
filename: pre-commit-coverage.sh
|
|
279
|
+
script: |
|
|
280
|
+
#!/bin/bash
|
|
281
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
282
|
+
# Pre-Commit Hook: Coverage Gate
|
|
283
|
+
#
|
|
284
|
+
# Enforces minimum coverage thresholds before a commit lands.
|
|
285
|
+
# Thresholds are read from vitest.config.ts (via --coverage).
|
|
286
|
+
# Only runs when files under src/ are staged — skips for
|
|
287
|
+
# docs-only, config-only, or test-only commits.
|
|
288
|
+
#
|
|
289
|
+
# Current thresholds (vitest.config.ts):
|
|
290
|
+
# Lines / Statements / Functions: {{coverage_minimum | default: 80}}%
|
|
291
|
+
# Branches: 70%
|
|
292
|
+
#
|
|
293
|
+
# Trigger: git pre-commit (via scripts/setup-hooks.sh)
|
|
294
|
+
# Exit: 1 blocks commit, 0 allows
|
|
295
|
+
# ──────────────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
298
|
+
|
|
299
|
+
if [ -z "$STAGED" ]; then
|
|
300
|
+
exit 0
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# ── Check if any src/ file is staged ─────────────────────────────────
|
|
304
|
+
SRC_STAGED=0
|
|
305
|
+
while IFS= read -r file; do
|
|
306
|
+
if echo "$file" | grep -qE '^src/'; then
|
|
307
|
+
SRC_STAGED=1
|
|
308
|
+
break
|
|
309
|
+
fi
|
|
310
|
+
done <<< "$STAGED"
|
|
311
|
+
|
|
312
|
+
if [ "$SRC_STAGED" -eq 0 ]; then
|
|
313
|
+
echo "📊 Coverage gate: no src/ files staged, skipping."
|
|
314
|
+
exit 0
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
# ── Run coverage ───────────────────────────────────────────────────────
|
|
318
|
+
echo "📊 Running coverage gate (src/ files staged)..."
|
|
319
|
+
|
|
320
|
+
if [ ! -f "package.json" ]; then
|
|
321
|
+
echo " ⚠️ No package.json found — skipping coverage check."
|
|
322
|
+
exit 0
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
if grep -q '"vitest"' package.json 2>/dev/null; then
|
|
326
|
+
OUTPUT=$(npx vitest run --coverage --reporter=verbose 2>&1)
|
|
327
|
+
EXIT_CODE=$?
|
|
328
|
+
|
|
329
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
330
|
+
echo "$OUTPUT" | grep -E "ERROR|Coverage|does not meet|%|FAIL|passed|failed" | head -40
|
|
331
|
+
echo ""
|
|
332
|
+
echo "❌ Coverage gate failed — thresholds not met."
|
|
333
|
+
echo " Run 'npx vitest run --coverage' locally to see the full report."
|
|
334
|
+
echo " Add tests until coverage meets the configured minimums."
|
|
335
|
+
exit 1
|
|
336
|
+
fi
|
|
337
|
+
echo " ✅ Coverage gate passed"
|
|
338
|
+
exit 0
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
if grep -q '"jest"' package.json 2>/dev/null; then
|
|
342
|
+
COVERAGE_MIN={{coverage_minimum | default: 80}}
|
|
343
|
+
npx jest --passWithNoTests --coverage \
|
|
344
|
+
--coverageThreshold="{\"global\":{\"lines\":$COVERAGE_MIN,\"statements\":$COVERAGE_MIN,\"functions\":$COVERAGE_MIN,\"branches\":70}}" \
|
|
345
|
+
--silent 2>&1
|
|
346
|
+
if [ $? -ne 0 ]; then
|
|
347
|
+
echo "❌ Coverage gate failed — thresholds not met."
|
|
348
|
+
exit 1
|
|
349
|
+
fi
|
|
350
|
+
echo " ✅ Coverage gate passed"
|
|
351
|
+
exit 0
|
|
352
|
+
fi
|
|
353
|
+
|
|
354
|
+
if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
|
|
355
|
+
if command -v pytest &> /dev/null; then
|
|
356
|
+
pytest --tb=no --quiet --cov=src --cov-fail-under={{coverage_minimum | default: 80}} 2>&1
|
|
357
|
+
if [ $? -ne 0 ]; then
|
|
358
|
+
echo "❌ Coverage gate failed — below {{coverage_minimum | default: 80}}%."
|
|
359
|
+
exit 1
|
|
360
|
+
fi
|
|
361
|
+
echo " ✅ Coverage gate passed"
|
|
362
|
+
fi
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
exit 0
|
|
366
|
+
|
|
367
|
+
- name: anti-pattern-detector
|
|
368
|
+
trigger: pre-commit
|
|
369
|
+
description: "Scan source files for production code anti-patterns. Respects .forgecraft/exceptions.json for recorded false positives."
|
|
370
|
+
filename: pre-commit-prod-quality.sh
|
|
371
|
+
checks:
|
|
372
|
+
- Hardcoded URLs/hosts (localhost, 127.0.0.1)
|
|
373
|
+
- Mock/stub data in production code
|
|
374
|
+
- Direct DB calls in route/controller layer (layer violation)
|
|
375
|
+
- Bare Error throws (custom hierarchy recommended)
|
|
376
|
+
- .env vs .env.example drift (warns on missing variables)
|
|
377
|
+
script: |
|
|
378
|
+
#!/bin/bash
|
|
379
|
+
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
380
|
+
SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(py|ts|tsx|js|jsx)$' | grep -vE '(test_|\.test\.|\.spec\.|__tests__|tests/|fixtures/|mock|conftest)')
|
|
381
|
+
if [ -z "$SOURCE_FILES" ]; then exit 0; fi
|
|
382
|
+
VIOLATIONS=0
|
|
383
|
+
WARNINGS=0
|
|
384
|
+
# Check if a file is covered by a hook exception in .forgecraft/exceptions.json
|
|
385
|
+
# Usage: is_excepted "layer-boundary" "src/migrations/001.ts"
|
|
386
|
+
# Add entries to .forgecraft/exceptions.json to record known false positives.
|
|
387
|
+
is_excepted() {
|
|
388
|
+
local hook_name="$1"
|
|
389
|
+
local file_path="$2"
|
|
390
|
+
if [ ! -f ".forgecraft/exceptions.json" ]; then return 1; fi
|
|
391
|
+
node -e "
|
|
392
|
+
const fs = require('fs');
|
|
393
|
+
const data = JSON.parse(fs.readFileSync('.forgecraft/exceptions.json', 'utf-8'));
|
|
394
|
+
const exc = (data.exceptions || []).find(e => {
|
|
395
|
+
if (e.hook !== '$hook_name') return false;
|
|
396
|
+
const pat = e.pattern.replace(/\\/g, '/').replace(/\./g, '\\\\.').replace(/\*\*/g, '<<<D>>>').replace(/\*/g, '[^/]*').replace(/<<<D>>>/g, '.*');
|
|
397
|
+
return new RegExp('^' + pat + '$').test('$file_path'.replace(/\\\\/g, '/'));
|
|
398
|
+
});
|
|
399
|
+
if (exc) { console.log('EXCEPTED: ' + exc.reason); process.exit(0); }
|
|
400
|
+
process.exit(1);
|
|
401
|
+
" 2>/dev/null
|
|
402
|
+
}
|
|
403
|
+
echo "🔍 Scanning for production code anti-patterns..."
|
|
404
|
+
for file in $SOURCE_FILES; do
|
|
405
|
+
if echo "$file" | grep -vqE '(config|settings|\.env)'; then
|
|
406
|
+
if grep -nE '(localhost|127\.0\.0\.1|0\.0\.0\.0)' "$file" | grep -vE '(#|//|""")' > /tmp/violations 2>/dev/null; then
|
|
407
|
+
if [ -s /tmp/violations ]; then
|
|
408
|
+
echo " ❌ $file — hardcoded URL/host"
|
|
409
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
410
|
+
fi
|
|
411
|
+
fi
|
|
412
|
+
fi
|
|
413
|
+
if ! is_excepted "anti-pattern/mock-data" "$file"; then
|
|
414
|
+
if grep -nEi '\b(mock_data|fake_data|dummy_data|stub_response)' "$file" > /tmp/violations 2>/dev/null; then
|
|
415
|
+
if [ -s /tmp/violations ]; then
|
|
416
|
+
echo " ❌ $file — mock/stub data in production code"
|
|
417
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
418
|
+
fi
|
|
419
|
+
fi
|
|
420
|
+
fi
|
|
421
|
+
# Layer boundary: no direct DB/ORM imports from route handlers / controllers
|
|
422
|
+
if echo "$file" | grep -qE '(routes|controllers|handlers|endpoints)'; then
|
|
423
|
+
if ! is_excepted "layer-boundary" "$file"; then
|
|
424
|
+
if grep -nE '\b(prisma\.|knex\(|mongoose\.|sequelize\.|db\.query|pool\.query)' "$file" > /tmp/violations 2>/dev/null; then
|
|
425
|
+
if [ -s /tmp/violations ]; then
|
|
426
|
+
echo " ❌ $file — direct DB call in route/controller (layer violation)"
|
|
427
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
428
|
+
fi
|
|
429
|
+
fi
|
|
430
|
+
fi
|
|
431
|
+
fi
|
|
432
|
+
# Bare Error throws in business logic (not test files)
|
|
433
|
+
if ! is_excepted "error-hierarchy" "$file"; then
|
|
434
|
+
if grep -nE 'throw new Error\(' "$file" > /tmp/violations 2>/dev/null; then
|
|
435
|
+
if [ -s /tmp/violations ]; then
|
|
436
|
+
echo " ⚠️ $file — bare 'throw new Error()' found — use custom error hierarchy"
|
|
437
|
+
WARNINGS=$((WARNINGS + 1))
|
|
438
|
+
fi
|
|
439
|
+
fi
|
|
440
|
+
fi
|
|
441
|
+
LINE_COUNT=$(wc -l < "$file")
|
|
442
|
+
if [ "$LINE_COUNT" -gt {{max_file_length | default: 300}} ]; then
|
|
443
|
+
echo " ⚠️ $file — $LINE_COUNT lines (max {{max_file_length | default: 300}})"
|
|
444
|
+
WARNINGS=$((WARNINGS + 1))
|
|
445
|
+
fi
|
|
446
|
+
done
|
|
447
|
+
rm -f /tmp/violations
|
|
448
|
+
if [ $VIOLATIONS -gt 0 ]; then
|
|
449
|
+
echo "❌ $VIOLATIONS violation(s) found — commit blocked."
|
|
450
|
+
exit 1
|
|
451
|
+
fi
|
|
452
|
+
if [ $WARNINGS -gt 0 ]; then
|
|
453
|
+
echo "⚠️ $WARNINGS warning(s) found — review recommended."
|
|
454
|
+
fi
|
|
455
|
+
echo "🔍 Production quality scan passed"
|
|
456
|
+
|
|
457
|
+
- name: function-length
|
|
458
|
+
trigger: pre-commit
|
|
459
|
+
description: "Warn when staged functions exceed the configured maximum line count"
|
|
460
|
+
filename: pre-commit-function-length.sh
|
|
461
|
+
script: |
|
|
462
|
+
#!/bin/bash
|
|
463
|
+
MAX_LENGTH={{max_function_length | default: 50}}
|
|
464
|
+
STAGED=$(git diff --cached --name-only --diff-filter=ACM)
|
|
465
|
+
SOURCE_FILES=$(echo "$STAGED" | grep -E '\.(ts|tsx|js|jsx)$' | grep -vE '(\.test\.|\.spec\.|__tests__|tests/)')
|
|
466
|
+
if [ -z "$SOURCE_FILES" ]; then exit 0; fi
|
|
467
|
+
WARNINGS=0
|
|
468
|
+
for file in $SOURCE_FILES; do
|
|
469
|
+
# Heuristic: find function/method declarations and count lines to next declaration or closing brace
|
|
470
|
+
awk -v max="$MAX_LENGTH" -v fname="$file" '
|
|
471
|
+
/^[[:space:]]*(export )?(async )?(function |const [a-zA-Z]+ = (async )?\(|[a-zA-Z]+\(.*\) \{|[a-zA-Z]+\(.*\): )/ {
|
|
472
|
+
if (start > 0 && NR - start > max) {
|
|
473
|
+
printf " ⚠️ %s:%d — function starting here is %d lines (max %d)\n", fname, start, NR - start, max
|
|
474
|
+
warnings++
|
|
475
|
+
}
|
|
476
|
+
start = NR
|
|
477
|
+
}
|
|
478
|
+
END {
|
|
479
|
+
if (start > 0 && NR - start > max) {
|
|
480
|
+
printf " ⚠️ %s:%d — function starting here is %d lines (max %d)\n", fname, start, NR - start, max
|
|
481
|
+
warnings++
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
' "$file"
|
|
485
|
+
WARNINGS=$((WARNINGS + $?))
|
|
486
|
+
done
|
|
487
|
+
# Warning only — does not block commit since bash heuristics aren't perfect
|
|
488
|
+
exit 0
|
|
489
|
+
|
|
490
|
+
- name: import-cycle-detector
|
|
491
|
+
trigger: pre-commit
|
|
492
|
+
description: "Detect circular import dependencies in TypeScript and Python projects"
|
|
493
|
+
filename: pre-commit-import-cycles.sh
|
|
494
|
+
script: |
|
|
495
|
+
#!/bin/bash
|
|
496
|
+
echo "🔄 Checking for circular imports..."
|
|
497
|
+
if [ -f "tsconfig.json" ]; then
|
|
498
|
+
if command -v npx &> /dev/null; then
|
|
499
|
+
RESULT=$(npx --yes madge --circular --extensions ts src/ 2>&1)
|
|
500
|
+
if echo "$RESULT" | grep -q "Found.*circular"; then
|
|
501
|
+
echo "❌ Circular imports detected in TypeScript:"
|
|
502
|
+
echo "$RESULT"
|
|
503
|
+
exit 1
|
|
504
|
+
fi
|
|
505
|
+
echo " ✅ No circular imports (TypeScript)"
|
|
506
|
+
fi
|
|
507
|
+
fi
|
|
508
|
+
if [ -f "pyproject.toml" ] || [ -f "setup.py" ]; then
|
|
509
|
+
if command -v lint-imports &> /dev/null; then
|
|
510
|
+
lint-imports 2>&1
|
|
511
|
+
if [ $? -ne 0 ]; then
|
|
512
|
+
echo "❌ Circular imports detected in Python"
|
|
513
|
+
exit 1
|
|
514
|
+
fi
|
|
515
|
+
echo " ✅ No circular imports (Python)"
|
|
516
|
+
fi
|
|
517
|
+
fi
|
|
518
|
+
echo "🔄 Import cycle check passed"
|
|
519
|
+
|
|
520
|
+
- name: code-review
|
|
521
|
+
trigger: pre-commit
|
|
522
|
+
description: "AI assistant reviews diff against project standards"
|
|
523
|
+
filename: pre-commit-review.sh
|
|
524
|
+
script: |
|
|
525
|
+
#!/bin/bash
|
|
526
|
+
DIFF=$(git diff --cached)
|
|
527
|
+
if [ -z "$DIFF" ]; then exit 0; fi
|
|
528
|
+
echo "📝 Staged changes ready for review"
|
|
529
|
+
# Full auto-review requires claude CLI integration
|
|
530
|
+
# This hook validates the diff is non-empty and staged
|
|
531
|
+
exit 0
|