@boyingliu01/xp-gate 0.28.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gate-3.sh +18 -1
- package/hooks/gate-3.sh +18 -1
- package/hooks/hook-module-resolution.sh +39 -0
- package/hooks/pre-commit +315 -38
- package/hooks/pre-push +26 -2
- package/lib/__tests__/doctor-module-resolution-parity.test.js +79 -11
- package/lib/__tests__/sprint-state-manager.test.js +31 -0
- package/lib/doctor.js +60 -27
- package/lib/hook-module-resolution.js +127 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/retro.js +24 -7
- package/lib/sprint-state-manager.js +14 -6
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/claude-code/skills/grilling/SKILL.md +1 -0
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/dsh/skills/grilling/SKILL.md +1 -0
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/grill-with-docs/SKILL.md +1 -0
- package/plugins/opencode/skills/grilling/SKILL.md +1 -0
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/batch-grill-me/SKILL.md +9 -2
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/grill-with-docs/SKILL.md +2 -2
- package/plugins/qoder/skills/grilling/SKILL.md +23 -5
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/scripts/sync-package-content.js +3 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/grill-with-docs/SKILL.md +1 -0
- package/skills/grilling/SKILL.md +1 -0
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -16,10 +16,20 @@
|
|
|
16
16
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
17
17
|
import { tmpdir } from 'node:os';
|
|
18
18
|
import { join, resolve } from 'node:path';
|
|
19
|
-
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { spawnSync, execFileSync } from 'node:child_process';
|
|
20
20
|
import { describe, expect, it } from 'vitest';
|
|
21
21
|
|
|
22
22
|
const PRE_COMMIT = readFileSync(resolve(__dirname, '..', '..', '..', '..', 'githooks', 'pre-commit'), 'utf8');
|
|
23
|
+
// #505 replaced the two hand-written shell chains with one data file generated
|
|
24
|
+
// from lib/hook-module-resolution.js — the same module doctor.js requires. The
|
|
25
|
+
// parity test therefore verifies the NEW contract: the generated data is current,
|
|
26
|
+
// and the hook + doctor both honour it. The scenarios below still drive the hook's
|
|
27
|
+
// real text through bash, so agreement is measured, not asserted.
|
|
28
|
+
const RESOLUTION_DATA = readFileSync(
|
|
29
|
+
resolve(__dirname, '..', '..', '..', '..', 'githooks', 'hook-module-resolution.sh'),
|
|
30
|
+
'utf8',
|
|
31
|
+
);
|
|
32
|
+
const { RESOLUTION_ORDER, GATE_RESOLUTION_ORDER, ADAPTER_DIR_ORDER } = require('../hook-module-resolution.js');
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* Extract one bash function's full text, brace-balanced from the declaration.
|
|
@@ -149,13 +159,28 @@ function hookResolvesAll(blocks, resolverText, t, lang, gateModule) {
|
|
|
149
159
|
`GLOBAL_ADAPTER_DIR='${t.globalAdapters}'`,
|
|
150
160
|
`PROJECT_GITHOOKS='${t.projectGithooks}'`,
|
|
151
161
|
`SCRIPT_DIR='${t.scriptDir}'`,
|
|
152
|
-
|
|
162
|
+
`ADAPTER_DIR=''`,
|
|
163
|
+
resolverText,
|
|
153
164
|
blocks.gateDir,
|
|
165
|
+
// The real chains, driven by the same data the hook loads.
|
|
166
|
+
'_hook_adapter_dir() {',
|
|
167
|
+
' local entry base suffix base_dir',
|
|
168
|
+
' eval "set -- \\"\\${HOOK_ADAPTER_DIR_TIERS[@]}\\""',
|
|
169
|
+
' for entry in "$@"; do',
|
|
170
|
+
' entry="${entry#*|}"; base="${entry%%|*}"; suffix="${entry#*|}"',
|
|
171
|
+
' base_dir="$(_hook_tier_base "$base")"',
|
|
172
|
+
' [ -n "$base_dir" ] || continue',
|
|
173
|
+
' if [ -f "$base_dir/$suffix" ]; then printf "%s" "$base_dir"; return 0; fi',
|
|
174
|
+
' done',
|
|
175
|
+
' return 1',
|
|
176
|
+
'}',
|
|
177
|
+
'ADAPTER_DIR="$(_hook_adapter_dir)" || ADAPTER_DIR="$SCRIPT_DIR"',
|
|
178
|
+
'GATE_DIR="$(_hook_resolve_dir "$HOOK_GATE_PROBE" HOOK_GATE_TIERS)" || GATE_DIR=""',
|
|
179
|
+
'if [ -z "$GATE_DIR" ]; then GATE_DIR="$ADAPTER_DIR"; fi',
|
|
154
180
|
'echo "ADAPTER=$ADAPTER_DIR"',
|
|
155
181
|
'echo "GATE=$GATE_DIR"',
|
|
156
182
|
`if [ -f "$ADAPTER_DIR/adapter-common.sh" ]; then echo "COMMON=$ADAPTER_DIR/adapter-common.sh"; else echo "COMMON="; fi`,
|
|
157
183
|
`if [ -f "$GATE_DIR/${gateModule}" ]; then echo "MODULE=$GATE_DIR/${gateModule}"; else echo "MODULE="; fi`,
|
|
158
|
-
resolverText,
|
|
159
184
|
// Captured by substitution rather than read off the last stdout line: the
|
|
160
185
|
// script ends with a newline, so "last line" is always the empty one.
|
|
161
186
|
`echo "LANG=$(resolve_adapter_path ${lang} || true)"`,
|
|
@@ -194,14 +219,53 @@ function hookResolves(fnText, tree, lang) {
|
|
|
194
219
|
}
|
|
195
220
|
|
|
196
221
|
describe('doctor module resolution parity with pre-commit (#495)', () => {
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
222
|
+
// #505: the resolver helpers come from the hook, the tier DATA from the
|
|
223
|
+
// generated file. Concatenating them is what the hook itself does, so the
|
|
224
|
+
// bash side here is the production code path, not a paraphrase of it.
|
|
225
|
+
const helpers = [
|
|
226
|
+
extractFunction(PRE_COMMIT, '_hook_tier_base() {'),
|
|
227
|
+
extractFunction(PRE_COMMIT, '_hook_resolve() {'),
|
|
228
|
+
extractFunction(PRE_COMMIT, '_hook_resolve_dir() {'),
|
|
229
|
+
extractFunction(PRE_COMMIT, 'resolve_adapter_path() {'),
|
|
230
|
+
].join('\n');
|
|
231
|
+
// `blocks` keeps the names the scenarios use: the data file supplies the
|
|
232
|
+
// ADAPTER_DIR/GATE_DIR chains that used to be matched out of the hook text.
|
|
201
233
|
const blocks = {
|
|
202
|
-
adapterDir:
|
|
203
|
-
gateDir:
|
|
234
|
+
adapterDir: '',
|
|
235
|
+
gateDir: '',
|
|
204
236
|
};
|
|
237
|
+
const resolverText = `${RESOLUTION_DATA}\n${helpers}`;
|
|
238
|
+
|
|
239
|
+
it('AC-505-01: the generated shell data is current with its JS source', () => {
|
|
240
|
+
// Stale data would silently run a different order than doctor reports — the
|
|
241
|
+
// exact failure mode #505 exists to make impossible.
|
|
242
|
+
expect(() => execFileSync(
|
|
243
|
+
process.execPath,
|
|
244
|
+
[resolve(__dirname, '..', '..', '..', '..', 'scripts', 'generate-hook-resolution.cjs'), '--check'],
|
|
245
|
+
{ stdio: 'pipe' },
|
|
246
|
+
)).not.toThrow();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('AC-505-02: the hook loads the generated data instead of carrying its own order', () => {
|
|
250
|
+
// Anti-vacuity for the whole feature: if the hook re-grew a literal cascade,
|
|
251
|
+
// the single-source-of-truth claim would be false even while scenarios pass.
|
|
252
|
+
expect(PRE_COMMIT).toContain('hook-module-resolution.sh');
|
|
253
|
+
expect(PRE_COMMIT).toContain('HOOK_ADAPTER_TIERS');
|
|
254
|
+
expect(PRE_COMMIT).toContain('HOOK_GATE_TIERS');
|
|
255
|
+
// The old hand-written chains must be gone.
|
|
256
|
+
expect(PRE_COMMIT).not.toContain('$ADAPTER_DIR/${lang}.sh');
|
|
257
|
+
expect(PRE_COMMIT).not.toMatch(/if \[ -f "\$ADAPTER_DIR\/gate-3\.sh" \]; then/);
|
|
258
|
+
// And every tier the JS declares must appear in the emitted shell data.
|
|
259
|
+
for (const t of RESOLUTION_ORDER) {
|
|
260
|
+
expect(RESOLUTION_DATA).toContain(`${t.tier_id}|${t.base}|${t.template}`);
|
|
261
|
+
}
|
|
262
|
+
for (const t of GATE_RESOLUTION_ORDER) {
|
|
263
|
+
expect(RESOLUTION_DATA).toContain(`'${t.tier_id}|${t.base}|'`);
|
|
264
|
+
}
|
|
265
|
+
for (const t of ADAPTER_DIR_ORDER) {
|
|
266
|
+
expect(RESOLUTION_DATA).toContain(`${t.tier_id}|${t.base}|${t.probe}`);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
205
269
|
|
|
206
270
|
it('AC-495-11: flat and nested present -> both languages pick the flat copy', () => {
|
|
207
271
|
const { listExecutedModules } = require('../doctor');
|
|
@@ -241,8 +305,12 @@ describe('doctor module resolution parity with pre-commit (#495)', () => {
|
|
|
241
305
|
}
|
|
242
306
|
|
|
243
307
|
// Anti-vacuity: the guard executed the hook's real text, not a paraphrase.
|
|
244
|
-
|
|
245
|
-
|
|
308
|
+
// #505 moved the literal paths out of the hook, so assert that the tiers it
|
|
309
|
+
// now reads still describe the two layouts those paths used to spell out.
|
|
310
|
+
expect(resolverText).toContain('HOOK_ADAPTER_TIERS');
|
|
311
|
+
const adapterTemplates = RESOLUTION_ORDER.map((t) => t.template);
|
|
312
|
+
expect(adapterTemplates).toContain('{name}.sh');
|
|
313
|
+
expect(adapterTemplates).toContain('adapters/{name}.sh');
|
|
246
314
|
});
|
|
247
315
|
|
|
248
316
|
// The scenarios below put a copy of the same module in EVERY tier the hook can
|
|
@@ -143,6 +143,37 @@ describe('SprintStateManager', () => {
|
|
|
143
143
|
expect(result.phase_history[0].duration_seconds).toBeGreaterThanOrEqual(0);
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
it('stamps a duration when a phase goes straight from pending to completed', () => {
|
|
147
|
+
// Regression: `xp-gate phase-transition N completed --render` never emits
|
|
148
|
+
// an in_progress step, so gating started_at on in_progress left
|
|
149
|
+
// duration_seconds permanently null — the blank duration column on the
|
|
150
|
+
// status panel. Observed on sprint-20261019-47 phases 2-6.
|
|
151
|
+
const initialState = createInitialState(tmpDir);
|
|
152
|
+
manager.write(initialState);
|
|
153
|
+
|
|
154
|
+
const result = manager.transitionPhase(2, 'completed');
|
|
155
|
+
|
|
156
|
+
expect(result.phase_history[0].status).toBe('completed');
|
|
157
|
+
expect(result.phase_history[0].started_at).toBeDefined();
|
|
158
|
+
expect(result.phase_history[0].completed_at).toBeDefined();
|
|
159
|
+
expect(result.phase_history[0].duration_seconds).not.toBeNull();
|
|
160
|
+
expect(result.phase_history[0].duration_seconds).toBeGreaterThanOrEqual(0);
|
|
161
|
+
expect(Number.isNaN(result.phase_history[0].duration_seconds)).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('never writes a NaN duration, even for an unparseable started_at', () => {
|
|
165
|
+
const state = createInitialState(tmpDir);
|
|
166
|
+
state.phase_history = [
|
|
167
|
+
{ phase: 2, phase_name: 'DESIGN', status: 'pending', started_at: 'not-a-date' },
|
|
168
|
+
];
|
|
169
|
+
manager.write(state);
|
|
170
|
+
|
|
171
|
+
const result = manager.transitionPhase(2, 'completed');
|
|
172
|
+
|
|
173
|
+
expect(Number.isNaN(result.phase_history[0].duration_seconds)).toBe(false);
|
|
174
|
+
expect(result.phase_history[0].duration_seconds).toBe(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
146
177
|
it('calls onTransition callback', () => {
|
|
147
178
|
const initialState = createInitialState(tmpDir);
|
|
148
179
|
manager.write(initialState);
|
package/lib/doctor.js
CHANGED
|
@@ -15,6 +15,9 @@ const {
|
|
|
15
15
|
} = require('./shared-paths.js');
|
|
16
16
|
const { checkUpgrade, formatUpgradeMsg } = require('./check-version.js');
|
|
17
17
|
const { GATE_CLI_TOOLS, checkCliTool, getToolInstallCmd, checkBash } = require('./detect-deps.js');
|
|
18
|
+
// #505: the single source of truth for module resolution order. The hook reads a
|
|
19
|
+
// generated shell projection of this same module.
|
|
20
|
+
const { RESOLUTION_ORDER } = require('./hook-module-resolution.js');
|
|
18
21
|
const {
|
|
19
22
|
diagnoseTuiRegistration, diagnoseInstalledSkills,
|
|
20
23
|
fixTuiRegistration, ensureTuiRegistration, readTuiJson,
|
|
@@ -398,6 +401,16 @@ function pathsEquivalent(a, b) {
|
|
|
398
401
|
*/
|
|
399
402
|
const DRIFT_COMPARED_HOOKS = ['pre-commit', 'pre-push', 'post-merge'];
|
|
400
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Non-hook files that must sit beside the installed hooks.
|
|
406
|
+
*
|
|
407
|
+
* These are not Git entry points, so they are not in DRIFT_COMPARED_HOOKS, but
|
|
408
|
+
* the hooks source them and die without them:
|
|
409
|
+
* - hook-module-resolution.sh (#505): pre-commit aborts FATAL when the module
|
|
410
|
+
* resolution order is missing. It is generated from lib/hook-module-resolution.js.
|
|
411
|
+
*/
|
|
412
|
+
const HOOK_SUPPORT_FILES = ['hook-module-resolution.sh'];
|
|
413
|
+
|
|
401
414
|
/**
|
|
402
415
|
* Compare the hooks that actually execute (the installed copy under
|
|
403
416
|
* core.hooksPath) against the repository's source-of-truth `githooks/` (#451).
|
|
@@ -546,35 +559,35 @@ function listExecutedModules(installedAdapterDir) {
|
|
|
546
559
|
const found = [];
|
|
547
560
|
const seen = new Set();
|
|
548
561
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
562
|
+
// #505: the tier order is DATA, not a second implementation. The hook reads a
|
|
563
|
+
// generated shell projection of the same module; both reference
|
|
564
|
+
// RESOLUTION_ORDER, so "which copy executes" has one answer. Previously this
|
|
565
|
+
// function re-derived the order by hand and a parity test was the only thing
|
|
566
|
+
// keeping it honest.
|
|
567
|
+
//
|
|
568
|
+
// Tiers are walked in order; a name is reported once, from the first tier that
|
|
569
|
+
// resolves it — matching the hook, which stops at the first existing file.
|
|
570
|
+
for (const tier of RESOLUTION_ORDER) {
|
|
571
|
+
// Only the two ADAPTER_DIR-rooted tiers describe the installed adapter dir.
|
|
572
|
+
if (tier.base !== 'ADAPTER_DIR') continue;
|
|
573
|
+
// `{name}.sh` (flat) -> the dir itself; `adapters/{name}.sh` -> <dir>/adapters.
|
|
574
|
+
const dir = tier.template.includes('/')
|
|
575
|
+
? path.join(installedAdapterDir, tier.template.split('/{name}.sh')[0])
|
|
576
|
+
: installedAdapterDir;
|
|
555
577
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
578
|
+
let entries;
|
|
579
|
+
try {
|
|
580
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
581
|
+
} catch {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
563
584
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
} catch {
|
|
571
|
-
nestedEntries = [];
|
|
572
|
-
}
|
|
573
|
-
for (const entry of nestedEntries) {
|
|
574
|
-
if (!entry.isFile() || !entry.name.endsWith('.sh')) continue;
|
|
575
|
-
if (seen.has(entry.name)) continue;
|
|
576
|
-
seen.add(entry.name);
|
|
577
|
-
found.push({ name: entry.name, file: path.join(nestedDir, entry.name) });
|
|
585
|
+
for (const entry of entries) {
|
|
586
|
+
if (!entry.isFile() || !entry.name.endsWith('.sh')) continue;
|
|
587
|
+
if (seen.has(entry.name)) continue;
|
|
588
|
+
seen.add(entry.name);
|
|
589
|
+
found.push({ name: entry.name, file: path.join(dir, entry.name) });
|
|
590
|
+
}
|
|
578
591
|
}
|
|
579
592
|
|
|
580
593
|
return found;
|
|
@@ -1153,6 +1166,26 @@ function syncGlobalHooksFromRepo(ctx = {}) {
|
|
|
1153
1166
|
}
|
|
1154
1167
|
}
|
|
1155
1168
|
|
|
1169
|
+
// Hook support files (#505): sourced by the hooks but not Git entry points, so
|
|
1170
|
+
// they need the same copy treatment or an upgraded install dies at the source
|
|
1171
|
+
// line. Treated like the hooks themselves (always refreshed, no refusal) —
|
|
1172
|
+
// they hold no machine-local content.
|
|
1173
|
+
for (const name of HOOK_SUPPORT_FILES) {
|
|
1174
|
+
const source = path.join(repoHooks, name);
|
|
1175
|
+
if (!fs.existsSync(source)) continue;
|
|
1176
|
+
const target = path.join(installedHooks, name);
|
|
1177
|
+
try {
|
|
1178
|
+
if (fs.existsSync(target) && filesAreIdentical(source, target)) {
|
|
1179
|
+
skipped.push(name);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
fs.copyFileSync(source, target);
|
|
1183
|
+
synced.push(name);
|
|
1184
|
+
} catch (err) {
|
|
1185
|
+
errors.push(`${name}: ${err.message}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1156
1189
|
// lib/ shared libraries: pre-commit sources lib/*.sh, so a synced hook file
|
|
1157
1190
|
// without its libraries dies at the source line on the machine (#488).
|
|
1158
1191
|
const repoLib = path.join(repoHooks, 'lib');
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Single source of truth for hook module resolution order (#505).
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM THIS SOLVES
|
|
6
|
+
* The order in which a hook finds its modules (adapter-common.sh, gate-*.sh,
|
|
7
|
+
* lib/*.sh, language adapters) used to be written out twice: once in shell
|
|
8
|
+
* (`resolve_adapter_path()` and the `GATE_DIR` cascade in githooks/pre-commit)
|
|
9
|
+
* and once in JavaScript (`listExecutedModules()` in lib/doctor.js). The two must
|
|
10
|
+
* agree or doctor reports a different module surface than the one that actually
|
|
11
|
+
* executes — exactly the divergence behind #495/#500. A parity test pinned the
|
|
12
|
+
* behaviour, but a test only fails AFTER someone writes the wrong thing; the
|
|
13
|
+
* order itself was still duplicated.
|
|
14
|
+
*
|
|
15
|
+
* THE FIX
|
|
16
|
+
* The order lives here, in RESOLUTION_ORDER, as data. Both sides read it:
|
|
17
|
+
* - JS: require('./hook-module-resolution.js') (doctor.js)
|
|
18
|
+
* - SH: the hook sources hook-module-resolution.sh, GENERATED from this file
|
|
19
|
+
* by `node scripts/generate-hook-resolution.sh` (run by sync-package-content).
|
|
20
|
+
* A change to the order reaches the shell automatically; drifting requires
|
|
21
|
+
* deliberately ignoring the generator.
|
|
22
|
+
*
|
|
23
|
+
* SCOPE NOTE: this is the ADAPTER/GATE lookup order only. It does not change
|
|
24
|
+
* which files are installed or how they are copied.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ordered resolution tiers. The first tier that contains the requested file wins.
|
|
29
|
+
*
|
|
30
|
+
* `base` names which directory variable supplies the root; `template` is the path
|
|
31
|
+
* relative to it, with `{name}` standing for the module basename.
|
|
32
|
+
*
|
|
33
|
+
* `tier_id` is stable and is what shell and JS compare on, so both can report the
|
|
34
|
+
* same tier for the same file without string-matching paths.
|
|
35
|
+
*/
|
|
36
|
+
const RESOLUTION_ORDER = Object.freeze([
|
|
37
|
+
Object.freeze({
|
|
38
|
+
tier_id: 'adapter_flat',
|
|
39
|
+
base: 'ADAPTER_DIR',
|
|
40
|
+
template: '{name}.sh',
|
|
41
|
+
comment: 'Global install layout: modules sit flat next to adapter-common.sh',
|
|
42
|
+
}),
|
|
43
|
+
Object.freeze({
|
|
44
|
+
tier_id: 'adapter_nested',
|
|
45
|
+
base: 'ADAPTER_DIR',
|
|
46
|
+
template: 'adapters/{name}.sh',
|
|
47
|
+
comment: 'Nested layout under the adapter dir',
|
|
48
|
+
}),
|
|
49
|
+
Object.freeze({
|
|
50
|
+
tier_id: 'project_nested',
|
|
51
|
+
base: 'PROJECT_GITHOOKS',
|
|
52
|
+
template: 'adapters/{name}.sh',
|
|
53
|
+
comment: 'Project githooks adapters/',
|
|
54
|
+
}),
|
|
55
|
+
Object.freeze({
|
|
56
|
+
tier_id: 'script_nested',
|
|
57
|
+
base: 'SCRIPT_DIR',
|
|
58
|
+
template: 'adapters/{name}.sh',
|
|
59
|
+
comment: 'Hook directory fallback',
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Order used when resolving GATE scripts. Gates are looked up flat first
|
|
65
|
+
* (co-located with adapter-common.sh), then in the project githooks root.
|
|
66
|
+
*/
|
|
67
|
+
const GATE_RESOLUTION_ORDER = Object.freeze([
|
|
68
|
+
Object.freeze({
|
|
69
|
+
tier_id: 'gate_adapter_dir',
|
|
70
|
+
base: 'ADAPTER_DIR',
|
|
71
|
+
template: '{name}',
|
|
72
|
+
comment: 'Gates co-located with adapter-common.sh',
|
|
73
|
+
}),
|
|
74
|
+
Object.freeze({
|
|
75
|
+
tier_id: 'gate_project',
|
|
76
|
+
base: 'PROJECT_GITHOOKS',
|
|
77
|
+
template: '{name}',
|
|
78
|
+
comment: 'Project githooks/',
|
|
79
|
+
}),
|
|
80
|
+
Object.freeze({
|
|
81
|
+
tier_id: 'gate_script_dir',
|
|
82
|
+
base: 'SCRIPT_DIR',
|
|
83
|
+
template: '{name}',
|
|
84
|
+
comment: 'Hook directory fallback',
|
|
85
|
+
}),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Order in which the hook picks the directory that holds adapter-common.sh
|
|
90
|
+
* (ADAPTER_DIR). Same reasoning as RESOLUTION_ORDER: this cascade decides which
|
|
91
|
+
* tree every later lookup searches, so it must have one definition.
|
|
92
|
+
*
|
|
93
|
+
* `probe` is the file whose presence in `base` makes that tier win.
|
|
94
|
+
*/
|
|
95
|
+
const ADAPTER_DIR_ORDER = Object.freeze([
|
|
96
|
+
Object.freeze({
|
|
97
|
+
tier_id: 'global',
|
|
98
|
+
base: 'GLOBAL_ADAPTER_DIR',
|
|
99
|
+
probe: 'adapter-common.sh',
|
|
100
|
+
comment: '~/.config/xp-gate/adapters — global install',
|
|
101
|
+
}),
|
|
102
|
+
Object.freeze({
|
|
103
|
+
tier_id: 'project',
|
|
104
|
+
base: 'PROJECT_GITHOOKS',
|
|
105
|
+
probe: 'adapter-common.sh',
|
|
106
|
+
comment: '<repo>/githooks — per-project init',
|
|
107
|
+
}),
|
|
108
|
+
Object.freeze({
|
|
109
|
+
tier_id: 'script',
|
|
110
|
+
base: 'SCRIPT_DIR',
|
|
111
|
+
probe: 'adapter-common.sh',
|
|
112
|
+
comment: 'Hook directory fallback',
|
|
113
|
+
}),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The probe file used to decide which tier supplies gate scripts. It is the
|
|
118
|
+
* first gate, so it is present wherever the gate suite is installed.
|
|
119
|
+
*/
|
|
120
|
+
const GATE_PROBE = 'gate-3.sh';
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
RESOLUTION_ORDER,
|
|
124
|
+
GATE_RESOLUTION_ORDER,
|
|
125
|
+
ADAPTER_DIR_ORDER,
|
|
126
|
+
GATE_PROBE,
|
|
127
|
+
};
|
package/lib/retro.js
CHANGED
|
@@ -23,6 +23,29 @@ const { execSync } = require('child_process');
|
|
|
23
23
|
*/
|
|
24
24
|
const MAX_SANE_DURATION_MS = 2 * 60 * 60 * 1000;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Whether an audit entry's duration_ms is a trustworthy sample.
|
|
28
|
+
*
|
|
29
|
+
* `e.duration_ms || 0` used to fold null into ZERO, which both dragged the mean
|
|
30
|
+
* down and hid that the sample was missing — the same class of lie as the
|
|
31
|
+
* epoch-sized values that started this line of work.
|
|
32
|
+
*
|
|
33
|
+
* BOTH signals must be honoured, not just one:
|
|
34
|
+
* - duration_anomaly: entries written before #475 carry an explicit flag.
|
|
35
|
+
* Dropping this check silently re-admitted every legacy poisoned sample, and
|
|
36
|
+
* the fixture average came out 33433ms instead of 150ms.
|
|
37
|
+
* - non-finite / out-of-range: a poisoned start now writes null or a
|
|
38
|
+
* nonsensical value outright, so those are excluded too.
|
|
39
|
+
*
|
|
40
|
+
* @param {{duration_ms?: number|null, duration_anomaly?: boolean}} entry
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
function isUsableDuration(entry) {
|
|
44
|
+
if (entry.duration_anomaly) return false;
|
|
45
|
+
const ms = entry.duration_ms;
|
|
46
|
+
return Number.isFinite(ms) && ms >= 0 && ms <= MAX_SANE_DURATION_MS;
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
/**
|
|
27
50
|
* Get git commit subjects within a date window.
|
|
28
51
|
* @param {string} dir - Repository root
|
|
@@ -113,13 +136,7 @@ function buildReport(opts) {
|
|
|
113
136
|
L.push('## Activity Summary');
|
|
114
137
|
const gateEntries = audit.filter(e => e.gate_id);
|
|
115
138
|
const passed = gateEntries.filter(e => e.passed).length;
|
|
116
|
-
|
|
117
|
-
// `e.duration_ms || 0` used to fold null into ZERO, which both dragged the
|
|
118
|
-
// mean down and hid the fact that the sample was missing — the same class of
|
|
119
|
-
// lie as the epoch-sized values this sprint removed. The legacy
|
|
120
|
-
// duration_anomaly flag is no longer the only signal: a poisoned start now
|
|
121
|
-
// writes null outright, so anything null must be excluded and counted.
|
|
122
|
-
const measurable = gateEntries.filter(e => Number.isFinite(e.duration_ms) && e.duration_ms >= 0 && e.duration_ms <= MAX_SANE_DURATION_MS);
|
|
139
|
+
const measurable = gateEntries.filter(isUsableDuration);
|
|
123
140
|
const excluded = gateEntries.length - measurable.length;
|
|
124
141
|
const nullDurations = gateEntries.filter(e => e.duration_ms === null).length;
|
|
125
142
|
const avgMs = measurable.length > 0
|
|
@@ -30,16 +30,24 @@ function applyHistoryEntry(state, phase, phaseName, status) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
entry.status = status;
|
|
33
|
-
|
|
33
|
+
// Stamp started_at on FIRST sight of any status, not only on `in_progress`.
|
|
34
|
+
// Phases are routinely moved straight from pending to completed (the `--render`
|
|
35
|
+
// path, and any sprint where the agent does not explicitly mark in_progress),
|
|
36
|
+
// so gating this on `in_progress` left started_at unset and duration_seconds
|
|
37
|
+
// permanently null — the empty-duration column on the status panel.
|
|
38
|
+
if (!entry.started_at) {
|
|
34
39
|
entry.started_at = new Date().toISOString();
|
|
35
40
|
}
|
|
36
41
|
if (status === 'completed' || status === 'skipped') {
|
|
37
42
|
entry.completed_at = new Date().toISOString();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
const startMs = new Date(entry.started_at).getTime();
|
|
44
|
+
const endMs = new Date(entry.completed_at).getTime();
|
|
45
|
+
// Never emit NaN/negative: a bad or future started_at degrades to 0, which
|
|
46
|
+
// the audit treats as "unknown" rather than inventing a number.
|
|
47
|
+
const elapsed = endMs - startMs;
|
|
48
|
+
entry.duration_seconds = Number.isFinite(elapsed) && elapsed >= 0
|
|
49
|
+
? Math.round(elapsed / 1000)
|
|
50
|
+
: 0;
|
|
43
51
|
}
|
|
44
52
|
return entry;
|
|
45
53
|
}
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-20
|
|
4
|
+
**Commit:** 81dd7464
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.30.0.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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-20
|
|
4
|
+
**Commit:** 81dd7464
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.30.0.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.30.0",
|
|
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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-20
|
|
4
|
+
**Commit:** 81dd7464
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.30.0.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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-20
|
|
4
|
+
**Commit:** 81dd7464
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.30.0.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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-20
|
|
4
|
+
**Commit:** 81dd7464
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.30.0.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.
|
|
3
|
+
"version": "0.30.0",
|
|
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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-20
|
|
4
|
+
**Commit:** 81dd7464
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.30.0.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-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-20
|
|
4
|
+
**Commit:** 81dd7464
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.30.0.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.
|