Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion graphify/always_on/agents-md.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ When the user types `/graphify`, use the installed graphify skill or instruction
Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` 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).
2 changes: 1 addition & 1 deletion graphify/always_on/antigravity-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<question>"` (CLI) or `query_graph` (MCP). Use `graphify path "<A>" "<B>"` / `shortest_path` for relationships and `graphify explain "<concept>"` / `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)
27 changes: 26 additions & 1 deletion graphify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<question>"` 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
Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions tests/test_hook_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# --------------------------------------------------------------------------- #
Expand Down