From 6225e87ed0a64a771d96132b5302e8697d0663d1 Mon Sep 17 00:00:00 2001
From: rztao
Date: Sun, 12 Apr 2026 14:08:32 +0800
Subject: [PATCH] feat: add Qoder platform support
Add Qoder as a supported platform alongside existing AI coding tools.
This includes:
- Platform config for Qoder (mcp.json path, hooks support)
- install_qoder_skills() to copy skills to .qoder/skills/
- QODER.md instruction injection support
- Qoder hooks installation (settings.json)
- Updated platform list in docs and diagrams
- New skills: debug-issue, explore-codebase, refactor-safely, review-changes
The implementation follows the same pattern as existing platforms
(claude, cursor, windsurf, etc.) for consistency.
---
.gitignore | 4 +++
CLAUDE.md | 39 +++++++++++++++++++++
README.md | 4 +--
code_review_graph/cli.py | 31 +++++++++++------
code_review_graph/skills.py | 59 ++++++++++++++++++++++++++++++--
diagrams/generate_diagrams.py | 6 ++--
docs/USAGE.md | 5 ++-
skills/debug-issue/SKILL.md | 27 +++++++++++++++
skills/explore-codebase/SKILL.md | 28 +++++++++++++++
skills/refactor-safely/SKILL.md | 28 +++++++++++++++
skills/review-changes/SKILL.md | 29 ++++++++++++++++
tests/test_skills.py | 57 ++++++++++++++++++++++++++++--
12 files changed, 296 insertions(+), 21 deletions(-)
create mode 100644 skills/debug-issue/SKILL.md
create mode 100644 skills/explore-codebase/SKILL.md
create mode 100644 skills/refactor-safely/SKILL.md
create mode 100644 skills/review-changes/SKILL.md
diff --git a/.gitignore b/.gitignore
index 5036fd6..333995b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,10 @@ code-review-graph-vscode/dist/
.claude/
.claude-plugin/
+# Qoder
+.qoder/
+QODER.md
+
# Coverage
htmlcov/
.coverage
diff --git a/CLAUDE.md b/CLAUDE.md
index 682cfea..a9adb7a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -113,3 +113,42 @@ uv run code-review-graph eval # Run evaluation benchmarks
- **type-check**: mypy
- **security**: bandit scan
- **test**: pytest matrix (3.10, 3.11, 3.12, 3.13) with 50% coverage minimum
+
+
+## MCP Tools: code-review-graph
+
+**IMPORTANT: This project has a knowledge graph. ALWAYS use the
+code-review-graph MCP tools BEFORE using Grep/Glob/Read 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 31760d9..80e33cc 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,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:
@@ -331,5 +331,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, and Antigravity
+Works with Codex, Claude Code, Cursor, Windsurf, Zed, Continue, OpenCode, Antigravity, Qwen, and Qoder
diff --git a/code_review_graph/cli.py b/code_review_graph/cli.py
index 4598861..4a730e0 100644
--- a/code_review_graph/cli.py
+++ b/code_review_graph/cli.py
@@ -38,6 +38,12 @@
logger = logging.getLogger(__name__)
+# Shared platform choices for install and init commands
+_PLATFORM_CHOICES = [
+ "codex", "claude", "claude-code", "cursor", "windsurf", "zed",
+ "continue", "opencode", "antigravity", "qwen", "qoder", "all",
+]
+
def _get_version() -> str:
"""Get the installed package version."""
@@ -211,6 +217,7 @@ def _handle_init(args: argparse.Namespace) -> None:
inject_platform_instructions,
install_git_hook,
install_hooks,
+ install_qoder_skills,
)
if not skip_skills:
@@ -237,9 +244,17 @@ def _handle_init(args: argparse.Namespace) -> None:
elif skip_instructions:
print("Skipped instruction injection (--no-instructions).")
- if not skip_hooks and target in ("claude", "all"):
- install_hooks(repo_root)
- print(f"Installed hooks in {repo_root / '.claude' / 'settings.json'}")
+
+ # Install Qoder skills (global user-level skills directory)
+ if not skip_skills and target in ("qoder", "all"):
+ qoder_skills_dir = install_qoder_skills(repo_root)
+ if qoder_skills_dir:
+ print(f"Installed Qoder skills to {qoder_skills_dir}")
+ if not skip_hooks and target in ("claude", "qoder", "all"):
+ platforms_to_install = [target] if target != "all" else ["claude", "qoder"]
+ for plat in platforms_to_install:
+ install_hooks(repo_root, platform=plat)
+ print(f"Installed hooks in {repo_root / f'.{plat}' / 'settings.json'}")
git_hook = install_git_hook(repo_root)
if git_hook:
print(f"Installed git pre-commit hook in {git_hook}")
@@ -293,10 +308,7 @@ def main() -> None:
help=argparse.SUPPRESS)
install_cmd.add_argument(
"--platform",
- choices=[
- "codex", "claude", "claude-code", "cursor", "windsurf", "zed",
- "continue", "opencode", "antigravity", "qwen", "all",
- ],
+ choices=_PLATFORM_CHOICES,
default="all",
help="Target platform for MCP config (default: all detected)",
)
@@ -331,10 +343,7 @@ def main() -> None:
help=argparse.SUPPRESS)
init_cmd.add_argument(
"--platform",
- choices=[
- "codex", "claude", "claude-code", "cursor", "windsurf", "zed",
- "continue", "opencode", "antigravity", "qwen", "all",
- ],
+ choices=_PLATFORM_CHOICES,
default="all",
help="Target platform for MCP config (default: all detected)",
)
diff --git a/code_review_graph/skills.py b/code_review_graph/skills.py
index fe3abab..72fcb46 100644
--- a/code_review_graph/skills.py
+++ b/code_review_graph/skills.py
@@ -100,6 +100,14 @@ def _zed_settings_path() -> Path:
"format": "object",
"needs_type": True,
},
+ "qoder": {
+ "name": "Qoder",
+ "config_path": lambda root: root / ".qoder" / "mcp.json",
+ "key": "mcpServers",
+ "detect": lambda: True,
+ "format": "object",
+ "needs_type": True,
+ },
}
@@ -481,16 +489,20 @@ def install_git_hook(repo_root: Path) -> Path | None:
return hook_path
-def install_hooks(repo_root: Path) -> None:
- """Write hooks config to .claude/settings.json.
+def install_hooks(repo_root: Path, platform: str = "claude") -> None:
+ """Write hooks config to platform-specific settings.json.
Merges with existing settings if present, preserving non-hook
configuration.
Args:
repo_root: Repository root directory.
+ platform: Target platform ("claude" or "qoder").
"""
- settings_dir = repo_root / ".claude"
+ if platform == "qoder":
+ settings_dir = repo_root / ".qoder"
+ else:
+ settings_dir = repo_root / ".claude"
settings_dir.mkdir(parents=True, exist_ok=True)
settings_path = settings_dir / "settings.json"
@@ -591,6 +603,7 @@ def inject_claude_md(repo_root: Path) -> None:
"GEMINI.md": ("antigravity",),
".cursorrules": ("cursor",),
".windsurfrules": ("windsurf",),
+ "QODER.md": ("qoder",),
}
@@ -615,3 +628,43 @@ def inject_platform_instructions(repo_root: Path, target: str = "all") -> list[s
if _inject_instructions(path, _CLAUDE_MD_SECTION_MARKER, _CLAUDE_MD_SECTION):
updated.append(filename)
return updated
+
+
+def install_qoder_skills(repo_root: Path) -> Path | None:
+ """Install skills to Qoder's project-level skills directory.
+
+ Qoder expects skills in .qoder/skills/{skillName}/SKILL.md format within the project.
+ This function copies the project's skills/ directory contents to that location.
+
+ Args:
+ repo_root: Repository root directory (where the skills/ folder is located).
+
+ Returns:
+ Path to the Qoder skills directory, or None if installation failed.
+ """
+ # Qoder skills directory (project-level)
+ qoder_skills_dir = repo_root / ".qoder" / "skills"
+ qoder_skills_dir.mkdir(parents=True, exist_ok=True)
+
+ # Source skills directory in the project
+ source_skills_dir = repo_root / "skills"
+ if not source_skills_dir.exists():
+ logger.warning("No skills/ directory found in %s", repo_root)
+ return None
+
+ installed_count = 0
+ for skill_dir in source_skills_dir.iterdir():
+ if skill_dir.is_dir():
+ skill_file = skill_dir / "SKILL.md"
+ if skill_file.exists():
+ target_dir = qoder_skills_dir / skill_dir.name
+ target_dir.mkdir(parents=True, exist_ok=True)
+ target_file = target_dir / "SKILL.md"
+ target_file.write_text(skill_file.read_text(encoding="utf-8"), encoding="utf-8")
+ logger.info("Installed Qoder skill: %s", skill_dir.name)
+ installed_count += 1
+
+ if installed_count > 0:
+ logger.info("Installed %d skill(s) to %s", installed_count, qoder_skills_dir)
+ return qoder_skills_dir
+ return None
diff --git a/diagrams/generate_diagrams.py b/diagrams/generate_diagrams.py
index 5d577d3..baf9fa7 100644
--- a/diagrams/generate_diagrams.py
+++ b/diagrams/generate_diagrams.py
@@ -595,6 +595,8 @@ def d8():
("Continue", "~/.continue/config.json", YLW, YLW_BG),
("OpenCode", ".opencode.json", RED, RED_BG),
("Antigravity", "~/.gemini/antigravity/mcp_config.json",GRY, GRY_BG),
+ ("Qwen Code", "~/.qwen/settings.json", BLU, PRP_BG), # blue-purple
+ ("Qoder", ".qoder/mcp.json", GRN, ORG_BG), # green-orange
]
# Central "install" node
@@ -604,8 +606,8 @@ def d8():
# Fan out to platforms
cols = len(platforms)
- card_w, card_h = 140, 80
- total_w = cols * card_w + (cols-1) * 20
+ card_w, card_h = 120, 80 # narrower cards for 9 platforms
+ total_w = cols * card_w + (cols-1) * 15 # tighter spacing
x0 = center_x - total_w/2
card_y = 360
diff --git a/docs/USAGE.md b/docs/USAGE.md
index b521f08..e2fa865 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -30,7 +30,10 @@ code-review-graph install --platform claude-code
| **Windsurf** | `.windsurf/mcp.json` |
| **Zed** | `.zed/settings.json` |
| **Continue** | `.continue/config.json` |
-| **OpenCode** | `.opencode/config.json` |
+| **OpenCode** | `.opencode.json` |
+| **Antigravity** | `~/.gemini/antigravity/mcp_config.json` |
+| **Qwen Code** | `~/.qwen/settings.json` |
+| **Qoder** | `.qoder/mcp.json` |
## Core Workflow
diff --git a/skills/debug-issue/SKILL.md b/skills/debug-issue/SKILL.md
new file mode 100644
index 0000000..ef1b38a
--- /dev/null
+++ b/skills/debug-issue/SKILL.md
@@ -0,0 +1,27 @@
+---
+name: Debug Issue
+description: Systematically debug issues using graph-powered code navigation
+---
+
+## Debug Issue
+
+Use the knowledge graph to systematically trace and debug issues.
+
+### Steps
+
+1. Use `semantic_search_nodes` to find code related to the issue.
+2. Use `query_graph` with `callers_of` and `callees_of` to trace call chains.
+3. Use `get_flow` to see full execution paths through suspected areas.
+4. Run `detect_changes` to check if recent changes caused the issue.
+5. Use `get_impact_radius` on suspected files to see what else is affected.
+
+### Tips
+
+- Check both callers and callees to understand the full context.
+- Look at affected flows to find the entry point that triggers the bug.
+- Recent changes are the most common source of new issues.
+
+## Token Efficiency Rules
+- ALWAYS start with `get_minimal_context(task="")` before any other graph tool.
+- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
+- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
diff --git a/skills/explore-codebase/SKILL.md b/skills/explore-codebase/SKILL.md
new file mode 100644
index 0000000..dc7ad10
--- /dev/null
+++ b/skills/explore-codebase/SKILL.md
@@ -0,0 +1,28 @@
+---
+name: Explore Codebase
+description: Navigate and understand codebase structure using the knowledge graph
+---
+
+## Explore Codebase
+
+Use the code-review-graph MCP tools to explore and understand the codebase.
+
+### Steps
+
+1. Run `list_graph_stats` to see overall codebase metrics.
+2. Run `get_architecture_overview` for high-level community structure.
+3. Use `list_communities` to find major modules, then `get_community` for details.
+4. Use `semantic_search_nodes` to find specific functions or classes.
+5. Use `query_graph` with patterns like `callers_of`, `callees_of`, `imports_of` to trace relationships.
+6. Use `list_flows` and `get_flow` to understand execution paths.
+
+### Tips
+
+- Start broad (stats, architecture) then narrow down to specific areas.
+- Use `children_of` on a file to see all its functions and classes.
+- Use `find_large_functions` to identify complex code.
+
+## Token Efficiency Rules
+- ALWAYS start with `get_minimal_context(task="")` before any other graph tool.
+- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
+- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
diff --git a/skills/refactor-safely/SKILL.md b/skills/refactor-safely/SKILL.md
new file mode 100644
index 0000000..cf84420
--- /dev/null
+++ b/skills/refactor-safely/SKILL.md
@@ -0,0 +1,28 @@
+---
+name: Refactor Safely
+description: Plan and execute safe refactoring using dependency analysis
+---
+
+## Refactor Safely
+
+Use the knowledge graph to plan and execute refactoring with confidence.
+
+### Steps
+
+1. Use `refactor_tool` with mode="suggest" for community-driven refactoring suggestions.
+2. Use `refactor_tool` with mode="dead_code" to find unreferenced code.
+3. For renames, use `refactor_tool` with mode="rename" to preview all affected locations.
+4. Use `apply_refactor_tool` with the refactor_id to apply renames.
+5. After changes, run `detect_changes` to verify the refactoring impact.
+
+### Safety Checks
+
+- Always preview before applying (rename mode gives you an edit list).
+- Check `get_impact_radius` before major refactors.
+- Use `get_affected_flows` to ensure no critical paths are broken.
+- Run `find_large_functions` to identify decomposition targets.
+
+## Token Efficiency Rules
+- ALWAYS start with `get_minimal_context(task="")` before any other graph tool.
+- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
+- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
diff --git a/skills/review-changes/SKILL.md b/skills/review-changes/SKILL.md
new file mode 100644
index 0000000..6bb3558
--- /dev/null
+++ b/skills/review-changes/SKILL.md
@@ -0,0 +1,29 @@
+---
+name: Review Changes
+description: Perform a structured code review using change detection and impact
+---
+
+## Review Changes
+
+Perform a thorough, risk-aware code review using the knowledge graph.
+
+### Steps
+
+1. Run `detect_changes` to get risk-scored change analysis.
+2. Run `get_affected_flows` to find impacted execution paths.
+3. For each high-risk function, run `query_graph` with pattern="tests_for" to check test coverage.
+4. Run `get_impact_radius` to understand the blast radius.
+5. For any untested changes, suggest specific test cases.
+
+### Output Format
+
+Provide findings grouped by risk level (high/medium/low) with:
+- What changed and why it matters
+- Test coverage status
+- Suggested improvements
+- Overall merge recommendation
+
+## Token Efficiency Rules
+- ALWAYS start with `get_minimal_context(task="")` before any other graph tool.
+- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
+- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
diff --git a/tests/test_skills.py b/tests/test_skills.py
index 15c5eeb..a922dc9 100644
--- a/tests/test_skills.py
+++ b/tests/test_skills.py
@@ -181,6 +181,27 @@ def test_creates_claude_directory(self, tmp_path):
install_hooks(tmp_path)
assert (tmp_path / ".claude").is_dir()
+ def test_install_qoder_hooks(self, tmp_path):
+ install_hooks(tmp_path, platform="qoder")
+ settings_path = tmp_path / ".qoder" / "settings.json"
+ assert settings_path.exists()
+ data = json.loads(settings_path.read_text())
+ assert "hooks" in data
+ assert "PostToolUse" in data["hooks"]
+ assert "SessionStart" in data["hooks"]
+
+ def test_install_qoder_hooks_merges_existing(self, tmp_path):
+ settings_dir = tmp_path / ".qoder"
+ settings_dir.mkdir(parents=True)
+ existing = {"customSetting": True}
+ (settings_dir / "settings.json").write_text(json.dumps(existing))
+
+ install_hooks(tmp_path, platform="qoder")
+
+ data = json.loads((settings_dir / "settings.json").read_text())
+ assert data["customSetting"] is True
+ assert "hooks" in data
+
class TestInjectClaudeMd:
def test_creates_section_in_new_file(self, tmp_path):
@@ -228,11 +249,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"}
+ assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules", "QODER.md"}
def test_default_is_all(self, tmp_path):
updated = inject_platform_instructions(tmp_path)
- assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules"}
+ assert set(updated) == {"AGENTS.md", "GEMINI.md", ".cursorrules", ".windsurfrules", "QODER.md"}
def test_claude_writes_nothing(self, tmp_path):
updated = inject_platform_instructions(tmp_path, target="claude")
@@ -241,12 +262,14 @@ def test_claude_writes_nothing(self, tmp_path):
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_cursor_writes_only_cursor_files(self, tmp_path):
updated = inject_platform_instructions(tmp_path, target="cursor")
assert set(updated) == {"AGENTS.md", ".cursorrules"}
assert not (tmp_path / "GEMINI.md").exists()
assert not (tmp_path / ".windsurfrules").exists()
+ assert not (tmp_path / "QODER.md").exists()
def test_windsurf_writes_only_windsurfrules(self, tmp_path):
updated = inject_platform_instructions(tmp_path, target="windsurf")
@@ -260,6 +283,14 @@ def test_opencode_writes_only_agents(self, tmp_path):
updated = inject_platform_instructions(tmp_path, target="opencode")
assert updated == ["AGENTS.md"]
+ def test_qoder_writes_only_qoder_md(self, tmp_path):
+ updated = inject_platform_instructions(tmp_path, target="qoder")
+ assert updated == ["QODER.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()
+
class TestInstallPlatformConfigs:
def test_install_codex_config(self, tmp_path):
@@ -534,3 +565,25 @@ def test_continue_array_no_duplicate(self, tmp_path):
install_platform_configs(tmp_path, target="continue")
data = json.loads(config_path.read_text())
assert len(data["mcpServers"]) == 1
+
+ def test_install_qoder_config(self, tmp_path):
+ qoder_config = tmp_path / ".qoder" / "mcp.json"
+ with patch.dict(
+ PLATFORMS,
+ {
+ "qoder": {
+ **PLATFORMS["qoder"],
+ "config_path": lambda root: qoder_config,
+ "detect": lambda: True,
+ },
+ },
+ ):
+ configured = install_platform_configs(tmp_path, target="qoder")
+ assert "Qoder" in configured
+ data = json.loads(qoder_config.read_text())
+ assert "mcpServers" in data
+ assert "code-review-graph" in data["mcpServers"]
+ assert data["mcpServers"]["code-review-graph"]["type"] == "stdio"
+ import shutil
+ expected_cmd = "uvx" if shutil.which("uvx") else "code-review-graph"
+ assert data["mcpServers"]["code-review-graph"]["command"] == expected_cmd