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 445a0dd..8c3a5ca 100644
--- a/ftl_project_expert/cli.py
+++ b/ftl_project_expert/cli.py
@@ -164,15 +164,36 @@ def _reasons_export():
 # --- CLI ---
 
 
+def _load_env_file(path):
+    """Load KEY=VALUE pairs from a file into os.environ (existing vars take precedence)."""
+    try:
+        with open(path) as f:
+            for line in f:
+                line = line.strip()
+                if not line or line.startswith("#"):
+                    continue
+                if "=" not in line:
+                    continue
+                key, _, value = line.partition("=")
+                key = key.strip()
+                value = value.strip().strip("'\"")
+                if key and key not in os.environ:
+                    os.environ[key] = value
+    except FileNotFoundError:
+        pass
+
+
 @click.group()
 @click.version_option(package_name="ftl-project-expert")
 @click.option("--quiet", "-q", is_flag=True, default=False,
               help="Suppress output to stdout")
 @click.option("--model", "-m", default="claude", help="Model to use (default: claude)")
 @click.option("--timeout", "-t", default=300, type=int, help="LLM timeout in seconds")
+@click.option("--env-file", default=".env", help="Path to .env file (default: .env)")
 @click.pass_context
-def cli(ctx, quiet, model, timeout):
+def cli(ctx, quiet, model, timeout, env_file):
     """Build expert knowledge bases from project management data."""
+    _load_env_file(env_file)
     ctx.ensure_object(dict)
     ctx.obj["quiet"] = quiet
     ctx.obj["model"] = model

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "cli_callers": {
    "symbol": "cli",
    "production_callers": [
      {
        "file": "ftl_project_expert/cli.py",
        "line": 13,
        "text": "import click",
        "context_function": null,
        "context_snippet": "   10: from datetime import date, datetime\n   11: from pathlib import Path\n   12: \n>> 13: import click\n   14: \n   15: from .llm import check_model_available, invoke, invoke_concurrent_sync, invoke_sync\n   16: from .prompts import ("
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 84,
        "text": "click.echo(text)",
        "context_function": "_emit",
        "context_snippet": "   81: \n   82: def _emit(ctx, text: str) -> None:\n   83:     if not ctx.obj.get(\"quiet\"):\n>> 84:         click.echo(text)\n   85: \n   86: \n   87: def _create_entry(topic: str, title: str, content: str) -> None:"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 97,
        "text": "click.echo(f\"Entry: {result.stdout.strip()}\", err=True)",
        "context_function": "_create_entry",
        "context_snippet": "   94:             capture_output=True, text=True,\n   95:         )\n   96:         if result.returncode == 0:\n>> 97:             click.echo(f\"Entry: {result.stdout.strip()}\", err=True)\n   98:         else:\n   99:             result = subprocess.run(\n   100:                 [\"entry\", \"create\", entry_name, title],"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 105,
        "text": "click.echo(f\"Entry: {result.stdout.strip()}\", err=True)",
        "context_function": "_create_entry",
        "context_snippet": "   102:                 capture_output=True, text=True,\n   103:             )\n   104:             if result.returncode == 0:\n>> 105:                 click.echo(f\"Entry: {result.stdout.strip()}\", err=True)\n   106:             else:\n   107:                 click.echo(f\"WARN: entry create failed: {result.stderr.strip()}\", err=True)\n   108:     except FileNotFoundError:"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 107,
        "text": "click.echo(f\"WARN: entry create failed: {result.stderr.strip()}\", err=True)",
        "context_function": "_create_entry",
        "context_snippet": "   104:             if result.returncode == 0:\n   105:                 click.echo(f\"Entry: {result.stdout.strip()}\", err=True)\n   106:             else:\n>> 107:                 click.echo(f\"WARN: entry create failed: {result.stderr.strip()}\", err=True)\n   108:     except FileNotFoundError:\n   109:         click.echo(\"WARN: entry CLI not found. Install with: uv tool install ftl-entry\", err=True)\n   110: "
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 109,
        "text": "click.echo(\"WARN: entry CLI not found. Install with: uv tool install ftl-entry\", err=True)",
        "context_function": "_create_entry",
        "context_snippet": "   106:             else:\n   107:                 click.echo(f\"WARN: entry create failed: {result.stderr.strip()}\", err=True)\n   108:     except FileNotFoundError:\n>> 109:         click.echo(\"WARN: entry CLI not found. Install with: uv tool install ftl-entry\", err=True)\n   110: \n   111: \n   112: def _enqueue_topics(response: str, source: str, project_dir: str | None = None) -> None:"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 118,
        "text": "click.echo(f\"Queued {added} new topic(s) ({total} pending)\", err=True)",
        "context_function": "_enqueue_topics",
        "context_snippet": "   115:         added = add_topics(new_topics, project_dir)\n   116:         if added:\n   117:             total = pending_count(project_dir)\n>> 118:             click.echo(f\"Queued {added} new topic(s) ({total} pending)\", err=True)\n   119: \n   120: \n   121: def _has_reasons() -> bool:"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 142,
        "text": "click.echo(f\"Surfaced {len(beliefs)} belief(s):\", err=True)",
        "context_function": "_report_beliefs",
        "context_snippet": "   139: def _report_beliefs(response: str) -> None:\n   140:     beliefs = _parse_beliefs_from_response(response)\n   141:     if beliefs:\n>> 142:         click.echo(f\"Surfaced {len(beliefs)} belief(s):\", err=True)\n   143:         for b in beliefs[:5]:\n   144:             click.echo(f\"  {b['id']}: {b['text'][:80]}\", err=True)\n   145: "
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 144,
        "text": "click.echo(f\"  {b['id']}: {b['text'][:80]}\", err=True)",
        "context_function": "_report_beliefs",
        "context_snippet": "   141:     if beliefs:\n   142:         click.echo(f\"Surfaced {len(beliefs)} belief(s):\", err=True)\n   143:         for b in beliefs[:5]:\n>> 144:             click.echo(f\"  {b['id']}: {b['text'][:80]}\", err=True)\n   145: \n   146: \n   147: def _reasons_export():"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 155,
        "text": "click.echo(f\"Updated {beliefs_path}\")",
        "context_function": "_reasons_export",
        "context_snippet": "   152:         capture_output=True, text=True,\n   153:     )\n   154:     if result.returncode == 0:\n>> 155:         click.echo(f\"Updated {beliefs_path}\")\n   156:     result = subprocess.run(\n   157:         [\"reasons\", \"export\", \"-o\", str(network_path)],\n   158:         capture_output=True, text=True,"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 161,
        "text": "click.echo(f\"Updated {network_path}\")",
        "context_function": "_reasons_export",
        "context_snippet": "   158:         capture_output=True, text=True,\n   159:     )\n   160:     if result.returncode == 0:\n>> 161:         click.echo(f\"Updated {network_path}\")\n   162: \n   163: \n   164: # --- CLI ---"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 186,
        "text": "@click.group()",
        "context_function": "_load_env_file",
        "context_snippet": "   183:         pass\n   184: \n   185: \n>> 186: @click.group()\n   187: @click.version_option(package_name=\"ftl-project-expert\")\n   188: @click.option(\"--quiet\", \"-q\", is_flag=True, default=False,\n   189:               help=\"Suppress output to stdout\")"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 187,
        "text": "@click.version_option(package_name=\"ftl-project-expert\")",
        "context_function": "_load_env_file",
        "context_snippet": "   184: \n   185: \n   186: @click.group()\n>> 187: @click.version_option(package_name=\"ftl-project-expert\")\n   188: @click.option(\"--quiet\", \"-q\", is_flag=True, default=False,\n   189:               help=\"Suppress output to stdout\")\n   190: @click.option(\"--model\", \"-m\", default=\"claude\", help=\"Model to use (default: claude)\")"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 188,
        "text": "@click.option(\"--quiet\", \"-q\", is_flag=True, default=False,",
        "context_function": "_load_env_file",
        "context_snippet": "   185: \n   186: @click.group()\n   187: @click.version_option(package_name=\"ftl-project-expert\")\n>> 188: @click.option(\"--quiet\", \"-q\", is_flag=True, default=False,\n   189:               help=\"Suppress output to stdout\")\n   190: @click.option(\"--model\", \"-m\", default=\"claude\", help=\"Model to use (default: claude)\")\n   191: @click.option(\"--timeout\", \"-t\", default=300, type=int, help=\"LLM timeout in seconds\")"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 190,
        "text": "@click.option(\"--model\", \"-m\", default=\"claude\", help=\"Model to use (default: claude)\")",
        "context_function": "_load_env_file",
        "context_snippet": "   187: @click.version_option(package_name=\"ftl-project-expert\")\n   188: @click.option(\"--quiet\", \"-q\", is_flag=True, default=False,\n   189:               help=\"Suppress output to stdout\")\n>> 190: @click.option(\"--model\", \"-m\", default=\"claude\", help=\"Model to use (default: claude)\")\n   191: @click.option(\"--timeout\", \"-t\", default=300, type=int, help=\"LLM timeout in seconds\")\n   192: @click.option(\"--env-file\", default=\".env\", help=\"Path to .env file (default: .env)\")\n   193: @click.pass_context"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 191,
        "text": "@click.option(\"--timeout\", \"-t\", default=300, type=int, help=\"LLM timeout in seconds\")",
        "context_function": "_load_env_file",
        "context_snippet": "   188: @click.option(\"--quiet\", \"-q\", is_flag=True, default=False,\n   189:               help=\"Suppress output to stdout\")\n   190: @click.option(\"--model\", \"-m\", default=\"claude\", help=\"Model to use (default: claude)\")\n>> 191: @click.option(\"--timeout\", \"-t\", default=300, type=int, help=\"LLM timeout in seconds\")\n   192: @click.option(\"--env-file\", default=\".env\", help=\"Path to .env file (default: .env)\")\n   193: @click.pass_context\n   194: def cli(ctx, quiet, model, timeout, env_file):"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 192,
        "text": "@click.option(\"--env-file\", default=\".env\", help=\"Path to .env file (default: .env)\")",
        "context_function": "_load_env_file",
        "context_snippet": "   189:               help=\"Suppress output to stdout\")\n   190: @click.option(\"--model\", \"-m\", default=\"claude\", help=\"Model to use (default: claude)\")\n   191: @click.option(\"--timeout\", \"-t\", default=300, type=int, help=\"LLM timeout in seconds\")\n>> 192: @click.option(\"--env-file\", default=\".env\", help=\"Path to .env file (default: .env)\")\n   193: @click.pass_context\n   194: def cli(ctx, quiet, model, timeout, env_file):\n   195:     \"\"\"Build expert knowledge bases from project management data.\"\"\""
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 193,
        "text": "@click.pass_context",
        "context_function": "_load_env_file",
        "context_snippet": "   190: @click.option(\"--model\", \"-m\", default=\"claude\", help=\"Model to use (default: claude)\")\n   191: @click.option(\"--timeout\", \"-t\", default=300, type=int, help=\"LLM timeout in seconds\")\n   192: @click.option(\"--env-file\", default=\".env\", help=\"Path to .env file (default: .env)\")\n>> 193: @click.pass_context\n   194: def cli(ctx, quiet, model, timeout, env_file):\n   195:     \"\"\"Build expert knowledge bases from project management data.\"\"\"\n   196:     _load_env_file(env_file)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 194,
        "text": "def cli(ctx, quiet, model, timeout, env_file):",
        "context_function": "_load_env_file",
        "context_snippet": "   191: @click.option(\"--timeout\", \"-t\", default=300, type=int, help=\"LLM timeout in seconds\")\n   192: @click.option(\"--env-file\", default=\".env\", help=\"Path to .env file (default: .env)\")\n   193: @click.pass_context\n>> 194: def cli(ctx, quiet, model, timeout, env_file):\n   195:     \"\"\"Build expert knowledge bases from project management data.\"\"\"\n   196:     _load_env_file(env_file)\n   197:     ctx.ensure_object(dict)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 206,
        "text": "@cli.command()",
        "context_function": "cli",
        "context_snippet": "   203: # --- init ---\n   204: \n   205: \n>> 206: @cli.command()\n   207: @click.argument(\"platform\", type=click.Choice([\"github\", \"gitlab\", \"jira\"]))\n   208: @click.argument(\"target\", type=str)\n   209: @click.option(\"--domain\", \"-d\", default=None, help=\"One-line project description\")"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 207,
        "text": "@click.argument(\"platform\", type=click.Choice([\"github\", \"gitlab\", \"jira\"]))"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 208,
        "text": "@click.argument(\"target\", type=str)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 209,
        "text": "@click.option(\"--domain\", \"-d\", default=None, help=\"One-line project description\")"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 210,
        "text": "@click.option(\"--jira-url\", default=None, help=\"Jira base URL (for jira platform)\")"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 211,
        "text": "@click.option(\"--github-repo\", default=None,"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 229,
        "text": "click.echo(f\"Error: {tool} not found on PATH\", err=True)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 232,
        "text": "click.echo(\"Error: neither reasons nor beliefs found on PATH\", err=True)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 237,
        "text": "click.echo(\"Error: gh CLI not found. Install from https://cli.github.com\", err=True)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 240,
        "text": "click.echo(\"Error: glab CLI not found.\", err=True)"
      },
      {
        "file": "ftl_project_expert/cli.py",
        "line": 244,
        "text": "click.echo(\"Error: --jira-url or JIRA_URL env var required for Jira\", err=True)"
      }
    ],
    "test_callers": [],
    "production_count": 568,
    "test_count": 0,
    "total_count": 568
  },
  "cli_function_body": {
    "function": "cli",
    "file": "ftl_project_expert/cli.py",
    "start_line": 186,
    "end_line": 200,
    "source": "@click.group()\n@click.version_option(package_name=\"ftl-project-expert\")\n@click.option(\"--quiet\", \"-q\", is_flag=True, default=False,\n              help=\"Suppress output to stdout\")\n@click.option(\"--model\", \"-m\", default=\"claude\", help=\"Model to use (default: claude)\")\n@click.option(\"--timeout\", \"-t\", default=300, type=int, help=\"LLM timeout in seconds\")\n@click.option(\"--env-file\", default=\".env\", help=\"Path to .env file (default: .env)\")\n@click.pass_context\ndef cli(ctx, quiet, model, timeout, env_file):\n    \"\"\"Build expert knowledge bases from project management data.\"\"\"\n    _load_env_file(env_file)\n    ctx.ensure_object(dict)\n    ctx.obj[\"quiet\"] = quiet\n    ctx.obj[\"model\"] = model\n    ctx.obj[\"timeout\"] = timeout"
  },
  "cli_tests": {
    "source_file": "ftl_project_expert/cli.py",
    "test_files": [],
    "test_count": 0
  },
  "cli_imports": {
    "file": "ftl_project_expert/cli.py",
    "imports": [
      "asyncio",
      "json",
      "os",
      "re",
      "shutil",
      "subprocess",
      "sys",
      "click"
    ],
    "from_imports": [
      {
        "module": "datetime",
        "names": [
          "date",
          "datetime"
        ]
      },
      {
        "module": "pathlib",
        "names": [
          "Path"
        ]
      },
      {
        "module": "llm",
        "names": [
          "check_model_available",
          "invoke",
          "invoke_concurrent_sync",
          "invoke_sync"
        ]
      },
      {
        "module": "prompts",
        "names": [
          "PROPOSE_BELIEFS_PROJECT",
          "RESEARCH_PROMPT",
          "build_explore_prompt",
          "build_scan_prompt",
          "build_sprint_plan_prompt",
          "build_summary_prompt"
        ]
      },
      {
        "module": "sources",
        "names": [
          "GitHubSource",
          "GitLabSource",
          "Issue",
          "JiraSource"
        ]
      },
      {
        "module": "topics",
        "names": [
          "Topic",
          "add_topics",
          "load_queue",
          "parse_topics_from_response",
          "pending_count",
          "pop_at",
          "pop_multiple",
          "pop_next",
          "skip_topic"
        ]
      }
    ],
    "import_section": "\"\"\"Command-line interface for project expert.\"\"\"\n\nimport asyncio\nimport json\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nfrom datetime import date, datetime\nfrom pathlib import Path\n\nimport click\n\nfrom .llm import check_model_available, invoke, invoke_concurrent_sync, invoke_sync\nfrom .prompts import (\n    PROPOSE_BELIEFS_PROJECT,\n    RESEARCH_PROMPT,\n    build_explore_prompt,\n    build_scan_prompt,\n    build_sprint_plan_prompt,\n    build_summary_prompt,\n)\nfrom .sources import GitHubSource, GitLabSource, Issue, JiraSource\nfrom .topics import (\n    Topic,\n    add_topics,\n    load_queue,\n    parse_topics_from_response,\n    pending_count,\n    pop_at,\n    pop_multiple,\n    pop_next,\n    skip_topic,\n)\n\nPROJECT_DIR = \".project-expert\"\n\n\n# --- Config helpers ---\n\n\ndef _load_config() -> dict | None:\n    config_path = Path.cwd() / PROJECT_DIR / \"config.json\"\n    if config_path.is_file():\n        return json.loads(config_path.read_text())\n    return None\n\n\ndef _save_config(config: dict) -> None:\n    config_dir = Path.cwd() / PROJECT_DIR\n    config_dir.mkdir(parents=True, exist_ok=True)\n    (config_dir / \"config.json\").write_text(json.dumps(config, indent=2))\n\n\ndef _get_project_dir() -> str:\n    return str(Path.cwd() / PROJECT_DIR)\n\n\n# --- Source helpers ---\n\n\ndef _get_source(config: dict) -> GitHubSource | GitLabSource | JiraSource:\n    \"\"\"Create the appropriate source from config.\"\"\""
  }
}
```

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.
