@boyingliu01/xp-gate 0.15.4 → 0.18.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.
- package/bin/xp-gate.js +29 -1
- package/hooks/lib/now-ms.sh +33 -0
- package/hooks/pre-commit +63 -47
- package/hooks/sprint-gate.sh +291 -0
- package/lib/__tests__/check-version.test.js +122 -0
- package/lib/__tests__/doctor.test.js +13 -15
- package/lib/__tests__/gate-audit.test.ts +38 -0
- package/lib/__tests__/phase-transition.test.js +241 -1
- package/lib/__tests__/retro.test.js +131 -0
- package/lib/__tests__/sprint-status-rework.test.js +184 -0
- package/lib/check-version.js +9 -0
- package/lib/doctor-tui.js +79 -9
- package/lib/doctor.js +15 -3
- package/lib/gate-audit.ts +18 -7
- package/lib/gate-runner.js +77 -30
- package/lib/init.js +10 -0
- package/lib/mattpocock-license-verdict.json +7 -0
- package/lib/phase-transition.js +251 -1
- package/lib/retro.js +222 -0
- package/lib/shared-utils.js +7 -0
- package/lib/sprint-init.js +207 -0
- package/lib/sprint-state-migrator.js +1 -0
- package/lib/sprint-status.js +111 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +3 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
- package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
- package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
- package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
- package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
- package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
- package/plugins/opencode/index.ts +1 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
- package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
- package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
- package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
- package/plugins/opencode/skills/grilling/SKILL.md +12 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
- package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
- package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
- package/scripts/__tests__/sync-package-content.test.js +50 -0
- package/scripts/check-mattpocock-license.cjs +302 -0
- package/scripts/migrate-audit-anomalies.mjs +78 -0
- package/scripts/sync-package-content.js +52 -5
- package/skills/batch-grill-me/SKILL.md +15 -0
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/delphi-review/SKILL.md +59 -4
- package/skills/delphi-review/references/requirements.md +390 -0
- package/skills/delphi-review/references/round-templates.md +67 -0
- package/skills/domain-modeling/ADR-FORMAT.md +64 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/skills/domain-modeling/SKILL.md +74 -0
- package/skills/grill-with-docs/SKILL.md +7 -0
- package/skills/grilling/SKILL.md +12 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/sprint-flow/SKILL.md +168 -43
- package/skills/sprint-flow/references/components/memory.md +1 -1
- package/skills/sprint-flow/references/components/middleware.md +1 -1
- package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
- package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
- package/skills/sprint-flow/references/orchestration-rules.md +14 -12
- package/skills/sprint-flow/references/phase-2-design.md +145 -133
- package/skills/sprint-flow/references/phase-3-build.md +69 -2
- package/skills/sprint-flow/references/phase-4-verify.md +93 -31
- package/skills/sprint-flow/references/phase-5-ship.md +99 -14
- package/skills/sprint-flow/references/phase-6-close.md +37 -5
- package/skills/sprint-flow/references/phase-overview.md +58 -53
- package/skills/test-specification-alignment/AGENTS.md +3 -3
- package/skills/test-specification-alignment/SKILL.md +8 -3
- package/skills/to-issues/SKILL.md +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TDD: written RED first (Phase G per design §12)
|
|
3
|
+
* @test REQ-REWORK-001 Sprint rework rate tracking (#369)
|
|
4
|
+
* @intent Validate xp-gate sprint-status --rework-check computes rework rate for closed sprints
|
|
5
|
+
*/
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const { handleSprintStatus } = require('../sprint-status');
|
|
12
|
+
|
|
13
|
+
let TMP_DIR;
|
|
14
|
+
let cwdSpy;
|
|
15
|
+
|
|
16
|
+
function git(cmd, dir) {
|
|
17
|
+
return execSync(`git ${cmd}`, { cwd: dir || TMP_DIR, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeSprintState(state, dir) {
|
|
21
|
+
const sd = path.join(dir || TMP_DIR, '.sprint-state');
|
|
22
|
+
fs.mkdirSync(sd, { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(sd, 'sprint-state.json'), JSON.stringify(state, null, 2));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeHistoryFile(name, state, dir) {
|
|
27
|
+
const hd = path.join(dir || TMP_DIR, '.sprint-state', 'sprint-history');
|
|
28
|
+
fs.mkdirSync(hd, { recursive: true });
|
|
29
|
+
fs.writeFileSync(path.join(hd, name), JSON.stringify(state, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeCommit(msg, dir) {
|
|
33
|
+
const d = dir || TMP_DIR;
|
|
34
|
+
fs.appendFileSync(path.join(d, 'file.txt'), `${msg}-${Date.now()}\n`);
|
|
35
|
+
git('add -A', d);
|
|
36
|
+
git(`commit --no-verify -m "${msg}"`, d);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function daysAgo(n) {
|
|
40
|
+
return new Date(Date.now() - n * 86400000).toISOString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeAll(() => {
|
|
44
|
+
TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'rework-test-'));
|
|
45
|
+
cwdSpy = vi.spyOn(process, 'cwd');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
cwdSpy.mockReturnValue(TMP_DIR);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
cwdSpy.mockRestore();
|
|
54
|
+
fs.rmSync(TMP_DIR, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('sprint-status --rework-check (#369)', () => {
|
|
58
|
+
// TDD: written RED first (G phase per design §12)
|
|
59
|
+
test('(a) no closed sprints → exit 0 + graceful message', async () => {
|
|
60
|
+
writeSprintState({
|
|
61
|
+
_schema_version: 1,
|
|
62
|
+
id: 'sprint-open',
|
|
63
|
+
task_description: 'Open sprint',
|
|
64
|
+
phase: 3,
|
|
65
|
+
status: 'in_progress',
|
|
66
|
+
started_at: daysAgo(1),
|
|
67
|
+
isolation: { worktree_path: null, branch: 'main' },
|
|
68
|
+
phase_history: [],
|
|
69
|
+
metrics: {},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
73
|
+
const code = await handleSprintStatus(['--rework-check', '--window-days', '7', '--dir', TMP_DIR]);
|
|
74
|
+
const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
|
|
75
|
+
logSpy.mockRestore();
|
|
76
|
+
|
|
77
|
+
expect(code).toBe(0);
|
|
78
|
+
expect(output).toContain('No closed sprints within');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// TDD: written RED first (G phase per design §12)
|
|
82
|
+
test('(b) 2 fix / 10 total → rework_rate 0.2, no alert, state file updated', async () => {
|
|
83
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rework-b-'));
|
|
84
|
+
cwdSpy.mockReturnValue(dir);
|
|
85
|
+
|
|
86
|
+
git('init', dir);
|
|
87
|
+
git('config user.email "test@test.com"', dir);
|
|
88
|
+
git('config user.name "Test"', dir);
|
|
89
|
+
makeCommit('feat: initial', dir);
|
|
90
|
+
makeCommit('fix: resolve login bug', dir);
|
|
91
|
+
makeCommit('fix(auth): handle timeout', dir);
|
|
92
|
+
|
|
93
|
+
writeSprintState({
|
|
94
|
+
_schema_version: 1,
|
|
95
|
+
id: 'sprint-b',
|
|
96
|
+
task_description: 'Sprint B',
|
|
97
|
+
phase: 6,
|
|
98
|
+
status: 'completed',
|
|
99
|
+
started_at: daysAgo(9),
|
|
100
|
+
isolation: { worktree_path: null, branch: 'main' },
|
|
101
|
+
phase_history: [],
|
|
102
|
+
metrics: { completed_at: daysAgo(2), total_sprint_commits: 10 },
|
|
103
|
+
}, dir);
|
|
104
|
+
|
|
105
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
106
|
+
const code = await handleSprintStatus(['--rework-check', '--window-days', '7', '--dir', dir]);
|
|
107
|
+
const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
|
|
108
|
+
logSpy.mockRestore();
|
|
109
|
+
|
|
110
|
+
expect(code).toBe(0);
|
|
111
|
+
expect(output).toContain('20.0%');
|
|
112
|
+
expect(output).not.toContain('⚠️');
|
|
113
|
+
|
|
114
|
+
const updated = JSON.parse(
|
|
115
|
+
fs.readFileSync(path.join(dir, '.sprint-state', 'sprint-state.json'), 'utf8')
|
|
116
|
+
);
|
|
117
|
+
expect(updated.metrics.rework_rate).toBeCloseTo(0.2, 1);
|
|
118
|
+
|
|
119
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// TDD: written RED first (G phase per design §12)
|
|
123
|
+
test('(c) 5 fix / 10 total → rework_rate 0.5, alert printed', async () => {
|
|
124
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rework-c-'));
|
|
125
|
+
cwdSpy.mockReturnValue(dir);
|
|
126
|
+
|
|
127
|
+
git('init', dir);
|
|
128
|
+
git('config user.email "test@test.com"', dir);
|
|
129
|
+
git('config user.name "Test"', dir);
|
|
130
|
+
makeCommit('fix: one', dir);
|
|
131
|
+
makeCommit('fix: two', dir);
|
|
132
|
+
makeCommit('bugfix: three', dir);
|
|
133
|
+
makeCommit('hotfix: four', dir);
|
|
134
|
+
makeCommit('patch: five', dir);
|
|
135
|
+
makeCommit('feat: six', dir);
|
|
136
|
+
|
|
137
|
+
writeSprintState({
|
|
138
|
+
_schema_version: 1,
|
|
139
|
+
id: 'sprint-c',
|
|
140
|
+
task_description: 'Sprint C',
|
|
141
|
+
phase: 6,
|
|
142
|
+
status: 'completed',
|
|
143
|
+
started_at: daysAgo(9),
|
|
144
|
+
isolation: { worktree_path: null, branch: 'main' },
|
|
145
|
+
phase_history: [],
|
|
146
|
+
metrics: { completed_at: daysAgo(2), total_sprint_commits: 10 },
|
|
147
|
+
}, dir);
|
|
148
|
+
|
|
149
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
150
|
+
const code = await handleSprintStatus(['--rework-check', '--window-days', '7', '--dir', dir]);
|
|
151
|
+
const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
|
|
152
|
+
logSpy.mockRestore();
|
|
153
|
+
|
|
154
|
+
expect(code).toBe(0);
|
|
155
|
+
expect(output).toContain('50.0%');
|
|
156
|
+
expect(output).toContain('⚠️');
|
|
157
|
+
expect(output).toContain('sprint-c');
|
|
158
|
+
|
|
159
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// TDD: written RED first (G phase per design §12)
|
|
163
|
+
test('(d) --window-days 1 filters out sprints closed >1 day ago', async () => {
|
|
164
|
+
writeHistoryFile('sprint-old.json', {
|
|
165
|
+
_schema_version: 1,
|
|
166
|
+
id: 'sprint-old',
|
|
167
|
+
task_description: 'Old sprint',
|
|
168
|
+
phase: 6,
|
|
169
|
+
status: 'completed',
|
|
170
|
+
started_at: daysAgo(20),
|
|
171
|
+
isolation: { worktree_path: null, branch: 'main' },
|
|
172
|
+
phase_history: [],
|
|
173
|
+
metrics: { completed_at: daysAgo(10), total_sprint_commits: 10 },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
177
|
+
const code = await handleSprintStatus(['--rework-check', '--window-days', '1', '--dir', TMP_DIR]);
|
|
178
|
+
const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
|
|
179
|
+
logSpy.mockRestore();
|
|
180
|
+
|
|
181
|
+
expect(code).toBe(0);
|
|
182
|
+
expect(output).toContain('No closed sprints within 1 day');
|
|
183
|
+
});
|
|
184
|
+
});
|
package/lib/check-version.js
CHANGED
|
@@ -89,6 +89,15 @@ function readCache() {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function writeCache(latest, publishedAt) {
|
|
92
|
+
// Validate version before writing — prevent corrupted/non-semver values
|
|
93
|
+
// from persisting in version-cache.json (e.g. undefined, empty string, placeholders).
|
|
94
|
+
// Uses 3-digit semver: MAJOR.MINOR.PATCH with optional pre-release tag.
|
|
95
|
+
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
96
|
+
if (typeof latest !== 'string' || !semverRegex.test(latest)) {
|
|
97
|
+
console.warn(`[version-cache] Invalid version "${latest}" — not writing to cache`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
const cp = cachePath();
|
|
93
102
|
if (!cp) return;
|
|
94
103
|
ensureDir(path.dirname(cp));
|
package/lib/doctor-tui.js
CHANGED
|
@@ -57,9 +57,44 @@ function diagnoseTuiRegistration(checks) {
|
|
|
57
57
|
return 1;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Extract version from SKILL.md YAML frontmatter (between first --- and second ---).
|
|
62
|
+
* Returns version string or undefined if no version field found.
|
|
63
|
+
* @param {string} content - SKILL.md file content
|
|
64
|
+
* @returns {string|undefined}
|
|
65
|
+
*/
|
|
66
|
+
function extractSkillVersion(content) {
|
|
67
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
68
|
+
if (!match) return undefined;
|
|
69
|
+
const frontmatter = match[1];
|
|
70
|
+
const versionMatch = frontmatter.match(/version:\s*(\S+)/);
|
|
71
|
+
return versionMatch ? versionMatch[1] : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compare two semver strings. Returns:
|
|
76
|
+
* -1 if a < b, 0 if a == b, 1 if a > b, undefined if either is not valid semver.
|
|
77
|
+
* @param {string} a
|
|
78
|
+
* @param {string} b
|
|
79
|
+
* @returns {number|undefined}
|
|
80
|
+
*/
|
|
81
|
+
function compareSemver(a, b) {
|
|
82
|
+
const aParts = a.split('.').map(Number);
|
|
83
|
+
const bParts = b.split('.').map(Number);
|
|
84
|
+
if (aParts.some(n => isNaN(n)) || bParts.some(n => isNaN(n))) return undefined;
|
|
85
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
86
|
+
const na = aParts[i] || 0;
|
|
87
|
+
const nb = bParts[i] || 0;
|
|
88
|
+
if (na < nb) return -1;
|
|
89
|
+
if (na > nb) return 1;
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
/**
|
|
61
95
|
* Check 9: Installed skills vs package-bundled skills (#332).
|
|
62
|
-
*
|
|
96
|
+
* Uses semver version comparison on SKILL.md frontmatter when available;
|
|
97
|
+
* falls back to byte-for-byte content comparison for skills without version field.
|
|
63
98
|
* @param {object} config
|
|
64
99
|
* @param {Array} checks
|
|
65
100
|
* @returns {number} issue count
|
|
@@ -102,15 +137,48 @@ function diagnoseInstalledSkills(config, checks) {
|
|
|
102
137
|
|
|
103
138
|
const bundledContent = fs.readFileSync(bundledSkillMd, 'utf8');
|
|
104
139
|
const userContent = fs.readFileSync(userSkillMd, 'utf8');
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
140
|
+
|
|
141
|
+
// Try semver comparison if both SKILL.md files have version frontmatter
|
|
142
|
+
const bundledVersion = extractSkillVersion(bundledContent);
|
|
143
|
+
const userVersion = extractSkillVersion(userContent);
|
|
144
|
+
|
|
145
|
+
if (bundledVersion !== undefined && userVersion !== undefined) {
|
|
146
|
+
const cmp = compareSemver(userVersion, bundledVersion);
|
|
147
|
+
if (cmp === undefined) {
|
|
148
|
+
// Version strings not valid semver — fall back to byte comparison
|
|
149
|
+
if (bundledContent !== userContent) {
|
|
150
|
+
checks.push({
|
|
151
|
+
name: `Skill: ${name}`,
|
|
152
|
+
status: 'WARN',
|
|
153
|
+
detail: `Outdated — run xp-gate update-skill --all (installed: ${userVersion}, bundled: ${bundledVersion})`,
|
|
154
|
+
});
|
|
155
|
+
issues++;
|
|
156
|
+
} else {
|
|
157
|
+
checks.push({ name: `Skill: ${name}`, status: 'PASS', detail: 'Up to date' });
|
|
158
|
+
}
|
|
159
|
+
} else if (cmp < 0) {
|
|
160
|
+
checks.push({
|
|
161
|
+
name: `Skill: ${name}`,
|
|
162
|
+
status: 'WARN',
|
|
163
|
+
detail: `Outdated — run xp-gate update-skill --all (installed: ${userVersion}, bundled: ${bundledVersion})`,
|
|
164
|
+
});
|
|
165
|
+
issues++;
|
|
166
|
+
} else {
|
|
167
|
+
// cmp >= 0: installed version is same or newer than bundled
|
|
168
|
+
checks.push({ name: `Skill: ${name}`, status: 'PASS', detail: 'Up to date' });
|
|
169
|
+
}
|
|
112
170
|
} else {
|
|
113
|
-
|
|
171
|
+
// No version in at least one file — fall back to byte-for-byte comparison
|
|
172
|
+
if (bundledContent !== userContent) {
|
|
173
|
+
checks.push({
|
|
174
|
+
name: `Skill: ${name}`,
|
|
175
|
+
status: 'WARN',
|
|
176
|
+
detail: 'Outdated — run xp-gate update-skill --all',
|
|
177
|
+
});
|
|
178
|
+
issues++;
|
|
179
|
+
} else {
|
|
180
|
+
checks.push({ name: `Skill: ${name}`, status: 'PASS', detail: 'Up to date' });
|
|
181
|
+
}
|
|
114
182
|
}
|
|
115
183
|
}
|
|
116
184
|
|
|
@@ -195,4 +263,6 @@ module.exports = {
|
|
|
195
263
|
diagnoseInstalledSkills,
|
|
196
264
|
fixTuiRegistration,
|
|
197
265
|
ensureTuiRegistration,
|
|
266
|
+
extractSkillVersion,
|
|
267
|
+
compareSemver,
|
|
198
268
|
};
|
package/lib/doctor.js
CHANGED
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
getTemplateDir,
|
|
15
15
|
} = require('./shared-paths.js');
|
|
16
16
|
const { checkUpgrade, formatUpgradeMsg } = require('./check-version.js');
|
|
17
|
-
const { GATE_CLI_TOOLS, checkCliTool, getToolInstallCmd } = require('./detect-deps.js');
|
|
17
|
+
const { GATE_CLI_TOOLS, checkCliTool, getToolInstallCmd, checkBash } = require('./detect-deps.js');
|
|
18
18
|
const {
|
|
19
19
|
diagnoseTuiRegistration, diagnoseInstalledSkills,
|
|
20
20
|
fixTuiRegistration, ensureTuiRegistration, readTuiJson,
|
|
@@ -156,8 +156,7 @@ function getCurrentHooksPathSync() {
|
|
|
156
156
|
async function checkEnv(checks) {
|
|
157
157
|
const envChecks = [
|
|
158
158
|
{ name: 'Node.js', cmd: 'node --version' },
|
|
159
|
-
{ name: 'Git', cmd: 'git --version' }
|
|
160
|
-
{ name: 'Bash', cmd: 'bash --version' }
|
|
159
|
+
{ name: 'Git', cmd: 'git --version' }
|
|
161
160
|
];
|
|
162
161
|
|
|
163
162
|
const results = await Promise.allSettled(
|
|
@@ -181,6 +180,19 @@ async function checkEnv(checks) {
|
|
|
181
180
|
}
|
|
182
181
|
}
|
|
183
182
|
|
|
183
|
+
// Bash check uses checkBash() for robust cross-platform detection
|
|
184
|
+
// (handles Windows Git Bash at common install paths)
|
|
185
|
+
const bashResult = checkBash();
|
|
186
|
+
if (bashResult.ok) {
|
|
187
|
+
const versionStr = bashResult.version
|
|
188
|
+
? `GNU bash, version ${bashResult.version} (${bashResult.path})`
|
|
189
|
+
: `bash found at ${bashResult.path}`;
|
|
190
|
+
checks.push({ name: 'Environment: Bash', status: 'PASS', detail: versionStr });
|
|
191
|
+
} else {
|
|
192
|
+
checks.push({ name: 'Environment: Bash', status: 'FAIL', detail: 'Not found' });
|
|
193
|
+
allOk = false;
|
|
194
|
+
}
|
|
195
|
+
|
|
184
196
|
return allOk;
|
|
185
197
|
}
|
|
186
198
|
|
package/lib/gate-audit.ts
CHANGED
|
@@ -36,6 +36,7 @@ export interface GateAuditEntry {
|
|
|
36
36
|
trigger: 'commit' | 'push' | 'manual';
|
|
37
37
|
repo_path: string;
|
|
38
38
|
commit_hash: string;
|
|
39
|
+
duration_anomaly?: boolean; // Issue #370: set when duration_ms > max_duration_ms
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
@@ -145,14 +146,19 @@ function parseAuditEntries(logPath: string): GateAuditEntry[] {
|
|
|
145
146
|
/**
|
|
146
147
|
* Aggregate audit entries by gate_id, computing sums for passed/ms/issues.
|
|
147
148
|
*/
|
|
148
|
-
function aggregateByGate(entries: GateAuditEntry[]): Map<string, { total: number; passed: number; ms: number; issues: number }> {
|
|
149
|
-
const buckets = new Map<string, { total: number; passed: number; ms: number; issues: number }>();
|
|
149
|
+
function aggregateByGate(entries: GateAuditEntry[]): Map<string, { total: number; passed: number; ms: number; issues: number; anomaly_count: number }> {
|
|
150
|
+
const buckets = new Map<string, { total: number; passed: number; ms: number; issues: number; anomaly_count: number }>();
|
|
150
151
|
|
|
151
152
|
for (const e of entries) {
|
|
152
|
-
const b = buckets.get(e.gate_id) ?? { total: 0, passed: 0, ms: 0, issues: 0 };
|
|
153
|
+
const b = buckets.get(e.gate_id) ?? { total: 0, passed: 0, ms: 0, issues: 0, anomaly_count: 0 };
|
|
153
154
|
b.total += 1;
|
|
154
155
|
if (e.passed) b.passed += 1;
|
|
155
|
-
|
|
156
|
+
// Issue #370: exclude anomalous durations from ms aggregation
|
|
157
|
+
if (e.duration_anomaly === true) {
|
|
158
|
+
b.anomaly_count += 1;
|
|
159
|
+
} else {
|
|
160
|
+
b.ms += e.duration_ms;
|
|
161
|
+
}
|
|
156
162
|
b.issues += e.issues_found;
|
|
157
163
|
buckets.set(e.gate_id, b);
|
|
158
164
|
}
|
|
@@ -165,7 +171,7 @@ function aggregateByGate(entries: GateAuditEntry[]): Map<string, { total: number
|
|
|
165
171
|
*/
|
|
166
172
|
export function computeStats(
|
|
167
173
|
repoRoot: string = process.cwd(),
|
|
168
|
-
): { gate_id: string; pass_pct: string; avg_ms: number; avg_issues: number }[] {
|
|
174
|
+
): { gate_id: string; pass_pct: string; avg_ms: number; avg_issues: number; anomaly_count: number }[] {
|
|
169
175
|
const logPath = join(repoRoot, AUDIT_DIR, AUDIT_FILE);
|
|
170
176
|
const entries = parseAuditEntries(logPath);
|
|
171
177
|
if (entries.length === 0) {
|
|
@@ -174,14 +180,17 @@ export function computeStats(
|
|
|
174
180
|
|
|
175
181
|
const buckets = aggregateByGate(entries);
|
|
176
182
|
|
|
177
|
-
const results: { gate_id: string; pass_pct: string; avg_ms: number; avg_issues: number }[] = [];
|
|
183
|
+
const results: { gate_id: string; pass_pct: string; avg_ms: number; avg_issues: number; anomaly_count: number }[] = [];
|
|
178
184
|
|
|
179
185
|
for (const [gate_id, b] of Array.from(buckets.entries())) {
|
|
186
|
+
// Issue #370: avg_ms computed only over non-anomalous entries
|
|
187
|
+
const normalCount = b.total - b.anomaly_count;
|
|
180
188
|
results.push({
|
|
181
189
|
gate_id,
|
|
182
190
|
pass_pct: (b.total > 0 ? ((b.passed / b.total) * 100).toFixed(1) + '%' : 'N/A'),
|
|
183
|
-
avg_ms:
|
|
191
|
+
avg_ms: normalCount > 0 ? Math.round(b.ms / normalCount) : 0,
|
|
184
192
|
avg_issues: b.total > 0 ? parseFloat((b.issues / b.total).toFixed(2)) : 0,
|
|
193
|
+
anomaly_count: b.anomaly_count,
|
|
185
194
|
});
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -247,6 +256,8 @@ function runCli(): void {
|
|
|
247
256
|
trigger: (opts['trigger'] as 'commit' | 'push' | 'manual') || 'manual',
|
|
248
257
|
repo_path: process.cwd(),
|
|
249
258
|
commit_hash: getCommitHash(),
|
|
259
|
+
// Issue #370: only set when explicitly flagged (omit field otherwise)
|
|
260
|
+
...(opts['duration-anomaly'] === 'true' ? { duration_anomaly: true } : {}),
|
|
250
261
|
};
|
|
251
262
|
appendAuditEntry(entry);
|
|
252
263
|
} else {
|
package/lib/gate-runner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { execSync, execFileSync } = require('child_process');
|
|
3
|
+
const { execSync, execFileSync, spawnSync } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
@@ -28,6 +28,54 @@ function runBashScript(scriptPath) {
|
|
|
28
28
|
// Gate metadata registry — maps gate IDs to names, descriptions, and how to run them.
|
|
29
29
|
// Standalone gates (<gate-id>: { run }) are invokable via xp-gate gate-<id>.
|
|
30
30
|
// Pre-commit-only gates (<gate-id>: { preCommitOnly: true }) run only in git commit context.
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run a TypeScript gate module via npx tsx.
|
|
34
|
+
* Finds the .ts entry point, spawns tsx with --run flag.
|
|
35
|
+
* Falls back to bash script if TypeScript module not found.
|
|
36
|
+
*/
|
|
37
|
+
function runTsGate(gateModule, targetPath) {
|
|
38
|
+
const entry = findTsGateEntry(gateModule);
|
|
39
|
+
if (!entry) {
|
|
40
|
+
// Fallback to bash script
|
|
41
|
+
const script = resolveGateScript(gateModule.replace('gate-', ''));
|
|
42
|
+
if (script) {
|
|
43
|
+
runBashScript(script);
|
|
44
|
+
} else {
|
|
45
|
+
console.log(`Gate ${gateModule}: TypeScript module not found and no bash fallback available.`);
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const args = ['-y', 'tsx', entry, '--run'];
|
|
51
|
+
if (targetPath) args.push('--cwd', targetPath);
|
|
52
|
+
|
|
53
|
+
const result = spawnSync('npx', args, {
|
|
54
|
+
stdio: 'inherit',
|
|
55
|
+
shell: process.platform === 'win32',
|
|
56
|
+
cwd: targetPath || process.cwd(),
|
|
57
|
+
timeout: 120000,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return result.status === null ? 1 : result.status;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function findTsGateEntry(gateModule) {
|
|
64
|
+
const candidates = [
|
|
65
|
+
// Development layout
|
|
66
|
+
path.resolve(__dirname, '..', '..', '..', 'src', 'gates', `${gateModule}.ts`),
|
|
67
|
+
// npm package bundled layout
|
|
68
|
+
path.resolve(__dirname, '..', 'gates', `${gateModule}.ts`),
|
|
69
|
+
// node_modules install
|
|
70
|
+
path.resolve(__dirname, '..', '..', 'src', 'gates', `${gateModule}.ts`),
|
|
71
|
+
// Repo root
|
|
72
|
+
path.resolve(process.cwd(), 'src', 'gates', `${gateModule}.ts`),
|
|
73
|
+
];
|
|
74
|
+
for (const c of candidates) {
|
|
75
|
+
if (fs.existsSync(c)) return c;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
31
79
|
const GATE_REGISTRY = {
|
|
32
80
|
'0': {
|
|
33
81
|
name: 'Version Consistency',
|
|
@@ -59,14 +107,7 @@ const GATE_REGISTRY = {
|
|
|
59
107
|
description: 'lizard cyclomatic complexity analysis',
|
|
60
108
|
aliases: ['complexity', 'ccn', '3'],
|
|
61
109
|
run: (targetPath) => {
|
|
62
|
-
|
|
63
|
-
if (script) {
|
|
64
|
-
runBashScript(script);
|
|
65
|
-
} else {
|
|
66
|
-
const target = targetPath || '.';
|
|
67
|
-
console.log(`Running lizard on ${target}...`);
|
|
68
|
-
execFileSync('lizard', [target, '--CCN', '10', '--length', '50', '--arguments', '5', '--warnings_only'], { stdio: 'inherit', shell: true });
|
|
69
|
-
}
|
|
110
|
+
return runTsGate('gate-3', targetPath);
|
|
70
111
|
},
|
|
71
112
|
},
|
|
72
113
|
'4': {
|
|
@@ -99,13 +140,7 @@ const GATE_REGISTRY = {
|
|
|
99
140
|
description: 'Infrastructure-as-Code security scanning (checkov, hadolint, kube-score, tflint)',
|
|
100
141
|
aliases: ['iac', 'infra', '7'],
|
|
101
142
|
run: (targetPath) => {
|
|
102
|
-
|
|
103
|
-
if (script) {
|
|
104
|
-
runBashScript(script);
|
|
105
|
-
} else {
|
|
106
|
-
console.log('IaC security scan requires git-staged context for changed files detection.');
|
|
107
|
-
console.log('Run: git commit to trigger Gate 7 automatically.');
|
|
108
|
-
}
|
|
143
|
+
return runTsGate('gate-7', targetPath);
|
|
109
144
|
},
|
|
110
145
|
},
|
|
111
146
|
'8': {
|
|
@@ -113,13 +148,7 @@ const GATE_REGISTRY = {
|
|
|
113
148
|
description: 'gitleaks secret and credential detection',
|
|
114
149
|
aliases: ['secrets', 'secret', '8'],
|
|
115
150
|
run: (targetPath) => {
|
|
116
|
-
|
|
117
|
-
if (script) {
|
|
118
|
-
runBashScript(script);
|
|
119
|
-
} else {
|
|
120
|
-
console.log('Secret scanning requires git-staged context for changed files detection.');
|
|
121
|
-
console.log('Run: git commit to trigger Gate 8 automatically.');
|
|
122
|
-
}
|
|
151
|
+
return runTsGate('gate-8', targetPath);
|
|
123
152
|
},
|
|
124
153
|
},
|
|
125
154
|
'9': {
|
|
@@ -127,13 +156,7 @@ const GATE_REGISTRY = {
|
|
|
127
156
|
description: 'semgrep static application security testing',
|
|
128
157
|
aliases: ['sast', 'semgrep', '9'],
|
|
129
158
|
run: (targetPath) => {
|
|
130
|
-
|
|
131
|
-
if (script) {
|
|
132
|
-
runBashScript(script);
|
|
133
|
-
} else {
|
|
134
|
-
console.log('SAST scanning requires git-staged context for changed files detection.');
|
|
135
|
-
console.log('Run: git commit to trigger Gate 9 automatically.');
|
|
136
|
-
}
|
|
159
|
+
return runTsGate('gate-9', targetPath);
|
|
137
160
|
},
|
|
138
161
|
},
|
|
139
162
|
'10': {
|
|
@@ -150,6 +173,30 @@ const GATE_REGISTRY = {
|
|
|
150
173
|
preCommitOnly: true,
|
|
151
174
|
reason: 'Requires sprint state (.sprint-state/) and git branch context',
|
|
152
175
|
},
|
|
176
|
+
'python-health': {
|
|
177
|
+
name: 'Python Environment Health',
|
|
178
|
+
description: 'Comprehensive Python environment diagnostics',
|
|
179
|
+
aliases: ['python-health', 'py-health'],
|
|
180
|
+
run: (targetPath) => {
|
|
181
|
+
return runTsGate('python-health', targetPath);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
'test-layers': {
|
|
185
|
+
name: 'Test Layer Analytics',
|
|
186
|
+
description: 'Report test distribution across unit/integration/e2e layers',
|
|
187
|
+
aliases: ['test-layers', 'layers', 'test-analytics'],
|
|
188
|
+
run: (targetPath) => {
|
|
189
|
+
return runTsGate('test-layers', targetPath);
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
'pbt': {
|
|
193
|
+
name: 'Property-Based Testing Detection',
|
|
194
|
+
description: 'Detect PBT framework usage and report coverage',
|
|
195
|
+
aliases: ['pbt', 'pbt-detect', 'property-based-testing'],
|
|
196
|
+
run: (targetPath) => {
|
|
197
|
+
return runTsGate('pbt-detect', targetPath);
|
|
198
|
+
},
|
|
199
|
+
},
|
|
153
200
|
};
|
|
154
201
|
|
|
155
202
|
// Reverse-lookup: alias → gate ID (built once at module load)
|
package/lib/init.js
CHANGED
|
@@ -318,6 +318,7 @@ async function init(args) {
|
|
|
318
318
|
console.log('====================\n');
|
|
319
319
|
|
|
320
320
|
// Check bash availability (required for shell hooks)
|
|
321
|
+
console.log('Checking bash availability...');
|
|
321
322
|
const bashCheck = checkBash();
|
|
322
323
|
if (bashCheck.ok) {
|
|
323
324
|
console.log(`Bash: ✓ ${bashCheck.path} (v${bashCheck.version})\n`);
|
|
@@ -327,8 +328,10 @@ async function init(args) {
|
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
// Check CLI tools availability (quality gates will SKIP silently if tools are missing)
|
|
331
|
+
console.log('Checking CLI tools...');
|
|
330
332
|
printCliToolStatus();
|
|
331
333
|
|
|
334
|
+
console.log('Checking platform dependencies...');
|
|
332
335
|
// Detect platform and check/auto-install dependencies
|
|
333
336
|
const platform = detectPlatform();
|
|
334
337
|
console.log(`Platform: ${platform}\n`);
|
|
@@ -534,18 +537,22 @@ async function setupGlobal(args) {
|
|
|
534
537
|
console.log(`Global hooks: ${GLOBAL_HOOKS_DIR}`);
|
|
535
538
|
console.log(`Global adapters: ${GLOBAL_ADAPTERS_DIR}\n`);
|
|
536
539
|
|
|
540
|
+
console.log('[setup-global] Creating global directories...');
|
|
537
541
|
fs.mkdirSync(GLOBAL_HOOKS_DIR, { recursive: true });
|
|
538
542
|
fs.mkdirSync(GLOBAL_ADAPTERS_DIR, { recursive: true });
|
|
539
543
|
|
|
544
|
+
console.log('[setup-global] Installing git hooks...');
|
|
540
545
|
copyHooks(srcDir, GLOBAL_HOOKS_DIR);
|
|
541
546
|
console.log('Installing hooks...');
|
|
542
547
|
console.log(` pre-commit -> ${GLOBAL_HOOKS_DIR}`);
|
|
543
548
|
console.log(` pre-push -> ${GLOBAL_HOOKS_DIR}`);
|
|
544
549
|
|
|
550
|
+
console.log('[setup-global] Installing adapters...');
|
|
545
551
|
copyAdapters(srcDir, GLOBAL_ADAPTERS_DIR);
|
|
546
552
|
console.log(` adapter-common.sh + adapters -> ${GLOBAL_ADAPTERS_DIR}`);
|
|
547
553
|
|
|
548
554
|
// Install principles/, mutation/, mock-policy/ to global modules dir
|
|
555
|
+
console.log('[setup-global] Installing quality gate modules...');
|
|
549
556
|
const globalModulesDir = path.join(CONFIG_DIR, 'modules');
|
|
550
557
|
['principles', 'mutation', 'mock-policy'].forEach(module => {
|
|
551
558
|
const moduleSrc = path.join(srcDir, module);
|
|
@@ -557,6 +564,7 @@ async function setupGlobal(args) {
|
|
|
557
564
|
}
|
|
558
565
|
});
|
|
559
566
|
|
|
567
|
+
console.log('[setup-global] Configuring git...');
|
|
560
568
|
const { execSync } = require('child_process');
|
|
561
569
|
try {
|
|
562
570
|
execSync(`git config --global core.hooksPath "${GLOBAL_HOOKS_DIR}"`);
|
|
@@ -567,6 +575,7 @@ async function setupGlobal(args) {
|
|
|
567
575
|
|
|
568
576
|
ensureConfigDir();
|
|
569
577
|
|
|
578
|
+
console.log('[setup-global] Generating installation manifest...');
|
|
570
579
|
const manifest = generateGlobalManifest(srcDir);
|
|
571
580
|
updateConfig({ lastInit: new Date().toISOString(), mode: 'global', templateDir: TEMPLATE_DIR, manifest });
|
|
572
581
|
|
|
@@ -581,6 +590,7 @@ async function setupGlobal(args) {
|
|
|
581
590
|
console.log('Per-project adapters can still override by creating <repo>/githooks/');
|
|
582
591
|
|
|
583
592
|
// Auto-install missing CLI tools (prompt user)
|
|
593
|
+
console.log('[setup-global] Checking quality gate CLI tools...');
|
|
584
594
|
const autoYes = args.includes('--yes') || args.includes('--auto-install');
|
|
585
595
|
await promptBootstrap(autoYes);
|
|
586
596
|
|