@boyingliu01/xp-gate 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adapter-common.sh +51 -2
- package/adapters/python.sh +63 -5
- package/hooks/adapter-common.sh +51 -2
- package/hooks/lib/coverage-total.sh +20 -0
- package/hooks/lib/python-incremental-typecheck.sh +104 -0
- package/hooks/lib/python-test-selection.sh +132 -0
- package/hooks/lib/typecheck.sh +10 -0
- package/hooks/pre-commit +157 -33
- package/lib/__tests__/init-gitleaks-notice.test.js +57 -0
- package/lib/__tests__/issue-519-phase-transition-guard.test.js +233 -0
- package/lib/init.js +46 -1
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/phase-transition.js +61 -0
- package/lib/sprint-state-manager.js +86 -39
- package/lib/sprint-state-migrator.js +60 -22
- package/mock-policy/AGENTS.md +3 -3
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/gremlins-runner.test.ts +167 -0
- package/mutation/__tests__/mutmut-runner.test.ts +20 -22
- package/mutation/__tests__/runners-index.test.ts +31 -3
- package/mutation/gate-m.ts +22 -4
- package/mutation/runners/gremlins-runner.ts +238 -0
- package/mutation/runners/index.ts +9 -1
- package/mutation/runners/mutmut-runner.ts +18 -12
- package/mutation/runners/types.ts +20 -0
- 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/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/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/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/delphi-review/AGENTS.md +3 -3
- 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/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
runnerRegistry,
|
|
11
11
|
registerAllRunners,
|
|
12
12
|
resolveRunner,
|
|
13
|
+
resolveRunners,
|
|
13
14
|
StrykerRunner,
|
|
14
15
|
MutmutRunner,
|
|
15
16
|
GoMutantRunner,
|
|
@@ -32,11 +33,12 @@ describe('runners/index', () => {
|
|
|
32
33
|
expect(runnerRegistry).toBeInstanceOf(Map);
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
it('registerAllRunners() registers all
|
|
36
|
+
it('registerAllRunners() registers all 5 runners', () => {
|
|
36
37
|
registerAllRunners();
|
|
37
38
|
|
|
38
|
-
expect(runnerRegistry.size).toBe(
|
|
39
|
+
expect(runnerRegistry.size).toBe(5);
|
|
39
40
|
expect(runnerRegistry.has('Stryker')).toBe(true);
|
|
41
|
+
expect(runnerRegistry.has('pytest-gremlins')).toBe(true);
|
|
40
42
|
expect(runnerRegistry.has('mutmut')).toBe(true);
|
|
41
43
|
expect(runnerRegistry.has('gomutants')).toBe(true);
|
|
42
44
|
expect(runnerRegistry.has('pitest')).toBe(true);
|
|
@@ -46,6 +48,7 @@ describe('runners/index', () => {
|
|
|
46
48
|
registerAllRunners();
|
|
47
49
|
|
|
48
50
|
expect(runnerRegistry.get('Stryker')).toBeInstanceOf(StrykerRunner);
|
|
51
|
+
expect(runnerRegistry.get('pytest-gremlins')).toBeDefined();
|
|
49
52
|
expect(runnerRegistry.get('mutmut')).toBeInstanceOf(MutmutRunner);
|
|
50
53
|
expect(runnerRegistry.get('gomutants')).toBeInstanceOf(GoMutantRunner);
|
|
51
54
|
expect(runnerRegistry.get('pitest')).toBeInstanceOf(PitestRunner);
|
|
@@ -59,7 +62,7 @@ describe('runners/index', () => {
|
|
|
59
62
|
it.each([
|
|
60
63
|
['ts', 'Stryker'],
|
|
61
64
|
['tsx', 'Stryker'],
|
|
62
|
-
['py', '
|
|
65
|
+
['py', 'pytest-gremlins'],
|
|
63
66
|
['go', 'gomutants'],
|
|
64
67
|
['java', 'pitest'],
|
|
65
68
|
['kt', 'pitest'],
|
|
@@ -83,6 +86,31 @@ describe('runners/index', () => {
|
|
|
83
86
|
});
|
|
84
87
|
});
|
|
85
88
|
|
|
89
|
+
describe('resolveRunners()', () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
registerAllRunners();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns the py runners in registration (priority) order for ".py"', () => {
|
|
95
|
+
const runners = resolveRunners('.py');
|
|
96
|
+
expect(runners.map(r => r.name)).toEqual(['pytest-gremlins', 'mutmut']);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns a single-element list for extensions with one runner', () => {
|
|
100
|
+
expect(resolveRunners('.go').map(r => r.name)).toEqual(['gomutants']);
|
|
101
|
+
expect(resolveRunners('.ts').map(r => r.name)).toEqual(['Stryker']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns an empty list for unknown extensions', () => {
|
|
105
|
+
expect(resolveRunners('.rb')).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('handles dot-prefixed extensions', () => {
|
|
109
|
+
const runners = resolveRunners('.py');
|
|
110
|
+
expect(runners.length).toBeGreaterThan(1);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
86
114
|
describe('re-exports', () => {
|
|
87
115
|
it('re-exports all runner classes', () => {
|
|
88
116
|
expect(StrykerRunner).toBeDefined();
|
package/mutation/gate-m.ts
CHANGED
|
@@ -12,8 +12,10 @@ import {
|
|
|
12
12
|
import { detectAITestCharacteristics } from './detect-ai-test';
|
|
13
13
|
import {
|
|
14
14
|
resolveRunner,
|
|
15
|
+
resolveRunners,
|
|
15
16
|
registerAllRunners,
|
|
16
17
|
MutationRunOutcome,
|
|
18
|
+
MutationRunner,
|
|
17
19
|
} from './runners';
|
|
18
20
|
|
|
19
21
|
// ── Pre-run auto-registration of all language runners ──
|
|
@@ -440,10 +442,14 @@ async function runAllRunners(
|
|
|
440
442
|
const fileScores: Record<string, number | null> = {};
|
|
441
443
|
|
|
442
444
|
for (const [, files] of Array.from(groups.entries())) {
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
445
|
+
const ext = path.extname(files[0]);
|
|
446
|
+
// #410 fallback: walk ALL runners for the extension in priority order and
|
|
447
|
+
// use the first one that is actually available. A pure-mutmut environment
|
|
448
|
+
// (no pytest-gremlins) thus still gets mutation testing instead of a
|
|
449
|
+
// silent SKIP — the design contract "gremlins 可用→GremlinsRunner,否则
|
|
450
|
+
// MutmutRunner" is honored here, not by registration order alone.
|
|
451
|
+
const runner = await resolveFirstAvailable(ext);
|
|
452
|
+
if (!runner) {
|
|
447
453
|
applyNullScores(files, fileScores);
|
|
448
454
|
continue;
|
|
449
455
|
}
|
|
@@ -455,6 +461,18 @@ async function runAllRunners(
|
|
|
455
461
|
return fileScores;
|
|
456
462
|
}
|
|
457
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Pick the first AVAILABLE runner for an extension, honoring registration
|
|
466
|
+
* priority. Returns undefined when no registered runner for the extension is
|
|
467
|
+
* installed on this machine.
|
|
468
|
+
*/
|
|
469
|
+
async function resolveFirstAvailable(ext: string): Promise<MutationRunner | undefined> {
|
|
470
|
+
for (const candidate of resolveRunners(ext)) {
|
|
471
|
+
if (await candidate.isAvailable()) return candidate;
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
|
|
458
476
|
function applyNullScores(files: string[], scores: Record<string, number | null>): void {
|
|
459
477
|
for (const f of files) scores[f] = null;
|
|
460
478
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// @no-test-required: gremlins wrapper — tested via gremlins-runner.test.ts + pre-push Gate M
|
|
2
|
+
import { spawn, execSync, type ChildProcess } from 'child_process';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
import type {
|
|
5
|
+
MutationRunner,
|
|
6
|
+
RunMutationOptions,
|
|
7
|
+
MutationRunOutcome,
|
|
8
|
+
MutationRunResult,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* pytest-gremlins mutation runner for Python files (#410).
|
|
13
|
+
*
|
|
14
|
+
* Why a second Python engine exists: mutmut 3.x is built on fork(), so on
|
|
15
|
+
* Windows it can only run under WSL — a native Windows user had a structural
|
|
16
|
+
* deadlock (installed => `mutmut run` exits 1 => push blocked; uninstalled =>
|
|
17
|
+
* Gate M skips). pytest-gremlins uses mutation *switching* (instrument once,
|
|
18
|
+
* flip an env var per mutant) with no fork and no per-mutant file I/O, making
|
|
19
|
+
* it natively cross-platform. It also matches Gate M's needs: coverage-guided
|
|
20
|
+
* test selection, an incremental content-hash cache, parallel workers and
|
|
21
|
+
* console/HTML/JSON reports.
|
|
22
|
+
*
|
|
23
|
+
* Constraint: pytest-gremlins requires Python >= 3.11. On older interpreters
|
|
24
|
+
* the import probe simply fails and the caller keeps the mutmut path (or
|
|
25
|
+
* SKIPs) — no new hard dependency.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* How long to wait after SIGTERM before escalating to SIGKILL. pytest-gremlins
|
|
29
|
+
* runs a whole pytest session per invocation, so a child that ignores SIGTERM
|
|
30
|
+
* (stuck in a C extension) still needs a hard backstop.
|
|
31
|
+
*/
|
|
32
|
+
const SIGKILL_ESCALATION_MS = 5000;
|
|
33
|
+
|
|
34
|
+
/** Bound on the one-shot import probe; a hung interpreter must not stall Gate M. */
|
|
35
|
+
const PROBE_TIMEOUT_MS = 10000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Exactly-once resolve bookkeeping for a spawned mutation run.
|
|
39
|
+
*
|
|
40
|
+
* A child can report `close`, `error`, a timeout, and the SIGKILL escalation in
|
|
41
|
+
* any interleaving; without a single latched gate the Promise resolves twice
|
|
42
|
+
* (subsequent resolves are silent no-ops, but the *first* one wins, which can
|
|
43
|
+
* be the wrong verdict). `settled` makes the ordering irrelevant. Extracted
|
|
44
|
+
* from `spawnGremlins` so that function stays under the long-function budget.
|
|
45
|
+
*
|
|
46
|
+
* A factory rather than a class: it holds no collaborators to inject, and the
|
|
47
|
+
* SOLID analyzer flags `new X()` as a dependency-inversion smell regardless of
|
|
48
|
+
* whether the type is injectable.
|
|
49
|
+
*/
|
|
50
|
+
function createSettleGuard(resolve: (o: MutationRunOutcome) => void) {
|
|
51
|
+
let settled = false;
|
|
52
|
+
let escalationId: NodeJS.Timeout | undefined;
|
|
53
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
54
|
+
|
|
55
|
+
const escalateToKill = (child: ChildProcess): void => {
|
|
56
|
+
if (!settled) child.kill('SIGKILL');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const guard = {
|
|
60
|
+
timedOut: false,
|
|
61
|
+
/** SIGTERM at `timeoutMs`, escalating to SIGKILL if the child ignores it. */
|
|
62
|
+
armTimeout(child: ChildProcess, timeoutMs: number): void {
|
|
63
|
+
timeoutId = setTimeout(() => {
|
|
64
|
+
guard.timedOut = true;
|
|
65
|
+
child.kill('SIGTERM');
|
|
66
|
+
if (settled) return;
|
|
67
|
+
escalationId = setTimeout(() => escalateToKill(child), SIGKILL_ESCALATION_MS);
|
|
68
|
+
}, timeoutMs);
|
|
69
|
+
},
|
|
70
|
+
settle(outcome: MutationRunOutcome): void {
|
|
71
|
+
if (settled) return;
|
|
72
|
+
settled = true;
|
|
73
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
74
|
+
if (escalationId) clearTimeout(escalationId);
|
|
75
|
+
resolve(outcome);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
return guard;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class GremlinsRunner implements MutationRunner {
|
|
82
|
+
readonly name = 'pytest-gremlins';
|
|
83
|
+
readonly extensions = ['py'];
|
|
84
|
+
|
|
85
|
+
async isAvailable(): Promise<boolean> {
|
|
86
|
+
try {
|
|
87
|
+
execSync('python -c "import pytest_gremlins"', {
|
|
88
|
+
stdio: 'pipe',
|
|
89
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
90
|
+
});
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async run(options: RunMutationOptions): Promise<MutationRunOutcome> {
|
|
98
|
+
if (!(await this.isAvailable())) {
|
|
99
|
+
return { report: null, timedOut: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Spawning can throw synchronously (e.g. ENOENT on a broken interpreter
|
|
103
|
+
// shim); that must surface as a null report, never as an unhandled
|
|
104
|
+
// rejection that aborts the whole Gate M orchestration.
|
|
105
|
+
try {
|
|
106
|
+
return await this.spawnGremlins(options);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return {
|
|
109
|
+
report: null,
|
|
110
|
+
timedOut: false,
|
|
111
|
+
error: err instanceof Error ? err.message : String(err),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the argv for `python -m pytest`.
|
|
118
|
+
*
|
|
119
|
+
* `-m pytest` rather than a bare `pytest`: the import probe in
|
|
120
|
+
* `isAvailable()` already proved *this* interpreter carries the plugin, so
|
|
121
|
+
* running through the same interpreter is the only way to be sure the plugin
|
|
122
|
+
* is actually loaded.
|
|
123
|
+
*/
|
|
124
|
+
private gremlinsArgs(options: RunMutationOptions): string[] {
|
|
125
|
+
return [
|
|
126
|
+
'-m',
|
|
127
|
+
'pytest',
|
|
128
|
+
'--gremlins',
|
|
129
|
+
`--gremlin-targets=${buildTargets(options.files)}`,
|
|
130
|
+
'--gremlin-report=console',
|
|
131
|
+
'--gremlin-parallel',
|
|
132
|
+
'--gremlin-cache',
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Spawn the child and settle exactly once, on close or on error. */
|
|
137
|
+
private spawnGremlins(options: RunMutationOptions): Promise<MutationRunOutcome> {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const guard = createSettleGuard(resolve);
|
|
140
|
+
let child: ChildProcess;
|
|
141
|
+
try {
|
|
142
|
+
child = spawn('python', this.gremlinsArgs(options), {
|
|
143
|
+
stdio: 'pipe',
|
|
144
|
+
cwd: options.cwd,
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
guard.settle({
|
|
148
|
+
report: null,
|
|
149
|
+
timedOut: false,
|
|
150
|
+
error: err instanceof Error ? err.message : String(err),
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let stdout = '';
|
|
156
|
+
let stderr = '';
|
|
157
|
+
guard.armTimeout(child, options.timeoutMs);
|
|
158
|
+
|
|
159
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
160
|
+
stdout += data.toString();
|
|
161
|
+
});
|
|
162
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
163
|
+
stderr += data.toString();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
child.on('close', (code) => {
|
|
167
|
+
// A missing report is NOT a failure verdict: parse returns null and the
|
|
168
|
+
// orchestrator keeps its non-blocking semantics (same contract as the
|
|
169
|
+
// mutmut runner's null report).
|
|
170
|
+
guard.settle({
|
|
171
|
+
report: parseConsoleReport(stdout),
|
|
172
|
+
timedOut: guard.timedOut,
|
|
173
|
+
error: code !== 0 ? stderr || stdout : undefined,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
child.on('error', (err) => {
|
|
178
|
+
guard.settle({ report: null, timedOut: guard.timedOut, error: err.message });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Comma-separated mutate targets, deduplicated, derived from the changed
|
|
186
|
+
* files' directories — `--gremlin-targets` accepts files OR directories, and
|
|
187
|
+
* the directories keep the mutant count bounded to the touched area.
|
|
188
|
+
*
|
|
189
|
+
* Module-level and pure so it is directly testable without instantiating the
|
|
190
|
+
* runner.
|
|
191
|
+
*/
|
|
192
|
+
function buildTargets(files: string[]): string {
|
|
193
|
+
const dirs = new Set<string>();
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
const dir = dirname(file);
|
|
196
|
+
dirs.add(dir && dir !== '.' ? dir : file);
|
|
197
|
+
}
|
|
198
|
+
return Array.from(dirs).join(',');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parse pytest-gremlins' console report.
|
|
203
|
+
*
|
|
204
|
+
* The report prints two lines (plus a banner):
|
|
205
|
+
* Zapped: 142 gremlins (89%)
|
|
206
|
+
* Survived: 18 gremlins (11%)
|
|
207
|
+
*
|
|
208
|
+
* Returns null when neither line is present — a run that never got to the
|
|
209
|
+
* report (import failure, collection error, timeout) must not be reported as
|
|
210
|
+
* a 0% score, which would be a fabricated verdict. `Zapped` maps to killed,
|
|
211
|
+
* `Survived` to survived; the totals are their sum, matching the mutmut
|
|
212
|
+
* runner's normalization exactly.
|
|
213
|
+
*/
|
|
214
|
+
export function parseConsoleReport(stdout: string): MutationRunResult | null {
|
|
215
|
+
const zapped = extractCount(stdout, /Zapped:\s*(\d+)\s*gremlins?/i);
|
|
216
|
+
const survived = extractCount(stdout, /Survived:\s*(\d+)\s*gremlins?/i);
|
|
217
|
+
|
|
218
|
+
if (zapped === null && survived === null) return null;
|
|
219
|
+
|
|
220
|
+
const nrOfKilledMutants = zapped ?? 0;
|
|
221
|
+
const nrOfSurvivedMutants = survived ?? 0;
|
|
222
|
+
const nrOfMutants = nrOfKilledMutants + nrOfSurvivedMutants;
|
|
223
|
+
if (nrOfMutants === 0) return null;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
mutationScore: (nrOfKilledMutants / nrOfMutants) * 100,
|
|
227
|
+
nrOfMutants,
|
|
228
|
+
nrOfKilledMutants,
|
|
229
|
+
nrOfSurvivedMutants,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function extractCount(text: string, pattern: RegExp): number | null {
|
|
234
|
+
const match = text.match(pattern);
|
|
235
|
+
if (!match?.[1]) return null;
|
|
236
|
+
const value = parseInt(match[1], 10);
|
|
237
|
+
return Number.isNaN(value) ? null : value;
|
|
238
|
+
}
|
|
@@ -7,15 +7,17 @@ export type {
|
|
|
7
7
|
MutationRunner,
|
|
8
8
|
} from './types';
|
|
9
9
|
|
|
10
|
-
export { registerRunner, resolveRunner, runnerRegistry } from './types';
|
|
10
|
+
export { registerRunner, resolveRunner, resolveRunners, runnerRegistry } from './types';
|
|
11
11
|
|
|
12
12
|
export { StrykerRunner } from './stryker-runner';
|
|
13
13
|
export { MutmutRunner } from './mutmut-runner';
|
|
14
|
+
export { GremlinsRunner } from './gremlins-runner';
|
|
14
15
|
export { GoMutantRunner } from './go-mutant-runner';
|
|
15
16
|
export { PitestRunner } from './pitest-runner';
|
|
16
17
|
|
|
17
18
|
import { StrykerRunner } from './stryker-runner';
|
|
18
19
|
import { MutmutRunner } from './mutmut-runner';
|
|
20
|
+
import { GremlinsRunner } from './gremlins-runner';
|
|
19
21
|
import { GoMutantRunner } from './go-mutant-runner';
|
|
20
22
|
import { PitestRunner } from './pitest-runner';
|
|
21
23
|
import { registerRunner } from './types';
|
|
@@ -23,6 +25,12 @@ import { registerRunner } from './types';
|
|
|
23
25
|
/** Auto-register all known runners. */
|
|
24
26
|
export function registerAllRunners(): void {
|
|
25
27
|
registerRunner(new StrykerRunner());
|
|
28
|
+
// Registration order decides the `py` winner (resolveRunner returns the
|
|
29
|
+
// FIRST runner whose extensions include the ext). pytest-gremlins is
|
|
30
|
+
// registered before mutmut deliberately (#410): it is natively
|
|
31
|
+
// cross-platform, so when both are installed it is the better default —
|
|
32
|
+
// mutmut stays reachable via its own runner on Unix-only setups.
|
|
33
|
+
registerRunner(new GremlinsRunner());
|
|
26
34
|
registerRunner(new MutmutRunner());
|
|
27
35
|
registerRunner(new GoMutantRunner());
|
|
28
36
|
registerRunner(new PitestRunner());
|
|
@@ -33,25 +33,31 @@ export class MutmutRunner implements MutationRunner {
|
|
|
33
33
|
constructor(private readonly platform: Platform = process.platform) {}
|
|
34
34
|
|
|
35
35
|
async isAvailable(): Promise<boolean> {
|
|
36
|
-
|
|
37
|
-
execSync('mutmut --version', { stdio: 'pipe', timeout: 5000 });
|
|
38
|
-
this.route = 'native';
|
|
39
|
-
return true;
|
|
40
|
-
} catch {
|
|
41
|
-
if (this.platform !== 'win32') {
|
|
42
|
-
this.route = 'unavailable';
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
|
|
36
|
+
if (this.platform !== 'win32') {
|
|
46
37
|
try {
|
|
47
|
-
execSync('
|
|
48
|
-
this.route = '
|
|
38
|
+
execSync('mutmut --version', { stdio: 'pipe', timeout: 5000 });
|
|
39
|
+
this.route = 'native';
|
|
49
40
|
return true;
|
|
50
41
|
} catch {
|
|
51
42
|
this.route = 'unavailable';
|
|
52
43
|
return false;
|
|
53
44
|
}
|
|
54
45
|
}
|
|
46
|
+
|
|
47
|
+
// Windows: mutmut 3.x is fork()-based and can ONLY run under WSL. A native
|
|
48
|
+
// `mutmut --version` succeeding therefore proves nothing about `mutmut run`
|
|
49
|
+
// — reporting route='native' here is exactly the #410 deadlock: every push
|
|
50
|
+
// reached `mutmut run`, which exits 1 with "please use the WSL", and the
|
|
51
|
+
// gate blocked on a structurally unachievable check. The route is decided
|
|
52
|
+
// by the WSL probe alone so an unavailable WSL yields a clean SKIP.
|
|
53
|
+
try {
|
|
54
|
+
execSync('wsl mutmut --version', { stdio: 'pipe', timeout: 10000 });
|
|
55
|
+
this.route = 'wsl';
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
this.route = 'unavailable';
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
async run(options: RunMutationOptions): Promise<MutationRunOutcome> {
|
|
@@ -90,6 +90,11 @@ export function registerRunner(runner: MutationRunner): void {
|
|
|
90
90
|
/**
|
|
91
91
|
* Resolve the correct runner for a given file extension.
|
|
92
92
|
* Returns undefined if no runner supports this extension.
|
|
93
|
+
*
|
|
94
|
+
* Registration order decides the winner when several runners share an
|
|
95
|
+
* extension. This is a synchronous fast-path (used by file filtering); the
|
|
96
|
+
* availability-aware fallback that honors "first AVAILABLE runner" lives in
|
|
97
|
+
* {@link resolveRunners}.
|
|
93
98
|
*/
|
|
94
99
|
export function resolveRunner(ext: string): MutationRunner | undefined {
|
|
95
100
|
const normalized = ext.replace(/^\./, '');
|
|
@@ -98,3 +103,18 @@ export function resolveRunner(ext: string): MutationRunner | undefined {
|
|
|
98
103
|
}
|
|
99
104
|
return undefined;
|
|
100
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve ALL runners supporting an extension, in registration (priority)
|
|
109
|
+
* order — e.g. for `.py` that is [pytest-gremlins, mutmut] (#410).
|
|
110
|
+
*
|
|
111
|
+
* The gate walks this list and uses the first runner whose `isAvailable()`
|
|
112
|
+
* returns true, so a pure-mutmut environment (no gremlins installed) still
|
|
113
|
+
* gets mutation testing instead of a silent SKIP. This is the actual
|
|
114
|
+
* implementation of the design contract "gremlins 可用 → GremlinsRunner,
|
|
115
|
+
* 否则 MutmutRunner".
|
|
116
|
+
*/
|
|
117
|
+
export function resolveRunners(ext: string): MutationRunner[] {
|
|
118
|
+
const normalized = ext.replace(/^\./, '');
|
|
119
|
+
return Array.from(runnerRegistry.values()).filter(r => r.extensions.includes(normalized));
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.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-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.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-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.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.26.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-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.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-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.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-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.0.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
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.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-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.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-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.0.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.
|
|
3
|
+
"version": "0.26.0",
|
|
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": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.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-16
|
|
4
|
+
**Commit:** 98096abe
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.26.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.
|