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 c3a19c5..3a6b9e0 100644
--- a/ftl_project_expert/cli.py
+++ b/ftl_project_expert/cli.py
@@ -2013,6 +2013,82 @@ def derive(ctx, output, auto_add, exhaust, max_rounds, dry_run):
     click.echo("Or re-run with --auto to add automatically.")
 
 
+# --- review-beliefs ---
+
+
+@cli.command("review-beliefs")
+@click.option("--auto-retract", is_flag=True, default=False,
+              help="Automatically retract beliefs found invalid")
+@click.option("--sample", type=int, default=None,
+              help="Randomly sample N beliefs to review")
+@click.option("--min-depth", type=int, default=None,
+              help="Only review beliefs at this depth or deeper")
+@click.option("--dry-run", is_flag=True, default=False,
+              help="Report findings without taking action")
+@click.option("--output", "-o", default=None,
+              help="Write findings to markdown file")
+@click.pass_context
+def review_beliefs(ctx, auto_retract, sample, min_depth, dry_run, output):
+    """Review derived beliefs for validity using LLM evaluation."""
+    if not _has_reasons():
+        click.echo("Error: reasons CLI required. Install with: uv tool install ftl-reasons", err=True)
+        sys.exit(1)
+
+    model = ctx.obj["model"]
+    timeout = ctx.obj["timeout"]
+
+    cmd = ["reasons", "review-beliefs", "-m", model, "--timeout", str(timeout)]
+    if auto_retract:
+        cmd.append("--auto-retract")
+    if sample:
+        cmd.extend(["--sample", str(sample)])
+    if min_depth:
+        cmd.extend(["--min-depth", str(min_depth)])
+    if dry_run:
+        cmd.append("--dry-run")
+    if output:
+        cmd.extend(["-o", output])
+
+    click.echo(f"Reviewing beliefs with {model}...", err=True)
+    result = subprocess.run(cmd, text=True)
+    if result.returncode != 0:
+        sys.exit(result.returncode)
+
+    _reasons_export()
+
+
+# --- repair ---
+
+
+@cli.command("repair")
+@click.option("--review-file", default=None,
+              help="Path to review-beliefs JSON report")
+@click.option("--dry-run", is_flag=True, default=False,
+              help="Report findings without applying changes")
+@click.pass_context
+def repair(ctx, review_file, dry_run):
+    """Repair beliefs flagged by review-beliefs."""
+    if not _has_reasons():
+        click.echo("Error: reasons CLI required. Install with: uv tool install ftl-reasons", err=True)
+        sys.exit(1)
+
+    model = ctx.obj["model"]
+    timeout = ctx.obj["timeout"]
+
+    cmd = ["reasons", "repair", "-m", model, "--timeout", str(timeout)]
+    if review_file:
+        cmd.extend(["--review-file", review_file])
+    if dry_run:
+        cmd.append("--dry-run")
+
+    click.echo(f"Repairing beliefs with {model}...", err=True)
+    result = subprocess.run(cmd, text=True)
+    if result.returncode != 0:
+        sys.exit(result.returncode)
+
+    _reasons_export()
+
+
 # --- summary ---
 
 
@@ -2578,10 +2654,11 @@ def _load_update_checkpoint(project_dir: str) -> str | None:
               help="Max concurrent LLM calls (default: 1, try 3 for speed)")
 @click.pass_context
 def update(ctx, since, since_last, state, limit, all_pages, max_explore, max_parallel):
-    """Automated update pipeline: scan, explore, extract beliefs, derive, summarize.
+    """Automated update pipeline: scan, explore, extract beliefs, derive, review, repair, summarize.
 
     Pulls all issues/PRs updated since a date, explores them, proposes and
-    accepts beliefs, derives logical consequences, and generates a summary.
+    accepts beliefs, derives logical consequences, reviews and repairs
+    derived beliefs, and generates a summary.
 
     Examples:
         project-expert update --since 2026-04-01
@@ -2748,9 +2825,39 @@ def update(ctx, since, since_last, state, limit, all_pages, max_explore, max_par
         errors.append(f"derive: {e}")
         click.echo(f"WARN: derive failed: {e}, continuing...", err=True)
 
-    # --- Step 7: Summary ---
+    # --- Step 7: Review beliefs ---
+    click.echo(f"\n{'=' * 40}", err=True)
+    click.echo("Step 7: Reviewing derived beliefs", err=True)
+    click.echo(f"{'=' * 40}", err=True)
+
+    try:
+        ctx.invoke(review_beliefs, auto_retract=True)
+    except SystemExit as e:
+        if e.code and e.code != 0:
+            errors.append(f"review-beliefs exited with code {e.code}")
+            click.echo(f"WARN: review-beliefs failed (exit {e.code}), continuing...", err=True)
+    except Exception as e:
+        errors.append(f"review-beliefs: {e}")
+        click.echo(f"WARN: review-beliefs failed: {e}, continuing...", err=True)
+
+    # --- Step 8: Repair ---
+    click.echo(f"\n{'=' * 40}", err=True)
+    click.echo("Step 8: Repairing flagged beliefs", err=True)
+    click.echo(f"{'=' * 40}", err=True)
+
+    try:
+        ctx.invoke(repair)
+    except SystemExit as e:
+        if e.code and e.code != 0:
+            errors.append(f"repair exited with code {e.code}")
+            click.echo(f"WARN: repair failed (exit {e.code}), continuing...", err=True)
+    except Exception as e:
+        errors.append(f"repair: {e}")
+        click.echo(f"WARN: repair failed: {e}, continuing...", err=True)
+
+    # --- Step 9: Summary ---
     click.echo(f"\n{'=' * 40}", err=True)
-    click.echo("Step 7: Generating summary", err=True)
+    click.echo("Step 9: Generating summary", err=True)
     click.echo(f"{'=' * 40}", err=True)
 
     try:

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "has_reasons_body": {
    "function": "_has_reasons",
    "file": "ftl_project_expert/cli.py",
    "start_line": 121,
    "end_line": 122,
    "source": "def _has_reasons() -> bool:\n    return shutil.which(\"reasons\") is not None"
  },
  "reasons_export_body": {
    "function": "_reasons_export",
    "file": "ftl_project_expert/cli.py",
    "start_line": 147,
    "end_line": 161,
    "source": "def _reasons_export():\n    beliefs_path = Path(\"beliefs.md\")\n    network_path = Path(\"network.json\")\n    result = subprocess.run(\n        [\"reasons\", \"export-markdown\", \"-o\", str(beliefs_path)],\n        capture_output=True, text=True,\n    )\n    if result.returncode == 0:\n        click.echo(f\"Updated {beliefs_path}\")\n    result = subprocess.run(\n        [\"reasons\", \"export\", \"-o\", str(network_path)],\n        capture_output=True, text=True,\n    )\n    if result.returncode == 0:\n        click.echo(f\"Updated {network_path}\")"
  },
  "update_full_body": {
    "function": "update",
    "file": "ftl_project_expert/cli.py",
    "start_line": 2640,
    "end_line": 2894,
    "source": "@cli.command(\"update\")\n@click.option(\"--since\", default=None,\n              help=\"Fetch issues updated since date (YYYY-MM-DD)\")\n@click.option(\"--since-last\", is_flag=True, default=False,\n              help=\"Resume from last update checkpoint\")\n@click.option(\"--state\", \"-s\", default=None,\n              help=\"Issue state filter (default: all)\")\n@click.option(\"--limit\", default=100, type=int,\n              help=\"Max issues per page (default: 100)\")\n@click.option(\"--all-pages\", is_flag=True, default=False,\n              help=\"Auto-paginate through all matching issues\")\n@click.option(\"--max-explore\", type=int, default=None,\n              help=\"Max topics to explore (default: all pending)\")\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 update(ctx, since, since_last, state, limit, all_pages, max_explore, max_parallel):\n    \"\"\"Automated update pipeline: scan, explore, extract beliefs, derive, review, repair, summarize.\n\n    Pulls all issues/PRs updated since a date, explores them, proposes and\n    accepts beliefs, derives logical consequences, reviews and repairs\n    derived beliefs, and generates a summary.\n\n    Examples:\n        project-expert update --since 2026-04-01\n        project-expert update --since-last\n        project-expert update --since \"2026-04-01\" --all-pages\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    project_dir = _get_project_dir()\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    # Resolve since date\n    if since_last:\n        since = _load_update_checkpoint(project_dir)\n        if not since:\n            click.echo(\"No checkpoint found. Use --since <date> for the first run.\", err=True)\n            sys.exit(1)\n        click.echo(f\"Resuming from checkpoint: since {since}\", err=True)\n    elif not since:\n        click.echo(\"Error: --since <date> or --since-last required.\", err=True)\n        sys.exit(1)\n\n    # Default to \"all\" states for update (we want to see everything that changed)\n    if state is None:\n        state = \"all\"\n\n    # Snapshot pre-run belief IDs\n    try:\n        network = _load_network()\n        pre_run_ids = set(network.get(\"nodes\", {}).keys())\n    except Exception:\n        pre_run_ids = set()\n\n    errors = []\n    project_name = config.get(\"repo\", config.get(\"project\", \"unknown\"))\n    source = _get_source(config)\n\n    # --- Step 1: Fetch and scan issues ---\n    click.echo(f\"\\n{'=' * 40}\", err=True)\n    click.echo(\"Step 1: Scanning issues/PRs\", err=True)\n    click.echo(f\"{'=' * 40}\", err=True)\n\n    try:\n        label_list = None\n        total_issues = 0\n\n        if all_pages:\n            current_page = 1\n            while True:\n                click.echo(f\"  Fetching page {current_page}...\", err=True)\n                issues = _fetch_issues(source, config, state=state, labels=label_list,\n                                       limit=limit, page=current_page, since=since)\n                if not issues:\n                    break\n                total_issues += len(issues)\n                _run_scan_step(ctx, config, source, issues, project_name,\n                               state, limit, current_page, project_dir, model, timeout)\n                if len(issues) < limit:\n                    break\n                current_page += 1\n        else:\n            issues = _fetch_issues(source, config, state=state, labels=label_list,\n                                   limit=limit, page=1, since=since)\n            if issues:\n                total_issues = len(issues)\n                _run_scan_step(ctx, config, source, issues, project_name,\n                               state, limit, 1, project_dir, model, timeout)\n\n        if total_issues == 0:\n            click.echo(\"No issues found since {since}.\", err=True)\n        else:\n            click.echo(f\"Scanned {total_issues} issue(s).\", err=True)\n\n    except SystemExit as e:\n        if e.code and e.code != 0:\n            errors.append(f\"scan exited with code {e.code}\")\n            click.echo(f\"WARN: scan failed (exit {e.code}), continuing...\", err=True)\n    except Exception as e:\n        errors.append(f\"scan: {e}\")\n        click.echo(f\"WARN: scan failed: {e}, continuing...\", err=True)\n\n    # --- Step 2: Explore all pending topics ---\n    click.echo(f\"\\n{'=' * 40}\", err=True)\n    click.echo(\"Step 2: Exploring topics\", err=True)\n    click.echo(f\"{'=' * 40}\", err=True)\n\n    try:\n        topic_limit = max_explore or 99\n        _explore_loop(ctx, project_dir, topic_limit, max_parallel)\n\n    except SystemExit as e:\n        if e.code and e.code != 0:\n            errors.append(f\"explore exited with code {e.code}\")\n            click.echo(f\"WARN: explore failed (exit {e.code}), continuing...\", err=True)\n    except Exception as e:\n        errors.append(f\"explore: {e}\")\n        click.echo(f\"WARN: explore failed: {e}, continuing...\", err=True)\n\n    # --- Step 3: Propose beliefs ---\n    click.echo(f\"\\n{'=' * 40}\", err=True)\n    click.echo(\"Step 3: Proposing beliefs\", err=True)\n    click.echo(f\"{'=' * 40}\", err=True)\n\n    try:\n        ctx.invoke(propose_beliefs, since=since, max_parallel=max_parallel)\n    except SystemExit as e:\n        if e.code and e.code != 0:\n            errors.append(f\"propose-beliefs exited with code {e.code}\")\n            click.echo(f\"WARN: propose-beliefs failed (exit {e.code}), continuing...\", err=True)\n    except Exception as e:\n        errors.append(f\"propose-beliefs: {e}\")\n        click.echo(f\"WARN: propose-beliefs failed: {e}, continuing...\", err=True)\n\n    # --- Step 4: Review proposals ---\n    click.echo(f\"\\n{'=' * 40}\", err=True)\n    click.echo(\"Step 4: Reviewing proposals\", err=True)\n    click.echo(f\"{'=' * 40}\", err=True)\n\n    try:\n        ctx.invoke(review_proposals, max_parallel=max_parallel)\n    except SystemExit as e:\n        if e.code and e.code != 0:\n            errors.append(f\"review-proposals exited with code {e.code}\")\n            click.echo(f\"WARN: review-proposals failed (exit {e.code}), continuing...\", err=True)\n    except Exception as e:\n        errors.append(f\"review-proposals: {e}\")\n        click.echo(f\"WARN: review-proposals failed: {e}, continuing...\", err=True)\n\n    # --- Step 5: Accept beliefs ---\n    click.echo(f\"\\n{'=' * 40}\", err=True)\n    click.echo(\"Step 5: Accepting beliefs\", err=True)\n    click.echo(f\"{'=' * 40}\", err=True)\n\n    try:\n        ctx.invoke(accept_beliefs)\n    except SystemExit as e:\n        if e.code and e.code != 0:\n            errors.append(f\"accept-beliefs exited with code {e.code}\")\n            click.echo(f\"WARN: accept-beliefs failed (exit {e.code}), continuing...\", err=True)\n    except Exception as e:\n        errors.append(f\"accept-beliefs: {e}\")\n        click.echo(f\"WARN: accept-beliefs failed: {e}, continuing...\", err=True)\n\n    # --- Step 6: Derive (exhaust) ---\n    click.echo(f\"\\n{'=' * 40}\", err=True)\n    click.echo(\"Step 6: Deriving logical consequences\", err=True)\n    click.echo(f\"{'=' * 40}\", err=True)\n\n    try:\n        ctx.invoke(derive, exhaust=True)\n    except SystemExit as e:\n        if e.code and e.code != 0:\n            errors.append(f\"derive exited with code {e.code}\")\n            click.echo(f\"WARN: derive failed (exit {e.code}), continuing...\", err=True)\n    except Exception as e:\n        errors.append(f\"derive: {e}\")\n        click.echo(f\"WARN: derive failed: {e}, continuing...\", err=True)\n\n    # --- Step 7: Review beliefs ---\n    click.echo(f\"\\n{'=' * 40}\", err=True)\n    click.echo(\"Step 7: Reviewing derived beliefs\", err=True)\n    click.echo(f\"{'=' * 40}\", err=True)\n\n    try:\n        ctx.invoke(review_beliefs, auto_retract=True)\n    except SystemExit as e:\n        if e.code and e.code != 0:\n            errors.append(f\"review-beliefs exited with code {e.code}\")\n            click.echo(f\"WARN: review-beliefs failed (exit {e.code}), continuing...\", err=True)\n    except Exception as e:",
    "truncated": true,
    "total_lines": 255
  },
  "review_beliefs_callers": {
    "symbol": "review_beliefs",
    "production_callers": [
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2031,
        "text": "def review_beliefs(ctx, auto_retract, sample, min_depth, dry_run, output):",
        "context_function": "derive",
        "context_snippet": "   2028: @click.option(\"--output\", \"-o\", default=None,\n   2029:               help=\"Write findings to markdown file\")\n   2030: @click.pass_context\n>> 2031: def review_beliefs(ctx, auto_retract, sample, min_depth, dry_run, output):\n   2032:     \"\"\"Review derived beliefs for validity using LLM evaluation.\"\"\"\n   2033:     if not _has_reasons():\n   2034:         click.echo(\"Error: reasons CLI required. Install with: uv tool install ftl-reasons\", err=True)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2834,
        "text": "ctx.invoke(review_beliefs, auto_retract=True)",
        "context_function": "update",
        "context_snippet": "   2831:     click.echo(f\"{'=' * 40}\", err=True)\n   2832: \n   2833:     try:\n>> 2834:         ctx.invoke(review_beliefs, auto_retract=True)\n   2835:     except SystemExit as e:\n   2836:         if e.code and e.code != 0:\n   2837:             errors.append(f\"review-beliefs exited with code {e.code}\")"
      }
    ],
    "test_callers": [],
    "production_count": 2,
    "test_count": 0,
    "total_count": 2
  },
  "repair_callers": {
    "symbol": "repair",
    "production_callers": [
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2060,
        "text": "# --- repair ---",
        "context_function": "review_beliefs",
        "context_snippet": "   2057:     _reasons_export()\n   2058: \n   2059: \n>> 2060: # --- repair ---\n   2061: \n   2062: \n   2063: @cli.command(\"repair\")"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2063,
        "text": "@cli.command(\"repair\")",
        "context_function": "review_beliefs",
        "context_snippet": "   2060: # --- repair ---\n   2061: \n   2062: \n>> 2063: @cli.command(\"repair\")\n   2064: @click.option(\"--review-file\", default=None,\n   2065:               help=\"Path to review-beliefs JSON report\")\n   2066: @click.option(\"--dry-run\", is_flag=True, default=False,"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2069,
        "text": "def repair(ctx, review_file, dry_run):",
        "context_function": "review_beliefs",
        "context_snippet": "   2066: @click.option(\"--dry-run\", is_flag=True, default=False,\n   2067:               help=\"Report findings without applying changes\")\n   2068: @click.pass_context\n>> 2069: def repair(ctx, review_file, dry_run):\n   2070:     \"\"\"Repair beliefs flagged by review-beliefs.\"\"\"\n   2071:     if not _has_reasons():\n   2072:         click.echo(\"Error: reasons CLI required. Install with: uv tool install ftl-reasons\", err=True)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2078,
        "text": "cmd = [\"reasons\", \"repair\", \"-m\", model, \"--timeout\", str(timeout)]",
        "context_function": "repair",
        "context_snippet": "   2075:     model = ctx.obj[\"model\"]\n   2076:     timeout = ctx.obj[\"timeout\"]\n   2077: \n>> 2078:     cmd = [\"reasons\", \"repair\", \"-m\", model, \"--timeout\", str(timeout)]\n   2079:     if review_file:\n   2080:         cmd.extend([\"--review-file\", review_file])\n   2081:     if dry_run:"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2657,
        "text": "\"\"\"Automated update pipeline: scan, explore, extract beliefs, derive, review, repair, summarize.",
        "context_function": "update",
        "context_snippet": "   2654:               help=\"Max concurrent LLM calls (default: 1, try 3 for speed)\")\n   2655: @click.pass_context\n   2656: def update(ctx, since, since_last, state, limit, all_pages, max_explore, max_parallel):\n>> 2657:     \"\"\"Automated update pipeline: scan, explore, extract beliefs, derive, review, repair, summarize.\n   2658: \n   2659:     Pulls all issues/PRs updated since a date, explores them, proposes and\n   2660:     accepts beliefs, derives logical consequences, reviews and repairs"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2660,
        "text": "accepts beliefs, derives logical consequences, reviews and repairs",
        "context_function": "update",
        "context_snippet": "   2657:     \"\"\"Automated update pipeline: scan, explore, extract beliefs, derive, review, repair, summarize.\n   2658: \n   2659:     Pulls all issues/PRs updated since a date, explores them, proposes and\n>> 2660:     accepts beliefs, derives logical consequences, reviews and repairs\n   2661:     derived beliefs, and generates a summary.\n   2662: \n   2663:     Examples:"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2849,
        "text": "ctx.invoke(repair)",
        "context_function": "update",
        "context_snippet": "   2846:     click.echo(f\"{'=' * 40}\", err=True)\n   2847: \n   2848:     try:\n>> 2849:         ctx.invoke(repair)\n   2850:     except SystemExit as e:\n   2851:         if e.code and e.code != 0:\n   2852:             errors.append(f\"repair exited with code {e.code}\")"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2852,
        "text": "errors.append(f\"repair exited with code {e.code}\")",
        "context_function": "update",
        "context_snippet": "   2849:         ctx.invoke(repair)\n   2850:     except SystemExit as e:\n   2851:         if e.code and e.code != 0:\n>> 2852:             errors.append(f\"repair exited with code {e.code}\")\n   2853:             click.echo(f\"WARN: repair failed (exit {e.code}), continuing...\", err=True)\n   2854:     except Exception as e:\n   2855:         errors.append(f\"repair: {e}\")"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2853,
        "text": "click.echo(f\"WARN: repair failed (exit {e.code}), continuing...\", err=True)",
        "context_function": "update",
        "context_snippet": "   2850:     except SystemExit as e:\n   2851:         if e.code and e.code != 0:\n   2852:             errors.append(f\"repair exited with code {e.code}\")\n>> 2853:             click.echo(f\"WARN: repair failed (exit {e.code}), continuing...\", err=True)\n   2854:     except Exception as e:\n   2855:         errors.append(f\"repair: {e}\")\n   2856:         click.echo(f\"WARN: repair failed: {e}, continuing...\", err=True)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2855,
        "text": "errors.append(f\"repair: {e}\")",
        "context_function": "update",
        "context_snippet": "   2852:             errors.append(f\"repair exited with code {e.code}\")\n   2853:             click.echo(f\"WARN: repair failed (exit {e.code}), continuing...\", err=True)\n   2854:     except Exception as e:\n>> 2855:         errors.append(f\"repair: {e}\")\n   2856:         click.echo(f\"WARN: repair failed: {e}, continuing...\", err=True)\n   2857: \n   2858:     # --- Step 9: Summary ---"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 2856,
        "text": "click.echo(f\"WARN: repair failed: {e}, continuing...\", err=True)",
        "context_function": "update",
        "context_snippet": "   2853:             click.echo(f\"WARN: repair failed (exit {e.code}), continuing...\", err=True)\n   2854:     except Exception as e:\n   2855:         errors.append(f\"repair: {e}\")\n>> 2856:         click.echo(f\"WARN: repair failed: {e}, continuing...\", err=True)\n   2857: \n   2858:     # --- Step 9: Summary ---\n   2859:     click.echo(f\"\\n{'=' * 40}\", err=True)"
      }
    ],
    "test_callers": [],
    "production_count": 11,
    "test_count": 0,
    "total_count": 11
  },
  "review_beliefs_tests": {
    "source_file": "ftl_project_expert/cli.py",
    "test_files": [],
    "test_count": 0
  },
  "subprocess_run_raises": {
    "function": "review_beliefs",
    "file": "ftl_project_expert/cli.py",
    "explicit_raises": [],
    "calls": [
      "option",
      "command",
      "append",
      "str",
      "run",
      "exit",
      "_reasons_export",
      "_has_reasons",
      "extend",
      "echo"
    ]
  },
  "derive_body": {
    "function": "derive",
    "file": "ftl_project_expert/cli.py",
    "start_line": 1844,
    "end_line": 2013,
    "source": "@cli.command(\"derive\")\n@click.option(\"--output\", \"-o\", default=\"proposed-derivations.md\",\n              help=\"Output file (default: proposed-derivations.md)\")\n@click.option(\"--auto\", \"auto_add\", is_flag=True, default=False,\n              help=\"Automatically add proposals to reasons (no review step)\")\n@click.option(\"--exhaust\", \"exhaust\", is_flag=True, default=False,\n              help=\"Loop until no new derivations (implies --auto)\")\n@click.option(\"--max-rounds\", type=int, default=5,\n              help=\"Max derivation rounds for --exhaust (default: 5)\")\n@click.option(\"--dry-run\", is_flag=True, default=False,\n              help=\"Show what would be sent to the LLM without invoking it\")\n@click.pass_context\ndef derive(ctx, output, auto_add, exhaust, max_rounds, dry_run):\n    \"\"\"Derive deeper reasoning chains from existing beliefs.\n\n    Analyzes the belief network for opportunities to combine existing\n    conclusions into higher-level project claims, and to connect positive\n    and negative chains via outlist semantics.\n\n    Example:\n        project-expert derive              # propose derivations\n        project-expert derive --auto       # propose and add automatically\n        project-expert derive --exhaust    # loop until convergence\n    \"\"\"\n    from .prompts.derive import DERIVE_BELIEFS_PROMPT\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    if exhaust:\n        total_added = 0\n        for round_num in range(1, max_rounds + 1):\n            click.echo(f\"\\n--- Derive round {round_num}/{max_rounds} ---\", err=True)\n            try:\n                added = _derive_once(model, timeout)\n            except Exception as e:\n                click.echo(f\"Error in round {round_num}: {e}\", err=True)\n                break\n            total_added += added\n            if added == 0:\n                click.echo(\"No new derivations. Converged.\", err=True)\n                break\n            click.echo(f\"Round {round_num}: added {added} derivation(s).\", err=True)\n        click.echo(f\"\\nExhaust complete: {total_added} total derivation(s) across {round_num} round(s).\", err=True)\n        return\n\n    # Single-round mode\n    network = _load_network()\n    nodes = network.get(\"nodes\", {})\n    if not nodes:\n        click.echo(\"No beliefs found. Run explorations first.\", err=True)\n        sys.exit(1)\n\n    derived = {k: v for k, v in nodes.items()\n               if v.get(\"justifications\") and len(v[\"justifications\"]) > 0}\n    in_nodes = {k: v for k, v in nodes.items() if v.get(\"truth_value\") == \"IN\"}\n    memo = {}\n    max_depth = max((_get_depth(k, nodes, derived, memo) for k in derived), default=0)\n\n    click.echo(f\"Network: {len(in_nodes)} IN beliefs, {len(derived)} derived, max depth {max_depth}\", err=True)\n\n    beliefs_section = _build_beliefs_section(nodes, derived)\n    derived_section = _build_derived_section(nodes, derived)\n\n    prompt = DERIVE_BELIEFS_PROMPT.format(\n        beliefs_section=beliefs_section,\n        derived_section=derived_section,\n        total_in=len(in_nodes),\n        total_derived=len(derived),\n        max_depth=max_depth,\n    )\n\n    if dry_run:\n        click.echo(f\"\\n=== Prompt ({len(prompt)} chars) ===\\n\")\n        click.echo(prompt[:3000])\n        if len(prompt) > 3000:\n            click.echo(f\"\\n... ({len(prompt) - 3000} more chars)\")\n        return\n\n    click.echo(f\"Deriving with {model}...\", err=True)\n    try:\n        result = asyncio.run(invoke(prompt, model, timeout=timeout))\n    except Exception as e:\n        click.echo(f\"Error: {e}\", err=True)\n        sys.exit(1)\n\n    proposals = _parse_derive_proposals(result)\n\n    if not proposals:\n        click.echo(\"No derivation proposals found in response.\")\n        click.echo(\"\\nRaw response:\\n\")\n        click.echo(result)\n        return\n\n    valid = []\n    for p in proposals:\n        missing = [a for a in p[\"antecedents\"] if a not in nodes]\n        missing_unless = [u for u in p[\"unless\"] if u not in nodes]\n        if missing or missing_unless:\n            click.echo(f\"  SKIP {p['id']}: missing nodes {missing + missing_unless}\", err=True)\n            continue\n        if p[\"id\"] in nodes:\n            click.echo(f\"  SKIP {p['id']}: already exists\", err=True)\n            continue\n        valid.append(p)\n\n    click.echo(f\"\\n{len(valid)} valid proposals ({len(proposals) - len(valid)} skipped)\", err=True)\n\n    if not valid:\n        return\n\n    if auto_add:\n        added = 0\n        for p in valid:\n            cmd = [\n                \"reasons\", \"add\", p[\"id\"], p[\"text\"],\n                \"--sl\", \",\".join(p[\"antecedents\"]),\n                \"--label\", p[\"label\"],\n            ]\n            if p[\"unless\"]:\n                cmd.extend([\"--unless\", \",\".join(p[\"unless\"])])\n\n            r = subprocess.run(cmd, capture_output=True, text=True)\n            if r.returncode == 0:\n                status = \"IN\" if \"IN\" in r.stdout else \"OUT\"\n                click.echo(f\"  Added {p['id']} [{status}]\")\n                added += 1\n            else:\n                click.echo(f\"  FAIL {p['id']}: {r.stderr.strip() or r.stdout.strip()}\", err=True)\n\n        if added:\n            click.echo(f\"\\nAdded {added} derived beliefs.\", err=True)\n            _reasons_export()\n        return\n\n    # Write proposals file for review\n    output_path = Path(output)\n    with output_path.open(\"w\") as f:\n        f.write(\"# Proposed Derivations\\n\\n\")\n        f.write(\"Review each proposal below. To accept, run:\\n\\n\")\n        f.write(\"```bash\\n\")\n        for p in valid:\n            sl = \",\".join(p[\"antecedents\"])\n            cmd = f'reasons add {p[\"id\"]} \"{p[\"text\"]}\" --sl {sl}'\n            if p[\"unless\"]:\n                cmd += f' --unless {\",\".join(p[\"unless\"])}'\n            cmd += f' --label \"{p[\"label\"]}\"'\n            f.write(f\"{cmd}\\n\")\n        f.write(\"```\\n\\n---\\n\\n\")\n\n        for p in valid:\n            kind_label = \"DERIVE\" if p[\"kind\"] == \"derive\" else \"GATE (outlist)\"\n            f.write(f\"### {kind_label}: `{p['id']}`\\n\\n\")\n            f.write(f\"{p['text']}\\n\\n\")\n            f.write(f\"- **Antecedents**: {', '.join(f'`{a}`' for a in p['antecedents'])}\\n\")\n            if p[\"unless\"]:\n                f.write(f\"- **Unless**: {', '.join(f'`{u}`' for u in p['unless'])}\\n\")\n            f.write(f\"- **Label**: {p['label']}\\n\\n\")\n\n    click.echo(f\"\\nWrote {output_path} ({len(valid)} proposals)\")\n    click.echo(\"Review, then run the commands from the file to accept.\")\n    click.echo(\"Or re-run with --auto to add automatically.\")"
  }
}
```

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.
