Tickets
Responsibility
A ticket source owns ticket CRUD, lifecycle transitions, claiming, labels, blockers and creation identity. The build's pinned spec, history, transcripts and artifacts live in the BuildStore; editing a ticket after claim doesn't replace the pinned spec. See Tickets are the interface for grooming and the ready gate.
Shipped adapters
Install with bun add -d --exact @defrex/autobuild. Examples are complete minimal autobuild.toml files. Credentials go in the dispatcher environment or an ignored .env, never TOML.
| Source | Choose it for | Credential |
|---|---|---|
file | tickets you edit as plain files | none |
linear | a team already grooming in Linear | LINEAR_API_KEY |
hosted | a project tracker shared by agents and clients | saved ab connect credential |
Local files (file)
Tickets live in a writable directory, .autobuild/tickets/ by default; the default follows a local Store root and stays local with a remote Store. An explicit dir resolves from the repository root and isn't git-ignored automatically.
[tickets]
source = "file"
dir = ".tickets"
readyState = "ready"
readyLabels = []
createState = "triage"
triageState = "triage"
proposalState = "triage"
doneState = "done"
- States are directories (
.tickets/ready/,.tickets/doing/). Claims move todoing;claimedStateisn't configurable. - Claiming needs
readyState, everyreadyLabelsentry and resolved blockers;[]adds no label gate.
Initialize with bun ab init; manage with bun ab ticket states and the other ticket commands.
Linear (linear)
Export a personal API key from Linear's API setup as LINEAR_API_KEY on the dispatcher host. It needs team access and must read issues, teams, workflow states, labels and dependencies, and write issues, comments, labels and blocking relationships.
[tickets]
source = "linear"
teamKey = "ENG"
readyState = "Todo"
readyLabels = ["autobuild"]
claimedState = "In Progress"
createState = "Backlog"
triageState = "Backlog"
proposalState = "Backlog"
doneState = "Done"
Replace ENG and the case-sensitive workflow names with your team's.
- Admission.
teamKeyselects the team;readyState, everyreadyLabelsentry and optionalreadyAssigneefilter it. - Labels.
readyLabelsdefaults to["autobuild"];[]removes the gate. - No
projectkey. Scope the queue through team access, states and labels. - Rate limits are shared with your other API callers.
Hosted (hosted)
An independent tracker (not a Linear proxy) that works with local builds too. Connect the project:
bun ab connect --tickets https://tickets.example.com
The saved credential authorizes ticket operations and policy publication.
[tickets]
source = "hosted"
teamKey = "ENG"
url = "https://tickets.example.com"
readyState = "ready"
readyLabels = []
claimedState = "doing"
createState = "triage"
triageState = "triage"
proposalState = "triage"
doneState = "done"
teamKeyidentifies the registered project's team.- URL.
tickets.urldefaults to the remote BuildStore service (AB_STOREor[store].url); it's required with a local Store, as here. - Authority. A separate ticket service needs its own project credential; a Store connection grants nothing there. Delegated child sessions are read-only and can't redefine policy.
Optional ticket tree
All three sources declare integrationTree: true, storing kind and target as frontmatter (file), transactional project data (hosted), or the exact node label plus the parent relationship (linear).
- Targets resolve to canonical ids in the same source and pick the integration destination, not execution order; blockers are a separate graph. See integration.
- Updates. Omitted
kindortargetkeeps the value. New tickets default to work;kind = "work"resets kind;target = nullclears a target in the typed API (not TOML syntax). - Validation. Target existence, node kind, self/ancestor cycles, corrupt ancestor chains and demoting a node with children are checked before any write; invalid metadata rejects the whole patch.
- Enumeration.
listAlllists every state, ignoring ready filters. - Without the capability, a source keeps CRUD but must refuse kind/target writes, not drop them.
Writing your own
Implement TicketSource, register it in the manifest's ticketSources map, and select it with tickets.source. Import from @defrex/autobuild/plugin-sdk, never internal paths. Writing a plugin covers factories, registration and publishing.
An adapter must:
- preserve lifecycle, claim and blocker-completion semantics
- create idempotently under retries, with stable visible creation keys
- return valid records from
listReadywith diagnostics for malformed ones, but propagate infrastructure failures - declare revision support honestly: guarded writes reject stale revisions; unsupported guards fail explicitly
- never report success for a partial mixed update
Test with a TicketSourceContractFactory returning a TicketSourceContractHarness (source, lifecycle states, a writable fixture label, cleanup). Live fixtures reserve creation keys before creating, so cleanup survives a lost response. Run describeTicketSourceContract unchanged, or expose the fixture through the plugin contract descriptor and run:
bun ab plugin test ticket-source acme
Declare tree support only if you keep the optional tree contract on the same port and harness: exhaustive pagination, canonical targets, revision coverage and atomic rejection.