@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.
- package/adapter-common.sh +55 -2
- 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/iac.sh +45 -5
- 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 +55 -2
- 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 +175 -0
- package/lib/__tests__/install-skill-full-dir.test.js +45 -2
- package/lib/__tests__/install-skill.test.js +84 -3
- package/lib/__tests__/mirror-sync.test.js +23 -5
- package/lib/__tests__/principles-csharp-coverage.test.js +227 -0
- package/lib/__tests__/uninstall-global-residue.test.js +192 -28
- package/lib/__tests__/update-skill-flags.test.js +78 -0
- package/lib/__tests__/update-skill.test.js +156 -3
- package/lib/detect-deps.js +24 -0
- package/lib/doctor.js +80 -12
- package/lib/install-skill.js +67 -18
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +9 -1
- package/lib/uninstall.js +96 -2
- package/lib/update-skill.js +143 -2
- 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
package/lib/install-skill.js
CHANGED
|
@@ -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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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) {
|
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
|
-
|
|
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
|
}
|
package/lib/update-skill.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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...');
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-11-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 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.
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-11-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 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": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
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-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 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-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 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-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 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,
|
|
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"}
|
package/plugins/dsh/lib/index.js
CHANGED
|
@@ -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
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
|
|
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));
|
package/plugins/dsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
-
"version": "0.
|
|
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-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 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-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 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-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 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,
|