@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.
Files changed (81) hide show
  1. package/adapters/java.sh +373 -27
  2. package/gate-3.sh +51 -3
  3. package/gate-4.sh +32 -1
  4. package/hooks/gate-3.sh +51 -3
  5. package/hooks/gate-4.sh +32 -1
  6. package/hooks/pre-commit +275 -36
  7. package/lib/__tests__/doctor-spawn-capability.test.js +116 -0
  8. package/lib/__tests__/doctor-tui-skill-freshness.test.js +49 -10
  9. package/lib/doctor-tui.js +21 -3
  10. package/lib/doctor.js +111 -8
  11. package/lib/gate-audit.ts +3 -0
  12. package/lib/mattpocock-license-verdict.json +1 -1
  13. package/mock-policy/AGENTS.md +2 -2
  14. package/mutation/AGENTS.md +2 -2
  15. package/package.json +1 -1
  16. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  17. package/plugins/claude-code/skills/admin-template-guidelines/SKILL.md +1 -0
  18. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +1 -0
  19. package/plugins/claude-code/skills/clipboard-vision/SKILL.md +1 -0
  20. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  21. package/plugins/claude-code/skills/domain-modeling/SKILL.md +1 -0
  22. package/plugins/claude-code/skills/grill-with-docs/SKILL.md +1 -0
  23. package/plugins/claude-code/skills/grilling/SKILL.md +1 -0
  24. package/plugins/claude-code/skills/improve-codebase-architecture/SKILL.md +1 -0
  25. package/plugins/claude-code/skills/ralph-loop/SKILL.md +1 -0
  26. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  27. package/plugins/claude-code/skills/test-driven-development/SKILL.md +1 -0
  28. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  29. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +1 -0
  30. package/plugins/claude-code/skills/to-issues/SKILL.md +1 -0
  31. package/plugins/dsh/package.json +1 -1
  32. package/plugins/dsh/skills/admin-template-guidelines/SKILL.md +1 -0
  33. package/plugins/dsh/skills/batch-grill-me/SKILL.md +1 -0
  34. package/plugins/dsh/skills/clipboard-vision/SKILL.md +1 -0
  35. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  36. package/plugins/dsh/skills/domain-modeling/SKILL.md +1 -0
  37. package/plugins/dsh/skills/grill-with-docs/SKILL.md +1 -0
  38. package/plugins/dsh/skills/grilling/SKILL.md +1 -0
  39. package/plugins/dsh/skills/improve-codebase-architecture/SKILL.md +1 -0
  40. package/plugins/dsh/skills/ralph-loop/SKILL.md +1 -0
  41. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  42. package/plugins/dsh/skills/test-driven-development/SKILL.md +1 -0
  43. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  44. package/plugins/dsh/skills/test-specification-alignment/SKILL.md +1 -0
  45. package/plugins/dsh/skills/to-issues/SKILL.md +1 -0
  46. package/plugins/opencode/package.json +1 -1
  47. package/plugins/opencode/skills/admin-template-guidelines/SKILL.md +1 -0
  48. package/plugins/opencode/skills/batch-grill-me/SKILL.md +1 -0
  49. package/plugins/opencode/skills/clipboard-vision/SKILL.md +1 -0
  50. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  51. package/plugins/opencode/skills/domain-modeling/SKILL.md +1 -0
  52. package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
  53. package/plugins/opencode/skills/grilling/SKILL.md +1 -0
  54. package/plugins/opencode/skills/improve-codebase-architecture/SKILL.md +1 -0
  55. package/plugins/opencode/skills/ralph-loop/SKILL.md +1 -0
  56. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  57. package/plugins/opencode/skills/test-driven-development/SKILL.md +1 -0
  58. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  59. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +1 -0
  60. package/plugins/opencode/skills/to-issues/SKILL.md +1 -0
  61. package/plugins/qoder/plugin.json +1 -1
  62. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  63. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  64. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  65. package/principles/AGENTS.md +2 -2
  66. package/principles/adapters/__tests__/java.test.ts +317 -0
  67. package/principles/adapters/java.ts +325 -13
  68. package/skills/admin-template-guidelines/SKILL.md +1 -0
  69. package/skills/batch-grill-me/SKILL.md +1 -0
  70. package/skills/clipboard-vision/SKILL.md +1 -0
  71. package/skills/delphi-review/AGENTS.md +2 -2
  72. package/skills/domain-modeling/SKILL.md +1 -0
  73. package/skills/grill-with-docs/SKILL.md +1 -0
  74. package/skills/grilling/SKILL.md +1 -0
  75. package/skills/improve-codebase-architecture/SKILL.md +1 -0
  76. package/skills/ralph-loop/SKILL.md +1 -0
  77. package/skills/sprint-flow/AGENTS.md +2 -2
  78. package/skills/test-driven-development/SKILL.md +1 -0
  79. package/skills/test-specification-alignment/AGENTS.md +2 -2
  80. package/skills/test-specification-alignment/SKILL.md +1 -0
  81. 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
- it('treats a versionless skill as up to date when only line endings differ', () => {
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
- // This is the branch the issue hit for 11 of 12 skills: no version field at all,
78
- // so freshness can only be decided by content.
79
- expect(bundled).not.toMatch(/^version:/m);
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 Outdated,
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('grilling', bundled.replace(/\r\n/g, '\n').replace(/\n/g, '\r'));
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 Outdated for a real content difference (anti-vacuity)', () => {
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
- if (!sameContentIgnoringLineEndings(bundledContent, userContent)) {
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: 'Outdated — run xp-gate update-skill --all',
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
- const statusSymbol = check.status === 'PASS' ? ' ✓' : check.status === 'WARN' ? ' ⚠' : ' ✗';
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 {
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-10-08T19:51:29.290Z",
5
+ "checked_at": "2026-10-12T10:19:27.219Z",
6
6
  "source": "network"
7
7
  }
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-08
3
+ **Generated:** 2026-10-12
4
4
  **Commit:** unknown
5
5
  **Branch:** unknown
6
- **Version:** 0.20.1.0
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.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-08
3
+ **Generated:** 2026-10-12
4
4
  **Commit:** unknown
5
5
  **Branch:** unknown
6
- **Version:** 0.20.1.0
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": "@boyingliu01/xp-gate",
3
- "version": "0.20.1",
3
+ "version": "0.20.2",
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.20.1",
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,5 +1,6 @@
1
1
  ---
2
2
  name: batch-grill-me
3
+ version: 1.0.0
3
4
  description: A relentless interview that asks every frontier question at once, round by round.
4
5
  disable-model-invocation: true
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: clipboard-vision
3
+ version: 1.0.0
3
4
  description: Use when the user explicitly asks to analyze, describe, OCR, or inspect a single image currently stored in the system clipboard.
4
5
  ---
5
6
 
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-08
3
+ **Generated:** 2026-10-12
4
4
  **Commit:** unknown
5
5
  **Branch:** unknown
6
- **Version:** 0.20.1.0
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: grill-with-docs
3
+ version: 1.0.0
3
4
  description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.
4
5
  disable-model-invocation: true
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: grilling
3
+ version: 1.0.0
3
4
  description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
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,5 +1,6 @@
1
1
  ---
2
2
  name: ralph-loop
3
+ version: 1.0.0
3
4
  description: Use when executing Sprint-Flow Phase 2 BUILD, processing one REQ, building the next requirement, iterating with clean context, or requiring full regression per requirement.
4
5
  maturity: stable
5
6
  ---
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-08
3
+ **Generated:** 2026-10-12
4
4
  **Commit:** unknown
5
5
  **Branch:** unknown
6
- **Version:** 0.20.1.0
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,5 +1,6 @@
1
1
  ---
2
2
  name: test-driven-development
3
+ version: 1.0.0
3
4
  description: Use when implementing any feature or bugfix, before writing implementation code
4
5
  ---
5
6
 
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-08
3
+ **Generated:** 2026-10-12
4
4
  **Commit:** unknown
5
5
  **Branch:** unknown
6
- **Version:** 0.20.1.0
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: test-specification-alignment
3
+ version: 1.0.0
3
4
  description: "Use when asked to run tests, verify tests, align tests with specification.yaml, before BUILD verification, or before release/ship."
4
5
  ---
5
6
 
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.20.1",
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",