@boyingliu01/xp-gate 0.35.23 → 0.36.0

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 (71) hide show
  1. package/adapter-common.sh +78 -36
  2. package/adapters/csharp.sh +245 -0
  3. package/adapters/gate-3.sh +1 -1
  4. package/adapters/gate-4.sh +1 -1
  5. package/adapters/gate-9.sh +37 -2
  6. package/adapters/go.sh +6 -11
  7. package/adapters/iac.sh +45 -5
  8. package/adapters/python.sh +6 -11
  9. package/bin/xp-gate.js +18 -11
  10. package/gate-10.sh +18 -3
  11. package/gate-13-compiler-strictness.sh +181 -0
  12. package/gate-3.sh +1 -1
  13. package/gate-4.sh +1 -1
  14. package/gate-9.sh +37 -2
  15. package/gates/common.ts +9 -2
  16. package/hooks/adapter-common.sh +78 -36
  17. package/hooks/gate-10.sh +18 -3
  18. package/hooks/gate-13-compiler-strictness.sh +181 -0
  19. package/hooks/gate-3.sh +1 -1
  20. package/hooks/gate-4.sh +1 -1
  21. package/hooks/gate-9.sh +37 -2
  22. package/hooks/gate-registry.sh +161 -0
  23. package/hooks/lib/test-failure.sh +29 -4
  24. package/hooks/pre-commit +122 -3
  25. package/lib/__tests__/agent-runtime-journal.test.js +42 -18
  26. package/lib/__tests__/check-single-file.test.js +55 -4
  27. package/lib/__tests__/detect-deps-dsh-profile.test.js +43 -0
  28. package/lib/__tests__/doctor-delphi.test.js +178 -3
  29. package/lib/__tests__/doctor-tui-skill-freshness.test.js +3 -3
  30. package/lib/__tests__/install-skill-full-dir.test.js +45 -2
  31. package/lib/__tests__/install-skill.test.js +191 -23
  32. package/lib/__tests__/mirror-sync.test.js +23 -5
  33. package/lib/__tests__/uninstall-global-residue.test.js +394 -9
  34. package/lib/__tests__/update-skill-flags.test.js +78 -0
  35. package/lib/__tests__/update-skill.test.js +334 -57
  36. package/lib/detect-deps.js +24 -0
  37. package/lib/doctor.js +80 -12
  38. package/lib/install-skill.js +85 -21
  39. package/lib/mattpocock-license-verdict.json +1 -1
  40. package/lib/uninstall.js +143 -12
  41. package/lib/update-skill.js +239 -28
  42. package/mock-policy/AGENTS.md +4 -4
  43. package/mutation/AGENTS.md +4 -4
  44. package/package.json +1 -1
  45. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  46. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  47. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  48. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  49. package/plugins/dsh/lib/index.d.ts.map +1 -1
  50. package/plugins/dsh/lib/index.js +7 -4
  51. package/plugins/dsh/package.json +1 -1
  52. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  53. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  54. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  55. package/plugins/dsh/src/index.test.ts +25 -0
  56. package/plugins/dsh/src/index.ts +7 -4
  57. package/plugins/opencode/package.json +1 -1
  58. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  59. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  60. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  61. package/plugins/qoder/plugin.json +1 -1
  62. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  63. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  64. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  65. package/principles/AGENTS.md +4 -4
  66. package/principles/adapters/csharp.ts +309 -0
  67. package/principles/analyzer.ts +5 -0
  68. package/scripts/delphi-external-review.cjs +130 -101
  69. package/skills/delphi-review/AGENTS.md +4 -4
  70. package/skills/sprint-flow/AGENTS.md +4 -4
  71. package/skills/test-specification-alignment/AGENTS.md +4 -4
@@ -1,7 +1,9 @@
1
1
  /**
2
- * @test install-skill
3
- * @intent Verify installSkill() handles deps check, bundle lookup, config, backup, and errors
4
- * @covers AC-416-01, AC-416-05
2
+ * @test REQ-416, REQ-725
3
+ * @intent Verify installSkill() handles deps check, bundle lookup, config, backup, and errors,
4
+ * and that it writes EVERY platform already carrying the skill rather than only the
5
+ * one detectPlatform() short-circuits to.
6
+ * @covers AC-416-01, AC-416-05, AC-725-06
5
7
  */
6
8
  const fs = require('fs');
7
9
  const path = require('path');
@@ -17,37 +19,60 @@ function listDirs(root) {
17
19
  .sort();
18
20
  }
19
21
 
20
- describe('install-skill', () => {
21
- let tmpHome, originalHome;
22
-
23
- beforeEach(() => {
24
- originalHome = process.env.HOME;
25
- tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'xpgate-in-'));
26
- process.env.HOME = tmpHome;
22
+ /**
23
+ * Shared fixture for the blocks that need a throwaway HOME and a clean module
24
+ * cache.
25
+ *
26
+ * install-skill reads caches HOME at module load, so every test has to reset
27
+ * four modules and silence three console channels. Two copies of that boilerplate
28
+ * tripped the clone detector, and the two copies had already drifted (one
29
+ * afterEach waited for a pending setImmediate, the other did not) -- which is
30
+ * exactly how a teardown race starts. The stricter teardown wins here.
31
+ */
32
+ function makeHomeFixture(prefix) {
33
+ const handle = { tmpHome: null, originalHome: null };
34
+ const MODULES = ['../install-skill', '../detect-deps', '../shared-paths', '../rollback'];
35
+
36
+ handle.dropModuleCache = () => {
37
+ for (const mod of MODULES) delete require.cache[require.resolve(mod)];
38
+ };
39
+
40
+ handle.beforeEach = () => {
41
+ handle.originalHome = process.env.HOME;
42
+ handle.tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
43
+ process.env.HOME = handle.tmpHome;
27
44
  vi.resetModules();
28
- for (const mod of ['../install-skill', '../detect-deps', '../shared-paths', '../rollback']) {
29
- delete require.cache[require.resolve(mod)];
30
- }
45
+ handle.dropModuleCache();
31
46
  vi.spyOn(console, 'log').mockImplementation(() => {});
32
47
  vi.spyOn(console, 'error').mockImplementation(() => {});
33
48
  vi.spyOn(console, 'warn').mockImplementation(() => {});
34
- });
49
+ };
35
50
 
36
- afterEach(async () => {
51
+ handle.afterEach = async () => {
37
52
  await new Promise((resolve) => setImmediate(resolve));
38
- process.env.HOME = originalHome;
53
+ process.env.HOME = handle.originalHome;
39
54
  vi.restoreAllMocks();
40
- if (fs.existsSync(tmpHome)) {
41
- fs.rmSync(tmpHome, { recursive: true, force: true });
55
+ if (fs.existsSync(handle.tmpHome)) {
56
+ fs.rmSync(handle.tmpHome, { recursive: true, force: true });
42
57
  }
43
- });
58
+ handle.dropModuleCache();
59
+ };
60
+
61
+ return handle;
62
+ }
63
+
64
+ describe('install-skill', () => {
65
+ const fx = makeHomeFixture('xpgate-in-');
66
+
67
+ beforeEach(fx.beforeEach);
68
+ afterEach(fx.afterEach);
44
69
 
45
70
  function skillsDir() {
46
- return path.join(tmpHome, '.config', 'opencode', 'skills');
71
+ return path.join(fx.tmpHome, '.config', 'opencode', 'skills');
47
72
  }
48
73
 
49
74
  function configDir() {
50
- return path.join(tmpHome, '.config', 'xp-gate');
75
+ return path.join(fx.tmpHome, '.config', 'xp-gate');
51
76
  }
52
77
 
53
78
  function setupValidDeps() {
@@ -161,8 +186,13 @@ describe('install-skill', () => {
161
186
  expect(fs.existsSync(configFile)).toBe(true);
162
187
  const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
163
188
  expect(config.installedSkills).toHaveProperty('delphi-review');
164
- // Read version dynamically from VERSION file — no hardcoded version to break on bump
165
- const expectedVersion = fs.readFileSync(path.join(__dirname, '..', '..', '..', '..', 'VERSION'), 'utf8').trim();
189
+ // Read the version dynamically so a bump never breaks this test. The source
190
+ // is package.json, not the repo's VERSION: that file is a release stamp
191
+ // ('0.35.23.0'), is not shipped in the npm package, and resolving it from
192
+ // __dirname only works inside a checkout (#713).
193
+ const expectedVersion = JSON.parse(
194
+ fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')
195
+ ).version;
166
196
  expect(config.installedSkills['delphi-review'].version).toBe(expectedVersion);
167
197
  expect(config.installedSkills['delphi-review'].installedAt).toMatch(/^\d{4}-/);
168
198
  });
@@ -246,3 +276,141 @@ describe('install-skill', () => {
246
276
  expect(result).toBe(0);
247
277
  });
248
278
  });
279
+
280
+ // ── #713: the "unknown skill" hint must not print a fake version ─────────────
281
+ // getCliVersion() resolved `../../../VERSION` relative to __dirname. In an
282
+ // installed package __dirname is
283
+ // `node_modules/@boyingliu01/xp-gate/lib`, so three levels up lands in
284
+ // `node_modules/` -- and the package does not ship a VERSION file at all. The
285
+ // catch therefore fed `0.0.0` to every caller that prints it, so a user seeing
286
+ //
287
+ // xp-gate 0.0.0 bundles: ...
288
+ //
289
+ // reasonably concluded their install was broken. The version has to come from
290
+ // the package.json the CLI is actually running out of.
291
+ describe('install-skill #713 CLI version reporting', () => {
292
+ const fx = makeHomeFixture('xpgate-ver-');
293
+
294
+ beforeEach(fx.beforeEach);
295
+ afterEach(fx.afterEach);
296
+
297
+ /**
298
+ * @test REQ-713 Honest version reporting
299
+ * @intent An unknown-skill error must name the real package version, never 0.0.0
300
+ * @covers AC-713-04
301
+ *
302
+ * The repo checkout masks this bug: `<repo>/VERSION` exists, so resolving
303
+ * `../../../VERSION` from `src/npm-package/lib/` lands on it and looks correct.
304
+ * An installed package has no such luck -- npm's `files` list does not include
305
+ * VERSION, and three levels up from `node_modules/@boyingliu01/xp-gate/lib` is
306
+ * `node_modules/`. So the assertion that matters is the one that holds in the
307
+ * shipped layout, which is measured here by demanding the value AGREE with
308
+ * package.json rather than with whatever VERSION happens to sit above the tree.
309
+ */
310
+ it('AC-713-04: unknown-skill error reports the package version, not 0.0.0 and not repo VERSION', async () => {
311
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8'));
312
+
313
+ // install-skill destructures checkDeps at require time, so a spy on the
314
+ // module object cannot intercept it. Inject a stub into the require cache
315
+ // BEFORE install-skill loads -- the deps gate runs ahead of the name check,
316
+ // and this test measures version reporting, not dependency probing.
317
+ const detectPath = require.resolve('../detect-deps.js');
318
+ require.cache[detectPath] = {
319
+ id: detectPath,
320
+ filename: detectPath,
321
+ loaded: true,
322
+ exports: {
323
+ ...require('../detect-deps.js'),
324
+ checkDeps: async () => ({ ok: true }),
325
+ },
326
+ };
327
+
328
+ const { installSkill } = require('../install-skill');
329
+ const code = await installSkill('definitely-not-a-bundled-skill', { force: true });
330
+ expect(code).toBe(1);
331
+
332
+ const said = console.error.mock.calls.map((c) => c.join(' ')).join('\n');
333
+ expect(said).toMatch(/Unknown skill/);
334
+ expect(said).not.toContain('0.0.0');
335
+ expect(said).toContain(`xp-gate ${pkg.version} bundles:`);
336
+ });
337
+
338
+ /**
339
+ * AC-725-06: a machine carrying the same skill on more than one platform must
340
+ * have every copy updated. `detectPlatform()` short-circuits to the first
341
+ * marker it finds, so the single-target path froze the other copy while
342
+ * reporting success -- and `doctor` then agreed, because both read the same
343
+ * short-circuited answer. Migration-era machines with Qoder AND OpenCode are
344
+ * the normal case, not an edge one.
345
+ */
346
+ it('AC-725-06: updates every deployed platform, not just the detected one', async () => {
347
+ // Two platforms carry the skill; detectPlatform() would only ever see one.
348
+ const qoderSkills = path.join(fx.tmpHome, '.qoder', 'skills');
349
+ const opencodeSkills = path.join(fx.tmpHome, '.config', 'opencode', 'skills');
350
+ const SKILL = 'sprint-flow';
351
+ for (const root of [qoderSkills, opencodeSkills]) {
352
+ fs.mkdirSync(path.join(root, SKILL), { recursive: true });
353
+ fs.writeFileSync(path.join(root, SKILL, 'SKILL.md'), '# stale copy\n');
354
+ }
355
+
356
+ const detectPath = require.resolve('../detect-deps.js');
357
+ require.cache[detectPath] = {
358
+ id: detectPath,
359
+ filename: detectPath,
360
+ loaded: true,
361
+ exports: {
362
+ ...require('../detect-deps.js'),
363
+ checkDeps: async () => ({ ok: true }),
364
+ },
365
+ };
366
+
367
+ const { installSkill } = require('../install-skill');
368
+ const code = await installSkill(SKILL, { force: true });
369
+ expect(code).toBe(0);
370
+
371
+ // Both copies must now carry the shipped content, not the stale fixture.
372
+ for (const root of [qoderSkills, opencodeSkills]) {
373
+ const installed = path.join(root, SKILL, 'SKILL.md');
374
+ expect(fs.existsSync(installed)).toBe(true);
375
+ const body = fs.readFileSync(installed, 'utf8');
376
+ expect(body).not.toBe('# stale copy\n');
377
+ expect(body.length).toBeGreaterThan(100);
378
+ }
379
+ });
380
+
381
+ it('AC-725-06: a platform whose copy cannot be read does not abort the others', async () => {
382
+ // Qoder's skills root is a FILE, so readdir throws ENOTDIR for it. That
383
+ // platform must be skipped, and OpenCode -- which is readable -- must still
384
+ // be updated. The prior single-target code never reached this case because
385
+ // detectPlatform() would have picked one platform and ignored the other.
386
+ const qoderSkills = path.join(fx.tmpHome, '.qoder', 'skills');
387
+ const opencodeSkills = path.join(fx.tmpHome, '.config', 'opencode', 'skills');
388
+ const SKILL = 'sprint-flow';
389
+ fs.mkdirSync(path.dirname(qoderSkills), { recursive: true });
390
+ fs.writeFileSync(qoderSkills, 'not a directory');
391
+ fs.mkdirSync(path.join(opencodeSkills, SKILL), { recursive: true });
392
+ fs.writeFileSync(path.join(opencodeSkills, SKILL, 'SKILL.md'), '# stale copy\n');
393
+
394
+ const detectPath = require.resolve('../detect-deps.js');
395
+ require.cache[detectPath] = {
396
+ id: detectPath,
397
+ filename: detectPath,
398
+ loaded: true,
399
+ exports: {
400
+ ...require('../detect-deps.js'),
401
+ checkDeps: async () => ({ ok: true }),
402
+ },
403
+ };
404
+
405
+ const { installSkill } = require('../install-skill');
406
+ const code = await installSkill(SKILL, { force: true });
407
+
408
+ // The readable platform must still have been written.
409
+ const healthy = path.join(opencodeSkills, SKILL, 'SKILL.md');
410
+ expect(fs.existsSync(healthy)).toBe(true);
411
+ expect(fs.readFileSync(healthy, 'utf8')).not.toBe('# stale copy\n');
412
+ // The unreadable root must not have been silently "updated" into a file.
413
+ expect(fs.statSync(qoderSkills).isFile()).toBe(true);
414
+ expect(code).toBe(0);
415
+ });
416
+ });
@@ -49,8 +49,15 @@ afterEach(() => {
49
49
  */
50
50
  function write(root, rel, content) {
51
51
  const full = path.join(root, rel);
52
- fs.mkdirSync(path.dirname(full), { recursive: true });
53
- fs.writeFileSync(full, content);
52
+ try {
53
+ fs.mkdirSync(path.dirname(full), { recursive: true });
54
+ fs.writeFileSync(full, content);
55
+ } catch (err) {
56
+ // Swallowing this would turn a broken fixture into a confusing assertion
57
+ // failure further down ("expected undefined"), so rethrow with the path that
58
+ // actually failed. The rule wants the IO handled, not hidden.
59
+ throw new Error(`failed to write fixture ${rel}: ${err instanceof Error ? err.message : String(err)}`);
60
+ }
54
61
  }
55
62
 
56
63
  /**
@@ -264,13 +271,24 @@ describe('syncMirrors — AC-713-04 safety', () => {
264
271
  it('preserves the executable bit so hooks stay runnable', () => {
265
272
  write(TMP_REPO, 'canonical/hook.sh', '#!/bin/sh\necho a\n');
266
273
  fs.chmodSync(path.join(TMP_REPO, 'canonical/hook.sh'), 0o755);
267
- write(TMP_REPO, 'mirror/hook.sh', '#!/bin/sh\necho a\n');
274
+ // The mirror must start as *drift*. The sync loop only touches files that
275
+ // are `missing` or `differing`, so identical content is skipped and the mode
276
+ // never gets rewritten. An earlier version gave both sides the same bytes,
277
+ // so the file never entered `toWrite`; on Linux the assertion then failed
278
+ // ("expected +0 to be truthy") while Windows skipped the whole block, which
279
+ // is how it stayed invisible locally. The test was measuring nothing.
280
+ write(TMP_REPO, 'mirror/hook.sh', '#!/bin/sh\necho stale\n');
268
281
  trackAll(TMP_REPO);
269
282
 
270
- syncMirrors(TMP_REPO, [{ canonical: 'canonical', mirror: 'mirror' }]);
271
- const mode = fs.statSync(path.join(TMP_REPO, 'mirror/hook.sh')).mode;
283
+ const result = syncMirrors(TMP_REPO, [{ canonical: 'canonical', mirror: 'mirror' }]);
284
+ // Prove the copy actually happened, so a green result cannot come from
285
+ // "nothing was touched".
286
+ expect(result.copied).toContain('hook.sh');
287
+ expect(fs.readFileSync(path.join(TMP_REPO, 'mirror/hook.sh'), 'utf8')).toBe('#!/bin/sh\necho a\n');
288
+
272
289
  // On Windows the exec bit is not meaningful; only assert where it applies.
273
290
  if (process.platform !== 'win32') {
291
+ const mode = fs.statSync(path.join(TMP_REPO, 'mirror/hook.sh')).mode;
274
292
  expect(mode & 0o111).toBeTruthy();
275
293
  }
276
294
  });