diff --git a/code_review_graph/cli.py b/code_review_graph/cli.py
index 0bf8e18..e32bb98 100644
--- a/code_review_graph/cli.py
+++ b/code_review_graph/cli.py
@@ -226,11 +226,13 @@ def main() -> None:
# build
build_cmd = sub.add_parser("build", help="Full graph build (re-parse all files)")
build_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
+ build_cmd.add_argument("-q", "--quiet", action="store_true", help="Suppress output")
# update
update_cmd = sub.add_parser("update", help="Incremental update (only changed files)")
update_cmd.add_argument("--base", default="HEAD~1", help="Git diff base (default: HEAD~1)")
update_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
+ update_cmd.add_argument("-q", "--quiet", action="store_true", help="Suppress output")
# watch
watch_cmd = sub.add_parser("watch", help="Watch for changes and auto-update")
@@ -239,6 +241,11 @@ def main() -> None:
# status
status_cmd = sub.add_parser("status", help="Show graph statistics")
status_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)")
+ status_cmd.add_argument("-q", "--quiet", action="store_true", help="Suppress output")
+ status_cmd.add_argument(
+ "--json", action="store_true", dest="json_output",
+ help="Output as JSON",
+ )
# visualize
vis_cmd = sub.add_parser("visualize", help="Generate interactive HTML graph visualization")
@@ -421,43 +428,62 @@ def main() -> None:
try:
if args.command == "build":
result = full_build(repo_root, store)
- print(
- f"Full build: {result['files_parsed']} files, "
- f"{result['total_nodes']} nodes, {result['total_edges']} edges"
- )
- if result["errors"]:
- print(f"Errors: {len(result['errors'])}")
+ if not args.quiet:
+ print(
+ f"Full build: {result['files_parsed']} files, "
+ f"{result['total_nodes']} nodes, {result['total_edges']} edges"
+ )
+ if result["errors"]:
+ print(f"Errors: {len(result['errors'])}")
elif args.command == "update":
result = incremental_update(repo_root, store, base=args.base)
- print(
- f"Incremental: {result['files_updated']} files updated, "
- f"{result['total_nodes']} nodes, {result['total_edges']} edges"
- )
+ if not args.quiet:
+ print(
+ f"Incremental: {result['files_updated']} files updated, "
+ f"{result['total_nodes']} nodes, {result['total_edges']} edges"
+ )
elif args.command == "status":
+ import json as json_mod
stats = store.get_stats()
- print(f"Nodes: {stats.total_nodes}")
- print(f"Edges: {stats.total_edges}")
- print(f"Files: {stats.files_count}")
- print(f"Languages: {', '.join(stats.languages)}")
- print(f"Last updated: {stats.last_updated or 'never'}")
- # Show branch info and warn if stale
stored_branch = store.get_metadata("git_branch")
stored_sha = store.get_metadata("git_head_sha")
- if stored_branch:
- print(f"Built on branch: {stored_branch}")
- if stored_sha:
- print(f"Built at commit: {stored_sha[:12]}")
from .incremental import _git_branch_info
current_branch, current_sha = _git_branch_info(repo_root)
+ stale_warning = None
if stored_branch and current_branch and stored_branch != current_branch:
- print(
- f"WARNING: Graph was built on '{stored_branch}' "
+ stale_warning = (
+ f"Graph was built on '{stored_branch}' "
f"but you are now on '{current_branch}'. "
f"Run 'code-review-graph build' to rebuild."
)
+ if getattr(args, "json_output", False):
+ data = {
+ "nodes": stats.total_nodes,
+ "edges": stats.total_edges,
+ "files": stats.files_count,
+ "languages": list(stats.languages),
+ "last_updated": stats.last_updated,
+ "branch": stored_branch,
+ "commit": stored_sha[:12] if stored_sha else None,
+ "stale": stale_warning,
+ }
+ print(json_mod.dumps(data))
+ elif not args.quiet:
+ print(f"Nodes: {stats.total_nodes}")
+ print(f"Edges: {stats.total_edges}")
+ print(f"Files: {stats.files_count}")
+ print(f"Languages: {', '.join(stats.languages)}")
+ print(f"Last updated: {stats.last_updated or 'never'}")
+ if stored_branch:
+ print(f"Built on branch: {stored_branch}")
+ if stored_sha:
+ print(f"Built at commit: {stored_sha[:12]}")
+ if stale_warning:
+ print(f"WARNING: {stale_warning}")
+
elif args.command == "watch":
watch(repo_root, store)
diff --git a/code_review_graph/skills.py b/code_review_graph/skills.py
index 1920709..1c8d40e 100644
--- a/code_review_graph/skills.py
+++ b/code_review_graph/skills.py
@@ -307,7 +307,7 @@ def generate_hooks_config() -> dict[str, Any]:
"""Generate Claude Code hooks configuration.
Returns a hooks config dict with PostToolUse, SessionStart, and
- PreCommit hooks for automatic graph updates.
+ PreToolUse hooks for automatic graph updates and pre-commit analysis.
Returns:
Dict with hooks configuration suitable for .claude/settings.json.
@@ -317,20 +317,38 @@ def generate_hooks_config() -> dict[str, Any]:
"PostToolUse": [
{
"matcher": "Edit|Write|Bash",
- "command": "code-review-graph update --quiet",
- "timeout": 5000,
+ "hooks": [
+ {
+ "type": "command",
+ "command": "code-review-graph update --quiet",
+ "timeout": 5000,
+ }
+ ],
},
],
"SessionStart": [
{
- "command": "code-review-graph status --json",
- "timeout": 3000,
+ "matcher": "",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "code-review-graph status --json",
+ "timeout": 3000,
+ }
+ ],
},
],
- "PreCommit": [
+ "PreToolUse": [
{
- "command": "code-review-graph detect-changes --brief",
- "timeout": 10000,
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "if": "Bash(git commit*)",
+ "command": "code-review-graph detect-changes --brief",
+ "timeout": 10000,
+ }
+ ],
},
],
}
diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
index 7caae26..2702e69 100644
--- a/docs/COMMANDS.md
+++ b/docs/COMMANDS.md
@@ -229,11 +229,14 @@ code-review-graph install --dry-run # Preview without writing files
# Build and update
code-review-graph build # Full build
+code-review-graph build --quiet # Full build, no output
code-review-graph update # Incremental update
+code-review-graph update --quiet # Incremental update, no output
code-review-graph update --base origin/main # Custom base ref
# Monitor and inspect
code-review-graph status # Graph statistics
+code-review-graph status --json # Machine-readable JSON output
code-review-graph watch # Auto-update on file changes
code-review-graph visualize # Generate interactive HTML graph
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index d3cb4aa..0b5dedd 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -106,6 +106,7 @@
- **6 MCP tools** for full graph interaction
- **3 review-first skills**: build-graph, review-delta, review-pr
- **PostToolUse hooks** (Write|Edit|Bash) for automatic background updates
+- **PreToolUse hooks** for pre-commit change analysis via `detect-changes`
- **FastMCP 3.0 compatible** stdio MCP server
## Privacy & Data
diff --git a/docs/LLM-OPTIMIZED-REFERENCE.md b/docs/LLM-OPTIMIZED-REFERENCE.md
index f3b4890..498af92 100644
--- a/docs/LLM-OPTIMIZED-REFERENCE.md
+++ b/docs/LLM-OPTIMIZED-REFERENCE.md
@@ -37,6 +37,7 @@ MIT license. 100% local. No telemetry. DB file: .code-review-graph/graph.db
Run: code-review-graph watch (auto-updates graph on file save via watchdog)
Or use PostToolUse (Write|Edit|Bash) hooks for automatic background updates.
+PreToolUse hooks with `if: "Bash(git commit*)"` run detect-changes before commits.
diff --git a/docs/architecture.md b/docs/architecture.md
index 6928b04..f244d49 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -10,11 +10,11 @@
┌──────────────────────────────────────────────────────────────┐
│ Claude Code │
│ │
-│ Skills (SKILL.md) Hooks (hooks.json) │
-│ ├── build-graph └── PostToolUse (Write|Edit|Bash) │
-│ ├── review-delta → incremental update │
-│ └── review-pr │
-│ │ │ │
+│ Skills (SKILL.md) Hooks (settings.json) │
+│ ├── build-graph ├── PostToolUse (Write|Edit|Bash) │
+│ ├── review-delta │ → incremental update │
+│ └── review-pr └── PreToolUse (git commit) │
+│ │ → detect-changes │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ MCP Server (stdio) │ │
diff --git a/tests/test_skills.py b/tests/test_skills.py
index 09e4fae..2ec9c5d 100644
--- a/tests/test_skills.py
+++ b/tests/test_skills.py
@@ -66,32 +66,43 @@ def test_returns_dict_with_hooks(self):
def test_has_post_tool_use(self):
config = generate_hooks_config()
assert "PostToolUse" in config["hooks"]
- hooks = config["hooks"]["PostToolUse"]
- assert len(hooks) >= 1
- assert hooks[0]["matcher"] == "Edit|Write|Bash"
- assert "update" in hooks[0]["command"]
- assert hooks[0]["timeout"] == 5000
+ entries = config["hooks"]["PostToolUse"]
+ assert len(entries) >= 1
+ assert entries[0]["matcher"] == "Edit|Write|Bash"
+ inner = entries[0]["hooks"]
+ assert len(inner) >= 1
+ assert inner[0]["type"] == "command"
+ assert "update" in inner[0]["command"]
+ assert inner[0]["timeout"] == 5000
def test_has_session_start(self):
config = generate_hooks_config()
assert "SessionStart" in config["hooks"]
- hooks = config["hooks"]["SessionStart"]
- assert len(hooks) >= 1
- assert "status" in hooks[0]["command"]
- assert hooks[0]["timeout"] == 3000
-
- def test_has_pre_commit(self):
+ entries = config["hooks"]["SessionStart"]
+ assert len(entries) >= 1
+ inner = entries[0]["hooks"]
+ assert len(inner) >= 1
+ assert inner[0]["type"] == "command"
+ assert "status" in inner[0]["command"]
+ assert inner[0]["timeout"] == 3000
+
+ def test_has_pre_tool_use(self):
config = generate_hooks_config()
- assert "PreCommit" in config["hooks"]
- hooks = config["hooks"]["PreCommit"]
- assert len(hooks) >= 1
- assert "detect-changes" in hooks[0]["command"]
- assert hooks[0]["timeout"] == 10000
-
- def test_has_all_three_hook_types(self):
+ assert "PreToolUse" in config["hooks"]
+ entries = config["hooks"]["PreToolUse"]
+ assert len(entries) >= 1
+ assert entries[0]["matcher"] == "Bash"
+ inner = entries[0]["hooks"]
+ assert len(inner) >= 1
+ assert inner[0]["type"] == "command"
+ assert inner[0]["if"] == "Bash(git commit*)"
+ assert "detect-changes" in inner[0]["command"]
+ assert inner[0]["timeout"] == 10000
+
+ def test_has_all_hook_types(self):
config = generate_hooks_config()
hook_types = set(config["hooks"].keys())
- assert hook_types == {"PostToolUse", "SessionStart", "PreCommit"}
+ assert hook_types == {"PostToolUse", "SessionStart", "PreToolUse"}
class TestInstallHooks:
@@ -114,7 +125,7 @@ def test_merges_with_existing(self, tmp_path):
assert data["customSetting"] is True
assert "PostToolUse" in data["hooks"]
assert "SessionStart" in data["hooks"]
- assert "PreCommit" in data["hooks"]
+ assert "PreToolUse" in data["hooks"]
def test_creates_claude_directory(self, tmp_path):
install_hooks(tmp_path)