Yeison Daza
5 min read

Make it hard for humans to write code in your project

Last week, a video by @poteto about closing 2K PRs a month went viral. Beyond the number, it got me thinking about something: before teams can work this way, they need to learn to trust what their agents produce.

This is the first in a series about what I am learning as I try to make that possible.

When rules felt like friction

At one of my first jobs, before AI, a good part of code review was spent discussing style: how to write a condition, where to put a comma, or the right way to structure a function.

At the time, Prettier was starting to gain traction, and I suggested we adopt it. I still remember a conversation about whether people would lose their personal touch when writing code.

It happened to me many times. Whenever I wanted to add a static-analysis rule or a validation, there was resistance. And I get it: when you write everything by hand, more rules and an existing codebase to fix feel like more work and less autonomy.

Now rules are part of the environment

Writing code with AI is already part of many teams’ day-to-day work. For agents to be useful, they need projects with strict, automated checks.

Over the last few weeks, I have been making the contracts in the project I work on stricter. Not to make contributing harder, but to make it clear what contributing well means.

Here are a few of the things we added.

More rules

The first step was increasing the number of things the project can catch on its own.

On the backend, a Python project, we use Ruff and went from around 100 to 400 active rules. On the frontend, we use oxlint and React Doctor.

The number of rules is not what matters. What matters is turning common mistakes and team conventions into automated checks. An agent can write code that works and still leave unused imports, duplicate logic, or follow a pattern the project has already decided to avoid.

Every rule gives the agent a concrete answer about what it needs to fix. Rather than waiting for someone to point it out in review, it can run the checks, fix the issue, and try again.

Dependency boundaries

By default, everything in a project can import everything else. It seems flexible, but it ends up being a trap: one layer learns about another’s implementation details, hard-to-undo coupling appears, and the direction of dependencies stops being clear.

That is why we added rules that control what each layer and package can import. For example, models cannot import views.

For an agent, importing whatever is closest is usually the easiest solution. It might make the code work today, but it can also introduce a dependency that makes it harder to reuse, test, or change tomorrow. Import rules protect the architecture while code is being written.

Type checking

Type checking does more than find errors. It also tells the agent what data it is creating, receiving, and transforming.

The more information the type system has, the less room there is for an agent to invent assumptions about an API, a model, or the result of a function.

Limits

One thing I have learned over the years is that anything that can grow without limits usually ends up being a problem. That applies to code too.

A file with no size limit, a function with no complexity limit, or control flow with too much nesting eventually become hard to understand and change. We can make those limits errors, not just suggestions.

In this project, we use radon to measure complexity. Not because a number can replace technical judgment, but because having a limit tells us when we need to stop and split a problem apart.

Agents find the gaps

Defining contracts is not enough: you have to validate them and keep making them stricter. Agents tend to look for the easiest path and end up finding gaps in the constraints.

For example, when I enabled import rules, the agent started creating files in the root of every package to get around them. The solution was not to keep telling it not to do that, but to add a validation that prevented new files from being created there.

And when a validation fails, it should not just say “failed.” It needs to explain the rule and show what to do next. A good validation teaches.

Instructions help, but they are not a guarantee. An executable contract can be.

Make writing by hand uncomfortable

I think a project should have rules strict enough that writing code by hand can feel uncomfortable.

This is not about creating bureaucracy for its own sake. The easy path should be the right one: consistent style, bounded complexity, valid dependencies, and prevention of common errors.

For an agent, these rules are not a nuisance. They are clear boundaries for how code is written in that project. If a check fails before a commit or in continuous integration, the agent can fix the issue and try again. A new rule does not have to turn into a discussion that drags on for days.

Sometimes an agent will attribute a failure to code that was already there. In those cases, instructions help define its responsibility, but validation remains the reference.

How to adopt this without stopping the project

In a medium or large project, enabling every rule and fixing the past all at once can be unfeasible. In my case, there were around 20K static-analysis errors.

The strategy does not have to be a perfect migration:

  1. Keep the problem from growing. New code has to follow the new rules from now on.
  2. Clean up gradually. Use agents to tackle the existing debt in small, verifiable changes.

The goal is to get to a project where anyone can add code, but where there are clear contracts for style, architecture, complexity, and correctness.

We are not taking the human touch out of code. We are stopping ourselves from spending that touch on remembering implicit conventions and fixing repeatable mistakes.

That is how we build an environment where an agent can be genuinely useful.

In the next post, I will share how we use mutation testing to make sure our tests actually give us confidence: Tests need to earn their place in my codebase.