@boyingliu01/xp-gate 0.35.23 → 0.36.0

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 (71) hide show
  1. package/adapter-common.sh +78 -36
  2. package/adapters/csharp.sh +245 -0
  3. package/adapters/gate-3.sh +1 -1
  4. package/adapters/gate-4.sh +1 -1
  5. package/adapters/gate-9.sh +37 -2
  6. package/adapters/go.sh +6 -11
  7. package/adapters/iac.sh +45 -5
  8. package/adapters/python.sh +6 -11
  9. package/bin/xp-gate.js +18 -11
  10. package/gate-10.sh +18 -3
  11. package/gate-13-compiler-strictness.sh +181 -0
  12. package/gate-3.sh +1 -1
  13. package/gate-4.sh +1 -1
  14. package/gate-9.sh +37 -2
  15. package/gates/common.ts +9 -2
  16. package/hooks/adapter-common.sh +78 -36
  17. package/hooks/gate-10.sh +18 -3
  18. package/hooks/gate-13-compiler-strictness.sh +181 -0
  19. package/hooks/gate-3.sh +1 -1
  20. package/hooks/gate-4.sh +1 -1
  21. package/hooks/gate-9.sh +37 -2
  22. package/hooks/gate-registry.sh +161 -0
  23. package/hooks/lib/test-failure.sh +29 -4
  24. package/hooks/pre-commit +122 -3
  25. package/lib/__tests__/agent-runtime-journal.test.js +42 -18
  26. package/lib/__tests__/check-single-file.test.js +55 -4
  27. package/lib/__tests__/detect-deps-dsh-profile.test.js +43 -0
  28. package/lib/__tests__/doctor-delphi.test.js +178 -3
  29. package/lib/__tests__/doctor-tui-skill-freshness.test.js +3 -3
  30. package/lib/__tests__/install-skill-full-dir.test.js +45 -2
  31. package/lib/__tests__/install-skill.test.js +191 -23
  32. package/lib/__tests__/mirror-sync.test.js +23 -5
  33. package/lib/__tests__/uninstall-global-residue.test.js +394 -9
  34. package/lib/__tests__/update-skill-flags.test.js +78 -0
  35. package/lib/__tests__/update-skill.test.js +334 -57
  36. package/lib/detect-deps.js +24 -0
  37. package/lib/doctor.js +80 -12
  38. package/lib/install-skill.js +85 -21
  39. package/lib/mattpocock-license-verdict.json +1 -1
  40. package/lib/uninstall.js +143 -12
  41. package/lib/update-skill.js +239 -28
  42. package/mock-policy/AGENTS.md +4 -4
  43. package/mutation/AGENTS.md +4 -4
  44. package/package.json +1 -1
  45. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  46. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  47. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  48. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  49. package/plugins/dsh/lib/index.d.ts.map +1 -1
  50. package/plugins/dsh/lib/index.js +7 -4
  51. package/plugins/dsh/package.json +1 -1
  52. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  53. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  54. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  55. package/plugins/dsh/src/index.test.ts +25 -0
  56. package/plugins/dsh/src/index.ts +7 -4
  57. package/plugins/opencode/package.json +1 -1
  58. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  59. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  60. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  61. package/plugins/qoder/plugin.json +1 -1
  62. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  63. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  64. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  65. package/principles/AGENTS.md +4 -4
  66. package/principles/adapters/csharp.ts +309 -0
  67. package/principles/analyzer.ts +5 -0
  68. package/scripts/delphi-external-review.cjs +130 -101
  69. package/skills/delphi-review/AGENTS.md +4 -4
  70. package/skills/sprint-flow/AGENTS.md +4 -4
  71. package/skills/test-specification-alignment/AGENTS.md +4 -4
package/adapter-common.sh CHANGED
@@ -38,8 +38,46 @@ run_without_git_context() (
38
38
  "$@"
39
39
  )
40
40
 
41
+ # ---------------------------------------------------------------------------
42
+ # C# / .NET manifests (#717)
43
+ #
44
+ # A .NET tree can declare itself three ways: a project file (*.csproj), a
45
+ # solution (*.sln), or a Directory.Build.props that carries the real settings
46
+ # for a whole directory subtree with no project file of its own. All three are
47
+ # checked, because a repo may carry only the latter two.
48
+ # ---------------------------------------------------------------------------
49
+
50
+ # Succeeds when the current directory holds a .NET manifest at depth 1.
51
+ has_dotnet_manifest() {
52
+ [ -f "global.json" ] && return 0
53
+ [ -f "Directory.Build.props" ] && return 0
54
+ [ -f "Directory.Build.targets" ] && return 0
55
+ # `find -maxdepth 1` rather than a glob loop: a repo with thousands of files
56
+ # in one directory would expand a `*.csproj` glob into an unusable argv.
57
+ # -not -path "./.*" keeps a stray file under .git/.vscode from declaring the
58
+ # project language.
59
+ [ -n "$(find . -maxdepth 1 -type f \( -name '*.csproj' -o -name '*.sln' \) 2>/dev/null | sed -n '1p; 1q')" ]
60
+ }
61
+
62
+ # Succeeds when a .cs source exists below the current directory.
63
+ has_csharp_sources() {
64
+ [ -n "$(find . -name "*.cs" -type f -not -path "./.git/*" -not -path "*/node_modules/*" -not -path "*/bin/*" -not -path "*/obj/*" 2>/dev/null | sed -n '1p; 1q')" ]
65
+ }
66
+
41
67
  detect_project_lang() {
42
- if [ -f "tsconfig.json" ]; then
68
+ # #717: the C# arms come FIRST, before tsconfig.json / package.json /
69
+ # .sh sniffing. Detection below is an exclusive first-match chain, and a .NET
70
+ # repo routinely carries a JS or shell half (the dsh-aot-runner shape in the
71
+ # issue is 2031 lines of C# plus ~900 lines of JS). Ordered after the
72
+ # TypeScript arm, that repo reported "typescript" and every C# file was
73
+ # invisible to language-routed gates -- the silent skip #717 is about.
74
+ #
75
+ # Placing C# first cannot steal an existing project: none of the languages
76
+ # matched afterwards ever ship a .csproj/.sln/Directory.Build.props, so the
77
+ # pre-existing arms are reached unchanged whenever those files are absent.
78
+ if has_dotnet_manifest; then
79
+ echo "csharp"
80
+ elif [ -f "tsconfig.json" ]; then
43
81
  echo "typescript"
44
82
  elif [ -f "pyproject.toml" ] || [ -f "requirements.txt" ] || [ -f "setup.py" ]; then
45
83
  echo "python"
@@ -88,6 +126,8 @@ detect_project_lang() {
88
126
  else
89
127
  echo "dart"
90
128
  fi
129
+ elif [ -n "$(find . -name "*.cs" -type f -not -path "./.git/*" -not -path "*/bin/*" -not -path "*/obj/*" | sed -n '1p; 1q')" ]; then
130
+ echo "csharp"
91
131
  elif [ -n "$(find . -name "*.swift" -type f | sed -n '1p; 1q')" ]; then
92
132
  echo "swift"
93
133
  elif [ -n "$(find . -name "*.cpp" -o -name "*.cc" -o -name "*.c" -o -name "*.h" -type f | sed -n '1p; 1q')" ]; then
@@ -104,41 +144,42 @@ detect_project_lang() {
104
144
  fi
105
145
  }
106
146
 
107
- route_to_adapter() {
108
- local action="$1"
109
- local lang
110
- lang=$(detect_project_lang)
111
-
112
- local adapter_file=""
113
-
114
- # 3-tier resolution matching pre-commit ADAPTER_DIR logic
115
- # Tier 1: Global flat layout (~/.config/xp-gate/adapters/lang.sh)
116
- if [ -f "$HOME/.config/xp-gate/adapters/${lang}.sh" ]; then
117
- adapter_file="$HOME/.config/xp-gate/adapters/${lang}.sh"
118
- # Tier 2: Project nested layout (githooks/adapters/lang.sh)
119
- elif [ -f "githooks/adapters/${lang}.sh" ]; then
120
- adapter_file="githooks/adapters/${lang}.sh"
121
- elif [ -f "./githooks/adapters/${lang}.sh" ]; then
122
- adapter_file="./githooks/adapters/${lang}.sh"
123
- fi
147
+ # ---------------------------------------------------------------------------
148
+ # NOTE (#717): a `detect_project_langs()` collector was added here and then
149
+ # REMOVED before this change landed. It had zero production callers -- only its
150
+ # own tests -- which is exactly the dead-dispatch shape #688 deleted from this
151
+ # same file (see below). The gates do NOT route through it: pre-commit collects
152
+ # languages with its own `_detect_ext_lang` into `PROJECT_LANGS`, and that is
153
+ # what actually decides which adapters run (AC-717-13/13b pin the dual-language
154
+ # case). Keeping a second, unconsumed collector beside it would have meant two
155
+ # language-detection paths drifting apart, with the untested one looking
156
+ # authoritative. If multi-language routing is ever reworked, the change belongs
157
+ # in pre-commit's collector, not in a parallel helper here.
124
158
 
125
- if [ -n "$adapter_file" ]; then
126
- # shellcheck source=./githooks/adapters/lang.sh
127
- source "$adapter_file"
128
-
129
- # Execute the requested action
130
- case "$action" in
131
- "static_analysis") run_static_analysis ;;
132
- "lint") run_lint ;;
133
- "tests") run_tests ;;
134
- "coverage") run_coverage ;;
135
- *) return 1 ;;
136
- esac
137
- else
138
- echo "No adapter found for language: $lang (searched: ~/.config/xp-gate/adapters/, githooks/adapters/)"
139
- return 1
140
- fi
141
- }
159
+ # ---------------------------------------------------------------------------
160
+ # REMOVED in #688: route_to_adapter()
161
+ #
162
+ # It was a dispatcher with zero production callers. pre-commit sources an
163
+ # adapter and calls `run_static_analysis` / `run_lint` / `run_tests` /
164
+ # `run_coverage` directly; nothing ever went through this function, so its
165
+ # `case "$action"` enumeration was dead too -- including the "add an
166
+ # `architecture` arm and it'll work" assumption in #688, which would have been
167
+ # a no-op.
168
+ #
169
+ # Its two never-invoked dependants are gone with it: `run_architecture()` in
170
+ # adapters/python.sh and adapters/go.sh. Gate 6 already implements both checks
171
+ # INLINE in pre-commit, and does it better than the adapter versions did:
172
+ # * Python: detects 4 config carriers (.import-linter.yml /
173
+ # import_linter_config.yml / .importlinter / pyproject [tool.importlinter]),
174
+ # where the adapter checked 2, and BLOCKs when lint-imports is missing
175
+ # instead of `require_tool ... || return 0` silently passing.
176
+ # * Go: reads the checker's own exit status via PIPESTATUS[0] (#630), where a
177
+ # `arch-go check | head` pipeline would have reported head's status.
178
+ # Wiring the adapter version back in would therefore have been a regression,
179
+ # so the dead code was deleted instead of connected.
180
+ #
181
+ # tests: githooks/__tests__/issue-688-dead-dispatcher-removed.test.bats
182
+ # ---------------------------------------------------------------------------
142
183
 
143
184
  check_if_tool_available() {
144
185
  local tool_name="$1"
@@ -221,7 +262,8 @@ detect_iac_project() {
221
262
 
222
263
  # Check for Kubernetes manifests (YAML with apiVersion/kind)
223
264
  if [ -n "$(find . -maxdepth 2 \( -name "*.yaml" -o -name "*.yml" \) -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')" ]; then
224
- local yaml_file=$(find . -maxdepth 2 \( -name "*.yaml" -o -name "*.yml" \) -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')
265
+ local yaml_file
266
+ yaml_file=$(find . -maxdepth 2 \( -name "*.yaml" -o -name "*.yml" \) -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')
225
267
  if grep -qE "^(apiVersion|kind):" "$yaml_file" 2>/dev/null; then
226
268
  has_iac=true
227
269
  fi
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env bash
2
+ # C# / .NET adapter (#717)
3
+ #
4
+ # Contract: pre-commit sources this file and calls run_static_analysis /
5
+ # run_lint / run_tests / run_coverage directly (#688 — no dispatcher sits in
6
+ # between). Every function must therefore exist and must return a status the
7
+ # caller can turn into a verdict.
8
+ #
9
+ # Two lessons from the sibling adapters are load-bearing here:
10
+ #
11
+ # 1. #706 (kotlin.sh): a failed BUILD must not swallow an analyzer that works
12
+ # on its own. `dotnet format` reads .cs sources and needs no successful
13
+ # build, so it runs regardless of what the build said. The build failure is
14
+ # recorded and reported, never hidden.
15
+ #
16
+ # 2. #703/#708 (java.sh): "tool not installed" and "tool ran and failed" need
17
+ # different fixes, so they get different messages. A missing dotnet is a
18
+ # SKIP with an install hint, not a BLOCK -- the project may not be a .NET
19
+ # one after all.
20
+ #
21
+ # Windows note (#717): on this machine `dotnet test` aborts before running a
22
+ # single case with `Win32Exception (5) Access is denied` from the VSTest
23
+ # testhost -- an environment fault, reproducible under both git-bash and
24
+ # PowerShell. run_tests() below therefore reports "test host did not start"
25
+ # separately from "tests failed", because a gate that reads the former as the
26
+ # latter would block every commit in a working repo.
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Project discovery
30
+ # ---------------------------------------------------------------------------
31
+
32
+ # Print the directory that owns the build, or nothing.
33
+ #
34
+ # A .sln at the root is the usual shape; a bare .csproj is the single-project
35
+ # shape; Directory.Build.props can declare a tree with neither. All three are
36
+ # accepted because `dotnet` itself accepts all three.
37
+ _detect_dotnet_root() {
38
+ local found
39
+ found=$(find . -maxdepth 2 -name "*.sln" -type f -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')
40
+ if [ -n "$found" ]; then
41
+ dirname "$found"
42
+ return 0
43
+ fi
44
+ found=$(find . -maxdepth 2 -name "*.csproj" -type f -not -path "./.git/*" 2>/dev/null | sed -n '1p; 1q')
45
+ if [ -n "$found" ]; then
46
+ dirname "$found"
47
+ return 0
48
+ fi
49
+ if [ -f "Directory.Build.props" ]; then
50
+ echo "."
51
+ return 0
52
+ fi
53
+ return 1
54
+ }
55
+
56
+ # Echo the dotnet entry point, or nothing when the SDK is absent.
57
+ #
58
+ # Deliberately does NOT bundle the bare `dotnet` invocation into a single
59
+ # string: the .exe suffix matters on Windows and callers need the tokens.
60
+ _detect_dotnet() {
61
+ if command -v dotnet >/dev/null 2>&1; then
62
+ echo "dotnet"
63
+ return 0
64
+ fi
65
+ if command -v dotnet.exe >/dev/null 2>&1; then
66
+ echo "dotnet.exe"
67
+ return 0
68
+ fi
69
+ return 1
70
+ }
71
+
72
+ # Print the first staged/changed .cs file, or nothing. Used only for messages.
73
+ _csharp_changed_hint() {
74
+ printf '%s\n' "${CHANGED_FILES:-}" | grep -E '\.cs$' | sed -n '1p'
75
+ }
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Code quality (Gate 1)
79
+ # ---------------------------------------------------------------------------
80
+
81
+ run_static_analysis() {
82
+ local dotnet_root dotnet ver build_out build_rc format_out format_rc
83
+ dotnet_root=$(_detect_dotnet_root) || {
84
+ echo "No .NET project marker found (.sln / *.csproj / Directory.Build.props)"
85
+ return 0
86
+ }
87
+
88
+ dotnet=$(_detect_dotnet) || {
89
+ # SKIP, not BLOCK: no SDK means nothing was measured, and the caller maps a
90
+ # non-zero return to "tools unavailable" which it reports honestly.
91
+ echo "dotnet SDK not found"
92
+ echo " Install: winget install Microsoft.DotNet.SDK.8 | brew install --cask dotnet-sdk | apt-get install dotnet-sdk-8.0"
93
+ return 2
94
+ }
95
+
96
+ ver=$("$dotnet" --version 2>/dev/null | sed -n '1p')
97
+ echo "dotnet SDK ${ver:-unknown} (project: ${dotnet_root})"
98
+
99
+ # --- build (recorded, not authoritative) -------------------------------
100
+ # `--nologo` keeps the banner out; `-warnaserror` is Gate 13's job, not Gate
101
+ # 1's, so the build here only needs to answer "does this compile".
102
+ build_out=$("$dotnet" build "$dotnet_root" --nologo -v quiet 2>&1)
103
+ build_rc=$?
104
+ if [ "$build_rc" -ne 0 ]; then
105
+ echo "BUILD FAILED (dotnet build exited $build_rc):"
106
+ printf '%s\n' "$build_out" | grep -iE "error|fail" | sed -n '1,10p'
107
+ echo " Analysis still runs below: dotnet format reads sources directly"
108
+ echo " and its verdict does not depend on the build."
109
+ fi
110
+
111
+ # --- analyzer (authoritative) -----------------------------------------
112
+ # `dotnet format --verify-no-changes` is the closest C# equivalent of
113
+ # `eslint --max-warnings 0`: formatting + the configured analyzers, exit 0
114
+ # only when everything is clean.
115
+ format_out=$("$dotnet" format "$dotnet_root" --verify-no-changes --verbosity quiet 2>&1)
116
+ format_rc=$?
117
+
118
+ # (#717) A command that could not RUN is not a command that found problems.
119
+ # 127 = not found, 126 = not executable. Without this arm a broken or shadowed
120
+ # `dotnet` made `dotnet format` exit 127, which the code below turned into
121
+ # "127 change(s) required or analyzer finding(s)" and returned 1 -- reported to
122
+ # the user as a code-quality FAILURE of their sources. Measured before the fix:
123
+ # a stub `dotnet` exiting 127 produced exactly that message.
124
+ case "$format_rc" in
125
+ 126|127)
126
+ echo "dotnet format could not execute (exit ${format_rc}) — NOT a source verdict"
127
+ echo " The 'dotnet' on PATH is missing or not executable."
128
+ return 2
129
+ ;;
130
+ esac
131
+
132
+ if [ "$format_rc" -eq 0 ]; then
133
+ echo "FORMAT OK - dotnet format reports no changes required"
134
+ else
135
+ printf '%s\n' "$format_out" | sed -n '1,20p'
136
+ echo "dotnet format: ${format_rc} change(s) required or analyzer finding(s)"
137
+ fi
138
+
139
+ # A real finding wins over a build failure: the build failure is already
140
+ # printed above, and returning 1 here makes the caller report the stronger
141
+ # statement. Returning the build's status when analysis is clean would hide
142
+ # which of the two actually happened.
143
+ if [ "$format_rc" -ne 0 ]; then
144
+ return 1
145
+ fi
146
+ # Same reasoning for the build: 126/127 means the build never ran, so there is
147
+ # no "compiles / does not compile" answer to report.
148
+ case "$build_rc" in
149
+ 126|127) echo "dotnet build could not execute (exit ${build_rc}) — NOT a build verdict"; return 2 ;;
150
+ esac
151
+ return "$build_rc"
152
+ }
153
+
154
+ run_lint() {
155
+ run_static_analysis
156
+ }
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Tests (Gate 5)
160
+ # ---------------------------------------------------------------------------
161
+
162
+ # Succeeds when any project in the tree declares a test dependency.
163
+ #
164
+ # Without this probe `dotnet test` on a solution with no test project exits 0
165
+ # and prints nothing, which reads as "all tests passed" for a project that has
166
+ # no tests at all -- the same silent-success shape #717 is about.
167
+ _has_test_project() {
168
+ local root="${1:-.}"
169
+ find "$root" -maxdepth 3 -name "*.csproj" -type f -not -path "./.git/*" 2>/dev/null | while IFS= read -r proj; do
170
+ if grep -qiE 'Microsoft\.NET\.Test\.Sdk|Microsoft\.TestPlatform<xunit|NUnit|MSTest\.TestFramework' "$proj" 2>/dev/null; then
171
+ printf '%s\n' "$proj"
172
+ break
173
+ fi
174
+ done | sed -n '1p'
175
+ }
176
+
177
+ run_tests() {
178
+ local dotnet_root dotnet out rc
179
+ dotnet_root=$(_detect_dotnet_root) || { echo "No .NET project marker found"; return 0; }
180
+ dotnet=$(_detect_dotnet) || { echo "dotnet SDK not found"; return 2; }
181
+
182
+ local test_proj
183
+ test_proj=$(_has_test_project "$dotnet_root")
184
+ if [ -z "$test_proj" ]; then
185
+ echo "No test project found in ${dotnet_root}"
186
+ echo " A .csproj must reference Microsoft.NET.Test.Sdk / xunit / NUnit / MSTest to run under dotnet test"
187
+ return 2
188
+ fi
189
+ echo "Test project: ${test_proj}"
190
+
191
+ out=$("$dotnet" test "$test_proj" --nologo 2>&1)
192
+ rc=$?
193
+ printf '%s\n' "$out" | sed -n '1,25p'
194
+
195
+ if [ "$rc" -ne 0 ] && printf '%s\n' "$out" | grep -qiE 'Win32Exception|Access is denied|拒绝访问|test host 进程已退出|Testhost.*exited'; then
196
+ # The testhost never started, so nothing was verified. Reporting this as a
197
+ # test FAILURE would block every commit in a repo whose tests are fine;
198
+ # reporting it as PASS would be a lie. 2 is the "could not execute" channel
199
+ # the adapters already use (java.sh's G1 contract).
200
+ echo "dotnet test could not start the test host (environment fault, not a test verdict)"
201
+ echo " Known on this host: VSTest testhost aborts with Win32Exception (5) even when invoked outside git-bash"
202
+ return 2
203
+ fi
204
+ return "$rc"
205
+ }
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Coverage (Gate 5)
209
+ # ---------------------------------------------------------------------------
210
+ #
211
+ # Silently returning 0 here would let Gate 5 read "no coverage file" as "no
212
+ # coverage requirement". The output just tells the caller which artifact the
213
+ # gate is looking for; the threshold verdict stays with the caller.
214
+ run_coverage() {
215
+ local dotnet_root dotnet
216
+ dotnet_root=$(_detect_dotnet_root) || { echo "No .NET project marker found"; return 0; }
217
+ dotnet=$(_detect_dotnet) || { echo "dotnet SDK not found"; return 2; }
218
+
219
+ local test_proj
220
+ test_proj=$(_has_test_project "$dotnet_root")
221
+ if [ -z "$test_proj" ]; then
222
+ echo "No test project found in ${dotnet_root} — coverage not measurable"
223
+ return 2
224
+ fi
225
+
226
+ echo "Collecting coverage: dotnet test --collect:\"XPlat Code Coverage\""
227
+ local out rc
228
+ out=$("$dotnet" test "$test_proj" --nologo --collect:"XPlat Code Coverage" 2>&1)
229
+ rc=$?
230
+ printf '%s\n' "$out" | sed -n '1,15p'
231
+
232
+ local report
233
+ report=$(find "$dotnet_root" -name "coverage.cobertura.xml" -type f -newermt "-10 minutes" 2>/dev/null | sed -n '1p; 1q')
234
+ if [ -z "$report" ]; then
235
+ report=$(find "$dotnet_root" -name "coverage.cobertura.xml" -type f 2>/dev/null | sed -n '1p; 1q')
236
+ fi
237
+
238
+ if [ -z "$report" ]; then
239
+ echo "coverage.cobertura.xml not produced — coverage NOT measured"
240
+ echo " Requires coverlet.collector in the test project"
241
+ return 2
242
+ fi
243
+ echo "Coverage report: ${report}"
244
+ return "$rc"
245
+ }
@@ -363,7 +363,7 @@ else
363
363
  LIZARD_PATH=$(eval echo "$LIZARD_CMD")
364
364
 
365
365
  # Get changed source files for complexity check (includes test files)
366
- CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt)$' || true)
366
+ CC_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|swift|cpp|c|hpp|h|m|mm|kt|cs)$' || true)
367
367
 
368
368
  # Grade exactly the population the baseline was written from. The two must
369
369
  # match or the ratchet reports pre-existing code as "new", which is a false
@@ -76,7 +76,7 @@ if [ "$PROJECT_LANG" = "documentation-only" ]; then
76
76
 
77
77
  else
78
78
  # Get source files to check against principles
79
- PRINCIPLES_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|kt|dart|swift|cpp|c|hpp|h|m|mm)$' || true)
79
+ PRINCIPLES_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(ts|tsx|js|jsx|py|go|java|kt|dart|swift|cpp|c|hpp|h|m|mm|cs)$' || true)
80
80
 
81
81
  # One path per line, into an array. Passing $PRINCIPLES_FILES unquoted relies on
82
82
  # word splitting to turn the list into separate argv entries, which also splits
@@ -1,3 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # ⚠️ DEAD CODE — NOT EXECUTED. Read this before changing anything here.
4
+ # ============================================================================
5
+ # This file is NOT sourced by githooks/pre-commit or githooks/pre-push (both
6
+ # source count = 0; verify with: grep -c "gate-9.sh" githooks/pre-commit).
7
+ # It is shipped by githooks/install.sh but never runs.
8
+ #
9
+ # The gate that actually runs semgrep SAST is githooks/gate-10.sh, and it
10
+ # reports as `gate10_sast`. Gate 9 in the report is `gate9_build_integrity`
11
+ # (Build Integrity), a different gate entirely.
12
+ #
13
+ # This mislabelling is not cosmetic: in #717 a C# SAST fix was first written
14
+ # into THIS file, where it changed nothing, while .cs stayed ungoverned. The
15
+ # C# ruleset loading that works lives in gate-10.sh. Do not relocate it here.
16
+ #
17
+ # Kept rather than deleted because githooks/install.sh:GATE_SCRIPTS ships it;
18
+ # removing it is a separate change. See issue #717 walkthrough MC-03.
19
+ # ============================================================================
1
20
  # ============================================================================
2
21
  # GATE 9: Semgrep SAST Security Scan
3
22
  # Detects security vulnerabilities (SQL injection, XSS, etc.) in staged files
@@ -112,8 +131,22 @@ EOF
112
131
  # code is demoted to "did the tool itself run". --error stays OFF: it would
113
132
  # add nothing the JSON does not already provide, and keeping it off leaves
114
133
  # failure exit codes unambiguous for the #475 diagnostics path.
134
+ # #717: p/security-audit carries NO C# rules. Measured: a .cs file with a
135
+ # textbook SQL injection (string-concatenated SqlCommand) and a
136
+ # Process.Start command injection produced 0 findings under
137
+ # p/security-audit and 1 ERROR under p/csharp. Since gate-9.sh:45 lists
138
+ # `cs` in the file filter, .cs was accepted and then scanned by a ruleset
139
+ # that cannot see C# -- the identical "filter looks like coverage" defect
140
+ # this issue removed from Gate 4. p/csharp is loaded IN ADDITION to
141
+ # p/security-audit (not instead of: the security-audit rules still apply
142
+ # to the other languages in a mixed commit).
143
+ SEMGREP_RULES=(--config=p/security-audit)
144
+ if printf "%s
145
+ " "$SEMGREP_FILES" | grep -qE ".cs$"; then
146
+ SEMGREP_RULES+=(--config=p/csharp)
147
+ fi
115
148
  SEMGREP_STDOUT_FILE=$(mktemp)
116
- SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan --config=p/security-audit --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
149
+ SEMGREP_OUTPUT=$("$SEMGREP_CMD" scan "${SEMGREP_RULES[@]}" --json --disable-version-check "${SEMGREP_ARGS[@]}" 2>&1 >"$SEMGREP_STDOUT_FILE")
117
150
  SEMGREP_EXIT=$?
118
151
 
119
152
  # One node pass over the JSON: first line = verdict counts, remaining lines =
@@ -191,7 +224,9 @@ console.log("");});
191
224
  echo " ❌ BLOCKED — blocking-severity vulnerability found"
192
225
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
193
226
  echo "$FINDING_DETAILS"
194
- echo " Run 'semgrep scan --config=p/security-audit' to review all findings."
227
+ # #717: name BOTH rulesets. A C# finding comes from p/csharp, so
228
+ # pointing the author at p/security-audit alone reproduces zero hits.
229
+ echo " Run 'semgrep scan --config=p/security-audit --config=p/csharp' to review all findings."
195
230
  GATE_9_STATUS="FAIL"
196
231
  exit 1
197
232
  else
package/adapters/go.sh CHANGED
@@ -43,17 +43,12 @@ run_lint() {
43
43
  return $?
44
44
  }
45
45
 
46
- run_architecture() {
47
- if ! require_tool arch-go "arch-go (Go architecture checker)" 2>/dev/null; then
48
- echo "⚠ arch-go not available, installing..."
49
- go install github.com/arch-go/arch-go@latest 2>/dev/null
50
- require_tool arch-go "arch-go" || return 0
51
- fi
52
-
53
- echo "Running Go architecture checks (arch-go)..."
54
- arch-go check
55
- return $?
56
- }
46
+ # REMOVED in #688: run_architecture()
47
+ # Never invoked -- its only would-be caller, route_to_adapter(), had no callers
48
+ # either. Gate 6 implements arch-go in pre-commit (grep "arch-go"), and reads
49
+ # the checker's own exit status via PIPESTATUS[0] (#630) instead of letting a
50
+ # `| head` pipeline report head's. See githooks/adapter-common.sh for the full
51
+ # rationale.
57
52
 
58
53
  run_tests() {
59
54
  require_tool go "Go SDK" || return 1
package/adapters/iac.sh CHANGED
@@ -78,7 +78,47 @@ run_static_analysis() {
78
78
  local exit_code=0
79
79
 
80
80
  # Try checkov first (recommended - supports multiple platforms)
81
- if command -v checkov >/dev/null 2>&1; then
81
+ # Resolve to an executable that actually RUNS, not merely one that exists. On
82
+ # MSYS2/Git Bash pip leaves an extensionless `checkov` shim whose shebang
83
+ # hands a MSYS path to the Windows interpreter, which fails every time:
84
+ # can't open file 'D:\\c\\Users\\...\\Scripts\\checkov'
85
+ # `command -v checkov` still succeeds, so a presence-only probe picks the
86
+ # broken shim and the gate reports "the scanner crashed" on IaC commits that
87
+ # are perfectly fine. Probing the version is the only way to tell the two
88
+ # apart; checkov.cmd is the form that works here.
89
+ #
90
+ # A timeout wraps the probe because the failure mode being screened for is a
91
+ # shim that does not answer. Observed breakage exits 2 at once, but a hung
92
+ # wrapper must not block the commit forever. `timeout` is absent on stock
93
+ # macOS, so it is used only when available; the probe is a local version
94
+ # print either way.
95
+ local checkov_bin=""
96
+ local _checkov_candidate
97
+ local _checkov_seen_nonexec=0
98
+ local _checkov_probe
99
+ for _checkov_candidate in checkov checkov.exe checkov.cmd; do
100
+ command -v "$_checkov_candidate" >/dev/null 2>&1 || continue
101
+ if command -v timeout >/dev/null 2>&1; then
102
+ _checkov_probe="timeout 20 $_checkov_candidate --version"
103
+ else
104
+ _checkov_probe="$_checkov_candidate --version"
105
+ fi
106
+ if $_checkov_probe >/dev/null 2>&1; then
107
+ checkov_bin=$(command -v "$_checkov_candidate")
108
+ break
109
+ fi
110
+ _checkov_seen_nonexec=1
111
+ done
112
+ # "Not installed" and "installed but unusable" both end up without a scanner,
113
+ # and the fallbacks below keep the gate useful -- but they are not the same
114
+ # situation, and silently treating a broken install as absent hides a lost
115
+ # scanner. Say which one it is.
116
+ if [ -z "$checkov_bin" ] && [ "$_checkov_seen_nonexec" -eq 1 ]; then
117
+ echo " ⚠️ checkov is on PATH but none of its forms could be executed;"
118
+ echo " treating it as unusable and falling back to other scanners."
119
+ echo " Reinstall with: pip install -U checkov"
120
+ fi
121
+ if [ -n "$checkov_bin" ]; then
82
122
  echo "Running checkov IaC security scan..."
83
123
 
84
124
  # checkov 3.x exits 0 on "found failures" (we parse the output for those),
@@ -114,7 +154,7 @@ run_static_analysis() {
114
154
  echo "Scanning Terraform files..."
115
155
  for file in $tf_files; do
116
156
  if [ -f "$file" ]; then
117
- checkov --file "$file" $checkov_args >> "$checkov_log" 2>&1
157
+ "$checkov_bin" --file "$file" $checkov_args >> "$checkov_log" 2>&1
118
158
  record_checkov_run $? "$file"
119
159
  fi
120
160
  done
@@ -135,7 +175,7 @@ run_static_analysis() {
135
175
  echo "Scanning Kubernetes manifests..."
136
176
  for file in $k8s_only; do
137
177
  if [ -f "$file" ]; then
138
- checkov --file "$file" $checkov_args --framework kubernetes >> "$checkov_log" 2>&1
178
+ "$checkov_bin" --file "$file" $checkov_args --framework kubernetes >> "$checkov_log" 2>&1
139
179
  record_checkov_run $? "$file"
140
180
  fi
141
181
  done
@@ -145,7 +185,7 @@ run_static_analysis() {
145
185
  echo "Scanning GitHub Actions workflows..."
146
186
  for file in $gh_files; do
147
187
  if [ -f "$file" ]; then
148
- checkov --file "$file" $checkov_args --framework github_actions >> "$checkov_log" 2>&1
188
+ "$checkov_bin" --file "$file" $checkov_args --framework github_actions >> "$checkov_log" 2>&1
149
189
  record_checkov_run $? "$file"
150
190
  fi
151
191
  done
@@ -156,7 +196,7 @@ run_static_analysis() {
156
196
  echo "Scanning Dockerfiles..."
157
197
  for file in $docker_files; do
158
198
  if [ -f "$file" ]; then
159
- checkov --file "$file" $checkov_args >> "$checkov_log" 2>&1
199
+ "$checkov_bin" --file "$file" $checkov_args >> "$checkov_log" 2>&1
160
200
  record_checkov_run $? "$file"
161
201
  fi
162
202
  done
@@ -180,17 +180,12 @@ run_lint() {
180
180
  fi
181
181
  }
182
182
 
183
- run_architecture() {
184
- if [ ! -f ".import-linter.yml" ] && [ ! -f "import_linter_config.yml" ]; then
185
- return 0
186
- fi
187
-
188
- require_tool lint-imports "import-linter" || return 0
189
-
190
- echo "Running Python architecture checks (import-linter)..."
191
- lint-imports
192
- return $?
193
- }
183
+ # REMOVED in #688: run_architecture()
184
+ # Never invoked -- its only would-be caller, route_to_adapter(), had no callers
185
+ # either. Gate 6 implements import-linter in pre-commit (grep "import-linter"),
186
+ # and more thoroughly: it detects 4 config carriers instead of the 2 checked
187
+ # here, and BLOCKs on a missing lint-imports rather than `|| return 0` passing.
188
+ # See githooks/adapter-common.sh for the full rationale.
194
189
 
195
190
  run_tests() {
196
191
  require_tool pytest "pytest" || return 1
package/bin/xp-gate.js CHANGED
@@ -228,18 +228,25 @@ const COMMANDS = {
228
228
  'update-skill': {
229
229
  description: 'Update installed skill(s)',
230
230
  run: subargs => {
231
- // Detect --all flag BEFORE extracting name (fix #347)
232
- const allFlagIndex = subargs.indexOf('--all');
233
- let name, options;
234
- if (allFlagIndex !== -1) {
235
- // Remove --all from args before parsing, name is undefined
236
- options = parseOptions([...subargs.slice(0, allFlagIndex), ...subargs.slice(allFlagIndex + 1)], 'update-skill');
237
- options.all = true;
238
- name = undefined; // explicit: no single name when --all is used
239
- } else {
240
- name = subargs[0];
241
- options = parseOptions(subargs.slice(1), 'update-skill');
231
+ // Extract the valueless flags BEFORE taking subargs[0] as the name.
232
+ //
233
+ // #347 fixed this for --all only, so `xp-gate update-skill --check` still
234
+ // took `--check` as a skill name and failed with "--check is not
235
+ // installed" (#725). Splitting the flags out once keeps the two cases from
236
+ // drifting apart again.
237
+ // Read the flag list from SKILL_FLAGS_BY_COMMAND so a flag added there is
238
+ // never mistaken for a skill name here -- that drift is what broke #347 and
239
+ // then #725 in the same place.
240
+ const flags = SKILL_FLAGS_BY_COMMAND['update-skill'] || [];
241
+ const present = flags.filter((f) => subargs.includes(f));
242
+ const rest = subargs.filter((s) => !flags.includes(s));
243
+
244
+ const options = parseOptions(rest.slice(1), 'update-skill');
245
+ for (const flag of present) {
246
+ options[flag.replace(/^--/, '')] = true;
242
247
  }
248
+ // An explicit name wins for the single-skill path; --all/--check ignore it.
249
+ const name = present.length > 0 ? undefined : rest[0];
243
250
  updateSkill(name, options).then(code => process.exit(code));
244
251
  },
245
252
  usage: 'xp-gate update-skill [<name>] [--all] [--check]'