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:
- Live surgical fetch missing on issues.
get_conversation is dispatched to PullRequestService.getConversation only (ai/mcp/server/github-workflow/services/toolService.mjs line 22), which queries GraphQL's pullRequest field (PullRequestService.mjs line 115). The OpenAPI schema requires pr_number (openapi.yaml lines 277-279). No code path serves issues.
- Empirical impact. During this session's epic-review on #10691, fetching the epic body to surface @neo-gemini-3-1-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
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:
- 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.
- New
GET_ISSUE_CONVERSATION GraphQL query in issueQueries.mjs, symmetric to GET_CONVERSATION for the issue field shape (title, body, author, comments.nodes).
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.
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.
- 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).
- 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
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"
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 callget_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:
get_conversationis dispatched toPullRequestService.getConversationonly (ai/mcp/server/github-workflow/services/toolService.mjsline 22), which queries GraphQL'spullRequestfield (PullRequestService.mjsline 115). The OpenAPI schema requirespr_number(openapi.yamllines 277-279). No code path serves issues.get_local_issue_by_idreturning the full markdown. The same review on a PR would have usedget_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_idprecedence >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— service dispatch table.get_conversationbinds toPullRequestServiceonly.ai/mcp/server/github-workflow/services/PullRequestService.mjs:80-152—getConversation(options)with selector precedence. Selector logic operates onpullRequest.comments?.nodes; same shape exists forissue.comments.nodes.ai/mcp/server/github-workflow/services/IssueService.mjs— nogetConversationmethod.ai/mcp/server/github-workflow/services/queries/pullRequestQueries.mjs:16—GET_CONVERSATIONGraphQL query, PR-shaped.ai/mcp/server/github-workflow/services/queries/issueQueries.mjs— already hosts issue queries; symmetricGET_ISSUE_CONVERSATIONslots in cleanly.ai/mcp/server/github-workflow/openapi.yaml:247-310—get_conversationoperation,pr_numberrequired, no issue path.The Fix
Extend the existing
get_conversationtool to accept eitherpr_numberorissue_number, reusing the selector contract. One tool, two backends, identical caller experience.Concrete touches:
IssueService.getConversation(options)mirroring the PR method shape. Same selector precedence (comment_id>since_comment_id>last_n> full). Queries the GraphQLissuefield instead ofpullRequest.GET_ISSUE_CONVERSATIONGraphQL query inissueQueries.mjs, symmetric toGET_CONVERSATIONfor theissuefield shape (title,body,author,comments.nodes).toolService.mjsdispatch update: routeget_conversationto a thin router that picksPullRequestService.getConversationorIssueService.getConversationbased on input shape (pr_numbervsissue_number). One tool, two backends.openapi.yamlschema update: changepr_numberfrom required to one-of (pr_numberxorissue_number). Keep selectors unchanged. Update tool description to mention both PR and issue use cases.getConversationsuite for the issue path. Cover all three selectors + full-fetch default + bad-input rejection (neither pr_number nor issue_number, or both supplied)..agents/skills/epic-review/references/epic-review-workflow.md§2 already prescribesget_conversationfor "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 matchingPullRequestService.getConversation.GET_ISSUE_CONVERSATIONGraphQL query inissueQueries.mjsreturns issue title, body, author, and comments shape.get_conversationMCP tool acceptspr_numberORissue_number(mutually exclusive); both selector sets work identically.MISSING_ARGUMENTSerror.pr_numberandissue_numberreturns a structured error (decide single source of truth — do not silently prefer one in PR review).comment_id(issue),since_comment_id(issue),last_n(issue), missing both, both supplied.get_conversationtest suite.Out of Scope
DiscussionServicedoesn't yet have agetConversationmethod. If needed, file a separate ticket — same pattern, different service.get_local_issue_by_idwith timeline-elision selectors is a complementary local-path optimization (stale-but-cheap vs live-and-surgical). Different concern; would deserve its own ticket.get_local_issue_by_idcallers. Agents adopt the new tool naturally as workflow docs reference it; no migration sweep needed.get_conversationcontract. Strictly additive.Avoided Traps / Gold Standards Rejected
get_issue_conversationtool. Adds parallel tool surface for an identical contract (selectors are the same; only the resource type differs). The existingget_conversationalready abstracts the conversation concern; dual-purposing the dispatch is the elegance.get_local_issue_by_idwith 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.ConversationQueryhelper. Tempting DRY, but the GraphQL field shapes differ enough between issue and pullRequest types that the abstraction would mostly be parameter passing. Premature.Related
.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"