From 952eb33c04a42d8d29a59546022729aa1780be1b Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:56:57 +0200 Subject: [PATCH 1/3] feat(enrichment): forward linked issue envelope to REES Resolve pr.linkedIssues into a compact linkedIssue payload via the local issue cache so REES history can correlate PR work with issue context. Co-authored-by: Cursor --- src/queue/processors.ts | 8 +++ src/review/enrichment-wire.ts | 18 ++++++ test/unit/enrichment-wire.test.ts | 86 +++++++++++++++++++++++++++++ test/unit/enrichment-wiring.test.ts | 21 ++++++- 4 files changed, 132 insertions(+), 1 deletion(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 24492d633d..070266feb0 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -335,6 +335,7 @@ import { buildReviewEnrichment, isEnrichmentEnabled, isReesGithubTokenForwardingEnabled, + resolveEnrichmentLinkedIssue, } from "../review/enrichment-wire"; import { captureReviewFailure } from "../selfhost/sentry"; import { evaluateWithSurfaceLane } from "../review/content-lane-wire"; @@ -3646,6 +3647,7 @@ export async function runAiReviewForAdvisory( title: string; body?: string | null | undefined; baseSha?: string | null | undefined; + linkedIssues?: number[] | undefined; }; author: string | null; confirmedContributor: boolean; @@ -3817,6 +3819,11 @@ export async function runAiReviewForAdvisory( // its public-safe brief splices into the prompt next to grounding + RAG. Flag-OFF (default) → no call, no branch, // byte-identical prompt. Fully fail-safe (any timeout/error/empty → undefined → review proceeds). const enrichmentDiff = buildAiReviewDiff(files); + const enrichmentLinkedIssue = await resolveEnrichmentLinkedIssue( + env, + args.repoFullName, + args.pr.linkedIssues ?? [], + ); const enrichment = isEnrichmentEnabled(env) && convergedRepoAllowed ? await buildReviewEnrichment(env, { @@ -3827,6 +3834,7 @@ export async function runAiReviewForAdvisory( title: args.pr.title, body: args.pr.body ?? undefined, author: args.author, + linkedIssue: enrichmentLinkedIssue, githubToken: isReesGithubTokenForwardingEnabled(env) ? await resolveReviewEnrichmentGithubToken( env, diff --git a/src/review/enrichment-wire.ts b/src/review/enrichment-wire.ts index 938235dbbc..2b967659a0 100644 --- a/src/review/enrichment-wire.ts +++ b/src/review/enrichment-wire.ts @@ -6,6 +6,7 @@ // Single env switch: GITTENSORY_REVIEW_ENRICHMENT (+ REES_URL must be set, so the hosted Worker — which sets neither // — is unaffected). Default OFF → gathers nothing, prompt byte-identical. FULLY FAIL-SAFE: any timeout / non-200 / // network / parse error, or an empty brief, returns undefined and the review proceeds on diff + grounding + RAG. +import { getIssue } from "../db/repositories"; import { sanitizePublicComment } from "../queue-intelligence"; import { neutralizePromptInjection } from "./prompt-injection"; import type { PullRequestFileRecord } from "../types"; @@ -155,6 +156,23 @@ interface EnrichmentInput { diff: string; } +/** Resolve the PR's primary linked issue into the compact REES envelope (#1478). */ +export async function resolveEnrichmentLinkedIssue( + env: Env, + repoFullName: string, + linkedIssues: number[], +): Promise { + const number = linkedIssues.find((candidate) => Number.isInteger(candidate) && candidate > 0); + if (!number) return undefined; + const issue = await getIssue(env, repoFullName, number).catch(() => null); + if (!issue) return { number }; + return { + number: issue.number, + ...(issue.title ? { title: issue.title } : {}), + ...(issue.body ? { body: issue.body } : {}), + }; +} + /** Optional comma-list of REES analyzers. Unset/"all" omits the field so REES runs its full registry. * An explicit typo-only list fails closed by sending [] rather than expanding to every analyzer. */ export function resolveReesAnalyzers(env: Env): string[] | undefined { diff --git a/test/unit/enrichment-wire.test.ts b/test/unit/enrichment-wire.test.ts index 33c85db7c0..39f77d61e0 100644 --- a/test/unit/enrichment-wire.test.ts +++ b/test/unit/enrichment-wire.test.ts @@ -7,7 +7,10 @@ import { resolveReesAnalyzerBudgetMs, resolveReesProfile, resolveReesTransportTimeoutMs, + resolveEnrichmentLinkedIssue, } from "../../src/review/enrichment-wire"; +import { createTestEnv } from "../helpers/d1"; +import { upsertIssueFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories"; const env = (o: Record) => o as unknown as Env; const input = { @@ -130,6 +133,27 @@ describe("buildReviewEnrichment", () => { ]); }); + it("includes linkedIssue in the REES POST when provided", async () => { + const calls: RequestInit[] = []; + globalThis.fetch = vi.fn(async (_url: unknown, init: RequestInit) => { + calls.push(init); + return { + ok: true, + json: async () => ({ promptSection: "brief" }), + } as Response; + }) as unknown as typeof fetch; + await buildReviewEnrichment(env({ REES_URL: "https://r" }), { + ...input, + linkedIssue: { number: 42, title: "Fix cache", body: "Details here." }, + }); + const body = JSON.parse(calls[0]!.body as string); + expect(body.linkedIssue).toEqual({ + number: 42, + title: "Fix cache", + body: "Details here.", + }); + }); + it("sends an analyzer budget below the transport timeout and accepts partial degraded briefs", async () => { let body: { budget?: { timeoutMs?: number; maxBriefChars?: number } } | undefined; globalThis.fetch = vi.fn(async (_url: unknown, init: RequestInit) => { @@ -538,6 +562,68 @@ describe("resolveReesAnalyzers", () => { }); }); +describe("resolveEnrichmentLinkedIssue", () => { + it("returns undefined when no linked issue numbers are provided", async () => { + const env = createTestEnv({}); + expect(await resolveEnrichmentLinkedIssue(env, "o/r", [])).toBeUndefined(); + expect(await resolveEnrichmentLinkedIssue(env, "o/r", [0, -1])).toBeUndefined(); + }); + + it("returns the compact envelope from the local issue cache", async () => { + const env = createTestEnv({}); + await upsertRepositoryFromGitHub( + env, + { name: "r", full_name: "o/r", private: false, owner: { login: "o" } }, + 1, + ); + await upsertIssueFromGitHub(env, "o/r", { + number: 42, + title: "Fix cache race", + body: "Repro steps inside.", + state: "open", + user: { login: "reporter" }, + labels: [], + html_url: "https://github.com/o/r/issues/42", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + }); + expect(await resolveEnrichmentLinkedIssue(env, "o/r", [42])).toEqual({ + number: 42, + title: "Fix cache race", + body: "Repro steps inside.", + }); + }); + + it("falls back to number-only when the issue is not cached locally", async () => { + const env = createTestEnv({}); + expect(await resolveEnrichmentLinkedIssue(env, "o/r", [99])).toEqual({ number: 99 }); + }); + + it("uses the first positive linked issue number", async () => { + const env = createTestEnv({}); + await upsertRepositoryFromGitHub( + env, + { name: "r", full_name: "o/r", private: false, owner: { login: "o" } }, + 1, + ); + await upsertIssueFromGitHub(env, "o/r", { + number: 7, + title: "Primary", + body: "", + state: "open", + user: { login: "reporter" }, + labels: [], + html_url: "https://github.com/o/r/issues/7", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + }); + expect(await resolveEnrichmentLinkedIssue(env, "o/r", [0, 7, 8])).toEqual({ + number: 7, + title: "Primary", + }); + }); +}); + describe("resolveReesProfile", () => { it("returns undefined for unset profiles", () => { expect(resolveReesProfile(env({}))).toBeUndefined(); diff --git a/test/unit/enrichment-wiring.test.ts b/test/unit/enrichment-wiring.test.ts index 28aeb81d4a..f08d1642e8 100644 --- a/test/unit/enrichment-wiring.test.ts +++ b/test/unit/enrichment-wiring.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { runAiReviewForAdvisory } from "../../src/queue/processors"; -import { upsertRepositoryFromGitHub } from "../../src/db/repositories"; +import { upsertRepositoryFromGitHub, upsertIssueFromGitHub } from "../../src/db/repositories"; import type { Advisory, RepositorySettings } from "../../src/types"; import { createTestEnv } from "../helpers/d1"; @@ -85,6 +85,17 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE REES_ANALYZERS: "secret,actionPin,redos", }); await seedRepoFile(env, "acme/widgets"); + await upsertIssueFromGitHub(env, "acme/widgets", { + number: 42, + title: "Linked bug", + body: "Issue context for history analyzer.", + state: "open", + user: { login: "reporter" }, + labels: [], + html_url: "https://github.com/acme/widgets/issues/42", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + }); const reesRequest: { url?: string; auth?: string | null; @@ -94,6 +105,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE author?: string; body?: string; githubToken?: string; + linkedIssue?: { number: number; title?: string; body?: string }; }; } = {}; const fetchSpy = vi @@ -108,6 +120,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE author?: string; body?: string; githubToken?: string; + linkedIssue?: { number: number; title?: string; body?: string }; }; return new Response( JSON.stringify({ @@ -128,6 +141,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE title: "Add a feature", body: "Implements the thing.", baseSha: "base7", + linkedIssues: [42], }, author: "alice", confirmedContributor: true, @@ -145,6 +159,11 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE expect(reesRequest.body?.author).toBe("alice"); expect(reesRequest.body?.body).toBe("Implements the thing."); expect(reesRequest.body?.githubToken).toBe("public-read-token"); + expect(reesRequest.body?.linkedIssue).toEqual({ + number: 42, + title: "Linked bug", + body: "Issue context for history analyzer.", + }); // The brief's content flows into the user prompt, but the system prompt carries our FIXED // enrichment suffix — the REES-supplied systemSuffix is untrusted and is never spliced in. expect(seenUser[0] ?? "").toContain("## EXTERNAL REVIEW BRIEF"); From 705899b2eba95bfb317c42fc9a25068398218aed Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:58:42 +0200 Subject: [PATCH 2/3] feat(enrichment): derive linked issue from PR body when unset When pr.linkedIssues is empty, parse Fixes #N from the PR description so REES history still receives linkedIssue context before sync catches up. Co-authored-by: Cursor --- src/queue/processors.ts | 3 +- src/review/enrichment-wire.ts | 12 +++++- test/unit/enrichment-wire.test.ts | 17 ++++++++ test/unit/enrichment-wiring.test.ts | 63 +++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 070266feb0..f9d2db74bf 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -336,6 +336,7 @@ import { isEnrichmentEnabled, isReesGithubTokenForwardingEnabled, resolveEnrichmentLinkedIssue, + resolveEnrichmentLinkedIssueNumbers, } from "../review/enrichment-wire"; import { captureReviewFailure } from "../selfhost/sentry"; import { evaluateWithSurfaceLane } from "../review/content-lane-wire"; @@ -3822,7 +3823,7 @@ export async function runAiReviewForAdvisory( const enrichmentLinkedIssue = await resolveEnrichmentLinkedIssue( env, args.repoFullName, - args.pr.linkedIssues ?? [], + resolveEnrichmentLinkedIssueNumbers(args.pr.linkedIssues, args.pr.body), ); const enrichment = isEnrichmentEnabled(env) && convergedRepoAllowed diff --git a/src/review/enrichment-wire.ts b/src/review/enrichment-wire.ts index 2b967659a0..eb2fc5f5eb 100644 --- a/src/review/enrichment-wire.ts +++ b/src/review/enrichment-wire.ts @@ -6,7 +6,7 @@ // Single env switch: GITTENSORY_REVIEW_ENRICHMENT (+ REES_URL must be set, so the hosted Worker — which sets neither // — is unaffected). Default OFF → gathers nothing, prompt byte-identical. FULLY FAIL-SAFE: any timeout / non-200 / // network / parse error, or an empty brief, returns undefined and the review proceeds on diff + grounding + RAG. -import { getIssue } from "../db/repositories"; +import { extractLinkedIssueNumbers, getIssue } from "../db/repositories"; import { sanitizePublicComment } from "../queue-intelligence"; import { neutralizePromptInjection } from "./prompt-injection"; import type { PullRequestFileRecord } from "../types"; @@ -156,6 +156,16 @@ interface EnrichmentInput { diff: string; } +/** Prefer explicit linkedIssues; fall back to Fixes #N parsing from the PR body. */ +export function resolveEnrichmentLinkedIssueNumbers( + linkedIssues: number[] | undefined, + body: string | null | undefined, +): number[] { + const explicit = (linkedIssues ?? []).filter((candidate) => Number.isInteger(candidate) && candidate > 0); + if (explicit.length > 0) return explicit; + return extractLinkedIssueNumbers(body ?? ""); +} + /** Resolve the PR's primary linked issue into the compact REES envelope (#1478). */ export async function resolveEnrichmentLinkedIssue( env: Env, diff --git a/test/unit/enrichment-wire.test.ts b/test/unit/enrichment-wire.test.ts index 39f77d61e0..bcca2297f5 100644 --- a/test/unit/enrichment-wire.test.ts +++ b/test/unit/enrichment-wire.test.ts @@ -8,6 +8,7 @@ import { resolveReesProfile, resolveReesTransportTimeoutMs, resolveEnrichmentLinkedIssue, + resolveEnrichmentLinkedIssueNumbers, } from "../../src/review/enrichment-wire"; import { createTestEnv } from "../helpers/d1"; import { upsertIssueFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories"; @@ -562,6 +563,22 @@ describe("resolveReesAnalyzers", () => { }); }); +describe("resolveEnrichmentLinkedIssueNumbers", () => { + it("prefers explicit linkedIssues over body parsing", () => { + expect(resolveEnrichmentLinkedIssueNumbers([7], "Fixes #42")).toEqual([7]); + }); + + it("parses Fixes #N from the PR body when linkedIssues is empty", () => { + expect(resolveEnrichmentLinkedIssueNumbers([], "Fixes #42\nCloses #99")).toEqual([42, 99]); + expect(resolveEnrichmentLinkedIssueNumbers(undefined, "Resolves #3")).toEqual([3]); + }); + + it("returns an empty list when neither source yields issue numbers", () => { + expect(resolveEnrichmentLinkedIssueNumbers([], "no issue refs")).toEqual([]); + expect(resolveEnrichmentLinkedIssueNumbers(undefined, undefined)).toEqual([]); + }); +}); + describe("resolveEnrichmentLinkedIssue", () => { it("returns undefined when no linked issue numbers are provided", async () => { const env = createTestEnv({}); diff --git a/test/unit/enrichment-wiring.test.ts b/test/unit/enrichment-wiring.test.ts index f08d1642e8..9352738a17 100644 --- a/test/unit/enrichment-wiring.test.ts +++ b/test/unit/enrichment-wiring.test.ts @@ -251,4 +251,67 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE fetchSpy.mockRestore(); } }); + + it("derives linkedIssue from Fixes #N in the PR body when linkedIssues is empty", async () => { + const run = vi.fn(async () => ({ response: notesJson })); + const env = createTestEnv({ + AI: { run } as unknown as Ai, + AI_SUMMARIES_ENABLED: "true", + AI_PUBLIC_COMMENTS_ENABLED: "true", + AI_DAILY_NEURON_BUDGET: "100000", + }); + Object.assign(env, { + GITTENSORY_REVIEW_ENRICHMENT: "true", + REES_URL: "https://rees.example", + REES_SHARED_SECRET: "sek", + }); + await seedRepoFile(env, "acme/widgets"); + await upsertIssueFromGitHub(env, "acme/widgets", { + number: 55, + title: "Body-linked bug", + body: "Parsed from PR description.", + state: "open", + user: { login: "reporter" }, + labels: [], + html_url: "https://github.com/acme/widgets/issues/55", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-01T00:00:00Z", + }); + let reesBody: { linkedIssue?: { number: number; title?: string; body?: string } } | undefined; + const fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockImplementation(async (url, init) => { + if (String(url).includes("/v1/enrich")) { + reesBody = JSON.parse(String(init?.body ?? "{}")) as { + linkedIssue?: { number: number; title?: string; body?: string }; + }; + return new Response(JSON.stringify({ promptSection: "brief" }), { + status: 200, + }); + } + return new Response("nope", { status: 404 }); + }); + try { + await runAiReviewForAdvisory(env, { + settings: { aiReviewMode: "advisory" } as RepositorySettings, + repoFullName: "acme/widgets", + pr: { + number: 7, + title: "Fix the bug", + body: "Fixes #55", + linkedIssues: [], + }, + author: "alice", + confirmedContributor: true, + advisory: adv("acme/widgets"), + }); + expect(reesBody?.linkedIssue).toEqual({ + number: 55, + title: "Body-linked bug", + body: "Parsed from PR description.", + }); + } finally { + fetchSpy.mockRestore(); + } + }); }); From c5a3465acb36ce20c4515a2236080f46916b5516 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:06:20 +0200 Subject: [PATCH 3/3] fix(enrichment): gate linked-issue resolution behind enrichment flag Move resolveEnrichmentLinkedIssue inside the isEnrichmentEnabled branch so flag-off reviews do no extra DB work. Addresses JSONbored review on #1863. Co-authored-by: Cursor --- src/queue/processors.ts | 14 ++++++++------ test/unit/enrichment-wiring.test.ts | 11 +++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/queue/processors.ts b/src/queue/processors.ts index f9d2db74bf..9d2f61b703 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -3820,11 +3820,6 @@ export async function runAiReviewForAdvisory( // its public-safe brief splices into the prompt next to grounding + RAG. Flag-OFF (default) → no call, no branch, // byte-identical prompt. Fully fail-safe (any timeout/error/empty → undefined → review proceeds). const enrichmentDiff = buildAiReviewDiff(files); - const enrichmentLinkedIssue = await resolveEnrichmentLinkedIssue( - env, - args.repoFullName, - resolveEnrichmentLinkedIssueNumbers(args.pr.linkedIssues, args.pr.body), - ); const enrichment = isEnrichmentEnabled(env) && convergedRepoAllowed ? await buildReviewEnrichment(env, { @@ -3835,7 +3830,14 @@ export async function runAiReviewForAdvisory( title: args.pr.title, body: args.pr.body ?? undefined, author: args.author, - linkedIssue: enrichmentLinkedIssue, + linkedIssue: await resolveEnrichmentLinkedIssue( + env, + args.repoFullName, + resolveEnrichmentLinkedIssueNumbers( + args.pr.linkedIssues, + args.pr.body, + ), + ), githubToken: isReesGithubTokenForwardingEnabled(env) ? await resolveReviewEnrichmentGithubToken( env, diff --git a/test/unit/enrichment-wiring.test.ts b/test/unit/enrichment-wiring.test.ts index 9352738a17..36a23f53f3 100644 --- a/test/unit/enrichment-wiring.test.ts +++ b/test/unit/enrichment-wiring.test.ts @@ -3,6 +3,7 @@ import { runAiReviewForAdvisory } from "../../src/queue/processors"; import { upsertRepositoryFromGitHub, upsertIssueFromGitHub } from "../../src/db/repositories"; import type { Advisory, RepositorySettings } from "../../src/types"; import { createTestEnv } from "../helpers/d1"; +import * as enrichmentWire from "../../src/review/enrichment-wire"; const notesJson = JSON.stringify({ assessment: "Looks fine.", @@ -174,7 +175,7 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE } }); - it("FLAG-OFF (default): the REES is never called", async () => { + it("FLAG-OFF (default): the REES is never called and linked issues are not resolved", async () => { const run = vi.fn(async () => ({ response: notesJson })); const env = createTestEnv({ AI: { run } as unknown as Ai, @@ -183,6 +184,10 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE AI_DAILY_NEURON_BUDGET: "100000", }); await seedRepoFile(env, "acme/off"); + const linkedIssueSpy = vi.spyOn( + enrichmentWire, + "resolveEnrichmentLinkedIssue", + ); let reesCalled = false; const fetchSpy = vi .spyOn(globalThis, "fetch") @@ -194,13 +199,15 @@ describe("review-enrichment wired into the processors review (flag GITTENSORY_RE await runAiReviewForAdvisory(env, { settings: { aiReviewMode: "advisory" } as RepositorySettings, repoFullName: "acme/off", - pr: { number: 7, title: "t", body: "b" }, + pr: { number: 7, title: "t", body: "Fixes #42", linkedIssues: [42] }, author: "alice", confirmedContributor: true, advisory: adv("acme/off"), }); expect(reesCalled).toBe(false); + expect(linkedIssueSpy).not.toHaveBeenCalled(); } finally { + linkedIssueSpy.mockRestore(); fetchSpy.mockRestore(); } });