Issue Graph — 确定性 issue 关系图(原生边 + 图计算 + 并发原语)
Repo profile — read
.claude/repo-profile.mdfirst. This skill is repo-agnostic; arc is the reference implementation. It resolves the repo from the git remote (repo_slug); its runtime scripts are referenced as<plugin_root>/skills/issue-graph/scripts/*.ts(the profile'splugin_root— so they resolve wherever the plugin is checked out). Arc's own issue-number provenance for any lessons is not inlined here.
「选择和传播都是纯计算,LLM 只负责做,不负责猜该做谁。」
GitHub 原生图(sub-issues + issue dependencies)是关系数据的唯一真相;本 skill 提供四个确定性脚本,把「哪些 issue 现在能做 / 哪些父 issue 该收尾 / 哪些 issue 被某次关闭解锁」从模型猜测变成每轮现算的图计算。图不落盘——唯一持久状态是 GitHub 本身,所以永远没有索引漂移。
设计来源、Phase 0 演练证据(149 条边、17 个 rollup 候选、fencing 实测)、并发 设计全文见 arc case-law 附录(repo-profile Case Law)。
脚本(scripts/,全部 REST-only + 手动分页)
为什么 REST-only:cloud routine 的出站代理挡
gh的 GraphQL(403 "not enabled"),--paginate对 issues 端点也有代理 bug(根 CLAUDE.md「按需安装 CLI 依赖」节)。所有脚本手动page=N循环,在本地与 cloud routine 行为一致。
graph-scan.ts — 图计算(只读,sweep 每轮调)
bun <plugin_root>/skills/issue-graph/scripts/graph-scan.ts [--window-hours 2] [--pretty]
输出 JSON:
| 字段 | 语义 | 消费方怎么用 |
|---|---|---|
ready |
open ∧ 无 open blocker;顺序已按 hostname 旋转(多机错峰) | 并入 sweep 候选集;hasHold=true 的按 hold 语义处理 |
blocked |
有 open blocker 的 issue + blocker 列表 | 确定性 SKIP(带原因),不再靠模型猜「轮没轮到」 |
rollupCandidates |
open ∧ 有孩子 ∧ 全部孩子已关 | 走 issue-review 的父级 rollup(fencing 互斥) |
kicks |
近窗口关闭的 issue 反查出的 open parent / 被解锁 dependent | 无需人类 comment 直接注入候选集——这就是修「子 issue 完成后要人 bump」的机制 |
窗口默认 2h > sweep 间隔 1h:同一关闭事件被两轮看见没关系,kick 只是注入候选, 后续统一走 terminal-comment 去重 + 锁 + 认领检查,重复注入零成本。
link.ts — 写边(幂等,开派生 issue 时必调)
bun <plugin_root>/skills/issue-graph/scripts/link.ts --parent <N> --child <M> # 父子边
bun <plugin_root>/skills/issue-graph/scripts/link.ts --issue <Y> --blocked-by <X> # 依赖边
写边纪律(图精确性的来源):agent 每开一个 spin-off / 派生 issue,除了 body
里的 <!-- spinoff-of: #N --> 标记(留作 provenance),必须同时调 link.ts
挂原生父子边;phase 之间有硬次序的加 --blocked-by。边已存在 = 幂等 OK;child
已有不同 parent = 报错停下(换 parent 是人类决定)。人手开的 issue 不带边也
没关系——孤立点走现有 label/catch-all 通道,图只增强、不替代。
claim.ts — claim-comment fencing(终态动作互斥)
bun <plugin_root>/skills/issue-graph/scripts/claim.ts --issue <N> --action rollup # exit 0=赢 / 3=输
bun <plugin_root>/skills/issue-graph/scripts/claim.ts --release <claimId> # 完成后必调
为什么存在:label 添加无 CAS,agent:processing 是 advisory(两机秒级内可双拿);
产出 PR 的工作有确定性分支碰撞兜底,但 comment + close 类终态动作(rollup)没有
任何硬兜底。comment 流是 GitHub 唯一 append-only 全序原语(comment id 单调),
先写后读、最早未过期 claim 赢——两台机器算出同一个 winner(实测:两并发
claimer,id 只差 1,仍恰好一胜一负,loser 自删 claim)。
调用方三条纪律:① claim 前先查动作是否已做过(rollup marker / issue 已关);
② 赢了才动手,动手前最后重读一次目标状态;③ 完成后 --release(崩溃靠 TTL
30min 过期兜底)。
backfill.ts — 存量标记迁移(一次性,幂等可重跑)
bun <plugin_root>/skills/issue-graph/scripts/backfill.ts # dry-run
bun <plugin_root>/skills/issue-graph/scripts/backfill.ts --execute # 真写(1 边/秒限速)
把存量 spinoff-of 标记迁成原生边。冲突(child 已有不同 parent)、parent 缺失、
单 parent 100 孩子上限,全部跳过并报告,绝不强写。
并发设计(多机并行防打架)
确定性选择会加剧撞车(各机算出相同 ready set + 相同排序,cron 同分钟起跑)。 四条对策,各管一面:
- 成员资格确定性,处理顺序随机化——ready set 是集合不是队列,
graph-scan已按 hostname 旋转输出顺序,锁竞争从"必然"变"罕见"。 - 产出 PR 的工作:现有两层兜底不变(
agent:processingadvisory 锁早短路 + 确定性分支claude/issue-<N>+ 开 PR 前认领检查硬去重)。 - 无分支兜底的终态动作(rollup 的 comment+close):
claim.tsfencing。 - 其余一切幂等:写边重复 = no-op;kick 窗口重叠 = 重复注入零成本。
Queue / producer(Phase 4)
producer.ts — 定期图计算 + label 对账(schedule 跑)
bun <plugin_root>/skills/issue-graph/scripts/producer.ts [--window-hours 2] [--dry-run]
职责铁律:只算图 + 对账索引,不执行任何实质工作。 producer 挂了消费方退化回
各自跑 graph-scan,无单点。
Label 语义(反 label-spam:绝不给全部"未被 block"的 issue 打标——那会让 queue 视图失去信号):
| label | 含义 | 谁加 | 谁摘 |
|---|---|---|---|
agent:ready |
图计算发现的可干事件(close-kick 目标 ∪ rollup 候选,排除 hold) | producer | 消费方处理完摘;producer 只清理失效(hold / 变 blocked)——kick 是瞬态事件,窗口过了不代表事没了,producer 不因"不在本轮计算里"摘 |
agent:blocked |
有 open blocker(确定性 SKIP 的人可见视图,稀疏) | producer | producer(状态非事件,严格全量对账) |
反漂移铁律
queue/label 只是索引提示,永远不是执行依据——worker 领到任务必须回 GitHub 重验(仍 open、仍 ready、无 hold、无新人类输入)。守住这条 queue 是缓存加速; 丢了就是又造一个会漂移的副本。规模上来后领取迁 AFS scheduler claim/lease queue (Phase 5,deferred),label 保留为人类可读视图。
消费方
| skill | 集成点 |
|---|---|
issue-sweep |
Step 1 前跑 graph-scan:kicks/rollupCandidates 注入候选集、blocked 确定性 SKIP |
issue-review |
开 spin-off 时调 link.ts(写边纪律);父级 rollup 终态动作(claim.ts fencing) |
| producer routine(Phase 4) | 定期 graph-scan + 对账 agent:ready label,只索引不干活 |