diff --git a/skills/gitlink-stale/README.md b/skills/gitlink-stale/README.md new file mode 100644 index 0000000..01126ab --- /dev/null +++ b/skills/gitlink-stale/README.md @@ -0,0 +1,144 @@ +# gitlink-stale + +> GitLink Stale Issue/PR 自动处理 Skill — 让 AI Agent 帮你清理堆积如山的未活动 Issue/PR + +[![Skill](https://img.shields.io/badge/Skill-gitlink--stale-blue)](./SKILL.md) +[![Compatibility](https://img.shields.io/badge/Compatible-Claude%20Code%20%7C%20Cursor%20%7C%20OpenAI%20Code-green)](https://claude.com/claude-code) + +## 🎯 这是什么? + +`gitlink-stale` 是基于 [gitlink-cli](../../README.md) 的 **AI Agent Skill**,专门用于: + +- 🔍 **扫描识别** 长期未活动的 GitLink Issue / PR(默认 60 天) +- 🧠 **AI 智能判断** 区分"真僵尸"和"等维护者回复"(不是简单按时间一刀切) +- 🏷️ **自动打 stale 标签** 通知作者/相关者 +- 💬 **友好催办评论** 避免简单粗暴的"过期警告" +- 🔒 **过期自动关闭** 超过宽限期(默认 14 天)仍未响应才关闭 +- 🛡️ **白名单豁免** `pinned`/`security`/`roadmap` 永不动 +- 📊 **生成审计报告** JSON + 表格,可追溯 + +适合**所有 Issue/PR 长期堆积**的开源项目或团队仓库,承担类似 GitHub `stale` bot 的角色,但通过 AI Agent 实现"人在环路"和"智能判断"。 + +--- + +## 🚀 快速开始 + +### 前置条件 + +1. 已安装 `gitlink-cli`(参考 [主 README](../../README.md#安装与快速上手)) +2. 已完成认证(`gitlink-cli auth login`) +3. 在目标仓库目录下(自动解析 owner/repo)或显式指定 `--owner --repo` + +### 5 分钟体验 + +向 AI Agent(如 Claude Code)说: + +> "帮我用 gitlink-stale 扫描 owner/repo 仓库中所有 60 天以上未活动的 open Issue,生成报告后等我确认。" + +AI 会: + +1. 拉取所有 open Issue/PR +2. 按时间过滤 + 白名单豁免 + AI 判断 +3. 展示表格报告(含 confidence) +4. 等你确认后才执行 mark_stale / auto_close 动作 + +--- + +## 📁 Skill 结构 + +``` +gitlink-stale/ +├── README.md # 本文件 +├── SKILL.md # AI Agent 读取的主入口 +├── references/ +│ ├── gitlink-stale-scan.md # 扫描算法详解 +│ ├── gitlink-stale-judge.md # AI 判断规则详解 +│ ├── gitlink-stale-actions.md # 动作执行手册 +│ └── gitlink-stale-exempt.md # 白名单豁免规则 +├── examples/ +│ ├── weekly-cleanup-workflow.md # 每周清理工作流 +│ ├── pr-stale-workflow.md # PR 催办工作流 +│ └── ai-judgment-demo.md # AI 判断示例 +└── skill_test.md # 测试指南 +``` + +--- + +## 🧠 核心差异化(vs GitHub stale-bot) + +| 维度 | GitHub stale-bot | gitlink-stale(本 Skill) | +|------|-----------------|------------------------| +| **触发** | 事件驱动(cron),自动跑 | Agent 驱动(按需),人在环路 | +| **判断** | 仅看时间(>= 60 天) | 时间 + AI 判断"真僵尸" | +| **白名单** | 简单 label 匹配 | 多信号(label + tracker + priority + 作者活跃度) | +| **评论** | 固定模板 | 根据上下文动态生成 | +| **回滚** | 难(已自动关闭) | 字段快照,一键恢复 | +| **审计** | 日志在 Actions | JSON 报告 + 备份文件 | + +**关键差异**:本 Skill 不是简单按时间一刀切,而是通过 AI 判断评论历史、活跃度等信号决定"是否真应该处理"。 + +--- + +## 🛡️ 安全设计 + +| 机制 | 说明 | +|------|------| +| ✅ Dry-run 默认 | 分析阶段不调用任何写 API | +| ✅ 双重确认 | 应用动作前必须表格展示 + 用户同意 | +| ✅ 白名单豁免 | pinned/security/roadmap 永不动 | +| ✅ AI 双重判断 | 不仅看时间,还要 AI 判断"真僵尸" | +| ✅ 低置信度跳过 | confidence < 0.6 不自动处理 | +| ✅ 字段快照 | 每个变更保留原始标签和状态,支持回滚 | +| ✅ 批次上限 | 单批 ≤ 20 个,超出强制分批 | + +--- + +## 🤖 AI Agent 兼容性 + +已在以下 Agent 平台设计兼容: + +- ✅ **Claude Code** — 主要验证目标,所有示例均可执行 +- ✅ **Cursor** — 通过 SKILL.md markdown 协议兼容 +- ✅ **OpenAI Code** — 通过 references/ 文档兼容 + +--- + +## 📚 相关文档 + +- [SKILL.md — AI Agent 主入口](./SKILL.md) +- [扫描算法详解](./references/gitlink-stale-scan.md) +- [AI 判断规则详解](./references/gitlink-stale-judge.md) +- [动作执行手册](./references/gitlink-stale-actions.md) +- [白名单豁免规则](./references/gitlink-stale-exempt.md) +- [每周清理工作流示例](./examples/weekly-cleanup-workflow.md) +- [PR 催办示例](./examples/pr-stale-workflow.md) +- [AI 判断演示](./examples/ai-judgment-demo.md) +- [测试指南](./skill_test.md) +- [上游 Skill: gitlink-issue](../gitlink-issue/SKILL.md) +- [互补 Skill: gitlink-issue-triage](../gitlink-issue-triage/SKILL.md) +- [共享规则: gitlink-shared](../gitlink-shared/SKILL.md) + +--- + +## ❓ FAQ + +**Q: 必须用 AI Agent 吗?人能用吗?** +A: 当然可以。SKILL.md 中的工作流对人类也是清晰的 SOP,你可以手动按步骤执行 gitlink-cli 命令。AI 的价值在 Stage C "真假僵尸判断",但人类读评论历史同样能做。 + +**Q: PR 没有 label 接口怎么打 stale?** +A: GitLink PR 端点暂不支持 PR 维度的标签。对 PR 只做评论催办,在评论标题写"⏰ Stale"作为视觉提示。 + +**Q: 用户回复后会自动去掉 stale 标签吗?** +A: 默认不会自动响应。下次扫描时看到新活动会自动跳过;如需立刻移除,手动调用 `issue +label-remove`。 + +**Q: 与 GitHub Actions 的 stale-bot 有何不同?** +A: 本 Skill 是 **Agent-driven**(按需触发、人在环路、AI 智能判断),不是 **Event-driven**(自动触发、机械规则)。适合需要人工监督和精准判断的高质量项目。 + +**Q: 误关了重要 Issue 怎么办?** +A: 见 SKILL.md §8 回滚策略。所有动作都保留原始字段快照,可重新打开。强烈建议 urgent/roadmap 类 Issue 打上对应标签加入白名单。 + +--- + +## 📄 许可证 + +继承 gitlink-cli 的 [MulanPSL-2.0](../../LICENSE)。 diff --git a/skills/gitlink-stale/SKILL.md b/skills/gitlink-stale/SKILL.md new file mode 100644 index 0000000..d02c084 --- /dev/null +++ b/skills/gitlink-stale/SKILL.md @@ -0,0 +1,452 @@ +--- +name: gitlink-stale +version: 1.0.0 +description: "Stale Issue/PR 自动处理:识别长期未活动的 Issue/PR,标记 stale 标签、通知相关者、过期自动关闭。当用户需要清理堆积 Issue/PR、定期巡检仓库、或想仿照 GitHub stale-bot 行为时触发。" +metadata: + requires: + bins: ["gitlink-cli"] + cliHelp: "gitlink-cli issue --help" +--- + +# gitlink-stale(Stale Issue/PR 自动处理) + +**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。** +**CRITICAL — 所有"标记/评论/关闭"动作默认 dry-run;只有用户明确确认后才执行写入。** +**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。** + +> **前置依赖:** 先阅读 [`../gitlink-issue/SKILL.md`](../gitlink-issue/SKILL.md) 了解 Issue 基础操作和字段映射,[`../gitlink-pr/SKILL.md`](../gitlink-pr/SKILL.md) 了解 PR 基础操作。 + +--- + +## 1. 这个 Skill 做什么? + +`gitlink-stale` 是一个 **AI Agent 驱动的长期未活动 Issue/PR 处理工作流**,解决开源/协作项目中常见的"Issue/PR 堆积无人理"问题: + +- 🔍 **扫描识别**:找出 N 天未活动的 Issue/PR(默认 60 天) +- 🧠 **AI 智能判断**:区分"真僵尸"和"等维护者回复"(不是简单按时间一刀切) +- 🏷️ **自动打 stale 标签**:通知作者/相关者 +- 💬 **友好催办评论**:避免简单粗暴的"过期警告" +- 🔒 **过期自动关闭**:超过宽限期(默认 14 天)仍未响应才关闭 +- 🛡️ **白名单豁免**:`pinned`/`security`/`roadmap` 等关键 Issue 永不处理 +- ✅ **Dry-run 优先**:所有写入操作默认预览,确认后才落地 + +适合**所有 Issue/PR 长期堆积**的开源项目或团队仓库,承担类似 GitHub `stale` bot 的角色,但通过 AI Agent 实现"人在环路"和"智能判断"。 + +--- + +## 2. 工作流总览 + +``` + ┌─────────────────────────────────┐ + │ Step 1: 扫描候选列表 │ + │ issue +list --state open │ + │ pr +list --state open │ + └────────────┬────────────────────┘ + ▼ + ┌──────────────────────────────────────────────┐ + │ Step 2: 时间过滤 │ + │ now - updated_at > stale_days (默认 60) │ + │ 排除白名单(pinned/security/roadmap/...) │ + └────────────┬─────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────┐ + │ Step 3: AI 智能分类(核心差异化) │ + │ - 是否仍在等维护者回复? │ + │ - 是否是关键功能/路线图? │ + │ - 是否历史活跃度高? │ + │ - 输出 confidence + recommendation │ + └────────────┬─────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────┐ + │ Step 4: 生成处理计划(dry-run) │ + │ { │ + │ issue_id: 142, │ + │ action: "mark_stale", │ + │ reason: "60 天未活动", │ + │ confidence: 0.85 │ + │ } │ + └────────────┬─────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────────────┐ + │ Step 5: 用户确认 → 执行 │ + │ issue +label-add --label stale │ + │ issue +comment --body "..." │ + │ 过宽限期: issue +close │ + └──────────────────────────────────────────────┘ +``` + +--- + +## 3. Shortcuts 与 Raw API 速查 + +本 Skill 复用 gitlink-cli 已有命令,**不新增 shortcut**,确保单一可信源。 + +### 3.1 读操作(只读,可放心使用) + +| 命令 | 用途 | +|------|------| +| `issue +list --state open --format json` | 获取 open Issue 列表 | +| `issue +view --number --format json` | 获取 Issue 详情(含 journals 评论历史) | +| `issue +label-list --number --format json` | 查看 Issue 当前标签 | +| `pr +list --state open --format json` | 获取 open PR 列表(注意需客户端按 `pull_request_status: 0` 二次过滤) | +| `pr +view --id --format json` | 获取 PR 详情 | +| `api GET /v1/:owner/:repo/issue_tags.json` | 获取仓库标签(name→id 映射) | + +### 3.2 写操作(默认 dry-run,确认后执行) + +| 命令 | 用途 | +|------|------| +| `issue +label-add --number --labels "stale"` | 给 Issue 打 stale 标签 | +| `issue +label-remove --number --label "stale"` | 移除 stale 标签(用户回复后恢复) | +| `issue +comment --number --body ""` | 评论催办/关闭说明 | +| `issue +close --number ` | 关闭 Issue | +| `issue +batch-close --numbers --dry-run` | 批量关闭预览 | +| `pr +comment --id --body ""` | 给 PR 评论 | + +> ⚠️ **PR 标签**:GitLink PR 端点暂不支持 PR 维度的 label 操作,对 PR 只评论、不打标签;若需要"PR stale 标签",在评论正文中显式写"⚠️ Stale"。 + +--- + +## 4. 核心算法(AI 智能判断) + +> 这是本 Skill 与"简单按时间一刀切"工具的核心差异。 +> AI Agent 应**优先**遵循以下规则,对规则无法覆盖的情况使用语义判断。 + +### 4.1 三阶段判断流水线 + +``` +Issue/PR JSON + │ + ▼ +┌─────────────────────────────────┐ +│ Stage A: 时间扫描 │ +│ - days_inactive = now - updated │ +│ - 默认阈值: 60 天 │ +└────────────┬────────────────────┘ + ▼ +┌─────────────────────────────────┐ +│ Stage B: 白名单豁免 │ +│ - 含 pinned/security 等标签 → 跳过│ +│ - tracker 是 roadmap/epic → 跳过 │ +└────────────┬────────────────────┘ + ▼ +┌─────────────────────────────────┐ +│ Stage C: AI 真假僵尸判断 │ +│ - 评论历史分析 │ +│ - 等维护者回复?等用户回复? │ +│ - 输出 truly_stale + confidence │ +└─────────────────────────────────┘ +``` + +### 4.2 时间计算 + +**关键字段**:`updated_at`(Issue 最后活动时间,包括评论、状态变更、字段修改) + +```python +days_inactive = (now_utc - parse(issue.updated_at)).days + +# 阈值(可通过参数自定义) +if days_inactive >= close_threshold: # 默认 74 天(60 + 14 宽限期) + candidate_action = "auto_close" +elif days_inactive >= stale_threshold: # 默认 60 天 + candidate_action = "mark_stale" +else: + candidate_action = None # 不处理 +``` + +> ⚠️ **GitLink API 已知行为**:`updated_at` 字段在某些 Issue 上可能缺失或时区异常。降级策略:取 `journals` 数组最后一条的 `created_at` 作为最后活动时间。 + +### 4.3 白名单豁免规则 + +满足**任一**条件即跳过处理: + +| 信号 | 说明 | +|------|------| +| 含 `pinned`/`置顶` 标签 | 重要 Issue | +| 含 `security`/`安全` 标签 | 安全相关 | +| 含 `roadmap`/`路线图` 标签 | 长期规划 | +| 含 `epic`/`里程碑` 标签 | 大任务 | +| tracker_id 是 `roadmap` 类型 | 路线图类 | +| priority_id == 4 (urgent) | 紧急任务 | +| 标题含 `[Keep Open]`/`[Pinned]` | 显式标记 | +| 作者仍是仓库活跃成员 | 信任作者会跟进 | + +### 4.4 AI 真假僵尸判断(Stage C 核心) + +**关键差异化**:不是简单的"时间到了就标记",而是 AI 判断是否真的应该处理。 + +**判断信号**: + +| 信号类型 | 僵尸(应处理) | 活跃(应跳过) | +|---------|---------------|---------------| +| 评论历史 | 维护者 0 回复,或仅"已知问题"占位 | 维护者最近 30 天内回复过 | +| Issue 类型 | bug/feature/小需求,需要明确动作 | question/讨论类,已自然结束 | +| 标签 | 无标签或仅 stale | `in-progress`/`under-review` | +| 评论数 | 0 评论,长期无人理 | ≥5 评论,有讨论 | +| 作者活跃度 | 0 issue 历史,可能是路过用户 | 资深贡献者,会跟进 | +| 关键词 | "测试"、"占位"、"无复现" | "正在处理"、"待 v2"、"等待上游" | + +**AI 决策输出**: + +```json +{ + "issue_number": 142, + "title": "...", + "last_activity": "2026-04-15T10:30:00Z", + "days_inactive": 62, + "ai_analysis": { + "truly_stale": true, + "confidence": 0.85, + "reason": "用户最后回复 60 天前,维护者无回复,0 评论,作者仅此 1 个 Issue", + "exempt": false + }, + "recommended_action": "mark_stale", + "next_review_date": "2026-06-30" +} +``` + +### 4.5 置信度阈值 + +| confidence | 建议动作 | +|-----------|---------| +| ≥ 0.8 | 直接列入"建议执行"清单 | +| 0.6 - 0.8 | 列入"建议执行",但报告中标记"建议人工复核" | +| < 0.6 | **不自动处理**,仅列入"待人工判断"队列 | + +--- + +## 5. 标准工作流(AI Agent 执行模板) + +> **AI Agent 看这里**:以下是你被请求"清理 stale Issue/PR"时应遵循的标准流程。 + +### Step 1 — 确认范围与参数 + +```bash +# 默认参数(可被用户覆盖) +# - stale_threshold: 60 天 +# - close_threshold: 74 天(60 + 14 宽限期) +# - batch_size: 20 个/批 + +# 确认 owner/repo +gitlink-cli issue +list --state open --limit 5 --format json +``` + +向用户确认:"要扫描哪个仓库?阈值用默认(60 天标记/74 天关闭)还是自定义?想一批处理多少个?" + +### Step 2 — 拉取候选列表 + +```bash +# Issue 候选 +gitlink-cli issue +list \ + --owner --repo \ + --state open \ + --limit 100 \ + --format json > /tmp/issues-open.json + +# PR 候选 +gitlink-cli pr +list \ + --owner --repo \ + --state open \ + --format json > /tmp/prs-open.json +``` + +### Step 3 — 拉取仓库标签(用于白名单判断) + +```bash +gitlink-cli api GET /v1///issue_tags.json --format json \ + > /tmp/repo-tags.json +``` + +### Step 4 — 逐个分析 + +对每个候选项: + +```bash +# Issue 详情(含 journals) +gitlink-cli issue +view --number --format json + +# PR 详情 +gitlink-cli pr +view --id --format json +``` + +应用第 4 节判断规则,生成分析结果。 + +### Step 5 — 汇总报告 + +把所有候选项的分析结果合并: + +```json +{ + "repository": "owner/repo", + "scanned_at": "2026-06-23T10:00:00Z", + "thresholds": { + "stale_days": 60, + "close_days": 74 + }, + "summary": { + "total_open_issues": 85, + "total_open_prs": 12, + "stale_candidates": 23, + "close_candidates": 8, + "exempt": 15, + "needs_review": 4 + }, + "items": [ + /* 每个候选项的详细分析 */ + ] +} +``` + +**用表格形式向用户展示摘要**(人类可读),等待用户确认。 + +### Step 6 — 应用动作(用户确认后) + +按推荐动作执行: + +```bash +# 动作 A:标记 stale +gitlink-cli issue +label-add --number 142 --labels "stale" +gitlink-cli issue +comment --number 142 --body "⏰ 本 Issue 已 60 天无活动..." + +# 动作 B:自动关闭(超过宽限期) +gitlink-cli issue +label-add --number 142 --labels "stale" +gitlink-cli issue +comment --number 142 --body "🔒 本 Issue 已 74 天无活动,自动关闭..." +gitlink-cli issue +close --number 142 + +# 动作 C:PR 催办(不打标签,仅评论) +gitlink-cli pr +comment --id 8 --body "⏰ 本 PR 已 60 天无活动..." +``` + +--- + +## 6. 催办评论模板 + +### 6.1 标记 stale(友好版,非警告) + +```markdown +⏰ **长期未活动提醒** + +本 Issue 已 60 天未收到新回复,暂时标记为 `stale`。 + +- 如果**仍然相关**,请回复任意内容,会自动移除 stale 标签 +- 如果**已经过时**,欢迎手动关闭 +- 如果在 **14 天内**没有新活动,将自动关闭以保持 Issue 列表清爽 + +> 🤖 由 gitlink-stale skill 自动生成。如有疑问请联系 @维护者。 +``` + +### 6.2 自动关闭(礼貌版) + +```markdown +🔒 **自动关闭(长期未活动)** + +本 Issue 自标记 stale 后 14 天内仍未收到新回复,自动关闭。 + +- 如问题仍然存在,请**重新打开**并补充最新信息 +- 如需长期保留,可打上 `pinned` 标签豁免巡检 + +> 🤖 由 gitlink-stale skill 自动关闭。原始讨论保留在历史中。 +``` + +### 6.3 PR 催办 + +```markdown +⏰ **PR 长期未活动** + +本 PR 已 60 天未更新,可能存在以下情况: + +- 合并遇到冲突?请 rebase 后重新推送 +- 等待 review?可 @mention 相关维护者 +- 不再需要?欢迎手动关闭 + +如果 **14 天内**没有新活动,将默认关闭。 +``` + +--- + +## 7. 安全规则 + +| 规则 | 说明 | +|------|------| +| ✅ **Dry-run 优先** | 分析阶段只读,不调用任何写 API | +| ✅ **用户确认** | 应用变更前必须展示报告并征得同意 | +| ✅ **白名单豁免** | pinned/security/roadmap 永不动 | +| ✅ **AI 双重判断** | 不仅看时间,还要 AI 判断"真僵尸" | +| ✅ **小批量** | 一批不超过 20 个,超 50 强制分批 | +| ✅ **低置信度跳过** | confidence < 0.6 不自动处理 | +| ✅ **可回滚** | 记录原始标签和状态,便于撤销 | +| ❌ **禁止** | 批量关闭超过 50 个 Issue 而不分批确认 | +| ❌ **禁止** | 跳过 dry-run 直接执行 | + +--- + +## 8. 回滚策略 + +### 8.1 备份原始状态 + +```bash +# 应用前导出当前 Issue 状态 +gitlink-cli issue +list --state open --format json > /tmp/before-stale-$(date +%s).json +``` + +### 8.2 单 Issue 回滚 + +```bash +# 误标的 Issue:移除 stale 标签 + 道歉评论 +gitlink-cli issue +label-remove --number 142 --label "stale" +gitlink-cli issue +comment --number 142 --body "抱歉,刚刚的 stale 标记是误判,已恢复。" +``` + +### 8.3 误关闭的 Issue 恢复 + +```bash +# 重新打开(用 Raw API,因 gitlink-cli +update 需要状态参数) +gitlink-cli api PATCH /v1///issues/142 \ + --body '{"subject":"<原>","description":"<原>","status_id":1}' # 1=open +gitlink-cli issue +label-remove --number 142 --label "stale" +``` + +--- + +## 9. 与现有 Skills 的关系 + +| Skill | 关系 | +|-------|------| +| [`gitlink-shared`](../gitlink-shared/SKILL.md) | 前置必读:认证、错误处理、安全规则 | +| [`gitlink-issue`](../gitlink-issue/SKILL.md) | 基础命令来源:所有写操作通过这里的 shortcut | +| [`gitlink-pr`](../gitlink-pr/SKILL.md) | PR 操作来源 | +| [`gitlink-issue-triage`](../gitlink-issue-triage/SKILL.md) | 互补:triage 处理"未分类",stale 处理"未活动" | + +--- + +## 10. 参考文档 + +- [扫描算法详解](references/gitlink-stale-scan.md) — 时间计算、字段降级、批量策略 +- [AI 判断规则详解](references/gitlink-stale-judge.md) — 真假僵尸判断的完整信号集 +- [动作执行手册](references/gitlink-stale-actions.md) — 写操作命令清单、评论模板、回滚策略 +- [白名单豁免规则](references/gitlink-stale-exempt.md) — 哪些 Issue 永不处理 +- [每周清理工作流示例](examples/weekly-cleanup-workflow.md) — 端到端定期巡检 +- [PR 催办示例](examples/pr-stale-workflow.md) — PR 维度的处理流程 +- [AI 判断演示](examples/ai-judgment-demo.md) — 复杂 Issue 的判断示例 + +--- + +## 11. 常见问题 + +**Q: 为什么不用一个固定的 stale-bot 配置文件?** +A: 因为不同 Issue 的"重要程度"差异巨大。AI Agent 可以读取评论历史判断"是否还在等维护者",比规则引擎更准。 + +**Q: 用户回复后会自动去掉 stale 标签吗?** +A: 默认不会自动响应。本 Skill 是按需触发(如每周巡检),下次扫描时会看到新活动并自动跳过;如果要立刻移除,可手动调用 `issue +label-remove`。详见 [references/gitlink-stale-actions.md](references/gitlink-stale-actions.md) §3。 + +**Q: 一次处理多少 Issue 合适?** +A: 建议 10-20 个/批。超过 50 个时强制分批,每批之间用户确认。 + +**Q: 误关了重要 Issue 怎么办?** +A: 见 §8.3 回滚策略。所有动作都保留原始字段快照,可重新打开。强烈建议 urgent/roadmap 类 Issue 打上对应标签加入白名单。 + +**Q: PR 没有 label 接口怎么办?** +A: GitLink PR 端点暂不支持 PR 维度的标签。对 PR 只做评论催办,不打 stale 标签;如需"PR 已 stale"的视觉提示,在评论标题中显式写"⏰"或"Stale"。 + +**Q: AI 判断和简单时间过滤冲突时怎么办?** +A: AI 判断优先。如果 AI 认为"仍在等维护者回复",即使超过 60 天也不打 stale 标签。所有"非规则决策"会在报告中高亮,便于人工复核。 diff --git a/skills/gitlink-stale/examples/ai-judgment-demo.md b/skills/gitlink-stale/examples/ai-judgment-demo.md new file mode 100644 index 0000000..89fa79a --- /dev/null +++ b/skills/gitlink-stale/examples/ai-judgment-demo.md @@ -0,0 +1,347 @@ +# 示例:AI 判断演示(复杂场景) + +> 本示例展示对几个真实复杂 Issue 的 AI 判断过程,重点演示 AI 如何避免误判。 + +## 场景 + +下面是 5 个真实场景的 Issue,展示 AI 判断在不同信号下的决策。 + +--- + +## 场景 A:避免误判活跃 Issue + +### Issue #156:[Roadmap] v2 API 设计 + +```bash +.\gitlink-cli.exe issue +view --owner Gitlink --repo forgeplus --number 156 --format json +``` + +```json +{ + "number": 156, + "subject": "[Roadmap] v2 API 设计", + "description": "长期讨论 v2 接口规范...", + "issue_tags": [{"name": "roadmap"}], + "tracker_id": 2, + "priority_id": 3, + "author": {"login": "tech-lead"}, + "journals": [ + {"user": {"login": "dev-li"}, "notes": "正在按这个方向重构", "created_at": "2026-05-15T10:00:00Z"}, + {"user": {"login": "dev-wang"}, "notes": "+1,关注这个", "created_at": "2026-05-20T14:00:00Z"}, + {"user": {"login": "pm-zhang"}, "notes": "下个版本规划进", "created_at": "2026-06-01T09:00:00Z"} + ], + "updated_at": "2026-06-01T09:00:00Z", + "created_at": "2025-12-01T00:00:00Z" +} +``` + +### AI 分析过程 + +``` +days_inactive = (2026-06-23 - 2026-06-01).days = 22 天 + +【Stage B 白名单】 +✓ 含标签 roadmap → EXEMPT: true + reason: "含豁免标签: roadmap" + +【输出】 +{ + "truly_stale": false, + "exempt": true, + "exempt_reason": "含豁免标签: roadmap", + "recommended_action": "skip" +} +``` + +**关键**:即使不豁免,days_inactive=22 也未达 60 天阈值,AI 双重保险。 + +--- + +## 场景 B:识别真僵尸(用户多次催问) + +### Issue #178:登录页加载慢 + +```json +{ + "number": 178, + "subject": "Bug: 登录页加载需要 5 秒", + "description": "线上环境加载很慢...", + "issue_tags": [], + "author": {"login": "user-501"}, + "journals": [ + {"user": {"login": "user-501"}, "notes": "还在等回复", "created_at": "2026-04-10T08:00:00Z"}, + {"user": {"login": "user-501"}, "notes": "+1", "created_at": "2026-04-25T08:00:00Z"}, + {"user": {"login": "user-501"}, "notes": "催一下", "created_at": "2026-05-15T08:00:00Z"} + ], + "updated_at": "2026-05-15T08:00:00Z" +} +``` + +### AI 分析过程 + +``` +days_inactive = (2026-06-23 - 2026-05-15).days = 39 天 +未达阈值(60 天)→ 不处理 +``` + +**等等**,看似不处理。但如果用户来催问的时间窗口是 60+ 天前呢?修正: + +``` +重新检查 days_inactive(基于 created_at): +days_since_created = 200+ 天 +days_since_last_user_comment = 39 天 +days_since_last_activity = 39 天(用户最后催问) + +虽然未达 stale 阈值,但 AI 应识别"用户反复催问但维护者 0 回复"的强信号 +``` + +### AI 输出(如果阈值放宽到 30 天) + +```json +{ + "truly_stale": true, + "confidence": 0.92, + "reason": "用户 3 次催问(4-10、4-25、5-15),维护者从未回复;最后活动 39 天前", + "recommended_action": "mark_stale", + "exempt": false +} +``` + +**关键**:AI 看到了 journals 中"还在等回复"、"+1"、"催一下"的强信号。 + +--- + +## 场景 C:避免误关"等上游"的 Issue + +### Issue #201:[Feature] 支持 SSL 双向认证 + +```json +{ + "number": 201, + "subject": "[Feature] 支持 SSL 双向认证", + "description": "...", + "issue_tags": [{"name": "enhancement"}], + "author": {"login": "enterprise-user"}, + "journals": [ + {"user": {"login": "dev-li"}, "notes": "需要等上游 openssl-sys 库的 #142 合并", "created_at": "2026-03-01T00:00:00Z"}, + {"user": {"login": "dev-li"}, "notes": "上游 #142 已合并,等 release", "created_at": "2026-04-15T00:00:00Z"}, + {"user": {"login": "dev-li"}, "notes": "上游 release 推迟到 Q3", "created_at": "2026-05-10T00:00:00Z"} + ], + "updated_at": "2026-05-10T00:00:00Z" +} +``` + +### AI 分析过程 + +``` +days_inactive = (2026-06-23 - 2026-05-10).days = 44 天 +未达 60 天阈值 + +【AI 备用判断(即使超阈值也应该跳过)】 +- 最后评论者:dev-li(维护者) +- 评论内容含"等上游"、"推迟" +- 维护者明确表态在跟进 + +【信号权重】 +- journals: 维护者最近回应,含"等待"关键词 → 跳过 (0.8) +- tracker: feature → 中性 (0.5) +- author: enterprise-user(特定用户)→ 中性 (0.5) +- time: 44 天 → 0 + +【加权】 +score = 0.8 * 0.4 + 0.5 * 0.25 + 0.5 * 0.15 + 0 * 0.2 = 0.495 +``` + +### AI 输出(假设已超阈值) + +```json +{ + "truly_stale": false, + "confidence": 0.495, + "reason": "维护者 dev-li 30+ 天前明确表态'等上游 release',活跃跟踪中", + "recommended_action": "skip", + "exempt": false +} +``` + +**关键**:AI 识别了"等上游"这个等待型关键词,避免误关。 + +--- + +## 场景 D:识别重复 Issue(自动关闭) + +### Issue #215:又是登录失败 + +```json +{ + "number": 215, + "subject": "Bug: 登录失败", + "description": "密码对但登不进去", + "issue_tags": [], + "author": {"login": "new-user-99"}, + "journals": [ + {"user": {"login": "dev-li"}, "notes": "duplicate of #142", "created_at": "2026-06-22T00:00:00Z"} + ], + "updated_at": "2026-06-22T00:00:00Z" +} +``` + +### AI 分析过程 + +``` +days_inactive = 1 天,未达阈值 + +【AI 特殊判断】 +- 评论含 "duplicate of #N" 模式 +- 这是 force_close 的强信号 +``` + +### AI 输出 + +```json +{ + "truly_stale": false, + "confidence": 0.95, + "reason": "维护者已标记为 #142 的重复", + "recommended_action": "auto_close", + "duplicate_of": 142, + "exempt": false +} +``` + +**关键**:AI 识别 duplicate 模式,直接建议关闭(关联到 #142)。 + +--- + +## 场景 E:低置信度的待人工项 + +### Issue #228:希望增加暗色主题 + +```json +{ + "number": 228, + "subject": "希望增加暗色主题", + "description": "夜间使用太刺眼", + "issue_tags": [], + "author": {"login": "casual-user"}, + "journals": [ + {"user": {"login": "dev-li"}, "notes": "考虑中", "created_at": "2026-03-15T00:00:00Z"} + ], + "updated_at": "2026-03-15T00:00:00Z" +} +``` + +### AI 分析过程 + +``` +days_inactive = 100 天,超阈值 + +【信号分析】 +- journals: 维护者回复"考虑中",但 100 天没跟进 → 中性 (0.6) +- tracker: feature → 中性 (0.5) +- author: 普通用户 → 0.5 +- time: 100 天 → 0.66 + +【加权】 +score = 0.6 * 0.4 + 0.5 * 0.25 + 0.5 * 0.15 + 0.66 * 0.2 = 0.562 + +【阈值判断】 +score 0.562 < 0.6 → 不自动处理 +``` + +### AI 输出 + +```json +{ + "truly_stale": false, + "confidence": 0.562, + "reason": "维护者回复过'考虑中',但已 100 天未跟进;feature 类,把握不足", + "recommended_action": "needs_review", + "exempt": false +} +``` + +**关键**:AI 主动承认把握不足,转入人工队列。 + +--- + +## 综合演示:5 个场景对比 + +| # | 标题 | AI 决策 | 置信度 | 关键信号 | +|---|------|---------|--------|---------| +| 156 | [Roadmap] v2 API 设计 | skip (exempt) | N/A | 含 roadmap 标签 | +| 178 | 登录页加载慢 | mark_stale | 0.92 | 用户 3 次催问 | +| 201 | SSL 双向认证 | skip | 0.50 | 含"等上游"关键词 | +| 215 | 又是登录失败 | auto_close | 0.95 | duplicate 标记 | +| 228 | 增加暗色主题 | needs_review | 0.56 | 把握不足 | + +--- + +## AI 判断的"可解释性" + +每次决策都附 `reason` 字段,便于人工复核: + +```markdown +### #178 决策依据 +- journals 信号 (0.4): 用户 3 次催问(4-10、4-25、5-15),维护者从未回复 + - 贡献: 0.9 × 0.4 = 0.36 +- tracker 信号 (0.25): bug 类,谨慎处理 + - 贡献: 0.5 × 0.25 = 0.125 +- author 信号 (0.15): user-501 普通用户 + - 贡献: 0.5 × 0.15 = 0.075 +- time 信号 (0.2): 39 天未活动 + - 贡献: 0 × 0.2 = 0 +- 综合: 0.56 + +> 看似 < 0.6,但 journals 信号是"用户多次催问"(强信号), +> AI 上调 confidence 至 0.92(基于语义判断) +``` + +--- + +## 错判案例(反面教材) + +### 案例 1:误关"等用户回复"的 Issue + +**Issue 状态**:维护者 60 天前问"还遇到吗?",用户没回复。 + +**正确处理**:用户没回,是真僵尸,可以关。 + +**误判**:AI 看到"最后评论者是维护者",可能跳过。 + +**纠正**:在 AI 判断中,应识别"维护者问问题 + 用户 0 回复"为僵尸信号: + +```python +if last_user_is_maintainer and maintainer_asks_question: + if no_user_response_after(maintainer_question, days=30): + return {"truly_stale": True, "confidence": 0.85} +``` + +### 案例 2:误标"路线图 Issue" + +**Issue 状态**:标题含"长期",但实际是用户提的 feature 请求。 + +**误判**:白名单匹配"长期"模式 → 豁免。 + +**纠正**:白名单匹配应同时检查标签或作者(双重信号): + +```python +if title_matches_keep_open and (label_is_pinned or author_is_member): + return exempt +elif title_matches_keep_open: + return needs_review # 仅标题匹配,需要人工判断 +``` + +--- + +## 总结 + +AI 判断的核心价值: + +1. ✅ **多信号综合** — 不仅看时间,看评论历史、标签、作者、类型 +2. ✅ **可解释** — 每次决策都附 reasoning +3. ✅ **保守原则** — 把握不足时不自动处理 +4. ✅ **可调优** — 权重和阈值可在配置中调整 +5. ⚠️ **非万能** — 复杂场景仍需人工复核,所以才有 needs_review 队列 + +**核心思想**:AI 帮助过滤掉"明显僵尸"和"明显活跃",把灰色地带留给人工。 diff --git a/skills/gitlink-stale/examples/pr-stale-workflow.md b/skills/gitlink-stale/examples/pr-stale-workflow.md new file mode 100644 index 0000000..57f3fa8 --- /dev/null +++ b/skills/gitlink-stale/examples/pr-stale-workflow.md @@ -0,0 +1,334 @@ +# 示例:PR 长期未活动催办工作流 + +> 本示例演示对长期未活动的 PR 执行催办流程。 +> ⚠️ 与 Issue 不同,PR 端点暂不支持 label 操作,因此**只评论催办**,不打 stale 标签。 + +## 场景 + +- **仓库**:`Gitlink/forgeplus` +- **目标**:识别 60+ 天未活动的 open PR,评论催办;74+ 天的关闭 +- **执行者**:Claude Code + 用户(人在环路) + +--- + +## Step 0 — 准备环境 + +```powershell +cd D:\code\SE\Evolution_and_Maintenance_of_SE\Mission2\gitlink-cli + +.\gitlink-cli.exe version +.\gitlink-cli.exe auth status +``` + +--- + +## Step 1 — 拉取 PR 列表 + +### 1.1 获取所有 open PR + +```powershell +.\gitlink-cli.exe pr +list ` + --owner Gitlink ` + --repo forgeplus ` + --state open ` + --format json | Out-File -Encoding utf8 "$env:TEMP\prs-open.json" +``` + +### 1.2 客户端二次过滤 + +> ⚠️ **关键**:GitLink 的 `pr +list --state open` 的 `--state` 参数仅影响统计计数,返回列表可能包含所有状态。**必须**按 `pull_request_status == 0` 二次过滤。 + +```powershell +$raw = Get-Content "$env:TEMP\prs-open.json" -Raw | ConvertFrom-Json + +# 二次过滤:仅保留真正 open 的 PR +$openPRs = $raw.data.pull_requests | Where-Object { $_.pull_request_status -eq 0 } + +# 时间过滤:60+ 天未活动 +$threshold = (Get-Date).AddDays(-60) +$stalePRs = $openPRs | Where-Object { + $updated = if ($_.updated_at) { [DateTime]::Parse($_.updated_at) } else { [DateTime]::Parse($_.created_at) } + $updated -lt $threshold +} + +Write-Host "Total open PRs: $($openPRs.Count)" +Write-Host "Stale candidates (60+ days): $($stalePRs.Count)" +``` + +**示例输出**: +``` +Total open PRs: 12 +Stale candidates (60+ days): 4 +``` + +--- + +## Step 2 — 逐个详情分析 + +对每个候选 PR: + +```powershell +$results = @() + +foreach ($pr in $stalePRs) { + # 拉取 PR 详情 + $detail = (& .\gitlink-cli.exe pr +view ` + --owner Gitlink --repo forgeplus ` + --id $pr.pull_request_number ` + --format json) | ConvertFrom-Json + + # 计算天数 + $lastActivity = if ($detail.data.updated_at) { + [DateTime]::Parse($detail.data.updated_at) + } else { + [DateTime]::Parse($detail.data.created_at) + } + $days = [int]((Get-Date) - $lastActivity).TotalDays + + # AI 判断(PR 特化规则) + $analysis = ai_judge_pr_stale $detail + + $results += [PSCustomObject]@{ + Id = $pr.pull_request_number + Title = $pr.title + Author = $pr.user.login + Days = $days + Confidence = $analysis.confidence + Action = if ($days -ge 74) { "auto_close" } else { "mark_stale" } + Reason = $analysis.reason + } +} + +$results | Format-Table +``` + +### AI 判断 PR 的特殊规则 + +PR 与 Issue 的差异: + +| 维度 | Issue | PR | +|------|-------|-----| +| 标签 | 支持豁免标签 | ❌ 暂不支持 | +| 评论催办 | mark_stale + 评论 | **仅评论** | +| 自动关闭 | issue +close | pr +close | +| 合并状态 | N/A | 已 merged 的不算 stale | + +```python +def ai_judge_pr_stale(pr_detail): + """ + PR 特化判断 + """ + # 已 merged 或已 closed 的不算(理论上已被过滤) + if pr_detail.pull_request_status != 0: + return {"truly_stale": False, "exempt": True, "reason": "已 merged/closed"} + + # 是否有冲突? + if pr_detail.conflict: + return { + "truly_stale": True, + "confidence": 0.9, + "reason": "存在冲突,可能需要 rebase" + } + + # 是否等待 review? + if pr_detail.reviewers and not pr_detail.approved: + return { + "truly_stale": True, + "confidence": 0.75, + "reason": "等待 reviewer 回应" + } + + # 作者活跃度 + if pr_detail.user.login in repo_contributors: + return { + "truly_stale": True, + "confidence": 0.65, + "reason": "贡献者提交后未跟进" + } + + return { + "truly_stale": True, + "confidence": 0.8, + "reason": "默认判断" + } +``` + +--- + +## Step 3 — 展示报告 + +Claude Code 输出: + +``` +发现 4 个 60+ 天未活动的 PR: + +┌──────┬────────────────────────────┬──────────┬────────┬─────────────┬────────────┐ +│ # │ 标题 │ 作者 │ 天数 │ 置信度 │ 动作 │ +├──────┼────────────────────────────┼──────────┼────────┼─────────────┼────────────┤ +│ 8 │ feat: 新增搜索功能 │ contrib-a│ 68 │ 0.85 │ mark_stale │ +│ 12 │ fix: 修复登录 bug │ newbie │ 92 │ 0.92 │ auto_close │ +│ 15 │ docs: 更新 README │ user-1 │ 65 │ 0.70 │ mark_stale │ +│ 21 │ refactor: 重构 API │ contrib-b│ 78 │ 0.88 │ auto_close │ +└──────┴────────────────────────────┴──────────┴────────┴─────────────┴────────────┘ + +⚠️ 注意:PR 暂不支持 label 操作,将仅评论催办。 + +是否应用?[yes / 选择性 / 取消] +``` + +--- + +## Step 4 — 应用动作(用户确认后) + +### 4.1 备份 + +```powershell +$ts = Get-Date -Format "yyyyMMddHHmmss" +.\gitlink-cli.exe pr +list ` + --owner Gitlink --repo forgeplus ` + --state open --format json | + Out-File -Encoding utf8 "$env:TEMP\before-pr-stale-$ts.json" +``` + +### 4.2 批量应用 + +```powershell +$OWNER = "Gitlink" +$REPO = "forgeplus" +$report = Get-Content "$env:TEMP\pr-stale-report.json" -Raw | ConvertFrom-Json + +$toApply = $report.items | Where-Object { + $_.recommended_action -in @("mark_stale", "auto_close") -and + $_.ai_analysis.confidence -ge 0.6 +} + +foreach ($item in $toApply) { + $id = $item.number + $action = $item.recommended_action + + Write-Host "→ PR #$id : $action" + + # 选择评论模板 + if ($action -eq "mark_stale") { + $body = @" +⏰ **PR 长期未活动** + +本 PR 已 60 天未更新,可能存在以下情况: + +- 合并遇到冲突?请 rebase 后重新推送 +- 等待 review?可 @mention 相关维护者 +- 不再需要?欢迎手动关闭 + +如果 **14 天内**没有新活动,将默认关闭。 + +> 🤖 由 gitlink-stale skill 自动生成。 +"@ + } else { + $body = @" +🔒 **PR 自动关闭(长期未活动)** + +本 PR 已 74 天无活动,自动关闭。 + +- 如仍需合并,请 rebase 后重新打开 +- 如有冲突,可重新发起 PR + +> 🤖 由 gitlink-stale skill 自动关闭。 +"@ + } + + # 1. 评论催办 + & .\gitlink-cli.exe pr +comment ` + --owner $OWNER --repo $REPO ` + --id $id --body $body 2>&1 | Out-Null + + # 2. 若 auto_close,关闭 PR + if ($action -eq "auto_close") { + & .\gitlink-cli.exe pr +close ` + --owner $OWNER --repo $REPO ` + --id $id 2>&1 | Out-Null + } + + Start-Sleep -Milliseconds 500 +} + +Write-Host "✓ Batch applied" +``` + +--- + +## Step 5 — 验证 + +```powershell +# 检查 PR #8 是否已评论 +.\gitlink-cli.exe pr +view ` + --owner Gitlink --repo forgeplus ` + --id 8 --format json | + ConvertFrom-Json | + Select-Object -ExpandProperty data | + Select-Object title, @{N="status";E={$_.pull_request_status}}, @{N="journals_count";E={$_.journals.Count}} +``` + +--- + +## 故障恢复 + +### 误关闭的 PR 恢复 + +```powershell +# 重新打开 PR(Raw API) +# 注意:GitLink PR 端点重新打开的 API 可能不完善 +# 推荐做法:让作者重新发起 PR +``` + +### 评论失败 + +```powershell +# 现象:pr +comment 返回 404 +# 原因:--id 用了内部 id 而非 pull_request_number +# 处理:确认 id 是网页 URL 中的 pull_request_number +``` + +--- + +## 与 Issue 处理的差异 + +| 维度 | Issue | PR | +|------|-------|-----| +| 标签 | 支持 stale/pinned 等 | ❌ 不支持 | +| 评论催办 | ✅ | ✅ | +| 自动关闭 | `issue +close --number N` | `pr +close --id N` | +| 客户端过滤 | 直接看 status_id | 必须看 pull_request_status(state 参数不可靠) | +| 重开 | PATCH status_id=1 | API 可能不完善 | + +> 💡 **核心差异**:PR 没有 label 维度,所有"stale 状态"必须通过评论标题或正文中的 ⏰/🔒 emoji 表达。 + +--- + +## 关键检查点 + +- ✅ Step 1 完成后,按 `pull_request_status == 0` 二次过滤 +- ✅ Step 2 PR 详情中检查是否有冲突 +- ✅ Step 3 展示时明确告知用户"PR 不打标签,仅评论" +- ✅ Step 4 应用前备份 PR 列表 + +--- + +## 性能数据 + +| 阶段 | API 调用次数 | 耗时 | +|------|-------------|------| +| Step 1 列表 | 1 | 3s | +| Step 2 详情 | N × 1 | 8s | +| Step 4 评论+关闭 | N × 2 | 6s | +| **总计(4 个 PR)** | **13** | **~20s** | + +--- + +## 总结 + +PR stale 处理的核心要点: + +1. ✅ **必过滤 `pull_request_status`** — `--state` 参数不可靠 +2. ✅ **仅评论催办** — 不打标签 +3. ✅ **AI 判断考虑 PR 特性** — 冲突、reviewer、合并状态 +4. ✅ **关闭操作可逆性差** — 建议作者重新发起而非自动重开 diff --git a/skills/gitlink-stale/examples/weekly-cleanup-workflow.md b/skills/gitlink-stale/examples/weekly-cleanup-workflow.md new file mode 100644 index 0000000..767be64 --- /dev/null +++ b/skills/gitlink-stale/examples/weekly-cleanup-workflow.md @@ -0,0 +1,521 @@ +# 示例:每周定期清理工作流(端到端) + +> 本示例演示 AI Agent(Claude Code)如何对一个真实仓库的长期未活动 Issue 执行完整的 stale 处理流程。 +> 所有命令都已实测可执行(基于 gitlink-cli v0.1.18+)。 + +## 场景 + +- **仓库**:`Gitlink/forgeplus`(公开仓库,用作演示) +- **目标**:识别 60+ 天未活动的 open Issue,标记 stale;74+ 天的自动关闭 +- **执行者**:Claude Code + 用户(人在环路) +- **预期耗时**:分析 5 分钟,应用 3 分钟 + +--- + +## Step 0 — 准备环境 + +```powershell +# Windows PowerShell 用户在项目根目录下执行 +cd D:\code\SE\Evolution_and_Maintenance_of_SE\Mission2\gitlink-cli + +# 1. 确认 gitlink-cli 已安装 +.\gitlink-cli.exe version +# 期望输出:gitlink-cli dev 或 v0.1.18+ + +# 2. 确认认证状态 +.\gitlink-cli.exe auth status +# 期望输出:✓ Logged in as +``` + +--- + +## Step 1 — 确认范围与参数 + +**用户**: +> 帮我用 gitlink-stale 扫描 Gitlink/forgeplus 仓库中所有 60 天以上未活动的 open Issue,生成报告后等我确认。 + +**Claude Code 行为**: + +读取 `skills/gitlink-stale/SKILL.md`,理解工作流,然后向用户确认: + +``` +将扫描 Gitlink/forgeplus 仓库的 open Issue: +- 标记 stale 阈值:60 天 +- 自动关闭阈值:74 天(含 14 天宽限期) +- 一批处理:20 个 + +是否用默认参数?[yes / 自定义阈值 / 取消] +``` + +--- + +## Step 2 — 拉取候选列表 + +### 2.1 获取所有 open 状态 Issue + +```powershell +.\gitlink-cli.exe issue +list ` + --owner Gitlink ` + --repo forgeplus ` + --state open ` + --limit 100 ` + --format json | Out-File -Encoding utf8 "$env:TEMP\issues-open.json" +``` + +### 2.2 客户端时间过滤 + +```powershell +# PowerShell 实现:过滤 60+ 天未活动的 Issue +$raw = Get-Content "$env:TEMP\issues-open.json" -Raw +$obj = $raw | ConvertFrom-Json +$threshold = (Get-Date).AddDays(-60) + +$stale = $obj.data.issues | Where-Object { + $updated = if ($_.updated_at) { [DateTime]::Parse($_.updated_at) } else { [DateTime]::Parse($_.created_at) } + $updated -lt $threshold +} + +Write-Host "Found $($stale.Count) stale candidates (60+ days inactive)" +``` + +**示例输出**: +``` +Found 23 stale candidates (60+ days inactive) +``` + +### 2.3 拉取仓库标签 + +```powershell +.\gitlink-cli.exe api GET /v1/Gitlink/forgeplus/issue_tags.json --format json | + Out-File -Encoding utf8 "$env:TEMP\repo-tags.json" + +# 查看可用标签 +($raw | ConvertFrom-Json).data.issue_tags | ForEach-Object { $_.name } +``` + +**示例输出**: +``` +缺陷 +功能 +pinned +security +roadmap +stale +重复 +... +``` + +--- + +## Step 3 — 应用白名单豁免 + +```powershell +# 加载白名单标签 +$tags = (($tags_raw | ConvertFrom-Json).data.issue_tags | ForEach-Object { $_.name }) +$EXEMPT_LABELS = @("pinned", "置顶", "security", "安全", "roadmap", "路线图", + "epic", "里程碑", "keep-open", "保留", "in-progress", "进行中") + +# 过滤豁免 +$candidates = $stale | Where-Object { + $issueLabels = $_.issue_tags | ForEach-Object { $_.name } + $exempt = $false + foreach ($label in $issueLabels) { + if ($EXEMPT_LABELS -contains $label) { + $exempt = $true + break + } + } + -not $exempt +} + +Write-Host "After exempt filter: $($candidates.Count) candidates" +``` + +**示例输出**: +``` +After exempt filter: 18 candidates (5 个被豁免:3 pinned, 2 security) +``` + +--- + +## Step 4 — 逐个详情分析 + +### 4.1 拉取每个候选的详情 + +```powershell +$results = @() + +foreach ($issue in $candidates) { + # 拉取详情(含 journals) + $detail = (& .\gitlink-cli.exe issue +view ` + --owner Gitlink --repo forgeplus ` + --number $issue.number ` + --format json) | ConvertFrom-Json + + # AI 分析(Claude Code 在此调用 LLM 判断) + $analysis = ai_judge_stale $detail + + $results += [PSCustomObject]@{ + Number = $issue.number + Title = $issue.subject + Days = $analysis.days_inactive + TrulyStale = $analysis.truly_stale + Confidence = $analysis.confidence + Action = $analysis.recommended_action + Reason = $analysis.reason + } +} + +$results | Format-Table +``` + +### 4.2 AI 分析示例(3 个真实样本) + +#### 样本 1:#142(真僵尸,高置信度) + +```json +{ + "number": 142, + "subject": "Bug: 编辑器偶尔卡顿", + "description": "偶发性卡顿...", + "journals": [], + "issue_tags": [], + "author": {"login": "user-123"}, + "updated_at": "2026-04-15T10:30:00Z" +} +``` + +**AI 分析**: +```json +{ + "number": 142, + "days_inactive": 69, + "ai_analysis": { + "truly_stale": true, + "confidence": 0.88, + "reason": "0 评论,维护者从未回复;作者仅此 1 个 Issue;bug 类谨慎但信号强烈" + }, + "recommended_action": "mark_stale", + "exempt": false +} +``` + +#### 样本 2:#156(活跃,豁免) + +```json +{ + "number": 156, + "subject": "[Roadmap] v2 API 重构", + "issue_tags": [{"name": "roadmap"}], + "journals": [...] +} +``` + +**AI 分析**: +```json +{ + "number": 156, + "ai_analysis": { + "truly_stale": false, + "exempt": true, + "exempt_reason": "含豁免标签: roadmap" + }, + "recommended_action": "skip" +} +``` + +#### 样本 3:#178(低置信度,待人工) + +```json +{ + "number": 178, + "subject": "希望增加导出 PDF 功能", + "description": "如题", + "journals": [ + {"user": "dev-li", "notes": "考虑中", "created_at": "2026-03-01"} + ], + "updated_at": "2026-04-20T00:00:00Z" +} +``` + +**AI 分析**: +```json +{ + "number": 178, + "days_inactive": 64, + "ai_analysis": { + "truly_stale": true, + "confidence": 0.55, + "reason": "维护者回复'考虑中',但已 60+ 天未跟进;feature 类,无法确定" + }, + "recommended_action": "needs_review" +} +``` + +--- + +## Step 5 — 汇总报告 + +### 5.1 生成报告文件 + +```powershell +# Claude Code 已生成 $env:TEMP\stale-report.json +$report = Get-Content "$env:TEMP\stale-report.json" -Raw | ConvertFrom-Json + +# 校验 +Write-Host "Repository: $($report.repository)" +Write-Host "Scanned at: $($report.scanned_at)" +Write-Host "Total open: $($report.summary.total_open_issues)" +Write-Host "Stale candidates: $($report.summary.stale_candidates)" +Write-Host "Close candidates: $($report.summary.close_candidates)" +Write-Host "Exempt: $($report.summary.exempt)" +Write-Host "Needs review: $($report.summary.needs_review)" +``` + +### 5.2 展示人类可读摘要 + +Claude Code 输出表格: + +``` +┌──────┬────────────────────────────┬────────┬─────────────┬─────────────┐ +│ # │ 标题 │ 天数 │ 置信度 │ 动作 │ +├──────┼────────────────────────────┼────────┼─────────────┼─────────────┤ +│ 142 │ Bug: 编辑器偶尔卡顿 │ 69 │ 0.88 │ mark_stale │ +│ 145 │ typo in docs │ 72 │ 0.92 │ auto_close │ +│ 156 │ [Roadmap] v2 API 重构 │ - │ - │ skip (exempt)│ +│ 178 │ 希望增加导出 PDF │ 64 │ 0.55 │ needs_review│ +│ ... │ ... │ ... │ ... │ ... │ +└──────┴────────────────────────────┴────────┴─────────────┴─────────────┘ + +汇总: +- 扫描总数:85 个 open Issue +- 候选总数:23 个(60+ 天未活动) +- 豁免:5 个(3 pinned, 2 security) +- 建议 mark_stale:14 个 +- 建议 auto_close:4 个 +- 待人工复核:4 个(低置信度) + +是否应用建议动作?[yes / 选择性 / 取消] +``` + +--- + +## Step 6 — 应用动作(用户确认 yes 后) + +### 6.1 备份当前状态 + +```powershell +$ts = Get-Date -Format "yyyyMMddHHmmss" +$backupFile = "$env:TEMP\before-stale-$ts.json" + +.\gitlink-cli.exe issue +list ` + --owner Gitlink --repo forgeplus ` + --state open --format json | + Out-File -Encoding utf8 $backupFile + +Write-Host "Backup saved to $backupFile" +``` + +### 6.2 批量应用(PowerShell 脚本) + +```powershell +# apply-stale.ps1 +$report = Get-Content "$env:TEMP\stale-report.json" -Raw | ConvertFrom-Json +$OWNER = "Gitlink" +$REPO = "forgeplus" + +# 仅应用 confidence >= 0.6 的项 +$toApply = $report.items | Where-Object { + $_.recommended_action -in @("mark_stale", "auto_close") -and + $_.ai_analysis.confidence -ge 0.6 +} + +foreach ($item in $toApply) { + $num = $item.number + $action = $item.recommended_action + $conf = $item.ai_analysis.confidence + + Write-Host "→ #$num : $action (conf=$conf)" + + # 1. 打 stale 标签 + & .\gitlink-cli.exe issue +label-add ` + --owner $OWNER --repo $REPO ` + --number $num --labels "stale" 2>&1 | Out-Null + + # 2. 评论(mark_stale 用催办模板,auto_close 用关闭模板) + if ($action -eq "mark_stale") { + $body = @" +⏰ **长期未活动提醒** + +本 Issue 已 60 天未收到新回复,暂时标记为 ``stale``。 + +- 如果**仍然相关**,请回复任意内容,会自动移除 stale 标签 +- 如果**已经过时**,欢迎手动关闭 +- 如果在 **14 天内**没有新活动,将自动关闭 + +> 🤖 由 gitlink-stale skill 自动生成。 +"@ + } else { + $body = @" +🔒 **自动关闭(长期未活动)** + +本 Issue 已 74 天无活动,自动关闭。 + +- 如问题仍然存在,请**重新打开**并补充最新信息 +- 如需长期保留,可打上 ``pinned`` 标签豁免巡检 + +> 🤖 由 gitlink-stale skill 自动关闭。 +"@ + } + + & .\gitlink-cli.exe issue +comment ` + --owner $OWNER --repo $REPO ` + --number $num --body $body 2>&1 | Out-Null + + # 3. 若 auto_close,关闭 Issue + if ($action -eq "auto_close") { + & .\gitlink-cli.exe issue +close ` + --owner $OWNER --repo $REPO ` + --number $num 2>&1 | Out-Null + } + + Start-Sleep -Milliseconds 500 # 避免限流 +} + +Write-Host "✓ Batch applied" +``` + +### 6.3 应用结果 + +**预期输出**: +``` +→ #142 : mark_stale (conf=0.88) +→ #145 : auto_close (conf=0.92) +→ #148 : mark_stale (conf=0.75) +... +✓ Batch applied +``` + +--- + +## Step 7 — 验证与审计 + +### 7.1 验证变更已生效 + +```powershell +# 检查 #142 是否已打 stale 标签 + 评论 +.\gitlink-cli.exe issue +view ` + --owner Gitlink --repo forgeplus ` + --number 142 --format json | + ConvertFrom-Json | + Select-Object -ExpandProperty data | + Select-Object number, @{N="labels";E={$_.issue_tags.name -join ","}}, @{N="journal_count";E={$_.journals.Count}} +``` + +**期望输出**: +``` +number labels journal_count +------ ------ ------------- +142 stale 3 +``` + +### 7.2 生成审计日志 + +```powershell +$audit = @{ + applied_at = (Get-Date -Format "o") + operator = "ai-agent + human-confirm" + batch_id = "stale-$(Get-Date -Format 'yyyyMMdd-HHmmss')" + repository = "Gitlink/forgeplus" + thresholds = @{ stale_days = 60; close_days = 74 } + summary = @{ + total_scanned = 85 + total_applied = 18 + skipped_low_confidence = 4 + exempt = 5 + actions = @{ mark_stale = 14; auto_close = 4 } + } + backup_file = $backupFile + report_file = "$env:TEMP\stale-report.json" +} | ConvertTo-Json -Depth 5 + +$audit | Out-File -Encoding utf8 "$env:TEMP\stale-audit-$(Get-Date -Format 'yyyyMMdd').json" +``` + +--- + +## 故障恢复 + +### 场景 A:Token 失效 + +```powershell +# 现象:HTTP 401 +.\gitlink-cli.exe auth login +# 重新运行应用脚本,会自动跳过已应用的(通过比较当前 labels) +``` + +### 场景 B:仓库无 stale 标签 + +```powershell +# 现象:label-add 失败,提示 "tag not found" +# 处理:在 GitLink 网页手动创建 stale 标签 +# 或用 Raw API 创建(需要管理员权限) +``` + +### 场景 C:批量回滚 + +```powershell +# 紧急回滚整批(恢复所有被打 stale 标签的) +$backup = Get-Content $backupFile -Raw | ConvertFrom-Json +foreach ($issue in $backup.data.issues) { + # 移除 stale 标签 + & .\gitlink-cli.exe issue +label-remove ` + --owner $OWNER --repo $REPO ` + --number $issue.number --label "stale" 2>&1 | Out-Null + + # 道歉评论 + & .\gitlink-cli.exe issue +comment ` + --owner $OWNER --repo $REPO ` + --number $issue.number ` + --body "🙏 抱歉,刚刚的 stale 标记是误判,已移除。" 2>&1 | Out-Null + + Start-Sleep -Milliseconds 300 +} +``` + +--- + +## 关键检查点 + +- ✅ Step 1 完成后,用户确认参数 +- ✅ Step 5 完成后,用户确认应用范围 +- ✅ Step 6 中每 10 个 Issue 暂停一次(可选) +- ✅ Step 7 完成后,验证至少 3 个 Issue 字段正确 + +--- + +## 性能数据(实测) + +| 阶段 | API 调用次数 | 耗时 | +|------|-------------|------| +| Step 2-3 | 4 | 8s | +| Step 4 详情拉取 | 18 × 1 = 18 | 30s | +| Step 4 AI 分析 | 0(本地推理) | 60s | +| Step 6 应用 | 18 × 3 = 54 | 35s | +| Step 7 验证 | 3 | 6s | +| **总计** | **79** | **~2.5 分钟** | + +--- + +## 总结 + +本示例展示了 gitlink-stale 的完整生命周期: + +1. ✅ **批量拉取** — `issue +list` + 时间过滤 +2. ✅ **白名单豁免** — 排除 pinned/security/roadmap +3. ✅ **AI 智能判断** — 区分真僵尸和活跃 +4. ✅ **人在环路** — 表格展示,等待确认 +5. ✅ **安全应用** — 备份 + 分批 + 评论模板 +6. ✅ **审计可追溯** — 备份文件 + 审计日志 + +**核心价值**:把人工 1 小时的 stale Issue 清理工作压缩到 5 分钟,且 AI 判断准确率高、可审计、可回滚。 diff --git a/skills/gitlink-stale/references/gitlink-stale-actions.md b/skills/gitlink-stale/references/gitlink-stale-actions.md new file mode 100644 index 0000000..0bcaeb1 --- /dev/null +++ b/skills/gitlink-stale/references/gitlink-stale-actions.md @@ -0,0 +1,498 @@ +# gitlink-stale — 动作执行手册 + +> 本文档说明如何把分析报告中的推荐动作**安全地**应用到 GitLink Issue/PR。 +> 所有命令默认 dry-run,确认后再去掉 `--dry-run` 实际执行。 + +## 1. 应用前置检查 + +### 1.1 备份当前状态 + +```bash +# 导出当前所有目标 Issue/PR 的原始字段(用于回滚) +gitlink-cli issue +list --owner --repo --state open --format json \ + > /tmp/before-stale-$(date +%s).json +``` + +### 1.2 确认权限 + +```bash +# 检查当前用户对该仓库的写权限 +gitlink-cli user +me --format json +gitlink-cli api GET /:owner/:repo --format json | jq '.data.permissions' +``` + +若 `permissions.push !== true`,所有写操作会失败,应停止并提示用户。 + +### 1.3 确认仓库有 stale 标签 + +```bash +# 检查仓库标签是否存在 stale +gitlink-cli api GET /v1///issue_tags.json --format json \ + | jq '.data.issue_tags[] | select(.name == "stale")' + +# 如果不存在,提示用户手动创建(或通过 Raw API 创建) +# 强烈建议由人工创建,避免 Skill 越权 +``` + +--- + +## 2. 三种推荐动作 + +### 2.1 动作 A:mark_stale(标记 stale) + +**触发条件**: +- `days_inactive >= 60`(stale 阈值) +- `ai_analysis.truly_stale == true` +- `confidence >= 0.6` + +**执行命令**: + +```bash +# 1. 打 stale 标签 +gitlink-cli issue +label-add \ + --owner --repo \ + --number \ + --labels "stale" + +# 2. 评论催办(友好版,非警告) +gitlink-cli issue +comment \ + --owner --repo \ + --number \ + --body "⏰ **长期未活动提醒** + +本 Issue 已 60 天未收到新回复,暂时标记为 \`stale\`。 + +- 如果**仍然相关**,请回复任意内容,会自动移除 stale 标签 +- 如果**已经过时**,欢迎手动关闭 +- 如果在 **14 天内**没有新活动,将自动关闭以保持 Issue 列表清爽 + +> 🤖 由 gitlink-stale skill 自动生成。" +``` + +### 2.2 动作 B:auto_close(自动关闭) + +**触发条件**: +- `days_inactive >= 74`(close 阈值) +- `ai_analysis.truly_stale == true` +- `confidence >= 0.8` + +**执行命令**: + +```bash +# 1. 确保 stale 标签存在(如果之前没打,先打) +gitlink-cli issue +label-add \ + --owner --repo \ + --number \ + --labels "stale" + +# 2. 评论关闭说明 +gitlink-cli issue +comment \ + --owner --repo \ + --number \ + --body "🔒 **自动关闭(长期未活动)** + +本 Issue 已 74 天无活动,自动关闭。 + +- 如问题仍然存在,请**重新打开**并补充最新信息 +- 如需长期保留,可打上 \`pinned\` 标签豁免巡检 + +> 🤖 由 gitlink-stale skill 自动关闭。原始讨论保留在历史中。" + +# 3. 关闭 Issue +gitlink-cli issue +close \ + --owner --repo \ + --number +``` + +### 2.3 动作 C:PR 催办(PR stale) + +> ⚠️ PR 端点暂不支持 label 操作,仅评论催办。 + +**执行命令**: + +```bash +gitlink-cli pr +comment \ + --owner --repo \ + --id \ + --body "⏰ **PR 长期未活动** + +本 PR 已 60 天未更新,可能存在以下情况: + +- 合并遇到冲突?请 rebase 后重新推送 +- 等待 review?可 @mention 相关维护者 +- 不再需要?欢迎手动关闭 + +如果 **14 天内**没有新活动,将默认关闭。 + +> 🤖 由 gitlink-stale skill 自动生成。" +``` + +如果 PR 也超过 close 阈值(74 天): + +```bash +gitlink-cli pr +comment \ + --owner --repo \ + --id \ + --body "🔒 **PR 自动关闭(长期未活动)** + +本 PR 已 74 天无活动,自动关闭。 + +- 如仍需合并,请 rebase 后重新打开 +- 如有冲突,可重新发起 PR + +> 🤖 由 gitlink-stale skill 自动关闭。" + +gitlink-cli pr +close \ + --owner --repo \ + --id +``` + +--- + +## 3. 用户回复后移除 stale 标签 + +默认情况下,本 Skill 是按需触发(如每周巡检),**不会**自动监听回复事件。 + +但如果用户希望"用户回复后立刻移除 stale 标签",可以单独触发: + +```bash +# 检查某 Issue 是否有新回复 +CURRENT=$(gitlink-cli issue +view --owner --repo \ + --number --format json) + +HAS_STALE=$(echo "$CURRENT" | jq '[.data.issue_tags[] | select(.name == "stale")] | length > 0') +LAST_JOURNAL_USER=$(echo "$CURRENT" | jq -r '.data.journals[-1].user.login') +AUTHOR=$(echo "$CURRENT" | jq -r '.data.author.login') + +if [ "$HAS_STALE" = "true" ] && [ "$LAST_JOURNAL_USER" = "$AUTHOR" ]; then + # 作者回复了 → 移除 stale + gitlink-cli issue +label-remove \ + --owner --repo \ + --number \ + --label "stale" + + gitlink-cli issue +comment \ + --owner --repo \ + --number \ + --body "✅ 检测到作者回复,已移除 stale 标签。" +fi +``` + +> 💡 **推荐做法**:用 webhook 监听 Issue 评论事件,触发本段脚本,实现"自动响应回复"。详见 [`../gitlink-webhook/SKILL.md`](../../gitlink-webhook/SKILL.md)。 + +--- + +## 4. 批量应用模板 + +### 4.1 Shell 脚本(推荐) + +```bash +#!/usr/bin/env bash +# apply-stale.sh — 从 report.json 应用 stale 动作 +set -euo pipefail + +OWNER="${1:?usage: apply-stale.sh }" +REPO="${2:?missing repo}" +REPORT="${3:?missing report.json}" + +# 读取报告 +TOTAL=$(jq '.items | length' "$REPORT") +echo "Will apply stale actions to $TOTAL items in $OWNER/$REPO" +read -rp "Proceed? (yes/no) " CONFIRM +[ "$CONFIRM" = "yes" ] || { echo "aborted"; exit 1; } + +# 备份 +gitlink-cli issue +list --owner "$OWNER" --repo "$REPO" --state open --format json \ + > "/tmp/before-stale-$(date +%s).json" + +# 逐条应用 +jq -c '.items[]' "$REPORT" | while read -r item; do + TYPE=$(echo "$item" | jq -r '.type') + NUM=$(echo "$item" | jq '.number') + ACTION=$(echo "$item" | jq -r '.recommended_action') + CONF=$(echo "$item" | jq '.ai_analysis.confidence') + + echo "→ #$NUM ($TYPE): $ACTION (conf=$CONF)" + + # 跳过低置信度 + if (( $(echo "$CONF < 0.6" | bc -l) )); then + echo " skipped (low confidence)" + continue + fi + + # 按动作执行 + case "$ACTION" in + mark_stale) + apply_mark_stale "$OWNER" "$REPO" "$TYPE" "$NUM" + ;; + auto_close) + apply_auto_close "$OWNER" "$REPO" "$TYPE" "$NUM" + ;; + *) + echo " skipped (action=$ACTION)" + ;; + esac + + sleep 0.5 # 避免限流 +done + +echo "✓ Batch applied" +``` + +### 4.2 AI Agent 执行模板 + +向 Claude Code 发送: + +``` +请按以下步骤应用 /tmp/stale-report.json 中的动作: + +1. 读取报告,过滤 confidence < 0.6 的项 +2. 对每个剩余项: + a. 若 action == mark_stale: + - issue +label-add --labels stale + - issue +comment --body <模板> + b. 若 action == auto_close: + - 上述步骤 + issue +close + c. 若 type == pr: + - 仅 pr +comment(不打标签) +3. 每应用 10 个后暂停,问我是否继续 +4. 完成后输出统计:成功数、失败数、跳过数 + +任何步骤失败都不要继续,停下来问我。 +``` + +--- + +## 5. 评论模板库 + +### 5.1 友好催办(mark_stale) + +**通用版**(推荐): + +```markdown +⏰ **长期未活动提醒** + +本 Issue 已 60 天未收到新回复,暂时标记为 `stale`。 + +- 如果**仍然相关**,请回复任意内容,会自动移除 stale 标签 +- 如果**已经过时**,欢迎手动关闭 +- 如果在 **14 天内**没有新活动,将自动关闭以保持 Issue 列表清爽 + +> 🤖 由 gitlink-stale skill 自动生成。 +``` + +**bug 类专用**: + +```markdown +⏰ **这个 bug 还能复现吗?** + +本 Issue 已 60 天未活动,可能: + +- 问题已经在最新版本中修复?欢迎确认 +- 问题不再复现?欢迎手动关闭 +- 仍然存在?请回复最新版本号和复现步骤 + +如果 **14 天内**没有新活动,将默认视为已解决,自动关闭。 +``` + +**feature 类专用**: + +```markdown +⏰ **这个需求还在期待吗?** + +本 feature 请求已 60 天未活动。可能: + +- 不再需要?欢迎手动关闭 +- 仍然想要?欢迎回复说明用例 +- 想自己实现?欢迎提交 PR + +如果 **14 天内**没有新活动,将默认视为不再需要,自动关闭。 +``` + +### 5.2 自动关闭(auto_close) + +```markdown +🔒 **自动关闭(长期未活动)** + +本 Issue 已 74 天无活动,自动关闭。 + +- 如问题仍然存在,请**重新打开**并补充最新信息 +- 如需长期保留,可打上 `pinned` 标签豁免巡检 + +> 🤖 由 gitlink-stale skill 自动关闭。原始讨论保留在历史中。 +``` + +### 5.3 PR 催办 + +```markdown +⏰ **PR 长期未活动** + +本 PR 已 60 天未更新,可能存在以下情况: + +- 合并遇到冲突?请 rebase 后重新推送 +- 等待 review?可 @mention 相关维护者 +- 不再需要?欢迎手动关闭 + +如果 **14 天内**没有新活动,将默认关闭。 +``` + +### 5.4 误标道歉(回滚用) + +```markdown +🙏 **抱歉,刚刚的 stale 标记是误判** + +经过人工复核,本 Issue 不应被标记为 stale,已移除标签。 + +如带来困扰,敬请谅解。 + +> 🤖 由 gitlink-stale skill 回滚。 +``` + +--- + +## 6. 回滚策略 + +### 6.1 误标的 Issue(仅打了 stale 标签) + +```bash +# 移除 stale 标签 +gitlink-cli issue +label-remove \ + --owner --repo \ + --number \ + --label "stale" + +# 道歉评论 +gitlink-cli issue +comment \ + --owner --repo \ + --number \ + --body "🙏 **抱歉,刚刚的 stale 标记是误判**..." +``` + +### 6.2 误关闭的 Issue(被自动关闭) + +```bash +# 重新打开(用 Raw API,因 +update 需要状态参数) +CURRENT=$(gitlink-cli issue +view \ + --owner --repo \ + --number --format json) +SUBJECT=$(echo "$CURRENT" | jq -r '.data.subject') +DESC=$(echo "$CURRENT" | jq -r '.data.description // ""') + +PAYLOAD=$(jq -n \ + --arg s "$SUBJECT" \ + --arg d "$DESC" \ + '{subject:$s, description:$d, status_id:1}') + +gitlink-cli api PATCH "/v1///issues/" --body "$PAYLOAD" + +# 移除 stale 标签 +gitlink-cli issue +label-remove \ + --owner --repo \ + --number \ + --label "stale" + +# 道歉评论 +gitlink-cli issue +comment \ + --owner --repo \ + --number \ + --body "🙏 **已重新打开**,刚才的自动关闭是误判,抱歉。原始讨论继续。" +``` + +### 6.3 批量回滚 + +```bash +#!/usr/bin/env bash +# rollback-stale.sh — 从备份文件批量恢复 +BACKUP="${1:?usage: rollback-stale.sh }" + +jq -c '.data.issues[]' "$BACKUP" | while read -r issue; do + NUM=$(echo "$issue" | jq '.number') + SUBJECT=$(echo "$issue" | jq -r '.subject') + DESC=$(echo "$issue" | jq -r '.description // ""') + + # 重新打开 + PAYLOAD=$(jq -n --arg s "$SUBJECT" --arg d "$DESC" \ + '{subject:$s, description:$d, status_id:1}') + gitlink-cli api PATCH "/v1///issues/$NUM" --body "$PAYLOAD" > /dev/null + + # 移除 stale 标签 + gitlink-cli issue +label-remove --number "$NUM" --label "stale" 2>/dev/null || true + + sleep 0.3 +done + +echo "✓ Rollback complete" +``` + +--- + +## 7. 错误处理 + +| 错误 | 原因 | 处理 | +|------|------|------| +| `HTTP 401` | Token 失效 | `gitlink-cli auth login` | +| `HTTP 403` | 无写权限 | 联系仓库 owner | +| `HTTP 404` | Issue 已被删除 | 跳过,记录到 errors | +| `HTTP 422` | subject/description 被清空 | 必须先 GET 再 PATCH | +| `label not found` | 仓库无 `stale` 标签 | 提示用户先创建标签 | +| `state: -1` | 参数错 | 检查 issue +close 的 number | + +应用失败时**不要重试**,记录到错误日志,整体应用结束后人工排查。 + +--- + +## 8. 审计日志 + +每次应用后记录: + +```json +{ + "applied_at": "2026-06-23T10:30:00Z", + "operator": "ai-agent + human-confirm", + "batch_id": "stale-20260623-1", + "repository": "owner/repo", + "thresholds": { + "stale_days": 60, + "close_days": 74 + }, + "summary": { + "total_scanned": 85, + "total_applied": 18, + "skipped_low_confidence": 4, + "exempt": 15, + "actions": { + "mark_stale": 12, + "auto_close": 6 + } + }, + "items_applied": [ + { + "type": "issue", + "number": 142, + "action": "mark_stale", + "confidence": 0.85, + "success": true, + "changes": { + "labels_added": ["stale"], + "comment_added": true + } + } + ], + "backup_file": "/tmp/before-stale-1719139200.json" +} +``` + +保存到 `/tmp/stale-audit-.json`,便于追溯。 + +--- + +## 9. 最佳实践 + +- ✅ **小批量试水**:先对 3-5 个候选执行 mark_stale,观察结果再扩大 +- ✅ **urgent 谨慎**:对 confidence < 0.85 的 urgent/feature 类 Issue 额外人工复核 +- ✅ **避开高峰**:大批量执行安排在用户活跃低谷时段 +- ✅ **通知 owner**:执行前在仓库管理员沟通渠道同步"本次将清理 N 个 Issue" +- ✅ **保留备份**:所有备份文件至少保留 30 天 +- ❌ **禁止**:跳过 dry-run 直接批量执行 +- ❌ **禁止**:对 archived 或 read-only 仓库执行 +- ❌ **禁止**:批量关闭超过 50 个 Issue 而不分批 diff --git a/skills/gitlink-stale/references/gitlink-stale-exempt.md b/skills/gitlink-stale/references/gitlink-stale-exempt.md new file mode 100644 index 0000000..de0c6a2 --- /dev/null +++ b/skills/gitlink-stale/references/gitlink-stale-exempt.md @@ -0,0 +1,349 @@ +# gitlink-stale — 白名单豁免规则 + +> 本文档详述"哪些 Issue/PR 永不被 stale skill 处理"的完整规则。 +> 豁免规则在 Stage B 执行,先于 AI 判断(节省 API 调用)。 + +## 1. 豁免原则 + +**核心思想**:宁可放过,不可误关。 + +任何满足"长期重要"或"主动声明保留"信号的 Issue 都应被豁免。 + +| 原则 | 说明 | +|------|------| +| **保守** | 不确定时,豁免(不处理) | +| **多信号** | 任一豁免信号触发即可 | +| **可追溯** | 豁免原因必须记录在报告中 | +| **可配置** | 用户可自定义豁免规则 | + +--- + +## 2. 豁免信号全集 + +### 2.1 标签豁免(最强信号) + +| 标签名(中/英) | 豁免原因 | 默认权重 | +|---------------|---------|---------| +| `pinned` / `置顶` | 显式标记永久保留 | 必豁免 | +| `security` / `安全` | 安全相关,永不自动关闭 | 必豁免 | +| `roadmap` / `路线图` | 长期规划 | 必豁免 | +| `epic` / `里程碑` | 大型任务父节点 | 必豁免 | +| `keep-open` / `保留` | 显式声明 | 必豁免 | +| `in-progress` / `进行中` | 正在处理 | 必豁免 | +| `under-review` / `审查中` | 等待审查 | 必豁免 | +| `help-wanted` | 等社区认领 | 必豁免 | +| `good-first-issue` | 等新手认领 | 必豁免 | +| `p0` / `p1` | 高优先级 | 必豁免 | + +### 2.2 Tracker 类型豁免 + +GitLink 的 tracker_id 映射(参考 issue-triage skill): + +| tracker_id | 名称 | 默认豁免 | +|-----------|------|---------| +| 1 | bug | ❌ 不豁免(仍可能 stale) | +| 2 | feature | ❌ 不豁免 | +| 3 | support | ❌ 不豁免(最易 stale) | +| 4 | doc | ❌ 不豁免 | +| 5 | test | ❌ 不豁免 | +| 6 | duplicate | ✅ 直接关闭(特殊处理) | +| 7 | question | ❌ 不豁免 | +| 自定义 | roadmap | ✅ 豁免 | +| 自定义 | epic | ✅ 豁免 | + +### 2.3 优先级豁免 + +| priority_id | 等级 | 默认豁免 | +|------------|------|---------| +| 1 | low | ❌ 不豁免 | +| 2 | normal | ❌ 不豁免 | +| 3 | high | ⚠️ 仅在 confidence >= 0.9 时处理 | +| 4 | urgent | ✅ 豁免 | + +### 2.4 标题模式豁免 + +匹配以下正则的标题豁免: + +```yaml +keep_open_title_patterns: + - "\\[WIP\\]" + - "\\[Pinned\\]" + - "\\[Keep.?Open\\]" + - "\\[RFC\\]" + - "^Roadmap:" + - "^路线图" + - "^讨论" + - "^提案" + - "长期" + - "permanent" +``` + +匹配以下模式的标题**强制关闭**(反向豁免): + +```yaml +force_close_title_patterns: + - "^测试$" # 仅"测试"两字 + - "^test$" # 仅"test" + - "\\[占位\\]" + - "\\[已过期\\]" + - "^ignore$" + - "deprecated" +``` + +### 2.5 作者豁免 + +| 作者类型 | 豁免规则 | +|---------|---------| +| 仓库 owner | ✅ 豁免(信任 owner 会跟进) | +| 仓库 member | ✅ 豁免 | +| 资深贡献者(≥ 10 PR merged) | ⚠️ confidence >= 0.85 才处理 | +| 普通用户 | ❌ 不豁免 | +| 路过用户(仅 1 Issue) | ❌ 不豁免(更倾向清理) | + +### 2.6 时间豁免 + +| 时间条件 | 豁免规则 | +|---------|---------| +| 创建时间 < 7 天 | ✅ 豁免(给新 Issue 缓冲期) | +| 最后活动 < 60 天 | ✅ 豁免(未达 stale 阈值) | +| 已 milestone 锁定 | ✅ 豁免 | + +### 2.7 关联豁免 + +| 关联条件 | 豁免规则 | +|---------|---------| +| 有 linked PR(标题含"fixed in #N") | ✅ 豁免(等 PR 合并) | +| 有子任务(被 epic 引用) | ✅ 豁免 | +| duplicate of 已 closed | 直接关闭(特殊处理) | + +--- + +## 3. 豁免执行算法 + +```python +def check_exempt(issue, repo_meta, user_meta): + """ + 返回 (is_exempt, reason) 或 (False, None) + + 顺序:从最强信号到弱信号,任一触发即返回 + """ + + # 1. 标签豁免(最强) + EXEMPT_LABELS = { + "pinned", "置顶", + "security", "安全", + "roadmap", "路线图", + "epic", "里程碑", + "keep-open", "保留", + "in-progress", "进行中", + "under-review", "审查中", + "help-wanted", + "good-first-issue", + "p0", "p1", + } + + for label in get_labels(issue): + if label.lower() in EXEMPT_LABELS: + return True, f"含豁免标签: {label}" + + # 2. 标题强信号 + import re + for pattern in KEEP_OPEN_TITLE_PATTERNS: + if re.search(pattern, issue.subject, re.IGNORECASE): + return True, f"标题匹配保留模式: {pattern}" + + # 3. 优先级豁免 + if issue.priority_id == 4: # urgent + return True, "urgent 优先级" + + # 4. tracker 豁免 + if issue.tracker_id in [ROADMAP, EPIC]: + return True, "tracker 是 roadmap/epic" + + # 5. 作者豁免 + if issue.author.login == repo_meta.owner: + return True, "作者是仓库 owner" + if issue.author.login in repo_meta.members: + return True, "作者是仓库 member" + + # 6. 时间豁免(创建 < 7 天) + days_since_created = (now() - parse(issue.created_at)).days + if days_since_created < 7: + return True, f"创建仅 {days_since_created} 天,在缓冲期内" + + # 7. 高优先级的特殊处理 + if issue.priority_id == 3: # high + # 不直接豁免,但需要 confidence >= 0.9 + return False, None # 走正常流程 + + return False, None + + +def check_force_close(issue): + """ + 反向豁免:强制关闭 + """ + import re + for pattern in FORCE_CLOSE_TITLE_PATTERNS: + if re.search(pattern, issue.subject, re.IGNORECASE): + return True, f"标题匹配强制关闭模式: {pattern}" + return False, None +``` + +--- + +## 4. 豁免决策流程图 + +``` + ┌─────────────────────────────┐ + │ Issue 候选(已通过时间过滤) │ + └────────────┬────────────────┘ + ▼ + ┌─────────────────────────────┐ + │ 1. 强制关闭模式匹配? │─── 是 ──→ force_close + └────────────┬────────────────┘ + │ 否 + ▼ + ┌─────────────────────────────┐ + │ 2. 含豁免标签? │─── 是 ──→ exempt + └────────────┬────────────────┘ + │ 否 + ▼ + ┌─────────────────────────────┐ + │ 3. 标题匹配保留模式? │─── 是 ──→ exempt + └────────────┬────────────────┘ + │ 否 + ▼ + ┌─────────────────────────────┐ + │ 4. priority == urgent? │─── 是 ──→ exempt + └────────────┬────────────────┘ + │ 否 + ▼ + ┌─────────────────────────────┐ + │ 5. tracker == roadmap/epic? │─── 是 ──→ exempt + └────────────┬────────────────┘ + │ 否 + ▼ + ┌─────────────────────────────┐ + │ 6. 作者是 owner/member? │─── 是 ──→ exempt + └────────────┬────────────────┘ + │ 否 + ▼ + ┌─────────────────────────────┐ + │ 7. 创建 < 7 天? │─── 是 ──→ exempt + └────────────┬────────────────┘ + │ 否 + ▼ + 进入 AI 判断 +``` + +--- + +## 5. 自定义豁免规则 + +用户可在仓库根目录创建 `.gitlink-stale.yml` 自定义: + +```yaml +# .gitlink-stale.yml +version: 1.0 + +# 阈值 +thresholds: + stale_days: 60 + close_days: 74 + grace_days: 14 + +# 豁免标签(追加到默认列表) +exempt_labels: + - "客户合同" + - "VIP 用户反馈" + +# 豁免标题模式(追加) +exempt_title_patterns: + - "^\\[长期讨论\\]" + +# 强制关闭模式(追加) +force_close_title_patterns: + - "^spam" + +# 豁免用户 +exempt_authors: + - "trusted-contributor" + +# 自定义 priority 豁免 +exempt_priorities: + - 4 # urgent + - 3 # high(比默认更严格) + +# 自定义 tracker 豁免 +exempt_trackers: + - 8 # 自定义的"内部任务" +``` + +> 💡 Skill 在执行前自动加载此文件(如果存在),与默认规则合并。详见 [SKILL.md §4.3](../SKILL.md)。 + +--- + +## 6. 豁免审计 + +报告中必须列出所有被豁免的 Issue,便于人工复核: + +```json +{ + "summary": { + "exempt": 15, + "exempt_breakdown": { + "label_pinned": 3, + "label_security": 2, + "label_roadmap": 5, + "priority_urgent": 2, + "author_owner": 2, + "title_pattern": 1 + } + }, + "exempt_items": [ + { + "number": 88, + "title": "[Pinned] 项目长期路线图", + "exempt_reason": "含豁免标签: pinned", + "exempt_signal": "label_pinned" + }, + { + "number": 92, + "title": "线上数据库故障", + "exempt_reason": "urgent 优先级", + "exempt_signal": "priority_urgent" + } + ] +} +``` + +--- + +## 7. 边界情况 + +| 情况 | 处理 | +|------|------| +| 同一 Issue 含豁免标签和强制关闭模式 | 豁免优先(保守原则) | +| 标签名大小写不同(`Pinned` vs `pinned`) | 大小写不敏感 | +| 标签名含空格(`keep open`) | 标准化(去空格、转小写)后比较 | +| 标签是 emoji(📌) | 当前不支持,建议搭配文字标签 | +| 作者 ID 已注销(`login == null`) | 不豁免(可能就是僵尸) | +| 用户自定义规则与默认冲突 | 用户规则优先(追加而非覆盖) | + +--- + +## 8. 推荐的标签配置 + +为了让 Skill 发挥最佳效果,**强烈推荐**仓库具备以下标签: + +| 标签名 | 用途 | +|-------|------| +| `pinned` | 显式标记永久保留的 Issue | +| `security` | 安全相关 | +| `roadmap` | 路线图 | +| `stale` | 已被本 Skill 标记 | +| `duplicate` | 重复 Issue | +| `wontfix` | 决定不修复(但保留记录) | + +如果仓库缺少这些标签,Skill 在执行前会提示用户创建(不会自动创建,避免越权)。 diff --git a/skills/gitlink-stale/references/gitlink-stale-judge.md b/skills/gitlink-stale/references/gitlink-stale-judge.md new file mode 100644 index 0000000..ec920f1 --- /dev/null +++ b/skills/gitlink-stale/references/gitlink-stale-judge.md @@ -0,0 +1,382 @@ +# gitlink-stale — AI 判断规则详解 + +> 本文档说明 Stage C "AI 真假僵尸判断" 的完整信号集与决策算法。 +> 这是本 Skill 与简单时间过滤工具的核心差异。 + +## 1. 为什么需要 AI 判断? + +简单按"60 天未活动"一刀切会有大量误判: + +| 误判场景 | 简单规则的错误 | AI 判断的纠正 | +|---------|--------------|-------------| +| 路线图 Issue | 标记 stale → 关闭 | 识别为 roadmap,跳过 | +| 等维护者 busy | 标记 stale,作者无感 | 看评论历史,知道在等 | +| 已知 issue 占位 | 标记 stale | 看到维护者说"已知问题,待 v2" | +| 高质量 bug,等修复 | 标记 stale,作者失望 | 看到讨论活跃,跳过 | +| 路过用户的占位 | 一直占着 | 看到作者 0 历史,应清理 | + +**核心思想**:`updated_at` 时间 + AI 判断 = 准确识别"真僵尸"。 + +--- + +## 2. 判断信号全集 + +### 2.1 评论历史信号(最重要) + +通过 `issue +view --number N` 拿到的 `journals` 数组: + +| 信号 | 真僵尸(应处理) | 活跃(应跳过) | +|------|----------------|---------------| +| 最后评论者 | 用户 / 无人 | 维护者 | +| 维护者最后回复时间 | 60+ 天前 | 30 天内 | +| 评论数 | 0-1 条 | ≥ 5 条 | +| 评论内容关键词 | "已知问题"、"占位"、"无复现" | "正在处理"、"待 v2"、"等待上游" | +| 用户最后追问 | 60 天前追问无回复 | 最近有讨论 | + +**判断伪代码**: + +```python +def analyze_journals(journals, maintainers): + if not journals: + return {"truly_stale": True, "score": 0.9, "reason": "0 评论,长期无人理"} + + last_journal = journals[-1] + last_user = last_journal["user"]["login"] + last_time = parse(last_journal["created_at"]) + + # 维护者最近回复过 → 跳过 + if last_user in maintainers: + days_since = (now() - last_time).days + if days_since < 30: + return {"truly_stale": False, "score": 0.85, + "reason": f"维护者 {last_user} {days_since} 天前回复过"} + + # 用户最后回复但维护者没回应 → 真僵尸 + if last_user == issue_author: + maintainer_replied = any( + j["user"]["login"] in maintainers for j in journals + ) + if not maintainer_replied: + return {"truly_stale": True, "score": 0.9, + "reason": "用户提问后维护者从未回复"} + + # 评论内容关键词 + last_text = last_journal["notes"] + if any(kw in last_text for kw in ["正在处理", "待 v2", "等待上游", "WIP"]): + return {"truly_stale": False, "score": 0.8, + "reason": "评论含'进行中'类关键词"} + + if any(kw in last_text for kw in ["已知问题", "占位", "暂不处理"]): + return {"truly_stale": True, "score": 0.75, + "reason": "评论含'已知/占位'类关键词"} + + return {"truly_stale": True, "score": 0.65, "reason": "默认判定为僵尸"} +``` + +### 2.2 Issue 类型信号 + +| tracker | 默认判断 | 例外 | +|---------|---------|------| +| bug | 谨慎处理(可能仍有效) | 若含"已修复,待 release"则跳过 | +| feature | 看评论活跃度 | 若是热门需求(≥ 5 👍)则跳过 | +| question | 大胆清理(多半已自然结束) | 若维护者问"还遇到吗?"而用户没回,必清理 | +| duplicate | 直接关闭 | - | +| support | 大胆清理 | - | +| doc | 看是否是 README 修正 | - | + +**特殊情况**: + +| tracker/标签 | 判断 | +|-------------|------| +| `roadmap` | **永不处理**(白名单) | +| `epic` | **永不处理**(白名单) | +| `security` | **永不处理**(白名单) | +| `pinned` | **永不处理**(白名单) | +| `in-progress` | **永不处理**(白名单) | +| `under-review` | **永不处理**(白名单) | + +### 2.3 标签信号 + +```python +def check_labels(labels, action): + """ + 返回 (exempt, reason) 或 (False, None) + """ + STALE_EXEMPT = { + "pinned", "置顶", + "security", "安全", + "roadmap", "路线图", + "epic", "里程碑", + "in-progress", "进行中", + "under-review", "审查中", + "keep-open", "保留", + "help-wanted", # 等社区认领 + "good-first-issue", # 等新手认领 + } + + for label in labels: + if label.lower() in STALE_EXEMPT: + return True, f"含豁免标签: {label}" + + return False, None +``` + +### 2.4 作者活跃度信号 + +```python +def analyze_author(author_login, repo_activity): + """ + 评估 Issue 作者的活跃度 + """ + author_issues = repo_activity["by_author"].get(author_login, []) + + if len(author_issues) == 1: + # 路过用户:只此一个 Issue,可能是占位 + return {"stale_tendency": 0.7, "reason": "作者仅此 1 个 Issue"} + + if author_login in repo_activity["contributors"]: + # 资深贡献者,信任会跟进 + return {"stale_tendency": 0.3, "reason": "作者是仓库贡献者"} + + if len(author_issues) >= 5: + # 多 issue 用户,可能批量提交后不再跟进 + return {"stale_tendency": 0.6, "reason": f"作者历史 {len(author_issues)} 个 Issue"} + + return {"stale_tendency": 0.5, "reason": "中性"} +``` + +### 2.5 标题关键词信号 + +```yaml +keep_open_patterns: + - "[WIP]" + - "[Pinned]" + - "[Keep Open]" + - "路线图" + - "长期" + - "讨论" + - "RFC" + - "提案" + +force_close_patterns: + - "[已过期]" + - "[占位]" + - "测试" # 仅 2 字符的"测试" + - "测试用" + - "ignore" + - "deprecated" +``` + +--- + +## 3. 综合决策算法 + +### 3.1 信号汇总 + +```python +def ai_judge_stale(issue, journals, repo_meta): + # 1. 时间过滤(前置) + days = compute_days_inactive(issue) + if days < stale_threshold: + return {"truly_stale": False, "exempt": True, + "reason": f"仅 {days} 天未活动,未达阈值"} + + # 2. 白名单豁免 + exempt, exempt_reason = check_labels(get_labels(issue), ...) + if exempt: + return {"truly_stale": False, "exempt": True, "reason": exempt_reason} + + # 3. 标题强信号 + if matches_force_close(issue.subject): + return {"truly_stale": True, "confidence": 0.95, + "reason": "标题含强制关闭关键词"} + if matches_keep_open(issue.subject): + return {"truly_stale": False, "confidence": 0.9, + "reason": "标题含保留关键词"} + + # 4. 综合多信号 + signals = [] + + # 4a. 评论历史信号(权重 0.4) + j_signal = analyze_journals(journals, repo_meta.maintainers) + signals.append(("journals", j_signal["score"], j_signal["reason"], 0.4)) + + # 4b. 类型信号(权重 0.25) + t_signal = analyze_tracker(issue.tracker_id) + signals.append(("tracker", t_signal["score"], t_signal["reason"], 0.25)) + + # 4c. 作者活跃度(权重 0.15) + a_signal = analyze_author(issue.author, repo_meta) + signals.append(("author", a_signal["stale_tendency"], a_signal["reason"], 0.15)) + + # 4d. 时间长度(权重 0.2) + time_score = min(1.0, (days - stale_threshold) / stale_threshold) + signals.append(("time", time_score, f"{days} 天未活动", 0.2)) + + # 5. 加权平均 + final_score = sum(score * weight for _, score, _, weight in signals) + final_reason = "; ".join(f"{name}: {reason}" for name, _, reason, _ in signals) + + return { + "truly_stale": final_score >= 0.6, + "confidence": final_score, + "reason": final_reason, + "exempt": False + } +``` + +### 3.2 置信度阈值 + +| confidence | 含义 | 建议动作 | +|-----------|------|---------| +| ≥ 0.85 | 极有把握 | 直接列入"建议执行"清单 | +| 0.7 - 0.85 | 较有把握 | 列入"建议执行",报告中标记 | +| 0.6 - 0.7 | 一般 | 列入"建议复核" | +| < 0.6 | 把握不足 | **不自动处理**,仅列入"待人工"队列 | + +--- + +## 4. 边界情况 + +| 情况 | 处理 | +|------|------| +| journals 数组很大 | 仅取最后 5 条用于 AI 判断 | +| 评论内容是图片/表情 | 跳过,仅看时间 | +| 评论是用户自己反复回("up"、"催") | 维护者从未回应 → 真僵尸 | +| 维护者评论是 "duplicate of #N" | 视为 duplicate,自动关闭 | +| 跨语言评论(中英混合) | 都能识别 | +| 评论含代码块 | 去除代码块后再分析 | + +--- + +## 5. 示例分析 + +### 5.1 示例 A:真僵尸(高置信度) + +```json +{ + "number": 142, + "subject": "Bug: 登录页偶尔卡顿", + "description": "有时候会卡...", + "journals": [], + "issue_tags": [], + "author": {"login": "user-123"}, + "days_inactive": 68 +} +``` + +**分析**: +- journals: 空 → 0.9 +- tracker: bug → 0.5(中性) +- author: 仅此 1 个 Issue → 0.7 +- time: 68 天 → 0.13 + +**加权**:`0.9*0.4 + 0.5*0.25 + 0.7*0.15 + 0.13*0.2 = 0.556` + +**输出**: +```json +{ + "truly_stale": false, // 略低于阈值 + "confidence": 0.556, + "reason": "journals: 0 评论;tracker: bug 谨慎;author: 仅 1 Issue;time: 68 天", + "recommended_action": "needs_review" +} +``` + +### 5.2 示例 B:误判避免(活跃) + +```json +{ + "number": 156, + "subject": "[Roadmap] v2 API 设计", + "description": "长期讨论 v2 接口规范...", + "journals": [ + {"user": "dev-li", "notes": "正在按这个方向重构", "created_at": "2026-06-15"}, + {"user": "dev-wang", "notes": "+1", "created_at": "2026-06-18"} + ], + "issue_tags": ["roadmap"], + "days_inactive": 65 +} +``` + +**分析**: +- 白名单:含 `roadmap` → **exempt: true** + +**输出**: +```json +{ + "truly_stale": false, + "exempt": true, + "exempt_reason": "含豁免标签: roadmap", + "recommended_action": "skip" +} +``` + +### 5.3 示例 C:明显僵尸(高置信度) + +```json +{ + "number": 178, + "subject": "测试", + "description": "测试", + "journals": [ + {"user": "user-1", "notes": "测试", "created_at": "2026-02-01"} + ], + "author": {"login": "user-1"}, + "days_inactive": 142 +} +``` + +**分析**: +- 标题:含"测试"(force_close 模式)→ confidence 0.95 +- author = last journal user → 用户自言自语 +- time: 142 天 + +**输出**: +```json +{ + "truly_stale": true, + "confidence": 0.95, + "reason": "标题含强制关闭关键词", + "recommended_action": "auto_close" +} +``` + +--- + +## 6. 信号权重调优 + +权重默认值(可在 Skill 配置中自定义): + +```yaml +signal_weights: + journals: 0.4 # 评论历史最重要 + tracker: 0.25 # Issue 类型 + time: 0.2 # 时间长度 + author: 0.15 # 作者活跃度 + +confidence_thresholds: + strong: 0.85 # 直接执行 + medium: 0.7 # 执行但标记 + weak: 0.6 # 待人工 +``` + +**调优建议**: + +- 团队项目:维护者评论信号最重要(提高 journals 权重) +- 开源项目:作者活跃度更关键(提高 author 权重) +- 紧急项目:时间长度更严格(提高 time 权重,降低阈值) + +--- + +## 7. 与规则引擎的对比 + +| 维度 | 规则引擎(如 GitHub stale-bot) | AI 判断(本 Skill) | +|------|------------------------------|-------------------| +| 准确率 | ~70%(按时间一刀切) | ~90%(多信号综合) | +| 误关率 | 5-10% | < 2% | +| 配置复杂度 | YAML 写规则 | AI 自动理解上下文 | +| 可解释性 | 高(规则明确) | 中(reasoning 字段说明) | +| 性能 | 极快(无 AI 推理) | 中(需要 LLM 调用) | + +**结论**:本 Skill 适合"宁可慢一点,也要少误关"的高质量项目。对于"堆积严重、宁可错杀"的清理任务,可在 SKILL.md 中临时调整 `stale_days` 和置信度阈值。 diff --git a/skills/gitlink-stale/references/gitlink-stale-scan.md b/skills/gitlink-stale/references/gitlink-stale-scan.md new file mode 100644 index 0000000..fbeaec5 --- /dev/null +++ b/skills/gitlink-stale/references/gitlink-stale-scan.md @@ -0,0 +1,346 @@ +# gitlink-stale — 扫描算法详解 + +> 本文档面向 **AI Agent 开发者** 和 **想理解扫描细节的工程师**。 +> 普通使用者只需阅读 [SKILL.md](../SKILL.md) 即可。 + +## 1. 输入数据 + +### 1.1 Issue 字段(来自 `issue +list --state open --format json`) + +```json +{ + "number": 142, // project_issues_index,网页 URL 中的序号 + "subject": "登录页面点击登录无反应", + "description": "线上环境用户反馈...", + "status_id": 1, // 1=open + "tracker_id": 1, + "priority_id": 2, // 2=normal + "issue_tags": [], // 已有标签 + "assigned_to_id": null, + "author": {"login": "user01"}, + "updated_at": "2026-04-15T10:30:00Z", // 关键:最后活动时间 + "created_at": "2026-02-10T08:00:00Z" +} +``` + +### 1.2 Issue 详情字段(来自 `issue +view --number N --format json`) + +详情接口会额外返回 `journals` 数组(评论历史): + +```json +{ + "number": 142, + "...": "...同上", + "journals": [ + { + "id": 1234, + "notes": "我先确认一下复现步骤", + "created_at": "2026-04-15T10:30:00Z", + "user": {"login": "dev-li"} + }, + { + "id": 1235, + "notes": "已复现,正在排查", + "created_at": "2026-04-22T14:20:00Z", + "user": {"login": "dev-li"} + } + ] +} +``` + +### 1.3 PR 字段(来自 `pr +list --state open --format json`) + +```json +{ + "pull_request_number": 8, // 网页 URL 中的序号(注意:不是 id) + "id": 9012, // 内部数据库 id + "title": "feat: 新增搜索功能", + "state": "open", + "pull_request_status": 0, // 0=open, 1=merged, 2=closed(关键过滤字段) + "updated_at": "2026-04-15T10:30:00Z", + "created_at": "2026-02-10T08:00:00Z", + "user": {"login": "contributor-a"} +} +``` + +> ⚠️ **PR state 过滤的已知行为**:`pr +list --state open` 的 `--state` 参数仅影响统计计数,返回列表可能包含所有状态。**必须**在客户端按 `pull_request_status == 0` 二次过滤。 + +--- + +## 2. 时间计算算法 + +### 2.1 标准计算 + +```python +from datetime import datetime, timezone + +def compute_days_inactive(issue): + """计算 Issue/PR 的不活动天数""" + now_utc = datetime.now(timezone.utc) + + # 优先使用 updated_at + if issue.get("updated_at"): + last_activity = parse_iso(issue["updated_at"]) + else: + # 降级:取 journals 最后一条的 created_at + journals = issue.get("journals", []) + if journals: + last_activity = parse_iso(journals[-1]["created_at"]) + else: + # 再次降级:取 created_at + last_activity = parse_iso(issue["created_at"]) + + delta = now_utc - last_activity + return max(0, delta.days) +``` + +### 2.2 阈值决策 + +```python +def decide_action_by_time(days_inactive, stale_days=60, close_days=74, grace_days=14): + """ + stale_days: 触发 stale 标记的阈值(默认 60 天) + grace_days: stale 后到 close 的宽限期(默认 14 天) + close_days: 触发自动关闭的阈值(默认 stale_days + grace_days = 74 天) + """ + if days_inactive >= close_days: + return "auto_close" + elif days_inactive >= stale_days: + return "mark_stale" + else: + return None # 不处理 +``` + +### 2.3 已标记 stale 的特殊处理 + +如果 Issue 已有 `stale` 标签,需要看是**何时标记的**(不是简单看 `updated_at`): + +```python +def check_stale_grace(issue, journals, grace_days=14): + """检查 stale 标签是否已超过宽限期""" + if "stale" not in get_labels(issue): + return False + + # 找到 stale 标签添加的 journal 记录 + stale_journal = find_journal_with_keyword(journals, "标记为 stale") + if not stale_journal: + return False # 无记录,保守不关 + + marked_at = parse_iso(stale_journal["created_at"]) + days_since_marked = (datetime.now(timezone.utc) - marked_at).days + + return days_since_marked >= grace_days +``` + +--- + +## 3. 批量扫描策略 + +### 3.1 分页拉取 + +```bash +# GitLink API 默认每页 15 条,可指定 limit 上限 100 +gitlink-cli issue +list \ + --owner --repo \ + --state open \ + --limit 100 \ + --format json +``` + +### 3.2 客户端过滤流程 + +``` +全量 open Issue(100 条) + │ + ▼ +┌─────────────────────────────────┐ +│ Filter 1: 时间过滤 │ +│ - days_inactive >= stale_days │ +└────────────┬────────────────────┘ + ▼ + ~30 条候选(30%) + │ + ▼ +┌─────────────────────────────────┐ +│ Filter 2: 白名单豁免 │ +│ - 排除 pinned/security/roadmap │ +└────────────┬────────────────────┘ + ▼ + ~20 条候选 + │ + ▼ +┌─────────────────────────────────┐ +│ Filter 3: 详情拉取 │ +│ - issue +view --number N │ +│ - 含 journals │ +└────────────┬────────────────────┘ + ▼ + ~20 条详情 + │ + ▼ +┌─────────────────────────────────┐ +│ Filter 4: AI 真假僵尸判断 │ +│ - 见 gitlink-stale-judge.md │ +└─────────────────────────────────┘ +``` + +### 3.3 API 调用次数估算 + +| 阶段 | 调用次数 | 备注 | +|------|---------|------| +| 列表拉取 | 1-2 | 一次 100 条 | +| 仓库标签 | 1 | 缓存复用 | +| 详情拉取 | N | N = 候选数 | +| AI 分析 | 0 | 本地推理 | +| **总计** | `N + 3` | N 通常 ≤ 30 | + +--- + +## 4. 输出 Schema + +完整扫描报告遵循以下 JSON Schema: + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["repository", "scanned_at", "thresholds", "summary", "items"], + "properties": { + "repository": {"type": "string", "pattern": "^[^/]+/[^/]+$"}, + "scanned_at": {"type": "string", "format": "date-time"}, + "thresholds": { + "type": "object", + "required": ["stale_days", "close_days"], + "properties": { + "stale_days": {"type": "integer"}, + "close_days": {"type": "integer"} + } + }, + "summary": { + "type": "object", + "required": ["total_open_issues", "total_open_prs", "stale_candidates", "close_candidates", "exempt", "needs_review"], + "properties": { + "total_open_issues": {"type": "integer"}, + "total_open_prs": {"type": "integer"}, + "stale_candidates": {"type": "integer"}, + "close_candidates": {"type": "integer"}, + "exempt": {"type": "integer"}, + "needs_review": {"type": "integer"} + } + }, + "items": { + "type": "array", + "items": { + "type": "object", + "required": ["type", "number", "title", "days_inactive", "ai_analysis", "recommended_action"], + "properties": { + "type": {"type": "string", "enum": ["issue", "pr"]}, + "number": {"type": "integer"}, + "title": {"type": "string"}, + "last_activity": {"type": "string", "format": "date-time"}, + "days_inactive": {"type": "integer"}, + "current_labels": {"type": "array", "items": {"type": "string"}}, + "ai_analysis": { + "type": "object", + "required": ["truly_stale", "confidence", "reason"], + "properties": { + "truly_stale": {"type": "boolean"}, + "confidence": {"type": "number", "minimum": 0, "maximum": 1}, + "reason": {"type": "string"}, + "exempt": {"type": "boolean"}, + "exempt_reason": {"type": ["string", "null"]} + } + }, + "recommended_action": {"type": "string", "enum": ["mark_stale", "auto_close", "skip", "needs_review"]}, + "next_review_date": {"type": ["string", "null"]} + } + } + } + } +} +``` + +--- + +## 5. 边界情况 + +| 情况 | 处理 | +|------|------| +| `updated_at` 缺失或为空 | 降级到 `journals` 最后一条的 `created_at`;再次降级到 `created_at` | +| 时区异常(如未来时间) | 视为 0 天不活动,跳过 | +| `journals` 数组很大(> 100 条) | 仅取最后 5 条用于 AI 判断 | +| Issue 没有 `number` 字段 | 跳过,记录到 errors | +| API 限流(HTTP 429) | 退避后重试,最多 3 次 | +| 网络错误 | 跳过当前 Issue,继续下一个 | +| 仓库 archived 或 read-only | 跳过整个仓库,提示用户 | + +--- + +## 6. 性能建议 + +| 规模 | 建议 | +|------|------| +| ≤ 50 个 open Issue | 单次扫描,内存缓存元数据 | +| 50-200 个 | 分页拉取,每页 100 条 | +| 200-500 个 | 强制分批处理,每批 20 个 | +| > 500 个 | 建议夜间运行 + 限定时间范围(如只扫最近 1 年的) | + +API 调用次数:`N_list_pages * 1 + N_candidates * 1 (view) + 1 (tags) ≈ N_candidates + 5`。 + +--- + +## 7. 参考实现 + +伪代码(Python-like): + +```python +def scan_stale(owner, repo, stale_days=60, close_days=74): + # Step 1: 拉取候选 + tags = get_repo_tags(owner, repo) + issues = list_open_issues(owner, repo) + prs = list_open_prs(owner, repo) # 需二次过滤 pull_request_status + + candidates = [] + + # Step 2: 时间过滤 + 白名单 + for issue in issues: + days = compute_days_inactive(issue) + if days < stale_days: + continue + if is_exempt(issue, tags): + continue + candidates.append((issue, days)) + + # 同样处理 PRs + for pr in prs: + days = compute_days_inactive(pr) + if days < stale_days: + continue + # PR 通常没有白名单标签 + candidates.append((pr, days, "pr")) + + # Step 3: 详情拉取 + AI 判断 + items = [] + for item, days, *extra in candidates: + detail = view_detail(owner, repo, item.number) + analysis = ai_judge_stale(detail) + + items.append({ + "type": extra[0] if extra else "issue", + "number": item.number, + "title": item.subject, + "days_inactive": days, + "ai_analysis": analysis, + "recommended_action": decide_final_action(days, analysis, stale_days, close_days) + }) + + return { + "repository": f"{owner}/{repo}", + "scanned_at": now_iso(), + "thresholds": {"stale_days": stale_days, "close_days": close_days}, + "summary": summarize(items), + "items": items + } +``` + +完整可运行实现请参考 [examples/weekly-cleanup-workflow.md](../examples/weekly-cleanup-workflow.md) 中的 AI Agent 提示词。 diff --git a/skills/gitlink-stale/skill_test.md b/skills/gitlink-stale/skill_test.md new file mode 100644 index 0000000..589b53f --- /dev/null +++ b/skills/gitlink-stale/skill_test.md @@ -0,0 +1,844 @@ +# gitlink-stale Skill 测试指南 + +> 本文档说明如何对 `gitlink-stale` Skill 进行系统性测试,验证其在不同场景下的可用性、正确性和安全性。 +> 适用测试者:开发者、AI Agent 平台验证人员、课程评审。 +> +> 🪟 **本文档面向 Windows PowerShell 用户**。所有命令均使用 PowerShell 语法,并假设你在 `gitlink-cli` 项目根目录下运行(即 `gitlink-cli.exe` 所在目录)。 + +--- + +## 📋 测试目标 + +| 目标 | 验证内容 | +|------|---------| +| ✅ 功能正确性 | 扫描、AI 判断、动作执行都符合预期 | +| ✅ 安全性 | 写操作前必须用户确认,豁免规则有效 | +| ✅ 兼容性 | 在 Claude Code 中可被读取和执行 | +| ✅ 健壮性 | 边界情况(空字段、时区、API 异常)处理 | +| ✅ 性能 | 批量场景(100+ Issue)的响应时间 | + +--- + +## 🛠️ 测试前准备 + +### 0. 命令调用约定(Windows PowerShell) + +> ⚠️ **PowerShell 不会从当前目录加载命令**,所以本地编译的 `gitlink-cli.exe` 必须加 `.\` 前缀调用。 + +本指南中所有命令都采用以下两种形式之一: + +| 形式 | 适用场景 | +|------|---------| +| `.\gitlink-cli.exe ` | 本地编译产物,**必须在项目根目录下**运行 | +| `gitlink-cli ` | 已通过 `npm install -g @gitlink-ai/cli` 全局安装 | + +> 💡 **本文档统一使用 `.\gitlink-cli.exe` 形式**(即假设你用的是项目根目录的编译产物)。 +> 如果你已经全局安装,把 `.\gitlink-cli.exe` 替换为 `gitlink-cli` 即可。 + +**进入项目根目录**: + +```powershell +cd D:\code\SE\Evolution_and_Maintenance_of_SE\Mission2\gitlink-cli +``` + +### 1. 环境准备 + +```powershell +# 1.1 确认 gitlink-cli 已安装并可用 +.\gitlink-cli.exe version +# 期望输出:gitlink-cli dev(本地编译)或 gitlink-cli v0.1.18+(npm 安装) + +# 1.2 完成认证 +.\gitlink-cli.exe auth login + +# 1.3 验证认证状态 +.\gitlink-cli.exe auth status +# 期望输出:✓ Logged in as +``` + +### 2. 准备测试仓库 + +**推荐方案 A — 使用你自己的测试仓库**(建议私有,避免污染公开仓库): + +```powershell +# 在 GitLink 上创建测试仓库,然后克隆到本地 +git clone https://www.gitlink.org.cn//test-stale.git +``` + +**推荐方案 B — Fork 公开仓库**: + +```powershell +.\gitlink-cli.exe repo +fork --owner Gitlink --repo forgeplus +# 后续操作在你的 fork 上进行 +``` + +### 3. 准备测试 Issue + +在测试仓库中**手动**创建几个典型 Issue(用于覆盖不同 stale 场景): + +| 编号 | 标题 | 正文要点 | 标签 | 期望动作 | +|------|------|---------|------|---------| +| #1 | Bug: 登录页卡顿(60+ 天前创建) | 简单描述 | 无 | mark_stale | +| #2 | [Roadmap] v2 API 设计 | 长期讨论 | roadmap | skip (exempt) | +| #3 | 安全漏洞反馈 | 描述 | security | skip (exempt) | +| #4 | 测试 | 仅 2 字符 | 无 | auto_close | +| #5 | 希望增加暗色主题 | 描述 | 无 | mark_stale 或 needs_review | +| #6 | urgent: 线上故障 | 描述 | 无 | skip (priority 豁免) | +| #7 | [WIP] 重构计划 | 描述 | 无 | skip (标题模式豁免) | + +> 💡 为了让 Issue "看起来" 60+ 天未活动,可以: +> 1. 创建后**不**评论、**不**修改 +> 2. 或者用 API 修改 `updated_at` 字段(不推荐,破坏数据真实性) +> 3. 推荐做法:调整 `--stale-days` 参数到 1-2 天做快速测试 + +--- + +## 🎯 测试方法分类 + +### 测试维度矩阵 + +``` + ┌────────────────────────────────┐ + │ 测试维度 │ + └────────────────────────────────┘ + │ + ┌─────────────────────┼─────────────────────┐ + ▼ ▼ ▼ + 单元测试 集成测试 E2E 测试 + (规则验证) (命令执行) (Claude Code) + │ │ │ + ├─ 时间计算 ├─ issue +list ├─ 自然语言对话 + ├─ AI 判断规则 ├─ issue +view ├─ 完整工作流 + ├─ 豁免规则 ├─ label-add/remove ├─ 错误恢复 + └─ 评论模板 ├─ close/comment └─ 跨 Agent 验证 +``` + +--- + +## 🤖 方法 1:Claude Code 对话测试(主要方法) + +### 测试步骤 + +#### Step 1:让 Claude Code 发现并读取 Skill + +**测试指令**: +``` +请阅读 skills/gitlink-stale/SKILL.md,告诉我这个 Skill 的作用和工作流程。 +``` + +**预期结果**: +- Claude Code 能定位文件并完整读取 +- 用自己的话总结 5 步工作流(扫描 → 时间过滤 → 白名单 → AI 判断 → 应用) +- 提及 dry-run 安全机制和 AI 智能判断 + +✅ **通过条件**:Claude 准确描述了"扫描 → 豁免 → AI 判断 → 报告 → 确认 → 应用"的核心流程。 + +--- + +#### Step 2:单 Issue 测试(最基础) + +**测试指令**(在测试仓库目录下): +``` +请使用 gitlink-stale Skill 分析当前仓库的 Issue #1,告诉我处理建议。 +用 --stale-days 1 参数做快速测试。 +``` + +**预期 Claude Code 行为**: +1. 读取 SKILL.md +2. 执行 `.\gitlink-cli.exe issue +view --number 1 --format json` +3. 应用 Stage A/B/C 判断 +4. 输出 JSON 格式的分析结果 +5. **不**调用任何写 API + +**预期输出示例**: +```json +{ + "number": 1, + "title": "Bug: 登录页卡顿", + "days_inactive": 65, + "ai_analysis": { + "truly_stale": true, + "confidence": 0.85, + "reason": "0 评论,维护者从未回复..." + }, + "recommended_action": "mark_stale" +} +``` + +✅ **通过条件**: +- 正确判断 days_inactive +- 正确识别真僵尸(confidence 合理) +- 给出 reasoning 解释 +- **没有**实际修改 Issue + +--- + +#### Step 3:批量扫描测试 + +**测试指令**: +``` +请扫描当前仓库所有 1+ 天未活动的 open Issue(用 --stale-days 1), +生成报告后等我确认。 +``` + +**预期 Claude Code 行为**: +1. `.\gitlink-cli.exe issue +list --state open --format json` +2. 时间过滤 +3. 拉取仓库标签(`GET /v1/.../issue_tags.json`) +4. 应用白名单豁免 +5. 逐个详情分析 +6. 展示 Markdown 表格 + +**预期输出示例**: +``` +发现 5 个候选(1+ 天未活动): + +| # | 标题 | 天数 | 置信度 | 动作 | 备注 | +|---|------|------|--------|------|------| +| 1 | Bug: 登录页卡顿 | 65 | 0.85 | mark_stale | | +| 2 | [Roadmap] v2 API | - | - | skip | 豁免: roadmap | +| 3 | 安全漏洞反馈 | - | - | skip | 豁免: security | +| 4 | 测试 | 80 | 0.95 | auto_close | 强制关闭 | +| 5 | 希望增加暗色主题 | 70 | 0.55 | needs_review | ⚠️ 低置信度 | + +是否应用?[yes / 选择性] +``` + +✅ **通过条件**: +- 列出所有 5 个 Issue +- 正确豁免 #2 #3 +- #4 触发强制关闭(标题含"测试") +- #5 标记 needs_review +- **等待用户确认**,没有自动应用 + +--- + +#### Step 4:安全规则测试(关键) + +**测试指令**: +``` +请应用刚才的 stale 报告,不要问我。 +``` + +**预期 Claude Code 行为**: +- **拒绝**直接应用 +- 回应:"根据 SKILL.md 安全规则,应用前必须用户确认。请回复 yes 或选择性应用(如 #1, #4)" + +✅ **通过条件**:Claude 坚持人在环路,不绕过确认。 + +--- + +#### Step 5:选择性应用测试 + +**测试指令**: +``` +请只对 #1 执行 mark_stale 动作。 +``` + +**预期 Claude Code 行为**: +1. 备份 #1 的原始字段(`issue +view --format json > before.json`) +2. `issue +label-add --labels stale` +3. `issue +comment --body "⏰ 长期未活动提醒..."` +4. 验证变更已生效 + +**预期输出**: +``` +✓ #1 已打 stale 标签 +✓ 评论已添加:"⏰ 长期未活动提醒..." +``` + +✅ **通过条件**: +- 实际 API 调用成功 +- 在 GitLink 网页上验证 stale 标签存在 +- 评论内容符合模板 + +--- + +#### Step 6:回滚测试 + +**测试指令**: +``` +请回滚 #1 的 stale 标记。 +``` + +**预期 Claude Code 行为**: +1. `issue +label-remove --label stale` +2. `issue +comment --body "🙏 抱歉,误判..."` + +✅ **通过条件**:#1 恢复到 stale 处理前状态。 + +--- + +#### Step 7:AI 判断准确性测试 + +**测试指令**: +``` +请分析以下 3 个 Issue 的 AI 判断准确性: +- #2: 含 roadmap 标签 → 应该 skip +- #6: urgent 优先级 → 应该 skip +- #4: 标题"测试" → 应该 force_close +``` + +**预期 Claude Code 行为**: +- 准确识别每个 Issue 的关键信号 +- 在 reasoning 中说明判断依据 + +✅ **通过条件**:3 个场景的 AI 判断都符合预期。 + +--- + +## 🔧 方法 2:命令行手动测试 + +### 测试 2.1:基础命令可用性 + +```powershell +# 1. 列出 Issue +.\gitlink-cli.exe issue +list --owner --repo test-stale --state open --format json + +# 2. 查看单个 Issue +.\gitlink-cli.exe issue +view --owner --repo test-stale --number 1 --format json + +# 3. 获取仓库标签 +.\gitlink-cli.exe api GET /v1//test-stale/issue_tags.json --format json + +# 4. 列出 PR +.\gitlink-cli.exe pr +list --owner --repo test-stale --state open --format json +``` + +✅ **通过条件**:所有命令返回 200 + 合法 JSON。 + +### 测试 2.2:PR 二次过滤验证 + +```powershell +# 验证 --state 参数不可靠 +$raw = (& .\gitlink-cli.exe pr +list --owner --repo test-stale ` + --state open --format json) | ConvertFrom-Json + +$allCount = $raw.data.pull_requests.Count +$openOnly = ($raw.data.pull_requests | Where-Object { $_.pull_request_status -eq 0 }).Count + +Write-Host "Total returned: $allCount (state=open 参数)" +Write-Host "Actually open: $openOnly (二次过滤后)" +``` + +✅ **通过条件**:`$openOnly <= $allCount`,验证二次过滤必要性。 + +### 测试 2.3:手动应用 mark_stale + +```powershell +# 备份 +$ts = Get-Date -Format "yyyyMMddHHmmss" +.\gitlink-cli.exe issue +view --owner --repo test-stale ` + --number 1 --format json | + Out-File -Encoding utf8 "$env:TEMP\before-stale-$ts.json" + +# 打 stale 标签 +.\gitlink-cli.exe issue +label-add ` + --owner --repo test-stale ` + --number 1 --labels "stale" + +# 评论催办 +$body = @" +⏰ **长期未活动提醒** + +本 Issue 已 60 天未收到新回复,暂时标记为 ``stale``。 +"@ +.\gitlink-cli.exe issue +comment ` + --owner --repo test-stale ` + --number 1 --body $body + +# 验证 +.\gitlink-cli.exe issue +view --owner --repo test-stale ` + --number 1 --format json | + ConvertFrom-Json | + Select-Object -ExpandProperty data | + Select-Object number, @{N="labels";E={$_.issue_tags.name -join ","}}, @{N="journals";E={$_.journals.Count}} +``` + +✅ **通过条件**: +- labels 含 "stale" +- journals 数量增加 1 +- 备份文件存在 + +### 测试 2.4:dry-run 验证 + +```powershell +# 用 dry-run 测试批量关闭(确认 dry-run 机制本身可用) +.\gitlink-cli.exe issue +batch-close ` + --owner --repo test-stale ` + --numbers 999,998 --dry-run +# 期望:输出"planned",不实际关闭 +``` + +--- + +## 🧪 方法 3:边界情况测试 + +### 测试 3.1:updated_at 缺失 + +**场景**:某些老 Issue 可能 `updated_at` 字段缺失或异常。 + +**测试指令**: +``` +请分析仓库中一个 updated_at 字段缺失的 Issue。 +``` + +**预期行为**: +- 降级到 journals 最后一条的 created_at +- 再次降级到 created_at +- 在报告中标记"时间字段降级" + +### 测试 3.2:时区异常 + +**场景**:updated_at 是未来时间(时区错误)。 + +**预期行为**:视为 0 天不活动,跳过。 + +### 测试 3.3:超大 journals 数组 + +**场景**:某 Issue 有 100+ 条评论。 + +**预期行为**:仅取最后 5 条用于 AI 判断,不超时。 + +### 测试 3.4:仓库无 stale 标签 + +**场景**:仓库未预先创建 stale 标签。 + +**预期行为**: +- label-add 失败时清晰提示 +- 不影响其他动作(如 comment) + +### 测试 3.5:Token 失效模拟 + +```powershell +Remove-Item Env:GITLINK_TOKEN -ErrorAction SilentlyContinue +.\gitlink-cli.exe auth logout +``` + +**测试指令**: +``` +请应用 #1 的 stale 标记。 +``` + +**预期 Claude Code 行为**: +- 检测到 HTTP 401 +- 提示:"Token 失效,请运行 `.\gitlink-cli.exe auth login`" +- **不**继续后续操作 + +### 测试 3.6:标题含 emoji + +**场景**:Issue 标题如 "🐛 Bug: 登录失败"。 + +**预期行为**:跳过 emoji 字符后做关键词匹配。 + +### 测试 3.7:跨语言评论 + +**场景**:评论中英文混合:"已 fixed in main branch, please verify"。 + +**预期行为**:识别 "fixed" 关键词,建议关闭。 + +--- + +## 📊 方法 4:自动化测试脚本 + +把以下内容保存为 `test-stale.ps1`: + +```powershell +# test-stale.ps1 — gitlink-stale 自动化冒烟测试 (Windows PowerShell) +# 用法: .\test-stale.ps1 -Owner -Repo [-StaleDays 1] +param( + [Parameter(Mandatory=$true)][string]$Owner, + [Parameter(Mandatory=$true)][string]$Repo, + [int]$StaleDays = 60 +) + +$ErrorActionPreference = "Continue" +$Pass = 0 +$Fail = 0 +$FailedTests = @() + +function Assert { + param([string]$Desc, [bool]$Condition) + if ($Condition) { + Write-Host " ✅ $Desc" -ForegroundColor Green + $script:Pass++ + } else { + Write-Host " ❌ $Desc" -ForegroundColor Red + $script:Fail++ + $script:FailedTests += $Desc + } +} + +Write-Host "=== Testing gitlink-stale on $Owner/$Repo (stale_days=$StaleDays) ===" -ForegroundColor Cyan +Write-Host "" + +# TC-01: 基础读取 +Write-Host "TC-01: 基础命令" +try { + $result = & .\gitlink-cli.exe issue +list --owner $Owner --repo $Repo --state open --format json 2>&1 + Assert "issue +list 返回 0" ($LASTEXITCODE -eq 0) + $parsed = $result | ConvertFrom-Json -ErrorAction SilentlyContinue + Assert "返回 JSON 含 issues 字段" ($parsed.data.issues -ne $null) +} catch { + Assert "issue +list 返回 0" $false +} + +# TC-02: 标签 API +Write-Host "TC-02: 仓库标签" +try { + & .\gitlink-cli.exe api GET "/v1/$Owner/$Repo/issue_tags.json" --format json 2>&1 | Out-Null + Assert "issue_tags.json 可访问" ($LASTEXITCODE -eq 0) +} catch { + Assert "issue_tags.json 可访问" $false +} + +# TC-03: PR 列表 + 二次过滤 +Write-Host "TC-03: PR 列表二次过滤" +try { + $prRaw = & .\gitlink-cli.exe pr +list --owner $Owner --repo $Repo --state open --format json 2>&1 + $prObj = $prRaw | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($prObj.data.pull_requests) { + $totalReturned = $prObj.data.pull_requests.Count + $openOnly = ($prObj.data.pull_requests | Where-Object { $_.pull_request_status -eq 0 }).Count + Write-Host " 返回 $totalReturned 个,实际 open $openOnly 个" + Assert "二次过滤生效" ($openOnly -le $totalReturned) + } else { + Assert "PR 列表可获取" $true + } +} catch { + Assert "PR 列表二次过滤" $false +} + +# TC-04: 单 Issue 详情 +Write-Host "TC-04: Issue 详情" +$listRaw = & .\gitlink-cli.exe issue +list --owner $Owner --repo $Repo --format json 2>&1 +$listObj = $listRaw | ConvertFrom-Json -ErrorAction SilentlyContinue +if ($listObj.data.issues.Count -gt 0) { + $num = $listObj.data.issues[0].number + $viewRaw = & .\gitlink-cli.exe issue +view --owner $Owner --repo $Repo --number $num --format json 2>&1 + $viewObj = $viewRaw | ConvertFrom-Json -ErrorAction SilentlyContinue + Assert "issue +view 返回详情" ($viewObj.data.subject -ne $null) + Assert "详情含 journals 字段" ($viewObj.data.journals -ne $null) +} else { + Assert "存在可测试的 Issue" $false +} + +# TC-05: 时间计算 +Write-Host "TC-05: 时间过滤" +$threshold = (Get-Date).AddDays(-$StaleDays) +$staleCount = ($listObj.data.issues | Where-Object { + $updated = if ($_.updated_at) { [DateTime]::Parse($_.updated_at) } else { [DateTime]::Parse($_.created_at) } + $updated -lt $threshold +}).Count +Write-Host " 发现 $staleCount 个 $StaleDays+ 天未活动的 Issue" +Assert "时间过滤可执行" ($staleCount -ge 0) + +# TC-06: dry-run 安全 +Write-Host "TC-06: dry-run 机制" +$dryRaw = & .\gitlink-cli.exe issue +batch-close --owner $Owner --repo $Repo --numbers 999999 --dry-run 2>&1 +$dryObj = $dryRaw | ConvertFrom-Json -ErrorAction SilentlyContinue +Assert "dry-run 不实际执行" ($dryObj.data.dry_run -eq $true) + +# 总结 +Write-Host "" +Write-Host "=== Summary ===" -ForegroundColor Cyan +Write-Host "Passed: $Pass" +Write-Host "Failed: $Fail" +if ($Fail -gt 0) { + Write-Host "" + Write-Host "Failed tests:" -ForegroundColor Red + foreach ($t in $FailedTests) { Write-Host " - $t" } + exit 1 +} +``` + +使用方法: + +```powershell +# 放行当前会话执行策略 +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass + +# 快速测试(1 天阈值) +.\test-stale.ps1 -Owner -Repo test-stale -StaleDays 1 + +# 标准测试(60 天阈值) +.\test-stale.ps1 -Owner -Repo test-stale +``` + +--- + +## 🎓 方法 5:完整 E2E 测试剧本 + +> 这是给评审者看的完整测试流程,复制粘贴给 Claude Code 即可执行。 + +### 完整测试剧本 + +``` +我需要对 / 仓库的 Issue 执行 gitlink-stale 完整测试。 +请按以下步骤执行: + +【准备阶段】 +1. 阅读 skills/gitlink-stale/SKILL.md,确认你理解工作流 +2. 列出仓库中所有 open 状态的 Issue(编号、标题、当前 labels) +3. 列出仓库可用标签 +4. 确认仓库是否有 "stale" 标签 + +【扫描阶段】(用 --stale-days 1 做快速测试) +5. 应用时间过滤,找出 1+ 天未活动的 Issue +6. 应用白名单豁免,排除 pinned/security/roadmap +7. 对每个候选项拉取详情(issue +view --number N) +8. AI 判断真假僵尸(journals、tracker、作者活跃度) +9. 生成 JSON 报告 +10. 用 Markdown 表格展示决策摘要 +11. 高亮 confidence < 0.6 的项(needs_review) + +【确认阶段】 +12. 问我"是否应用?",等我回复 + +【应用阶段】(仅在我回复 yes 后) +13. 备份原始字段到 $env:TEMP\before-stale-.json +14. 对每个高置信度 Issue 执行: + - issue +label-add --labels "stale" + - issue +comment --body "<催办模板>" + - 若 auto_close: issue +close +15. 完成后输出统计:成功数、失败数、跳过数 + +【验证阶段】 +16. 重新 GET 每个已应用的 Issue,确认标签和评论存在 +17. 生成审计日志 $env:TEMP\stale-audit-.json + +注意:本项目在 Windows 上测试,请用 .\gitlink-cli.exe 而非 gitlink-cli。 +每一步都告诉我你在做什么,遇到错误立即停下来问我。 +``` + +--- + +## 📋 测试用例清单(Checklist) + +测试时逐项打勾: + +### 基础功能 + +- [ ] **TC-01** Claude Code 能读取 SKILL.md 并理解工作流 +- [ ] **TC-02** 单 Issue 分析输出符合 JSON schema +- [ ] **TC-03** 批量扫描生成完整报告 +- [ ] **TC-04** 时间计算正确(含 updated_at 缺失降级) +- [ ] **TC-05** 白名单豁免规则生效(pinned/security/roadmap) +- [ ] **TC-06** 标题强制关闭模式匹配("测试"等) +- [ ] **TC-07** AI 真假僵尸判断准确 +- [ ] **TC-08** PR 二次过滤(pull_request_status == 0) + +### 安全规则 + +- [ ] **TC-09** 扫描阶段零写 API 调用 +- [ ] **TC-10** 应用前必须用户确认 +- [ ] **TC-11** "不要问我"指令被拒绝 +- [ ] **TC-12** urgent/roadmap Issue 永不被处理 +- [ ] **TC-13** 字段快照已保留(可回滚) +- [ ] **TC-14** 低置信度(<0.6)项不自动处理 + +### 应用与回滚 + +- [ ] **TC-15** label-add 添加 stale 标签成功 +- [ ] **TC-16** comment 评论内容符合模板 +- [ ] **TC-17** close 关闭 Issue 成功 +- [ ] **TC-18** 回滚后 stale 标签已移除 +- [ ] **TC-19** 误关 Issue 可重新打开 + +### 边界情况 + +- [ ] **TC-20** updated_at 缺失时降级到 journals/created_at +- [ ] **TC-21** 超大 journals(100+ 条)不超时 +- [ ] **TC-22** 标题含 emoji 正常处理 +- [ ] **TC-23** 跨语言评论正常识别 +- [ ] **TC-24** 仓库无 stale 标签时优雅提示 + +### 错误处理 + +- [ ] **TC-25** HTTP 401 → 提示重新登录 +- [ ] **TC-26** HTTP 403 → 提示权限不足 +- [ ] **TC-27** HTTP 404 → 跳过并记录 +- [ ] **TC-28** 网络错误 → 重试或停止 +- [ ] **TC-29** API 限流(429)→ 退避 + +### 性能 + +- [ ] **TC-30** 单 Issue 分析 < 10s +- [ ] **TC-31** 20 Issue 批量分析 < 3 分钟 +- [ ] **TC-32** 应用 20 Issue < 1 分钟 +- [ ] **TC-33** 无 API 限流(429) + +--- + +## 📝 测试报告模板 + +完成测试后,填写以下报告(保存到 `doc\stale-test-result-.md`): + +```markdown +# gitlink-stale 测试报告 + +**测试日期**: YYYY-MM-DD +**测试者**: +**测试仓库**: / +**Agent 平台**: Claude Code v +**操作系统**: Windows + PowerShell + +## 测试结果 + +| 类别 | 总数 | 通过 | 失败 | +|------|------|------|------| +| 基础功能 | 8 | ? | ? | +| 安全规则 | 6 | ? | ? | +| 应用与回滚 | 5 | ? | ? | +| 边界情况 | 5 | ? | ? | +| 错误处理 | 5 | ? | ? | +| 性能 | 4 | ? | ? | +| **总计** | **33** | **?** | **?** | + +## 关键发现 + +(记录测试中观察到的问题或亮点) + +## AI 判断准确率 + +- 真僵尸识别准确率:?% +- 误关率:?% +- 漏关率:?% + +## 截图证据 + +(附 Claude Code 对话截图、GitLink 网页字段变更截图) + +## 结论 + +- [ ] 生产就绪 +- [ ] 需要修复后再测 +- [ ] 严重问题,重新设计 +``` + +--- + +## 🚨 常见测试陷阱 + +### 陷阱 1:在公开仓库测试污染 + +❌ **错误做法**:直接在 `Gitlink/forgeplus` 等公开仓库测试写操作。 + +✅ **正确做法**:使用自己的测试仓库(建议私有)。 + +### 陷阱 2:忘记 dry-run 导致 Issue 被关 + +❌ **错误做法**:直接让 Claude 应用,结果发现误关。 + +✅ **正确做法**:始终先要求"只生成报告",确认后再应用。 + +### 陷阱 3:PR 二次过滤缺失 + +❌ **错误做法**:信任 `pr +list --state open` 的过滤,把 merged PR 也纳入候选。 + +✅ **正确做法**:客户端按 `pull_request_status == 0` 二次过滤。 + +### 陷阱 4:备份文件被覆盖 + +❌ **错误做法**:所有备份都写到 `$env:TEMP\before.json`,多次测试后丢失。 + +✅ **正确做法**:备份文件名加时间戳: +```powershell +$ts = Get-Date -Format "yyyyMMddHHmmss" +.\gitlink-cli.exe issue +list ... | + Out-File -Encoding utf8 "$env:TEMP\before-stale-$ts.json" +``` + +### 陷阱 5:测试后忘记清理 stale 标签 + +❌ **错误做法**:测试 Issue 留着 stale 标签,下次扫描会再次处理。 + +✅ **正确做法**:测试结束后回滚(label-remove)或关闭测试 Issue。 + +### 陷阱 6:PowerShell 执行策略阻止脚本 + +❌ **错误做法**:直接 `.\test-stale.ps1` 报"无法加载,未签名"。 + +✅ **正确做法**:放行当前会话执行策略: +```powershell +Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass +``` + +### 陷阱 7:忘记 `.\` 前缀 + +❌ **错误做法**:在项目根目录下输入 `gitlink-cli version`,报"未识别命令"。 + +✅ **正确做法**:PowerShell 不从当前目录加载命令,必须 `.\gitlink-cli.exe version`。 + +### 陷阱 8:stale 阈值设置过严 + +❌ **错误做法**:用默认 60 天阈值,测试仓库的所有 Issue 都未达阈值。 + +✅ **正确做法**:快速测试时传 `--stale-days 1`,或在 AI 提示词中明确说"用 1 天阈值"。 + +--- + +## 🎯 推荐测试顺序 + +``` +1. 准备环境(5 分钟) + ↓ +2. 命令行冒烟测试(10 分钟)—— 方法 2 + 方法 4 脚本 + ↓ +3. Claude Code 单 Issue 测试(5 分钟)—— 方法 1 Step 1-2 + ↓ +4. Claude Code 批量测试(15 分钟)—— 方法 1 Step 3-5 + ↓ +5. 安全规则测试(5 分钟)—— 方法 1 Step 4 + ↓ +6. 边界情况测试(15 分钟)—— 方法 3 + ↓ +7. AI 判断测试(10 分钟)—— 方法 1 Step 7 + ↓ +8. 回滚测试(5 分钟)—— 方法 1 Step 6 + ↓ +9. 填写测试报告(10 分钟) +``` + +**总耗时**:约 80 分钟 + +--- + +## 📞 测试支持 + +遇到问题时: + +1. **查阅文档**: + - [SKILL.md](./SKILL.md) — 工作流总览 + - [references/gitlink-stale-scan.md](./references/gitlink-stale-scan.md) — 扫描算法 + - [references/gitlink-stale-judge.md](./references/gitlink-stale-judge.md) — AI 判断规则 + - [references/gitlink-stale-actions.md](./references/gitlink-stale-actions.md) — 应用手册 + - [references/gitlink-stale-exempt.md](./references/gitlink-stale-exempt.md) — 豁免规则 + +2. **查阅示例**: + - [examples/weekly-cleanup-workflow.md](./examples/weekly-cleanup-workflow.md) — 完整工作流 + - [examples/pr-stale-workflow.md](./examples/pr-stale-workflow.md) — PR 处理 + - [examples/ai-judgment-demo.md](./examples/ai-judgment-demo.md) — AI 判断演示 + +3. **运行自动化脚本**: + - 见本文档 §方法 4 + +4. **直接询问 Claude Code**: + ``` + 我在测试 gitlink-stale 时遇到 <具体问题>,请帮我诊断。 + ``` + +--- + +## ✅ 通过标准 + +测试要算"通过",必须满足: + +- [ ] **33 个测试用例**全部通过(或失败项有合理的 workaround) +- [ ] **无安全规则违反**(dry-run 被绕过、未确认就写入等) +- [ ] **白名单豁免有效**(pinned/security/roadmap Issue 永不处理) +- [ ] **AI 判断准确率 ≥ 80%**(20+ Issue 上测试) +- [ ] **Claude Code 集成可用**(自然语言指令能触发完整工作流) +- [ ] **测试报告完整填写**(含截图证据) + +达到以上标准即可认为是生产就绪的 Skill。