# From issue to pull request: running a feature with an agent

> Source: https://learn-python.com/ai/spec-to-pr/
> Part of Learn Python, free to read.

Here is the loop that works, on a real Python codebase, for a change of meaningful size. It is not complicated. The discipline is in doing steps two and five, which are the ones everyone drops first and misses most.

## 1. Write the spec as tests, or as a sentence you could test

The prompt that produces good output is not a longer description. It is a description with a **checkable finish line**.

> **Weak:** "Add rate limiting to the API."
>
> **Strong:** "Add per-API-key rate limiting to the FastAPI app: 100 requests per minute, sliding window, backed by the existing Redis connection in `adapters/cache.py`. Over the limit returns 429 with a `Retry-After` header in seconds. Health check endpoints are exempt. Limits are configurable per key, defaulting to 100. Write the tests first; I want to read them before you touch `src/`."

The second version is not longer because verbosity helps. It is longer because it makes six decisions that would otherwise be made silently and wrongly: the scope of the key, the algorithm, where state lives, the response shape, the exemption, the default.

If you cannot write that paragraph, you do not yet know what you want, and an agent will not discover it for you — it will pick something plausible and you will find out at review.

## 2. Make it plan before it edits

The step people skip. Ask for the approach first, with no code.

```text
Before writing anything: which files will you change, what is the
approach for the sliding window, and what are you unsure about?
Do not edit yet.
```

This costs thirty seconds and catches the expensive class of error — right code, wrong design — while it is still free to fix. It also surfaces the thing you forgot: agents are good at noticing "you did not say what happens when Redis is unavailable", and that question is much cheaper now than in review.

Most tools have a first-class version of this (a plan or ask mode). Use it. If yours does not, the instruction above works fine.

:::tip The one question worth always asking
"What are you unsure about?" reliably produces the list of assumptions the model is about to make. Reading that list is the highest information-per-second moment in the whole workflow.
:::

## 3. Tests first, and read them before the implementation

Have it write the tests, then stop and read them yourself before any implementation exists.

This is the point where you find out whether it understood you. A test suite is a specification you can read in ninety seconds, and reading it *before* the implementation exists means you are judging the understanding rather than being anchored by working code.

```python tests/api/test_rate_limit.py
async def test_allows_up_to_limit(client, api_key): ...
async def test_blocks_over_limit(client, api_key): ...
async def test_429_includes_retry_after_seconds(client, api_key): ...
async def test_window_slides(client, api_key, frozen_time): ...
async def test_health_endpoint_is_exempt(client): ...
async def test_limits_are_per_key(client, api_key, other_key): ...
async def test_redis_unavailable_fails_open(client, api_key, broken_redis): ...
```

That last test is the one you argue about — fail open or fail closed? — and it is much better to have that argument here than after an incident.

## 4. Let it implement, and leave it alone

With a plan agreed and tests written, this part is genuinely hands-off. Let it iterate against the suite. Interrupting mid-loop to make suggestions usually makes things worse, because you are injecting a change of direction into a process that was converging.

Watch for two things only:

- **It changed the tests.** Sometimes legitimate; always worth reading.
- **It has been going for a long time without the suite getting greener.** That is the signal to stop and re-plan, not to wait longer. Repeated failed attempts pollute the context with dead ends.

## 5. Review the diff properly

The other step people skip. Some specific things to look for in Python, beyond the general [failure-mode catalogue](/review/failure-modes/):

```bash
git diff --stat                       # is the size what you expected?
git diff -- tests/                    # read the test changes first, separately
git diff -- src/
git diff | grep -nE 'type: ignore|noqa|except Exception|TODO|pass$'
uv run ruff check . && uv run mypy src
```

Read the test diff **before** the source diff. If the tests changed, you need to know that before you form an opinion about whether the source is correct.

Questions worth asking every time:

- **Is the diff bigger than the change?** Reformatting, renamed variables, "while I was here" refactors. Ask for them to be removed or split out. A large diff is not reviewed; it is skimmed.
- **Did it add a dependency?** Check the name character by character — see [hallucinated packages](/review/dependencies/) — and ask whether the stdlib already does it.
- **Did it duplicate something?** The most common structural failure. Search for one distinctive phrase from the new code; if a near-identical function exists elsewhere, the agent did not find it and you now have two.
- **Does it match the surrounding code?** Not just style: error handling, logging, how config is read. Local consistency matters more than global correctness for maintainability.

:::warn The plausibility trap
Generated code reads better than most human code — consistent naming, tidy structure, complete docstrings. Fluency is not correctness, and the polish makes it harder to stay suspicious. If you notice yourself approving quickly because it "looks clean", that is precisely the moment to slow down.
:::

## 6. Commit history that a human can read

Ask for small commits with real messages, and write the PR description yourself — or at least edit it hard. A generated PR description tends to describe *what the diff contains*, which the reviewer can see. What they cannot see is why you chose this approach, what you rejected, and what you are unsure about.

```text
Commits: one per behavioural change. Message says why, not what.
PR description: I'll write it. Give me three bullets on anything you
were uncertain about or any decision you made that I did not specify.
```

Those three bullets are the most valuable output of the entire session.

## 7. Before merge

```bash
make check                            # lint, types, full suite
uv run pytest -q                      # including integration
git diff main --stat
```

And one question that no tool answers: **would I be comfortable being paged for this at 3am?** If the honest answer is no because you do not really understand a part of it, go back and understand that part. Shipping code you cannot debug is the actual risk in all of this, and it is not a technical problem.

:::promo boot-dev
:::

## Common questions

### How big a change can I hand over in one go?

Roughly: one that you could review in twenty minutes. Beyond that, review quality collapses and you are approving rather than reviewing. For anything larger, break it at a natural seam and run the loop twice — the second run starts from a codebase that already has the first half in it, so it is not twice the work.

### Should I let it commit and push?

Commit, yes — small commits are useful and reversible. Push and open a PR, only behind a confirmation. The asymmetry is that a local commit is invisible to everyone else and trivially undone, while a push is visible and a PR notifies people.

### It keeps failing the same test. What now?

Stop. Repeated failure means the context now contains several wrong approaches, which makes the next attempt worse rather than better. Start a fresh session with a summary of what did not work, or do that piece yourself — often the fastest path once you have seen three failed attempts is to write the tricky ten lines and hand back the rest.

### Is the planning step really worth it on small changes?

No. For a change you could make yourself in five minutes, skip straight to the diff and review it. The planning step earns its cost somewhere around "I would need to think for a bit before starting", which is also roughly where handing it over starts to be worth doing at all.
