@boyingliu01/xp-gate 0.20.1 → 0.20.2
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/adapters/java.sh +373 -27
- package/gate-3.sh +51 -3
- package/gate-4.sh +32 -1
- package/hooks/gate-3.sh +51 -3
- package/hooks/gate-4.sh +32 -1
- package/hooks/pre-commit +275 -36
- package/lib/__tests__/doctor-spawn-capability.test.js +116 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +49 -10
- package/lib/doctor-tui.js +21 -3
- package/lib/doctor.js +111 -8
- package/lib/gate-audit.ts +3 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/claude-code/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +1 -0
- package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/claude-code/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/claude-code/skills/to-issues/SKILL.md +1 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/dsh/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/dsh/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/dsh/skills/grilling/SKILL.md +1 -0
- package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/dsh/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/dsh/skills/to-issues/SKILL.md +1 -0
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +1 -0
- package/plugins/opencode/skills/batch-grill-me/SKILL.md +1 -0
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +1 -0
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/domain-modeling/SKILL.md +1 -0
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/opencode/skills/grilling/SKILL.md +1 -0
- package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/plugins/opencode/skills/ralph-loop/SKILL.md +1 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-driven-development/SKILL.md +1 -0
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/SKILL.md +1 -0
- package/plugins/opencode/skills/to-issues/SKILL.md +1 -0
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/principles/adapters/__tests__/java.test.ts +317 -0
- package/principles/adapters/java.ts +325 -13
- package/skills/admin-template-guidelines/SKILL.md +1 -0
- package/skills/batch-grill-me/SKILL.md +1 -0
- package/skills/clipboard-vision/SKILL.md +1 -0
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/domain-modeling/SKILL.md +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/grilling/SKILL.md +1 -0
- package/skills/improve-codebase-architecture/SKILL.md +1 -0
- package/skills/ralph-loop/SKILL.md +1 -0
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-driven-development/SKILL.md +1 -0
- package/skills/test-specification-alignment/AGENTS.md +2 -2
- package/skills/test-specification-alignment/SKILL.md +1 -0
- package/skills/to-issues/SKILL.md +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test #525 doctor: detect sync-spawn EBUSY (WorkBuddy sandbox) and attribute it
|
|
3
|
+
* @intent The Spawn capability check reports a WARN with fixed attribution when
|
|
4
|
+
* spawnSync fails with EBUSY, PASS when sync spawn works, SKIP on any other
|
|
5
|
+
* probe error — and never adds to the issue count (environmental, not a
|
|
6
|
+
* defect). execWithTimeout surfaces the same attribution on stderr once
|
|
7
|
+
* instead of returning a bare null that reads as "tool missing".
|
|
8
|
+
* @note Every probe is mocked: a test asserting the real spawn path would
|
|
9
|
+
* itself be EBUSY-blocked inside a WorkBuddy session (the very condition
|
|
10
|
+
* under test) and green elsewhere — a test that changes verdict with the
|
|
11
|
+
* terminal it runs in is not a test.
|
|
12
|
+
*/
|
|
13
|
+
describe('checkSpawnCapability (#525)', () => {
|
|
14
|
+
let doc;
|
|
15
|
+
let spawnSpy;
|
|
16
|
+
|
|
17
|
+
function loadDoctorWithSpawn(syncImpl) {
|
|
18
|
+
vi.resetModules();
|
|
19
|
+
delete require.cache[require.resolve('../doctor')];
|
|
20
|
+
spawnSpy = vi.spyOn(require('child_process'), 'spawnSync').mockImplementation(syncImpl);
|
|
21
|
+
doc = require('../doctor');
|
|
22
|
+
return spawnSpy;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
if (spawnSpy) spawnSpy.mockRestore();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('reports PASS with the node version when sync spawn works', () => {
|
|
30
|
+
loadDoctorWithSpawn(() => ({ error: null, stdout: 'v22.11.0\n', stderr: '' }));
|
|
31
|
+
const checks = [];
|
|
32
|
+
const issues = doc.checkSpawnCapability(checks);
|
|
33
|
+
expect(issues).toBe(0);
|
|
34
|
+
expect(checks).toHaveLength(1);
|
|
35
|
+
expect(checks[0].name).toBe('Spawn capability');
|
|
36
|
+
expect(checks[0].status).toBe('PASS');
|
|
37
|
+
expect(checks[0].detail).toContain('v22.11.0');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('reports WARN with fixed attribution on EBUSY and never counts it', () => {
|
|
41
|
+
loadDoctorWithSpawn(() => ({
|
|
42
|
+
error: Object.assign(new Error('spawn EBUSY'), { code: 'EBUSY' }),
|
|
43
|
+
stdout: '',
|
|
44
|
+
stderr: '',
|
|
45
|
+
}));
|
|
46
|
+
const checks = [];
|
|
47
|
+
const issues = doc.checkSpawnCapability(checks);
|
|
48
|
+
expect(issues).toBe(0);
|
|
49
|
+
expect(checks).toHaveLength(1);
|
|
50
|
+
expect(checks[0].status).toBe('WARN');
|
|
51
|
+
expect(checks[0].detail).toBe(doc.EBUSY_ATTRIBUTION);
|
|
52
|
+
// The attribution must name the actual situation, not a vague failure.
|
|
53
|
+
expect(doc.EBUSY_ATTRIBUTION).toContain('WorkBuddy');
|
|
54
|
+
expect(doc.EBUSY_ATTRIBUTION).toContain('async child_process.spawn');
|
|
55
|
+
expect(doc.EBUSY_ATTRIBUTION).toContain('NOT a code or antivirus problem');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reports SKIP (not a sandbox WARN) on non-EBUSY probe errors', () => {
|
|
59
|
+
loadDoctorWithSpawn(() => ({
|
|
60
|
+
error: Object.assign(new Error('permission denied'), { code: 'EACCES' }),
|
|
61
|
+
stdout: '',
|
|
62
|
+
stderr: '',
|
|
63
|
+
}));
|
|
64
|
+
const checks = [];
|
|
65
|
+
const issues = doc.checkSpawnCapability(checks);
|
|
66
|
+
expect(issues).toBe(0);
|
|
67
|
+
expect(checks[0].status).toBe('SKIP');
|
|
68
|
+
expect(checks[0].detail).toContain('EACCES');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('probes via process.execPath so PATH differences cannot skew the verdict', () => {
|
|
72
|
+
const spy = loadDoctorWithSpawn(() => ({ error: null, stdout: 'vX\n', stderr: '' }));
|
|
73
|
+
doc.checkSpawnCapability([]);
|
|
74
|
+
expect(spy).toHaveBeenCalled();
|
|
75
|
+
expect(spy.mock.calls[0][0]).toBe(process.execPath);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('probe failure (thrown) degrades to SKIP instead of crashing doctor', () => {
|
|
79
|
+
loadDoctorWithSpawn(() => {
|
|
80
|
+
throw new Error('hostile environment');
|
|
81
|
+
});
|
|
82
|
+
const checks = [];
|
|
83
|
+
const issues = doc.checkSpawnCapability(checks);
|
|
84
|
+
expect(issues).toBe(0);
|
|
85
|
+
expect(checks[0].status).toBe('SKIP');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('execWithTimeout EBUSY attribution (#525)', () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
vi.resetModules();
|
|
92
|
+
delete require.cache[require.resolve('../doctor')];
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('emits the attribution on stderr once for repeated EBUSY, never a bare null read', async () => {
|
|
96
|
+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
97
|
+
const cp = require('child_process');
|
|
98
|
+
// doctor.js destructures `exec` at require time, so the spy must be in
|
|
99
|
+
// place BEFORE the fresh require below.
|
|
100
|
+
const execSpy = vi.spyOn(cp, 'exec').mockImplementation((cmd, opts, cb) => {
|
|
101
|
+
const callback = typeof opts === 'function' ? opts : cb;
|
|
102
|
+
setImmediate(() => callback(Object.assign(new Error('busy'), { code: 'EBUSY' })));
|
|
103
|
+
return { killed: false, exitCode: null, kill() {} };
|
|
104
|
+
});
|
|
105
|
+
const doc = require('../doctor');
|
|
106
|
+
|
|
107
|
+
await doc.execWithTimeout('anything');
|
|
108
|
+
await doc.execWithTimeout('anything-again');
|
|
109
|
+
|
|
110
|
+
const attrLines = errSpy.mock.calls.filter((c) => String(c[0]).includes('WorkBuddy'));
|
|
111
|
+
expect(attrLines).toHaveLength(1);
|
|
112
|
+
expect(String(attrLines[0][0])).toContain('async child_process.spawn');
|
|
113
|
+
execSpy.mockRestore();
|
|
114
|
+
errSpy.mockRestore();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -72,34 +72,73 @@ describe('doctor-tui skill freshness (#439)', () => {
|
|
|
72
72
|
expect(check.status).toBe('PASS');
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* AC-439-04: every bundled skill now carries a version, so the versionless
|
|
77
|
+
* fallback branch is only reachable when the INSTALLED copy lacks one (e.g.
|
|
78
|
+
* written by an older tool version). Strip the version line to simulate it.
|
|
79
|
+
*/
|
|
80
|
+
function stripVersion(content) {
|
|
81
|
+
return content.replace(/^version:[^\n]*\n/m, '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
it('treats an installed copy without version as up to date when only line endings differ', () => {
|
|
76
85
|
const bundled = readBundled('grilling');
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
expect(bundled).
|
|
80
|
-
installSkill('grilling', flipLineEndings(bundled));
|
|
86
|
+
// AC-439-04: grilling used to be one of the 11 skills without a version
|
|
87
|
+
// field; all bundled skills now declare one.
|
|
88
|
+
expect(bundled).toMatch(/^version:/m);
|
|
89
|
+
installSkill('grilling', flipLineEndings(stripVersion(bundled)));
|
|
81
90
|
const { check } = runFreshness('grilling');
|
|
82
91
|
expect(check, JSON.stringify(check)).toBeDefined();
|
|
83
92
|
expect(check.status).toBe('PASS');
|
|
84
93
|
});
|
|
85
94
|
|
|
86
|
-
// AC-439-02 promises "differences beyond line endings" still report
|
|
95
|
+
// AC-439-02 promises "differences beyond line endings" still report WARN,
|
|
87
96
|
// which means every line-ending convention has to be tolerated, not just CRLF
|
|
88
97
|
// (Round 2 feasibility FC-05: the helper's name already says as much).
|
|
89
98
|
it('treats a lone-CR skill file as up to date too', () => {
|
|
90
99
|
const bundled = readBundled('grilling');
|
|
91
|
-
installSkill(
|
|
100
|
+
installSkill(
|
|
101
|
+
'grilling',
|
|
102
|
+
stripVersion(bundled).replace(/\r\n/g, '\n').replace(/\n/g, '\r')
|
|
103
|
+
);
|
|
92
104
|
const { check } = runFreshness('grilling');
|
|
93
105
|
expect(check, JSON.stringify(check)).toBeDefined();
|
|
94
106
|
expect(check.status).toBe('PASS');
|
|
95
107
|
});
|
|
96
108
|
|
|
97
|
-
it('still reports
|
|
109
|
+
it('still reports WARN for a real content difference (anti-vacuity)', () => {
|
|
98
110
|
const bundled = readBundled('grilling');
|
|
99
|
-
installSkill('grilling', `${flipLineEndings(bundled)}\n## Diverged section\n`);
|
|
111
|
+
installSkill('grilling', `${flipLineEndings(stripVersion(bundled))}\n## Diverged section\n`);
|
|
100
112
|
const { issues, check } = runFreshness('grilling');
|
|
101
113
|
expect(check.status).toBe('WARN');
|
|
102
|
-
expect(check.detail).toContain('Outdated');
|
|
103
114
|
expect(issues).toBe(1);
|
|
104
115
|
});
|
|
116
|
+
|
|
117
|
+
// AC-439-03: without a version the doctor cannot claim "Outdated", and the
|
|
118
|
+
// advice must NOT be the destructive `update-skill --all` -- it pulls from
|
|
119
|
+
// main, which may be older than the installed copy (#439).
|
|
120
|
+
it('versionless content diff advises manual comparison, never the destructive update', () => {
|
|
121
|
+
const bundled = readBundled('grilling');
|
|
122
|
+
installSkill('grilling', `${flipLineEndings(stripVersion(bundled))}\n## Diverged section\n`);
|
|
123
|
+
const { check } = runFreshness('grilling');
|
|
124
|
+
expect(check.status).toBe('WARN');
|
|
125
|
+
expect(check.detail).toContain('Content differs');
|
|
126
|
+
expect(check.detail).not.toMatch(/update-skill\s+--all/);
|
|
127
|
+
expect(check.detail).toContain('update-skill'); // the caveat IS mentioned
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// AC-439-04: the 11 skills that used to fall into the byte-comparison branch
|
|
131
|
+
// now declare a version, making semver equality the primary freshness path.
|
|
132
|
+
it('every bundled skill carries a version frontmatter', () => {
|
|
133
|
+
const skills = fs
|
|
134
|
+
.readdirSync(bundledSkillsDir, { withFileTypes: true })
|
|
135
|
+
.filter((d) => d.isDirectory())
|
|
136
|
+
.map((d) => d.name);
|
|
137
|
+
expect(skills.length).toBeGreaterThanOrEqual(13);
|
|
138
|
+
const missing = skills.filter((name) => {
|
|
139
|
+
const md = path.join(bundledSkillsDir, name, 'SKILL.md');
|
|
140
|
+
return !/^version:\s*\d+\.\d+\.\d+/m.test(fs.readFileSync(md, 'utf8'));
|
|
141
|
+
});
|
|
142
|
+
expect(missing, `skills missing version frontmatter: ${missing.join(', ')}`).toEqual([]);
|
|
143
|
+
});
|
|
105
144
|
});
|
package/lib/doctor-tui.js
CHANGED
|
@@ -87,6 +87,16 @@ function sameContentIgnoringLineEndings(a, b) {
|
|
|
87
87
|
return normalize(a) === normalize(b);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Drop the frontmatter `version:` line, if any. Version METADATA is not content:
|
|
92
|
+
* when one side lacks a version, that line alone would otherwise read as a real
|
|
93
|
+
* content difference and every pre-#439-format installed copy would be flagged
|
|
94
|
+
* forever (#439).
|
|
95
|
+
*/
|
|
96
|
+
function withoutVersionLine(content) {
|
|
97
|
+
return content.replace(/^version:[^\n]*\r?\n/m, '');
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
/**
|
|
91
101
|
* Compare two semver strings. Returns:
|
|
92
102
|
* -1 if a < b, 0 if a == b, 1 if a > b, undefined if either is not valid semver.
|
|
@@ -185,12 +195,20 @@ function diagnoseInstalledSkills(config, checks) {
|
|
|
185
195
|
}
|
|
186
196
|
} else {
|
|
187
197
|
// No version in at least one file — compare content, but not byte-for-byte:
|
|
188
|
-
// line endings alone are not evidence of staleness (#439).
|
|
189
|
-
|
|
198
|
+
// line endings alone are not evidence of staleness (#439). And without a
|
|
199
|
+
// version we cannot call it "Outdated": update-skill pulls from main,
|
|
200
|
+
// which may be OLDER than what the user has installed, so the advice is
|
|
201
|
+
// "compare manually", never the destructive update command (#439). The
|
|
202
|
+
// version line itself is excluded from the comparison -- metadata, not
|
|
203
|
+
// content.
|
|
204
|
+
if (!sameContentIgnoringLineEndings(
|
|
205
|
+
withoutVersionLine(bundledContent),
|
|
206
|
+
withoutVersionLine(userContent)
|
|
207
|
+
)) {
|
|
190
208
|
checks.push({
|
|
191
209
|
name: `Skill: ${name}`,
|
|
192
210
|
status: 'WARN',
|
|
193
|
-
detail: '
|
|
211
|
+
detail: 'Content differs and version cannot be determined (missing version frontmatter) — compare manually before running update-skill (it pulls from main, which may be older than the installed copy)',
|
|
194
212
|
});
|
|
195
213
|
issues++;
|
|
196
214
|
} else {
|
package/lib/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { execSync } = require('child_process');
|
|
3
|
+
const { execSync, spawnSync } = require('child_process');
|
|
4
4
|
const { exec } = require('child_process');
|
|
5
5
|
const util = require('util');
|
|
6
6
|
const execAsync = util.promisify(exec);
|
|
@@ -62,10 +62,47 @@ const EXEC_TIMEOUT_MS = 1500;
|
|
|
62
62
|
*/
|
|
63
63
|
const GLOBAL_DIAGNOSIS_TIMEOUT_MS = 10000;
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Fixed attribution text for the WorkBuddy-desktop sandbox restriction (#525).
|
|
67
|
+
*
|
|
68
|
+
* In a WorkBuddy desktop session, a node process's *synchronous* child-process
|
|
69
|
+
* calls (spawnSync/execSync) always fail with EBUSY, while async spawn works;
|
|
70
|
+
* external terminals, CI and git hook chains are unaffected. The interception
|
|
71
|
+
* layer belongs to the host application, not to xp-gate or any antivirus, so
|
|
72
|
+
* the only correct response is to name it and point at the working channels.
|
|
73
|
+
*/
|
|
74
|
+
const EBUSY_ATTRIBUTION = 'sync spawn blocked by host sandbox (EBUSY) -- likely WorkBuddy desktop session; '
|
|
75
|
+
+ 'use async child_process.spawn in-session, verify via CI / external terminal / git hook chain. '
|
|
76
|
+
+ 'NOT a code or antivirus problem.';
|
|
77
|
+
|
|
65
78
|
/**
|
|
66
79
|
* Execute a command with a hard timeout using async exec.
|
|
67
80
|
* Returns { stdout, stderr } on success, null on failure/timeout.
|
|
68
81
|
*/
|
|
82
|
+
/**
|
|
83
|
+
* Dedup scope: one attribution line per *diagnosis* is a diagnosis; one per
|
|
84
|
+
* check is noise. `diagnoseAsync` resets this, so a long-lived host process
|
|
85
|
+
* (or a --fix pre/post double run) still attributes each report round (#525 R1).
|
|
86
|
+
*/
|
|
87
|
+
let ebusyAttributionShown = false;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Shared EBUSY attribution printer for BOTH spawn families (#525 R1).
|
|
91
|
+
*
|
|
92
|
+
* Note the asymmetry the platform sandbox exhibits: synchronous child-process
|
|
93
|
+
* calls (execSync/spawnSync) always fail with EBUSY in a WorkBuddy session,
|
|
94
|
+
* while async spawn/exec currently pass. The sync call sites below are where
|
|
95
|
+
* the false reds actually come from; the async branch in execWithTimeout is
|
|
96
|
+
* kept as a defense-in-depth no-op today -- if the sandbox ever widens its
|
|
97
|
+
* interception, the attribution is already wired.
|
|
98
|
+
*/
|
|
99
|
+
function noteSandboxAttribution(err) {
|
|
100
|
+
if (err && err.code === 'EBUSY' && !ebusyAttributionShown) {
|
|
101
|
+
ebusyAttributionShown = true;
|
|
102
|
+
console.error(` ⚠ ${EBUSY_ATTRIBUTION}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
69
106
|
async function execWithTimeout(cmd, opts = {}) {
|
|
70
107
|
try {
|
|
71
108
|
const { stdout, stderr } = await execAsync(cmd, {
|
|
@@ -73,11 +110,54 @@ async function execWithTimeout(cmd, opts = {}) {
|
|
|
73
110
|
timeout: opts.timeout || EXEC_TIMEOUT_MS,
|
|
74
111
|
});
|
|
75
112
|
return { stdout: stdout.trim(), stderr: stderr.trim() };
|
|
76
|
-
} catch {
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// #525: a bare null here reads as "tool missing" and has already cost a
|
|
115
|
+
// misdiagnosis round. Name the sandbox once when it is the sandbox.
|
|
116
|
+
noteSandboxAttribution(err);
|
|
77
117
|
return null;
|
|
78
118
|
}
|
|
79
119
|
}
|
|
80
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Probe whether synchronous child-process creation works in this process (#525).
|
|
123
|
+
*
|
|
124
|
+
* @param {Array<{name: string, status: string, detail: string}>} checks — mutated
|
|
125
|
+
* @returns {number} always 0, for assertion convenience in tests only — callers
|
|
126
|
+
* MUST ignore it: a sandbox WARN is environmental, never an issue count.
|
|
127
|
+
* (`formatDoctorJson` likewise excludes this check from missing_tools.)
|
|
128
|
+
*/
|
|
129
|
+
function checkSpawnCapability(checks) {
|
|
130
|
+
let result;
|
|
131
|
+
try {
|
|
132
|
+
result = spawnSync(process.execPath, ['--version'], {
|
|
133
|
+
encoding: 'utf8',
|
|
134
|
+
timeout: EXEC_TIMEOUT_MS,
|
|
135
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
checks.push({ name: 'Spawn capability', status: 'SKIP', detail: `probe could not run: ${err.message}` });
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
if (!result.error) {
|
|
142
|
+
checks.push({
|
|
143
|
+
name: 'Spawn capability',
|
|
144
|
+
status: 'PASS',
|
|
145
|
+
detail: `sync spawn OK (node ${(result.stdout || '').trim() || 'unknown'})`,
|
|
146
|
+
});
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
if (result.error.code === 'EBUSY') {
|
|
150
|
+
checks.push({ name: 'Spawn capability', status: 'WARN', detail: EBUSY_ATTRIBUTION });
|
|
151
|
+
// Context for the rest of the report (#525 R1): the sandbox hits every
|
|
152
|
+
// sync spawn, so sibling FAIL entries ("Not found" et al) may share this
|
|
153
|
+
// cause instead of being real machine problems.
|
|
154
|
+
console.error(' ⚠ Other FAIL/WARN entries in this report may share this cause (they run sync spawns too).');
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
checks.push({ name: 'Spawn capability', status: 'SKIP', detail: `probe error: ${result.error.code || result.error.message}` });
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
81
161
|
function isXpGateFile(filePath, signature) {
|
|
82
162
|
if (!fs.existsSync(filePath)) return false;
|
|
83
163
|
try {
|
|
@@ -121,7 +201,8 @@ function getGitDirSync() {
|
|
|
121
201
|
timeout: EXEC_TIMEOUT_MS,
|
|
122
202
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
123
203
|
}).trim();
|
|
124
|
-
} catch {
|
|
204
|
+
} catch (err) {
|
|
205
|
+
noteSandboxAttribution(err); // a sync-spawn EBUSY here reads as "not a git repo" otherwise (#525)
|
|
125
206
|
return null;
|
|
126
207
|
}
|
|
127
208
|
}
|
|
@@ -144,7 +225,8 @@ function getCurrentHooksPathSync() {
|
|
|
144
225
|
timeout: EXEC_TIMEOUT_MS,
|
|
145
226
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
146
227
|
}).trim();
|
|
147
|
-
} catch {
|
|
228
|
+
} catch (err) {
|
|
229
|
+
noteSandboxAttribution(err); // a sync-spawn EBUSY here reads as "hooksPath not set" otherwise (#525)
|
|
148
230
|
return null;
|
|
149
231
|
}
|
|
150
232
|
}
|
|
@@ -786,8 +868,10 @@ function defaultProjectGithooks() {
|
|
|
786
868
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
787
869
|
}).trim();
|
|
788
870
|
if (toplevel) return path.join(toplevel, 'githooks');
|
|
789
|
-
} catch {
|
|
871
|
+
} catch (err) {
|
|
790
872
|
// Not a git repository -- fall back to the directory we were called in.
|
|
873
|
+
// But an EBUSY is not "not a repository": name the sandbox instead (#525).
|
|
874
|
+
noteSandboxAttribution(err);
|
|
791
875
|
}
|
|
792
876
|
return path.join(process.cwd(), 'githooks');
|
|
793
877
|
}
|
|
@@ -957,8 +1041,10 @@ function printEffectiveHooks() {
|
|
|
957
1041
|
timeout: EXEC_TIMEOUT_MS,
|
|
958
1042
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
959
1043
|
}).trim();
|
|
960
|
-
} catch {
|
|
1044
|
+
} catch (err) {
|
|
961
1045
|
// No explicit core.hooksPath -> git falls back to .git/hooks.
|
|
1046
|
+
// But an EBUSY is not "no hooksPath configured": attribute it (#525).
|
|
1047
|
+
noteSandboxAttribution(err);
|
|
962
1048
|
configured = '';
|
|
963
1049
|
}
|
|
964
1050
|
|
|
@@ -1170,6 +1256,13 @@ async function diagnoseAsync() {
|
|
|
1170
1256
|
const checks = [];
|
|
1171
1257
|
let issues = 0;
|
|
1172
1258
|
|
|
1259
|
+
// Spawn capability probe FIRST (sync, no config dependency) — #525: it must
|
|
1260
|
+
// report even when the config early-exits below, and its WARN (environmental,
|
|
1261
|
+
// never an issue count) primes the interpretation of every later FAIL. The
|
|
1262
|
+
// attribution dedup scope is one diagnosis, so reset it here.
|
|
1263
|
+
ebusyAttributionShown = false;
|
|
1264
|
+
checkSpawnCapability(checks);
|
|
1265
|
+
|
|
1173
1266
|
// --- Phase 1: Config-dependent checks (sequential, data dependencies) ---
|
|
1174
1267
|
const configResult = checkConfig();
|
|
1175
1268
|
if (configResult.config === null) {
|
|
@@ -1183,6 +1276,7 @@ async function diagnoseAsync() {
|
|
|
1183
1276
|
issues += diagnoseVersion(config, getPackageVersion(), checks);
|
|
1184
1277
|
// TemplateDir check (sync, no I/O)
|
|
1185
1278
|
issues += diagnoseTemplateDir(config, checks);
|
|
1279
|
+
// (Spawn capability probe already ran at the top of diagnoseAsync — #525.)
|
|
1186
1280
|
|
|
1187
1281
|
// --- Phase 2: Fetch gitDir early (needed by hooks and adapters) ---
|
|
1188
1282
|
const gitDirPromise = getGitDir();
|
|
@@ -1459,7 +1553,10 @@ function printReport(checks) {
|
|
|
1459
1553
|
console.log('-----------------');
|
|
1460
1554
|
|
|
1461
1555
|
for (const check of checks) {
|
|
1462
|
-
|
|
1556
|
+
// SKIP is "checked, not applicable" — rendering it as the failure symbol
|
|
1557
|
+
// made a healthy machine look broken (#525 R1 touched this via the
|
|
1558
|
+
// Spawn capability probe's SKIP branch).
|
|
1559
|
+
const statusSymbol = check.status === 'PASS' ? ' ✓' : check.status === 'WARN' ? ' ⚠' : check.status === 'SKIP' ? ' –' : ' ✗';
|
|
1463
1560
|
console.log(` ${statusSymbol} ${check.name}: ${check.detail}`);
|
|
1464
1561
|
}
|
|
1465
1562
|
}
|
|
@@ -2235,8 +2332,11 @@ async function doctor(args) {
|
|
|
2235
2332
|
}
|
|
2236
2333
|
|
|
2237
2334
|
function formatDoctorJson(checks, issues) {
|
|
2335
|
+
// missing_tools means "installable CLI tool absent" (#525 R1): the Spawn
|
|
2336
|
+
// capability WARN is environmental (host sandbox) and must not appear here,
|
|
2337
|
+
// or machine consumers would read a healthy machine as needing an install.
|
|
2238
2338
|
const missing_tools = checks
|
|
2239
|
-
.filter(c => c.status === 'WARN' || c.status === 'FAIL')
|
|
2339
|
+
.filter(c => c.name !== 'Spawn capability' && (c.status === 'WARN' || c.status === 'FAIL'))
|
|
2240
2340
|
.map(c => ({ name: c.name, detail: c.detail }));
|
|
2241
2341
|
return {
|
|
2242
2342
|
ok: issues === 0,
|
|
@@ -2272,4 +2372,7 @@ module.exports = {
|
|
|
2272
2372
|
repoModuleSource,
|
|
2273
2373
|
contentDelta,
|
|
2274
2374
|
pathsEquivalent,
|
|
2375
|
+
checkSpawnCapability,
|
|
2376
|
+
execWithTimeout,
|
|
2377
|
+
EBUSY_ATTRIBUTION,
|
|
2275
2378
|
};
|
package/lib/gate-audit.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface GateAuditEntry {
|
|
|
37
37
|
repo_path: string;
|
|
38
38
|
commit_hash: string;
|
|
39
39
|
duration_anomaly?: boolean; // Issue #370: set when duration_ms > max_duration_ms
|
|
40
|
+
detail?: string; // #507: optional gate-specific payload (e.g. effective CCN threshold)
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
@@ -258,6 +259,8 @@ function runCli(): void {
|
|
|
258
259
|
commit_hash: getCommitHash(),
|
|
259
260
|
// Issue #370: only set when explicitly flagged (omit field otherwise)
|
|
260
261
|
...(opts['duration-anomaly'] === 'true' ? { duration_anomaly: true } : {}),
|
|
262
|
+
// #507: only set when provided (omit field otherwise)
|
|
263
|
+
...(opts['detail'] ? { detail: opts['detail'] } : {}),
|
|
261
264
|
};
|
|
262
265
|
appendAuditEntry(entry);
|
|
263
266
|
} else {
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
3
|
+
**Generated:** 2026-10-12
|
|
4
4
|
**Commit:** unknown
|
|
5
5
|
**Branch:** unknown
|
|
6
|
-
**Version:** 0.20.
|
|
6
|
+
**Version:** 0.20.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
3
|
+
**Generated:** 2026-10-12
|
|
4
4
|
**Commit:** unknown
|
|
5
5
|
**Branch:** unknown
|
|
6
|
-
**Version:** 0.20.
|
|
6
|
+
**Version:** 0.20.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: admin-template-guidelines
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: "6 maintainability rules for AI-generated admin interfaces (Fastify + Nunjucks + HTMX + Alpine.js). Prevents route bloat, test inconsistency, Nunjucks traps, repeated data logic, auth blind spots, and HTMX/Alpine confusion."
|
|
4
5
|
maturity: alpha
|
|
5
6
|
---
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
3
|
+
**Generated:** 2026-10-12
|
|
4
4
|
**Commit:** unknown
|
|
5
5
|
**Branch:** unknown
|
|
6
|
-
**Version:** 0.20.
|
|
6
|
+
**Version:** 0.20.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: domain-modeling
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: improve-codebase-architecture
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: Find deepening opportunities in a codebase, informed by domain language in CONTEXT.md and decisions in ADRs. Use when user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more AI-navigable. Run periodically (weekly or after surges of development).
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
3
|
+
**Generated:** 2026-10-12
|
|
4
4
|
**Commit:** unknown
|
|
5
5
|
**Branch:** unknown
|
|
6
|
-
**Version:** 0.20.
|
|
6
|
+
**Version:** 0.20.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
3
|
+
**Generated:** 2026-10-12
|
|
4
4
|
**Commit:** unknown
|
|
5
5
|
**Branch:** unknown
|
|
6
|
-
**Version:** 0.20.
|
|
6
|
+
**Version:** 0.20.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: to-issues
|
|
3
|
+
version: 1.0.0
|
|
3
4
|
description: Break a plan, spec, or PRD into independently-grabbable issues using tracer-bullet vertical slices. Use when user wants to convert a plan into issues, create implementation tickets, or break down work into vertical slices. Integrates with XP-Gate sprint-flow Phase 1.
|
|
4
5
|
---
|
|
5
6
|
|
package/plugins/dsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
4
4
|
"description": "XP-Gate deterministic quality gates + AI workflow skills as a DeepSeek Harness (DSH) native plugin: gate-check / gate-principles / gate-arch tools + 12 SKILL.md skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|