## Code Review: Parallel LLM Support + `--per-issue` Scan Mode

### ftl_project_expert/llm.py:invoke_concurrent
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Clean semaphore-based concurrency with `asyncio.gather(return_exceptions=True)`. The `_guarded` wrapper correctly limits concurrency. Return type `list[str | Exception]` is appropriate and callers check `isinstance(result, Exception)` consistently. The sync wrapper uses `asyncio.run()` which is correct for CLI entry points that don't already have an event loop.

---

### ftl_project_expert/cli.py:_cache_issues
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Bug fix — previously the function always started with `data = {}`, silently dropping all previously cached issues. Now it loads existing data first. This is correct and necessary for `--per-issue` (multi-page scans) and `--all-pages` workflows where `_cache_issues` is called per page.

---

### ftl_project_expert/cli.py:_build_topic_prompt
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Clean extraction of prompt-building logic from `_run_topic`. The function is called sequentially even in parallel paths (prompts are built into a list before passing to `invoke_concurrent_sync`), so side effects like `click.echo` warnings and network calls via `_get_source` don't cause concurrency issues. Both `_run_topic` and all parallel paths use it.

---

### ftl_project_expert/cli.py:_run_topic
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Now delegates to `_build_topic_prompt` and retains only the LLM invocation + post-processing. Behavior is preserved. Still used in the sequential `_explore_loop` path and single-topic explore.

---

### ftl_project_expert/cli.py:scan (--per-issue)
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: `--per-issue` queues topics without invoking the LLM, but the `scan()` function still calls `check_model_available(model)` at line 291 and exits if unavailable. This means `--per-issue` fails in environments without a model CLI installed, even though it never uses one. The check should be skipped or deferred when `per_issue` is True. Also, line 357 has a redundant local `from .topics import Topic, add_topics` — both are already imported at module level (lines 27-28).

---

### ftl_project_expert/cli.py:explore (--parallel with --pick)
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: When `max_parallel > 1` and multiple topics are picked, prompts are built sequentially, then invoked concurrently, then results processed sequentially. Post-processing (`_create_entry`, `_enqueue_topics`, `_report_beliefs`, `_emit`) runs in the main loop with no concurrency hazard. Falls back to sequential path correctly when `max_parallel == 1` or only one topic.

---

### ftl_project_expert/cli.py:_explore_loop_parallel
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Batched parallel exploration. `pop_next` is called sequentially within each batch, so no queue file race. `explored` increments even on errors (line 674 in observations) — this is intentional and consistent with how `max_topics` acts as a hard cap, not a success count. Batch size correctly caps at `min(max_parallel, max_topics - explored)`.

---

### ftl_project_expert/cli.py:propose_beliefs (--parallel)
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Prompts are pre-built from batches, then dispatched concurrently or sequentially based on `max_parallel`. Error handling is consistent between paths. The `update` command correctly threads `max_parallel` through via `ctx.invoke(propose_beliefs, since=since, max_parallel=max_parallel)`.

---

### ftl_project_expert/cli.py:review_proposals (--parallel)
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Same pattern as `propose_beliefs`. Prompts pre-built, dispatched concurrently, results merged into `all_decisions` dict sequentially. Context sections (`issue_state`, `existing_beliefs`) are shared and read-only. The `update` command threads `max_parallel` through correctly.

---

### ftl_project_expert/cli.py:update (--parallel)
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: `--parallel` is threaded through to `_explore_loop`, `propose_beliefs`, and `review_proposals`. The `scan` step in `update` uses its own `_run_scan_step` path which doesn't get parallel support — this is reasonable since scan batches are page-sequential by nature.

---

### SELF_REVIEW
LIMITATIONS: No test files exist for either `cli.py` or `llm.py`, so I cannot verify test coverage claims or check for broken test assertions. I did not have visibility into `pop_multiple` or `pop_at` implementations to verify they correctly mark topics as done before the parallel path processes them (though the observation for `pop_next` shows it does). I could not verify whether `_get_source` creates expensive connections that would make sequential prompt-building a bottleneck in the parallel path.

---

### FEATURE_REQUESTS
- Include implementations of functions called by modified code when they aren't in the diff (e.g., `pop_multiple`, `pop_at`, `_load_cached_issues`) — these are needed to verify correctness of the integration
- Flag when a pre-existing guard (like `check_model_available`) blocks a new code path that doesn't need the guard (the `--per-issue` case)
