Tickets live in the repository

2026-08-25

Every repository on this host now has a tickets page:

https://gitwal.io/project/tickets

It lists that repository's open tickets. It is read-only — narwal renders them and offers no way to change them. The tracker is ticgit; this is a window onto what ticgit wrote.

The part worth writing down is how little had to be built.

The tickets were already here

ticgit stores tickets as git-meta metadata: ordinary Git commits on refs/meta/main. Not a database, not a sidecar service — a ref, in your repository, next to your branches.

Narwal validates ref names with git check-ref-format and otherwise does not care what they are called. So refs/meta/main was always going to work: it arrives through receive-pack, lands in the write-ahead log as an entry like any other, and replays into the local cache when a node materializes the repository. We checked this against the live host before writing a line of the feature, and the monitor came back with both refs side by side:

refs: [('refs/heads/main', '2381099'), ('refs/meta/main', '874c152')]
wal entries: 3

No new storage path. No new bucket prefix. No IAM change. git push and git fetch are untouched — the tickets got here through the same code that moves your commits.

Reading it back

git-meta materializes its index as a plain tree, which makes the read boring in the best way:

project/ticgit/tickets/{uuid}/title/__value      "Per-push cost still grows…"
project/ticgit/tickets/{uuid}/status/__value     "open"
project/ticgit/tickets/{uuid}/state/__value      "new"
project/ticgit/tickets/{uuid}/tags/__set/{sha}   one blob per tag

Blobs are raw values — no encoding, no framing. Rendering the page is ls-tree -r plus cat-file, run against the repository's leased cache generation: the same on-disk bare repository that upload-pack serves.

That choice carries the freshness rule for free. A read lease only exists for a generation whose recorded ETag matches the index this request loaded from S3, so the tickets page cannot show you a stale generation for the same reason a clone cannot. It also never takes the writer lock, so a busy tracker page does not queue behind a push.

Open means status, not state

git-meta carries two fields that both sound like the answer. status is binary: open or closed. state is the granular label — new, assigned, in-progress, blocked, review, then resolved, wontfix, duplicate, invalid.

The page filters on status. Filtering on state would mean hardcoding a nine-value enum that ticgit is free to grow, and every new state ticgit adds would silently vanish from this page until someone updated a match arm. status is the field the tool itself maintains for exactly this question. state still renders, as a chip, because it is what a person actually wants to see.

The payoff

None of this is specific to our repository. The route is /{name}/tickets for any {name}. Push ticgit metadata to a repository here and it has a tickets page, immediately, with no configuration and nothing registered anywhere:

ti init
ti new --title "fix the parser"
ti push

A repository with no metadata is not an error — it gets an empty state naming those three commands. And a private repository's tickets are exactly as invisible as its refs: same visibility rule as the monitor, because they are the same data.

What it is not

It is not an issue tracker. There is no create, no comment, no close, no assign. The moment narwal writes tickets it owns a second data model beside the write-ahead log, with its own consistency story and its own way to be wrong — and the whole design here rests on there being exactly one.

ti writes. Narwal reads. That division is the feature.


remote:

blog · gitwal · AGENTS.md