Druware Software Designs
Druware Software DesignsNative software & developer tools · Est. 1995
ISSUES.md Alternative: In-Repo Issue Tracking for AI Coding Agents

ISSUES.md Alternative: In-Repo Issue Tracking for AI Coding Agents

Dru

If you use Claude Code, Cursor, or Windsurf on a real project, you've probably already reinvented ISSUES.md. Nobody standardized it, but it spread anyway, because it solves a real problem: give the agent a work list it can read without a network call, and a place to write back what it fixed. No server, no account, no context switch out of the editor. It lives and dies with the commit history like everything else in the repo.

It also breaks down. Not immediately — at five issues, a hand-written markdown list is completely fine. The trouble starts around fifty.

I Have Issues for macOS showing an in-repo issue tracker backlog stored as a schema-versioned JSON .issues file

Why does ISSUES.md break down at fifty issues?

A markdown checklist has no schema. Nothing enforces that every entry looks the same, so nothing keeps them looking the same. Six months in, a typical ISSUES.md looks like this:

## Bugs

- **Markdown export drops the trailing newline**
  - **Severity:** High
  - **Component:** Export

## Enhancements

- **Add a compact view for the issue list**
  - **Priority:** P2
  - **Area:** Widgets

Two sections, two field names for the same concept — Severity in one, Priority in the other; Component here, Area there. Neither entry has a stable identity, so renumbering the list breaks every cross-reference that pointed at "#12." There's no way to say "this blocks that" except in prose, which a script or an agent has to parse and hope it guessed right. And when two branches both touch the file, git resolves the conflict as text, not data — it has no idea both edits touched the same issue's status field, only that two regions of the same lines changed.

None of that is really a markdown problem. It's an anything-without-a-schema problem. You'd hit the same wall with a TODO.md or a hand-rolled YAML list. The format was never the point — the convention was right, the file just had no shape.

The insight: keep the file, add a schema

The good parts of ISSUES.md are worth keeping exactly as they are: one file, in the repository, versioned with the code, readable offline, no server, no account. What was missing was never the location — it was structure. .issues is that same idea with a schema attached: a single UTF-8 JSON file beside the code it tracks, with required structure, a version number, and a canonical byte-for-byte encoding so it reads cleanly in a git diff. Markdown doesn't disappear — the apps still export a clean ## Open / ## Resolved rendering — it just stops being the source of truth.

What a resolved issue looks like as a diff

This is the part that matters for a coding agent's workflow: closing an issue is a small, reviewable change, not a rewrite of a paragraph. Here's an agent marking a bug fixed, as a real git diff inside the .issues file:

   {
     "area" : "Export",
     "priority" : "high",
-    "status" : "open",
+    "resolution" : "Restored trailing newline in MarkdownExporter.swift:118",
+    "resolutionKind" : "fixed",
+    "resolvedAt" : "2026-02-14T16:02:00Z",
+    "status" : "resolved",
     "title" : "Markdown export drops the trailing newline",
     "type" : "bug",
     "uuid" : "3B8A0C51-2F44-4E6D-B0A7-1C9E5D4F8A02"
   }

Four lines added, one removed, sorted alphabetically at two-space indent — a change a human reviewer can approve in a pull request without leaving the diff view. The resolution string names the file and line that changed, so the audit trail from "reported" to "fixed" lives in the same document, in the same commit history, as the fix itself.

The same backlog, before and after a schema

Same two issues from the drifted markdown list above, expressed as schema'd JSON instead:

{
  "issues" : [
    {
      "area" : "Export",
      "priority" : "high",
      "status" : "open",
      "title" : "Markdown export drops the trailing newline",
      "type" : "bug",
      "uuid" : "3B8A0C51-2F44-4E6D-B0A7-1C9E5D4F8A02"
    },
    {
      "area" : "Widgets",
      "priority" : "medium",
      "status" : "open",
      "title" : "Add a compact view for the issue list",
      "type" : "feature",
      "uuid" : "7E1D2A44-9B6C-4F03-8E71-2A5C0D9B4416"
    }
  ],
  "schemaVersion" : 1
}

One field name per concept, always — area, priority, status, not a coin flip between Severity and Priority depending on who wrote the entry. Every issue carries a permanent uuid. The human-facing number you'd see in a markdown export is display-only and safe to renumber; typed relations — blocks, blockedBy, duplicateOf, parent, child, relatedTo — point at uuids, so reordering the list never breaks a reference.

What the format actually guarantees

  • schemaVersion is required, and currently 1. A file written by a newer build than yours is refused outright, rather than silently mangled. An older file is accepted.
  • Diff-friendly by construction. Pretty-printed, two-space indent, keys sorted alphabetically at every nesting level, slashes unescaped, ISO-8601 UTC timestamps at whole-second precision, a trailing newline. Encoding the same model twice produces identical bytes — the file doesn't rewrite itself on every save.
  • No field for a credential, ever. .issues files get committed, forked, and printed into CI logs. The integrations block carries only non-secret coordinates — owner, repository, project. Tokens live in the OS credential store (Keychain, Credential Manager, KWallet, libsecret), keyed off those coordinates, never the document.
  • Tolerant decoding, except where a guess would lie. Unknown keys survive round trips through older builds. Two fields are the deliberate exception: resolutionKind decodes to null rather than inventing a reason work stopped, and an unrecognized remote-link provider is preserved verbatim rather than coerced — because a provider is sync identity, and mapping an unrecognized one to github could aim a real sync at the wrong repository.

How an agent actually uses this

I Have Issues installs a skill into your project for Claude Code (.claude/commands/issues.md), Cursor (.cursor/rules/issues.mdc), and Windsurf (.windsurf/rules/issues.md) — byte-identical instructions from whichever client offers it. It teaches a four-step loop: read the file (no network call, no API token, no rate limit — the backlog is already checked out with the rest of the repo); select issues whose status is open, inProgress, or blocked; implement the fix using the description, reproduction steps, and environment notes already in the document; write the resolution back. The apps watch the file on disk and reload automatically when an agent — or a plain git checkout — changes it underneath them.

Proof the spec is real: independent implementations agree on the bytes

Anyone can claim a format is well-specified. The interesting test is whether two independently-written codebases, sharing no code, produce the same bytes from the same input. The shared C++ core, libs/issueskit, is pure C++17 standard library — no GTK, no Qt, no platform toolkit linked in. That invariant is enforced by check-portable.sh, a script that greps every #include for Be API, GTK/GLib, and Qt/KDE headers and fails the build on a hit. Run today, its test suite passed all 167 checks: 77 on the core format contract, 36 on token-store account-key derivation, 44 on sync (the library implements the flow even though no client currently exposes it), and 10 on byte-exact round-tripping.

More important than the count: the Swift and Kotlin ports are independent reimplementations of the .issues format — separate codebases, not consumers of that C++ core. That makes the written specification the actual contract, instead of "whatever one codebase happens to do." The Android suite decodes a sample file produced by the Apple app and asserts its own re-encoded output matches it byte-for-byte — genuine cross-implementation parity, not just internal self-consistency. That's a rare thing to say about a file format built by one developer.

The honest engineering story: what building on real hardware caught

Reading code doesn't find every bug; running it does. Porting to Linux surfaced problems no amount of review would have caught: the KDE client, downported from Qt 6 / KDE Frameworks 6 to Qt 5 / KDE Frameworks 5 to match a Debian 12 floor, had an illegal -- sequence inside an XML comment in its UI resource file — the kind of thing that aborts the app at startup every time — plus an ECM 5 install-directory variable that needed the exact spelling KXMLGUI5DIR. The GNOME client, downported the same way to GTK 4.8 / libadwaita 1.2, now completes meson setup, ninja, ninja install, and launch on real Debian 12 aarch64 hardware. Verifying the already-shipping Windows client turned up a genuine portability defect it had been carrying quietly: C# raw string literals inherit the source file's line endings, so on a checkout with core.autocrlf=true, the markdown export emitted CRLF line endings. Found, fixed, and pinned by a test.

To be precise about status, because overstating it would undercut the point of this whole project: GNOME and KDE build and run on Debian 12 today. Neither is packaged or released. Haiku is written but has never been compiled — no Haiku machine has been available.

Who this is for — and who it isn't

This is for a project already keeping (or wanting to keep) an in-repo backlog that an agent reads, once that backlog has outgrown a hand-written list — drifting field names, no stable IDs, painful merges. It's for developers who want their issue tracker to live in git history, in plain diffs, with no server and no account. It's for anyone building or extending an agent workflow that needs to read and write issue state deterministically instead of parsing free prose.

It is not a replacement for GitHub Issues, Jira, or Linear for a larger team that needs per-issue permissions, a web UI, notifications, or multi-team workflows. There's no server component and no web client, by design — and no real-time collaborative editing, because .issues is a file, merged the way any other source file is merged.

Availability

I Have Issues is free on macOS (App Store, macOS 14.6+, direct download also available) and free on Windows 11 (direct download, x64 and ARM64), built against the .NET 10 SDK. GNOME and KDE builds run on Debian 12 but aren't packaged for distribution yet. iOS and Android aren't available yet. The format specification itself is free and open for anyone to implement — it's a data format, not software.

FAQ

Why not just use GitHub Issues? GitHub Issues is a hosted product with a web UI, notifications, and per-issue permissions — genuinely useful for teams that need those things. .issues solves a narrower problem: an agent-readable backlog that lives in the repo, works offline, and needs no account. The two aren't mutually exclusive, but .issues doesn't try to replace GitHub Issues.

What happens on a merge conflict? The same thing that happens to any JSON file merged by git: a textual conflict if two branches touched the same or adjacent lines — resolving an issue, for instance, touches resolution, resolutionKind, resolvedAt, and status together, all next to each other once keys are sorted. The schema doesn't make conflicts rarer. It makes them legible: a conflict lands on a named field of an identifiable issue, something you can reason about and resolve deliberately, instead of a conflict buried inside a free-form paragraph.

Is it free? Yes, on every platform it currently ships on — macOS and Windows. The format spec is free and open for anyone to implement. Donations fund development; at $10,000 in total donations, the full source is released under GPLv3. A commercial licence will be available separately for anyone embedding it in a closed-source product without GPLv3's source-disclosure requirement.

Does it sync with GitHub Issues or Azure DevOps? Not currently, on any client. The format reserves fields for it, and integration data written by one client round-trips safely through another — but there's no working sync today. It's planned, not present.

Can I just hand-edit the JSON instead of using an app? Yes. It's a plain UTF-8 JSON file with a documented schema — a text editor or a script works fine. The apps exist for the sidebar-and-detail-panel workflow, not because the format requires them.

  • ihaveissues
  • macOS
  • ios
  • android
  • kde
  • liniux
  • gnome
  • claude
  • windsurf
  • issuemanagement