Skip to content

[Feat] Resolve bare issue references (#234, ENG-512) against configured repositories#722

Draft
daniel-lxs wants to merge 3 commits into
developfrom
feature/router-bare-issue-references
Draft

[Feat] Resolve bare issue references (#234, ENG-512) against configured repositories#722
daniel-lxs wants to merge 3 commits into
developfrom
feature/router-bare-issue-references

Conversation

@daniel-lxs

Copy link
Copy Markdown
Member

What changed

Stacked on #718 (provider registry) — review and merge that first; this PR's base is its branch, so once #718 lands this can be retargeted to develop and will show only the bare-reference diff.

Closes the last gap in the lookup gate: the routing precheck already extracts references like #234, acme/web#42, or ENG-512 into its externalReference field, but nothing acted on it when the message had no pasted URL — the flag stayed telemetry-only for bare references. Now, when the precheck returns needsExternalLookup=true and URL parsing finds nothing, gatherExternalIssueContext resolves the reference deterministically:

  • ENG-512-style identifiers → Linear get_issue directly.
  • owner/repo#N → fetched only if that repo is in the deployment's configured repositoryNames (case-insensitive match, configured casing wins).
  • Bare #N / N → fans out across configured repositories, but only when there are at most 3 (else fail open); each result is labeled per repo ([acme/web#234]) so the informed re-route call disambiguates.

Bare references normalize to canonical URLs and run through #718's registry, so fetch-attempt construction (incl. GitHub's issue_readget_issue fallback) stays single-source. Pasted URLs always take precedence over the precheck reference.

Security posture unchanged

The model proposes reference text only; fetch targets come from a closed candidate set (the deployment's own configured repos / the connected Linear workspace), execution stays in server code under the existing shared 8s fail-open deadline. A hallucinated or injection-steered reference can at most read issues the deployment already routes for — the same authority a pasted URL has.

Tests

Five new external-issue-context cases (bare Linear id, multi-repo fan-out with per-repo labels, repo-qualified validation incl. unconfigured-repo rejection, too-many-repos fail-open, URL precedence) plus an end-to-end router-service case (Check issue #234 → fetch → phase: 'mcp'). 13 files / 104 router tests green, pnpm knip clean, tsc --noEmit clean.

🤖 Generated with Claude Code

daniel-lxs and others added 2 commits July 23, 2026 10:04
parseExternalIssueReferences hardcoded the GitHub and Linear URL shapes
and fetchExternalIssueContext hardcoded each provider's MCP call
sequence. Both now read from a declarative registry
(external-issue-providers.ts): each provider states its hostnames, the
pathname pattern that identifies one of its issues, and the ordered MCP
fetch attempts a match produces (preserving GitHub's issue_read ->
get_issue fallback chain). Adding SCM support becomes one registry
entry alongside the MCP server itself and its mcp-policy allowlist.

Pure internal refactor: URL extraction, the 2-issue and 8k-char caps,
the shared 8s fail-open deadline, the untrusted-context output format,
and the model-never-picks-fetch-targets boundary are all unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The routing precheck already extracts references like #234, acme/web#42,
or ENG-512 into externalReference, but nothing acted on it when the
message carried no pasted URL. When the precheck asks for a lookup and
URL parsing finds nothing, the reference now resolves deterministically
against the deployment's own configuration: repo-qualified numbers must
match a configured repository, bare numbers fan out across configured
repositories only when there are at most three (labeled per repo so the
informed re-route can disambiguate), and Linear-style identifiers fetch
directly. Bare references normalize to canonical URLs and reuse the
provider registry, so fetch-attempt construction stays single-source.

The model still never picks fetch targets: it proposes reference text,
code validates against a closed candidate set and executes under the
existing shared 8s fail-open deadline. Pasted URLs take precedence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@roomote-roomote

roomote-roomote Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

No new code issues found. See task

Reviewed e89da82

…xpires

A new fake-timer test for the shared 8s deadline exposed that
withExternalLookupTimeout received an already-invoked promise, so an
exhausted deadline still fired the next MCP call and merely ignored its
result (pre-existing since #693). The helper now takes a thunk and
checks the remaining budget before dispatching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@daniel-lxs

Copy link
Copy Markdown
Member Author

Added the fake-timer test for the shared 8s deadline — and it caught a real (pre-existing, since #693) bug: withExternalLookupTimeout received an already-invoked promise, so once the deadline was exhausted the next MCP fetch attempt was still dispatched and merely never awaited. The deadline gated waiting, not dispatching. Fixed in e89da82 by making the helper take a thunk and check the remaining budget before invoking. Net behavior change: a hung first fetch no longer fires a second fire-and-forget MCP call after the budget expires. 105 router tests green.

Base automatically changed from feature/external-issue-provider-registry to develop July 23, 2026 16:49
@daniel-lxs
daniel-lxs marked this pull request as draft July 23, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant