Here's the review:

---

### ftl_project_expert/prompts/sprint_plan.py
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: MEETS
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Clean prompt template. All seven parameters are threaded through from the CLI. The five output sections (Sprint Goal, Prioritized Backlog, Assignment Recommendations, Escalation Flags, Risk Summary) match the issue spec. The instruction to "not invent information not present in the data" is good grounding. The `--format markdown` option from the issue isn't wired here, but markdown is the default output and the only format that makes sense for Phase 1.

---

### ftl_project_expert/prompts/__init__.py
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Correct re-export of `build_sprint_plan_prompt` added to both the import and `__all__`. Matches the import in `cli.py`.

---

### ftl_project_expert/cli.py:_compute_gating_analysis
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: MEETS
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The BFS in `_transitive_count` traverses the `dependents_map` which merges both antecedent-dependents and outlist-dependents into one graph. This means the "downstream count" includes nodes reachable through both justification pathways — reasonable for impact analysis. However, `_transitive_count` is called once per node in `dependents_map`, making this O(N * (N + E)) in the worst case. On the 10,800-node network mentioned in the issue, this could be slow — the issue specifically calls out "handle large networks gracefully" (referencing issues #7 and #8). There's no caching of transitive counts, and no progress indicator for this potentially expensive computation. Consider memoizing or doing a single topological-sort pass to compute all counts in O(N + E).

---

### ftl_project_expert/cli.py:_compute_team_signals
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: MEETS
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Two issues. (1) Timezone handling: `datetime.now()` returns a naive local time, but `datetime.fromisoformat(closed_str.replace("Z", "+00:00"))` produces a timezone-aware datetime. The code then strips tzinfo via `.replace(tzinfo=None)` before subtraction — this silently interprets UTC timestamps as local time. For a 30-day window the error is negligible, but it's a latent bug if the window shrinks. (2) The `assignees` field in cached issues may contain strings or dicts depending on the source platform. The code handles both (`assignee if isinstance(assignee, str) else assignee.get("login", ...)`) which is good, but the `_format_backlog_section` function joins assignees directly as strings without this same check (line `assignee_str = ", ".join(assignees)`), so if assignees are dicts, that section will render `{'login': 'foo'}` as a string.

---

### ftl_project_expert/cli.py:_format_backlog_section
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: MEETS
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The belief-to-issue cross-referencing uses substring matching (`if cached_id and cached_id in text`). This has false positive risk: an issue ID like "GH-12" would match belief text containing "GH-123". For IDs with the platform prefix format (GH-/GL-/PROJ-) this is a minor concern, but it could inflate `belief_impact` scores for issues with short numeric suffixes. Also, as noted above, `", ".join(assignees)` assumes assignees are strings — `_compute_team_signals` handles the dict case but this function does not.

---

### ftl_project_expert/cli.py:sprint_plan (command)
VERDICT: CONCERN
CORRECTNESS: VALID
SPEC_COMPLIANCE: PARTIAL
ISSUE_COMPLIANCE: PARTIAL
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The command is well-structured and follows the established pattern from the `summary` command closely. The `--dry-run` option works as specified. However, two options from the issue spec are not implemented: `--format` (markdown/json/etc.) and `--retrospective` (for feeding back previous sprint outcomes). The issue acknowledges retrospective as optional ("if available"), and format defaults to markdown via the LLM prompt, so this is a reasonable Phase 1 scope. The beliefs loading logic differs slightly from `summary` — it uses a non-`elif` fallback which is actually an improvement (falls back to beliefs.md even when reasons exists but returns empty). One nit: the `if not dry_run and not check_model_available(model)` check happens before the expensive `_load_network()` and `_compute_gating_analysis()` calls, which is efficient.

---

### ftl_project_expert/cli.py:_cache_issues (closed field)
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The `Issue` dataclass already has a `closed: str = ""` field (models.py:124). This change adds it to the cache serialization dict so it's persisted and available to `_compute_team_signals` for the 30-day recent-close window. Correct and necessary for the sprint-plan feature.

---

### ftl_project_expert/cli.py:_format_gating_section
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: MEETS
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Straightforward formatting function. Truncates text at 120 chars, limits gated conclusions display to 5, caps output at `max_items` with a count of omitted items. Clean and correct.

---

### ftl_project_expert/cli.py:_format_team_section
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: MEETS
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Clean formatting. Filters out "none" priority from the display string. Handles empty team gracefully.

---

## Summary of Concerns

1. **Performance on large networks**: `_compute_gating_analysis` has O(N*(N+E)) complexity with no memoization. On 10,800+ node networks, this could be very slow. The issue explicitly mentions graceful handling of large networks.

2. **Assignee type inconsistency**: `_compute_team_signals` handles assignees-as-dicts but `_format_backlog_section` does not — it will render dict `repr()` strings if assignees aren't plain strings.

3. **No tests**: The entire project has zero test files. For a new feature with multiple pure-function helpers (`_compute_gating_analysis`, `_compute_team_signals`, `_format_*`), these are highly testable and would benefit from unit tests.

4. **Missing spec options**: `--retrospective` and `--format` from the issue are not implemented, though this is likely acceptable for Phase 1.

---

### SELF_REVIEW
LIMITATIONS: Could not verify `invoke()` and `check_model_available()` signatures (defined in `llm.py`, not included in observations). Could not test the `reasons list --by-impact` flag — if that flag doesn't exist, the beliefs section silently falls back to empty. No test files exist in the project to check for regression.

---

### FEATURE_REQUESTS
- Include transitive dependency files (like `llm.py` here) when the diff calls functions from them
- Flag when a project has zero test coverage so the reviewer can weight the UNTESTED verdicts appropriately
- Show runtime complexity analysis for functions operating on known-large datasets (the issue mentions 10,800+ nodes)
