@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.
- package/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +269 -13
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- 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/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- 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/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- 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/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-entry-total-warnings.test.ts +76 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/__tests__/cpp-code-block.test.ts +123 -0
- package/principles/adapters/__tests__/cpp-scanner-equivalence.test.ts +67 -0
- package/principles/adapters/__tests__/typescript-block-comment-lines.test.ts +53 -0
- package/principles/adapters/__tests__/typescript-masking.test.ts +96 -0
- package/principles/adapters/cpp.ts +102 -50
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +548 -178
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/baseline.ts +58 -70
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- 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
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-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 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
|
-
|
|
35
|
-
|
|
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
|
});
|
package/mutation/gate-m.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
105
|
-
|
|
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
|
-
|
|
157
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
158
|
+
stdio: 'pipe',
|
|
159
|
+
cwd: options.cwd,
|
|
160
|
+
});
|
|
161
|
+
|
|
114
162
|
let stdout = '';
|
|
115
|
-
let
|
|
116
|
-
let
|
|
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
|
-
|
|
177
|
+
tracker.markTimedOut();
|
|
119
178
|
child.kill('SIGTERM');
|
|
120
|
-
if (
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 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-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 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
|