@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
@@ -9,10 +9,49 @@
9
9
  */
10
10
 
11
11
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
12
+ import { execSync } from 'node:child_process';
13
+ import crypto from 'node:crypto';
12
14
  import fs from 'node:fs';
13
- import path from 'node:path';
14
15
  import os from 'node:os';
15
- import { handlePhaseTransition, renderDashboard, PHASE_NAMES, validateEvidence } from '../phase-transition.js';
16
+ import path from 'node:path';
17
+ import {
18
+ handlePhaseTransition,
19
+ renderDashboard,
20
+ PHASE_NAMES,
21
+ validateEvidence,
22
+ checkWalkthrough,
23
+ checkBypassAudit,
24
+ } from '../phase-transition.js';
25
+
26
+ function git(command, cwd) {
27
+ const env = { ...process.env };
28
+ for (const name of [
29
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS',
30
+ 'GIT_CONFIG_COUNT', 'GIT_OBJECT_DIRECTORY', 'GIT_DIR', 'GIT_WORK_TREE',
31
+ 'GIT_IMPLICIT_WORK_TREE', 'GIT_GRAFT_FILE', 'GIT_INDEX_FILE',
32
+ 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_PREFIX',
33
+ 'GIT_SHALLOW_FILE', 'GIT_COMMON_DIR',
34
+ ]) {
35
+ delete env[name];
36
+ }
37
+ return execSync(`git ${command}`, {
38
+ cwd,
39
+ env,
40
+ encoding: 'utf8',
41
+ stdio: ['pipe', 'pipe', 'pipe'],
42
+ }).trim();
43
+ }
44
+
45
+ function createRepository(dir, message) {
46
+ git('init --quiet --template=', dir);
47
+ git('config core.hooksPath /dev/null', dir);
48
+ git('config user.email "test@test.com"', dir);
49
+ git('config user.name "Test"', dir);
50
+ fs.writeFileSync(path.join(dir, 'file.txt'), message);
51
+ git('add file.txt', dir);
52
+ git(`commit --quiet -m "${message}"`, dir);
53
+ return git('rev-parse HEAD', dir);
54
+ }
16
55
 
17
56
  describe('phase-transition', () => {
18
57
  let tmpDir;
@@ -27,22 +66,42 @@ describe('phase-transition', () => {
27
66
 
28
67
  /** Helper: write phase 2 evidence (requirements-reviewed.json) so phase 2 completed passes */
29
68
  function writePhase2Evidence(dir) {
69
+ const headCommit = fs.existsSync(path.join(dir, '.git'))
70
+ ? git('rev-parse HEAD', dir)
71
+ : createRepository(dir, 'phase 2 evidence');
72
+ const requirementsStatement = 'Phase 2 evidence fixture';
73
+ const timestamp = new Date().toISOString();
74
+ const requirementsHash = crypto.createHash('sha256')
75
+ .update(`${requirementsStatement}${timestamp.slice(0, 10)}`, 'utf8')
76
+ .digest('hex');
30
77
  const outputsDir = path.join(dir, '.sprint-state', 'phase-outputs');
31
78
  fs.mkdirSync(outputsDir, { recursive: true });
32
79
  fs.writeFileSync(path.join(outputsDir, 'requirements-reviewed.json'), JSON.stringify({
33
80
  verdict: 'APPROVED',
34
- requirements_hash: 'test-hash-placeholder',
35
- timestamp: new Date().toISOString(),
81
+ requirements_statement: requirementsStatement,
82
+ context_file_used: null,
83
+ requirements_hash: requirementsHash,
84
+ head_commit: headCommit,
85
+ consensus_ratio: 0.9,
86
+ expert_verdicts: [
87
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
88
+ { role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
89
+ { role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
90
+ ],
91
+ timestamp,
36
92
  }));
37
93
  }
38
94
 
39
95
  /** Helper: write phase 4 evidence (test-alignment-report.json) so phase 4 completed passes */
40
96
  function writePhase4Evidence(dir) {
97
+ const headCommit = fs.existsSync(path.join(dir, '.git'))
98
+ ? git('rev-parse HEAD', dir)
99
+ : createRepository(dir, 'phase 4 evidence');
41
100
  const outputsDir = path.join(dir, '.sprint-state', 'phase-outputs');
42
101
  fs.mkdirSync(outputsDir, { recursive: true });
43
102
  fs.writeFileSync(path.join(outputsDir, 'test-alignment-report.json'), JSON.stringify({
44
103
  alignment_status: 'PASS',
45
- head_commit: 'unknown',
104
+ head_commit: headCommit,
46
105
  spec_hash: null,
47
106
  timestamp: new Date().toISOString(),
48
107
  }));
@@ -196,6 +255,138 @@ describe('phase-transition', () => {
196
255
  expect(result).toContain('sprint-min');
197
256
  });
198
257
  });
258
+
259
+ describe('repository-scoped Git checks', () => {
260
+ it('checks walkthrough ancestry in projectDir when hook Git variables point elsewhere', () => {
261
+ const outerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-outer-'));
262
+ const targetCommit = createRepository(tmpDir, 'target commit');
263
+ createRepository(outerDir, 'outer commit');
264
+ fs.writeFileSync(path.join(tmpDir, '.code-walkthrough-result.json'), JSON.stringify({
265
+ verdict: 'APPROVED',
266
+ commit: targetCommit,
267
+ expires: new Date(Date.now() + 3600000).toISOString(),
268
+ }));
269
+ const previousGitDir = process.env.GIT_DIR;
270
+ const previousWorkTree = process.env.GIT_WORK_TREE;
271
+ process.env.GIT_DIR = path.join(outerDir, '.git');
272
+ process.env.GIT_WORK_TREE = outerDir;
273
+
274
+ try {
275
+ expect(checkWalkthrough(tmpDir).ok).toBe(true);
276
+ } finally {
277
+ if (previousGitDir === undefined) delete process.env.GIT_DIR;
278
+ else process.env.GIT_DIR = previousGitDir;
279
+ if (previousWorkTree === undefined) delete process.env.GIT_WORK_TREE;
280
+ else process.env.GIT_WORK_TREE = previousWorkTree;
281
+ fs.rmSync(outerDir, { recursive: true, force: true });
282
+ }
283
+ });
284
+
285
+ it('rejects a walkthrough bound to an older ancestor commit', () => {
286
+ const reviewedCommit = createRepository(tmpDir, 'reviewed commit');
287
+ fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'new unreviewed change');
288
+ git('add file.txt', tmpDir);
289
+ git('commit --quiet -m "unreviewed commit"', tmpDir);
290
+ fs.writeFileSync(path.join(tmpDir, '.code-walkthrough-result.json'), JSON.stringify({
291
+ verdict: 'APPROVED',
292
+ commit: reviewedCommit,
293
+ expires: new Date(Date.now() + 3600000).toISOString(),
294
+ }));
295
+
296
+ expect(checkWalkthrough(tmpDir).ok).toBe(false);
297
+ });
298
+
299
+ it('accepts a walkthrough bound to the current HEAD commit', () => {
300
+ const reviewedCommit = createRepository(tmpDir, 'reviewed commit');
301
+ fs.writeFileSync(path.join(tmpDir, '.code-walkthrough-result.json'), JSON.stringify({
302
+ verdict: 'APPROVED',
303
+ commit: reviewedCommit,
304
+ expires: new Date(Date.now() + 3600000).toISOString(),
305
+ }));
306
+
307
+ expect(checkWalkthrough(tmpDir).ok).toBe(true);
308
+ });
309
+
310
+ it('checks bypass commits in projectDir when hook Git variables point elsewhere', () => {
311
+ const outerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'phase-outer-'));
312
+ const targetCommit = createRepository(tmpDir, 'target bypass');
313
+ createRepository(outerDir, 'outer commit');
314
+ const auditDir = path.join(tmpDir, '.xp-gate');
315
+ fs.mkdirSync(auditDir, { recursive: true });
316
+ fs.writeFileSync(path.join(auditDir, 'bypass-audit.jsonl'), JSON.stringify({
317
+ type: 'precommit_bypass',
318
+ commit: targetCommit,
319
+ }));
320
+ const previousGitDir = process.env.GIT_DIR;
321
+ const previousWorkTree = process.env.GIT_WORK_TREE;
322
+ process.env.GIT_DIR = path.join(outerDir, '.git');
323
+ process.env.GIT_WORK_TREE = outerDir;
324
+
325
+ try {
326
+ const result = checkBypassAudit(tmpDir);
327
+ expect(result.ok).toBe(false);
328
+ expect(result.bypassedCommits).toEqual([targetCommit]);
329
+ } finally {
330
+ if (previousGitDir === undefined) delete process.env.GIT_DIR;
331
+ else process.env.GIT_DIR = previousGitDir;
332
+ if (previousWorkTree === undefined) delete process.env.GIT_WORK_TREE;
333
+ else process.env.GIT_WORK_TREE = previousWorkTree;
334
+ fs.rmSync(outerDir, { recursive: true, force: true });
335
+ }
336
+ });
337
+
338
+ it('does not execute shell syntax from a walkthrough commit value', () => {
339
+ const targetCommit = createRepository(tmpDir, 'target commit');
340
+ const markerFile = path.join(tmpDir, 'walkthrough-injection-marker');
341
+ fs.writeFileSync(path.join(tmpDir, '.code-walkthrough-result.json'), JSON.stringify({
342
+ verdict: 'APPROVED',
343
+ commit: `${targetCommit}; touch ${markerFile}; #`,
344
+ expires: new Date(Date.now() + 3600000).toISOString(),
345
+ }));
346
+
347
+ const result = checkWalkthrough(tmpDir);
348
+
349
+ expect(result.ok).toBe(false);
350
+ expect(fs.existsSync(markerFile)).toBe(false);
351
+ });
352
+
353
+ it.each([
354
+ ['missing', undefined],
355
+ ['null', null],
356
+ ['numeric', 123],
357
+ ['empty', ''],
358
+ ['malformed', 'not-a-commit'],
359
+ ])('rejects a %s walkthrough commit', (_label, commit) => {
360
+ createRepository(tmpDir, 'target commit');
361
+ const walkthrough = {
362
+ verdict: 'APPROVED',
363
+ expires: new Date(Date.now() + 3600000).toISOString(),
364
+ };
365
+ if (commit !== undefined) walkthrough.commit = commit;
366
+ fs.writeFileSync(
367
+ path.join(tmpDir, '.code-walkthrough-result.json'),
368
+ JSON.stringify(walkthrough)
369
+ );
370
+
371
+ expect(checkWalkthrough(tmpDir).ok).toBe(false);
372
+ });
373
+
374
+ it('does not execute shell syntax from a bypass-audit commit value', () => {
375
+ const targetCommit = createRepository(tmpDir, 'target bypass');
376
+ const markerFile = path.join(tmpDir, 'bypass-injection-marker');
377
+ const auditDir = path.join(tmpDir, '.xp-gate');
378
+ fs.mkdirSync(auditDir, { recursive: true });
379
+ fs.writeFileSync(path.join(auditDir, 'bypass-audit.jsonl'), JSON.stringify({
380
+ type: 'precommit_bypass',
381
+ commit: `${targetCommit}; touch ${markerFile}; #`,
382
+ }));
383
+
384
+ const result = checkBypassAudit(tmpDir);
385
+
386
+ expect(result.bypassedCommits).toEqual([]);
387
+ expect(fs.existsSync(markerFile)).toBe(false);
388
+ });
389
+ });
199
390
  describe('Layer 1: Pre-transition gate check', () => {
200
391
  it('warns when previous phase not completed', async () => {
201
392
  // Set up: Phase 1 completed, then try Phase 3 in_progress (skipping Phase 2)
@@ -333,6 +524,375 @@ describe('phase-transition', () => {
333
524
  fs.writeFileSync(path.join(outputsDir, 'test-alignment-report.json'), JSON.stringify(report, null, 2));
334
525
  }
335
526
 
527
+ function writeRequirementsReview(dir, report) {
528
+ const outputsDir = path.join(dir, '.sprint-state', 'phase-outputs');
529
+ fs.mkdirSync(outputsDir, { recursive: true });
530
+ fs.writeFileSync(path.join(outputsDir, 'requirements-reviewed.json'), JSON.stringify(report, null, 2));
531
+ }
532
+
533
+ function validRequirementsReview(dir, overrides = {}) {
534
+ const headCommit = fs.existsSync(path.join(dir, '.git'))
535
+ ? git('rev-parse HEAD', dir)
536
+ : createRepository(dir, 'reviewed requirements');
537
+ const review = {
538
+ verdict: 'APPROVED',
539
+ requirements_statement: 'The release must preserve current quality gates.',
540
+ context_file_used: null,
541
+ timestamp: '2026-08-20T10:30:00.000Z',
542
+ head_commit: headCommit,
543
+ consensus_ratio: 0.9,
544
+ expert_verdicts: [
545
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
546
+ { role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
547
+ { role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
548
+ ],
549
+ ...overrides,
550
+ };
551
+ const contextContent = review.context_file_used
552
+ ? fs.readFileSync(path.join(dir, review.context_file_used), 'utf8')
553
+ : '';
554
+ return {
555
+ ...review,
556
+ requirements_hash: overrides.requirements_hash ?? crypto
557
+ .createHash('sha256')
558
+ .update(`${review.requirements_statement}${contextContent}${review.timestamp.slice(0, 10)}`, 'utf8')
559
+ .digest('hex'),
560
+ };
561
+ }
562
+
563
+ it('BLOCKs schema-v2 requirements evidence without head_commit', () => {
564
+ createSprintState(tmpDir, 2);
565
+ writeRequirementsReview(tmpDir, {
566
+ verdict: 'APPROVED',
567
+ requirements_hash: 'requirements-v1',
568
+ });
569
+
570
+ const result = validateEvidence(2, tmpDir);
571
+
572
+ expect(result.ok).toBe(false);
573
+ expect(result.errors.some(error => error.includes('head_commit'))).toBe(true);
574
+ });
575
+
576
+ it('BLOCKs schema-v2 requirements evidence bound to a stale HEAD', () => {
577
+ const reviewedCommit = createRepository(tmpDir, 'reviewed requirements');
578
+ fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'requirements changed');
579
+ git('add file.txt', tmpDir);
580
+ git('commit --quiet -m "changed requirements"', tmpDir);
581
+ createSprintState(tmpDir, 2);
582
+ writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, { head_commit: reviewedCommit }));
583
+
584
+ const result = validateEvidence(2, tmpDir);
585
+
586
+ expect(result.ok).toBe(false);
587
+ expect(result.errors.some(error => error.includes('head_commit mismatch'))).toBe(true);
588
+ });
589
+
590
+ it('accepts schema-v2 requirements evidence bound to the current HEAD', () => {
591
+ createSprintState(tmpDir, 2);
592
+ writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir));
593
+
594
+ const result = validateEvidence(2, tmpDir);
595
+
596
+ expect(result.ok).toBe(true);
597
+ expect(result.errors).toHaveLength(0);
598
+ });
599
+
600
+ it.each([
601
+ ['requirements_statement', { requirements_statement: undefined }],
602
+ ['timestamp', { timestamp: undefined }],
603
+ ['consensus_ratio', { consensus_ratio: undefined }],
604
+ ['expert_verdicts', { expert_verdicts: undefined }],
605
+ ['head_commit', { head_commit: undefined }],
606
+ ['requirements_hash', { requirements_hash: undefined }],
607
+ ])('BLOCKs schema-v2 requirements evidence missing %s', (field, overrides) => {
608
+ createSprintState(tmpDir, 2);
609
+ const evidence = validRequirementsReview(tmpDir);
610
+ delete evidence[field];
611
+ writeRequirementsReview(tmpDir, { ...evidence, ...overrides });
612
+
613
+ const result = validateEvidence(2, tmpDir);
614
+
615
+ expect(result.ok).toBe(false);
616
+ expect(result.errors.some(error => error.includes(field))).toBe(true);
617
+ });
618
+
619
+ it.each([
620
+ ['non-hex hash', 'requirements-v1'],
621
+ ['short hash', 'abc123'],
622
+ ['valid-looking stale hash', '0'.repeat(64)],
623
+ ])('BLOCKs schema-v2 requirements evidence with %s', (_label, requirementsHash) => {
624
+ createSprintState(tmpDir, 2);
625
+ writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, {
626
+ requirements_hash: requirementsHash,
627
+ }));
628
+
629
+ const result = validateEvidence(2, tmpDir);
630
+
631
+ expect(result.ok).toBe(false);
632
+ expect(result.errors.some(error => error.includes('requirements_hash'))).toBe(true);
633
+ });
634
+
635
+ it('accepts an uppercase recomputable requirements hash', () => {
636
+ createSprintState(tmpDir, 2);
637
+ const evidence = validRequirementsReview(tmpDir);
638
+ evidence.requirements_hash = evidence.requirements_hash.toUpperCase();
639
+ writeRequirementsReview(tmpDir, evidence);
640
+
641
+ expect(validateEvidence(2, tmpDir).ok).toBe(true);
642
+ });
643
+
644
+ it('BLOCKs when requirements_statement changes after hashing', () => {
645
+ createSprintState(tmpDir, 2);
646
+ const evidence = validRequirementsReview(tmpDir);
647
+ evidence.requirements_statement = 'Changed after review.';
648
+ writeRequirementsReview(tmpDir, evidence);
649
+
650
+ const result = validateEvidence(2, tmpDir);
651
+
652
+ expect(result.ok).toBe(false);
653
+ expect(result.errors.some(error => error.includes('requirements_hash mismatch'))).toBe(true);
654
+ });
655
+
656
+ it('accepts a recomputable hash using a project-relative context file', () => {
657
+ createSprintState(tmpDir, 2);
658
+ fs.writeFileSync(path.join(tmpDir, 'CONTEXT.md'), 'bounded context\n', 'utf8');
659
+ writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, {
660
+ context_file_used: 'CONTEXT.md',
661
+ }));
662
+
663
+ expect(validateEvidence(2, tmpDir).ok).toBe(true);
664
+ });
665
+
666
+ it('BLOCKs when context file content changes after hashing', () => {
667
+ createSprintState(tmpDir, 2);
668
+ fs.writeFileSync(path.join(tmpDir, 'CONTEXT.md'), 'original context\n', 'utf8');
669
+ const evidence = validRequirementsReview(tmpDir, { context_file_used: 'CONTEXT.md' });
670
+ fs.writeFileSync(path.join(tmpDir, 'CONTEXT.md'), 'changed context\n', 'utf8');
671
+ writeRequirementsReview(tmpDir, evidence);
672
+
673
+ const result = validateEvidence(2, tmpDir);
674
+
675
+ expect(result.ok).toBe(false);
676
+ expect(result.errors.some(error => error.includes('requirements_hash mismatch'))).toBe(true);
677
+ });
678
+
679
+ it.each([
680
+ ['missing path', 'missing/CONTEXT.md'],
681
+ ['absolute path', '/tmp/CONTEXT.md'],
682
+ ['traversal path', '../CONTEXT.md'],
683
+ ])('BLOCKs schema-v2 requirements evidence with %s context', (_label, contextFileUsed) => {
684
+ createSprintState(tmpDir, 2);
685
+ const evidence = validRequirementsReview(tmpDir);
686
+ evidence.context_file_used = contextFileUsed;
687
+ writeRequirementsReview(tmpDir, evidence);
688
+
689
+ const result = validateEvidence(2, tmpDir);
690
+
691
+ expect(result.ok).toBe(false);
692
+ expect(result.errors.some(error => error.includes('context_file_used'))).toBe(true);
693
+ });
694
+
695
+ it('BLOCKs a context symlink that resolves outside the project root', () => {
696
+ createSprintState(tmpDir, 2);
697
+ const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'requirements-context-'));
698
+ const outsideFile = path.join(outsideDir, 'CONTEXT.md');
699
+ fs.writeFileSync(outsideFile, 'outside context\n', 'utf8');
700
+ fs.symlinkSync(outsideFile, path.join(tmpDir, 'CONTEXT.md'));
701
+ const evidence = validRequirementsReview(tmpDir, { context_file_used: 'CONTEXT.md' });
702
+ writeRequirementsReview(tmpDir, evidence);
703
+
704
+ const result = validateEvidence(2, tmpDir);
705
+
706
+ expect(result.ok).toBe(false);
707
+ expect(result.errors.some(error => error.includes('context_file_used'))).toBe(true);
708
+ fs.rmSync(outsideDir, { recursive: true, force: true });
709
+ });
710
+
711
+ it.each([
712
+ ['invalid timestamp', 'not-a-timestamp'],
713
+ ['invalid calendar date', '2026-02-30T10:30:00.000Z'],
714
+ ])('BLOCKs schema-v2 requirements evidence with %s', (_label, timestamp) => {
715
+ createSprintState(tmpDir, 2);
716
+ const evidence = validRequirementsReview(tmpDir);
717
+ evidence.timestamp = timestamp;
718
+ writeRequirementsReview(tmpDir, evidence);
719
+
720
+ const result = validateEvidence(2, tmpDir);
721
+
722
+ expect(result.ok).toBe(false);
723
+ expect(result.errors.some(error => error.includes('timestamp'))).toBe(true);
724
+ });
725
+
726
+ it.each([
727
+ ['missing expert_verdicts', { expert_verdicts: undefined }, 'expert_verdicts'],
728
+ ['too few expert_verdicts', { expert_verdicts: [
729
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
730
+ { role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
731
+ ] }, 'exactly 3'],
732
+ ['too many expert_verdicts', { expert_verdicts: [
733
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
734
+ { role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
735
+ { role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
736
+ { role: 'extra', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-d' },
737
+ ] }, 'exactly 3'],
738
+ ['duplicate role', { expert_verdicts: [
739
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
740
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
741
+ { role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
742
+ ] }, 'roles'],
743
+ ['missing role', { expert_verdicts: [
744
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
745
+ { role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
746
+ { verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
747
+ ] }, 'roles'],
748
+ ['non-approved expert verdict', { expert_verdicts: [
749
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
750
+ { role: 'technical', verdict: 'REQUEST_CHANGES', result_type: 'delphi_expert_result', requested_model: 'model-b' },
751
+ { role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
752
+ ] }, 'APPROVED'],
753
+ ['wrong result_type', { expert_verdicts: [
754
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'summary', requested_model: 'model-a' },
755
+ { role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
756
+ { role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
757
+ ] }, 'result_type'],
758
+ ['blank requested_model', { expert_verdicts: [
759
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: ' ' },
760
+ { role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
761
+ { role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
762
+ ] }, 'requested_model'],
763
+ ['duplicate trimmed requested_model', { expert_verdicts: [
764
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a' },
765
+ { role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: ' model-a ' },
766
+ { role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
767
+ ] }, 'distinct'],
768
+ ])('BLOCKs schema-v2 requirements evidence with %s', (_label, overrides, expectedError) => {
769
+ createSprintState(tmpDir, 2);
770
+ writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, overrides));
771
+
772
+ const result = validateEvidence(2, tmpDir);
773
+
774
+ expect(result.ok).toBe(false);
775
+ expect(result.errors.some(error => error.includes(expectedError))).toBe(true);
776
+ });
777
+
778
+ it.each([
779
+ ['error true', { error: true }],
780
+ ['error string', { error: 'provider failed' }],
781
+ ['error false-shaped string', { error: 'false' }],
782
+ ['fallback true', { fallback: true }],
783
+ ['fallback object', { fallback: { provider: 'local' } }],
784
+ ['fallback false-shaped number', { fallback: 0 }],
785
+ ])('BLOCKs schema-v2 requirements evidence with an expert %s marker', (_label, marker) => {
786
+ createSprintState(tmpDir, 2);
787
+ const evidence = validRequirementsReview(tmpDir);
788
+ Object.assign(evidence.expert_verdicts[0], marker);
789
+ writeRequirementsReview(tmpDir, evidence);
790
+
791
+ const result = validateEvidence(2, tmpDir);
792
+
793
+ expect(result.ok).toBe(false);
794
+ expect(result.errors.some(error => error.includes('error or fallback marker'))).toBe(true);
795
+ });
796
+
797
+ it('accepts schema-v2 expert records with omitted or explicitly clear markers', () => {
798
+ createSprintState(tmpDir, 2);
799
+ const evidence = validRequirementsReview(tmpDir);
800
+ evidence.expert_verdicts[0].error = false;
801
+ evidence.expert_verdicts[0].fallback = null;
802
+ evidence.expert_verdicts[1].error = null;
803
+ evidence.expert_verdicts[1].fallback = false;
804
+ writeRequirementsReview(tmpDir, evidence);
805
+
806
+ expect(validateEvidence(2, tmpDir).ok).toBe(true);
807
+ });
808
+
809
+ it('does not include arbitrary expert payloads in validation errors', () => {
810
+ createSprintState(tmpDir, 2);
811
+ writeRequirementsReview(tmpDir, validRequirementsReview(tmpDir, {
812
+ verdict: 'REQUEST_CHANGES',
813
+ expert_verdicts: [{ private_payload: 'do-not-leak' }],
814
+ }));
815
+
816
+ const result = validateEvidence(2, tmpDir);
817
+
818
+ expect(result.ok).toBe(false);
819
+ expect(result.errors.join('\n')).not.toContain('do-not-leak');
820
+ });
821
+
822
+ it.each([
823
+ ['missing', undefined],
824
+ ['non-number', '0.95'],
825
+ ['below threshold', 0.89],
826
+ ['above one', 1.01],
827
+ ['non-finite', Number.POSITIVE_INFINITY],
828
+ ])('BLOCKs schema-v2 requirements evidence with %s consensus_ratio', (_label, consensusRatio) => {
829
+ createSprintState(tmpDir, 2);
830
+ const evidence = validRequirementsReview(tmpDir, { consensus_ratio: consensusRatio });
831
+ if (consensusRatio === undefined) delete evidence.consensus_ratio;
832
+ writeRequirementsReview(tmpDir, evidence);
833
+
834
+ const result = validateEvidence(2, tmpDir);
835
+
836
+ expect(result.ok).toBe(false);
837
+ expect(result.errors.some(error => error.includes('consensus_ratio'))).toBe(true);
838
+ });
839
+
840
+ it('BLOCKs schema-v2 requirements evidence when Git HEAD cannot be resolved', () => {
841
+ createSprintState(tmpDir, 2);
842
+ const evidence = validRequirementsReview(tmpDir);
843
+ fs.rmSync(path.join(tmpDir, '.git'), { recursive: true, force: true });
844
+ writeRequirementsReview(tmpDir, evidence);
845
+
846
+ const result = validateEvidence(2, tmpDir);
847
+
848
+ expect(result.ok).toBe(false);
849
+ expect(result.errors.some(error => error.includes('resolve current Git HEAD'))).toBe(true);
850
+ });
851
+
852
+ it.each([
853
+ ['invalid expert evidence', { expert_verdicts: [] }, 'expert_verdicts'],
854
+ ['expert failure marker', { expert_verdicts: [
855
+ { role: 'architecture', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-a', error: true },
856
+ { role: 'technical', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-b' },
857
+ { role: 'feasibility', verdict: 'APPROVED', result_type: 'delphi_expert_result', requested_model: 'model-c' },
858
+ ] }, 'error or fallback marker'],
859
+ ['unresolved HEAD', null, 'resolve current Git HEAD'],
860
+ ['stale HEAD', { head_commit: 'deadbeef00000000000000000000000000000000' }, 'head_commit mismatch'],
861
+ ['stale requirements hash', { requirements_hash: '0'.repeat(64) }, 'requirements_hash'],
862
+ ['invalid timestamp', { timestamp: 'not-a-timestamp' }, 'timestamp'],
863
+ ['unsafe context path', { context_file_used: '../CONTEXT.md' }, 'context_file_used'],
864
+ ])('WARNs but accepts legacy requirements evidence with %s', (_label, overrides, expectedWarning) => {
865
+ const evidence = validRequirementsReview(tmpDir);
866
+ if (overrides) Object.assign(evidence, overrides);
867
+ if (overrides === null) fs.rmSync(path.join(tmpDir, '.git'), { recursive: true, force: true });
868
+ createSprintState(tmpDir);
869
+ writeRequirementsReview(tmpDir, evidence);
870
+
871
+ const result = validateEvidence(2, tmpDir);
872
+
873
+ expect(result.ok).toBe(true);
874
+ expect(result.warnings.some(warning => warning.includes(expectedWarning))).toBe(true);
875
+ });
876
+
877
+ it.each([
878
+ ['missing', undefined],
879
+ ['stale', 'deadbeef00000000000000000000000000000000'],
880
+ ])('WARNs but accepts legacy requirements evidence with %s head_commit', (_label, headCommit) => {
881
+ createRepository(tmpDir, 'reviewed requirements');
882
+ createSprintState(tmpDir);
883
+ const report = {
884
+ verdict: 'APPROVED',
885
+ requirements_hash: 'requirements-v1',
886
+ };
887
+ if (headCommit !== undefined) report.head_commit = headCommit;
888
+ writeRequirementsReview(tmpDir, report);
889
+
890
+ const result = validateEvidence(2, tmpDir);
891
+
892
+ expect(result.ok).toBe(true);
893
+ expect(result.warnings.some(warning => warning.includes('head_commit'))).toBe(true);
894
+ });
895
+
336
896
  it('(a) BLOCKs when evidence file is missing for new sprint (evidence_schema_version >= 2)', () => {
337
897
  createSprintState(tmpDir, 2);
338
898
  const result = validateEvidence(4, tmpDir);
@@ -355,6 +915,7 @@ describe('phase-transition', () => {
355
915
  });
356
916
 
357
917
  it('(c) BLOCKs when head_commit is stale (does not match current HEAD)', () => {
918
+ createRepository(tmpDir, 'aligned tests');
358
919
  createSprintState(tmpDir, 2);
359
920
  writeAlignmentReport(tmpDir, {
360
921
  alignment_status: 'PASS',
@@ -363,12 +924,38 @@ describe('phase-transition', () => {
363
924
  timestamp: new Date().toISOString(),
364
925
  });
365
926
  const result = validateEvidence(4, tmpDir);
366
- // In tmpDir (no git repo), current HEAD is 'unknown'.
367
- // A non-'unknown' head_commit that doesn't match 'unknown' should BLOCK.
368
927
  expect(result.ok).toBe(false);
369
928
  expect(result.errors.some(e => e.includes('head_commit'))).toBe(true);
370
929
  });
371
930
 
931
+ it('BLOCKs schema-v2 phase 4 evidence when Git HEAD cannot be resolved', () => {
932
+ createSprintState(tmpDir, 2);
933
+ writeAlignmentReport(tmpDir, {
934
+ alignment_status: 'PASS',
935
+ head_commit: 'unknown',
936
+ spec_hash: null,
937
+ });
938
+
939
+ const result = validateEvidence(4, tmpDir);
940
+
941
+ expect(result.ok).toBe(false);
942
+ expect(result.errors.some(error => error.includes('resolve current Git HEAD'))).toBe(true);
943
+ });
944
+
945
+ it('WARNs but accepts legacy phase 4 evidence when Git HEAD cannot be resolved', () => {
946
+ createSprintState(tmpDir);
947
+ writeAlignmentReport(tmpDir, {
948
+ alignment_status: 'PASS',
949
+ head_commit: 'unknown',
950
+ spec_hash: null,
951
+ });
952
+
953
+ const result = validateEvidence(4, tmpDir);
954
+
955
+ expect(result.ok).toBe(true);
956
+ expect(result.warnings.some(warning => warning.includes('resolve current Git HEAD'))).toBe(true);
957
+ });
958
+
372
959
  it('(d) BLOCKs when evidence file is malformed JSON (treated as missing)', () => {
373
960
  createSprintState(tmpDir, 2);
374
961
  const outputsDir = path.join(tmpDir, '.sprint-state', 'phase-outputs');
@@ -404,6 +991,7 @@ describe('phase-transition', () => {
404
991
  expect(lastEntry.event).toBe('evidence_skipped');
405
992
  expect(lastEntry.phase).toBe(4);
406
993
  expect(lastEntry.reason).toBe('Emergency hotfix');
994
+ expect(lastEntry.commit_hash).toBe('unknown');
407
995
  });
408
996
 
409
997
  it('(g) rejects --skip-evidence without --reason', async () => {
@@ -424,11 +1012,12 @@ describe('phase-transition', () => {
424
1012
  }
425
1013
  });
426
1014
 
427
- it('accepts valid evidence with head_commit matching current HEAD (unknown in non-git dir)', () => {
1015
+ it('accepts valid phase 4 evidence with head_commit matching current HEAD', () => {
1016
+ const currentHead = createRepository(tmpDir, 'aligned tests');
428
1017
  createSprintState(tmpDir, 2);
429
1018
  writeAlignmentReport(tmpDir, {
430
1019
  alignment_status: 'PASS',
431
- head_commit: 'unknown', // matches non-git tmpDir
1020
+ head_commit: currentHead,
432
1021
  spec_hash: null,
433
1022
  timestamp: new Date().toISOString(),
434
1023
  });
@@ -438,6 +1027,7 @@ describe('phase-transition', () => {
438
1027
  });
439
1028
 
440
1029
  it('validates spec_hash when specification.yaml exists', () => {
1030
+ const currentHead = createRepository(tmpDir, 'aligned specification');
441
1031
  createSprintState(tmpDir, 2);
442
1032
  // Create a specification.yaml
443
1033
  const specContent = 'requirements:\n - id: REQ-001\n';
@@ -449,7 +1039,7 @@ describe('phase-transition', () => {
449
1039
 
450
1040
  writeAlignmentReport(tmpDir, {
451
1041
  alignment_status: 'PASS',
452
- head_commit: 'unknown',
1042
+ head_commit: currentHead,
453
1043
  spec_hash: expectedHash,
454
1044
  timestamp: new Date().toISOString(),
455
1045
  });
@@ -458,11 +1048,12 @@ describe('phase-transition', () => {
458
1048
  });
459
1049
 
460
1050
  it('BLOCKs when spec_hash does not match specification.yaml', () => {
1051
+ const currentHead = createRepository(tmpDir, 'stale specification');
461
1052
  createSprintState(tmpDir, 2);
462
1053
  fs.writeFileSync(path.join(tmpDir, 'specification.yaml'), 'requirements:\n - id: REQ-001\n');
463
1054
  writeAlignmentReport(tmpDir, {
464
1055
  alignment_status: 'PASS',
465
- head_commit: 'unknown',
1056
+ head_commit: currentHead,
466
1057
  spec_hash: 'wronghash000000000000000000000000000000000000000000000000000000',
467
1058
  timestamp: new Date().toISOString(),
468
1059
  });
@@ -498,10 +1089,11 @@ describe('phase-transition', () => {
498
1089
  });
499
1090
 
500
1091
  it('handlePhaseTransition allows phase 4 completed with valid evidence', async () => {
1092
+ const currentHead = createRepository(tmpDir, 'aligned tests');
501
1093
  createSprintState(tmpDir, 2);
502
1094
  writeAlignmentReport(tmpDir, {
503
1095
  alignment_status: 'PASS',
504
- head_commit: 'unknown',
1096
+ head_commit: currentHead,
505
1097
  spec_hash: null,
506
1098
  timestamp: new Date().toISOString(),
507
1099
  });