issue-graph
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseIssue Graph — 确定性 issue 关系图(原生边 + 图计算 + 并发原语)
Issue Graph — Deterministic Issue Relationship Graph (Native Edges + Graph Computation + Concurrency Primitives)
Repo profile — readfirst. This skill is repo-agnostic; arc is the reference implementation. It resolves the repo from the git remote (.claude/repo-profile.md); its runtime scripts are referenced asrepo_slug(the profile's<plugin_root>/skills/issue-graph/scripts/*.ts— so they resolve wherever the plugin is checked out). Arc's own issue-number provenance for any lessons is not inlined here.plugin_root
「选择和传播都是纯计算,LLM 只负责做,不负责猜该做谁。」
GitHub 原生图(sub-issues + issue dependencies)是关系数据的唯一真相;本 skill
提供四个确定性脚本,把「哪些 issue 现在能做 / 哪些父 issue 该收尾 / 哪些 issue
被某次关闭解锁」从模型猜测变成每轮现算的图计算。图不落盘——唯一持久状态是
GitHub 本身,所以永远没有索引漂移。
设计来源、Phase 0 演练证据(149 条边、17 个 rollup 候选、fencing 实测)、并发
设计全文见 arc case-law 附录(repo-profile Case Law)。
Repo profile — readfirst. This skill is repo-agnostic; arc is the reference implementation. It resolves the repo from the git remote (.claude/repo-profile.md); its runtime scripts are referenced asrepo_slug(the profile's<plugin_root>/skills/issue-graph/scripts/*.ts— so they resolve wherever the plugin is checked out). Arc's own issue-number provenance for any lessons is not inlined here.plugin_root
"Selection and propagation are pure computation; LLMs only execute tasks, not guess which ones to execute."
The GitHub native graph (sub-issues + issue dependencies) is the single source of truth for relational data; this skill provides four deterministic scripts that turn "which issues are actionable now / which parent issues should be rolled up / which issues are unlocked by a closure" from model guesswork into per-round graph computations. The graph is not persisted — the only persistent state is GitHub itself, so there's never index drift.
The design origin, Phase 0 drill-down evidence (149 edges, 17 rollup candidates, fencing tests), and full concurrency design are available in the arc case-law appendix (repo-profile Case Law).
脚本(scripts/
,全部 REST-only + 手动分页)
scripts/Scripts (scripts/
, all REST-only + manual pagination)
scripts/为什么 REST-only:cloud routine 的出站代理挡的 GraphQL(403 "not enabled"),gh对 issues 端点也有代理 bug(根 CLAUDE.md「按需安装 CLI 依赖」节)。所有脚本手动--paginate循环,在本地与 cloud routine 行为一致。page=N
Why REST-only: The cloud routine's outbound proxy blocksGraphQL (403 "not enabled"), andghhas proxy bugs for the issues endpoint (see the "Install CLI Dependencies On-Demand" section in the root CLAUDE.md). All scripts use manual--paginateloops to ensure consistent behavior between local and cloud routine environments.page=N
graph-scan.ts — 图计算(只读,sweep 每轮调)
graph-scan.ts — Graph Computation (Read-Only, Called Every Sweep Cycle)
bash
bun <plugin_root>/skills/issue-graph/scripts/graph-scan.ts [--window-hours 2] [--pretty]输出 JSON:
| 字段 | 语义 | 消费方怎么用 |
|---|---|---|
| open ∧ 无 open blocker;顺序已按 hostname 旋转(多机错峰) | 并入 sweep 候选集; |
| 有 open blocker 的 issue + blocker 列表 | 确定性 SKIP(带原因),不再靠模型猜「轮没轮到」 |
| open ∧ 有孩子 ∧ 全部孩子已关 | 走 issue-review 的父级 rollup(fencing 互斥) |
| 近窗口关闭的 issue 反查出的 open parent / 被解锁 dependent | 无需人类 comment 直接注入候选集——这就是修「子 issue 完成后要人 bump」的机制 |
窗口默认 2h > sweep 间隔 1h:同一关闭事件被两轮看见没关系,kick 只是注入候选,
后续统一走 terminal-comment 去重 + 锁 + 认领检查,重复注入零成本。
bash
bun <plugin_root>/skills/issue-graph/scripts/graph-scan.ts [--window-hours 2] [--pretty]Output JSON:
| Field | Semantics | How Consumers Use It |
|---|---|---|
| Open ∧ no open blockers; order rotated by hostname (peak-sharding across multiple machines) | Merged into sweep candidate set; items with |
| Issues with open blockers + blocker list | Deterministic SKIP (with reason), no more model guesswork about "whether it's time" |
| Open ∧ has children ∧ all children closed | Trigger parent rollup in issue-review (with fencing mutual exclusion) |
| Open parent / unlocked dependent issues reverse-engineered from recently closed issues within the window | Directly injected into candidate set without human comments — this fixes the "need human bump after sub-issue completion" problem |
Default window is 2h > sweep interval 1h: It's okay if the same closure event is detected in two cycles; kicks only inject candidates, and subsequent steps use terminal-comment deduplication + locking + claim checks, so duplicate injections have zero cost.
link.ts — 写边(幂等,开派生 issue 时必调)
link.ts — Write Edges (Idempotent, Must Be Called When Creating Spin-Off Issues)
bash
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
里的 标记(留作 provenance),必须同时调
挂原生父子边;phase 之间有硬次序的加 。边已存在 = 幂等 OK;child
已有不同 parent = 报错停下(换 parent 是人类决定)。人手开的 issue 不带边也
没关系——孤立点走现有 label/catch-all 通道,图只增强、不替代。
<!-- spinoff-of: #N -->link.ts--blocked-bybash
bun <plugin_root>/skills/issue-graph/scripts/link.ts --parent <N> --child <M> # Parent-child edge
bun <plugin_root>/skills/issue-graph/scripts/link.ts --issue <Y> --blocked-by <X> # Dependency edgeEdge-Writing Discipline (Source of Graph Accuracy): Every time an agent creates a spin-off/derived issue, in addition to the marker in the body (for provenance), it must call to attach native parent-child edges; add for hard phase ordering. If the edge already exists = idempotent OK; if the child has a different existing parent = error and stop (changing parents is a human decision). Issues created manually without edges are fine — isolated nodes use existing label/catch-all channels, and the graph only enhances, not replaces, existing workflows.
<!-- spinoff-of: #N -->link.ts--blocked-byclaim.ts — claim-comment fencing(终态动作互斥)
claim.ts — Claim-Comment Fencing (Mutual Exclusion for Terminal Actions)
bash
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, 是 advisory(两机秒级内可双拿);
产出 PR 的工作有确定性分支碰撞兜底,但 comment + close 类终态动作(rollup)没有
任何硬兜底。comment 流是 GitHub 唯一 append-only 全序原语(comment id 单调),
先写后读、最早未过期 claim 赢——两台机器算出同一个 winner(实测:两并发
claimer,id 只差 1,仍恰好一胜一负,loser 自删 claim)。
agent:processing调用方三条纪律:① claim 前先查动作是否已做过(rollup marker / issue 已关);
② 赢了才动手,动手前最后重读一次目标状态;③ 完成后 (崩溃靠 TTL
30min 过期兜底)。
--releasebash
bun <plugin_root>/skills/issue-graph/scripts/claim.ts --issue <N> --action rollup # Exit 0=win / 3=lose
bun <plugin_root>/skills/issue-graph/scripts/claim.ts --release <claimId> # Must be called after completionRationale: Label additions have no CAS, is advisory (two machines can claim the same task within seconds); work that produces PRs has deterministic branch collision safeguards, but terminal actions like comment + close (rollup) have no hard safeguards. The comment stream is GitHub's only append-only total-order primitive (comment IDs are monotonic); first-write-then-read, the earliest unexpired claim wins — two machines will compute the same winner (tested: two concurrent claimers with comment IDs differing by 1, exactly one wins and one loses, the loser deletes its claim).
agent:processingThree rules for callers: ① Check if the action has already been performed (rollup marker / issue closed) before claiming; ② Only proceed if you win, and recheck the target state one last time before acting; ③ Call after completion (TTL of 30min acts as fallback for crashes).
--releasebackfill.ts — 存量标记迁移(一次性,幂等可重跑)
backfill.ts — Legacy Marker Migration (One-Time, Idempotent, Rerunnable)
bash
bun <plugin_root>/skills/issue-graph/scripts/backfill.ts # dry-run
bun <plugin_root>/skills/issue-graph/scripts/backfill.ts --execute # 真写(1 边/秒限速)把存量 标记迁成原生边。冲突(child 已有不同 parent)、parent 缺失、
单 parent 100 孩子上限,全部跳过并报告,绝不强写。
spinoff-ofbash
bun <plugin_root>/skills/issue-graph/scripts/backfill.ts # dry-run
bun <plugin_root>/skills/issue-graph/scripts/backfill.ts --execute # actual write (1 edge/sec rate limit)Migrates legacy markers to native edges. Conflicts (child has different existing parent), missing parents, and the 100-child per parent limit are all skipped and reported; no forced writes.
spinoff-of并发设计(多机并行防打架)
Concurrency Design (Prevent Conflicts in Multi-Machine Parallelism)
确定性选择会加剧撞车(各机算出相同 ready set + 相同排序,cron 同分钟起跑)。
四条对策,各管一面:
- 成员资格确定性,处理顺序随机化——ready set 是集合不是队列,已按 hostname 旋转输出顺序,锁竞争从"必然"变"罕见"。
graph-scan - 产出 PR 的工作:现有两层兜底不变(advisory 锁早短路 + 确定性分支
agent:processing+ 开 PR 前认领检查硬去重)。claude/issue-<N> - 无分支兜底的终态动作(rollup 的 comment+close):fencing。
claim.ts - 其余一切幂等:写边重复 = no-op;kick 窗口重叠 = 重复注入零成本。
Deterministic selection exacerbates collisions (all machines compute the same ready set + same order, cron runs at the same minute). Four countermeasures, each addressing a different aspect:
- Deterministic membership, randomized processing order — the ready set is a set, not a queue; already rotates output order by hostname, turning lock contention from "inevitable" to "rare".
graph-scan - PR-producing work: Existing two-layer safeguards remain (advisory lock early short-circuit + deterministic branch
agent:processing+ claim check before opening PR for hard deduplication).claude/issue-<N> - Terminal actions without branch safeguards (rollup's comment+close): fencing.
claim.ts - Everything else is idempotent: Duplicate edge writes = no-op; overlapping kick windows = zero cost for duplicate injections.
Queue / producer(Phase 4)
Queue / Producer (Phase 4)
producer.ts — 定期图计算 + label 对账(schedule 跑)
producer.ts — Periodic Graph Computation + Label Reconciliation (Scheduled Run)
bash
bun <plugin_root>/skills/issue-graph/scripts/producer.ts [--window-hours 2] [--dry-run]职责铁律:只算图 + 对账索引,不执行任何实质工作。 producer 挂了消费方退化回
各自跑 ,无单点。
graph-scanLabel 语义(反 label-spam:绝不给全部"未被 block"的 issue 打标——那会让
queue 视图失去信号):
| label | 含义 | 谁加 | 谁摘 |
|---|---|---|---|
| 图计算发现的可干事件(close-kick 目标 ∪ rollup 候选,排除 hold) | producer | 消费方处理完摘;producer 只清理失效(hold / 变 blocked)——kick 是瞬态事件,窗口过了不代表事没了,producer 不因"不在本轮计算里"摘 |
| 有 open blocker(确定性 SKIP 的人可见视图,稀疏) | producer | producer(状态非事件,严格全量对账) |
bash
bun <plugin_root>/skills/issue-graph/scripts/producer.ts [--window-hours 2] [--dry-run]Ironclad Responsibility Rule: Only compute graphs + reconcile indexes, never perform any substantive work. If the producer fails, consumers degrade to running individually, with no single point of failure.
graph-scanLabel Semantics (Anti Label-Spam: Never label all "unblocked" issues — this would make the queue view lose signal):
| label | Meaning | Who Adds It | Who Removes It |
|---|---|---|---|
| Actionable events detected by graph computation (close-kick targets ∪ rollup candidates, excluding holds) | producer | Consumers remove after processing; producer only cleans up invalid entries (hold / became blocked) — kicks are transient events, so producer doesn't remove just because "it's not in this round's computation" |
| Has open blockers (human-visible view for deterministic SKIP, sparse) | producer | producer (state, not event; strict full reconciliation) |
反漂移铁律
Anti-Drift Ironclad Rule
queue/label 只是索引提示,永远不是执行依据——worker 领到任务必须回 GitHub
重验(仍 open、仍 ready、无 hold、无新人类输入)。守住这条 queue 是缓存加速;
丢了就是又造一个会漂移的副本。规模上来后领取迁 AFS scheduler claim/lease queue
(Phase 5,deferred),label 保留为人类可读视图。
Queue/labels are only index hints, never execution criteria — when a worker receives a task, it must re-verify against GitHub (still open, still ready, no hold, no new human input). If this rule is upheld, the queue is a cache for acceleration; if broken, it becomes another drifting copy. At scale, task assignment will migrate to the AFS scheduler claim/lease queue (Phase 5, deferred), while labels remain as a human-readable view.
消费方
Consumers
| skill | 集成点 |
|---|---|
| Step 1 前跑 |
| 开 spin-off 时调 |
| producer routine(Phase 4) | 定期 |
| skill | Integration Point |
|---|---|
| Run |
| Call |
| producer routine (Phase 4) | Periodic |