Issue Graph — Deterministic Issue Relationship Graph (Native Edges + Graph Computation + Concurrency Primitives)
Repo profile — read first. This skill is repo-agnostic;
arc is the reference implementation. It resolves the repo from the git remote
(
); its runtime scripts are referenced as
<plugin_root>/skills/issue-graph/scripts/*.ts
(the profile's
— 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 (, all REST-only + manual pagination)
Why REST-only: The cloud routine's outbound proxy blocks
GraphQL (403 "not
enabled"), and
has proxy bugs for the issues endpoint (see the "Install CLI Dependencies On-Demand" section in the root CLAUDE.md). All scripts use manual
loops to ensure consistent behavior between local and cloud routine environments.
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]
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 are handled per hold semantics |
| 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 — 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> # 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
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.
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=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,
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
after completion (TTL of 30min acts as fallback for crashes).
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 # 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.
Concurrency Design (Prevent Conflicts in Multi-Machine Parallelism)
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".
- PR-producing work: Existing two-layer safeguards remain ( advisory lock early short-circuit + deterministic branch + claim check before opening PR for hard deduplication).
- Terminal actions without branch safeguards (rollup's comment+close): fencing.
- Everything else is idempotent: Duplicate edge writes = no-op; overlapping kick windows = zero cost for duplicate injections.
Queue / Producer (Phase 4)
producer.ts — Periodic Graph Computation + Label Reconciliation (Scheduled Run)
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.
Label 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/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 | Integration Point |
|---|
| Run before Step 1: inject kicks/rollupCandidates into candidate set, deterministic SKIP for blocked issues |
| Call when creating spin-offs (edge-writing discipline); terminal action for parent rollup ( fencing) |
| producer routine (Phase 4) | Periodic + reconcile labels, only index, no work |