@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.
Files changed (117) hide show
  1. package/bin/xp-gate.js +29 -1
  2. package/hooks/lib/now-ms.sh +33 -0
  3. package/hooks/pre-commit +63 -47
  4. package/hooks/sprint-gate.sh +291 -0
  5. package/lib/__tests__/check-version.test.js +122 -0
  6. package/lib/__tests__/doctor.test.js +13 -15
  7. package/lib/__tests__/gate-audit.test.ts +38 -0
  8. package/lib/__tests__/phase-transition.test.js +241 -1
  9. package/lib/__tests__/retro.test.js +131 -0
  10. package/lib/__tests__/sprint-status-rework.test.js +184 -0
  11. package/lib/check-version.js +9 -0
  12. package/lib/doctor-tui.js +79 -9
  13. package/lib/doctor.js +15 -3
  14. package/lib/gate-audit.ts +18 -7
  15. package/lib/gate-runner.js +77 -30
  16. package/lib/init.js +10 -0
  17. package/lib/mattpocock-license-verdict.json +7 -0
  18. package/lib/phase-transition.js +251 -1
  19. package/lib/retro.js +222 -0
  20. package/lib/shared-utils.js +7 -0
  21. package/lib/sprint-init.js +207 -0
  22. package/lib/sprint-state-migrator.js +1 -0
  23. package/lib/sprint-status.js +111 -0
  24. package/mock-policy/AGENTS.md +3 -3
  25. package/mutation/AGENTS.md +3 -3
  26. package/package.json +3 -2
  27. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  28. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
  29. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  30. package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
  31. package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
  32. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
  33. package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
  34. package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  35. package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
  36. package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
  37. package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
  38. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  39. package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
  40. package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
  41. package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
  42. package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  43. package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  44. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
  45. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
  46. package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
  47. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
  48. package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
  49. package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
  50. package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
  51. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  52. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
  53. package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
  54. package/plugins/opencode/index.ts +1 -0
  55. package/plugins/opencode/package.json +1 -1
  56. package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
  57. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  58. package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
  59. package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
  60. package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
  61. package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
  62. package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  63. package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
  64. package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
  65. package/plugins/opencode/skills/grilling/SKILL.md +12 -0
  66. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  67. package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
  68. package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
  69. package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
  70. package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  71. package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  72. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
  73. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
  74. package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
  75. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
  76. package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
  77. package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
  78. package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
  79. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  80. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
  81. package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
  82. package/plugins/qoder/plugin.json +1 -1
  83. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  84. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  85. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  86. package/principles/AGENTS.md +3 -3
  87. package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
  88. package/scripts/__tests__/sync-package-content.test.js +50 -0
  89. package/scripts/check-mattpocock-license.cjs +302 -0
  90. package/scripts/migrate-audit-anomalies.mjs +78 -0
  91. package/scripts/sync-package-content.js +52 -5
  92. package/skills/batch-grill-me/SKILL.md +15 -0
  93. package/skills/delphi-review/AGENTS.md +3 -3
  94. package/skills/delphi-review/SKILL.md +59 -4
  95. package/skills/delphi-review/references/requirements.md +390 -0
  96. package/skills/delphi-review/references/round-templates.md +67 -0
  97. package/skills/domain-modeling/ADR-FORMAT.md +64 -0
  98. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  99. package/skills/domain-modeling/SKILL.md +74 -0
  100. package/skills/grill-with-docs/SKILL.md +7 -0
  101. package/skills/grilling/SKILL.md +12 -0
  102. package/skills/sprint-flow/AGENTS.md +3 -3
  103. package/skills/sprint-flow/SKILL.md +168 -43
  104. package/skills/sprint-flow/references/components/memory.md +1 -1
  105. package/skills/sprint-flow/references/components/middleware.md +1 -1
  106. package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  107. package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  108. package/skills/sprint-flow/references/orchestration-rules.md +14 -12
  109. package/skills/sprint-flow/references/phase-2-design.md +145 -133
  110. package/skills/sprint-flow/references/phase-3-build.md +69 -2
  111. package/skills/sprint-flow/references/phase-4-verify.md +93 -31
  112. package/skills/sprint-flow/references/phase-5-ship.md +99 -14
  113. package/skills/sprint-flow/references/phase-6-close.md +37 -5
  114. package/skills/sprint-flow/references/phase-overview.md +58 -53
  115. package/skills/test-specification-alignment/AGENTS.md +3 -3
  116. package/skills/test-specification-alignment/SKILL.md +8 -3
  117. 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
+ });
@@ -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
- * Reports WARN for each installed skill whose SKILL.md differs from the bundled version.
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
- if (bundledContent !== userContent) {
106
- checks.push({
107
- name: `Skill: ${name}`,
108
- status: 'WARN',
109
- detail: 'Outdated — run xp-gate update-skill --all',
110
- });
111
- issues++;
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
- checks.push({ name: `Skill: ${name}`, status: 'PASS', detail: 'Up to date' });
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
- b.ms += e.duration_ms;
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: b.total > 0 ? Math.round(b.ms / b.total) : 0,
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 {
@@ -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
- const script = resolveGateScript('3');
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
- const script = resolveGateScript('7');
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
- const script = resolveGateScript('8');
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
- const script = resolveGateScript('9');
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
 
@@ -0,0 +1,7 @@
1
+ {
2
+ "verdict": "PASS",
3
+ "license_type": "MIT",
4
+ "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
+ "checked_at": "2026-07-27T08:49:43.679Z",
6
+ "source": "network"
7
+ }