skillhub/.github/scripts/issue-triage-merge.ts
wowo 8f694ddc7c
[codex] add issue triage automation mvp (#268)
* add issue triage automation mvp

* Document issue automation design in Chinese

* Fix legacy compat slug tests
2026-04-09 15:04:33 +08:00

166 lines
4.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { TriageResult, TriageSnapshot } from "./issue-triage-types.ts";
import { buildMaintainerHandoffBrief } from "./issue-handoff-brief.ts";
export function mergeRuleAndLlm(ruleResult: TriageResult): TriageResult {
const llm = ruleResult.llm;
if (!llm || llm.failed || llm.mode !== "assist") {
return {
...ruleResult,
handoffBrief: ruleResult.route === "core"
? buildMaintainerHandoffBrief(ruleResult)
: undefined,
mode: llm && !llm.failed && llm.mode === "shadow"
? "llm-shadow"
: "rules-only",
inputHash: llm?.inputHash ?? ruleResult.inputHash,
};
}
const impact = nudgeScore(ruleResult.impact, llm.impact);
const urgency = nudgeScore(ruleResult.urgency, llm.urgency);
const effort = nudgeScore(ruleResult.effort, llm.effort);
const confidence = nudgeScore(ruleResult.confidence, llm.confidence);
const missingFields = unique([
...ruleResult.missingFields,
...llm.missingInfo,
]);
const highRiskReasons = unique([
...ruleResult.highRiskReasons,
...llm.riskFlags.map((flag) =>
`LLM 标记了高风险区域:${flag} / LLM flagged high-risk area: ${flag}.`
),
]);
const requiresCoreMaintainer = ruleResult.requiresCoreMaintainer;
const riskLevel = highRiskReasons.length > 0 ? "high" : "low";
const priority = clamp(
roundToOneDecimal(
impact * 0.45 +
urgency * 0.35 +
ruleResult.ageBoost +
ruleResult.engagementBoost,
),
1,
5,
);
const route = determineRoute(
priority,
effort,
confidence,
riskLevel,
missingFields,
requiresCoreMaintainer,
);
const nextAction = describeNextAction(route, missingFields);
const reasons = unique([
...ruleResult.reasons,
...llm.rationale,
llm.summary
? `LLM 摘要:${llm.summaryZh || llm.summary} / LLM summary: ${
llm.summaryEn || llm.summary
}`
: "",
]).slice(0, 6);
const mergedSnapshot: TriageSnapshot = {
route,
riskLevel,
requiresCoreMaintainer,
openDays: ruleResult.openDays,
impact,
urgency,
effort,
confidence,
priority,
ageBoost: ruleResult.ageBoost,
priorityFloor: ruleResult.priorityFloor,
engagementBoost: ruleResult.engagementBoost,
missingFields,
reasons,
highRiskReasons,
nextAction,
};
return {
...ruleResult,
...mergedSnapshot,
mode: "llm-assist",
inputHash: llm.inputHash,
handoffBrief: route === "core"
? buildMaintainerHandoffBrief({
...ruleResult,
...mergedSnapshot,
mode: "llm-assist",
inputHash: llm.inputHash,
})
: undefined,
};
}
export function determineRoute(
priority: number,
effort: number,
confidence: number,
riskLevel: "low" | "high",
missingFields: string[],
requiresCoreMaintainer = false,
) {
if (requiresCoreMaintainer) {
return "core";
}
if (missingFields.length > 0 || confidence <= 2) {
return "needs-info";
}
if (priority < 3.6) {
return "deferred";
}
if (riskLevel === "high" || effort >= 4 || confidence <= 3) {
return "core";
}
return "agent-ready";
}
export function describeNextAction(
route: TriageResult["route"],
missingFields: string[],
) {
if (route === "needs-info") {
return `等待补充更多信息;作者更新 issue 或评论 \`/retriage\` 后重新分流 / Wait for more detail, then rerun triage after the author edits the issue or comments \`/retriage\`. Missing: ${
missingFields.join(", ")
}.`;
}
if (route === "deferred") {
return "将 issue 保留在 deferred 队列,并由 6 小时一次的 rescore 持续抬升;最晚在第 10 天强制进入 active lane。若第 14 天仍未闭环,应按 SLA 视为 P0 升级目标,并在下一次 triage 中重点处理 / Keep the issue in the deferred queue and let the 6-hour rescore keep lifting it; it is forced into an active lane by day 10. If it is still open on day 14, treat it as a P0 escalation target under the SLA and prioritize it in the next triage pass.";
}
if (route === "core") {
return "交给 core maintainer并结合本地编程Agent协助完成复现、收敛范围与验证闭环 / Hand the issue to a core maintainer and use a local programming agent for reproduction, scoping, and validation.";
}
return "在 self-hosted issue-agent runner 启用后,将其标记为低风险 agent 可执行候选 / Mark as a candidate for low-risk agent execution once the self-hosted issue-agent runner is enabled.";
}
function nudgeScore(ruleScore: number, llmScore: number) {
if (llmScore === ruleScore) {
return ruleScore;
}
return clamp(ruleScore + Math.sign(llmScore - ruleScore), 1, 5);
}
function unique(values: string[]) {
return [...new Set(values.filter((value) => value.trim().length > 0))];
}
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(max, value));
}
function roundToOneDecimal(value: number) {
return Math.round(value * 10) / 10;
}