@boyingliu01/xp-gate 0.30.2 → 0.30.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/adapters/iac.sh +45 -4
  2. package/adapters/java.sh +82 -16
  3. package/adapters/powershell.sh +70 -9
  4. package/gate-10.sh +110 -13
  5. package/gate-3.sh +17 -0
  6. package/gate-4.sh +12 -0
  7. package/gate-7.sh +4 -0
  8. package/hooks/gate-10.sh +110 -13
  9. package/hooks/gate-3.sh +17 -0
  10. package/hooks/gate-4.sh +12 -0
  11. package/hooks/gate-7.sh +4 -0
  12. package/hooks/hook-module-resolution.sh +14 -0
  13. package/hooks/lib/coverage-total.sh +1 -1
  14. package/hooks/pre-commit +138 -17
  15. package/hooks/pre-push +150 -9
  16. package/hooks/sprint-gate.sh +81 -2
  17. package/lib/__tests__/hook-health.test.js +85 -0
  18. package/lib/coverage-total.sh +92 -0
  19. package/lib/doctor.js +44 -0
  20. package/lib/hook-health.js +121 -0
  21. package/lib/mattpocock-license-verdict.json +1 -1
  22. package/mock-policy/AGENTS.md +2 -2
  23. package/mutation/AGENTS.md +2 -2
  24. package/mutation/__tests__/gate-m-survivors.test.ts +133 -0
  25. package/mutation/gate-m.ts +114 -11
  26. package/package.json +1 -1
  27. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  28. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  29. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  30. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  31. package/plugins/dsh/package.json +1 -1
  32. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  33. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  34. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  35. package/plugins/opencode/package.json +1 -1
  36. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  37. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  38. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  39. package/plugins/qoder/plugin.json +1 -1
  40. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  41. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  42. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  43. package/principles/AGENTS.md +2 -2
  44. package/skills/delphi-review/AGENTS.md +2 -2
  45. package/skills/sprint-flow/AGENTS.md +2 -2
  46. package/skills/test-specification-alignment/AGENTS.md +2 -2
  47. package/sprint-gate.sh +81 -2
package/gate-4.sh CHANGED
@@ -56,11 +56,15 @@ GATE_4_START=$(gate_start_ms)
56
56
  GATE_4_STATUS=""
57
57
  # #537: every non-run records WHY (no files / tool missing / unsupported lang)
58
58
  # so the quality report can distinguish "not applicable" from "not covered".
59
+ # Consumed by the sourcing parent, not here (see #548).
60
+ # shellcheck disable=SC2034
59
61
  GATE_4_REASON=""
60
62
 
61
63
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
62
64
  echo "⏭️ SKIPPED - Principles check (documentation project)."
63
65
  GATE_4_STATUS="SKIP"
66
+ # Consumed by the sourcing parent, not here (see #548).
67
+ # shellcheck disable=SC2034
64
68
  GATE_4_REASON="documentation_project"
65
69
 
66
70
  else
@@ -156,6 +160,8 @@ else
156
160
  echo "⚠️ Warning: Principles checker execution failed"
157
161
  echo "⏭️ SKIPPED - Principles check (execution issue)"
158
162
  GATE_4_STATUS="SKIP"
163
+ # Consumed by the sourcing parent, not here (see #548).
164
+ # shellcheck disable=SC2034
159
165
  GATE_4_REASON="checker_execution_error"
160
166
  rm -f "$PRINCIPLES_JSON"
161
167
  else
@@ -191,17 +197,23 @@ else
191
197
  echo "ℹ️ npx/tsx not available - skipping principles check"
192
198
  echo "⏭️ SKIPPED - Principles check (no Node.js/tsx)"
193
199
  GATE_4_STATUS="SKIP"
200
+ # Consumed by the sourcing parent, not here (see #548).
201
+ # shellcheck disable=SC2034
194
202
  GATE_4_REASON="tool_not_installed"
195
203
  fi
196
204
  else
197
205
  echo "ℹ️ Principles checker not found in project - skipping"
198
206
  echo "⏭️ SKIPPED - Principles check (checker not in project)"
199
207
  GATE_4_STATUS="SKIP"
208
+ # Consumed by the sourcing parent, not here (see #548).
209
+ # shellcheck disable=SC2034
200
210
  GATE_4_REASON="checker_not_found"
201
211
  fi
202
212
  else
203
213
  echo "⏭️ SKIPPED - Principles check (no matching source files changed)"
204
214
  GATE_4_STATUS="SKIP"
215
+ # Consumed by the sourcing parent, not here (see #548).
216
+ # shellcheck disable=SC2034
205
217
  GATE_4_REASON="no_matching_files_changed"
206
218
  fi
207
219
  fi
package/gate-7.sh CHANGED
@@ -12,11 +12,15 @@ GATE_7_START=$(gate_start_ms)
12
12
  IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
13
13
  # #536: scanned_files makes a trivial PASS (nothing to scan) auditable in the
14
14
  # quality report instead of reading like a covered security check.
15
+ # Consumed by the sourcing parent, not here (see #548).
16
+ # shellcheck disable=SC2034
15
17
  GATE_7_SCANNED_FILES=0
16
18
  if [ -z "$IAC_FILES" ]; then
17
19
  echo "✅ PASSED - No IaC files detected in changes."
18
20
  GATE_7_STATUS="PASS"
19
21
  else
22
+ # Consumed by the sourcing parent, not here (see #548).
23
+ # shellcheck disable=SC2034
20
24
  GATE_7_SCANNED_FILES=$(printf '%s\n' "$IAC_FILES" | sed -n '/./p' | wc -l | tr -d '[:space:]')
21
25
  # Run IaC adapter. Resolution follows the documented 3-tier convention
22
26
  # (adapter-common.sh): global ~/.config/xp-gate/adapters -> project
package/hooks/gate-10.sh CHANGED
@@ -12,7 +12,20 @@ GATE_10_START=$(gate_start_ms)
12
12
  GATE_10_STATUS=""
13
13
  # #536: audit trail for "did this commit actually get scanned?". The report
14
14
  # surfaces reason + scanned_files so a non-scan never reads as a clean PASS.
15
+ #
16
+ # GATE_10_REASON and GATE_10_SCANNED_FILES are consumed by the sourcing parent
17
+ # (pre-commit builds its report JSON from them after `source gate-10.sh`), not
18
+ # inside this file. The linter checks one file at a time and cannot see the
19
+ # consumer, so each assignment is flagged SC2034 "appears unused" -- which
20
+ # blocks Gate 1 for any repo that vendors this adapter, because that repo's own
21
+ # lint pass runs over the vendored copy (#548). Each assignment carries a
22
+ # scoped directive with that reason stated at the site.
23
+ #
24
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
25
+ # shellcheck disable=SC2034
15
26
  GATE_10_REASON=""
27
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
28
+ # shellcheck disable=SC2034
16
29
  GATE_10_SCANNED_FILES=0
17
30
 
18
31
  # Semgrep availability check
@@ -29,14 +42,21 @@ if [ -z "$SEMGREP_CMD" ]; then
29
42
  echo " Pre-cache rules: semgrep --config=p/security-audit"
30
43
  echo " Gate 10: SAST Security (WARN, semgrep not installed)"
31
44
  GATE_10_STATUS="WARN"
45
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
46
+ # shellcheck disable=SC2034
32
47
  GATE_10_REASON="tool_not_installed"
33
48
  else
34
- # Get staged files filtered to Semgrep-supported languages
35
- SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift)$' || true)
49
+ # Get staged files filtered to Semgrep-supported languages.
50
+ # #547 finding 3: ps1|psm1 were missing, so a PowerShell-only commit never
51
+ # reached semgrep even when Gate 10 ran -- the SAST gate was simply absent
52
+ # for that language.
53
+ SEMGREP_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(ts|tsx|js|jsx|py|go|java|c|cpp|cs|rb|php|scala|swift|ps1|psm1)$' || true)
36
54
 
37
55
  if [ -z "$SEMGREP_FILES" ]; then
38
56
  echo " ⏭️ SKIPPED - SAST (no supported language files changed)"
39
57
  GATE_10_STATUS="SKIP"
58
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
59
+ # shellcheck disable=SC2034
40
60
  GATE_10_REASON="no_supported_files"
41
61
  else
42
62
  # One path per line into an array. Quoting the newline-joined list would hand
@@ -71,11 +91,27 @@ else
71
91
  [ -n "$_semgrep_file" ] || continue
72
92
  SEMGREP_FILE_COUNT=$((SEMGREP_FILE_COUNT + 1))
73
93
  SEMGREP_ARGS+=("--include=${_semgrep_file##*/}")
74
- _semgrep_dir="./${_semgrep_file%/*}"
75
- # A repo-root file has no directory component ("./x.ts" strips to "./").
76
- [ "$_semgrep_dir" = "./" ] && _semgrep_dir="."
94
+ # A repo-root file has no slash at all, so "%/*" returns it unchanged and
95
+ # the old comment's assumption (`"./x.ts" strips to "./"`) never held:
96
+ # the target became the FILE path dressed as a directory ("./x.ts").
97
+ # Strip only when a slash is present; otherwise the target is the repo
98
+ # root, spelled ".". Scanning "." widens the scan to the whole tree, which
99
+ # is the safe side (see the note above) and is what a root-level staged
100
+ # file requires in order to be analysed at all.
101
+ case "$_semgrep_file" in
102
+ */*) _semgrep_dir="./${_semgrep_file%/*}" ;;
103
+ *) _semgrep_dir="." ;;
104
+ esac
105
+ # #558: dedup must match WHOLE lines. The previous `case "$SEEN" in
106
+ # *"$_semgrep_dir"*)` was an unanchored substring test: once
107
+ # ./fastdl/tests was recorded, the parent ./fastdl matched it and was
108
+ # never added as a target, so a staged fastdl/a.py contributed an
109
+ # --include= with no directory to scan it in. Nothing errored and nothing
110
+ # was reported -- the file was simply not analysed, which for a security
111
+ # gate is the worst outcome: a silent net loss that looks like a pass.
112
+ # Sibling prefixes (src/api vs src/app) collapsed the same way.
77
113
  case "$SEMGREP_DIRS_SEEN" in
78
- *"$_semgrep_dir"*) : ;;
114
+ *$'\n'"$_semgrep_dir"$'\n'*) : ;;
79
115
  *) SEMGREP_ARGS+=("$_semgrep_dir"); SEMGREP_DIRS_SEEN="$SEMGREP_DIRS_SEEN$_semgrep_dir"$'\n' ;;
80
116
  esac
81
117
  done <<EOF
@@ -91,6 +127,8 @@ EOF
91
127
  echo " Install node (https://nodejs.org); without it SAST findings cannot be classified"
92
128
  echo " ⏭️ SKIPPED - SAST (node not installed)"
93
129
  GATE_10_STATUS="SKIP"
130
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
131
+ # shellcheck disable=SC2034
94
132
  GATE_10_REASON="tool_not_installed"
95
133
  else
96
134
 
@@ -109,9 +147,41 @@ EOF
109
147
  # code is demoted to "did the tool itself run". --error stays OFF: it would
110
148
  # add nothing the JSON does not already provide, and keeping it off leaves
111
149
  # failure exit codes unambiguous for the #475 diagnostics path.
112
- SEMGREP_STDOUT_FILE=$(mktemp)
113
- SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
114
- SEMGREP_EXIT=$?
150
+ # #559: bound the run. Without a timeout, a wedged semgrep blocks the
151
+ # invocation for as long as its process lives; the fail-closed verdict logic
152
+ # below is correct but unreachable, so the hook hangs instead of judging.
153
+ # A gate that can wedge an interactive `git commit` gets bypassed with
154
+ # --no-verify, which is worse than any verdict it could have produced.
155
+ # `-k` after the primary signal: pysemgrep spinning in a syscall ignores
156
+ # SIGTERM, and the shipped 0.29 code used the same `-k 10` shape.
157
+ SEMGREP_TIMEOUT_S="${XP_GATE_SEMGREP_TIMEOUT:-180}"
158
+ SEMGREP_TIMED_OUT=false
159
+ if command -v timeout >/dev/null 2>&1; then
160
+ SEMGREP_STDOUT_FILE=$(mktemp)
161
+ SEMGREP_OUTPUT=$(timeout -k 10 "$SEMGREP_TIMEOUT_S" \
162
+ "$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
163
+ "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
164
+ SEMGREP_EXIT=$?
165
+ # 124 = timeout's own "I gave up" code. 137 = 128+SIGKILL, which is what
166
+ # `timeout -k` returns when the child ignored SIGTERM and had to be
167
+ # killed -- measured with a semgrep stand-in that traps TERM. Both mean
168
+ # "bounded, no verdict", so both must be reported as a timeout; checking
169
+ # only 124 told the user their semgrep was unparseable when in fact the
170
+ # scan had been cut off.
171
+ case "$SEMGREP_EXIT" in
172
+ 124|137) SEMGREP_TIMED_OUT=true ;;
173
+ esac
174
+ else
175
+ # No `timeout` on this platform (stock macOS). Say so: an unbounded run
176
+ # nobody mentioned is exactly how #559 stayed invisible for a whole
177
+ # release. The scan still runs -- refusing to scan would be a worse
178
+ # trade -- but the unbounded window is now stated.
179
+ echo " ⚠️ 'timeout' command not available — semgrep runs UNBOUNDED (install coreutils or set up gtimeout)"
180
+ SEMGREP_STDOUT_FILE=$(mktemp)
181
+ SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check \
182
+ "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
183
+ SEMGREP_EXIT=$?
184
+ fi
115
185
 
116
186
  # One node pass over the JSON: first line = verdict counts, remaining lines =
117
187
  # finding details. The severity vocabulary is the UNION of the two scales
@@ -153,7 +223,18 @@ console.log("");});
153
223
  if [ "$V_PARSE" != "OK" ] || [ -z "$V_RESULTS" ] || [ -z "$V_ERRORS" ] || [ -z "$V_BLOCKING" ] || [ -z "$V_WARNINGS" ]; then
154
224
  # Unparseable output: nothing the exit code claims can be trusted (#518).
155
225
  # Fail closed — SKIP with diagnostics, never a PASS.
156
- echo " ⚠️ semgrep output unparseable or verdict fields missing (exit code ${SEMGREP_EXIT}) — cannot determine verdict"
226
+ #
227
+ # #559: name a timeout as a timeout. The generic "unparseable" message
228
+ # would be true but useless here -- it points at the JSON parser when the
229
+ # real problem is that the scan was cut off. Same verdict (SKIP), but a
230
+ # diagnosis the reader can act on.
231
+ if [ "$SEMGREP_TIMED_OUT" = true ]; then
232
+ echo " ⏱️ semgrep exceeded ${SEMGREP_TIMEOUT_S}s and was killed — no verdict is possible"
233
+ echo " This is NOT a finding and NOT a clean scan. Raise XP_GATE_SEMGREP_TIMEOUT"
234
+ echo " if the repo legitimately takes longer; investigate if semgrep itself is wedged."
235
+ else
236
+ echo " ⚠️ semgrep output unparseable or verdict fields missing (exit code ${SEMGREP_EXIT}) — cannot determine verdict"
237
+ fi
157
238
  if [ -n "$SEMGREP_OUTPUT" ]; then
158
239
  echo " ── what semgrep reported on stderr ──"
159
240
  printf '%s\n' "$SEMGREP_OUTPUT" | tail -10 | sed 's/^/ /'
@@ -162,9 +243,19 @@ console.log("");});
162
243
  echo " ── what semgrep wrote to stdout (tail) ──"
163
244
  printf '%s\n' "$SEMGREP_STDOUT_TAIL" | sed 's/^/ /'
164
245
  fi
165
- echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
166
- GATE_10_STATUS="SKIP"
167
- GATE_10_REASON="output_unparseable"
246
+ if [ "$SEMGREP_TIMED_OUT" = true ]; then
247
+ echo " ⏭️ SKIPPED - SAST (semgrep timed out after ${SEMGREP_TIMEOUT_S}s)"
248
+ GATE_10_STATUS="SKIP"
249
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
250
+ # shellcheck disable=SC2034
251
+ GATE_10_REASON="semgrep_timeout"
252
+ else
253
+ echo " ⏭️ SKIPPED - SAST (semgrep output unparseable)"
254
+ GATE_10_STATUS="SKIP"
255
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
256
+ # shellcheck disable=SC2034
257
+ GATE_10_REASON="output_unparseable"
258
+ fi
168
259
  elif [ "$V_RESULTS" -eq 0 ] && { [ "$V_ERRORS" -gt 0 ] || [ "$SEMGREP_EXIT" -ne 0 ]; }; then
169
260
  # Nothing was scanned to a verdict: semgrep either failed outright
170
261
  # (nonzero exit) or completed but reported scan errors for every input
@@ -178,6 +269,8 @@ console.log("");});
178
269
  fi
179
270
  echo " ⏭️ SKIPPED - SAST (semgrep runtime error)"
180
271
  GATE_10_STATUS="SKIP"
272
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
273
+ # shellcheck disable=SC2034
181
274
  GATE_10_REASON="semgrep_runtime_error"
182
275
  elif [ "$V_BLOCKING" -gt 0 ]; then
183
276
  echo ""
@@ -192,9 +285,13 @@ console.log("");});
192
285
  echo "$FINDING_DETAILS"
193
286
  echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
194
287
  GATE_10_STATUS="FAIL"
288
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
289
+ # shellcheck disable=SC2034
195
290
  GATE_10_SCANNED_FILES=$SEMGREP_FILE_COUNT
196
291
  exit 1
197
292
  else
293
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
294
+ # shellcheck disable=SC2034
198
295
  GATE_10_SCANNED_FILES=$SEMGREP_FILE_COUNT
199
296
  echo ""
200
297
  echo " ✅ PASSED - No blocking vulnerabilities (files scanned: $SEMGREP_FILE_COUNT)"
package/hooks/gate-3.sh CHANGED
@@ -8,11 +8,15 @@
8
8
  2>&1 echo "→ Gate 3: Cyclomatic complexity..."
9
9
  GATE_3_START=$(gate_start_ms)
10
10
  # #537: every non-run records WHY so the report can say why a gate is SKIP.
11
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
12
+ # shellcheck disable=SC2034
11
13
  GATE_3_REASON=""
12
14
 
13
15
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
14
16
  echo "⏭️ SKIPPED - Complexity (documentation project)."
15
17
  GATE_3_STATUS="SKIP"
18
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
19
+ # shellcheck disable=SC2034
16
20
  GATE_3_REASON="documentation_project"
17
21
 
18
22
  elif [ "$PROJECT_LANG" = "powershell" ]; then
@@ -21,6 +25,8 @@ elif [ "$PROJECT_LANG" = "powershell" ]; then
21
25
  GATE_3_STATUS="SKIP"
22
26
  # #537: a PowerShell-primary repo was invisible to this dimension while the
23
27
  # score implied full coverage — say exactly why nothing ran.
28
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
29
+ # shellcheck disable=SC2034
24
30
  GATE_3_REASON="no_powershell_tool"
25
31
 
26
32
  else
@@ -159,7 +165,10 @@ else
159
165
  # empty stdin — the verdict comes back empty and Gate 3 passes on zero
160
166
  # rows (measured while wiring #544: same input passed or blocked
161
167
  # depending only on where the prefix sat).
168
+ # Consumed by evaluate_ccn_ratchet in the sourced lib, not here.
169
+ # shellcheck disable=SC2034
162
170
  CCN_WARN_THRESHOLD="$CCN_THRESHOLD"
171
+ # shellcheck disable=SC2034
163
172
  CCN_BLOCK_THRESHOLD=$((CCN_THRESHOLD * 2))
164
173
  RATCHET_VERDICT=$(printf '%s\n' "$CC_CSV" | evaluate_ccn_ratchet ".ccn-baseline.json")
165
174
  CC_VIOLATIONS=$(printf '%s\n' "$RATCHET_VERDICT" | grep -c '^BLOCK ' || true)
@@ -205,6 +214,8 @@ else
205
214
  END { exit(found ? 0 : 1) }'; then
206
215
  echo "⚠️ WARN - lizard produced no parsable results; complexity not verified"
207
216
  GATE_3_STATUS="WARN"
217
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
218
+ # shellcheck disable=SC2034
208
219
  GATE_3_REASON="lizard_output_unparsable"
209
220
  elif [ "$CC_WARNINGS" -gt 0 ]; then
210
221
  echo "$CC_OUTPUT"
@@ -212,6 +223,8 @@ else
212
223
  echo "❌ BLOCKED - $CC_WARNINGS functions with CCN > $CCN_THRESHOLD found."
213
224
  echo "Refactor high-complexity functions to keep below $CCN_THRESHOLD complexity."
214
225
  GATE_3_STATUS="BLOCK"
226
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
227
+ # shellcheck disable=SC2034
215
228
  GATE_3_REASON="ccn_over_threshold"
216
229
  exit 1
217
230
  else
@@ -222,6 +235,8 @@ else
222
235
  else
223
236
  echo "⏭️ SKIPPED - Complexity (no source files to check)."
224
237
  GATE_3_STATUS="SKIP"
238
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
239
+ # shellcheck disable=SC2034
225
240
  GATE_3_REASON="no_source_files_changed"
226
241
  fi
227
242
  else
@@ -229,6 +244,8 @@ else
229
244
  echo " Install with: pip install --user lizard"
230
245
  echo " Gate 3: Complexity check (WARN, tool not available)"
231
246
  GATE_3_STATUS="WARN"
247
+ # Consumed by the sourcing parent (pre-commit report JSON), not here.
248
+ # shellcheck disable=SC2034
232
249
  GATE_3_REASON="tool_not_installed"
233
250
  fi
234
251
  fi
package/hooks/gate-4.sh CHANGED
@@ -56,11 +56,15 @@ GATE_4_START=$(gate_start_ms)
56
56
  GATE_4_STATUS=""
57
57
  # #537: every non-run records WHY (no files / tool missing / unsupported lang)
58
58
  # so the quality report can distinguish "not applicable" from "not covered".
59
+ # Consumed by the sourcing parent, not here (see #548).
60
+ # shellcheck disable=SC2034
59
61
  GATE_4_REASON=""
60
62
 
61
63
  if [ "$PROJECT_LANG" = "documentation-only" ]; then
62
64
  echo "⏭️ SKIPPED - Principles check (documentation project)."
63
65
  GATE_4_STATUS="SKIP"
66
+ # Consumed by the sourcing parent, not here (see #548).
67
+ # shellcheck disable=SC2034
64
68
  GATE_4_REASON="documentation_project"
65
69
 
66
70
  else
@@ -156,6 +160,8 @@ else
156
160
  echo "⚠️ Warning: Principles checker execution failed"
157
161
  echo "⏭️ SKIPPED - Principles check (execution issue)"
158
162
  GATE_4_STATUS="SKIP"
163
+ # Consumed by the sourcing parent, not here (see #548).
164
+ # shellcheck disable=SC2034
159
165
  GATE_4_REASON="checker_execution_error"
160
166
  rm -f "$PRINCIPLES_JSON"
161
167
  else
@@ -191,17 +197,23 @@ else
191
197
  echo "ℹ️ npx/tsx not available - skipping principles check"
192
198
  echo "⏭️ SKIPPED - Principles check (no Node.js/tsx)"
193
199
  GATE_4_STATUS="SKIP"
200
+ # Consumed by the sourcing parent, not here (see #548).
201
+ # shellcheck disable=SC2034
194
202
  GATE_4_REASON="tool_not_installed"
195
203
  fi
196
204
  else
197
205
  echo "ℹ️ Principles checker not found in project - skipping"
198
206
  echo "⏭️ SKIPPED - Principles check (checker not in project)"
199
207
  GATE_4_STATUS="SKIP"
208
+ # Consumed by the sourcing parent, not here (see #548).
209
+ # shellcheck disable=SC2034
200
210
  GATE_4_REASON="checker_not_found"
201
211
  fi
202
212
  else
203
213
  echo "⏭️ SKIPPED - Principles check (no matching source files changed)"
204
214
  GATE_4_STATUS="SKIP"
215
+ # Consumed by the sourcing parent, not here (see #548).
216
+ # shellcheck disable=SC2034
205
217
  GATE_4_REASON="no_matching_files_changed"
206
218
  fi
207
219
  fi
package/hooks/gate-7.sh CHANGED
@@ -12,11 +12,15 @@ GATE_7_START=$(gate_start_ms)
12
12
  IAC_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E "\.(tf|yaml|yml)$|Dockerfile" || true)
13
13
  # #536: scanned_files makes a trivial PASS (nothing to scan) auditable in the
14
14
  # quality report instead of reading like a covered security check.
15
+ # Consumed by the sourcing parent, not here (see #548).
16
+ # shellcheck disable=SC2034
15
17
  GATE_7_SCANNED_FILES=0
16
18
  if [ -z "$IAC_FILES" ]; then
17
19
  echo "✅ PASSED - No IaC files detected in changes."
18
20
  GATE_7_STATUS="PASS"
19
21
  else
22
+ # Consumed by the sourcing parent, not here (see #548).
23
+ # shellcheck disable=SC2034
20
24
  GATE_7_SCANNED_FILES=$(printf '%s\n' "$IAC_FILES" | sed -n '/./p' | wc -l | tr -d '[:space:]')
21
25
  # Run IaC adapter. Resolution follows the documented 3-tier convention
22
26
  # (adapter-common.sh): global ~/.config/xp-gate/adapters -> project
@@ -7,12 +7,22 @@
7
7
  # is its shell projection. Editing it by hand makes the hook disagree with
8
8
  # doctor.js, which reads the JS module directly — the exact divergence
9
9
  # #495/#500 were about.
10
+ #
11
+ # Every HOOK_* constant below is read by the sourcing parent, not by this
12
+ # file. A linter checking this file alone cannot see that consumer, so each
13
+ # declaration carries a scoped directive with the reason stated at the site
14
+ # (#548 -- the same false SC2034 used to block consumer repos that vendor
15
+ # these hook modules). The directives are generated, not hand-added.
10
16
 
11
17
  # Probe file used to pick the gate tier.
18
+ # Consumed by the sourcing parent, not here.
19
+ # shellcheck disable=SC2034
12
20
  HOOK_GATE_PROBE="gate-3.sh"
13
21
 
14
22
  # ADAPTER_DIR cascade, highest priority first.
15
23
  # Each entry is "tier_id|base_var|probe_file".
24
+ # Consumed by the sourcing parent, not here.
25
+ # shellcheck disable=SC2034
16
26
  HOOK_ADAPTER_DIR_TIERS=(
17
27
  'global|GLOBAL_ADAPTER_DIR|adapter-common.sh'
18
28
  'project|PROJECT_GITHOOKS|adapter-common.sh'
@@ -21,6 +31,8 @@ HOOK_ADAPTER_DIR_TIERS=(
21
31
 
22
32
  # Adapter resolution tiers, highest priority first.
23
33
  # Each entry is "tier_id|base_var|template" with {name} as the basename.
34
+ # Consumed by the sourcing parent, not here.
35
+ # shellcheck disable=SC2034
24
36
  HOOK_ADAPTER_TIERS=(
25
37
  'adapter_flat|ADAPTER_DIR|{name}.sh'
26
38
  'adapter_nested|ADAPTER_DIR|adapters/{name}.sh'
@@ -32,6 +44,8 @@ HOOK_ADAPTER_TIERS=(
32
44
  # Semantics differ from adapter tiers: each entry names the DIRECTORY that
33
45
  # would hold the gate probe, so a match yields the directory (GATE_DIR), not
34
46
  # the probe file itself. Entry format: "tier_id|base_var|(empty suffix)".
47
+ # Consumed by the sourcing parent, not here.
48
+ # shellcheck disable=SC2034
35
49
  HOOK_GATE_TIERS=(
36
50
  'gate_adapter_dir|ADAPTER_DIR|'
37
51
  'gate_project|PROJECT_GITHOOKS|'
@@ -70,7 +70,7 @@ enforce_python_coverage() {
70
70
  fi
71
71
 
72
72
  if [ -n "$COVERAGE_PERCENT" ]; then
73
- if [ "$COVERAGE_PERCENT" -lt 80 ]; then
73
+ if [ "$COVERAGE_PERCENT" -lt "${COVERAGE_THRESHOLD:-80}" ]; then
74
74
  # #399: a subset run (smart test selection) measures only the tested
75
75
  # files, so its global total is meaningless as a verdict — the full
76
76
  # suite may well be >=80% even when the subset is not. Warn exactly as