Now I have full context. Here is the review:

---

### ftl_project_expert/cli.py:_compute_gating_analysis
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: BFS traversal of the dependency graph to compute transitive downstream counts is correct. The `visited` set prevents infinite loops in cyclic graphs. Filtering out nodes not in `nodes` (line 2143) is a good guard for dangling references. One performance note: this computes `_transitive_count` for every node in `dependents_map`, which is O(N * (N+E)) worst case. For the 10,800-node networks mentioned in the issue this should be acceptable but could be slow on very large dense graphs. The outlist semantics correctly identify nodes that gate other conclusions via defeat relationships.
---

### ftl_project_expert/cli.py:_compute_team_signals
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The timezone handling at line 2199 is fragile: `datetime.now()` returns a naive datetime, but `datetime.fromisoformat(closed_str.replace("Z", "+00:00"))` produces a timezone-aware datetime. Then `.replace(tzinfo=None)` strips the tz to make subtraction work, but this silently drops the timezone offset — a `+05:30` timestamp would be compared against local time as if it were local. This won't crash but could miscategorize "recent" closes by up to a day depending on timezone difference. The `labels` key is initialized in the defaultdict factory (line 2167) but never populated — dead code that wastes memory per team member. The issue extraction logic (lines 2173-2179) duplicates the same pattern in `_format_backlog_section` — minor duplication but not a blocker.
---

### ftl_project_expert/cli.py:_format_gating_section
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Straightforward formatting function. The `gated` variable is computed but only used when `gated_conclusions` is non-empty, which is correct since the fallback "indirect" string on line 2235 is never rendered (line 2240 guards on the same condition). No issues.
---

### ftl_project_expert/cli.py:_format_backlog_section
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The `refs_in_beliefs` cross-reference on line 2288 compares `str(issue.get("id", ""))` against `gating_ids` which are the `id` fields from the belief network nodes. These are belief node IDs (e.g. `"claim_42"`), not issue tracker IDs (e.g. `"GL-71"`). This cross-reference will almost never match unless belief node IDs happen to be issue IDs. The issue specification describes ranking by "downstream beliefs unblocked" — this requires a mapping from issue tracker IDs to belief nodes that reference them, which isn't implemented. The `priority_order` dict is rebuilt inside the loop body on every iteration (line 2290) — minor inefficiency, should be hoisted.
---

### ftl_project_expert/cli.py:sprint_plan
VERDICT: CONCERN
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: PARTIAL
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The command follows the same well-established pattern as `summary` — load config, check model, gather data, build prompt, invoke LLM, create entry, emit. This is solid. However, the issue requests `--retrospective` support for feeding back previous sprint outcomes, and `--format` for output format selection — neither is implemented. The `--dry-run` flag requested in the issue is implemented. The `sprint_length` is passed as a string to the prompt but not validated (e.g. "2w" is meaningful but "banana" would be accepted silently). The beliefs loading (lines 2394-2410) is a near-duplicate of the same logic in `summary` (lines 2040-2068) — this is a maintainability concern since bugs fixed in one must be manually propagated to the other. The `prompt_size_kb` variable on line 2439 shadows the one that would have been computed in the dry-run path (line 2430), but since dry-run returns early this is fine.
---

### ftl_project_expert/prompts/sprint_plan.py
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The prompt is well-structured and covers all five output sections requested in the issue: sprint goal, prioritized backlog, assignment recommendations, escalation flags, and risk summary. The instruction to "not invent information not present in the data" is important for grounding. The prompt sections align with the data sections computed in the CLI. The f-string interpolation is clean with no injection risk since all inputs are internally generated strings/ints.
---

### 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: Correctly imports and re-exports `build_sprint_plan_prompt` from the new module. Consistent with the existing pattern for other prompt functions.
---

## Summary

The implementation is a solid Phase 1 that follows the project's established patterns. Three concerns worth noting:

1. **Belief-to-issue cross-reference is ineffective** (`_format_backlog_section` line 2288): The `gating_ids` are belief node IDs, not issue tracker IDs. The `refs_in_beliefs` flag will almost never be true, meaning the "ranked by downstream beliefs unblocked" sorting doesn't actually work. This is the core value proposition of the sprint-plan command per the issue — issues should be ranked by how many beliefs they unblock.

2. **No tests**: The project has no test files at all, so this is consistent with the existing codebase, but for a feature with non-trivial data processing logic (`_compute_gating_analysis`, `_compute_team_signals`), unit tests would catch edge cases.

3. **Timezone handling in `_compute_team_signals`**: Mixing naive and aware datetimes via `.replace(tzinfo=None)` is a known antipattern that can shift the 30-day recent-close boundary.

### SELF_REVIEW
LIMITATIONS: Could not verify the actual structure of `network.json` node IDs vs issue cache IDs — my concern about the cross-reference mismatch (#1) is based on the naming conventions visible in the issue description (belief IDs like `claim_42` vs issue IDs like `GL-71`), but the actual ID format depends on how `reasons` generates node IDs. If the project uses issue IDs as belief node IDs, the cross-reference would work. Could not see `invoke()` or `check_model_available()` implementations to verify error behavior. No test files exist in the project to check for regressions.
---

### FEATURE_REQUESTS
- Include sample data from referenced files (e.g. a snippet of `network.json` or `issues-cache.json`) so reviewers can verify cross-referencing logic like the gating_ids match
- Show the full body of imported functions from other modules (e.g. `invoke`, `check_model_available`) when they're used in error-handling paths
---
