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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ code-review-graph install # auto-detects and configures all supported p
code-review-graph build # parse your codebase
```

One command sets up everything. `install` detects which AI coding tools you have, writes the correct MCP configuration for each one, installs platform-native hooks/skills where supported, and injects graph-aware instructions into your platform rules. It auto-detects whether you installed via `uvx` or `pip`/`pipx` and generates the right config. Restart your editor/tool after installing.
One command sets up everything. `install` detects which AI coding tools you have, writes the correct MCP configuration for each one, installs platform-native hooks/skills where supported, and injects graph-aware instructions into your platform rules. It auto-detects whether you installed via `uvx` or `pip`/`pipx` and generates the right config. Restart or refresh your editor/tool after installing so its MCP and Skill inventory is reloaded.

<p align="center">
<img src="diagrams/diagram8_supported_platforms.png" alt="One Install, Every Platform: auto-detects Codex, Claude Code, CodeBuddy Code, Cursor, Windsurf, Zed, Continue, OpenCode, Antigravity, Gemini CLI, Qwen, Qoder, Kiro, GitHub Copilot, and GitHub Copilot CLI" width="85%" />
Expand All @@ -88,6 +88,14 @@ code-review-graph install --platform copilot-cli # configure only GitHub Copilo
code-review-graph install --platform codebuddy # configure only CodeBuddy Code
```

For Codex, the installer also places a global `code-review-graph` Skill under
`$CODEX_HOME/skills/code-review-graph/` (default:
`~/.codex/skills/code-review-graph/`). It prefers CRG MCP tools and falls back
to the bundled read-only CLI when MCP is unavailable; a missing, empty, or
stale graph never triggers an implicit build or update. WSL and Windows Codex
runtimes have separate `CODEX_HOME` directories and tool inventories, so
install and refresh the runtime that will execute the task.

Requires Python 3.10+. For the best experience, install [uv](https://docs.astral.sh/uv/) (the MCP config will use `uvx` if available, otherwise falls back to the `code-review-graph` command directly).

To remove CRG from a Git or SVN project, use the symmetric uninstall command
Expand Down
60 changes: 60 additions & 0 deletions code_review_graph/assets/code-review-graph/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
name: code-review-graph
description: Use the local code-review-graph knowledge graph for compact repository exploration, architecture questions, code review, debugging, dependency tracing, impact analysis, and refactoring safety checks. Use when the user mentions CRG/code-review-graph, asks to use its graph, or an existing graph can materially narrow repository work; do not invoke it repeatedly for an ordinary repository with no graph and no graph request.
---

# Code Review Graph

Use CRG as a local structural index, not as a replacement for source inspection. The workflow is safe by default: read-only inspection may use an existing graph, while graph creation or maintenance always requires explicit user intent.

## Decide whether to invoke CRG

- Invoke this skill when the user names CRG/code-review-graph, asks for graph-backed exploration/review/debugging/impact analysis, or the repository already contains a usable CRG graph and graph context would reduce broad scanning.
- For an ordinary repository with no graph and no request to use or build CRG, do not call the CLI or MCP merely to keep checking. A single first-task health check is allowed only after this skill has been triggered by the conditions above.
- “Use the graph” and “understand this code” do not authorize building a missing graph. Ask before any mutating CRG command.

## Entry workflow

1. Resolve the active repository root from the workspace or `git rev-parse --show-toplevel`. Never embed the checkout used during installation.
2. Inspect the current tool inventory for CRG MCP tools. Prefer MCP when its tools are actually exposed.
3. On the first CRG task for a repository, perform exactly one read-only health check. With MCP, use `get_minimal_context_tool(task=<short task>, repo_root=<root>)` or the available graph-stats tool. Without MCP, run the bundled helper’s `status --json` command. Reuse the result; do not poll status on every turn.
4. If the graph is healthy and fresh, use the smallest relevant MCP query or CLI read-only query to narrow the work. If an MCP call fails, retry at most once, then use the CLI helper.
5. If the graph is missing, empty, or stale, report that once and stop using CRG for this task unless the user explicitly authorizes maintenance. Continue with the smallest useful source/test inspection.

## Health and mutation boundary

- A graph is not ready when its database is missing, `nodes` or `files` is zero, or `last_updated` is null. It is stale when the built commit does not match the current repository commit.
- Never run `build`, `update`, `postprocess`, `embed`, or `watch` unless the user explicitly asks to create/maintain the graph or has already granted that authority for this task. Verify status once after an authorized mutation.
- Never launch `serve` as a one-shot query or implicitly use `uvx`; `serve` is a long-running MCP process. Use the installed CLI executable for fallback queries.
- Keep all CRG operations local. Do not send credentials, private keys, or unrelated source to external services.
- Graph results only select a smaller reading scope. Read the actual implementation and relevant tests before making or reporting behavioral conclusions; if graph and source disagree, trust source.

## Route by task

- Explore/architecture: start with minimal context or `architecture --detail-level minimal`, then search symbols and trace only relevant callers, callees, imports, tests, communities, or flows.
- Review changes: use change detection, affected flows, impact radius, and `tests_for`; request source snippets only for changed/high-risk areas.
- Debug: search the suspected symbols/terms, trace callers and callees, inspect one relevant flow, and verify hypotheses against source, logs, and tests.
- Refactor/rename: preview impact and tests first; never apply a graph-backed refactor without the user’s explicit edit request.

## CLI fallback

Use the bundled read-only wrapper (replace `<skill-root>` with this skill’s directory):

```bash
python3 <skill-root>/scripts/crg_readonly.py status --repo "<repo-root>"
python3 <skill-root>/scripts/crg_readonly.py architecture --repo "<repo-root>"
python3 <skill-root>/scripts/crg_readonly.py search "<query>" --repo "<repo-root>"
python3 <skill-root>/scripts/crg_readonly.py query callers_of "<symbol>" --repo "<repo-root>"
python3 <skill-root>/scripts/crg_readonly.py impact --repo "<repo-root>"
python3 <skill-root>/scripts/crg_readonly.py detect-changes --brief --repo "<repo-root>"
```

The wrapper resolves/validates the repository, passes arguments without shell interpolation, and exposes only read-only commands. Set `CRG_BIN` only when the installed executable has a non-standard name/path. Preserve and report its errors; do not turn a failed fallback into permission to build.

## WSL and Windows scope

Codex processes in WSL and Windows have separate homes and configuration/tool inventories. Resolve the `CODEX_HOME` of the runtime that is actually running the task, and restart or refresh Codex after installing a skill or MCP entry. A Windows-mounted path such as `/mnt/e/...` is not by itself a failure: verify that the selected runtime can execute the CLI and access that path. Do not assume a WSL MCP registration is visible to a Windows Codex session, or vice versa.

## Output discipline

State whether evidence came from CRG MCP or the CLI fallback, include the repository root and graph readiness/freshness when material, and keep graph-derived relationships concise. Do not claim CRG was used if its tools and fallback both failed.
4 changes: 4 additions & 0 deletions code_review_graph/assets/code-review-graph/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Code Review Graph"
short_description: "Safe graph context with CLI fallback"
default_prompt: "Use $code-review-graph to explore this repository with safe graph context."
256 changes: 256 additions & 0 deletions code_review_graph/assets/code-review-graph/scripts/crg_readonly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Run safe, read-only code-review-graph CLI queries for one repository."""

from __future__ import annotations

import argparse
import os
import shlex
import shutil
import subprocess
import sys
from pathlib import Path

QUERY_PATTERNS = (
"callers_of",
"callees_of",
"imports_of",
"importers_of",
"children_of",
"tests_for",
"inheritors_of",
"file_summary",
)
SEARCH_KINDS = ("File", "Class", "Function", "Type", "Test")
READ_ONLY_COMMANDS = {
"status",
"search",
"query",
"impact",
"detect-changes",
"architecture",
"flows",
"flow",
"communities",
"community",
"large-functions",
"refactor",
}


def _resolve_repo(raw: str | None) -> Path:
if raw:
root = Path(raw).expanduser().resolve()
if not root.is_dir():
raise ValueError(f"Repository directory does not exist: {root}")
return root
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
cwd=Path.cwd(),
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
timeout=5,
check=False,
)
except (FileNotFoundError, OSError, subprocess.TimeoutExpired) as exc:
raise ValueError("Could not resolve the repository root with Git") from exc
if result.returncode != 0 or not result.stdout.strip():
raise ValueError("Current directory is not inside a Git repository; pass --repo")
return Path(result.stdout.strip()).expanduser().resolve()


def _crg_command() -> list[str]:
override = os.environ.get("CRG_BIN", "").strip()
if override:
parts = shlex.split(override)
if not parts:
raise ValueError("CRG_BIN is empty")
return parts
binary = shutil.which("code-review-graph")
if binary:
return [binary]
raise FileNotFoundError(
"code-review-graph is not on PATH; install it or set CRG_BIN to its executable"
)


def _add_common(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--repo", default=None, help="Repository root (auto-detected)")


def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Run read-only code-review-graph queries against a repository"
)
sub = parser.add_subparsers(dest="command", required=True)

status = sub.add_parser("status", help="Show graph statistics as JSON")
_add_common(status)
search = sub.add_parser("search", help="Search graph entities")
search.add_argument("query")
search.add_argument("--kind", choices=SEARCH_KINDS, default=None)
search.add_argument("--limit", type=int, default=20)
_add_common(search)
query = sub.add_parser("query", help="Query graph relationships")
query.add_argument("pattern", choices=QUERY_PATTERNS)
query.add_argument("target")
_add_common(query)
impact = sub.add_parser("impact", help="Analyze change blast radius")
impact.add_argument("--files", nargs="+", default=None)
impact.add_argument("--depth", type=int, default=2)
impact.add_argument("--max-results", type=int, default=500)
impact.add_argument("--base", default="HEAD~1")
_add_common(impact)
detect = sub.add_parser("detect-changes", help="Analyze changed files")
detect.add_argument("--base", default="HEAD~1")
detect.add_argument("--brief", action="store_true")
detect.add_argument("--churn", action="store_true")
detect.add_argument("--verify", action="store_true")
_add_common(detect)
architecture = sub.add_parser("architecture", help="Show architecture overview")
architecture.add_argument(
"--detail-level", choices=("minimal", "standard"), default="minimal"
)
_add_common(architecture)
flows = sub.add_parser("flows", help="List stored execution flows")
flows.add_argument(
"--sort",
choices=("criticality", "depth", "node_count", "file_count", "name"),
default="criticality",
)
flows.add_argument("--limit", type=int, default=50)
flows.add_argument("--kind", default=None)
_add_common(flows)
flow = sub.add_parser("flow", help="Show one stored flow")
selector = flow.add_mutually_exclusive_group(required=True)
selector.add_argument("--id", type=int)
selector.add_argument("--name")
flow.add_argument("--source", action="store_true")
_add_common(flow)
communities = sub.add_parser("communities", help="List graph communities")
communities.add_argument(
"--sort", choices=("size", "cohesion", "name"), default="size"
)
communities.add_argument("--min-size", type=int, default=0)
_add_common(communities)
community = sub.add_parser("community", help="Show one graph community")
selector = community.add_mutually_exclusive_group(required=True)
selector.add_argument("--id", type=int)
selector.add_argument("--name")
community.add_argument("--members", action="store_true")
_add_common(community)
large = sub.add_parser("large-functions", help="Find oversized graph nodes")
large.add_argument("--min-lines", type=int, default=50)
large.add_argument(
"--kind", choices=("Function", "Class", "File", "Test"), default=None
)
large.add_argument("--path", default=None)
large.add_argument("--limit", type=int, default=50)
_add_common(large)
refactor = sub.add_parser("refactor", help="Preview graph-backed refactors")
refactor.add_argument("mode", choices=("rename", "dead_code", "suggest"))
refactor.add_argument("--old-name", default=None)
refactor.add_argument("--new-name", default=None)
refactor.add_argument("--kind", choices=("Function", "Class"), default=None)
refactor.add_argument("--path", default=None)
_add_common(refactor)
return parser


def _command_args(args: argparse.Namespace, root: Path) -> list[str]:
command = args.command
result = [command]
if command == "status":
result.append("--json")
elif command == "search":
result.append(args.query)
if args.kind:
result.extend(("--kind", args.kind))
result.extend(("--limit", str(args.limit)))
elif command == "query":
result.extend((args.pattern, args.target))
elif command == "impact":
if args.files:
result.extend(("--files", *args.files))
result.extend(
(
"--depth",
str(args.depth),
"--max-results",
str(args.max_results),
"--base",
args.base,
)
)
elif command == "detect-changes":
result.extend(("--base", args.base))
for flag in ("brief", "churn", "verify"):
if getattr(args, flag):
result.append(f"--{flag}")
elif command == "architecture":
result.extend(("--detail-level", args.detail_level))
elif command == "flows":
result.extend(("--sort", args.sort, "--limit", str(args.limit)))
if args.kind:
result.extend(("--kind", args.kind))
elif command == "flow":
result.extend(
("--id", str(args.id))
if args.id is not None
else ("--name", args.name)
)
if args.source:
result.append("--source")
elif command == "communities":
result.extend(("--sort", args.sort, "--min-size", str(args.min_size)))
elif command == "community":
result.extend(
("--id", str(args.id))
if args.id is not None
else ("--name", args.name)
)
if args.members:
result.append("--members")
elif command == "large-functions":
result.extend(("--min-lines", str(args.min_lines), "--limit", str(args.limit)))
if args.kind:
result.extend(("--kind", args.kind))
if args.path:
result.extend(("--path", args.path))
elif command == "refactor":
result.append(args.mode)
for option, value in (
("--old-name", args.old_name),
("--new-name", args.new_name),
("--kind", args.kind),
("--path", args.path),
):
if value:
result.extend((option, value))
result.extend(("--repo", str(root)))
return result


def main() -> int:
args = _build_parser().parse_args()
if args.command not in READ_ONLY_COMMANDS:
print(f"Unsupported command: {args.command}", file=sys.stderr)
return 2
try:
root = _resolve_repo(args.repo)
command = _crg_command() + _command_args(args, root)
except (FileNotFoundError, ValueError) as exc:
print(f"CRG fallback unavailable: {exc}", file=sys.stderr)
return 127
try:
completed = subprocess.run(command, cwd=str(root), check=False)
except (FileNotFoundError, OSError) as exc:
print(f"CRG fallback unavailable: {exc}", file=sys.stderr)
return 127
return completed.returncode


if __name__ == "__main__":
raise SystemExit(main())
9 changes: 8 additions & 1 deletion code_review_graph/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ def _handle_init(args: argparse.Namespace) -> None:
install_codebuddy_hooks,
install_codebuddy_skills,
install_codex_hooks,
install_codex_skill,
install_cursor_hooks,
install_gemini_cli_hooks,
install_gemini_cli_skills,
Expand All @@ -319,8 +320,14 @@ def _handle_init(args: argparse.Namespace) -> None:
install_opencode_plugin,
install_qoder_skills,
)

if not skip_skills:
codex_detected = target == "codex" or (
target == "all" and PLATFORMS["codex"]["detect"]()
)
if codex_detected:
codex_skill_dir = install_codex_skill()
print(f"Installed Codex skill in {codex_skill_dir}")

# Claude Code skills are only relevant for Claude (or full install).
if target in ("claude", "all"):
skills_dir = generate_skills(repo_root)
Expand Down
Loading