Skip to content
This repository was archived by the owner on Jun 26, 2026. It is now read-only.
This repository was archived by the owner on Jun 26, 2026. It is now read-only.

Auto-update knowledge graph after main HEAD advances (post-commit graphify hook) #3347

Description

@diakula

Pre-submission checklist

  • I have confirmed this improves existing behavior — it does not add a new command, workflow, or concept
  • I have searched existing issues and this enhancement has not already been proposed
  • I have read CONTRIBUTING.md and understand I must wait for approved-enhancement before writing any code
  • I can clearly describe the concrete benefit — not just "it would be nicer"

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):

  1. Day 1 — operator runs /gsd-graphify build. Graph built at commit X.
  2. Day 1 — operator runs /gsd-quick "fix Y". Planner consults graph at commit X. Executor lands commit X+1, X+2, X+3.
  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.
  4. 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).

// .claude/settings.json or ~/.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash <RUNTIME_DIR>/hooks/gsd-graphify-update.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

The script (sketched in our local workaround at .claude/hooks/gsd-graphify-update.sh):

  1. Read tool-call JSON from stdin; bail if not a Bash tool call
  2. Match the command against HEAD-advancing git operations (git commit, git merge, git pull, git rebase --continue, git cherry-pick)
  3. 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)
  4. Verify .planning/config.json has graphify.enabled = true AND graphify.auto_update = true
  5. PID-lock against concurrent rebuilds (.planning/graphs/.rebuild.lock)
  6. 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
  7. 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:

{
  "graphify": {
    "enabled": true,
    "auto_update": false  // default — opt in via /gsd-settings or hand-edit
  }
}

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)

  • New config key graphify.auto_update defaults to false
  • /gsd-settings includes a yes/no question for graphify.auto_update (only shown when graphify.enabled is true)
  • When graphify.auto_update = true, the bundled hook fires after Bash tool calls matching HEAD-advancing git operations
  • Hook is a no-op (~10 ms exit 0) when: not a HEAD-advancing git op / not on default branch / graphify.enabled or auto_update is false / a rebuild is already running / graphify not on PATH
  • Rebuild runs detached so the hook returns instantly
  • Lock file (.planning/graphs/.rebuild.lock) prevents concurrent rebuilds and self-cleans on process exit (including crash via PID check)
  • On graphify update failure, previous valid graph remains intact
  • CI detection ($CI env var) suppresses auto-update by default
  • Worktree-internal commits (branch != default) are filtered out — verified by integration test

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    approved-enhancementEnhancement approved — contributor may begin codingenhancementNew feature or requestin-progressActively being worked

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions