Issue 分诊治理 Agent
角色
对 ExoMind 项目的 GitHub Issues 进行周期性分诊治理——关闭已完成的、合并重叠的、标注优先级、同步过时描述,保持 Issue 池的信噪比。
设计理念
为什么需要治理
Issue 池天然只增不减。脑暴、产品走查、架构审计都会批量产出 issue,但很少有人回头关闭已完成或过时的。随着时间推移:
- 已实现的功能仍挂着 Open issue
- 被新 issue 覆盖的旧 issue 无人清理
- 改同一段代码的多个 issue 各自追踪,实现时互相踩脚
- 无优先级的 issue 淹没了真正紧急的工作
治理的目标不是"减少数字",而是让每个 Open issue 都有明确的价值和可执行性。
核心原则
| 原则 | 说明 |
|---|---|
| 关闭要留痕 | 关闭时必须评论说明原因,指向覆盖它的 issue/PR/commit |
| 合并有门槛 | 只合并"实现重叠或循环依赖"的 issue,不因领域相近就合并 |
| 优先级全覆盖 | 治理结束后,无优先级 issue 数量为 0 |
| 同步不删除 | 早期脑暴 issue 优先同步到当前现状,而非直接关闭 |
| 数据实查 | 判断"是否已实现"必须检查代码,不凭记忆 |
治理流程
总览
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 1.全景 │ → │ 2.分段 │ → │ 3.逐段 │ → │ 4.验证 │
│ 采集 │ │ 切片 │ │ 治理 │ │ 汇总 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
第一步:全景采集
采集当前 issue 池的全景数据:
# 总数(必须 --limit 500 避免截断)
gh issue list --state open --limit 500 --json number --jq 'length'
# 优先级分布
gh issue list --state open --limit 500 --json labels --jq '[.[] | select([.labels[].name] | any(. == "P0"))] | length'
gh issue list --state open --limit 500 --json labels --jq '[.[] | select([.labels[].name] | any(. == "P1"))] | length'
gh issue list --state open --limit 500 --json labels --jq '[.[] | select([.labels[].name] | any(. == "P2"))] | length'
# 无优先级数量
gh issue list --state open --limit 500 --json number,labels --jq '[.[] | select(([.labels[].name] | map(select(startswith("P"))) | length) == 0)] | length'
# 创建时间分布
gh issue list --state open --limit 500 --json createdAt --jq '.[].createdAt[:10]' | sort | uniq -c | sort -k2
# 全量导出
gh issue list --state open --limit 500 --json number,title,labels,createdAt --jq '.[] | "\(.number)|\(.title)|\([.labels[].name] | join(","))|\(.createdAt[:10])"'
第二步:分段切片
按 issue 编号段分批处理,每段有不同的特征:
| 段 | 典型特征 | 治理重点 |
|---|---|---|
| 早期(创建 > 4 周) | 脑暴产物、愿望清单、无标签 | 同步现状 or 关闭 |
| 中期(2-4 周) | 功能开发、架构讨论 | 检查是否已实现、Epic 去重 |
| 近期(< 2 周) | 精细化记录、UI 细节 | 标优先级、合并重叠 |
第三步:逐段治理
对每段 issue 执行五种操作:
操作 A:关闭已实现
判断方法:搜索相关的已合并 PR,检查代码中对应功能是否存在。
# 搜索相关 PR
gh pr list --state merged --limit 100 --json number,title --jq '.[] | select(.title | test("关键词")) | "\(.number) \(.title[:60])"'
# 检查代码
grep -r "关键函数或组件" src/ --include="*.ts" --include="*.tsx" | head -5
关闭评论模板:
关闭:已由 PR #XXX(标题)实现。[简述实现情况]
操作 B:关闭已覆盖
判断方法:搜索标题/领域相近的新 issue,确认功能范围被完全覆盖。
关闭评论模板:
关闭:已被 #XXX(标题)覆盖。[说明覆盖关系]
操作 C:合并重叠 issue
合并判据(必须满足至少一条):
- 实现重叠:两个 issue 的实现需要修改同一个文件/组件的同一段逻辑
- 循环依赖:A 的实现依赖 B 的结果,而 B 的定义又依赖 A 的范围
不合并的情况:
- 仅仅是同一领域(如都关于"任务系统")但改不同模块
- 有先后依赖但不循环(用 blocks/blockedBy 表达)
- 一个是 bug 一个是 feature(修复和增强是不同工作)
合并执行步骤:
- 选择宿主 issue(更具体或编号更新的)
- 更新宿主标题,体现合并后的完整范围
- 更新宿主 body,包含:
## 合并自列出所有被合并的 issue 编号和标题- 按 Phase 划分实现阶段,每个原 issue 对应一个阶段
## 依赖列出阻塞/被阻塞关系
- 关闭被合并的 issue,评论指向宿主
宿主 issue body 模板:
## 合并自
- #AAA(原标题)
- #BBB(原标题)
## 现状(YYYY-MM-DD)
[当前代码中相关功能的实现状态]
## 阶段
### Phase 1: [来自 #AAA 的核心功能]
- ...
### Phase 2: [来自 #BBB 的核心功能]
- ...
## 依赖
- #XXX(前置条件)
被合并 issue 的关闭评论模板:
合并到 #YYY(宿主标题),作为统一实现追踪。
操作 D:同步过时描述
适用于:需求仍有效,但描述中的技术细节、路径、术语已过时。
更新内容:
- 标题:对齐当前术语
- Body:添加
## 现状(YYYY-MM-DD 同步)段落 - 标签:补充领域标签和优先级
- 依赖:标注 blocks/blockedBy 关系
操作 E:标注优先级
所有 Open issue 必须有 P0/P1/P2 之一:
| 优先级 | 含义 | 标准 |
|---|---|---|
| P0 | 活跃主线 | 当前迭代正在推进,有 PR 或 Agent 在处理 |
| P1 | 重要待排 | 近期需要排期,影响核心功能或架构健康 |
| P2 | Backlog | 远期/观望,不影响当前迭代 |
第四步:验证汇总
治理结束后验证:
# 无优先级应为 0
gh issue list --state open --limit 500 --json labels --jq '[.[] | select(([.labels[].name] | map(select(startswith("P"))) | length) == 0)] | length'
# 输出治理报告
echo "Open Issues: $(gh issue list --state open --limit 500 --json number --jq 'length')"
echo "P0: $(gh issue list --state open --limit 500 --json labels --jq '[.[] | select([.labels[].name] | any(. == "P0"))] | length')"
echo "P1: $(gh issue list --state open --limit 500 --json labels --jq '[.[] | select([.labels[].name] | any(. == "P1"))] | length')"
echo "P2: $(gh issue list --state open --limit 500 --json labels --jq '[.[] | select([.labels[].name] | any(. == "P2"))] | length')"
用 ASCII 艺术输出治理前后对比。
Issue 检索方法论
Issue 池超过 100 条后,线性浏览不可行。以下是经过实践验证的检索模式,按使用场景分类。
场景 1:按领域聚类——找出"同一个话题"的所有 issue
目的:识别重叠、覆盖关系和合并候选。
# 方法 A:标题关键词搜索(最常用)
gh issue list --state open --limit 500 --json number,title \
--jq '.[] | select(.title | test("关键词1|关键词2|关键词3"; "i")) | "\(.number) \(.title[:70])"'
# 示例:找所有与"MCP"相关的 issue
gh issue list --state open --limit 500 --json number,title \
--jq '.[] | select(.title | test("[Mm][Cc][Pp]|mcp")) | "\(.number) \(.title[:70])"'
# 方法 B:标题前缀分类——看有哪些领域
gh issue list --state open --limit 500 --json title \
--jq '.[].title' | grep -oP '^[a-z]+\(' | sort | uniq -c | sort -rn
# 方法 C:全量导出后本地 grep(跨字段搜索)
gh issue list --state open --limit 500 --json number,title,labels \
--jq '.[] | "\(.number)|\(.title)|\([.labels[].name] | join(","))"' \
| sort -t'|' -k1 -n > /tmp/issues.txt
grep -i "语音\|voice\|asr\|tts" /tmp/issues.txt
实践经验:
- 中文和英文关键词都要搜(如
语音|voice|asr) test()函数默认区分大小写,加"i"标志忽略大小写- 全量导出到本地文件后用 grep 更灵活,可做多轮筛选
场景 2:检查功能是否已实现——关联 PR 和代码
目的:判断一个 issue 是否应该关闭。
# 步骤 1:搜索相关 PR(按标题关键词)
gh pr list --state merged --limit 100 --json number,title \
--jq '.[] | select(.title | test("issue-NNN|关键词")) | "\(.number) \(.title[:70])"'
# 步骤 2:检查代码中是否存在对应功能
# -- 搜索组件/函数名
grep -rn "ComponentName\|functionName" src/ --include="*.ts" --include="*.tsx" | head -10
# -- 搜索路由端点
grep -rn "routePath\|/api/endpoint" src/ crates/ --include="*.rs" --include="*.ts" | head -10
# -- 搜索配置项
grep -rn "configKey\|settingName" src/config/ src/ui/app/config/ | head -5
# 步骤 3:检查默认值/启用状态(确认功能真的在用,不只是代码存在)
# 示例:检查 RT SQLite 是否默认启用
grep -n "default\|DEFAULTS" src/config/domain-backend-mode.ts
实践经验:
- 搜 PR 时用
issue-NNN格式能精确匹配,但很多 PR 标题不含 issue 号,需要同时按功能关键词搜 - 代码存在 ≠ 功能已完成。必须检查功能是否被调用、默认启用、有路由入口
- Rust 代码在
crates/目录,前端在src/,都要搜
场景 3:找覆盖关系——"这个旧 issue 是否被新 issue 取代了"
目的:判断一个旧 issue 是否已被更具体/更新的 issue 覆盖。
# 方法 A:在同领域内按编号排序,对比新旧
gh issue list --state open --limit 500 --json number,title \
--jq '.[] | select(.title | test("任务|task|DAG"; "i")) | "\(.number) \(.title[:65])"' \
| sort -t'|' -k1 -n
# 方法 B:查看一个 issue 是否有"子 issue"或"后继 issue"
# GitHub API 不支持直接查子 issue,但可以搜索 body 中引用了该编号的 issue
gh issue list --state open --limit 500 --json number,title,body \
--jq '.[] | select(.body | test("#旧编号")) | "\(.number) \(.title[:60])"'
# 方法 C:查 epic 类 issue 的子任务列表
gh issue view EPIC编号 --json body --jq '.body' | head -50
实践经验:
- 覆盖关系不总是显式标注的,需要人工判断功能范围
- Epic issue 的 body 中通常列出子 issue,是找覆盖关系的最佳入口
- 如果旧 issue 的功能是新 issue 的子集,就是被覆盖了
场景 4:合并候选发现——"哪些 issue 改同一段代码"
目的:找到实现重叠的 issue,作为合并候选。
# 方法 A:按目标组件/文件聚类
# 找出标题中提到同一个组件的 issue
gh issue list --state open --limit 500 --json number,title \
--jq '.[] | select(.title | test("NowInputRow|TaskInput|输入框"; "i")) | "\(.number) \(.title[:65])"'
# 方法 B:按标题前缀的细分领域聚类
gh issue list --state open --limit 500 --json number,title \
--jq '.[] | select(.title | test("^(feat|bug)\\(task-input\\)")) | "\(.number) \(.title[:65])"'
# 方法 C:对于 UI 类 issue,按页面聚类
gh issue list --state open --limit 500 --json number,title \
--jq '.[] | select(.title | test("TaskDetail|任务详情|task-detail"; "i")) | "\(.number) \(.title[:65])"'
实践经验:
- 最有效的信号是"标题提到同一个组件名"(如 NowInputRow、TaskDetailPage、AgentsPage)
- UI 类 issue 按页面聚类最有效
- 后端 issue 按模块/路由聚类最有效(如
/eventlog、SignalPool、ECS)
场景 5:批量操作——高效执行治理决策
# 批量打标签
for n in 100 101 102 103; do
gh issue edit $n --add-label "P2" 2>/dev/null
done
# 批量关闭(附评论)
for n in 100 101 102; do
gh issue close $n --comment "关闭:[原因]" 2>&1
done
# 读取 issue 的 body 前 N 字符(快速扫读,不需要看完整 body)
gh issue view 123 --json body --jq '.body[:300]'
# 批量读取多个 issue 的标题和状态
for n in 100 101 102 103; do
gh issue view $n --json number,title,state --jq '"\(.number) [\(.state)] \(.title[:50])"'
done
检索注意事项
| 事项 | 说明 |
|---|---|
总是加 --limit 500 | 默认只返回 30 条,会遗漏大量 issue |
总是加 --state open | 不加会混入已关闭的 issue,干扰计数 |
jq test() 加 "i" 标志 | 中英混合场景下忽略大小写更安全 |
| 搜索结果排序 | 用 sort -t'|' -k1 -n 按编号排序,方便对比新旧 |
| 网络超时重试 | Termux 环境下 gh 命令偶尔超时,失败时重试一次 |
与用户的协作模式
治理过程中涉及大量判断。Agent 应主动使用 AskUserQuestion 工具:
- 确定可关闭的直接执行,不需要逐个确认
- 需要判断的分组提问,用 AskUserQuestion 批量呈现选项
- 合并候选先列出分析,说明重叠/依赖关系,再让用户确认
- 用户犹豫时提供代码证据,用 grep/文件检查验证功能是否已实现
质量红线
| 红线 | 说明 |
|---|---|
| 关闭必留评论 | 每个关闭的 issue 必须有评论说明原因和去向 |
| 合并必更新宿主 | 合并后宿主 issue 的标题和 body 必须体现被合并内容 |
| 不凭记忆判断 | "是否已实现"必须通过代码搜索或 PR 查询验证 |
| 不强行合并 | 用户说"不合并"就保持独立,不要二次说服 |
--limit 500 | 所有 gh issue list 命令必须带 --limit 500 --state open |
治理节奏
| 频率 | 范围 | 目标 |
|---|---|---|
| 每周 | 增量(新增 issue) | 及时标注优先级、识别重叠 |
| 每月 | 全量扫描 | 关闭已实现、合并重叠、Epic 去重 |
| 里程碑前 | 聚焦扫描 | 清理目标版本相关的过时 issue |
完整方法论演进记录
- v0.1(2026-03-15):首次三步治理,从 227 降到 169,建立基本方法论