DEV Community

Ian Johnson
Ian Johnson

Posted on

How to Write a Ticket an Agent Can Act On

The ticket said "fix the login bug." The agent shipped a 400-line PR that touched the password reset flow, the session middleware, and a feature flag I had not heard of. None of the changes were technically wrong. None of them fixed the bug.

The bug was a typo in an error message.

The agent did exactly what the ticket asked. The ticket asked for a vague thing, so the agent did a vague thing. The fault was not in the model. The fault was in the spec.

This is the failure mode most teams write off as "the agent got confused." The agent did not get confused. The agent treated the ticket as a contract and delivered against it. The contract was bad.

The literal-minded reader

A human picking up that ticket would have done one of two things. They would have asked which login bug, or they would have looked at recent bug reports and figured it out from context. Either way, the gap between "fix the login bug" and the actual fix gets closed by judgment, intuition, and the cheap cost of a Slack message.

The agent has none of that. It has the ticket, the codebase, and whatever context the harness loads. It does not know which login bug. It does not have a relationship with the bug reporter. It does not pattern-match to "the typo Sarah mentioned in standup." It reads what is on the page, makes a reasonable interpretation, and goes.

If the ticket leaves room for interpretation, the agent will interpret. If the interpretation is wrong, the PR will be wrong. The cost of a vague ticket has gone up, because the consumer is no longer a human who can ask.

The shape of the fix is to write tickets that do not require interpretation.

A ticket as a contract

A contract has a few properties. It names what is in scope. It names what is out of scope. It names the test for success. It names the parties and the constraints.

A good agent ticket has the same shape.

What changes. Name the behavior that will be different after the ticket is done. Not "fix the bug." "The error message on the login page should read 'Invalid email or password' instead of 'Invalid emial or password.'" The change is observable. The agent can verify it.

What stays the same. Name the things you do not want touched. The password reset flow, the session middleware, the feature flag system. The agent reads the negatives and stays out. Without them, the agent treats every related file as fair game, and a 10-line fix becomes a 400-line PR.

The acceptance criteria. A list of conditions, each verifiable. The error message displays correctly. The existing tests still pass. A new test asserts on the corrected string. No other files are modified. The agent can read the list, do the work, and check each item against the diff.

The shape of the test. This is the part most teams skip. If the test were to exist, what would it assert? The agent does not have to guess at the verification strategy. The strategy is in the ticket.

A ticket with those four pieces is a ticket the agent can finish without asking. The interpretation has been moved from the agent's reasoning to the ticket itself, which is where it belongs.

What out-of-scope buys you

The single highest-leverage section of an agent ticket is the explicit out-of-scope list. It is also the section that feels most awkward to write, because it reads like distrust.

It is not distrust. It is precision.

When the ticket does not say "leave the session middleware alone," the agent does a quick scan, notices a thing it could improve, and improves it. From the agent's point of view, this is helpful. From the reviewer's point of view, this is a 300-line diff hiding a 10-line bug fix.

The out-of-scope section is the cheapest way to bound the blast radius. Five lines in the ticket prevent two hours in review. The line "do not modify files outside src/auth/login.tsx" carries more weight than any in-CLAUDE.md rule, because it is task-specific. The harness cannot guess where the boundary lives. The ticket can.

I have started writing out-of-scope sections for every non-trivial agent task, even when the in-scope is obvious. The discipline forces me to think about what the change is not, which often surfaces hidden assumptions before the agent does.

Starting from the test

The strongest tickets I write start from the test, not the feature.

A test names the behavior in the language of cause and effect. "When the user submits the login form with an invalid email, the page displays 'Invalid email or password' in a red alert below the form." There is no room for interpretation. The test fails or passes. The agent's job is to make it pass.

If I cannot articulate the test, I do not yet know what I am asking for. That is the signal to stop and figure it out before I write the ticket. Writing a vague ticket and hoping the agent will refine it is how the 400-line PRs happen.

Starting from the test also has a second benefit. The test usually points at exactly one file, or at most a handful. The scope falls out of the test naturally. The agent does not need to be told to stay in scope, because the scope is already defined by what the test exercises.

The file paths matter

Every ticket I write now includes the file paths. Not "the login form." src/auth/login.tsx. Not "the session logic." src/middleware/session.ts:34-62. The agent uses the path as both an instruction and a boundary. The work happens here. Nowhere else.

The cost of including the path is twenty seconds of poking around the repo before opening the ticket. The cost of not including it is fifteen minutes of the agent searching, plus the risk that the agent finds the wrong file and works there for the rest of the session.

This is one of those discoveries that feels embarrassingly obvious in retrospect. The harness can encode a thousand conventions. None of them are as load-bearing as the line that says "edit this file."

The format I have settled on

The shape that works for me, after a few months of iteration:

**What changes**
[One or two sentences naming the observable behavior change.]

**Where**
[File paths. Specific. Including line numbers when relevant.]

**Out of scope**
- [File or area not to touch]
- [Refactor not to do]
- [Side improvement not to ship]

**Acceptance criteria**
- [Verifiable condition]
- [Verifiable condition]
- [Test that should exist]

**Notes**
[Anything the agent needs that is not in the codebase: a link to a related ticket, a constraint from a stakeholder, a deadline that affects the approach.]
Enter fullscreen mode Exit fullscreen mode

This is not a heavy template. It is five sections and most are short. A typical ticket runs twenty lines. A complex one runs forty. Either is shorter than the PR review when the spec was vague.

What this changes about how I work

Writing tickets like this is more work up front. The work pays for itself the first time, every time. The PR comes back in scope. The review takes ten minutes, not an hour. The change is the change I asked for, not the change I have to redirect three times to get to.

The discipline is also good for me. Half the time, writing the spec surfaces that I do not actually know what I want. That is information. It is cheaper to learn it before the agent has spent forty minutes producing the wrong PR than after.

The agent is a literal-minded reader. The fix is not to make the agent less literal. The fix is to write tickets that are precise enough to deserve a literal reading. That is harder for the writer. It is faster for everyone else.

The contract is the unlock.

Top comments (0)