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_code_expert/cli.py b/ftl_code_expert/cli.py
index 2baf921..e9dd5a3 100644
--- a/ftl_code_expert/cli.py
+++ b/ftl_code_expert/cli.py
@@ -34,6 +34,7 @@
     PROPOSE_BELIEFS_CODE,
     RESEARCH_INFER_FILES_PROMPT,
     REVIEW_PROMPT,
+    VERIFY_INFER_FILE_PROMPT,
     VERIFY_OBSERVE_PROMPT,
     VERIFY_PROMPT,
     build_diff_prompt,
@@ -3146,6 +3147,36 @@ async def _verify_belief_with_observations(
         belief, node, repo_path, project_dir,
     )
 
+    tree = get_repo_structure(repo_path, max_depth=3)
+
+    if src_file is None:
+        try:
+            infer_prompt = VERIFY_INFER_FILE_PROMPT.format(
+                belief_id=bid,
+                belief_text=belief["text"],
+                repo_tree=tree,
+            )
+            infer_response = await invoke(infer_prompt, model, timeout=timeout)
+            inferred = _parse_inferred_files(infer_response)
+            if inferred:
+                candidate = inferred[0]
+                if not os.path.isabs(candidate) and ".." not in candidate.split("/"):
+                    abs_candidate = os.path.join(repo_path, candidate)
+                    if os.path.isfile(abs_candidate):
+                        src_file = candidate
+                        from .observations import read_file
+                        file_result = await read_file(src_file, repo_path, max_lines=500)
+                        auto_obs[f"source_file:{src_file}"] = file_result
+                        click.echo(f"  Inferred source file for {bid}: {src_file}", err=True)
+                        if _has_reasons() and Path("reasons.db").exists():
+                            try:
+                                from reasons_lib.api import set_metadata
+                                set_metadata(bid, "source_file", src_file)
+                            except Exception:
+                                pass
+        except Exception:
+            pass
+
     seed_context = ""
     source_key = f"source_file:{src_file}" if src_file else None
     if source_key and source_key in auto_obs:
@@ -3155,8 +3186,6 @@ async def _verify_belief_with_observations(
             if len(content) > 4000:
                 content = content[:4000] + "\n... (truncated)"
             seed_context = f"### {src_file}\n```\n{content}\n```"
-
-    tree = get_repo_structure(repo_path, max_depth=2)
     observe_prompt = VERIFY_OBSERVE_PROMPT.format(
         belief_id=bid,
         belief_text=belief["text"],
@@ -4112,6 +4141,134 @@ def _collect_source_files(nid):
         click.echo("\nCannot retract: reasons CLI not available.", err=True)
 
 
+# --- infer-sources ---
+
+
+@cli.command("infer-sources")
+@click.argument("belief_ids", nargs=-1)
+@click.option("--all", "infer_all", is_flag=True, default=False,
+              help="Infer source files for all IN beliefs missing source_file")
+@click.option("--dry-run", is_flag=True, default=False,
+              help="Show what would be inferred without calling LLM")
+@click.pass_context
+def infer_sources(ctx, belief_ids, infer_all, dry_run):
+    """Infer source files for beliefs that lack source_file metadata.
+
+    Uses an LLM to identify which source file a belief refers to based on
+    the belief text and repository structure, then stamps source_file metadata
+    so verify can find the relevant code.
+
+    Examples:
+        code-expert infer-sources my-belief-id
+        code-expert infer-sources --all --dry-run
+        code-expert infer-sources --all
+    """
+    model = ctx.obj["model"]
+    timeout = ctx.obj["timeout"]
+    parallel = ctx.obj["parallel"]
+    repo_path = _get_repo(ctx)
+    abs_repo = os.path.abspath(repo_path)
+
+    if not check_model_available(model):
+        click.echo(f"Error: Model '{model}' CLI not available", err=True)
+        sys.exit(1)
+
+    if not _has_reasons() or not Path("reasons.db").exists():
+        click.echo("Error: reasons.db not found", err=True)
+        sys.exit(1)
+
+    _reasons_export()
+
+    try:
+        network = _load_network()
+        nodes = network.get("nodes", {})
+    except Exception as e:
+        click.echo(f"Error loading belief network: {e}", err=True)
+        sys.exit(1)
+
+    candidates = []
+    if belief_ids:
+        for bid in belief_ids:
+            node = nodes.get(bid)
+            if not node:
+                click.echo(f"  Belief not found: {bid}", err=True)
+                continue
+            src = (node.get("metadata") or {}).get("source_file")
+            if src:
+                click.echo(f"  {bid}: already has source_file={src}", err=True)
+                continue
+            candidates.append({"id": bid, "text": node.get("text", "")})
+    elif infer_all:
+        for nid, node in nodes.items():
+            if node.get("truth_value") != "IN":
+                continue
+            src = (node.get("metadata") or {}).get("source_file")
+            if src:
+                continue
+            src = _extract_source_file(node.get("source", ""), _get_project_dir(ctx))
+            if src:
+                continue
+            candidates.append({"id": nid, "text": node.get("text", "")})
+    else:
+        click.echo("Specify belief IDs, or use --all.", err=True)
+        sys.exit(1)
+
+    if not candidates:
+        click.echo("No beliefs need source file inference.")
+        return
+
+    click.echo(f"Found {len(candidates)} belief(s) needing source file inference", err=True)
+
+    if dry_run:
+        for c in candidates:
+            click.echo(f"  {c['id']}: {c['text'][:100]}", err=True)
+        click.echo(f"\n--dry-run: stopping before LLM inference.", err=True)
+        return
+
+    tree = get_repo_structure(abs_repo, max_depth=3)
+    prompts = [
+        VERIFY_INFER_FILE_PROMPT.format(
+            belief_id=c["id"],
+            belief_text=c["text"],
+            repo_tree=tree,
+        )
+        for c in candidates
+    ]
+
+    click.echo(f"Inferring source files with {model}...", err=True)
+    results = invoke_concurrent_sync(prompts, model=model, timeout=timeout, max_concurrent=parallel)
+
+    from reasons_lib.api import set_metadata
+    stamped = 0
+    for i, result in enumerate(results):
+        bid = candidates[i]["id"]
+        if isinstance(result, Exception):
+            click.echo(f"  {bid}: error — {result}", err=True)
+            continue
+        inferred = _parse_inferred_files(result)
+        if not inferred:
+            click.echo(f"  {bid}: no file inferred", err=True)
+            continue
+        candidate = inferred[0]
+        if os.path.isabs(candidate) or ".." in candidate.split("/"):
+            click.echo(f"  {bid}: unsafe path {candidate}", err=True)
+            continue
+        abs_path = os.path.join(abs_repo, candidate)
+        if not os.path.isfile(abs_path):
+            click.echo(f"  {bid}: {candidate} not found", err=True)
+            continue
+        try:
+            set_metadata(bid, "source_file", candidate)
+            stamped += 1
+            click.echo(f"  {bid}: {candidate}", err=True)
+        except Exception as e:
+            click.echo(f"  {bid}: failed to stamp — {e}", err=True)
+
+    click.echo(f"\nInferred source_file for {stamped}/{len(candidates)} belief(s)", err=True)
+    if stamped:
+        _reasons_export()
+
+
 # --- update ---
 
 
diff --git a/ftl_code_expert/prompts/__init__.py b/ftl_code_expert/prompts/__init__.py
index 2d6fb0b..1333276 100644
--- a/ftl_code_expert/prompts/__init__.py
+++ b/ftl_code_expert/prompts/__init__.py
@@ -9,7 +9,7 @@
 from .propose import PROPOSE_BELIEFS_CODE
 from .research import RESEARCH_INFER_FILES_PROMPT
 from .review import REVIEW_PROMPT
-from .verify import VERIFY_OBSERVE_PROMPT, VERIFY_PROMPT
+from .verify import VERIFY_INFER_FILE_PROMPT, VERIFY_OBSERVE_PROMPT, VERIFY_PROMPT
 from .repo import build_repo_prompt
 from .scan import build_scan_prompt
 from .spec import GENERATE_SPEC_PROMPT
@@ -21,6 +21,7 @@
     "PROPOSE_BELIEFS_CODE",
     "RESEARCH_INFER_FILES_PROMPT",
     "REVIEW_PROMPT",
+    "VERIFY_INFER_FILE_PROMPT",
     "VERIFY_OBSERVE_PROMPT",
     "VERIFY_PROMPT",
     "TOPICS_INSTRUCTIONS",
diff --git a/ftl_code_expert/prompts/verify.py b/ftl_code_expert/prompts/verify.py
index 67ae6cc..4dea558 100644
--- a/ftl_code_expert/prompts/verify.py
+++ b/ftl_code_expert/prompts/verify.py
@@ -83,3 +83,34 @@
 ## Beliefs to Verify
 
 {beliefs}"""
+
+
+VERIFY_INFER_FILE_PROMPT = """\
+You are identifying which source file a belief about a codebase refers to.
+
+## Belief
+
+**ID:** {belief_id}
+
+**Claim:** {belief_text}
+
+## Repository structure
+
+```
+{repo_tree}
+```
+
+## Instructions
+
+Based on the belief ID and claim text, identify the single source file in the repository \
+that this belief is primarily about. The file must exist in the repository structure above.
+
+Output a JSON array with exactly one file path relative to the repository root.
+
+Example output:
+```json
+["src/auth/handler.py"]
+```
+
+Output ONLY the JSON array, nothing else.
+"""

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "verify_belief_full": {
    "function": "_verify_belief_with_observations",
    "file": "ftl_code_expert/cli.py",
    "start_line": 3129,
    "end_line": 3214,
    "source": "async def _verify_belief_with_observations(\n    belief: dict,\n    node: dict,\n    repo_path: str,\n    project_dir: str | None,\n    model: str,\n    timeout: int,\n) -> tuple[str, str]:\n    \"\"\"Gather code context for a belief using the observe pattern.\n\n    1. Auto-gather: read source file + find_usages/grep for key symbols\n    2. Ask LLM what additional observations it needs\n    3. Execute LLM-requested observations in parallel\n    4. Return combined context\n    \"\"\"\n    bid = belief[\"id\"]\n\n    src_file, auto_obs = await _auto_gather_verify_observations(\n        belief, node, repo_path, project_dir,\n    )\n\n    tree = get_repo_structure(repo_path, max_depth=3)\n\n    if src_file is None:\n        try:\n            infer_prompt = VERIFY_INFER_FILE_PROMPT.format(\n                belief_id=bid,\n                belief_text=belief[\"text\"],\n                repo_tree=tree,\n            )\n            infer_response = await invoke(infer_prompt, model, timeout=timeout)\n            inferred = _parse_inferred_files(infer_response)\n            if inferred:\n                candidate = inferred[0]\n                if not os.path.isabs(candidate) and \"..\" not in candidate.split(\"/\"):\n                    abs_candidate = os.path.join(repo_path, candidate)\n                    if os.path.isfile(abs_candidate):\n                        src_file = candidate\n                        from .observations import read_file\n                        file_result = await read_file(src_file, repo_path, max_lines=500)\n                        auto_obs[f\"source_file:{src_file}\"] = file_result\n                        click.echo(f\"  Inferred source file for {bid}: {src_file}\", err=True)\n                        if _has_reasons() and Path(\"reasons.db\").exists():\n                            try:\n                                from reasons_lib.api import set_metadata\n                                set_metadata(bid, \"source_file\", src_file)\n                            except Exception:\n                                pass\n        except Exception:\n            pass\n\n    seed_context = \"\"\n    source_key = f\"source_file:{src_file}\" if src_file else None\n    if source_key and source_key in auto_obs:\n        file_result = auto_obs[source_key]\n        if \"content\" in file_result:\n            content = file_result[\"content\"]\n            if len(content) > 4000:\n                content = content[:4000] + \"\\n... (truncated)\"\n            seed_context = f\"### {src_file}\\n```\\n{content}\\n```\"\n    observe_prompt = VERIFY_OBSERVE_PROMPT.format(\n        belief_id=bid,\n        belief_text=belief[\"text\"],\n        seed_context=seed_context or \"(no initial source file)\",\n        tree=tree,\n    )\n    observe_response = await invoke(observe_prompt, model, timeout=timeout)\n    requested_obs = parse_observation_requests(observe_response)\n\n    llm_obs = {}\n    if requested_obs:\n        llm_obs = await run_observations(requested_obs, repo_path)\n\n    all_obs = {**auto_obs, **llm_obs}\n\n    context_parts: list[str] = []\n    if seed_context:\n        context_parts.append(seed_context)\n    if all_obs:\n        display_obs = {k: v for k, v in all_obs.items() if k != source_key}\n        if display_obs:\n            context_parts.append(\n                f\"## Observations\\n\\n```json\\n{json.dumps(display_obs, indent=2, default=str)}\\n```\"\n            )\n\n    return bid, \"\\n\\n\".join(context_parts) if context_parts else \"(no code context found)\""
  },
  "parse_inferred_files_body": {
    "function": "_parse_inferred_files",
    "file": "ftl_code_expert/cli.py",
    "start_line": 3700,
    "end_line": 3709,
    "source": "def _parse_inferred_files(response: str) -> list[str]:\n    \"\"\"Extract JSON array of file paths from LLM response.\"\"\"\n    for m in re.finditer(r\"\\[.*?\\]\", response, re.DOTALL):\n        try:\n            files = json.loads(m.group(0))\n            if isinstance(files, list) and all(isinstance(f, str) for f in files):\n                return files\n        except json.JSONDecodeError:\n            continue\n    return []"
  },
  "invoke_raises": {
    "function": "invoke",
    "file": "ftl_code_expert/cli.py",
    "explicit_raises": [],
    "calls": [],
    "error": "Function 'invoke' not found"
  },
  "invoke_concurrent_sync_body": {
    "error": "No function found at 'invoke_concurrent_sync'",
    "file": "ftl_code_expert/cli.py"
  },
  "read_file_body": {
    "function": "read_file",
    "file": "ftl_code_expert/observations.py",
    "start_line": 64,
    "end_line": 96,
    "source": "async def read_file(file_path: str, repo_path: str, start_line: int = 0, max_lines: int = 200) -> dict[str, Any]:\n    \"\"\"\n    Read a file's contents.\n\n    Args:\n        file_path: Path to the file (relative to repo)\n        repo_path: Repository root\n        start_line: Starting line (0-indexed)\n        max_lines: Maximum lines to return\n\n    Returns:\n        Dict with file content and metadata\n    \"\"\"\n    try:\n        full_path = Path(repo_path) / file_path\n        if not full_path.is_file():\n            return {\"error\": f\"File not found: {file_path}\", \"file\": file_path}\n\n        content = full_path.read_text(encoding=\"utf-8\")\n        lines = content.split(\"\\n\")\n        total_lines = len(lines)\n\n        selected = lines[start_line:start_line + max_lines]\n\n        return {\n            \"file\": file_path,\n            \"total_lines\": total_lines,\n            \"start_line\": start_line,\n            \"lines_returned\": len(selected),\n            \"content\": \"\\n\".join(selected),\n        }\n    except Exception as e:\n        return {\"error\": str(e), \"file\": file_path}"
  },
  "extract_source_file_body": {
    "function": "_extract_source_file",
    "file": "ftl_code_expert/cli.py",
    "start_line": 3000,
    "end_line": 3012,
    "source": "def _extract_source_file(entry_path: str, project_dir: str | None = None) -> str | None:\n    \"\"\"Extract source file path from an entry's header line (# File: ...).\"\"\"\n    base = project_dir or \".\"\n    full_path = Path(base) / entry_path\n    if not full_path.is_file():\n        return None\n    try:\n        for line in full_path.read_text().splitlines()[:5]:\n            if line.startswith(\"# File: \"):\n                return line[8:].strip()\n    except OSError:\n        pass\n    return None"
  },
  "get_repo_structure_body": {
    "error": "No function found at 'get_repo_structure'",
    "file": "ftl_code_expert/cli.py"
  },
  "has_reasons_body": {
    "function": "_has_reasons",
    "file": "ftl_code_expert/cli.py",
    "start_line": 159,
    "end_line": 161,
    "source": "def _has_reasons() -> bool:\n    \"\"\"Check if ftl-reasons CLI is available.\"\"\"\n    return shutil.which(\"reasons\") is not None"
  },
  "set_metadata_callers": {
    "symbol": "set_metadata",
    "usages": [
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 438,
        "text": "api.set_metadata(belief_id, \"repair_action\", \"search_and_link\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 457,
        "text": "api.set_metadata(belief_id, \"repair_action\", \"softened\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 469,
        "text": "api.set_metadata(belief_id, \"repair_action\", \"abandoned\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 476,
        "text": "api.set_metadata("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 481,
        "text": "api.set_metadata(belief_id, \"repair_action\", \"research\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/pg.py",
        "line": 1521,
        "text": "def set_metadata(self, node_id, key, value):"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/api.py",
        "line": 838,
        "text": "def set_metadata("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/api.py",
        "line": 847,
        "text": "return _pg_dispatch(pg_conninfo, project_id, \"set_metadata\","
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/api.py",
        "line": 3217,
        "text": "set_metadata(nid, \"repair_action\", \"rewritten\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/api.py",
        "line": 3226,
        "text": "set_metadata(nid, \"repair_action\", \"retracted\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/cli.py",
        "line": 397,
        "text": "def cmd_set_metadata(args):"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/cli.py",
        "line": 399,
        "text": "result = api.set_metadata("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/cli.py",
        "line": 2925,
        "text": "\"set-metadata\": cmd_set_metadata,"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 3173,
        "text": "from reasons_lib.api import set_metadata"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 3174,
        "text": "set_metadata(bid, \"source_file\", src_file)"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4111,
        "text": "from reasons_lib.api import set_metadata"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4116,
        "text": "set_metadata(bid, \"verified_at\", now)"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4241,
        "text": "from reasons_lib.api import set_metadata"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4261,
        "text": "set_metadata(bid, \"source_file\", candidate)"
      }
    ],
    "production_usages": [
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 438,
        "text": "api.set_metadata(belief_id, \"repair_action\", \"search_and_link\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 457,
        "text": "api.set_metadata(belief_id, \"repair_action\", \"softened\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 469,
        "text": "api.set_metadata(belief_id, \"repair_action\", \"abandoned\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 476,
        "text": "api.set_metadata("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/repair.py",
        "line": 481,
        "text": "api.set_metadata(belief_id, \"repair_action\", \"research\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/pg.py",
        "line": 1521,
        "text": "def set_metadata(self, node_id, key, value):"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/api.py",
        "line": 838,
        "text": "def set_metadata("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/api.py",
        "line": 847,
        "text": "return _pg_dispatch(pg_conninfo, project_id, \"set_metadata\","
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/api.py",
        "line": 3217,
        "text": "set_metadata(nid, \"repair_action\", \"rewritten\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/api.py",
        "line": 3226,
        "text": "set_metadata(nid, \"repair_action\", \"retracted\", db_path=db_path)"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/cli.py",
        "line": 397,
        "text": "def cmd_set_metadata(args):"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/cli.py",
        "line": 399,
        "text": "result = api.set_metadata("
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons/cli.py",
        "line": 2925,
        "text": "\"set-metadata\": cmd_set_metadata,"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 3173,
        "text": "from reasons_lib.api import set_metadata"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 3174,
        "text": "set_metadata(bid, \"source_file\", src_file)"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4111,
        "text": "from reasons_lib.api import set_metadata"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4116,
        "text": "set_metadata(bid, \"verified_at\", now)"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4241,
        "text": "from reasons_lib.api import set_metadata"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4261,
        "text": "set_metadata(bid, \"source_file\", candidate)"
      }
    ],
    "test_usages": [],
    "production_count": 19,
    "test_count": 0,
    "total_count": 19
  },
  "infer_sources_tests": {
    "source_file": "ftl_code_expert/cli.py",
    "test_files": [],
    "test_count": 0
  },
  "project_deps": {
    "repo": ".",
    "pyproject_toml": "[project]\nname = \"ftl-code-expert\"\nversion = \"0.8.0\"\ndescription = \"Build expert knowledge bases from codebases\"\nreadme = \"README.md\"\nrequires-python = \">=3.12\"\ndependencies = [\"click>=8.0\", \"ftl-reasons>=0.48.0\"]\n\n[project.optional-dependencies]\nembeddings = [\"fastembed>=0.7.4\", \"numpy\"]\n\n[project.scripts]\ncode-expert = \"ftl_code_expert.cli:cli\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"ftl_code_expert\"]\n\n[dependency-groups]\ndev = []\n",
    "dependencies": [
      "click>=8.0",
      "ftl-reasons>=0.48.0"
    ],
    "optional_dependencies": {
      "embeddings": [
        "fastembed>=0.7.4",
        "numpy"
      ]
    }
  },
  "verify_infer_prompt_usages": {
    "symbol": "VERIFY_INFER_FILE_PROMPT",
    "usages": [
      {
        "file": "ftl_code_expert/cli.py",
        "line": 37,
        "text": "VERIFY_INFER_FILE_PROMPT,"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 3154,
        "text": "infer_prompt = VERIFY_INFER_FILE_PROMPT.format("
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4230,
        "text": "VERIFY_INFER_FILE_PROMPT.format("
      },
      {
        "file": "ftl_code_expert/prompts/__init__.py",
        "line": 12,
        "text": "from .verify import VERIFY_INFER_FILE_PROMPT, VERIFY_OBSERVE_PROMPT, VERIFY_PROMPT"
      },
      {
        "file": "ftl_code_expert/prompts/__init__.py",
        "line": 24,
        "text": "\"VERIFY_INFER_FILE_PROMPT\","
      },
      {
        "file": "ftl_code_expert/prompts/verify.py",
        "line": 88,
        "text": "VERIFY_INFER_FILE_PROMPT = \"\"\"\\"
      }
    ],
    "production_usages": [
      {
        "file": "ftl_code_expert/cli.py",
        "line": 37,
        "text": "VERIFY_INFER_FILE_PROMPT,"
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 3154,
        "text": "infer_prompt = VERIFY_INFER_FILE_PROMPT.format("
      },
      {
        "file": "ftl_code_expert/cli.py",
        "line": 4230,
        "text": "VERIFY_INFER_FILE_PROMPT.format("
      },
      {
        "file": "ftl_code_expert/prompts/__init__.py",
        "line": 12,
        "text": "from .verify import VERIFY_INFER_FILE_PROMPT, VERIFY_OBSERVE_PROMPT, VERIFY_PROMPT"
      },
      {
        "file": "ftl_code_expert/prompts/__init__.py",
        "line": 24,
        "text": "\"VERIFY_INFER_FILE_PROMPT\","
      },
      {
        "file": "ftl_code_expert/prompts/verify.py",
        "line": 88,
        "text": "VERIFY_INFER_FILE_PROMPT = \"\"\"\\"
      }
    ],
    "test_usages": [],
    "production_count": 6,
    "test_count": 0,
    "total_count": 6
  }
}
```

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.
