@boyingliu01/xp-gate 0.19.3 → 0.20.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 (173) hide show
  1. package/adapters/gate-4.sh +83 -18
  2. package/adapters/gate-8.sh +49 -5
  3. package/adapters/iac.sh +1 -1
  4. package/adapters/typescript.sh +15 -60
  5. package/bin/xp-gate.js +15 -16
  6. package/gate-10.sh +35 -2
  7. package/gate-4.sh +110 -21
  8. package/gate-8.sh +49 -5
  9. package/hooks/gate-10.sh +35 -2
  10. package/hooks/gate-4.sh +110 -21
  11. package/hooks/gate-8.sh +49 -5
  12. package/hooks/lib/jscpd-run.sh +63 -0
  13. package/hooks/lib/sprint-gate-report.sh +58 -0
  14. package/hooks/lib/test-failure.sh +152 -0
  15. package/hooks/lib/typecheck.sh +141 -0
  16. package/hooks/lib/validate-code-walkthrough.cjs +43 -5
  17. package/hooks/post-merge +100 -0
  18. package/hooks/pre-commit +389 -162
  19. package/hooks/pre-push +75 -19
  20. package/lib/__tests__/bootstrap.test.js +23 -41
  21. package/lib/__tests__/check-version.test.js +8 -1
  22. package/lib/__tests__/detect-deps.test.js +16 -0
  23. package/lib/__tests__/doctor-drift-round2.test.js +212 -0
  24. package/lib/__tests__/doctor-executed-surface.test.js +209 -0
  25. package/lib/__tests__/doctor-hook-drift.test.js +329 -0
  26. package/lib/__tests__/doctor-machine-mutation.test.js +116 -0
  27. package/lib/__tests__/doctor-module-drift.test.js +206 -0
  28. package/lib/__tests__/doctor-module-resolution-parity.test.js +314 -0
  29. package/lib/__tests__/doctor-path-equivalence.test.js +59 -0
  30. package/lib/__tests__/doctor-repair-boundary.test.js +52 -0
  31. package/lib/__tests__/doctor-tui-skill-freshness.test.js +105 -0
  32. package/lib/__tests__/doctor.test.js +155 -0
  33. package/lib/__tests__/gate-runner.test.js +19 -3
  34. package/lib/__tests__/helpers/lib-sandbox.cjs +55 -0
  35. package/lib/__tests__/init.test.js +44 -17
  36. package/lib/__tests__/install-skill-full-dir.test.js +151 -0
  37. package/lib/__tests__/install-skill.test.js +51 -132
  38. package/lib/__tests__/language-tools-detect.test.js +158 -0
  39. package/lib/__tests__/phase-transition.test.js +15 -1
  40. package/lib/__tests__/qoder-delphi-agents.test.js +285 -0
  41. package/lib/__tests__/shared-paths.test.js +20 -0
  42. package/lib/__tests__/shared-utils.test.js +69 -0
  43. package/lib/__tests__/skill-flags.test.js +48 -0
  44. package/lib/__tests__/ui-detector.test.ts +6 -2
  45. package/lib/__tests__/uninstall.test.js +20 -0
  46. package/lib/__tests__/update-hooks.test.js +12 -0
  47. package/lib/__tests__/update-skill.test.js +7 -4
  48. package/lib/detect-deps.js +3 -20
  49. package/lib/doctor-tui.js +22 -5
  50. package/lib/doctor.js +1178 -31
  51. package/lib/init.js +184 -33
  52. package/lib/install-cmd.js +7 -4
  53. package/lib/install-skill.js +38 -95
  54. package/lib/language-tools.js +83 -4
  55. package/lib/mattpocock-license-verdict.json +1 -1
  56. package/lib/shared-paths.js +5 -2
  57. package/lib/shared-utils.js +47 -3
  58. package/lib/uninstall.js +12 -0
  59. package/lib/update-hooks.js +4 -4
  60. package/lib/update-skill.js +6 -7
  61. package/mock-policy/AGENTS.md +4 -4
  62. package/mutation/AGENTS.md +4 -4
  63. package/mutation/__tests__/gate-m.test.ts +66 -14
  64. package/mutation/__tests__/mutmut-runner.test.ts +91 -42
  65. package/mutation/__tests__/test-filter-parity.test.ts +37 -0
  66. package/mutation/gate-m.ts +21 -8
  67. package/package.json +1 -1
  68. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  69. package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  70. package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
  71. package/plugins/claude-code/skills/delphi-review/SKILL.md +98 -14
  72. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +36 -3
  73. package/plugins/claude-code/skills/delphi-review/references/requirements.md +6 -6
  74. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
  75. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +3 -3
  76. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
  77. package/plugins/dsh/README.md +169 -4
  78. package/plugins/dsh/dsh-overlay.patch.yml +67 -0
  79. package/plugins/dsh/package.json +1 -1
  80. package/plugins/dsh/scripts/prepack.cjs +24 -19
  81. package/plugins/dsh/setup-dsh.ps1 +160 -0
  82. package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  83. package/plugins/dsh/skills/delphi-review/AGENTS.md +4 -4
  84. package/plugins/dsh/skills/delphi-review/SKILL.md +98 -14
  85. package/plugins/dsh/skills/delphi-review/references/code-walkthrough.md +36 -3
  86. package/plugins/dsh/skills/delphi-review/references/requirements.md +6 -6
  87. package/plugins/dsh/skills/sprint-flow/AGENTS.md +4 -4
  88. package/plugins/dsh/skills/sprint-flow/references/phase-2-design.md +3 -3
  89. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +4 -4
  90. package/plugins/dsh/src/command.test.ts +66 -2
  91. package/plugins/dsh/src/command.ts +36 -14
  92. package/plugins/dsh/src/contract.test.ts +31 -1
  93. package/plugins/dsh/src/delphi-config.test.ts +190 -0
  94. package/plugins/dsh/src/delphi-config.ts +223 -0
  95. package/plugins/dsh/src/delphi-run.test.ts +181 -0
  96. package/plugins/dsh/src/delphi-run.ts +129 -0
  97. package/plugins/dsh/src/delphi.test.ts +222 -0
  98. package/plugins/dsh/src/delphi.ts +247 -0
  99. package/plugins/dsh/src/dialect.test.ts +59 -0
  100. package/plugins/dsh/src/dialect.ts +40 -0
  101. package/plugins/dsh/src/index.ts +70 -4
  102. package/plugins/dsh/src/skills.test.ts +26 -19
  103. package/plugins/opencode/package.json +1 -1
  104. package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  105. package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
  106. package/plugins/opencode/skills/delphi-review/SKILL.md +98 -14
  107. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +36 -3
  108. package/plugins/opencode/skills/delphi-review/references/requirements.md +6 -6
  109. package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +3 -3
  111. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
  112. package/plugins/qoder/agents/delphi-architecture.md +1 -1
  113. package/plugins/qoder/agents/delphi-feasibility.md +1 -1
  114. package/plugins/qoder/agents/delphi-technical.md +1 -1
  115. package/plugins/qoder/plugin.json +1 -1
  116. package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  117. package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
  118. package/plugins/qoder/skills/delphi-review/SKILL.md +98 -14
  119. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +36 -3
  120. package/plugins/qoder/skills/delphi-review/references/requirements.md +6 -6
  121. package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
  122. package/plugins/qoder/skills/sprint-flow/references/phase-2-design.md +3 -3
  123. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
  124. package/principles/AGENTS.md +9 -8
  125. package/principles/__tests__/adapter-metrics.test.ts +292 -0
  126. package/principles/__tests__/analyzer.test.ts +29 -6
  127. package/principles/__tests__/baseline-prune-timeout.test.ts +71 -0
  128. package/principles/__tests__/baseline-prune.test.ts +338 -0
  129. package/principles/__tests__/boy-scout-init-baseline.test.ts +167 -0
  130. package/principles/__tests__/boy-scout-integration.test.ts +97 -6
  131. package/principles/__tests__/boy-scout.test.ts +253 -7
  132. package/principles/__tests__/config-enforcement.test.ts +469 -0
  133. package/principles/__tests__/config-git-timeout.test.ts +66 -0
  134. package/principles/__tests__/direct-execution.test.ts +187 -0
  135. package/principles/__tests__/index.test.ts +16 -3
  136. package/principles/__tests__/warnings-baseline-budgets.test.ts +74 -0
  137. package/principles/adapters/typescript.ts +613 -13
  138. package/principles/analyzer.ts +22 -0
  139. package/principles/baseline-prune.ts +217 -0
  140. package/principles/boy-scout.ts +481 -51
  141. package/principles/config.ts +362 -54
  142. package/principles/direct-execution.ts +60 -0
  143. package/principles/index.ts +40 -17
  144. package/principles/rules/__tests__/clean-code/many-exports.test.ts +16 -10
  145. package/principles/rules/clean-code/code-duplication.ts +8 -8
  146. package/principles/rules/clean-code/deep-nesting.ts +8 -8
  147. package/principles/rules/clean-code/god-class.ts +8 -8
  148. package/principles/rules/clean-code/large-file.ts +15 -11
  149. package/principles/rules/clean-code/long-function.ts +8 -8
  150. package/principles/rules/clean-code/magic-numbers.ts +10 -7
  151. package/principles/rules/clean-code/many-exports.ts +7 -7
  152. package/principles/rules/clean-code/missing-error-handling.ts +5 -5
  153. package/principles/rules/clean-code/too-many-params.ts +8 -8
  154. package/principles/rules/clean-code/unused-imports.ts +5 -5
  155. package/principles/rules/solid/dip.ts +18 -8
  156. package/principles/rules/solid/isp.ts +9 -8
  157. package/principles/rules/solid/lsp.ts +6 -6
  158. package/principles/rules/solid/ocp.ts +6 -5
  159. package/principles/rules/solid/srp.ts +7 -8
  160. package/principles/types.ts +15 -0
  161. package/scripts/__tests__/sync-package-content.test.js +28 -0
  162. package/scripts/delphi-external-review.cjs +193 -10
  163. package/scripts/sync-package-content.js +7 -4
  164. package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -1
  165. package/skills/delphi-review/AGENTS.md +4 -4
  166. package/skills/delphi-review/SKILL.md +98 -14
  167. package/skills/delphi-review/references/code-walkthrough.md +36 -3
  168. package/skills/delphi-review/references/requirements.md +6 -6
  169. package/skills/sprint-flow/AGENTS.md +4 -4
  170. package/skills/sprint-flow/references/phase-2-design.md +3 -3
  171. package/skills/test-specification-alignment/AGENTS.md +4 -4
  172. package/lib/__tests__/download-skill.test.js +0 -281
  173. package/lib/download-skill.js +0 -96
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Delphi cross-review over an external OpenAI-compatible provider.
3
+ *
4
+ * This is the codified form of the manual run that produced PR #456: three
5
+ * DISTINCT models, each asked the same question independently and anonymously,
6
+ * iterated until the approval ratio clears the consensus threshold.
7
+ *
8
+ * Why a plugin tool rather than a shell one-liner: the DSH tool plane is where
9
+ * a session actually lives, so the mechanism has to be callable from there.
10
+ * DSH's own `tool-subagent` cannot serve this purpose -- subagents inherit one
11
+ * host-configured model, so they cannot guarantee three distinct models, and
12
+ * Delphi's whole value rests on that distinctness.
13
+ *
14
+ * @test REQ-DSH-016
15
+ * @intent 用外部 provider 的多个不同模型做匿名交叉评审并判定共识,
16
+ * 把「多模型交叉评审」固化成 DSH 可调用的机制而非一次性脚本。
17
+ * @covers AC-DSH-016-01
18
+ */
19
+
20
+ /** One expert seat. `model` is the id sent to the provider. */
21
+ export interface ExpertSeat {
22
+ readonly id: string
23
+ readonly role: string
24
+ readonly model: string
25
+ /** Reasoning models burn hidden tokens, so seats need independent budgets. */
26
+ readonly maxTokens?: number
27
+ }
28
+
29
+ /** A single expert's parsed verdict for one round. */
30
+ export interface ExpertVerdict {
31
+ readonly id: string
32
+ readonly role: string
33
+ readonly requestedModel: string
34
+ readonly resolvedModel: string | null
35
+ readonly verdict: "APPROVED" | "REQUEST_CHANGES" | "INVALID_NO_VERDICT" | "ERROR"
36
+ readonly confidence: number
37
+ /** Only well-formed verdicts may count toward consensus. */
38
+ readonly counted: boolean
39
+ readonly report: string
40
+ readonly error?: string
41
+ }
42
+
43
+ /** Aggregated outcome of one round. */
44
+ export interface RoundResult {
45
+ readonly round: number
46
+ readonly verdicts: readonly ExpertVerdict[]
47
+ readonly validCount: number
48
+ readonly approvals: number
49
+ readonly consensusRatio: number
50
+ readonly reachedConsensus: boolean
51
+ }
52
+
53
+ export interface DelphiOptions {
54
+ /** OpenAI-compatible base URL, e.g. https://host/gpt-proxy */
55
+ readonly baseUrl: string
56
+ /** Bearer token. Callers read this from the environment; never hardcode it. */
57
+ readonly apiKey: string
58
+ readonly seats: readonly ExpertSeat[]
59
+ /** Percent, 0-100. Delphi default is 90. */
60
+ readonly thresholdPercent?: number
61
+ readonly maxRounds?: number
62
+ /** Per-request ceiling; the caller owns the overall timeout. */
63
+ readonly timeoutMs?: number
64
+ readonly fetchImpl?: typeof fetch
65
+ }
66
+
67
+ /** Delphi's defining invariant: exactly three seats and a 90% approval bar. */
68
+ const REQUIRED_EXPERTS = 3
69
+ const DEFAULT_THRESHOLD_PERCENT = 90
70
+
71
+ const DEFAULT_MAX_TOKENS = 8000
72
+ const DEFAULT_TIMEOUT_MS = 180_000
73
+
74
+ /** Raised when the configured seats cannot satisfy the Delphi invariants. */
75
+ export class DelphiConfigError extends Error {}
76
+
77
+ /**
78
+ * Reject a seat configuration that cannot produce a valid Delphi review.
79
+ * Checked BEFORE any network call so a misconfiguration never looks like a
80
+ * provider failure.
81
+ */
82
+ export function validateSeats(seats: readonly ExpertSeat[]): void {
83
+ if (seats.length !== REQUIRED_EXPERTS) {
84
+ throw new DelphiConfigError(
85
+ `Delphi requires exactly ${REQUIRED_EXPERTS} experts, got ${seats.length}.`,
86
+ )
87
+ }
88
+ for (const s of seats) {
89
+ if (!s.model || s.model.trim() === "") {
90
+ throw new DelphiConfigError(`Expert "${s.id}" has no model (a local fallback cannot count).`)
91
+ }
92
+ if (s.model.includes("${")) {
93
+ throw new DelphiConfigError(`Expert "${s.id}" model "${s.model}" looks like an unexpanded env var.`)
94
+ }
95
+ }
96
+ const distinct = new Set(seats.map((s) => s.model.trim()))
97
+ if (distinct.size !== REQUIRED_EXPERTS) {
98
+ throw new DelphiConfigError(
99
+ `All ${REQUIRED_EXPERTS} experts must use distinct models; got: ${[...distinct].join(", ")}.`,
100
+ )
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Parse an expert report into a verdict.
106
+ *
107
+ * The verdict token usually sits on the line AFTER a "### VERDICT" heading, so
108
+ * a same-line `VERDICT: X` pattern misses every real report. A report with no
109
+ * recognisable verdict is INVALID, never a guess: inferring approval from prose
110
+ * would let the gate pass on an assumption.
111
+ */
112
+ export function parseVerdict(text: string): {
113
+ verdict: ExpertVerdict["verdict"]
114
+ confidence: number
115
+ } {
116
+ const section = /VERDICT[\s\S]{0,120}?\b(APPROVED|REQUEST_CHANGES|PASS_WITH_CAVEATS|BLOCKED)\b/i.exec(text)
117
+ if (!section) return { verdict: "INVALID_NO_VERDICT", confidence: 0 }
118
+
119
+ const raw = section[1].toUpperCase()
120
+ // PASS_WITH_CAVEATS / BLOCKED are not approvals under the 90% rule.
121
+ const verdict = raw === "APPROVED" ? "APPROVED" : "REQUEST_CHANGES"
122
+
123
+ const c = /CONFIDENCE[\s\S]{0,40}?(\d{1,2})/i.exec(text)
124
+ const confidence = c ? Math.min(10, Math.max(0, Number(c[1]))) : 0
125
+ return { verdict, confidence }
126
+ }
127
+
128
+ interface ChatResponse {
129
+ model?: string
130
+ choices?: Array<{ finish_reason?: string; message?: { content?: string } }>
131
+ }
132
+
133
+ /** One provider call. Throws on transport failure or empty content. */
134
+ async function chatOnce(
135
+ opts: DelphiOptions,
136
+ seat: ExpertSeat,
137
+ system: string,
138
+ user: string,
139
+ ): Promise<{ content: string; resolvedModel: string | null; finishReason: string }> {
140
+ const doFetch = opts.fetchImpl ?? fetch
141
+ const controller = new AbortController()
142
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
143
+
144
+ try {
145
+ const res = await doFetch(`${opts.baseUrl.replace(/\/+$/, "")}/v1/chat/completions`, {
146
+ method: "POST",
147
+ headers: {
148
+ Authorization: `Bearer ${opts.apiKey}`,
149
+ "Content-Type": "application/json",
150
+ },
151
+ body: JSON.stringify({
152
+ model: seat.model,
153
+ messages: [
154
+ { role: "system", content: system },
155
+ { role: "user", content: user },
156
+ ],
157
+ max_tokens: seat.maxTokens ?? DEFAULT_MAX_TOKENS,
158
+ temperature: 0.2,
159
+ }),
160
+ signal: controller.signal,
161
+ })
162
+
163
+ if (!res.ok) {
164
+ const body = await res.text().catch(() => "")
165
+ throw new Error(`HTTP ${res.status}: ${body.slice(0, 200)}`)
166
+ }
167
+
168
+ const json = (await res.json()) as ChatResponse
169
+ const content = json.choices?.[0]?.message?.content ?? ""
170
+ const finishReason = json.choices?.[0]?.finish_reason ?? "unknown"
171
+
172
+ // Empty content is a real failure -- typically a reasoning model that spent
173
+ // its whole budget on hidden tokens. Never let it pass as an empty review.
174
+ if (content.trim() === "") {
175
+ throw new Error(
176
+ `empty content (finish_reason=${finishReason}, max_tokens=${seat.maxTokens ?? DEFAULT_MAX_TOKENS}); ` +
177
+ `raise max_tokens if this is a reasoning model`,
178
+ )
179
+ }
180
+
181
+ return { content, resolvedModel: json.model ?? null, finishReason }
182
+ } finally {
183
+ clearTimeout(timer)
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Run ONE round. All seats are asked concurrently and never see each other's
189
+ * output, which is what makes the round anonymous.
190
+ */
191
+ export async function runRound(
192
+ opts: DelphiOptions,
193
+ round: number,
194
+ system: string,
195
+ buildPrompt: (seat: ExpertSeat) => string,
196
+ ): Promise<RoundResult> {
197
+ const settled = await Promise.allSettled(
198
+ opts.seats.map(async (seat) => {
199
+ const r = await chatOnce(opts, seat, system, buildPrompt(seat))
200
+ return { seat, ...r }
201
+ }),
202
+ )
203
+
204
+ const verdicts: ExpertVerdict[] = settled.map((s, i) => {
205
+ const seat = opts.seats[i]
206
+ if (s.status !== "fulfilled") {
207
+ return {
208
+ id: seat.id,
209
+ role: seat.role,
210
+ requestedModel: seat.model,
211
+ resolvedModel: null,
212
+ verdict: "ERROR",
213
+ confidence: 0,
214
+ counted: false,
215
+ report: "",
216
+ error: s.reason instanceof Error ? s.reason.message : String(s.reason),
217
+ }
218
+ }
219
+ const { verdict, confidence } = parseVerdict(s.value.content)
220
+ return {
221
+ id: seat.id,
222
+ role: seat.role,
223
+ requestedModel: seat.model,
224
+ resolvedModel: s.value.resolvedModel,
225
+ verdict,
226
+ confidence,
227
+ counted: verdict !== "INVALID_NO_VERDICT",
228
+ report: s.value.content,
229
+ }
230
+ })
231
+
232
+ const counted = verdicts.filter((v) => v.counted)
233
+ const approvals = counted.filter((v) => v.verdict === "APPROVED").length
234
+ // Consensus is measured over VALID responses: a malformed report is not a
235
+ // dissent, but it cannot be counted as agreement either.
236
+ const ratio = counted.length === 0 ? 0 : approvals / counted.length
237
+ const threshold = opts.thresholdPercent ?? DEFAULT_THRESHOLD_PERCENT
238
+
239
+ return {
240
+ round,
241
+ verdicts,
242
+ validCount: counted.length,
243
+ approvals,
244
+ consensusRatio: ratio,
245
+ reachedConsensus: counted.length === REQUIRED_EXPERTS && ratio * 100 >= threshold,
246
+ }
247
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { detectDialect, shellQuote, shq } from "./dialect.js"
3
+
4
+ /**
5
+ * @test REQ-DSH-010
6
+ * @intent 验证 shell 方言探测与转义:win32 必须选 powershell(DSH 在 win32 启用 pwsh
7
+ * executor),其余平台选 posix,且两套转义都不能让引号/元字符逃逸出字符串
8
+ * @covers AC-DSH-010-01
9
+ */
10
+ describe("detectDialect", () => {
11
+ it("selects powershell on win32 and posix elsewhere", () => {
12
+ expect(detectDialect("win32")).toBe("powershell")
13
+ expect(detectDialect("linux")).toBe("posix")
14
+ expect(detectDialect("darwin")).toBe("posix")
15
+ })
16
+
17
+ it("matches the platform DSH actually enables an executor for", () => {
18
+ // dsh-base enables pwsh-sandbox/tool-pwsh only on win32, and
19
+ // bash-sandbox/tool-bash everywhere else.
20
+ expect(detectDialect()).toBe(process.platform === "win32" ? "powershell" : "posix")
21
+ })
22
+ })
23
+
24
+ describe("shellQuote", () => {
25
+ it("uses the POSIX quote-close idiom", () => {
26
+ expect(shellQuote("a'b", "posix")).toBe("'a'\\''b'")
27
+ expect(shellQuote("abc", "posix")).toBe("'abc'")
28
+ })
29
+
30
+ it("doubles embedded quotes for PowerShell", () => {
31
+ expect(shellQuote("a'b", "powershell")).toBe("'a''b'")
32
+ expect(shellQuote("abc", "powershell")).toBe("'abc'")
33
+ })
34
+
35
+ it("neutralizes POSIX shell metacharacters literally", () => {
36
+ const payload = "$(touch /tmp/pwned); `id`"
37
+ expect(shellQuote(payload, "posix")).toBe("'$(touch /tmp/pwned); `id`'")
38
+ })
39
+
40
+ /**
41
+ * @test REQ-DSH-011
42
+ * @intent 验证 PowerShell 单引号内 $() 与反引号均为字面量,且内嵌引号被双写,
43
+ * 使命令注入无法闭合字符串逃逸
44
+ * @covers AC-DSH-011-01
45
+ */
46
+ it("neutralizes PowerShell metacharacters and prevents quote escape", () => {
47
+ const payload = "$(touch /tmp/pwned); `id`"
48
+ expect(shellQuote(payload, "powershell")).toBe("'$(touch /tmp/pwned); `id`'")
49
+ // A payload that tries to close the string must have its quote doubled so it
50
+ // stays inside a single literal argument.
51
+ expect(shellQuote("a'; Remove-Item x; '", "powershell")).toBe("'a''; Remove-Item x; '''")
52
+ })
53
+ })
54
+
55
+ describe("shq (deprecated POSIX wrapper)", () => {
56
+ it("delegates to shellQuote with the posix dialect", () => {
57
+ expect(shq("a'b")).toBe(shellQuote("a'b", "posix"))
58
+ })
59
+ })
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shell dialect selection and quoting.
3
+ *
4
+ * DSH chooses its shell backend per platform: `dsh-base` disables
5
+ * `bash-sandbox`/`tool-bash` and enables `pwsh-sandbox`/`tool-pwsh` on win32.
6
+ * A command string written for the wrong dialect fails to parse, so callers
7
+ * must pick the dialect to match the executor rather than assume POSIX.
8
+ */
9
+
10
+ export type ShellDialect = "posix" | "powershell"
11
+
12
+ /** Quote one argument for the target dialect. */
13
+ export function shellQuote(value: string, dialect: ShellDialect): string {
14
+ if (dialect === "posix") {
15
+ // POSIX single-quote escaping: `'` becomes `'\''`.
16
+ return `'${value.replace(/'/g, "'\\''")}'`
17
+ }
18
+ // PowerShell single-quoted strings escape an embedded quote by doubling it.
19
+ // Backticks and `$(…)` are literal inside single quotes, so no further
20
+ // escaping is needed to keep them inert.
21
+ return `'${value.replace(/'/g, "''")}'`
22
+ }
23
+
24
+ /**
25
+ * POSIX single-quote shell escaping: `'` becomes `'\''`.
26
+ * @deprecated Use {@link shellQuote} with an explicit dialect.
27
+ */
28
+ export function shq(value: string): string {
29
+ return shellQuote(value, "posix")
30
+ }
31
+
32
+ /**
33
+ * Pick the dialect for the current platform.
34
+ *
35
+ * `process.platform` is the reliable signal available inside a tool body; it
36
+ * mirrors exactly where DSH enables each sandbox/executor pair.
37
+ */
38
+ export function detectDialect(platform: NodeJS.Platform = process.platform): ShellDialect {
39
+ return platform === "win32" ? "powershell" : "posix"
40
+ }
@@ -1,7 +1,9 @@
1
1
  import { defineTool } from "@deepseek-ai/dsh-tools"
2
2
  import type { Context } from "@deepseek-ai/cordis"
3
- import { GATE_WHITELIST, buildCommand, resolveTarget } from "./command.js"
3
+ import { GATE_WHITELIST, buildCommand, detectDialect, resolveTarget } from "./command.js"
4
4
  import { runXpGate } from "./gate-runner.js"
5
+ import { DelphiSetupError, loadDelphiConfig, toDelphiOptions } from "./delphi-config.js"
6
+ import { formatOutcome, runReview } from "./delphi-run.js"
5
7
 
6
8
  export const name = "tool-xp-gate"
7
9
  export const inject = ["tools", "shell"]
@@ -10,6 +12,9 @@ const DEFAULT_TIMEOUT_MS = 120_000
10
12
 
11
13
  export function apply(ctx: Context, _config: unknown = {}): void {
12
14
  const shell = ctx.shell
15
+ // DSH runs a POSIX shell off-Windows and PowerShell on Windows; generating the
16
+ // wrong dialect makes every tool fail to parse rather than degrade gracefully.
17
+ const dialect = detectDialect()
13
18
 
14
19
  ctx.tools.register(
15
20
  defineTool({
@@ -36,7 +41,7 @@ export function apply(ctx: Context, _config: unknown = {}): void {
36
41
  async execute(args, exec) {
37
42
  const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
38
43
  const target = resolveTarget(args.path, cwd)
39
- const command = buildCommand({ subcommand: "check", target, gates: args.gates })
44
+ const command = buildCommand({ subcommand: "check", target, gates: args.gates, dialect })
40
45
  return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
41
46
  },
42
47
  }),
@@ -62,7 +67,7 @@ export function apply(ctx: Context, _config: unknown = {}): void {
62
67
  async execute(args, exec) {
63
68
  const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
64
69
  const target = resolveTarget(args.path, cwd)
65
- const command = buildCommand({ subcommand: "principles", target })
70
+ const command = buildCommand({ subcommand: "principles", target, dialect })
66
71
  return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
67
72
  },
68
73
  }),
@@ -86,9 +91,70 @@ export function apply(ctx: Context, _config: unknown = {}): void {
86
91
  timeoutMs: DEFAULT_TIMEOUT_MS,
87
92
  async execute(args, exec) {
88
93
  const cwd = exec.agent?.session?.header?.cwd ?? process.cwd()
89
- const command = buildCommand({ subcommand: "arch", config: args.config })
94
+ const command = buildCommand({ subcommand: "arch", config: args.config, dialect })
90
95
  return runXpGate(shell, exec.signal, command, cwd, DEFAULT_TIMEOUT_MS)
91
96
  },
92
97
  }),
93
98
  )
99
+
100
+ // Delphi multi-model cross-review.
101
+ //
102
+ // DSH's own subagents cannot do this: they inherit one host-configured model,
103
+ // so three "experts" would be three instances of the same model, and Delphi's
104
+ // value rests entirely on the reviewers being DIFFERENT models. This tool
105
+ // therefore calls an external OpenAI-compatible provider directly, with one
106
+ // distinct model per seat.
107
+ ctx.tools.register(
108
+ defineTool({
109
+ name: "delphi-review",
110
+ description:
111
+ "Run a Delphi cross-review of a subject (a diff, a design, a spec) using three DISTINCT models from an external OpenAI-compatible provider configured in .delphi-config.json. Round 1 is anonymous and concurrent; later rounds feed back the aggregate until the approval ratio reaches the configured threshold (default 90%) or the round cap (default 5). Use this whenever a change needs independent multi-model review rather than a single model's opinion.",
112
+ parameters: {
113
+ subject: {
114
+ type: "string",
115
+ required: true,
116
+ description:
117
+ "The material to review, inline. For a diff, pass the diff text (or a summarized form if very large).",
118
+ },
119
+ addressed: {
120
+ type: "string",
121
+ description:
122
+ "Optional description of what changed since a previous round, fed back to later rounds so reviewers can revise their position.",
123
+ },
124
+ maxRounds: {
125
+ type: "number",
126
+ description: "Override the configured round cap (Delphi default is 5).",
127
+ },
128
+ cwd: {
129
+ type: "string",
130
+ description: "Project root containing .delphi-config.json (defaults to the session cwd).",
131
+ },
132
+ },
133
+ output: {
134
+ schema: { type: "string" },
135
+ render: (_args, value) => [{ type: "text", text: value }],
136
+ },
137
+ // Multi-round reviews over large inputs are slow; the transport applies
138
+ // its own per-request timeout on top of this ceiling.
139
+ timeoutMs: 900_000,
140
+ async execute(args, exec) {
141
+ const cwd = args.cwd ?? exec.agent?.session?.header?.cwd ?? process.cwd()
142
+ try {
143
+ const cfg = loadDelphiConfig(cwd)
144
+ const outcome = await runReview(
145
+ { ...toDelphiOptions(cfg), maxRounds: args.maxRounds ?? cfg.maxRounds },
146
+ args.subject,
147
+ args.addressed ?? "",
148
+ )
149
+ const seats = cfg.seats.map((s) => s.model).join(", ")
150
+ return `${formatOutcome(outcome)}\n\nProvider: ${cfg.provider}\nSeats: ${seats}`
151
+ } catch (e) {
152
+ // Config problems are the common failure; report them as guidance
153
+ // rather than a stack trace.
154
+ if (e instanceof DelphiSetupError) return `Delphi review not started: ${e.message}`
155
+ throw e
156
+ }
157
+ },
158
+ }),
159
+ )
94
160
  }
@@ -7,22 +7,22 @@ import { parse } from "yaml"
7
7
  /** Mirrors `SKILL_NAME` in @deepseek-ai/dsh-skill. */
8
8
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
9
9
 
10
- const EXPECTED_SKILLS = [
11
- "admin-template-guidelines",
12
- "batch-grill-me",
13
- "delphi-review",
14
- "domain-modeling",
15
- "grilling",
16
- "grill-with-docs",
17
- "improve-codebase-architecture",
18
- "ralph-loop",
19
- "sprint-flow",
20
- "test-driven-development",
21
- "test-specification-alignment",
22
- "to-issues",
23
- ]
10
+ const PLUGIN_ROOT = resolve(fileURLToPath(new URL("..", import.meta.url)))
11
+ const SKILLS_DIR = join(PLUGIN_ROOT, "skills")
12
+ const REPO_SKILLS_DIR = resolve(PLUGIN_ROOT, "..", "..", "skills")
24
13
 
25
- const SKILLS_DIR = resolve(fileURLToPath(new URL("..", import.meta.url)), "skills")
14
+ /**
15
+ * The expected set is the REPO's skill directories, discovered rather than
16
+ * hardcoded. A hardcoded list silently drifts: it previously named 12 skills and
17
+ * omitted clipboard-vision, while prepack shipped 12 of 13 (#448).
18
+ */
19
+ function repoSkills(): string[] {
20
+ return readdirSync(REPO_SKILLS_DIR, { withFileTypes: true })
21
+ .filter((d) => d.isDirectory())
22
+ .map((d) => d.name)
23
+ .filter((name) => existsSync(join(REPO_SKILLS_DIR, name, "SKILL.md")))
24
+ .sort()
25
+ }
26
26
 
27
27
  function readFrontmatter(file: string): Record<string, unknown> {
28
28
  const raw = readFileSync(file, "utf8")
@@ -32,22 +32,25 @@ function readFrontmatter(file: string): Record<string, unknown> {
32
32
  }
33
33
 
34
34
  describe("bundled skills", () => {
35
- it("ships all 12 skills as directories", () => {
35
+ it("ships every repo skill as a directory", () => {
36
36
  expect(existsSync(SKILLS_DIR)).toBe(true)
37
37
  const dirs = readdirSync(SKILLS_DIR, { withFileTypes: true })
38
38
  .filter((d) => d.isDirectory())
39
39
  .map((d) => d.name)
40
40
  .sort()
41
- expect(dirs).toEqual([...EXPECTED_SKILLS].sort())
41
+ expect(dirs).toEqual(repoSkills())
42
42
  })
43
43
 
44
44
  /**
45
45
  * @test REQ-DSH-006
46
- * @intent 验证随包 12 个 skills 目录里各 SKILL.md 的 frontmatter 满足 DSH 契约(name kebab-case + description 非空)
46
+ * @intent 验证随包 skills 目录里各 SKILL.md 的 frontmatter 满足 DSH 契约
47
+ * (name kebab-case + description 非空),且集合与仓库 skills/ 一致(#448)
47
48
  * @covers AC-DSH-006-01
48
49
  */
49
50
  it("every SKILL.md satisfies the DSH frontmatter contract", () => {
50
- for (const name of EXPECTED_SKILLS) {
51
+ const names = repoSkills()
52
+ expect(names.length).toBeGreaterThan(0)
53
+ for (const name of names) {
51
54
  const fm = readFrontmatter(join(SKILLS_DIR, name, "SKILL.md"))
52
55
  expect(fm.name, `${name}: frontmatter.name`).toBeTypeOf("string")
53
56
  expect(SKILL_NAME.test(fm.name as string), `${name}: kebab-case name`).toBe(true)
@@ -55,4 +58,8 @@ describe("bundled skills", () => {
55
58
  expect((fm.description as string).trim().length, `${name}: non-empty description`).toBeGreaterThan(0)
56
59
  }
57
60
  })
61
+
62
+ it("bundles clipboard-vision, which the old hardcoded list omitted (#448)", () => {
63
+ expect(existsSync(join(SKILLS_DIR, "clipboard-vision", "SKILL.md"))).toBe(true)
64
+ })
58
65
  })
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/opencode-plugin",
3
- "version": "0.19.3",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
@@ -19,7 +19,6 @@ describe('clipboard-vision distribution', () => {
19
19
  'scripts/build-plugin.mjs',
20
20
  'scripts/build-plugin.sh',
21
21
  'scripts/test-plugins.mjs',
22
- 'scripts/test-plugins.sh',
23
22
  'src/npm-package/scripts/sync-package-content.js',
24
23
  'plugins/opencode/scripts/prepack.cjs',
25
24
  'src/npm-package/plugins/opencode/scripts/prepack.cjs',
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-09-24
4
- **Commit:** 467174d
5
- **Branch:** main
6
- **Version:** 0.19.3.0
3
+ **Generated:** 2026-10-07
4
+ **Commit:** ff55f35b
5
+ **Branch:** fix/batch-defects-20261007
6
+ **Version:** 0.20.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.