get-shit-done 修复 3653:graphify 自动更新 Hook 如何"看见" SDK 内部提交的 git commit
本篇围绕 changeset 3653-graphify-hook-sdk-commit-visibility.md 展开,剖析 get-shit-done(GSd)中一个隐蔽的静默缺陷:PostToolUse 自动更新 Hook 因只匹配 shell 字面 git commit 而漏掉 gsd-sdk query commit 这类"壳内提交",导致知识图谱目录在每个 Phase 结束时滞后一个或多个提交且无任何报错。读完本篇,你将理解该缺陷的根因(SDK 通过 spawnSync('git', [...]) 绕过 shell 调用 git)、修复方案(Gate 2 精确匹配 gsd-sdk query commit 命令形态并排除 commit-to-subrepo 等前缀碰撞),以及 Hook 八道闸门与配套测试的完整验证逻辑。
缺陷背景:graphify 自动更新 Hook 的工作机制
GSd 内置了一个可选开启(opt-in)的知识图谱自动重建机制,由 PostToolUse Hook gsd-graphify-update.sh 承担。其设计目标是:当默认分支的 HEAD 因 git 操作前进时,自动在后台重建项目知识图谱,把产物同步到 .planning/graphs/。
启用需要 .planning/config.json 中同时满足两个开关(依据 CONFIGURATION.md 中 graphify.auto_update 条目,issue #3347 的 AC):
{
"graphify": {
"enabled": true,
"auto_update": true
}
}
auto_update 默认 false,保证既有用户升级后行为不变。Hook 的触发链路在文件头注释中写得很明确:只有当八道闸门全部通过时,才同步写入 .planning/graphs/.last-build-status.json(status="running"),再派生一个脱离父进程的后台任务 gsd-graphify-rebuild.sh 执行 graphify update .,把 graphify-out/{graph.json,graph.html,GRAPH_REPORT.md} 拷贝进 .planning/graphs/,最后改写状态文件为 ok 或 failed。
八道闸门按快速失败(fast-fail)顺序排列:
- stdin 载荷存在且
tool_name == "Bash"; tool_input.command匹配"推进 HEAD 的 git 操作"(本缺陷的修复点即在此闸门);$CI未设置(CI 环境静默跳过);- 当前位于 git 仓库内(
git rev-parse --git-dir成功); - 当前分支等于默认分支(
git.base_branch可覆盖,否则探测main/master/trunk); - 配置中
graphify.enabled === true && graphify.auto_update === true; graphify二进制在PATH上;- 没有已存在且存活的构建进程(PID 锁,
kill -0探测,容忍陈旧锁)。
所有闸门的设计取向一致:任何情况下返回 0,绝不阻塞用户可见的工具调用。
根因:spawnSync 让 git commit 在 Hook 的"视野"之外消失
缺陷的本质是匹配语义与调用方式错配。
原 Gate 2 依赖对 Bash 工具命令字符串的子串匹配,只覆盖"shell 直连"的 git 操作。而 GSd 的提交路径有两条:Agent 既可能直接执行 git commit ...,也可能执行用户面命令 gsd-sdk query commit。后者的实际实现在 commit.ts:
export function execGit(cwd: string, args: string[]): { exitCode: number; stdout: string; stderr: string } {
const result = spawnSync('git', args, {
cwd,
stdio: 'pipe',
encoding: 'utf-8',
});
// ...
}
execGit 通过 spawnSync('git', [...]) 直接 exec 出 git 进程,完全不经过 shell。后果是:Bash 工具上报的 tool_input.command 里只有 gsd-sdk query commit ... 字样,字面 git commit 子串永远不会出现。于是 Gate 2 对每一次 SDK 发起的提交都静默放行——没有错误、没有日志、没有告警。
被漏掉的提交并不罕见。changeset 特别指出,其中包括每个 Phase 完成时紧跟 phase.complete 的收尾提交(它负责关闭 Phase 状态并把 .planning/ 文档入库)。因此实际影响是:每个 Phase 收尾时,.planning/graphs/ 都会相对 HEAD 静默漂移一个或多个提交。图谱是后续 planner 的 load_graph_context 步骤的输入(见 INVENTORY.md 中对 planner-graphify-auto-update.md 的说明),漂移意味着 Agent 在下一个规划环节消费的是过期拓扑。
修复方案:Gate 2 精确匹配 gsd-sdk query commit 命令形态
修复落在 gsd-graphify-update.sh 的 Gate 2 上,匹配规则变为:
# Gate 2 — HEAD-advancing git op (shell-direct or exact `gsd-sdk query commit`)
case "$COMMAND" in
*"git commit"*|*"git merge"*|*"git pull"*|*"git rebase --continue"*|*"git cherry-pick"*) ;;
*"gsd-sdk query commit"|*"gsd-sdk query commit "*) ;;
*) exit 0 ;;
esac
新增分支的设计有两点值得注意:
1. 只匹配"精确命令形态",而非裸子串 commit。 模式写成 *"gsd-sdk query commit"(命令以此结尾)或 *"gsd-sdk query commit "*(其后还有空格与参数),覆盖 gsd-sdk query commit "docs: x" --files ... 以及 npx gsd-sdk query commit ... 这类经 npx 转发的形态。之所以用带空格的子串而不是 *"commit"* 或 *"commit- "*,是为了精确锁定"用户面调用、触发 SDK 内部 spawnSync('git', 'commit', ...)"的那一条路径。
2. 主动排除前缀碰撞的兄弟动词。 SDK 中存在一个名字前缀完全包含 commit 的动词 commit-to-subrepo(注册于 command-static-catalog-domain.ts,并在 command-manifest.non-family.ts 中标记为 mutation: true)。若匹配规则写成 *"gsd-sdk query commit"*(星号紧跟 commit),gsd-sdk query commit-to-subrepo ... 会被误判命中。changeset 明确说明:新规则不会匹配 commit-to-subrepo 这类兄弟动词——从源码结构看,该动词面向子仓库路径(--files packages/foo)操作提交,不推进外层仓库 HEAD,命中它只会触发无意义的重建。
边界决策:为什么其他 gsd-sdk query 动词保持不匹配
changeset 还给出了一条容易被忽略但很关键的边界:其余 gsd-sdk query 动词继续不匹配。具体点名了三个:
| 动词 | 行为特征 | 是否触发 git | 是否纳入 Gate 2 |
|---|---|---|---|
commit |
SDK 内部 spawnSync('git', ['commit', ...]) |
是(推进 HEAD) | 是(本次修复新增) |
commit-to-subrepo |
面向子仓库路径的提交 | 不推进外层 HEAD | 否(前缀碰撞,显式排除) |
phase.complete |
修改 .planning/ 下的 Phase 状态文档 |
否 | 否 |
roadmap.update-plan-progress |
更新 roadmap 进度文档 | 否 | 否 |
state.begin-phase |
变更 .planning/STATE.md 状态 |
否 | 否 |
后三个动词只改写 markdown 状态文件、自身不调用 git,如果把它们纳入匹配,每次状态变更都会引发一次图谱重建(spurious rebuild per state mutation),而 HEAD 根本没动,重建出的图谱与旧图等价,纯属浪费。这个取舍体现了该 Hook 匹配规则的第一性原则:只匹配"确实推进 HEAD"的动作,无论它发生在 shell 层还是 SDK 层。
验证:测试如何锁死匹配边界
配套的回归测试位于 graphify-auto-update.test.cjs 的 "HEAD-advancing command matchers" 组中,用临时 git 仓库 + mock 的 graphify 二进制驱动真实 Hook 脚本,断言依据是 .planning/graphs/.last-build-status.json 是否被写出:
必须分发的命令(含 #3653 修复点):
git commit -m fix
git merge feature
git pull --ff-only
git rebase --continue
git cherry-pick abc123
gsd-sdk query commit "docs: probe" --files .planning/STATE.md
npx gsd-sdk query commit "docs: probe" --files .planning/STATE.md
其中两条 SDK 形态命令正是本次修复新增的覆盖:测试注释直接引用了根因——"gsd-sdk query commit invokes git via spawnSync('git', [...]), so the substring git commit never appears in tool_input.command. The hook must match the user-facing SDK invocation directly."(测试环境限定 POSIX,Windows 上因需 bash 执行 .sh Hook 而 skip。)
必须不分发的命令(负例):
gsd-sdk query commit-to-subrepo "msg" --files packages/foo
gsd-sdk query phase.complete 109
gsd-sdk query roadmap.update-plan-progress 109 W001
gsd-sdk query state.begin-phase 110
负例断言 Hook 返回 0 且不产生状态文件,分别对应上文两类边界:前缀碰撞排除与非 HEAD 推进动词排除。这组正负用例组合,把 changeset 描述的匹配契约完整翻译成了可重复执行的回归护栏。
小结
#3653 是一类"静默失效"型缺陷的典型样本:Hook 的匹配逻辑建立在"git 操作必然以 shell 命令形式出现"的隐含假设上,而 SDK 的 spawnSync 直连方式打破了这一假设,且全链路无错误、无日志,只能靠产物(.planning/graphs/)与 HEAD 的比对才能发现。修复本身很克制——只给 Gate 2 增加一条精确的命令形态匹配,并显式划清 commit-to-subrepo 与其他状态类动词的边界,避免"多匹配"引入比"少匹配"更麻烦的无谓重建。对阅读该仓库的其他 Hook(如工作流守卫、上下文监控)而言,这条 changeset 也提示了一个通用检查点:凡是依赖 tool_input.command 子串匹配做语义判断的 Hook,都需要确认"命令字面量"与"实际副作用"之间不存在 SDK/二进制中转层的偏移。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00