@boyingliu01/xp-gate 0.18.5 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/adapter-common.sh +27 -0
  2. package/adapters/gate-3.sh +0 -0
  3. package/adapters/gate-4.sh +0 -0
  4. package/adapters/gate-7.sh +0 -0
  5. package/adapters/gate-8.sh +0 -0
  6. package/adapters/gate-9.sh +0 -0
  7. package/adapters/powershell.sh +114 -35
  8. package/adapters/typescript.sh +8 -8
  9. package/build-integrity/__tests__/import-resolver.test.ts +22 -0
  10. package/build-integrity/__tests__/orchestrator.test.ts +1 -2
  11. package/build-integrity/gate-10.ts +7 -0
  12. package/gate-12-file-hygiene.sh +230 -0
  13. package/hooks/adapter-common.sh +27 -0
  14. package/hooks/gate-10.sh +105 -0
  15. package/hooks/gate-12-file-hygiene.sh +0 -0
  16. package/hooks/lib/validate-code-walkthrough.cjs +125 -0
  17. package/hooks/pre-commit +130 -33
  18. package/hooks/pre-push +15 -91
  19. package/lib/__tests__/check-version.test.js +67 -46
  20. package/lib/__tests__/doctor.test.js +7 -6
  21. package/lib/__tests__/gate-runner.test.js +267 -0
  22. package/lib/__tests__/init.test.js +32 -0
  23. package/lib/__tests__/npm-package.test.js +13 -0
  24. package/lib/__tests__/phase-transition.test.js +604 -12
  25. package/lib/__tests__/sprint-discovery.test.ts +0 -1
  26. package/lib/__tests__/sprint-status-rework.test.js +100 -1
  27. package/lib/__tests__/update-hooks.test.js +143 -10
  28. package/lib/__tests__/upgrade-exec.test.js +73 -45
  29. package/lib/__tests__/upgrade-skill-sync.test.js +1 -1
  30. package/lib/__tests__/upgrade.test.js +12 -11
  31. package/lib/doctor.js +2 -1
  32. package/lib/gate-runner.js +68 -23
  33. package/lib/init.js +34 -5
  34. package/lib/install-skill.js +1 -0
  35. package/lib/mattpocock-license-verdict.json +1 -1
  36. package/lib/phase-transition.js +203 -23
  37. package/lib/shared-paths.js +2 -0
  38. package/lib/sprint-status.js +15 -4
  39. package/lib/test-alignment.ts +3 -13
  40. package/lib/update-hooks.js +129 -15
  41. package/mock-policy/AGENTS.md +4 -4
  42. package/mock-policy/__tests__/gate-m3.test.ts +0 -5
  43. package/mock-policy/__tests__/scope-scanner.test.ts +8 -2
  44. package/mutation/AGENTS.md +3 -3
  45. package/mutation/__tests__/mutmut-runner.test.ts +443 -2
  46. package/mutation/__tests__/update-baseline.test.ts +0 -2
  47. package/mutation/runners/mutmut-runner.ts +67 -17
  48. package/package.json +9 -3
  49. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  50. package/plugins/claude-code/skills/clipboard-vision/SKILL.md +47 -0
  51. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  52. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  53. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  54. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  55. package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
  56. package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.sh +161 -0
  57. package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  58. package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  59. package/plugins/claude-code/skills/delphi-review/.delphi-config.json.example +24 -27
  60. package/plugins/claude-code/skills/delphi-review/AGENTS.md +12 -11
  61. package/plugins/claude-code/skills/delphi-review/INSTALL.md +15 -17
  62. package/plugins/claude-code/skills/delphi-review/SKILL.md +30 -29
  63. package/plugins/claude-code/skills/delphi-review/opencode.json.delphi.example +6 -10
  64. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +33 -49
  65. package/plugins/claude-code/skills/delphi-review/references/requirements.md +66 -54
  66. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +7 -7
  67. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  68. package/plugins/claude-code/skills/sprint-flow/SKILL.md +4 -4
  69. package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +2 -2
  70. package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +37 -35
  71. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +1 -1
  72. package/plugins/claude-code/skills/sprint-flow/references/phase-1-prep.md +1 -1
  73. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +24 -14
  74. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +6 -6
  75. package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +2 -2
  76. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  77. package/plugins/dsh/README.md +25 -0
  78. package/plugins/dsh/cordis.patch.yml +7 -0
  79. package/plugins/dsh/package.json +70 -0
  80. package/plugins/dsh/scripts/prepack.cjs +82 -0
  81. package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +113 -0
  82. package/plugins/dsh/skills/admin-template-guidelines/references/rule-1.md +91 -0
  83. package/plugins/dsh/skills/admin-template-guidelines/references/rule-2.md +155 -0
  84. package/plugins/dsh/skills/admin-template-guidelines/references/rule-3.md +70 -0
  85. package/plugins/dsh/skills/admin-template-guidelines/references/rule-4.md +154 -0
  86. package/plugins/dsh/skills/admin-template-guidelines/references/rule-5.md +126 -0
  87. package/plugins/dsh/skills/admin-template-guidelines/references/rule-6.md +180 -0
  88. package/plugins/dsh/skills/batch-grill-me/SKILL.md +15 -0
  89. package/plugins/dsh/skills/clipboard-vision/SKILL.md +47 -0
  90. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  91. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  92. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  93. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  94. package/plugins/dsh/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
  95. package/plugins/dsh/skills/clipboard-vision/describe-clipboard.sh +161 -0
  96. package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  97. package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  98. package/plugins/dsh/skills/delphi-review/.delphi-config.json.example +70 -0
  99. package/plugins/dsh/skills/delphi-review/AGENTS.md +67 -0
  100. package/plugins/dsh/skills/delphi-review/INSTALL.md +150 -0
  101. package/plugins/dsh/skills/delphi-review/SKILL.md +559 -0
  102. package/plugins/dsh/skills/delphi-review/hooks.md +19 -0
  103. package/plugins/dsh/skills/delphi-review/opencode.json.delphi.example +52 -0
  104. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +470 -0
  105. package/plugins/dsh/skills/delphi-review/references/orchestrator-dispatch.md +64 -0
  106. package/plugins/dsh/skills/delphi-review/references/requirements.md +402 -0
  107. package/plugins/dsh/skills/delphi-review/references/round-templates.md +134 -0
  108. package/plugins/dsh/skills/delphi-review/scripts/verify-consensus.sh +43 -0
  109. package/plugins/dsh/skills/delphi-review/tools.md +29 -0
  110. package/plugins/dsh/skills/domain-modeling/ADR-FORMAT.md +64 -0
  111. package/plugins/dsh/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  112. package/plugins/dsh/skills/domain-modeling/SKILL.md +74 -0
  113. package/plugins/dsh/skills/grill-with-docs/SKILL.md +7 -0
  114. package/plugins/dsh/skills/grilling/SKILL.md +12 -0
  115. package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +156 -0
  116. package/plugins/dsh/skills/ralph-loop/SKILL.md +198 -0
  117. package/plugins/dsh/skills/ralph-loop/references/components.md +113 -0
  118. package/plugins/dsh/skills/ralph-loop/references/phase-2-build-ralph.md +89 -0
  119. package/plugins/dsh/skills/ralph-loop/references/workflow-details.md +99 -0
  120. package/plugins/dsh/skills/ralph-loop/templates/progress-log.md +36 -0
  121. package/plugins/dsh/skills/sprint-flow/AGENTS.md +103 -0
  122. package/plugins/dsh/skills/sprint-flow/SKILL.md +411 -0
  123. package/plugins/dsh/skills/sprint-flow/__tests__/sprint-flow.test.cjs +457 -0
  124. package/plugins/dsh/skills/sprint-flow/hooks.md +18 -0
  125. package/plugins/dsh/skills/sprint-flow/references/components/memory.md +87 -0
  126. package/plugins/dsh/skills/sprint-flow/references/components/middleware.md +75 -0
  127. package/plugins/dsh/skills/sprint-flow/references/components/skill-invocations.md +105 -0
  128. package/plugins/dsh/skills/sprint-flow/references/components/system-prompt.md +27 -0
  129. package/plugins/dsh/skills/sprint-flow/references/components/tool-descriptions.md +95 -0
  130. package/plugins/dsh/skills/sprint-flow/references/force-levels.md +204 -0
  131. package/plugins/dsh/skills/sprint-flow/references/orchestration-rules.md +236 -0
  132. package/plugins/dsh/skills/sprint-flow/references/phase-1-prep.md +294 -0
  133. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +273 -0
  134. package/plugins/dsh/skills/sprint-flow/references/phase-3-build.md +348 -0
  135. package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +225 -0
  136. package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +222 -0
  137. package/plugins/dsh/skills/sprint-flow/references/phase-6-close.md +266 -0
  138. package/plugins/dsh/skills/sprint-flow/references/phase-overview.md +486 -0
  139. package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +38 -0
  140. package/plugins/dsh/skills/sprint-flow/templates/auto-estimate-learning-log.md +136 -0
  141. package/plugins/dsh/skills/sprint-flow/templates/auto-estimate-output-template.md +110 -0
  142. package/plugins/dsh/skills/sprint-flow/templates/emergent-issues-template.md +120 -0
  143. package/plugins/dsh/skills/sprint-flow/templates/pain-document-template.md +115 -0
  144. package/plugins/dsh/skills/sprint-flow/templates/sprint-progress-template.md +140 -0
  145. package/plugins/dsh/skills/sprint-flow/templates/sprint-summary-template.md +121 -0
  146. package/plugins/dsh/skills/sprint-flow/tools.md +29 -0
  147. package/plugins/dsh/skills/test-driven-development/SKILL.md +371 -0
  148. package/plugins/dsh/skills/test-driven-development/testing-anti-patterns.md +299 -0
  149. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +56 -0
  150. package/plugins/dsh/skills/test-specification-alignment/SKILL.md +205 -0
  151. package/plugins/dsh/skills/test-specification-alignment/references/alignment-verification-algorithm.md +493 -0
  152. package/plugins/dsh/skills/test-specification-alignment/references/phase2-constraint-enforcement.md +431 -0
  153. package/plugins/dsh/skills/test-specification-alignment/references/specification-format.md +348 -0
  154. package/plugins/dsh/skills/to-issues/SKILL.md +277 -0
  155. package/plugins/dsh/src/command.test.ts +102 -0
  156. package/plugins/dsh/src/command.ts +75 -0
  157. package/plugins/dsh/src/contract.test.ts +62 -0
  158. package/plugins/dsh/src/gate-runner.test.ts +100 -0
  159. package/plugins/dsh/src/gate-runner.ts +58 -0
  160. package/plugins/dsh/src/index.test.ts +97 -0
  161. package/plugins/dsh/src/index.ts +94 -0
  162. package/plugins/dsh/src/skills.test.ts +58 -0
  163. package/plugins/dsh/tsconfig.build.json +13 -0
  164. package/plugins/dsh/tsconfig.json +19 -0
  165. package/plugins/dsh/vitest.config.ts +8 -0
  166. package/plugins/opencode/package.json +1 -1
  167. package/plugins/opencode/scripts/prepack.cjs +1 -0
  168. package/plugins/opencode/skills/clipboard-vision/SKILL.md +47 -0
  169. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  170. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  171. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  172. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  173. package/plugins/opencode/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
  174. package/plugins/opencode/skills/clipboard-vision/describe-clipboard.sh +161 -0
  175. package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  176. package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  177. package/plugins/opencode/skills/delphi-review/.delphi-config.json.example +24 -27
  178. package/plugins/opencode/skills/delphi-review/AGENTS.md +12 -11
  179. package/plugins/opencode/skills/delphi-review/INSTALL.md +15 -17
  180. package/plugins/opencode/skills/delphi-review/SKILL.md +30 -29
  181. package/plugins/opencode/skills/delphi-review/opencode.json.delphi.example +6 -10
  182. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +33 -49
  183. package/plugins/opencode/skills/delphi-review/references/requirements.md +66 -54
  184. package/plugins/opencode/skills/delphi-review/references/round-templates.md +7 -7
  185. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  186. package/plugins/opencode/skills/sprint-flow/SKILL.md +4 -4
  187. package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +2 -2
  188. package/plugins/opencode/skills/sprint-flow/references/force-levels.md +37 -35
  189. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +1 -1
  190. package/plugins/opencode/skills/sprint-flow/references/phase-1-prep.md +1 -1
  191. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +24 -14
  192. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +6 -6
  193. package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +2 -2
  194. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  195. package/plugins/qoder/plugin.json +1 -1
  196. package/plugins/qoder/skills/clipboard-vision/SKILL.md +47 -0
  197. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  198. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  199. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  200. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  201. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/SKILL.md +47 -0
  202. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  203. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  204. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  205. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  206. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  207. package/plugins/qoder/skills/clipboard-vision/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  208. package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  209. package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  210. package/plugins/qoder/skills/delphi-review/.delphi-config.json.example +3 -11
  211. package/plugins/qoder/skills/delphi-review/AGENTS.md +14 -16
  212. package/plugins/qoder/skills/delphi-review/INSTALL.md +12 -12
  213. package/plugins/qoder/skills/delphi-review/SKILL.md +58 -25
  214. package/plugins/qoder/skills/delphi-review/opencode.json.delphi.example +1 -1
  215. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +33 -49
  216. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  217. package/plugins/qoder/skills/sprint-flow/SKILL.md +2 -2
  218. package/plugins/qoder/skills/sprint-flow/references/force-levels.md +38 -35
  219. package/plugins/qoder/skills/sprint-flow/references/phase-1-prep.md +1 -1
  220. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +48 -6
  221. package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +5 -6
  222. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  223. package/principles/AGENTS.md +3 -3
  224. package/scripts/__tests__/sync-package-content.test.js +30 -0
  225. package/scripts/delphi-external-review.cjs +194 -63
  226. package/scripts/sync-package-content.js +9 -2
  227. package/skills/clipboard-vision/SKILL.md +47 -0
  228. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +65 -0
  229. package/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +185 -0
  230. package/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +138 -0
  231. package/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +273 -0
  232. package/skills/clipboard-vision/describe-clipboard.ps1 +115 -0
  233. package/skills/clipboard-vision/describe-clipboard.sh +161 -0
  234. package/skills/clipboard-vision/scripts/clipboard-vision.ps1 +86 -0
  235. package/skills/clipboard-vision/scripts/clipboard-vision.sh +137 -0
  236. package/skills/delphi-review/.delphi-config.json.example +24 -27
  237. package/skills/delphi-review/AGENTS.md +12 -11
  238. package/skills/delphi-review/INSTALL.md +15 -17
  239. package/skills/delphi-review/SKILL.md +30 -29
  240. package/skills/delphi-review/opencode.json.delphi.example +6 -10
  241. package/skills/delphi-review/references/code-walkthrough.md +33 -49
  242. package/skills/delphi-review/references/requirements.md +66 -54
  243. package/skills/delphi-review/references/round-templates.md +7 -7
  244. package/skills/sprint-flow/AGENTS.md +4 -4
  245. package/skills/sprint-flow/SKILL.md +4 -4
  246. package/skills/sprint-flow/references/components/skill-invocations.md +2 -2
  247. package/skills/sprint-flow/references/force-levels.md +37 -35
  248. package/skills/sprint-flow/references/orchestration-rules.md +1 -1
  249. package/skills/sprint-flow/references/phase-1-prep.md +1 -1
  250. package/skills/sprint-flow/references/phase-2-design.md +24 -14
  251. package/skills/sprint-flow/references/phase-4-verify.md +6 -6
  252. package/skills/sprint-flow/references/phase-overview.md +2 -2
  253. package/skills/test-specification-alignment/AGENTS.md +3 -3
  254. package/sprint-gate.sh +291 -0
@@ -1,17 +1,24 @@
1
- 'use strict';
2
-
3
- const { execSync, execFileSync, spawnSync } = require('child_process');
4
- const fs = require('fs');
5
- const path = require('path');
1
+ const { execSync, execFileSync, spawnSync } = require('node:child_process');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
6
5
 
7
6
  /**
8
- * Run a bash shell script cross-platform.
9
- * On Unix: uses bash directly.
10
- * On Windows: tries bash from PATH (Git Bash / WSL), falls back to a clear message.
7
+ * Run a gate adapter fragment standalone.
8
+ *
9
+ * The gate fragments in githooks/adapters/gate-*.sh are designed to be *sourced*
10
+ * into the pre-commit hook, which pre-defines the context they rely on:
11
+ * - PROJECT_LANG (from detect_project_lang)
12
+ * - CHANGED_FILES (from git)
13
+ * - gate_start_ms() / record_gate_audit() / now_ms() audit & timing helpers
14
+ *
15
+ * When `xp-gate check` invokes a gate standalone, that context is missing and
16
+ * the fragments fail with "command not found". This wrapper reconstructs the
17
+ * same preamble the pre-commit hook sets up, then *sources* the fragment so it
18
+ * behaves exactly as it does inside the commit hook.
11
19
  */
12
- function runBashScript(scriptPath) {
20
+ function runGateAdapter(scriptPath, cwd = process.cwd()) {
13
21
  if (process.platform === 'win32') {
14
- // Check if bash is available on Windows (Git Bash, MSYS2, WSL)
15
22
  try {
16
23
  execSync('bash --version', { stdio: 'pipe', timeout: 5000 });
17
24
  } catch {
@@ -21,8 +28,44 @@ function runBashScript(scriptPath) {
21
28
  return;
22
29
  }
23
30
  }
24
- // bash is available — run the script (Unix always, or Windows after check above)
25
- execSync(`bash "${scriptPath}"`, { stdio: 'inherit' });
31
+ const scriptDir = path.dirname(scriptPath);
32
+ const commonScript = [
33
+ path.join(scriptDir, 'adapter-common.sh'),
34
+ path.join(scriptDir, '..', 'adapter-common.sh'),
35
+ path.join(cwd, 'githooks', 'adapter-common.sh'),
36
+ ].find(candidate => fs.existsSync(candidate)) || '';
37
+ const nowMsScript = [
38
+ path.join(scriptDir, 'lib', 'now-ms.sh'),
39
+ path.join(scriptDir, '..', 'lib', 'now-ms.sh'),
40
+ path.resolve(__dirname, '..', 'hooks', 'lib', 'now-ms.sh'),
41
+ ].find(candidate => fs.existsSync(candidate)) || '';
42
+ const changedFilesParameter = ['$', '{CHANGED_FILES}'].join('');
43
+
44
+ // Build the shell preamble the pre-commit hook would otherwise inject before
45
+ // sourcing the gate fragment, then source that fragment. Deliberately NOT
46
+ // set -u (the pre-commit hook does not use it), so fragments that reference
47
+ // a not-yet-defined variable fail the same way inside the commit hook.
48
+ const wrap = [
49
+ 'if [ -n "$1" ] && [ -f "$1" ]; then source "$1"; fi',
50
+ 'if [ -n "$2" ] && [ -f "$2" ]; then source "$2"; fi',
51
+ 'if ! command -v now_ms >/dev/null 2>&1; then now_ms() { "$4" -e "console.log(Date.now())"; }; fi',
52
+ 'gate_start_ms() { now_ms; }',
53
+ 'record_gate_audit() { :; }',
54
+ 'PROJECT_LANG="$(detect_project_lang 2>/dev/null || echo unknown)"',
55
+ 'CHANGED_FILES="$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)"',
56
+ `if [ -z "${changedFilesParameter}" ] && [ -n "$(git rev-parse HEAD 2>/dev/null)" ]; then`,
57
+ ' CHANGED_FILES="$(git diff HEAD --name-only --diff-filter=ACM 2>/dev/null || true)"',
58
+ 'fi',
59
+ 'source "$3"',
60
+ ].join('\n');
61
+
62
+ // execFileSync avoids an extra /bin/sh layer, so bash receives the script as a
63
+ // single argv arg (no shell-metacharacter injection through the path).
64
+ execFileSync(
65
+ 'bash',
66
+ ['--noprofile', '--norc', '-c', wrap, 'xp-gate', commonScript, nowMsScript, scriptPath, process.execPath],
67
+ { stdio: 'inherit', cwd }
68
+ );
26
69
  }
27
70
 
28
71
  // Gate metadata registry — maps gate IDs to names, descriptions, and how to run them.
@@ -35,12 +78,14 @@ function runBashScript(scriptPath) {
35
78
  * Falls back to bash script if TypeScript module not found.
36
79
  */
37
80
  function runTsGate(gateModule, targetPath) {
81
+ const targetCwd = targetPath ? path.resolve(targetPath) : process.cwd();
38
82
  const entry = findTsGateEntry(gateModule);
39
83
  if (!entry) {
40
84
  // Fallback to bash script
41
- const script = resolveGateScript(gateModule.replace('gate-', ''));
85
+ const gateNum = gateModule.replace('gate-', '');
86
+ const script = resolveGateScript(gateNum, targetCwd);
42
87
  if (script) {
43
- runBashScript(script);
88
+ runGateAdapter(script, targetCwd);
44
89
  } else {
45
90
  console.log(`Gate ${gateModule}: TypeScript module not found and no bash fallback available.`);
46
91
  }
@@ -53,7 +98,7 @@ function runTsGate(gateModule, targetPath) {
53
98
  const result = spawnSync('npx', args, {
54
99
  stdio: 'inherit',
55
100
  shell: process.platform === 'win32',
56
- cwd: targetPath || process.cwd(),
101
+ cwd: targetCwd,
57
102
  timeout: 120000,
58
103
  });
59
104
 
@@ -76,6 +121,7 @@ function findTsGateEntry(gateModule) {
76
121
  }
77
122
  return null;
78
123
  }
124
+ // allow: SIZE_OK - this module's bulk is the declarative gate registry below.
79
125
  const GATE_REGISTRY = {
80
126
  '0': {
81
127
  name: 'Version Consistency',
@@ -130,7 +176,7 @@ const GATE_REGISTRY = {
130
176
  name: 'Architecture + Boy Scout Rule',
131
177
  description: 'Architecture layer boundary validation and warning baseline enforcement',
132
178
  aliases: ['architecture', 'arch', '6'],
133
- run: (targetPath) => {
179
+ run: () => {
134
180
  const { arch } = require('./arch.js');
135
181
  return arch([]);
136
182
  },
@@ -242,17 +288,16 @@ function findConfig(basePath, fileName) {
242
288
  return null;
243
289
  }
244
290
 
245
- function resolveGateScript(gateNum) {
291
+ function resolveGateScript(gateNum, cwd = process.cwd()) {
246
292
  const candidates = [
247
- path.join(process.cwd(), 'githooks', 'gates', `gate-${gateNum}-*.sh`),
248
- path.join(process.cwd(), 'githooks', `gate-${gateNum}.sh`),
293
+ path.join(cwd, 'githooks', 'gates', `gate-${gateNum}-*.sh`),
294
+ path.join(cwd, 'githooks', `gate-${gateNum}.sh`),
249
295
  ];
250
296
 
251
297
  // Try glob patterns
252
298
  for (const pattern of candidates) {
253
299
  if (pattern.includes('*')) {
254
300
  const dir = path.dirname(pattern);
255
- const prefix = path.basename(pattern).replace('*', '');
256
301
  if (fs.existsSync(dir)) {
257
302
  const match = fs.readdirSync(dir).find(f => f.startsWith(`gate-${gateNum}-`) && f.endsWith('.sh'));
258
303
  if (match) return path.join(dir, match);
@@ -263,7 +308,7 @@ function resolveGateScript(gateNum) {
263
308
  }
264
309
 
265
310
  // Check global install
266
- const globalDir = path.join(require('os').homedir(), '.config', 'xp-gate', 'adapters');
311
+ const globalDir = path.join(os.homedir(), '.config', 'xp-gate', 'adapters');
267
312
  const globalScript = path.join(globalDir, `gate-${gateNum}.sh`);
268
313
  if (fs.existsSync(globalScript)) return globalScript;
269
314
 
@@ -289,7 +334,7 @@ async function runGate(gateId, targetPath) {
289
334
  const gate = GATE_REGISTRY[String(gateId)];
290
335
  if (!gate) {
291
336
  console.error(`Unknown gate: ${gateId}`);
292
- console.error('Available gates: ' + Object.keys(GATE_REGISTRY).join(', '));
337
+ console.error(`Available gates: ${Object.keys(GATE_REGISTRY).join(', ')}`);
293
338
  return 1;
294
339
  }
295
340
 
@@ -319,4 +364,4 @@ async function runGate(gateId, targetPath) {
319
364
  return 1;
320
365
  }
321
366
 
322
- module.exports = { GATE_REGISTRY, getGateInfo, getAllGates, runGate, resolveAlias, getAliases };
367
+ module.exports = { GATE_REGISTRY, getGateInfo, getAllGates, runGate, resolveAlias, getAliases, runGateAdapter, findTsGateEntry, resolveGateScript };
package/lib/init.js CHANGED
@@ -24,6 +24,27 @@ function copyRecursive(src, dest) {
24
24
  }
25
25
  }
26
26
 
27
+ // Third-party packages imported by the copied quality-gate modules. When the
28
+ // modules run via `npx tsx` from <CONFIG_DIR>/modules/<module>, these must be
29
+ // resolvable from an ancestor directory, so they are installed once into
30
+ // <CONFIG_DIR>/node_modules. Keep in sync with each module's imports.
31
+ const MODULE_RUNTIME_DEPS = { 'js-yaml': '^5.2.2', 'zod': '^4.4.3' };
32
+
33
+ function installModuleRuntimeDeps(configDir) {
34
+ const { execSync } = require('child_process');
35
+ const pkgPath = path.join(configDir, 'package.json');
36
+ try {
37
+ const existing = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, 'utf8')) : {};
38
+ const mergedDeps = { ...(existing.dependencies || {}), ...MODULE_RUNTIME_DEPS };
39
+ fs.writeFileSync(pkgPath, JSON.stringify({ ...existing, dependencies: mergedDeps }, null, 2) + '\n');
40
+ execSync('npm install --omit=dev --no-audit --no-fund', { cwd: configDir, stdio: ['ignore', 'inherit', 'inherit'] });
41
+ console.log(` module runtime deps -> ${path.join(configDir, 'node_modules')}`);
42
+ } catch (e) {
43
+ console.warn(` Warning: could not install module runtime deps into ${configDir} (${e.message})`);
44
+ console.warn(' Gate ML (mock-policy) may SKIP until deps are available.');
45
+ }
46
+ }
47
+
27
48
  function logDeps(depCheck) {
28
49
  if (!depCheck.ok) {
29
50
  console.warn('Warning: Missing dependencies');
@@ -471,9 +492,9 @@ async function installLocal(args) {
471
492
  copyAdapters(srcDir, path.join(projectRoot, 'githooks'));
472
493
  console.log(` adapter-common.sh + adapters -> ${projectRoot}/githooks/`);
473
494
 
474
- // Install principles/, mutation/, mock-policy/ to .xp-gate/modules/
495
+ // Install principles/, mutation/, mock-policy/, build-integrity/ to .xp-gate/modules/
475
496
  const modulesDir = path.join(projectRoot, '.xp-gate', 'modules');
476
- ['principles', 'mutation', 'mock-policy'].forEach(module => {
497
+ ['principles', 'mutation', 'mock-policy', 'build-integrity'].forEach(module => {
477
498
  const moduleSrc = path.join(srcDir, module);
478
499
  const moduleDest = path.join(modulesDir, module);
479
500
  if (fs.existsSync(moduleSrc)) {
@@ -482,6 +503,7 @@ async function installLocal(args) {
482
503
  console.log(` ${module}/ -> .xp-gate/modules/${module}/`);
483
504
  }
484
505
  });
506
+ installModuleRuntimeDeps(path.join(projectRoot, '.xp-gate'));
485
507
 
486
508
  fs.mkdirSync(TEMPLATE_DIR, { recursive: true });
487
509
  copyHooks(srcDir, TEMPLATE_DIR);
@@ -551,10 +573,10 @@ async function setupGlobal(args) {
551
573
  copyAdapters(srcDir, GLOBAL_ADAPTERS_DIR);
552
574
  console.log(` adapter-common.sh + adapters -> ${GLOBAL_ADAPTERS_DIR}`);
553
575
 
554
- // Install principles/, mutation/, mock-policy/ to global modules dir
576
+ // Install principles/, mutation/, mock-policy/, build-integrity/ to global modules dir
555
577
  console.log('[setup-global] Installing quality gate modules...');
556
578
  const globalModulesDir = path.join(CONFIG_DIR, 'modules');
557
- ['principles', 'mutation', 'mock-policy'].forEach(module => {
579
+ ['principles', 'mutation', 'mock-policy', 'build-integrity'].forEach(module => {
558
580
  const moduleSrc = path.join(srcDir, module);
559
581
  const moduleDest = path.join(globalModulesDir, module);
560
582
  if (fs.existsSync(moduleSrc)) {
@@ -564,6 +586,13 @@ async function setupGlobal(args) {
564
586
  }
565
587
  });
566
588
 
589
+ // Copied modules are pure TS sourced into <CONFIG_DIR>/modules/. Any of them
590
+ // that import third-party packages (currently only mock-policy: js-yaml, zod)
591
+ // resolve them via Node's ancestor lookup up from the module location, so we
592
+ // install those deps into <CONFIG_DIR>/node_modules once. Best-effort: a
593
+ // missing register/network must not break global setup.
594
+ installModuleRuntimeDeps(CONFIG_DIR);
595
+
567
596
  console.log('[setup-global] Configuring git...');
568
597
  const { execSync } = require('child_process');
569
598
  try {
@@ -742,4 +771,4 @@ function injectKarpathyPrinciples(projectRoot) {
742
771
  }
743
772
  }
744
773
 
745
- module.exports = { init, promptBootstrap };
774
+ module.exports = { init, promptBootstrap, installModuleRuntimeDeps };
@@ -32,6 +32,7 @@ function getCliVersion() {
32
32
  const SKILLS_REGISTRY = {
33
33
  'admin-template-guidelines': { repo: 'boyingliu01/xp-gate', path: 'skills/admin-template-guidelines' },
34
34
  'batch-grill-me': { repo: 'boyingliu01/xp-gate', path: 'skills/batch-grill-me' },
35
+ 'clipboard-vision': { repo: 'boyingliu01/xp-gate', path: 'skills/clipboard-vision' },
35
36
  'delphi-review': { repo: 'boyingliu01/xp-gate', path: 'skills/delphi-review' },
36
37
  'domain-modeling': { repo: 'boyingliu01/xp-gate', path: 'skills/domain-modeling' },
37
38
  'grill-with-docs': { repo: 'boyingliu01/xp-gate', path: 'skills/grill-with-docs' },
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-07-29T02:23:45.690Z",
5
+ "checked_at": "2026-08-30T15:25:50.166Z",
6
6
  "source": "network"
7
7
  }
@@ -25,7 +25,7 @@
25
25
  const path = require('path');
26
26
  const fs = require('fs');
27
27
  const crypto = require('crypto');
28
- const { execSync } = require('child_process');
28
+ const { execFileSync, execSync } = require('child_process');
29
29
  const { SprintStateManager } = require('./sprint-state-manager');
30
30
 
31
31
  /**
@@ -45,7 +45,10 @@ const PHASE_NAMES = {
45
45
  const EVIDENCE_FILES = {
46
46
  2: {
47
47
  path: '.sprint-state/phase-outputs/requirements-reviewed.json',
48
- requiredFields: ['verdict', 'requirements_hash'],
48
+ requiredFields: [
49
+ 'verdict', 'requirements_statement', 'timestamp', 'consensus_ratio',
50
+ 'expert_verdicts', 'head_commit', 'requirements_hash',
51
+ ],
49
52
  blockingCheck: (data) => data.verdict === 'APPROVED',
50
53
  blockingMessage: 'Requirements review verdict is not APPROVED',
51
54
  },
@@ -57,22 +60,79 @@ const EVIDENCE_FILES = {
57
60
  },
58
61
  };
59
62
 
63
+ const GIT_REPOSITORY_ENV_VARS = [
64
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS',
65
+ 'GIT_CONFIG_COUNT', 'GIT_OBJECT_DIRECTORY', 'GIT_DIR', 'GIT_WORK_TREE',
66
+ 'GIT_IMPLICIT_WORK_TREE', 'GIT_GRAFT_FILE', 'GIT_INDEX_FILE',
67
+ 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_PREFIX',
68
+ 'GIT_SHALLOW_FILE', 'GIT_COMMON_DIR',
69
+ ];
70
+
71
+ function createRepositoryGitEnv() {
72
+ const env = { ...process.env };
73
+ for (const name of GIT_REPOSITORY_ENV_VARS) {
74
+ delete env[name];
75
+ }
76
+ return env;
77
+ }
78
+
60
79
  /**
61
80
  * Get current HEAD commit hash from a project directory.
62
- * Returns 'unknown' if not in a git repo.
81
+ * Returns null if HEAD cannot be resolved.
63
82
  * @param {string} projectDir
64
- * @returns {string}
83
+ * @returns {string | null}
65
84
  */
66
85
  function getCurrentHeadCommit(projectDir) {
67
86
  try {
68
87
  return execSync('git rev-parse HEAD', {
69
88
  cwd: projectDir,
89
+ env: createRepositoryGitEnv(),
70
90
  encoding: 'utf8',
71
91
  stdio: ['pipe', 'pipe', 'pipe'],
72
92
  }).trim();
73
93
  } catch {
74
- return 'unknown';
94
+ return null;
95
+ }
96
+ }
97
+
98
+ function validateRequirementsExpertEvidence(data) {
99
+ const validationErrors = [];
100
+ if (!Array.isArray(data.expert_verdicts) || data.expert_verdicts.length !== 3) {
101
+ validationErrors.push('expert_verdicts must contain exactly 3 expert results');
102
+ return validationErrors;
103
+ }
104
+
105
+ const expectedRoles = new Set(['architecture', 'technical', 'feasibility']);
106
+ const roles = data.expert_verdicts.map(expert => expert?.role);
107
+ if (roles.some(role => !expectedRoles.has(role)) || new Set(roles).size !== expectedRoles.size) {
108
+ validationErrors.push('expert_verdicts roles must be exactly architecture, technical, and feasibility');
109
+ }
110
+ if (data.expert_verdicts.some(expert => expert?.verdict !== 'APPROVED')) {
111
+ validationErrors.push('every expert verdict must be APPROVED');
112
+ }
113
+ if (data.expert_verdicts.some(expert => expert?.result_type !== 'delphi_expert_result')) {
114
+ validationErrors.push('every expert result_type must be delphi_expert_result');
75
115
  }
116
+ if (data.expert_verdicts.some(expert =>
117
+ (expert?.error !== undefined && expert.error !== false && expert.error !== null)
118
+ || (expert?.fallback !== undefined && expert.fallback !== false && expert.fallback !== null)
119
+ )) {
120
+ validationErrors.push('expert results must not contain an error or fallback marker');
121
+ }
122
+
123
+ const requestedModels = data.expert_verdicts.map(expert =>
124
+ typeof expert?.requested_model === 'string' ? expert.requested_model.trim() : ''
125
+ );
126
+ if (requestedModels.some(model => model.length === 0)) {
127
+ validationErrors.push('every requested_model must be a non-empty trimmed string');
128
+ } else if (new Set(requestedModels).size !== requestedModels.length) {
129
+ validationErrors.push('requested_model values must be distinct after trimming');
130
+ }
131
+
132
+ if (!Number.isFinite(data.consensus_ratio) || data.consensus_ratio < 0.9 || data.consensus_ratio > 1) {
133
+ validationErrors.push('consensus_ratio must be a finite number from 0.90 through 1');
134
+ }
135
+ return validationErrors;
76
136
  }
77
137
 
78
138
  /**
@@ -85,6 +145,66 @@ function computeFileHash(filePath) {
85
145
  return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
86
146
  }
87
147
 
148
+ function parseEvidenceDate(timestamp) {
149
+ if (typeof timestamp !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(timestamp)) {
150
+ return null;
151
+ }
152
+ const parsed = new Date(timestamp);
153
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== timestamp.slice(0, 10)) {
154
+ return null;
155
+ }
156
+ return timestamp.slice(0, 10);
157
+ }
158
+
159
+ function readRequirementsContext(data, projectDir) {
160
+ const contextFile = data.context_file_used;
161
+ if (contextFile === null || contextFile === undefined || contextFile === '') {
162
+ return { content: '' };
163
+ }
164
+ if (typeof contextFile !== 'string' || path.isAbsolute(contextFile)) {
165
+ return { error: 'context_file_used must be a safe project-relative regular file path' };
166
+ }
167
+
168
+ const projectRoot = fs.realpathSync(projectDir);
169
+ const candidatePath = path.resolve(projectRoot, contextFile);
170
+ const relativeCandidate = path.relative(projectRoot, candidatePath);
171
+ if (relativeCandidate.startsWith('..') || path.isAbsolute(relativeCandidate) || !fs.existsSync(candidatePath)) {
172
+ return { error: 'context_file_used must be a safe project-relative regular file path' };
173
+ }
174
+
175
+ const realContextPath = fs.realpathSync(candidatePath);
176
+ const relativeRealPath = path.relative(projectRoot, realContextPath);
177
+ if (relativeRealPath.startsWith('..') || path.isAbsolute(relativeRealPath) || !fs.statSync(realContextPath).isFile()) {
178
+ return { error: 'context_file_used must be a safe project-relative regular file path' };
179
+ }
180
+ return { content: fs.readFileSync(realContextPath, 'utf8') };
181
+ }
182
+
183
+ function computeRequirementsHash(data, projectDir) {
184
+ if (typeof data.requirements_statement !== 'string') {
185
+ return { error: 'requirements_statement must be a string' };
186
+ }
187
+ const evidenceDate = parseEvidenceDate(data.timestamp);
188
+ if (evidenceDate === null) {
189
+ return { error: 'timestamp must be a valid UTC ISO-8601 timestamp' };
190
+ }
191
+
192
+ let context;
193
+ try {
194
+ context = readRequirementsContext(data, projectDir);
195
+ } catch {
196
+ return { error: 'context_file_used must be a safe project-relative regular file path' };
197
+ }
198
+ if (context.error) {
199
+ return context;
200
+ }
201
+ return {
202
+ hash: crypto.createHash('sha256')
203
+ .update(`${data.requirements_statement}${context.content}${evidenceDate}`, 'utf8')
204
+ .digest('hex'),
205
+ };
206
+ }
207
+
88
208
  /**
89
209
  * Validate phase evidence files for a given phase transition.
90
210
  *
@@ -149,6 +269,15 @@ function validateEvidence(phase, projectDir) {
149
269
  return { ok: false, errors, warnings };
150
270
  }
151
271
 
272
+ if (data === null || typeof data !== 'object' || Array.isArray(data)) {
273
+ const msg = `${fileName} must contain a JSON object`;
274
+ if (isLegacySprint) {
275
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
276
+ return { ok: true, errors: [], warnings };
277
+ }
278
+ return { ok: false, errors: [msg], warnings };
279
+ }
280
+
152
281
  // Check required fields
153
282
  for (const field of evidenceConfig.requiredFields) {
154
283
  if (!(field in data)) {
@@ -168,7 +297,7 @@ function validateEvidence(phase, projectDir) {
168
297
 
169
298
  // Run blocking check
170
299
  if (!evidenceConfig.blockingCheck(data)) {
171
- const msg = `${fileName}: ${evidenceConfig.blockingMessage} (got: ${JSON.stringify(data[evidenceConfig.requiredFields[0]])})`;
300
+ const msg = `${fileName}: ${evidenceConfig.blockingMessage}`;
172
301
  if (isLegacySprint) {
173
302
  warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
174
303
  return { ok: true, errors: [], warnings };
@@ -178,10 +307,16 @@ function validateEvidence(phase, projectDir) {
178
307
  }
179
308
 
180
309
  // Phase-specific anti-staleness checks
181
- if (phase === 4) {
182
- // head_commit check
310
+ if (phase === 2 || phase === 4) {
183
311
  const currentHead = getCurrentHeadCommit(projectDir);
184
- if (data.head_commit !== currentHead) {
312
+ if (currentHead === null) {
313
+ const msg = `${fileName}: unable to resolve current Git HEAD; evidence identity cannot be verified`;
314
+ if (isLegacySprint) {
315
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
316
+ } else {
317
+ errors.push(msg);
318
+ }
319
+ } else if (data.head_commit !== currentHead) {
185
320
  const msg = `${fileName}: head_commit mismatch — report has "${data.head_commit}", current HEAD is "${currentHead}"`;
186
321
  if (isLegacySprint) {
187
322
  warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
@@ -189,7 +324,9 @@ function validateEvidence(phase, projectDir) {
189
324
  errors.push(msg);
190
325
  }
191
326
  }
327
+ }
192
328
 
329
+ if (phase === 4) {
193
330
  // spec_hash check (only if specification.yaml exists)
194
331
  const specPath = path.join(projectDir, 'specification.yaml');
195
332
  if (fs.existsSync(specPath)) {
@@ -205,10 +342,42 @@ function validateEvidence(phase, projectDir) {
205
342
  }
206
343
  }
207
344
 
208
- // Phase 2: requirements_hash existence check (full verification in v0.18.0)
209
345
  if (phase === 2) {
210
- if (typeof data.requirements_hash !== 'string' || data.requirements_hash.length === 0) {
211
- const msg = `${fileName}: requirements_hash must be a non-empty string`;
346
+ if (typeof data.requirements_hash !== 'string' || !/^[a-fA-F0-9]{64}$/.test(data.requirements_hash)) {
347
+ const msg = `${fileName}: requirements_hash must be a 64-character hexadecimal SHA-256 digest`;
348
+ if (isLegacySprint) {
349
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
350
+ } else {
351
+ errors.push(msg);
352
+ }
353
+ } else {
354
+ const computedHash = computeRequirementsHash(data, projectDir);
355
+ const hashError = computedHash.error || (
356
+ data.requirements_hash.toLowerCase() !== computedHash.hash
357
+ ? 'requirements_hash mismatch; regenerate the requirements review from current inputs'
358
+ : null
359
+ );
360
+ if (hashError) {
361
+ const msg = `${fileName}: ${hashError}`;
362
+ if (isLegacySprint) {
363
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
364
+ } else {
365
+ errors.push(msg);
366
+ }
367
+ }
368
+ }
369
+ for (const validationError of validateRequirementsExpertEvidence(data)) {
370
+ const msg = `${fileName}: ${validationError}`;
371
+ if (isLegacySprint) {
372
+ warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
373
+ } else {
374
+ errors.push(msg);
375
+ }
376
+ }
377
+
378
+ const currentHead = getCurrentHeadCommit(projectDir);
379
+ if (data.head_commit !== currentHead) {
380
+ const msg = `${fileName}: head_commit mismatch — review has "${data.head_commit}", current HEAD is "${currentHead}"; regenerate the requirements review for the current commit`;
212
381
  if (isLegacySprint) {
213
382
  warnings.push(`WARNING: ${msg}. Upgrade sprint with evidence_schema_version >= 2 to enforce.`);
214
383
  } else {
@@ -240,7 +409,7 @@ function logEvidenceSkip(projectDir, phase, reason) {
240
409
  event: 'evidence_skipped',
241
410
  phase,
242
411
  reason,
243
- commit_hash: getCurrentHeadCommit(projectDir),
412
+ commit_hash: getCurrentHeadCommit(projectDir) ?? 'unknown',
244
413
  };
245
414
  fs.appendFileSync(path.join(auditDir, 'audit.jsonl'), JSON.stringify(entry) + '\n', 'utf8');
246
415
  }
@@ -539,16 +708,25 @@ function checkWalkthrough(projectDir) {
539
708
  return { ok: false, errors, warnings };
540
709
  }
541
710
 
542
- // Check commit is ancestor of HEAD
543
- if (data.commit) {
544
- try {
545
- execSync(`git merge-base --is-ancestor ${data.commit} HEAD`, {
546
- cwd: projectDir, stdio: 'pipe',
547
- });
548
- } catch {
549
- errors.push(`Code walkthrough commit ${data.commit} is not an ancestor of HEAD`);
711
+ if (typeof data.commit !== 'string' || !/^[0-9a-f]{7,64}$/i.test(data.commit)) {
712
+ errors.push('Code walkthrough commit must be a hexadecimal Git commit ID');
713
+ return { ok: false, errors, warnings };
714
+ }
715
+
716
+ try {
717
+ const currentCommit = execFileSync('git', ['rev-parse', 'HEAD'], {
718
+ cwd: projectDir,
719
+ env: createRepositoryGitEnv(),
720
+ encoding: 'utf8',
721
+ stdio: 'pipe',
722
+ }).trim();
723
+ if (data.commit !== currentCommit) {
724
+ errors.push(`Code walkthrough commit ${data.commit} does not match HEAD ${currentCommit}`);
550
725
  return { ok: false, errors, warnings };
551
726
  }
727
+ } catch {
728
+ errors.push('Unable to resolve the current Git commit for code walkthrough validation');
729
+ return { ok: false, errors, warnings };
552
730
  }
553
731
 
554
732
  // Check expiration
@@ -585,8 +763,10 @@ function checkBypassAudit(projectDir) {
585
763
  if (entry.type === 'precommit_bypass' && entry.commit) {
586
764
  // Verify the commit is on the current branch
587
765
  try {
588
- execSync(`git merge-base --is-ancestor ${entry.commit} HEAD`, {
589
- cwd: projectDir, stdio: 'pipe',
766
+ execFileSync('git', ['merge-base', '--is-ancestor', entry.commit, 'HEAD'], {
767
+ cwd: projectDir,
768
+ env: createRepositoryGitEnv(),
769
+ stdio: 'pipe',
590
770
  });
591
771
  bypassedCommits.push(entry.commit);
592
772
  } catch {
@@ -20,6 +20,7 @@ const CONFIG_DIR = path.join(HOME_DIR, '.config', 'xp-gate');
20
20
  const CONFIG_FILE = path.join(CONFIG_DIR, 'xp-gate.json');
21
21
  const GLOBAL_HOOKS_DIR = path.join(CONFIG_DIR, 'hooks');
22
22
  const GLOBAL_ADAPTERS_DIR = path.join(CONFIG_DIR, 'adapters');
23
+ const GLOBAL_MODULES_DIR = path.join(CONFIG_DIR, 'modules');
23
24
 
24
25
  /**
25
26
  * Detect which AI agent platform is currently in use.
@@ -61,6 +62,7 @@ module.exports = {
61
62
  TEMPLATE_DIR,
62
63
  GLOBAL_HOOKS_DIR,
63
64
  GLOBAL_ADAPTERS_DIR,
65
+ GLOBAL_MODULES_DIR,
64
66
  detectPlatform,
65
67
  getTemplateDir,
66
68
  };
@@ -9,7 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
- const { execSync } = require('child_process');
12
+ const { execFileSync } = require('child_process');
13
13
  const { discoverActiveSprints } = require('./sprint-discovery');
14
14
  const { SprintStateManager } = require('./sprint-state-manager');
15
15
 
@@ -229,9 +229,20 @@ const FIX_RE = /^(fix(\(.+\))?:)|\b(fix|bugfix|hotfix|patch|修复)\b/i;
229
229
  */
230
230
  function getFixCommitCount(repoDir, sinceDate) {
231
231
  try {
232
- const out = execSync(
233
- `git log --all --since="${sinceDate}" --pretty=format:"%s"`,
234
- { cwd: repoDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
232
+ const gitEnv = { ...process.env };
233
+ for (const name of [
234
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS',
235
+ 'GIT_CONFIG_COUNT', 'GIT_OBJECT_DIRECTORY', 'GIT_DIR', 'GIT_WORK_TREE',
236
+ 'GIT_IMPLICIT_WORK_TREE', 'GIT_GRAFT_FILE', 'GIT_INDEX_FILE',
237
+ 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_PREFIX',
238
+ 'GIT_SHALLOW_FILE', 'GIT_COMMON_DIR',
239
+ ]) {
240
+ delete gitEnv[name];
241
+ }
242
+ const out = execFileSync(
243
+ 'git',
244
+ ['log', '--all', '--since', sinceDate, '--pretty=format:%s'],
245
+ { cwd: repoDir, env: gitEnv, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
235
246
  );
236
247
  return out.split('\n').filter(l => l.trim() && FIX_RE.test(l.trim())).length;
237
248
  } catch {