From e7306f73792236e7114147a948f4ec8ba74df071 Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 19 Jul 2026 13:18:50 +0800 Subject: [PATCH 1/2] fix(hook): nudge when wiki/ predates graph.json (#2006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wiki/ is only ever written by the explicit `export wiki` subcommand — never by `update` or watch — so it can silently go stale relative to graph.json with no signal anywhere in the loop. Extend the existing PreToolUse staleness check (which already compares graph.json's mtime against the target file) to also compare wiki/index.md's mtime against graph.json's when a wiki file is read, and nudge back to source/query when wiki is the older of the two. Also updates AGENTS.md and the always_on/ prose variants that unconditionally told agents to trust the wiki, so they point at this new signal instead of assuming wiki/ is always current. --- AGENTS.md | 2 +- graphify/always_on/agents-md.md | 2 +- graphify/always_on/antigravity-rules.md | 2 +- graphify/cli.py | 27 +++++++++++++++++- tests/test_hook_guard.py | 38 +++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b919654c4..ee950bdf3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,5 +4,5 @@ This project has a graphify knowledge graph at graphify-out/. Rules: - Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure -- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files +- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files — unless a PreToolUse nudge flags it as predating graph.json (wiki/ is only regenerated by `graphify export wiki`, never by `update`/watch), in which case fall back to source or `graphify query` - After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) diff --git a/graphify/always_on/agents-md.md b/graphify/always_on/agents-md.md index 6511cd1dd..222ce344c 100644 --- a/graphify/always_on/agents-md.md +++ b/graphify/always_on/agents-md.md @@ -7,6 +7,6 @@ When the user types `/graphify`, use the installed graphify skill or instruction Rules: - For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. - Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. -- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. +- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing — unless a PreToolUse nudge flags it as predating graph.json (wiki/ is only regenerated by `graphify export wiki`, never by `update`/watch), in which case fall back to source or `graphify query`. - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). diff --git a/graphify/always_on/antigravity-rules.md b/graphify/always_on/antigravity-rules.md index 0fc786414..66eaafc24 100644 --- a/graphify/always_on/antigravity-rules.md +++ b/graphify/always_on/antigravity-rules.md @@ -9,6 +9,6 @@ This project has a graphify knowledge graph at graphify-out/. Rules: - For codebase or architecture questions, when `graphify-out/graph.json` exists, first run `graphify query ""` (CLI) or `query_graph` (MCP). Use `graphify path "" ""` / `shortest_path` for relationships and `graphify explain ""` / `get_node` for focused concepts. These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output. -- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files +- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files — unless a PreToolUse nudge flags it as predating graph.json (wiki/ is only regenerated by `graphify export wiki`, never by `update`/watch), in which case fall back to source or `graphify query` - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context - After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) diff --git a/graphify/cli.py b/graphify/cli.py index 35e397e60..343427e3c 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -50,6 +50,18 @@ ), } }, ensure_ascii=False, separators=(",", ":")) + "\n" +_READ_NUDGE_WIKI_STALE = json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": ( + 'graphify-out/wiki/ predates the current graph.json. Unlike graph.json, ' + 'wiki/ is only regenerated by `graphify export wiki` — it is not refreshed ' + 'by `graphify update` or watch. Treat it as possibly stale: prefer ' + '`graphify query ""` or read the source directly, and run ' + '`graphify export wiki` to refresh it.' + ), + } +}, ensure_ascii=False, separators=(",", ":")) + "\n" # Strict-mode block (opt-in). Claude Code PreToolUse honors # hookSpecificOutput.permissionDecision == "deny" and shows permissionDecisionReason # to the model. Fires at most once per session (see _mark_session_denied) so it can @@ -467,7 +479,20 @@ def _run_hook_guard(kind: str, strict: bool = False) -> None: if "." in seg ] under_out = "graphify-out/" in j or (GRAPHIFY_OUT_NAME.lower() + "/") in j - if under_out or not any(tl in _HOOK_SOURCE_EXTS for tl in tails): + if under_out: + # #2006: wiki/ is only ever regenerated by the explicit `export + # wiki` subcommand, never by `update`/watch, so it can go stale + # relative to graph.json with no other signal to catch it. + if "/wiki/" in j: + try: + wiki_mtime = os.stat(str(out_path("wiki", "index.md"))).st_mtime + gmtime = os.stat(str(out_path("graph.json"))).st_mtime + if wiki_mtime < gmtime: + sys.stdout.write(_READ_NUDGE_WIKI_STALE) + except OSError: + pass + return + if not any(tl in _HOOK_SOURCE_EXTS for tl in tails): return # #1840 (a): skip files outside the graph's project. cwd (or # CLAUDE_PROJECT_DIR, which Claude Code sets) is the project root, since diff --git a/tests/test_hook_guard.py b/tests/test_hook_guard.py index 5146ef250..f64b1ffaa 100644 --- a/tests/test_hook_guard.py +++ b/tests/test_hook_guard.py @@ -179,6 +179,44 @@ def test_read_nudges_source_outside_custom_output_dir(tmp_path, monkeypatch): assert "graphify query" in out +# --------------------------------------------------------------------------- # +# read: wiki/ has no automatic regeneration (#2006) — nudge only when it +# predates the current graph.json, stay silent when it's at least as fresh. +# --------------------------------------------------------------------------- # +def test_read_wiki_nudges_when_stale(tmp_path, monkeypatch): + out_dir = tmp_path / "graphify-out" + (out_dir / "wiki").mkdir(parents=True) + (out_dir / "wiki" / "index.md").write_text("stale wiki", encoding="utf-8") + (out_dir / "graph.json").write_text("{}", encoding="utf-8") + os.utime(out_dir / "wiki" / "index.md", (1_000_000, 1_000_000)) + os.utime(out_dir / "graph.json", (2_000_000, 2_000_000)) + out = _invoke("read", {"tool_input": {"file_path": "graphify-out/wiki/index.md"}}, + tmp_path, monkeypatch, graph=False) + assert "predates the current graph.json" in out, out + assert json.loads(out)["hookSpecificOutput"]["hookEventName"] == "PreToolUse" + + +def test_read_wiki_silent_when_fresh(tmp_path, monkeypatch): + out_dir = tmp_path / "graphify-out" + (out_dir / "wiki").mkdir(parents=True) + (out_dir / "wiki" / "index.md").write_text("fresh wiki", encoding="utf-8") + (out_dir / "graph.json").write_text("{}", encoding="utf-8") + os.utime(out_dir / "graph.json", (1_000_000, 1_000_000)) + os.utime(out_dir / "wiki" / "index.md", (2_000_000, 2_000_000)) + out = _invoke("read", {"tool_input": {"file_path": "graphify-out/wiki/index.md"}}, + tmp_path, monkeypatch, graph=False) + assert out.strip() == "", out + + +def test_read_wiki_silent_without_graph_json(tmp_path, monkeypatch): + out_dir = tmp_path / "graphify-out" + (out_dir / "wiki").mkdir(parents=True) + (out_dir / "wiki" / "index.md").write_text("wiki, no graph.json", encoding="utf-8") + out = _invoke("read", {"tool_input": {"file_path": "graphify-out/wiki/index.md"}}, + tmp_path, monkeypatch, graph=False) + assert out.strip() == "", out + + # --------------------------------------------------------------------------- # # fail-open: malformed / empty stdin never crashes or blocks # --------------------------------------------------------------------------- # From 076d61946f089e4aad5513f81a860c1e0d6c4bc7 Mon Sep 17 00:00:00 2001 From: Tony Date: Sun, 19 Jul 2026 13:33:48 +0800 Subject: [PATCH 2/2] fix(hook): name the actual output dir in staleness nudges; fix skillgen fragment drift Two fixes on top of the wiki-staleness nudge: 1. Both _READ_NUDGE_STALE and the new _READ_NUDGE_WIKI_STALE hardcoded 'graphify-out/' in their message text, even though the hook-guard's detection logic already honors a custom GRAPHIFY_OUT_NAME. A project using a custom output dir would get a nudge pointing at a path that doesn't exist. Both are now functions parameterized by the actual configured out_name (flagged by Copilot's PR review on #2024). 2. The previous commit hand-edited the *generated* graphify/always_on/ agents-md.md and antigravity-rules.md directly instead of their fragment sources under tools/skillgen/fragments/always-on/, which test_always_on_files_are_guarded_by_check (correctly) flagged as drift. Fixed by editing the fragments and regenerating, then registering the wording change in gen.ALWAYS_ON_SANCTIONED_EDITS (alongside the existing #1530 entry) so the byte-for-byte roundtrip guard treats it as an intentional, reviewable edit rather than silent baseline drift. Adds 2 tests for the dynamic output-dir naming and updates test_always_on_roundtrip_is_byte_faithful's fixed expectations to cover both new sanctioned edits. --- graphify/cli.py | 54 ++++++++++--------- tests/test_hook_guard.py | 34 ++++++++++++ tests/test_skillgen.py | 48 +++++++++++++++-- .../graphify__always_on__agents-md.md | 2 +- .../graphify__always_on__antigravity-rules.md | 2 +- .../skillgen/fragments/always-on/agents-md.md | 2 +- .../fragments/always-on/antigravity-rules.md | 2 +- tools/skillgen/gen.py | 24 +++++++++ 8 files changed, 134 insertions(+), 34 deletions(-) diff --git a/graphify/cli.py b/graphify/cli.py index 343427e3c..d29817d63 100644 --- a/graphify/cli.py +++ b/graphify/cli.py @@ -39,29 +39,33 @@ ), } }, ensure_ascii=False, separators=(",", ":")) + "\n" -_READ_NUDGE_STALE = json.dumps({ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "additionalContext": ( - 'graphify-out/graph.json exists but may be STALE for this file (the file ' - 'changed after the last build). Prefer `graphify query ""` for ' - 'orientation, and run `graphify update` to refresh the graph. Reading the ' - 'file directly is fine.' - ), - } -}, ensure_ascii=False, separators=(",", ":")) + "\n" -_READ_NUDGE_WIKI_STALE = json.dumps({ - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "additionalContext": ( - 'graphify-out/wiki/ predates the current graph.json. Unlike graph.json, ' - 'wiki/ is only regenerated by `graphify export wiki` — it is not refreshed ' - 'by `graphify update` or watch. Treat it as possibly stale: prefer ' - '`graphify query ""` or read the source directly, and run ' - '`graphify export wiki` to refresh it.' - ), - } -}, ensure_ascii=False, separators=(",", ":")) + "\n" +def _read_nudge_stale(out_name: str) -> str: + return json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": ( + f'{out_name}/graph.json exists but may be STALE for this file (the file ' + 'changed after the last build). Prefer `graphify query ""` for ' + 'orientation, and run `graphify update` to refresh the graph. Reading the ' + 'file directly is fine.' + ), + } + }, ensure_ascii=False, separators=(",", ":")) + "\n" + + +def _read_nudge_wiki_stale(out_name: str) -> str: + return json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": ( + f'{out_name}/wiki/ predates the current graph.json. Unlike graph.json, ' + 'wiki/ is only regenerated by `graphify export wiki` — it is not refreshed ' + 'by `graphify update` or watch. Treat it as possibly stale: prefer ' + '`graphify query ""` or read the source directly, and run ' + '`graphify export wiki` to refresh it.' + ), + } + }, ensure_ascii=False, separators=(",", ":")) + "\n" # Strict-mode block (opt-in). Claude Code PreToolUse honors # hookSpecificOutput.permissionDecision == "deny" and shows permissionDecisionReason # to the model. Fires at most once per session (see _mark_session_denied) so it can @@ -488,7 +492,7 @@ def _run_hook_guard(kind: str, strict: bool = False) -> None: wiki_mtime = os.stat(str(out_path("wiki", "index.md"))).st_mtime gmtime = os.stat(str(out_path("graph.json"))).st_mtime if wiki_mtime < gmtime: - sys.stdout.write(_READ_NUDGE_WIKI_STALE) + sys.stdout.write(_read_nudge_wiki_stale(GRAPHIFY_OUT_NAME)) except OSError: pass return @@ -540,7 +544,7 @@ def _run_hook_guard(kind: str, strict: bool = False) -> None: except Exception: pass if stale: - sys.stdout.write(_READ_NUDGE_STALE) + sys.stdout.write(_read_nudge_stale(GRAPHIFY_OUT_NAME)) return # Strict block: Read tool only, first time per session, not recently # oriented, and the file is demonstrably indexed. diff --git a/tests/test_hook_guard.py b/tests/test_hook_guard.py index f64b1ffaa..4b308e4fd 100644 --- a/tests/test_hook_guard.py +++ b/tests/test_hook_guard.py @@ -179,6 +179,24 @@ def test_read_nudges_source_outside_custom_output_dir(tmp_path, monkeypatch): assert "graphify query" in out +def test_read_stale_nudge_uses_custom_output_dir_name(tmp_path, monkeypatch): + # Same requirement as the wiki nudge: the STALE message must name the + # actual configured output dir, not a hardcoded 'graphify-out/'. + out_dir = tmp_path / "build-out" + out_dir.mkdir() + (out_dir / "graph.json").write_text("{}", encoding="utf-8") + os.utime(out_dir / "graph.json", (1_000_000, 1_000_000)) + src = tmp_path / "src" + src.mkdir() + f = src / "app.py" + f.write_text("x = 1\n", encoding="utf-8") + os.utime(f, (2_000_000, 2_000_000)) + out = _invoke("read", {"tool_input": {"file_path": "src/app.py"}}, + tmp_path, monkeypatch, graph=False, out_name="build-out") + assert "build-out/graph.json" in out, out + assert "graphify-out" not in out, out + + # --------------------------------------------------------------------------- # # read: wiki/ has no automatic regeneration (#2006) — nudge only when it # predates the current graph.json, stay silent when it's at least as fresh. @@ -208,6 +226,22 @@ def test_read_wiki_silent_when_fresh(tmp_path, monkeypatch): assert out.strip() == "", out +def test_read_wiki_nudge_uses_custom_output_dir_name(tmp_path, monkeypatch): + # The nudge text must reflect the actual configured output dir, not a + # hardcoded 'graphify-out/' (a custom dir would otherwise get a + # misleading message pointing at a path that doesn't exist). + out_dir = tmp_path / "build-out" + (out_dir / "wiki").mkdir(parents=True) + (out_dir / "wiki" / "index.md").write_text("stale wiki", encoding="utf-8") + (out_dir / "graph.json").write_text("{}", encoding="utf-8") + os.utime(out_dir / "wiki" / "index.md", (1_000_000, 1_000_000)) + os.utime(out_dir / "graph.json", (2_000_000, 2_000_000)) + out = _invoke("read", {"tool_input": {"file_path": "build-out/wiki/index.md"}}, + tmp_path, monkeypatch, graph=False, out_name="build-out") + assert "build-out/wiki/" in out, out + assert "graphify-out" not in out, out + + def test_read_wiki_silent_without_graph_json(tmp_path, monkeypatch): out_dir = tmp_path / "graphify-out" (out_dir / "wiki").mkdir(parents=True) diff --git a/tests/test_skillgen.py b/tests/test_skillgen.py index 0c09e601e..e767f03d9 100644 --- a/tests/test_skillgen.py +++ b/tests/test_skillgen.py @@ -616,8 +616,9 @@ def test_always_on_roundtrip_is_byte_faithful(): contracts silently change. """ # The guard passes with zero problems: every always-on block reproduces its - # frozen baseline, with the agents-md block allowed exactly the #1530 - # sanctioned substitution recorded in gen.ALWAYS_ON_SANCTIONED_EDITS. + # frozen baseline, with the agents-md/antigravity-rules blocks allowed + # exactly the sanctioned substitutions recorded in + # gen.ALWAYS_ON_SANCTIONED_EDITS (#1530, #2006 x2). problems = gen.always_on_roundtrip() assert problems == [] @@ -634,18 +635,55 @@ def test_always_on_roundtrip_is_byte_faithful(): "When the user types `/graphify`, use the installed graphify skill or instructions " "before doing anything else." ) - # The sanctioned-edit registry holds exactly this single old->new substitution. + old_wiki_agents = ( + "- If graphify-out/wiki/index.md exists, use it for broad navigation " + "instead of raw source browsing." + ) + new_wiki_agents = ( + "- If graphify-out/wiki/index.md exists, use it for broad navigation instead of " + "raw source browsing — unless a PreToolUse nudge flags it as predating " + "graph.json (wiki/ is only regenerated by `graphify export wiki`, never by " + "`update`/watch), in which case fall back to source or `graphify query`." + ) + # The sanctioned-edit registry holds exactly these two old->new substitutions + # for _AGENTS_MD_SECTION: #1530 (install guidance) and #2006 (wiki staleness). assert gen.ALWAYS_ON_SANCTIONED_EDITS["_AGENTS_MD_SECTION"] == ( (old_instruction, new_instruction), + (old_wiki_agents, new_wiki_agents), ) baseline_agents = gen._always_on_constants(gen.ALWAYS_ON_BASELINE_REF)["_AGENTS_MD_SECTION"] - # The ONLY divergence from the frozen baseline is the sanctioned sentence — + # The ONLY divergence from the frozen baseline is the two sanctioned edits — # any other byte drift would have surfaced as a problem above. assert old_instruction in baseline_agents - assert baseline_agents.replace(old_instruction, new_instruction) == rendered_agents + assert old_wiki_agents in baseline_agents + assert ( + baseline_agents.replace(old_instruction, new_instruction).replace(old_wiki_agents, new_wiki_agents) + == rendered_agents + ) assert "`skill` tool" not in rendered_agents assert 'skill: "graphify"' not in rendered_agents + rendered_antigravity = next( + a.content + for a in gen.render_always_on() + if a.path == "graphify/always_on/antigravity-rules.md" + ) + old_wiki_antigravity = "- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files" + new_wiki_antigravity = ( + "- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files — " + "unless a PreToolUse nudge flags it as predating graph.json (wiki/ is only " + "regenerated by `graphify export wiki`, never by `update`/watch), in which case " + "fall back to source or `graphify query`" + ) + # The sanctioned-edit registry holds exactly this single #2006 substitution + # for _ANTIGRAVITY_RULES. + assert gen.ALWAYS_ON_SANCTIONED_EDITS["_ANTIGRAVITY_RULES"] == ( + (old_wiki_antigravity, new_wiki_antigravity), + ) + baseline_antigravity = gen._always_on_constants(gen.ALWAYS_ON_BASELINE_REF)["_ANTIGRAVITY_RULES"] + assert old_wiki_antigravity in baseline_antigravity + assert baseline_antigravity.replace(old_wiki_antigravity, new_wiki_antigravity) == rendered_antigravity + def test_extracted_constants_equal_the_packaged_always_on_files(): """The live module constants now equal the packaged files they read at load.""" diff --git a/tools/skillgen/expected/graphify__always_on__agents-md.md b/tools/skillgen/expected/graphify__always_on__agents-md.md index 6511cd1dd..222ce344c 100644 --- a/tools/skillgen/expected/graphify__always_on__agents-md.md +++ b/tools/skillgen/expected/graphify__always_on__agents-md.md @@ -7,6 +7,6 @@ When the user types `/graphify`, use the installed graphify skill or instruction Rules: - For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. - Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. -- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. +- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing — unless a PreToolUse nudge flags it as predating graph.json (wiki/ is only regenerated by `graphify export wiki`, never by `update`/watch), in which case fall back to source or `graphify query`. - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). diff --git a/tools/skillgen/expected/graphify__always_on__antigravity-rules.md b/tools/skillgen/expected/graphify__always_on__antigravity-rules.md index 0fc786414..66eaafc24 100644 --- a/tools/skillgen/expected/graphify__always_on__antigravity-rules.md +++ b/tools/skillgen/expected/graphify__always_on__antigravity-rules.md @@ -9,6 +9,6 @@ This project has a graphify knowledge graph at graphify-out/. Rules: - For codebase or architecture questions, when `graphify-out/graph.json` exists, first run `graphify query ""` (CLI) or `query_graph` (MCP). Use `graphify path "" ""` / `shortest_path` for relationships and `graphify explain ""` / `get_node` for focused concepts. These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output. -- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files +- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files — unless a PreToolUse nudge flags it as predating graph.json (wiki/ is only regenerated by `graphify export wiki`, never by `update`/watch), in which case fall back to source or `graphify query` - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context - After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) diff --git a/tools/skillgen/fragments/always-on/agents-md.md b/tools/skillgen/fragments/always-on/agents-md.md index 6511cd1dd..222ce344c 100644 --- a/tools/skillgen/fragments/always-on/agents-md.md +++ b/tools/skillgen/fragments/always-on/agents-md.md @@ -7,6 +7,6 @@ When the user types `/graphify`, use the installed graphify skill or instruction Rules: - For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. - Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. -- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. +- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing — unless a PreToolUse nudge flags it as predating graph.json (wiki/ is only regenerated by `graphify export wiki`, never by `update`/watch), in which case fall back to source or `graphify query`. - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). diff --git a/tools/skillgen/fragments/always-on/antigravity-rules.md b/tools/skillgen/fragments/always-on/antigravity-rules.md index 0fc786414..66eaafc24 100644 --- a/tools/skillgen/fragments/always-on/antigravity-rules.md +++ b/tools/skillgen/fragments/always-on/antigravity-rules.md @@ -9,6 +9,6 @@ This project has a graphify knowledge graph at graphify-out/. Rules: - For codebase or architecture questions, when `graphify-out/graph.json` exists, first run `graphify query ""` (CLI) or `query_graph` (MCP). Use `graphify path "" ""` / `shortest_path` for relationships and `graphify explain ""` / `get_node` for focused concepts. These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output. -- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files +- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files — unless a PreToolUse nudge flags it as predating graph.json (wiki/ is only regenerated by `graphify export wiki`, never by `update`/watch), in which case fall back to source or `graphify query` - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context - After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) diff --git a/tools/skillgen/gen.py b/tools/skillgen/gen.py index 93e403f27..eb1417a44 100644 --- a/tools/skillgen/gen.py +++ b/tools/skillgen/gen.py @@ -110,6 +110,30 @@ def _v8_baseline_ref(platform_key: str) -> str: "When the user types `/graphify`, use the installed graphify skill or instructions " "before doing anything else.", ), + # #2006: wiki/ is only ever regenerated by the explicit `export wiki` + # subcommand, never by `update`/watch, so the unconditional "navigate + # it instead of reading raw files" instruction can point agents at a + # months-stale wiki with no signal anywhere in the loop. Point at the + # new PreToolUse staleness nudge (cli.py) as the escape hatch instead. + ( + "- If graphify-out/wiki/index.md exists, use it for broad navigation " + "instead of raw source browsing.", + "- If graphify-out/wiki/index.md exists, use it for broad navigation instead of " + "raw source browsing — unless a PreToolUse nudge flags it as predating " + "graph.json (wiki/ is only regenerated by `graphify export wiki`, never by " + "`update`/watch), in which case fall back to source or `graphify query`.", + ), + ), + # #2006: same fix as _AGENTS_MD_SECTION above, applied to antigravity's + # wording of the same rule. + "_ANTIGRAVITY_RULES": ( + ( + "- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files", + "- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files — " + "unless a PreToolUse nudge flags it as predating graph.json (wiki/ is only " + "regenerated by `graphify export wiki`, never by `update`/watch), in which case " + "fall back to source or `graphify query`", + ), ), }