issue-graph

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Issue Graph — 确定性 issue 关系图(原生边 + 图计算 + 并发原语)

Issue Graph — Deterministic Issue Relationship Graph (Native Edges + Graph Computation + Concurrency Primitives)

Repo profile — read
.claude/repo-profile.md
first.
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's
plugin_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)。
Repo profile — read
.claude/repo-profile.md
first.
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's
plugin_root
— so they resolve wherever the plugin is checked out). Arc's own issue-number provenance for any lessons is not inlined here.
"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/
, all REST-only + manual pagination)

为什么 REST-only:cloud routine 的出站代理挡
gh
的 GraphQL(403 "not enabled"),
--paginate
对 issues 端点也有代理 bug(根 CLAUDE.md「按需安装 CLI 依赖」节)。所有脚本手动
page=N
循环,在本地与 cloud routine 行为一致。
Why REST-only: The cloud routine's outbound proxy blocks
gh
GraphQL (403 "not enabled"), and
--paginate
has proxy bugs for the issues endpoint (see the "Install CLI Dependencies On-Demand" section in the root CLAUDE.md). All scripts use manual
page=N
loops to ensure consistent behavior between local and cloud routine environments.

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:
字段语义消费方怎么用
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 去重 + 锁 + 认领检查,重复注入零成本。
bash
bun <plugin_root>/skills/issue-graph/scripts/graph-scan.ts [--window-hours 2] [--pretty]
Output JSON:
FieldSemanticsHow Consumers Use It
ready
Open ∧ no open blockers; order rotated by hostname (peak-sharding across multiple machines)Merged into sweep candidate set; items with
hasHold=true
are handled per hold semantics
blocked
Issues with open blockers + blocker listDeterministic SKIP (with reason), no more model guesswork about "whether it's time"
rollupCandidates
Open ∧ has children ∧ all children closedTrigger parent rollup in issue-review (with fencing mutual exclusion)
kicks
Open parent / unlocked dependent issues reverse-engineered from recently closed issues within the windowDirectly 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 里的
<!-- spinoff-of: #N -->
标记(留作 provenance),必须同时调
link.ts
挂原生父子边;phase 之间有硬次序的加
--blocked-by
。边已存在 = 幂等 OK;child 已有不同 parent = 报错停下(换 parent 是人类决定)。人手开的 issue 不带边也 没关系——孤立点走现有 label/catch-all 通道,图只增强、不替代。
bash
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 edge
Edge-Writing Discipline (Source of Graph Accuracy): Every time an agent creates a spin-off/derived issue, in addition to the
<!-- spinoff-of: #N -->
marker in the body (for provenance), it must call
link.ts
to attach native parent-child edges
; add
--blocked-by
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.

claim.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,
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 过期兜底)。
bash
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 completion
Rationale: Label additions have no CAS,
agent:processing
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).
Three 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
--release
after completion (TTL of 30min acts as fallback for crashes).

backfill.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 边/秒限速)
把存量
spinoff-of
标记迁成原生边。冲突(child 已有不同 parent)、parent 缺失、 单 parent 100 孩子上限,全部跳过并报告,绝不强写。
bash
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
spinoff-of
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.

并发设计(多机并行防打架)

Concurrency Design (Prevent Conflicts in Multi-Machine Parallelism)

确定性选择会加剧撞车(各机算出相同 ready set + 相同排序,cron 同分钟起跑)。 四条对策,各管一面:
  1. 成员资格确定性,处理顺序随机化——ready set 是集合不是队列,
    graph-scan
    已按 hostname 旋转输出顺序,锁竞争从"必然"变"罕见"。
  2. 产出 PR 的工作:现有两层兜底不变(
    agent:processing
    advisory 锁早短路 + 确定性分支
    claude/issue-<N>
    + 开 PR 前认领检查硬去重)。
  3. 无分支兜底的终态动作(rollup 的 comment+close):
    claim.ts
    fencing。
  4. 其余一切幂等:写边重复 = 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:
  1. Deterministic membership, randomized processing order — the ready set is a set, not a queue;
    graph-scan
    already rotates output order by hostname, turning lock contention from "inevitable" to "rare".
  2. PR-producing work: Existing two-layer safeguards remain (
    agent:processing
    advisory lock early short-circuit + deterministic branch
    claude/issue-<N>
    + claim check before opening PR for hard deduplication).
  3. Terminal actions without branch safeguards (rollup's comment+close):
    claim.ts
    fencing.
  4. 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-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 的人可见视图,稀疏)producerproducer(状态非事件,严格全量对账)
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
graph-scan
individually, with no single point of failure.
Label Semantics (Anti Label-Spam: Never label all "unblocked" issues — this would make the queue view lose signal):
labelMeaningWho Adds ItWho Removes It
agent:ready
Actionable events detected by graph computation (close-kick targets ∪ rollup candidates, excluding holds)producerConsumers 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"
agent:blocked
Has open blockers (human-visible view for deterministic SKIP, sparse)producerproducer (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集成点
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,只索引不干活
skillIntegration Point
issue-sweep
Run
graph-scan
before Step 1: inject kicks/rollupCandidates into candidate set, deterministic SKIP for blocked issues
issue-review
Call
link.ts
when creating spin-offs (edge-writing discipline); terminal action for parent rollup (
claim.ts
fencing)
producer routine (Phase 4)Periodic
graph-scan
+ reconcile
agent:ready
labels, only index, no work