---
id: 10702
title: Extend get_conversation to issues (selector parity with PRs)
state: CLOSED
labels:
  - enhancement
  - ai
assignees:
  - neo-opus-ada
createdAt: '2026-05-04T18:56:07Z'
updatedAt: '2026-06-07T17:23:59Z'
githubUrl: 'https://github.com/neomjs/neo/issues/10702'
author: neo-opus-ada
commentsCount: 1
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-22T08:10:05Z'
---
# Extend get_conversation to issues (selector parity with PRs)

## Context

This session's epic-review on #10691 surfaced a gap in surgical-fetch primitives. The PR-side comment-id selector contract shipped via #10272 (`get_conversation({comment_id, since_comment_id, last_n})`) has no issue twin. Agents reviewing epics or fetching specific issue comments must currently call `get_local_issue_by_id`, which returns the entire markdown file — frontmatter, body, and the full timeline of cross-references.

For long-lived epics like #10030 (3000+ lines, ~80% timeline cross-references with no review-time signal), this drains context budget for content that isn't load-bearing.

## The Problem

Two related symptoms:

1. **Live surgical fetch missing on issues.** `get_conversation` is dispatched to `PullRequestService.getConversation` only ([`ai/mcp/server/github-workflow/services/toolService.mjs`](../blob/dev/ai/mcp/server/github-workflow/services/toolService.mjs) line 22), which queries GraphQL's `pullRequest` field ([`PullRequestService.mjs`](../blob/dev/ai/mcp/server/github-workflow/services/PullRequestService.mjs) line 115). The OpenAPI schema requires `pr_number` (`openapi.yaml` lines 277-279). No code path serves issues.
2. **Empirical impact.** During this session's epic-review on #10691, fetching the epic body to surface @neo-gemini-pro's prior comment required `get_local_issue_by_id` returning the full markdown. The same review on a PR would have used `get_conversation({pr_number, comment_id})` for ~10× less surface. The cost compounds across epic-review + ticket-intake + memory-mining workflows that all touch issues.

The selector contract itself (`comment_id` precedence > `since_comment_id` > `last_n` > full) is generic. Only the GraphQL query is PR-specific.

## The Architectural Reality

- [`ai/mcp/server/github-workflow/services/toolService.mjs:18-38`](../blob/dev/ai/mcp/server/github-workflow/services/toolService.mjs) — service dispatch table. `get_conversation` binds to `PullRequestService` only.
- [`ai/mcp/server/github-workflow/services/PullRequestService.mjs:80-152`](../blob/dev/ai/mcp/server/github-workflow/services/PullRequestService.mjs) — `getConversation(options)` with selector precedence. Selector logic operates on `pullRequest.comments?.nodes`; same shape exists for `issue.comments.nodes`.
- [`ai/mcp/server/github-workflow/services/IssueService.mjs`](../blob/dev/ai/mcp/server/github-workflow/services/IssueService.mjs) — no `getConversation` method.
- [`ai/mcp/server/github-workflow/services/queries/pullRequestQueries.mjs:16`](../blob/dev/ai/mcp/server/github-workflow/services/queries/pullRequestQueries.mjs) — `GET_CONVERSATION` GraphQL query, PR-shaped.
- [`ai/mcp/server/github-workflow/services/queries/issueQueries.mjs`](../blob/dev/ai/mcp/server/github-workflow/services/queries/issueQueries.mjs) — already hosts issue queries; symmetric `GET_ISSUE_CONVERSATION` slots in cleanly.
- [`ai/mcp/server/github-workflow/openapi.yaml:247-310`](../blob/dev/ai/mcp/server/github-workflow/openapi.yaml) — `get_conversation` operation, `pr_number` required, no issue path.

## The Fix

Extend the existing `get_conversation` tool to accept either `pr_number` or `issue_number`, reusing the selector contract. One tool, two backends, identical caller experience.

**Concrete touches:**

1. **New `IssueService.getConversation(options)`** mirroring the PR method shape. Same selector precedence (`comment_id` > `since_comment_id` > `last_n` > full). Queries the GraphQL `issue` field instead of `pullRequest`.
2. **New `GET_ISSUE_CONVERSATION` GraphQL query** in `issueQueries.mjs`, symmetric to `GET_CONVERSATION` for the `issue` field shape (`title`, `body`, `author`, `comments.nodes`).
3. **`toolService.mjs` dispatch update**: route `get_conversation` to a thin router that picks `PullRequestService.getConversation` or `IssueService.getConversation` based on input shape (`pr_number` vs `issue_number`). One tool, two backends.
4. **`openapi.yaml` schema update**: change `pr_number` from required to one-of (`pr_number` xor `issue_number`). Keep selectors unchanged. Update tool description to mention both PR and issue use cases.
5. **Tests**: Playwright unit tests symmetric to the PR `getConversation` suite for the issue path. Cover all three selectors + full-fetch default + bad-input rejection (neither pr_number nor issue_number, or both supplied).
6. **Workflow doc verification**: `.agents/skills/epic-review/references/epic-review-workflow.md` §2 already prescribes `get_conversation` for "the live epic issue body and comment thread directly from GitHub" — once this lands, the prescription will actually work end-to-end. No skill content change needed; verify in PR.

**Token-budget anchor:** for an epic the size of #10030 (3000+ lines, mostly timeline cross-references), `get_conversation({issue_number, comment_id})` would surface the same target content in ~50 lines. Multi-cycle session savings compound across epic-review, ticket-intake, and memory-mining workflows.

## Acceptance Criteria

- [ ] `IssueService.getConversation(options)` exists with selector precedence matching `PullRequestService.getConversation`.
- [ ] `GET_ISSUE_CONVERSATION` GraphQL query in `issueQueries.mjs` returns issue title, body, author, and comments shape.
- [ ] `get_conversation` MCP tool accepts `pr_number` OR `issue_number` (mutually exclusive); both selector sets work identically.
- [ ] Calling with neither parameter returns a structured `MISSING_ARGUMENTS` error.
- [ ] Calling with both `pr_number` and `issue_number` returns a structured error (decide single source of truth — do not silently prefer one in PR review).
- [ ] OpenAPI schema reflects the dual-input shape; description mentions both PR and issue use cases.
- [ ] Unit tests cover: full fetch (issue), `comment_id` (issue), `since_comment_id` (issue), `last_n` (issue), missing both, both supplied.
- [ ] No regression on the existing PR-side `get_conversation` test suite.

## Out of Scope

- **Discussion-conversation surgical fetch.** `DiscussionService` doesn't yet have a `getConversation` method. If needed, file a separate ticket — same pattern, different service.
- **Local-side surgical fetch.** Extending `get_local_issue_by_id` with timeline-elision selectors is a complementary local-path optimization (stale-but-cheap vs live-and-surgical). Different concern; would deserve its own ticket.
- **Bulk migration of existing `get_local_issue_by_id` callers.** Agents adopt the new tool naturally as workflow docs reference it; no migration sweep needed.
- **Changing the existing PR-side `get_conversation` contract.** Strictly additive.

## Avoided Traps / Gold Standards Rejected

- **Rejected: separate `get_issue_conversation` tool.** Adds parallel tool surface for an identical contract (selectors are the same; only the resource type differs). The existing `get_conversation` already abstracts the conversation concern; dual-purposing the dispatch is the elegance.
- **Rejected: bulk-extend `get_local_issue_by_id` with selectors instead.** Solves a different problem (local stale-but-cheap path vs live surgical path). Mixing them obscures both. Local-path optimization is a separate ticket if pursued.
- **Rejected: lift the GraphQL conversation query into a shared `ConversationQuery` helper.** Tempting DRY, but the GraphQL field shapes differ enough between issue and pullRequest types that the abstraction would mostly be parameter passing. Premature.

## Related

- Predecessor: #10272 (Comment-ID-aware PR workflow — shipped the PR-side primitive)
- Empirical anchor: epic-review on #10691 (surfaced this gap during the current session — see comment IC_kwDODSospM8AAAABBK8osQ)
- Adjacent token-budget vectors: #10537 (skill modularization), #10083 (AGENTS.md §9 single-full-read softening)
- Workflow consumers (post-fix): `.agents/skills/epic-review/references/epic-review-workflow.md` §2; `.agents/skills/pr-review/...`; `.agents/skills/ticket-intake/...`

Origin Session ID: 7e52099b-9632-4c67-a2a1-4e1a1ad1c414

Retrieval Hint: "get_conversation issue surgical fetch comment_id selector parity PR token budget"

## Timeline

- 2026-05-04T18:56:08Z @neo-opus-ada added the `enhancement` label
- 2026-05-04T18:56:08Z @neo-opus-ada added the `ai` label
### @neo-opus-ada - 2026-05-22T02:32:01Z

**Input from Claude Opus 4.7 (Claude Code):**

> ❋ `[TOOLING_GAP]` **Re-surfaced 2026-05-22 — operator-flagged; recurred during `ticket-intake`.**
> 
> @tobiu flagged `get_conversation`'s PR-only scope again today. It recurred live while I ran `ticket-intake` on #11724: `get_conversation({pr_number: 11724})` → `Could not resolve to a PullRequest with the number of 11724` (#11724 is an issue).
> 
> **Priority-elevating angle not yet in the body:** `ticket-intake-workflow.md` §1.1 carries a hard `MUST` — *"You MUST use the `get_conversation` tool to fetch the live issue body and comment thread directly from GitHub"* — for **every** issue intake. With `get_conversation` PR-only, that mandated step always errors on issues; every agent running `ticket-intake` silently falls back to `gh issue view`. So this isn't only the surgical-fetch / token-budget parity the body frames (epic-review anchor) — it's a **live broken skill-contract**: a `MUST`-step that cannot succeed as written. ROI reframes from "nice selector parity" to "fix a mandated workflow step that currently always fails."
> 
> The Fix prescription still holds — I re-verified `get_conversation` still dispatches to `PullRequestService.getConversation` only. One freshness note for whoever implements: the github-workflow MCP source layout has shifted since this was filed (2026-05-04) — the `toolService.mjs` dispatch is now at `ai/mcp/server/github-workflow/toolService.mjs:98`, not the `services/`-prefixed path the body links. Refresh the §Architectural Reality `file:line` anchors at implementation time.
> 
> Still OPEN + unassigned — clean pickable lane.

- 2026-05-22T02:55:02Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-22T07:08:39Z @neo-opus-ada cross-referenced by PR #11746
- 2026-05-22T07:27:46Z @neo-opus-ada referenced in commit `36e72b8` - "docs(github-workflow): get_conversation issue+PR guide sync (#10702)

GitHubWorkflow.md section 4.3 still described get_conversation as PR-only
via GET_CONVERSATION. Sync it to the dual-input contract: pull request or
issue, exactly one of pr_number / issue_number, shared comment selectors.
Addresses the PR #11746 cycle-1 Required Action.

Co-Authored-By: neo-opus-4-7 <neo-opus-4-7@neomjs.com>"
- 2026-05-22T08:10:05Z @tobiu referenced in commit `a2bb8b6` - "feat(github-workflow): extend get_conversation to issues (#10702) (#11746)

* feat(github-workflow): route get_conversation to issues + PRs (#10702)

get_conversation dispatched to PullRequestService only; issue numbers were
unresolvable. Adds IssueService.getConversation + GET_ISSUE_CONVERSATION
(issue-side twins of the PR path, identical selector contract), a
getConversationRouter dispatch picking the service by pr_number xor
issue_number, and the dual-input openapi schema. Also strips pre-existing
trailing whitespace the whole-file check-whitespace hook flagged in the two
touched files.

Co-Authored-By: neo-opus-4-7 <neo-opus-4-7@neomjs.com>

* test(github-workflow): cover get_conversation issue routing + selectors (#10702)

IssueService.getConversation: 11 cases mirroring the PR selector suite
(full / comment_id / since_comment_id / last_n / precedence / missing
issue_number / GraphQL-error). getConversationRouter: 5 cases pinning
pr_number-xor-issue_number routing + AMBIGUOUS/MISSING rejections.

Co-Authored-By: neo-opus-4-7 <neo-opus-4-7@neomjs.com>

* docs(github-workflow): get_conversation issue+PR guide sync (#10702)

GitHubWorkflow.md section 4.3 still described get_conversation as PR-only
via GET_CONVERSATION. Sync it to the dual-input contract: pull request or
issue, exactly one of pr_number / issue_number, shared comment selectors.
Addresses the PR #11746 cycle-1 Required Action.

Co-Authored-By: neo-opus-4-7 <neo-opus-4-7@neomjs.com>

---------

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-22T08:10:05Z @tobiu closed this issue
- 2026-05-25T02:11:32Z @neo-opus-ada cross-referenced by PR #11949

