@boyingliu01/xp-gate 0.27.0 → 0.27.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/lib/__tests__/doctor-exec-retry.test.js +152 -0
- package/lib/__tests__/windows-bash-probe.test.js +57 -7
- package/lib/detect-deps.js +69 -13
- package/lib/doctor.js +45 -11
- 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/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- 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/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-DOCTOR retry-on-empty-stream guard
|
|
3
|
+
* @intent On win32, cmd.exe (exec's shell) can deliver exit 0 with an EMPTY
|
|
4
|
+
* stdout+stderr when many spawns are in flight — doctor read that as
|
|
5
|
+
* "Not found" and red-flagged Node/Git/hooksPath on a healthy machine.
|
|
6
|
+
* execWithTimeout must retry an empty-but-successful probe before
|
|
7
|
+
* reporting it, and must NOT retry real failures or real output.
|
|
8
|
+
* @covers AC-DOCTOR-RETRY-01
|
|
9
|
+
*/
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { describe, it, expect, afterEach } = await import('vitest');
|
|
12
|
+
|
|
13
|
+
const cpPath = require.resolve('child_process');
|
|
14
|
+
|
|
15
|
+
function stubExec(behaviors) {
|
|
16
|
+
// doctor builds its async exec at load time via util.promisify(exec), which
|
|
17
|
+
// binds Node's INTERNAL custom promisify implementation — replacing the plain
|
|
18
|
+
// `exec` property afterwards is invisible to it (measured: stub fired 0-1
|
|
19
|
+
// times while the real cmd.exe child ran). The only patch point that holds
|
|
20
|
+
// across a fresh require is util.promisify itself.
|
|
21
|
+
const util = require('util');
|
|
22
|
+
const originalPromisify = util.promisify;
|
|
23
|
+
const calls = [];
|
|
24
|
+
util.promisify = function (fn) {
|
|
25
|
+
if (fn && fn.name === 'exec') {
|
|
26
|
+
stubWasConsumed = true;
|
|
27
|
+
return (cmd, opts = {}) => {
|
|
28
|
+
const attempt = calls.length;
|
|
29
|
+
calls.push(cmd);
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
behaviors[attempt]((err, stdout, stderr) => {
|
|
32
|
+
if (err) reject(err);
|
|
33
|
+
else resolve({ stdout, stderr });
|
|
34
|
+
}, attempt);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return originalPromisify.call(util, fn);
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
calls,
|
|
42
|
+
restore: () => { util.promisify = originalPromisify; },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Node couples exec to promisify via a custom symbol; if a future runtime drops
|
|
47
|
+
// that coupling the name-based stub above silently stops intercepting and the
|
|
48
|
+
// real cmd.exe child runs. Each test flips this to true from inside its own
|
|
49
|
+
// stubbed load (never inherited from an earlier test), so afterEach can fail
|
|
50
|
+
// loudly instead of passing vacuously.
|
|
51
|
+
let stubWasConsumed = false;
|
|
52
|
+
|
|
53
|
+
describe('execWithTimeout empty-stream retry (#525 follow-up)', () => {
|
|
54
|
+
let stub = null;
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
if (stub) { stub.restore(); stub = null; }
|
|
57
|
+
// Fail loudly if the fn.name === 'exec' stub never intercepted this test's
|
|
58
|
+
// doctor load — that would mean Node dropped the exec/promisify custom-
|
|
59
|
+
// symbol coupling and the assertions ran against real cmd.exe children.
|
|
60
|
+
expect(stubWasConsumed, `stub did not intercept the doctor load in this test`).toBe(true);
|
|
61
|
+
stubWasConsumed = false;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
function freshDoctor() {
|
|
65
|
+
const doctorPath = require.resolve('../doctor.js');
|
|
66
|
+
delete require.cache[doctorPath];
|
|
67
|
+
return require(doctorPath).execWithTimeout;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
it('AC-DOCTOR-RETRY-01: retries an exit-0 empty-stdout probe and keeps its own timeout', async () => {
|
|
71
|
+
stub = stubExec([
|
|
72
|
+
(cb) => cb(null, '', ''), // the lost-stream failure mode
|
|
73
|
+
(cb) => cb(null, 'v24.19.0\n', ''), // the same probe, stream intact
|
|
74
|
+
]);
|
|
75
|
+
const execWithTimeout = freshDoctor();
|
|
76
|
+
const result = await execWithTimeout('node --version', { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
77
|
+
expect(stub.calls.length).toBe(2);
|
|
78
|
+
expect(result).toEqual({ stdout: 'v24.19.0', stderr: '' });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('AC-DOCTOR-RETRY-01: returns null after exhausting attempts on a persistently empty probe', async () => {
|
|
82
|
+
stub = stubExec([
|
|
83
|
+
(cb) => cb(null, '', ''),
|
|
84
|
+
(cb) => cb(null, '', ''),
|
|
85
|
+
(cb) => cb(null, '', ''),
|
|
86
|
+
(cb) => cb(null, 'unexpected fourth attempt', ''),
|
|
87
|
+
]);
|
|
88
|
+
const execWithTimeout = freshDoctor();
|
|
89
|
+
const result = await execWithTimeout('node --version', {});
|
|
90
|
+
// win32: 3 attempts then null; other platforms: single attempt, still null.
|
|
91
|
+
expect(result).toBeNull();
|
|
92
|
+
expect(stub.calls.length).toBe(process.platform === 'win32' ? 3 : 1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('AC-DOCTOR-RETRY-01: does not retry when the probe produced output', async () => {
|
|
96
|
+
stub = stubExec([(cb) => cb(null, 'git version 2.54.0\n', '')]);
|
|
97
|
+
const execWithTimeout = freshDoctor();
|
|
98
|
+
const result = await execWithTimeout('git --version', {});
|
|
99
|
+
expect(stub.calls.length).toBe(1);
|
|
100
|
+
expect(result.stdout).toContain('git version');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('AC-DOCTOR-RETRY-01: does not retry a failing command', async () => {
|
|
104
|
+
stub = stubExec([(cb) => cb(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }), '', '')]);
|
|
105
|
+
const execWithTimeout = freshDoctor();
|
|
106
|
+
const result = await execWithTimeout('definitely-not-a-tool --version', {});
|
|
107
|
+
expect(stub.calls.length).toBe(1);
|
|
108
|
+
expect(result).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('AC-DOCTOR-RETRY-01: attempts:1 opts a side-effecting call out of the retry', async () => {
|
|
112
|
+
stub = stubExec([
|
|
113
|
+
(cb) => cb(null, '', ''),
|
|
114
|
+
(cb) => cb(null, 'unexpected second attempt', ''),
|
|
115
|
+
]);
|
|
116
|
+
const execWithTimeout = freshDoctor();
|
|
117
|
+
const result = await execWithTimeout('some-mutating-command', { attempts: 1 });
|
|
118
|
+
expect(stub.calls.length).toBe(1);
|
|
119
|
+
expect(result).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('AC-DOCTOR-RETRY-01: the attempts option is consumed, not forwarded to exec', async () => {
|
|
123
|
+
let forwarded = null;
|
|
124
|
+
stub = stubExec([(cb) => { cb(null, 'ok\n', ''); }]);
|
|
125
|
+
// Capture what doctor hands to the promisified exec by re-stubbing with an
|
|
126
|
+
// options recorder: stubExec's behaviors receive only the callback, so
|
|
127
|
+
// assert forwarding through a second fresh module with a capturing wrapper.
|
|
128
|
+
const util = require('util');
|
|
129
|
+
const originalPromisify = util.promisify;
|
|
130
|
+
const execWithTimeout = (() => {
|
|
131
|
+
util.promisify = function (fn) {
|
|
132
|
+
if (fn && fn.name === 'exec') {
|
|
133
|
+
stubWasConsumed = true;
|
|
134
|
+
return (cmd, opts = {}) => {
|
|
135
|
+
forwarded = opts;
|
|
136
|
+
return Promise.resolve({ stdout: 'ok\n', stderr: '' });
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return originalPromisify.call(util, fn);
|
|
140
|
+
};
|
|
141
|
+
const doctorPath = require.resolve('../doctor.js');
|
|
142
|
+
delete require.cache[doctorPath];
|
|
143
|
+
const f = require(doctorPath).execWithTimeout;
|
|
144
|
+
util.promisify = originalPromisify;
|
|
145
|
+
return f;
|
|
146
|
+
})();
|
|
147
|
+
const result = await execWithTimeout('git --version', { attempts: 2, timeout: 999 });
|
|
148
|
+
expect(result.stdout).toBe('ok');
|
|
149
|
+
expect(forwarded.attempts).toBeUndefined();
|
|
150
|
+
expect(forwarded.timeout).toBe(999);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* leads with it. Behavior is exercised through an injected fake spawner,
|
|
7
7
|
* so no real interpreter configuration is assumed on the host (#469
|
|
8
8
|
* follow-up, Delphi walkthrough R1 MN-06).
|
|
9
|
-
* @covers AC-WINBASH-01
|
|
9
|
+
* @covers AC-WINBASH-01, AC-WINBASH-02
|
|
10
10
|
*/
|
|
11
11
|
const { describe, it, expect } = await import('vitest');
|
|
12
|
-
const { resolveWindowsBashCandidates } = require('../detect-deps.js');
|
|
12
|
+
const { resolveWindowsBashCandidates, checkBashVersion } = require('../detect-deps.js');
|
|
13
13
|
|
|
14
14
|
// The candidate loop reads `where bash` from the host; tests inject both the
|
|
15
15
|
// discovery runner and the probe spawner, so results are deterministic on any
|
|
@@ -39,10 +39,12 @@ function fakeSpawn(behaviors) {
|
|
|
39
39
|
|
|
40
40
|
describe('resolveWindowsBashCandidates (win32 authoritative bash)', () => {
|
|
41
41
|
it('AC-WINBASH-01: rejects a WSL stub whose $1 is empty and uname is Linux', () => {
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
42
|
+
// The resolver must find nothing rather than settle for the stub — and on
|
|
43
|
+
// current Windows the System32 stub is skipped by name without even being
|
|
44
|
+
// spawned (cold-launch stall); this world uses a non-System32 stub to keep
|
|
45
|
+
// testing the output-based rejection path itself.
|
|
46
|
+
const spawn = fakeSpawn({ 'C:\\wsl-stub\\bash.exe': '[ ]\nLinux\n' });
|
|
47
|
+
expect(resolveWindowsBashCandidates(spawn, fakeWhere('C:\\wsl-stub\\bash.exe\r\n'))).toBeNull();
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
it('AC-WINBASH-01: accepts the first candidate with argv round-trip + MINGW uname', () => {
|
|
@@ -52,7 +54,24 @@ describe('resolveWindowsBashCandidates (win32 authoritative bash)', () => {
|
|
|
52
54
|
});
|
|
53
55
|
const result = resolveWindowsBashCandidates(spawn, fakeWhere('C:\\a\\bash.exe\r\nC:\\b\\bash.exe\r\n'));
|
|
54
56
|
expect(result).toBe('C:\\b\\bash.exe');
|
|
55
|
-
|
|
57
|
+
// Known Git paths are probed BEFORE PATH discoveries (cold WSL launches
|
|
58
|
+
// stall the event loop for seconds), so a/b keep their relative order but
|
|
59
|
+
// trail the hardcoded fallbacks.
|
|
60
|
+
expect(spawn.probed.slice(-2)).toEqual(['C:\\a\\bash.exe', 'C:\\b\\bash.exe']);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('AC-WINBASH-02: probes known Git paths before PATH discoveries', () => {
|
|
64
|
+
// Regression for the doctor fan-out starvation: System32\bash.exe leads
|
|
65
|
+
// `where bash` on WSL machines and its cold launch blocks ~3.4s. It must
|
|
66
|
+
// never be spawned at all — neither from where-output nor as any fallback.
|
|
67
|
+
const spawn = fakeSpawn({
|
|
68
|
+
'C:\\Program Files\\Git\\bin\\bash.exe': '[XPGATE_ARGV_PROBE]\nMSYS_NT-10.0\n',
|
|
69
|
+
});
|
|
70
|
+
const { result } = (() => ({
|
|
71
|
+
result: resolveWindowsBashCandidates(spawn, fakeWhere('C:\\Windows\\System32\\bash.exe\r\n')),
|
|
72
|
+
}))();
|
|
73
|
+
expect(result).toBe('C:\\Program Files\\Git\\bin\\bash.exe');
|
|
74
|
+
expect(spawn.probed).not.toContain('C:\\Windows\\System32\\bash.exe');
|
|
56
75
|
});
|
|
57
76
|
|
|
58
77
|
it('AC-WINBASH-01: a Linux uname with intact argv still fails the platform test', () => {
|
|
@@ -70,4 +89,35 @@ describe('resolveWindowsBashCandidates (win32 authoritative bash)', () => {
|
|
|
70
89
|
const result = resolveWindowsBashCandidates(spawn, fakeWhere(new Error('where unavailable')));
|
|
71
90
|
expect(result).toBe('C:\\msys64\\usr\\bin\\bash.exe');
|
|
72
91
|
});
|
|
92
|
+
|
|
93
|
+
it('AC-WINBASH-02: WSL stubs are skipped by name regardless of separators or case', () => {
|
|
94
|
+
// The filter normalizes slashes and ignores case/drive: forward-slash and
|
|
95
|
+
// lowercase variants of the same stub must still produce NO spawn call.
|
|
96
|
+
// The AppX alias lives under <Windows>\SystemApps\Microsoft.WSL_* — a
|
|
97
|
+
// user's own install merely NAMED ...\WindowsApps\... is NOT the stub and
|
|
98
|
+
// must still be probed (output-based rejection covers it).
|
|
99
|
+
const spawn = fakeSpawn({});
|
|
100
|
+
resolveWindowsBashCandidates(spawn, fakeWhere(
|
|
101
|
+
'c:/windows/sysnative/bash.exe\r\nC:\\WINDOWS\\System32\\bash.exe\r\nC:\\Windows\\SysWOW64\\bash.exe\r\nC:\\Users\\me\\AppData\\Local\\Microsoft\\WindowsApps\\bash.exe\r\n'
|
|
102
|
+
));
|
|
103
|
+
expect(spawn.probed.filter(p => /system32|sysnative|syswow64|appdata\\local\\microsoft\\windowsapps/i.test(p.replace(/\//g, '\\')))).toEqual([]);
|
|
104
|
+
const userOwned = fakeSpawn({});
|
|
105
|
+
resolveWindowsBashCandidates(userOwned, fakeWhere('D:\\Tools\\WindowsApps\\bash.exe\r\n'));
|
|
106
|
+
expect(userOwned.probed).toContain('D:\\Tools\\WindowsApps\\bash.exe');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('AC-WINBASH-01: checkBashVersion returns the bash VERSION tuple, not the uname line', () => {
|
|
110
|
+
// Contract regression (walkthrough R5 MJ-01/FC-02): doctor prints "GNU
|
|
111
|
+
// bash, version <field>" — the field must be the version number.
|
|
112
|
+
const spawn = (file, args) => ({
|
|
113
|
+
stdout: 'MINGW64_NT-10.0-26300\nGNU bash, version 5.3.9(1)-release (x86_64-pc-cygwin)\n',
|
|
114
|
+
});
|
|
115
|
+
expect(checkBashVersion(spawn, 'C:\\bash.exe')).toBe('5.3.9(1)-release');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('AC-WINBASH-01: checkBashVersion degrades to unknown, never a crash, on odd output', () => {
|
|
119
|
+
expect(checkBashVersion(() => ({ stdout: '' }), 'C:\\bash.exe')).toBe('unknown');
|
|
120
|
+
expect(checkBashVersion(() => ({ stdout: 'MINGW64_NT-10.0\n' }), 'C:\\bash.exe')).toBe('unknown');
|
|
121
|
+
expect(checkBashVersion(() => { throw new Error('spawn failed'); }, 'C:\\bash.exe')).toBe('unknown');
|
|
122
|
+
});
|
|
73
123
|
});
|
package/lib/detect-deps.js
CHANGED
|
@@ -296,10 +296,14 @@ function probeWindowsBash() {
|
|
|
296
296
|
function resolveWindowsBashCandidates(spawnProbe, whereExecSync) {
|
|
297
297
|
const _execSync = whereExecSync || execSync;
|
|
298
298
|
const candidates = [];
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
299
|
+
// Well-known interpreters FIRST. Two reasons, both measured on win32:
|
|
300
|
+
// 1. A cold WSL launch (System32\bash.exe leads `where bash` when WSL is
|
|
301
|
+
// installed) blocks its spawnSync for ~3.4s. checkBash runs inside
|
|
302
|
+
// doctor's parallel fan-out, and that synchronous stall starves the
|
|
303
|
+
// event loop past EXEC_TIMEOUT_MS — node/git then falsely report
|
|
304
|
+
// "Not found" while spawning them standalone takes ~300ms.
|
|
305
|
+
// 2. The known Git paths ARE the answer we are looking for; probing them
|
|
306
|
+
// first also skips pointless launches of stubs we would reject anyway.
|
|
303
307
|
candidates.push(
|
|
304
308
|
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
305
309
|
'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
|
|
@@ -308,6 +312,33 @@ function resolveWindowsBashCandidates(spawnProbe, whereExecSync) {
|
|
|
308
312
|
'C:\\msys64\\usr\\bin\\bash.exe',
|
|
309
313
|
'C:\\cygwin64\\bin\\bash.exe'
|
|
310
314
|
);
|
|
315
|
+
try {
|
|
316
|
+
const found = _execSync('where bash', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
317
|
+
for (const line of found.split(/\r?\n/)) {
|
|
318
|
+
const p = line.trim();
|
|
319
|
+
// Skip the WSL launcher outright: uname would reject it, and a cold
|
|
320
|
+
// launch costs seconds (see above). WindowsApps\bash.exe is the same
|
|
321
|
+
// WSL stub family (it just errors differently when WSL is absent).
|
|
322
|
+
if (!p) continue;
|
|
323
|
+
// Match on normalized separators: forward slashes, mixed case, or a
|
|
324
|
+
// non-C drive would otherwise slip a WSL stub past the filter and back
|
|
325
|
+
// into the multi-second cold-launch stall.
|
|
326
|
+
const norm = p.replace(/\//g, '\\');
|
|
327
|
+
// The WSL launcher appears as System32/Sysnative/SysWOW64\bash.exe, the
|
|
328
|
+
// AppX alias <Windows>\SystemApps\Microsoft.WSL_*\bash.exe, and the
|
|
329
|
+
// per-user AppX execution alias ...\AppData\Local\Microsoft\WindowsApps\
|
|
330
|
+
// bash.exe. A user's own install merely NAMED ...\WindowsApps\... (not
|
|
331
|
+
// under Microsoft\) is NOT the stub and stays in the list.
|
|
332
|
+
if (/\\windowsapps\\bash\.exe$/i.test(norm)
|
|
333
|
+
&& (/\\systemapps\\/i.test(norm) || /\\appdata\\local\\microsoft\\windowsapps\\/i.test(norm))) continue;
|
|
334
|
+
// The WSL stub lives in the matching-arch System32 view: 64-bit
|
|
335
|
+
// C:\Windows\System32\bash.exe AND the 32-bit emulation redirect target
|
|
336
|
+
// C:\Windows\SysWOW64\bash.exe (where reports either depending on the
|
|
337
|
+
// bitness of the caller).
|
|
338
|
+
if (/\\windows\\system32\\bash\.exe$/i.test(norm) || /\\windows\\sysnative\\bash\.exe$/i.test(norm) || /\\windows\\syswow64\\bash\.exe$/i.test(norm)) continue;
|
|
339
|
+
if (!candidates.includes(p)) candidates.push(p);
|
|
340
|
+
}
|
|
341
|
+
} catch { /* where unavailable */ }
|
|
311
342
|
for (const candidate of candidates) {
|
|
312
343
|
try {
|
|
313
344
|
const r = spawnProbe(candidate, ['-c', 'echo "[$1]"; uname -s', 'sh', 'XPGATE_ARGV_PROBE'], {
|
|
@@ -327,22 +358,46 @@ function resolveWindowsBashCandidates(spawnProbe, whereExecSync) {
|
|
|
327
358
|
/**
|
|
328
359
|
* Check if bash is available on the system.
|
|
329
360
|
* XP-Gate hooks are bash scripts — Windows users need Git Bash installed.
|
|
330
|
-
* @returns {{ok: boolean, path?: string, message?: string}}
|
|
361
|
+
* @returns {{ok: boolean, path?: string, version?: string, message?: string}}
|
|
331
362
|
*/
|
|
363
|
+
// Split out for behavior tests: checkBashVersion takes the resolved bash path
|
|
364
|
+
// and a spawner so the version-field contract (a bash VERSION tuple, not the
|
|
365
|
+
// uname string) is assertable without a real interpreter on the host.
|
|
366
|
+
function checkBashVersion(spawnProbe, winBash) {
|
|
367
|
+
let version = 'unknown';
|
|
368
|
+
try {
|
|
369
|
+
// --version through the SAME probe-style argv as the candidate loop:
|
|
370
|
+
// Git Bash's login startup (`bash --version`) costs ~1.7s, and checkBash
|
|
371
|
+
// sits on hot paths (doctor fan-out, every gate-runner launch) where a
|
|
372
|
+
// synchronous stall starves concurrent execs past their timeouts. The
|
|
373
|
+
// -c form is non-login (~300ms). uname proves the MSYS family AND bash
|
|
374
|
+
// --version inside it still yields the real bash version number — the
|
|
375
|
+
// `version` field must stay a bash version, not the uname string. No
|
|
376
|
+
// sed in the command: the parse lives in JS below, so a stripped MSYS
|
|
377
|
+
// install without sed cannot silently degrade to "unknown".
|
|
378
|
+
// NOTE the cp. prefix: this module binds only execSync at file scope,
|
|
379
|
+
// and checkBash sits outside probeWindowsBash's local spawnSync binding
|
|
380
|
+
// — a bare spawnSync here is a ReferenceError swallowed by the catch,
|
|
381
|
+
// which used to pin the detail line at "unknown" forever.
|
|
382
|
+
const result = spawnProbe(winBash, ['-c', 'uname -s; bash --version', 'sh'], { encoding: 'utf8', timeout: 5000 });
|
|
383
|
+
const out = String((result && result.stdout) || '').trim();
|
|
384
|
+
// Keep the field in the same shape checkEnv prints for every other tool
|
|
385
|
+
// ("GNU bash, version X.Y.Z(...)"): doctor prefixes "GNU bash, version ",
|
|
386
|
+
// so hand back the bare version tuple, not the whole line.
|
|
387
|
+
const versionLine = out.split('\n').find(line => /version/.test(line));
|
|
388
|
+
const match = versionLine && versionLine.match(/version\s+(\S+)/);
|
|
389
|
+
if (match) version = match[1];
|
|
390
|
+
} catch { /* keep unknown */ }
|
|
391
|
+
return version;
|
|
392
|
+
}
|
|
393
|
+
|
|
332
394
|
function checkBash() {
|
|
333
395
|
// On Windows resolve explicitly: PATH may lead with the WSL System32 stub,
|
|
334
396
|
// which cannot execute these hooks (wrong filesystem view, dropped $1).
|
|
335
397
|
if (process.platform === 'win32') {
|
|
336
398
|
const winBash = probeWindowsBash();
|
|
337
399
|
if (winBash) {
|
|
338
|
-
|
|
339
|
-
try {
|
|
340
|
-
// spawnSync with discrete argv — never interpolate a discovered path
|
|
341
|
-
// into a shell string (a PATH dir name may carry cmd metacharacters).
|
|
342
|
-
const result = spawnSync(winBash, ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
343
|
-
const versionMatch = String(result.stdout || '').match(/version\s+([^\s]+)/);
|
|
344
|
-
if (versionMatch) version = versionMatch[1];
|
|
345
|
-
} catch { /* keep unknown */ }
|
|
400
|
+
const version = checkBashVersion((...args) => require('child_process').spawnSync(...args), winBash);
|
|
346
401
|
return { ok: true, path: winBash, version };
|
|
347
402
|
}
|
|
348
403
|
return {
|
|
@@ -567,6 +622,7 @@ function compareVersions(a, b) {
|
|
|
567
622
|
module.exports = {
|
|
568
623
|
checkDeps,
|
|
569
624
|
checkBash,
|
|
625
|
+
checkBashVersion,
|
|
570
626
|
resolveWindowsBashCandidates,
|
|
571
627
|
autoInstallDeps,
|
|
572
628
|
detectPlatform,
|
package/lib/doctor.js
CHANGED
|
@@ -104,18 +104,52 @@ function noteSandboxAttribution(err) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
async function execWithTimeout(cmd, opts = {}) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
// Win32 false-negative guard. Under cmd.exe (exec's shell), a spawned probe
|
|
108
|
+
// can exit 0 while its stdout never reaches us — measured on this machine:
|
|
109
|
+
// `node --version` returns exit 0 with an EMPTY stream when many spawns are
|
|
110
|
+
// in flight at once (doctor's parallel fan-out), while the same command
|
|
111
|
+
// standalone prints fine in ~300ms. The old code read that empty stream as
|
|
112
|
+
// "Not found" and red-flagged Node/Git/hooksPath. A version/config probe is
|
|
113
|
+
// idempotent, so quiet retries distinguish the lost stream from a tool that
|
|
114
|
+
// genuinely fails. Contract: every caller here runs an IDEMPOTENT probe
|
|
115
|
+
// (`--version`, `config --get`, `rev-parse`) — a future side-effecting call
|
|
116
|
+
// site must pass { attempts: 1 } to opt out of the retry below. The option
|
|
117
|
+
// is consumed here and never forwarded to exec.
|
|
118
|
+
// Scope note: only the exit-0-with-empty-stream shape is retried. A probe
|
|
119
|
+
// that exits non-zero is a REAL failure verdict (cmd.exe reports a missing
|
|
120
|
+
// tool as exit 1 + "'x' is not recognized"), and re-running it would only
|
|
121
|
+
// burn time, so that path still returns null immediately.
|
|
122
|
+
const { attempts, ...execOpts } = opts;
|
|
123
|
+
const maxAttempts = process.platform === 'win32' ? (attempts || 3) : 1;
|
|
124
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
125
|
+
try {
|
|
126
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
127
|
+
...execOpts,
|
|
128
|
+
timeout: execOpts.timeout || EXEC_TIMEOUT_MS,
|
|
129
|
+
});
|
|
130
|
+
if (!stdout.trim() && !stderr.trim()) {
|
|
131
|
+
// Exit-0-but-empty: nothing usable came back. Retry while attempts
|
|
132
|
+
// remain; report null once they are exhausted, so every caller's
|
|
133
|
+
// existing null check ("probe produced nothing") applies — an empty
|
|
134
|
+
// non-null result would read as a present tool with no version.
|
|
135
|
+
if (attempt + 1 < maxAttempts) {
|
|
136
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return { stdout: stdout.trim(), stderr: stderr.trim() };
|
|
142
|
+
} catch (err) {
|
|
143
|
+
// #525: a bare null here reads as "tool missing" and has already cost a
|
|
144
|
+
// misdiagnosis round. Name the sandbox once when it is the sandbox.
|
|
145
|
+
noteSandboxAttribution(err);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
118
148
|
}
|
|
149
|
+
// Exhausted all attempts with exit-0-but-empty streams: report null so every
|
|
150
|
+
// caller's existing null check ("probe produced nothing usable") applies —
|
|
151
|
+
// an empty non-null result would read as a present tool with no version.
|
|
152
|
+
return null;
|
|
119
153
|
}
|
|
120
154
|
|
|
121
155
|
/**
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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.27.
|
|
3
|
+
"version": "0.27.1",
|
|
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,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/plugins/dsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.1",
|
|
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",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Qoder. 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,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.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
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0b556f5f
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.27.
|
|
6
|
+
**Version:** 0.27.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|