get-shit-done 的 Graphify 自动更新钩子:主分支 HEAD 前进后自动重建代码知识图谱
本文围绕 get-shit-done 仓库的 changeset 文档 .changeset/3347747-graphify-auto-update-hook.md(PR 3557,关闭 issue #3347)展开,讲清「知识图谱消费方始终拿到最新语义关系」这一机制的完整设计:新增的 graphify.auto_update 配置项、捆绑的 PostToolUse 钩子 hooks/gsd-graphify-update.sh 的八道触发闸门、.last-build-status.json 状态文件的三态生命周期,以及 gsd-planner / gsd-phase-researcher 如何零改动地感知自动重建状态。读完你可以完整理解该功能的配置方式、触发条件、失败面设计,以及从钩子到状态消费端的源码级调用链。
问题背景:图谱的生产与消费之间存在静默漂移
get-shit-done 的 Graphify 子系统会把项目代码构建成知识图谱,落在 .planning/graphs/graph.json,并被 gsd-planner 与 gsd-phase-researcher 在每一步的 load_graph_context 阶段自动消费;但在此之前,图谱的生产是手动的(每次会话最多跑一次 /gsd:graphify build)。也就是说:图谱被自动消费、却被手动生产,每多一次 commit,生产者与消费者之间的差距就静默地扩大一点。
原有的 stale: true 标注只能告诉消费方「文件 mtime 老了」,无法区分三种状态:自动重建钩子正在跑、刚才失败了、还是根本没人跑过。changeset 文档给出的核心意图正是消除这个盲区:
new config key
graphify.auto_update(defaultfalse) and bundled PostToolUse hookhooks/gsd-graphify-update.shkeep the.planning/graphs/graph.jsonconsumed bygsd-plannerandgsd-phase-researchercurrent without manual/gsd:graphify buildruns.
设计原则是 opt-in:graphify.auto_update 默认 false,未开启的用户升级后行为完全不变;开启后,钩子在主分支上发生 HEAD 前进的 git 操作之后,以脱离父进程的后台子进程方式执行 graphify update .,钩子本身永远同步快速返回、绝不阻塞用户可见的工具调用。
配置项:graphify.enabled 与 graphify.auto_update
自动更新由两个布尔配置项联合控制,均位于项目的 .planning/config.json:
| 配置键 | 类型 | 默认值 | 含义 |
|---|---|---|---|
graphify.enabled |
boolean | false |
启用项目知识图谱(/gsd:graphify) |
graphify.auto_update |
boolean | false |
主分支 HEAD 前进后自动重建图谱 |
两者必须同时为 true 钩子才会动作(见 docs/CONFIGURATION.md 中 graphify.auto_update 的参数说明,以及 get-shit-done/workflows/settings.md 中的配置清单)。
在交互式设置入口 /gsd:settings 中,graphify.auto_update 以「Graph auto-update」问题呈现,且条件可见:只有当用户选择的 graphify.enabled 为 on 时才会出现该问题;若 graphify.enabled 为 off,则省略该问题并保留配置中已有的 graphify.auto_update 值,不做覆盖。settings 工作流原文规定:
Conditional visibility — graphify.auto_update: This question is shown only when the user's chosen
graphify.enabledvalue is on. Ifgraphify.enabledis off, omit thegraphify.auto_updatequestion and preserve the existinggraphify.auto_updatevalue in config (do not overwrite). Implementation: ask Graphify first; only ask Graph auto-update when Graphify is enabled.
配置落盘后形如:
{
"graphify": {
"enabled": true,
"auto_update": true
}
}
钩子实现:hooks/gsd-graphify-update.sh 的八道触发闸门
捆绑的钩子是 hooks/gsd-graphify-update.sh,一个匹配 Bash 工具的 PostToolUse 钩子。它的文件头注释完整列出了闸门设计(按快速失败顺序排列,每一道都削减「常见不派发路径」上的工作):
- Gate 1 — stdin 载荷存在且
tool_name == "Bash":钩子从 stdin 读取工具调用 JSON,用 Node 解析出tool_name与tool_input.command,非 Bash 调用直接exit 0; - Gate 2 — 命令是「推进 HEAD 的 git 操作」:直接 shell 形式匹配
git commit/git merge/git pull/git rebase --continue/git cherry-pick子串,或精确的gsd-sdk query commit命令形态。之所以要匹配后者,是因为 SDK 命令内部调用 git、命令行中从未出现字面量git commit(见 issues #3653); - Gate 3 —
$CI环境变量未设置或为空(CI 中抑制); - Gate 4 — 当前目录在一个 git 仓库内(
git rev-parse --git-dir验证); - Gate 5 — 当前分支等于默认分支:优先读
.planning/config.json的git.base_branch覆盖值,否则按main/master/trunk依次探测; - Gate 6 —
.planning/config.json同时满足graphify.enabled === true && graphify.auto_update === true; - Gate 7 —
graphify可执行文件在PATH上,否则静默退出; - Gate 8 — 没有正在进行的重建:读取锁文件
.planning/graphs/.rebuild.lock中的 PID 并用kill -0探活;进程存活则退出,进程已死(陈旧锁)则容忍并继续。
闸门 2 与闸门 5 的关键代码节选自 gsd-graphify-update.sh:
# 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
...
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
[ "$CURRENT_BRANCH" = "$DEFAULT_BRANCH" ] || exit 0
注意「rebase --continue 匹配、但裸 rebase 不匹配」的细节:只有 git rebase --continue 才代表 rebase 序列真正落盘推进了 HEAD,普通 git rebase 启动时未必前进 HEAD。
状态文件:三态生命周期的 .last-build-status.json
当八道闸门全部通过,钩子先同步写入初始状态文件,再把真正的重建工作派发到脱离的子进程。状态文件 .planning/graphs/.last-build-status.json 的结构(节选自 planner-graphify-auto-update.md):
{
"ts": "2026-05-15T14:02:23Z",
"status": "running",
"exit_code": null,
"duration_ms": null,
"head_at_build": "<commit-sha>",
"graphify_version": null
}
| 字段 | 说明 |
|---|---|
ts |
UTC 时间戳,ISO 8601 |
status |
running / ok / failed 三态 |
exit_code |
running 时为 null,终态时为 graphify update . 的退出码 |
duration_ms |
重建耗时(毫秒),running 时为 null |
head_at_build |
重建启动时记录的 HEAD SHA |
graphify_version |
预留字段,当前恒为 null |
同步写 running,再脱离派发
gsd-graphify-update.sh 中,钩子在派发前用 Node 写出 status: "running" 的初始状态(携带 head_at_build),这样下一次 planner 调用即使赶在重建完成之前,也能看到「正在进行中」的信号。随后它以后台作业方式启动 hooks/lib/gsd-graphify-rebuild.sh:
bash "$REBUILD_SCRIPT" \
"$STATUS_FILE" \
"$LOCK_FILE" \
"$HEAD_SHA" \
"$MS_START" \
"$GRAPHIFY_BIN" \
</dev/null >/dev/null 2>&1 &
REBUILD_PID=$!
echo "$REBUILD_PID" > "$LOCK_FILE"
disown "$REBUILD_PID" 2>/dev/null || true
这里有一个刻意的并发设计:钩子把重建进程以普通后台作业启动、通过 $! 同步捕获 PID 并在钩子返回之前写入锁文件。源码注释解释了动机——消除一个启动竞态:如果让子进程自己写锁,观察方(比如测试清理逻辑)在「锁不存在」时无法区分「子进程还没启动」和「子进程已经结束」。锁由父进程同步落盘后,锁的存在性本身就是可靠的进行中信号。
重建执行器:gsd-graphify-rebuild.sh
脱离的 hooks/lib/gsd-graphify-rebuild.sh 负责真正的重建,流程为:
- 把自己的 PID 写入
LOCK_FILE并设置trap ... EXIT保证任何退出路径都清理锁; - 在项目根目录执行
graphify update .(cwd 继承自调用方),捕获退出码; - 仅在成功时复制产物:
graphify-out/graph.json→.planning/graphs/graph.json,graph.html与GRAPH_REPORT.md一并复制,并把新图谱另存为.planning/graphs/.last-build-snapshot.json供后续graphifyDiff拓扑对比使用。失败路径保留上一份有效图谱,绝不落盘半成品; - 计算
duration_ms,重写状态文件为ok(graphify退出码 0)或failed(携带exit_code)。
消费端如何感知:零提示词改动的 stale 折叠
changeset 文档强调,planner 与 researcher 的 load_graph_context 步骤现在会把自动重建状态与既有的陈旧度标注一起呈现——包括 issue 评审中认定的必备失败面场景("auto-rebuild FAILED at {ts}; context is from the prior build")。
实现位于 get-shit-done/bin/lib/graphify.cjs 的 graphifyStatus():它读取 .last-build-status.json,把 running / failed 两个状态折叠进既有的 stale: true 信号:
// Auto-update status (#3347)
const statusPath = path.join(planningDir, 'graphs', '.last-build-status.json');
const lastBuildAutoUpdate = fs.existsSync(statusPath) ? safeReadJson(statusPath) : null;
const autoUpdateStale =
lastBuildAutoUpdate &&
(lastBuildAutoUpdate.status === 'failed' || lastBuildAutoUpdate.status === 'running');
return {
...
stale: age > STALE_MS || Boolean(autoUpdateStale),
...
last_build_auto_update: lastBuildAutoUpdate || null,
};
planner 和 researcher 的 <step name="load_graph_context"> 块中本来就执行 node ... graphify status,并且已有一条规则:
If the status response has
stale: true, note for later: "Graph is{age_hours}hold — treat semantic relationships as approximate."
因此,这条既有规则现在会额外覆盖三种情况(引自 planner-graphify-auto-update.md):
| 触发条件 | 用户看到的效果 |
|---|---|
自动重建状态 = failed |
既有「按近似对待」提示触发(因为 stale: true);完整 last_build_auto_update 对象(退出码 / 耗时 / commit SHA)随 JSON 返回 |
自动重建状态 = running |
同上——下一次 planner 调用知道图谱正在重建中,在脱离进程完成前按近似对待 |
状态 = ok 且 mtime < 24h |
标注静默——图谱新鲜且最近一次自动重建成功 |
| 状态文件缺失 | 静默(操作者未 opt-in,或开启后尚未发生过推进 HEAD 的 git 操作) |
这套设计的三个考量(见参考文档):
- 无 planner 侧提示词改动:折叠进
stale: true复用了既有规则,agents/gsd-planner.md不新增任何内容(该文件已接近 48K 的分解大小上限); - 测试钉住接缝行为:
tests/graphify-auto-update.test.cjs针对graphifyStatus在 status =failed/running/ok/ 文件缺失四种情况下的行为做回归断言; - 向后兼容:不读
last_build_auto_update的旧调用方看到的 JSON 形状不变,stale同时反映 mtime 与自动重建状态。
触发条件全景与可验证边界
把散落在 changeset、源码与测试中的行为约束汇总成一张判定表:
| 场景 | 钩子行为 |
|---|---|
| 工具调用不是 Bash(如 Read/Edit) | Gate 1 拦截,exit 0 |
Bash 命令不含 commit/merge/pull/rebase --continue/cherry-pick/gsd-sdk query commit 形态 |
Gate 2 拦截 |
$CI 已设置 |
Gate 3 拦截(CI-aware) |
| 非 git 仓库 | Gate 4 拦截 |
当前分支 ≠ 默认分支(git.base_branch 覆盖,否则 main/master/trunk) |
Gate 5 拦截 |
graphify.enabled 或 graphify.auto_update 为 false |
Gate 6 拦截 |
graphify 不在 PATH |
Gate 7 拦截,静默退出 |
已有重建在跑(锁 PID 存活,kill -0 探活) |
Gate 8 拦截;陈旧锁(进程已死)容忍并继续 |
| 全部通过 | 同步写 running 状态 → 锁文件落 PID → 脱离派发 graphify update . |
无论哪条路径,钩子总是返回 0,永不阻塞用户可见的工具调用——这是文件头注释明确声明的契约("Returns 0 in all cases. Never blocks the user-facing tool call.")。
验证边界可以落到 tests/graphify-auto-update.test.cjs:该测试文件在临时目录中模拟 .planning/config.json 与钩子执行,断言各种闸门组合下 .planning/graphs/.last-build-status.json 是否产生,并验证「graphify.auto_update 默认必须为 false(opt-in,issue #3347 验收条件)」。更完整的端到端行为矩阵(命令形态匹配、CI 抑制、分支判定、锁语义)可以沿该测试文件与 hooks/gsd-graphify-update.sh 的闸门注释逐条对照。
实战路径:从手动 build 到自动保鲜
对于已启用 Graphify 的项目,启用自动更新的最短路径是:
- 确认
graphify已安装且在 PATH 上(构建流程的graphifyBuild()会做安装检查,安装指引见 get-shit-done/bin/lib/graphify.cjs 中给出的uv pip install graphifyy && graphify install); - 运行
/gsd:settings,在 Features 分区先开启 Graphify,再回答条件出现的「Graph auto-update」问题为 on——或直接把graphify.enabled与graphify.auto_update写入项目.planning/config.json; - 此后在默认分支上的
git commit/git merge/git pull/git rebase --continue/git cherry-pick(以及gsd-sdk query commit形态的 SDK 提交)会自动触发脱离式重建; - 观察保鲜效果:
.planning/graphs/.last-build-status.json会经历running → ok/failed的状态迁移,而gsd-planner/gsd-phase-researcher的load_graph_context输出中stale与last_build_auto_update字段即为其状态面。
适用前提与限制:该机制仅在默认分支上触发(feature 分支上的提交不会触发重建);$CI 环境下被抑制;graphify update . 的产物只有退出码为 0 才会覆盖 .planning/graphs/,失败时消费者看到的是上一份有效图谱加上 failed 标注(「auto-rebuild FAILED at {ts}; context is from the prior build」语义),不会出现半成品图谱。对于多分支并行、长 rebase 会话较多的团队,这正是把「图谱陈旧度」从隐性漂移变成显式可观测状态的关键一步。
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 StartedRust0624
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