The on-disk layout
The folder hierarchy is not packaging around the data — it is part of the data model. Containment is expressed as directory nesting, which is why find answers structural questions and why a git mv is a legitimate way to restructure a graph.
The reference layout
The flagship live graph — 71 nodes, 141 link entries, 84 files — is fully hierarchical. Containment nests, all the way down.
.issues/
├── config/
│ ├── node-types.json ← 12 node types
│ └── link-types.json ← 10 verb/inverse pairs, with domain and range
└── issues/
└── Project-1/
└── issues/
└── Phase-1/
└── issues/
└── Feature-9/
└── issues/
└── Task-N/
└── issue.json
Maximum depth is 8 path segments, and 55 of the 71 nodes sit at that depth — this is not a shallow tree with one deep branch, it is a graph whose containment structure is genuinely five levels of hierarchy.
Two consequences worth noticing:
- A node's ancestry is its path. Which project a task belongs to is not a field to look up; it is the directories above it.
- Containment is the one relationship you never need an edge for. Every other relationship —
blocks,depends-on,assigned-to— is stored in the node'slinks. Containment is stored in the filesystem.
Three incompatible layouts are in circulation
This is a real interoperability problem and it is stated rather than smoothed over. Three layouts are in live use across the ecosystem, and the documentation describes a fourth.
| Layout | Where it is live | Shape |
|---|---|---|
Hierarchical issues/ | SGraph-AI__App__Send — the 71-node graph | Nested issues/<Label>/issues/…/issue.json, depth 8 |
Flat data/ | Issues-FS (24 nodes), Issues-FS__CLI (3 nodes) | One flat directory, no nesting |
| Mixed | Issues-FS__Service__UI (49 nodes) | 44 under data/, 4 under issues/, 1 at the root |
| A fourth | Nowhere | Described in the documentation only |
This site publishes the hierarchical layout as the reference, because it is the one the largest and most complete graph uses and the one that expresses containment as containment. But the practical answer for a reader is different, and it is the one that matters: run issues-fs init and use whatever it produces. Do not hand-build a tree to match a document — including this one. Which layout is canonical is open question Q2.
Why _index.json is a cache, not a source
Some graphs carry an _index.json summarising their contents. The evidence says not to trust it.
| Graph | What the index says | What is on disk |
|---|---|---|
Issues-FS/.issues/ | total_issues: 22, 6 bugs | 24 issues, 8 bugs |
SGraph-AI__App__Send/.issues/ | No _index.json at any level | 71 nodes — and everything works |
The index is stale wherever it exists, and the largest, healthiest graph in the ecosystem does not have one.
That is an argument for the design, not an embarrassment. The CLI walks the tree; it does not read an index to find nodes. So the index is a derived artefact — useful for a fast summary, never authoritative — and a stale one costs nothing because nothing depends on it. Contrast the alternative: had the index been load-bearing, the 22-versus-24 divergence would have been a bug that hid two issues from every reader. Instead it is a cache that is out of date.
The rule that follows is short: walk the tree. If you are writing a tool, do not read _index.json to enumerate nodes; glob for issue.json. If you are an agent, the same. Whether the file should exist at all is open question Q3.
Reading a graph with nothing installed
Because the layout is the model, the standard tools are a complete read-only client.
# every node in the graph
find .issues -name issue.json
# how many, by type
find .issues -name issue.json | xargs grep -h '"node_type"' | sort | uniq -c | sort -rn
# find a node by its label
grep -rl '"label": "Bug-27"' .issues
# what does Task-1 relate to
grep -A2 '"verb"' $(grep -rl '"label": "Task-1"' .issues)
# what contains this node — the path is the answer
grep -rl '"label": "Task-42"' .issues
None of that is a fallback for when the CLI is unavailable; it is the same data through a different door. It is also why the tracker survives its own tooling: a graph written by issues-fs 0.7.0 is readable by cat in ten years.
What git sees
Two properties fall out of storing one node per file, and both are reasons to prefer it to a single database file.
- A diff is legible. Changing a status changes one line in one file, and the pull request shows it. A change to a monolithic index or a binary database shows nothing a reviewer can read.
- Merges are per-node. Two agents editing two different issues do not conflict. Two agents editing the same issue conflict on the lines they both touched, which is the correct outcome and the one a human can resolve.
The cost is that a restructure is a lot of renames, and that a graph with thousands of nodes is a lot of small files. Neither has bitten yet at the sizes in use — the largest live graph is 84 files.
For an agent
Enumerate nodes by walking the tree for issue.json — never by reading _index.json, which is a cache and is stale wherever it exists (one live graph's index reports 22 issues where the disk has 24; the largest graph has no index at all). Containment is the directory structure, not an edge: a node's ancestors are the labelled directories above it. Type configuration lives in .issues/config/node-types.json and .issues/config/link-types.json. Three layouts are in circulation — hierarchical issues/ nesting, flat data/, and mixed — so detect the shape rather than assuming it, and if you are creating a graph, let issues-fs init decide the layout.