Skip to content

ADR 0025: Tickets

Date: 2026-11-12

Status

Proposed

This ADR is an alternative to ADR 0024. Both solve the same problem. Accept one of them, not both.

Related: ADR 0020 (agent tasks), ADR 0021 (workflows, not changed here), ADR 0022 (harness), ADR 0023 (realtime channel).

Context

Software work is tracked in Jira today. The goal is to track it in Imbi and to have agents carry each piece of work from report to verified change with no person starting each step.

The scope is set by four decisions:

  • Agents work every ticket. Imbi does not track work that only people do. People report work, answer questions, review, and approve.
  • Work starts on its own. Creating a ticket starts analysis, and analysis leads to implementation with no go from a person. If many tickets start at once and runners run short, that is a runner capacity problem and is fixed there.
  • Jira is not synced. Jira keeps running beside Imbi until the move is done. A ticket in Imbi exists only in Imbi.
  • Judgment first, proof later. An agent decides when a ticket is verified. The design must let a later decision require proof.

Five facts about the current code shape the decision:

  1. An agent task is one bounded run of one agent. It needs an agent, a budget, and a service account. closed is final (ADR 0020). Retry creates a new task that is not linked to the old one, so a finished task that needs a little more work has nowhere to put it.
  2. A task that waits holds a runner pod. A blocked chain keeps its pod until the request is resolved (ADR 0022). A wait of days for a review or a deploy cannot live inside a task.
  3. Nothing wakes an existing task. A webhook, a schedule, or a workflow step can only create a new task. Joining a new trigger to an existing task is not decided (ADR 0020).
  4. A workflow run cannot hold a ticket. A run is an acyclic graph that runs once. No step waits for an outside event or for a person, and a closed run is final (ADR 0021). ADR 0024 amends all of these.
  5. Imbi already knows pull requests and deploys. The pr-sync capability records pull requests, and deployments are recorded per project and environment.

Decision

Three parts

  • A ticket is the record of one piece of work. It lasts from the report until the change is verified, and it can reopen.
  • A manager is an agent that moves the ticket forward. It runs for a short time whenever something happens on the ticket.
  • Agent tasks do the work: analysis, implementation, review, and verification. ADR 0020 tasks do not change apart from one link.

The ticket carries the continuity, so nothing else has to. Tasks stay short and final, no workflow run has to last for weeks, and the manager keeps no state of its own.

Name

The entity is a ticket, short id TK-<n>. T-<n> is taken by agent tasks.

Storage

A new Postgres schema, tickets, managed with the same tooling as agent_runtime:

Table Holds
tickets One row per ticket: identity, fields, status, stage, owner, origin, budgets, last_seq
events The append-only log of each ticket
asks One row per question put to a person, and its answer
ticket_projects One row per ticket and project, with what added it
pull_request_links One row per pull request linked to a ticket, with how it was matched
id_sequences One counter per organization for short ids

Rows refer to graph nodes (organization, project, user, agent) by id. Application code validates those ids on write.

A state change and the event that records it commit in one transaction, as in ADR 0020. Every event is published through Iggy to the ClickHouse table ticket_events after its transaction commits. Events stay in Postgres, because a ticket can reopen. There is no archive sweep in v1.

The ticket

  • short_id: TK-<n>, from an atomic per-organization counter.
  • title, description, labels, priority, links. All editable. priority is one of highest, high, medium, low, lowest, or unset. links holds URLs, such as the Jira issue a ticket came from.
  • status and stage: see States and stages.
  • owner_id: the person accountable for the ticket. Never empty. Defaults to the creator.
  • reporter_id: the person the work is for.
  • manager_agent_id: the agent that manages the ticket. Defaults to the organization's manager agent.
  • Projects: any number, in ticket_projects, with no primary. A ticket can start with none. Analysis names them, and linking a pull request adds its project.
  • origin: how the ticket was created. Set at creation and immutable. One of human (with the channel), webhook, schedule, or task (an agent created it).
  • budget and manager_budget: see Guardrails.
  • closed_at, closed_by.

Events

Ticket events use the envelope of ADR 0020's TaskEvent without session_id. Standard types:

Type Payload
ticket.created Title, description, origin, owner
field.changed Field, from, to. One event per field
status.changed From, to, reason
stage.changed From, to, reason
comment.added Body, author
project.added / project.removed Project, source, reason
pull_request.linked / pull_request.unlinked Repository, number, how it was matched
pull_request.changed Repository, number, what changed: opened, checks, review, merged, closed
deploy.reached Environment, project, the deploy
released Project, version, the release tag
task.started Task short id, agent, who started it
task.closed Task short id, agent, outcome, the task's result summary
ask.opened / ask.answered / ask.cancelled Ask id, holder, question, answer
budget.reached Which budget, the value
timer.fired Why

Readers ignore types they do not know.

States and stages

status is one of open, in_progress, done, or cancelled. It means the same thing on every ticket, so lists and reports can use it.

stage says where an open ticket is. The list is fixed and ordered in v1:

Stage What happens Leaves when
Analysis An analysis task checks that the ticket has what an implementer needs and names the projects. The manager asks the reporter about gaps The ticket is specified and has projects
Implementation One implement task per project opens pull requests. A failed check starts another implement task Every linked pull request is open with passing checks
Review A review task, a person, or both review the pull requests. Requested changes start another implement task Every linked pull request is merged
Release The ticket waits for the merge commits to be deployed, or for a release tag on a library The change is live where it will be verified
Verification A verify task checks the change where it is live The verify task reports that the change works
  • The manager sets the stage. It can move a ticket backward, for example from Verification to Implementation when the fix does not hold. Each change writes stage.changed with a reason.
  • Status follows stage. A ticket in Analysis is open. A ticket in any later stage is in_progress. done and cancelled are set by closing the ticket.
  • Merge has no stage of its own. A pull request merges once it is approved and its checks pass, so Review ends at the merge.
  • Waiting on a person is an open ask, not a stage. A ticket in Review with an open ask is still in Review.
  • Reopen. A person or the manager can reopen a done or cancelled ticket. Reopening writes status.changed with a reason, clears closed_at, and wakes the manager. The manager reads the log and picks the stage. A ticket that needs a small change goes straight to Implementation.

The manager

The manager is an ordinary agent (ADR 0020), with versions, a budget per task, and a service account. The organization names one agent as its manager. A ticket can name another, so different kinds of work can have different managers.

The manager runs when something happens. A wake event is an event that no manager task wrote, of one of these types: ticket.created, field.changed, comment.added, status.changed, project.added, project.removed, pull_request.linked, pull_request.unlinked, pull_request.changed, task.closed, ask.answered, deploy.reached, released, or timer.fired.

After a wake event commits, imbi-api creates a manager task for the ticket when both of these hold:

  • the ticket has no open manager task
  • the ticket is under its manager budget (see Guardrails)

When a manager task closes without failing, imbi-api looks for an unread wake event. If there is one and the ticket is under its manager budget, imbi-api creates the next manager task.

What counts as read. A manager task fails when it closes with any outcome other than done_acted or done_nothing_to_act_on. An event is read when a manager task that closed without failing either got it from get_ticket or wrote it. Events that a failed task got or wrote stay unread, and the next manager task gets them.

Each run starts from the ticket, not from memory. A manager task reads the ticket's current state and its unread events. It does the next thing and reports an outcome. A missed event or a second run on the same state does no harm, because the second run finds nothing to do and closes with done_nothing_to_act_on.

A run's cost does not grow with the ticket's age. get_ticket returns only what a run needs:

  • the ticket's fields and its deploy state
  • the unread events. Older events come only on request, for example on a reopen
  • open work tasks and open asks. Closed tasks, answered asks, and manager tasks are not listed. They are in the events
  • linked pull requests that are open or merged

Each list comes one page at a time, with a fixed page size. The manager asks for the next page when it needs one. A manager that read everything on every run would pass its task budget (ADR 0020) on a long ticket, and then fail the same way on every later run.

A manager task has no projects and clones nothing. Work that needs the code is a task for another agent.

A failed manager task keeps what it already did. Each tool call commits when it is made, with its event. Nothing is deferred until the task closes, and nothing is undone when the task fails. A ticket the task closed stays closed, a stage it set stays set, and a work task it started keeps running.

  • This is safe because every call passes the guardrails on its own. A close_ticket that succeeded was allowed, whatever the task did next.
  • The next manager task reads the ticket as the failed task left it. It also gets the events the failed task wrote, so it sees what was done and continues from there.
  • The ticket page shows the failure.

A failed manager task is retried later, not at once. A manager that fails every time would otherwise run in a loop. A new wake event runs the manager again, a person can run it by hand, and the timer retries it. After three failed manager tasks in a row, imbi-api opens an ask for the owner, if the ticket has no open ask about manager failures, and the timer stops retrying. The next manager task that succeeds closes that ask.

A timer covers silence and retries. A scheduler job writes timer.fired in two cases:

  • Silence. An open ticket has had no event, no open task, and no open ask for a set time. The manager then decides whether anything is stuck.
  • Retry. For a set time a ticket, open or closed, has had an unread wake event and no open manager task. The ticket must have fewer than three failed manager tasks in a row and no open ask about its manager budget. Other open asks do not block this case.

The retry case does not check the manager budget. On a ticket that has spent it, the timer.fired event is refused a manager task like any other wake event. That refusal writes budget.reached and opens the ask for the owner (see Guardrails), and the open ask then stops the timer.

Manager tools. A manager task gets these Imbi tools. Its token allows them for the ticket in its origin only.

Tool Does
get_ticket Reads the ticket's fields, open work tasks, open asks, linked pull requests, deploy state, and unread events. Returns older events and further pages on request
update_ticket Sets stage, labels, priority, and projects
comment_on_ticket Adds a comment
start_ticket_task Starts a task for a named agent on named projects, with an instruction and a budget
open_ask / cancel_ask Puts a question to a person, or withdraws one
close_ticket Sets done or cancelled, with a reason
reopen_ticket Reopens a done or cancelled ticket, with a reason

The manager agent's settings list the agents it may start. Ticket text can come from outside the organization, so the manager must not be able to start an agent that the organization did not choose for ticket work.

Tasks on a ticket

agent_runtime.tasks gains a nullable ticket_id. It is set at creation, or once on a ticketless task (see below), and never changes after that.

  • A new origin kind, ticket. A task the manager starts has origin ticket, with the ticket id and the manager task that started it. Its owner is the ticket's owner. A manager task has the same origin kind, with the ticket event that woke it.
  • Work tasks. A work task is any task on a ticket that is not a manager task: a task the manager starts, a child of one, or a task a person starts on the ticket.
  • A task with a ticket origin is the root of its own chain. A chain is a root task and its task origin children (ADR 0022). A ticket origin is not a child, so the manager's pod is not held while the work runs, and tasks for several projects run on several runners. A work task can still delegate with create_child_task, and its children copy ticket_id.
  • More work is a new task. A failed check, requested changes, a failed verification, and a reopened ticket all lead to the same action: the manager starts another task on the ticket. The new task gets its context from the ticket log and the pull request. Tasks stay final, and ADR 0020 does not change.
  • One log across tasks. When a work task closes, imbi-api writes task.closed on the ticket with the outcome and the task's result summary. Each task's own log stays the full record of that task. A manager task writes no task.started or task.closed on the ticket, so one manager run never wakes the next.
  • Worker tools. A work task gets get_ticket and comment_on_ticket for its ticket, and its prompt gets the ticket's short id.
  • A person can start a task on a ticket from the ticket page. The task records the ticket and keeps its human origin.
  • Tasks without a ticket are unchanged. A scheduled job, a webhook task, a workflow step, and a task someone runs by hand have no ticket.
  • Follow up. A closed task with no ticket gains a Follow up action. It creates a ticket from the task's title and description, links the closed task to it, and starts the manager. This replaces the unlinked retry for work that succeeded and then needed more.

ADR 0024 cites requirements F17 and I8: an agent's rework stays in its task, woken by review comments and check results. This ADR does not build that. If waking a task is built later, the manager can reply to an open task instead of starting a new one.

Asks

An ask is one question put to one person about a ticket. The manager cannot use an ADR 0020 request for this, because a blocked task holds a pod. The manager opens an ask and its task ends. The answer wakes it.

  • An ask has a holder, a question, optional choices, optional links, and a due date. To ask several people, the manager opens several asks.
  • The manager chooses the holder from the people get_ticket returns: the owner, the reporter, the requested reviewers of linked pull requests, and the owners of the ticket's projects.
  • The holder or a person with ticket:write answers, with a choice or with text. Answering writes ask.answered.
  • A person with ticket:write can hand an ask to someone else.
  • The manager can cancel an ask that no longer matters, for example a review ask whose pull request was approved on GitHub.
  • Closing or cancelling a ticket cancels its open asks.

Waiting on me

Waiting on me lists what one person must act on:

  • their open asks
  • open requests on agent tasks they own (ADR 0020)

Entries sort by ticket priority, then by how long they have waited, oldest first. The UI, Slack, and the MCP tool waiting_on_me all read this list.

When an ask opens, the slackbot sends its holder a direct message with the question and a link. A scheduler job sends each person a daily list of their open asks.

Pull requests and deploys

  • Linking. A pull request is linked to a ticket when its title, branch, or body contains the ticket's short id as a whole identifier. TK-1 does not match inside TK-10. Work tasks are told to put the short id in the branch name. A person can link or unlink one by hand. Linking adds the pull request's project to the ticket.
  • Changes. When pr-sync records a change to a linked pull request, imbi-api writes pull_request.changed if one of these changed: opened, the combined result of its checks, its review state, merged, or closed. A single check that finishes while others still run writes nothing.
  • Live. A project is live in an environment when the merge commit of every linked pull request in that project is in the project's current deploy there. The first time that becomes true, imbi-api writes deploy.reached. A library has no deploys. It writes released when a release tag contains those merge commits. This is the rule of ADR 0024, computed from the same records.
  • A pull request closed without merging stays linked and is left out of these rules.

Guardrails

The manager uses judgment. imbi-api enforces the rules that must not depend on it:

  • A ticket has at most one open manager task.
  • close_ticket with done is refused while a linked pull request is open or a work task is open. The manager task that makes the call is not a work task, so it does not block its own call.
  • Cancelling a ticket cancels its open work tasks and asks.
  • A manager task can act on its own ticket only.

Budgets. A ticket has two budgets, budget and manager_budget. Both default from organization settings. A person with ticket:write can set them on one ticket, and the manager cannot change them. The defaults are generous. They exist to stop runaway cost, such as a manager that keeps starting implement tasks against a check that never passes, or a ticket whose events keep waking the manager. They are not for planning what a ticket should cost.

  • budget limits the total cost of the ticket's work tasks, child tasks included.
  • manager_budget limits the total cost of the ticket's manager tasks.
  • Each budget is a total for the whole ticket. Every task also keeps its own task budget (ADR 0020).
  • Nothing limits the number of tasks. Cost is what matters. A work task that fails before it costs anything still costs a manager run to notice, so the manager budget bounds that loop.
  • A closed task that Follow up linked to the ticket counts toward neither budget. Its cost was spent before the ticket existed.
  • Every path that creates a task on a ticket checks the budgets. The paths are start_ticket_task, create_child_task, a person starting a task from the ticket page, and the manager task that imbi-api creates on a wake event.
  • A work task is refused once the cost of work tasks reaches budget. The manager still runs. It can close a ticket whose work is finished, cancel asks that no longer matter, and comment on why work stopped.
  • A manager task is refused once the cost of manager tasks reaches manager_budget. A ticket is under its manager budget while that cost is less. Past it, the ticket waits for the owner.
  • When a budget refuses a task and the ticket has no open ask about that budget, imbi-api writes budget.reached and opens an ask for the owner. The owner raises the budget or cancels the ticket.
  • Raising a budget closes its ask and writes field.changed, which wakes the manager. If the ticket reaches the raised budget later, the next refusal opens a new ask.
  • Tasks that are already open keep running, each within its own task budget. A ticket can pass a budget by at most the remaining task budgets of the tasks open at that moment.

Proof later. Verification is a task, and its outcome is on the ticket log. A later decision can require proof with one more rule: done is refused unless a task of a named verify agent closed with done_acted after the last merge. check.reported events (ADR 0020) can carry the evidence. Neither needs a change to this design.

Creating tickets

A ticket is created from the UI, the API, the MCP, the slackbot, a gateway webhook action create_ticket, or an agent task. Text that comes from a webhook is fenced as untrusted, as the gateway does for create_agent_task.

Permissions

Permission Allows
ticket:read List and read tickets and their events
ticket:create Create a ticket
ticket:write Edit, comment, reopen, close, answer or hand over an ask, link pull requests, start a task
ticket:delete Delete a ticket

The member role gets read, create, and write by default.

Realtime

When ADR 0023 is accepted, ticket writes publish to ticket:<org>:<short_id>, and changes to a person's Waiting on me publish to user:<user_id>.

Compared with ADR 0024

Both ADRs add a record that is separate from the agent task, with a coarse status that can reopen, a stage, an event log in the ADR 0020 envelope, a nullable link on tasks, pull request linking by short id, live computed from deploy records, and one Waiting on me list.

They differ here:

ADR 0024 This ADR
What moves the work One workflow run per work item, lasting as long as the work item A manager agent that runs briefly on each event
ADR 0021 Amended: send-backs, for each, parked steps, no run limit, four step types, workflows with no trigger Not changed
Rework The same task is woken A new task on the same ticket
Reopen The workflow starts again from the beginning The manager reads the log and picks the stage
Waiting on a person Turns, roles, and role resolvers Asks, with the holder chosen by the manager
Work only people do Supported Out of scope
Jira Synced through an issues capability Not synced
Where the process is defined A drawn workflow per work item type The manager agent's prompt
New tables 10 6

Not decided here

  • Stages that an organization defines. The list is fixed in v1.
  • Stages per environment. The manager's prompt says where a change is verified. Verifying in testing and again in production needs a decision on whether Release and Verification repeat.
  • Dispatch by ticket priority. Runners claim the oldest queued task.
  • Grouping several events into one manager run. Each wake event runs the manager as soon as no manager task is open. A delay is added only if the number of runs becomes a problem in practice.
  • A workflow as the manager. If ADR 0021 gains waits and loops, a ticket could name a workflow in place of a manager agent.
  • Moving open Jira issues into tickets.
  • Quiet hours, digests, and reminders beyond the daily list.
  • A retention policy for ticket events.

Consequences

  • The process is in a prompt, not in a drawn graph. It is easy to change and can handle cases nobody drew. It is also less predictable: two runs of the manager on the same ticket can choose differently. The guardrails bound what a wrong choice can do.
  • Every wake costs a manager task: a runner pod and a model call. A ticket takes many wakes over its life.
  • Tickets start work with no limit on how many are in progress. Runner pools are the limit. The manager agent must have no low max_concurrent_tasks, or tickets wait behind it.
  • A rework task starts with a fresh workspace and learns what happened from the ticket log and the pull request. Tasks on a ticket must leave useful result summaries and comments.
  • agent_runtime.tasks gains a nullable column and a new origin kind. Both need migrations that keep the old code working during a rolling deploy.
  • ADR 0020 and ADR 0022 keep their contracts. ADR 0021 is not touched.
  • Writes that add a ticket event also decide whether to wake the manager. That check runs inside imbi-api after the write commits.
  • Imbi does not write to Jira. While both run, a piece of work is in one system or the other, and people must know which.
  • Adding the stream means updating TOPICS, clickhouse/schemata.toml, and compose.ci.yaml IGGY_TOPICS, and restarting iggy-connect in each environment after the first deploy.