@boyingliu01/xp-gate 0.19.3 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/adapters/gate-4.sh +83 -18
  2. package/adapters/gate-8.sh +49 -5
  3. package/adapters/iac.sh +1 -1
  4. package/adapters/typescript.sh +15 -60
  5. package/bin/xp-gate.js +15 -16
  6. package/gate-10.sh +35 -2
  7. package/gate-4.sh +110 -21
  8. package/gate-8.sh +49 -5
  9. package/hooks/gate-10.sh +35 -2
  10. package/hooks/gate-4.sh +110 -21
  11. package/hooks/gate-8.sh +49 -5
  12. package/hooks/lib/jscpd-run.sh +63 -0
  13. package/hooks/lib/sprint-gate-report.sh +58 -0
  14. package/hooks/lib/test-failure.sh +152 -0
  15. package/hooks/lib/typecheck.sh +141 -0
  16. package/hooks/lib/validate-code-walkthrough.cjs +43 -5
  17. package/hooks/post-merge +100 -0
  18. package/hooks/pre-commit +389 -162
  19. package/hooks/pre-push +75 -19
  20. package/lib/__tests__/bootstrap.test.js +23 -41
  21. package/lib/__tests__/check-version.test.js +8 -1
  22. package/lib/__tests__/detect-deps.test.js +16 -0
  23. package/lib/__tests__/doctor-drift-round2.test.js +212 -0
  24. package/lib/__tests__/doctor-executed-surface.test.js +209 -0
  25. package/lib/__tests__/doctor-hook-drift.test.js +329 -0
  26. package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
  27. package/lib/__tests__/doctor-module-drift.test.js +206 -0
  28. package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
  29. package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
  30. package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
  31. package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
  32. package/lib/__tests__/doctor.test.js +155 -0
  33. package/lib/__tests__/gate-runner.test.js +19 -3
  34. package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
  35. package/lib/__tests__/init.test.js +44 -17
  36. package/lib/__tests__/install-skill-full-dir.test.js +151 -0
  37. package/lib/__tests__/install-skill.test.js +51 -132
  38. package/lib/__tests__/language-tools-detect.test.js +158 -0
  39. package/lib/__tests__/phase-transition.test.js +15 -1
  40. package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
  41. package/lib/__tests__/shared-paths.test.js +20 -0
  42. package/lib/__tests__/shared-utils.test.js +69 -0
  43. package/lib/__tests__/skill-flags.test.js +48 -0
  44. package/lib/__tests__/ui-detector.test.ts +6 -2
  45. package/lib/__tests__/uninstall.test.js +20 -0
  46. package/lib/__tests__/update-hooks.test.js +12 -0
  47. package/lib/__tests__/update-skill.test.js +7 -4
  48. package/lib/detect-deps.js +3 -20
  49. package/lib/doctor-tui.js +22 -5
  50. package/lib/doctor.js +1178 -31
  51. package/lib/init.js +184 -33
  52. package/lib/install-cmd.js +7 -4
  53. package/lib/install-skill.js +38 -95
  54. package/lib/language-tools.js +83 -4
  55. package/lib/mattpocock-license-verdict.json +1 -1
  56. package/lib/shared-paths.js +5 -2
  57. package/lib/shared-utils.js +47 -3
  58. package/lib/uninstall.js +12 -0
  59. package/lib/update-hooks.js +4 -4
  60. package/lib/update-skill.js +6 -7
  61. package/mock-policy/AGENTS.md +4 -4
  62. package/mutation/AGENTS.md +4 -4
  63. package/mutation/__tests__/gate-m.test.ts +66 -14
  64. package/mutation/__tests__/mutmut-runner.test.ts +91 -42
  65. package/mutation/__tests__/test-filter-parity.test.ts +37 -0
  66. package/mutation/gate-m.ts +21 -8
  67. package/package.json +1 -1
  68. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  69. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  70. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  71. package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
  72. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
  73. package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
  74. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  75. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
  76. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  77. package/plugins/dsh/README.md +169 -4
  78. package/plugins/dsh/dsh-overlay.patch.yml +67 -0
  79. package/plugins/dsh/package.json +1 -1
  80. package/plugins/dsh/scripts/prepack.cjs +24 -19
  81. package/plugins/dsh/setup-dsh.ps1 +160 -0
  82. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  84. package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
  85. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
  86. package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
  87. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  88. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
  89. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  90. package/plugins/dsh/src/command.test.ts +66 -2
  91. package/plugins/dsh/src/command.ts +36 -14
  92. package/plugins/dsh/src/contract.test.ts +31 -1
  93. package/plugins/dsh/src/delphi-config.test.ts +190 -0
  94. package/plugins/dsh/src/delphi-config.ts +223 -0
  95. package/plugins/dsh/src/delphi-run.test.ts +181 -0
  96. package/plugins/dsh/src/delphi-run.ts +129 -0
  97. package/plugins/dsh/src/delphi.test.ts +222 -0
  98. package/plugins/dsh/src/delphi.ts +247 -0
  99. package/plugins/dsh/src/dialect.test.ts +59 -0
  100. package/plugins/dsh/src/dialect.ts +40 -0
  101. package/plugins/dsh/src/index.ts +70 -4
  102. package/plugins/dsh/src/skills.test.ts +26 -19
  103. package/plugins/opencode/package.json +1 -1
  104. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  105. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  106. package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
  107. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
  108. package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
  109. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
  111. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  112. package/plugins/qoder/agents/delphi-architecture.md +1 -1
  113. package/plugins/qoder/agents/delphi-feasibility.md +1 -1
  114. package/plugins/qoder/agents/delphi-technical.md +1 -1
  115. package/plugins/qoder/plugin.json +1 -1
  116. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  117. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  118. package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
  119. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
  120. package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  122. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
  123. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  124. package/principles/AGENTS.md +9 -8
  125. package/principles/__tests__/adapter-metrics.test.ts +292 -0
  126. package/principles/__tests__/analyzer.test.ts +29 -6
  127. package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
  128. package/principles/__tests__/baseline-prune.test.ts +338 -0
  129. package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
  130. package/principles/__tests__/boy-scout-integration.test.ts +97 -6
  131. package/principles/__tests__/boy-scout.test.ts +253 -7
  132. package/principles/__tests__/config-enforcement.test.ts +469 -0
  133. package/principles/__tests__/config-git-timeout.test.ts +66 -0
  134. package/principles/__tests__/direct-execution.test.ts +187 -0
  135. package/principles/__tests__/index.test.ts +16 -3
  136. package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
  137. package/principles/adapters/typescript.ts +613 -13
  138. package/principles/analyzer.ts +22 -0
  139. package/principles/baseline-prune.ts +217 -0
  140. package/principles/boy-scout.ts +481 -51
  141. package/principles/config.ts +362 -54
  142. package/principles/direct-execution.ts +60 -0
  143. package/principles/index.ts +40 -17
  144. package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
  145. package/principles/rules/clean-code/code-duplication.ts +8 -8
  146. package/principles/rules/clean-code/deep-nesting.ts +8 -8
  147. package/principles/rules/clean-code/god-class.ts +8 -8
  148. package/principles/rules/clean-code/large-file.ts +15 -11
  149. package/principles/rules/clean-code/long-function.ts +8 -8
  150. package/principles/rules/clean-code/magic-numbers.ts +10 -7
  151. package/principles/rules/clean-code/many-exports.ts +7 -7
  152. package/principles/rules/clean-code/missing-error-handling.ts +5 -5
  153. package/principles/rules/clean-code/too-many-params.ts +8 -8
  154. package/principles/rules/clean-code/unused-imports.ts +5 -5
  155. package/principles/rules/solid/dip.ts +18 -8
  156. package/principles/rules/solid/isp.ts +9 -8
  157. package/principles/rules/solid/lsp.ts +6 -6
  158. package/principles/rules/solid/ocp.ts +6 -5
  159. package/principles/rules/solid/srp.ts +7 -8
  160. package/principles/types.ts +15 -0
  161. package/scripts/__tests__/sync-package-content.test.js +28 -0
  162. package/scripts/delphi-external-review.cjs +193 -10
  163. package/scripts/sync-package-content.js +7 -4
  164. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  165. package/skills/delphi-review/AGENTS.md +4 -4
  166. package/skills/delphi-review/SKILL.md +98 -14
  167. package/skills/delphi-review/references/code-walkthrough.md +36 -3
  168. package/skills/delphi-review/references/requirements.md +6 -6
  169. package/skills/sprint-flow/AGENTS.md +4 -4
  170. package/skills/sprint-flow/references/phase-2-design.md +3 -3
  171. package/skills/test-specification-alignment/AGENTS.md +4 -4
  172. package/lib/__tests__/download-skill.test.js +0 -281
  173. package/lib/download-skill.js +0 -96
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Gate 1 TypeScript static analysis — the single place that decides HOW a project
4
+ # is type-checked (#436).
5
+ #
6
+ # Before this file, `githooks/pre-commit` and `githooks/adapters/typescript.sh` each
7
+ # hardcoded `npx tsc --noEmit`. Plain tsc cannot resolve `.vue` single-file
8
+ # components, so every `import X from './Foo.vue'` became a TS2307 and a Vue project
9
+ # whose own `vue-tsc --noEmit` run is clean was blocked on every commit. Because
10
+ # pre-commit runs under `set -o pipefail`, those false findings really did block.
11
+ #
12
+ # Resolution order:
13
+ # 1. GATE_TS_TYPECHECK_CMD explicit override, same precedent as SKIP_VERSION_CHECK
14
+ # 2. package.json scripts.typecheck
15
+ # 3. package.json scripts.type-check (the name create-vue generates)
16
+ # 4. `npx tsc --noEmit --skipLibCheck` — the pre-#436 behavior, unchanged
17
+ #
18
+ # Return codes follow the gate exit-code contract: 0 clean, 1 findings, 2 checker
19
+ # unavailable (a tool failure must never be read as "no type errors").
20
+
21
+ resolve_typecheck_command() {
22
+ if [ -n "${GATE_TS_TYPECHECK_CMD:-}" ]; then
23
+ printf '%s' "$GATE_TS_TYPECHECK_CMD"
24
+ return 0
25
+ fi
26
+
27
+ [ -f "package.json" ] || return 1
28
+ command -v node >/dev/null 2>&1 || return 1
29
+
30
+ local declared_script
31
+ declared_script=$(node -e '
32
+ let pkg;
33
+ try {
34
+ pkg = JSON.parse(require("fs").readFileSync("package.json", "utf8"));
35
+ } catch {
36
+ process.exit(0);
37
+ }
38
+ const scripts = (pkg && pkg.scripts) || {};
39
+ if (scripts.typecheck) process.stdout.write("typecheck");
40
+ else if (scripts["type-check"]) process.stdout.write("type-check");
41
+ ' 2>/dev/null) || return 1
42
+
43
+ [ -n "$declared_script" ] || return 1
44
+
45
+ printf 'npm run %s' "$declared_script"
46
+ return 0
47
+ }
48
+
49
+ # The #293 extra pass: projects commonly exclude test files from tsconfig.json, so
50
+ # their type errors accumulate silently. Only meaningful for the tsc fallback —
51
+ # a project checker covers the files its own config declares.
52
+ run_test_file_pass() {
53
+ local has_tests_in_tsc=false
54
+ if npx tsc --noEmit --skipLibCheck --listFiles 2>/dev/null | grep -qE '__tests__/|\.test\.ts|\.spec\.ts'; then
55
+ has_tests_in_tsc=true
56
+ fi
57
+ [ "$has_tests_in_tsc" = false ] || return 0
58
+
59
+ local test_pass_exit=0
60
+ if [ -f "tsconfig.tests.json" ]; then
61
+ echo "Checking test files with tsconfig.tests.json..."
62
+ npx tsc --noEmit --project tsconfig.tests.json 2>&1 | head -30
63
+ test_pass_exit=${PIPESTATUS[0]}
64
+ else
65
+ echo "Checking test files for type errors..."
66
+ local temp_tsconfig=".tsconfig.withtests.json"
67
+ node -e "
68
+ const cfg = JSON.parse(require('fs').readFileSync('tsconfig.json','utf8'));
69
+ delete cfg.exclude;
70
+ cfg.include = cfg.include || ['src/**/*'];
71
+ cfg.include.push('src/**/__tests__/**','src/**/*.test.ts','src/**/*.spec.ts');
72
+ require('fs').writeFileSync('$temp_tsconfig', JSON.stringify(cfg, null, 2));
73
+ " 2>/dev/null
74
+ npx tsc --noEmit --project "$temp_tsconfig" --skipLibCheck 2>&1 | head -30
75
+ test_pass_exit=${PIPESTATUS[0]}
76
+ rm -f "$temp_tsconfig"
77
+ fi
78
+
79
+ if [ "$test_pass_exit" -ne 0 ]; then
80
+ echo ""
81
+ echo "❌ BLOCKED - TYPE ERRORS in test files"
82
+ echo "Your project's tsconfig.json excludes test files from type checking."
83
+ echo "Add a tsconfig.tests.json to customise test file checking."
84
+ return 1
85
+ fi
86
+ return 0
87
+ }
88
+
89
+ run_typescript_typecheck() {
90
+ if ! command -v npx >/dev/null 2>&1; then
91
+ echo "ℹ️ SKIP - npx not available (no TypeScript checker)"
92
+ return 2
93
+ fi
94
+
95
+ local project_checker
96
+ if project_checker=$(resolve_typecheck_command); then
97
+ # Word-split deliberately on the command name only: `npm run <script>` from
98
+ # package.json, or a user-set GATE_TS_TYPECHECK_CMD. Never a path list.
99
+ local checker_argv=()
100
+ read -r -a checker_argv <<< "$project_checker"
101
+ echo "Running project typecheck (${project_checker})..."
102
+ "${checker_argv[@]}" 2>&1 | head -30
103
+ if [ "${PIPESTATUS[0]}" -ne 0 ]; then
104
+ echo ""
105
+ echo "❌ BLOCKED - TYPE ERRORS detected by the project's own typecheck script"
106
+ echo "Fix the type errors above before committing."
107
+ return 1
108
+ fi
109
+ echo "✅ PASSED - project typecheck."
110
+ return 0
111
+ fi
112
+
113
+ # Verify tsc is the real compiler (npx may fetch the deprecated tsc@2.0.4 placeholder).
114
+ local tsc_version
115
+ tsc_version=$(npx tsc --version 2>&1)
116
+ if ! echo "$tsc_version" | grep -q "Version"; then
117
+ echo "ℹ️ SKIP - tsc not properly installed ($tsc_version)"
118
+ echo "Install typescript (npm install typescript --save-dev) for type checking."
119
+ return 2
120
+ fi
121
+
122
+ if [ ! -f "tsconfig.json" ]; then
123
+ echo "ℹ️ SKIP - tsconfig.json not found (no TypeScript project config)"
124
+ return 2
125
+ fi
126
+
127
+ echo "Running TypeScript static analysis ($tsc_version)..."
128
+ npx tsc --noEmit --skipLibCheck 2>&1 | head -30
129
+ if [ "${PIPESTATUS[0]}" -ne 0 ]; then
130
+ echo ""
131
+ echo "❌ BLOCKED - TYPE ERRORS detected"
132
+ echo "Fix the type errors above before committing."
133
+ return 1
134
+ fi
135
+ echo "✅ PASSED - TypeScript static analysis."
136
+
137
+ if [ -d "src/__tests__" ] || [ -d "src/tests" ] || [ -d "tests" ] || [ -d "__tests__" ]; then
138
+ run_test_file_pass || return 1
139
+ fi
140
+ return 0
141
+ }
@@ -6,6 +6,17 @@ const fs = require('node:fs');
6
6
  const REQUIRED_ROLES = ['architecture', 'technical', 'feasibility'];
7
7
  const VALIDITY_MS = 60 * 60 * 1000;
8
8
 
9
+ /**
10
+ * Allowed provenance values for an expert record.
11
+ *
12
+ * `channel` is an EXPLICIT whitelist. An absent field is rejected rather than
13
+ * defaulted: treating "missing" as `local` (or as anything else) would be a
14
+ * zero-cost bypass, since omitting one key would let evidence that never came
15
+ * from a real model call pass the gate. Only these exact strings are accepted;
16
+ * casing and whitespace are not normalised.
17
+ */
18
+ const VALID_CHANNELS = ['external', 'local'];
19
+
9
20
  function fail(message) {
10
21
  console.error(`Invalid code walkthrough evidence: ${message}`);
11
22
  process.exit(1);
@@ -47,7 +58,7 @@ function parseTimestamp(value, field) {
47
58
  return date.getTime();
48
59
  }
49
60
 
50
- function validateExpert(expert, index, roles, models) {
61
+ function validateExpert(expert, index, roles, models, resolvedModels) {
51
62
  if (!isPlainObject(expert)) fail(`experts[${index}] must be a plain object.`);
52
63
  if (!REQUIRED_ROLES.includes(expert.role)) fail(`experts[${index}].role is invalid.`);
53
64
  if (roles.has(expert.role)) fail(`expert role ${expert.role} is duplicated.`);
@@ -61,6 +72,22 @@ function validateExpert(expert, index, roles, models) {
61
72
  fail(`expert ${expert.role} contains an error or fallback marker.`);
62
73
  }
63
74
 
75
+ // Provenance is mandatory and must be an exact whitelist value (#423). Without
76
+ // this, three hand-written records with distinct model NAMES satisfied the old
77
+ // check while no model was ever called.
78
+ if (!VALID_CHANNELS.includes(expert.channel)) {
79
+ fail(
80
+ `expert ${expert.role} channel must be one of: ${VALID_CHANNELS.join(', ')}.`
81
+ );
82
+ }
83
+
84
+ // requested_model is compared EXACTLY (after trimming); resolved_model below is
85
+ // compared case-INSENSITIVELY. The asymmetry is intentional, not an oversight:
86
+ // a requested ID is a case-sensitive gateway identifier and AGENTS.md requires
87
+ // "three distinct trimmed requested model IDs", whereas resolved_model answers
88
+ // "did these actually run on one model?" -- and gateways do report the same model
89
+ // with different casing per call, which is how #423 slipped through. Pinned by
90
+ // scripts/__tests__/gate-mw-provenance.test.cjs (AC-423-08).
64
91
  if (typeof expert.requested_model !== 'string' || expert.requested_model.trim() === '') {
65
92
  fail(`expert ${expert.role} requested_model must be non-empty.`);
66
93
  }
@@ -68,10 +95,18 @@ function validateExpert(expert, index, roles, models) {
68
95
  if (models.has(model)) fail(`requested_model ${model} is duplicated.`);
69
96
  models.add(model);
70
97
 
71
- if (expert.resolved_model !== null
72
- && (typeof expert.resolved_model !== 'string' || expert.resolved_model.trim() === '')) {
73
- fail(`expert ${expert.role} resolved_model must be non-empty or null.`);
98
+ // `resolved_model` must be a real, DISTINCT model. Previously null was allowed,
99
+ // so an expert that never resolved a model still counted as successful; and
100
+ // nothing compared resolved models, so all three could report the same one
101
+ // while `requested_model` differed.
102
+ if (typeof expert.resolved_model !== 'string' || expert.resolved_model.trim() === '') {
103
+ fail(`expert ${expert.role} resolved_model must be a non-empty string.`);
104
+ }
105
+ const resolved = expert.resolved_model.trim().toLowerCase();
106
+ if (resolvedModels.has(resolved)) {
107
+ fail(`expert ${expert.role} resolved_model ${expert.resolved_model.trim()} is duplicated.`);
74
108
  }
109
+ resolvedModels.add(resolved);
75
110
  }
76
111
 
77
112
  function validateEvidence(evidence, expectedCommit, expectedBranch, now) {
@@ -100,8 +135,11 @@ function validateEvidence(evidence, expectedCommit, expectedBranch, now) {
100
135
  }
101
136
  const roles = new Set();
102
137
  const models = new Set();
138
+ // Scoped to this validation run, so one document's models cannot leak into the
139
+ // next (a long-lived process validating several pushes).
140
+ const resolvedModels = new Set();
103
141
  evidence.experts.forEach((expert, index) => {
104
- validateExpert(expert, index, roles, models);
142
+ validateExpert(expert, index, roles, models, resolvedModels);
105
143
  });
106
144
  if (roles.size !== REQUIRED_ROLES.length) fail('all required expert roles must be present.');
107
145
  }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # post-merge — XP-Gate VERSION sync hook
4
+ #
5
+ # Detects whether VERSION changed during a merge (via ORIG_HEAD comparison)
6
+ # and, if so, runs scripts/sync-version.cjs (cross-platform Node — preferred)
7
+ # or falls back to scripts/sync-version.sh (legacy, kept for compatibility).
8
+ #
9
+ # Safety features:
10
+ # - Guard file (.xp-gate/.version-synced) prevents infinite loops
11
+ # - Runtime check (node/bash) before running the sync script
12
+ # - [skip ci] in auto-commit message to prevent recursive CI triggers
13
+ # - ORIG_HEAD fallback: if ORIG_HEAD is missing (shallow clone / first commit),
14
+ # the check is skipped with a warning.
15
+ #
16
+ set -e
17
+
18
+ PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo '')"
19
+ if [[ -z "$PROJECT_ROOT" ]]; then
20
+ # not a git repo — silently exit
21
+ exit 0
22
+ fi
23
+
24
+ GUARD_FILE="$PROJECT_ROOT/.xp-gate/.version-synced"
25
+
26
+ # ── Guard: skip if already synced for this commit ──
27
+ CURRENT_HEAD="$(git rev-parse HEAD 2>/dev/null || echo '')"
28
+ if [[ -f "$GUARD_FILE" ]]; then
29
+ read -r GUARD_HASH < "$GUARD_FILE" 2>/dev/null || true
30
+ if [[ "$GUARD_HASH" == "$CURRENT_HEAD" ]]; then
31
+ # already synced for this commit — skip
32
+ exit 0
33
+ fi
34
+ fi
35
+
36
+ # ── Only proceed if this is an xp-gate project ──
37
+ SYNC_CJS="$PROJECT_ROOT/scripts/sync-version.cjs"
38
+ SYNC_SH="$PROJECT_ROOT/scripts/sync-version.sh"
39
+ if [[ ! -f "$SYNC_CJS" && ! -f "$SYNC_SH" ]]; then
40
+ exit 0
41
+ fi
42
+
43
+ # ── ORIG_HEAD check: detect VERSION change during merge ──
44
+ ORIG_HEAD="$(git rev-parse ORIG_HEAD 2>/dev/null || echo '')"
45
+ if [[ -z "$ORIG_HEAD" ]]; then
46
+ # ORIG_HEAD not available (shallow clone, first commit, or non-merge operation)
47
+ exit 0
48
+ fi
49
+
50
+ VERSION_CHANGED=false
51
+ if git diff-tree --name-only -r "$ORIG_HEAD" HEAD 2>/dev/null | grep -q '^VERSION$'; then
52
+ VERSION_CHANGED=true
53
+ fi
54
+
55
+ if [[ "$VERSION_CHANGED" != "true" ]]; then
56
+ # VERSION not changed — zero overhead
57
+ exit 0
58
+ fi
59
+
60
+ # ── Run sync-version — .cjs preferred (cross-platform, covers more targets) ──
61
+ SYNC_OK=false
62
+
63
+ if [[ -f "$SYNC_CJS" ]] && command -v node &>/dev/null; then
64
+ echo "[xp-gate] VERSION changed in merge — running sync-version.cjs..."
65
+ if node "$SYNC_CJS"; then
66
+ SYNC_OK=true
67
+ else
68
+ echo "[xp-gate] Warning: sync-version.cjs failed — VERSION files may be out of sync." >&2
69
+ fi
70
+ elif [[ -f "$SYNC_SH" ]]; then
71
+ # Legacy fallback — the .sh implementation is Unix-only (MSYS path bug on Windows)
72
+ if ! command -v bash &>/dev/null; then
73
+ echo "[xp-gate] Warning: bash not found — cannot run sync-version.sh" >&2
74
+ exit 0
75
+ fi
76
+ echo "[xp-gate] VERSION changed in merge — running sync-version.sh (fallback)..."
77
+ if bash "$SYNC_SH"; then
78
+ SYNC_OK=true
79
+ else
80
+ echo "[xp-gate] Warning: sync-version.sh failed — VERSION files may be out of sync." >&2
81
+ fi
82
+ else
83
+ echo "[xp-gate] Warning: node not found — cannot run sync-version.cjs" >&2
84
+ fi
85
+
86
+ if [[ "$SYNC_OK" == "true" ]]; then
87
+ # record guard
88
+ mkdir -p "$(dirname "$GUARD_FILE")"
89
+ echo "$CURRENT_HEAD" > "$GUARD_FILE"
90
+ echo "[xp-gate] VERSION synced. Affected files:"
91
+ echo " - VERSION"
92
+ echo " - package.json (+ src/npm-package/package.json)"
93
+ echo " - plugins/{claude-code,opencode,qoder,dsh} manifests"
94
+ echo " - src/npm-package/plugins/** mirrors"
95
+ echo " - AGENTS.md headers (date/commit/branch/version)"
96
+ echo "[skip ci]"
97
+ echo "Run 'git add' and 'git commit' to commit these changes."
98
+ fi
99
+
100
+ exit 0