@boyingliu01/xp-gate 0.15.5 → 0.18.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 (165) hide show
  1. package/adapter-common.sh +0 -0
  2. package/adapters/cpp.sh +0 -0
  3. package/adapters/dart.sh +0 -0
  4. package/adapters/flutter.sh +0 -0
  5. package/adapters/go.sh +0 -0
  6. package/adapters/iac.sh +0 -0
  7. package/adapters/java.sh +0 -0
  8. package/adapters/kotlin.sh +0 -0
  9. package/adapters/objectivec.sh +0 -0
  10. package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
  11. package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
  12. package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
  13. package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
  14. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
  15. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
  16. package/adapters/powershell.sh +0 -0
  17. package/adapters/python.sh +0 -0
  18. package/adapters/shell.sh +0 -0
  19. package/adapters/swift.sh +0 -0
  20. package/adapters/typescript.sh +0 -0
  21. package/bin/xp-gate.js +29 -1
  22. package/gate-3.sh +0 -0
  23. package/gate-4.sh +0 -0
  24. package/gate-7.sh +0 -0
  25. package/gate-8.sh +0 -0
  26. package/gate-9.sh +0 -0
  27. package/hooks/adapter-common.sh +0 -0
  28. package/hooks/gate-3.sh +0 -0
  29. package/hooks/gate-4.sh +0 -0
  30. package/hooks/gate-7.sh +0 -0
  31. package/hooks/gate-8.sh +0 -0
  32. package/hooks/gate-9.sh +0 -0
  33. package/hooks/lib/now-ms.sh +33 -0
  34. package/hooks/pre-commit +63 -47
  35. package/hooks/pre-push +0 -0
  36. package/hooks/sprint-gate.sh +291 -0
  37. package/lib/__tests__/check-version.test.js +122 -0
  38. package/lib/__tests__/doctor.test.js +13 -15
  39. package/lib/__tests__/gate-audit.test.ts +38 -0
  40. package/lib/__tests__/phase-transition.test.js +512 -272
  41. package/lib/__tests__/retro.test.js +131 -0
  42. package/lib/__tests__/sprint-audit.test.js +207 -207
  43. package/lib/__tests__/sprint-status-rework.test.js +184 -0
  44. package/lib/check-version.js +9 -0
  45. package/lib/doctor-tui.js +268 -198
  46. package/lib/doctor.js +15 -3
  47. package/lib/gate-audit.ts +18 -7
  48. package/lib/gate-runner.js +77 -30
  49. package/lib/init.js +10 -0
  50. package/lib/language-tools.js +833 -833
  51. package/lib/mattpocock-license-verdict.json +7 -0
  52. package/lib/phase-transition.js +499 -249
  53. package/lib/retro.js +222 -0
  54. package/lib/shared-utils.js +7 -0
  55. package/lib/sprint-audit.js +235 -235
  56. package/lib/sprint-init.js +207 -0
  57. package/lib/sprint-state-migrator.js +1 -0
  58. package/lib/sprint-status.js +111 -0
  59. package/mock-policy/AGENTS.md +3 -3
  60. package/mutation/AGENTS.md +3 -3
  61. package/package.json +3 -2
  62. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  63. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  64. package/plugins/claude-code/bin/xp-gate-check +0 -0
  65. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
  66. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  67. package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
  68. package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
  69. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
  70. package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
  71. package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  72. package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
  73. package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
  74. package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
  75. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  76. package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
  77. package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
  78. package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
  79. package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  80. package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  81. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
  82. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
  83. package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
  84. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
  85. package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
  86. package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
  87. package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
  88. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  89. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
  90. package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
  91. package/plugins/opencode/index.ts +1 -0
  92. package/plugins/opencode/package.json +1 -1
  93. package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
  94. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  95. package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
  96. package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
  97. package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
  98. package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
  99. package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  100. package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
  101. package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
  102. package/plugins/opencode/skills/grilling/SKILL.md +12 -0
  103. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  104. package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
  105. package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
  106. package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
  107. package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  108. package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  109. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
  111. package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
  112. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
  113. package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
  114. package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
  115. package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
  116. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  117. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
  118. package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
  119. package/plugins/qoder/agents/delphi-architecture.md +48 -48
  120. package/plugins/qoder/agents/delphi-feasibility.md +48 -48
  121. package/plugins/qoder/agents/delphi-technical.md +48 -48
  122. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  123. package/plugins/qoder/bin/xp-gate-check +0 -0
  124. package/plugins/qoder/plugin.json +1 -1
  125. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  126. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  127. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  128. package/principles/AGENTS.md +3 -3
  129. package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
  130. package/scripts/__tests__/sync-package-content.test.js +50 -0
  131. package/scripts/check-mattpocock-license.cjs +302 -0
  132. package/scripts/delphi-external-review.cjs +432 -432
  133. package/scripts/migrate-audit-anomalies.mjs +78 -0
  134. package/scripts/sync-package-content.js +52 -5
  135. package/skills/batch-grill-me/SKILL.md +15 -0
  136. package/skills/delphi-review/AGENTS.md +3 -3
  137. package/skills/delphi-review/SKILL.md +59 -4
  138. package/skills/delphi-review/references/requirements.md +390 -0
  139. package/skills/delphi-review/references/round-templates.md +67 -0
  140. package/skills/domain-modeling/ADR-FORMAT.md +64 -0
  141. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  142. package/skills/domain-modeling/SKILL.md +74 -0
  143. package/skills/grill-with-docs/SKILL.md +7 -0
  144. package/skills/grilling/SKILL.md +12 -0
  145. package/skills/sprint-flow/AGENTS.md +3 -3
  146. package/skills/sprint-flow/SKILL.md +168 -43
  147. package/skills/sprint-flow/references/components/memory.md +1 -1
  148. package/skills/sprint-flow/references/components/middleware.md +1 -1
  149. package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  150. package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  151. package/skills/sprint-flow/references/orchestration-rules.md +14 -12
  152. package/skills/sprint-flow/references/phase-2-design.md +145 -133
  153. package/skills/sprint-flow/references/phase-3-build.md +69 -2
  154. package/skills/sprint-flow/references/phase-4-verify.md +93 -31
  155. package/skills/sprint-flow/references/phase-5-ship.md +99 -14
  156. package/skills/sprint-flow/references/phase-6-close.md +37 -5
  157. package/skills/sprint-flow/references/phase-overview.md +58 -53
  158. package/skills/test-specification-alignment/AGENTS.md +3 -3
  159. package/skills/test-specification-alignment/SKILL.md +8 -3
  160. package/skills/to-issues/SKILL.md +1 -1
  161. package/adapters/gate-3.sh +0 -68
  162. package/adapters/gate-4.sh +0 -80
  163. package/adapters/gate-7.sh +0 -45
  164. package/adapters/gate-8.sh +0 -56
  165. package/adapters/gate-9.sh +0 -104
package/adapter-common.sh CHANGED
File without changes
package/adapters/cpp.sh CHANGED
File without changes
package/adapters/dart.sh CHANGED
File without changes
File without changes
package/adapters/go.sh CHANGED
File without changes
package/adapters/iac.sh CHANGED
File without changes
package/adapters/java.sh CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/adapters/shell.sh CHANGED
File without changes
package/adapters/swift.sh CHANGED
File without changes
File without changes
package/bin/xp-gate.js CHANGED
@@ -147,7 +147,23 @@ const COMMANDS = {
147
147
  const { handleSprintStatus } = require('../lib/sprint-status.js');
148
148
  handleSprintStatus(subargs).then(code => process.exit(code));
149
149
  },
150
- usage: 'xp-gate sprint-status [--json] [--watch] [--dir <path>]'
150
+ usage: 'xp-gate sprint-status [--json] [--watch] [--dir <path>] [--rework-check] [--window-days N]'
151
+ },
152
+ 'retro': {
153
+ description: 'Generate sprint engineering retrospective (#369)',
154
+ run: subargs => {
155
+ const { handleRetro } = require('../lib/retro.js');
156
+ handleRetro(subargs).then(code => process.exit(code));
157
+ },
158
+ usage: 'xp-gate retro [--days N] [--all] [--json] [--sprint <id>]'
159
+ },
160
+ 'sprint-init': {
161
+ description: 'Initialize sprint state (single entry point for sprint creation, resolves #366)',
162
+ run: subargs => {
163
+ const { handleSprintInit } = require('../lib/sprint-init.js');
164
+ handleSprintInit(subargs).then(code => process.exit(code));
165
+ },
166
+ usage: 'xp-gate sprint-init "<task_description>" [--issues "<#123>"] [--force] [--dry-run] [--dir <path>]'
151
167
  },
152
168
  'next-sprint': {
153
169
  description: 'Analyze remaining open issues and plan next iteration',
@@ -248,6 +264,18 @@ for (let i = 0; i <= 11; i++) {
248
264
  };
249
265
  }
250
266
 
267
+ // Named utility gates (analytics/report tools)
268
+ for (const gateId of ['python-health', 'test-layers', 'pbt']) {
269
+ const gateInfo = gateRunner.GATE_REGISTRY[gateId];
270
+ if (gateInfo) {
271
+ COMMANDS[gateId] = {
272
+ description: gateInfo.description,
273
+ run: subargs => gateRunner.runGate(gateId, subargs[0]).then(code => process.exit(code)),
274
+ usage: `xp-gate ${gateId} [<path>]`
275
+ };
276
+ }
277
+ }
278
+
251
279
  function printHelp() {
252
280
  console.log('xp-gate - AI development workflow tool');
253
281
  console.log('');
package/gate-3.sh CHANGED
File without changes
package/gate-4.sh CHANGED
File without changes
package/gate-7.sh CHANGED
File without changes
package/gate-8.sh CHANGED
File without changes
package/gate-9.sh CHANGED
File without changes
File without changes
package/hooks/gate-3.sh CHANGED
File without changes
package/hooks/gate-4.sh CHANGED
File without changes
package/hooks/gate-7.sh CHANGED
File without changes
package/hooks/gate-8.sh CHANGED
File without changes
package/hooks/gate-9.sh CHANGED
File without changes
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # ============================================================================
3
+ # now_ms() — Cross-platform epoch-milliseconds timestamp (Issue #370 fix)
4
+ #
5
+ # Windows Git Bash's `date +%s%3N` outputs literal "N" (e.g. "1753001234N")
6
+ # and exits 0, so || fallback chains never trigger. This function:
7
+ # 1. Prefers `node -e 'console.log(Date.now())'` (cross-platform, always ms)
8
+ # 2. Validates output with ^[0-9]+$ regex
9
+ # 3. Falls back to `date +%s` (seconds × 1000) if node output is invalid
10
+ # 4. Final fallback: echo 0
11
+ #
12
+ # Source this file: source "$(dirname "${BASH_SOURCE[0]}")/lib/now-ms.sh"
13
+ # ============================================================================
14
+
15
+ now_ms() {
16
+ local ts
17
+ # Preferred: node (cross-platform, always produces epoch milliseconds)
18
+ ts=$(node -e 'console.log(Date.now())' 2>/dev/null)
19
+ if [[ "$ts" =~ ^[0-9]+$ ]]; then
20
+ echo "$ts"
21
+ return
22
+ fi
23
+
24
+ # Fallback: date +%s (seconds only — multiply by 1000 for ms)
25
+ ts=$(date +%s 2>/dev/null)
26
+ if [[ "$ts" =~ ^[0-9]+$ ]]; then
27
+ echo $((ts * 1000))
28
+ return
29
+ fi
30
+
31
+ # Final fallback
32
+ echo 0
33
+ }
package/hooks/pre-commit CHANGED
@@ -165,6 +165,69 @@ file_exists_in_subdir() {
165
165
  fi
166
166
  }
167
167
 
168
+ # ============================================================================
169
+ # Audit/Timing helpers — must be defined BEFORE first gate call (line ~197)
170
+ # Bash executes top-to-bottom; gate_start_ms + record_gate_audit are called
171
+ # in every gate, so their definitions lead the first call.
172
+ # ============================================================================
173
+ AUDIT_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
174
+ PROJECT_ROOT_FOR_AUDIT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$AUDIT_SCRIPT_DIR/../..")"
175
+
176
+ # Issue #370: source cross-platform now_ms() helper (node-preferred, regex-validated)
177
+ source "${AUDIT_SCRIPT_DIR}/lib/now-ms.sh"
178
+
179
+ # Read max_duration_ms from .xp-gate-config.json (default 7200000 = 2 hours)
180
+ _MAX_DURATION_MS=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('${PROJECT_ROOT_FOR_AUDIT}/.xp-gate-config.json','utf8'));console.log((c.audit&&c.audit.max_duration_ms)||7200000);}catch(e){console.log(7200000);}" 2>/dev/null || echo "7200000")
181
+
182
+ # Get milliseconds timestamp (used by gates for duration measurement)
183
+ # Issue #370: delegates to now_ms() — node-preferred, regex-validated, cross-platform
184
+ gate_start_ms() {
185
+ now_ms
186
+ }
187
+
188
+ # Audit helper — records gate execution to .xp-gate/audit.jsonl
189
+ # NEVER blocks commit on failure (try/catch around npx tsx).
190
+ # Usage: record_gate_audit <gate_id> <gate_name> <passed> <issues_found> <start_ms>
191
+ record_gate_audit() {
192
+ local gate_id="$1"
193
+ local gate_name="$2"
194
+ local passed="$3"
195
+ local issues_found="$4"
196
+ local start_ms="$5"
197
+ local end_ms
198
+ end_ms=$(now_ms)
199
+ local duration_ms=$((end_ms - start_ms))
200
+ if [ "$duration_ms" -lt 0 ] 2>/dev/null; then
201
+ duration_ms=0
202
+ fi
203
+
204
+ # Issue #370: detect anomalous duration (poisoned timestamp arithmetic)
205
+ local duration_anomaly_flag=""
206
+ if [ "$duration_ms" -gt "$_MAX_DURATION_MS" ] 2>/dev/null; then
207
+ duration_anomaly_flag="--duration-anomaly true"
208
+ fi
209
+
210
+ # Determine gate result: PASS or FAIL based on status variable
211
+ local gate_result="true"
212
+ if [ "$passed" != "PASS" ] && [ "$passed" != "SKIP" ]; then
213
+ gate_result="false"
214
+ fi
215
+
216
+ # Fire-and-forget: audit must never block the commit
217
+ {
218
+ npx tsx "${PROJECT_ROOT_FOR_AUDIT}/src/npm-package/lib/gate-audit.ts" record \
219
+ --gate-id "$gate_id" \
220
+ --gate-name "$gate_name" \
221
+ --passed "$gate_result" \
222
+ --issues-found "$issues_found" \
223
+ --duration-ms "$duration_ms" \
224
+ $duration_anomaly_flag \
225
+ --trigger commit \
226
+ 2>/dev/null
227
+ } &
228
+ disown 2>/dev/null || true
229
+ }
230
+
168
231
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
169
232
  echo " QUALITY GATES - PRE-COMMIT CHECK"
170
233
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -713,53 +776,6 @@ has_project_lang() {
713
776
  done
714
777
  return 1
715
778
  }
716
-
717
- # ============================================================================
718
- # Audit helper — records gate execution to .xp-gate/audit.jsonl
719
- # NEVER blocks commit on failure (try/catch around npx tsx).
720
- # Usage: record_gate_audit <gate_id> <gate_name> <passed> <issues_found> <start_ms>
721
- # ============================================================================
722
- AUDIT_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
723
- PROJECT_ROOT_FOR_AUDIT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$AUDIT_SCRIPT_DIR/../..")"
724
-
725
- record_gate_audit() {
726
- local gate_id="$1"
727
- local gate_name="$2"
728
- local passed="$3"
729
- local issues_found="$4"
730
- local start_ms="$5"
731
- local end_ms
732
- end_ms=$(date +%s%3N 2>/dev/null || node -e "console.log(Date.now())" 2>/dev/null || echo "0")
733
- local duration_ms=$((end_ms - start_ms))
734
- if [ "$duration_ms" -lt 0 ] 2>/dev/null; then
735
- duration_ms=0
736
- fi
737
-
738
- # Determine gate result: PASS or FAIL based on status variable
739
- local gate_result="true"
740
- if [ "$passed" != "PASS" ] && [ "$passed" != "SKIP" ]; then
741
- gate_result="false"
742
- fi
743
-
744
- # Fire-and-forget: audit must never block the commit
745
- {
746
- npx tsx "${PROJECT_ROOT_FOR_AUDIT}/src/npm-package/lib/gate-audit.ts" record \
747
- --gate-id "$gate_id" \
748
- --gate-name "$gate_name" \
749
- --passed "$gate_result" \
750
- --issues-found "$issues_found" \
751
- --duration-ms "$duration_ms" \
752
- --trigger commit \
753
- 2>/dev/null
754
- } &
755
- disown 2>/dev/null || true
756
- }
757
-
758
- # Get milliseconds timestamp (used by gates for duration measurement)
759
- gate_start_ms() {
760
- date +%s%3N 2>/dev/null || node -e "console.log(Date.now())" 2>/dev/null || echo "0"
761
- }
762
-
763
779
  # Switch to subdirectory if detected (for single-language subdirectory projects).
764
780
  # Multi-language projects stay at root — each adapter handles its own path.
765
781
  ORIGINAL_DIR=""
package/hooks/pre-push CHANGED
File without changes
@@ -0,0 +1,291 @@
1
+ #!/bin/bash
2
+ # sprint-gate.sh — Sprint Flow Validation Gate
3
+ #
4
+ # PURPOSE: Enforce Sprint Flow phase discipline at git hook level.
5
+ # Validates delphi-review approval, sprint state, and specification
6
+ # artifacts before allowing commits and pushes on sprint branches.
7
+ #
8
+ # INTEGRATION: Called from pre-commit (Gate 10) and pre-push (Gate S),
9
+ # or run independently for CI/manual validation.
10
+ #
11
+ # USAGE:
12
+ # sprint-gate.sh --pre-commit # Validate before commit
13
+ # sprint-gate.sh --pre-push # Validate before push
14
+ #
15
+ # MECHANISM:
16
+ # --pre-commit:
17
+ # - If .sprint-state/ missing → SKIP (not a sprint project)
18
+ # - If sprint-state.json missing/invalid → SKIP (non-sprint commit)
19
+ # - If phase >= 1 (PLAN completed) and delphi-reviewed.json missing → DENY
20
+ # - If delphi-reviewed.json exists, verdict must be "APPROVED"
21
+ # - If jq AND node unavailable → WARN but ALLOW (graceful degradation)
22
+ # - If jq unavailable but node available → use Node.js JSON parser fallback
23
+ #
24
+ # --pre-push:
25
+ # - If .sprint-state/ missing → SKIP
26
+ # - If branch doesn't match sprint/* → SKIP
27
+ # - If delphi-reviewed.json missing or verdict != "APPROVED" → DENY
28
+ # - If phase < 2 (BUILD not started) → DENY
29
+ # - If phase >= 2 and specification.yaml missing → DENY
30
+ # - If jq AND node unavailable → DENY (cannot verify verdict)
31
+ #
32
+ # GRACEFUL DEGRADATION:
33
+ # - If .sprint-state/ directory doesn't exist → SKIP (not a sprint project)
34
+ # - If sprint-state.json is missing/invalid → SKIP (non-sprint commit)
35
+ # - JSON parser fallback chain: jq → Node.js → explicit degradation message
36
+ #
37
+ # OUTPUT: JSON to stdout — {"decision":"allow|deny|skip",...}
38
+ # EXIT: 0 = pass/skip, 1 = block
39
+
40
+ set -euo pipefail
41
+
42
+ # --- Argument parsing ---
43
+ MODE=""
44
+ case "${1:-}" in
45
+ --pre-commit) MODE="pre-commit" ;;
46
+ --pre-push) MODE="pre-push" ;;
47
+ *)
48
+ echo '{"decision":"deny","reason":"Usage: sprint-gate.sh --pre-commit | --pre-push"}'
49
+ exit 1
50
+ ;;
51
+ esac
52
+
53
+ # --- Locate repo root and sprint state ---
54
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
55
+ echo '{"decision":"skip","reason":"not inside a git repository"}'
56
+ exit 0
57
+ }
58
+
59
+ SPRINT_STATE_DIR="$REPO_ROOT/.sprint-state"
60
+ SPRINT_STATE_JSON="$SPRINT_STATE_DIR/sprint-state.json"
61
+ APPROVED_FILE="$SPRINT_STATE_DIR/delphi-reviewed.json"
62
+
63
+ # If no .sprint-state directory, this isn't a sprint project → SKIP
64
+ if [ ! -d "$SPRINT_STATE_DIR" ]; then
65
+ echo '{"decision":"skip","reason":"not a sprint project"}'
66
+ exit 0
67
+ fi
68
+
69
+ # --- Helper: check tool availability ---
70
+ HAS_JQ=false
71
+ if command -v jq &>/dev/null; then
72
+ HAS_JQ=true
73
+ fi
74
+
75
+ HAS_NODE=false
76
+ if command -v node &>/dev/null; then
77
+ HAS_NODE=true
78
+ fi
79
+
80
+ # JSON_PARSER: "jq" | "node" | "none"
81
+ if $HAS_JQ; then
82
+ JSON_PARSER="jq"
83
+ elif $HAS_NODE; then
84
+ JSON_PARSER="node"
85
+ else
86
+ JSON_PARSER="none"
87
+ fi
88
+
89
+ # --- Helper: validate JSON file ---
90
+ # Returns 0 if valid, 1 if invalid or no parser available
91
+ json_validate() {
92
+ local file="$1"
93
+ if [ ! -f "$file" ]; then
94
+ return 1
95
+ fi
96
+ case "$JSON_PARSER" in
97
+ jq)
98
+ jq empty "$file" 2>/dev/null
99
+ return $?
100
+ ;;
101
+ node)
102
+ node -e "try{JSON.parse(require('fs').readFileSync('$file','utf8'));process.exit(0)}catch(e){process.exit(1)}" 2>/dev/null
103
+ return $?
104
+ ;;
105
+ *)
106
+ return 1
107
+ ;;
108
+ esac
109
+ }
110
+
111
+ # --- Helper: extract JSON field ---
112
+ # Usage: json_extract <file> <jq-filter>
113
+ # jq-filter is a jq expression like '.phase // -1' or '.verdict // ""'
114
+ # Falls back to Node.js when jq is unavailable
115
+ json_extract() {
116
+ local file="$1"
117
+ local filter="$2"
118
+ case "$JSON_PARSER" in
119
+ jq)
120
+ jq -r "$filter" "$file" 2>/dev/null
121
+ return $?
122
+ ;;
123
+ node)
124
+ # Translate simple jq filters to Node.js
125
+ # Supports: .field // default patterns
126
+ local field default_val
127
+ field=$(echo "$filter" | sed 's/^\.//;s/ *\/\/.*$//')
128
+ default_val=$(echo "$filter" | sed -n 's/.*\/\/ *//p')
129
+ node -e "
130
+ try {
131
+ var d=JSON.parse(require('fs').readFileSync('$file','utf8'));
132
+ var v=d['${field}'];
133
+ if(v===undefined||v===null){v=${default_val:-null}}
134
+ process.stdout.write(String(v));
135
+ }catch(e){process.stdout.write('${default_val:-}');process.exit(1)}
136
+ " 2>/dev/null
137
+ return $?
138
+ ;;
139
+ *)
140
+ echo ""
141
+ return 1
142
+ ;;
143
+ esac
144
+ }
145
+
146
+ # --- Helper: read phase from sprint-state.json ---
147
+ # Returns phase number or -1 if unreadable
148
+ read_phase() {
149
+ if [ ! -f "$SPRINT_STATE_JSON" ]; then
150
+ echo "-1"
151
+ return
152
+ fi
153
+ if [ "$JSON_PARSER" = "none" ]; then
154
+ echo "-1"
155
+ return
156
+ fi
157
+ if ! json_validate "$SPRINT_STATE_JSON"; then
158
+ echo "-1"
159
+ return
160
+ fi
161
+ local phase
162
+ phase=$(json_extract "$SPRINT_STATE_JSON" '.phase // -1') || phase="-1"
163
+ echo "$phase"
164
+ }
165
+
166
+ # --- Helper: read delphi verdict ---
167
+ # Returns verdict string or empty if unreadable
168
+ read_verdict() {
169
+ if [ ! -f "$APPROVED_FILE" ]; then
170
+ echo ""
171
+ return
172
+ fi
173
+ if [ "$JSON_PARSER" = "none" ]; then
174
+ echo ""
175
+ return
176
+ fi
177
+ if ! json_validate "$APPROVED_FILE"; then
178
+ echo ""
179
+ return
180
+ fi
181
+ json_extract "$APPROVED_FILE" '.verdict // ""' || echo ""
182
+ }
183
+
184
+ # --- Pre-commit mode ---
185
+ if [ "$MODE" = "pre-commit" ]; then
186
+ PHASE=$(read_phase)
187
+
188
+ # If sprint-state.json is missing or invalid, skip (non-sprint commit)
189
+ if [ "$PHASE" = "-1" ]; then
190
+ if [ ! -f "$SPRINT_STATE_JSON" ]; then
191
+ echo '{"decision":"skip","reason":"sprint-state.json not found, non-sprint commit"}'
192
+ else
193
+ echo '{"decision":"skip","warning":"sprint-state.json is not valid JSON or no JSON parser available (install jq or node), allowing commit"}'
194
+ fi
195
+ exit 0
196
+ fi
197
+
198
+ # Phase >= 1 means PLAN is completed — delphi-review must be APPROVED
199
+ if [ "$PHASE" -ge 1 ] 2>/dev/null; then
200
+ # Check if delphi-reviewed.json exists
201
+ if [ ! -f "$APPROVED_FILE" ]; then
202
+ echo '{"decision":"deny","reason":"delphi-review not APPROVED. Complete Phase 1 before committing code changes."}'
203
+ exit 1
204
+ fi
205
+
206
+ # If no JSON parser available, warn but allow (graceful degradation)
207
+ if [ "$JSON_PARSER" = "none" ]; then
208
+ echo '{"decision":"allow","warning":"neither jq nor node available, cannot verify delphi-review verdict. Install jq or node for full protection."}'
209
+ exit 0
210
+ fi
211
+
212
+ # Validate delphi-reviewed.json is valid JSON
213
+ if ! json_validate "$APPROVED_FILE"; then
214
+ echo '{"decision":"deny","reason":"delphi-reviewed.json is not valid JSON. Re-run: /delphi-review"}'
215
+ exit 1
216
+ fi
217
+
218
+ # Check verdict
219
+ VERDICT=$(read_verdict)
220
+ if [ "$VERDICT" != "APPROVED" ]; then
221
+ echo "{\"decision\":\"deny\",\"reason\":\"delphi-review verdict is '${VERDICT}', not APPROVED. Fix issues and re-run: /delphi-review\"}"
222
+ exit 1
223
+ fi
224
+
225
+ # All checks passed
226
+ echo "{\"decision\":\"allow\",\"message\":\"delphi-review APPROVED, sprint phase ${PHASE} validated\"}"
227
+ exit 0
228
+ fi
229
+
230
+ # Phase < 1 (pre-PLAN) — no delphi enforcement needed
231
+ echo "{\"decision\":\"allow\",\"reason\":\"sprint phase ${PHASE} (pre-PLAN), no delphi enforcement required\"}"
232
+ exit 0
233
+ fi
234
+
235
+ # --- Pre-push mode ---
236
+ if [ "$MODE" = "pre-push" ]; then
237
+ # Only enforce on sprint/* branches
238
+ BRANCH="$(git branch --show-current 2>/dev/null)" || BRANCH=""
239
+ case "$BRANCH" in
240
+ sprint/*) ;; # continue
241
+ *)
242
+ echo "{\"decision\":\"skip\",\"reason\":\"branch '${BRANCH}' does not match sprint/* pattern\"}"
243
+ exit 0
244
+ ;;
245
+ esac
246
+
247
+ # Validate sprint-state.json exists and is readable
248
+ PHASE=$(read_phase)
249
+ if [ "$PHASE" = "-1" ] && [ ! -f "$SPRINT_STATE_JSON" ]; then
250
+ echo "{\"decision\":\"deny\",\"reason\":\"sprint-state.json not found on sprint branch '${BRANCH}'\"}"
251
+ exit 1
252
+ fi
253
+
254
+ # Delphi review must be APPROVED
255
+ if [ ! -f "$APPROVED_FILE" ]; then
256
+ echo '{"decision":"deny","reason":"delphi-reviewed.json not found. Complete Phase 1 delphi-review before pushing."}'
257
+ exit 1
258
+ fi
259
+
260
+ if [ "$JSON_PARSER" = "none" ]; then
261
+ echo '{"decision":"deny","reason":"neither jq nor node available, cannot verify delphi-review verdict. Install jq or node to push from sprint branch."}'
262
+ exit 1
263
+ fi
264
+
265
+ if ! json_validate "$APPROVED_FILE"; then
266
+ echo '{"decision":"deny","reason":"delphi-reviewed.json is not valid JSON. Re-run: /delphi-review"}'
267
+ exit 1
268
+ fi
269
+
270
+ VERDICT=$(read_verdict)
271
+ if [ "$VERDICT" != "APPROVED" ]; then
272
+ echo "{\"decision\":\"deny\",\"reason\":\"delphi-review verdict is '${VERDICT}', not APPROVED. Fix issues and re-run: /delphi-review\"}"
273
+ exit 1
274
+ fi
275
+
276
+ # specification.yaml must exist (Phase 1 PLAN output)
277
+ SPEC_FILE="$REPO_ROOT/specification.yaml"
278
+ if [ ! -f "$SPEC_FILE" ]; then
279
+ # Also check sprint-state output directory
280
+ SPEC_FILE="$SPRINT_STATE_DIR/phase-outputs/specification.yaml"
281
+ if [ ! -f "$SPEC_FILE" ]; then
282
+ echo '{"decision":"deny","reason":"specification.yaml not found. Phase 1 PLAN must produce specification.yaml before pushing."}'
283
+ exit 1
284
+ fi
285
+ fi
286
+
287
+ # All checks passed — comprehensive success output
288
+ SPEC_SIZE=$(wc -c < "$SPEC_FILE" 2>/dev/null | tr -d ' ')
289
+ echo "{\"decision\":\"allow\",\"message\":\"sprint push validated\",\"branch\":\"${BRANCH}\",\"phase\":${PHASE},\"verdict\":\"APPROVED\",\"specification_size_bytes\":${SPEC_SIZE:-0}}"
290
+ exit 0
291
+ fi