Here's my review of the research command changes.

---

### 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: Clean prompt template. The four investigation dimensions (state changes, factual accuracy, reference integrity, staleness) are well-chosen for belief verification. The structured output format (VERDICT line followed by sections) enables reliable parsing downstream. The template variables align with what `_research_one` provides.
---

### 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: Standard re-export. `RESEARCH_PROMPT` added to imports and `__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: Follows the exact pattern of `get_issue` (line 50) and reuses the existing `_normalize_pr` (line 113). The JSON fields requested match what `_normalize_pr` expects — they're identical to those in `list_prs` (line 96). The `hasattr(source, "get_pr")` guard in `_fetch_artifacts` correctly handles GitLabSource and JiraSource which lack this method.
---

### 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 via line-prefix matching (`Text:`, `Status:`, etc.). This is fragile — if the output format changes, parsing silently produces empty strings rather than failing. More importantly, multi-line `Text:` values (belief text spanning multiple lines) would only capture the first line. The `dependents` field is parsed but never used — `_get_dependent_beliefs` reads from `network.json` instead. Not a bug, but dead code. Minor: should this use `reasons show --json` or similar structured output if available?
---

### ftl_project_expert/cli.py:_extract_issue_refs
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Correctly deduplicates via `seen` set. The Jira-key regex `[A-Z][A-Z0-9]+-\d+` is standard. Cross-platform prefix filtering (`GH-`, `PR-`, `GL-`, `MR-`) prevents double-matching. The `(?<!\w)` lookbehind on `#123` avoids matching hex colors or anchors. Could produce false positives on abbreviations like `HTTP-200` but this is an inherent limitation of regex-based extraction and acceptable.
---

### ftl_project_expert/cli.py:_fetch_artifacts
VERDICT: CONCERN
CORRECTNESS: QUESTIONABLE
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Two concerns: (1) For Jira, `get_issue` takes `key: str`, but for GitHub/GitLab it takes `number: int`. The routing logic (`"key" in ref` → Jira path, else → `ref["number"]`) is correct today because `_extract_issue_refs` only puts `"key"` on Jira-style refs and `"number"` on numeric refs. But this coupling is implicit and fragile — a ref with both `"key"` and `"number"` would take the Jira path regardless of platform. (2) GitLabSource has no `get_pr` method, so `MR-123` references on GitLab silently fall through to the `else` branch producing `(Could not fetch pr #123)`. This is handled but worth noting as a gap — users on GitLab who reference MRs in beliefs will get no verification data for those references.
---

### ftl_project_expert/cli.py:_select_beliefs_for_research
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: UNTESTED
INTEGRATION: WIRED
REASONING: Selection logic is straightforward. `--negative` filters to OUT beliefs, default is IN (matching commit message). `--high-impact` sorts by dependent count descending. Stable fallback sort by node ID. The `limit` parameter caps output. No issues.
---

### 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 patterns established by `review_proposals` (parallel LLM invocation, entry creation, error handling). Three concerns: (1) **No tests at all** — `research_tests.test_count = 0`. The helper functions (`_extract_issue_refs`, `_select_beliefs_for_research`, `_get_belief_info`) are all unit-testable and should have coverage, especially the regex extraction. (2) **Verdict extraction is fragile** — scans for `VERDICT:` prefix but the LLM could format it differently (e.g., `**VERDICT:**`, `Verdict:`, embedded in markdown). A regex like `r"^VERDICT:\s*"` with `re.IGNORECASE` would be more robust. (3) The `_emit(ctx, result)` at the end outputs the full LLM response to stdout (non-quiet mode), which is correct for piping, but combined with the entry creation means the same content goes to two destinations — this is fine but means the command is chatty.
---

### SELF_REVIEW
LIMITATIONS: Could not verify `reasons show` output format to confirm parsing correctness in `_get_belief_info`. Could not see if `reasons` supports structured (JSON) output that would eliminate fragile text parsing. No test files were available to check — the observations confirm zero test coverage. Could not verify whether `_load_network` might return stale data if `network.json` exists on disk but is outdated (vs. live `reasons export`).
---

### FEATURE_REQUESTS
- Show the `reasons show` CLI output format (or its `--help`) so I can verify the text parser matches actual output
- Include related source classes (GitLabSource, JiraSource) in full when new methods are added to one source but not others, to assess feature parity gaps
- Flag when a new command has zero test coverage as a structured finding, not just an observation
---

**Summary**: The research command is well-integrated and follows existing codebase patterns. The main gap is **zero test coverage** — `_extract_issue_refs` and `_select_beliefs_for_research` in particular are pure functions that should have unit tests. Secondary concerns are fragile text parsing in `_get_belief_info` (consider structured output from `reasons`) and fragile verdict extraction from LLM output. No blockers.
