skillhub/.github/scripts/issue-handoff-brief.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

257 lines
9.3 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 { MaintainerHandoffBrief, TriageResult } from "./issue-triage-types.ts";
const AREA_RULES: Array<{ keywords: string[]; area: string }> = [
{
keywords: ["clawhub publish", "publish skill", "publish", "namespace"],
area:
"CLI 发布命令参数解析与 namespace 感知发布流程 / CLI publish command option parsing and namespace-aware publish flow",
},
{
keywords: ["clawhub install", "install skill", "install"],
area:
"技能安装流程与 registry/lockfile 集成 / Skill installation flow and registry/lockfile integration",
},
{
keywords: ["clawhub update", "update skill", "update"],
area:
"已安装技能更新流程与版本解析 / Installed skill update flow and version resolution",
},
{
keywords: ["clawhub sync", "sync skill", "sync"],
area:
"本地技能同步流程与发布 diff 检测 / Local skill sync flow and publish diff detection",
},
{
keywords: ["inspect", "search", "explore"],
area:
"Registry 发现与 CLI 查询流程 / Registry discovery and CLI query workflow",
},
{
keywords: ["auth", "login", "ldap", "sso", "token"],
area:
"认证、会话与身份集成 / Authentication, session, and identity integration",
},
{
keywords: ["openapi", "sdk", "api contract", "contract"],
area:
"公开 API 契约、生成 SDK 与兼容性表面 / Public API contract, generated SDKs, and compatibility surface",
},
{
keywords: ["docs", "documentation", "manual", "help", "--help"],
area:
"文档、操作指引与 CLI help 输出 / Documentation, operator guidance, and CLI help output",
},
{
keywords: ["scanner", "security", "audit"],
area:
"安全扫描流程与审计/报告行为 / Security scanner pipeline and audit/reporting behavior",
},
];
export function buildMaintainerHandoffBrief(
result: TriageResult,
): MaintainerHandoffBrief | undefined {
if (result.route !== "core") {
return undefined;
}
const summary = buildSummary(result);
const whyCore = unique([
result.requiresCoreMaintainer
? "阻塞 OpenClaw/ClawHub 核心工作流,因此即便改动范围看起来可控,也需要 maintainer judgment / Blocks an OpenClaw/ClawHub core workflow, so maintainer judgment is required even if the code change looks bounded."
: "",
result.riskLevel === "high"
? "触及高风险区域,未经 maintainer 审查不应直接信任自动修复 / Touches a higher-risk area where automated fixes should not be trusted without maintainer review."
: "",
result.effort >= 4
? "大概率跨多个模块或公共兼容面 / Likely spans multiple modules or a public compatibility surface."
: "",
result.confidence <= 3
? "问题本身重要,但仍需要 maintainer 先收敛范围再实施 / The issue is important, but a maintainer still needs to tighten scope before implementation."
: "",
...result.highRiskReasons,
]).slice(0, 4);
const reproduction = buildReproduction(result);
const suspectedAreas = inferSuspectedAreas(result);
const risks = buildRisks(result, suspectedAreas);
const validation = buildValidation(result, suspectedAreas);
return {
summary,
whyCore,
reproduction,
suspectedAreas,
risks,
validation,
};
}
function buildSummary(result: TriageResult) {
const llmSummary = result.llm?.summaryZh ?? result.llm?.summary ??
result.llm?.summaryEn;
if (llmSummary && llmSummary.trim().length > 0) {
return llmSummary.trim();
}
const preferred = [
result.sections["summary"],
result.sections["problem"],
result.sections["expected behavior"],
].find((value) => value && value.trim().length > 0);
if (preferred) {
return compact(preferred);
}
return result.issue.title.replace(/^\[[^\]]+\]\s*/, "").trim();
}
function buildReproduction(result: TriageResult) {
const commandFocusedSteps = extractCommandAndErrorLines(
result.sections["steps to reproduce"],
);
if (commandFocusedSteps.length > 0) {
return commandFocusedSteps.slice(0, 4);
}
const steps = splitIntoBullets(result.sections["steps to reproduce"]);
if (steps.length > 0) {
return steps.slice(0, 5);
}
const problem = splitIntoBullets(result.sections["problem"]);
if (problem.length > 0) {
return problem.slice(0, 4);
}
return [
"按 issue 中描述的操作路径复现,并确认当前失败模式 / Recreate the operator flow described in the issue and confirm the current failure mode.",
];
}
function inferSuspectedAreas(result: TriageResult) {
const text = [
result.issue.title,
result.sections["summary"] ?? "",
result.sections["problem"] ?? "",
result.sections["steps to reproduce"] ?? "",
result.sections["impact"] ?? "",
result.sections["api contract impact"] ?? "",
result.sections["contract or sdk impact"] ?? "",
]
.join("\n")
.toLowerCase();
const areas = AREA_RULES.filter((rule) =>
rule.keywords.some((keyword) => text.includes(keyword))
).map((rule) => rule.area);
if (areas.length > 0) {
return unique(areas).slice(0, 5);
}
return [
"最接近该失败路径的 owner-facing 工作流模块 / The closest owner-facing workflow module for the issue's reported failure path",
"当前对外承诺该行为的文档或 help 文本 / Any docs or help text that currently promise the affected behavior",
];
}
function buildRisks(result: TriageResult, suspectedAreas: string[]) {
const risks = unique([
...result.highRiskReasons,
result.llm?.riskFlags.includes("cli-protocol")
? "CLI 行为、文档和操作预期可能发生漂移,需要同步更新命令 help 与兼容性说明 / CLI behavior, docs, and operator expectations may drift unless command help and compatibility notes are updated together."
: "",
suspectedAreas.some((area) => area.toLowerCase().includes("namespace"))
? "namespace 范围行为如果没有保留 fallback routing可能回归默认 publish/install 流程 / Namespace-scoped behavior can regress default publish/install flows if fallback routing is not preserved."
: "",
result.requiresCoreMaintainer
? "该问题影响已定义主流程,回归会很快被终端用户感知 / This issue affects a documented primary workflow, so regressions would be visible to end users quickly."
: "",
]);
return risks.length > 0 ? risks.slice(0, 4) : [
"合并前检查相邻用户路径是否出现回归 / Check for regressions in adjacent user-facing workflow paths before merging.",
];
}
function buildValidation(result: TriageResult, suspectedAreas: string[]) {
const validation = unique([
result.sections["steps to reproduce"]
? "按 issue 中的复现步骤逐条回放,确认报告的问题已消失 / Replay the exact reproduction steps from the issue and confirm the reported failure disappears."
: "修复后端到端验证主报告流程 / Validate the primary reported workflow end-to-end after the fix.",
result.sections["expected behavior"]
? `确认最终行为符合 issue 期望 / Confirm the final behavior matches the issue's expected outcome: ${
compact(result.sections["expected behavior"])
}`
: "",
suspectedAreas.some((area) => area.toLowerCase().includes("documentation"))
? "更新或核对文档与 CLI help 输出,确保其与实现行为一致 / Update or verify documentation and CLI help output so they match the implemented behavior."
: "",
suspectedAreas.some((area) => area.toLowerCase().includes("api contract"))
? "发布前检查下游 API/SDK/CLI 的兼容性预期 / Check for downstream API/SDK/CLI compatibility expectations before shipping."
: "",
suspectedAreas.some((area) => area.toLowerCase().includes("namespace"))
? "同时验证 namespace 范围行为与默认非 namespace 流程 / Verify both namespace-scoped behavior and the default non-namespace flow."
: "",
result.requiresCoreMaintainer
? "围绕受影响的 OpenClaw/ClawHub 用户路径执行最小必要回归测试 / Run the smallest relevant regression test around the affected OpenClaw/ClawHub user journey."
: "",
]);
return validation.slice(0, 5);
}
function splitIntoBullets(value: string | undefined) {
if (!value) {
return [];
}
return value
.split("\n")
.map((line) => line.trim())
.filter((line) =>
line.length > 0 &&
line !== "```" &&
!line.startsWith("PS ") &&
!line.startsWith("Usage:") &&
!line.startsWith("Options:") &&
!line.startsWith("Arguments:")
)
.map((line) => line.replace(/^[*-]\s*/, ""))
.slice(0, 6);
}
function extractCommandAndErrorLines(value: string | undefined) {
if (!value) {
return [];
}
return value
.split("\n")
.map((line) => line.trim())
.filter((line) =>
line.length > 0 &&
(
line.toLowerCase().includes("clawhub ") ||
line.toLowerCase().startsWith("error:") ||
line.toLowerCase().includes("unknown option") ||
line.toLowerCase().includes("usage:")
)
)
.map((line) => line.replace(/^[>*-]\s*/, ""))
.slice(0, 4);
}
function compact(value: string) {
return value.replace(/\s+/g, " ").trim();
}
function unique(values: string[]) {
return [...new Set(values.filter((value) => value.trim().length > 0))];
}