@boyingliu01/xp-gate 0.31.3 → 0.32.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.
Files changed (151) hide show
  1. package/adapters/java.sh +256 -4
  2. package/adapters/powershell.sh +45 -6
  3. package/bin/xp-gate.js +8 -2
  4. package/build-integrity/__tests__/gate-10.test.ts +88 -1
  5. package/build-integrity/gate-10.ts +51 -16
  6. package/dsh.bundle.patch.yml +15 -0
  7. package/gate-13-compiler-strictness.sh +1031 -0
  8. package/gate-3.sh +48 -13
  9. package/hooks/gate-13-compiler-strictness.sh +1031 -0
  10. package/hooks/gate-3.sh +48 -13
  11. package/hooks/lib/coverage-total.sh +18 -2
  12. package/hooks/lib/java-test-selection.sh +251 -0
  13. package/hooks/lib/jscpd-run.sh +9 -0
  14. package/hooks/lib/validate-code-walkthrough.cjs +186 -29
  15. package/hooks/pre-commit +269 -13
  16. package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
  17. package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
  18. package/lib/__tests__/gate-runner.test.js +29 -2
  19. package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
  20. package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
  21. package/lib/__tests__/shared-utils.test.js +21 -0
  22. package/lib/check.js +6 -2
  23. package/lib/detect-deps.js +42 -10
  24. package/lib/doctor-tui.js +111 -21
  25. package/lib/gate-runner.js +111 -37
  26. package/lib/mattpocock-license-verdict.json +1 -1
  27. package/lib/principles.js +48 -6
  28. package/lib/shared-utils.js +8 -0
  29. package/mock-policy/AGENTS.md +3 -3
  30. package/mock-policy/gate-m3.ts +57 -23
  31. package/mutation/AGENTS.md +3 -3
  32. package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
  33. package/mutation/__tests__/test-filter-parity.test.ts +5 -2
  34. package/mutation/gate-m.ts +30 -24
  35. package/mutation/runners/mutmut-runner.ts +80 -30
  36. package/mutation/runners/pitest-runner.ts +60 -42
  37. package/package.json +14 -2
  38. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  39. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  40. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  41. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  42. package/plugins/dsh/lib/command.d.ts +38 -0
  43. package/plugins/dsh/lib/command.d.ts.map +1 -0
  44. package/plugins/dsh/lib/command.js +87 -0
  45. package/plugins/dsh/lib/credentials.d.ts +39 -0
  46. package/plugins/dsh/lib/credentials.d.ts.map +1 -0
  47. package/plugins/dsh/lib/credentials.js +57 -0
  48. package/plugins/dsh/lib/delphi-config.d.ts +129 -0
  49. package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
  50. package/plugins/dsh/lib/delphi-config.js +369 -0
  51. package/plugins/dsh/lib/delphi-run.d.ts +24 -0
  52. package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
  53. package/plugins/dsh/lib/delphi-run.js +113 -0
  54. package/plugins/dsh/lib/delphi.d.ts +94 -0
  55. package/plugins/dsh/lib/delphi.d.ts.map +1 -0
  56. package/plugins/dsh/lib/delphi.js +207 -0
  57. package/plugins/dsh/lib/dialect.d.ts +24 -0
  58. package/plugins/dsh/lib/dialect.d.ts.map +1 -0
  59. package/plugins/dsh/lib/dialect.js +36 -0
  60. package/plugins/dsh/lib/evidence.d.ts +70 -0
  61. package/plugins/dsh/lib/evidence.d.ts.map +1 -0
  62. package/plugins/dsh/lib/evidence.js +92 -0
  63. package/plugins/dsh/lib/gate-runner.d.ts +49 -0
  64. package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
  65. package/plugins/dsh/lib/gate-runner.js +95 -0
  66. package/plugins/dsh/lib/git-context.d.ts +26 -0
  67. package/plugins/dsh/lib/git-context.d.ts.map +1 -0
  68. package/plugins/dsh/lib/git-context.js +30 -0
  69. package/plugins/dsh/lib/hooks.d.ts +42 -0
  70. package/plugins/dsh/lib/hooks.d.ts.map +1 -0
  71. package/plugins/dsh/lib/hooks.js +113 -0
  72. package/plugins/dsh/lib/index.d.ts +20 -0
  73. package/plugins/dsh/lib/index.d.ts.map +1 -0
  74. package/plugins/dsh/lib/index.js +430 -0
  75. package/plugins/dsh/lib/skill-provider.d.ts +15 -0
  76. package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
  77. package/plugins/dsh/lib/skill-provider.js +205 -0
  78. package/plugins/dsh/lib/sprint.d.ts +60 -0
  79. package/plugins/dsh/lib/sprint.d.ts.map +1 -0
  80. package/plugins/dsh/lib/sprint.js +92 -0
  81. package/plugins/dsh/package.json +14 -8
  82. package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
  84. package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
  85. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
  86. package/plugins/dsh/src/command-single-form.test.ts +67 -0
  87. package/plugins/dsh/src/command.test.ts +21 -17
  88. package/plugins/dsh/src/command.ts +19 -21
  89. package/plugins/dsh/src/compat.test.ts +200 -0
  90. package/plugins/dsh/src/contract.test.ts +7 -1
  91. package/plugins/dsh/src/credentials.test.ts +93 -0
  92. package/plugins/dsh/src/credentials.ts +71 -0
  93. package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
  94. package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
  95. package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
  96. package/plugins/dsh/src/delphi-config.ts +146 -53
  97. package/plugins/dsh/src/delphi-run.ts +27 -4
  98. package/plugins/dsh/src/delphi.ts +133 -65
  99. package/plugins/dsh/src/evidence.test.ts +74 -0
  100. package/plugins/dsh/src/evidence.ts +141 -0
  101. package/plugins/dsh/src/gate-runner.test.ts +14 -6
  102. package/plugins/dsh/src/gate-runner.ts +68 -5
  103. package/plugins/dsh/src/git-context.test.ts +39 -0
  104. package/plugins/dsh/src/git-context.ts +38 -0
  105. package/plugins/dsh/src/hooks.test.ts +87 -0
  106. package/plugins/dsh/src/hooks.ts +126 -0
  107. package/plugins/dsh/src/index.test.ts +63 -6
  108. package/plugins/dsh/src/index.ts +340 -13
  109. package/plugins/dsh/src/skill-provider.test.ts +194 -0
  110. package/plugins/dsh/src/skill-provider.ts +231 -0
  111. package/plugins/dsh/src/sprint.test.ts +64 -0
  112. package/plugins/dsh/src/sprint.ts +122 -0
  113. package/plugins/dsh/src/tri-state.test.ts +123 -0
  114. package/plugins/opencode/index.ts +192 -58
  115. package/plugins/opencode/package.json +1 -1
  116. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  117. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  118. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  119. package/plugins/qoder/plugin.json +1 -1
  120. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  122. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  123. package/principles/AGENTS.md +3 -3
  124. package/principles/__tests__/analyze-read-failure.test.ts +70 -0
  125. package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
  126. package/principles/__tests__/baseline-prune.test.ts +61 -0
  127. package/principles/__tests__/cli-files-separators.test.ts +69 -0
  128. package/principles/__tests__/config-merge-warnings.test.ts +148 -0
  129. package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
  130. package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
  131. package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
  132. package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
  133. package/principles/adapters/cpp.ts +102 -50
  134. package/principles/adapters/java.ts +22 -3
  135. package/principles/adapters/typescript.ts +548 -178
  136. package/principles/analyzer.ts +36 -3
  137. package/principles/baseline-prune.ts +19 -7
  138. package/principles/baseline.ts +58 -70
  139. package/principles/config.ts +56 -35
  140. package/principles/index.ts +122 -40
  141. package/principles/rules/clean-code/deep-nesting.ts +36 -22
  142. package/principles/rules/clean-code/unused-imports.ts +30 -19
  143. package/principles/rules/solid/dip.ts +30 -20
  144. package/principles/rules/solid/lsp.ts +17 -2
  145. package/principles/rules/solid/ocp.ts +33 -23
  146. package/principles/rules/solid/srp.ts +65 -23
  147. package/scripts/delphi-external-review.cjs +25 -2
  148. package/scripts/sync-package-content.js +28 -1
  149. package/skills/delphi-review/AGENTS.md +3 -3
  150. package/skills/sprint-flow/AGENTS.md +3 -3
  151. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-10-26
4
- **Commit:** 7a6d9971
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.3.0
6
+ **Version:** 0.32.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.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @test REQ-630
3
+ * @intent A Gradle build file alone is not enough: PITest is launched through
4
+ * `./gradlew` or a `gradle` on PATH. The pre-existing isAvailable tests
5
+ * mocked EVERY existsSync to true and mocked the gradle launcher to
6
+ * succeed, so "build.gradle configures pitest but no launcher can be
7
+ * run" was never exercised, and deleting the launcher guard left the
8
+ * suite green. On a machine with a pom-free Gradle project and no gradle
9
+ * installed, isAvailable() would claim the runner works, and Gate M
10
+ * would fail later with a confusing "gradle not found".
11
+ *
12
+ * These cases live here rather than in pitest-runner.test.ts because
13
+ * that file carries pre-existing warnings; editing it would trigger the
14
+ * Boy Scout "<=5 warnings must clear to zero" rule for unrelated code.
15
+ * @covers AC-630-01, AC-630-02
16
+ */
17
+
18
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
19
+ import { PitestRunner } from '../runners/pitest-runner';
20
+
21
+ // `fs` is mocked module-wide; each case sets the answers it needs.
22
+ vi.mock('fs', () => ({
23
+ existsSync: vi.fn(),
24
+ readFileSync: vi.fn(),
25
+ readdirSync: vi.fn(),
26
+ }));
27
+ // `execSync` decides whether a launcher runs; `spawn` is unused by isAvailable
28
+ // but is imported by the runner module.
29
+ vi.mock('child_process', () => ({ execSync: vi.fn(), spawn: vi.fn() }));
30
+
31
+ describe('PitestRunner.isAvailable — Gradle launcher requirement', () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks();
34
+ });
35
+
36
+ it('AC-630-01: no ./gradlew and gradle not runnable -> unavailable despite a pitest build file', async () => {
37
+ const { existsSync, readFileSync } = await import('fs');
38
+ const { execSync } = await import('child_process');
39
+ // Maven throws; the gradle launcher also throws (not installed).
40
+ vi.mocked(execSync).mockImplementation(() => {
41
+ throw new Error('not found');
42
+ });
43
+ // build.gradle exists and configures pitest; ./gradlew does NOT exist.
44
+ vi.mocked(existsSync).mockImplementation((p: unknown) =>
45
+ String(p).endsWith('build.gradle'),
46
+ );
47
+ vi.mocked(readFileSync).mockReturnValue(
48
+ 'plugins { id "info.solidsoft.pitest" version "1.15.0" }',
49
+ );
50
+
51
+ const runner = new PitestRunner();
52
+ expect(await runner.isAvailable()).toBe(false);
53
+ });
54
+
55
+ it('AC-630-02: ./gradlew present -> the gradle launcher is not required on PATH', async () => {
56
+ const { existsSync, readFileSync } = await import('fs');
57
+ const { execSync } = await import('child_process');
58
+ // Every launcher probe throws, but ./gradlew exists, so a wrapper-based
59
+ // build must still count as available.
60
+ vi.mocked(execSync).mockImplementation(() => {
61
+ throw new Error('not found');
62
+ });
63
+ vi.mocked(existsSync).mockImplementation((p: unknown) => {
64
+ const s = String(p);
65
+ return s.includes('gradlew') || s.endsWith('build.gradle.kts');
66
+ });
67
+ vi.mocked(readFileSync).mockReturnValue(
68
+ 'plugins { id("info.solidsoft.gradle.pitest") version "1.15.0" }',
69
+ );
70
+
71
+ const runner = new PitestRunner();
72
+ expect(await runner.isAvailable()).toBe(true);
73
+ });
74
+ });
@@ -31,7 +31,10 @@ describe('test-path filter parity between pre-push and gate-m (#480)', () => {
31
31
  it('both sides exclude declaration files and spec-named tests', () => {
32
32
  expect(prePushFilter).toContain(String.raw`\.d\.ts$`);
33
33
  expect(prePushFilter).toContain(String.raw`\.spec\.`);
34
- expect(gateM).toContain("endsWith('.d.ts')");
35
- expect(gateM).toContain("includes('.spec.')");
34
+ // gate-m states each exclusion as an entry in EXCLUDED_PATH_PATTERNS rather
35
+ // than as an endsWith/includes call, so the parity assertion follows the
36
+ // regex. What it pins is unchanged: the TS side still excludes both.
37
+ expect(gateM).toContain(String.raw`pattern: /\.d\.ts$/`);
38
+ expect(gateM).toContain(String.raw`pattern: /\.spec\./`);
36
39
  });
37
40
  });
@@ -80,39 +80,45 @@ function parseTimeout(options: GateMOptions, args: string[], i: number): void {
80
80
 
81
81
  // ── File filtering: excludes tests, declarations, adapters ──
82
82
  // Delegate per-language filtering to a helper.
83
-
83
+ //
84
+ // Every exclusion is a "does this normalized path look like X?" question, so
85
+ // they are stated as data rather than as a chain of early returns. Keeping the
86
+ // list declarative is what makes the rules reviewable: a new convention is one
87
+ // line, and the reasons stay attached to the pattern they justify.
84
88
  export function filterSourceFiles(files: string[]): string[] {
85
89
  return files.filter(file => {
86
- const ext = path.extname(file);
87
90
  // One separator view for every pattern below: the path arrives from the
88
91
  // adapter layer, which on Windows yields backslash-delimited relative paths.
89
92
  const normalized = file.replace(/\\/g, '/');
90
- // Skip test files
91
- if (normalized.includes('.test.') || normalized.includes('_test.')) return false;
92
- // Skip spec-named test files: the pre-push filter already excludes them, and
93
- // a mutant inside a *.spec.ts is scored against a production budget (#480).
94
- if (normalized.includes('.spec.')) return false;
95
- // Skip test-tree directories: helpers/fixtures/factories there are test
96
- // infrastructure judged by the suites that run them, not by mutation
97
- // scoring budgeted for production code (#480)
98
- if (/(^|\/)(tests?|__tests__)\//.test(normalized)) return false;
99
- // Skip Java/Kotlin test files: FooTest.java, FooSpec.kt, etc. Case-sensitive
100
- // "Test"/"Spec"/"IT" suffix matching is what the Surefire/Failsafe conventions
101
- // actually produce -- `[A-Z]Test\.only` caught MyTest.java but not FooTest.java,
102
- // so most test classes stayed in the mutation candidate list.
103
- if (/Test\.(java|kt|kts)$/.test(normalized)) return false;
104
- if (/Tests\.(java|kt|kts)$/.test(normalized)) return false;
105
- if (/(IT|Spec)\.(java|kt|kts)$/.test(normalized)) return false;
106
- // Skip declaration files
107
- if (normalized.endsWith('.d.ts')) return false;
108
- // Skip adapter files
109
- if (normalized.includes('/adapters/')) return false;
93
+ if (isExcludedPath(normalized)) return false;
110
94
  // Skip files with no registered runner
111
- const runner = resolveRunner(ext);
112
- return runner !== undefined;
95
+ return resolveRunner(path.extname(file)) !== undefined;
113
96
  });
114
97
  }
115
98
 
99
+ const EXCLUDED_PATH_PATTERNS: readonly { pattern: RegExp; reason: string }[] = [
100
+ { pattern: /\.test\.|_test\./, reason: 'test files' },
101
+ // Spec-named test files: the pre-push filter already excludes them, and a
102
+ // mutant inside a *.spec.ts is scored against a production budget (#480).
103
+ { pattern: /\.spec\./, reason: 'spec files' },
104
+ // Test-tree directories: helpers/fixtures/factories there are test
105
+ // infrastructure judged by the suites that run them, not by mutation
106
+ // scoring budgeted for production code (#480)
107
+ { pattern: /(^|\/)(tests?|__tests__)\//, reason: 'test trees' },
108
+ // Java/Kotlin test files: FooTest.java, FooSpec.kt, etc. Case-sensitive
109
+ // "Test"/"Spec"/"IT" suffix matching is what the Surefire/Failsafe
110
+ // conventions actually produce -- `[A-Z]Test\.only` caught MyTest.java but
111
+ // not FooTest.java, so most test classes stayed in the candidate list.
112
+ { pattern: /(Test|Tests|IT|Spec)\.(java|kt|kts)$/, reason: 'JVM test classes' },
113
+ { pattern: /\.d\.ts$/, reason: 'declaration files' },
114
+ { pattern: /\/adapters\//, reason: 'adapter files' },
115
+ ];
116
+
117
+ /** True when `normalized` matches any exclusion rule. */
118
+ function isExcludedPath(normalized: string): boolean {
119
+ return EXCLUDED_PATH_PATTERNS.some(({ pattern }) => pattern.test(normalized));
120
+ }
121
+
116
122
  async function fileExists(filePath: string): Promise<boolean> {
117
123
  try {
118
124
  await fs.access(filePath);
@@ -99,38 +99,90 @@ export class MutmutRunner implements MutationRunner {
99
99
  }
100
100
  }
101
101
 
102
+ /**
103
+ * Build the terminal-outcome handler for one `mutmut run`.
104
+ *
105
+ * Three separate events (close / error / timeout) can end a run, and each one
106
+ * has to cancel both timers, mark the promise settled exactly once, and hand
107
+ * back the right shape. Factoring that into a single small object keeps
108
+ * `runMutmut` down to spawning and wiring, which is all it should express.
109
+ */
110
+ private settleTracker(
111
+ resolve: (outcome: MutationRunOutcome) => void,
112
+ cancelTimers: () => void,
113
+ ): {
114
+ didTimeOut: () => boolean;
115
+ markTimedOut: () => void;
116
+ settle: (outcome: MutationRunOutcome) => void;
117
+ isSettled: () => boolean;
118
+ } {
119
+ let settled = false;
120
+ let timedOut = false;
121
+ return {
122
+ didTimeOut: () => timedOut,
123
+ markTimedOut: () => {
124
+ timedOut = true;
125
+ },
126
+ isSettled: () => settled,
127
+ settle: (outcome) => {
128
+ if (settled) return;
129
+ settled = true;
130
+ cancelTimers();
131
+ resolve(outcome);
132
+ },
133
+ };
134
+ }
135
+
136
+ /**
137
+ * How to invoke `mutmut run` for the current route, or null when a WSL path
138
+ * could not be derived (in which case the run must be skipped, not attempted).
139
+ */
140
+ private buildInvocation(cwd: string): { command: string; args: string[] } | null {
141
+ if (this.route !== 'wsl') {
142
+ return { command: 'mutmut', args: ['run'] };
143
+ }
144
+ const wslPath = this.toWslPath(cwd);
145
+ if (wslPath === null) return null;
146
+ return { command: 'wsl', args: ['--cd', wslPath, 'mutmut', 'run'] };
147
+ }
148
+
102
149
  private async runMutmut(options: RunMutationOptions): Promise<MutationRunOutcome> {
103
150
  return new Promise((resolve) => {
104
- const command = this.route === 'wsl' ? 'wsl' : 'mutmut';
105
- const wslPath = this.route === 'wsl' ? this.toWslPath(options.cwd) : options.cwd;
106
- if (wslPath === null) {
151
+ const invocation = this.buildInvocation(options.cwd);
152
+ if (invocation === null) {
107
153
  resolve({ report: null, timedOut: false });
108
154
  return;
109
155
  }
110
- const args = this.route === 'wsl' ? ['--cd', wslPath, 'mutmut', 'run'] : ['run'];
111
- const child = spawn(command, args, { stdio: 'pipe', cwd: options.cwd });
112
156
 
113
- let stderr = '';
157
+ const child = spawn(invocation.command, invocation.args, {
158
+ stdio: 'pipe',
159
+ cwd: options.cwd,
160
+ });
161
+
114
162
  let stdout = '';
115
- let didTimeOut = false;
116
- let settled = false;
163
+ let stderr = '';
164
+ let escalationId: NodeJS.Timeout | undefined;
165
+
166
+ // Both timers must be cancelled from three different terminal events
167
+ // (close, error, timeout). Keeping the pair in one place is what stops the
168
+ // settle path from drifting out of step with the timeout path.
169
+ const cancelTimers = (): void => {
170
+ clearTimeout(timeoutId);
171
+ if (escalationId) clearTimeout(escalationId);
172
+ };
173
+
174
+ const tracker = this.settleTracker(resolve, cancelTimers);
175
+
117
176
  const timeoutId = setTimeout(() => {
118
- didTimeOut = true;
177
+ tracker.markTimedOut();
119
178
  child.kill('SIGTERM');
120
- if (settled) return;
179
+ if (tracker.isSettled()) return;
180
+ // SIGTERM may be ignored; escalate once, and let the retry live in a
181
+ // variable so `cancelTimers` can reach it.
121
182
  escalationId = setTimeout(() => {
122
- if (!settled) child.kill('SIGKILL');
183
+ if (!tracker.isSettled()) child.kill('SIGKILL');
123
184
  }, 5000);
124
185
  }, options.timeoutMs);
125
- let escalationId: NodeJS.Timeout | undefined;
126
-
127
- const settle = (outcome: MutationRunOutcome): void => {
128
- if (settled) return;
129
- settled = true;
130
- if (timeoutId) clearTimeout(timeoutId);
131
- if (escalationId) clearTimeout(escalationId);
132
- resolve(outcome);
133
- };
134
186
 
135
187
  child.stdout?.on('data', (data: Buffer) => {
136
188
  stdout += data.toString();
@@ -141,25 +193,23 @@ export class MutmutRunner implements MutationRunner {
141
193
  });
142
194
 
143
195
  child.on('close', (code) => {
144
- clearTimeout(timeoutId);
145
-
196
+ // A null exit code means the process was killed ("timed out" when we did
197
+ // it, "unknown" otherwise); there is no report to parse either way.
146
198
  if (code === null) {
147
- settle({ report: null, timedOut: didTimeOut });
199
+ tracker.settle({ report: null, timedOut: tracker.didTimeOut() });
148
200
  return;
149
201
  }
150
202
 
151
- // Parse results from mutmut run stdout (v3.x emoji progress)
152
- const report = this.parseEmojiProgress(stdout);
153
- settle({
154
- report,
155
- timedOut: didTimeOut,
203
+ tracker.settle({
204
+ // Parse results from mutmut run stdout (v3.x emoji progress)
205
+ report: this.parseEmojiProgress(stdout),
206
+ timedOut: tracker.didTimeOut(),
156
207
  error: code !== 0 ? stderr || stdout : undefined,
157
208
  });
158
209
  });
159
210
 
160
211
  child.on('error', (err) => {
161
- clearTimeout(timeoutId);
162
- settle({ report: null, timedOut: didTimeOut, error: err.message });
212
+ tracker.settle({ report: null, timedOut: tracker.didTimeOut(), error: err.message });
163
213
  });
164
214
  });
165
215
  }
@@ -38,48 +38,7 @@ export class PitestRunner implements MutationRunner {
38
38
  readonly extensions = ['java', 'kt', 'kts'];
39
39
 
40
40
  async isAvailable(): Promise<boolean> {
41
- // Check Maven availability: mvn exists + pom.xml with pitest-maven plugin
42
- try {
43
- execSync('mvn --version', { stdio: 'pipe', timeout: 5000 });
44
- if (existsSync('pom.xml')) {
45
- const pom = readFileSync('pom.xml', 'utf-8');
46
- if (
47
- pom.includes('pitest-maven') ||
48
- pom.includes('org.pitest')
49
- ) {
50
- return true;
51
- }
52
- }
53
- } catch {
54
- // Maven not available
55
- }
56
-
57
- // Check Gradle availability: gradle/gradlew exists + build.gradle(.kts) with pitest plugin
58
- const gradlewPath = './gradlew';
59
- const hasGradlew = existsSync(gradlewPath);
60
- try {
61
- if (!hasGradlew) {
62
- execSync('gradle --version', { stdio: 'pipe', timeout: 5000 });
63
- }
64
- const buildGradle = existsSync('build.gradle.kts')
65
- ? 'build.gradle.kts'
66
- : existsSync('build.gradle')
67
- ? 'build.gradle'
68
- : null;
69
- if (buildGradle) {
70
- const content = readFileSync(buildGradle, 'utf-8');
71
- if (
72
- content.includes('info.solidsoft.pitest') ||
73
- content.includes('info.solidsoft.gradle.pitest')
74
- ) {
75
- return true;
76
- }
77
- }
78
- } catch {
79
- return false;
80
- }
81
-
82
- return false;
41
+ return mavenPitestConfigured() || gradlePitestConfigured();
83
42
  }
84
43
 
85
44
  async run(options: RunMutationOptions): Promise<MutationRunOutcome> {
@@ -288,3 +247,62 @@ export class PitestRunner implements MutationRunner {
288
247
  }
289
248
  }
290
249
  }
250
+
251
+ /**
252
+ * True when `command` can be executed at all.
253
+ *
254
+ * `execSync` with a short timeout is the cheapest cross-platform probe: the
255
+ * command either resolves and runs, or throws (missing / not executable). Only
256
+ * the throw matters here — the output is irrelevant.
257
+ */
258
+ function commandRuns(command: string): boolean {
259
+ try {
260
+ execSync(command, { stdio: 'pipe', timeout: 5000 });
261
+ return true;
262
+ } catch {
263
+ return false;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * True when `file` exists and contains any of `markers`.
269
+ *
270
+ * The read is guarded: a file can exist and still be unreadable (permissions,
271
+ * a directory named like a build file, a transient IO error). Treating that as
272
+ * "does not mention" keeps the caller's answer honest — an unreadable build file
273
+ * does not configure pitest — instead of throwing out of `isAvailable`.
274
+ */
275
+ function fileMentions(file: string, markers: readonly string[]): boolean {
276
+ if (!existsSync(file)) return false;
277
+ try {
278
+ const content = readFileSync(file, 'utf-8');
279
+ return markers.some((marker) => content.includes(marker));
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+
285
+ /** Maven is usable when `mvn` runs and the POM configures the pitest plugin. */
286
+ function mavenPitestConfigured(): boolean {
287
+ if (!commandRuns('mvn --version')) return false;
288
+ return fileMentions('pom.xml', ['pitest-maven', 'org.pitest']);
289
+ }
290
+
291
+ /**
292
+ * The Gradle build file PITest would read, preferring the Kotlin DSL.
293
+ * @returns the file name, or null when neither exists.
294
+ */
295
+ function gradleBuildFile(): string | null {
296
+ if (existsSync('build.gradle.kts')) return 'build.gradle.kts';
297
+ if (existsSync('build.gradle')) return 'build.gradle';
298
+ return null;
299
+ }
300
+
301
+ /** Gradle is usable when a launcher runs and the build file configures pitest. */
302
+ function gradlePitestConfigured(): boolean {
303
+ const hasWrapper = existsSync('./gradlew');
304
+ if (!hasWrapper && !commandRuns('gradle --version')) return false;
305
+ const buildFile = gradleBuildFile();
306
+ if (buildFile === null) return false;
307
+ return fileMentions(buildFile, ['info.solidsoft.pitest', 'info.solidsoft.gradle.pitest']);
308
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.31.3",
3
+ "version": "0.32.0",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "bin/xp-gate.js"
@@ -12,6 +12,7 @@
12
12
  "lib/",
13
13
  "skills/",
14
14
  "plugins/",
15
+ "dsh.bundle.patch.yml",
15
16
  "adapter-common.sh",
16
17
  "principles/",
17
18
  "mutation/",
@@ -25,10 +26,21 @@
25
26
  "gate-9.sh",
26
27
  "gate-10.sh",
27
28
  "gate-12-file-hygiene.sh",
29
+ "gate-13-compiler-strictness.sh",
28
30
  "sprint-gate.sh"
29
31
  ],
32
+ "dsh": {
33
+ "bundle": {
34
+ "patch": "./dsh.bundle.patch.yml"
35
+ }
36
+ },
30
37
  "exports": {
31
- "./tui": "./plugins/opencode/tui-plugin.ts"
38
+ "./tui": "./plugins/opencode/tui-plugin.ts",
39
+ "./dsh": {
40
+ "import": "./plugins/dsh/lib/index.js",
41
+ "default": "./plugins/dsh/lib/index.js"
42
+ },
43
+ "./package.json": "./package.json"
32
44
  },
33
45
  "repository": {
34
46
  "type": "git",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.31.3",
3
+ "version": "0.32.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-26
4
- **Commit:** 7a6d9971
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.3.0
6
+ **Version:** 0.32.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-26
4
- **Commit:** 7a6d9971
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.3.0
6
+ **Version:** 0.32.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-26
4
- **Commit:** 7a6d9971
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.3.0
6
+ **Version:** 0.32.0.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -0,0 +1,38 @@
1
+ import { type ShellDialect } from "./dialect.js";
2
+ export { type ShellDialect, detectDialect, shellQuote, shq } from "./dialect.js";
3
+ /**
4
+ * Allowlist of xp-gate gate aliases that are safe to invoke standalone
5
+ * (non-preCommitOnly gates + common aliases). Anything outside this set is
6
+ * rejected before reaching the shell.
7
+ */
8
+ export declare const GATE_WHITELIST: readonly ["duplicates", "complexity", "principles", "arch", "architecture", "iac", "secrets", "sast"];
9
+ export type GateId = (typeof GATE_WHITELIST)[number];
10
+ export declare function isGateAllowed(gate: string): boolean;
11
+ export interface BuildCommandOptions {
12
+ subcommand: "check" | "principles" | "arch";
13
+ target?: string;
14
+ gates?: readonly string[];
15
+ config?: string;
16
+ /**
17
+ * Executor dialect. Defaults to `"posix"` so existing callers keep working;
18
+ * callers running under DSH on Windows must pass `"powershell"`.
19
+ */
20
+ dialect?: ShellDialect;
21
+ }
22
+ /**
23
+ * Build a single unconditional shell command.
24
+ *
25
+ * There is deliberately no presence probe. An earlier version wrapped the call
26
+ * in `if command -v xp-gate …; then …; else print hint; fi`, which left the exit
27
+ * code at 0 on the else branch — so a CLI that resolved but then failed produced
28
+ * empty stdout with exit 0, and the model read that as a pass.
29
+ *
30
+ * Running unconditionally turns "not installed" into an ordinary command-not-found
31
+ * (nonzero exit, empty output), which renderGateResult classifies as
32
+ * INDETERMINATE and pairs with an install hint. It also removes the branch that
33
+ * could not distinguish "absent" from "present but broken".
34
+ */
35
+ export declare function buildCommand(options: BuildCommandOptions): string;
36
+ /** Resolve a model-supplied path against the session cwd; absolutes pass through. */
37
+ export declare function resolveTarget(rawPath: string, cwd: string): string;
38
+ //# sourceMappingURL=command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,YAAY,EAAc,MAAM,cAAc,CAAA;AAI5D,OAAO,EAAE,KAAK,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AAEhF;;;;GAIG;AACH,eAAO,MAAM,cAAc,uGASjB,CAAA;AAEV,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEpD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,GAAG,YAAY,GAAG,MAAM,CAAA;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB;AA+BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAajE;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAElE"}
@@ -0,0 +1,87 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+ import { shellQuote } from "./dialect.js";
3
+ // Re-exported so existing importers of this module keep working; the dialect
4
+ // primitives themselves live in ./dialect.ts.
5
+ export { detectDialect, shellQuote, shq } from "./dialect.js";
6
+ /**
7
+ * Allowlist of xp-gate gate aliases that are safe to invoke standalone
8
+ * (non-preCommitOnly gates + common aliases). Anything outside this set is
9
+ * rejected before reaching the shell.
10
+ */
11
+ export const GATE_WHITELIST = [
12
+ "duplicates",
13
+ "complexity",
14
+ "principles",
15
+ "arch",
16
+ "architecture",
17
+ "iac",
18
+ "secrets",
19
+ "sast",
20
+ ];
21
+ export function isGateAllowed(gate) {
22
+ return GATE_WHITELIST.includes(gate);
23
+ }
24
+ function buildInner(options, dialect) {
25
+ const { subcommand, target, gates, config } = options;
26
+ const q = (value) => shellQuote(value, dialect);
27
+ if (subcommand === "check") {
28
+ const tokens = ["xp-gate", "check"];
29
+ if (target !== undefined)
30
+ tokens.push(q(target));
31
+ if (gates !== undefined && gates.length > 0) {
32
+ // Defense-in-depth: the JSON-schema enum already rejects unknown gate ids,
33
+ // but we also whitelist here so a non-whitelisted value can never reach the shell.
34
+ const allowed = gates.filter(isGateAllowed);
35
+ if (allowed.length > 0)
36
+ tokens.push("--gates", q(allowed.join(",")));
37
+ }
38
+ return tokens.join(" ");
39
+ }
40
+ if (subcommand === "principles") {
41
+ const tokens = ["xp-gate", "principles"];
42
+ if (target !== undefined)
43
+ tokens.push(q(target));
44
+ return tokens.join(" ");
45
+ }
46
+ // subcommand === "arch" — the only remaining member of the union.
47
+ // #447: the default path runs the pinned local @archlinter/cli with its
48
+ // built-in rules — no config needed. Forcing `--config architecture.yaml`
49
+ // (the old default) hit the pre-0.16 schema that archlint itself rejects.
50
+ // Only forward an explicitly provided config, via the opt-in --use-config.
51
+ const tokens = ["xp-gate", "arch"];
52
+ if (config !== undefined)
53
+ tokens.push("--use-config", q(config));
54
+ return tokens.join(" ");
55
+ }
56
+ /**
57
+ * Build a single unconditional shell command.
58
+ *
59
+ * There is deliberately no presence probe. An earlier version wrapped the call
60
+ * in `if command -v xp-gate …; then …; else print hint; fi`, which left the exit
61
+ * code at 0 on the else branch — so a CLI that resolved but then failed produced
62
+ * empty stdout with exit 0, and the model read that as a pass.
63
+ *
64
+ * Running unconditionally turns "not installed" into an ordinary command-not-found
65
+ * (nonzero exit, empty output), which renderGateResult classifies as
66
+ * INDETERMINATE and pairs with an install hint. It also removes the branch that
67
+ * could not distinguish "absent" from "present but broken".
68
+ */
69
+ export function buildCommand(options) {
70
+ const dialect = options.dialect ?? "posix";
71
+ const inner = buildInner(options, dialect);
72
+ if (dialect === "posix") {
73
+ // Merge stderr into stdout so a failure that only writes to stderr still
74
+ // reaches the renderer, then echo the status so the exit code survives the
75
+ // pipeline.
76
+ return `${inner} 2>&1; printf '\\n[XP-Gate] exit:%s\\n' "$?"`;
77
+ }
78
+ // PowerShell: merge streams and report $LASTEXITCODE explicitly. The
79
+ // .cmd/.ps1 shims npm writes under %APPDATA%\npm are visible here but not to a
80
+ // Git-Bash PATH lookup, which is why Windows uses this dialect.
81
+ return `${inner} 2>&1; Write-Output "[XP-Gate] exit:$LASTEXITCODE"`;
82
+ }
83
+ /** Resolve a model-supplied path against the session cwd; absolutes pass through. */
84
+ export function resolveTarget(rawPath, cwd) {
85
+ return isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath);
86
+ }
87
+ //# sourceMappingURL=command.js.map