From c5311f5f5c2e16e69c582e2fca7e9535fc4dba02 Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Fri, 29 May 2026 22:19:04 -0700 Subject: [PATCH 1/4] feat(mcp): add current-branch GitHub status hints Bind cached GitHub PR review and check state to local branch analysis so blockers and public-safe packets include current PR status context. --- src/signals/local-branch.ts | 90 ++++++++++++++++++++- test/unit/local-branch.test.ts | 138 +++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 1 deletion(-) diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index 8f6b61ebc1..26d567c3e1 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -78,6 +78,16 @@ type ObservedPullRequestScenarios = { notes: string[]; }; +type GitHubBranchStatus = { + source: "cached_github_data"; + status: "approved" | "failing_checks" | "needs_author" | "blocked" | "pending_review" | "no_pr" | "unknown"; + pullNumber?: number | undefined; + title?: string | undefined; + reviewDecision?: string | null | undefined; + mergeableState?: string | null | undefined; + notes: string[]; +}; + export type LocalBranchAnalysis = { login: string; repoFullName: string; @@ -112,6 +122,7 @@ export type LocalBranchAnalysis = { blockedBy: ScorePreviewResult["blockedBy"]; }; observedPullRequestScenarios: ObservedPullRequestScenarios; + githubBranchStatus: GitHubBranchStatus; rewardRisk: RepoRewardRisk; scoreBlockers: string[]; branchQualityBlockers: string[]; @@ -204,6 +215,7 @@ export function buildLocalBranchAnalysis(args: { pullRequests: args.contributorPullRequests ?? args.pullRequests, repositories: args.repositories, }); + const githubBranchStatus = buildGitHubBranchStatus(args.input, args.pullRequests); const scoreInput = buildLocalScoreInput({ input: args.input, changedFiles, @@ -243,7 +255,7 @@ export function buildLocalBranchAnalysis(args: { issues: args.issues, pullRequests: args.pullRequests, }); - const localFindings = buildLocalFindings(args.input, changedFiles, preflight, scorePreview, baseFreshness); + const localFindings = buildLocalFindings(args.input, changedFiles, preflight, scorePreview, baseFreshness, githubBranchStatus); const branchQualityBlockers = branchQualityBlockersFor(preflight, localFindings); const accountStateBlockers = accountStateBlockersFor(scorePreview); const currentScenario = scorePreview.scenarioPreviews.find((scenario) => scenario.name === "current") ?? scorePreview.scenarioPreviews[0]!; @@ -267,6 +279,7 @@ export function buildLocalBranchAnalysis(args: { laneSummary: lane.summary, localFindings, baseFreshness, + githubBranchStatus, recommendedRerunCondition, }); const scoreBlockers = [ @@ -288,6 +301,7 @@ export function buildLocalBranchAnalysis(args: { scorePreview, scenarioScorePreview, observedPullRequestScenarios, + githubBranchStatus, rewardRisk, scoreBlockers: [...new Set(scoreBlockers)], branchQualityBlockers, @@ -419,6 +433,45 @@ function observedPullRequestNotes(scenarios: Omit Boolean(value)).map((value) => value.toLowerCase())); + const match = pullRequests.find( + (pr) => + pr.state === "open" && + (Boolean(input.headSha && pr.headSha === input.headSha) || Boolean(pr.headRef && branchKeys.has(pr.headRef.toLowerCase()))), + ); + if (!match) return { source: "cached_github_data", status: "no_pr", notes: ["No open GitHub PR was matched to the current branch metadata."] }; + const reviewDecision = (match.reviewDecision ?? "").toLowerCase(); + const mergeableState = (match.mergeableState ?? "").toLowerCase(); + const status = + reviewDecision === "approved" || isApprovedOrMergeableOpenPr(match) + ? "approved" + : reviewDecision === "changes_requested" + ? "needs_author" + : ["dirty", "blocked", "conflicting", "unstable"].includes(mergeableState) + ? "failing_checks" + : mergeableState === "unknown" + ? "unknown" + : "pending_review"; + return { + source: "cached_github_data", + status, + pullNumber: match.number, + title: match.title, + reviewDecision: match.reviewDecision, + mergeableState: match.mergeableState, + notes: githubBranchStatusNotes(status, match), + }; +} + +function githubBranchStatusNotes(status: GitHubBranchStatus["status"], pr: PullRequestRecord): string[] { + if (status === "approved") return [`PR #${pr.number} is approved or mergeable in cached GitHub metadata.`]; + if (status === "needs_author") return [`PR #${pr.number} has requested changes in cached GitHub metadata.`]; + if (status === "failing_checks") return [`PR #${pr.number} has failing, blocked, or conflicting GitHub status metadata.`]; + if (status === "unknown") return [`PR #${pr.number} has incomplete GitHub status metadata; refresh checks before relying on it.`]; + return [`PR #${pr.number} is open but not yet approved or clearly blocked in cached GitHub metadata.`]; +} + function isMaintainerAuthoredPr(pr: PullRequestRecord, repo: RepositoryRecord | undefined, login: string): boolean { return sameLogin(repo?.owner, login) || ["owner", "member", "collaborator"].includes((pr.authorAssociation ?? "").toLowerCase()); } @@ -446,6 +499,7 @@ function buildLocalFindings( preflight: LocalDiffPreflightResult, scorePreview: ScorePreviewResult, baseFreshness: LocalBranchAnalysis["baseFreshness"], + githubBranchStatus: GitHubBranchStatus, ): LocalBranchAnalysis["localFindings"] { const failedValidation = (input.validation ?? []).filter((entry) => entry.status === "failed"); return [ @@ -498,6 +552,7 @@ function buildLocalFindings( }, ] : []), + ...githubBranchFindings(githubBranchStatus), ...scorePreview.warnings.map((warning) => ({ code: "score_preview_warning", severity: /not registered|no active|exceeds|credibility/i.test(warning) ? ("warning" as const) : ("info" as const), @@ -514,6 +569,32 @@ function buildLocalFindings( ]; } +function githubBranchFindings(status: GitHubBranchStatus): LocalBranchAnalysis["localFindings"] { + if (status.status === "failing_checks" || status.status === "needs_author") { + return [ + { + code: "github_status_needs_work", + severity: "warning" as const, + title: status.status === "needs_author" ? "GitHub review needs author" : "GitHub checks need attention", + detail: status.notes.join(" "), + action: "Resolve GitHub review/check blockers before asking for maintainer review.", + }, + ]; + } + if (status.status === "unknown") { + return [ + { + code: "github_status_unknown", + severity: "info" as const, + title: "GitHub status is incomplete", + detail: status.notes.join(" "), + action: "Refresh GitHub checks and reviews before final submission.", + }, + ]; + } + return []; +} + function buildBaseFreshness( input: LocalBranchAnalysisInput, changedFileCount: number, @@ -623,6 +704,7 @@ function buildPublicSafePrPacket(args: { laneSummary: string; localFindings: LocalBranchAnalysis["localFindings"]; baseFreshness: LocalBranchAnalysis["baseFreshness"]; + githubBranchStatus: GitHubBranchStatus; recommendedRerunCondition: string; }): LocalBranchAnalysis["prPacket"] { const topPaths = args.changedFiles.slice(0, 8).map(changedFileSummary); @@ -652,6 +734,7 @@ function buildPublicSafePrPacket(args: { lines: args.preflight.linkedIssues.length > 0 ? args.preflight.linkedIssues.map((issue) => `- Closes #${issue}`) : ["- No linked issue detected; explain why this is a no-issue PR."], }, { heading: "Branch Freshness", lines: branchFreshnessLines(args.baseFreshness) }, + { heading: "GitHub Status", lines: githubStatusLines(args.githubBranchStatus) }, { heading: "Overlap/WIP Check", lines: overlapCautionLines(args.preflight.collisions) }, { heading: "Changed Paths", @@ -681,6 +764,11 @@ function branchFreshnessLines(freshness: LocalBranchAnalysis["baseFreshness"]): return [`- Base freshness: ${freshness.status}.`, ...freshness.warnings.filter(isPublicSafeText).map((warning) => `- ${warning}`), freshness.passedValidationCount > 0 ? `- Validation evidence supplied: ${freshness.passedValidationCount} passed command(s).` : "- No passed validation evidence was supplied."]; } +function githubStatusLines(status: GitHubBranchStatus): string[] { + if (status.status === "no_pr") return ["- No open GitHub PR was matched to this branch."]; + return [`- PR #${status.pullNumber}: ${status.status.replace(/_/g, " ")}.`, ...status.notes.map((note) => `- ${note}`)].filter(isPublicSafeText); +} + function overlapCautionLines(collisions: LocalDiffPreflightResult["collisions"]): string[] { if (collisions.length === 0) return ["- No active overlap or WIP was detected from cached issue/PR metadata."]; return collisions diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index 81863cb06e..f64e073543 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -261,6 +261,144 @@ describe("local branch analysis", () => { expect(analysis.scenarioScorePreview.afterApprovedPrsMerge?.gates.openPrCount).toBe(1); }); + it("binds cached GitHub PR status to the current branch", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + branchName: "fix-cache", + headSha: "head-sha", + body: "Fixes #7", + changedFiles: [ + { path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }, + { path: "src/cache.test.ts", additions: 20, deletions: 0, status: "added" }, + ], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [{ repoFullName: repo.fullName, number: 7, title: "Cache edge", state: "open", labels: ["bug"], linkedPrs: [] }], + pullRequests: [ + { + repoFullName: repo.fullName, + number: 14, + title: "Cache branch", + state: "open", + authorLogin: "oktofeesh1", + authorAssociation: "CONTRIBUTOR", + headSha: "head-sha", + headRef: "fix-cache", + mergeableState: "UNSTABLE", + labels: ["bug"], + linkedIssues: [7], + }, + ], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.githubBranchStatus).toMatchObject({ status: "failing_checks", pullNumber: 14 }); + expect(analysis.branchQualityBlockers).toContain("GitHub checks need attention"); + expect(analysis.prPacket.markdown).toContain("## GitHub Status"); + expect(analysis.prPacket.markdown).toContain("PR #14"); + expect(JSON.stringify(analysis.prPacket)).not.toMatch(/reward|score|wallet|hotkey|farming|payout|ranking|trust score/i); + }); + + it("feeds approved current-branch PRs into private pending scenarios", () => { + const approvedPr = { + repoFullName: repo.fullName, + number: 15, + title: "Approved cache branch", + state: "open", + authorLogin: "oktofeesh1", + authorAssociation: "CONTRIBUTOR", + headRef: "fix-cache-approved", + reviewDecision: "APPROVED", + labels: ["bug"], + linkedIssues: [7], + }; + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + branchName: "fix-cache-approved", + body: "Fixes #7", + changedFiles: [ + { path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }, + { path: "src/cache.test.ts", additions: 20, deletions: 0, status: "added" }, + ], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [{ repoFullName: repo.fullName, number: 7, title: "Cache edge", state: "open", labels: ["bug"], linkedPrs: [] }], + pullRequests: [approvedPr], + contributorPullRequests: [approvedPr], + profile, + outcomeHistory: { ...outcomeHistory, totals: { ...outcomeHistory.totals, openPullRequests: 1, credibility: 0.2 } }, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.githubBranchStatus).toMatchObject({ status: "approved", pullNumber: 15 }); + expect(analysis.observedPullRequestScenarios.approvedOrMergeable).toBe(1); + expect(analysis.scenarioScorePreview.afterApprovedPrsMerge).toMatchObject({ source: "github_observed", gates: { openPrCount: 0 } }); + expect(analysis.scenarioScorePreview.afterApprovedPrsMerge?.gates.credibilityObserved).toBeGreaterThanOrEqual(0.8); + }); + + it("falls back cleanly when no current-branch PR or complete status is cached", () => { + const noPr = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + branchName: "local-only", + changedFiles: [{ path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [], + pullRequests: [], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + expect(noPr.githubBranchStatus.status).toBe("no_pr"); + expect(noPr.branchQualityBlockers.join(" ")).not.toMatch(/GitHub/i); + + const unknown = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + branchName: "unknown-status", + changedFiles: [{ path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [], + pullRequests: [ + { + repoFullName: repo.fullName, + number: 16, + title: "Unknown status", + state: "open", + authorLogin: "oktofeesh1", + authorAssociation: "CONTRIBUTOR", + headRef: "unknown-status", + mergeableState: "UNKNOWN", + labels: [], + linkedIssues: [], + }, + ], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + expect(unknown.githubBranchStatus).toMatchObject({ status: "unknown", pullNumber: 16 }); + expect(unknown.localFindings).toEqual(expect.arrayContaining([expect.objectContaining({ code: "github_status_unknown" })])); + }); + it("classifies stale base state and treats passed validation as test evidence", () => { const analysis = buildLocalBranchAnalysis({ input: { From c96b95e91fd0e867abeb8919ca555cc4301dc72e Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Fri, 29 May 2026 22:29:51 -0700 Subject: [PATCH 2/4] test(readiness): keep freshness fixtures current --- test/integration/api.test.ts | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index cb45405b5d..de4f64d520 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -1140,7 +1140,7 @@ describe("api routes", () => { fetchedCount: 2, expectedCount: 2, pageCount: 1, - completedAt: "2026-05-23T00:00:00.000Z", + completedAt: new Date().toISOString(), warnings: [], }); const refreshingReadiness = await app.request("/v1/readiness", { headers: apiHeaders(refreshingEnv) }, refreshingEnv); @@ -2351,6 +2351,9 @@ async function mcpJson(response: Response): Promise { } async function seedSignalData(env: Env): Promise { + const freshAt = new Date().toISOString(); + const previousFreshAt = new Date(Date.now() - 60_000).toISOString(); + await upsertInstallation(env, { installation: { id: 123, @@ -2371,7 +2374,7 @@ async function seedSignalData(env: Env): Promise { missingEvents: [], permissions: { metadata: "read", pull_requests: "read", issues: "write" }, events: ["issues", "pull_request", "repository"], - checkedAt: "2026-05-23T00:00:00.000Z", + checkedAt: freshAt, }); const snapshot = normalizeRegistryPayload( { @@ -2384,7 +2387,7 @@ async function seedSignalData(env: Env): Promise { }, }, { kind: "raw-github", url: "https://example.test/master_repositories.json" }, - "2026-05-23T00:00:00.000Z", + freshAt, ); await persistRegistrySnapshot( env, @@ -2399,7 +2402,7 @@ async function seedSignalData(env: Env): Promise { }, }, { kind: "raw-github", url: "https://example.test/old_master_repositories.json" }, - "2026-05-22T00:00:00.000Z", + previousFreshAt, ), ); await persistRegistrySnapshot(env, snapshot); @@ -2414,7 +2417,7 @@ async function seedSignalData(env: Env): Promise { id: "scoring-1", sourceKind: "test", sourceUrl: "fixture://scoring", - fetchedAt: "2026-05-23T00:00:00.000Z", + fetchedAt: freshAt, activeModel: "current_density_model", constants: { OSS_EMISSION_SHARE: 0.9, @@ -2459,7 +2462,7 @@ async function seedSignalData(env: Env): Promise { closedUnmergedPullRequestsTotal: 0, labelsTotal: 2, sourceKind: "github", - fetchedAt: "2026-05-23T00:00:00.000Z", + fetchedAt: freshAt, payload: {}, }); await Promise.all( @@ -2482,7 +2485,7 @@ async function seedSignalData(env: Env): Promise { fetchedCount: record.fetchedCount, expectedCount: record.expectedCount, pageCount: 1, - completedAt: "2026-05-23T00:00:00.000Z", + completedAt: freshAt, warnings: [], }), ), @@ -2516,7 +2519,7 @@ async function seedSignalData(env: Env): Promise { missingEvents: [], permissions: { metadata: "read", pull_requests: "read", issues: "write" }, events: ["issues", "issue_comment", "pull_request", "repository"], - checkedAt: "2026-05-23T00:00:00.000Z", + checkedAt: freshAt, }); await upsertIssueFromGitHub(env, "entrius/allways-ui", { number: 7, @@ -2552,10 +2555,10 @@ async function seedSignalData(env: Env): Promise { repoFullName: "entrius/allways-ui", pullNumber: 12, status: "complete", - filesSyncedAt: "2026-05-23T00:00:00.000Z", - reviewsSyncedAt: "2026-05-23T00:00:00.000Z", - checksSyncedAt: "2026-05-23T00:00:00.000Z", - lastSyncedAt: "2026-05-23T00:00:00.000Z", + filesSyncedAt: freshAt, + reviewsSyncedAt: freshAt, + checksSyncedAt: freshAt, + lastSyncedAt: freshAt, }); await upsertPullRequestFile(env, { repoFullName: "entrius/allways-ui", @@ -2600,10 +2603,10 @@ async function seedSignalData(env: Env): Promise { repoFullName: "entrius/allways-ui", pullNumber: 13, status: "complete", - filesSyncedAt: "2026-05-23T00:00:00.000Z", - reviewsSyncedAt: "2026-05-23T00:00:00.000Z", - checksSyncedAt: "2026-05-23T00:00:00.000Z", - lastSyncedAt: "2026-05-23T00:00:00.000Z", + filesSyncedAt: freshAt, + reviewsSyncedAt: freshAt, + checksSyncedAt: freshAt, + lastSyncedAt: freshAt, }); await upsertRecentMergedPullRequest(env, { repoFullName: "entrius/allways-ui", From 4b2b4bb4a7d06ddcb12e66ac902f6c19cdb7acc5 Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Sat, 30 May 2026 00:40:28 -0700 Subject: [PATCH 3/4] fix(mcp): tighten current branch status hints --- src/openapi/schemas.ts | 9 ++++++ src/signals/local-branch.ts | 15 ++++++---- test/unit/local-branch.test.ts | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 0eccc0eadc..622a6959e3 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -1276,6 +1276,15 @@ export const LocalBranchAnalysisSchema = z maintainerLane: z.number(), notes: z.array(z.string()), }), + githubBranchStatus: z.object({ + source: z.literal("cached_github_data"), + status: z.enum(["approved", "failing_checks", "needs_author", "blocked", "pending_review", "no_pr", "unknown"]), + pullNumber: z.number().optional(), + title: z.string().optional(), + reviewDecision: z.string().nullable().optional(), + mergeableState: z.string().nullable().optional(), + notes: z.array(z.string()), + }), rewardRisk: RepoRewardRiskSchema, scoreBlockers: z.array(z.string()), branchQualityBlockers: z.array(z.string()), diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index 26d567c3e1..1e4aedb167 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -438,21 +438,23 @@ function buildGitHubBranchStatus(input: LocalBranchAnalysisInput, pullRequests: const match = pullRequests.find( (pr) => pr.state === "open" && - (Boolean(input.headSha && pr.headSha === input.headSha) || Boolean(pr.headRef && branchKeys.has(pr.headRef.toLowerCase()))), + (Boolean(input.headSha && pr.headSha === input.headSha) || Boolean(pr.headRef && branchKeys.has(pr.headRef.toLowerCase()) && sameLogin(pr.authorLogin, input.login))), ); if (!match) return { source: "cached_github_data", status: "no_pr", notes: ["No open GitHub PR was matched to the current branch metadata."] }; const reviewDecision = (match.reviewDecision ?? "").toLowerCase(); const mergeableState = (match.mergeableState ?? "").toLowerCase(); const status = - reviewDecision === "approved" || isApprovedOrMergeableOpenPr(match) - ? "approved" - : reviewDecision === "changes_requested" - ? "needs_author" + reviewDecision === "changes_requested" + ? "needs_author" + : match.isDraft + ? "pending_review" : ["dirty", "blocked", "conflicting", "unstable"].includes(mergeableState) ? "failing_checks" : mergeableState === "unknown" ? "unknown" - : "pending_review"; + : reviewDecision === "approved" || isApprovedOrMergeableOpenPr(match) + ? "approved" + : "pending_review"; return { source: "cached_github_data", status, @@ -468,6 +470,7 @@ function githubBranchStatusNotes(status: GitHubBranchStatus["status"], pr: PullR if (status === "approved") return [`PR #${pr.number} is approved or mergeable in cached GitHub metadata.`]; if (status === "needs_author") return [`PR #${pr.number} has requested changes in cached GitHub metadata.`]; if (status === "failing_checks") return [`PR #${pr.number} has failing, blocked, or conflicting GitHub status metadata.`]; + if (status === "pending_review" && pr.isDraft) return [`PR #${pr.number} is still a draft in cached GitHub metadata.`]; if (status === "unknown") return [`PR #${pr.number} has incomplete GitHub status metadata; refresh checks before relying on it.`]; return [`PR #${pr.number} is open but not yet approved or clearly blocked in cached GitHub metadata.`]; } diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index f64e073543..763005a80a 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -346,6 +346,59 @@ describe("local branch analysis", () => { expect(analysis.scenarioScorePreview.afterApprovedPrsMerge?.gates.credibilityObserved).toBeGreaterThanOrEqual(0.8); }); + it("prioritizes requested changes, draft state, and contributor ownership for current-branch status", () => { + const basePr = { + repoFullName: repo.fullName, + state: "open", + authorAssociation: "CONTRIBUTOR", + headRef: "fix-cache", + labels: ["bug"], + linkedIssues: [7], + }; + const changesRequested = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + branchName: "fix-cache", + changedFiles: [{ path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [], + pullRequests: [ + { ...basePr, number: 20, title: "Wrong contributor same branch", authorLogin: "other", reviewDecision: "APPROVED", mergeableState: "CLEAN" }, + { ...basePr, number: 21, title: "Needs author", authorLogin: "oktofeesh1", reviewDecision: "CHANGES_REQUESTED", mergeableState: "CLEAN" }, + ], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(changesRequested.githubBranchStatus).toMatchObject({ status: "needs_author", pullNumber: 21 }); + expect(changesRequested.localFindings).toEqual(expect.arrayContaining([expect.objectContaining({ code: "github_status_needs_work" })])); + + const draft = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + branchName: "draft-cache", + changedFiles: [{ path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [], + pullRequests: [{ ...basePr, number: 22, title: "Draft clean branch", authorLogin: "oktofeesh1", headRef: "draft-cache", reviewDecision: "APPROVED", mergeableState: "CLEAN", isDraft: true }], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(draft.githubBranchStatus).toMatchObject({ status: "pending_review", pullNumber: 22 }); + expect(draft.githubBranchStatus.notes.join(" ")).toMatch(/draft/i); + }); + it("falls back cleanly when no current-branch PR or complete status is cached", () => { const noPr = buildLocalBranchAnalysis({ input: { From 37558a4a9cd0458becd75d71b2b9f2f0294b982f Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Sat, 30 May 2026 01:21:11 -0700 Subject: [PATCH 4/4] fix(signals): tighten current branch PR status matching --- src/api/routes.ts | 7 ++ src/mcp/server.ts | 8 ++ src/services/agent-orchestrator.ts | 8 ++ src/signals/local-branch.ts | 51 ++++++++- test/unit/local-branch.test.ts | 173 ++++++++++++++++++++++++++++- 5 files changed, 241 insertions(+), 6 deletions(-) diff --git a/src/api/routes.ts b/src/api/routes.ts index f14f12ac4a..ff6874b0c6 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -785,6 +785,7 @@ export function createApp() { ]); const fit = buildContributorFit(context.profile, context.repositories, [], [], context.syncStates, context.repoStats); const scoringProfile = buildContributorScoringProfile({ login: parsed.data.login, fit, scoringSnapshot: snapshot }); + const checkSummaries = await loadCheckSummariesForPullRequests(c.env, parsed.data.repoFullName, pullRequests); const analysis = buildLocalBranchAnalysis({ input: parsed.data, repo, @@ -793,6 +794,7 @@ export function createApp() { contributorPullRequests: context.contributorPullRequests, recentMergedPullRequests, repositories: context.repositories, + checkSummaries, profile: context.profile, outcomeHistory: context.outcomeHistory, scoringSnapshot: snapshot, @@ -1312,6 +1314,11 @@ async function loadContributorFastContext(env: Env, login: string) { }; } +async function loadCheckSummariesForPullRequests(env: Env, repoFullName: string, pullRequests: Array<{ number: number; state?: string | null | undefined }>) { + const openPulls = pullRequests.filter((pr) => pr.state === "open"); + return (await Promise.all(openPulls.map((pr) => listCheckSummaries(env, repoFullName, pr.number)))).flat(); +} + async function loadRepoDataQuality(env: Env, fullName: string) { const [syncStates, syncSegments] = await Promise.all([listRepoSyncStates(env), listRepoSyncSegments(env, fullName)]); return buildRepoDataQuality( diff --git a/src/mcp/server.ts b/src/mcp/server.ts index bcf3164680..11dc9a9dd7 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -11,6 +11,7 @@ import { getLatestRepoGithubTotalsSnapshot, getIssue, getRepository, + listCheckSummaries, listContributorRepoStats, listContributorIssues, listContributorPullRequests, @@ -829,6 +830,7 @@ export class GittensoryMcp { ]); const fit = buildContributorFit(context.profile, context.repositories, [], [], context.syncStates, context.repoStats); const scoringProfile = buildContributorScoringProfile({ login: input.login, fit, scoringSnapshot: snapshot }); + const checkSummaries = await this.loadCheckSummariesForPullRequests(input.repoFullName, pullRequests); return { ...buildLocalBranchAnalysis({ input, @@ -838,6 +840,7 @@ export class GittensoryMcp { contributorPullRequests: context.contributorPullRequests, recentMergedPullRequests, repositories: context.repositories, + checkSummaries, profile: context.profile, outcomeHistory: context.outcomeHistory, scoringSnapshot: snapshot, @@ -848,6 +851,11 @@ export class GittensoryMcp { }; } + private async loadCheckSummariesForPullRequests(repoFullName: string, pullRequests: Array<{ number: number; state?: string | null | undefined }>) { + const openPulls = pullRequests.filter((pr) => pr.state === "open"); + return (await Promise.all(openPulls.map((pr) => listCheckSummaries(this.env, repoFullName, pr.number)))).flat(); + } + private async getBountyAdvisory(id: string): Promise { const bounty = await getBounty(this.env, id); if (!bounty) throw new Error("Bounty not found."); diff --git a/src/services/agent-orchestrator.ts b/src/services/agent-orchestrator.ts index 7c3deaf62d..ff133f44c4 100644 --- a/src/services/agent-orchestrator.ts +++ b/src/services/agent-orchestrator.ts @@ -2,6 +2,7 @@ import { createAgentRun, getAgentRun, getRepository, + listCheckSummaries, listAgentActions, listAgentContextSnapshots, listContributorIssues, @@ -304,6 +305,7 @@ async function analyzeLocalBranch(env: Env, input: LocalBranchAnalysisInput): Pr const outcomeHistory = buildContributorOutcomeHistory({ login: input.login, profile, repositories, pullRequests: contributorPullRequests, issues: contributorIssues, repoStats }); const fit = buildContributorFit(profile, repositories, [], [], syncStates, repoStats); const scoringProfile = buildContributorScoringProfile({ login: input.login, fit, scoringSnapshot }); + const checkSummaries = await loadCheckSummariesForPullRequests(env, input.repoFullName, pullRequests); return buildLocalBranchAnalysis({ input, repo, @@ -312,6 +314,7 @@ async function analyzeLocalBranch(env: Env, input: LocalBranchAnalysisInput): Pr contributorPullRequests, recentMergedPullRequests, repositories, + checkSummaries, profile, outcomeHistory, scoringSnapshot, @@ -320,6 +323,11 @@ async function analyzeLocalBranch(env: Env, input: LocalBranchAnalysisInput): Pr }); } +async function loadCheckSummariesForPullRequests(env: Env, repoFullName: string, pullRequests: Array<{ number: number; state?: string | null | undefined }>) { + const openPulls = pullRequests.filter((pr) => pr.state === "open"); + return (await Promise.all(openPulls.map((pr) => listCheckSummaries(env, repoFullName, pr.number)))).flat(); +} + function buildDecisionActions(run: AgentRunRecord, pack: ContributorDecisionPack, decisions: RepoDecision[]): AgentActionRecord[] { const decisionByRepo = new Map(decisions.map((decision) => [decision.repoFullName, decision])); const candidateActions = pack.topActions diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index 1e4aedb167..6f99acf828 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -1,6 +1,6 @@ import type { ScorePreviewInput, ScorePreviewResult } from "../scoring/preview"; import { buildScorePreview } from "../scoring/preview"; -import type { IssueRecord, PullRequestRecord, RecentMergedPullRequestRecord, RepositoryRecord, ScoringModelSnapshotRecord } from "../types"; +import type { CheckSummaryRecord, IssueRecord, PullRequestRecord, RecentMergedPullRequestRecord, RepositoryRecord, ScoringModelSnapshotRecord } from "../types"; import { nowIso } from "../utils/json"; import { buildLaneAdvice, @@ -168,6 +168,7 @@ export function buildLocalBranchAnalysis(args: { contributorPullRequests?: PullRequestRecord[] | undefined; recentMergedPullRequests?: RecentMergedPullRequestRecord[] | undefined; repositories?: RepositoryRecord[] | undefined; + checkSummaries?: CheckSummaryRecord[] | undefined; profile: ContributorProfile; outcomeHistory: ContributorOutcomeHistory; scoringSnapshot: ScoringModelSnapshotRecord; @@ -215,7 +216,7 @@ export function buildLocalBranchAnalysis(args: { pullRequests: args.contributorPullRequests ?? args.pullRequests, repositories: args.repositories, }); - const githubBranchStatus = buildGitHubBranchStatus(args.input, args.pullRequests); + const githubBranchStatus = buildGitHubBranchStatus(args.input, args.pullRequests, args.checkSummaries ?? []); const scoreInput = buildLocalScoreInput({ input: args.input, changedFiles, @@ -433,23 +434,31 @@ function observedPullRequestNotes(scenarios: Omit Boolean(value)).map((value) => value.toLowerCase())); + const inputBaseRef = normalizeRefForMatch(input.baseRef); const match = pullRequests.find( (pr) => pr.state === "open" && - (Boolean(input.headSha && pr.headSha === input.headSha) || Boolean(pr.headRef && branchKeys.has(pr.headRef.toLowerCase()) && sameLogin(pr.authorLogin, input.login))), + sameLogin(pr.authorLogin, input.login) && + sameBaseRef(inputBaseRef, pr.baseRef) && + (Boolean(input.headSha && pr.headSha === input.headSha) || Boolean(pr.headRef && branchKeys.has(pr.headRef.toLowerCase()))), ); if (!match) return { source: "cached_github_data", status: "no_pr", notes: ["No open GitHub PR was matched to the current branch metadata."] }; const reviewDecision = (match.reviewDecision ?? "").toLowerCase(); const mergeableState = (match.mergeableState ?? "").toLowerCase(); + const matchedChecks = matchingCheckSummaries(match, checkSummaries); const status = reviewDecision === "changes_requested" ? "needs_author" + : mergeableState === "behind" + ? "needs_author" : match.isDraft ? "pending_review" - : ["dirty", "blocked", "conflicting", "unstable"].includes(mergeableState) + : ["dirty", "blocked", "conflicting", "unstable"].includes(mergeableState) || hasFailingCheck(matchedChecks) ? "failing_checks" + : hasPendingCheck(matchedChecks) + ? "pending_review" : mergeableState === "unknown" ? "unknown" : reviewDecision === "approved" || isApprovedOrMergeableOpenPr(match) @@ -468,6 +477,7 @@ function buildGitHubBranchStatus(input: LocalBranchAnalysisInput, pullRequests: function githubBranchStatusNotes(status: GitHubBranchStatus["status"], pr: PullRequestRecord): string[] { if (status === "approved") return [`PR #${pr.number} is approved or mergeable in cached GitHub metadata.`]; + if (status === "needs_author" && (pr.mergeableState ?? "").toLowerCase() === "behind") return [`PR #${pr.number} is behind its base branch in cached GitHub metadata.`]; if (status === "needs_author") return [`PR #${pr.number} has requested changes in cached GitHub metadata.`]; if (status === "failing_checks") return [`PR #${pr.number} has failing, blocked, or conflicting GitHub status metadata.`]; if (status === "pending_review" && pr.isDraft) return [`PR #${pr.number} is still a draft in cached GitHub metadata.`]; @@ -475,6 +485,37 @@ function githubBranchStatusNotes(status: GitHubBranchStatus["status"], pr: PullR return [`PR #${pr.number} is open but not yet approved or clearly blocked in cached GitHub metadata.`]; } +function normalizeRefForMatch(ref: string | null | undefined): string | undefined { + const value = ref?.trim().toLowerCase(); + if (!value) return undefined; + return value.replace(/^refs\/heads\//, "").replace(/^refs\/remotes\/[^/]+\//, "").replace(/^(origin|upstream)\//, ""); +} + +function sameBaseRef(inputBaseRef: string | undefined, prBaseRef: string | null | undefined): boolean { + if (!inputBaseRef) return true; + return normalizeRefForMatch(prBaseRef) === inputBaseRef; +} + +function matchingCheckSummaries(pr: PullRequestRecord, checkSummaries: CheckSummaryRecord[]): CheckSummaryRecord[] { + return checkSummaries.filter( + (check) => + (check.pullNumber !== undefined && check.pullNumber !== null && check.pullNumber === pr.number) || + (check.pullNumber === undefined || check.pullNumber === null ? Boolean(pr.headSha && check.headSha === pr.headSha) : false), + ); +} + +function hasFailingCheck(checks: CheckSummaryRecord[]): boolean { + return checks.some((check) => ["failure", "failed", "timed_out", "cancelled", "action_required", "startup_failure"].includes((check.conclusion ?? check.status).toLowerCase())); +} + +function hasPendingCheck(checks: CheckSummaryRecord[]): boolean { + return checks.some((check) => { + const status = check.status.toLowerCase(); + const conclusion = check.conclusion?.toLowerCase(); + return !conclusion && !["completed", "success"].includes(status); + }); +} + function isMaintainerAuthoredPr(pr: PullRequestRecord, repo: RepositoryRecord | undefined, login: string): boolean { return sameLogin(repo?.owner, login) || ["owner", "member", "collaborator"].includes((pr.authorAssociation ?? "").toLowerCase()); } diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index 763005a80a..65239d0534 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -360,14 +360,16 @@ describe("local branch analysis", () => { login: "oktofeesh1", repoFullName: repo.fullName, branchName: "fix-cache", + headSha: "shared-sha", changedFiles: [{ path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }], validation: [{ command: "npm test -- cache", status: "passed" }], }, repo, issues: [], pullRequests: [ + { ...basePr, number: 19, title: "Wrong contributor same SHA", authorLogin: "other", headSha: "shared-sha", reviewDecision: "APPROVED", mergeableState: "CLEAN" }, { ...basePr, number: 20, title: "Wrong contributor same branch", authorLogin: "other", reviewDecision: "APPROVED", mergeableState: "CLEAN" }, - { ...basePr, number: 21, title: "Needs author", authorLogin: "oktofeesh1", reviewDecision: "CHANGES_REQUESTED", mergeableState: "CLEAN" }, + { ...basePr, number: 21, title: "Needs author", authorLogin: "oktofeesh1", headSha: "shared-sha", reviewDecision: "CHANGES_REQUESTED", mergeableState: "CLEAN" }, ], profile, outcomeHistory, @@ -399,6 +401,175 @@ describe("local branch analysis", () => { expect(draft.githubBranchStatus.notes.join(" ")).toMatch(/draft/i); }); + it("requires base-ref matches and check summaries before approving current-branch status", () => { + const basePr = { + repoFullName: repo.fullName, + state: "open", + authorLogin: "oktofeesh1", + authorAssociation: "CONTRIBUTOR", + headSha: "shared-sha", + headRef: "fix-cache", + reviewDecision: "APPROVED", + mergeableState: "CLEAN", + labels: ["bug"], + linkedIssues: [7], + }; + const failingChecks = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + baseRef: "origin/main", + branchName: "fix-cache", + headSha: "shared-sha", + changedFiles: [{ path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [], + pullRequests: [ + { ...basePr, number: 23, title: "Release branch status", baseRef: "release/1.0" }, + { ...basePr, number: 24, title: "Main branch status", baseRef: "main" }, + ], + checkSummaries: [ + { + id: "check-24", + repoFullName: repo.fullName, + pullNumber: 24, + headSha: "shared-sha", + name: "validate", + status: "completed", + conclusion: "failure", + payload: {}, + }, + ], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(failingChecks.githubBranchStatus).toMatchObject({ status: "failing_checks", pullNumber: 24 }); + expect(failingChecks.localFindings).toEqual(expect.arrayContaining([expect.objectContaining({ code: "github_status_needs_work" })])); + + const pendingChecks = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + baseRef: "origin/main", + branchName: "fix-cache", + headSha: "shared-sha", + changedFiles: [{ path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [], + pullRequests: [{ ...basePr, number: 26, title: "Main branch status", baseRef: "main" }], + checkSummaries: [ + { + id: "check-26", + repoFullName: repo.fullName, + pullNumber: 26, + headSha: "shared-sha", + name: "validate", + status: "in_progress", + payload: {}, + }, + ], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(pendingChecks.githubBranchStatus).toMatchObject({ status: "pending_review", pullNumber: 26 }); + + const behind = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + baseRef: "refs/remotes/origin/main", + branchName: "fix-cache", + changedFiles: [{ path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }], + validation: [{ command: "npm test -- cache", status: "passed" }], + }, + repo, + issues: [], + pullRequests: [{ ...basePr, number: 25, title: "Behind branch", baseRef: "refs/heads/main", headSha: undefined, mergeableState: "BEHIND" }], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(behind.githubBranchStatus).toMatchObject({ status: "needs_author", pullNumber: 25 }); + expect(behind.githubBranchStatus.notes.join(" ")).toMatch(/behind/i); + }); + + it("does not apply another open PR's check summary just because the head SHA matches", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + baseRef: "main", + branchName: "shared-head", + headSha: "shared-sha", + changedFiles: [{ path: "src/checks.ts", additions: 10, deletions: 0, status: "modified" }], + }, + repo, + issues: [], + pullRequests: [ + { + repoFullName: repo.fullName, + number: 31, + title: "Current branch", + state: "open", + authorLogin: "oktofeesh1", + authorAssociation: "CONTRIBUTOR", + headSha: "shared-sha", + headRef: "shared-head", + baseRef: "main", + reviewDecision: "APPROVED", + mergeableState: "CLEAN", + labels: [], + linkedIssues: [], + }, + { + repoFullName: repo.fullName, + number: 32, + title: "Other base with same SHA", + state: "open", + authorLogin: "oktofeesh1", + authorAssociation: "CONTRIBUTOR", + headSha: "shared-sha", + headRef: "shared-head", + baseRef: "release/1.0", + reviewDecision: "APPROVED", + mergeableState: "CLEAN", + labels: [], + linkedIssues: [], + }, + ], + checkSummaries: [ + { + id: "check-32", + repoFullName: repo.fullName, + pullNumber: 32, + headSha: "shared-sha", + name: "validate", + status: "completed", + conclusion: "failure", + payload: {}, + }, + ], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.githubBranchStatus).toMatchObject({ status: "approved", pullNumber: 31 }); + }); + it("falls back cleanly when no current-branch PR or complete status is cached", () => { const noPr = buildLocalBranchAnalysis({ input: {