Skip to main content
sourceSource: corpus/docs/examples/bug-fix.mdModified: 2026-06-23

Example: a bug fix

Works today — plain markdown plus your agent; no corpus tooling required.

The bug shape of the loop: Pull → Spec amend → Task → Run → Review → Close. A bug is a disagreement between the code and the spec — or, as here, a place where the spec was silent. Every artifact appears in its frozen template shape (templates/).

Step 1 — Pull the bug ticket

Same first move as any externally-sourced work: snapshot the ticket verbatim into intake/. The incident log excerpt is the most valuable part — it rides along unedited.

intake/PAY-88.md

---
type: intake
source: PAY-88
url: https://linear.app/acme/issue/PAY-88
captured: 2026-06-10
---

# Intake: Customer charged twice during processor outage

PAY-88 — Customer charged twice during processor outage
Reported by: on-call (incident #2031) · Priority: Urgent

During yesterday's processor outage (14:02–14:19 UTC) order #58213 was
captured twice: $79.90 ×2 for one checkout. The processor returned a 5xx on
the first attempt; our retry captured again.

Log excerpt:

    14:03:11 charge attempt order=58213 key=chg_8841 -> 502
    14:03:14 charge retry   order=58213 key=chg_9907 -> 201 captured
    14:07:40 processor webhook: chg_8841 captured (delayed)

Note the two keys. The retry minted a NEW idempotency key, so the processor
saw two independent charges. Refund issued; we need this to be impossible.

Step 2 — Spec amend: what did the spec actually require?

A bug fix starts at the spec, not the code. The payments service already has one — here it is, as it stood when the incident happened.

specs/payment-retry/spec.md — before the amendment:

---
type: spec
id: SPEC-PAYMENT-RETRY
title: Bounded retry on processor 5xx
status: ready
owner: payments
sources:
  - intake/JIRA-097.md
---

# Bounded retry on processor 5xx

## Intent

A transient processor outage is absorbed by bounded retries; an exhausted
retry budget surfaces a structured error instead of hanging the request.

## Non-goals

- No changes to the checkout UI; this spec covers the payments service only.

## Requirements

### AC-001 — Bounded retry

When the processor returns a 5xx, the payments service must retry the
charge at most 3 times.

Verify with: `npx vitest run server/tests/payment-5xx.spec.ts`

### AC-002 — Exhausted budget surfaces a 502

When the retry budget for a charge is exhausted, the payments service must
return HTTP 502 with a structured `processor-unavailable` error body.

Verify with: `npx vitest run server/tests/payment-fail.spec.ts`

## Open questions

- None.

## Affected areas

- `server/src/payments/charge.ts`

The check comes back uncomfortable: the code violates neither requirement — the bug lives in what the spec never said. Nothing requires a retry to reuse the original idempotency key, so the spec is amended in place with one new requirement (existing IDs keep their numbers; on the workboard, the spec's row moves back from done until the fix lands):

### AC-003 — One idempotency key per charge

When a charge is retried after a processor 5xx, the payments service must
reuse the idempotency key persisted before the first capture attempt — a
retry never mints a new key.

Verify with: `npx vitest run server/tests/payment-retry-idempotency.spec.ts`

This codebase was familiar ground. In an unfamiliar brownfield area, the loop would start with an inventory of what's actually there before the spec check — see Brownfield and change plans.

Step 3 — Task

The task implements the new requirement and explicitly preserves the two old ones — a bug fix that breaks the bounded retry is not a fix.

tasks/payment-retry-key.md

---
type: task
id: TASK-PAYMENT-RETRY-KEY
source:
  - SPEC-PAYMENT-RETRY
scope: [AC-003, AC-001, AC-002]
status: ready
---

# Task: Persist one idempotency key across 5xx retries

## Source

- Spec: `specs/payment-retry/spec.md` (SPEC-PAYMENT-RETRY)

## Scope

Implement or preserve:

- AC-003 — implement: retries reuse the key persisted before the first
  capture attempt; write the regression test first and show it red
- AC-001 — preserve: retry stays bounded at 3
- AC-002 — preserve: exhausted budget still returns the structured 502

## Do not change

- The processor client wrapper (`server/src/processor/`)
- Refund and webhook handling

## Affected areas

- `server/src/payments/charge.ts`

## Verify

- [ ] `npx vitest run server/tests/payment-retry-idempotency.spec.ts` (AC-003)
- [ ] `npx vitest run server/tests/payment-5xx.spec.ts` (AC-001)
- [ ] `npx vitest run server/tests/payment-fail.spec.ts` (AC-002)

## Agent instructions

1. Read the source spec (and change plan, if any) first.
2. Stay inside this task's scope. If a requirement can't be met as written,
   stop and say why instead of improvising.
3. Run every Verify item and paste the real output — a claim without output
   counts as unverified.
4. Before finishing, re-read your own diff as a skeptic: what would a
   reviewer flag?
5. Fill `## Run summary` below — changed files, one line per Verify command
   citing its pasted output above, out-of-scope edits, blocked questions —
   and drop anything durable in `## Findings`.

## Findings

<!-- Anything durable discovered during the task — moved to findings/ at Close. -->

Step 4 — Run: red, then green

The agent fills the packet's ## Run summary section (and ## Findings). The red run is load-bearing: it proves the new test can fail — that it actually reproduces PAY-88 — before the fix makes it pass.

## Findings

- Idempotency keys were minted per capture _attempt_, not per charge — the
  durable lesson behind PAY-88; candidate for Close
  (FINDING-5XX-RETRY-IDEMPOTENCY).

## Run summary

The run summary for TASK-PAYMENT-RETRY-KEY.

Changed files:

- server/src/payments/charge.ts — persist the idempotency key before the
  first capture attempt; retries read the persisted key, never mint one
- server/tests/payment-retry-idempotency.spec.ts — new regression test

Regression test first, against the unfixed code (red):

    $ npx vitest run server/tests/payment-retry-idempotency.spec.ts

     ❯ server/tests/payment-retry-idempotency.spec.ts (1 test | 1 failed)
       × reuses the persisted idempotency key across 5xx retries
         → expected 1 distinct key for order 58213, got 2 (chg_8841, chg_9907)

     Test Files  1 failed (1)
          Tests  1 failed (1)

After the fix, full payments suite (green):

    $ npx vitest run server/tests/

     ✓ server/tests/payment-retry-idempotency.spec.ts (1 test) 502ms
     ✓ server/tests/payment-5xx.spec.ts (3 tests) 689ms
     ✓ server/tests/payment-fail.spec.ts (2 tests) 297ms

     Test Files  3 passed (3)
          Tests  6 passed (6)

Worth saving: the double-capture needed no concurrency — the first attempt's
capture succeeded processor-side after the 502 had already been returned to
us. Any retry that mints a new key risks a double-capture all by itself.

Step 5 — Review

Every row is green, and the packet still routes exceptions: payments code is a risky file, a trigger class no green row cancels (Reviewing agent output has the full list). The reviewer spot-checked one green row by re-running the regression test before accepting the table.

reviews/payment-retry-key.md

---
type: review
id: REVIEW-PAYMENT-RETRY-KEY
task: TASK-PAYMENT-RETRY-KEY
pr: https://github.com/acme/payments/pull/233
reviewer: priya@acme (human; the agent implemented)
status: pass
---

# Review: Persist one idempotency key across 5xx retries

## Summary

The regression test reproduces PAY-88 red against the old code and passes
after the fix; the bounded-retry and 502 behaviors are preserved with suite
output. The money-moving capture path was touched, so it gets a human look
regardless of the green column.

## Changed files

- `server/src/payments/charge.ts`
- `server/tests/payment-retry-idempotency.spec.ts`

## Requirement coverage

| ID     | Result | Evidence                                                      | Human attention |
| ------ | ------ | ------------------------------------------------------------- | --------------- |
| AC-003 | Pass   | regression test red-then-green, output pasted in PR #233      | no              |
| AC-001 | Pass   | `payment-5xx.spec.ts` — 3 tests passed in the same suite run  | no              |
| AC-002 | Pass   | `payment-fail.spec.ts` — 2 tests passed in the same suite run | no              |

Spot-checked: AC-003 — re-ran `npx vitest run server/tests/payment-retry-idempotency.spec.ts` myself before accepting.

## Human attention

1. `server/src/payments/charge.ts` is a money-moving path — a risky file by
   any definition. Read the key-persistence diff even though every row is
   green; confirm the key is persisted before the first capture attempt,
   not after it.
2. Finding candidate: a 5xx retry without a persisted idempotency key risks
   double-capture — worth saving for every future payment-path task.

## Suggested decision

Merge.

Step 6 — Close

The incident's lesson is bigger than this fix, so it is saved as a finding with its evidence attached (Saving findings).

findings/payment-5xx-idempotency.md

---
type: finding
id: FINDING-5XX-RETRY-IDEMPOTENCY
status: candidate
from: REVIEW-PAYMENT-RETRY-KEY
date: 2026-06-11
related: [SPEC-PAYMENT-RETRY#AC-003]
---

# Finding: 5xx retry without a persisted idempotency key risks double-capture

## What we learned

A charge retried after a processor 5xx can double-capture unless every
attempt reuses one idempotency key persisted before the first capture
attempt: the first attempt may have captured processor-side after the 5xx
was returned, and a retry under a fresh key is an independent charge.

## Evidence

`reviews/payment-retry-key.md` — regression test red against the old code,
green after the fix (PR #233); incident log in `intake/PAY-88.md`.

## Where it applies

- Any retry of a side-effecting external call (charges, transfers, order
  placement) where the provider deduplicates by idempotency key.

## Where it does not apply

- Read-only or naturally idempotent calls, where a retry cannot duplicate
  an effect.

## Future guidance

Before adding a retry around any money-moving call, check that the
idempotency key is persisted before the first attempt and reused by every
retry — and write the many-attempts-one-key test before the fix.

status.md (the rows this fix touched — the closed task links its review packet):

| Item                          | Type    | State     | Link                                  |
| ----------------------------- | ------- | --------- | ------------------------------------- |
| SPEC-PAYMENT-RETRY            | spec    | done      | `specs/payment-retry/spec.md`         |
| TASK-PAYMENT-RETRY-KEY        | task    | closed    | `reviews/payment-retry-key.md`        |
| FINDING-5XX-RETRY-IDEMPOTENCY | finding | candidate | `findings/payment-5xx-idempotency.md` |

That's the bug shape end to end: ticket preserved, spec checked and amended with one requirement, red-then-green evidence, an all-green packet that still routed the risky file to a human, and a finding that outlives everyone's memory of incident #2031.

Other examples

  • A feature from a ticket — the six-step happy path with every artifact shown in full.
  • A large PR review — the main demo: a change-plan-driven refactor and the packet that makes a 41-file agent PR reviewable.

Ready to run the loop on your own repo? Get started — copy the kit and write your first spec.