# An issue tracker shared by people and coding agents

> My own live product: an issue tracker where people work in the browser and coding agents work through a command-line client, on the same workspaces, issues and discussion.

Source: https://simasrazinskas.com/work/agent-issue-tracker  
Published: 2026-06-30  
Reviewed: 2026-10-07

## At a glance

- **Problem**: Coding agents need a place to read the backlog, record progress and leave results, and the people they work for need to see and review that work as it happens.
- **Constraint**: Agents retry, run in parallel and act on stale reads, so every write had to survive a retry or a concurrent human edit without losing anyone's text.
- **What I built**: One ASP.NET Core process on one SQLite file, serving browser pages and a typed API for a command-line client, with revision-checked edits, idempotency keys, browser-approved device credentials and content-free live updates.
- **Result**: A stale write returns a conflict instead of overwriting, a retried create returns the first result, and work an agent finishes lands in a review queue on its person's phone.
- **Stack**: C# · .NET 10 · ASP.NET Core Razor Pages · EF Core · SQLite · SignalR · System.CommandLine · Docker

## Overview

Coding agents need somewhere to keep track of work: what to do next, what was decided, what they finished.
This product is that place: workspaces, projects, numbered issues, labels, assignees, comments, an activity log and an inbox.
People use it in the browser, including as an installed app on a phone.
Agents use a command-line client that reads and writes the same issues through a typed API.
The repository's own agent instructions tell coding agents to track plans, progress and decisions in it.

It has been rebuilt twice.
It started as TypeScript with Drizzle on Postgres, moved to Go on Postgres in July 2026, and since 21 September 2026 runs as C# on .NET 10 with SQLite.
The .NET rebuild commit deleted about 79,000 lines and added about 11,600.
The live deployment's version endpoint reports the same commit as the repository's main branch.

## Agents sign in as their person

Earlier versions treated agents as a separate kind of actor.
They had their own identities and API keys, an MCP server, capability routing, a work-in-progress cap and a background worker that expired assignment leases.
In September 2026 I deleted all of it, about 22,000 lines, and made every actor a person.

An agent now signs in as the person it works for.
The CLI starts a device request, the person approves the displayed code in a browser where they are already signed in, and the CLI polls every 5 seconds until it receives a credential.
The request expires after 10 minutes.
The credential is stored only as a SHA-256 hash, expires after 90 days and can be revoked from the account page.

Every activity event records whether it came from the browser or the CLI.
When an issue is marked done from the CLI, the person gets a push notification ("Finished in your terminal. Tap to review.").
The issue then waits in a Review section on the home screen until the person marks it reviewed.
The queue has no flag column: it is derived from the activity log, where an issue waits while its latest state change is that person's CLI completion.

This gives oversight without an agent permission model.
The product contract now states it directly: no MCP, no agent orchestration, no automatic prioritization.

## Writes that survive retries and stale reads

Agents retry after timeouts and edit from reads that may be minutes old.
Two rules make that safe.

Issue and comment edits carry the revision the caller read.
If the stored revision has moved on, the write fails with HTTP 409, which the CLI maps to exit code 5.
The bundled agent guide tells the caller to re-read and reconcile, and never to resubmit a stale draft with the newest revision swapped in.
In the browser, the same conflict keeps the person's draft.

Creating an issue or a comment accepts an idempotency key.
The server stores the key with the user, the endpoint and a SHA-256 hash of the request body for 24 hours.
A retry with the same key and body returns the first result.
The same key with a different body returns 409.
After an uncertain result, an agent repeats the identical request with the same key and cannot create a duplicate.

## One process, one SQLite file

The .NET version runs as one container with one SQLite volume behind Traefik.
There is no separate database server, and the operations guide forbids scaling the service past one instance.

Every write goes through one in-process gate, a `SemaphoreSlim` with a single slot, around a short transaction that covers the permission check and the change together:

```csharp
await gate.Gate.WaitAsync();
try
{
    await using var tx = await db.Database.BeginTransactionAsync();
    var result = await action();
    await db.SaveChangesAsync();
    await tx.CommitAsync();
    foreach (var workspaceId in changedWorkspaces)
        await live.Notify(workspaceId);
    return result;
}
finally
{
    gate.Gate.Release();
}
```

*One writer at a time, and live notifications only after commit.*

SQLite runs in WAL mode with foreign keys on and a 15-second lock timeout.
If SQLite still reports busy or locked, the API returns 503 with a `busy` code, which the CLI maps to exit code 6, a temporary failure.
Inside the gate, issue numbers are simple: the project's counter increments in the same transaction that inserts the issue and its activity event.

Releases run from my machine with `make ship`: lint and tests, a secret scan and the image build run in parallel, then the deploy stops the app, takes a backup, migrates, starts it and checks health.
A failed migration leaves the app stopped next to its pre-release backup; there is no automatic downgrade.
Backups use SQLite's online backup API and must pass `PRAGMA integrity_check` and `PRAGMA foreign_key_check`.
The test suite has 112 test methods, including browser journeys for human and CLI sharing work and for live updates.

## Live updates that carry no content

After a write commits, the server sends a SignalR message named `changed` to the workspace's current members.
It carries only the workspace ID, or nothing for personal changes.
Open pages re-fetch what they display from the server, which re-checks membership on every read.

The page patches display regions and keyed rows in place and leaves an active editor, its selection and its revision alone.
Only a remote change to the field being edited shows a conflict notice.
If the connection drops, SignalR reconnects after 0, 2, 10 and 30 seconds.
A 20-second timer and the focus, visibility and online events also trigger a refresh, so a lost message delays a visible page by about 20 seconds at most.
For the same reason, a failed notification is only logged on the server.

Optional issue analysis by an external model follows the same rule: it never blocks a save.
Title, description, state and comment changes increment an analysis generation inside the write transaction.
An in-process worker calls the model outside any transaction, and a result is stored only if the issue's generation has not moved since.
Failed calls retry after 5 minutes.

## Limitations

- One instance and one writer at a time cap write throughput; that fits a small team, and scaling out would need a different store.
- A device credential carries the full permissions of the person who approved it; there are no scoped agent roles.
- Nothing stops two agents from starting the same issue; coordination is left to people and to issue state.
- Search matches literal substrings, so finding related work depends on trying several terms.
- List pages are not snapshots, so a list read during concurrent writes can shift between pages.
- Analysis classifies the text only: a test someone reported counts as reported, and the software itself is never run.
- The .NET rebuild started with fresh accounts and data instead of migrating; acceptable for my own product, and not a path I would take with customer data.

## Related case studies

- [One job queue instead of five ways to pick up work](https://simasrazinskas.com/work/durable-job-queue): Background work survives deploys and crashes, and retries no longer repeat customer-facing actions.
- [One governed platform for a company's internal AI automation](https://simasrazinskas.com/work/internal-ai-platform): New internal AI tools launch on one shared, access-controlled platform, not from scratch.

## Start with one process

A one-hour call costs €80. Afterwards you get a written plan, whether or not we work together.

[Book a call](https://simasrazinskas.com/book-me) · [simas@simasrazinskas.com](mailto:simas@simasrazinskas.com)
