@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.
Files changed (28) hide show
  1. package/lib/__tests__/doctor-exec-retry.test.js +152 -0
  2. package/lib/__tests__/windows-bash-probe.test.js +57 -7
  3. package/lib/detect-deps.js +69 -13
  4. package/lib/doctor.js +45 -11
  5. package/lib/mattpocock-license-verdict.json +1 -1
  6. package/mock-policy/AGENTS.md +2 -2
  7. package/mutation/AGENTS.md +2 -2
  8. package/package.json +1 -1
  9. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  10. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  11. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  12. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  13. package/plugins/dsh/package.json +1 -1
  14. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  15. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  16. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  17. package/plugins/opencode/package.json +1 -1
  18. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  19. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  20. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  21. package/plugins/qoder/plugin.json +1 -1
  22. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  23. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  24. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  25. package/principles/AGENTS.md +2 -2
  26. package/skills/delphi-review/AGENTS.md +2 -2
  27. package/skills/sprint-flow/AGENTS.md +2 -2
  28. 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
- const spawn = fakeSpawn({ 'C:\\Windows\\System32\\bash.exe': '[ ]\nLinux\n' });
43
- // No MSYS candidate exists in this world (hardcoded fallbacks report no
44
- // stdout), so the resolver must find nothing rather than settle for the stub.
45
- expect(resolveWindowsBashCandidates(spawn, fakeWhere('C:\\Windows\\System32\\bash.exe\r\n'))).toBeNull();
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
- expect(spawn.probed.slice(0, 2)).toEqual(['C:\\a\\bash.exe', 'C:\\b\\bash.exe']);
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
  });
@@ -296,10 +296,14 @@ function probeWindowsBash() {
296
296
  function resolveWindowsBashCandidates(spawnProbe, whereExecSync) {
297
297
  const _execSync = whereExecSync || execSync;
298
298
  const candidates = [];
299
- try {
300
- const found = _execSync('where bash', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
301
- for (const line of found.split(/\r?\n/)) if (line.trim()) candidates.push(line.trim());
302
- } catch { /* where unavailable */ }
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
- let version = 'unknown';
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
- try {
108
- const { stdout, stderr } = await execAsync(cmd, {
109
- ...opts,
110
- timeout: opts.timeout || EXEC_TIMEOUT_MS,
111
- });
112
- return { stdout: stdout.trim(), stderr: stderr.trim() };
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);
117
- return null;
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
  /**
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-10-16T13:39:44.004Z",
5
+ "checked_at": "2026-10-16T16:19:41.473Z",
6
6
  "source": "network"
7
7
  }
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-16
4
- **Commit:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-16
4
- **Commit:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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": "@boyingliu01/xp-gate",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "./bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.27.0",
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.27.0",
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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": "@boyingliu01/opencode-plugin",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-16
4
- **Commit:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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.0",
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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
  # PRINCIPLES CHECKER MODULE
2
2
 
3
3
  **Generated:** 2026-10-16
4
- **Commit:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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:** 429f73c5
4
+ **Commit:** 0b556f5f
5
5
  **Branch:** main
6
- **Version:** 0.27.0.0
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.