Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import {
import { attachDataQuality, buildCoreSignalFidelity, buildFreshnessSloReport, buildRepoDataQuality, buildSignalFidelity } from "../signals/data-quality";
import { buildPullRequestReviewability } from "../signals/reward-risk";
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import { buildRepoSettingsPreview } from "../signals/settings-preview";
import { fileUpstreamDriftIssues, loadUpstreamStatus, refreshUpstreamDrift } from "../upstream/ruleset";
import type { BountyLifecycleEventRecord, ContributorEvidenceRecord, DataQuality, InstallationHealthRecord, JobMessage, JsonValue, RegistrySnapshot, RepoSyncSegmentRecord, RepositoryRecord, ScoringModelSnapshotRecord } from "../types";
Expand Down Expand Up @@ -222,6 +223,7 @@ const localBranchAnalysisSchema = z
expectedOpenPrCountAfterMerge: z.number().int().min(0).optional(),
projectedCredibility: z.number().min(0).max(1).optional(),
scenarioNotes: z.array(z.string().max(MAX_LOCAL_BRANCH_TEXT_CHARS)).max(20).optional(),
focusManifest: z.record(z.unknown()).optional(),
})
.strict();

Expand Down Expand Up @@ -1206,7 +1208,7 @@ export function createApp() {
if (!parsed.success) return c.json({ error: "invalid_local_branch_analysis_request", issues: parsed.error.issues }, 400);
const unauthorized = await requireContributorAccess(c, parsed.data.login);
if (unauthorized) return unauthorized;
const [context, repo, issues, pullRequests, recentMergedPullRequests, bounties, snapshot, issueQuality] = await Promise.all([
const [context, repo, issues, pullRequests, recentMergedPullRequests, bounties, snapshot, issueQuality, repoManifest] = await Promise.all([
loadContributorFastContext(c.env, parsed.data.login),
getRepository(c.env, parsed.data.repoFullName),
listIssues(c.env, parsed.data.repoFullName),
Expand All @@ -1215,12 +1217,17 @@ export function createApp() {
listBountiesByRepo(c.env, parsed.data.repoFullName),
getOrCreateScoringModelSnapshot(c.env),
loadOrComputeIssueQualityResponse(c.env, parsed.data.repoFullName),
loadRepoFocusManifest(c.env, parsed.data.repoFullName),
]);
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, parsed.data, pullRequests);
// Caller-supplied focusManifest wins; otherwise fall back to the repo-owned manifest when present.
const analysisInput = parsed.data.focusManifest !== undefined || !repoManifest.present
? parsed.data
: { ...parsed.data, focusManifest: repoManifest as unknown };
const analysis = buildLocalBranchAnalysis({
input: parsed.data,
input: analysisInput,
repo,
issues,
pullRequests,
Expand Down
11 changes: 9 additions & 2 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
buildRoleContext,
} from "../signals/engine";
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import { buildRepoDataQuality } from "../signals/data-quality";
import { loadUpstreamStatus } from "../upstream/ruleset";

Expand Down Expand Up @@ -159,6 +160,7 @@ const localBranchAnalysisShape = {
expectedOpenPrCountAfterMerge: z.number().int().min(0).optional(),
projectedCredibility: z.number().min(0).max(1).optional(),
scenarioNotes: z.array(z.string()).max(20).optional(),
focusManifest: z.record(z.unknown()).optional(),
localScorer: z
.object({
mode: z.enum(["metadata_only", "external_command", "gittensor_root"]),
Expand Down Expand Up @@ -852,7 +854,7 @@ export class GittensoryMcp {

private async analyzeLocalBranch(input: z.infer<z.ZodObject<typeof localBranchAnalysisShape>>) {
this.requireContributorAccess(input.login);
const [context, repo, issues, pullRequests, recentMergedPullRequests, bounties, snapshot, issueQuality] = await Promise.all([
const [context, repo, issues, pullRequests, recentMergedPullRequests, bounties, snapshot, issueQuality, repoManifest] = await Promise.all([
this.loadContributorFastContext(input.login),
getRepository(this.env, input.repoFullName),
listIssues(this.env, input.repoFullName),
Expand All @@ -861,13 +863,18 @@ export class GittensoryMcp {
listBountiesByRepo(this.env, input.repoFullName),
getOrCreateScoringModelSnapshot(this.env),
loadOrComputeIssueQualityResponse(this.env, input.repoFullName),
loadRepoFocusManifest(this.env, input.repoFullName),
]);
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, input, pullRequests);
// Caller-supplied focusManifest wins; otherwise fall back to the repo-owned manifest when present.
const analysisInput = input.focusManifest !== undefined || !repoManifest.present
? input
: { ...input, focusManifest: repoManifest as unknown };
return {
...buildLocalBranchAnalysis({
input,
input: analysisInput,
repo,
issues,
pullRequests,
Expand Down
14 changes: 14 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,20 @@ export const LocalBranchAnalysisSchema = z
reasons: z.array(z.string()),
risks: z.array(z.string()),
}),
manifestGuidance: z.object({
present: z.boolean(),
source: z.enum(["repo_file", "api_record", "none"]),
linkedIssuePolicy: z.enum(["required", "preferred", "optional"]),
issueDiscoveryPolicy: z.enum(["encouraged", "neutral", "discouraged"]),
matchedWantedPaths: z.array(z.string()),
matchedBlockedPaths: z.array(z.string()),
preferredLabelHits: z.array(z.string()),
findings: z.array(z.object({ code: z.string(), severity: z.enum(["info", "warning", "critical"]), title: z.string(), detail: z.string(), action: z.string().optional() })),
publicNextSteps: z.array(z.string()),
maintainerNotes: z.array(z.string()),
warnings: z.array(z.string()),
summary: z.string(),
}),
prPacket: z.object({
titleSuggestion: z.string(),
markdown: z.string(),
Expand Down
10 changes: 8 additions & 2 deletions src/services/agent-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { loadOrComputeIssueQualityResponse } from "./issue-quality";
import { summarizeAgentBundleWithAi } from "./ai-summaries";
import { buildContributorFit, buildContributorOutcomeHistory, buildContributorProfile, buildContributorScoringProfile } from "../signals/engine";
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest, type LocalBranchAnalysis, type LocalBranchAnalysisInput } from "../signals/local-branch";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import type {
AgentActionRecord,
AgentActionStatus,
Expand Down Expand Up @@ -286,7 +287,7 @@ async function executeLocalBranchRun(env: Env, run: AgentRunRecord, kind: string
}

async function analyzeLocalBranch(env: Env, input: LocalBranchAnalysisInput): Promise<LocalBranchAnalysis & { dataQuality?: { status: "complete" | "degraded" | "blocked" | "unknown"; warnings: string[] } }> {
const [github, contributorPullRequests, contributorIssues, repositories, syncStates, cachedRepoStats, gittensorSnapshot, repo, issues, pullRequests, recentMergedPullRequests, bounties, scoringSnapshot, issueQuality] =
const [github, contributorPullRequests, contributorIssues, repositories, syncStates, cachedRepoStats, gittensorSnapshot, repo, issues, pullRequests, recentMergedPullRequests, bounties, scoringSnapshot, issueQuality, repoManifest] =
await Promise.all([
fetchPublicContributorProfile(input.login),
listContributorPullRequests(env, input.login),
Expand All @@ -302,15 +303,20 @@ async function analyzeLocalBranch(env: Env, input: LocalBranchAnalysisInput): Pr
listBountiesByRepo(env, input.repoFullName),
getOrCreateScoringModelSnapshot(env),
loadOrComputeIssueQualityResponse(env, input.repoFullName),
loadRepoFocusManifest(env, input.repoFullName),
]);
const repoStats = contributorRepoStatsFromGittensor(gittensorSnapshot).length > 0 ? contributorRepoStatsFromGittensor(gittensorSnapshot) : cachedRepoStats;
const profile = buildContributorProfile(input.login, github, contributorPullRequests, contributorIssues, repoStats, gittensorSnapshot);
const outcomeHistory = buildContributorOutcomeHistory({ login: input.login, profile, repositories, pullRequests: contributorPullRequests, issues: contributorIssues, repoStats, cachedRepoStats });
const fit = buildContributorFit(profile, repositories, [], [], syncStates, repoStats);
const scoringProfile = buildContributorScoringProfile({ login: input.login, fit, scoringSnapshot });
const checkSummaries = await loadCheckSummariesForPullRequests(env, input.repoFullName, input, pullRequests);
// Caller-supplied focusManifest wins; otherwise fall back to the repo-owned manifest when present.
const analysisInput = input.focusManifest !== undefined || !repoManifest.present
? input
: { ...input, focusManifest: repoManifest as unknown };
return buildLocalBranchAnalysis({
input,
input: analysisInput,
repo,
issues,
pullRequests,
Expand Down
91 changes: 87 additions & 4 deletions src/services/decision-pack.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { loadRepoFocusManifests } from "../signals/focus-manifest-loader";
import type { FocusManifest, FocusManifestIssueDiscoveryPolicy, FocusManifestLinkedIssuePolicy, FocusManifestSource } from "../signals/focus-manifest";
import { isFocusManifestPublicSafe } from "../signals/focus-manifest";
import {
hasRecentAuditEvent,
listAllIssues,
Expand Down Expand Up @@ -138,6 +141,18 @@ export type RepoDecision = {
nextActions: string[];
publicNextActions: string[];
issueQuality?: IssueQualitySummary | undefined;
manifestSummary?: RepoDecisionManifestSummary | undefined;
};

export type RepoDecisionManifestSummary = {
present: boolean;
source: FocusManifestSource;
linkedIssuePolicy: FocusManifestLinkedIssuePolicy;
issueDiscoveryPolicy: FocusManifestIssueDiscoveryPolicy;
wantedPathCount: number;
blockedPathCount: number;
preferredLabels: string[];
publicNotes: string[];
};

export type DecisionAction = {
Expand Down Expand Up @@ -287,6 +302,10 @@ export async function buildAndPersistContributorDecisionPack(env: Env, login: st
]);
const repoStats = authoritativeContributorRepoStats(gittensorSnapshot, cachedRepoStats);
const issueQualityByRepo = await loadIssueQualityReportMap(env, repositories);
const focusManifests = await loadRepoFocusManifests(
env,
repositories.filter((repo) => repo.isRegistered).map((repo) => repo.fullName),
);
const profile = buildContributorProfile(login, github, contributorPullRequests, contributorIssues, repoStats, gittensorSnapshot);
const outcomeHistory = buildContributorOutcomeHistory({
login,
Expand All @@ -312,6 +331,7 @@ export async function buildAndPersistContributorDecisionPack(env: Env, login: st
contributorPullRequests,
contributorIssues,
issueQualityByRepo,
focusManifests,
});

await upsertContributorEvidence(env, {
Expand Down Expand Up @@ -362,6 +382,7 @@ function buildContributorDecisionPack(args: {
contributorPullRequests: Parameters<typeof buildRoleContext>[0]["pullRequests"];
contributorIssues: Parameters<typeof buildRoleContext>[0]["issues"];
issueQualityByRepo?: Map<string, IssueQualityReport> | undefined;
focusManifests?: Map<string, FocusManifest> | undefined;
}): ContributorDecisionPack {
const registeredRepositories = args.repositories.filter((repo) => repo.isRegistered);
const syncByRepo = new Map(args.syncStates.map((state) => [state.repoFullName.toLowerCase(), state]));
Expand Down Expand Up @@ -395,6 +416,7 @@ function buildContributorDecisionPack(args: {
languageSet,
labelHistory,
issueQuality: issueQualityByRepo.get(key),
focusManifest: args.focusManifests?.get(key),
});
})
.sort((left, right) => right.priorityScore - left.priorityScore || left.repoFullName.localeCompare(right.repoFullName));
Expand Down Expand Up @@ -445,6 +467,7 @@ function buildRepoDecision(args: {
languageSet?: Set<string> | undefined;
labelHistory?: Set<string> | undefined;
issueQuality?: IssueQualityReport | undefined;
focusManifest?: FocusManifest | undefined;
}): RepoDecision {
const lane = buildLaneAdvice(args.repo, args.repo.fullName);
const config = args.repo.registryConfig;
Expand Down Expand Up @@ -494,6 +517,9 @@ function buildRepoDecision(args: {
labelFit,
issueQuality,
};
const manifest = args.focusManifest;
const manifestSummary = manifest && manifest.present ? buildRepoDecisionManifestSummary(manifest) : undefined;
const manifestReasons = manifest && manifest.present ? buildRepoDecisionManifestReasons(manifest) : { whyThisHelps: [], nextActions: [], publicNextActions: [], riskReasons: [] };
return {
repoFullName: args.repo.fullName,
recommendation,
Expand All @@ -506,11 +532,68 @@ function buildRepoDecision(args: {
languageMatch,
labelFit,
scoreBlockers: blockers,
riskReasons,
whyThisHelps: whyThisHelpsFor(recommendation, copyContext),
nextActions: nextActionsFor(recommendation, copyContext),
publicNextActions: publicNextActionsFor(recommendation, copyContext),
riskReasons: [...riskReasons, ...manifestReasons.riskReasons],
whyThisHelps: [...whyThisHelpsFor(recommendation, copyContext), ...manifestReasons.whyThisHelps],
nextActions: [...nextActionsFor(recommendation, copyContext), ...manifestReasons.nextActions],
publicNextActions: [...publicNextActionsFor(recommendation, copyContext), ...manifestReasons.publicNextActions],
issueQuality,
manifestSummary,
};
}

/**
* Public-safe per-repo summary of a maintainer's focus manifest, intentionally excluding the
* manifest's private `maintainerNotes`. The contributor-facing decision pack must never carry
* maintainer-private reviewer text.
*/
function buildRepoDecisionManifestSummary(manifest: FocusManifest): RepoDecisionManifestSummary {
return {
present: true,
source: manifest.source,
linkedIssuePolicy: manifest.linkedIssuePolicy,
issueDiscoveryPolicy: manifest.issueDiscoveryPolicy,
wantedPathCount: manifest.wantedPaths.length,
blockedPathCount: manifest.blockedPaths.length,
preferredLabels: manifest.preferredLabels.slice(0, 8),
publicNotes: manifest.publicNotes.filter(isFocusManifestPublicSafe).slice(0, 4),
};
}

function buildRepoDecisionManifestReasons(manifest: FocusManifest): { whyThisHelps: string[]; nextActions: string[]; publicNextActions: string[]; riskReasons: string[] } {
const whyThisHelps: string[] = [];
const nextActions: string[] = [];
const publicNextActions: string[] = [];
const riskReasons: string[] = [];
if (manifest.wantedPaths.length > 0) {
whyThisHelps.push(`Maintainer focus manifest declares ${manifest.wantedPaths.length} wanted path(s) for this repo.`);
publicNextActions.push("Target the maintainer-wanted areas for this repo when picking a change.");
}
if (manifest.blockedPaths.length > 0) {
riskReasons.push(`Maintainer focus manifest blocks ${manifest.blockedPaths.length} path pattern(s) for this repo.`);
publicNextActions.push("Avoid the maintainer-blocked areas for this repo.");
}
if (manifest.linkedIssuePolicy === "required") {
nextActions.push("Link a tracked issue on every PR; the maintainer's manifest requires it.");
publicNextActions.push("Link a tracked issue on every PR; the maintainer requires linked issues.");
} else if (manifest.linkedIssuePolicy === "preferred") {
publicNextActions.push("Prefer linking a tracked issue; the maintainer prefers linked issues.");
}
if (manifest.preferredLabels.length > 0) {
publicNextActions.push(`Use a maintainer-preferred label when applicable (${manifest.preferredLabels.slice(0, 3).join(", ")}).`);
}
if (manifest.issueDiscoveryPolicy === "discouraged") {
publicNextActions.push("Prefer direct fixes over new issue-discovery reports here.");
} else if (manifest.issueDiscoveryPolicy === "encouraged") {
publicNextActions.push("High-quality issue-discovery reports are welcomed by the maintainer.");
}
for (const note of manifest.publicNotes) {
if (isFocusManifestPublicSafe(note)) publicNextActions.push(note);
}
return {
whyThisHelps,
nextActions,
publicNextActions: [...new Set(publicNextActions)].filter(isFocusManifestPublicSafe),
riskReasons,
};
}

Expand Down
Loading
Loading