Skip to content
g! grite refs/grite/wal grite init

← Back to field notes

Issue tracking that doesn't pull devs out of the editor

Neul Labs · ·
workflowcli

There is a tax on every engineering hour that nobody bothers to budget for. It is the cost of leaving the editor.

You’re in the middle of a refactor. You remember there was a related issue. You hit cmd-tab. The browser is on the wrong tab. You search. The page loads. You forgot the project. You click the project. You scroll. You find the issue. You read three replies that are no longer relevant. You comment. You close the tab. You go back to the editor. You stare at the cursor for a moment trying to remember what variable you were renaming.

That whole loop is maybe ninety seconds of clock time. The cognitive cost is closer to ten minutes, because what you actually paid for was not the lookup — it was the destruction of the mental model you were carrying when you started.

We built grite around the premise that this is the wrong shape for a developer tool. The editor and the terminal are the two surfaces engineers already work in. Everything that has to happen during code time should happen there, or it shouldn’t happen during code time.

What “lives in the repo” actually means

The phrase gets thrown around to mean “we made a CLI” or “we have a markdown export.” That is not the property we are after.

Grite stores its entire state as an append-only event log inside git refs — specifically refs/grite/wal. When you clone a grite-using repository, the issues come with it. When you fetch, they update. When you push, they sync. There is no second authority to keep aligned with the code. The repo is the issue store.

The practical consequence in a workday is that every issue operation is a sub-second CLI invocation against local state. grite issue list is on the order of 10ms for a thousand issues, because it reads from a local sled materialized view. grite issue create is around 5ms — a single event appended to the WAL. You don’t notice the latency because there isn’t any.

The cognitive consequence is more important. You are never reaching across a network for a piece of state your repo could have given you. The issue ID for the bug you are fixing right now is something you can paste into a commit message without thinking about whether the tracker is up.

The unix-shape interface

The grite CLI is deliberately a small set of nouns and verbs. grite issue create, grite issue update, grite issue list, grite issue close. Locks get grite lock acquire and grite lock release. Sync is grite sync. Health is grite doctor. Most teams learn the surface in an hour, and the rest is discoverability through --help.

Two design choices matter more than they look:

Every command supports --json. That sounds like a small affordance, but it is what makes grite composable with the rest of your workflow. A grep over grite issue list --json can filter to anything. A shell function can pull the current issue ID from the branch name. An editor plugin can render the open issues for the current file in a sidebar. There is no API surface to manage, no rate limit to respect, no credential to rotate — the source of truth is on disk and machine-readable.

Every output is also human-readable. The default grite issue list produces a column-aligned table you can read straight in the terminal. The same goes for grite issue show, which renders the issue, its labels, its dependency edges, and its comment thread in a way that fits in one screen. The point is to make CLI reading as low-friction as web reading is for hosted trackers — but at terminal latency.

Editor integration without a plugin ecosystem

We thought about shipping editor plugins for the major IDEs. We decided not to. The reason is that the CLI is small enough that any editor with shell-command bindings can build the integration in fifty lines.

The pattern most teams adopt is some variation of:

# In your shell rc — show open issues for the current file
grite-here() {
  grite issue list --json \
    | jq --arg f "$1" '.[] | select(.body | contains($f))'
}

# In your editor — bind cmd-shift-i to "grite issue create" against the current selection

Once you have that, the workflow stops feeling like issue tracking at all. You hit a hotkey, type a one-line title, and the next time you git push, the issue is on every teammate’s branch. There was no app. There was no tab switch. There was no break in flow.

The thing nobody warns you about with hosted trackers is how much of the workflow is just transcription. You read code, you remember what is broken, and you type it into a form. Grite collapses that to a single command because the form was the friction.

Locking is the unsung hero

The feature we did not realise would change daily workflow as much as it has is grite lock acquire. Distributed locks with TTL leases sound like a distributed-systems primitive — and they are — but the daily-use shape is much smaller and more useful.

If three of us are paired with three agents on the same module, the question “is anybody touching parser.rs right now” goes from a slack thread to a one-line command. The lock can be held for thirty minutes with --ttl 1800. If the agent crashes, the lease expires and the next claimant gets it. If the work runs over, you renew. If you’re done, you release.

The reason it matters for editor-side workflow is that the lock can be checked in a pre-commit hook. We have seen teams configure .git/hooks/pre-commit to call grite lock check for the touched files; if the lock is held by another actor, the commit fails with a clear message. That is a coordination primitive that simply does not exist in hosted trackers, because hosted trackers do not have the latency budget to be on the hot path of every commit.

What we deliberately did not build

A few things we have been asked for repeatedly, and decided not to ship:

A web UI. There isn’t one. Grite is for repo-time work. If you need a triage view, export to Markdown and put it in a project README, or pipe grite issue list --json into the dashboard tool you already use.

A push notification surface. We do not run a server, so there is no place to push from. If you want notifications you can run grite sync on a timer and diff the WAL.

A worklog or time-tracking integration. The point of grite is that the queue is so close to the editor that “what did I do today” can be answered by grite issue list --closed --since 1d. That is the worklog.

The omissions are not laziness; they are the design. Every feature added to the CLI is one more thing a future agent or contributor has to learn. Every feature added to a hypothetical web UI is a vector for the tracker to start pulling the work out of the editor again.

The change in measurable terms

Teams that adopt grite for repo-local work tend to report two numbers. The first is the number of weekly tab switches into the tracker, which drops to roughly zero — not because the tracker is gone, but because there is no tracker to switch into. The second is the number of “stale” issues, which also drops, because the friction of creating an issue is so low that issues get created at the moment of insight rather than later, and the friction of closing them is even lower.

Both numbers are downstream of a more boring claim: when the tracker is in the repo, the tracker stops being a context switch. That is the only result we were after, and the architecture exists to make it possible. Everything else — the CRDT, the WAL, the Ed25519 signing — is in service of that one outcome.

The editor is where engineers think. Issue tracking belongs as close to that surface as we can put it. We put it inside the repo because that is as close as it gets.