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)