@boyingliu01/xp-gate 0.35.21 → 0.35.22
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/bin/xp-gate.js +12 -0
- package/lib/__tests__/agent-runtime-journal.test.js +302 -0
- package/lib/__tests__/mirror-sync.test.js +277 -0
- package/lib/__tests__/runtime-journal.test.js +164 -0
- package/lib/agent-runtime-journal.js +347 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/mirror-sync.js +335 -0
- package/lib/runtime-journal.js +156 -0
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror sync — bring every canonical tree / npm-package mirror back to parity.
|
|
3
|
+
*
|
|
4
|
+
* xp-gate keeps several trees in two places: `githooks/` is the source of truth
|
|
5
|
+
* and `src/npm-package/hooks/` is the copy that ships. Four `check-*-mirror.sh`
|
|
6
|
+
* scripts guard that duplication, but all four only *detect* drift; repairing it
|
|
7
|
+
* is a hand-copy, and the hand-copy is where the real cost shows up: over the
|
|
8
|
+
* three days to 2026-11-02, 29 of 141 commits (20.6%) touched only mirrors,
|
|
9
|
+
* version stamps, AGENTS.md copies and CHANGELOG, and the mirror/sync scripts
|
|
10
|
+
* were invoked 306 times.
|
|
11
|
+
*
|
|
12
|
+
* Two defects are fixed here:
|
|
13
|
+
*
|
|
14
|
+
* 1. **False drift from untracked build artifacts.** `check-hook-lib-mirror.sh`
|
|
15
|
+
* compared directory listings, so a `githooks/lib/__pycache__/` created by a
|
|
16
|
+
* Python test run made a byte-identical tree look out of sync. The set of
|
|
17
|
+
* files that matter is the set git tracks — an artifact that is not in the
|
|
18
|
+
* repository cannot be drift, because it cannot be committed.
|
|
19
|
+
* 2. **No repair path.** `syncMirrors` copies what is missing, rewrites what
|
|
20
|
+
* differs, and prunes what the canonical side no longer has — idempotently,
|
|
21
|
+
* so a second run is a no-op and the operation can run on every commit
|
|
22
|
+
* without becoming a churn generator itself.
|
|
23
|
+
*
|
|
24
|
+
* @module mirror-sync
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const { execFileSync } = require('child_process');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Directory names that are never mirrored, even when tracked.
|
|
35
|
+
*
|
|
36
|
+
* A build artifact directory is machine state, not source. Copying it would
|
|
37
|
+
* make the mirror differ between machines for reasons no commit explains.
|
|
38
|
+
*/
|
|
39
|
+
const SYNC_EXCLUDES = Object.freeze(['__pycache__', 'node_modules', '.git', '.DS_Store']);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The canonical -> mirror pairs this repository maintains for whole trees.
|
|
43
|
+
*
|
|
44
|
+
* Declared rather than discovered recursively: a glob that silently matches
|
|
45
|
+
* nothing would report "in sync" forever, which is the false green this whole
|
|
46
|
+
* task exists to remove.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately NOT including `githooks` -> `src/npm-package/hooks`: that mapping
|
|
49
|
+
* is a curated *file list* (see `HOOK_MIRROR_FILES` in
|
|
50
|
+
* `scripts/check-hook-mirror.sh`), not the whole directory. `githooks/` also
|
|
51
|
+
* holds `__tests__/`, `AGENTS.md` and the adapters subtree, none of which ship.
|
|
52
|
+
* Mirroring the directory wholesale would copy 500+ files into the npm package —
|
|
53
|
+
* treating "canonical has it" as "the mirror should have it" where the project
|
|
54
|
+
* never said so.
|
|
55
|
+
*
|
|
56
|
+
* Per-pair `exclude` mirrors the guard scripts exactly. `src/principles` already
|
|
57
|
+
* excludes `__tests__` (check-hook-mirror.sh:98); a sync that copied them would
|
|
58
|
+
* ship test code in the package and make the guard fail immediately afterwards.
|
|
59
|
+
*/
|
|
60
|
+
const KNOWN_PAIRS = Object.freeze([
|
|
61
|
+
{ canonical: 'githooks/adapters', mirror: 'src/npm-package/adapters' },
|
|
62
|
+
{ canonical: 'githooks/lib', mirror: 'src/npm-package/hooks/lib' },
|
|
63
|
+
{ canonical: 'src/principles', mirror: 'src/npm-package/principles', exclude: ['__tests__'] },
|
|
64
|
+
{ canonical: 'src/build-integrity', mirror: 'src/npm-package/build-integrity' },
|
|
65
|
+
{ canonical: 'skills', mirror: 'src/npm-package/skills' },
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* List files under `dir` relative to `dir`, honouring exclusions.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} root repository root
|
|
72
|
+
* @param {string} dir directory relative to root
|
|
73
|
+
* @returns {string[]} relative slash-separated paths
|
|
74
|
+
*/
|
|
75
|
+
function listFiles(root, dir) {
|
|
76
|
+
const abs = path.join(root, dir);
|
|
77
|
+
const out = [];
|
|
78
|
+
const walk = (current, prefix) => {
|
|
79
|
+
let entries;
|
|
80
|
+
try {
|
|
81
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (SYNC_EXCLUDES.includes(entry.name)) continue;
|
|
87
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
walk(path.join(current, entry.name), rel);
|
|
90
|
+
} else if (entry.isFile()) {
|
|
91
|
+
out.push(rel);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
walk(abs, '');
|
|
96
|
+
return out.sort();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* List the files git tracks under `dir` that also exist on disk.
|
|
101
|
+
*
|
|
102
|
+
* The disk check matters: `git ls-files` reports the index, so a file deleted
|
|
103
|
+
* from the working tree (a mirror copy removed to simulate drift, or a fix in
|
|
104
|
+
* progress) is still listed. Reading it would throw ENOENT inside the diff.
|
|
105
|
+
*
|
|
106
|
+
* When `dir` is not inside a git repository (or git is unavailable), fall back to
|
|
107
|
+
* a plain walk — the caller is then responsible for the artifact caveat, but a
|
|
108
|
+
* check that crashes outside git is worse than one that reports what it sees.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} root repository root
|
|
111
|
+
* @param {string} dir directory relative to root
|
|
112
|
+
* @returns {string[]}
|
|
113
|
+
*/
|
|
114
|
+
function listTrackedFiles(root, dir) {
|
|
115
|
+
const normalised = dir.replace(/\\/g, '/');
|
|
116
|
+
let tracked;
|
|
117
|
+
try {
|
|
118
|
+
const out = execFileSync('git', ['ls-files', '-z', '--', dir], {
|
|
119
|
+
cwd: root,
|
|
120
|
+
encoding: 'utf8',
|
|
121
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
122
|
+
});
|
|
123
|
+
tracked = out
|
|
124
|
+
.split('\0')
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.map((p) => p.replace(/\\/g, '/'))
|
|
127
|
+
.filter((p) => p.startsWith(`${normalised}/`))
|
|
128
|
+
.map((p) => p.slice(normalised.length + 1))
|
|
129
|
+
.filter((p) => !p.split('/').some((seg) => SYNC_EXCLUDES.includes(seg)));
|
|
130
|
+
} catch {
|
|
131
|
+
return listFiles(root, dir);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Keep only what is actually on disk; a tracked-but-deleted file must read as
|
|
135
|
+
// "missing from this side", which is what the caller then repairs.
|
|
136
|
+
return tracked
|
|
137
|
+
.filter((p) => fs.existsSync(path.join(root, dir, p)))
|
|
138
|
+
.sort();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compare a canonical tree against its mirror.
|
|
143
|
+
*
|
|
144
|
+
* Both sides are compared on the tracked file set so an untracked artifact in
|
|
145
|
+
* either tree cannot be mistaken for drift.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} root repository root
|
|
148
|
+
* @param {string} canonical canonical dir (relative)
|
|
149
|
+
* @param {string} mirror mirror dir (relative)
|
|
150
|
+
* @param {{exclude?: string[]}} [options] extra directory names to skip for this pair
|
|
151
|
+
* @returns {{inSync: boolean, missing: string[], differing: string[], extra: string[]}}
|
|
152
|
+
*/
|
|
153
|
+
function diffTrees(root, canonical, mirror, options = {}) {
|
|
154
|
+
const exclude = options.exclude || [];
|
|
155
|
+
const keep = (rel) => !rel.split('/').some((seg) => exclude.includes(seg));
|
|
156
|
+
const canonicalFiles = listTrackedFiles(root, canonical).filter(keep);
|
|
157
|
+
const mirrorFiles = listTrackedFiles(root, mirror).filter(keep);
|
|
158
|
+
const mirrorSet = new Set(mirrorFiles);
|
|
159
|
+
const canonicalSet = new Set(canonicalFiles);
|
|
160
|
+
|
|
161
|
+
const missing = canonicalFiles.filter((f) => !mirrorSet.has(f));
|
|
162
|
+
const extra = mirrorFiles.filter((f) => !canonicalSet.has(f));
|
|
163
|
+
const differing = [];
|
|
164
|
+
for (const rel of canonicalFiles) {
|
|
165
|
+
if (!mirrorSet.has(rel)) continue;
|
|
166
|
+
const a = fs.readFileSync(path.join(root, canonical, rel));
|
|
167
|
+
const b = fs.readFileSync(path.join(root, mirror, rel));
|
|
168
|
+
if (!a.equals(b)) differing.push(rel);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
inSync: missing.length === 0 && differing.length === 0 && extra.length === 0,
|
|
173
|
+
missing,
|
|
174
|
+
differing,
|
|
175
|
+
extra,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Discover the known pairs that actually exist in this repository.
|
|
181
|
+
*
|
|
182
|
+
* A pair is only returned when both sides are present on disk: reporting a pair
|
|
183
|
+
* whose mirror does not exist yet would make `syncMirrors` create trees the
|
|
184
|
+
* project never asked for.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} root repository root
|
|
187
|
+
* @returns {Array<{canonical: string, mirror: string}>}
|
|
188
|
+
*/
|
|
189
|
+
function collectMirrorPairs(root) {
|
|
190
|
+
return KNOWN_PAIRS.filter(
|
|
191
|
+
(pair) =>
|
|
192
|
+
fs.existsSync(path.join(root, pair.canonical)) && fs.existsSync(path.join(root, pair.mirror))
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Copy one file, preserving its executable bit.
|
|
198
|
+
* @param {string} from
|
|
199
|
+
* @param {string} to
|
|
200
|
+
*/
|
|
201
|
+
function copyFile(from, to) {
|
|
202
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
203
|
+
fs.copyFileSync(from, to);
|
|
204
|
+
// Hooks that lose +x stop running, and the failure looks like "the gate is
|
|
205
|
+
// silently skipped" rather than a permissions error.
|
|
206
|
+
try {
|
|
207
|
+
const mode = fs.statSync(from).mode;
|
|
208
|
+
fs.chmodSync(to, mode & 0o777);
|
|
209
|
+
} catch {
|
|
210
|
+
// chmod is a no-op on Windows; the content copy already succeeded.
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Bring mirrors into parity with their canonical trees.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} root repository root
|
|
218
|
+
* @param {Array<{canonical: string, mirror: string}>} [pairs] defaults to the discoverable known pairs
|
|
219
|
+
* @param {{dryRun?: boolean, prune?: boolean}} [options]
|
|
220
|
+
* @returns {{copied: string[], cleaned: string[], pairs: number, dryRun: boolean}}
|
|
221
|
+
*/
|
|
222
|
+
function syncMirrors(root, pairs, options = {}) {
|
|
223
|
+
const dryRun = options.dryRun === true;
|
|
224
|
+
const prune = options.prune !== false;
|
|
225
|
+
const selected = pairs || collectMirrorPairs(root);
|
|
226
|
+
|
|
227
|
+
const copied = [];
|
|
228
|
+
const cleaned = [];
|
|
229
|
+
|
|
230
|
+
for (const pair of selected) {
|
|
231
|
+
const canonicalAbs = path.join(root, pair.canonical);
|
|
232
|
+
if (!fs.existsSync(canonicalAbs) || !fs.statSync(canonicalAbs).isDirectory()) {
|
|
233
|
+
// Refusing beats guessing: treating a missing canonical tree as "empty"
|
|
234
|
+
// would mark every mirrored file an orphan and delete working code.
|
|
235
|
+
throw new Error(`canonical directory does not exist: ${pair.canonical}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const diff = diffTrees(root, pair.canonical, pair.mirror, { exclude: pair.exclude });
|
|
239
|
+
const toWrite = [...diff.missing, ...diff.differing];
|
|
240
|
+
|
|
241
|
+
for (const rel of toWrite) {
|
|
242
|
+
copied.push(rel);
|
|
243
|
+
if (dryRun) continue;
|
|
244
|
+
copyFile(path.join(root, pair.canonical, rel), path.join(root, pair.mirror, rel));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (prune) {
|
|
248
|
+
for (const rel of diff.extra) {
|
|
249
|
+
cleaned.push(rel);
|
|
250
|
+
if (dryRun) continue;
|
|
251
|
+
try {
|
|
252
|
+
fs.rmSync(path.join(root, pair.mirror, rel));
|
|
253
|
+
} catch {
|
|
254
|
+
// A file that vanished between listing and prune is already the goal.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { copied, cleaned, pairs: selected.length, dryRun };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Run the `sync-mirrors` subcommand.
|
|
265
|
+
*
|
|
266
|
+
* Defaults to a dry run. Writing is opt-in via `--write`, because the failure
|
|
267
|
+
* mode of a wrong pair declaration is silent mass overwrite — previewing costs
|
|
268
|
+
* one round trip and makes the destructive path a deliberate choice.
|
|
269
|
+
*
|
|
270
|
+
* `root` is a parameter rather than always `process.cwd()` so tests can point at
|
|
271
|
+
* a fixture; `process.chdir` is unavailable inside a vitest worker.
|
|
272
|
+
*
|
|
273
|
+
* @param {string[]} argv
|
|
274
|
+
* @param {{root?: string}} [options]
|
|
275
|
+
* @returns {number} exit code (0 when in sync / repaired; 1 when drift remains after a dry run)
|
|
276
|
+
*/
|
|
277
|
+
function handleSyncMirrors(argv, options = {}) {
|
|
278
|
+
const args = argv || [];
|
|
279
|
+
const write = args.includes('--write');
|
|
280
|
+
const json = args.includes('--json');
|
|
281
|
+
const root = options.root || process.cwd();
|
|
282
|
+
|
|
283
|
+
const pairs = collectMirrorPairs(root);
|
|
284
|
+
if (pairs.length === 0) {
|
|
285
|
+
process.stdout.write('sync-mirrors: NOT MEASURABLE (no mirror pairs found — is this the xp-gate repo?)\n');
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const before = pairs.map((pair) => ({
|
|
290
|
+
pair: `${pair.canonical} -> ${pair.mirror}`,
|
|
291
|
+
diff: diffTrees(root, pair.canonical, pair.mirror, { exclude: pair.exclude }),
|
|
292
|
+
}));
|
|
293
|
+
const drifted = before.filter((entry) => !entry.diff.inSync);
|
|
294
|
+
|
|
295
|
+
const result = syncMirrors(root, pairs, { dryRun: !write });
|
|
296
|
+
|
|
297
|
+
if (json) {
|
|
298
|
+
process.stdout.write(
|
|
299
|
+
`${JSON.stringify({ root, write, driftedBefore: drifted.length, ...result }, null, 2)}\n`
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
for (const entry of before) {
|
|
303
|
+
const status = entry.diff.inSync ? 'IN SYNC' : 'DRIFT ';
|
|
304
|
+
process.stdout.write(` ${status} ${entry.pair}\n`);
|
|
305
|
+
if (!entry.diff.inSync) {
|
|
306
|
+
if (entry.diff.missing.length) process.stdout.write(` missing: ${entry.diff.missing.slice(0, 5).join(', ')}\n`);
|
|
307
|
+
if (entry.diff.differing.length) process.stdout.write(` differing: ${entry.diff.differing.slice(0, 5).join(', ')}\n`);
|
|
308
|
+
if (entry.diff.extra.length) process.stdout.write(` extra: ${entry.diff.extra.slice(0, 5).join(', ')}\n`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (result.copied.length === 0 && result.cleaned.length === 0) {
|
|
312
|
+
process.stdout.write('All mirrors in sync.\n');
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
const verb = write ? 'Wrote' : 'Would write';
|
|
316
|
+
process.stdout.write(`${verb} ${result.copied.length} file(s), prune ${result.cleaned.length}.\n`);
|
|
317
|
+
if (!write) {
|
|
318
|
+
process.stdout.write('Re-run with --write to apply.\n');
|
|
319
|
+
return 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return write ? 0 : drifted.length > 0 ? 1 : 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
SYNC_EXCLUDES,
|
|
328
|
+
KNOWN_PAIRS,
|
|
329
|
+
listFiles,
|
|
330
|
+
listTrackedFiles,
|
|
331
|
+
diffTrees,
|
|
332
|
+
collectMirrorPairs,
|
|
333
|
+
syncMirrors,
|
|
334
|
+
handleSyncMirrors,
|
|
335
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xp-gate runtime-journal` — CLI for the AGENTS.md runtime-efficiency rules.
|
|
3
|
+
*
|
|
4
|
+
* Implements the same "measure don't restate" stance as `gate-rules`: the three
|
|
5
|
+
* rules in AGENTS.md §AGENT RUNTIME DISCIPLINE were written on 2026-10-30, but
|
|
6
|
+
* by 2026-11-02 only rule 1 had taken effect (pure-read shell: 32.7% -> 0.2%)
|
|
7
|
+
* while rule 2 stayed at 1.02 calls/step and rule 3 was violated 103 times with
|
|
8
|
+
* an explicit `wait: true`. A rule nobody measures is a rule nobody follows, so
|
|
9
|
+
* this surfaces the cost as numbers.
|
|
10
|
+
*
|
|
11
|
+
* Advisory by design — it reports share-of-time and never blocks a commit.
|
|
12
|
+
* The behaviour it measures (how the agent waits) is not something a git hook
|
|
13
|
+
* can decide.
|
|
14
|
+
*
|
|
15
|
+
* @module runtime-journal
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
analyseSessions,
|
|
25
|
+
discoverSessionDirs,
|
|
26
|
+
summariseForSlot,
|
|
27
|
+
} = require('./agent-runtime-journal');
|
|
28
|
+
|
|
29
|
+
/** Default lookback for the report, in days. */
|
|
30
|
+
const DEFAULT_DAYS = 3;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Candidate roots that hold DSH session journals, most specific first.
|
|
34
|
+
*
|
|
35
|
+
* `DSH_SESSIONS_DIR` wins so a CI job or a different machine can point at the
|
|
36
|
+
* store without code changes.
|
|
37
|
+
*
|
|
38
|
+
* The DSH store names each project root `--<slug>--`, where the slug is the
|
|
39
|
+
* absolute cwd with every path separator replaced by a dash — e.g.
|
|
40
|
+
* `D:\projects\xp-gate` becomes `--D-projects-xp-gate--`. Deriving it from the
|
|
41
|
+
* full path matters: a basename-only guess (`--xp-gate--`) names a directory
|
|
42
|
+
* that does not exist.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} [cwd] project directory; defaults to `process.cwd()`
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
function sessionRootCandidates(cwd = process.cwd()) {
|
|
48
|
+
const home = os.homedir();
|
|
49
|
+
const dirs = [];
|
|
50
|
+
if (process.env.DSH_SESSIONS_DIR) dirs.push(process.env.DSH_SESSIONS_DIR);
|
|
51
|
+
const base = path.join(home, '.dsh', 'sessions');
|
|
52
|
+
// Each separator becomes one dash, then the store wraps the slug in an extra
|
|
53
|
+
// dash on both sides: `D:\projects\xp-gate` -> `D-projects-xp-gate` ->
|
|
54
|
+
// `--D-projects-xp-gate--`. The wrapping pair is not redundant; without it
|
|
55
|
+
// the path names a directory that does not exist.
|
|
56
|
+
const slug = String(cwd).replace(/[\\/:]+/g, '-');
|
|
57
|
+
if (slug) dirs.push(path.join(base, `--${slug}--`));
|
|
58
|
+
dirs.push(base);
|
|
59
|
+
return dirs;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Find the session root that actually exists and contains journals.
|
|
64
|
+
* @param {string[]} candidates
|
|
65
|
+
* @returns {string|null}
|
|
66
|
+
*/
|
|
67
|
+
function pickSessionRoot(candidates) {
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
if (discoverSessionDirs(candidate).length > 0) return candidate;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse `--days N` from argv.
|
|
76
|
+
* @param {string[]} argv
|
|
77
|
+
* @param {number} fallback
|
|
78
|
+
* @returns {number}
|
|
79
|
+
*/
|
|
80
|
+
function parseDays(argv, fallback) {
|
|
81
|
+
const idx = argv.indexOf('--days');
|
|
82
|
+
if (idx === -1) return fallback;
|
|
83
|
+
const raw = argv[idx + 1];
|
|
84
|
+
const n = Number(raw);
|
|
85
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run the `runtime-journal` subcommand.
|
|
90
|
+
*
|
|
91
|
+
* Always exits 0: this is a diagnostic. Callers that want a gate can read the
|
|
92
|
+
* JSON and apply their own threshold, which keeps the measurement honest
|
|
93
|
+
* (a tool that fails on bad news gets suppressed, not fixed).
|
|
94
|
+
*
|
|
95
|
+
* @param {string[]} argv
|
|
96
|
+
* @returns {number} exit code (always 0)
|
|
97
|
+
*/
|
|
98
|
+
function handleRuntimeJournal(argv) {
|
|
99
|
+
const args = argv || [];
|
|
100
|
+
const json = args.includes('--json');
|
|
101
|
+
const days = parseDays(args, DEFAULT_DAYS);
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const sinceMs = now - days * 24 * 60 * 60 * 1000;
|
|
104
|
+
|
|
105
|
+
const explicitDir = (() => {
|
|
106
|
+
const idx = args.indexOf('--sessions-dir');
|
|
107
|
+
return idx === -1 ? null : args[idx + 1];
|
|
108
|
+
})();
|
|
109
|
+
|
|
110
|
+
const root = explicitDir
|
|
111
|
+
? (discoverSessionDirs(explicitDir).length ? explicitDir : null)
|
|
112
|
+
: pickSessionRoot(sessionRootCandidates(process.cwd()));
|
|
113
|
+
|
|
114
|
+
if (!root) {
|
|
115
|
+
// NOT MEASURABLE rather than a zero report: a zero that means "no data"
|
|
116
|
+
// reads as "no problem", the false-green that #636 set out to remove.
|
|
117
|
+
if (json) {
|
|
118
|
+
process.stdout.write(`${JSON.stringify({ measurable: false, reason: 'no session journals found', days }, null, 2)}\n`);
|
|
119
|
+
} else {
|
|
120
|
+
process.stdout.write('Runtime efficiency: NOT MEASURABLE (no session journals found)\n');
|
|
121
|
+
process.stdout.write('Hint: set DSH_SESSIONS_DIR, or pass --sessions-dir <path>.\n');
|
|
122
|
+
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const report = analyseSessions(discoverSessionDirs(root), { sinceMs, untilMs: now });
|
|
127
|
+
|
|
128
|
+
if (json) {
|
|
129
|
+
const payload = {
|
|
130
|
+
measurable: report.toolCalls > 0,
|
|
131
|
+
days,
|
|
132
|
+
window: { from: new Date(sinceMs).toISOString(), to: new Date(now).toISOString() },
|
|
133
|
+
sessionsDir: root,
|
|
134
|
+
...report,
|
|
135
|
+
};
|
|
136
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
process.stdout.write(`Runtime efficiency — last ${days} day(s)\n`);
|
|
141
|
+
process.stdout.write(`Sessions: ${root}\n`);
|
|
142
|
+
const slots = summariseForSlot(report);
|
|
143
|
+
for (const line of slots) process.stdout.write(` ${line}\n`);
|
|
144
|
+
if (report.toolCalls > 0) {
|
|
145
|
+
process.stdout.write(` measured ${report.toolCalls} tool call(s), median ${report.toolMedianMs} ms\n`);
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
DEFAULT_DAYS,
|
|
152
|
+
handleRuntimeJournal,
|
|
153
|
+
sessionRootCandidates,
|
|
154
|
+
pickSessionRoot,
|
|
155
|
+
parseDays,
|
|
156
|
+
};
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.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
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.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.35.
|
|
3
|
+
"version": "0.35.22",
|
|
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
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.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
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.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
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/plugins/dsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.22",
|
|
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
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.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
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.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
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.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
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.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
3
|
**Generated:** 2026-11-02
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 57ef51ed
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.22.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.22",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Qoder. 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": {
|