diff --git a/.github/workflows/codegraph-pr.yml b/.github/workflows/codegraph-pr.yml
index 9b4abef..6754ea8 100644
--- a/.github/workflows/codegraph-pr.yml
+++ b/.github/workflows/codegraph-pr.yml
@@ -35,12 +35,25 @@ jobs:
run: |
# Resolve the npm-installed binary for this platform
BIN="$(npm root -g)/@astudioplus/codegraph-mcp/bin/codegraph-server-linux-x64"
- chmod +x "$BIN"
- "$BIN" \
- --graph-only \
- --run-tool codegraph_pr_context \
- --tool-args "{\"baseBranch\":\"${{ github.base_ref }}\",\"format\":\"markdown\"}" \
- > review.md 2>/dev/null || echo "CodeGraph review failed" > review.md
+ # stderr → log (visible in the Actions run); stdout → comment body
+ # Use origin/ — in a PR checkout the base branch exists as
+ # a remote-tracking ref, not a local branch. (codegraph 0.17.6+
+ # auto-resolves this, but origin/ works on all versions.)
+ if "$BIN" \
+ --graph-only \
+ --run-tool codegraph_pr_context \
+ --tool-args "{\"baseBranch\":\"origin/${{ github.base_ref }}\",\"format\":\"markdown\"}" \
+ > review.md 2> review.err; then
+ echo "CodeGraph review succeeded"
+ else
+ echo "::warning::CodeGraph review failed — see stderr below"
+ cat review.err
+ {
+ echo "## 🔍 CodeGraph PR Review"
+ echo ""
+ echo "Review could not be generated for this PR. (CodeGraph)"
+ } > review.md
+ fi
- name: Post or update PR comment
uses: actions/github-script@v7
diff --git a/crates/codegraph-server/src/mcp/server.rs b/crates/codegraph-server/src/mcp/server.rs
index 4bd7fa3..77b15ea 100644
--- a/crates/codegraph-server/src/mcp/server.rs
+++ b/crates/codegraph-server/src/mcp/server.rs
@@ -11,6 +11,8 @@
/// happen in the Rust binary — the JS wrapper handles PostHog ingestion.
///
/// Silently dropped if serialization fails (never blocks the server).
+/// The wrapper parses these lines from stderr; stdout stays reserved for
+/// the JSON-RPC channel.
fn emit_tel(value: serde_json::Value) {
if let Ok(json) = serde_json::to_string(&value) {
eprintln!("TEL: {json}");
@@ -3339,6 +3341,36 @@ impl McpServer {
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| ".".to_string());
+ // Resolve the base ref. In CI (GitHub Actions etc.) the
+ // checkout is a detached HEAD and the base branch only
+ // exists as a remote-tracking ref (origin/main), not a
+ // local branch. Try the bare ref first (works locally),
+ // then fall back to origin/[ (works in CI).
+ let base = {
+ let probe = tokio::process::Command::new("git")
+ .args(["rev-parse", "--verify", "--quiet", base])
+ .current_dir(&workspace_root)
+ .output()
+ .await;
+ let bare_ok = probe.map(|o| o.status.success()).unwrap_or(false);
+ if bare_ok {
+ base.to_string()
+ } else {
+ let remote = format!("origin/{}", base);
+ let probe2 = tokio::process::Command::new("git")
+ .args(["rev-parse", "--verify", "--quiet", &remote])
+ .current_dir(&workspace_root)
+ .output()
+ .await;
+ if probe2.map(|o| o.status.success()).unwrap_or(false) {
+ remote
+ } else {
+ base.to_string() // let the diff surface the error
+ }
+ }
+ };
+ let base = base.as_str();
+
// ── Step 1: git diff --name-only for file list ──
let name_output = tokio::process::Command::new("git")
.args(["diff", "--name-only", &format!("{}...HEAD", base)])
]