diff --git a/.github/code-review-graph.instruction.md b/.github/code-review-graph.instruction.md
new file mode 100644
index 00000000..1d51118b
--- /dev/null
+++ b/.github/code-review-graph.instruction.md
@@ -0,0 +1,43 @@
+---
+applyTo: '**'
+description: Use code-review-graph MCP tools for token-efficient codebase exploration and code review instead of built-in file/search tools.
+---
+
+
+## MCP Tools: code-review-graph
+
+**IMPORTANT: This project has a knowledge graph. ALWAYS use the
+code-review-graph MCP tools BEFORE using #tool:read/readFile #tool:search/fileSearch #tool:search/textSearch to explore
+the codebase.** The graph is faster, cheaper (fewer tokens), and gives
+you structural context (callers, dependents, test coverage) that file
+scanning cannot.
+
+### When to use graph tools FIRST
+
+- **Exploring code**: `semantic_search_nodes` or `query_graph` instead of Grep
+- **Understanding impact**: `get_impact_radius` instead of manually tracing imports
+- **Code review**: `detect_changes` + `get_review_context` instead of reading entire files
+- **Finding relationships**: `query_graph` with callers_of/callees_of/imports_of/tests_for
+- **Architecture questions**: `get_architecture_overview` + `list_communities`
+
+Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need.
+
+### Key Tools
+
+| Tool | Use when |
+| ------ | ---------- |
+| `detect_changes` | Reviewing code changes — gives risk-scored analysis |
+| `get_review_context` | Need source snippets for review — token-efficient |
+| `get_impact_radius` | Understanding blast radius of a change |
+| `get_affected_flows` | Finding which execution paths are impacted |
+| `query_graph` | Tracing callers, callees, imports, tests, dependencies |
+| `semantic_search_nodes` | Finding functions/classes by name or keyword |
+| `get_architecture_overview` | Understanding high-level codebase structure |
+| `refactor_tool` | Planning renames, finding dead code |
+
+### Workflow
+
+1. The graph auto-updates on file changes (via hooks).
+2. Use `detect_changes` for code review.
+3. Use `get_affected_flows` to understand impact.
+4. Use `query_graph` pattern="tests_for" to check coverage.
diff --git a/README.md b/README.md
index 07249031..69ec730f 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +45,7 @@ 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, 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.
-
+
To target a specific platform:
@@ -55,6 +55,8 @@ code-review-graph install --platform codex # configure only Codex
code-review-graph install --platform cursor # configure only Cursor
code-review-graph install --platform claude-code # configure only Claude Code
code-review-graph install --platform kiro # configure only Kiro
+code-review-graph install --platform copilot # configure only GitHub Copilot (VS Code)
+code-review-graph install --platform copilot-cli # configure only GitHub Copilot CLI
```
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).
@@ -511,5 +513,5 @@ MIT. See [LICENSE](LICENSE).
code-review-graph.com
pip install code-review-graph && code-review-graph install
-Works with Codex, Claude Code, Cursor, Windsurf, Zed, Continue, OpenCode, Antigravity, Qwen, Qoder, and Kiro
+Works with Codex, Claude Code, Cursor, Windsurf, Zed, Continue, OpenCode, Antigravity, Qwen, Qoder, Kiro, GitHub Copilot, and GitHub Copilot CLI
diff --git a/code_review_graph/cli.py b/code_review_graph/cli.py
index 18e9d527..86411aeb 100644
--- a/code_review_graph/cli.py
+++ b/code_review_graph/cli.py
@@ -52,7 +52,8 @@
# Shared platform choices for install and init commands
_PLATFORM_CHOICES = [
"codex", "claude", "claude-code", "cursor", "windsurf", "zed",
- "continue", "opencode", "antigravity", "qwen", "kiro", "qoder", "all",
+ "continue", "opencode", "antigravity", "qwen", "kiro", "qoder",
+ "copilot", "copilot-cli", "all",
]
diff --git a/code_review_graph/skills.py b/code_review_graph/skills.py
index 66265534..efddc136 100644
--- a/code_review_graph/skills.py
+++ b/code_review_graph/skills.py
@@ -120,6 +120,22 @@ def _zed_settings_path() -> Path:
"format": "object",
"needs_type": True,
},
+ "copilot": {
+ "name": "GitHub Copilot",
+ "config_path": lambda root: root / ".vscode" / "mcp.json",
+ "key": "servers",
+ "detect": lambda: (Path.home() / ".vscode").exists(),
+ "format": "object",
+ "needs_type": True,
+ },
+ "copilot-cli": {
+ "name": "GitHub Copilot CLI",
+ "config_path": lambda root: Path.home() / ".copilot" / "mcp-config.json",
+ "key": "servers",
+ "detect": lambda: (Path.home() / ".copilot").exists(),
+ "format": "object",
+ "needs_type": True,
+ },
}
@@ -681,6 +697,59 @@ def install_hooks(repo_root: Path, platform: str = "claude") -> None:
4. Use `query_graph` pattern=\"tests_for\" to check coverage.
"""
+# Copilot-specific instruction file content: uses VS Code tool references and
+# includes YAML front matter so Copilot Chat applies it across the workspace.
+_COPILOT_SECTION = f"""---
+applyTo: '**'
+description: Use code-review-graph MCP tools for token-efficient codebase exploration and code review instead of built-in file/search tools.
+---
+
+{_CLAUDE_MD_SECTION_MARKER}
+## MCP Tools: code-review-graph
+
+**IMPORTANT: This project has a knowledge graph. ALWAYS use the
+code-review-graph MCP tools BEFORE using #tool:read/readFile #tool:search/fileSearch #tool:search/textSearch to explore
+the codebase.** The graph is faster, cheaper (fewer tokens), and gives
+you structural context (callers, dependents, test coverage) that file
+scanning cannot.
+
+### When to use graph tools FIRST
+
+- **Exploring code**: `semantic_search_nodes` or `query_graph` instead of #tool:search/fileSearch
+- **Understanding impact**: `get_impact_radius` instead of manually tracing imports
+- **Code review**: `detect_changes` + `get_review_context` instead of reading entire files
+- **Finding relationships**: `query_graph` with callers_of/callees_of/imports_of/tests_for
+- **Architecture questions**: `get_architecture_overview` + `list_communities`
+
+Fall back to #tool:read/readFile, #tool:search/fileSearch, or #tool:search/textSearch **only** when the graph doesn't cover what you need.
+
+### Key Tools
+
+| Tool | Use when |
+| ------ | ---------- |
+| `detect_changes` | Reviewing code changes — gives risk-scored analysis |
+| `get_review_context` | Need source snippets for review — token-efficient |
+| `get_impact_radius` | Understanding blast radius of a change |
+| `get_affected_flows` | Finding which execution paths are impacted |
+| `query_graph` | Tracing callers, callees, imports, tests, dependencies |
+| `semantic_search_nodes` | Finding functions/classes by name or keyword |
+| `get_architecture_overview` | Understanding high-level codebase structure |
+| `refactor_tool` | Planning renames, finding dead code |
+
+### Workflow
+
+1. The graph auto-updates on file changes (via hooks).
+2. Use `detect_changes` for code review.
+3. Use `get_affected_flows` to understand impact.
+4. Use `query_graph` pattern=\"tests_for\" to check coverage.
+"""
+
+# Maps instruction file path → (marker, section) for files that need content
+# different from the default _CLAUDE_MD_SECTION.
+_PLATFORM_INSTRUCTION_CUSTOM_SECTIONS: dict[str, tuple[str, str]] = {
+ ".github/code-review-graph.instruction.md": (_CLAUDE_MD_SECTION_MARKER, _COPILOT_SECTION),
+}
+
def _inject_instructions(file_path: Path, marker: str, section: str) -> bool:
"""Append an instruction section to a file if not already present.
@@ -725,6 +794,7 @@ def inject_claude_md(repo_root: Path) -> None:
".windsurfrules": ("windsurf",),
"QODER.md": ("qoder",),
".kiro/steering/code-review-graph.md": ("kiro",),
+ ".github/code-review-graph.instruction.md": ("copilot", "copilot-cli"),
}
@@ -746,7 +816,10 @@ def inject_platform_instructions(repo_root: Path, target: str = "all") -> list[s
if target != "all" and target not in owners:
continue
path = repo_root / filename
- if _inject_instructions(path, _CLAUDE_MD_SECTION_MARKER, _CLAUDE_MD_SECTION):
+ marker, section = _PLATFORM_INSTRUCTION_CUSTOM_SECTIONS.get(
+ filename, (_CLAUDE_MD_SECTION_MARKER, _CLAUDE_MD_SECTION)
+ )
+ if _inject_instructions(path, marker, section):
updated.append(filename)
return updated
diff --git a/tests/test_skills.py b/tests/test_skills.py
index 873ed2f8..b0dddae7 100644
--- a/tests/test_skills.py
+++ b/tests/test_skills.py
@@ -329,11 +329,11 @@ def test_idempotent_with_existing_content(self, tmp_path):
class TestInjectPlatformInstructionsFiltering:
def test_all_writes_every_file(self, tmp_path):
updated = inject_platform_instructions(tmp_path, target="all")
- assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules", "QODER.md", ".kiro/steering/code-review-graph.md"}
+ assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules", "QODER.md", ".kiro/steering/code-review-graph.md", ".github/code-review-graph.instruction.md"}
def test_default_is_all(self, tmp_path):
updated = inject_platform_instructions(tmp_path)
- assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules", "QODER.md", ".kiro/steering/code-review-graph.md"}
+ assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules", "QODER.md", ".kiro/steering/code-review-graph.md", ".github/code-review-graph.instruction.md"}
def test_claude_writes_nothing(self, tmp_path):
updated = inject_platform_instructions(tmp_path, target="claude")
@@ -343,6 +343,7 @@ def test_claude_writes_nothing(self, tmp_path):
assert not (tmp_path / ".cursorrules").exists()
assert not (tmp_path / ".windsurfrules").exists()
assert not (tmp_path / "QODER.md").exists()
+ assert not (tmp_path / ".github" / "code-review-graph.instruction.md").exists()
def test_cursor_writes_only_cursor_files(self, tmp_path):
updated = inject_platform_instructions(tmp_path, target="cursor")
@@ -925,6 +926,236 @@ def test_kiro_dry_run(self, tmp_path):
assert not config_path.exists()
+class TestCopilotPlatform:
+ """Tests for GitHub Copilot platform support."""
+
+ def test_copilot_platform_entry_exists(self):
+ """PLATFORMS dict has a 'copilot' key with correct metadata."""
+ assert "copilot" in PLATFORMS
+ copilot = PLATFORMS["copilot"]
+ assert copilot["name"] == "GitHub Copilot"
+ assert copilot["key"] == "servers"
+ assert copilot["format"] == "object"
+ assert copilot["needs_type"] is True
+
+ def test_install_copilot_config(self, tmp_path):
+ """install_platform_configs creates .vscode/mcp.json with 'servers' key."""
+ configured = install_platform_configs(tmp_path, target="copilot")
+ assert "GitHub Copilot" in configured
+ config_path = tmp_path / ".vscode" / "mcp.json"
+ assert config_path.exists()
+ data = json.loads(config_path.read_text())
+ assert "code-review-graph" in data["servers"]
+ entry = data["servers"]["code-review-graph"]
+ assert entry["type"] == "stdio"
+ assert "serve" in entry["args"]
+
+ def test_install_copilot_preserves_existing_servers(self, tmp_path):
+ """Existing server entries are preserved when adding code-review-graph."""
+ config_path = tmp_path / ".vscode" / "mcp.json"
+ config_path.parent.mkdir(parents=True)
+ config_path.write_text(
+ json.dumps({"servers": {"other-server": {"command": "other"}}}),
+ encoding="utf-8",
+ )
+ install_platform_configs(tmp_path, target="copilot")
+ data = json.loads(config_path.read_text())
+ assert "other-server" in data["servers"]
+ assert "code-review-graph" in data["servers"]
+
+ def test_install_copilot_no_duplicate(self, tmp_path):
+ """Second install skips when code-review-graph already exists."""
+ install_platform_configs(tmp_path, target="copilot")
+ config_path = tmp_path / ".vscode" / "mcp.json"
+ first_content = config_path.read_text()
+ install_platform_configs(tmp_path, target="copilot")
+ second_content = config_path.read_text()
+ assert first_content == second_content
+ data = json.loads(second_content)
+ assert list(data["servers"].keys()).count("code-review-graph") == 1
+
+ def test_copilot_instructions_file_written(self, tmp_path):
+ """inject_platform_instructions creates .github/code-review-graph.instruction.md."""
+ updated = inject_platform_instructions(tmp_path, target="copilot")
+ assert ".github/code-review-graph.instruction.md" in updated
+ instructions = tmp_path / ".github" / "code-review-graph.instruction.md"
+ assert instructions.exists()
+ content = instructions.read_text()
+ assert _CLAUDE_MD_SECTION_MARKER in content
+
+ def test_copilot_instructions_idempotent(self, tmp_path):
+ """Running inject twice produces identical content."""
+ inject_platform_instructions(tmp_path, target="copilot")
+ first = (tmp_path / ".github" / "code-review-graph.instruction.md").read_text()
+ inject_platform_instructions(tmp_path, target="copilot")
+ second = (tmp_path / ".github" / "code-review-graph.instruction.md").read_text()
+ assert first == second
+
+ def test_copilot_dry_run(self, tmp_path):
+ """dry_run=True does not create any files."""
+ configured = install_platform_configs(tmp_path, target="copilot", dry_run=True)
+ assert "GitHub Copilot" in configured
+ config_path = tmp_path / ".vscode" / "mcp.json"
+ assert not config_path.exists()
+
+ def test_copilot_writes_only_copilot_instructions(self, tmp_path):
+ """inject_platform_instructions with target='copilot' writes only copilot file."""
+ updated = inject_platform_instructions(tmp_path, target="copilot")
+ assert updated == [".github/code-review-graph.instruction.md"]
+ assert not (tmp_path / "AGENTS.md").exists()
+ assert not (tmp_path / "GEMINI.md").exists()
+ assert not (tmp_path / ".cursorrules").exists()
+ assert not (tmp_path / ".windsurfrules").exists()
+ assert not (tmp_path / "QODER.md").exists()
+
+ def test_copilot_included_in_all_when_detected(self, tmp_path):
+ """install_platform_configs with target='all' includes Copilot when ~/.vscode exists."""
+ fake_home = tmp_path / "fakehome"
+ (fake_home / ".vscode").mkdir(parents=True)
+ with patch("code_review_graph.skills.Path.home", return_value=fake_home):
+ configured = install_platform_configs(tmp_path, target="all")
+ assert "GitHub Copilot" in configured
+ config_path = tmp_path / ".vscode" / "mcp.json"
+ assert config_path.exists()
+
+
+class TestCopilotCLIPlatform:
+ """Tests for GitHub Copilot CLI platform support."""
+
+ def test_copilot_cli_platform_entry_exists(self):
+ """PLATFORMS dict has a 'copilot-cli' key with correct metadata."""
+ assert "copilot-cli" in PLATFORMS
+ copilot_cli = PLATFORMS["copilot-cli"]
+ assert copilot_cli["name"] == "GitHub Copilot CLI"
+ assert copilot_cli["key"] == "servers"
+ assert copilot_cli["format"] == "object"
+ assert copilot_cli["needs_type"] is True
+
+ def test_install_copilot_cli_config(self, tmp_path):
+ """install_platform_configs creates ~/.copilot/mcp-config.json with 'servers' key."""
+ fake_home = tmp_path / "fakehome"
+ (fake_home / ".copilot").mkdir(parents=True)
+ config_path = fake_home / ".copilot" / "mcp-config.json"
+ with patch.dict(
+ PLATFORMS,
+ {
+ "copilot-cli": {
+ **PLATFORMS["copilot-cli"],
+ "config_path": lambda root: config_path,
+ "detect": lambda: True,
+ },
+ },
+ ):
+ configured = install_platform_configs(tmp_path, target="copilot-cli")
+ assert "GitHub Copilot CLI" in configured
+ assert config_path.exists()
+ data = json.loads(config_path.read_text())
+ assert "code-review-graph" in data["servers"]
+ entry = data["servers"]["code-review-graph"]
+ assert entry["type"] == "stdio"
+ assert "serve" in entry["args"]
+
+ def test_install_copilot_cli_preserves_existing_servers(self, tmp_path):
+ """Existing server entries are preserved when adding code-review-graph."""
+ fake_home = tmp_path / "fakehome"
+ config_path = fake_home / ".copilot" / "mcp-config.json"
+ config_path.parent.mkdir(parents=True)
+ config_path.write_text(
+ json.dumps({"servers": {"other-server": {"command": "other"}}}),
+ encoding="utf-8",
+ )
+ with patch.dict(
+ PLATFORMS,
+ {
+ "copilot-cli": {
+ **PLATFORMS["copilot-cli"],
+ "config_path": lambda root: config_path,
+ "detect": lambda: True,
+ },
+ },
+ ):
+ install_platform_configs(tmp_path, target="copilot-cli")
+ data = json.loads(config_path.read_text())
+ assert "other-server" in data["servers"]
+ assert "code-review-graph" in data["servers"]
+
+ def test_install_copilot_cli_no_duplicate(self, tmp_path):
+ """Second install skips when code-review-graph already exists."""
+ fake_home = tmp_path / "fakehome"
+ config_path = fake_home / ".copilot" / "mcp-config.json"
+ with patch.dict(
+ PLATFORMS,
+ {
+ "copilot-cli": {
+ **PLATFORMS["copilot-cli"],
+ "config_path": lambda root: config_path,
+ "detect": lambda: True,
+ },
+ },
+ ):
+ install_platform_configs(tmp_path, target="copilot-cli")
+ first_content = config_path.read_text()
+ install_platform_configs(tmp_path, target="copilot-cli")
+ second_content = config_path.read_text()
+ assert first_content == second_content
+ data = json.loads(second_content)
+ assert list(data["servers"].keys()).count("code-review-graph") == 1
+
+ def test_copilot_cli_injects_copilot_instructions(self, tmp_path):
+ """inject_platform_instructions with target='copilot-cli' writes .github/code-review-graph.instruction.md."""
+ updated = inject_platform_instructions(tmp_path, target="copilot-cli")
+ assert ".github/code-review-graph.instruction.md" in updated
+ instructions = tmp_path / ".github" / "code-review-graph.instruction.md"
+ assert instructions.exists()
+ content = instructions.read_text()
+ assert _CLAUDE_MD_SECTION_MARKER in content
+
+ def test_copilot_cli_dry_run(self, tmp_path):
+ """dry_run=True does not create any files."""
+ fake_home = tmp_path / "fakehome"
+ config_path = fake_home / ".copilot" / "mcp-config.json"
+ with patch.dict(
+ PLATFORMS,
+ {
+ "copilot-cli": {
+ **PLATFORMS["copilot-cli"],
+ "config_path": lambda root: config_path,
+ "detect": lambda: True,
+ },
+ },
+ ):
+ configured = install_platform_configs(tmp_path, target="copilot-cli", dry_run=True)
+ assert "GitHub Copilot CLI" in configured
+ assert not config_path.exists()
+
+ def test_copilot_cli_included_in_all_when_detected(self, tmp_path):
+ """install_platform_configs with target='all' includes Copilot CLI when ~/.copilot exists."""
+ fake_home = tmp_path / "fakehome"
+ (fake_home / ".copilot").mkdir(parents=True)
+ config_path = fake_home / ".copilot" / "mcp-config.json"
+ with patch.dict(
+ PLATFORMS,
+ {
+ "copilot-cli": {
+ **PLATFORMS["copilot-cli"],
+ "config_path": lambda root: config_path,
+ "detect": lambda: True,
+ },
+ "copilot": {**PLATFORMS["copilot"], "detect": lambda: False},
+ },
+ ):
+ configured = install_platform_configs(tmp_path, target="all")
+ assert "GitHub Copilot CLI" in configured
+ assert config_path.exists()
+
+ def test_copilot_cli_user_level_config_not_in_project(self, tmp_path):
+ """Copilot CLI config is user-level (not inside the repo root)."""
+ config_path = PLATFORMS["copilot-cli"]["config_path"](tmp_path)
+ assert not str(config_path).startswith(str(tmp_path)), (
+ "copilot-cli config should be user-level (~/.copilot/), not project-level"
+ )
+
+
class TestDetectServeCommand:
"""Tests for _detect_serve_command() and its helpers."""
diff --git a/uv.lock b/uv.lock
index 62a32add..c40535b3 100644
--- a/uv.lock
+++ b/uv.lock
@@ -411,6 +411,7 @@ requires-dist = [
{ name = "pyyaml", marker = "extra == 'eval'", specifier = ">=6.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0,<1" },
{ name = "sentence-transformers", marker = "extra == 'embeddings'", specifier = ">=3.0.0,<4" },
+ { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.0,<3" },
{ name = "tomli", marker = "python_full_version < '3.11' and extra == 'dev'", specifier = ">=2.0" },
{ name = "tree-sitter", specifier = ">=0.23.0,<1" },
{ name = "tree-sitter-language-pack", specifier = ">=0.3.0,<1" },