Now I have the full picture. Here's the review:

---

## Code Review: `research` command

### 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 with well-structured output format. The `VERDICT:` line at the top of the output is parseable and the format placeholders match exactly what `_research_one` provides. No issues.
---

### 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, consistent with existing pattern.
---

### 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 same pattern as `get_issue` (line 50-63) and uses the same JSON fields as `list_prs` (line 82-111). Delegates to `_normalize_pr` which already exists and is well-tested via the `list_prs` path. 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: The parsing is fragile — it assumes `reasons show` outputs lines like `Text: ...`, `Status: ...`, etc. with exactly those prefixes. If the `reasons` CLI output format changes (e.g., multi-line text values, indentation), this will silently truncate or miss data. The `Text:` field only captures the remainder of the first matching line, so multi-line belief text will be incomplete. Not a blocker since this is a best-effort diagnostic tool, but worth noting.
---

### 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: Deduplication via `seen` set is correct. The Jira-key regex correctly excludes the `GH-`/`PR-`/`GL-`/`MR-` prefixes to avoid double-matching. The `#123` pattern uses a negative lookbehind `(?<!\w)` to avoid matching inside URLs or other tokens. One minor note: `#123` is added as `("issue", num)` but could also be a PR on GitHub — however the code handles this gracefully since `_fetch_artifacts` falls back to `get_issue` for non-PR types on non-GitHub platforms or when `get_pr` is unavailable.
---

### 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: PARTIAL
REASONING: Two issues: (1) `GitHubSource.get_issue` takes `number: int` but `JiraSource.get_issue` takes `key: str`. The code passes `ref["number"]` (an int) for the issue branch and `ref["key"]` (a string) for the Jira branch, which is correct for the Jira-key case. However, for `#123` references against a Jira source, the code will enter the `ref["type"] == "issue"` branch and call `source.get_issue(ref["number"])` with an int — Jira's `get_issue` expects a string key like `PROJ-123`, so this will fail at the Jira API level. This is a minor edge case since `#123` references are unlikely in a Jira-configured project, and the exception is caught. (2) GitLab sources have no `get_pr` method, which is fine — the `hasattr(source, "get_pr")` guard handles this — but GitLab MR references (`MR-123`) are excluded from `_extract_issue_refs` entirely, so MRs won't be fetched for GitLab projects. This is a gap but not a regression.
---

### 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: The filter logic for the non-negative case is effectively a no-op — the `if not negative and not has_justifications: pass` block does nothing, so all beliefs pass through regardless. This means `--negative` filters to OUT beliefs, but without `--negative`, every belief is a candidate (premises and derived alike). This may be intentional (research any belief), but the `pass` statement is dead code that suggests unfinished filtering logic. Additionally, when `negative=True`, the filter checks `tv != "OUT"` but the truth_value field name depends on `_load_network()` returning data from `reasons export` — worth verifying the field name matches.
---

### 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 the same patterns as `review_proposals` (parallel/sequential LLM invocation, entry creation, emit). Two minor points: (1) `_load_network()` is called twice — once in the `else` branch when selecting beliefs, and unconditionally again at line ~1512. The second call could reuse the result from the first. Not a correctness bug but wasteful when `reasons export` must be invoked. (2) `project_dir = _get_project_dir()` is assigned but never used — dead code. (3) The verdict extraction loop (`for line in result.splitlines()`) correctly finds the first `VERDICT:` line, which matches the prompt's output format.
---

### Overall Assessment
VERDICT: CONCERN
REASONING: The feature is well-integrated, follows existing patterns, and the prompt template is solid. The main concerns are: (1) **zero test coverage** — no tests exist for any of the new functions (`_get_belief_info`, `_extract_issue_refs`, `_fetch_artifacts`, `_select_beliefs_for_research`, or the `research` command itself), (2) a dead `pass` statement in `_select_beliefs_for_research` suggesting incomplete filtering logic, (3) double `_load_network()` call, and (4) unused `project_dir` variable. None of these are blockers, but `_extract_issue_refs` in particular would benefit from unit tests given the regex complexity.

---

### SELF_REVIEW
LIMITATIONS: Could not verify the exact output format of `reasons show` to confirm `_get_belief_info` parsing correctness. Could not verify whether `truth_value` is the correct field name in the `reasons export` JSON schema. No test files were included or found — confirmed zero test coverage via observation results.
---

### FEATURE_REQUESTS
- Include the `reasons show` CLI output format (or a sample) when the diff touches `reasons` parsing code, to verify field name assumptions
- Auto-detect and flag unused variable assignments in the diff (like `project_dir` here)
---
