Skip to content

Anatomy of a ticket: one contract, one run record, every field

If you have read the orchestration lifecycle post, you know the unit of work in my AI build loop is a ticket: a contract with a goal, a scope, a ## Touches block, acceptance criteria, and verification commands. That post walked how a ticket is scheduled and run. Its companion, How a ticket picks its brain, zoomed all the way in on a single field -- the routing header that decides which model runs the work -- and dissected it alone.

This post does the thing both of those skipped: it opens a real ticket and its real run record and walks every field, in order, on one concrete example. Not a made-up template with <placeholder> values -- an actual merged ticket from this repository, with the real text next to the reason each field exists. If the lifecycle post was the machine and the brain-picking post was one dial, this post is the parts diagram for the single component both of them are built around.

The anchor: one real, merged, boring ticket

The example is deliberately unglamorous. It is a merged, non-bug, developer-experience infrastructure ticket that lives in the archive:

  • Ticket: the ticket archive
  • Run record: the ticket archive

Its title tells you the whole job:

TICKET: Add --fix flag to project-side dev.sh lint verbs (and a top-level lint-fix umbrella)

I picked it for three reasons. It is relatable -- a lint affordance, --fix, the kind of quality-of-life change any developer has wanted. It shipped cleanly, outcome: merged, so it walks the happy path end to end. And it needs zero game-domain knowledge to follow: you do not have to understand Bluff or Memory Match or a WebSocket protocol to read it. A boring ticket is the best possible teaching ticket, because nothing about the domain distracts from the shape of the contract.

The point of walking a real one instead of a template is that a template can only show you the slots. It cannot show you the judgment -- why this ticket set bug_heuristic: false, why its scope-out list enumerates specific verbs, why the agent later recorded a complexity that disagreed with the ticket's own prediction. The reasoning lives in the real text, not in the empty slots.

Half of it is a machine-readable header

Every ticket opens with YAML frontmatter -- a block of key: value pairs a script can parse without reading the prose. Here is the anchor's, verbatim:


state: confirmed source: owner chat 2026-06-17 -- AI-authored PRs frequently get blocked at review preflight by mechanically-fixable lint output (em-dash, curly quote, ellipsis glyph, ruff style, stale codemap auto blocks); owner wants a --fix affordance on the project-side dev.sh lint verbs analogous to ruff check --fix; confirmed 2026-06-17 priority: P2 tags: [dx, lint, scripts] agent_tier: low agent_effort: medium agent_context: normal agent_profile: autonomous bug_heuristic: false


Read top to bottom, each line is a lever the orchestrator pulls:

  • state: confirmed is the lifecycle gate. Only confirmed tickets are eligible for a batch run. A ticket left at draft is held back and silently skipped -- which is the single most common "why didn't my ticket run?" surprise.
  • source is the one-line provenance: who asked, when, and the paraphrased need. It is not decoration. It is what lets the agent make judgment calls when the spec goes quiet mid-run, and -- crucially -- it is the text a heuristic scans to decide whether this is a bug-report ticket. Note the backticked `--fix` and `ruff check --fix`: prose describing the ask, not a claim that the ticket edits ruff.
  • priority: P2 is the batch sort key. P0 is "front of the next batch," P2 is the normal "wanted, not urgent" band a DX improvement lives in.
  • tags: [dx, lint, scripts] are inline-array labels for grouping and archive filing. The inline array is the required form; the block-list YAML shape is a schema error.
  • The four agent_* fields -- agent_tier: low, agent_effort: medium, agent_context: normal, agent_profile: autonomous -- are the routing request. They describe the kind of capacity the work wants, not a vendor model, and a control plane maps them onto a concrete engine and model. That mapping is the entire subject of How a ticket picks its brain, so I will not re-derive it here; the thing to notice is that a ticket asks for low capacity and never names a model.
  • bug_heuristic: false is an explicit opt-out. This ticket's source mentions "lint" and "fixable," but it is infrastructure, not a user-visible bug fix, so the author opted out of the extra rigor a bug ticket carries. The opt-out is honest here. Setting it on a genuine bug fix to dodge that rigor would be a review-blocking defect -- the field exists to be truthful about a ticket's nature, not to duck requirements.

The header is where the ticket stops being English and becomes something the scheduler can act on. Everything below it is the part written for the agent.

The other half is a contract written for the agent

The body sections each remove one specific way an unsupervised agent goes wrong.

## Depends on is the execution graph, not narrative order. The anchor says - none, which is reserved for true foundations that can run against the primary branch with nothing merged ahead of them. A consumer ticket instead names the concrete predecessor stems whose output it needs. The orchestrator reads only this section to decide ordering; writing "do this after X" in prose does nothing.

## Goal and ## Why are the judgment fuel. The goal states one observable outcome you can check after merge -- for this ticket, that you can run the umbrella lint-fix verb from a dirty tree and have the mechanical lints correct themselves in one pass. The "why" grounds the design choices so the agent can reason when the spec is silent. They are not there so the agent has nice prose to read; they are there so it can make the right call at the fork the ticket did not anticipate.

## Scope -- in and ## Scope -- out are the most load-bearing sections for autonomous work, because scope creep is the number-one way an agent goes wrong. Scope-in is the change. Scope-out fences off the tempting adjacent cleanups -- and the anchor's scope-out is unusually strong: it enumerates the exact lint verbs that must keep refusing --fix because they require judgment rather than a mechanical substitution. That list doubles as an acceptance surface. "What this must not touch" is as important as what it must.

## Touches is a machine-readable list of the repo-root-relative path prefixes the ticket is allowed to edit. It is not documentation. The scheduler reads it and refuses to run two tickets with overlapping ## Touches in the same parallel wave, so it is both scope-at-a-glance for a reviewer and collision-avoidance for the planner. It is distinct from ## Context, which lists the read-first files: a path can be in both (read it, then edit it), but a read-only reference belongs in Context, not Touches.

## Acceptance criteria is a literal tick-through. Every item is measurable -- "exits 0 when...", "is unchanged" -- never "improved" or "better." An agent and a human can both tell whether each box is checked.

## Verification commands is where autonomy actually lives. It holds the exact command strings the agent runs to check its own work instead of asking a human. The anchor's block mixes targeted unit tests, an end-to-end smoke, negative checks that the refuse-autofix verbs bail correctly, and a full-suite regression. A ticket without this section is a ticket that has to phone home.

## Definition of rejected tells the agent when to stop -- when to throw the branch away rather than churn down a dead end. Each bullet is a shape-of-wrong-fix, mirroring the scope-out prohibitions as hard failure conditions.

## Time budget anchors on observed agent wall-clock, not human-engineer hours. The anchor budgets roughly a small-ticket window with a stuck threshold past it, plus the most likely cause of a stall so a supervisor knows what "stuck" would look like here.

That is the contract: a header a script can schedule, and a body an agent can execute and self-check without a human in the room.

The run record is what actually happened

The ticket is the promise. The run record is the receipt. Every ticket emits a *.run.md sibling in the same directory -- for the anchor, lint-verbs-add-fix-flag.run.md -- written by the implementing agent before it pushes. Its frontmatter, verbatim:


ticket: lint-verbs-add-fix-flag date_started: 2026-06-17 date_completed: 2026-06-17 agent_engine: codex agent_tier: low agent_effort: medium agent_model: gpt-5 outcome: merged complexity: medium wall_time_estimate: ~65 min


Three of these lines are quietly interesting:

  • agent_engine: codex and agent_model: gpt-5 record the resolved reality. The ticket never pinned an engine, so the control plane was free to choose; the run record captures which it chose. The request was semantic; the receipt is concrete.
  • complexity: medium is the agent's post-hoc read -- and it disagrees with the ticket. The frontmatter asked for agent_tier: low, i.e. the author expected a small job; the agent, having done the work, called it medium. That honest divergence between predicted and actual is exactly the signal calibration wants. A run record that always agreed with its ticket would be useless.
  • wall_time_estimate: ~65 min is a fallback duration used only when the orchestrator could not measure real timestamps. It is not a second prediction, and a measured elapsed time would override it.

The prose sections are the part the code itself cannot tell you. The anchor's ## Surprises / judgment calls records that the project scripts already had an unadvertised partial --fix path, and the judgment the agent made about it. Its ## Dead ends is a negative-result log -- what failed first and why -- noting a test-loader bug that was fixed without changing production code. Its ## Project agent-rule changes and ## Feedback artifact changes are required yes/no answers (both "no" here), and its ## Review findings falls through clean with "None." in each slot.

Two properties make this more than a diary. It is co-located and greppable: the run record sits beside its ticket, so "is this shipped, and what happened when it ran?" is answerable by listing one folder. And it is the per-ticket audit trail that makes an autonomous run debuggable after the fact: when you come back weeks later and wonder why a thing was built the way it was, the answer is in the dead-ends and judgment-calls sections, not in a chat window you closed.

One example cannot show every field -- and that is the honest edge

This anchor is a clean, merged, non-bug ticket. That is exactly why it is a good teaching example, and also exactly why it does not exercise every field the schema defines. It never fills in the bug-report-only sections (a root-cause hypothesis, observed-versus-expected behavior, the alternative locations an agent must rule out), because bug_heuristic: false opts out of them. It never populates the escalation-only surfaces, because it shipped clean instead of shipped-with-escalation. And because it merged in mid-2026, its run record predates a few current-schema fields, carrying author-typed dates where a current run would carry orchestrator-measured timestamps.

None of that is a gap in the post -- it is the reason the canonical reference exists. the workflow docs walks this same anchor and then, for every surface this one ticket leaves unset, borrows a short attributed excerpt from a second real archived ticket rather than inventing a value. That is the honest way to document a schema no single real ticket fully covers: show one example whole, then fill the remaining slots from other real examples, and never fabricate a field just to have something in the box.

The takeaway is the same shape as every tradeoff in this project. A ticket is two artifacts working together: a header a machine schedules and a body an agent executes, paired after the fact with a receipt that records what the work actually cost and what it ran into. The ticket says what was asked. The run record says what happened. Read side by side, they are what turns "an AI wrote this" into "here is exactly what was asked, what it did, and why" -- which is the whole difference between an autonomous system you can trust and one you cannot.


That is the anatomy: a machine-readable routing header, a contract body that fences scope and encodes its own verification, and a co-located run record that captures the honest reality of the run. If you came here from The orchestration lifecycle, this is the contract that post schedules; if you came from How a ticket picks its brain, this is the whole ticket that post's one routing field sits inside. And when you want every field annotated in place, including the bug and escalation surfaces this example does not reach, the precise reference is the workflow docs.


Or: subscribe for more posts on building autonomous AI development infrastructure.