Fork me on GitHub

把 gsd-core 接进 DeepSeek Harness 的一次适配

把 gsd-core 接进 DeepSeek Harness 的一次适配

这是一次把 open-gsd/gsd-core 接进 DeepSeek Harness(下文简称 dsh)的适配记录。gsd-core 是一个面向 AI 编码代理的 spec-driven 开发系统,版本 1.16.0,commit 77a12fa8。dsh 是另一个 AI 编程 harness。两者本来互不相干,本文记录把它们接起来的过程、撞到的两道坎,以及适配过程中对两套扩展机制的实测结论。

起点

gsd-core 在 dsh 的插件页直接安装,报错。第一次报的是 pnpm 拦截构建脚本,第二次报的是「这个包没有声明组合包,不能作为插件管理」。这两个错误其实是同一件事的两层——gsd-core 根本不是 dsh 的组合包,装不上是预期内的结果。

第一道坎 构建脚本被 pnpm 拦截

dsh 自带 pnpm 11.7.0 和 node 24.21.0。从 GitHub 地址装 gsd-core 时,pnpm 报 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED,拒绝执行 git 依赖的 prepare 脚本。gsd-core 的 prepare 是 npm run build:lib,即 tsc 编译。

pnpm 11 把 pnpm 10 的 onlyBuiltDependencies 换成了 pnpm-workspace.yaml 里的 allowBuilds。放行一个 git 依赖,键必须用完整的 dep-path 形式,包名加 at 符号再加包解析 ID,只写包名无效。键要以 at 符号开头,在 YAML 里是非法 plain scalar,必须加引号;而 pnpm 报错提示里打印的是不带引号的版本,直接粘贴会让 pnpm 报 bad indentation。键还与 commit SHA 绑定,插件升级到新 commit 后要重新授权。

按这个机制放行后,构建脚本执行成功,pnpm 一共装了 110 个包。但马上撞到第二道坎。

第二道坎 没有组合包声明

构建通过后,dsh 报 not-bundle,措辞是「这个包没有声明组合包,不能作为插件管理」,package.json 被回滚。

判定条件写死在 dsh 的 launcher 源码里——包必须在 package.json 声明 dsh.bundle.patch,指向一个 cordis.patch.yml。gsd-core 的 package.json 里没有这个字段,也没有任何 cordis 模块。这不是 bug,是定位差异。gsd-core 是 Claude Code 生态的插件,dsh 组合包是另一套契约,改字段或加权限都解决不了,要做适配包。

到这里,问题从「怎么装」变成了「gsd-core 有哪些可以接进 dsh 的面」。

换一条路 MCP 服务

gsd-core 自带一个 stdio MCP 服务,bin/gsd-mcp-server.js,设计目标就是「任意 MCP 宿主、无需专用插件」。实测 initialize 和 tools/list 正常,暴露三个工具

gsd_invoke_command   按 family / subcommand 走命令路由
gsd_read_state       读 .planning/ 下的状态文件
gsd_write_state      写 .planning/ 下的状态文件

dsh 侧已有 dsh-mcp-client,profile 的 cordis.patch.yml 里有现成写法可照抄。于是 MCP 面可以做一个薄组合包——组合包本身只做一件事,用一行 patch 把 gsd-core 的 MCP 服务挂进 dsh。

npm registry 上有 @opengsd/gsd-core@1.16.0 的发布包,解包 1125 个文件。走 registry 安装不触发构建脚本,这解决了第一道坎的残留问题。

dsh 扩展面的实测结论

MCP 只覆盖命令路由和状态读写,gsd-core 的技能、斜杠命令、hooks 不在 MCP 通道里。要把技能和命令也接进来,得先搞清楚 dsh 的技能扩展面到底支持什么。这些结论都回到 dsh 的 app.asar 里的源码实测,不是看文档转述。

技能名正则 [1]+(?:-[a-z0-9]+)*$,允许数字开头,禁大写和下划线。

description 在模型目录渲染时截断到 500 字符,超出部分变 497 加省略号。whenToUse 不渲染进目录,无长度限制。

frontmatter 的未知键被静默忽略,parseSkillFile 只按白名单读 name、description、whenToUse、metadata 和两个调用键。但 camelCase 调用键致命,disableModelInvocation、modelInvocable、userInvocable 会触发 rejectLegacyInvocationKey 抛错,整条技能被丢弃。正确的写法是短横线 disable-model-invocation、user-invocable。

没有独立命令目录。文件来源的用户命令只有技能手势 /技能名,不支持参数替换,$ARGUMENTS 不会被替换,名字后的文字留在用户消息里。

没有文件化子代理定义面。子代理由 Lead 在运行时创建,persona 是内联文本,注册表不扫描目录。

工具结果裁剪的阈值来自 profile 的 tool-result-pruner 行,thresholdChars 8192、headChars 4096、tailChars 1024。这条很容易被误解成「技能正文不能超过 8192」。实测机制是,裁剪器只在上下文压力触发压缩后运行,而且只作用于工具结果。技能经技能工具加载属工具结果,但 /name 手势注入走用户角色消息、不经裁剪器。所以 8192 是风险位而非硬上限,超限的技能照常完整加载,风险只在于长会话压缩期中段内容静默消失。

两个交付物

第一个是 gsd-core-mcp,一个纯 config 组合包。package.json 声明 dsh.bundle.patch 指向 cordis.patch.yml,patch 里插一条 dsh-mcp-client 行,serverName 为 gsd,command 用 node,args 指向 gsd-core 预构建副本的 MCP 服务入口。

第二个是 gsd-adapt,把 gsd-core 的 72 个技能和 72 个命令改写成 dsh 技能形态。技能面 72 个,模型与用户都可调用;命令面 72 个,设 disable-model-invocation true 加 user-invocable true,只作为用户命令出现,不进模型目录。两面 name 相同,同时装到同一技能根会互相遮蔽,安装脚本默认拒绝并提示先卸载另一面。

转换是幂等纯文本转换,同一输入跑两次输出字节一致。Claude 侧 frontmatter 的 argument-hint、allowed-tools 在 dsh 侧不存在,属于应剥离的字段,剥离原因是清洁性而非正确性。$ARGUMENTS 按 dsh 无参数通道的事实改写成 {{GSD_ARGS}},并在正文顶部注入 adapter header 引导模型从用户消息取参数。

转换与验证

144 个产物过了四条硬约束,每一条都有对应的反向对照证明检查器能失败

frontmatter 只含 name、description 和命令面的两个调用键,无 camelCase 致命键,无未知键残留。

name 全部匹配 kebab 正则。

description 最长 99 码点,低于 500 上限。

正文最长 6908 码点,0 个超 8192。四个超长的把细节拆到 references 目录,其中 gsd-graphify 两份从 15438 瘦到 5796,两份逐字节一致。

验证里有个量纲问题值得单独说。正文预算按 Unicode 码点计,但 wc -m 数的是 UTF-16 码元,wc -c 数的是字节,两者都不能直接和 8192 比较。64 个 agent 文件里只有 3 个含星光面字符,所以误差是可预测的固定偏移,但这三种量纲的差别是踩一次才记住的。

踩过的坑

四条都属 Git Bash / MSYS 编写环境,与 dsh 无关,记录是为了复现时避开。

冒号紧跟反斜杠的转义会被改写,\n 变成 /n,正则恒不匹配产生假阴性,MSYS 开关对此无效。

以 / 开头的参数会被转成 Windows 路径,MSYS_NO_PATHCONV=1 对此有效,不能删这个开关。

长度量纲如上一节所述。

Bash 命令里出现 PowerShell 工具名字面量会被安全层拒绝执行,报 Invoking PowerShell from Bash bypasses PowerShell security checks,规避方式是在脚本内拼字符串。

上游发现的问题

适配过程中发现 gsd-core 上游两处可改进的地方。

第一处,MCP 投影层把 subcommand 标成必填。gsd-core/bin/lib/mcp-server.cjs 的 schema 里 required 是 family 和 subcommand 两个,实现里 if (!family || !subcommand) 直接拒绝,报错 gsd_invoke_command requires string "family" and "subcommand"。但底层 dispatcher 在 gsd-core/bin/lib/shell-command-projection.cjs 里明确支持空 subcommand,注释写着「空 subcommand 被省略,config-path 等 family 不需要 subcommand」,实现是 ...(subcommand ? [subcommand] : [])。结果是 config-path、current-timestamp、stats、progress 这类 family-only 命令,直接跑 CLI 有输出,经 MCP 被拒。上下两层自相矛盾。

第二处,package.json 声明 engines.node >=24.0.0,但 node 22.22.2 实测能跑完整工具调用,check auto-mode、phase next-decimal、状态读写闭环都正常,和 node 24 结果一致。声明比实际严格。


  1. a-z0-9 ↩︎

posted @ 2026-10-12 21:46  郭幸坤  阅读(30)  评论(0)    收藏  举报
1