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 fd99f16..7b7d0b8 100644
--- a/ftl_code_expert/cli.py
+++ b/ftl_code_expert/cli.py
@@ -4029,20 +4029,37 @@ def verify(ctx, belief_ids, category, gated, negative, verify_all, retract, dry_
         elif verdict == "STALE":
             stale.append(bid)
             click.echo(f"  {bid}: STALE — {reason}", err=True)
         else:
             inconclusive.append(bid)
             click.echo(f"  {bid}: INCONCLUSIVE — {reason}", err=True)
 
     click.echo(f"\nResults: {len(confirmed)} confirmed, {len(stale)} stale, "
                f"{len(inconclusive)} inconclusive", err=True)
 
+    # Stamp verified_at on confirmed beliefs
+    if confirmed:
+        try:
+            from reasons_lib.api import _with_network
+            from datetime import datetime, timezone
+            with _with_network("reasons.db", write=True) as net:
+                now = datetime.now(timezone.utc).isoformat(timespec="seconds")
+                stamped = 0
+                for bid in confirmed:
+                    if bid in net.nodes:
+                        net.nodes[bid].verified_at = now
+                        stamped += 1
+                if stamped:
+                    click.echo(f"Stamped verified_at on {stamped} belief(s)", err=True)
+        except Exception as e:
+            click.echo(f"  Could not stamp verified_at: {e}", err=True)
+
     # Retract stale beliefs
     if retract and stale and _has_reasons():
         click.echo(f"\nRetracting {len(stale)} stale belief(s)...", err=True)
         for bid in stale:
             reason = all_results.get(bid, {}).get("reason", "stale per verify")
             result = subprocess.run(
                 ["reasons", "retract", bid, "--reason", reason],
                 capture_output=True, text=True,
             )
             if result.returncode == 0:
diff --git a/pyproject.toml b/pyproject.toml
index a76bd77..4eddae0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,10 +11,18 @@ embeddings = ["fastembed>=0.7.4", "numpy"]
 
 [project.scripts]
 code-expert = "ftl_code_expert.cli:cli"
 
 [build-system]
 requires = ["hatchling"]
 build-backend = "hatchling.build"
 
 [tool.hatch.build.targets.wheel]
 packages = ["ftl_code_expert"]
+
+[tool.uv.sources]
+ftl-reasons = { path = "../ftl-reasons" }
+
+[dependency-groups]
+dev = [
+    "ftl-reasons",
+]

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "ftl_code_expert/cli.py:verify": {
    "function": "verify",
    "file": "ftl_code_expert/cli.py",
    "start_line": 3860,
    "end_line": 4072,
    "source": "@cli.command(\"verify\")\n@click.argument(\"belief_ids\", nargs=-1)\n@click.option(\"--category\", default=None,\n              help=\"Verify IN beliefs matching keyword in ID or text\")\n@click.option(\"--gated\", is_flag=True, default=False,\n              help=\"Verify IN beliefs that actively gate downstream chains\")\n@click.option(\"--negative\", is_flag=True, default=False,\n              help=\"Verify negative IN beliefs (bugs, gaps, risks)\")\n@click.option(\"--all\", \"verify_all\", is_flag=True, default=False,\n              help=\"Verify all IN beliefs (expensive)\")\n@click.option(\"--retract\", is_flag=True, default=False,\n              help=\"Retract STALE beliefs via reasons\")\n@click.option(\"--dry-run\", is_flag=True, default=False,\n              help=\"Show what would be verified without calling LLM\")\n@click.option(\"--batch-size\", type=int, default=10,\n              help=\"Beliefs per LLM batch (default: 10)\")\n@click.option(\"--no-observe\", is_flag=True, default=False,\n              help=\"Skip observation loop; use simple file read + grep for context\")\n@click.pass_context\ndef verify(ctx, belief_ids, category, gated, negative, verify_all, retract, dry_run, batch_size, no_observe):\n    \"\"\"Check whether beliefs still hold against current source code.\n\n    Reads the current source code for each belief and asks an LLM whether\n    the claim is CONFIRMED, STALE, or INCONCLUSIVE.\n\n    Examples:\n        code-expert verify login-audit-always-success-info\n        code-expert verify --category auth\n        code-expert verify --gated\n        code-expert verify --negative --retract\n        code-expert verify --all --dry-run\n    \"\"\"\n    from .caffeinate import hold as _caffeinate\n    _caffeinate()\n\n    model = ctx.obj[\"model\"]\n    timeout = ctx.obj[\"timeout\"]\n    repo_path = _get_repo(ctx)\n    abs_repo = os.path.abspath(repo_path)\n    project_dir = _get_project_dir(ctx)\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    # Re-export to ensure fresh metadata (fixes stale network.json)\n    if not dry_run and _has_reasons():\n        _reasons_export()\n\n    # Load belief network\n    try:\n        network = _load_network()\n        nodes = network.get(\"nodes\", {})\n    except Exception as e:\n        click.echo(f\"Error loading belief network: {e}\", err=True)\n        sys.exit(1)\n\n    if not nodes:\n        click.echo(\"No beliefs found. Run explorations and propose-beliefs first.\")\n        return\n\n    # Select beliefs to verify\n    beliefs: list[dict] = []\n\n    if belief_ids:\n        for bid in belief_ids:\n            node = nodes.get(bid)\n            if node:\n                beliefs.append({\"id\": bid, \"text\": node.get(\"text\", \"\")})\n            else:\n                click.echo(f\"  Belief not found: {bid}\", err=True)\n\n    elif gated:\n        gated_beliefs = _find_gated_out_beliefs(nodes)\n        blocker_ids = set()\n        for gb in gated_beliefs:\n            for blocker in gb.get(\"blockers\", []):\n                blocker_ids.add(blocker[\"id\"])\n        for bid in blocker_ids:\n            node = nodes.get(bid, {})\n            beliefs.append({\"id\": bid, \"text\": node.get(\"text\", \"\")})\n        click.echo(f\"Found {len(beliefs)} active blocker(s) gating {len(gated_beliefs)} downstream belief(s)\", err=True)\n\n    elif negative:\n        beliefs = _get_negative_beliefs(nodes, model=model)\n        click.echo(f\"Found {len(beliefs)} negative IN belief(s)\", err=True)\n\n    elif category:\n        keyword = category.lower()\n        for nid, node in nodes.items():\n            if node.get(\"truth_value\") != \"IN\":\n                continue\n            if keyword in nid.lower() or keyword in node.get(\"text\", \"\").lower():\n                beliefs.append({\"id\": nid, \"text\": node.get(\"text\", \"\")})\n        click.echo(f\"Found {len(beliefs)} IN belief(s) matching '{category}'\", err=True)\n\n    elif verify_all:\n        for nid, node in nodes.items():\n            if node.get(\"truth_value\") == \"IN\":\n                beliefs.append({\"id\": nid, \"text\": node.get(\"text\", \"\")})\n        click.echo(f\"Found {len(beliefs)} IN belief(s) to verify\", err=True)\n\n    else:\n        click.echo(\"Specify belief IDs, or use --category, --gated, --negative, or --all.\", err=True)\n        sys.exit(1)\n\n    if not beliefs:\n        click.echo(\"No beliefs to verify.\")\n        return\n\n    if dry_run:\n        click.echo(f\"\\n{len(beliefs)} belief(s) would be verified:\", err=True)\n        for b in beliefs:\n            click.echo(f\"  {b['id']}: {b['text'][:100]}\", err=True)\n        click.echo(\"\\n--dry-run: stopping before LLM verification.\", err=True)\n        return\n\n    # Verify in batches\n    all_results: dict[str, dict] = {}\n    batches = [beliefs[i:i + batch_size] for i in range(0, len(beliefs), batch_size)]\n\n    for i, batch in enumerate(batches):\n        click.echo(f\"\\nVerifying batch {i + 1}/{len(batches)} ({len(batch)} beliefs)...\", err=True)\n\n        if no_observe:\n            contexts = asyncio.run(\n                _gather_confirmation_context(batch, nodes, abs_repo, project_dir)\n            )\n        else:\n            contexts = asyncio.run(\n                _gather_verify_contexts(batch, nodes, abs_repo, project_dir, model, timeout)\n            )\n\n        beliefs_section = []\n        for belief in batch:\n            ctx_text = contexts.get(belief[\"id\"], \"(no code context found)\")\n            beliefs_section.append(\n                f\"### `{belief['id']}`\\n{belief['text']}\\n\\n\"\n                f\"**Code context:**\\n{ctx_text}\"\n            )\n\n        prompt = VERIFY_PROMPT.format(beliefs=\"\\n\\n---\\n\\n\".join(beliefs_section))\n\n        try:\n            response = invoke_sync(prompt, model=model, timeout=timeout)\n            results = _parse_verify_response(response)\n            all_results.update(results)\n        except Exception as e:\n            click.echo(f\"  Error: {e}\", err=True)\n\n    # Report results\n    confirmed = []\n    stale = []\n    inconclusive = []\n\n    for belief in beliefs:\n        bid = belief[\"id\"]\n        result = all_results.get(bid)\n        if not result:\n            inconclusive.append(bid)\n            click.echo(f\"  {bid}: INCONCLUSIVE (no verdict returned)\", err=True)\n            continue\n\n        verdict = result[\"verdict\"]\n        reason = result.get(\"reason\", \"\")\n\n        if verdict == \"CONFIRMED\":\n            confirmed.append(bid)\n            click.echo(f\"  {bid}: CONFIRMED \u2014 {reason}\", err=True)\n        elif verdict == \"STALE\":\n            stale.append(bid)\n            click.echo(f\"  {bid}: STALE \u2014 {reason}\", err=True)\n        else:\n            inconclusive.append(bid)\n            click.echo(f\"  {bid}: INCONCLUSIVE \u2014 {reason}\", err=True)\n\n    click.echo(f\"\\nResults: {len(confirmed)} confirmed, {len(stale)} stale, \"\n               f\"{len(inconclusive)} inconclusive\", err=True)\n\n    # Stamp verified_at on confirmed beliefs\n    if confirmed:\n        try:\n            from reasons_lib.api import _with_network\n            from datetime import datetime, timezone\n            with _with_network(\"reasons.db\", write=True) as net:\n                now = datetime.now(timezone.utc).isoformat(timespec=\"seconds\")\n                stamped = 0\n                for bid in confirmed:\n                    if bid in net.nodes:\n                        net.nodes[bid].verified_at = now\n                        stamped += 1\n                if stamped:\n                    click.echo(f\"Stamped verified_at on {stamped} belief(s)\", err=True)\n        except Exception as e:\n            click.echo(f\"  Could not stamp verified_at: {e}\", err=True)\n\n    # Retract stale beliefs\n    if retract and stale and _has_reasons():\n        click.echo(f\"\\nRetracting {len(stale)} stale belief(s)...\", err=True)\n        for bid in stale:",
    "truncated": true,
    "total_lines": 213
  },
  "with_network_body": {
    "error": "File not found: reasons_lib/api.py",
    "file": "reasons_lib/api.py"
  },
  "with_network_usages": {
    "symbol": "_with_network",
    "usages": [
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 55,
        "text": "def _with_network(db_path: str, write: bool = False):"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 120,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 143,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 226,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 339,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 375,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 415,
        "text": "with _with_network(db_path, write=False) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 479,
        "text": "with _with_network(db_path, write=False) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 557,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 569,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 584,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 610,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 650,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 673,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 697,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 717,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 732,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 744,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 764,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 777,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 798,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 833,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 859,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 879,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 893,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 917,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 931,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 948,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 1020,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 1081,
        "text": "with _with_network(db_path, write=True) as net:"
      }
    ],
    "production_usages": [
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 55,
        "text": "def _with_network(db_path: str, write: bool = False):"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 120,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 143,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 226,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 339,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 375,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 415,
        "text": "with _with_network(db_path, write=False) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 479,
        "text": "with _with_network(db_path, write=False) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 557,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 569,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 584,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 610,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 650,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 673,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 697,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 717,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 732,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 744,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 764,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 777,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 798,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 833,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 859,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 879,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 893,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 917,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 931,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 948,
        "text": "with _with_network(db_path) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 1020,
        "text": "with _with_network(db_path, write=True) as net:"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/reasons_lib/api.py",
        "line": 1081,
        "text": "with _with_network(db_path, write=True) as net:"
      }
    ],
    "test_usages": [],
    "production_count": 58,
    "test_count": 0,
    "total_count": 58
  },
  "verify_function_body": {
    "function": "verify",
    "file": "ftl_code_expert/cli.py",
    "start_line": 3860,
    "end_line": 4072,
    "source": "@cli.command(\"verify\")\n@click.argument(\"belief_ids\", nargs=-1)\n@click.option(\"--category\", default=None,\n              help=\"Verify IN beliefs matching keyword in ID or text\")\n@click.option(\"--gated\", is_flag=True, default=False,\n              help=\"Verify IN beliefs that actively gate downstream chains\")\n@click.option(\"--negative\", is_flag=True, default=False,\n              help=\"Verify negative IN beliefs (bugs, gaps, risks)\")\n@click.option(\"--all\", \"verify_all\", is_flag=True, default=False,\n              help=\"Verify all IN beliefs (expensive)\")\n@click.option(\"--retract\", is_flag=True, default=False,\n              help=\"Retract STALE beliefs via reasons\")\n@click.option(\"--dry-run\", is_flag=True, default=False,\n              help=\"Show what would be verified without calling LLM\")\n@click.option(\"--batch-size\", type=int, default=10,\n              help=\"Beliefs per LLM batch (default: 10)\")\n@click.option(\"--no-observe\", is_flag=True, default=False,\n              help=\"Skip observation loop; use simple file read + grep for context\")\n@click.pass_context\ndef verify(ctx, belief_ids, category, gated, negative, verify_all, retract, dry_run, batch_size, no_observe):\n    \"\"\"Check whether beliefs still hold against current source code.\n\n    Reads the current source code for each belief and asks an LLM whether\n    the claim is CONFIRMED, STALE, or INCONCLUSIVE.\n\n    Examples:\n        code-expert verify login-audit-always-success-info\n        code-expert verify --category auth\n        code-expert verify --gated\n        code-expert verify --negative --retract\n        code-expert verify --all --dry-run\n    \"\"\"\n    from .caffeinate import hold as _caffeinate\n    _caffeinate()\n\n    model = ctx.obj[\"model\"]\n    timeout = ctx.obj[\"timeout\"]\n    repo_path = _get_repo(ctx)\n    abs_repo = os.path.abspath(repo_path)\n    project_dir = _get_project_dir(ctx)\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    # Re-export to ensure fresh metadata (fixes stale network.json)\n    if not dry_run and _has_reasons():\n        _reasons_export()\n\n    # Load belief network\n    try:\n        network = _load_network()\n        nodes = network.get(\"nodes\", {})\n    except Exception as e:\n        click.echo(f\"Error loading belief network: {e}\", err=True)\n        sys.exit(1)\n\n    if not nodes:\n        click.echo(\"No beliefs found. Run explorations and propose-beliefs first.\")\n        return\n\n    # Select beliefs to verify\n    beliefs: list[dict] = []\n\n    if belief_ids:\n        for bid in belief_ids:\n            node = nodes.get(bid)\n            if node:\n                beliefs.append({\"id\": bid, \"text\": node.get(\"text\", \"\")})\n            else:\n                click.echo(f\"  Belief not found: {bid}\", err=True)\n\n    elif gated:\n        gated_beliefs = _find_gated_out_beliefs(nodes)\n        blocker_ids = set()\n        for gb in gated_beliefs:\n            for blocker in gb.get(\"blockers\", []):\n                blocker_ids.add(blocker[\"id\"])\n        for bid in blocker_ids:\n            node = nodes.get(bid, {})\n            beliefs.append({\"id\": bid, \"text\": node.get(\"text\", \"\")})\n        click.echo(f\"Found {len(beliefs)} active blocker(s) gating {len(gated_beliefs)} downstream belief(s)\", err=True)\n\n    elif negative:\n        beliefs = _get_negative_beliefs(nodes, model=model)\n        click.echo(f\"Found {len(beliefs)} negative IN belief(s)\", err=True)\n\n    elif category:\n        keyword = category.lower()\n        for nid, node in nodes.items():\n            if node.get(\"truth_value\") != \"IN\":\n                continue\n            if keyword in nid.lower() or keyword in node.get(\"text\", \"\").lower():\n                beliefs.append({\"id\": nid, \"text\": node.get(\"text\", \"\")})\n        click.echo(f\"Found {len(beliefs)} IN belief(s) matching '{category}'\", err=True)\n\n    elif verify_all:\n        for nid, node in nodes.items():\n            if node.get(\"truth_value\") == \"IN\":\n                beliefs.append({\"id\": nid, \"text\": node.get(\"text\", \"\")})\n        click.echo(f\"Found {len(beliefs)} IN belief(s) to verify\", err=True)\n\n    else:\n        click.echo(\"Specify belief IDs, or use --category, --gated, --negative, or --all.\", err=True)\n        sys.exit(1)\n\n    if not beliefs:\n        click.echo(\"No beliefs to verify.\")\n        return\n\n    if dry_run:\n        click.echo(f\"\\n{len(beliefs)} belief(s) would be verified:\", err=True)\n        for b in beliefs:\n            click.echo(f\"  {b['id']}: {b['text'][:100]}\", err=True)\n        click.echo(\"\\n--dry-run: stopping before LLM verification.\", err=True)\n        return\n\n    # Verify in batches\n    all_results: dict[str, dict] = {}\n    batches = [beliefs[i:i + batch_size] for i in range(0, len(beliefs), batch_size)]\n\n    for i, batch in enumerate(batches):\n        click.echo(f\"\\nVerifying batch {i + 1}/{len(batches)} ({len(batch)} beliefs)...\", err=True)\n\n        if no_observe:\n            contexts = asyncio.run(\n                _gather_confirmation_context(batch, nodes, abs_repo, project_dir)\n            )\n        else:\n            contexts = asyncio.run(\n                _gather_verify_contexts(batch, nodes, abs_repo, project_dir, model, timeout)\n            )\n\n        beliefs_section = []\n        for belief in batch:\n            ctx_text = contexts.get(belief[\"id\"], \"(no code context found)\")\n            beliefs_section.append(\n                f\"### `{belief['id']}`\\n{belief['text']}\\n\\n\"\n                f\"**Code context:**\\n{ctx_text}\"\n            )\n\n        prompt = VERIFY_PROMPT.format(beliefs=\"\\n\\n---\\n\\n\".join(beliefs_section))\n\n        try:\n            response = invoke_sync(prompt, model=model, timeout=timeout)\n            results = _parse_verify_response(response)\n            all_results.update(results)\n        except Exception as e:\n            click.echo(f\"  Error: {e}\", err=True)\n\n    # Report results\n    confirmed = []\n    stale = []\n    inconclusive = []\n\n    for belief in beliefs:\n        bid = belief[\"id\"]\n        result = all_results.get(bid)\n        if not result:\n            inconclusive.append(bid)\n            click.echo(f\"  {bid}: INCONCLUSIVE (no verdict returned)\", err=True)\n            continue\n\n        verdict = result[\"verdict\"]\n        reason = result.get(\"reason\", \"\")\n\n        if verdict == \"CONFIRMED\":\n            confirmed.append(bid)\n            click.echo(f\"  {bid}: CONFIRMED \u2014 {reason}\", err=True)\n        elif verdict == \"STALE\":\n            stale.append(bid)\n            click.echo(f\"  {bid}: STALE \u2014 {reason}\", err=True)\n        else:\n            inconclusive.append(bid)\n            click.echo(f\"  {bid}: INCONCLUSIVE \u2014 {reason}\", err=True)\n\n    click.echo(f\"\\nResults: {len(confirmed)} confirmed, {len(stale)} stale, \"\n               f\"{len(inconclusive)} inconclusive\", err=True)\n\n    # Stamp verified_at on confirmed beliefs\n    if confirmed:\n        try:\n            from reasons_lib.api import _with_network\n            from datetime import datetime, timezone\n            with _with_network(\"reasons.db\", write=True) as net:\n                now = datetime.now(timezone.utc).isoformat(timespec=\"seconds\")\n                stamped = 0\n                for bid in confirmed:\n                    if bid in net.nodes:\n                        net.nodes[bid].verified_at = now\n                        stamped += 1\n                if stamped:\n                    click.echo(f\"Stamped verified_at on {stamped} belief(s)\", err=True)\n        except Exception as e:\n            click.echo(f\"  Could not stamp verified_at: {e}\", err=True)\n\n    # Retract stale beliefs\n    if retract and stale and _has_reasons():\n        click.echo(f\"\\nRetracting {len(stale)} stale belief(s)...\", err=True)\n        for bid in stale:",
    "truncated": true,
    "total_lines": 213
  },
  "project_deps": {
    "repo": "/Users/ben/git/code-expert",
    "pyproject_toml": "[project]\nname = \"ftl-code-expert\"\nversion = \"0.7.0\"\ndescription = \"Build expert knowledge bases from codebases\"\nreadme = \"README.md\"\nrequires-python = \">=3.12\"\ndependencies = [\"click>=8.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[tool.uv.sources]\nftl-reasons = { path = \"../ftl-reasons\" }\n\n[dependency-groups]\ndev = [\n    \"ftl-reasons\",\n]\n",
    "dependencies": [
      "click>=8.0"
    ],
    "optional_dependencies": {
      "embeddings": [
        "fastembed>=0.7.4",
        "numpy"
      ]
    }
  },
  "verify_tests": {
    "source_file": "ftl_code_expert/cli.py",
    "test_files": [],
    "test_count": 0
  },
  "with_network_raises": {
    "error": "File not found: reasons_lib/api.py",
    "function": "_with_network"
  },
  "cli_imports": {
    "file": "ftl_code_expert/cli.py",
    "imports": [
      "asyncio",
      "json",
      "os",
      "re",
      "shutil",
      "subprocess",
      "sys",
      "click"
    ],
    "from_imports": [
      {
        "module": "datetime",
        "names": [
          "date",
          "datetime"
        ]
      },
      {
        "module": "pathlib",
        "names": [
          "Path"
        ]
      },
      {
        "module": "git_utils",
        "names": [
          "commits_since_checkpoint",
          "extract_symbol",
          "find_related_tests",
          "get_commit_log",
          "get_diff",
          "get_diff_since",
          "get_diff_since_commit",
          "get_file_content",
          "get_imports",
          "get_repo_structure",
          "list_commits_with_files",
          "load_diff_checkpoint",
          "save_diff_checkpoint"
        ]
      },
      {
        "module": "llm",
        "names": [
          "check_model_available",
          "invoke",
          "invoke_concurrent",
          "invoke_concurrent_sync",
          "invoke_sync"
        ]
      },
      {
        "module": "observations",
        "names": [
          "parse_observation_requests",
          "run_observations"
        ]
      },
      {
        "module": "prompts",
        "names": [
          "PROPOSE_BELIEFS_CODE",
          "RESEARCH_INFER_FILES_PROMPT",
          "REVIEW_PROMPT",
          "VERIFY_OBSERVE_PROMPT",
          "VERIFY_PROMPT",
          "build_diff_prompt",
          "build_diff_summary_prompt",
          "build_file_prompt",
          "build_function_prompt",
          "build_observe_prompt",
          "build_repo_prompt",
          "build_scan_prompt"
        ]
      },
      {
        "module": "topics",
        "names": [
          "add_topics",
          "load_queue",
          "parse_topics_from_response",
          "pending_count",
          "pop_at",
          "pop_batch",
          "pop_multiple",
          "pop_next",
          "skip_topic"
        ]
      }
    ],
    "import_section": "\"\"\"Command-line interface for code 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 .git_utils import (\n    commits_since_checkpoint,\n    extract_symbol,\n    find_related_tests,\n    get_commit_log,\n    get_diff,\n    get_diff_since,\n    get_diff_since_commit,\n    get_file_content,\n    get_imports,\n    get_repo_structure,\n    list_commits_with_files,\n    load_diff_checkpoint,\n    save_diff_checkpoint,\n)\nfrom .llm import check_model_available, invoke, invoke_concurrent, invoke_concurrent_sync, invoke_sync\nfrom .observations import parse_observation_requests, run_observations\nfrom .prompts import (\n    PROPOSE_BELIEFS_CODE,\n    RESEARCH_INFER_FILES_PROMPT,\n    REVIEW_PROMPT,\n    VERIFY_OBSERVE_PROMPT,\n    VERIFY_PROMPT,\n    build_diff_prompt,\n    build_diff_summary_prompt,\n    build_file_prompt,\n    build_function_prompt,\n    build_observe_prompt,\n    build_repo_prompt,\n    build_scan_prompt,\n)\nfrom .topics import (\n    add_topics,\n    load_queue,\n    parse_topics_from_response,\n    pending_count,\n    pop_at,\n    pop_batch,\n    pop_multiple,\n    pop_next,\n    skip_topic,\n)\n\nPROJECT_DIR = \".code-expert\"\n\n\n# --- Config helpers ---\n\n\ndef _load_config() -> dict | None:\n    \"\"\"Load .code-expert/config.json if it exists.\"\"\""
  }
}
```

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.
