Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
70 changes: 48 additions & 22 deletions code_review_graph/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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)

Expand Down
34 changes: 26 additions & 8 deletions code_review_graph/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
}
],
},
],
}
Expand Down
3 changes: 3 additions & 0 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/LLM-OPTIMIZED-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ MIT license. 100% local. No telemetry. DB file: .code-review-graph/graph.db
<section name="watch">
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.
</section>

<section name="embeddings">
Expand Down
10 changes: 5 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) │ │
Expand Down
51 changes: 31 additions & 20 deletions tests/test_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
Loading