@boyingliu01/xp-gate 0.31.4 → 0.32.1
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 +65 -16
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +65 -16
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +101 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +259 -57
- 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-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/java.ts +22 -3
- package/principles/adapters/typescript.ts +70 -29
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- 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
|
@@ -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.1",
|
|
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.1",
|
|
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-29
|
|
4
|
+
**Commit:** 101a0ff3
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.1.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-29
|
|
4
|
+
**Commit:** 101a0ff3
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.1.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-29
|
|
4
|
+
**Commit:** 101a0ff3
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.1.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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the API key that a Delphi provider config points at, without assuming the
|
|
3
|
+
* value is in this process's environment.
|
|
4
|
+
*
|
|
5
|
+
* The credential service shipped with DSH layers environment, a provider-managed
|
|
6
|
+
* store, and `.env` files behind one `CredentialRef`. That is exactly the order the
|
|
7
|
+
* plugin needs: on the machine that prompted this work the key lived in a user-level
|
|
8
|
+
* environment variable and in `~/.dsh/.credentials.yaml`, while DSH's own process
|
|
9
|
+
* inherited neither — so `delphi-review` failed with a missing-variable error while
|
|
10
|
+
* every other gate worked.
|
|
11
|
+
*
|
|
12
|
+
* Resolution is deliberately single-layer. When `ctx.credentials` is present and
|
|
13
|
+
* reports `undefined`, this module does NOT fall back to `process.env`: the service
|
|
14
|
+
* has already consulted the environment, so a second look would resurrect a value it
|
|
15
|
+
* deliberately reported as absent and would make a DSH session resolve differently
|
|
16
|
+
* from a terminal session.
|
|
17
|
+
*/
|
|
18
|
+
/** A credential service, narrowed to the one method this module needs. */
|
|
19
|
+
export interface CredentialService {
|
|
20
|
+
resolve(ref: unknown): Promise<{
|
|
21
|
+
value: string;
|
|
22
|
+
} | undefined>;
|
|
23
|
+
}
|
|
24
|
+
/** Resolve one environment-variable-style reference to its current value. */
|
|
25
|
+
export type ApiKeyResolver = (ref: string) => Promise<string | undefined>;
|
|
26
|
+
/**
|
|
27
|
+
* Build an `ApiKeyResolver` over the available credential service.
|
|
28
|
+
*
|
|
29
|
+
* @param credentials - `ctx.credentials`, or undefined when the host does not offer
|
|
30
|
+
* one. Absence degrades to reading the process environment directly, which keeps
|
|
31
|
+
* the plugin working outside DSH (CLI use, tests).
|
|
32
|
+
* @param env - environment to read in that fallback. Injected rather than read from
|
|
33
|
+
* the global so the module stays testable offline.
|
|
34
|
+
* @param brand - converts a raw name into the service's nominal `CredentialRef`.
|
|
35
|
+
* Injected so this module needs no import of the peer package and tests can pass
|
|
36
|
+
* an identity function.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCredentialResolver(credentials: CredentialService | undefined, env: NodeJS.ProcessEnv, brand?: (value: string) => unknown): ApiKeyResolver;
|
|
39
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAAA;CAC9D;AAED,6EAA6E;AAC7E,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;AAYzE;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,iBAAiB,GAAG,SAAS,EAC1C,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,KAAK,GAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAA0B,GACnD,cAAc,CAkBhB"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the API key that a Delphi provider config points at, without assuming the
|
|
3
|
+
* value is in this process's environment.
|
|
4
|
+
*
|
|
5
|
+
* The credential service shipped with DSH layers environment, a provider-managed
|
|
6
|
+
* store, and `.env` files behind one `CredentialRef`. That is exactly the order the
|
|
7
|
+
* plugin needs: on the machine that prompted this work the key lived in a user-level
|
|
8
|
+
* environment variable and in `~/.dsh/.credentials.yaml`, while DSH's own process
|
|
9
|
+
* inherited neither — so `delphi-review` failed with a missing-variable error while
|
|
10
|
+
* every other gate worked.
|
|
11
|
+
*
|
|
12
|
+
* Resolution is deliberately single-layer. When `ctx.credentials` is present and
|
|
13
|
+
* reports `undefined`, this module does NOT fall back to `process.env`: the service
|
|
14
|
+
* has already consulted the environment, so a second look would resurrect a value it
|
|
15
|
+
* deliberately reported as absent and would make a DSH session resolve differently
|
|
16
|
+
* from a terminal session.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Environment-variable names: a POSIX shell identifier. Anything else cannot name a
|
|
20
|
+
* credential, so it reads as "not set" rather than reaching the store.
|
|
21
|
+
*/
|
|
22
|
+
const REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
23
|
+
function isReferenceName(value) {
|
|
24
|
+
return REFERENCE_NAME.test(value);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build an `ApiKeyResolver` over the available credential service.
|
|
28
|
+
*
|
|
29
|
+
* @param credentials - `ctx.credentials`, or undefined when the host does not offer
|
|
30
|
+
* one. Absence degrades to reading the process environment directly, which keeps
|
|
31
|
+
* the plugin working outside DSH (CLI use, tests).
|
|
32
|
+
* @param env - environment to read in that fallback. Injected rather than read from
|
|
33
|
+
* the global so the module stays testable offline.
|
|
34
|
+
* @param brand - converts a raw name into the service's nominal `CredentialRef`.
|
|
35
|
+
* Injected so this module needs no import of the peer package and tests can pass
|
|
36
|
+
* an identity function.
|
|
37
|
+
*/
|
|
38
|
+
export function createCredentialResolver(credentials, env, brand = (value) => value) {
|
|
39
|
+
return async (ref) => {
|
|
40
|
+
if (!isReferenceName(ref))
|
|
41
|
+
return undefined;
|
|
42
|
+
if (credentials !== undefined) {
|
|
43
|
+
try {
|
|
44
|
+
const resolved = await credentials.resolve(brand(ref));
|
|
45
|
+
return resolved?.value;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A broken provider must not take the tool down; the caller turns the
|
|
49
|
+
// missing value into a DelphiSetupError that names the variable.
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const value = env[ref];
|
|
54
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=credentials.js.map
|