Pre-submission checklist
What existing feature or behavior does this improve?
Token reduction in using graphify during research.
Current behavior
The knowledge graph at .planning/graphs/graph.json is consumed by gsd-planner's <step name="load_graph_context"> and gsd-phase-researcher's equivalent step (queries with budget 1500-2000 tokens, annotated "treat as approximate" when stale). But there is no mechanism to keep the graph current — the only update path is a manual /gsd-graphify build. As a result, every planning session after the first quietly degrades into stale-context territory.
Proposed behavior
This issue requests an opt-in mechanism (default off) that rebuilds the graph automatically when main HEAD advances, so consumers always see a graph that's at most one tool-call cycle behind reality.
Reason and benefit
Motivation
Concrete scenario from a real project (this repo):
- Day 1 — operator runs
/gsd-graphify build. Graph built at commit X.
- Day 1 — operator runs
/gsd-quick "fix Y". Planner consults graph at commit X. Executor lands commit X+1, X+2, X+3.
- Day 2 — operator runs
/gsd-quick "feature Z" whose impact area touches files added in X+1. Planner queries graph — misses them entirely (they don't exist in the graph yet). The "treat as approximate" annotation does not say "we have no idea about these new files" — just that the graph might be slightly stale.
- Plan ships against an incomplete impact map.
Repeated multiple times per day, the planner's context drifts further from HEAD until the next manual /gsd-graphify build. The graph's value-add asymptotically approaches "decoration."
For the same reason a project enables graphify.enabled = true (deeper context for planning + research), the project also wants the graph to actually be fresh during those steps. The manual rebuild is the single weakest link.
Scope of changes
Why "main HEAD advances" specifically
Three rebuild trigger candidates were considered; only one is correct:
| Candidate |
Why it fails |
| Every Edit/Write tool call |
~10s rebuild × dozens of edits per session = unacceptable latency on every keystroke. Also rebuilds during incomplete-state edits (mid-refactor) producing unstable graphs. |
| Every git commit (any branch) |
The gsd-executor agent in worktree-isolation mode commits 2-N atomic commits per task on a worktree-agent-* branch. Rebuilding on each is wasteful; the work isn't visible to the next agent until merged. |
| Main HEAD advances (commit / merge / pull / rebase --continue / cherry-pick on main) |
Exactly the surface that makes work visible to the next consumer. One rebuild per merge-back, regardless of how many atomic commits the executor made inside the worktree. |
This also matches GSD's own architecture: the orchestrator's "merge worktree branch back to main" step in workflows/quick.md (and similar in execute-phase.md) is the canonical "work is now visible" boundary. The auto-update should fire there.
Breaking changes
No
Alternatives considered
Proposed shape
Option A — built-in PostToolUse hook (preferred)
Add a hook script to the bundled hooks/ directory and wire it via bin/install.js. Suggested name: hooks/gsd-graphify-update.sh. Suggested config gate: graphify.auto_update (default false so existing users opt in explicitly).
The script (sketched in our local workaround at .claude/hooks/gsd-graphify-update.sh):
- Read tool-call JSON from stdin; bail if not a Bash tool call
- Match the command against HEAD-advancing git operations (
git commit, git merge, git pull, git rebase --continue, git cherry-pick)
- Verify current branch is the project's default (
main / master / trunk / configurable via git.base_branch in .planning/config.json — the same key already used by complete-milestone for branch detection)
- Verify
.planning/config.json has graphify.enabled = true AND graphify.auto_update = true
- PID-lock against concurrent rebuilds (
.planning/graphs/.rebuild.lock)
setsid bash -c "graphify update . && cp graphify-out/{graph.json,graph.html,GRAPH_REPORT.md} .planning/graphs/ && cp .planning/graphs/graph.json .planning/graphs/.last-build-snapshot.json" detached + backgrounded
- Hook returns in <10 ms; rebuild runs out-of-band
The hook returns instantly so it never blocks the next tool call. Worst case, the graph lags by one tool-call cycle — the same staleness window the existing "treat as approximate" annotation already accommodates.
Option B — workflow-end rebuild step
Add a "rebuild graph" step to workflows/quick.md Step 8 (final commit), workflows/execute-phase.md (after waves complete), and workflows/complete-milestone.md (after milestone close). Same end result, but:
- Costs ~10-15s per workflow instead of ~10-15s per merge (workflows often consolidate multiple merges)
- Doesn't fire on operator-initiated direct commits to main
- Edits to workflow files get wiped on
/gsd-update — local-patch reapply needed (which is exactly the brittleness /gsd-update --reapply already navigates, but it's another knob users must remember)
Recommend Option A. It's simpler, more robust, and decouples graph freshness from workflow-internal logic.
Option C — both
Option A as the default mechanism, plus an explicit "rebuild graph if dirty" step at the end of complete-milestone.md (which always wants a fresh graph for the audit / verifier consumers). Option C is what an established team would land on after running Option A for a few weeks.
Area affected
Core workflow (init, plan, build, verify)
Additional context
Configuration surface
Add one new key to graphify.* in .planning/config.json:
Add a question to workflows/settings.md step that already gates graphify.enabled:
Auto-rebuild graph after main HEAD advances?
- Yes — keeps the graph fresh for the next /gsd-quick or /gsd-plan-phase
- No (Recommended) — manual /gsd-graphify build only
(Recommended-no because it's a behavior change that affects every commit and silently runs graphify update . in the background. Opt-in respects user agency around when work-product is built.)
Reference implementation
Our local workaround in .claude/hooks/gsd-graphify-update.sh (project-scoped, written ~10 minutes after diagnosing this gap) is included below for reference. Tested working: PostToolUse hook fires on Bash, dispatches graphify update . in a setsid background process, lock file appears + clears, graph mtime advances. The only quirk is that mid-session changes to .claude/settings.json aren't picked up by Claude Code until restart — which is a Claude Code behavior, not a GSD issue.
#!/usr/bin/env bash
set -uo pipefail
INPUT=$(cat 2>/dev/null || true)
[ -n "$INPUT" ] || exit 0
COMMAND=$(printf '%s' "$INPUT" | /usr/bin/node -e '
let d = "";
process.stdin.on("data", c => d += c);
process.stdin.on("end", () => {
try { console.log(JSON.parse(d).tool_input?.command || ""); } catch { console.log(""); }
});' 2>/dev/null || echo "")
case "$COMMAND" in
*"git commit"*|*"git merge"*|*"git pull"*|*"git rebase --continue"*|*"git cherry-pick"*) ;;
*) exit 0 ;;
esac
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
[ "$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" = "main" ] || exit 0
[ -f .planning/config.json ] || exit 0
ENABLED=$(/usr/bin/node -e '
const fs = require("fs");
try {
const c = JSON.parse(fs.readFileSync(".planning/config.json", "utf8"));
console.log(c.graphify && c.graphify.enabled === true ? "true" : "false");
} catch { console.log("false"); }' 2>/dev/null || echo "false")
[ "$ENABLED" = "true" ] || exit 0
LOCK_FILE=".planning/graphs/.rebuild.lock"
mkdir -p .planning/graphs
if [ -f "$LOCK_FILE" ]; then
PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
[ -n "$PID" ] && kill -0 "$PID" 2>/dev/null && exit 0
fi
GRAPHIFY_BIN=$(command -v graphify 2>/dev/null || true)
[ -n "$GRAPHIFY_BIN" ] || exit 0
setsid bash -c "
echo \$\$ > '$LOCK_FILE'
trap 'rm -f $LOCK_FILE' EXIT
'$GRAPHIFY_BIN' update . >/dev/null 2>&1
if [ -f graphify-out/graph.json ]; then
cp graphify-out/graph.json .planning/graphs/graph.json
cp graphify-out/graph.html .planning/graphs/graph.html 2>/dev/null
cp graphify-out/GRAPH_REPORT.md .planning/graphs/GRAPH_REPORT.md 2>/dev/null
cp .planning/graphs/graph.json .planning/graphs/.last-build-snapshot.json
fi
" </dev/null >/dev/null 2>&1 &
disown
exit 0
Edge cases the bundled implementation should handle
- Default branch is not
main: read from git.base_branch in .planning/config.json (same key complete-milestone uses) before falling back to main. If that key is unset, derive from git symbolic-ref refs/remotes/origin/HEAD.
- Multiple repos under one Claude session: hook runs in cwd; the cwd-relative
.planning/config.json lookup naturally scopes to the current repo. No cross-project leakage.
graphify not on PATH: bail silently (command -v graphify check). The local fallback should not break commits.
- Worktree-internal commits: filtered by branch check (worktree branches are named
worktree-agent-*, never main). Confirmed in our local testing — a git commit inside a worktree-agent branch correctly does NOT fire the hook.
- CI environments: hooks should be skipped if
CI=true or similar. Otherwise CI runs that commit-then-test would each trigger a graphify build, slowing CI for no consumer benefit.
- Stale lock file from a previous crashed rebuild: PID check (
kill -0) detects the dead process and proceeds. The trap on EXIT removes the lock on normal exit; signal-killed processes leave a stale lock that the PID check handles.
- Disk-full /
graphify update failure: rebuild fails silently (background process), lock cleared by the trap, next commit retries. The previous valid graph at .planning/graphs/graph.json remains intact (we only cp the new outputs over the old after graphify update succeeds).
Why now
Workflow files (quick.md, execute-phase.md, complete-milestone.md) all explicitly invoke the planner / researcher steps that depend on graph freshness. Without auto-update, every project that enables graphify.enabled = true has a known-broken consumer-producer relationship: the producers never run after install. We confirmed this by reading every workflow + hook file on disk for our v1.41.1 install — nothing fires graphify update post-execution.
Adding the hook is ~80 lines of bash + ~5 lines of installer wiring + ~10 lines of settings-question. The behavior is opt-in by default, so there's no breaking change for users who prefer manual control.
Acceptance criteria (suggestion)
Related
agents/gsd-planner.md:886-920 (<step name="load_graph_context">) — primary consumer
agents/gsd-phase-researcher.md:538-571 — second consumer
workflows/settings.md:303-310 — existing question that gates graphify.enabled
commands/gsd/gsd-graphify.md (skill) — manual build/status/diff commands
Environment
- OS: Linux 6.8.0-106-generic
- Node: v22.22.2
- GSD: v1.41.1 (latest at time of report)
- Runtime: Claude Code (
~/.claude config dir)
- Repo scale: 620 source files, 3,488 graph nodes, 3,766 edges (one indicative point — rebuild takes ~10-12 s on this machine; smaller repos rebuild in 2-5 s)
Pre-submission checklist
approved-enhancementbefore writing any codeWhat existing feature or behavior does this improve?
Token reduction in using graphify during research.
Current behavior
The knowledge graph at
.planning/graphs/graph.jsonis consumed bygsd-planner's<step name="load_graph_context">andgsd-phase-researcher's equivalent step (queries with budget 1500-2000 tokens, annotated "treat as approximate" when stale). But there is no mechanism to keep the graph current — the only update path is a manual/gsd-graphify build. As a result, every planning session after the first quietly degrades into stale-context territory.Proposed behavior
This issue requests an opt-in mechanism (default off) that rebuilds the graph automatically when main HEAD advances, so consumers always see a graph that's at most one tool-call cycle behind reality.
Reason and benefit
Motivation
Concrete scenario from a real project (this repo):
/gsd-graphify build. Graph built at commit X./gsd-quick "fix Y". Planner consults graph at commit X. Executor lands commit X+1, X+2, X+3./gsd-quick "feature Z"whose impact area touches files added in X+1. Planner queries graph — misses them entirely (they don't exist in the graph yet). The "treat as approximate" annotation does not say "we have no idea about these new files" — just that the graph might be slightly stale.Repeated multiple times per day, the planner's context drifts further from HEAD until the next manual
/gsd-graphify build. The graph's value-add asymptotically approaches "decoration."For the same reason a project enables
graphify.enabled = true(deeper context for planning + research), the project also wants the graph to actually be fresh during those steps. The manual rebuild is the single weakest link.Scope of changes
Why "main HEAD advances" specifically
Three rebuild trigger candidates were considered; only one is correct:
gsd-executoragent in worktree-isolation mode commits 2-N atomic commits per task on aworktree-agent-*branch. Rebuilding on each is wasteful; the work isn't visible to the next agent until merged.This also matches GSD's own architecture: the orchestrator's "merge worktree branch back to main" step in
workflows/quick.md(and similar inexecute-phase.md) is the canonical "work is now visible" boundary. The auto-update should fire there.Breaking changes
No
Alternatives considered
Proposed shape
Option A — built-in PostToolUse hook (preferred)
Add a hook script to the bundled
hooks/directory and wire it viabin/install.js. Suggested name:hooks/gsd-graphify-update.sh. Suggested config gate:graphify.auto_update(defaultfalseso existing users opt in explicitly).The script (sketched in our local workaround at
.claude/hooks/gsd-graphify-update.sh):git commit,git merge,git pull,git rebase --continue,git cherry-pick)main/master/trunk/ configurable viagit.base_branchin.planning/config.json— the same key already used bycomplete-milestonefor branch detection).planning/config.jsonhasgraphify.enabled = trueANDgraphify.auto_update = true.planning/graphs/.rebuild.lock)setsid bash -c "graphify update . && cp graphify-out/{graph.json,graph.html,GRAPH_REPORT.md} .planning/graphs/ && cp .planning/graphs/graph.json .planning/graphs/.last-build-snapshot.json"detached + backgroundedThe hook returns instantly so it never blocks the next tool call. Worst case, the graph lags by one tool-call cycle — the same staleness window the existing "treat as approximate" annotation already accommodates.
Option B — workflow-end rebuild step
Add a "rebuild graph" step to
workflows/quick.mdStep 8 (final commit),workflows/execute-phase.md(after waves complete), andworkflows/complete-milestone.md(after milestone close). Same end result, but:/gsd-update— local-patch reapply needed (which is exactly the brittleness/gsd-update --reapplyalready navigates, but it's another knob users must remember)Recommend Option A. It's simpler, more robust, and decouples graph freshness from workflow-internal logic.
Option C — both
Option A as the default mechanism, plus an explicit "rebuild graph if dirty" step at the end of
complete-milestone.md(which always wants a fresh graph for the audit / verifier consumers). Option C is what an established team would land on after running Option A for a few weeks.Area affected
Core workflow (init, plan, build, verify)
Additional context
Configuration surface
Add one new key to
graphify.*in.planning/config.json:{ "graphify": { "enabled": true, "auto_update": false // default — opt in via /gsd-settings or hand-edit } }Add a question to
workflows/settings.mdstep that already gatesgraphify.enabled:(Recommended-no because it's a behavior change that affects every commit and silently runs
graphify update .in the background. Opt-in respects user agency around when work-product is built.)Reference implementation
Our local workaround in
.claude/hooks/gsd-graphify-update.sh(project-scoped, written ~10 minutes after diagnosing this gap) is included below for reference. Tested working: PostToolUse hook fires on Bash, dispatchesgraphify update .in a setsid background process, lock file appears + clears, graph mtime advances. The only quirk is that mid-session changes to.claude/settings.jsonaren't picked up by Claude Code until restart — which is a Claude Code behavior, not a GSD issue.Edge cases the bundled implementation should handle
main: read fromgit.base_branchin.planning/config.json(same keycomplete-milestoneuses) before falling back tomain. If that key is unset, derive fromgit symbolic-ref refs/remotes/origin/HEAD..planning/config.jsonlookup naturally scopes to the current repo. No cross-project leakage.graphifynot on PATH: bail silently (command -v graphifycheck). The local fallback should not break commits.worktree-agent-*, nevermain). Confirmed in our local testing — agit commitinside a worktree-agent branch correctly does NOT fire the hook.CI=trueor similar. Otherwise CI runs that commit-then-test would each trigger a graphify build, slowing CI for no consumer benefit.kill -0) detects the dead process and proceeds. The trap on EXIT removes the lock on normal exit; signal-killed processes leave a stale lock that the PID check handles.graphify updatefailure: rebuild fails silently (background process), lock cleared by thetrap, next commit retries. The previous valid graph at.planning/graphs/graph.jsonremains intact (we onlycpthe new outputs over the old aftergraphify updatesucceeds).Why now
Workflow files (
quick.md,execute-phase.md,complete-milestone.md) all explicitly invoke the planner / researcher steps that depend on graph freshness. Without auto-update, every project that enablesgraphify.enabled = truehas a known-broken consumer-producer relationship: the producers never run after install. We confirmed this by reading every workflow + hook file on disk for our v1.41.1 install — nothing firesgraphify updatepost-execution.Adding the hook is ~80 lines of bash + ~5 lines of installer wiring + ~10 lines of settings-question. The behavior is opt-in by default, so there's no breaking change for users who prefer manual control.
Acceptance criteria (suggestion)
graphify.auto_updatedefaults tofalse/gsd-settingsincludes a yes/no question forgraphify.auto_update(only shown whengraphify.enabledis true)graphify.auto_update = true, the bundled hook fires after Bash tool calls matching HEAD-advancing git operationsgraphify.enabledorauto_updateis false / a rebuild is already running /graphifynot on PATH.planning/graphs/.rebuild.lock) prevents concurrent rebuilds and self-cleans on process exit (including crash via PID check)graphify updatefailure, previous valid graph remains intact$CIenv var) suppresses auto-update by defaultRelated
agents/gsd-planner.md:886-920(<step name="load_graph_context">) — primary consumeragents/gsd-phase-researcher.md:538-571— second consumerworkflows/settings.md:303-310— existing question that gatesgraphify.enabledcommands/gsd/gsd-graphify.md(skill) — manual build/status/diff commandsEnvironment
~/.claudeconfig dir)