Skip to content
Merged
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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@

- Add deterministic base-agent orchestrator (#14)

- Add settings preview diagnostics
- Make next-action recommendations repo-specific



Expand All @@ -77,5 +77,15 @@

- Ignore stale beta api origins

- Restore actionKind guards and add split-lane copy



### Tests

- Tighten and extend decision-pack regression coverage

- Cover review-requested branches and tier sanitization



16 changes: 9 additions & 7 deletions src/services/agent-orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ function actionFromDecisionAction(run: AgentRunRecord, action: DecisionAction, d
maintainerImpact: maintainerImpactFor(decision),
blockedBy: decision.scoreBlockers.map((blocker) => blocker.code),
rerunWhen: rerunWhenForDecision(decision),
publicSafeSummary: sanitizePublicSummary(`${decision.repoFullName}: ${action.nextActions[0] ?? "Use Gittensory preflight before posting public PR context."}`),
publicSafeSummary: sanitizePublicSummary(action.publicNextActions?.[0] ?? decision.publicNextActions?.[0] ?? `${decision.repoFullName}: Use Gittensory preflight before posting public PR context.`),
payload: {
action: action as unknown as JsonValue,
decision: decision as unknown as JsonValue,
Expand All @@ -436,7 +436,7 @@ function actionFromRepoDecision(run: AgentRunRecord, decision: RepoDecision, ind
maintainerImpact: maintainerImpactFor(decision),
blockedBy: decision.scoreBlockers.map((blocker) => blocker.code),
rerunWhen: rerunWhenForDecision(decision),
publicSafeSummary: sanitizePublicSummary(`${decision.repoFullName}: ${decision.nextActions[0] ?? "Use local branch preflight before posting."}`),
publicSafeSummary: sanitizePublicSummary(decision.publicNextActions?.[0] ?? `${decision.repoFullName}: Use local branch preflight before posting.`),
payload: { decision: decision as unknown as JsonValue },
});
}
Expand Down Expand Up @@ -538,11 +538,13 @@ function mapDecisionAction(kind: DecisionAction["actionKind"]): AgentActionType
}

function recommendationText(action: DecisionAction, decision: RepoDecision): string {
if (action.actionKind === "cleanup_existing_prs") return "Clean up existing PR pressure before opening new work.";
if (action.actionKind === "land_existing_prs") return "Focus on landing or closing already-open PRs.";
if (action.actionKind === "file_issue_discovery") return "Only file an actionable, non-duplicate issue-discovery report.";
if (decision.recommendation === "maintainer_lane") return "Treat this as maintainer-lane repo health work, not outside-contributor work.";
return action.nextActions[0] ?? "Pick narrow work and run branch preflight before opening a PR.";
if (action.actionKind === "cleanup_existing_prs") return `${decision.repoFullName}: clean up existing PR pressure before opening new work.`;
if (action.actionKind === "land_existing_prs") return `${decision.repoFullName}: focus on landing or closing already-open PRs.`;
if (action.actionKind === "file_issue_discovery") return `${decision.repoFullName}: only file an actionable, non-duplicate issue-discovery report.`;
if (action.actionKind === "maintainer_lane_improve_repo" || action.actionKind === "maintainer_cut_readiness") {
return `${decision.repoFullName}: maintainer-lane repo health work, not outside-contributor evidence.`;
}
return action.nextActions[0] ?? `${decision.repoFullName}: pick narrow work and run branch preflight before opening a PR.`;
}

function maintainerImpactFor(decision: RepoDecision): string {
Expand Down
150 changes: 131 additions & 19 deletions src/services/decision-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ export type DecisionPackRefreshNeeded = {
dataQuality?: ContributorDecisionPack["dataQuality"] | undefined;
};

export type LanguageMatch = {
language: string | null;
match: boolean;
};

export type RepoDecision = {
repoFullName: string;
recommendation: DecisionRecommendation;
Expand All @@ -99,10 +104,13 @@ export type RepoDecision = {
issueDiscoveryShare: number;
maintainerCut: number;
};
languageMatch: LanguageMatch;
labelFit: string[];
scoreBlockers: ScoreBlocker[];
riskReasons: string[];
whyThisHelps: string[];
nextActions: string[];
publicNextActions: string[];
};

export type DecisionAction = {
Expand All @@ -112,6 +120,7 @@ export type DecisionAction = {
recommendation: DecisionRecommendation;
whyThisHelps: string[];
nextActions: string[];
publicNextActions: string[];
};

export type ScoreBlocker = {
Expand Down Expand Up @@ -233,6 +242,8 @@ function buildContributorDecisionPack(args: {
const syncByRepo = new Map(args.syncStates.map((state) => [state.repoFullName.toLowerCase(), state]));
const totalsByRepo = new Map(args.totals.map((total) => [total.repoFullName.toLowerCase(), total]));
const outcomeByRepo = new Map(args.outcomeHistory.repoOutcomes.map((outcome) => [outcome.repoFullName.toLowerCase(), outcome]));
const languageSet = new Set((args.profile.github?.topLanguages ?? []).map((language) => language.toLowerCase()));
const labelHistory = new Set(args.profile.registeredRepoActivity?.dominantLabels ?? []);
const roleContexts = registeredRepositories.map((repo) =>
buildRoleContext({
login: args.login,
Expand All @@ -253,6 +264,8 @@ function buildContributorDecisionPack(args: {
outcome: outcomeByRepo.get(key),
syncState: syncByRepo.get(key),
totals: totalsByRepo.get(key),
languageSet,
labelHistory,
});
})
.sort((left, right) => right.priorityScore - left.priorityScore || left.repoFullName.localeCompare(right.repoFullName));
Expand Down Expand Up @@ -297,6 +310,8 @@ function buildRepoDecision(args: {
outcome?: ContributorOutcomeHistory["repoOutcomes"][number] | undefined;
syncState?: RepoSyncStateRecord | undefined;
totals?: RepoGithubTotalsSnapshotRecord | undefined;
languageSet?: Set<string> | undefined;
labelHistory?: Set<string> | undefined;
}): RepoDecision {
const lane = buildLaneAdvice(args.repo, args.repo.fullName);
const config = args.repo.registryConfig;
Expand Down Expand Up @@ -324,6 +339,24 @@ function buildRepoDecision(args: {
];
const recommendation = recommendationFor(lane.lane, args.roleContext, args.outcome, blockers);
const priorityScore = priorityFor(recommendation, rewardUpside, args.outcome, queue, blockers);
const syncLanguage = args.syncState?.primaryLanguage ?? null;
const languageMatch: LanguageMatch = {
language: syncLanguage,
match: Boolean(syncLanguage && args.languageSet?.has(syncLanguage.toLowerCase())),
};
const labelHistory = args.labelHistory;
const labelFit = labelHistory
? Object.keys(args.repo.registryConfig?.labelMultipliers ?? {}).filter((label) => labelHistory.has(label))
: [];
const copyContext: RepoCopyContext = {
repoFullName: args.repo.fullName,
lane: lane.lane,
queue,
rewardUpside,
outcome: args.outcome,
languageMatch,
labelFit,
};
return {
repoFullName: args.repo.fullName,
recommendation,
Expand All @@ -333,10 +366,13 @@ function buildRepoDecision(args: {
outcome: args.outcome,
queue,
rewardUpside,
languageMatch,
labelFit,
scoreBlockers: blockers,
riskReasons,
whyThisHelps: whyThisHelpsFor(recommendation, args.repo.fullName, args.outcome, rewardUpside),
nextActions: nextActionsFor(recommendation, lane.lane),
whyThisHelps: whyThisHelpsFor(recommendation, copyContext),
nextActions: nextActionsFor(recommendation, copyContext),
publicNextActions: publicNextActionsFor(recommendation, copyContext),
};
}

Expand Down Expand Up @@ -371,7 +407,10 @@ function priorityFor(
const upside = Math.max(rewardUpside.directPrShare, rewardUpside.issueDiscoveryShare, rewardUpside.emissionShare * 0.35) * 1000;
const history = (outcome?.mergedPullRequests ?? 0) * 2 + (outcome?.validSolvedIssues ?? 0) * 3 - (outcome?.closedPullRequests ?? 0) * 1.5;
const queuePenalty = Math.min(30, queue.openPullRequests * 0.25);
const blockerPenalty = blockers.reduce((sum, blocker) => sum + (blocker.severity === "critical" ? 35 : blocker.severity === "warning" ? 15 : 5), 0);
const penalizedBlockers = recommendation === "cleanup_first"
? blockers.filter((blocker) => blocker.code !== "open_pr_pressure")
: blockers;
const blockerPenalty = penalizedBlockers.reduce((sum, blocker) => sum + (blocker.severity === "critical" ? 35 : blocker.severity === "warning" ? 15 : 5), 0);
const base = recommendation === "cleanup_first" ? 75 : recommendation === "pursue" ? 65 : recommendation === "maintainer_lane" ? 55 : recommendation === "watch" ? 35 : 20;
return clamp(round(base + upside + history - queuePenalty - blockerPenalty), 0, 100);
}
Expand Down Expand Up @@ -399,28 +438,100 @@ function action(kind: DecisionActionKind, decision: RepoDecision, priorityScore:
recommendation: decision.recommendation,
whyThisHelps: decision.whyThisHelps,
nextActions: decision.nextActions,
publicNextActions: decision.publicNextActions,
};
}

function whyThisHelpsFor(
recommendation: DecisionRecommendation,
repoFullName: string,
outcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined,
rewardUpside: RepoDecision["rewardUpside"],
): string[] {
if (recommendation === "cleanup_first") return [`${repoFullName}: cleaning up active PR pressure protects scoreability and reduces maintainer friction.`];
if (recommendation === "maintainer_lane") return [`${repoFullName}: maintainer-owned work should improve repo health, intake quality, labels, and queue clarity.`];
if (recommendation === "pursue") return [`${repoFullName}: direct PR lane has ${round(rewardUpside.directPrShare)} lane share and no hard personal blocker in current signals.`];
if (recommendation === "watch") return [`${repoFullName}: issue-discovery context is useful only when the report is actionable, non-duplicate, and likely solvable.`];
type RepoCopyContext = {
repoFullName: string;
lane: string;
queue: RepoDecision["queue"];
rewardUpside: RepoDecision["rewardUpside"];
outcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined;
languageMatch: LanguageMatch;
labelFit: string[];
};

function whyThisHelpsFor(recommendation: DecisionRecommendation, context: RepoCopyContext): string[] {
const { repoFullName, rewardUpside, outcome, languageMatch, labelFit, lane } = context;
const labelPhrase = labelFit.length > 0 ? ` Label overlap with your history: ${labelFit.slice(0, 3).join(", ")}.` : "";
const languagePhrase = languageMatch.match && languageMatch.language ? ` Primary language ${languageMatch.language} matches your top languages.` : "";
if (recommendation === "cleanup_first") {
const openCount = outcome?.openPullRequests ?? 0;
return [`${repoFullName}: ${openCount} of your open PR(s) here block scoreability; clearing them lowers maintainer friction.${labelPhrase}`];
}
if (recommendation === "maintainer_lane") {
return [`${repoFullName}: maintainer-owned work should improve repo health, intake quality, labels, and queue clarity. Maintainer cut: ${round(rewardUpside.maintainerCut)}.`];
}
if (recommendation === "pursue") {
const merged = outcome?.mergedPullRequests ?? 0;
const historyPhrase = merged > 0 ? ` You have ${merged} merged PR(s) in this repo already.` : "";
if (lane === "split") {
return [`${repoFullName}: split lane (direct PR ${round(rewardUpside.directPrShare)}, issue-discovery ${round(rewardUpside.issueDiscoveryShare)}); both lanes are useful here.${languagePhrase}${labelPhrase}${historyPhrase}`];
}
return [`${repoFullName}: direct PR lane share ${round(rewardUpside.directPrShare)} with no hard personal blocker.${languagePhrase}${labelPhrase}${historyPhrase}`];
}
if (recommendation === "watch") {
return [`${repoFullName}: ${lane === "issue_discovery" ? "issue-discovery-only" : "low-direct-PR"} lane; only actionable, non-duplicate issue reports add value.${labelPhrase}`];
}
return [`${repoFullName}: risk-adjusted priority is low until blockers improve.`];
}

function nextActionsFor(recommendation: DecisionRecommendation, lane: string): string[] {
if (recommendation === "cleanup_first") return ["Close, update, or land existing open PRs before opening more work.", "Use local branch preflight on each active PR to reduce review friction."];
if (recommendation === "maintainer_lane") return ["Improve contributor intake health, label clarity, and queue hygiene.", "Review maintainer_cut readiness separately from outside-contributor strategy."];
if (recommendation === "pursue") return ["Pick one narrow change, link context clearly, run tests, and use local branch analysis before opening the PR."];
if (lane === "issue_discovery") return ["File only high-confidence, actionable, non-duplicate issue-discovery reports."];
return ["Choose a different repo or wait for cleaner lane/credibility conditions."];
function nextActionsFor(recommendation: DecisionRecommendation, context: RepoCopyContext): string[] {
const { repoFullName, queue, outcome, languageMatch, labelFit, lane } = context;
const labelHint = labelFit.length > 0 ? ` (target labels: ${labelFit.slice(0, 3).join(", ")})` : "";
const languageHint = languageMatch.match && languageMatch.language ? ` in ${languageMatch.language}` : "";
if (recommendation === "cleanup_first") {
const openCount = outcome?.openPullRequests ?? 0;
return [
`${repoFullName}: close, update, or land your ${openCount} open PR(s) before opening more work${labelHint}.`,
"Use local branch preflight on each active PR to reduce review friction.",
];
}
if (recommendation === "maintainer_lane") {
return [
`${repoFullName}: improve contributor intake health, label clarity, and queue hygiene as repo owner.`,
"Review maintainer_cut readiness separately from outside-contributor strategy.",
];
}
if (recommendation === "pursue") {
if (lane === "split") {
return [
`${repoFullName}: split lane — choose direct PR${languageHint}${labelHint} OR file an actionable issue-discovery report; queue has ${queue.openPullRequests} open PR(s) and ${queue.openIssues} open issue(s).`,
];
}
return [
`${repoFullName}: pick one narrow change${languageHint}${labelHint}; run tests + branch preflight before opening the PR. Queue has ${queue.openPullRequests} open PR(s).`,
];
}
if (recommendation === "watch" || lane === "issue_discovery") {
return [
`${repoFullName}: file only high-confidence, actionable, non-duplicate issue-discovery reports${labelHint}. Open issues in queue: ${queue.openIssues}.`,
];
}
return [`${repoFullName}: choose a different repo or wait for cleaner lane/credibility conditions.`];
}

function publicNextActionsFor(recommendation: DecisionRecommendation, context: RepoCopyContext): string[] {
const { repoFullName, languageMatch, labelFit, lane } = context;
const languageHint = languageMatch.match && languageMatch.language ? ` in ${languageMatch.language}` : "";
const labelHint = labelFit.length > 0 ? ` (consider labels: ${labelFit.slice(0, 3).join(", ")})` : "";
if (recommendation === "cleanup_first") {
return [`${repoFullName}: resolve open PR pressure before opening additional review load.`];
}
if (recommendation === "maintainer_lane") {
return [`${repoFullName}: as repo owner, improve intake health, label clarity, and queue hygiene.`];
}
if (recommendation === "pursue") {
if (lane === "split") {
return [`${repoFullName}: split lane — direct PR or actionable issue report${languageHint}${labelHint}; use Gittensory preflight before posting public PR context.`];
}
return [`${repoFullName}: pick a narrow change${languageHint}${labelHint}; use Gittensory preflight before posting public PR context.`];
}
if (recommendation === "watch" || lane === "issue_discovery") {
return [`${repoFullName}: file only actionable, non-duplicate issue-discovery reports${labelHint}.`];
}
return [`${repoFullName}: consider a different repo until lane/credibility signals improve.`];
}

function sanitizeOfficialStats(profile: ContributorProfile): ContributorDecisionPack["profile"]["officialStats"] {
Expand Down Expand Up @@ -479,6 +590,7 @@ export const __decisionPackInternals = {
actionsForDecision,
whyThisHelpsFor,
nextActionsFor,
publicNextActionsFor,
sanitizeOfficialStats,
withSnapshotMetadata,
snapshotAgeMs,
Expand Down
Loading