Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 12 min read

Give your coding agent a target state rather than a ticket

I was talking to a friend the other day regarding how you should approach spec work and realized that I am thinking about it in a declarative way while the rest of the world seems to be thinking imperatively. In a nutsh

I was talking to a friend the other day regarding how you should approach spec work and realized that I am thinking about it in a declarative way while the rest of the world seems to be thinking imperatively.

In a nutshell, chances are that if you want to, say, "add the email function to the product", you would think about this as a sequence of tasks: your flavor of spec, dev, QA, deploy. From my perspective, tasks are a derivative of a diff between two product states.

The current state is whatever is in production now, and its spec. The target state is the product with the email sending functions live. The diff is added/changed/removed spec items. The tasks are derived, because you can always reconstruct the tasks from the spec diff, but not vice versa.

One ticket, two readings

Let's consider a hypothetical product. An online shop refunds an order when the customer asks within 30 days of delivery. The team decides that members should get 60 days and the ticket reads: "Extend the refund window to 60 days for members." A coding agent picks it up, plans three steps, changes the eligibility check, updates the tests, and opens a pull request. The ticket closes.

The ticket names the work. What the shop does once the work ships has to be read out of the code: whether a member's request on the 61st day is refused, whether a gift-card order, refunded as store credit, gets the longer window too, and whether the refund email changes. A year later, the answer to "what are our refund rules?" is spread across every closed ticket that touched refunds, read in order.

The same change reads differently as two states of the product's specification: the refund rules as last approved, which the code is built to, and the rules as they should be once members get 60 days. The change is the difference between them and both states can live in the repository beside the code the agent works on.

Here is the ticket as the agent sees it:

Extend the refund window to 60 days for members

  • Add a membership check to refund eligibility
  • Allow 60 days for members
  • Update the eligibility tests

And here is the target state of the feature it touches, compared with the approved one.

 Feature: Return an order

-Requirement: When a customer requests a refund within 30 days of delivery, the shop shall refund the order to the payment method it was paid with.
+Requirement: When a customer requests a refund within 30 days of delivery, or within 60 days for a member, the shop shall refund the order to the payment method it was paid with.
   Criterion: A request on the 30th day after delivery is refunded.
-  Criterion: A request on the 31st day after delivery is refused.
+  Criterion: A request on the 31st day after delivery is refused for a customer who is not a member.
+  Criterion: A member's request on the 60th day after delivery is refunded.
+  Criterion: A member's request on the 61st day after delivery is refused.

 Requirement: When a refund is issued, the shop shall email the customer the refunded amount.
   Criterion: The email names the refunded amount.

 Requirement: When a customer requests a refund of an order paid by gift card, the shop shall refund the order as store credit.
   Criterion: The store credit equals the order total.

The ticket closes when the pull request merges. The target state holds the whole feature, including the two requirements the change leaves as they were, and becomes the next approved version once the branch merges.

Where today's tools start

Coding agents follow what engineers are already used to: a task as a unit of work (an issue, a ticket, a prompt, or a thread). Copilot, Codex, Devin, and Cursor can each be handed an issue from a tracker. GitHub's agent reads the issue's title, description, and comments as they stand when it is assigned, and after that it "will not be aware of" new comments (GitHub's guide to assigning an issue). Plans and to-do lists last as long as the run.

Spec-driven tools add a written specification, usually one per change. Birgitta BΓΆckeler's review of Kiro, Spec Kit, and Tessl found that Spec Kit treats a spec "as a living artifact for the lifetime of a change request, not the lifetime of a feature."

OpenSpec comes closest. Its main specs "describe how your system currently behaves," and each change carries deltas that add, modify, or remove requirements, merged in when the change is archived (OpenSpec's concepts). Its documentation gives the reason for writing deltas in place of the full spec:

"Reading a full spec, you'd have to diff it mentally against the current version."

That reason holds only while a person does the diff. OpenSpec moves it from the reader's head to the author's hand: a delta is the diff, written out. The other way round, the author writes the target state and a tool computes the diff, with the same result every time. Folding finished work back in as a separate step has a cost: a study of public OpenSpec projects found that in projects with main specs, 39% of open changes had every task ticked and had never been archived.

Operations made this move years ago. The Kubernetes API conventions ask that a resource's spec hold "the desired state, not actions intended to yield the desired state," and call the system's behavior "level-based rather than edge-based". There, a controller acts on how far the cluster is from its spec, whatever sequence of events led there. A ticket is an edge and a target state is a level.

Why tasks come second

A task list is one way of getting from the approved state to the target. It is worked out from the change, the code, and the path the team chooses, and the derivation runs in one direction only.

Tasks lose information. The approved state and the target state are enough to plan the work and usually enough for several valid plans. Going back fails: three ticked boxes record the steps of the work, and they leave out what the shop does afterward and what had to stay the same. The refund email and the gift-card rule appear nowhere in the member ticket and both are in the target state.

Here are two valid plans for the same change.

Plan A. The shop checks the window when a refund is requested.

  1. Read the customer's membership in the refund check
  2. Allow 60 days for members
  3. Test the 31st, 60th, and 61st days

Plan B. The shop stores a refund deadline on each order at delivery.

  1. Set members' deadlines at 60 days
  2. Recompute the deadlines of delivered orders placed by members
  3. Test the 31st, 60th, and 61st days

Both plans meet the same criteria. Plan B's second step comes from how the code stores deadlines. The specification is the same for both. And two requirements of the target state appear in neither plan:

  • When a refund is issued, the shop shall email the customer the refunded amount.
  • When a customer requests a refund of an order paid by gift card, the shop shall refund the order as store credit.

Once the work merges. the code holds both and an agent can read them out of it. That reading is an inference. Two agents, or one agent run twice, can describe the same code differently, and each description is an unreviewed guess. And it reports what was built. I.e. if Plan B refuses members on the 60th day through an off-by-one, the code says the 60th day is refused and a faithful reading calls that the rule.

The target state is the deterministic side. Every reader gets the same approved text, the difference is computed from it with the same result every run, and since it was written before the code, it can check the code.

Inferred from the code Read from the target state
Who produces it An agent, at each reading The team, once, reviewed and approved
Two readings Can differ Identical
The 60th-day bug Read as the rule Fails the 60th-day criterion
Available After the merge Before the code is written

Tasks also hold only from the state they were written against. In Promise Theory, Jan Bergstra and Mark Burgess call a workflow built from one-off instructions fragile, since it is "driven by a particular initial state." Suppose that while the member ticket waits, a teammate merges another change: refunds by bank transfer for orders paid by invoice, and a new name for the feature, "Request a refund." The member ticket's plan was written for a shop without invoice refunds, and nothing in it says so. The target state still holds: merge main into the member branch, and the difference is computed again against the new base.

The merge also raises one more question: does a member's order paid by invoice get 60 days as well? Bertrand Meyer made the general point in 2018: "A user story describes one case; piling up case over case does not give you a specification" ("Making Sense of Agile Methods"). Features interact, and the interactions show up only against the whole. With the invoice rule and the member window in one target state, the question comes up before any code is written.

A rename is a change

The strongest objection to working from states comes from database schema tools. Given two schemas, a tool can't tell a renamed column from a dropped column and a new one, and the wrong guess loses data. Prisma's documentation shows a renamed field turning into a migration that creates a new column and drops the old one with its data (Prisma, "Customizing migrations"). In May 2026, Atlas moved the answer into the desired state: a rename is declared with a renamed_from attribute that "lives next to the renamed resource, is reviewable in pull requests, and is resolved without prompts" (Atlas, "Declarative Schema Renames").

A specification needs the same rule. Give every feature, requirement, and criterion an ID that is written once and stays with it through every edit, and compute the difference by ID, so a renamed or reworded item is still the same item:

- id: F-7
  feature: Request a refund
  requirements:
    - id: RQ-31
      text: >-
        When a customer requests a refund within 30 days of delivery, or within
        60 days for a member, the shop shall refund the order to the payment
        method it was paid with.
      criteria:
        - id: AC-90
          text: A request on the 30th day after delivery is refunded.
        - id: AC-91
          text: A request on the 31st day after delivery is refused for a customer who is not a member.

When the teammate renamed "Return an order" to "Request a refund," the difference by ID was one changed feature. Here is the member branch compared with main after the merge:

Change ID Kind Text
added AC-112 criterion A member's request on the 60th day after delivery is refunded.
added AC-113 criterion A member's request on the 61st day after delivery is refused.
changed RQ-31 requirement When a customer requests a refund within 30 days of delivery, or within 60 days for a member, the shop shall refund the order to the payment method it was paid with.
changed AC-91 criterion A request on the 31st day after delivery is refused for a customer who is not a member.

The rename and the invoice rule came in with the merge and belong to the base, so the difference is still the member change alone.

Two differences

The change is the difference between the approved state and the target state. What has to be built is a second difference, between the target state and what the tests show that the code already meets. Keeping them apart takes one more record: a link from each acceptance criterion to the tests that check it, reviewed against the criterion's text, and a report of which criteria have no current link.

Say the shop's code already has passing tests for the 30-day rule and for store credit, and none for the refund email. The change touches four items, and the work comes out differently. When a criterion's text changes, its links to tests go stale, so the changed 31st-day criterion is flagged for review, while the email criterion, untested before the change, is debt the code already carried.

Criterion In the change Tests today Work
A request on the 30th day after delivery is refunded. as it was passing none
A request on the 31st day after delivery is refused for a customer who is not a member. changed link stale review and update the test
A member's request on the 60th day after delivery is refunded. added none test and code
A member's request on the 61st day after delivery is refused. added none test and code
The email names the refunded amount. as it was none debt from before, reported apart
The store credit equals the order total. as it was passing none

The changed requirement is left out and only its criteria are listed. The changed and added rows are the change; the email row was there before it.

Where tasks belong

Tasks stay. Some work comes from somewhere other than the two states, and it belongs in the plan.

The first kind is the path. Which step lands first depends on the code and on what is safe to run in between. Plan B's recomputed deadlines are one case. A column renamed under live traffic is another, done by parallel change, also called expand and contract: add the new form, move every reader to it, then remove the old one. The two end states name neither step in between.

The second is discovery. "Find out where memberships are stored" comes before the target state can be written, since its job is to settle what the target is. It is part of specifying the change.

So the state lives in the specification, and the path lives on the branch. The agent breaks the build plan into bounded tasks, each naming the criteria it meets, and the tasks stay with the branch as the record of how this change was built.

A year later

Go back to the question from the start: a year from now, what are the shop's refund rules? Built from tickets, the answer is a reading of the code, made again each time the question comes up, by whichever agent is asked. Built from states, the answer is the specification on main: the same text for every reader, written before the code and checked against it.

Agents will keep getting better at reading code, and the first answer will improve with them. But still, it stays an inference. A perfect reader of the code still reports what was built, so a bug in prod reads as the rule. Only a target state written before the code can say the code is wrong.

Tickets still get the work done. They are the plan for getting from one state to the next, and once the branch merges, a closed ticket can stay closed, because the change it served is still there to read: the difference between two versions of the specification.

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.