Now I have full context on the cached issue format and data model. 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 injected properly. The five output sections (Sprint Goal, Prioritized Backlog, Assignment Recommendations, Escalation Flags, Risk Summary) match the issue spec. The "Be concrete" guardrail at the end is good practice to prevent hallucinated data.
---

### 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: Standard wiring — import added and exported in `__all__`. Matches the import in `cli.py`.
---

### ftl_project_expert/cli.py:_compute_gating_analysis
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: MEETS
ISSUE_COMPLIANCE: ADDRESSES
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: BFS-based transitive downstream count is correct. Deduplication via `visited` set prevents double-counting. The `outlist_gates` tracking correctly identifies which conclusions are directly gated (as opposed to transitively downstream). Results sorted descending by impact. One edge note: the function builds `dependents_map` from both `antecedents` and `outlist`, which is correct — both represent dependency edges. The `_transitive_count` function excludes the node itself from the count (`len(visited) - 1`), which is the right behavior.
---

### ftl_project_expert/cli.py:_compute_team_signals
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: PARTIAL
ISSUE_COMPLIANCE: PARTIAL
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Two issues:

1. **`closed` field not in cache** (line ~2179): The code does `closed_str = issue.get("closed") or issue.get("updated")`. But `_cache_issues` (line 436-466) never persists the `closed` field from the Issue model — it's omitted from the cached dict. So `issue.get("closed")` always returns `None` when reading from the cache, and it silently falls back to `updated`. This means issues that were updated recently but closed long ago are miscounted as "recently closed", inflating `closed_recent` velocity signals. This could produce misleading capacity recommendations.

2. **Naive vs aware datetime comparison** (line ~2183): `datetime.now()` is timezone-naive, while `datetime.fromisoformat(closed_str.replace("Z", "+00:00"))` produces a timezone-aware datetime. The `closed_dt.replace(tzinfo=None)` strips it to compare, which is fine for the ~30-day granularity here, but inconsistent with best practice.

Issue (1) is the more substantive concern — the team capacity signal is a core differentiator of this feature, and systematically inflating velocity undermines assignment recommendations.

---

### 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 cross-referencing from belief text to issue IDs has a correctness concern. `_extract_issue_refs` parses `GH-123` from belief text and returns `{"type": "issue", "number": 123}`. The code then generates *both* `GH-123` and `GL-123` as candidate keys (line ~2282-2288). In a mixed-platform project (both GitHub and GitLab issues), a belief mentioning `GH-123` would incorrectly match `GL-123` in the cache (and vice versa), inflating the belief impact for the wrong issue. The issue spec calls for this feature to work with GitHub, GitLab, and Jira — so mixed-platform use is plausible. A better approach: `_extract_issue_refs` already knows the prefix (GH vs GL), so the match should use it.

Additionally, the Jira-style ref path (`"key" in ref`) correctly uses `ref["key"]` directly, which is fine since Jira keys like `PROJ-123` are already unique.

---

### 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. Truncates node text at 120 chars, caps gated conclusions at 5, includes overflow note. No issues.
---

### 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 of team signals. Filters out "none" priority from display. Handles empty team gracefully.
---

### 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 patterns of the existing `summary` command. It correctly:
- Loads config, network, cached issues
- Computes all analysis sections
- Supports `--dry-run` for inspection
- Creates an entry for history tracking
- Writes to file with `--output`

However, two options from the issue spec are missing:
- `--retrospective last-sprint-outcomes.json` — the issue describes previous sprint outcomes as an input for plan-vs-actual feedback loops. This is listed as part of the proposed usage.
- `--format markdown` — the issue shows `--format` as a CLI option.

These are Phase 1 omissions that are reasonable to defer, but the issue spec explicitly lists them in the "Proposed Design > Usage" section. The `--retrospective` flag is more significant since it's central to the closed-loop sprint planning workflow described in "Relationship to Hybrid EM Model."

The beliefs loading section (lines ~2406-2419) has a reasonable fallback chain: `reasons list --by-impact` → `beliefs.md` → "No beliefs available." This mirrors the pattern from `summary`.

---

### Test Coverage (overall)
VERDICT: CONCERN
CORRECTNESS: N/A
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: PARTIAL
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: N/A
REASONING: No tests for any of the new functions. The pure-logic functions (`_compute_gating_analysis`, `_compute_team_signals`, `_format_backlog_section`) are highly testable with unit tests and would benefit from them — they do non-trivial graph traversal, date arithmetic, and cross-referencing. The issue references #7 and #8 as prior bugs with similar infrastructure, which argues for test coverage to prevent regressions.
---

### SELF_REVIEW
LIMITATIONS:
- Could not verify the `invoke()` function signature (observation returned "No function found") — it's likely in the `llm` module but I couldn't confirm timeout parameter handling.
- Could not run the code to check behavior with a real `issues-cache.json` or `network.json`.
- Did not see existing test files to gauge the project's test conventions or coverage expectations.
---

### FEATURE_REQUESTS
- Include related test files automatically when they exist for the modified source files
- Show the full function body around diff hunks (not just the diff) for modified functions — the observation results partially covered this but had gaps (e.g., `invoke` not found)
- When the Issue model is a dataclass, show its field definitions alongside functions that consume its dict form — the cache serialization gap (`closed` field omitted) was only catchable by reading both the model and the cache function
---
