@boyingliu01/xp-gate 0.31.0 → 0.31.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/adapter-common.sh +41 -3
- package/adapters/gate-3.sh +8 -1
- package/adapters/gate-8.sh +67 -6
- package/adapters/go.sh +18 -1
- package/adapters/iac.sh +18 -1
- package/adapters/powershell.sh +110 -7
- package/adapters/python.sh +18 -4
- package/gate-3.sh +10 -1
- package/gate-8.sh +67 -6
- package/hooks/adapter-common.sh +41 -3
- package/hooks/gate-3.sh +10 -1
- package/hooks/gate-8.sh +67 -6
- package/hooks/lib/coverage-total.sh +20 -5
- package/hooks/lib/gate-5c-alignment.sh +145 -0
- package/hooks/lib/jscpd-run.sh +40 -1
- package/hooks/lib/native-path.sh +106 -0
- package/hooks/lib/prepush-score.py +143 -0
- package/hooks/lib/sprint-gate-report.sh +23 -0
- package/hooks/lib/toolchain-path.sh +370 -0
- package/hooks/lib/validate-code-walkthrough.cjs +224 -23
- package/hooks/pre-commit +371 -163
- package/hooks/pre-push +192 -74
- package/hooks/sprint-gate.sh +14 -3
- package/lib/__tests__/check-alignment-cli.test.mjs +20 -3
- package/lib/__tests__/check-alignment-reasons.test.mjs +91 -0
- package/lib/__tests__/doctor-delphi.test.js +46 -0
- package/lib/__tests__/doctor-exec-retry.test.js +20 -10
- package/lib/__tests__/doctor-hook-signatures.test.js +65 -0
- package/lib/__tests__/doctor-path-equivalence.test.js +12 -1
- package/lib/__tests__/doctor.test.js +7 -1
- package/lib/__tests__/hook-health-path.test.js +9 -1
- package/lib/__tests__/issue-566-sync-hooks-backup.test.js +221 -0
- package/lib/__tests__/issue-583-hook-deploy-log.test.js +121 -0
- package/lib/__tests__/issue-586-principles-cmdline.test.js +183 -0
- package/lib/__tests__/issue-589-check-all-gate3.test.js +190 -0
- package/lib/__tests__/issue-590-archlint-vacuous-pass.test.js +138 -0
- package/lib/__tests__/shared-paths-lazy-home.test.js +67 -0
- package/lib/arch.js +261 -44
- package/lib/check-alignment-runner.cjs +23 -0
- package/lib/check.js +4 -1
- package/lib/doctor.js +194 -49
- package/lib/gate-runner.js +265 -58
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +228 -6
- package/lib/shared-paths.js +53 -19
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +50 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/delphi-review/SKILL.md +50 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/delphi-config-env-diagnostics.test.ts +210 -0
- package/plugins/dsh/src/delphi-config.ts +151 -9
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +50 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/delphi-review/SKILL.md +50 -0
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/issue-587-java-member-count.test.ts +279 -0
- package/principles/__tests__/issue-588-lsp-java-false-positives.test.ts +213 -0
- package/principles/adapters/java.ts +127 -11
- package/principles/rules/__tests__/solid/issue-588-lsp-base-contract.test.ts +252 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +26 -22
- package/principles/rules/solid/lsp.ts +348 -23
- package/scripts/__tests__/sync-package-content.test.js +32 -1
- package/scripts/sync-package-content.js +6 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +50 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/sprint-gate.sh +14 -3
package/adapter-common.sh
CHANGED
|
@@ -155,19 +155,57 @@ require_tool() {
|
|
|
155
155
|
local tool_name="$1"
|
|
156
156
|
local gate_name="${2:-Gate}"
|
|
157
157
|
local install_hint="${3:-}"
|
|
158
|
-
|
|
158
|
+
|
|
159
|
+
# #593: on Windows the hook runs under git-bash, which does not see the
|
|
160
|
+
# toolchains installed on the Windows side (%APPDATA%\npm, the node dir,
|
|
161
|
+
# pip --user Scripts, ~/bin). Bootstrapping before probing turns "not on this
|
|
162
|
+
# shell's PATH" back into "found", instead of reporting an installed tool as
|
|
163
|
+
# missing. lib/toolchain-path.sh is sourced by pre-commit before this runs; the
|
|
164
|
+
# guard keeps require_tool usable from adapters that load on their own.
|
|
165
|
+
if command -v bootstrap_toolchain_path >/dev/null 2>&1; then
|
|
166
|
+
bootstrap_toolchain_path
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# #580: on WSL (appendWindowsPath=false) and in git-bash, `command -v node`
|
|
170
|
+
# returns 1 while node.exe sits in the same directory and runs. One root cause
|
|
171
|
+
# gave Gate 1 a silent SKIP, Gate 1 a false "Biome check failed" and Gate 8 a
|
|
172
|
+
# false "gitleaks not installed" — an environment fault reported as a code
|
|
173
|
+
# verdict in all three. tool_is_available also counts the .exe suffix and the
|
|
174
|
+
# XP_GATE_<TOOL>_BIN override.
|
|
175
|
+
if command -v tool_is_available >/dev/null 2>&1 && tool_is_available "$tool_name"; then
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
159
179
|
if command -v "$tool_name" >/dev/null 2>&1; then
|
|
160
180
|
return 0
|
|
161
181
|
fi
|
|
162
|
-
|
|
182
|
+
|
|
183
|
+
if command -v "${tool_name}.exe" >/dev/null 2>&1; then
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
|
|
163
187
|
if command -v npx >/dev/null 2>&1 && npx --no-install "$tool_name" --version >/dev/null 2>&1; then
|
|
164
188
|
return 0
|
|
165
189
|
fi
|
|
166
|
-
|
|
190
|
+
|
|
191
|
+
# #593: monorepos keep the tool in a subproject's node_modules/.bin, where
|
|
192
|
+
# neither PATH nor `npx` from the repo root can see it.
|
|
193
|
+
if command -v resolve_tool_path >/dev/null 2>&1 && resolve_tool_path "$tool_name" >/dev/null 2>&1; then
|
|
194
|
+
if command -v ensure_tool_dir_on_path >/dev/null 2>&1; then
|
|
195
|
+
ensure_tool_dir_on_path "$tool_name" || true
|
|
196
|
+
fi
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
167
200
|
echo "❌ BLOCKED - Required tool '$tool_name' not available for $gate_name"
|
|
168
201
|
if [ -n "$install_hint" ]; then
|
|
169
202
|
echo " Install: $install_hint"
|
|
170
203
|
fi
|
|
204
|
+
# #593 point 3: "not installed" and "installed but not found" need different
|
|
205
|
+
# fixes, so name every location that was searched.
|
|
206
|
+
if command -v describe_tool_search >/dev/null 2>&1; then
|
|
207
|
+
describe_tool_search "$tool_name"
|
|
208
|
+
fi
|
|
171
209
|
echo " Per QUALITY-GATES-CODE-OF-CONDUCT.md: tool unavailable = BLOCK, not SKIP"
|
|
172
210
|
return 1
|
|
173
211
|
}
|
package/adapters/gate-3.sh
CHANGED
|
@@ -109,7 +109,14 @@ else
|
|
|
109
109
|
GATE_3_STATUS="PASS"
|
|
110
110
|
fi
|
|
111
111
|
else
|
|
112
|
-
|
|
112
|
+
# #589: "no source files to check" described an empty PROJECT when the real
|
|
113
|
+
# cause is an empty CHANGED_FILES. The old wording read like a pass.
|
|
114
|
+
if [ -n "${GATE_SCOPE:-}" ] && [ "$GATE_SCOPE" = "all" ]; then
|
|
115
|
+
echo "⏭️ SKIPPED - Complexity: no tracked source files in this repository (scope: --all, whole tree)."
|
|
116
|
+
else
|
|
117
|
+
echo "⏭️ SKIPPED - Complexity: no changed source files in this run (scope: diff)."
|
|
118
|
+
echo " Run 'xp-gate check . --all' to scan the whole tree."
|
|
119
|
+
fi
|
|
113
120
|
GATE_3_STATUS="SKIP"
|
|
114
121
|
GATE_3_REASON="no_source_files_changed"
|
|
115
122
|
fi
|
package/adapters/gate-8.sh
CHANGED
|
@@ -33,10 +33,23 @@ fi
|
|
|
33
33
|
|
|
34
34
|
GITLEAKS_CONFIG=""
|
|
35
35
|
if [ -f ".gitleaks.toml" ]; then
|
|
36
|
-
# gitleaks is a native binary and may not understand MSYS paths, so pass an
|
|
36
|
+
# gitleaks is a native binary and may not understand MSYS/WSL paths, so pass an
|
|
37
37
|
# absolute path in the host's own format when we can derive one (#449).
|
|
38
|
-
|
|
38
|
+
#
|
|
39
|
+
# #582 found this the hard way: `cygpath` exists only in Git-for-Windows/MSYS, and on
|
|
40
|
+
# WSL its absence meant the fallback handed `/mnt/d/projects/<repo>/.gitleaks.toml`
|
|
41
|
+
# to the native gitleaks.exe. It could not open the file, exited 1, and Gate 8
|
|
42
|
+
# reported "Secrets detected". The conversion now lives in a shared library so the
|
|
43
|
+
# next gate handing a path to a native binary does not re-derive it (#581).
|
|
44
|
+
#
|
|
45
|
+
# This script may also run standalone (archived/copied hook) without the library, so
|
|
46
|
+
# the converter is used when present and the inline branch kept as the fallback.
|
|
47
|
+
if declare -F to_native_path >/dev/null 2>&1; then
|
|
48
|
+
GITLEAKS_CONFIG="--config=$(to_native_path "$(pwd)/.gitleaks.toml")"
|
|
49
|
+
elif command -v cygpath >/dev/null 2>&1; then
|
|
39
50
|
GITLEAKS_CONFIG="--config=$(cygpath -w "$(pwd)/.gitleaks.toml")"
|
|
51
|
+
elif command -v wslpath >/dev/null 2>&1; then
|
|
52
|
+
GITLEAKS_CONFIG="--config=$(wslpath -w "$(pwd)/.gitleaks.toml")"
|
|
40
53
|
else
|
|
41
54
|
GITLEAKS_CONFIG="--config=$(pwd)/.gitleaks.toml"
|
|
42
55
|
fi
|
|
@@ -75,10 +88,41 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
75
88
|
echo " ✅ PASSED - No secrets detected."
|
|
76
89
|
GATE_8_STATUS="PASS"
|
|
77
90
|
elif [ "$GITLEAKS_FINDINGS" = "?" ]; then
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
# #592: exit 0 with no usable report has TWO very different causes, and the
|
|
92
|
+
# operator can only act if we tell them which one happened:
|
|
93
|
+
#
|
|
94
|
+
# (a) the scan never ran — config unreadable, FTL, bad --report-path.
|
|
95
|
+
# Nothing was checked, so this must fail closed with the tool's own
|
|
96
|
+
# words, not be filed as "no evidence about the code".
|
|
97
|
+
# (b) the scan ran and found nothing, but the tool wrote no file (reported
|
|
98
|
+
# for gitleaks 8.30.1 on WSL; measured NOT to happen in git-bash with
|
|
99
|
+
# the same version, so the code must handle both). Only stdout is left,
|
|
100
|
+
# and stdout is weaker evidence than an artifact, so it still may not
|
|
101
|
+
# produce a PASS.
|
|
102
|
+
#
|
|
103
|
+
# The order matters and is the point: artifact first, tool failure second,
|
|
104
|
+
# tool output last (#449 established the principle; #592 re-opens it).
|
|
105
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load'; then
|
|
106
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 0 but reported a failure, so"
|
|
107
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
108
|
+
echo " about the staged code."
|
|
109
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable' | head -3 | sed 's/^/ /'
|
|
110
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -q 'no leaks found'; then
|
|
115
|
+
# The scan demonstrably ran and found nothing, but there is no artifact to
|
|
116
|
+
# prove it beyond this line. Reporting PASS from log wording is exactly what
|
|
117
|
+
# #449 forbade, so this stays SKIP — and says WHY, which the old message did
|
|
118
|
+
# not: without that, SKIP is indistinguishable from "tool missing".
|
|
119
|
+
echo " ⚠️ gitleaks scanned and reported no leaks, but wrote no report file"
|
|
120
|
+
echo " (${GITLEAKS_REPORT} missing). No artifact means no PASS (#449)."
|
|
121
|
+
echo " ⏭️ SKIPPED - secret scanning produced no artifact (scan reported clean)."
|
|
122
|
+
else
|
|
123
|
+
echo " ⚠️ gitleaks exited 0 but produced no readable report and no scan summary"
|
|
124
|
+
echo " ⏭️ SKIPPED - secret scanning produced no evidence (not a pass)"
|
|
125
|
+
fi
|
|
82
126
|
GATE_8_STATUS="SKIP"
|
|
83
127
|
else
|
|
84
128
|
echo " ❌ BLOCKED - gitleaks reported $GITLEAKS_FINDINGS finding(s)"
|
|
@@ -88,6 +132,23 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
88
132
|
exit 1
|
|
89
133
|
fi
|
|
90
134
|
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
135
|
+
# #582: gitleaks exits 1 for BOTH "leaks found" and "I could not start" (unreadable
|
|
136
|
+
# config, bad path, IO error). Treating every exit 1 as a finding produced the
|
|
137
|
+
# loudest possible wrong answer: "Secrets detected in staged files", plus
|
|
138
|
+
# instructions to rotate a key, on a repository where nothing had been scanned --
|
|
139
|
+
# because WSL handed a POSIX path to the native binary (#582's own case).
|
|
140
|
+
#
|
|
141
|
+
# #592 established this distinction for exit 0 + FTL. The same test applies here,
|
|
142
|
+
# and the order matters as much: a scanner that could not start must never be
|
|
143
|
+
# reported as having found something, or the gate teaches people to disable it.
|
|
144
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load|cannot find the path'; then
|
|
145
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 1 because it could not start, so"
|
|
146
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
147
|
+
echo " about the staged code."
|
|
148
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable|cannot find' | head -3 | sed 's/^/ /'
|
|
149
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
91
152
|
# Secrets found — output details
|
|
92
153
|
echo "$GITLEAKS_OUTPUT"
|
|
93
154
|
echo ""
|
package/adapters/go.sh
CHANGED
|
@@ -4,7 +4,24 @@
|
|
|
4
4
|
# Tools: go vet, golangci-lint, arch-go (architecture), go test
|
|
5
5
|
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
|
|
7
|
+
# #597: the library can be beside the adapters or one level up, depending on how
|
|
8
|
+
# the package was installed:
|
|
9
|
+
# global install ~/.config/xp-gate/adapters/adapter-common.sh (inside)
|
|
10
|
+
# npm package src/npm-package/adapters/../adapter-common.sh (above)
|
|
11
|
+
# The old single "$SCRIPT_DIR/../adapter-common.sh" only ever matched the second,
|
|
12
|
+
# so in the global layout the library never loaded - and "2>/dev/null || true"
|
|
13
|
+
# made that indistinguishable from success, leaving every shared function
|
|
14
|
+
# undefined with no warning.
|
|
15
|
+
for _ac_candidate in \
|
|
16
|
+
"$SCRIPT_DIR/adapter-common.sh" \
|
|
17
|
+
"$SCRIPT_DIR/../adapter-common.sh"; do
|
|
18
|
+
if [ -f "$_ac_candidate" ]; then
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "$_ac_candidate"
|
|
21
|
+
break
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
unset _ac_candidate
|
|
8
25
|
|
|
9
26
|
run_static_analysis() {
|
|
10
27
|
require_tool go "Go SDK" || return 1
|
package/adapters/iac.sh
CHANGED
|
@@ -5,7 +5,24 @@
|
|
|
5
5
|
# Tools: checkov (primary), hadolint (Docker), kube-score (K8s)
|
|
6
6
|
|
|
7
7
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
-
|
|
8
|
+
# #597: the library can be beside the adapters or one level up, depending on how
|
|
9
|
+
# the package was installed:
|
|
10
|
+
# global install ~/.config/xp-gate/adapters/adapter-common.sh (inside)
|
|
11
|
+
# npm package src/npm-package/adapters/../adapter-common.sh (above)
|
|
12
|
+
# The old single "$SCRIPT_DIR/../adapter-common.sh" only ever matched the second,
|
|
13
|
+
# so in the global layout the library never loaded - and "2>/dev/null || true"
|
|
14
|
+
# made that indistinguishable from success, leaving every shared function
|
|
15
|
+
# undefined with no warning.
|
|
16
|
+
for _ac_candidate in \
|
|
17
|
+
"$SCRIPT_DIR/adapter-common.sh" \
|
|
18
|
+
"$SCRIPT_DIR/../adapter-common.sh"; do
|
|
19
|
+
if [ -f "$_ac_candidate" ]; then
|
|
20
|
+
# shellcheck source=/dev/null
|
|
21
|
+
source "$_ac_candidate"
|
|
22
|
+
break
|
|
23
|
+
fi
|
|
24
|
+
done
|
|
25
|
+
unset _ac_candidate
|
|
9
26
|
|
|
10
27
|
# Detect IaC files in changed files
|
|
11
28
|
detect_iac_files() {
|
package/adapters/powershell.sh
CHANGED
|
@@ -81,6 +81,11 @@ _find_powershell_files() {
|
|
|
81
81
|
local entry
|
|
82
82
|
while IFS= read -r entry; do
|
|
83
83
|
entry="${entry%$'\r'}"
|
|
84
|
+
# #578: strip a UTF-8 BOM before anything else. PowerShell 5.1's
|
|
85
|
+
# `Set-Content -Encoding UTF8` writes one, and it glues itself to the first
|
|
86
|
+
# line -- so the first directory name would silently never match, and a
|
|
87
|
+
# leading '#' comment would stop looking like a comment.
|
|
88
|
+
entry="${entry#$'\xEF\xBB\xBF'}"
|
|
84
89
|
# Strip surrounding whitespace, then the ./ prefix and trailing / a repo
|
|
85
90
|
# may reasonably write; -name matches a bare directory name.
|
|
86
91
|
entry="${entry#"${entry%%[![:space:]]*}"}"
|
|
@@ -89,6 +94,30 @@ _find_powershell_files() {
|
|
|
89
94
|
entry="${entry#./}"
|
|
90
95
|
entry="${entry%/}"
|
|
91
96
|
[ -n "$entry" ] || continue
|
|
97
|
+
|
|
98
|
+
# #574: discovery feeds THREE denominators at once (test discovery,
|
|
99
|
+
# coverage, static-analysis scope), so a bad entry does not merely fail to
|
|
100
|
+
# prune -- it silently shrinks all three while the file still looks
|
|
101
|
+
# reasonable.
|
|
102
|
+
# -name '*' matches EVERY directory -> prunes the whole tree -> the
|
|
103
|
+
# gate analyses nothing and still exits 0.
|
|
104
|
+
# -name 'a/b' matches nothing (names have no separators) -> a silent
|
|
105
|
+
# no-op that reads as working config.
|
|
106
|
+
# Only bare directory NAMES are honoured; anything else is reported and
|
|
107
|
+
# ignored. Reported, not silently dropped: an ignored entry is a config
|
|
108
|
+
# error the operator has to see.
|
|
109
|
+
case "$entry" in
|
|
110
|
+
*/*|*\\*|*'*'*|*'?'*|*'['*)
|
|
111
|
+
echo " ⚠️ $prune_file: ignoring invalid entry '$entry' (must be a bare directory name, no separators or glob metacharacters)" >&2
|
|
112
|
+
continue
|
|
113
|
+
;;
|
|
114
|
+
.|..)
|
|
115
|
+
echo " ⚠️ $prune_file: ignoring invalid entry '$entry'" >&2
|
|
116
|
+
continue
|
|
117
|
+
;;
|
|
118
|
+
esac
|
|
119
|
+
|
|
120
|
+
echo " ℹ️ pruning directory name '$entry' (from $prune_file)" >&2
|
|
92
121
|
prune_args[${#prune_args[@]}]=-o
|
|
93
122
|
prune_args[${#prune_args[@]}]=-name
|
|
94
123
|
prune_args[${#prune_args[@]}]="$entry"
|
|
@@ -109,6 +138,10 @@ _find_powershell_files() {
|
|
|
109
138
|
-name .workbuddy -o \
|
|
110
139
|
-name .sprint-state -o \
|
|
111
140
|
-name wrc-drill -o \
|
|
141
|
+
-name .xp-gate -o \
|
|
142
|
+
-name .vscode -o \
|
|
143
|
+
-name .idea -o \
|
|
144
|
+
-name .qoder -o \
|
|
112
145
|
-path './plugins/claude-code/skills' -o \
|
|
113
146
|
-path './plugins/opencode/skills' -o \
|
|
114
147
|
-path './plugins/qoder/skills' -o \
|
|
@@ -155,6 +188,7 @@ run_static_analysis() {
|
|
|
155
188
|
local PSSA_OUT
|
|
156
189
|
local scope_file=".xp-gate-powershell-analysis-paths"
|
|
157
190
|
local analysis_targets=()
|
|
191
|
+
local scope_missing=0
|
|
158
192
|
|
|
159
193
|
# #571: repo-local analysis scope. Pester test files legitimately trip
|
|
160
194
|
# dozens of rule warnings under the Pester execution model, so a whole-repo
|
|
@@ -175,6 +209,12 @@ run_static_analysis() {
|
|
|
175
209
|
local declared=0
|
|
176
210
|
while IFS= read -r target_line; do
|
|
177
211
|
target_line="${target_line%$'\r'}"
|
|
212
|
+
# #578: a UTF-8 BOM (PowerShell 5.1's `Set-Content -Encoding UTF8` writes
|
|
213
|
+
# one) glues itself to the FIRST line. On a path that manufactures a
|
|
214
|
+
# nonexistent entry -- and since #576 that returns 3, so a perfectly valid
|
|
215
|
+
# scope file would fail closed on every commit. On the '#' header it turns
|
|
216
|
+
# a comment into a phantom path. Strip it before classifying.
|
|
217
|
+
target_line="${target_line#$'\xEF\xBB\xBF'}"
|
|
178
218
|
# Strip surrounding whitespace before classifying: a line of spaces is
|
|
179
219
|
# not a path, and `case` alone would treat it as one — measured, a
|
|
180
220
|
# whitespace-only line became an "analysis path not found" entry.
|
|
@@ -186,6 +226,7 @@ run_static_analysis() {
|
|
|
186
226
|
# shellcheck disable=SC2004 # explicit index arithmetic on an array
|
|
187
227
|
analysis_targets[${#analysis_targets[@]}]="$target_line"
|
|
188
228
|
else
|
|
229
|
+
scope_missing=$((scope_missing + 1))
|
|
189
230
|
echo " ℹ️ analysis path not found (skipped): $target_line"
|
|
190
231
|
echo "XP_G1_REASON: analysis-path-missing:$target_line"
|
|
191
232
|
fi
|
|
@@ -218,13 +259,18 @@ run_static_analysis() {
|
|
|
218
259
|
try { Import-Module PSScriptAnalyzer -ErrorAction Stop | Out-Null } catch { exit 3 }
|
|
219
260
|
\$settings = Join-Path (Get-Location) 'PSScriptAnalyzerSettings.psd1'
|
|
220
261
|
\$all = @()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
262
|
+
try {
|
|
263
|
+
foreach (\$p in $target_array) {
|
|
264
|
+
\$recurse = [System.IO.Directory]::Exists(\$p)
|
|
265
|
+
if (Test-Path -LiteralPath \$settings) {
|
|
266
|
+
\$all += @(Invoke-ScriptAnalyzer -Path \$p -Recurse:\$recurse -Severity Error,Warning -Settings \$settings -ErrorAction Stop)
|
|
267
|
+
} else {
|
|
268
|
+
\$all += @(Invoke-ScriptAnalyzer -Path \$p -Recurse:\$recurse -Severity Error,Warning -ErrorAction Stop)
|
|
269
|
+
}
|
|
227
270
|
}
|
|
271
|
+
} catch {
|
|
272
|
+
Write-Output ('PSSA_RUN_ERROR: ' + \$_.Exception.Message)
|
|
273
|
+
exit 3
|
|
228
274
|
}
|
|
229
275
|
if (\$all) {
|
|
230
276
|
\$all | Format-Table -AutoSize
|
|
@@ -233,12 +279,48 @@ run_static_analysis() {
|
|
|
233
279
|
exit 0
|
|
234
280
|
" 2>&1) || PSSA_RC=$?
|
|
235
281
|
else
|
|
282
|
+
# #595: the unscoped path used to be a bare `-Path . -Recurse`, which walks
|
|
283
|
+
# node_modules. In a repo with Playwright installed that analyses
|
|
284
|
+
# playwright-core/bin/*.ps1 -- vendored files that use Write-Host and WMI
|
|
285
|
+
# cmdlets by design, that `npm ci` overwrites, and that the author cannot
|
|
286
|
+
# edit. Gate 1 then blocked EVERY commit, permanently. A gate that can never
|
|
287
|
+
# pass is not strict; it is one people learn to bypass.
|
|
288
|
+
#
|
|
289
|
+
# The adapter already had the right discovery: _find_powershell_files prunes
|
|
290
|
+
# .git, node_modules, dist, coverage and the rest. The two paths disagreed
|
|
291
|
+
# only because this one never called it. Now both use it, so "what this
|
|
292
|
+
# adapter refuses to analyse" has one definition.
|
|
293
|
+
local legacy_files
|
|
294
|
+
legacy_files=$(_find_powershell_files '*.ps1')
|
|
295
|
+
if [ -z "$legacy_files" ]; then
|
|
296
|
+
# Nothing to analyse is NOT a pass: say so via the reason protocol the
|
|
297
|
+
# module-missing branch uses, so it stays visible in the gate output.
|
|
298
|
+
echo "XP_G1_REASON: no PowerShell files found outside the pruned directories"
|
|
299
|
+
return 3
|
|
300
|
+
fi
|
|
301
|
+
local legacy_array
|
|
302
|
+
legacy_array=$(_powershell_path_array $legacy_files)
|
|
236
303
|
PSSA_OUT=$("$PWSH" "${POWERSHELL_ARGS[@]}" "
|
|
237
304
|
if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) {
|
|
238
305
|
Write-Output 'PSSA_MODULE_MISSING'
|
|
239
306
|
exit 0
|
|
240
307
|
}
|
|
241
|
-
\$
|
|
308
|
+
\$ErrorActionPreference = 'Stop'
|
|
309
|
+
try { Import-Module PSScriptAnalyzer -ErrorAction Stop | Out-Null } catch { exit 3 }
|
|
310
|
+
\$settings = Join-Path (Get-Location) 'PSScriptAnalyzerSettings.psd1'
|
|
311
|
+
\$results = @()
|
|
312
|
+
try {
|
|
313
|
+
foreach (\$p in $legacy_array) {
|
|
314
|
+
if (Test-Path -LiteralPath \$settings) {
|
|
315
|
+
\$results += @(Invoke-ScriptAnalyzer -Path \$p -Severity Error,Warning -Settings \$settings -ErrorAction Stop)
|
|
316
|
+
} else {
|
|
317
|
+
\$results += @(Invoke-ScriptAnalyzer -Path \$p -Severity Error,Warning -ErrorAction Stop)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
Write-Output ('PSSA_RUN_ERROR: ' + \$_.Exception.Message)
|
|
322
|
+
exit 3
|
|
323
|
+
}
|
|
242
324
|
if (\$results) {
|
|
243
325
|
\$results | Format-Table -AutoSize
|
|
244
326
|
exit 1
|
|
@@ -258,11 +340,32 @@ run_static_analysis() {
|
|
|
258
340
|
echo "XP_G1_REASON: PSScriptAnalyzer module not installed (Install-Module PSScriptAnalyzer)"
|
|
259
341
|
return 3
|
|
260
342
|
fi
|
|
343
|
+
# #576: a scope entry that vanished means product code may have ESCAPED
|
|
344
|
+
# the gate. The reason line was emitted above, but pre-commit switches on
|
|
345
|
+
# the RETURN CODE and only uses the reason for the message text -- so
|
|
346
|
+
# returning 0 here made the reason dead weight and Gate 1 printed
|
|
347
|
+
# PASSED. Measured before this fix: "RETURN CODE = 0". The scan being
|
|
348
|
+
# clean does not make an incomplete scope a pass.
|
|
349
|
+
if [ "$scope_missing" -gt 0 ]; then
|
|
350
|
+
echo " ⚠️ $scope_missing scope entry(ies) did not exist - product code may have escaped the gate"
|
|
351
|
+
return 3
|
|
352
|
+
fi
|
|
261
353
|
return 0
|
|
262
354
|
;;
|
|
263
355
|
1)
|
|
264
356
|
return 1 # real findings -> BLOCK upstream
|
|
265
357
|
;;
|
|
358
|
+
3)
|
|
359
|
+
# #576: the PowerShell fragment exits 3 when the scan itself raised. That
|
|
360
|
+
# is "could not verify", not "found violations" -- pwsh would exit 1 for
|
|
361
|
+
# findings, so mapping this to 1 would report an analysis crash as lint
|
|
362
|
+
# errors. Surface the actual exception where the fragment captured one.
|
|
363
|
+
echo "XP_G1_REASON: PSScriptAnalyzer raised an error during the run"
|
|
364
|
+
printf '%s\n' "$PSSA_OUT" | grep '^PSSA_RUN_ERROR: ' | sed 's/^PSSA_RUN_ERROR: //' | head -3 | while IFS= read -r _ps_err; do
|
|
365
|
+
echo " ⚠️ $_ps_err"
|
|
366
|
+
done
|
|
367
|
+
return 2 # unusable -> fail closed upstream
|
|
368
|
+
;;
|
|
266
369
|
*)
|
|
267
370
|
echo "XP_G1_REASON: PSScriptAnalyzer run failed (exit $PSSA_RC)"
|
|
268
371
|
return 2 # unusable -> fail closed upstream
|
package/adapters/python.sh
CHANGED
|
@@ -4,10 +4,24 @@
|
|
|
4
4
|
# Tools: mypy, ruff/flake8, pytest, import-linter (architecture)
|
|
5
5
|
|
|
6
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
7
|
+
# #597: the library can be beside the adapters or one level up, depending on how
|
|
8
|
+
# the package was installed:
|
|
9
|
+
# global install ~/.config/xp-gate/adapters/adapter-common.sh (inside)
|
|
10
|
+
# npm package src/npm-package/adapters/../adapter-common.sh (above)
|
|
11
|
+
# The old single "$SCRIPT_DIR/../adapter-common.sh" only ever matched the second,
|
|
12
|
+
# so in the global layout the library never loaded - and "2>/dev/null || true"
|
|
13
|
+
# made that indistinguishable from success, leaving every shared function
|
|
14
|
+
# undefined with no warning.
|
|
15
|
+
for _ac_candidate in \
|
|
16
|
+
"$SCRIPT_DIR/adapter-common.sh" \
|
|
17
|
+
"$SCRIPT_DIR/../adapter-common.sh"; do
|
|
18
|
+
if [ -f "$_ac_candidate" ]; then
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
source "$_ac_candidate"
|
|
21
|
+
break
|
|
22
|
+
fi
|
|
23
|
+
done
|
|
24
|
+
unset _ac_candidate
|
|
11
25
|
|
|
12
26
|
# A mypy target must exist AND actually contain .py files: a bare directory
|
|
13
27
|
# check lets through TS repos' src/ (mypy exits 2 with "There are no .py[i]
|
package/gate-3.sh
CHANGED
|
@@ -233,7 +233,16 @@ else
|
|
|
233
233
|
fi
|
|
234
234
|
fi
|
|
235
235
|
else
|
|
236
|
-
|
|
236
|
+
# #589: "no source files to check" described an empty PROJECT when the real
|
|
237
|
+
# cause is an empty CHANGED_FILES — an empty DIFF, or a `--all` run whose
|
|
238
|
+
# tree genuinely has no sources. The old wording read like a pass, which is
|
|
239
|
+
# why the reporter worked around the gate by running lizard by hand.
|
|
240
|
+
if [ -n "${GATE_SCOPE:-}" ] && [ "$GATE_SCOPE" = "all" ]; then
|
|
241
|
+
echo "⏭️ SKIPPED - Complexity: no tracked source files in this repository (scope: --all, whole tree)."
|
|
242
|
+
else
|
|
243
|
+
echo "⏭️ SKIPPED - Complexity: no changed source files in this run (scope: diff)."
|
|
244
|
+
echo " Run 'xp-gate check . --all' to scan the whole tree."
|
|
245
|
+
fi
|
|
237
246
|
GATE_3_STATUS="SKIP"
|
|
238
247
|
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
239
248
|
# shellcheck disable=SC2034
|
package/gate-8.sh
CHANGED
|
@@ -33,10 +33,23 @@ fi
|
|
|
33
33
|
|
|
34
34
|
GITLEAKS_CONFIG=""
|
|
35
35
|
if [ -f ".gitleaks.toml" ]; then
|
|
36
|
-
# gitleaks is a native binary and may not understand MSYS paths, so pass an
|
|
36
|
+
# gitleaks is a native binary and may not understand MSYS/WSL paths, so pass an
|
|
37
37
|
# absolute path in the host's own format when we can derive one (#449).
|
|
38
|
-
|
|
38
|
+
#
|
|
39
|
+
# #582 found this the hard way: `cygpath` exists only in Git-for-Windows/MSYS, and on
|
|
40
|
+
# WSL its absence meant the fallback handed `/mnt/d/projects/<repo>/.gitleaks.toml`
|
|
41
|
+
# to the native gitleaks.exe. It could not open the file, exited 1, and Gate 8
|
|
42
|
+
# reported "Secrets detected". The conversion now lives in a shared library so the
|
|
43
|
+
# next gate handing a path to a native binary does not re-derive it (#581).
|
|
44
|
+
#
|
|
45
|
+
# This script may also run standalone (archived/copied hook) without the library, so
|
|
46
|
+
# the converter is used when present and the inline branch kept as the fallback.
|
|
47
|
+
if declare -F to_native_path >/dev/null 2>&1; then
|
|
48
|
+
GITLEAKS_CONFIG="--config=$(to_native_path "$(pwd)/.gitleaks.toml")"
|
|
49
|
+
elif command -v cygpath >/dev/null 2>&1; then
|
|
39
50
|
GITLEAKS_CONFIG="--config=$(cygpath -w "$(pwd)/.gitleaks.toml")"
|
|
51
|
+
elif command -v wslpath >/dev/null 2>&1; then
|
|
52
|
+
GITLEAKS_CONFIG="--config=$(wslpath -w "$(pwd)/.gitleaks.toml")"
|
|
40
53
|
else
|
|
41
54
|
GITLEAKS_CONFIG="--config=$(pwd)/.gitleaks.toml"
|
|
42
55
|
fi
|
|
@@ -75,10 +88,41 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
75
88
|
echo " ✅ PASSED - No secrets detected."
|
|
76
89
|
GATE_8_STATUS="PASS"
|
|
77
90
|
elif [ "$GITLEAKS_FINDINGS" = "?" ]; then
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
# #592: exit 0 with no usable report has TWO very different causes, and the
|
|
92
|
+
# operator can only act if we tell them which one happened:
|
|
93
|
+
#
|
|
94
|
+
# (a) the scan never ran — config unreadable, FTL, bad --report-path.
|
|
95
|
+
# Nothing was checked, so this must fail closed with the tool's own
|
|
96
|
+
# words, not be filed as "no evidence about the code".
|
|
97
|
+
# (b) the scan ran and found nothing, but the tool wrote no file (reported
|
|
98
|
+
# for gitleaks 8.30.1 on WSL; measured NOT to happen in git-bash with
|
|
99
|
+
# the same version, so the code must handle both). Only stdout is left,
|
|
100
|
+
# and stdout is weaker evidence than an artifact, so it still may not
|
|
101
|
+
# produce a PASS.
|
|
102
|
+
#
|
|
103
|
+
# The order matters and is the point: artifact first, tool failure second,
|
|
104
|
+
# tool output last (#449 established the principle; #592 re-opens it).
|
|
105
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load'; then
|
|
106
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 0 but reported a failure, so"
|
|
107
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
108
|
+
echo " about the staged code."
|
|
109
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable' | head -3 | sed 's/^/ /'
|
|
110
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -q 'no leaks found'; then
|
|
115
|
+
# The scan demonstrably ran and found nothing, but there is no artifact to
|
|
116
|
+
# prove it beyond this line. Reporting PASS from log wording is exactly what
|
|
117
|
+
# #449 forbade, so this stays SKIP — and says WHY, which the old message did
|
|
118
|
+
# not: without that, SKIP is indistinguishable from "tool missing".
|
|
119
|
+
echo " ⚠️ gitleaks scanned and reported no leaks, but wrote no report file"
|
|
120
|
+
echo " (${GITLEAKS_REPORT} missing). No artifact means no PASS (#449)."
|
|
121
|
+
echo " ⏭️ SKIPPED - secret scanning produced no artifact (scan reported clean)."
|
|
122
|
+
else
|
|
123
|
+
echo " ⚠️ gitleaks exited 0 but produced no readable report and no scan summary"
|
|
124
|
+
echo " ⏭️ SKIPPED - secret scanning produced no evidence (not a pass)"
|
|
125
|
+
fi
|
|
82
126
|
GATE_8_STATUS="SKIP"
|
|
83
127
|
else
|
|
84
128
|
echo " ❌ BLOCKED - gitleaks reported $GITLEAKS_FINDINGS finding(s)"
|
|
@@ -88,6 +132,23 @@ if [ "$GITLEAKS_EXIT" -eq 0 ]; then
|
|
|
88
132
|
exit 1
|
|
89
133
|
fi
|
|
90
134
|
elif [ "$GITLEAKS_EXIT" -eq 1 ]; then
|
|
135
|
+
# #582: gitleaks exits 1 for BOTH "leaks found" and "I could not start" (unreadable
|
|
136
|
+
# config, bad path, IO error). Treating every exit 1 as a finding produced the
|
|
137
|
+
# loudest possible wrong answer: "Secrets detected in staged files", plus
|
|
138
|
+
# instructions to rotate a key, on a repository where nothing had been scanned --
|
|
139
|
+
# because WSL handed a POSIX path to the native binary (#582's own case).
|
|
140
|
+
#
|
|
141
|
+
# #592 established this distinction for exit 0 + FTL. The same test applies here,
|
|
142
|
+
# and the order matters as much: a scanner that could not start must never be
|
|
143
|
+
# reported as having found something, or the gate teaches people to disable it.
|
|
144
|
+
if printf '%s\n' "$GITLEAKS_OUTPUT" | grep -qiE '(^|[[:space:]])(FTL|fatal error)[[:space:]:]|unable to load|cannot find the path'; then
|
|
145
|
+
echo " ❌ BLOCKED - ENVIRONMENT: gitleaks exited 1 because it could not start, so"
|
|
146
|
+
echo " NO secret scan took place. This is a tooling problem, not a verdict"
|
|
147
|
+
echo " about the staged code."
|
|
148
|
+
printf '%s\n' "$GITLEAKS_OUTPUT" | grep -iE 'FTL|fatal|unable|cannot find' | head -3 | sed 's/^/ /'
|
|
149
|
+
echo " Fix the gitleaks config/invocation above and commit again."
|
|
150
|
+
exit 1
|
|
151
|
+
fi
|
|
91
152
|
# Secrets found — output details
|
|
92
153
|
echo "$GITLEAKS_OUTPUT"
|
|
93
154
|
echo ""
|
package/hooks/adapter-common.sh
CHANGED
|
@@ -155,19 +155,57 @@ require_tool() {
|
|
|
155
155
|
local tool_name="$1"
|
|
156
156
|
local gate_name="${2:-Gate}"
|
|
157
157
|
local install_hint="${3:-}"
|
|
158
|
-
|
|
158
|
+
|
|
159
|
+
# #593: on Windows the hook runs under git-bash, which does not see the
|
|
160
|
+
# toolchains installed on the Windows side (%APPDATA%\npm, the node dir,
|
|
161
|
+
# pip --user Scripts, ~/bin). Bootstrapping before probing turns "not on this
|
|
162
|
+
# shell's PATH" back into "found", instead of reporting an installed tool as
|
|
163
|
+
# missing. lib/toolchain-path.sh is sourced by pre-commit before this runs; the
|
|
164
|
+
# guard keeps require_tool usable from adapters that load on their own.
|
|
165
|
+
if command -v bootstrap_toolchain_path >/dev/null 2>&1; then
|
|
166
|
+
bootstrap_toolchain_path
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# #580: on WSL (appendWindowsPath=false) and in git-bash, `command -v node`
|
|
170
|
+
# returns 1 while node.exe sits in the same directory and runs. One root cause
|
|
171
|
+
# gave Gate 1 a silent SKIP, Gate 1 a false "Biome check failed" and Gate 8 a
|
|
172
|
+
# false "gitleaks not installed" — an environment fault reported as a code
|
|
173
|
+
# verdict in all three. tool_is_available also counts the .exe suffix and the
|
|
174
|
+
# XP_GATE_<TOOL>_BIN override.
|
|
175
|
+
if command -v tool_is_available >/dev/null 2>&1 && tool_is_available "$tool_name"; then
|
|
176
|
+
return 0
|
|
177
|
+
fi
|
|
178
|
+
|
|
159
179
|
if command -v "$tool_name" >/dev/null 2>&1; then
|
|
160
180
|
return 0
|
|
161
181
|
fi
|
|
162
|
-
|
|
182
|
+
|
|
183
|
+
if command -v "${tool_name}.exe" >/dev/null 2>&1; then
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
|
|
163
187
|
if command -v npx >/dev/null 2>&1 && npx --no-install "$tool_name" --version >/dev/null 2>&1; then
|
|
164
188
|
return 0
|
|
165
189
|
fi
|
|
166
|
-
|
|
190
|
+
|
|
191
|
+
# #593: monorepos keep the tool in a subproject's node_modules/.bin, where
|
|
192
|
+
# neither PATH nor `npx` from the repo root can see it.
|
|
193
|
+
if command -v resolve_tool_path >/dev/null 2>&1 && resolve_tool_path "$tool_name" >/dev/null 2>&1; then
|
|
194
|
+
if command -v ensure_tool_dir_on_path >/dev/null 2>&1; then
|
|
195
|
+
ensure_tool_dir_on_path "$tool_name" || true
|
|
196
|
+
fi
|
|
197
|
+
return 0
|
|
198
|
+
fi
|
|
199
|
+
|
|
167
200
|
echo "❌ BLOCKED - Required tool '$tool_name' not available for $gate_name"
|
|
168
201
|
if [ -n "$install_hint" ]; then
|
|
169
202
|
echo " Install: $install_hint"
|
|
170
203
|
fi
|
|
204
|
+
# #593 point 3: "not installed" and "installed but not found" need different
|
|
205
|
+
# fixes, so name every location that was searched.
|
|
206
|
+
if command -v describe_tool_search >/dev/null 2>&1; then
|
|
207
|
+
describe_tool_search "$tool_name"
|
|
208
|
+
fi
|
|
171
209
|
echo " Per QUALITY-GATES-CODE-OF-CONDUCT.md: tool unavailable = BLOCK, not SKIP"
|
|
172
210
|
return 1
|
|
173
211
|
}
|
package/hooks/gate-3.sh
CHANGED
|
@@ -233,7 +233,16 @@ else
|
|
|
233
233
|
fi
|
|
234
234
|
fi
|
|
235
235
|
else
|
|
236
|
-
|
|
236
|
+
# #589: "no source files to check" described an empty PROJECT when the real
|
|
237
|
+
# cause is an empty CHANGED_FILES — an empty DIFF, or a `--all` run whose
|
|
238
|
+
# tree genuinely has no sources. The old wording read like a pass, which is
|
|
239
|
+
# why the reporter worked around the gate by running lizard by hand.
|
|
240
|
+
if [ -n "${GATE_SCOPE:-}" ] && [ "$GATE_SCOPE" = "all" ]; then
|
|
241
|
+
echo "⏭️ SKIPPED - Complexity: no tracked source files in this repository (scope: --all, whole tree)."
|
|
242
|
+
else
|
|
243
|
+
echo "⏭️ SKIPPED - Complexity: no changed source files in this run (scope: diff)."
|
|
244
|
+
echo " Run 'xp-gate check . --all' to scan the whole tree."
|
|
245
|
+
fi
|
|
237
246
|
GATE_3_STATUS="SKIP"
|
|
238
247
|
# Consumed by the sourcing parent (pre-commit report JSON), not here.
|
|
239
248
|
# shellcheck disable=SC2034
|