@boyingliu01/xp-gate 0.15.5 → 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 (165) hide show
  1. package/adapter-common.sh +0 -0
  2. package/adapters/cpp.sh +0 -0
  3. package/adapters/dart.sh +0 -0
  4. package/adapters/flutter.sh +0 -0
  5. package/adapters/go.sh +0 -0
  6. package/adapters/iac.sh +0 -0
  7. package/adapters/java.sh +0 -0
  8. package/adapters/kotlin.sh +0 -0
  9. package/adapters/objectivec.sh +0 -0
  10. package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
  11. package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
  12. package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
  13. package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
  14. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
  15. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
  16. package/adapters/powershell.sh +0 -0
  17. package/adapters/python.sh +0 -0
  18. package/adapters/shell.sh +0 -0
  19. package/adapters/swift.sh +0 -0
  20. package/adapters/typescript.sh +0 -0
  21. package/bin/xp-gate.js +29 -1
  22. package/gate-3.sh +0 -0
  23. package/gate-4.sh +0 -0
  24. package/gate-7.sh +0 -0
  25. package/gate-8.sh +0 -0
  26. package/gate-9.sh +0 -0
  27. package/hooks/adapter-common.sh +0 -0
  28. package/hooks/gate-3.sh +0 -0
  29. package/hooks/gate-4.sh +0 -0
  30. package/hooks/gate-7.sh +0 -0
  31. package/hooks/gate-8.sh +0 -0
  32. package/hooks/gate-9.sh +0 -0
  33. package/hooks/lib/now-ms.sh +33 -0
  34. package/hooks/pre-commit +63 -47
  35. package/hooks/pre-push +0 -0
  36. package/hooks/sprint-gate.sh +291 -0
  37. package/lib/__tests__/check-version.test.js +122 -0
  38. package/lib/__tests__/doctor.test.js +13 -15
  39. package/lib/__tests__/gate-audit.test.ts +38 -0
  40. package/lib/__tests__/phase-transition.test.js +512 -272
  41. package/lib/__tests__/retro.test.js +131 -0
  42. package/lib/__tests__/sprint-audit.test.js +207 -207
  43. package/lib/__tests__/sprint-status-rework.test.js +184 -0
  44. package/lib/check-version.js +9 -0
  45. package/lib/doctor-tui.js +268 -198
  46. package/lib/doctor.js +15 -3
  47. package/lib/gate-audit.ts +18 -7
  48. package/lib/gate-runner.js +77 -30
  49. package/lib/init.js +10 -0
  50. package/lib/language-tools.js +833 -833
  51. package/lib/mattpocock-license-verdict.json +7 -0
  52. package/lib/phase-transition.js +499 -249
  53. package/lib/retro.js +222 -0
  54. package/lib/shared-utils.js +7 -0
  55. package/lib/sprint-audit.js +235 -235
  56. package/lib/sprint-init.js +207 -0
  57. package/lib/sprint-state-migrator.js +1 -0
  58. package/lib/sprint-status.js +111 -0
  59. package/mock-policy/AGENTS.md +3 -3
  60. package/mutation/AGENTS.md +3 -3
  61. package/package.json +3 -2
  62. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  63. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  64. package/plugins/claude-code/bin/xp-gate-check +0 -0
  65. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
  66. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  67. package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
  68. package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
  69. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
  70. package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
  71. package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  72. package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
  73. package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
  74. package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
  75. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  76. package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
  77. package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
  78. package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
  79. package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  80. package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  81. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
  82. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
  83. package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
  84. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
  85. package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
  86. package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
  87. package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
  88. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  89. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
  90. package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
  91. package/plugins/opencode/index.ts +1 -0
  92. package/plugins/opencode/package.json +1 -1
  93. package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
  94. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  95. package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
  96. package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
  97. package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
  98. package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
  99. package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  100. package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
  101. package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
  102. package/plugins/opencode/skills/grilling/SKILL.md +12 -0
  103. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  104. package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
  105. package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
  106. package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
  107. package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  108. package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  109. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
  111. package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
  112. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
  113. package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
  114. package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
  115. package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
  116. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  117. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
  118. package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
  119. package/plugins/qoder/agents/delphi-architecture.md +48 -48
  120. package/plugins/qoder/agents/delphi-feasibility.md +48 -48
  121. package/plugins/qoder/agents/delphi-technical.md +48 -48
  122. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  123. package/plugins/qoder/bin/xp-gate-check +0 -0
  124. package/plugins/qoder/plugin.json +1 -1
  125. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  126. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  127. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  128. package/principles/AGENTS.md +3 -3
  129. package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
  130. package/scripts/__tests__/sync-package-content.test.js +50 -0
  131. package/scripts/check-mattpocock-license.cjs +302 -0
  132. package/scripts/delphi-external-review.cjs +432 -432
  133. package/scripts/migrate-audit-anomalies.mjs +78 -0
  134. package/scripts/sync-package-content.js +52 -5
  135. package/skills/batch-grill-me/SKILL.md +15 -0
  136. package/skills/delphi-review/AGENTS.md +3 -3
  137. package/skills/delphi-review/SKILL.md +59 -4
  138. package/skills/delphi-review/references/requirements.md +390 -0
  139. package/skills/delphi-review/references/round-templates.md +67 -0
  140. package/skills/domain-modeling/ADR-FORMAT.md +64 -0
  141. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  142. package/skills/domain-modeling/SKILL.md +74 -0
  143. package/skills/grill-with-docs/SKILL.md +7 -0
  144. package/skills/grilling/SKILL.md +12 -0
  145. package/skills/sprint-flow/AGENTS.md +3 -3
  146. package/skills/sprint-flow/SKILL.md +168 -43
  147. package/skills/sprint-flow/references/components/memory.md +1 -1
  148. package/skills/sprint-flow/references/components/middleware.md +1 -1
  149. package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  150. package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  151. package/skills/sprint-flow/references/orchestration-rules.md +14 -12
  152. package/skills/sprint-flow/references/phase-2-design.md +145 -133
  153. package/skills/sprint-flow/references/phase-3-build.md +69 -2
  154. package/skills/sprint-flow/references/phase-4-verify.md +93 -31
  155. package/skills/sprint-flow/references/phase-5-ship.md +99 -14
  156. package/skills/sprint-flow/references/phase-6-close.md +37 -5
  157. package/skills/sprint-flow/references/phase-overview.md +58 -53
  158. package/skills/test-specification-alignment/AGENTS.md +3 -3
  159. package/skills/test-specification-alignment/SKILL.md +8 -3
  160. package/skills/to-issues/SKILL.md +1 -1
  161. package/adapters/gate-3.sh +0 -68
  162. package/adapters/gate-4.sh +0 -80
  163. package/adapters/gate-7.sh +0 -45
  164. package/adapters/gate-8.sh +0 -56
  165. package/adapters/gate-9.sh +0 -104
package/lib/doctor-tui.js CHANGED
@@ -1,198 +1,268 @@
1
- /**
2
- * TUI registration and installed-skills diagnostics for doctor.
3
- * Extracted from doctor.js to keep file size under the large-file threshold.
4
- */
5
- const fs = require('fs');
6
- const path = require('path');
7
- const { HOME_DIR, detectPlatform } = require('./shared-paths.js');
8
-
9
- // npm package source dir (template hooks/adapters)
10
- const PKG_DIR = path.dirname(__dirname);
11
-
12
- /**
13
- * TUI registration path and expected plugin entry.
14
- */
15
- const TUI_JSON_PATH = path.join(HOME_DIR, '.config', 'opencode', 'tui.json');
16
- const TUI_PLUGIN_ENTRY = '@boyingliu01/opencode-plugin/tui';
17
-
18
- /**
19
- * Read and parse tui.json, returning { data, error }.
20
- * data = parsed object on success, null on file missing, undefined on corrupt.
21
- */
22
- function readTuiJson() {
23
- if (!fs.existsSync(TUI_JSON_PATH)) return { data: null, error: null };
24
- try {
25
- const raw = fs.readFileSync(TUI_JSON_PATH, 'utf8');
26
- const data = JSON.parse(raw);
27
- return { data, error: null };
28
- } catch (e) {
29
- return { data: undefined, error: `Corrupt JSON: ${e.message}` };
30
- }
31
- }
32
-
33
- /**
34
- * Check 9: TUI auto-registration in ~/.config/opencode/tui.json.
35
- * @returns {number} issue count
36
- */
37
- function diagnoseTuiRegistration(checks) {
38
- const { data, error } = readTuiJson();
39
-
40
- if (error) {
41
- checks.push({ name: 'TUI registration', status: 'FAIL', detail: error });
42
- return 1;
43
- }
44
-
45
- if (data === null) {
46
- checks.push({ name: 'TUI registration', status: 'FAIL', detail: 'Not registered' });
47
- return 1;
48
- }
49
-
50
- const plugins = Array.isArray(data.plugin) ? data.plugin : [];
51
- if (plugins.includes(TUI_PLUGIN_ENTRY)) {
52
- checks.push({ name: 'TUI registration', status: 'PASS', detail: `${TUI_PLUGIN_ENTRY} registered` });
53
- return 0;
54
- }
55
-
56
- checks.push({ name: 'TUI registration', status: 'FAIL', detail: 'Not registered' });
57
- return 1;
58
- }
59
-
60
- /**
61
- * Check 9: Installed skills vs package-bundled skills (#332).
62
- * Reports WARN for each installed skill whose SKILL.md differs from the bundled version.
63
- * @param {object} config
64
- * @param {Array} checks
65
- * @returns {number} issue count
66
- */
67
- function diagnoseInstalledSkills(config, checks) {
68
- const installedSkills = config.installedSkills || {};
69
- const skillNames = Object.keys(installedSkills);
70
- if (skillNames.length === 0) {
71
- checks.push({ name: 'Installed skills', status: 'SKIP', detail: 'No skills installed' });
72
- return 0;
73
- }
74
-
75
- const bundledSkillsDir = path.join(PKG_DIR, 'skills');
76
- if (!fs.existsSync(bundledSkillsDir)) {
77
- checks.push({ name: 'Installed skills', status: 'SKIP', detail: 'Package skills dir not found' });
78
- return 0;
79
- }
80
-
81
- const platform = detectPlatform();
82
- let userSkillsDir;
83
- if (platform === 'qoder') {
84
- userSkillsDir = path.join(HOME_DIR, '.qoder', 'skills');
85
- } else if (platform === 'claude-code') {
86
- userSkillsDir = path.join(HOME_DIR, '.claude', 'skills');
87
- } else {
88
- userSkillsDir = path.join(HOME_DIR, '.config', 'opencode', 'skills');
89
- }
90
-
91
- let issues = 0;
92
- for (const name of skillNames) {
93
- const bundledSkillMd = path.join(bundledSkillsDir, name, 'SKILL.md');
94
- const userSkillMd = path.join(userSkillsDir, name, 'SKILL.md');
95
-
96
- if (!fs.existsSync(bundledSkillMd)) continue;
97
- if (!fs.existsSync(userSkillMd)) {
98
- checks.push({ name: `Skill: ${name}`, status: 'FAIL', detail: 'Installed SKILL.md missing' });
99
- issues++;
100
- continue;
101
- }
102
-
103
- const bundledContent = fs.readFileSync(bundledSkillMd, 'utf8');
104
- 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++;
112
- } else {
113
- checks.push({ name: `Skill: ${name}`, status: 'PASS', detail: 'Up to date' });
114
- }
115
- }
116
-
117
- return issues;
118
- }
119
-
120
- /**
121
- * Fix TUI registration: ensure @boyingliu01/opencode-plugin/tui is in tui.json.
122
- * Uses atomic write (tmp + renameSync) for JSON safety.
123
- * On corrupt JSON: backup to .corrupt-{timestamp}.bak then rebuild.
124
- * Idempotent: skips if already registered.
125
- * @returns {boolean} Whether a fix was applied
126
- */
127
- function fixTuiRegistration() {
128
- const { data, error } = readTuiJson();
129
-
130
- // Corrupt JSON: backup old file, rebuild from scratch
131
- if (error && data === undefined) {
132
- const ts = Date.now();
133
- const backupPath = `${TUI_JSON_PATH}.corrupt-${ts}.bak`;
134
- try {
135
- fs.copyFileSync(TUI_JSON_PATH, backupPath);
136
- console.log(` ⚠ TUI config corrupted — backed up to ${backupPath}`);
137
- } catch { /* non-critical */ }
138
- // Rebuild fresh
139
- const dir = path.dirname(TUI_JSON_PATH);
140
- fs.mkdirSync(dir, { recursive: true });
141
- const newConfig = { plugin: [TUI_PLUGIN_ENTRY] };
142
- const tmpPath = `${TUI_JSON_PATH}.tmp`;
143
- fs.writeFileSync(tmpPath, JSON.stringify(newConfig, null, 2));
144
- fs.renameSync(tmpPath, TUI_JSON_PATH);
145
- console.log(` ✓ Registered ${TUI_PLUGIN_ENTRY} in TUI (rebuilt after corrupt backup)`);
146
- return true;
147
- }
148
-
149
- // File doesn't exist: create it
150
- if (data === null) {
151
- const dir = path.dirname(TUI_JSON_PATH);
152
- fs.mkdirSync(dir, { recursive: true });
153
- const newConfig = { plugin: [TUI_PLUGIN_ENTRY] };
154
- const tmpPath = `${TUI_JSON_PATH}.tmp`;
155
- fs.writeFileSync(tmpPath, JSON.stringify(newConfig, null, 2));
156
- fs.renameSync(tmpPath, TUI_JSON_PATH);
157
- console.log(` ✓ Created TUI config with ${TUI_PLUGIN_ENTRY}`);
158
- return true;
159
- }
160
-
161
- // File exists, check if plugin already registered (idempotent)
162
- const plugins = Array.isArray(data.plugin) ? data.plugin : [];
163
- if (plugins.includes(TUI_PLUGIN_ENTRY)) return false;
164
-
165
- // Append plugin entry
166
- data.plugin = plugins.concat([TUI_PLUGIN_ENTRY]);
167
- const tmpPath = `${TUI_JSON_PATH}.tmp`;
168
- fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
169
- fs.renameSync(tmpPath, TUI_JSON_PATH);
170
- console.log(` ✓ Added ${TUI_PLUGIN_ENTRY} to TUI config`);
171
- return true;
172
- }
173
-
174
- /**
175
- * Wrapper for init.js: ensure TUI is registered without console output.
176
- * Returns true if the plugin is already registered or was just added.
177
- */
178
- function ensureTuiRegistration() {
179
- const { data } = readTuiJson();
180
- if (data === null || data === undefined) {
181
- fixTuiRegistration();
182
- return;
183
- }
184
- const plugins = Array.isArray(data.plugin) ? data.plugin : [];
185
- if (!plugins.includes(TUI_PLUGIN_ENTRY)) {
186
- fixTuiRegistration();
187
- }
188
- }
189
-
190
- module.exports = {
191
- TUI_JSON_PATH,
192
- TUI_PLUGIN_ENTRY,
193
- readTuiJson,
194
- diagnoseTuiRegistration,
195
- diagnoseInstalledSkills,
196
- fixTuiRegistration,
197
- ensureTuiRegistration,
198
- };
1
+ /**
2
+ * TUI registration and installed-skills diagnostics for doctor.
3
+ * Extracted from doctor.js to keep file size under the large-file threshold.
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { HOME_DIR, detectPlatform } = require('./shared-paths.js');
8
+
9
+ // npm package source dir (template hooks/adapters)
10
+ const PKG_DIR = path.dirname(__dirname);
11
+
12
+ /**
13
+ * TUI registration path and expected plugin entry.
14
+ */
15
+ const TUI_JSON_PATH = path.join(HOME_DIR, '.config', 'opencode', 'tui.json');
16
+ const TUI_PLUGIN_ENTRY = '@boyingliu01/opencode-plugin/tui';
17
+
18
+ /**
19
+ * Read and parse tui.json, returning { data, error }.
20
+ * data = parsed object on success, null on file missing, undefined on corrupt.
21
+ */
22
+ function readTuiJson() {
23
+ if (!fs.existsSync(TUI_JSON_PATH)) return { data: null, error: null };
24
+ try {
25
+ const raw = fs.readFileSync(TUI_JSON_PATH, 'utf8');
26
+ const data = JSON.parse(raw);
27
+ return { data, error: null };
28
+ } catch (e) {
29
+ return { data: undefined, error: `Corrupt JSON: ${e.message}` };
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Check 9: TUI auto-registration in ~/.config/opencode/tui.json.
35
+ * @returns {number} issue count
36
+ */
37
+ function diagnoseTuiRegistration(checks) {
38
+ const { data, error } = readTuiJson();
39
+
40
+ if (error) {
41
+ checks.push({ name: 'TUI registration', status: 'FAIL', detail: error });
42
+ return 1;
43
+ }
44
+
45
+ if (data === null) {
46
+ checks.push({ name: 'TUI registration', status: 'FAIL', detail: 'Not registered' });
47
+ return 1;
48
+ }
49
+
50
+ const plugins = Array.isArray(data.plugin) ? data.plugin : [];
51
+ if (plugins.includes(TUI_PLUGIN_ENTRY)) {
52
+ checks.push({ name: 'TUI registration', status: 'PASS', detail: `${TUI_PLUGIN_ENTRY} registered` });
53
+ return 0;
54
+ }
55
+
56
+ checks.push({ name: 'TUI registration', status: 'FAIL', detail: 'Not registered' });
57
+ return 1;
58
+ }
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
+
94
+ /**
95
+ * Check 9: Installed skills vs package-bundled skills (#332).
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.
98
+ * @param {object} config
99
+ * @param {Array} checks
100
+ * @returns {number} issue count
101
+ */
102
+ function diagnoseInstalledSkills(config, checks) {
103
+ const installedSkills = config.installedSkills || {};
104
+ const skillNames = Object.keys(installedSkills);
105
+ if (skillNames.length === 0) {
106
+ checks.push({ name: 'Installed skills', status: 'SKIP', detail: 'No skills installed' });
107
+ return 0;
108
+ }
109
+
110
+ const bundledSkillsDir = path.join(PKG_DIR, 'skills');
111
+ if (!fs.existsSync(bundledSkillsDir)) {
112
+ checks.push({ name: 'Installed skills', status: 'SKIP', detail: 'Package skills dir not found' });
113
+ return 0;
114
+ }
115
+
116
+ const platform = detectPlatform();
117
+ let userSkillsDir;
118
+ if (platform === 'qoder') {
119
+ userSkillsDir = path.join(HOME_DIR, '.qoder', 'skills');
120
+ } else if (platform === 'claude-code') {
121
+ userSkillsDir = path.join(HOME_DIR, '.claude', 'skills');
122
+ } else {
123
+ userSkillsDir = path.join(HOME_DIR, '.config', 'opencode', 'skills');
124
+ }
125
+
126
+ let issues = 0;
127
+ for (const name of skillNames) {
128
+ const bundledSkillMd = path.join(bundledSkillsDir, name, 'SKILL.md');
129
+ const userSkillMd = path.join(userSkillsDir, name, 'SKILL.md');
130
+
131
+ if (!fs.existsSync(bundledSkillMd)) continue;
132
+ if (!fs.existsSync(userSkillMd)) {
133
+ checks.push({ name: `Skill: ${name}`, status: 'FAIL', detail: 'Installed SKILL.md missing' });
134
+ issues++;
135
+ continue;
136
+ }
137
+
138
+ const bundledContent = fs.readFileSync(bundledSkillMd, 'utf8');
139
+ const userContent = fs.readFileSync(userSkillMd, 'utf8');
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
+ }
170
+ } else {
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
+ }
182
+ }
183
+ }
184
+
185
+ return issues;
186
+ }
187
+
188
+ /**
189
+ * Fix TUI registration: ensure @boyingliu01/opencode-plugin/tui is in tui.json.
190
+ * Uses atomic write (tmp + renameSync) for JSON safety.
191
+ * On corrupt JSON: backup to .corrupt-{timestamp}.bak then rebuild.
192
+ * Idempotent: skips if already registered.
193
+ * @returns {boolean} Whether a fix was applied
194
+ */
195
+ function fixTuiRegistration() {
196
+ const { data, error } = readTuiJson();
197
+
198
+ // Corrupt JSON: backup old file, rebuild from scratch
199
+ if (error && data === undefined) {
200
+ const ts = Date.now();
201
+ const backupPath = `${TUI_JSON_PATH}.corrupt-${ts}.bak`;
202
+ try {
203
+ fs.copyFileSync(TUI_JSON_PATH, backupPath);
204
+ console.log(` ⚠ TUI config corrupted — backed up to ${backupPath}`);
205
+ } catch { /* non-critical */ }
206
+ // Rebuild fresh
207
+ const dir = path.dirname(TUI_JSON_PATH);
208
+ fs.mkdirSync(dir, { recursive: true });
209
+ const newConfig = { plugin: [TUI_PLUGIN_ENTRY] };
210
+ const tmpPath = `${TUI_JSON_PATH}.tmp`;
211
+ fs.writeFileSync(tmpPath, JSON.stringify(newConfig, null, 2));
212
+ fs.renameSync(tmpPath, TUI_JSON_PATH);
213
+ console.log(` ✓ Registered ${TUI_PLUGIN_ENTRY} in TUI (rebuilt after corrupt backup)`);
214
+ return true;
215
+ }
216
+
217
+ // File doesn't exist: create it
218
+ if (data === null) {
219
+ const dir = path.dirname(TUI_JSON_PATH);
220
+ fs.mkdirSync(dir, { recursive: true });
221
+ const newConfig = { plugin: [TUI_PLUGIN_ENTRY] };
222
+ const tmpPath = `${TUI_JSON_PATH}.tmp`;
223
+ fs.writeFileSync(tmpPath, JSON.stringify(newConfig, null, 2));
224
+ fs.renameSync(tmpPath, TUI_JSON_PATH);
225
+ console.log(` ✓ Created TUI config with ${TUI_PLUGIN_ENTRY}`);
226
+ return true;
227
+ }
228
+
229
+ // File exists, check if plugin already registered (idempotent)
230
+ const plugins = Array.isArray(data.plugin) ? data.plugin : [];
231
+ if (plugins.includes(TUI_PLUGIN_ENTRY)) return false;
232
+
233
+ // Append plugin entry
234
+ data.plugin = plugins.concat([TUI_PLUGIN_ENTRY]);
235
+ const tmpPath = `${TUI_JSON_PATH}.tmp`;
236
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
237
+ fs.renameSync(tmpPath, TUI_JSON_PATH);
238
+ console.log(` ✓ Added ${TUI_PLUGIN_ENTRY} to TUI config`);
239
+ return true;
240
+ }
241
+
242
+ /**
243
+ * Wrapper for init.js: ensure TUI is registered without console output.
244
+ * Returns true if the plugin is already registered or was just added.
245
+ */
246
+ function ensureTuiRegistration() {
247
+ const { data } = readTuiJson();
248
+ if (data === null || data === undefined) {
249
+ fixTuiRegistration();
250
+ return;
251
+ }
252
+ const plugins = Array.isArray(data.plugin) ? data.plugin : [];
253
+ if (!plugins.includes(TUI_PLUGIN_ENTRY)) {
254
+ fixTuiRegistration();
255
+ }
256
+ }
257
+
258
+ module.exports = {
259
+ TUI_JSON_PATH,
260
+ TUI_PLUGIN_ENTRY,
261
+ readTuiJson,
262
+ diagnoseTuiRegistration,
263
+ diagnoseInstalledSkills,
264
+ fixTuiRegistration,
265
+ ensureTuiRegistration,
266
+ extractSkillVersion,
267
+ compareSemver,
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 {