@boyingliu01/dsh-plugin-xp-gate
XP-Gate 确定性质量门禁 + AI 工作流技能,作为 DeepSeek Harness (DSH) 原生插件(bundle)。
关联 Issue: #393 设计文档:
docs/plans/2026-08-23-dsh-plugin-design.md集成评估:docs/plans/2026-09-30-dsh-integration-assessment.md
Tools
| Tool | Purpose |
|---|---|
gate-check | Run XP-Gate quality gates (xp-gate check) on a file or directory |
gate-principles | Run the Clean Code/SOLID rules (xp-gate principles, Gate 4) |
gate-arch | Run architecture validation (xp-gate arch, Gate 6) |
delphi-review | 多模型交叉评审:用 3 个不同模型独立评审同一份材料并迭代到共识(见下) |
Install
dsh plugin --profile <name> add @boyingliu01/dsh-plugin-xp-gate
The gate-* tools require a global xp-gate CLI:
npm install -g @boyingliu01/xp-gate
When xp-gate is missing those tools degrade gracefully (tool-missing → SKIP-style message) rather than erroring.
跨平台:shell 方言层
DSH 在非 Windows 上使用 POSIX shell、在 Windows 上使用 PowerShell executor
(dsh-base bundle 里 bash-sandbox 在 win32 被禁用、pwsh-sandbox 启用)。
插件最初硬编码 POSIX 命令串,导致三个工具在 Windows 上全部解析失败:
[stderr] ParserError:
1 | if command -v xp-gate >/dev/null 2>&1; then xp-gate principles 'D:\pr …
| ~
| if语句中的'if'后缺少"(”。
[exit code: 1]
现已按平台生成对应方言(src/dialect.ts):
| POSIX | PowerShell |
|---|---|
command -v X | Get-Command X -ErrorAction SilentlyContinue |
printf '%s\n' '...' | Write-Output '...' |
'\''(单引号转义) | ''(单引号内双写) |
实测(Windows + PowerShell executor):gate-check exit 0、gate-principles exit 0(Rules run: 15)。
多模型交叉评审(delphi-review)
为什么需要它,而不是用 DSH 的 subagent
Delphi 方法的价值完全建立在「评审者是不同模型」之上。DSH 的 subagent 工具继承宿主
配置的单一模型,因此三个"专家"会是同一个模型的三个实例 —— 按 delphi-review skill
自身的规则必须 BLOCK。
所以本工具直接调用外部 OpenAI 兼容网关,每个席位绑定一个不同模型。
另一条路线是打开 DSH 的
subagentModelSelection(Web GUI → Plugins),让 subagent 也能选模型。两者并不冲突:那条路线让 DSH 原生 subagent 具备多模型能力,本工具则在 不依赖宿主配置的前提下保证不变量成立(包括 CI / 一次性会话)。
配置
复用项目既有的 .delphi-config.json(skill 与工具同一份配置):
{
"active_profile": "whalecloud",
"profiles": {
"whalecloud": {
"providers": {
"whalecloud": {
"base_url": "https://<gateway>/gpt-proxy",
"api_key": "${WHALECLOUD_API_KEY}" // 支持 ${VAR} 与 {env:VAR} 两种写法
}
},
"experts": { // 对象形式;key 即角色
"architecture": { "provider": "whalecloud", "model": "g-glm-5.3-flash" },
"technical": { "provider": "whalecloud", "model": "g-qwen3.8-flash" },
"feasibility": { "provider": "whalecloud", "model": "g-deepseek-flash" }
}
}
},
"consensus": { "threshold_percent": 90, "max_review_rounds": 5 }
}
密钥只从环境变量读取,绝不写入配置文件。
experts 同时接受对象形式(key 为角色)与数组形式(explicit role 字段)。
强制的不变量(代码强制,非文档约定)
| 不变量 | 违反时 |
|---|---|
| 恰好 3 位专家 | DelphiConfigError |
3 个 requested_model 去空白后两两不同 | DelphiConfigError |
模型不得为未展开的 ${...} 占位符 | DelphiConfigError(在网络调用之前) |
| 缺密钥 / 缺 provider / 专家跨网关 | DelphiSetupError,报出变量名而非回显配置 |
报告缺 ### VERDICT 段 | 记为 INVALID_NO_VERDICT,不计入共识(绝不从散文里猜裁决) |
| 退出码 0 但内容为空 | 记为 ERROR(推理模型可能把预算全烧在隐藏 token 上) |
| 共识按全体席位数计算 | 一份畸形报告不能靠其余全票"凑"出共识 |
共识阈值为百分比:3 席时 ≥90% 意味着必须 3/3 通过。
轮次语义
- Round 1 匿名且并发:各席位互不可见。
- Round 2+:回传汇总票型(
architecture: APPROVED…)与"已修改项", 各席位据此修正或坚持立场。个体报告始终匿名 —— 只共享计票,这是方法可信的前提。 - 达到阈值即停;否则到
max_review_rounds(默认 5)为止,并如实返回NO CONSENSUS。
已知行为(实测)
用一段故意有缺陷的样例代码喂给三个模型,15 次调用(5 轮 × 3 模型)全部成功且
resolved_model 两两不同,三个模型独立指出同一个真实缺陷:
{ ok: true } 是无条件返回的常量,"把退出码契约换成了一个常量,而不是真正的结果信号"。
机制拒绝给出共识,而不是盖章通过 —— 这正是它该有的行为。
配置界面(Plugins 页)
插件声明了 dsh.client(platform: web),在 Web GUI 里注册一个行配置页:
侧边栏 Plugins → @boyingliu01/dsh-plugin-xp-gate → 组件 tool-xp-gate → 配置
该页面可以编辑 Delphi 三席(角色 / 模型)、网关、密钥引用与共识参数;模型可从 宿主已配置的 provider 目录下拉选择,也可手填。
来源优先级
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 | 本插件配置里的 delphi 段(本页写入) | 一旦保存了席位,评审就用它 |
| 2 | 项目根 .delphi-config.json | 席位为空时自动回落,老项目无需改动 |
页面上如果 seats 为空,评审继续走 .delphi-config.json,所以打开页面不会静默改变行为。
宿主强制的不变量(不是表单装饰)
表单只是方便,宿主是权威。每次评审前 seatsFromSettings 重新校验:
- 恰好 3 席
- 3 个 model 去空白后两两不同
- 模型不得为空或未展开的
${...}占位符 - 配了席位就必须有
base_url api_key必须是凭据引用(${VAR}/{env:VAR});写入明文 token 会被拒绝, 因为那会把密钥落到 profile 文件里
为什么不在表单里就拦死:DSH 的 settings 表单只能编辑
.volatile()字段, 而 volatile 意味着保存即热生效。对"每次运行都花钱"的配置来说,前端校验不够, 所以校验放在宿主、在发出任何请求之前。
关于 bash 守卫
delphiGuard 用只读工具白名单判定:只有 read / grep / glob / read_image /
XP-Gate 自己的报告类工具算只读,其余一律视为可写(包括 bash、pwsh 和将来新增的工具)。
早期版本用"已知写工具黑名单"(write / edit / str_replace_editor),
bash 一条命令就能改文件,等于这道 HARD-GATE 可绕过。白名单的代价是可能误拦新加的
只读工具——那会表现为"拦得太狠",而不是"悄悄放行"。
Skills
本包在 publish 时通过 scripts/prepack.cjs 捆绑仓库 skills/(当前 12 个)。
要让 DSH 直接看到仓库版 skill(而非 ~/.agents/skills 里指向 ~/.qoder/skills 的 junction),
运行仓库提供的准备脚本:
pwsh -File plugins/dsh/setup-dsh.ps1 # 全部(skill 链接 + 编译 + 提示)
pwsh -File plugins/dsh/setup-dsh.ps1 -WhatIf # 只预览
该脚本在 $DSH_HOME/skills(DSH 发现根 rank 400)为仓库每个 skill 建 junction,保持单一真源。
DSH 只读 skill frontmatter 的 6 个字段:
name、description、whenToUse、metadata、disable-model-invocation、user-invocable。 本仓库 skill 里的allowed-tools/hooks/triggers/maturity/auto_continue会被静默忽略(无害,但不生效)。references/子目录可用 —— DSH 的skill工具返回 base directory,模型按需用 read 工具加载。
备选路线:启用 DSH 原生 subagent 模型选择
上面的 delphi-review 工具不依赖宿主配置即可保证不变量。若你希望 DSH 原生
subagent 工具也能指定模型(例如让 Agent Teams 的队友跑不同模型),需显式启用
(Web GUI → Plugins 页面):
subagent model selection:
enabled = true
allowedModels = [
{ provider: 'whalecloud', model: 'g-glm-5.3-flash' },
{ provider: 'whalecloud', model: 'g-qwen3.8-flash' },
{ provider: 'whalecloud', model: 'g-deepseek-flash' }
]
启用后 subagent 工具会新增 provider / model / reasoning_effort 参数,
并注册 list_subagent_models 工具。
注意:
dsh-tool-subagent的modelSelectionSettings: true只是能力开关; 实际策略来自宿主设置SubagentModelSelectionConfig(enabled默认false,allowedModels默认[])。两者都要满足才会生效。
Build
npm install --ignore-scripts # --ignore-scripts 绕过 esbuild postinstall(受限环境会 EBUSY)
npm run build # tsc -> lib/,并把 client/client.js 复制成 lib/client.js
npm run prepare-skills # 从仓库 skills/ 复制
npm test # vitest
或在仓库根目录:
npm run build:dsh-plugin # = node scripts/build-plugin.mjs --platform dsh
client/client.js是手写的浏览器 bundle,不走 tsc:它以 DSH 的客户端模块格式 (window.__ModuleLoader__.load) 编写,引用了浏览器全局,不是 TypeScript 模块。npm run build的第二步(scripts/copy-client.cjs)负责把它放进lib/。
注意
lib/与skills/是构建产物(已 gitignore),不提交;client/client.js是源文件,要提交。scripts/prepack.cjs的 skill 列表为动态发现(读取仓库skills/),不硬编码。plugins/dsh/package.json的 peerDeps 仍 pin0.1.1-rc.2,但实测运行在0.2.0-rc.2上defineTool/TOOL_ABORTED/HarnessError签名未变,无需改写。- 配置项只有在导出
Config之后才会显示。 0.31.4 及更早的构建没有导出Config,所以dsh --profile <name> --dump-config-schema里该行落在unknownConfig,Plugins 页也就没有可编辑的字段。安装 0.32.x 后这一项自愈。