@boyingliu01/xp-gate 0.14.8 → 0.14.9

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 (40) hide show
  1. package/bin/xp-gate.js +23 -2
  2. package/lib/__tests__/update-hooks.test.js +12 -0
  3. package/lib/__tests__/upgrade-skill-sync.test.js +65 -0
  4. package/lib/check.js +115 -20
  5. package/lib/doctor.js +63 -0
  6. package/lib/gate-runner.js +209 -0
  7. package/lib/shared-utils.js +1 -1
  8. package/lib/sprint-status.js +3 -1
  9. package/lib/update-hooks.js +1 -1
  10. package/lib/upgrade.js +8 -0
  11. package/mock-policy/AGENTS.md +3 -3
  12. package/mutation/AGENTS.md +3 -3
  13. package/mutation/__tests__/init-baseline.test.ts +347 -0
  14. package/mutation/__tests__/mutmut-runner.test.ts +561 -0
  15. package/mutation/__tests__/runners-index.test.ts +134 -0
  16. package/mutation/__tests__/stryker-runner.test.ts +455 -0
  17. package/mutation/__tests__/update-baseline.test.ts +393 -0
  18. package/package.json +1 -1
  19. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  20. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  21. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  22. package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
  23. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +34 -8
  24. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  25. package/plugins/opencode/package.json +1 -1
  26. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  27. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  28. package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
  29. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +34 -8
  30. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  31. package/plugins/qoder/plugin.json +1 -1
  32. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  33. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  34. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  35. package/principles/AGENTS.md +3 -3
  36. package/skills/delphi-review/AGENTS.md +3 -3
  37. package/skills/sprint-flow/AGENTS.md +3 -3
  38. package/skills/sprint-flow/SKILL.md +1 -1
  39. package/skills/sprint-flow/references/phase-2-design.md +34 -8
  40. package/skills/test-specification-alignment/AGENTS.md +3 -3
package/bin/xp-gate.js CHANGED
@@ -14,6 +14,7 @@ const { arch } = require('../lib/arch.js');
14
14
  const { upgrade } = require('../lib/upgrade.js');
15
15
  const { bootstrap } = require('../lib/bootstrap.js');
16
16
  const { updateHooks } = require('../lib/update-hooks.js');
17
+ const gateRunner = require('../lib/gate-runner.js');
17
18
 
18
19
  function handleUIReview() {
19
20
  const { execSync } = require('child_process');
@@ -105,9 +106,9 @@ const COMMANDS = {
105
106
  usage: 'xp-gate baseline <create|show|reset|diff>'
106
107
  },
107
108
  'check': {
108
- description: 'Run user-invokable quality gates (Gate 4 Principles + Gate 6 Architecture) on a path',
109
+ description: 'Run quality gates (--list, --all, --gates <ids>, or <gate-id>)',
109
110
  run: subargs => check(subargs).then(code => process.exit(code)),
110
- usage: 'xp-gate check <file_or_directory> [--gates principles,arch]'
111
+ usage: 'xp-gate check <file_or_directory|gate_number> [--gates gates] [--all] [--list]'
111
112
  },
112
113
  'principles': {
113
114
  description: 'Run Clean Code + SOLID principles checker (Gate 4 standalone)',
@@ -173,9 +174,29 @@ const COMMANDS = {
173
174
  install(subargs).then(code => process.exit(code));
174
175
  },
175
176
  usage: 'xp-gate install [--global]'
177
+ },
178
+ 'gate-list': {
179
+ description: 'List all quality gates and their availability',
180
+ run: subargs => check(['--list']).then(code => process.exit(code)),
181
+ usage: 'xp-gate gate-list'
182
+ },
183
+ 'gate-all': {
184
+ description: 'Run all invokable quality gates',
185
+ run: subargs => check(['--all', ...subargs]).then(code => process.exit(code)),
186
+ usage: 'xp-gate gate-all [<path>]'
176
187
  }
177
188
  };
178
189
 
190
+ for (let i = 0; i <= 11; i++) {
191
+ const gateId = String(i);
192
+ const gateInfo = gateRunner.GATE_REGISTRY[gateId];
193
+ COMMANDS[`gate-${gateId}`] = {
194
+ description: `Run Gate ${gateId}: ${gateInfo?.name || 'Unknown'}`,
195
+ run: subargs => gateRunner.runGate(gateId, subargs[0]).then(code => process.exit(code)),
196
+ usage: `xp-gate gate-${gateId} [<path>]`
197
+ };
198
+ }
199
+
179
200
  function printHelp() {
180
201
  console.log('xp-gate - AI development workflow tool');
181
202
  console.log('');
@@ -140,6 +140,18 @@ describe('updateHooks', () => {
140
140
  expect(fs.readFileSync(path.join(dest, 'gate-4.sh'), 'utf8')).toContain('gate4-v2');
141
141
  });
142
142
 
143
+ it('copies sprint-gate.sh alongside gate-*.sh scripts (#335)', () => {
144
+ createPackageSource();
145
+ fs.writeFileSync(path.join(tmpPackage, 'sprint-gate.sh'), '#!/bin/bash\necho "sprint-gate-v1"');
146
+ const mod = getModule();
147
+ const dest = path.join(tmpProject, 'gates-sprint');
148
+ fs.mkdirSync(dest, { recursive: true });
149
+
150
+ mod.copyGateScripts(tmpPackage, dest, false, true);
151
+
152
+ expect(fs.readFileSync(path.join(dest, 'sprint-gate.sh'), 'utf8')).toContain('sprint-gate-v1');
153
+ });
154
+
143
155
  it('does not copy non-gate files', () => {
144
156
  createPackageSource();
145
157
  fs.writeFileSync(path.join(tmpPackage, 'some-other.sh'), 'echo other');
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @test REQ-332 xp-gate upgrade --apply must update installed skills
3
+ * @intent After upgrade --apply, all installed skills should be refreshed to match the new package version
4
+ * @covers AC-332-01, AC-332-02, AC-332-03, AC-332-04
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ describe('upgrade --apply skill sync — #332', () => {
11
+ // AC-332-01: upgrade.js handleApplyMode calls updateSkill after successful upgrade
12
+ it('AC-332-01: upgrade.js contains updateSkill call after plugin upgrade', () => {
13
+ const source = fs.readFileSync(require.resolve('../upgrade.js'), 'utf8');
14
+ expect(source).toContain("require('./update-skill.js')");
15
+ expect(source).toContain('updateSkill(null, { all: true');
16
+ });
17
+
18
+ // AC-332-02: updateSkill call is inside try/catch (non-blocking)
19
+ it('AC-332-02: updateSkill call is wrapped in try/catch for non-blocking behavior', () => {
20
+ const source = fs.readFileSync(require.resolve('../upgrade.js'), 'utf8');
21
+ const lines = source.split('\n');
22
+ let foundRequire = false;
23
+ let inTryBlock = false;
24
+ for (let i = 0; i < lines.length; i++) {
25
+ if (lines[i].includes("require('./update-skill.js')")) {
26
+ foundRequire = true;
27
+ for (let j = i - 1; j >= 0; j--) {
28
+ if (/^\s*try\s*\{/.test(lines[j])) {
29
+ inTryBlock = true;
30
+ break;
31
+ }
32
+ if (/^\s*(function|async function|=>)/.test(lines[j])) break;
33
+ }
34
+ break;
35
+ }
36
+ }
37
+ expect(foundRequire).toBe(true);
38
+ expect(inTryBlock).toBe(true);
39
+ });
40
+
41
+ // AC-332-03: updateSkill call is placed after upgradeOpenCodePlugin (correct ordering)
42
+ it('AC-332-03: updateSkill is called after upgradeOpenCodePlugin in handleApplyMode', () => {
43
+ const source = fs.readFileSync(require.resolve('../upgrade.js'), 'utf8');
44
+ const pluginIdx = source.indexOf('upgradeOpenCodePlugin()');
45
+ const skillIdx = source.indexOf("require('./update-skill.js')");
46
+ expect(pluginIdx).toBeGreaterThan(-1);
47
+ expect(skillIdx).toBeGreaterThan(pluginIdx);
48
+ });
49
+ });
50
+
51
+ describe('doctor skill version check — #332', () => {
52
+ // AC-332-04: doctor.js diagnose() includes skill version consistency check
53
+ it('AC-332-04: doctor.js diagnose() contains diagnoseInstalledSkills call', () => {
54
+ const source = fs.readFileSync(require.resolve('../doctor.js'), 'utf8');
55
+ expect(source).toContain('diagnoseInstalledSkills');
56
+ });
57
+
58
+ // AC-332-05: diagnoseInstalledSkills compares installed skills against package-bundled skills
59
+ it('AC-332-05: diagnoseInstalledSkills reads config installedSkills and package skills dir', () => {
60
+ const source = fs.readFileSync(require.resolve('../doctor.js'), 'utf8');
61
+ expect(source).toContain('installedSkills');
62
+ expect(source).toContain('diagnoseInstalledSkills');
63
+ expect(source).toContain('skills');
64
+ });
65
+ });
package/lib/check.js CHANGED
@@ -2,49 +2,144 @@
2
2
 
3
3
  const { principles } = require('./principles.js');
4
4
  const { arch } = require('./arch.js');
5
+ const { getGateInfo, runGate } = require('./gate-runner.js');
6
+
7
+ function parseCheckArgs(args) {
8
+ const flags = { all: false, list: false, gateIds: null };
9
+ const posArgs = [];
10
+ let i = 0;
11
+ while (i < args.length) {
12
+ const arg = args[i];
13
+ if (arg === '--all') { flags.all = true; i++; }
14
+ else if (arg === '--list') { flags.list = true; i++; }
15
+ else if (arg === '--gates' && i + 1 < args.length) {
16
+ flags.gateIds = args[i + 1].split(',').map(s => s.trim()).filter(Boolean);
17
+ i += 2;
18
+ }
19
+ else if (arg.startsWith('--gates=')) {
20
+ flags.gateIds = arg.split('=')[1].split(',').map(s => s.trim()).filter(Boolean);
21
+ i++;
22
+ }
23
+ else { posArgs.push(arg); i++; }
24
+ }
25
+ const rawTarget = posArgs[0] || null;
26
+ const isGateNumber = rawTarget && /^\d+$/.test(rawTarget);
27
+ return {
28
+ ...flags,
29
+ positionals: posArgs,
30
+ pathArg: isGateNumber ? posArgs[1] || null : rawTarget,
31
+ gateNumber: isGateNumber ? rawTarget : null,
32
+ };
33
+ }
5
34
 
6
- // Runs the user-invokable subset of quality gates on a path. Mirrors what the
7
- // pre-commit hook does, but as a standalone CLI surface. Intentionally narrower
8
- // than the full pre-commit run (no git staging context, no Boy Scout baseline).
9
- // Currently delegates to: Gate 4 (Principles) + Gate 6 (Architecture).
10
35
  async function check(args) {
11
- const target = args[0];
12
- if (!target) {
13
- console.error('Usage: xp-gate check <file_or_directory> [--gates principles,arch]');
36
+ const opts = parseCheckArgs(args);
37
+
38
+ if (opts.list) return listGates();
39
+ if (opts.gateNumber && !opts.all && !opts.gateIds) return runGate(opts.gateNumber, opts.pathArg);
40
+ if (opts.all) return checkAllGates(opts.pathArg);
41
+ if (opts.gateIds && opts.gateIds.length > 0) return runGatesByIds(opts.gateIds, opts.pathArg);
42
+
43
+ if (!opts.pathArg) {
44
+ console.error('Usage: xp-gate check <file_or_directory|gate_number> [options]');
45
+ console.error('');
46
+ console.error('Options:');
47
+ console.error(' --gates <ids> Run specific gates (comma-separated, e.g. "3,4,6")');
48
+ console.error(' --all Run all invokable gates');
49
+ console.error(' --list List all available gates');
50
+ console.error('');
51
+ console.error('Examples:');
52
+ console.error(' xp-gate check src/ # Run principles + architecture check');
53
+ console.error(' xp-gate check 3 # Run Gate 3 (Complexity)');
54
+ console.error(' xp-gate check --gates 3,4,6 # Run gates 3, 4, and 6');
55
+ console.error(' xp-gate check --all # Run all invokable gates');
56
+ console.error(' xp-gate check --list # List available gates');
14
57
  return 1;
15
58
  }
16
59
 
17
- const gatesIdx = args.indexOf('--gates');
18
- const gateList = gatesIdx >= 0
19
- ? args[gatesIdx + 1].split(',').map(s => s.trim()).filter(Boolean)
20
- : ['principles', 'arch'];
60
+ return runDefaultChecks(opts.pathArg);
61
+ }
62
+
63
+ async function runDefaultChecks(target) {
64
+ let failures = 0;
65
+ let ran = 0;
66
+
67
+ console.log('━━━ Gate 4: Principles ━━━');
68
+ const principlesCode = await principles([target]);
69
+ if (principlesCode !== 0) failures += 1;
70
+ ran += 1;
71
+
72
+ console.log('━━━ Gate 6: Architecture ━━━');
73
+ const archCode = await arch([]);
74
+ if (archCode !== 0) failures += 1;
75
+ ran += 1;
76
+
77
+ console.log('');
78
+ console.log(`[xp-gate check] ${ran - failures}/${ran} gate(s) passed`);
79
+ return failures === 0 ? 0 : 1;
80
+ }
21
81
 
82
+ async function checkAllGates(target) {
83
+ const { getAllGates } = require('./gate-runner.js');
84
+ const invokableGates = getAllGates().filter(g => !g.preCommitOnly);
22
85
  let failures = 0;
23
86
  let ran = 0;
24
87
 
25
- if (gateList.includes('principles')) {
26
- console.log('━━━ Gate 4: Principles ━━━');
27
- const code = await principles([target]);
88
+ for (const gate of invokableGates) {
89
+ const code = await runGate(gate.id, target);
28
90
  if (code !== 0) failures += 1;
29
91
  ran += 1;
92
+ console.log('');
30
93
  }
31
94
 
32
- if (gateList.includes('arch')) {
33
- console.log('━━━ Gate 6: Architecture ━━━');
34
- const code = await arch([]);
95
+ console.log(`[xp-gate check --all] ${ran - failures}/${ran} gate(s) passed`);
96
+ return failures === 0 ? 0 : 1;
97
+ }
98
+
99
+ async function runGatesByIds(gateIds, target) {
100
+ let failures = 0;
101
+ let ran = 0;
102
+
103
+ for (const gateId of gateIds) {
104
+ const code = await runGate(gateId, target);
35
105
  if (code !== 0) failures += 1;
36
106
  ran += 1;
107
+ console.log('');
37
108
  }
38
109
 
39
110
  if (ran === 0) {
40
- console.error(`[xp-gate check] No valid gates in --gates list: ${gateList.join(',')}`);
41
- console.error(' Available gates: principles, arch');
111
+ console.error(`[xp-gate check] No valid gates in --gates list: ${gateIds.join(',')}`);
112
+ console.error(' Run: xp-gate check --list to see available gates');
42
113
  return 1;
43
114
  }
44
115
 
45
- console.log('');
46
116
  console.log(`[xp-gate check] ${ran - failures}/${ran} gate(s) passed`);
47
117
  return failures === 0 ? 0 : 1;
48
118
  }
49
119
 
120
+ function listGates() {
121
+ const { getAllGates } = require('./gate-runner.js');
122
+ const allGates = getAllGates();
123
+
124
+ console.log('');
125
+ console.log('XP-Gate Quality Gates:');
126
+ console.log('┌──────┬──────────────────────────────┬──────────┬──────────────────────────────────────────┐');
127
+ console.log('│ Gate │ Name │ CLI │ Description │');
128
+ console.log('├──────┼──────────────────────────────┼──────────┼──────────────────────────────────────────┤');
129
+ for (const gate of allGates) {
130
+ const id = gate.id.padEnd(4);
131
+ const name = gate.name.slice(0, 28).padEnd(28);
132
+ const cli = gate.preCommitOnly ? 'commit' : 'check';
133
+ const desc = gate.description.slice(0, 40).padEnd(40);
134
+ console.log(`│ ${id} │ ${name} │ ${cli.padEnd(8)} │ ${desc} │`);
135
+ }
136
+ console.log('└──────┴──────────────────────────────┴──────────┴──────────────────────────────────────────┘');
137
+ console.log('');
138
+ console.log('Legend:');
139
+ console.log(' check — invokable via xp-gate check <gate-id>');
140
+ console.log(' commit — only runs during git commit (pre-commit hook)');
141
+ console.log('');
142
+ return 0;
143
+ }
144
+
50
145
  module.exports = { check };
package/lib/doctor.js CHANGED
@@ -263,6 +263,9 @@ function diagnose() {
263
263
  // --- Check 8: TUI auto-registration ---
264
264
  issues += diagnoseTuiRegistration(checks);
265
265
 
266
+ // --- Check 9: Installed skills version consistency (#332) ---
267
+ issues += diagnoseInstalledSkills(config, checks);
268
+
266
269
  return { checks, issues };
267
270
  }
268
271
 
@@ -689,6 +692,66 @@ function diagnoseTuiRegistration(checks) {
689
692
  return 1;
690
693
  }
691
694
 
695
+ /**
696
+ * Check 9: Installed skills vs package-bundled skills (#332).
697
+ * Reports WARN for each installed skill whose SKILL.md differs from the bundled version.
698
+ * @param {object} config
699
+ * @param {Array} checks
700
+ * @returns {number} issue count
701
+ */
702
+ function diagnoseInstalledSkills(config, checks) {
703
+ const installedSkills = config.installedSkills || {};
704
+ const skillNames = Object.keys(installedSkills);
705
+ if (skillNames.length === 0) {
706
+ checks.push({ name: 'Installed skills', status: 'SKIP', detail: 'No skills installed' });
707
+ return 0;
708
+ }
709
+
710
+ const bundledSkillsDir = path.join(PKG_DIR, 'skills');
711
+ if (!fs.existsSync(bundledSkillsDir)) {
712
+ checks.push({ name: 'Installed skills', status: 'SKIP', detail: 'Package skills dir not found' });
713
+ return 0;
714
+ }
715
+
716
+ const platform = detectPlatform();
717
+ let userSkillsDir;
718
+ if (platform === 'qoder') {
719
+ userSkillsDir = path.join(HOME_DIR, '.qoder', 'skills');
720
+ } else if (platform === 'claude-code') {
721
+ userSkillsDir = path.join(HOME_DIR, '.claude', 'skills');
722
+ } else {
723
+ userSkillsDir = path.join(HOME_DIR, '.config', 'opencode', 'skills');
724
+ }
725
+
726
+ let issues = 0;
727
+ for (const name of skillNames) {
728
+ const bundledSkillMd = path.join(bundledSkillsDir, name, 'SKILL.md');
729
+ const userSkillMd = path.join(userSkillsDir, name, 'SKILL.md');
730
+
731
+ if (!fs.existsSync(bundledSkillMd)) continue;
732
+ if (!fs.existsSync(userSkillMd)) {
733
+ checks.push({ name: `Skill: ${name}`, status: 'FAIL', detail: 'Installed SKILL.md missing' });
734
+ issues++;
735
+ continue;
736
+ }
737
+
738
+ const bundledContent = fs.readFileSync(bundledSkillMd, 'utf8');
739
+ const userContent = fs.readFileSync(userSkillMd, 'utf8');
740
+ if (bundledContent !== userContent) {
741
+ checks.push({
742
+ name: `Skill: ${name}`,
743
+ status: 'WARN',
744
+ detail: 'Outdated — run xp-gate update-skill --all',
745
+ });
746
+ issues++;
747
+ } else {
748
+ checks.push({ name: `Skill: ${name}`, status: 'PASS', detail: 'Up to date' });
749
+ }
750
+ }
751
+
752
+ return issues;
753
+ }
754
+
692
755
  /**
693
756
  * Fix TUI registration: ensure @boyingliu01/opencode-plugin/tui is in tui.json.
694
757
  * Uses atomic write (tmp + renameSync) for JSON safety.
@@ -0,0 +1,209 @@
1
+ 'use strict';
2
+
3
+ const { execSync, execFileSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ // Gate metadata registry — maps gate IDs to names, descriptions, and how to run them.
8
+ // Standalone gates (<gate-id>: { run }) are invokable via xp-gate gate-<id>.
9
+ // Pre-commit-only gates (<gate-id>: { preCommitOnly: true }) run only in git commit context.
10
+ const GATE_REGISTRY = {
11
+ '0': {
12
+ name: 'Version Consistency',
13
+ description: 'Check VERSION file matches all package.json versions',
14
+ preCommitOnly: true,
15
+ reason: 'Requires git-staged context to compare VERSION with package.json files',
16
+ },
17
+ '1': {
18
+ name: 'Code Quality',
19
+ description: 'Language-specific static analysis and linting (ESLint, Ruff, govet, etc.)',
20
+ preCommitOnly: true,
21
+ reason: 'Requires language detection from changed files and adapter routing',
22
+ },
23
+ '2': {
24
+ name: 'Duplicate Code',
25
+ description: 'jscpd duplicate code detection',
26
+ run: (targetPath) => {
27
+ const jscpdConfig = findConfig(targetPath, 'jscpd.conf.json');
28
+ const args = [targetPath || '.'];
29
+ if (jscpdConfig) args.push('--config', jscpdConfig);
30
+ execFileSync('npx', ['-y', 'jscpd', ...args], { stdio: 'inherit' });
31
+ },
32
+ },
33
+ '3': {
34
+ name: 'Cyclomatic Complexity',
35
+ description: 'lizard cyclomatic complexity analysis',
36
+ run: (targetPath) => {
37
+ const script = resolveGateScript('3');
38
+ if (script) {
39
+ execSync(`bash "${script}"`, { stdio: 'inherit' });
40
+ } else {
41
+ const target = targetPath || '.';
42
+ console.log(`Running lizard on ${target}...`);
43
+ execFileSync('lizard', [target, '--CCN', '10', '--length', '50', '--arguments', '5', '--warnings_only'], { stdio: 'inherit' });
44
+ }
45
+ },
46
+ },
47
+ '4': {
48
+ name: 'Clean Code + SOLID Principles',
49
+ description: '14 Clean Code/SOLID rules across 9 languages',
50
+ run: (targetPath) => {
51
+ const { principles } = require('./principles.js');
52
+ return principles([targetPath || '.']);
53
+ },
54
+ },
55
+ '5': {
56
+ name: 'Tests + Coverage',
57
+ description: 'Unit test execution and code coverage enforcement (≥80%)',
58
+ preCommitOnly: true,
59
+ reason: 'Requires language detection and adapter-sourced test execution with coverage reports',
60
+ },
61
+ '6': {
62
+ name: 'Architecture + Boy Scout Rule',
63
+ description: 'Architecture layer boundary validation and warning baseline enforcement',
64
+ run: (targetPath) => {
65
+ const { arch } = require('./arch.js');
66
+ return arch([]);
67
+ },
68
+ },
69
+ '7': {
70
+ name: 'IaC Security',
71
+ description: 'Infrastructure-as-Code security scanning (checkov, hadolint, kube-score, tflint)',
72
+ run: (targetPath) => {
73
+ const script = resolveGateScript('7');
74
+ if (script) {
75
+ execSync(`bash "${script}"`, { stdio: 'inherit' });
76
+ } else {
77
+ console.log('IaC security scan requires git-staged context for changed files detection.');
78
+ console.log('Run: git commit to trigger Gate 7 automatically.');
79
+ }
80
+ },
81
+ },
82
+ '8': {
83
+ name: 'Secret Scanning',
84
+ description: 'gitleaks secret and credential detection',
85
+ run: (targetPath) => {
86
+ const script = resolveGateScript('8');
87
+ if (script) {
88
+ execSync(`bash "${script}"`, { stdio: 'inherit' });
89
+ } else {
90
+ console.log('Secret scanning requires git-staged context for changed files detection.');
91
+ console.log('Run: git commit to trigger Gate 8 automatically.');
92
+ }
93
+ },
94
+ },
95
+ '9': {
96
+ name: 'SAST Security',
97
+ description: 'semgrep static application security testing',
98
+ run: (targetPath) => {
99
+ const script = resolveGateScript('9');
100
+ if (script) {
101
+ execSync(`bash "${script}"`, { stdio: 'inherit' });
102
+ } else {
103
+ console.log('SAST scanning requires git-staged context for changed files detection.');
104
+ console.log('Run: git commit to trigger Gate 9 automatically.');
105
+ }
106
+ },
107
+ },
108
+ '10': {
109
+ name: 'Build Integrity',
110
+ description: 'TypeScript compilation, npm pack, and import validation',
111
+ preCommitOnly: true,
112
+ reason: 'Build integrity checks require language detection and build tool context',
113
+ },
114
+ '11': {
115
+ name: 'Sprint Flow Enforcement',
116
+ description: 'Sprint state and delphi-review validation',
117
+ preCommitOnly: true,
118
+ reason: 'Requires sprint state (.sprint-state/) and git branch context',
119
+ },
120
+ };
121
+
122
+ function findConfig(basePath, fileName) {
123
+ const candidates = [
124
+ path.join(basePath || '.', fileName),
125
+ path.join(process.cwd(), fileName),
126
+ ];
127
+ for (const c of candidates) {
128
+ if (fs.existsSync(c)) return c;
129
+ }
130
+ return null;
131
+ }
132
+
133
+ function resolveGateScript(gateNum) {
134
+ const candidates = [
135
+ path.join(process.cwd(), 'githooks', 'gates', `gate-${gateNum}-*.sh`),
136
+ path.join(process.cwd(), 'githooks', `gate-${gateNum}.sh`),
137
+ ];
138
+
139
+ // Try glob patterns
140
+ for (const pattern of candidates) {
141
+ if (pattern.includes('*')) {
142
+ const dir = path.dirname(pattern);
143
+ const prefix = path.basename(pattern).replace('*', '');
144
+ if (fs.existsSync(dir)) {
145
+ const match = fs.readdirSync(dir).find(f => f.startsWith(`gate-${gateNum}-`) && f.endsWith('.sh'));
146
+ if (match) return path.join(dir, match);
147
+ }
148
+ } else if (fs.existsSync(pattern)) {
149
+ return pattern;
150
+ }
151
+ }
152
+
153
+ // Check global install
154
+ const globalDir = path.join(require('os').homedir(), '.config', 'xp-gate', 'adapters');
155
+ const globalScript = path.join(globalDir, `gate-${gateNum}.sh`);
156
+ if (fs.existsSync(globalScript)) return globalScript;
157
+
158
+ return null;
159
+ }
160
+
161
+ function getGateInfo(gateId) {
162
+ return GATE_REGISTRY[String(gateId)] || null;
163
+ }
164
+
165
+ function getAllGates() {
166
+ return Object.entries(GATE_REGISTRY).map(([id, info]) => ({
167
+ id,
168
+ name: info.name,
169
+ description: info.description,
170
+ preCommitOnly: info.preCommitOnly || false,
171
+ reason: info.reason || null,
172
+ }));
173
+ }
174
+
175
+ async function runGate(gateId, targetPath) {
176
+ const gate = GATE_REGISTRY[String(gateId)];
177
+ if (!gate) {
178
+ console.error(`Unknown gate: ${gateId}`);
179
+ console.error('Available gates: ' + Object.keys(GATE_REGISTRY).join(', '));
180
+ return 1;
181
+ }
182
+
183
+ if (gate.preCommitOnly) {
184
+ console.log(`━━━ Gate ${gateId}: ${gate.name} ━━━`);
185
+ console.log(`⏭️ SKIPPED - ${gate.reason}`);
186
+ console.log(' This gate runs automatically during git commit.');
187
+ console.log(' Run: git commit to trigger all gates.');
188
+ return 0;
189
+ }
190
+
191
+ if (gate.run) {
192
+ console.log(`━━━ Gate ${gateId}: ${gate.name} ━━━`);
193
+ try {
194
+ const result = gate.run(targetPath);
195
+ if (result && typeof result.then === 'function') {
196
+ return await result;
197
+ }
198
+ return 0;
199
+ } catch (err) {
200
+ console.error(` Gate ${gateId} FAILED: ${err.message}`);
201
+ return 1;
202
+ }
203
+ }
204
+
205
+ console.error(`Gate ${gateId}: ${gate.name} — no execution handler available`);
206
+ return 1;
207
+ }
208
+
209
+ module.exports = { GATE_REGISTRY, getGateInfo, getAllGates, runGate };
@@ -60,7 +60,7 @@ function copyAdapters(srcDir, destDir) {
60
60
  const githooksDir = path.resolve(srcDir, '..', '..', '..', 'githooks');
61
61
  if (fs.existsSync(githooksDir)) {
62
62
  fs.readdirSync(githooksDir).forEach(f => {
63
- if (f.startsWith('gate-') && f.endsWith('.sh')) {
63
+ if ((f.startsWith('gate-') || f === 'sprint-gate.sh') && f.endsWith('.sh')) {
64
64
  fs.copyFileSync(path.join(githooksDir, f), path.join(destDir, f));
65
65
  }
66
66
  });
@@ -70,6 +70,7 @@ function statusIcon(status) {
70
70
  switch (status) {
71
71
  case 'completed': return '✅';
72
72
  case 'in_progress': return '🔄';
73
+ case 'skipped': return '⏭️';
73
74
  default: return '⏳';
74
75
  }
75
76
  }
@@ -145,7 +146,8 @@ function formatPhaseLine(key, history, maxNameLen) {
145
146
  const icon = statusIcon(status);
146
147
  const dur = formatDuration(history?.duration_seconds);
147
148
  const statusLabel = status === 'completed' ? 'Completed' :
148
- status === 'in_progress' ? 'In Progress' : 'Pending';
149
+ status === 'in_progress' ? 'In Progress' :
150
+ status === 'skipped' ? 'Skipped' : 'Pending';
149
151
  return ` Phase ${key.padStart(4)} ${name.padEnd(maxNameLen + 1)} ${icon} ${dur.padEnd(5)} ${statusLabel}`;
150
152
  }
151
153
 
@@ -190,7 +190,7 @@ function copyAdapters(srcDir, destDir, dryRun, noBackup) {
190
190
  function copyGateScripts(srcDir, destDir, dryRun, noBackup) {
191
191
  if (!fs.existsSync(srcDir)) return;
192
192
  fs.readdirSync(srcDir).forEach(f => {
193
- if (f.startsWith('gate-') && f.endsWith('.sh')) {
193
+ if ((f.startsWith('gate-') || f === 'sprint-gate.sh') && f.endsWith('.sh')) {
194
194
  const src = path.join(srcDir, f);
195
195
  const dest = path.join(destDir, f);
196
196
  atomicCopyFile(src, dest, dryRun, noBackup, f);
package/lib/upgrade.js CHANGED
@@ -95,6 +95,14 @@ async function handleApplyMode(result, pkgName) {
95
95
  // Also check for and update the local OpenCode plugin if installed.
96
96
  await upgradeOpenCodePlugin();
97
97
 
98
+ // Update all installed skills to match the newly upgraded package (#332)
99
+ try {
100
+ const { updateSkill } = require('./update-skill.js');
101
+ await updateSkill(null, { all: true, verbose: false });
102
+ } catch {
103
+ // Non-blocking: skill update failure should never break the main upgrade flow.
104
+ }
105
+
98
106
  return 0;
99
107
  } catch (err) {
100
108
  const msg = err.message || '';
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-13
4
- **Commit:** d83bb96
5
- **Branch:** main
6
- **Version:** 0.14.8.0
4
+ **Commit:** cb954fe
5
+ **Branch:** sprint/2026-07-13-01
6
+ **Version:** 0.14.9.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-13
4
- **Commit:** d83bb96
5
- **Branch:** main
6
- **Version:** 0.14.8.0
4
+ **Commit:** cb954fe
5
+ **Branch:** sprint/2026-07-13-01
6
+ **Version:** 0.14.9.0
7
7
 
8
8
  ## OVERVIEW
9
9
  **Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.