From 64869e56a91e2fae2133ae796090d5b10aa9998d Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Sat, 11 Jul 2026 08:07:18 -0700 Subject: [PATCH] fix(chat): restrict grounding to public summaries --- src/services/ai-chat-qa.ts | 9 +++------ test/unit/ai-chat-qa.test.ts | 25 ++++++++++++++++++++----- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/services/ai-chat-qa.ts b/src/services/ai-chat-qa.ts index 11e1bfcd2..3e02c2642 100644 --- a/src/services/ai-chat-qa.ts +++ b/src/services/ai-chat-qa.ts @@ -51,8 +51,9 @@ const CHAT_QA_SYSTEM_PROMPT = "question, say so plainly and suggest running `@gittensory preflight` or `@gittensory blockers`."; // Private decision-pack blocker codes and boundary terms are redacted (not thrown on) before the grounding -// bundle is ever put in a prompt -- publicSafeSummary is already public-safe, but raw `blockedBy`/`why` can -// carry these. Mirrors github/commands.ts's publicBlockerDetail redaction intent without importing it. +// bundle is ever put in a prompt. Chat grounding intentionally uses only public-safe summaries and omits raw +// action rationale/blocker arrays, which can carry private readiness context. Mirrors github/commands.ts's +// publicBlockerDetail redaction intent without importing it. const PRIVATE_DECISION_BLOCKER_PATTERN = /\b(?:open_pr_pressure|closed_pr_credibility|low_credibility|maintainer_lane|inactive_or_unknown_lane|issue_discovery_only|merged_pr_history_floor|issue_discovery_validity_floor)\b/gi; const PRIVATE_BOUNDARY_TERM_PATTERN = @@ -67,8 +68,6 @@ type ChatGroundingAction = { actionType: string; status: string; publicSafeSummary: string; - why: string[]; - blockedBy: string[]; }; type ChatGroundingBundle = { @@ -189,8 +188,6 @@ function compactChatSignalBundle(bundle: AgentRunBundle): ChatGroundingBundle { actionType: action.actionType, status: action.status, publicSafeSummary: redactGroundingText(action.publicSafeSummary), - why: action.why.slice(0, 4).map(redactGroundingText).filter((line) => line.length > 0), - blockedBy: action.blockedBy.slice(0, 4).map(redactGroundingText).filter((line) => line.length > 0), })), freshnessWarnings: bundle.contextSnapshots.flatMap((snapshot) => snapshot.freshnessWarnings).slice(0, 8).map(redactGroundingText), }; diff --git a/test/unit/ai-chat-qa.test.ts b/test/unit/ai-chat-qa.test.ts index c0f7ea1c9..c894d18a5 100644 --- a/test/unit/ai-chat-qa.test.ts +++ b/test/unit/ai-chat-qa.test.ts @@ -254,6 +254,8 @@ describe("generateChatQaAnswer", () => { expect(userMessage).not.toMatch(/\bopen_pr_pressure\b/); expect(userMessage).not.toMatch(/\bwallet\b/i); expect(userMessage).not.toMatch(/\blikely_duplicate\b/); + expect(userMessage).not.toContain('"why"'); + expect(userMessage).not.toContain('"blockedBy"'); }); it("honors a custom model override and clamps output tokens", async () => { @@ -342,11 +344,24 @@ describe("__chatQaInternals", () => { expect(redactGroundingText("perfectly safe text")).toBe("perfectly safe text"); }); - it("compacts a bundle to at most 5 actions and filters out blank why/blockedBy lines after redaction", () => { - const compact = compactChatSignalBundle(bundleFixture()); - expect(compact.actions).toHaveLength(1); - expect(compact.actions[0]?.why).toHaveLength(2); - expect(compact.actions[0]?.why.every((line) => line.length > 0)).toBe(true); + it("compacts a bundle using only public-safe action summaries", () => { + const compact = compactChatSignalBundle( + bundleFixture(undefined, { + why: ["Closed PR rate is 35%.", "direct PR lane share 0.37"], + blockedBy: ["closed_pr_credibility"], + publicSafeSummary: "Use the public preflight summary for contributor-visible context.", + }), + ); + expect(compact.actions).toEqual([ + { + actionType: "cleanup_existing_prs", + status: "recommended", + publicSafeSummary: "Use the public preflight summary for contributor-visible context.", + }, + ]); + expect(JSON.stringify(compact)).not.toContain("Closed PR rate"); + expect(JSON.stringify(compact)).not.toContain("direct PR lane share"); + expect(JSON.stringify(compact)).not.toContain("closed_pr_credibility"); expect(compact.freshnessWarnings).toEqual(["fresh enough"]); });