@boyingliu01/xp-gate 0.31.4 → 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 (144) 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 +149 -2
  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-prune.test.ts +61 -0
  126. package/principles/__tests__/cli-files-separators.test.ts +69 -0
  127. package/principles/__tests__/config-merge-warnings.test.ts +148 -0
  128. package/principles/adapters/java.ts +22 -3
  129. package/principles/adapters/typescript.ts +70 -29
  130. package/principles/analyzer.ts +36 -3
  131. package/principles/baseline-prune.ts +19 -7
  132. package/principles/config.ts +56 -35
  133. package/principles/index.ts +122 -40
  134. package/principles/rules/clean-code/deep-nesting.ts +36 -22
  135. package/principles/rules/clean-code/unused-imports.ts +30 -19
  136. package/principles/rules/solid/dip.ts +30 -20
  137. package/principles/rules/solid/lsp.ts +17 -2
  138. package/principles/rules/solid/ocp.ts +33 -23
  139. package/principles/rules/solid/srp.ts +65 -23
  140. package/scripts/delphi-external-review.cjs +25 -2
  141. package/scripts/sync-package-content.js +28 -1
  142. package/skills/delphi-review/AGENTS.md +3 -3
  143. package/skills/sprint-flow/AGENTS.md +3 -3
  144. 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 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.4",
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.4",
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:** dfe3bf53
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.4.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:** dfe3bf53
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.4.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:** dfe3bf53
3
+ **Generated:** 2026-10-28
4
+ **Commit:** 0f58c300
5
5
  **Branch:** main
6
- **Version:** 0.31.4.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
@@ -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