IssuegraphSpecFAQDemoPlayGitHub
Issuegraph / Spec / v0.3.0

Rendered from SPEC.md · plain text for agents: /llms-full.txt

Issuegraph — Specification

Version: 0.3.0 (draft) Status: Draft for implementation. Not stable. Field names and semantics may change until 1.0. See Versioning.

Issuegraph is a specification for machine-readable work relationships and ordering, written directly onto the issues of an existing issue tracker. It defines a small data format (what you can write on an issue), writing rules (who writes it and when), and reading rules (how a scheduler turns a backlog into correctly ordered, safely parallel work).


1. Motivation

A list of tasks does not tell you what order to do them in.

Human teams solve ordering by talking. The knowledge — "don't start the invoice screen until the money rounding is fixed" — lives in conversations and in people's heads. That works because a team of five has a shared head. A fleet of autonomous agents has none: an agent picks an issue, reads it, and starts. If issue B secretly needed issue A to land first, nothing stops it. Best case, a wasted run. Worst case, work that looks right, passes tests, and is wrong, because it was built on an assumption about to change.

In practice, operating an autonomous development pipeline without machine-readable ordering produces a recognizable set of workarounds, each of which this specification exists to retire:

Ordering knowledge written as prose — "blocked on #123", "do this after the auth work" — is readable by humans and useless to schedulers. A scheduler cannot act on prose. Issuegraph makes the same knowledge machine-readable, in place, on the tracker the work already lives in.

2. Scope and non-goals

Issuegraph describes work. It never describes execution.

No claims, no leases, no in-progress states, no retries, no heartbeats, no logs, no "who is working on this right now." Those are properties of the system doing the work, not of the work itself, and embedding them couples the format to one executor's lifetime. The test for any proposed field: would a perfect execution system still need this written on the issue? If a sufficiently good scheduler could derive it or tolerate its absence, it does not go in.

Consequences of that test, recorded here so they are not relitigated field by field:

Issuegraph is a specification — a data format plus rules for reading and writing it. It is not a protocol (nothing exchanges messages) and does not call itself a standard (a title earned by adoption, not claimed at birth).

3. Conformance language

MUST, MUST NOT, SHOULD, and MAY are used as in RFC 2119. Two conformance roles exist:

4. The format

4.1 Location

Issuegraph data is YAML frontmatter on the issue body — the standard ----delimited block at the top, the same convention markdown files and their entire tooling ecosystem already read and write — containing a top-level issuegraph key:

---
issuegraph:
  blocked-by: [123, 124]
  priority: 1
---

Nothing bespoke is introduced here, deliberately: the delimiters, the YAML, and the top-of-document placement are the existing universal convention, chosen because every human, model, and parser already knows it. Rules:

4.2 Issue references

A reference is an opaque tracker-scoped identifier. It MAY carry a leading # sigil, and it MAY be qualified by a owner/repo prefix separated by #. All four of these are references, and readers MUST support all four:

form meaning
123 an issue in the same repository/project
"#123" the same issue — the sigil is spelling, not identity
ABC-123 a non-numeric tracker identifier (Jira, Linear, and anything else)
owner/repo#123 an issue in another repository/project

The identifier is opaque: a reader compares references, it does not interpret them. 123 and #123 denote the same issue and MUST parse to the same reference, so the sigil is never part of the stored identifier.

Opaque does not mean unconstrained, and the grammar is normative. An identifier is one or more characters from A-Z a-z 0-9 . _ -, beginning with a letter or a digit. Anything else is not a reference: a reader MUST reject it (dropping the field with a diagnostic, per 4.1) and a writer MUST refuse to emit it.

The bound exists because three separate mechanisms in this format read the identifier back, and each excludes something:

A specification that said "whatever string the tracker uses" would be unimplementable: two conforming readers would disagree about " a b ", and a conforming writer could emit a block its own reader cannot parse. So the class is stated, and this table is the conformance surface:

identifier accepted why
231 yes numeric, canonical, in range
ABC-123 yes the Jira and Linear shape
a.b_c-1 yes every permitted punctuation mark
0 no not a positive integer
007 no an all-digits identifier must be canonical
a b no whitespace
a/b no / separates owner from repo
a#b no # separates the sigil and the qualifier
a"b no would break the writer's quoted rendering
ABC:123 no outside the class — see below

An identifier is never typed. Readers MUST take a plain scalar's identifier from its source text, not from whatever a YAML schema makes of it. 1e5, 0x1F and 007 are identifiers, not numbers — a reader that materializes them first sees 100000, 31 and 7, silently pointing the edge at a different issue and bypassing the canonical-number rule above. By the same rule a token a schema would read as a boolean is still just an identifier; it will not resolve against any tracker, and an unresolvable reference blocks (6.2), which is the safe direction.

Widening the class is a spec revision, not an implementation choice. A tracker whose identifiers carry other punctuation is not modelled today, and the failure is loud on both sides rather than silent — the reader reports a dropped field and the writer throws — so a consumer learns immediately rather than filing a graph that lies. A future revision MAY widen it; the constraints it must keep are the three above.

Numeric identifiers carry one bound. An all-digits identifier MUST be a positive integer exactly representable by the host language's integer type, written canonically (no leading zeros). This is not arithmetic on the identifier — it is what makes a reference survive a re-render: a value outside that range comes back from a naive round-trip in a spelling (1e+21) no reader accepts, so an identifier that parsed cleanly would be written back unparseable. Identifiers that are not all digits carry no such bound.

#-sigil references MUST be quoted. # opens a comment in YAML, and the two unquoted spellings fail differently — one loudly, one silently:

written result
blocked-by: [#123, #124] a parse error
blocked-by: / - #123 parses to [null, null] — silently empty
blocked-by: ["#123", "#124"] the two references

The block-sequence case is why this is normative rather than advice: it is a successful parse, with no error and no warning, of a body that declares two edges and produces none. An issue whose blockers vanished reads exactly like an issue with no blockers — a park indistinguishable from a free issue. Readers SHOULD refuse a null list member rather than dropping it, for the same reason.

A qualified reference needs no quoting, because no whitespace precedes its #; writers MAY quote it anyway for consistency.

4.3 Fields

All fields are optional. An issue with no issuegraph frontmatter is a valid node with no edges and default priority.

Field Type Meaning
blocked-by list of refs These issues must reach closure before this one may start. The only hard ordering fact.
decomposed-from ref Provenance: this issue was produced by decomposing that one. Not a scheduling edge.
duplicate-of ref This issue is the same work as that one; readers ignore this issue and treat the target as canonical.
serialize-with ref This issue must not run concurrently with the referenced issue or anything transitively linked to it (see 4.3.4). No ordering implied.
together-with ref This issue and the referenced issue (and anything transitively linked) are distinct issues forming one unit of work — selected, claimed, and worked together (see 4.3.7).
priority integer 0–3 0 is most urgent. Absent means 2. Carrier precedence in 4.3.5.
evidence asserted | verified Whether the issue's factual claims were verified by the writer or merely asserted. Absent means asserted for machine-written issues.

4.3.1 blocked-by

Stored on the blocked issue, never the blocker — the fact lives where the decision ("can I start this?") is read. There is no blocks field; a reverse index is the reader's job. Everything else about sequencing reduces to this field.

4.3.2 decomposed-from

Provenance, not hierarchy. Issuegraph has no container issues: there is no issue kind that exists only to hold other issues. What legacy processes called a "tracking issue" is, under this specification, an ordinary issue whose work product was a decomposition (see 5.2) — it closes when the split is done, like any other issue. The story it used to hold is carried by decomposed-from edges and reconstructed by query. A "verify the composed whole" gate, when one is wanted, is an ordinary workable issue blocked-by all its siblings.

Because there are no containers, there is no containment-inheritance question: blocking never inherits "downward," because there is no downward.

4.3.3 duplicate-of

Points at the canonical issue. The set of duplicates is derived by readers (everything pointing, transitively, at the canonical node); it is never maintained as a roster. Writers detecting a duplicate MUST point the newer issue at the older unless there is a specific reason to prefer otherwise, and SHOULD say why when they deviate.

4.3.4 serialize-with

Expresses mutual exclusion without ordering: "these must not run at the same time, in whatever order they happen to run." The canonical use is a conflict forecast — two features known to gut the same code, where concurrent runs produce rebase disasters despite no logical dependency either way.

Semantics: the serialize group is the connected component over serialize-with edges, treated as undirected. A writer joins a group by pointing at any one existing member; it does not matter which, and chains (B→A, C→B, D→C) produce the same component as stars (B→A, C→A, D→A). Leaving a group is deleting your own field. No issue ever edits another issue's data to maintain a group, and no roster exists anywhere.

Readers MUST admit at most one actively-claimed issue per serialize group at a time (a width-1 semaphore). Direction therefore emerges at claim time: whichever member is picked up first excludes the rest until it completes. Members that close leave the active group automatically (only open issues count) but remain valid anchors for other members' edges — no cleanup is required or expected.

Accidentally linking two groups merges them. This is deliberate fail-safety: the error direction is more serialization, which costs throughput, never correctness.

serialize-with earns its place only because a conflict forecast is knowledge the scheduler cannot derive — file overlap is only computable after the diffs exist, which is after the work is done. Together with together-with (4.3.7) it forms the format's complete coordination vocabulary beyond blocked-by, and both are hard constraints with exact reader behavior. Implementers are urged to resist preference-shaped additions ("affinity," "prefer-after," "not-near"): a preference a scheduler may ignore is a field that will go stale and mislead the reader that eventually honors it.

4.3.5 priority

An integer 0–3, 0 most urgent, matching the common P0–P3 convention; absent means 2. This is the declared priority; readers compute effective priority from it (6.3).

Carrier precedence — the rule for scalar fields, the reverse of the relationship fields. The scalar fields (priority, and evidence in 4.3.6) annotate a single issue with a value a human might flip; relationship fields carry references between issues. The precedence rule: where the tracker has an established convention for a scalar field (priority labels, an evidence label pair), the native convention is canonical and the frontmatter's field is an optional mirror; where no convention is established, the frontmatter carries the value. The reasoning is the same one that keeps holds out of the format (§2, 6.8): truth belongs in the carrier people actually edit. Humans and triage tooling flip labels; nobody re-edits YAML in an issue body to bump a priority — a frontmatter-canonical rule for scalars would put the authoritative value in the carrier guaranteed to go stale. Relationship fields don't have this problem (most trackers have no native edge convention worth the name), which is why the frontmatter stays canonical for them (4.1).

Readers resolve declared priority: the tracker's established convention if one exists, else the frontmatter field, else 2. Grooming surfaces disagreements between carriers (5.4).

4.3.6 evidence

Machine-filed issues are hypotheses. Operational experience shows agents filing issues whose central claims are false — a function said to misbehave that did not, a component said to be dead that was referenced, a dependency said to exist that was absent — each caught only because the next agent re-verified before acting. A human-written issue carries implicit authority; a machine-written one has not earned it. evidence: asserted tells the next worker to verify before building; verified says the writer reproduced or otherwise confirmed the claim, and SHOULD be accompanied in the body by how.

As a scalar field, evidence follows the carrier-precedence rule (4.3.5): an executor that establishes a native convention for it (e.g. an evidence:verified label an agent flips after reproducing the claim) makes that convention canonical, with the frontmatter as fallback where none exists. The flip from asserted to verified is exactly the kind of single-value edit label conventions handle better than body-YAML edits.

4.3.7 together-with

Expresses a unit of work spanning distinct issues: "these are separate issues, but they must be selected, claimed, and worked as one." Two canonical uses:

Encoding mirrors serialize-with: symmetric, one edge per joining issue pointing at any existing member; the together group is the connected component; joining is one write on the joiner, leaving is deleting your own field; accidental linkage merges groups.

Reader semantics — a together group is one schedulable unit:

Boundaries with neighbors, in one line each: duplicate-of says same work — keep one; together-with says different work — one unit; serialize-with says either order — never overlapping; blocked-by says this order — no overlap question.

A caution symmetric to 4.3.4's: together-with inside a single repository is often a decomposition smell — if two halves of a split cannot stand alone, ask whether the split was right before coupling them back together. Its home ground is coupling that cannot be merged into one issue.

5. Writing rules

These rules bind every writer — human or machine, front-of-pipeline or back. A pipeline's grooming pass, residual filer, and triage bot are writers in exactly the sense a decomposing agent is, and emit graph data under the same rules. Writing the graph is not a special activity; it is part of writing any issue.

5.1 Write at creation time

Graph fields are written when the issue is written, by the writer that has the knowledge, not retrofitted later by someone reconstructing intent. A triage pass that assigns priority writes priority. A grooming pass that detects duplication writes duplicate-of — a dedupe verdict is an edge; discarding the relationship and keeping only a close-action throws structure away. A residual filer creating follow-up work writes blocked-by when the follow-up genuinely cannot land first.

5.2 The size rule

An issue is either small enough to work, or the only work permitted on it is decomposition.

Decomposition is a defined job with a defined deliverable: the smaller issues plus the edges between them — blocked-by where order exists, serialize-with where a conflict forecast exists, decomposed-from on every produced issue — written at split time, by the splitter. An issue that was decomposed closes when its decomposition is complete; it does not linger as a container (4.3.2).

What counts as "small enough" is executor policy (a pipeline that wants small PRs draws the line accordingly); that the line exists and that crossing it produces edges is what this specification fixes.

5.3 Closure semantics

blocked-by is satisfied by closure, of any kind. A dependent MUST NOT remain blocked on an issue that will never be worked.

However, closure kinds differ in what they imply for dependents:

5.4 Grooming obligations

A conforming grooming pass (any recurring maintenance process, human or machine) SHOULD surface:

5.5 Human gates: name the question

Work is often blocked on a person rather than on another issue: a decision to make, an approval to grant, an account action only a human can take. Two mechanisms exist, and choosing between them follows one rule: if you can name the question, make it a node; if you can't, hold the issue.

A hold that turns out to contain a specific question SHOULD be converted: file the decision issue, add the edge, drop the hold.

6. Reading rules

6.1 Graph construction

A reader parses the canonical frontmatter from each issue in its scope, resolves references, and builds: the dependency graph (blocked-by), the duplicate mapping (duplicate-of closure to canonical), the serialize components (serialize-with, undirected, union-find), and provenance (decomposed-from). Readers maintain whatever indexes they need; the format never stores derived data.

6.2 The ready set

An issue is ready when all of the following hold:

  1. it is open;
  2. it is not a duplicate (no duplicate-of, directly or transitively);
  3. every issue in its blocked-by list is closed — for members of a together group, evaluated over boundary-crossing edges only (4.3.7);
  4. its serialize group (if any) has no actively-claimed member;
  5. if it belongs to a together group, every other open member also satisfies 1–4 (the group is ready as a unit or not at all).

Issues that are not ready MUST be invisible to selection. This retires pre-claiming: unready work needs no protection from an eager scheduler, because the scheduler cannot see it.

6.3 Effective priority

Importance flows backward along blocking edges. If a minor issue blocks an urgent one, it is not minor — it is the most urgent thing in the system, because nothing else unblocks the urgent one.

The effective priority of an issue is the highest declared priority (numerically lowest value) among itself and every open issue that transitively depends on it through blocked-by. A together group's effective priority is the highest over its members. Readers MUST select by effective priority, not declared priority. A naive scheduler that sorts by declared priority finds the urgent item blocked, moves on to the next-most-urgent thing it can do, and leaves the actual critical path at the bottom of the queue while everything looks busy.

6.4 Selection

A reader selects work by: effective priority among ready, eligible issues, newest first as the default tiebreak.

Readiness is the graph's verdict (§6.2); eligibility is the executor's (§6.8). Both must hold. Together groups enter selection as single units: one candidate, one claim, group effective priority (4.3.7).

Why newest-first. The tiebreak's old job — starvation avoidance — is done better by the graph itself: an old issue that matters blocks something, and effective priority hands it the urgency of whatever waits on it. What a tiebreak can still buy is premise freshness: issue bodies rot as the code moves, and newer issues describe the codebase that exists. An executor MAY substitute another deterministic tiebreak where its domain argues for one; what is NOT conformant is a non-deterministic pick.

This is deliberately a cheap query: one indexed pass, incrementally recomputable on events (an issue closed, an edge written). "Issue closed" is the event that moves the frontier: dependents whose last blocker closed become ready at that moment — no polling.

6.5 Parallelism

Ready issues are safe to run concurrently by construction — anything unsafe is either blocked (not ready) or serialized (group admission). How many ready issues to dispatch at once is scheduler policy, out of scope (§2).

6.6 Cycles

A blocked-by cycle is detected on read and surfaced as a stuck group; writers are not required to prevent it at write time. Write-time rejection pushes writers into describing the dependency in prose instead, which is strictly worse than a cycle a groomer can see. Issues in a cycle are not ready.

The cycle graph is over schedulable units, not over issues. A together group is one unit of work (4.3.7), so a blocker on any member blocks every member: contract each group to a single vertex before searching, using boundary-crossing edges only. Internal blocked-by edges stay advisory, exactly as they are for readiness — they would make every group carrying its own ordering read as stuck. A self-loop on a single issue is unaffected, because a lone issue is its own unit.

Without the contraction a real deadlock has no surface at all. #1 blocked-by #2, #3 blocked-by #1, #2 together-with #3 can never start — #2 waits on its partner #3, #3 waits on #1, #1 waits on #2 — yet every issue reports an ordinary open blocker and the stuck group is empty. That is precisely the state detect-on-read exists to prevent, so a reader that does not contract is not conforming.

A stuck group is reported as issue keys, every open member of every unit in it. A groomer needs issues it can open.

A reader MAY also carry the group's ordered walk, in blocked-by direction — a → b reads a waits on b — with each member named once and the return edge to the head implied. It starts at the lexicographically smallest member key, so two reads of one backlog agree. The walk is presentational: it changes no readiness, and a reader that omits it is still conforming.

It is emitted only where the component is exactly one simple cycle over units and every unit on it holds exactly one open member. Both limits exist because the alternative is asserting an ordering that is not there. A component carrying several simple cycles has no canonical representative and one is not invented. A multi-member unit cannot go on an issue-level blocked-by arrow at all: in the worked example above the unit cycle is {1} → {2,3} → {1}, carried by 1 waits on 2 and 3 waits on 1, so an issue-level line would have to hop from #2 to #3 across the group's own together-with — which is not a dependency, and drawing it as one asserts an edge that does not exist. The cost is worth stating: that example is this section's own flagship deadlock, and it is exactly the case that gets no walk.

6.7 Unresolvable references

A blocked-by reference that cannot be resolved MUST be treated as blocking (fail-safe: unknown state is not "closed") and MUST be surfaced for grooming. Unresolvable serialize-with references contribute no linkage but are likewise surfaced.

6.8 Holds and eligibility

The ready set (§6.2) is the graph's answer to "may this start?" — it is necessary, not sufficient. Executors compose it with their own eligibility gates: tracker-native hold conventions (a needs-human label, a paused state), claim protocols, capability classifications (§2), operational switches. A held issue is ready but ineligible: invisible to selection, exactly like an unready one.

Two rules keep the composition clean:

7. Prior art

8. Versioning and stability

This is v0.3.0, a draft. It is published for implementation, not for adoption claims: the intent is to implement it against at least one real backlog with a real automated pipeline, amend it from what breaks, and only then stamp 1.0. Fields that survive contact stay; fields nobody writes get cut. Breaking changes before 1.0 are expected and will be recorded in the changelog.


Stewarded by Autonomy LLC. Licensed under Apache-2.0.