首页
/ get-shit-done 的 Graphify 自动更新钩子:主分支 HEAD 前进后自动重建代码知识图谱

get-shit-done 的 Graphify 自动更新钩子:主分支 HEAD 前进后自动重建代码知识图谱

2026-09-06 14:03:45作者:凌朦慧Richard

本文围绕 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-plannergsd-phase-researcher 在每一步的 load_graph_context 阶段自动消费;但在此之前,图谱的生产是手动的(每次会话最多跑一次 /gsd:graphify build)。也就是说:图谱被自动消费、却被手动生产,每多一次 commit,生产者与消费者之间的差距就静默地扩大一点。

原有的 stale: true 标注只能告诉消费方「文件 mtime 老了」,无法区分三种状态:自动重建钩子正在跑、刚才失败了、还是根本没人跑过。changeset 文档给出的核心意图正是消除这个盲区:

new config key graphify.auto_update (default false) and bundled PostToolUse hook hooks/gsd-graphify-update.sh keep the .planning/graphs/graph.json consumed by gsd-planner and gsd-phase-researcher current without manual /gsd:graphify build runs.

设计原则是 opt-ingraphify.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.mdgraphify.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.enabled value is on. If graphify.enabled is off, omit the graphify.auto_update question and preserve the existing graphify.auto_update value 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 钩子。它的文件头注释完整列出了闸门设计(按快速失败顺序排列,每一道都削减「常见不派发路径」上的工作):

  1. Gate 1 — stdin 载荷存在且 tool_name == "Bash":钩子从 stdin 读取工具调用 JSON,用 Node 解析出 tool_nametool_input.command,非 Bash 调用直接 exit 0
  2. 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);
  3. Gate 3$CI 环境变量未设置或为空(CI 中抑制);
  4. Gate 4 — 当前目录在一个 git 仓库内(git rev-parse --git-dir 验证);
  5. Gate 5 — 当前分支等于默认分支:优先读 .planning/config.jsongit.base_branch 覆盖值,否则按 main / master / trunk 依次探测;
  6. Gate 6.planning/config.json 同时满足 graphify.enabled === true && graphify.auto_update === true
  7. Gate 7graphify 可执行文件在 PATH 上,否则静默退出;
  8. 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 负责真正的重建,流程为:

  1. 把自己的 PID 写入 LOCK_FILE 并设置 trap ... EXIT 保证任何退出路径都清理锁;
  2. 在项目根目录执行 graphify update .(cwd 继承自调用方),捕获退出码;
  3. 仅在成功时复制产物:graphify-out/graph.json.planning/graphs/graph.jsongraph.htmlGRAPH_REPORT.md 一并复制,并把新图谱另存为 .planning/graphs/.last-build-snapshot.json 供后续 graphifyDiff 拓扑对比使用。失败路径保留上一份有效图谱,绝不落盘半成品;
  4. 计算 duration_ms,重写状态文件为 okgraphify 退出码 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.cjsgraphifyStatus():它读取 .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}h old — 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.enabledgraphify.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 的项目,启用自动更新的最短路径是:

  1. 确认 graphify 已安装且在 PATH 上(构建流程的 graphifyBuild() 会做安装检查,安装指引见 get-shit-done/bin/lib/graphify.cjs 中给出的 uv pip install graphifyy && graphify install);
  2. 运行 /gsd:settings,在 Features 分区先开启 Graphify,再回答条件出现的「Graph auto-update」问题为 on——或直接把 graphify.enabledgraphify.auto_update 写入项目 .planning/config.json
  3. 此后在默认分支上的 git commit / git merge / git pull / git rebase --continue / git cherry-pick(以及 gsd-sdk query commit 形态的 SDK 提交)会自动触发脱离式重建;
  4. 观察保鲜效果:.planning/graphs/.last-build-status.json 会经历 running → ok/failed 的状态迁移,而 gsd-planner / gsd-phase-researcherload_graph_context 输出中 stalelast_build_auto_update 字段即为其状态面。

适用前提与限制:该机制仅在默认分支上触发(feature 分支上的提交不会触发重建);$CI 环境下被抑制;graphify update . 的产物只有退出码为 0 才会覆盖 .planning/graphs/,失败时消费者看到的是上一份有效图谱加上 failed 标注(「auto-rebuild FAILED at {ts}; context is from the prior build」语义),不会出现半成品图谱。对于多分支并行、长 rebase 会话较多的团队,这正是把「图谱陈旧度」从隐性漂移变成显式可观测状态的关键一步。

登录后查看全文
热门项目推荐
相关项目推荐