Introduction

OverviewArchitectureAgent Experience

The product

This repository

Structure

Usage

Other

Concepts

Runs and events

How one attempt at a task is recorded, and how status streams back from the machine that did the work

Not built yet. There is no Run table and no Event table. packages/database still ships a single stub model inherited from the template this repository forked. This page describes the intended shape, taken from spec/01-domain-model.md and spec/02-agent-interfaces.md.

Nothing executes inside this application. Work happens on a remote workstation running Claude Code or Codex. A Run is the record of one such attempt, and the Event timeline is how that attempt becomes visible to everyone else.

Run

One attempt at a Task by an Agent on a Machine. A Task has many Runs; a Run has exactly one Task. Runs are how "what actually happened" is reconstructed after the fact, including the attempts that failed.

FieldNotes
agentId, machineIdWho attempted it and where.
startedAt, endedAtServer timestamps, never client clocks.
statusrunning, succeeded, failed, abandoned, cancelled.
exitReasonWhy it ended, in machine-readable form.
branch, prUrlThe work product in the repository.
tokensIn, tokensOut, costUsdWhat the attempt cost.
logArtifactIdThe full log, stored as an Artifact rather than inlined.

abandoned is the status a Run reaches without anyone reporting it. A runner that stops heartbeating past the lease TTL is assumed dead, not done: the lease expires, the Run is marked abandoned, and the Task returns to todo. See approvals and leasing.

Event

An append-only timeline. Every meaningful thing that happens writes one row.

FieldNotes
actorType, actorIduser, agent or system, plus the identity.
verbtask.claimed, run.started, artifact.attached, approval.requested, and so on.
taskId, runIdWhat the event is about.
payloadjsonb, the details of that verb.
atServer timestamp.

Events are immutable. They are never updated and never deleted. One table drives three things: the realtime UI feed, the audit log, and agent catch-up.

Every state transition writes an Event inside the same transaction as the write it describes, so partial state is never observable. If the Event is missing, the write did not happen.

Catch-up and resumption

Because Event ids are sortable ULIDs, "everything after this point" is unambiguous. An agent that crashed and restarted asks for events since the last one it saw, rather than re-reading the whole task. That single query is what makes a runner resumable without any in-memory state:

whoami → open leases → tasks_get → events_stream?since=<lastEventId> → back to work

Comments

A Comment is a threaded conversation on a Task between humans and agents. Agents post progress notes there, and questions. It is the human-readable channel next to the machine-readable Event timeline. A comment posted with blocking: true and left unanswered moves the Task to blocked.

The write-back contract

The product depends on remote runners reporting honestly. The minimum a runner sends over the course of one Run:

  1. tasks_claim to get the lease and the working context.
  2. runs_heartbeat at least every leaseTtl / 3, carrying a progress percentage and a one-line human-readable status.
  3. comments_post on any decision a human would want to see, and on every blocker.
  4. artifacts_attach for the diff, the pull request link, and any screenshot proving the work.
  5. runs_finish with the true outcome: the real exit reason, the real usage numbers, and acceptance criteria checkboxes that reflect what was actually verified.
  6. documents_upsert for anything the next agent would otherwise have to rediscover.

A failed run reported as failed is worth more than a green lie. Any feature that makes it easier to report success than to report a blocker is a design defect, and reviewers reject it on that basis.

How events leave the system

Three planned paths, in order of preference:

  • Server-sent events. GET /api/v1/events/stream for REST, and a GraphQL eventAdded subscription over graphql-sse. Both take the same since cursor.
  • Webhooks. A WebhookSubscription holds a url, an events[] filter, a signing secret, and a delivery log with retries. This is how a remote runner gets woken instead of polling for work.
  • Polling. A fallback, not the design.

Details of each door are in the REST interface and the GraphQL interface.

On this page

GitHubEdit this page on GitHub