@boyingliu01/xp-gate 0.35.24 → 0.36.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 (70) hide show
  1. package/adapter-common.sh +55 -2
  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/iac.sh +45 -5
  7. package/bin/xp-gate.js +18 -11
  8. package/gate-10.sh +18 -3
  9. package/gate-13-compiler-strictness.sh +181 -0
  10. package/gate-3.sh +1 -1
  11. package/gate-4.sh +1 -1
  12. package/gate-9.sh +37 -2
  13. package/gates/common.ts +9 -2
  14. package/hooks/adapter-common.sh +55 -2
  15. package/hooks/gate-10.sh +18 -3
  16. package/hooks/gate-13-compiler-strictness.sh +181 -0
  17. package/hooks/gate-3.sh +1 -1
  18. package/hooks/gate-4.sh +1 -1
  19. package/hooks/gate-9.sh +37 -2
  20. package/hooks/gate-registry.sh +161 -0
  21. package/hooks/lib/test-failure.sh +29 -4
  22. package/hooks/pre-commit +122 -3
  23. package/lib/__tests__/agent-runtime-journal.test.js +42 -18
  24. package/lib/__tests__/check-single-file.test.js +55 -4
  25. package/lib/__tests__/detect-deps-dsh-profile.test.js +43 -0
  26. package/lib/__tests__/doctor-delphi.test.js +175 -0
  27. package/lib/__tests__/install-skill-full-dir.test.js +45 -2
  28. package/lib/__tests__/install-skill.test.js +84 -3
  29. package/lib/__tests__/mirror-sync.test.js +23 -5
  30. package/lib/__tests__/principles-csharp-coverage.test.js +227 -0
  31. package/lib/__tests__/uninstall-global-residue.test.js +192 -28
  32. package/lib/__tests__/update-skill-flags.test.js +78 -0
  33. package/lib/__tests__/update-skill.test.js +156 -3
  34. package/lib/detect-deps.js +24 -0
  35. package/lib/doctor.js +80 -12
  36. package/lib/install-skill.js +67 -18
  37. package/lib/mattpocock-license-verdict.json +1 -1
  38. package/lib/principles.js +9 -1
  39. package/lib/uninstall.js +96 -2
  40. package/lib/update-skill.js +143 -2
  41. package/mock-policy/AGENTS.md +4 -4
  42. package/mutation/AGENTS.md +4 -4
  43. package/package.json +1 -1
  44. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  45. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  46. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  47. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  48. package/plugins/dsh/lib/index.d.ts.map +1 -1
  49. package/plugins/dsh/lib/index.js +7 -4
  50. package/plugins/dsh/package.json +1 -1
  51. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  52. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  53. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  54. package/plugins/dsh/src/index.test.ts +25 -0
  55. package/plugins/dsh/src/index.ts +7 -4
  56. package/plugins/opencode/package.json +1 -1
  57. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  58. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  59. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  60. package/plugins/qoder/plugin.json +1 -1
  61. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  62. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  63. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  64. package/principles/AGENTS.md +4 -4
  65. package/principles/adapters/csharp.ts +309 -0
  66. package/principles/analyzer.ts +5 -0
  67. package/scripts/delphi-external-review.cjs +130 -101
  68. package/skills/delphi-review/AGENTS.md +4 -4
  69. package/skills/sprint-flow/AGENTS.md +4 -4
  70. package/skills/test-specification-alignment/AGENTS.md +4 -4
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { checkDeps } = require('./detect-deps.js');
3
+ const { checkDeps, detectInstalledPlatforms, getSkillsDirs } = require('./detect-deps.js');
4
4
  const { rollback } = require('./rollback.js');
5
5
  const { HOME_DIR, CONFIG_DIR, detectPlatform } = require('./shared-paths.js');
6
6
  const { copyDirRecursive } = require('./shared-utils');
@@ -16,6 +16,29 @@ function getSkillsDir() {
16
16
  return path.join(HOME_DIR, '.config', 'opencode', 'skills');
17
17
  }
18
18
 
19
+ /**
20
+ * Every skills directory that belongs to an installed platform.
21
+ *
22
+ * #725 fixed the single-platform assumption, but the enumeration still hardcoded
23
+ * the three pre-dsh platforms (`.qoder`, `.config/opencode`, `.claude`) and so
24
+ * missed `~/.dsh/skills` -- the DSH platform `detectPlatform()` never returns and
25
+ * `PLATFORM_PROFILES` does carry. Derive the target set from the profiles instead
26
+ * of a parallel list: a machine with Qoder, OpenCode and DSH all present must
27
+ * have all three updated, and a migration period (two tools live) must not leave
28
+ * one silently frozen.
29
+ *
30
+ * A machine with no agent installed yet still needs one target, so fall back to
31
+ * the platform `detectPlatform()` selects and keep plain `install-skill` working.
32
+ *
33
+ * @returns {string[]} home-relative skills directories, de-duplicated
34
+ */
35
+ function deployedSkillsDirs() {
36
+ const installed = detectInstalledPlatforms();
37
+ if (installed.length === 0) return [getSkillsDir()];
38
+ const dirs = installed.map((platform) => getSkillsDirs(platform)[0]);
39
+ return [...new Set(dirs)];
40
+ }
41
+
19
42
  // The npm package ships the canonical skill content; `files` includes skills/.
20
43
  const BUNDLED_SKILLS_DIR = path.join(__dirname, '..', 'skills');
21
44
 
@@ -87,26 +110,52 @@ async function installSkill(name, options = {}) {
87
110
  return 1;
88
111
  }
89
112
 
90
- const targetDir = path.join(getSkillsDir(), name);
91
- const dupError = checkDuplicateInstall(targetDir, force);
92
- if (dupError) {
93
- console.error(dupError);
94
- return 1;
95
- }
113
+ // #725: update EVERY platform that already carries this skill, not just the
114
+ // one `detectPlatform()` picks. A machine with both Qoder and OpenCode used to
115
+ // get one silently-frozen copy. Each target is installed independently so a
116
+ // failure on one platform does not abort the others -- a half-updated machine
117
+ // that reports success is exactly the bug being fixed.
118
+ //
119
+ // deployedSkillsDirs() already falls back to the detected platform when nothing
120
+ // is deployed anywhere, so it never returns an empty list.
121
+ const finalTargets = deployedSkillsDirs().map((dir) => path.join(dir, name));
122
+
123
+ // `lastError` alone was wrong: an unreadable platform (e.g. ~/.qoder/skills is
124
+ // a file, so readdir throws ENOTDIR) set it to 1 and nothing reset it when the
125
+ // other platform installed fine -- the CLI then reported failure for a machine
126
+ // that had in fact been updated. Track successes instead: the command failed
127
+ // only when EVERY target failed.
128
+ let succeeded = 0;
129
+ let lastError = 0;
130
+ for (const targetDir of finalTargets) {
131
+ const dupError = checkDuplicateInstall(targetDir, force);
132
+ if (dupError) {
133
+ console.error(dupError);
134
+ lastError = 1;
135
+ continue;
136
+ }
96
137
 
97
- const installId = `${name}-${Date.now()}`;
98
- const backupDir = path.join(CONFIG_DIR, 'backup', installId);
99
- backupExisting(targetDir, installId, backupDir);
138
+ // The platform segment keeps concurrent per-platform backups distinct; a bare
139
+ // `Date.now()` collided when both platforms were written in the same tick.
140
+ const platformTag = path.basename(path.dirname(targetDir));
141
+ const installId = `${name}-${Date.now()}-${platformTag}`;
142
+ const backupDir = path.join(CONFIG_DIR, 'backup', installId);
143
+ backupExisting(targetDir, installId, backupDir);
100
144
 
101
- try {
102
- const result = await performInstall(name, targetDir, verbose);
103
- if (result !== 0) return result;
104
- return 0;
105
- } catch (err) {
106
- console.error(`Error: Install failed - ${err.message}`);
107
- await rollback(installId);
108
- return 1;
145
+ try {
146
+ if (verbose && finalTargets.length > 1) console.log(`Target: ${targetDir}`);
147
+ const result = await performInstall(name, targetDir, verbose);
148
+ if (result !== 0) lastError = result;
149
+ else succeeded += 1;
150
+ } catch (err) {
151
+ console.error(`Error: Install failed - ${err.message}`);
152
+ await rollback(installId);
153
+ lastError = 1;
154
+ }
109
155
  }
156
+
157
+ if (succeeded > 0) return 0;
158
+ return lastError || 1;
110
159
  }
111
160
 
112
161
  function checkDuplicateInstall(targetDir, force) {
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-11-03T07:08:33.951Z",
5
+ "checked_at": "2026-11-05T16:16:15.014Z",
6
6
  "source": "network"
7
7
  }
package/lib/principles.js CHANGED
@@ -209,10 +209,18 @@ function findPrinciplesEntry() {
209
209
  // Extensions the principles engine has an adapter for. Kept as a literal rather
210
210
  // than imported from src/principles/analyzer.ts because this file must stay
211
211
  // loadable by plain Node with no transpile step (the package ships zero runtime
212
- // dependencies and no build). Must match `getAdapterForFile`'s adapterMap.
212
+ // dependencies and no build). Must match `getAdapterForFile`'s adapterMap —
213
+ // `__tests__/principles-csharp-coverage.test.js` now derives the expected set
214
+ // from that adapterMap and fails when the two drift, because "kept in sync by
215
+ // comment" did not survive contact with #717.
216
+ //
217
+ // `.cs`/`.csx` were the gap: analyzer.ts registered CSharpAdapter, this walk
218
+ // never offered the files, so `xp-gate principles <csharp-dir>` reported
219
+ // "Files checked: 0" — indistinguishable from "no violations".
213
220
  const SOURCE_EXTENSIONS = new Set([
214
221
  '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
215
222
  '.py', '.go', '.java', '.kt', '.kts',
223
+ '.cs', '.csx',
216
224
  '.dart', '.swift', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp',
217
225
  '.m', '.mm',
218
226
  ]);
package/lib/uninstall.js CHANGED
@@ -52,6 +52,27 @@ const GLOBAL_USER_DATA = [
52
52
  { key: '.delphi-config.json', label: '.delphi-config.json (your Delphi expert config)' },
53
53
  ];
54
54
 
55
+ /**
56
+ * Directories under the config dir that the uninstall snapshot must NOT copy.
57
+ *
58
+ * #724: `createBackupSnapshot()` copied every plan entry, so an uninstall wrote a
59
+ * full second copy of `node_modules/` (~1000 files, ~12 MB) into
60
+ * `.uninstall-backup/`. That is the one thing here with no rollback value: it is
61
+ * a dependency tree npm regenerates from `package.json` in seconds, and it was
62
+ * the difference between a snapshot of a few config files and a 12 MB directory.
63
+ */
64
+ const UNBACKUPABLE_DIRS = new Set(['node_modules', 'modules']);
65
+
66
+ /**
67
+ * How many uninstall snapshots to keep.
68
+ *
69
+ * #724: the snapshot directory is named `uninstall-<timestamp>` and nothing ever
70
+ * pruned it, so every uninstall added another full copy and the directory only
71
+ * grew. Keeping the most recent few preserves the "I uninstalled by accident"
72
+ * recovery path without letting the directory become unbounded.
73
+ */
74
+ const BACKUP_RETENTION = 3;
75
+
55
76
  /**
56
77
  * Signature strings used to verify file ownership.
57
78
  * Each hook/adapter file contains a unique marker string.
@@ -149,6 +170,11 @@ function unsetHooksPath() {
149
170
 
150
171
  /**
151
172
  * Create a backup snapshot of files/dirs before deletion.
173
+ *
174
+ * #724: two scoping rules keep this proportionate. `node_modules/` and
175
+ * `modules/` are skipped outright -- they are regenerable from package.json, and
176
+ * copying them made the snapshot ~1000 files / 12 MB. Everything else is copied
177
+ * as before, so the recovery path for real config is unchanged.
152
178
  */
153
179
  function createBackupSnapshot(plan) {
154
180
  // Use a unique backup dir for this uninstall session
@@ -159,6 +185,9 @@ function createBackupSnapshot(plan) {
159
185
 
160
186
  for (const item of plan) {
161
187
  if (!item.path || !fs.existsSync(item.path)) continue;
188
+ // Retained user data is not being deleted, so there is nothing to roll back.
189
+ if (item.retain === true) continue;
190
+ if (UNBACKUPABLE_DIRS.has(path.basename(item.path))) continue;
162
191
 
163
192
  const relPath = path.basename(item.path);
164
193
  const destPath = path.join(sessionBackup, relPath);
@@ -176,9 +205,49 @@ function createBackupSnapshot(plan) {
176
205
  }
177
206
  }
178
207
 
208
+ pruneOldBackups(sessionBackup);
209
+
179
210
  return backupMeta;
180
211
  }
181
212
 
213
+ /**
214
+ * Keep only the newest BACKUP_RETENTION uninstall snapshots (#724).
215
+ *
216
+ * Snapshots are named `uninstall-<epoch-ms>`, so a plain reverse-sort of the
217
+ * directory names is a correct newest-first order without reading any metadata.
218
+ * An empty snapshot (every entry was skipped) is removed rather than counted
219
+ * toward the retention budget -- otherwise a run that backed up nothing would
220
+ * evict a snapshot that actually held config.
221
+ */
222
+ function pruneOldBackups(sessionBackup) {
223
+ try {
224
+ if (isDirEmpty(sessionBackup)) {
225
+ fs.rmdirSync(sessionBackup);
226
+ return;
227
+ }
228
+ const sessions = fs
229
+ .readdirSync(BACKUP_DIR, { withFileTypes: true })
230
+ .filter((e) => e.isDirectory() && e.name.startsWith('uninstall-'))
231
+ .map((e) => e.name)
232
+ .sort()
233
+ .reverse();
234
+ for (const stale of sessions.slice(BACKUP_RETENTION)) {
235
+ fs.rmSync(path.join(BACKUP_DIR, stale), { recursive: true, force: true });
236
+ }
237
+ } catch {
238
+ // Pruning is housekeeping. Failing it must never fail the uninstall, which
239
+ // has already done the work the user asked for.
240
+ }
241
+ }
242
+
243
+ function isDirEmpty(dir) {
244
+ try {
245
+ return fs.readdirSync(dir).length === 0;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+
182
251
  function copyDirSync(src, dest) {
183
252
  fs.mkdirSync(dest, { recursive: true });
184
253
  const entries = fs.readdirSync(src, { withFileTypes: true });
@@ -561,11 +630,27 @@ async function uninstall(args) {
561
630
  // §4.3 Step 4: Print plan
562
631
  console.log(`XP-Gate Uninstall (${mode} mode)`);
563
632
  console.log('=======================\n');
564
- console.log('The following will be removed:\n');
565
633
 
566
- for (const item of plan) {
634
+ // #724: 'retain' entries used to be printed under "The following will be
635
+ // removed" and then kept, with no further mention anywhere in the output.
636
+ // A user scripting cleanup from this preview would delete a directory they
637
+ // had been told was gone. The two groups are now stated separately, and the
638
+ // retained list is echoed again at the end.
639
+ const removable = plan.filter((item) => item.retain !== true);
640
+ const retained = plan.filter((item) => item.retain === true);
641
+
642
+ console.log('The following will be removed:\n');
643
+ for (const item of removable) {
567
644
  console.log(` • ${item.label}`);
568
645
  }
646
+
647
+ if (retained.length > 0) {
648
+ console.log('\nThe following will be KEPT (your data):\n');
649
+ for (const item of retained) {
650
+ console.log(` • ${item.label}`);
651
+ }
652
+ }
653
+
569
654
  if (options.purge) {
570
655
  console.log('\nPurge mode — additional cleanup:');
571
656
  for (const d of purgeDirs) {
@@ -642,6 +727,15 @@ async function uninstall(args) {
642
727
  console.log('Note: Qoder Delphi expert agents (.qoder/agents/delphi-*.md) are user-owned files');
643
728
  console.log(' that init never overwrites, so they were left in place — delete them by hand');
644
729
  console.log(' (then restart the Qoder session) if you want them gone.');
730
+ // #724: restate the retained data. The preview said it once, but the last thing
731
+ // a user reads before closing the terminal is the summary -- and an uninstall
732
+ // that leaves files behind owes them an explicit accounting of what it kept.
733
+ for (const item of retained) {
734
+ console.log(`Kept: ${item.label}`);
735
+ }
736
+ if (backupMeta && backupMeta.entries.length > 0) {
737
+ console.log(`Rollback snapshot: ${backupMeta.sessionBackup}`);
738
+ }
645
739
  if (hadErrors) {
646
740
  console.log('Some items could not be removed. Run doctor for diagnostics.');
647
741
  }
@@ -128,17 +128,158 @@ function readSkillsDirEntries(detect, platform) {
128
128
  return entries.map((entry) => ({ name: entry.name, isDirectory: entry.isDirectory(), dir }));
129
129
  }
130
130
 
131
+ /**
132
+ * The version of the CLI that is running, for comparing against what is on disk.
133
+ *
134
+ * Same source as install-skill.js reads (#713): the installed package.json, not
135
+ * the repo-root VERSION file, which the published tarball does not ship.
136
+ *
137
+ * @returns {string} semver, or 'unknown' when the package metadata is unreadable
138
+ */
139
+ function getCliVersion() {
140
+ try {
141
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
142
+ return typeof pkg.version === 'string' && pkg.version ? pkg.version : 'unknown';
143
+ } catch {
144
+ return 'unknown';
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Compare a 4-part release stamp against a 3-part npm semver.
150
+ *
151
+ * A trailing `.0` is the only difference between the two schemes, so trimming it
152
+ * is enough -- this is deliberately not a full semver comparison, which would
153
+ * imply support for pre-release ordering that neither source produces.
154
+ *
155
+ * The trailing segment is dropped, not "every trailing zero": a naive
156
+ * `.replace(/.0+$/, '')` also ate the dot of the segment before it, so
157
+ * `0.36.0.0` normalized to `0.36.0` while `0.36.0` normalized to `0.36` and a
158
+ * current copy was reported as behind. That stayed hidden while releases ended
159
+ * in a non-zero micro digit (`0.35.24.0` vs `0.35.24` agree either way) and
160
+ * broke the first time the micro digit was 0.
161
+ *
162
+ * @returns {boolean} true when both name the same release
163
+ */
164
+ function sameVersion(a, b) {
165
+ // A 4-part stamp whose micro digit is 0 names the same release as its 3-part
166
+ // form; anything else is compared as-is so a real difference is never hidden.
167
+ const norm = (v) => {
168
+ const parts = String(v).split('.');
169
+ return parts.length === 4 && parts[3] === '0' ? parts.slice(0, 3).join('.') : String(v);
170
+ };
171
+ return norm(a) === norm(b);
172
+ }
173
+
174
+ /**
175
+ * Whether a deployed copy is byte-identical to the bundle this CLI ships.
176
+ *
177
+ * Most skills carry no AGENTS.md, so their only version marker is `unknown`. That
178
+ * is not evidence of staleness -- treating it as such flagged every skill on a
179
+ * healthy machine. Content comparison answers the question the version stamp was
180
+ * standing in for, and answers it for those skills too.
181
+ *
182
+ * Only SKILL.md is compared: it is the file every skill has, and it is the one
183
+ * `update-skill` rewrites. Anything else a user added locally must not make their
184
+ * copy look outdated.
185
+ *
186
+ * @param {string} skillName
187
+ * @param {{dir: string}} entry a deployed copy
188
+ * @returns {boolean} true when the copy already matches the bundle
189
+ */
190
+ function copyMatchesBundle(skillName, entry) {
191
+ const bundled = path.join(__dirname, '..', 'skills', skillName, 'SKILL.md');
192
+ const installed = path.join(entry.dir, 'SKILL.md');
193
+ try {
194
+ return fs.readFileSync(installed, 'utf8') === fs.readFileSync(bundled, 'utf8');
195
+ } catch {
196
+ // Unreadable either way: fall back to the version stamp's verdict rather than
197
+ // asserting a match we could not verify.
198
+ return false;
199
+ }
200
+ }
201
+
131
202
  function handleCheckMode() {
132
203
  console.log('Checking for updates...');
133
204
  const config = getConfig();
134
205
  const skills = config.installedSkills || {};
206
+
207
+ // #725: this used to print config.installedSkills[name].version and call it a
208
+ // day. That value is what the last install WROTE DOWN, not what is on disk, so
209
+ // a copy left behind by a single-platform update reported the new version while
210
+ // its files still said the old one. Reporting the registry as if it were the
211
+ // installation is how a stale skill looked healthy.
212
+ const bundledVersion = getCliVersion();
213
+ let stale = 0;
214
+
135
215
  for (const [skillName, info] of Object.entries(skills)) {
136
- console.log(` ${skillName}: ${info.version || 'unknown'}`);
216
+ const registry = info.version || 'unknown';
217
+ const onDisk = deployedSkillVersions(skillName);
218
+ if (onDisk.length === 0) {
219
+ console.log(` ${skillName}: ${registry} (registry only - no copy found on disk)`);
220
+ continue;
221
+ }
222
+ // AGENTS.md stamps the 4-part release version (0.35.24.0) while package.json
223
+ // carries the 3-part npm semver (0.35.24). Comparing them raw marked every
224
+ // copy on a healthy machine as behind -- a false alarm is as useless as no
225
+ // check at all, so normalize before comparing.
226
+ // A copy is behind when its version stamp says so. Only a copy with NO stamp
227
+ // falls back to content comparison -- a known-old stamp must not be excused by
228
+ // an identical SKILL.md, which is exactly the delphi-review case: the two
229
+ // platform copies differ only in the generated AGENTS.md header.
230
+ const behind = onDisk.filter((entry) => (entry.version === 'unknown'
231
+ ? !copyMatchesBundle(skillName, entry)
232
+ : !sameVersion(entry.version, bundledVersion)));
233
+ if (behind.length > 0) {
234
+ stale += behind.length;
235
+ for (const entry of behind) {
236
+ console.log(` ${skillName} [${entry.platform}]: ${entry.version} -> ${bundledVersion} available`);
237
+ }
238
+ } else {
239
+ console.log(` ${skillName}: ${bundledVersion} (up to date on ${onDisk.length} platform(s))`);
240
+ }
137
241
  }
138
- console.log('Update check complete');
242
+
243
+ console.log(stale > 0 ? `Update check complete — ${stale} copy(ies) behind.` : 'Update check complete');
139
244
  return 0;
140
245
  }
141
246
 
247
+ /**
248
+ * Versions actually written into each platform's copy of \`skillName\`.
249
+ *
250
+ * Reads the \`**Version:**\` stamp from the generated AGENTS.md, which is the only
251
+ * per-copy version marker an installed skill carries. A copy without one (older
252
+ * layouts, or a hand-written skill) reports \`unknown\` rather than being skipped --
253
+ * silently omitting it would hide exactly the stale copy this check exists for.
254
+ *
255
+ * @param {string} skillName
256
+ * @returns {Array<{platform: string, version: string, dir: string}>} one entry per deployed copy
257
+ */
258
+ function deployedSkillVersions(skillName) {
259
+ let detect;
260
+ try {
261
+ detect = require('./detect-deps.js');
262
+ } catch {
263
+ return [];
264
+ }
265
+ const found = [];
266
+ for (const platform of Object.keys(detect.PLATFORM_PROFILES)) {
267
+ for (const entry of readSkillsDirEntries(detect, platform)) {
268
+ if (!entry.isDirectory || entry.name !== skillName) continue;
269
+ const agentsFile = path.join(entry.dir, entry.name, 'AGENTS.md');
270
+ let version = 'unknown';
271
+ try {
272
+ const match = fs.readFileSync(agentsFile, 'utf8').match(/^\*\*Version:\*\*\s*(\S+)/m);
273
+ if (match) version = match[1];
274
+ } catch {
275
+ // No AGENTS.md: still count the copy, with an unknown stamp.
276
+ }
277
+ found.push({ platform, version, dir: path.join(entry.dir, entry.name) });
278
+ }
279
+ }
280
+ return found;
281
+ }
282
+
142
283
  async function handleAllMode(verbose) {
143
284
  const names = installedSkillNames();
144
285
  console.log('Updating all skills...');
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-11-03
4
- **Commit:** fa007d1f
5
- **Branch:** main
6
- **Version:** 0.35.24.0
3
+ **Generated:** 2026-11-04
4
+ **Commit:** eb2097b0
5
+ **Branch:** sprint/2026-11-03-02
6
+ **Version:** 0.36.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
- **Generated:** 2026-11-03
4
- **Commit:** fa007d1f
5
- **Branch:** main
6
- **Version:** 0.35.24.0
3
+ **Generated:** 2026-11-04
4
+ **Commit:** eb2097b0
5
+ **Branch:** sprint/2026-11-03-02
6
+ **Version:** 0.36.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.35.24",
3
+ "version": "0.36.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.35.24",
3
+ "version": "0.36.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
- **Generated:** 2026-11-03
4
- **Commit:** fa007d1f
5
- **Branch:** main
6
- **Version:** 0.35.24.0
3
+ **Generated:** 2026-11-04
4
+ **Commit:** eb2097b0
5
+ **Branch:** sprint/2026-11-03-02
6
+ **Version:** 0.36.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
- **Generated:** 2026-11-03
4
- **Commit:** fa007d1f
5
- **Branch:** main
6
- **Version:** 0.35.24.0
3
+ **Generated:** 2026-11-04
4
+ **Commit:** eb2097b0
5
+ **Branch:** sprint/2026-11-03-02
6
+ **Version:** 0.36.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
- **Generated:** 2026-11-03
4
- **Commit:** fa007d1f
5
- **Branch:** main
6
- **Version:** 0.35.24.0
3
+ **Generated:** 2026-11-04
4
+ **Commit:** eb2097b0
5
+ **Branch:** sprint/2026-11-03-02
6
+ **Version:** 0.36.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 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,CAAC,MAAM,0BAA0B,CAAA;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AA0BlD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,MAAM;;;GAA6C,CAAA;AAElG,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAGlC,eAAO,MAAM,MAAM,UAA+B,CAAA;AAkQlD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKrE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAQtC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE9E;AA8ID;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aA4BjB,CAAA;AAEF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,OAAY,GAAG,IAAI,CAgC9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,CAAC,MAAM,0BAA0B,CAAA;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AA0BlD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,MAAM;;;GAA6C,CAAA;AAElG,eAAO,MAAM,IAAI,iBAAiB,CAAA;AAGlC,eAAO,MAAM,MAAM,UAA+B,CAAA;AAkQlD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKrE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAQtC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE9E;AA8ID;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aA4BjB,CAAA;AAEF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,OAAY,GAAG,IAAI,CAmC9D"}
@@ -483,10 +483,13 @@ export function apply(ctx, config = {}) {
483
483
  // delphi-review work on a machine whose key lives in the credential store rather
484
484
  // than in the host process's inherited environment.
485
485
  //
486
- // The cast is needed because the service is attached by a peer package's module
487
- // augmentation, which only applies when that package is in the compilation; a
488
- // consumer may install this plugin without it.
489
- const credentials = ctx.credentials;
486
+ // Read via ctx.get() rather than a bare ctx.credentials (#721): Cordis's context
487
+ // proxy throws "cannot get property ... without inject" on any property that is
488
+ // neither a declared service nor in this plugin's inject list. "credentials" is
489
+ // deliberately NOT in inject — adding it would stop the plugin from loading on a
490
+ // host without the service, discarding the process.env fallback below. ctx.get()
491
+ // is the inject-free read and returns undefined when the host provides none.
492
+ const credentials = ctx.get?.("credentials");
490
493
  const resolveApiKey = createCredentialResolver(credentials, process.env);
491
494
  ctx.tools.register(gateCheckTool(shell, dialect));
492
495
  ctx.tools.register(gatePrinciplesTool(shell, dialect));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/dsh-plugin-xp-gate",
3
- "version": "0.35.24",
3
+ "version": "0.36.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
- **Generated:** 2026-11-03
4
- **Commit:** fa007d1f
5
- **Branch:** main
6
- **Version:** 0.35.24.0
3
+ **Generated:** 2026-11-04
4
+ **Commit:** eb2097b0
5
+ **Branch:** sprint/2026-11-03-02
6
+ **Version:** 0.36.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
- **Generated:** 2026-11-03
4
- **Commit:** fa007d1f
5
- **Branch:** main
6
- **Version:** 0.35.24.0
3
+ **Generated:** 2026-11-04
4
+ **Commit:** eb2097b0
5
+ **Branch:** sprint/2026-11-03-02
6
+ **Version:** 0.36.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
- **Generated:** 2026-11-03
4
- **Commit:** fa007d1f
5
- **Branch:** main
6
- **Version:** 0.35.24.0
3
+ **Generated:** 2026-11-04
4
+ **Commit:** eb2097b0
5
+ **Branch:** sprint/2026-11-03-02
6
+ **Version:** 0.36.1.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -80,6 +80,31 @@ describe("apply", () => {
80
80
  expect(names).toContain("gate-arch")
81
81
  })
82
82
 
83
+ /**
84
+ * @test REQ-721
85
+ * @intent 验证 apply(ctx) 在宿主未提供 credentials 服务时仍能启动。Cordis 的
86
+ * 上下文是一个代理:对既未在 inject 声明、也未 provide 的服务做裸属性
87
+ * 读取会直接抛 "cannot get property ... without inject"(#721 根因)。
88
+ * credentials 是可选服务,必须经 ctx.get() 读取(未提供时返回 undefined,
89
+ * 走 process.env 回落),而不是 ctx.credentials。
90
+ * @covers AC-721-01
91
+ */
92
+ it("applies without a credentials service (Cordis throws on undeclared property reads)", () => {
93
+ const { ctx, captured } = makeCtx("ok")
94
+ // 复现 Cordis 的 context proxy 语义:目标上不存在、也未声明的属性读取抛错,
95
+ // 而不是返回 undefined —— 裸的 ctx.credentials 会在这里把整个插件启动炸掉。
96
+ const cordisLike = new Proxy(ctx as object, {
97
+ get(target, prop, receiver) {
98
+ if (prop === "get") return (_name: string) => undefined
99
+ if (Reflect.has(target, prop)) return Reflect.get(target, prop, receiver)
100
+ throw new Error(`cannot get property "${String(prop)}" without inject`)
101
+ },
102
+ })
103
+ expect(() => apply(cordisLike as unknown as Context)).not.toThrow()
104
+ // apply 存活后,工具仍按契约注册。
105
+ expect(captured.map((c) => (c as CapturedTool).name)).toContain("gate-check")
106
+ })
107
+
83
108
  /**
84
109
  * @test REQ-DSH-006
85
110
  * @intent 验证 apply(ctx) 通过 ctx.skills.registerProvider 注册了 bundled skill provider,