I asked an agent to “clean up the Checkout folder” once. It came back with a PR that touched the page, three child forms, a shared hook two levels up, and a utility used by Settings. Reviewing it felt like reviewing four PRs stapled together.
Green CI. Unreadable blast radius.
So I stopped saying “refactor the feature.” I cap the agent to one tree layer per PR.
What “one tree layer” means
In a typical React tree:
CheckoutPage ├── ShippingForm ├── PaymentForm └── OrderSummary └── LineItemRow One layer means: change ShippingForm or CheckoutPage or LineItemRow — not all of them in the same diff. Sibling components at the same depth can share a PR only if the change is the same mechanical rename and the prompt says so.
Hooks and utils count as their own layer. If the agent needs a new useShippingAddress extracted from ShippingForm, that extraction is PR 1; wiring it into a sibling is PR 2.
Why unbounded refactors go sideways
Agents are good at local pattern matching and bad at your mental model of ownership. Give them a whole folder and they will:
- “helpfully” rename props across parents and children in one go
- move shared state up “for cleanliness” while changing leaf UI
- rewrite a utility used by unrelated routes because the types looked similar
- collapse three tiny components into one mega-file because the prompt said “simplify”
Each of those might be fine alone. Together they make bisect useless and force reviewers to trust the model instead of reading the diff.
I’ve also seen agents “finish” a layer by rewriting the parent’s error boundary “while they were there.” That while-they-were-there clause is how one-layer work becomes a rewrite.
How I prompt it
I name the exact file (or folder of siblings) and ban climbing the tree:
Refactor only `src/checkout/ShippingForm.tsx` and files it exclusively owns (styles / tests colocated under the same folder). Do not edit: - parent `CheckoutPage` - siblings `PaymentForm`, `OrderSummary` - shared hooks under `src/hooks/` - anything under `src/settings/` If a change truly requires a parent or shared hook, stop and list the follow-up PR instead of editing it now. Max ~200 lines of net change. Prefer extract + thin wrapper over rewrite. If I’m using a ownership map (one file → one owner component), I paste the relevant rows and say “stay inside these paths.”
What I accept vs reject in review
Accept
- Prop rename inside one component + its colocated test
- Extracting a private helper used only by that file
- Splitting a bloated leaf into two leaves without changing the parent’s public API in the same PR
Reject / split
- Parent layout change + child form rewrite in one PR
- New shared hook + adoption in three features
- “Also fixed Settings while matching the pattern”
- Deleting a prop from a shared type that other trees still need
When the agent stops and proposes PR 2 for the parent, that’s success — not incompleteness.
Tiny checklist before I hit merge
-
git diff --stat— mostly one directory? - Does the commit message name one component or layer?
- Could I revert this without breaking a half-migrated parent?
- Did shared modules change? If yes, was that the whole point of this PR, or drive-by?
If (3) is no, the layer cap failed.
Prompt snippet I reuse
One tree layer only. Target: <ComponentOrPath>. Edit that layer’s files only. No parent/sibling/shared climbs. If blocked by a parent or shared API, output a short “PR 2 proposal” and stop. Do not rewrite unrelated utilities to match style. Tiny habit
Next time you ask an agent to refactor React, don’t hand it a feature folder. Hand it one node in the tree.
Smaller PRs merge faster, bisect cleaner, and keep the agent from turning “tidy ShippingForm” into “accidentally redesign Checkout.”

