Here's the review:

---

### ftl_project_expert/prompts/research.py
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Well-structured prompt template. Uses clear placeholders that match the `.format()` call in `_research_one`. The four investigation dimensions (state changes, factual accuracy, reference integrity, staleness) are appropriate for belief verification. Output format with VERDICT line is parseable by the calling code.
---

### ftl_project_expert/prompts/__init__.py
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Correctly re-exports `RESEARCH_PROMPT` and adds it to `__all__`.
---

### ftl_project_expert/sources/github.py:get_pr
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Mirrors the pattern from `get_issue` and `list_prs`. The `--json` fields match what `_normalize_pr` expects (same fields as `list_prs`). Returns a `PullRequest` via `self._normalize_pr(raw)`, which already exists and handles all these fields correctly. Clean addition.
---

### ftl_project_expert/cli.py:_get_belief_info
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Parses `reasons show` output by looking for `Text:`, `Status:`, `Source:`, `Dependents:` prefixes. This is fragile — if `reasons show` output format changes or if the text/source values contain newlines, parsing breaks. The `Dependents:` parsing assumes comma-separated on a single line, but `_get_dependent_beliefs` doesn't actually use `info["dependents"]` — it reads from the network dict instead. Minor: the parsed dependents field is only used in the `dep_count` check at line 1549 (`info.get("dependents", [])`) which is informational only. Not a blocker but worth noting the fragility.
---

### ftl_project_expert/cli.py:_extract_issue_refs
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The Jira-key regex `[A-Z][A-Z0-9]+-\d+` will match prefixes like `GH-`, `PR-`, `GL-`, `MR-` and then skip them, but it also matches any uppercase-prefixed pattern like `HTTP-200`, `TLS-123`, `SHA-256`. In project management text this could produce false positives. The `seen` set uses mixed key types — tuples `(kind, num)` for GH/PR/GL/MR/#-refs, but bare strings for Jira keys — which works but is unusual. No tests for any of these patterns.
---

### ftl_project_expert/cli.py:_fetch_artifacts
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The `hasattr(source, "get_pr")` guard at line 1396 correctly handles platforms that don't implement `get_pr` (GitLab, Jira). PR references on non-GitHub platforms fall through to the `else` branch with a descriptive message rather than crashing. The broad `except Exception` is acceptable here since failures should be reported but not abort the research.
---

### ftl_project_expert/cli.py:_select_beliefs_for_research
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: When `negative=False` (default), ALL beliefs are candidates — there's no filter for IN-only beliefs. Running `project-expert research` with no flags will select the first N beliefs alphabetically, including OUT beliefs. This may be intentional (research anything) but is surprising given that `--negative` exists as a flag. A `--positive` or default-to-IN filter might be expected. Also, when called without `--high-impact` and without a `belief_id`, the sort is lexicographic by node ID — this produces a stable but arbitrary selection that doesn't prioritize beliefs most in need of verification.
---

### ftl_project_expert/cli.py:research
VERDICT: CONCERN
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: The command is well-structured and follows established patterns (matches `review_proposals` for parallel invocation, `_create_entry`/`_emit` usage). Several concerns: (1) Zero tests for a 300-line feature with non-trivial logic (regex parsing, subprocess interaction, LLM integration). (2) The `_load_network()` call at line 1492 happens before `_get_source` and before checking if a belief exists — this is fine but means the network is loaded even if the belief_id is invalid. (3) The `_research_one` inner function writes to stderr via `click.echo` but is called in the prompt-building phase — if used with `--parallel`, multiple beliefs will interleave their stderr progress messages. Not a bug but noisy. (4) The `select_limit` default is 1, which is sensible for the auto-select case.
---

### Overall Assessment

VERDICT: CONCERN

The feature is well-integrated and follows existing codebase patterns. The main concerns are:

1. **No tests at all** — `_extract_issue_refs` is pure logic with regex patterns and deserves unit tests. The Jira-key regex false positives (matching things like `HTTP-200`) would be caught immediately.
2. **`_get_belief_info` fragility** — parsing subprocess text output line-by-line is brittle. If `reasons show` gains multi-line text fields, this breaks silently.
3. **Selection logic** — the default (no flags) selects beliefs in arbitrary alphabetical order including OUT beliefs. Consider whether this is the right default UX.

None of these are blockers — the code is correct for the happy path and handles errors gracefully.

---

### SELF_REVIEW
LIMITATIONS: Could not verify the exact output format of `reasons show` to confirm the parsing logic in `_get_belief_info` is correct. No test files exist in the project at all, so the "UNTESTED" verdicts reflect the entire project's testing posture, not just this change. Could not see the full `_normalize_pr` method signature to confirm it handles all the `--json` fields requested by `get_pr`.
---

### FEATURE_REQUESTS
- Include the output format of external CLI tools (like `reasons show`) when the code parses their output, so reviewers can verify parsing logic
- Flag when an entire project has zero tests, distinguishing "this change has no tests" from "this project has no test infrastructure at all"
---
