@boyingliu01/xp-gate 0.14.7 → 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 (44) hide show
  1. package/bin/xp-gate.js +31 -2
  2. package/lib/__tests__/next-sprint.test.js +231 -0
  3. package/lib/__tests__/update-hooks.test.js +12 -0
  4. package/lib/__tests__/upgrade-skill-sync.test.js +65 -0
  5. package/lib/check.js +115 -20
  6. package/lib/doctor.js +63 -0
  7. package/lib/gate-runner.js +209 -0
  8. package/lib/next-sprint.js +129 -0
  9. package/lib/shared-utils.js +1 -1
  10. package/lib/sprint-status.js +3 -1
  11. package/lib/update-hooks.js +1 -1
  12. package/lib/upgrade.js +8 -0
  13. package/mock-policy/AGENTS.md +4 -4
  14. package/mock-policy/__tests__/gate-m3.test.ts +384 -0
  15. package/mock-policy/__tests__/schema.test.ts +239 -0
  16. package/mutation/AGENTS.md +4 -4
  17. package/mutation/__tests__/init-baseline.test.ts +347 -0
  18. package/mutation/__tests__/mutmut-runner.test.ts +561 -0
  19. package/mutation/__tests__/runners-index.test.ts +134 -0
  20. package/mutation/__tests__/stryker-runner.test.ts +455 -0
  21. package/mutation/__tests__/update-baseline.test.ts +393 -0
  22. package/package.json +1 -1
  23. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  24. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  25. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  26. package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
  27. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +34 -8
  28. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  29. package/plugins/opencode/package.json +1 -1
  30. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  31. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  32. package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
  33. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +34 -8
  34. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  35. package/plugins/qoder/plugin.json +1 -1
  36. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  37. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  38. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  39. package/principles/AGENTS.md +4 -4
  40. package/skills/delphi-review/AGENTS.md +4 -4
  41. package/skills/sprint-flow/AGENTS.md +4 -4
  42. package/skills/sprint-flow/SKILL.md +1 -1
  43. package/skills/sprint-flow/references/phase-2-design.md +34 -8
  44. package/skills/test-specification-alignment/AGENTS.md +4 -4
@@ -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 };
@@ -0,0 +1,129 @@
1
+ const childProcess = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ function readSprintState(dir) {
6
+ try {
7
+ const stateFile = path.join(dir, '.sprint-state', 'sprint-state.json');
8
+ if (!fs.existsSync(stateFile)) return null;
9
+ return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function getCurrentSprintReqs(state) {
16
+ if (!state?.phase_history) return [];
17
+ const reqs = [];
18
+ for (const phase of state.phase_history) {
19
+ if (phase.reqs) {
20
+ for (const [id, req] of Object.entries(phase.reqs)) {
21
+ reqs.push({ id, name: req.name, status: req.status });
22
+ }
23
+ }
24
+ }
25
+ return reqs;
26
+ }
27
+
28
+ function fetchOpenIssues(repoRoot) {
29
+ try {
30
+ const result = childProcess.execSync('gh issue list --state open --json number,title,labels,createdAt', {
31
+ cwd: repoRoot,
32
+ encoding: 'utf8',
33
+ timeout: 30000,
34
+ });
35
+ return JSON.parse(result);
36
+ } catch (err) {
37
+ if (err.message.includes('gh: command not found')) {
38
+ console.error('Error: gh CLI not found. Install it from https://cli.github.com/');
39
+ return null;
40
+ }
41
+ if (err.message.includes('not logged in')) {
42
+ console.error('Error: Not logged in to GitHub. Run "gh auth login" first.');
43
+ return null;
44
+ }
45
+ console.error('Error fetching issues:', err.message);
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function filterRemainingIssues(issues, sprintReqs) {
51
+ const sprintTitles = new Set(sprintReqs.map(r => r.name?.toLowerCase()));
52
+ return issues.filter(issue => {
53
+ const titleLower = issue.title.toLowerCase();
54
+ return !sprintTitles.has(titleLower);
55
+ });
56
+ }
57
+
58
+ function formatIssuesTable(issues) {
59
+ if (issues.length === 0) return 'No remaining issues found.';
60
+ const lines = ['Remaining Open Issues:', ''];
61
+ lines.push('┌──────┬────────────────────────────────────────────┬─────────────────────┬───────────┐');
62
+ lines.push('│ # │ Title │ Labels │ Created │');
63
+ lines.push('├──────┼────────────────────────────────────────────┼─────────────────────┼───────────┤');
64
+ for (const issue of issues) {
65
+ const num = String(issue.number).padEnd(4);
66
+ const title = issue.title.slice(0, 40).padEnd(40);
67
+ const labels = (issue.labels || []).map(l => l.name).join(', ').slice(0, 19).padEnd(19);
68
+ const created = issue.createdAt ? issue.createdAt.slice(0, 10) : 'N/A';
69
+ lines.push(`│ ${num} │ ${title} │ ${labels} │ ${created} │`);
70
+ }
71
+ lines.push('└──────┴────────────────────────────────────────────┴─────────────────────┴───────────┘');
72
+ lines.push(`Total: ${issues.length} remaining issues`);
73
+ return lines.join('\n');
74
+ }
75
+
76
+ function generateSprintPlan(issues) {
77
+ if (issues.length === 0) return null;
78
+ const lines = ['Suggested Next Sprint Plan:', ''];
79
+ const prioritized = issues.slice(0, 5);
80
+ for (let i = 0; i < prioritized.length; i++) {
81
+ const issue = prioritized[i];
82
+ lines.push(`${i + 1}. #${issue.number} - ${issue.title}`);
83
+ }
84
+ if (issues.length > 5) {
85
+ lines.push(`... and ${issues.length - 5} more issues`);
86
+ }
87
+ lines.push('');
88
+ lines.push('To start a sprint, run:');
89
+ lines.push(` /sprint-flow "Implement #${prioritized[0].number}: ${prioritized[0].title}"`);
90
+ return lines.join('\n');
91
+ }
92
+
93
+ async function handleNextSprint(args = []) {
94
+ const jsonFlag = args.includes('--json');
95
+ const planFlag = args.includes('--plan');
96
+ const dirIdx = args.indexOf('--dir');
97
+ let repoRoot = process.cwd();
98
+ if (dirIdx >= 0 && dirIdx + 1 < args.length) {
99
+ repoRoot = path.resolve(args[dirIdx + 1]);
100
+ }
101
+ const state = readSprintState(repoRoot);
102
+ const sprintReqs = state ? getCurrentSprintReqs(state) : [];
103
+ const issues = fetchOpenIssues(repoRoot);
104
+ if (issues === null) return 1;
105
+ const remaining = filterRemainingIssues(issues, sprintReqs);
106
+ if (jsonFlag) {
107
+ console.log(JSON.stringify({ current_sprint: state?.id || null, sprint_reqs: sprintReqs, remaining_issues: remaining }, null, 2));
108
+ return 0;
109
+ }
110
+ console.log(formatIssuesTable(remaining));
111
+ if (planFlag) {
112
+ const plan = generateSprintPlan(remaining);
113
+ if (plan) {
114
+ console.log('');
115
+ console.log(plan);
116
+ }
117
+ }
118
+ return 0;
119
+ }
120
+
121
+ module.exports = {
122
+ handleNextSprint,
123
+ fetchOpenIssues,
124
+ filterRemainingIssues,
125
+ formatIssuesTable,
126
+ generateSprintPlan,
127
+ readSprintState,
128
+ getCurrentSprintReqs,
129
+ };
@@ -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
- **Generated:** 2026-07-10
4
- **Commit:** a9276f6
5
- **Branch:** main
6
- **Version:** 0.14.6.2
3
+ **Generated:** 2026-07-13
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.