@boyingliu01/xp-gate 0.15.5 → 0.18.1
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/adapter-common.sh +0 -0
- package/adapters/cpp.sh +0 -0
- package/adapters/dart.sh +0 -0
- package/adapters/flutter.sh +0 -0
- package/adapters/go.sh +0 -0
- package/adapters/iac.sh +0 -0
- package/adapters/java.sh +0 -0
- package/adapters/kotlin.sh +0 -0
- package/adapters/objectivec.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
- package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
- package/adapters/powershell.sh +0 -0
- package/adapters/python.sh +0 -0
- package/adapters/shell.sh +0 -0
- package/adapters/swift.sh +0 -0
- package/adapters/typescript.sh +0 -0
- package/bin/xp-gate.js +29 -1
- package/gate-3.sh +0 -0
- package/gate-4.sh +0 -0
- package/gate-7.sh +0 -0
- package/gate-8.sh +0 -0
- package/gate-9.sh +0 -0
- package/hooks/adapter-common.sh +0 -0
- package/hooks/gate-3.sh +0 -0
- package/hooks/gate-4.sh +0 -0
- package/hooks/gate-7.sh +0 -0
- package/hooks/gate-8.sh +0 -0
- package/hooks/gate-9.sh +0 -0
- package/hooks/lib/now-ms.sh +33 -0
- package/hooks/pre-commit +63 -47
- package/hooks/pre-push +0 -0
- 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 +512 -272
- package/lib/__tests__/retro.test.js +131 -0
- package/lib/__tests__/sprint-audit.test.js +207 -207
- package/lib/__tests__/sprint-status-rework.test.js +184 -0
- package/lib/check-version.js +9 -0
- package/lib/doctor-tui.js +268 -198
- 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/language-tools.js +833 -833
- package/lib/mattpocock-license-verdict.json +7 -0
- package/lib/phase-transition.js +499 -249
- package/lib/retro.js +222 -0
- package/lib/shared-utils.js +7 -0
- package/lib/sprint-audit.js +235 -235
- 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/bin/delphi-review-guard.sh +0 -0
- package/plugins/claude-code/bin/xp-gate-check +0 -0
- 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/agents/delphi-architecture.md +48 -48
- package/plugins/qoder/agents/delphi-feasibility.md +48 -48
- package/plugins/qoder/agents/delphi-technical.md +48 -48
- package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
- package/plugins/qoder/bin/xp-gate-check +0 -0
- 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/delphi-external-review.cjs +432 -432
- 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
- package/adapters/gate-3.sh +0 -68
- package/adapters/gate-4.sh +0 -80
- package/adapters/gate-7.sh +0 -45
- package/adapters/gate-8.sh +0 -56
- 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
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @param {
|
|
64
|
-
* @
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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 {
|