@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.
- package/adapter-common.sh +78 -36
- package/adapters/csharp.sh +245 -0
- package/adapters/gate-3.sh +1 -1
- package/adapters/gate-4.sh +1 -1
- package/adapters/gate-9.sh +37 -2
- package/adapters/go.sh +6 -11
- package/adapters/iac.sh +45 -5
- package/adapters/python.sh +6 -11
- package/bin/xp-gate.js +18 -11
- package/gate-10.sh +18 -3
- package/gate-13-compiler-strictness.sh +181 -0
- package/gate-3.sh +1 -1
- package/gate-4.sh +1 -1
- package/gate-9.sh +37 -2
- package/gates/common.ts +9 -2
- package/hooks/adapter-common.sh +78 -36
- package/hooks/gate-10.sh +18 -3
- package/hooks/gate-13-compiler-strictness.sh +181 -0
- package/hooks/gate-3.sh +1 -1
- package/hooks/gate-4.sh +1 -1
- package/hooks/gate-9.sh +37 -2
- package/hooks/gate-registry.sh +161 -0
- package/hooks/lib/test-failure.sh +29 -4
- package/hooks/pre-commit +122 -3
- package/lib/__tests__/agent-runtime-journal.test.js +42 -18
- package/lib/__tests__/check-single-file.test.js +55 -4
- package/lib/__tests__/detect-deps-dsh-profile.test.js +43 -0
- package/lib/__tests__/doctor-delphi.test.js +178 -3
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +3 -3
- package/lib/__tests__/install-skill-full-dir.test.js +45 -2
- package/lib/__tests__/install-skill.test.js +191 -23
- package/lib/__tests__/mirror-sync.test.js +23 -5
- package/lib/__tests__/uninstall-global-residue.test.js +394 -9
- package/lib/__tests__/update-skill-flags.test.js +78 -0
- package/lib/__tests__/update-skill.test.js +334 -57
- package/lib/detect-deps.js +24 -0
- package/lib/doctor.js +80 -12
- package/lib/install-skill.js +85 -21
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/uninstall.js +143 -12
- package/lib/update-skill.js +239 -28
- package/mock-policy/AGENTS.md +4 -4
- package/mutation/AGENTS.md +4 -4
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/lib/index.d.ts.map +1 -1
- package/plugins/dsh/lib/index.js +7 -4
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/dsh/src/index.test.ts +25 -0
- package/plugins/dsh/src/index.ts +7 -4
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/principles/adapters/csharp.ts +309 -0
- package/principles/analyzer.ts +5 -0
- package/scripts/delphi-external-review.cjs +130 -101
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/test-specification-alignment/AGENTS.md +4 -4
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @test
|
|
3
|
-
* @intent Verify installSkill() handles deps check, bundle lookup, config, backup, and errors
|
|
4
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
165
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|