@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
package/hooks/pre-push CHANGED
@@ -867,116 +867,47 @@ if [ ! -f "$RESULT_FILE" ]; then
867
867
  exit 1
868
868
  fi
869
869
 
870
- # Check jq availability (MANDATORY - zero degradation)
871
- if ! command -v jq &> /dev/null; then
870
+ # Check Node.js availability (MANDATORY - zero degradation)
871
+ if ! command -v node >/dev/null 2>&1; then
872
872
  echo ""
873
873
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
874
874
  echo " ❌ ENVIRONMENT ERROR - PUSH BLOCKED"
875
875
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
876
876
  echo ""
877
- echo "jq is NOT installed. Required for JSON validation."
877
+ echo "Node.js is NOT installed. Required for JSON validation."
878
878
  echo ""
879
- echo "Install jq:"
880
- echo " apt-get install jq (Debian/Ubuntu)"
881
- echo " brew install jq (macOS)"
882
- echo " choco install jq (Windows)"
883
- echo ""
884
- echo "Zero degradation principle: Cannot proceed without jq."
879
+ echo "Install Node.js 18 or newer, then retry the push."
885
880
  echo ""
886
881
  exit 1
887
882
  fi
888
883
 
889
- # Validate result file content
890
- # SHA self-reference paradox fix: if the result file was committed as part of
891
- # HEAD, its content references HEAD^ (the commit being reviewed), not HEAD.
892
- CURRENT_COMMIT=$(git rev-parse HEAD)
893
- if git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | grep -qx "$RESULT_FILE"; then
894
- # Result file is part of the latest commit — compare against the reviewed commit
895
- EXPECTED_COMMIT=$(git rev-parse HEAD^ 2>/dev/null || echo "unknown")
896
- else
897
- # Result file is working-tree only — compare against current HEAD
898
- EXPECTED_COMMIT="$CURRENT_COMMIT"
899
- fi
884
+ # Walkthrough evidence must bind to the exact feature-branch HEAD being pushed.
885
+ EXPECTED_COMMIT=$(git rev-parse HEAD)
900
886
 
901
- # Check JSON validity
902
- if ! jq empty "$RESULT_FILE" 2>/dev/null; then
887
+ # Validate the complete evidence contract without interpolating JSON into shell.
888
+ HOOK_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
889
+ MW_VALIDATOR="$HOOK_DIR/lib/validate-code-walkthrough.cjs"
890
+ if [ ! -f "$MW_VALIDATOR" ]; then
903
891
  echo ""
904
892
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
905
893
  echo " ❌ RESULT FILE INVALID - PUSH BLOCKED"
906
894
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
907
895
  echo ""
908
- echo "$RESULT_FILE is not valid JSON."
896
+ echo "Gate MW validator is missing: $MW_VALIDATOR"
909
897
  echo ""
910
898
  echo "Re-run: /delphi-review --mode code-walkthrough"
911
899
  echo ""
912
900
  exit 1
913
901
  fi
914
902
 
915
- RESULT_COMMIT=$(jq -r '.commit' "$RESULT_FILE")
916
- RESULT_VERDICT=$(jq -r '.verdict' "$RESULT_FILE")
917
- RESULT_EXPIRES=$(jq -r '.expires' "$RESULT_FILE")
918
- RESULT_BRANCH=$(jq -r '.branch' "$RESULT_FILE")
919
903
  CURRENT_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
920
-
921
- # Branch match check (optional but recommended)
922
- if [ "$RESULT_BRANCH" != "$CURRENT_BRANCH" ]; then
923
- echo ""
924
- echo "⚠️ WARNING: Result file is for different branch"
925
- echo " Expected: $CURRENT_BRANCH"
926
- echo " Found: $RESULT_BRANCH"
927
- echo ""
928
- echo "Proceeding with commit verification..."
929
- fi
930
-
931
- # Commit match check
932
- if [ "$RESULT_COMMIT" != "$EXPECTED_COMMIT" ]; then
904
+ if ! VALIDATION_ERROR=$(node "$MW_VALIDATOR" "$RESULT_FILE" "$EXPECTED_COMMIT" "$CURRENT_BRANCH" "$CURRENT_TIME" 2>&1); then
933
905
  echo ""
934
906
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
935
907
  echo " ❌ RESULT FILE OUTDATED - PUSH BLOCKED"
936
908
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
937
909
  echo ""
938
- echo "Result file is for a different commit:"
939
- echo " Expected: $EXPECTED_COMMIT"
940
- echo " Found: $RESULT_COMMIT"
941
- echo ""
942
- echo "Re-run: /delphi-review --mode code-walkthrough"
943
- echo ""
944
- exit 1
945
- fi
946
-
947
- # Verdict check
948
- if [ "$RESULT_VERDICT" != "APPROVED" ]; then
949
- echo ""
950
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
951
- echo " ❌ GATE MW: CODE WALKTHROUGH NOT APPROVED - PUSH BLOCKED"
952
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
953
- echo ""
954
- echo "Verdict: $RESULT_VERDICT"
955
- echo ""
956
-
957
- # Show issues if available
958
- ISSUES=$(jq -r '.issues[] | "- [\(.severity)] \(.description)"' "$RESULT_FILE" 2>/dev/null)
959
- if [ -n "$ISSUES" ]; then
960
- echo "Issues found:"
961
- echo "$ISSUES"
962
- echo ""
963
- fi
964
-
965
- echo "Fix issues and re-run: /delphi-review --mode code-walkthrough"
966
- echo ""
967
- exit 1
968
- fi
969
-
970
- # Expiration check
971
- if [[ "$CURRENT_TIME" > "$RESULT_EXPIRES" ]]; then
972
- echo ""
973
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
974
- echo " ❌ RESULT FILE EXPIRED - PUSH BLOCKED"
975
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
976
- echo ""
977
- echo "Code walkthrough result has expired:"
978
- echo " Expired: $RESULT_EXPIRES"
979
- echo " Current: $CURRENT_TIME"
910
+ echo "$VALIDATION_ERROR"
980
911
  echo ""
981
912
  echo "Re-run: /delphi-review --mode code-walkthrough"
982
913
  echo ""
@@ -996,16 +927,9 @@ echo "Files changed: $FILES_CHANGED"
996
927
  echo "Lines: +$LINES_ADDED -$LINES_DELETED"
997
928
  echo ""
998
929
  echo "Code walkthrough result:"
999
- echo " Commit: $RESULT_COMMIT"
930
+ echo " Commit: $EXPECTED_COMMIT"
1000
931
  echo " Verdict: APPROVED"
1001
- echo " Expires: $RESULT_EXPIRES"
1002
-
1003
- # Show confidence if available
1004
- CONFIDENCE=$(jq -r '.confidence' "$RESULT_FILE" 2>/dev/null)
1005
- if [ -n "$CONFIDENCE" ] && [ "$CONFIDENCE" != "null" ]; then
1006
- echo " Confidence: $CONFIDENCE/10"
1007
- fi
1008
932
 
1009
933
  echo ""
1010
934
  echo "Proceeding with push..."
1011
- exit 0
935
+ exit 0
@@ -25,20 +25,58 @@ function cleanupDir(dir) {
25
25
  describe('check-version.js — REQ-001-01', () => {
26
26
  let mod;
27
27
  let origHome;
28
+ let origCacheDir;
28
29
  let tmpHome;
30
+ let tmpCacheDir;
29
31
  let origReadFileSync;
32
+ let origHttpsGet;
33
+
34
+ function installFakeHttpsGet(latestVersion) {
35
+ const https = require('https');
36
+ const saved = https.get;
37
+ https.get = (_url, options, callbackArg) => {
38
+ const callback = typeof options === 'function' ? options : callbackArg;
39
+ if (!callback) return { on: () => undefined, destroy: () => undefined };
40
+ const response = {
41
+ statusCode: 200,
42
+ on: (event, handler) => {
43
+ const eventArgs = {
44
+ data: [JSON.stringify({ latest: latestVersion })],
45
+ end: [],
46
+ };
47
+ if (Object.hasOwn(eventArgs, event)) handler(...eventArgs[event]);
48
+ return response;
49
+ },
50
+ };
51
+ callback(response);
52
+ return { on: () => undefined, destroy: () => undefined };
53
+ };
54
+ return () => {
55
+ https.get = saved;
56
+ };
57
+ }
30
58
 
31
59
  beforeEach(() => {
32
60
  vi.resetModules();
33
61
  origHome = process.env.HOME;
34
- tmpHome = fakeHome();
62
+ origCacheDir = process.env.XP_GATE_CACHE_DIR;
35
63
  origReadFileSync = fs.readFileSync;
64
+ origHttpsGet = require('https').get;
65
+ tmpHome = undefined;
66
+ tmpCacheDir = undefined;
67
+ tmpHome = fakeHome();
68
+ tmpCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ckv-cache-'));
69
+ process.env.XP_GATE_CACHE_DIR = tmpCacheDir;
36
70
  });
37
71
 
38
72
  afterEach(() => {
39
73
  process.env.HOME = origHome;
74
+ if (origCacheDir === undefined) delete process.env.XP_GATE_CACHE_DIR;
75
+ else process.env.XP_GATE_CACHE_DIR = origCacheDir;
40
76
  cleanupDir(tmpHome);
41
- fs.readFileSync = origReadFileSync;
77
+ cleanupDir(tmpCacheDir);
78
+ if (origReadFileSync) fs.readFileSync = origReadFileSync;
79
+ if (origHttpsGet) require('https').get = origHttpsGet;
42
80
  });
43
81
 
44
82
  // ──────────────────────────────────────────
@@ -159,6 +197,14 @@ describe('check-version.js — REQ-001-01', () => {
159
197
  expect(() => mod.clearCache()).not.toThrow();
160
198
  });
161
199
 
200
+ it('does not touch the home-directory cache when XP_GATE_CACHE_DIR is set', () => {
201
+ const homeCache = path.join(tmpHome, '.xp-gate', 'version-cache.json');
202
+ fs.mkdirSync(path.dirname(homeCache), { recursive: true });
203
+ fs.writeFileSync(homeCache, 'sentinel');
204
+ mod.clearCache();
205
+ expect(fs.readFileSync(homeCache, 'utf8')).toBe('sentinel');
206
+ });
207
+
162
208
  it('cachePath returns null when XP_GATE_DIR is inaccessible', () => {
163
209
  // Verify clearCache handles null cachePath gracefully
164
210
  expect(() => mod.clearCache()).not.toThrow();
@@ -216,31 +262,14 @@ describe('check-version.js — REQ-001-01', () => {
216
262
  delete require.cache[resolved];
217
263
  }
218
264
 
219
- function withMockedHttps(latestVersion, fn) {
265
+ async function withMockedHttps(latestVersion, fn) {
220
266
  const fsm = require('fs');
221
- const osm = require('os');
222
- const cpPath = require('path').join(osm.homedir(), '.xp-gate', 'version-cache.json');
267
+ const cpPath = require('path').join(process.env.XP_GATE_CACHE_DIR, 'version-cache.json');
223
268
  if (fsm.existsSync(cpPath)) {
224
269
  try { fsm.unlinkSync(cpPath); } catch { }
225
270
  }
226
271
  evictCache();
227
- const https = require('https');
228
- const saved = https.get;
229
- const body = JSON.stringify({ latest: latestVersion });
230
- https.get = (_url, _opts, cb) => {
231
- const callback = typeof _opts === 'function' ? _opts : cb;
232
- if (!callback) return { on: () => undefined, destroy: () => undefined };
233
- const mockRes = {
234
- statusCode: 200,
235
- on: (evt, handler) => {
236
- if (evt === 'data') handler(body);
237
- if (evt === 'end') handler();
238
- return mockRes;
239
- },
240
- };
241
- callback(mockRes);
242
- return { on: () => undefined, destroy: () => undefined };
243
- };
272
+ const restoreHttpsGet = installFakeHttpsGet(latestVersion);
244
273
  // Mock getLocalVersion to return a fixed version so tests don't
245
274
  // depend on the real package.json version (which drifts over time).
246
275
  const savedReadFileSync = fsm.readFileSync;
@@ -256,9 +285,9 @@ describe('check-version.js — REQ-001-01', () => {
256
285
  };
257
286
  try {
258
287
  const m = require('../check-version');
259
- return fn(m);
288
+ return await fn(m);
260
289
  } finally {
261
- https.get = saved;
290
+ restoreHttpsGet();
262
291
  fsm.readFileSync = savedReadFileSync;
263
292
  }
264
293
  }
@@ -301,39 +330,31 @@ describe('check-version.js — REQ-001-01', () => {
301
330
  delete require.cache[resolved];
302
331
  }
303
332
 
304
- function withMockedHttpsBox(latestVersion, fn) {
333
+ async function withMockedHttpsBox(latestVersion, fn) {
305
334
  const fsm = require('fs');
306
- const osm = require('os');
307
- const cpPath = require('path').join(osm.homedir(), '.xp-gate', 'version-cache.json');
335
+ const cpPath = require('path').join(process.env.XP_GATE_CACHE_DIR, 'version-cache.json');
308
336
  if (fsm.existsSync(cpPath)) {
309
337
  try { fsm.unlinkSync(cpPath); } catch { }
310
338
  }
311
339
  evictCache();
312
- const https = require('https');
313
- const saved = https.get;
314
- const body = JSON.stringify({ latest: latestVersion });
315
- https.get = (_url, _opts, cb) => {
316
- const callback = typeof _opts === 'function' ? _opts : cb;
317
- if (!callback) return { on: () => undefined, destroy: () => undefined };
318
- const mockRes = {
319
- statusCode: 200,
320
- on: (evt, handler) => {
321
- if (evt === 'data') handler(body);
322
- if (evt === 'end') handler();
323
- return mockRes;
324
- },
325
- };
326
- callback(mockRes);
327
- return { on: () => undefined, destroy: () => undefined };
328
- };
340
+ const restoreHttpsGet = installFakeHttpsGet(latestVersion);
329
341
  try {
330
342
  const m = require('../check-version');
331
- return fn(m, cpPath);
343
+ return await fn(m, cpPath);
332
344
  } finally {
333
- https.get = saved;
345
+ restoreHttpsGet();
334
346
  }
335
347
  }
336
348
 
349
+ it('restores HTTPS after an asynchronous callback rejects', async () => {
350
+ const savedGet = require('https').get;
351
+ await expect(withMockedHttpsBox('0.18.0', async () => {
352
+ await Promise.resolve();
353
+ throw new Error('callback failed');
354
+ })).rejects.toThrow('callback failed');
355
+ expect(require('https').get).toBe(savedGet);
356
+ });
357
+
337
358
  it('rejects null version from registry — cache NOT written', async () => {
338
359
  await withMockedHttpsBox(null, async (m, cpPath) => {
339
360
  const r = await m.getRemoteVersion('@test/pkg');
@@ -66,7 +66,7 @@ describe('doctor', () => {
66
66
  // and reloading it would restore the full tool list, breaking the test mock setup.
67
67
  const modulesToInvalidate = [
68
68
  '../doctor', '../uninstall', '../init', '../shared-paths',
69
- '../check-version.js',
69
+ '../check-version.js', '../doctor-tui',
70
70
  ];
71
71
  for (const mod of modulesToInvalidate) {
72
72
  try { delete require.cache[require.resolve(mod)]; } catch { /* first load */ }
@@ -126,7 +126,7 @@ describe('doctor', () => {
126
126
  fs.mkdirSync(dir, { recursive: true });
127
127
  fs.writeFileSync(
128
128
  path.join(dir, 'pre-push'),
129
- '#!/bin/bash\n# Pre-push Hook - Code Walkthrough Result Validator\n'
129
+ '#!/bin/bash\n# Pre-push Hook — 8 gates:\n'
130
130
  );
131
131
  }
132
132
 
@@ -148,7 +148,7 @@ describe('doctor', () => {
148
148
  path.join(dir, 'adapters', 'python.sh'),
149
149
  '#!/usr/bin/env bash\necho "py adapter"\n'
150
150
  );
151
- const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh'];
151
+ const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh', 'gate-10.sh'];
152
152
  for (const script of gateScripts) {
153
153
  fs.writeFileSync(path.join(dir, 'adapters', script), `#!/bin/bash\n# ${script}\n`);
154
154
  }
@@ -187,7 +187,7 @@ describe('doctor', () => {
187
187
  fs.mkdirSync(globalAdaptersDir(), { recursive: true });
188
188
  fs.writeFileSync(path.join(globalAdaptersDir(), 'typescript.sh'), '#!/usr/bin/env bash\necho "ts adapter"\n');
189
189
  fs.writeFileSync(path.join(globalAdaptersDir(), 'python.sh'), '#!/usr/bin/env bash\necho "py adapter"\n');
190
- const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh'];
190
+ const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh', 'gate-10.sh'];
191
191
  for (const script of gateScripts) {
192
192
  fs.writeFileSync(path.join(globalAdaptersDir(), script), `#!/bin/bash\n# ${script}\n`);
193
193
  }
@@ -941,7 +941,7 @@ describe('doctor', () => {
941
941
 
942
942
  function createGateScripts(dir) {
943
943
  fs.mkdirSync(dir, { recursive: true });
944
- const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh'];
944
+ const gateScripts = ['gate-3.sh', 'gate-4.sh', 'gate-7.sh', 'gate-8.sh', 'gate-9.sh', 'gate-10.sh'];
945
945
  for (const script of gateScripts) {
946
946
  fs.writeFileSync(path.join(dir, script), `#!/bin/bash\n# ${script}\n`);
947
947
  }
@@ -962,7 +962,7 @@ describe('doctor', () => {
962
962
  expect.stringContaining('Gate scripts')
963
963
  );
964
964
  expect(logSpy).toHaveBeenCalledWith(
965
- expect.stringContaining('5 gate script(s)')
965
+ expect.stringContaining('6 gate script(s)')
966
966
  );
967
967
  });
968
968
 
@@ -1005,6 +1005,7 @@ describe('doctor', () => {
1005
1005
  expect(fs.existsSync(path.join(projectAdaptersDir(), 'gate-7.sh'))).toBe(true);
1006
1006
  expect(fs.existsSync(path.join(projectAdaptersDir(), 'gate-8.sh'))).toBe(true);
1007
1007
  expect(fs.existsSync(path.join(projectAdaptersDir(), 'gate-9.sh'))).toBe(true);
1008
+ expect(fs.existsSync(path.join(projectAdaptersDir(), 'gate-10.sh'))).toBe(true);
1008
1009
  expect(logSpy).toHaveBeenCalledWith(
1009
1010
  expect.stringContaining('Restored')
1010
1011
  );
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @test REQ-standalone-gates gate-runner.js
3
+ * @intent 验证 runGateAdapter 独立运行 gate 适配器片段时能提供钩子上下文
4
+ * (gate_start_ms / record_gate_audit / now_ms / PROJECT_LANG / CHANGED_FILES),
5
+ * 避免 "command not found" 误报。
6
+ * @covers AC-STANDALONE-GATES-01, AC-STANDALONE-GATES-02, AC-STANDALONE-GATES-03, AC-STANDALONE-GATES-04, AC-STANDALONE-GATES-05
7
+ */
8
+ const fs = require('node:fs');
9
+ const path = require('node:path');
10
+ const os = require('node:os');
11
+ const { execFileSync, spawnSync } = require('node:child_process');
12
+
13
+ let tmpProject;
14
+ let tmpAdapters;
15
+
16
+ beforeEach(() => {
17
+ tmpProject = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-runner-'));
18
+ tmpAdapters = path.join(tmpProject, 'githooks', 'adapters');
19
+ fs.mkdirSync(tmpAdapters, { recursive: true });
20
+ });
21
+
22
+ afterEach(() => {
23
+ if (tmpProject && fs.existsSync(tmpProject)) {
24
+ fs.rmSync(tmpProject, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ function writeAdapterCommon() {
29
+ fs.writeFileSync(
30
+ path.join(tmpAdapters, 'adapter-common.sh'),
31
+ 'detect_project_lang() { echo "typescript"; }'
32
+ );
33
+ }
34
+
35
+ function writeGateFragment(fragmentLines) {
36
+ fs.writeFileSync(
37
+ path.join(tmpAdapters, 'gate-9.sh'),
38
+ ['# gate-9 fragment', ...fragmentLines, 'exit 0'].join('\n')
39
+ );
40
+ }
41
+
42
+ function shellParameter(expression) {
43
+ return ['$', '{', expression, '}'].join('');
44
+ }
45
+
46
+ function initializeGitProject() {
47
+ execFileSync('git', ['init', '-q'], { cwd: tmpProject });
48
+ execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: tmpProject });
49
+ execFileSync('git', ['config', 'user.name', 'XP-Gate Test'], { cwd: tmpProject });
50
+ fs.writeFileSync(path.join(tmpProject, 'tracked.ts'), 'const value = 1;\n');
51
+ execFileSync('git', ['add', 'tracked.ts'], { cwd: tmpProject });
52
+ execFileSync('git', ['-c', 'core.hooksPath=/dev/null', 'commit', '-qm', 'initial'], {
53
+ cwd: tmpProject,
54
+ });
55
+ }
56
+
57
+ // Runs runGateAdapter in a child node process (worker threads forbid process
58
+ // chdir, and runGateAdapter execs bash), capturing stdout/exit status.
59
+ function runChildAdapter(gateRunnerAbsPath, fragmentPath, cwd = tmpProject) {
60
+ const script = [
61
+ `const { runGateAdapter } = require(${JSON.stringify(gateRunnerAbsPath)});`,
62
+ `runGateAdapter(${JSON.stringify(fragmentPath)}, ${JSON.stringify(cwd)});`,
63
+ "console.log('__ADAPTER_DONE__');",
64
+ ].join('\n');
65
+ try {
66
+ const stdout = execFileSync(process.execPath, ['-e', script], {
67
+ cwd,
68
+ encoding: 'utf8',
69
+ timeout: 30000,
70
+ stdio: ['ignore', 'pipe', 'pipe'],
71
+ });
72
+ return { status: 0, stdout };
73
+ } catch (err) {
74
+ return { status: err.status ?? 1, stdout: (err.stdout || '').toString() };
75
+ }
76
+ }
77
+
78
+ describe('gate-runner.js — runGateAdapter', () => {
79
+ it('sources the fragment with hook helpers available (REQ-standalone-gates-01)', () => {
80
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
81
+ writeAdapterCommon();
82
+ writeGateFragment([
83
+ 'USES_START=$(gate_start_ms)',
84
+ `if [ -z "${shellParameter('PROJECT_LANG:-')}" ]; then exit 99; fi`,
85
+ `if [ -z "${shellParameter('CHANGED_FILES+x')}" ]; then exit 98; fi`,
86
+ 'record_gate_audit "gate-9" "sast" "PASS" "0" "$USES_START"',
87
+ ]);
88
+ const fragPath = path.join(tmpAdapters, 'gate-9.sh');
89
+ const { status, stdout } = runChildAdapter(runnerAbsPath, fragPath);
90
+ expect(status).toBe(0);
91
+ expect(stdout).toContain('__ADAPTER_DONE__');
92
+ }, 30000);
93
+
94
+ it('defines now_ms/gate_start_ms/record_gate_audit/PROJECT_LANG/CHANGED_FILES before sourcing (REQ-standalone-gates-02)', () => {
95
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
96
+ writeAdapterCommon();
97
+ writeGateFragment([
98
+ 'if ! command -v gate_start_ms >/dev/null 2>&1; then exit 97; fi',
99
+ 'if ! command -v record_gate_audit >/dev/null 2>&1; then exit 96; fi',
100
+ 'if ! command -v now_ms >/dev/null 2>&1; then exit 95; fi',
101
+ `if [ -z "${shellParameter('PROJECT_LANG:-')}" ]; then exit 94; fi`,
102
+ `if [ -z "${shellParameter('CHANGED_FILES+x')}" ]; then exit 93; fi`,
103
+ ]);
104
+ const fragPath = path.join(tmpAdapters, 'gate-9.sh');
105
+ const { status, stdout } = runChildAdapter(runnerAbsPath, fragPath);
106
+ expect(status).toBe(0);
107
+ expect(stdout).toContain('__ADAPTER_DONE__');
108
+ }, 30000);
109
+
110
+ it('sources adapter-common.sh to provide detect_project_lang (REQ-standalone-gates-03)', () => {
111
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
112
+ writeAdapterCommon();
113
+ writeGateFragment([
114
+ `if [ "${shellParameter('PROJECT_LANG:-')}" != "typescript" ]; then exit 92; fi`,
115
+ ]);
116
+ const fragPath = path.join(tmpAdapters, 'gate-9.sh');
117
+ const { status, stdout } = runChildAdapter(runnerAbsPath, fragPath);
118
+ expect(status).toBe(0);
119
+ expect(stdout).toContain('__ADAPTER_DONE__');
120
+ }, 30000);
121
+
122
+ it('propagates changed files from the project git working tree (REQ-standalone-gates-04)', () => {
123
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
124
+ writeAdapterCommon();
125
+ initializeGitProject();
126
+ fs.writeFileSync(path.join(tmpProject, 'tracked.ts'), 'const value = 2;\n');
127
+ writeGateFragment([
128
+ `if [ "${shellParameter('CHANGED_FILES')}" != "tracked.ts" ]; then exit 91; fi`,
129
+ ]);
130
+
131
+ const fragPath = path.join(tmpAdapters, 'gate-9.sh');
132
+ const { status, stdout } = runChildAdapter(runnerAbsPath, fragPath);
133
+
134
+ expect(status).toBe(0);
135
+ expect(stdout).toContain('__ADAPTER_DONE__');
136
+ }, 30000);
137
+
138
+ it('runs a standalone gate bash fallback through the public CLI (REQ-standalone-gates-05)', () => {
139
+ const packageFixture = path.join(tmpProject, 'package');
140
+ fs.cpSync(path.resolve(__dirname, '../..'), packageFixture, { recursive: true });
141
+ const cliPath = path.join(packageFixture, 'bin', 'xp-gate.js');
142
+ const gatesDir = path.join(tmpProject, 'githooks', 'gates');
143
+ fs.mkdirSync(gatesDir, { recursive: true });
144
+ fs.writeFileSync(
145
+ path.join(gatesDir, 'gate-9-public-fallback.sh'),
146
+ [
147
+ 'if ! command -v gate_start_ms >/dev/null 2>&1; then exit 90; fi',
148
+ `if [ -z "${shellParameter('CHANGED_FILES+x')}" ]; then exit 89; fi`,
149
+ 'echo "__PUBLIC_FALLBACK__"',
150
+ ].join('\n')
151
+ );
152
+
153
+ const result = spawnSync(process.execPath, [cliPath, 'check', '9'], {
154
+ cwd: tmpProject,
155
+ encoding: 'utf8',
156
+ timeout: 30000,
157
+ });
158
+
159
+ expect(result.status).toBe(0);
160
+ expect(result.stdout).toContain('__PUBLIC_FALLBACK__');
161
+ }, 30000);
162
+
163
+ it('isolates fallback resolution and git context to the requested target path (REQ-standalone-gates-06)', () => {
164
+ const packageFixture = path.join(tmpProject, 'package');
165
+ const callerProject = path.join(tmpProject, 'caller');
166
+ const targetProject = path.join(tmpProject, 'target');
167
+ fs.cpSync(path.resolve(__dirname, '../..'), packageFixture, { recursive: true });
168
+ fs.mkdirSync(path.join(callerProject, 'githooks', 'gates'), { recursive: true });
169
+ fs.mkdirSync(path.join(targetProject, 'githooks', 'gates'), { recursive: true });
170
+ fs.writeFileSync(
171
+ path.join(callerProject, 'githooks', 'gates', 'gate-9-caller.sh'),
172
+ 'echo "__CALLER_REPO__"\n'
173
+ );
174
+ fs.writeFileSync(
175
+ path.join(targetProject, 'githooks', 'gates', 'gate-9-target.sh'),
176
+ [
177
+ 'if [ "$PWD" != "$EXPECTED_TARGET" ]; then exit 88; fi',
178
+ 'echo "__TARGET_REPO__"',
179
+ ].join('\n')
180
+ );
181
+
182
+ const result = spawnSync(
183
+ process.execPath,
184
+ [path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '9', targetProject],
185
+ {
186
+ cwd: callerProject,
187
+ env: { ...process.env, EXPECTED_TARGET: targetProject },
188
+ encoding: 'utf8',
189
+ timeout: 30000,
190
+ }
191
+ );
192
+
193
+ expect(result.status).toBe(0);
194
+ expect(result.stdout).toContain('__TARGET_REPO__');
195
+ expect(result.stdout).not.toContain('__CALLER_REPO__');
196
+ }, 30000);
197
+
198
+ it('sources fragment and helper paths containing shell metacharacters literally (REQ-standalone-gates-07)', () => {
199
+ const runnerAbsPath = path.resolve(__dirname, '../gate-runner.js');
200
+ const hostileRoot = path.join(tmpProject, 'space $HOME `marker` "quoted"');
201
+ tmpAdapters = path.join(hostileRoot, 'githooks', 'adapters');
202
+ fs.mkdirSync(tmpAdapters, { recursive: true });
203
+ writeAdapterCommon();
204
+ writeGateFragment([
205
+ `if [ "${shellParameter('PROJECT_LANG')}" != "typescript" ]; then exit 87; fi`,
206
+ 'echo "__HOSTILE_PATH_OK__"',
207
+ ]);
208
+
209
+ const result = runChildAdapter(
210
+ runnerAbsPath,
211
+ path.join(tmpAdapters, 'gate-9.sh'),
212
+ hostileRoot
213
+ );
214
+
215
+ expect(result.status).toBe(0);
216
+ expect(result.stdout).toContain('__HOSTILE_PATH_OK__');
217
+ }, 30000);
218
+
219
+ it('loads parent-layout adapter-common and packaged now-ms through the public CLI (REQ-standalone-gates-08)', () => {
220
+ const packageFixture = path.join(tmpProject, 'package');
221
+ fs.cpSync(path.resolve(__dirname, '../..'), packageFixture, { recursive: true });
222
+ const gatesDir = path.join(tmpProject, 'githooks', 'gates');
223
+ fs.mkdirSync(gatesDir, { recursive: true });
224
+ fs.writeFileSync(
225
+ path.join(tmpProject, 'githooks', 'adapter-common.sh'),
226
+ 'detect_project_lang() { echo "parent-layout-language"; }\n'
227
+ );
228
+ fs.writeFileSync(
229
+ path.join(gatesDir, 'gate-9-parent-helper.sh'),
230
+ [
231
+ `if [ "${shellParameter('PROJECT_LANG')}" != "parent-layout-language" ]; then exit 86; fi`,
232
+ 'if [ "$(now_ms)" != "424242" ]; then exit 85; fi',
233
+ 'echo "__PARENT_HELPER_OK__"',
234
+ ].join('\n')
235
+ );
236
+ fs.writeFileSync(
237
+ path.join(packageFixture, 'hooks', 'lib', 'now-ms.sh'),
238
+ 'now_ms() { echo 424242; }\n'
239
+ );
240
+
241
+ const result = spawnSync(
242
+ process.execPath,
243
+ [path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '9'],
244
+ { cwd: tmpProject, encoding: 'utf8', timeout: 30000 }
245
+ );
246
+
247
+ expect(result.status).toBe(0);
248
+ expect(result.stdout).toContain('__PARENT_HELPER_OK__');
249
+ }, 30000);
250
+
251
+ it('propagates a failing fragment exit through the public CLI (REQ-standalone-gates-09)', () => {
252
+ const packageFixture = path.join(tmpProject, 'package');
253
+ const targetProject = path.join(tmpProject, 'failing-target');
254
+ fs.cpSync(path.resolve(__dirname, '../..'), packageFixture, { recursive: true });
255
+ const gatesDir = path.join(targetProject, 'githooks', 'gates');
256
+ fs.mkdirSync(gatesDir, { recursive: true });
257
+ fs.writeFileSync(path.join(gatesDir, 'gate-9-failure.sh'), 'exit 23\n');
258
+
259
+ const result = spawnSync(
260
+ process.execPath,
261
+ [path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '9', targetProject],
262
+ { cwd: tmpProject, encoding: 'utf8', timeout: 30000 }
263
+ );
264
+
265
+ expect(result.status).toBe(1);
266
+ }, 30000);
267
+ });