You are a senior code reviewer. Review the following code changes.

## Specification

No specification provided. Focus on correctness, tests, and integration.





## 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}")

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "ftl_project_expert/cli.py:init": {
    "function": "init",
    "file": "ftl_project_expert/cli.py",
    "start_line": 180,
    "end_line": 260,
    "source": "@cli.command()\n@click.argument(\"platform\", type=click.Choice([\"github\", \"gitlab\", \"jira\"]))\n@click.argument(\"target\", type=str)\n@click.option(\"--domain\", \"-d\", default=None, help=\"One-line project description\")\n@click.option(\"--jira-url\", default=None, help=\"Jira base URL (for jira platform)\")\n@click.option(\"--github-repo\", default=None,\n              help=\"GitHub owner/repo for cross-platform PR lookups (e.g., owner/repo)\")\ndef init(platform, target, domain, jira_url, github_repo):\n    \"\"\"Bootstrap a project-expert knowledge base.\n\n    TARGET is owner/repo for GitHub/GitLab, or project key for Jira.\n\n    Examples:\n        project-expert init github owner/repo\n        project-expert init gitlab group/project\n        project-expert init jira MYPROJ --jira-url https://myco.atlassian.net\n    \"\"\"\n    if not domain:\n        domain = target\n\n    # Check prerequisites\n    for tool in [\"entry\"]:\n        if not shutil.which(tool):\n            click.echo(f\"Error: {tool} not found on PATH\", err=True)\n            sys.exit(1)\n    if not shutil.which(\"reasons\") and not shutil.which(\"beliefs\"):\n        click.echo(\"Error: neither reasons nor beliefs found on PATH\", err=True)\n        sys.exit(1)\n\n    # Check platform CLI\n    if platform == \"github\" and not shutil.which(\"gh\"):\n        click.echo(\"Error: gh CLI not found. Install from https://cli.github.com\", err=True)\n        sys.exit(1)\n    if platform == \"gitlab\" and not shutil.which(\"glab\"):\n        click.echo(\"Error: glab CLI not found.\", err=True)\n        sys.exit(1)\n    if platform == \"jira\":\n        if not jira_url and not os.environ.get(\"JIRA_URL\"):\n            click.echo(\"Error: --jira-url or JIRA_URL env var required for Jira\", err=True)\n            sys.exit(1)\n\n    # Create project dir\n    project_dir = Path.cwd() / PROJECT_DIR\n    project_dir.mkdir(parents=True, exist_ok=True)\n\n    # Save config\n    config = {\n        \"platform\": platform,\n        \"domain\": domain,\n        \"created\": date.today().isoformat(),\n    }\n    if platform in (\"github\", \"gitlab\"):\n        config[\"repo\"] = target\n    else:\n        config[\"project\"] = target\n        config[\"jira_url\"] = jira_url or os.environ.get(\"JIRA_URL\", \"\")\n\n    if github_repo:\n        config[\"github_repo\"] = github_repo\n\n    _save_config(config)\n\n    # Create entries dir\n    Path(\"entries\").mkdir(exist_ok=True)\n\n    # Init belief store\n    if _has_reasons():\n        if not Path(\"reasons.db\").exists():\n            subprocess.run([\"reasons\", \"init\"], capture_output=True)\n            click.echo(\"Initialized reasons.db\")\n        if not Path(\"beliefs.md\").exists():\n            _reasons_export()\n    elif not Path(\"beliefs.md\").exists():\n        subprocess.run([\"beliefs\", \"init\"], capture_output=True)\n        click.echo(\"Initialized beliefs.md\")\n\n    click.echo(f\"\\nInitialized project-expert\")\n    click.echo(f\"  Platform: {platform}\")\n    click.echo(f\"  Target:   {target}\")\n    click.echo(f\"  Domain:   {domain}\")\n    click.echo(f\"\\nNext: project-expert scan\")"
  },
  "ftl_project_expert/cli.py:_extract_issue_refs": {
    "function": "_extract_issue_refs",
    "file": "ftl_project_expert/cli.py",
    "start_line": 1353,
    "end_line": 1388,
    "source": "def _extract_issue_refs(text: str) -> list[dict]:\n    \"\"\"Extract issue and PR references from text.\n\n    Returns list of dicts with 'type' (issue/pr) and 'number' or 'key'.\n    \"\"\"\n    refs = []\n    seen = set()\n\n    # GH-123, PR-123, GL-123, MR-123\n    for match in re.finditer(r\"\\b(GH|PR|GL|MR)-(\\d+)\\b\", text):\n        prefix = match.group(1)\n        num = int(match.group(2))\n        kind = \"pr\" if prefix in (\"PR\", \"MR\") else \"issue\"\n        key = (kind, num)\n        if key not in seen:\n            seen.add(key)\n            refs.append({\"type\": kind, \"number\": num})\n\n    # #123 (common GitHub/GitLab shorthand)\n    for match in re.finditer(r\"(?<!\\w)#(\\d+)\\b\", text):\n        num = int(match.group(1))\n        key = (\"issue\", num)\n        if key not in seen:\n            seen.add(key)\n            refs.append({\"type\": \"issue\", \"number\": num})\n\n    # PROJ-123 (Jira-style keys)\n    for match in re.finditer(r\"\\b([A-Z][A-Z0-9]+-\\d+)\\b\", text):\n        jira_key = match.group(1)\n        if jira_key.startswith((\"GH-\", \"PR-\", \"GL-\", \"MR-\")):\n            continue\n        if jira_key not in seen:\n            seen.add(jira_key)\n            refs.append({\"type\": \"issue\", \"key\": jira_key})\n\n    return refs"
  },
  "ftl_project_expert/cli.py:research": {
    "function": "research",
    "file": "ftl_project_expert/cli.py",
    "start_line": 1470,
    "end_line": 1631,
    "source": "@cli.command(\"research\")\n@click.argument(\"belief_id\", required=False)\n@click.option(\"--negative\", is_flag=True, default=False,\n              help=\"Select from OUT/negative beliefs\")\n@click.option(\"--high-impact\", is_flag=True, default=False,\n              help=\"Select by dependent count (highest first)\")\n@click.option(\"--limit\", \"select_limit\", type=int, default=1,\n              help=\"Number of beliefs to research (default: 1)\")\n@click.option(\"--parallel\", \"max_parallel\", type=int, default=1,\n              help=\"Max concurrent LLM calls (default: 1, try 3 for speed)\")\n@click.pass_context\ndef research(ctx, belief_id, negative, high_impact, select_limit, max_parallel):\n    \"\"\"Verify a belief against its source system.\n\n    Fetches the current state of issues/PRs referenced by a belief,\n    compares against the belief's claims, and reports discrepancies.\n\n    Examples:\n        project-expert research my-belief-id\n        project-expert research --negative\n        project-expert research --high-impact --limit 5\n        project-expert research --high-impact --limit 5 --parallel 3\n    \"\"\"\n    config = _load_config()\n    if not config:\n        click.echo(\"Not initialized. Run: project-expert init <platform> <target>\")\n        sys.exit(1)\n\n    model = ctx.obj[\"model\"]\n    timeout = ctx.obj[\"timeout\"]\n\n    if not _has_reasons():\n        click.echo(\"Error: reasons CLI required. Install with: uv tool install ftl-reasons\", err=True)\n        sys.exit(1)\n\n    if not check_model_available(model):\n        click.echo(f\"Error: Model '{model}' CLI not available\", err=True)\n        sys.exit(1)\n\n    # Determine which beliefs to research\n    network = _load_network()\n    if belief_id:\n        belief_ids = [belief_id]\n    else:\n        belief_ids = _select_beliefs_for_research(\n            network, negative=negative, high_impact=high_impact,\n            limit=select_limit,\n        )\n        if not belief_ids:\n            label = \"negative \" if negative else \"\"\n            click.echo(f\"No {label}beliefs found to research.\")\n            return\n        click.echo(f\"Selected {len(belief_ids)} belief(s) for research:\", err=True)\n        for bid in belief_ids:\n            click.echo(f\"  {bid}\", err=True)\n\n    source = _get_source(config)\n\n    github_source = None\n    gh_repo = config.get(\"github_repo\")\n    if gh_repo and config[\"platform\"] != \"github\":\n        github_source = GitHubSource(gh_repo)\n        click.echo(f\"Using GitHub repo {gh_repo} for PR lookups\", err=True)\n\n    def _research_one(bid: str) -> str | None:\n        \"\"\"Build a research prompt for a single belief.\"\"\"\n        info = _get_belief_info(bid)\n        if not info:\n            click.echo(f\"Belief not found: {bid}\", err=True)\n            return None\n\n        click.echo(f\"\\nResearching: {bid}\", err=True)\n        click.echo(f\"  Claim: {info['text'][:100]}\", err=True)\n        click.echo(f\"  Status: {info['status']}\", err=True)\n\n        # Read source entry\n        source_entry = \"(No source entry found)\"\n        source_path = info.get(\"source\", \"\")\n        if source_path:\n            entry_path = Path(source_path)\n            if entry_path.is_file():\n                content = entry_path.read_text()\n                if len(content) > 15000:\n                    content = content[:15000] + \"\\n[Truncated]\"\n                source_entry = content\n                click.echo(f\"  Source: {source_path}\", err=True)\n\n        # Extract and fetch artifacts\n        all_text = f\"{info['text']}\\n{source_entry}\"\n        refs = _extract_issue_refs(all_text)\n        click.echo(f\"  Found {len(refs)} reference(s)\", err=True)\n\n        artifacts = _fetch_artifacts(refs, source, config, github_source=github_source)\n\n        # Get dependent beliefs\n        dependents = _get_dependent_beliefs(bid, network)\n        dep_count = len(info.get(\"dependents\", []))\n        if dep_count:\n            click.echo(f\"  {dep_count} dependent belief(s)\", err=True)\n\n        return RESEARCH_PROMPT.format(\n            belief_id=bid,\n            belief_text=info[\"text\"],\n            belief_status=info[\"status\"],\n            source_entry=source_entry,\n            artifacts=artifacts,\n            dependents=dependents,\n        )\n\n    # Build prompts\n    prompts_with_ids = []\n    for bid in belief_ids:\n        prompt = _research_one(bid)\n        if prompt:\n            prompts_with_ids.append((bid, prompt))\n\n    if not prompts_with_ids:\n        click.echo(\"No beliefs could be researched.\")\n        return\n\n    # Invoke LLM\n    ids = [p[0] for p in prompts_with_ids]\n    prompts = [p[1] for p in prompts_with_ids]\n\n    click.echo(f\"\\nRunning {model} on {len(prompts)} belief(s)...\", err=True)\n\n    if max_parallel > 1 and len(prompts) > 1:\n        results = invoke_concurrent_sync(prompts, model=model, timeout=timeout,\n                                         max_concurrent=max_parallel)\n    else:\n        results = []\n        for i, prompt in enumerate(prompts):\n            click.echo(f\"  [{i + 1}/{len(prompts)}] {ids[i]}...\", err=True)\n            try:\n                result = invoke_sync(prompt, model=model, timeout=timeout)\n                results.append(result)\n            except Exception as e:\n                click.echo(f\"  ERROR: {e}\", err=True)\n                results.append(e)\n\n    # Process results\n    for bid, result in zip(ids, results):\n        if isinstance(result, Exception):\n            click.echo(f\"\\nERROR researching {bid}: {result}\", err=True)\n            continue\n\n        # Extract verdict\n        verdict = \"UNKNOWN\"\n        for line in result.splitlines():\n            line = line.strip()\n            if line.startswith(\"VERDICT:\"):\n                verdict = line[8:].strip()\n                break\n\n        click.echo(f\"\\n{'=' * 40}\", err=True)\n        click.echo(f\"  {bid}: {verdict}\", err=True)\n        click.echo(f\"{'=' * 40}\", err=True)\n\n        safe_id = re.sub(r\"[^a-zA-Z0-9_-]\", \"-\", bid)[:80]\n        _create_entry(f\"research-{safe_id}\", f\"Research: {bid} [{verdict}]\", result)\n\n        _emit(ctx, result)"
  },
  "ftl_project_expert/cli.py:_research_one": {
    "function": "_research_one",
    "file": "ftl_project_expert/cli.py",
    "start_line": 1534,
    "end_line": 1577,
    "source": "    def _research_one(bid: str) -> str | None:\n        \"\"\"Build a research prompt for a single belief.\"\"\"\n        info = _get_belief_info(bid)\n        if not info:\n            click.echo(f\"Belief not found: {bid}\", err=True)\n            return None\n\n        click.echo(f\"\\nResearching: {bid}\", err=True)\n        click.echo(f\"  Claim: {info['text'][:100]}\", err=True)\n        click.echo(f\"  Status: {info['status']}\", err=True)\n\n        # Read source entry\n        source_entry = \"(No source entry found)\"\n        source_path = info.get(\"source\", \"\")\n        if source_path:\n            entry_path = Path(source_path)\n            if entry_path.is_file():\n                content = entry_path.read_text()\n                if len(content) > 15000:\n                    content = content[:15000] + \"\\n[Truncated]\"\n                source_entry = content\n                click.echo(f\"  Source: {source_path}\", err=True)\n\n        # Extract and fetch artifacts\n        all_text = f\"{info['text']}\\n{source_entry}\"\n        refs = _extract_issue_refs(all_text)\n        click.echo(f\"  Found {len(refs)} reference(s)\", err=True)\n\n        artifacts = _fetch_artifacts(refs, source, config, github_source=github_source)\n\n        # Get dependent beliefs\n        dependents = _get_dependent_beliefs(bid, network)\n        dep_count = len(info.get(\"dependents\", []))\n        if dep_count:\n            click.echo(f\"  {dep_count} dependent belief(s)\", err=True)\n\n        return RESEARCH_PROMPT.format(\n            belief_id=bid,\n            belief_text=info[\"text\"],\n            belief_status=info[\"status\"],\n            source_entry=source_entry,\n            artifacts=artifacts,\n            dependents=dependents,\n        )"
  },
  "ftl_project_expert/cli.py:status": {
    "function": "status",
    "file": "ftl_project_expert/cli.py",
    "start_line": 2049,
    "end_line": 2108,
    "source": "@cli.command()\ndef status():\n    \"\"\"Show project-expert dashboard.\"\"\"\n    config = _load_config()\n\n    click.echo(\"=== Project Expert Status ===\\n\")\n\n    if config:\n        click.echo(f\"Platform: {config.get('platform', 'unknown')}\")\n        click.echo(f\"Target:   {config.get('repo', config.get('project', 'unknown'))}\")\n        click.echo(f\"Domain:   {config.get('domain', 'unknown')}\")\n        click.echo(f\"Created:  {config.get('created', 'unknown')}\")\n        if config.get(\"github_repo\"):\n            click.echo(f\"GitHub:   {config['github_repo']}\")\n    else:\n        click.echo(\"Not initialized. Run: project-expert init <platform> <target>\")\n        return\n\n    click.echo()\n\n    # Entries\n    entries_dir = Path(\"entries\")\n    entry_count = len(list(entries_dir.rglob(\"*.md\"))) if entries_dir.exists() else 0\n    click.echo(f\"Entries:  {entry_count}\")\n\n    # Beliefs\n    if _has_reasons() and Path(\"reasons.db\").exists():\n        result = subprocess.run([\"reasons\", \"list\"], capture_output=True, text=True)\n        if result.returncode == 0:\n            lines = result.stdout.splitlines()\n            r_in = sum(1 for l in lines if l.strip().startswith(\"[+]\"))\n            r_out = sum(1 for l in lines if l.strip().startswith(\"[-]\"))\n            click.echo(f\"Beliefs:  {r_in} IN, {r_out} OUT\")\n    else:\n        beliefs_path = Path(\"beliefs.md\")\n        if beliefs_path.exists():\n            text = beliefs_path.read_text()\n            b_in = len(re.findall(r\"^### \\S+ \\[IN\\]\", text, re.MULTILINE))\n            click.echo(f\"Beliefs:  {b_in} IN\")\n\n    # Topics\n    project_dir = _get_project_dir()\n    queue = load_queue(project_dir)\n    pending = sum(1 for t in queue if t.status == \"pending\")\n    done = sum(1 for t in queue if t.status == \"done\")\n    skipped = sum(1 for t in queue if t.status == \"skipped\")\n    click.echo(f\"Topics:   {pending} pending, {done} done, {skipped} skipped\")\n\n    # Cached issues\n    cached = _load_cached_issues(project_dir)\n    if cached:\n        click.echo(f\"Cached:   {len(cached)} issues\")\n\n    # Proposals\n    proposals_path = Path(\"proposed-beliefs.md\")\n    if proposals_path.exists():\n        text = proposals_path.read_text()\n        total = len(re.findall(r\"^### \\[(?:ACCEPT|REJECT|ACCEPT/REJECT)\\]\", text, re.MULTILINE))\n        accepted = len(re.findall(r\"^### \\[ACCEPT\\]\", text, re.MULTILINE))\n        click.echo(f\"Proposed: {total} candidates ({accepted} accepted)\")"
  },
  "fetch_artifacts_callers": {
    "symbol": "_fetch_artifacts",
    "production_callers": [
      {
        "file": "ftl_project_expert/cli.py",
        "line": 1391,
        "text": "def _fetch_artifacts(refs: list[dict], source, config: dict,",
        "context_function": "_extract_issue_refs",
        "context_snippet": "   1388:     return refs\n   1389: \n   1390: \n>> 1391: def _fetch_artifacts(refs: list[dict], source, config: dict,\n   1392:                      github_source=None) -> str:\n   1393:     \"\"\"Fetch current state for each issue/PR reference.\"\"\"\n   1394:     parts = []"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 1562,
        "text": "artifacts = _fetch_artifacts(refs, source, config, github_source=github_source)",
        "context_function": "_research_one",
        "context_snippet": "   1559:         refs = _extract_issue_refs(all_text)\n   1560:         click.echo(f\"  Found {len(refs)} reference(s)\", err=True)\n   1561: \n>> 1562:         artifacts = _fetch_artifacts(refs, source, config, github_source=github_source)\n   1563: \n   1564:         # Get dependent beliefs\n   1565:         dependents = _get_dependent_beliefs(bid, network)"
      }
    ],
    "test_callers": [],
    "production_count": 2,
    "test_count": 0,
    "total_count": 2
  },
  "github_source_init": {
    "function": "_get_source",
    "file": "ftl_project_expert/cli.py",
    "start_line": 62,
    "end_line": 75,
    "source": "def _get_source(config: dict) -> GitHubSource | GitLabSource | JiraSource:\n    \"\"\"Create the appropriate source from config.\"\"\"\n    platform = config[\"platform\"]\n    if platform == \"github\":\n        return GitHubSource(config[\"repo\"])\n    elif platform == \"gitlab\":\n        return GitLabSource(config[\"repo\"])\n    elif platform == \"jira\":\n        return JiraSource(\n            config[\"project\"],\n            url=config.get(\"jira_url\"),\n        )\n    else:\n        raise ValueError(f\"Unknown platform: {platform}\")"
  },
  "github_source_class": {
    "error": "File not found: ftl_project_expert/sources.py",
    "file": "ftl_project_expert/sources.py"
  },
  "github_source_get_pr": {
    "error": "File not found: ftl_project_expert/sources.py",
    "file": "ftl_project_expert/sources.py"
  },
  "github_source_get_pr_raises": {
    "error": "File not found: ftl_project_expert/sources.py",
    "function": "GitHubSource.get_pr"
  },
  "github_source_init_raises": {
    "error": "File not found: ftl_project_expert/sources.py",
    "function": "GitHubSource.__init__"
  },
  "cli_test_files": {
    "source_file": "ftl_project_expert/cli.py",
    "test_files": [],
    "test_count": 0
  },
  "cli_imports": {
    "file": "ftl_project_expert/cli.py",
    "imports": [
      "asyncio",
      "json",
      "os",
      "re",
      "shutil",
      "subprocess",
      "sys",
      "click"
    ],
    "from_imports": [
      {
        "module": "datetime",
        "names": [
          "date",
          "datetime"
        ]
      },
      {
        "module": "pathlib",
        "names": [
          "Path"
        ]
      },
      {
        "module": "llm",
        "names": [
          "check_model_available",
          "invoke",
          "invoke_concurrent_sync",
          "invoke_sync"
        ]
      },
      {
        "module": "prompts",
        "names": [
          "PROPOSE_BELIEFS_PROJECT",
          "RESEARCH_PROMPT",
          "build_explore_prompt",
          "build_scan_prompt",
          "build_summary_prompt"
        ]
      },
      {
        "module": "sources",
        "names": [
          "GitHubSource",
          "GitLabSource",
          "Issue",
          "JiraSource"
        ]
      },
      {
        "module": "topics",
        "names": [
          "Topic",
          "add_topics",
          "load_queue",
          "parse_topics_from_response",
          "pending_count",
          "pop_at",
          "pop_multiple",
          "pop_next",
          "skip_topic"
        ]
      }
    ],
    "import_section": "\"\"\"Command-line interface for project expert.\"\"\"\n\nimport asyncio\nimport json\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nfrom datetime import date, datetime\nfrom pathlib import Path\n\nimport click\n\nfrom .llm import check_model_available, invoke, invoke_concurrent_sync, invoke_sync\nfrom .prompts import (\n    PROPOSE_BELIEFS_PROJECT,\n    RESEARCH_PROMPT,\n    build_explore_prompt,\n    build_scan_prompt,\n    build_summary_prompt,\n)\nfrom .sources import GitHubSource, GitLabSource, Issue, JiraSource\nfrom .topics import (\n    Topic,\n    add_topics,\n    load_queue,\n    parse_topics_from_response,\n    pending_count,\n    pop_at,\n    pop_multiple,\n    pop_next,\n    skip_topic,\n)\n\nPROJECT_DIR = \".project-expert\"\n\n\n# --- Config helpers ---\n\n\ndef _load_config() -> dict | None:\n    config_path = Path.cwd() / PROJECT_DIR / \"config.json\"\n    if config_path.is_file():\n        return json.loads(config_path.read_text())\n    return None\n\n\ndef _save_config(config: dict) -> None:\n    config_dir = Path.cwd() / PROJECT_DIR\n    config_dir.mkdir(parents=True, exist_ok=True)\n    (config_dir / \"config.json\").write_text(json.dumps(config, indent=2))\n\n\ndef _get_project_dir() -> str:\n    return str(Path.cwd() / PROJECT_DIR)\n\n\n# --- Source helpers ---\n\n\ndef _get_source(config: dict) -> GitHubSource | GitLabSource | JiraSource:\n    \"\"\"Create the appropriate source from config.\"\"\""
  },
  "fetch_artifacts_full": {
    "function": "_fetch_artifacts",
    "file": "ftl_project_expert/cli.py",
    "start_line": 1391,
    "end_line": 1423,
    "source": "def _fetch_artifacts(refs: list[dict], source, config: dict,\n                     github_source=None) -> str:\n    \"\"\"Fetch current state for each issue/PR reference.\"\"\"\n    parts = []\n    platform = config[\"platform\"]\n\n    for ref in refs:\n        ref_label = ref.get(\"key\") or f\"#{ref.get('number')}\"\n        try:\n            if \"key\" in ref:\n                if platform != \"jira\":\n                    parts.append(f\"(Skipping Jira key {ref['key']} on {platform})\")\n                    continue\n                issue = source.get_issue(ref[\"key\"])\n                parts.append(issue.to_prompt_text())\n            elif ref[\"type\"] == \"pr\":\n                if hasattr(source, \"get_pr\"):\n                    pr = source.get_pr(ref[\"number\"])\n                    parts.append(pr.to_prompt_text())\n                elif github_source:\n                    pr = github_source.get_pr(ref[\"number\"])\n                    parts.append(pr.to_prompt_text())\n                else:\n                    parts.append(f\"(Could not fetch pr {ref_label} \u2014 no GitHub repo configured)\")\n            elif ref[\"type\"] == \"issue\":\n                issue = source.get_issue(ref[\"number\"])\n                parts.append(issue.to_prompt_text())\n            else:\n                parts.append(f\"(Could not fetch {ref['type']} {ref_label})\")\n        except Exception as e:\n            parts.append(f\"(Error fetching {ref['type']} {ref_label}: {e})\")\n\n    return \"\\n\\n---\\n\\n\".join(parts) if parts else \"(No artifacts fetched)\""
  }
}
```

Use these results to inform your review. Do not request the same observations again.


## Instructions

For each significant change (new file, modified function, etc.), provide a structured verdict.

Use this exact format for each change:

### <file_path or file_path:function_name>
VERDICT: PASS | CONCERN | BLOCK
CORRECTNESS: VALID | QUESTIONABLE | BROKEN
SPEC_COMPLIANCE: MEETS | PARTIAL | VIOLATES | N/A
ISSUE_COMPLIANCE: ADDRESSES | PARTIAL | UNRELATED | N/A
BELIEF_COMPLIANCE: CONSISTENT | VIOLATES | N/A
TEST_COVERAGE: COVERED | PARTIAL | UNTESTED
INTEGRATION: WIRED | PARTIAL | MISSING
REASONING: <brief explanation of your assessment>
---

## Review Criteria

1. **CORRECTNESS**: Does the code do what it claims? Is the logic sound?
   - VALID: Logic is correct, no bugs apparent
   - QUESTIONABLE: Logic may have edge cases or unclear behavior
   - BROKEN: Clear bugs or incorrect behavior

2. **SPEC_COMPLIANCE**: Does it meet MUST requirements from the spec?
   - MEETS: All relevant spec requirements satisfied
   - PARTIAL: Some requirements met, others missing or incomplete
   - VIOLATES: Contradicts spec requirements
   - N/A: No spec provided or not applicable

3. **ISSUE_COMPLIANCE** (only when an issue is provided): Do the changes address the problem or feature described in the issue?
   - ADDRESSES: Changes directly solve the issue's stated problem or implement the requested feature
   - PARTIAL: Changes partially address the issue but leave some aspects unresolved
   - UNRELATED: Changes do not appear related to the issue
   - N/A: No issue provided

4. **TEST_COVERAGE**: Are there tests for the new/changed code?
   - COVERED: Tests exist and cover the changes
   - PARTIAL: Some tests exist but coverage is incomplete
   - UNTESTED: No tests for the changes

5. **INTEGRATION**: Are callers updated? Is the feature usable end-to-end?
   - WIRED: Feature is fully integrated and usable
   - PARTIAL: Interface exists but callers not updated, or integration incomplete
   - MISSING: No integration with existing code

6. **BELIEF_COMPLIANCE** (only when beliefs are provided): Do the changes respect known architectural invariants, contracts, and rules?
   - CONSISTENT: Changes align with or reinforce known beliefs
   - VIOLATES: Changes contradict a specific belief — cite the belief ID
   - N/A: No beliefs provided or no relevant beliefs apply

## Verdict Guidelines

- **BLOCK**: Security issues, broken functionality, spec violations, or missing critical integration
- **CONCERN**: Missing tests, partial integration, questionable patterns, or unclear logic
- **PASS**: Correct, tested, well-integrated code

## Important

- Full function bodies for modified functions may be available in the observations section — use them to verify the complete logic, not just the diff hunks
- Related test files (prefixed with ``related_test:``) may be included in observations — check whether existing test assertions still match modified return types, signatures, or behavior. Flag any test that would break due to the changes
- If duplicate test coverage is detected (multiple test files covering the same source), note it in your review
- Focus on actual issues, not style preferences
- If a method signature is added but callers aren't updated, that's PARTIAL integration
- Be specific in reasoning - reference line numbers or function names
- When in doubt, use CONCERN rather than PASS

## Self-Review

After completing your review, add a brief self-assessment:

### SELF_REVIEW
LIMITATIONS: <what context were you missing that affected review quality?>
---

Examples of limitations:
- "Could not see full class to verify no other methods access the modified field"
- "Test file not included in diff - cannot verify coverage claims"
- "Spec file referenced but not provided"


## Feature Requests

If this review tool could be improved to help you do a better job, suggest features:

### FEATURE_REQUESTS
- <suggestion 1>
- <suggestion 2>
---

Examples:
- "Include full file context for modified functions, not just diff hunks"
- "Show callers of modified methods to verify integration"
- "Include test file alongside implementation changes"

Only include this section if you have specific suggestions. Skip if none.
