You are a senior code reviewer preparing to review code changes.

## Code Changes

```diff
diff --git a/ftl_project_expert/cli.py b/ftl_project_expert/cli.py
index 1410f17..9c061ce 100644
--- a/ftl_project_expert/cli.py
+++ b/ftl_project_expert/cli.py
@@ -175,21 +175,23 @@ def cli(ctx, quiet, model, timeout):
 
 
 # --- init ---
 
 
 @cli.command()
 @click.argument("platform", type=click.Choice(["github", "gitlab", "jira"]))
 @click.argument("target", type=str)
 @click.option("--domain", "-d", default=None, help="One-line project description")
 @click.option("--jira-url", default=None, help="Jira base URL (for jira platform)")
-def init(platform, target, domain, jira_url):
+@click.option("--github-repo", default=None,
+              help="GitHub owner/repo for cross-platform PR lookups (e.g., owner/repo)")
+def init(platform, target, domain, jira_url, github_repo):
     """Bootstrap a project-expert knowledge base.
 
     TARGET is owner/repo for GitHub/GitLab, or project key for Jira.
 
     Examples:
         project-expert init github owner/repo
         project-expert init gitlab group/project
         project-expert init jira MYPROJ --jira-url https://myco.atlassian.net
     """
     if not domain:
@@ -225,20 +227,23 @@ def init(platform, target, domain, jira_url):
         "platform": platform,
         "domain": domain,
         "created": date.today().isoformat(),
     }
     if platform in ("github", "gitlab"):
         config["repo"] = target
     else:
         config["project"] = target
         config["jira_url"] = jira_url or os.environ.get("JIRA_URL", "")
 
+    if github_repo:
+        config["github_repo"] = github_repo
+
     _save_config(config)
 
     # Create entries dir
     Path("entries").mkdir(exist_ok=True)
 
     # Init belief store
     if _has_reasons():
         if not Path("reasons.db").exists():
             subprocess.run(["reasons", "init"], capture_output=True)
             click.echo("Initialized reasons.db")
@@ -1376,37 +1381,44 @@ def _extract_issue_refs(text: str) -> list[dict]:
         jira_key = match.group(1)
         if jira_key.startswith(("GH-", "PR-", "GL-", "MR-")):
             continue
         if jira_key not in seen:
             seen.add(jira_key)
             refs.append({"type": "issue", "key": jira_key})
 
     return refs
 
 
-def _fetch_artifacts(refs: list[dict], source, config: dict) -> str:
+def _fetch_artifacts(refs: list[dict], source, config: dict,
+                     github_source=None) -> str:
     """Fetch current state for each issue/PR reference."""
     parts = []
     platform = config["platform"]
 
     for ref in refs:
         ref_label = ref.get("key") or f"#{ref.get('number')}"
         try:
             if "key" in ref:
                 if platform != "jira":
                     parts.append(f"(Skipping Jira key {ref['key']} on {platform})")
                     continue
                 issue = source.get_issue(ref["key"])
                 parts.append(issue.to_prompt_text())
-            elif ref["type"] == "pr" and hasattr(source, "get_pr"):
-                pr = source.get_pr(ref["number"])
-                parts.append(pr.to_prompt_text())
+            elif ref["type"] == "pr":
+                if hasattr(source, "get_pr"):
+                    pr = source.get_pr(ref["number"])
+                    parts.append(pr.to_prompt_text())
+                elif github_source:
+                    pr = github_source.get_pr(ref["number"])
+                    parts.append(pr.to_prompt_text())
+                else:
+                    parts.append(f"(Could not fetch pr {ref_label} — no GitHub repo configured)")
             elif ref["type"] == "issue":
                 issue = source.get_issue(ref["number"])
                 parts.append(issue.to_prompt_text())
             else:
                 parts.append(f"(Could not fetch {ref['type']} {ref_label})")
         except Exception as e:
             parts.append(f"(Error fetching {ref['type']} {ref_label}: {e})")
 
     return "\n\n---\n\n".join(parts) if parts else "(No artifacts fetched)"
 
@@ -1506,20 +1518,26 @@ def research(ctx, belief_id, negative, high_impact, select_limit, max_parallel):
         if not belief_ids:
             label = "negative " if negative else ""
             click.echo(f"No {label}beliefs found to research.")
             return
         click.echo(f"Selected {len(belief_ids)} belief(s) for research:", err=True)
         for bid in belief_ids:
             click.echo(f"  {bid}", err=True)
 
     source = _get_source(config)
 
+    github_source = None
+    gh_repo = config.get("github_repo")
+    if gh_repo and config["platform"] != "github":
+        github_source = GitHubSource(gh_repo)
+        click.echo(f"Using GitHub repo {gh_repo} for PR lookups", err=True)
+
     def _research_one(bid: str) -> str | None:
         """Build a research prompt for a single belief."""
         info = _get_belief_info(bid)
         if not info:
             click.echo(f"Belief not found: {bid}", err=True)
             return None
 
         click.echo(f"\nResearching: {bid}", err=True)
         click.echo(f"  Claim: {info['text'][:100]}", err=True)
         click.echo(f"  Status: {info['status']}", err=True)
@@ -1534,21 +1552,21 @@ def research(ctx, belief_id, negative, high_impact, select_limit, max_parallel):
                 if len(content) > 15000:
                     content = content[:15000] + "\n[Truncated]"
                 source_entry = content
                 click.echo(f"  Source: {source_path}", err=True)
 
         # Extract and fetch artifacts
         all_text = f"{info['text']}\n{source_entry}"
         refs = _extract_issue_refs(all_text)
         click.echo(f"  Found {len(refs)} reference(s)", err=True)
 
-        artifacts = _fetch_artifacts(refs, source, config)
+        artifacts = _fetch_artifacts(refs, source, config, github_source=github_source)
 
         # Get dependent beliefs
         dependents = _get_dependent_beliefs(bid, network)
         dep_count = len(info.get("dependents", []))
         if dep_count:
             click.echo(f"  {dep_count} dependent belief(s)", err=True)
 
         return RESEARCH_PROMPT.format(
             belief_id=bid,
             belief_text=info["text"],
@@ -2033,20 +2051,22 @@ def status():
     """Show project-expert dashboard."""
     config = _load_config()
 
     click.echo("=== Project Expert Status ===\n")
 
     if config:
         click.echo(f"Platform: {config.get('platform', 'unknown')}")
         click.echo(f"Target:   {config.get('repo', config.get('project', 'unknown'))}")
         click.echo(f"Domain:   {config.get('domain', 'unknown')}")
         click.echo(f"Created:  {config.get('created', 'unknown')}")
+        if config.get("github_repo"):
+            click.echo(f"GitHub:   {config['github_repo']}")
     else:
         click.echo("Not initialized. Run: project-expert init <platform> <target>")
         return
 
     click.echo()
 
     # Entries
     entries_dir = Path("entries")
     entry_count = len(list(entries_dir.rglob("*.md"))) if entries_dir.exists() else 0
     click.echo(f"Entries:  {entry_count}")

```

## Your Task

Analyze the diff and identify what additional information you need to render confident verdicts.
Do NOT render verdicts yet. Only request observations.

## Available Observation Tools

| Tool | Purpose | When to use |
|------|---------|-------------|
| `exception_hierarchy` | Show exception MRO and subclasses | Retry logic, exception handling |
| `raises_analysis` | What exceptions a function raises | New function calls, error paths |
| `call_graph` | What a function calls | Impact analysis |
| `find_usages` | Where a symbol is used (with prod/test split) | Quick integration lookup |
| `find_callers` | Caller analysis with prod/test split and calling context | Method signature changes, return type changes, constructor modifications, integration verification |
| `test_coverage` | Find tests for a file (uses coverage-map if available) | Test coverage claims |
| `coverage_map_tests` | Find tests covering a file (from coverage-map.json) | Precise test coverage from actual execution |
| `coverage_map_files` | Find files covered by tests matching a pattern | Impact analysis for test changes |
| `function_body` | Full source of a function/method | Need complete function context beyond diff hunks |
| `file_imports` | Extract imports from a file | Verify import changes, check dependencies |
| `project_dependencies` | Get pyproject.toml/requirements.txt | Verify new imports have dependencies |
| `related_test_files` | Find test files for a source file | Discover tests by naming, imports, and coverage map |
| `class_hierarchy` | Show base classes and their `__init__` signatures | Class changes its parent, modifies `__init__`, or uses `super()` |
| `symbol_migration` | Check if a rename is complete across the repo | Symbol renamed in diff — verify old name is fully removed |
| `generator_info` | Report whether a function uses `yield` | Function might be a generator — affects return value semantics |

## What to Look For

1. **Exception handling**: Any `retry_if_exception_type`, `except`, or exception class references
2. **New dependencies**: Calls to external libraries where you don't know the error behavior
3. **Behavioral changes**: Modified logic where you need to verify callers/callees
4. **Test claims**: References to tests you can't see in the diff
5. **Inheritance changes**: Class definition changes, new base classes, `super()` calls
6. **Renames**: Symbols that appear to have been renamed in the diff
7. **Factory methods**: Calls to `@classmethod` / `@staticmethod` constructors (e.g. `Result.error(...)`) — request `function_body` to see their implementation

## Output Format

Output a JSON array of observation requests:

```json
[
  {"name": "descriptive_name", "tool": "tool_name", "params": {"param": "value"}},
  ...
]
```

If you don't need any observations (simple changes, all context is in the diff), output:

```json
[]
```

## Examples

For a diff containing `retry_if_exception_type((OSError, httpx.TransportError))`:
```json
[
  {"name": "oserror_subclasses", "tool": "exception_hierarchy", "params": {"class_name": "builtins.OSError"}},
  {"name": "transport_errors", "tool": "exception_hierarchy", "params": {"class_name": "httpx.TransportError"}}
]
```

For a diff adding a new function that calls `oauth_client.get_access_token()`:
```json
[
  {"name": "oauth_exceptions", "tool": "raises_analysis", "params": {"file_path": "src/auth/oauth.py", "function_name": "get_access_token"}}
]
```

For a diff modifying a method but you need the full function to verify:
```json
[
  {"name": "full_getattr", "tool": "function_body", "params": {"file_path": "src/proxy.py", "function_name": "__getattr__"}}
]
```

For a diff changing a method signature or return type (verify all callers):
```json
[
  {"name": "handle_request_callers", "tool": "find_callers", "params": {"symbol": "handle_request"}}
]
```

For a diff adding new imports (e.g., `import httpx`):
```json
[
  {"name": "file_imports", "tool": "file_imports", "params": {"file_path": "src/client.py"}},
  {"name": "project_deps", "tool": "project_dependencies", "params": {}}
]
```

For a diff calling a factory method like `ModuleResult.error_result(msg)`:
```json
[
  {"name": "error_result_body", "tool": "function_body", "params": {"file_path": "src/models.py", "function_name": "error_result"}}
]
```

For a diff where a class changes its parent class:
```json
[
  {"name": "client_hierarchy", "tool": "class_hierarchy", "params": {"class_name": "MyClient", "file_path": "src/client.py"}}
]
```

For a diff that renames a symbol (e.g., `OldClient` to `NewClient`):
```json
[
  {"name": "client_rename", "tool": "symbol_migration", "params": {"old_name": "OldClient", "new_name": "NewClient"}}
]
```

For a diff modifying a function that might be a generator:
```json
[
  {"name": "process_gen", "tool": "generator_info", "params": {"file_path": "src/pipeline.py", "function_name": "process_items"}}
]
```

Now analyze the diff above and output your observation requests as JSON:
