From bf315900e162cbab3317787f3027e76986621365 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:58:59 -0600 Subject: [PATCH] fix: keep repo outcome patterns private --- src/github/commands.ts | 15 ++++++++---- src/services/decision-pack.ts | 6 ++--- test/unit/decision-pack.test.ts | 10 ++++---- test/unit/github-commands.test.ts | 40 +++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/github/commands.ts b/src/github/commands.ts index 0ec6a8171e..2c35c16c21 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -906,7 +906,7 @@ function duplicateCheckSections(bundle: AgentRunBundle | null | undefined): stri lines.push(`- ${publicBlockerLabel(code)}`); } const caution = [...action.why, action.riskImpact ?? ""] - .filter((item) => item.trim().length > 0 && (mentionsDuplicateRiskText(item) || /\blikely_duplicate\b/i.test(item))) + .filter(isPublicDuplicateCautionLine) .slice(0, 3) .map((item) => `- ${publicBlockerDetail(item)}`); lines.push(...caution); @@ -1271,15 +1271,22 @@ function formatActionBullets( } function mentionsDuplicateRisk(action: AgentActionRecord): boolean { - return [action.publicSafeSummary, action.recommendation, action.riskImpact ?? "", ...action.why, ...action.blockedBy].some((item) => - mentionsDuplicateRiskText(item), - ); + return [action.publicSafeSummary, action.recommendation, action.riskImpact ?? "", ...action.why, ...action.blockedBy].some(isPublicDuplicateCautionLine); } function mentionsDuplicateRiskText(value: string): boolean { return /\b(duplicate|overlap|wip|collision|concurrent|in[- ]progress)\b/i.test(value); } +function isPublicDuplicateCautionLine(value: string): boolean { + const detail = value.trim(); + return detail.length > 0 && !mentionsRepoOutcomePatternDetail(detail) && (mentionsDuplicateRiskText(detail) || /\blikely_duplicate\b/i.test(detail)); +} + +function mentionsRepoOutcomePatternDetail(value: string): boolean { + return /\bPRs (?:touching|labeled|with|that|from) .+\b(?:merge well|high closure risk) here \(\d+\/\d+ merged\)\./i.test(value); +} + function publicBlockerLabel(code: string): string { const normalized = code.trim().toLowerCase(); const labels: Record = { diff --git a/src/services/decision-pack.ts b/src/services/decision-pack.ts index 21d061a96f..08e7a51f55 100644 --- a/src/services/decision-pack.ts +++ b/src/services/decision-pack.ts @@ -737,8 +737,6 @@ function buildRepoDecision(args: { const manifestSummary = manifest && manifest.present ? buildRepoDecisionManifestSummary(manifest) : undefined; const manifestReasons = manifest && manifest.present ? buildRepoDecisionManifestReasons(manifest) : { whyThisHelps: [], nextActions: [], publicNextActions: [], riskReasons: [] }; const repoOutcomePatterns = summarizeRepoOutcomePatterns(args.repoOutcomePatterns); - const outcomeRiskLines = args.roleContext.maintainerLane ? [] : (repoOutcomePatterns?.riskPatterns ?? []).slice(0, 2).map((pattern) => pattern.detail); - const outcomeSuccessLines = recommendation === "pursue" ? (repoOutcomePatterns?.successPatterns ?? []).slice(0, 1).map((pattern) => pattern.detail) : []; const recommendationFeedbackRiskLines = args.roleContext.maintainerLane ? [] : recommendationFeedbackRiskReasons(recommendationFeedback); const recommendationFeedbackSuccessLines = recommendationFeedbackWhyThisHelps(recommendationFeedback); const tradeoffSummary = buildRepoDecisionTradeoffSummary({ @@ -751,8 +749,8 @@ function buildRepoDecision(args: { manifestSummary, blockers, }); - const finalRiskReasons = [...new Set([...riskReasons, ...manifestReasons.riskReasons, ...outcomeRiskLines, ...recommendationFeedbackRiskLines])]; - const finalWhyThisHelps = [...new Set([...whyThisHelpsFor(recommendation, copyContext), ...manifestReasons.whyThisHelps, ...outcomeSuccessLines, ...recommendationFeedbackSuccessLines])]; + const finalRiskReasons = [...new Set([...riskReasons, ...manifestReasons.riskReasons, ...recommendationFeedbackRiskLines])]; + const finalWhyThisHelps = [...new Set([...whyThisHelpsFor(recommendation, copyContext), ...manifestReasons.whyThisHelps, ...recommendationFeedbackSuccessLines])]; const finalNextActions = [...new Set([...nextActionsFor(recommendation, copyContext), ...manifestReasons.nextActions])]; const finalPublicNextActions = [...new Set([...publicNextActionsFor(recommendation, copyContext), ...manifestReasons.publicNextActions])]; const counterfactualReasons = buildRepoDecisionCounterfactualReasons({ diff --git a/test/unit/decision-pack.test.ts b/test/unit/decision-pack.test.ts index 57dd17f960..a2e946f9bc 100644 --- a/test/unit/decision-pack.test.ts +++ b/test/unit/decision-pack.test.ts @@ -159,7 +159,7 @@ describe("decision-pack service", () => { expect(negative.directPrShare).toBeCloseTo(0.01 * 0.9 * 1, 10); // 0.009 }); - it("feeds repo outcome patterns into repo decisions without inflating maintainer-lane evidence", () => { + it("keeps repo outcome patterns scoped to the private pattern field", () => { const outsideRole = { maintainerLane: false } as any; const maintainerRole = { maintainerLane: true } as any; const patterns = { @@ -178,10 +178,12 @@ describe("decision-pack service", () => { }); expect(pursue.recommendation).toBe("pursue"); expect(pursue.repoOutcomePatterns?.sampleSize).toBe(8); - expect(pursue.whyThisHelps.some((line) => line.includes("PRs touching src/ merge well here"))).toBe(true); - expect(pursue.riskReasons.some((line) => line.includes("high closure risk"))).toBe(true); + expect(pursue.repoOutcomePatterns?.successPatterns[0]?.detail).toContain("PRs touching src/ merge well here"); + expect(pursue.repoOutcomePatterns?.riskPatterns[0]?.detail).toContain("high closure risk"); + expect(pursue.whyThisHelps.some((line) => line.includes("PRs touching src/ merge well here"))).toBe(false); + expect(pursue.riskReasons.some((line) => line.includes("high closure risk"))).toBe(false); - // Maintainer-lane repos surface the patterns for context but never fold the risk into the contributor's own risk reasons. + // Maintainer-lane repos also surface the patterns for private context without folding risk into generic reasons. const maintainer = __decisionPackInternals.buildRepoDecision({ repo: repo("owner/direct", 0.03, 0), roleContext: maintainerRole, diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts index 3a1653b468..02172dfffc 100644 --- a/test/unit/github-commands.test.ts +++ b/test/unit/github-commands.test.ts @@ -173,6 +173,46 @@ describe("GitHub mention commands", () => { expect(body).not.toMatch(/wallet|hotkey|raw trust score|payout|reward estimate|farming|private reviewability|public score estimate/i); }); + it("does not publish repo outcome-pattern details in duplicate-check comments", () => { + const body = buildPublicAgentCommandComment({ + command: parseGittensoryMentionCommand("@gittensory duplicate-check")!, + repo: null, + issue: { number: 99, title: "PR", state: "open", pull_request: {} }, + pullRequest: null, + actorKind: "maintainer", + bundle: { + run: completedRun("run-duplicate-outcome-pattern"), + actions: [ + { + id: "repo-outcome-pattern-action", + runId: "run-duplicate-outcome-pattern", + actionType: "check_duplicate_risk" as const, + status: "recommended" as const, + recommendation: "Open direct PR", + why: [ + "PRs touching duplicate/ have high closure risk here (0/3 merged).", + 'PRs labeled "wip" merge well here (3/3 merged).', + ], + blockedBy: [], + riskImpact: "PRs touching collision/ have high closure risk here (0/3 merged).", + publicSafeSummary: "Consider a narrow public-safe change.", + approvalRequired: true, + safetyClass: "private" as const, + payload: {}, + }, + ], + contextSnapshots: [], + summary: "duplicate outcome-pattern guard", + }, + }); + + expect(body).toContain("**Duplicate & WIP caution**"); + expect(body).toContain("Consider a narrow public-safe change."); + expect(body).not.toContain("PRs touching duplicate/"); + expect(body).not.toContain("high closure risk here (0/3 merged)"); + expect(body).not.toContain("merge well here (3/3 merged)"); + }); + it("renders command-specific sections for preflight, blockers, duplicate-check, and next-action", () => { const bundle = sampleBundle();