Don't let your agent improve the code

Try this with your agent

Open the companion prompt. Copy it all, or choose just the section you need.

Ask an agent to fix the evening times on the booking page. It makes the fix, then renames two helpers, moves a type into its own file, switches a callback to async/await, and adds unit tests in a style the repo doesn't use. Each extra edit looks reasonable. The booking fix is now buried in a diff about everything else.

This is part 2 of Agents on the team, a series about bringing AI agents into Wonderby, a Metorial customer that makes booking software for beauty businesses. Part 1 set up the company notes.

At the end of part 1, I said we'd start with the tests. We did speed them up. Before that, we wrote down the repo conventions so agents wouldn't edit around a task, and built a way for them to investigate production bugs. The full setup prompt covers all three. It starts by asking which agents you use and where your production problems show up.

Agents have opinions about code

An agent can write perfectly reasonable code that still doesn't fit our repos. In wonderby-api, we prefer end-to-end tests. Unit tests are for isolated logic such as the calendar builder, though agents tend to write them first. In wonderby-web, colors come from --wb-* tokens, never hex values. We knew these conventions; the agents didn't.

Some rules only make sense once you know why. Our backend AGENTS.md has a few like this about migrations:

Assume the previous release runs against the new schema forever.

Renames do not exist as a safe operation; they are always expand and contract.

Prisma attributes that the client populates, such as @updatedAt, do not produce a database default.

The last one is easy to miss. The old release keeps running while the new one deploys. If an agent adds a NOT NULL column with @updatedAt, the old release can't insert rows anymore. An agent with good taste writes the clean version and breaks production.

Notes in each repo

In part 1, I said coding rules would stay with each codebase. AGENTS.md gives every session a short map, under about 200 lines, and links to the company notes in wonderby-headquarters. The fuller rules live in docs/, each with a reason and a file that follows it. Skills like triage link to those docs instead of copying them.

The prompt that writes them starts by studying the repo, not by filling in templates. A repo makes the same kinds of things again and again, like endpoints or screens. For each kind, the prompt picks the two or three newest examples that got a good review and compares everything else to them. A finding looks something like "14 of 16 modules do it this way; the other two are legacy." It also reads the git history. Files that always change together usually mean a rule nobody wrote down, and a review comment that keeps coming back belongs in the docs.

Then it writes the repo notes with two rules. First, never invent a convention. If the code doesn't follow a rule yet, the docs either list the places that break it or leave the rule out. Second, every rule says why, like the migration rules above. A rule without a reason gets applied in the wrong places.

Follow by default, refactor when asked

This is the part I care about most. A lot of advice for coding agents tries to make them write better code. We want ours to write code that looks like it was already there. So every repo now has two modes:

Match
Default
Fix the evening times on the booking page
  • Copies the newest example named in docs/
  • Keeps the same file layout, names, error handling, and test style
  • Leaves code outside the task alone
  • Lists anything that looks wrong at the end of its report
One small merge request: the fix and its test
Refactor
Only when asked
Refactor how the booking page groups times
  • Suggests a plan before it changes anything
  • Names the old pattern and every place that uses it
  • Updates the docs and examples in the same change
  • Keeps the refactor separate from features and bug fixes
A plan to approve, then its own merge request

Match mode keeps a fix reviewable. When the agent tidies nearby code along the way, each task leaves another variation of the same pattern behind. Changing the architecture deserves its own decision, so we look through the agent's "this looks wrong" notes now and then. A few become refactors with their own requests. Most don't.

When a repo has two patterns for the same thing, the docs say which one is current: the newest example wins, and the old one is marked legacy. That's what went wrong in part 1. Without the notes, the agent picked the violet gradient button because more files used it. Here's the rule in each repo's AGENTS.md:

MDAGENTS.md
1## Change scope
2
3- Match mode is the default. Write new code the way the closest example in
4 `docs/` is written: same layout, naming, error handling, and test style.
5- Do not rename, move, reformat, or "clean up" code the task doesn't need.
6- If an existing pattern looks wrong, finish the task the existing way, then
7 list the concern in your final report.
8- Refactor mode starts only when the request says "refactor", "restructure",
9 or asks for architecture changes. Propose a plan first and wait for approval.

A triage skill in the repo

When a production bug comes in, we first need to find the affected business, request, release, and line of code. We gave that investigation to an agent. Its skill lives in wonderby-api at .agents/skills/wonderby-triager/ and reads five sources through one Metorial Magic MCP server:

  • Sentry: errors, traces, and releases
  • Amazon Web Services: CloudWatch logs and ECS
  • PostgreSQL: our production database on PlanetScale, one read-only query at a time
  • GitLab: pipelines, deploys, and which commit is live
  • Linear: bug reports

The Metorial CLI handles the connections, the same way we connected Linear and Google Drive in part 1. The prompt connects the sources and turns on only the read tools.

SentryErrors and releases
AWSCloudWatch and ECS
PostgreSQLPlanetScale database
GitLabPipelines and deploys
LinearBug reports
MetorialApproved sources, read-only
Your agentWith Metorial Magic MCP connected
Only the tools you approve are connected.

Read-only tools aren't the only guard. The database user is read-only on the server, and the AWS access is read-only too.

The connections give the agent evidence. The skill also tells it how our systems can mislead it. These are three entries from its "Common mistakes" table:

MistakeWhat the skill does instead
Counting error-level log lines as an outageThey include handled 4xx errors, like bots trying pages that don't exist. It checks the exception type first.
Treating a request ID as uniqueIDs are a counter inside each ECS task, so the same ID shows up in different tasks. It pins each one to its log stream.
Quietly applying an obvious fixIt stops at diagnosis and asks for the fix as a separate step.

One rule in the skill sums it up: permission to look into a problem is not permission to fix it. None of this is in the Sentry or AWS docs, so it goes in the skill. The skill even includes an example of how a request should start. Before it touches anything, the agent says what it will check:

From the skill's example intake
~/work/wonderby-apiwonderby-triager
The app is getting 500s since this afternoon. What is failing?
I'll rank production failure modes in CloudWatch for the last 6 hours (Europe/Warsaw afternoon = roughly 10:00Z onward), list the recent HTTP 500s with their routes, and reconstruct one occurrence to get the exception and stack. Confirm the window and, if you have it, the route or a request ID. Everything I run is read-only.
CloudWatch(error-summary, api-backend + api-backend-background-jobs)
CloudWatch(http-500, api-backend)
CloudWatch(request, one req.id + @logStream)

The skill starts in two ways:

wonderby-triager
Read-only in production
Reads
SentryCloudWatchECSPlanetScaleGitLabLinear
On demandSomeone asks
"We've had 500s since this afternoon. What's failing?"
  1. Asks for the business, time window, and timezone
  2. Reads logs, errors, data, and the deployed code
  3. Reports what it found and how sure it is
Ends withA report in chat. A fix is a separate request.
On a scheduleRuns by itself
Every few hours, for new errors and bug reports
  1. Collects new Sentry issues, 500s, failed pipelines, and Linear bugs
  2. Groups duplicates and checks each group
  3. Fixes what it can on a branch, then a second agent reviews the fix
Ends withMerge requests waiting for a person

On demand, it ends with one of four answers: the confirmed cause, a likely cause and how unsure it is, an idea it ruled out, or "I can't tell without X". If we need the fix right away, we ask for it as a separate step, and the agent fixes it in match mode.

The scheduled run isn't on yet. It's based on Mender, an agent we run every hour at Metorial on our integration errors. Mender fixes errors on local branches, has a second agent review each fix, and never pushes. It marks expired credentials, rate limits, and outages as not fixable instead of "fixing" them. It also refuses to start if earlier fix branches haven't reached the remote. Wonderby's scheduled run will work the same way with production errors, failed pipelines, and new Linear bugs. It'll open GitLab merge requests and link each one from its Linear issue. People still merge, and people still deploy.

Make the pipeline keep up

A two-minute fix doesn't help much when tests and a dev deploy can take another twenty. More agents would add to that queue, so we worked on the pipeline before turning on scheduled triage.

Before13 to 26 min
Agent fix2 min
Tests7 to 12 min
Deploy to dev4 to 12 min
After7 to 8 min
Agent fix2 min
Tests4 to 5 min
Deploy to dev1 min
Slower runs. Solid bars show the fastest runs we measured.
Before
Passed
00:21:531 week ago
After
Passed
00:06:051 hour ago
Two real pipelines from our GitLab, before and after.

Agents also watch the pipeline themselves. The AGENTS.md in wonderby-api and wonderby-web says:

MDAGENTS.md
1Whenever you are asked to push to `dev` or `main`, or to deploy or release, the
2task is not finished when the push succeeds. Monitor every pipeline it starts
3until each one finishes, and fix any failure.
4
5- Do not make CI pass by skipping or deleting tests, weakening checks, or
6 retrying jobs blindly. Retry a job only when its trace shows an infrastructure
7 failure, such as a runner, network, or registry error, and say that you did.

We read the timings first. A few real GitLab jobs showed that most of the time went to work that repeated on every run: installing packages, pulling base images, a cold npm ci, and exporting the image.

Most of the fixes were standard. We build each image once per commit, tag it with the SHA, and use that same image for tests, dev, and production. A build takes about 50 seconds, or is skipped if the commit was already built. Runners also keep their Docker cache between jobs, dependencies install before the source is copied, and deploy tools live in their own image.

The bigger surprises in wonderby-api weren't in the build at all:

  • The base image used a floating node:24 tag. Each time upstream republished it, every cached layer below it was thrown away, including the dependency install. Now it's pinned by digest.

    DOCKERDockerfile
    1# Before: changes whenever upstream republishes
    2FROM node:24
    3
    4# After: always the exact same image
    5FROM node:24.21.0-bookworm-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6
  • A VOLUME for node_modules copied every dependency into a new volume each time a container started. That was 5 minutes of a 6.5-minute dev deploy. Without it, migrations start in 5 seconds instead of 45.

    DOCKERDockerfile
    1# Don't add this: every container start copies all of node_modules
    2VOLUME /app/node_modules

    If a local setup mounts the source over /app, it declares the volume in its own Compose file instead.

  • On the dev server, Node ran as PID 1, and Linux ignores the default stop signal for PID 1. Every stop waited 120 seconds and then killed the worker. Where Node started through npm run or sh -c, the opposite happened: the wrapper exited right away and took Node down before its job queue was empty. Using tini and starting node directly fixed both. Stops now take 1 to 3 seconds.

    DOCKERDockerfile
    1# Before: npm starts Node, and nothing passes the stop signal on properly
    2CMD ["npm", "run", "worker:start:prod"]
    3
    4# After: tini passes the stop signal to Node, and Node starts directly
    5ENTRYPOINT ["/usr/bin/tini", "--", "docker-entrypoint.sh"]
    6CMD ["node", "dist/worker.js"]

Running tests in parallel also helped, once each worker had its own database and its own Redis databases. Without that, two tests book the same employee at 10:00 and one of them fails for no clear reason.

wonderby-web has a few speedups of its own. ESLint and TypeScript caches are kept for each branch, checks run while the image builds, Playwright browsers come in a prebuilt CI image, and browser tests use a fake API so they don't fail at random.

The Turbopack build cache (.next/cache) takes one line in the Dockerfile. The cache stays in the runner's builder and never goes into the image, so the next build only redoes what changed:

DOCKERDockerfile
1RUN --mount=type=cache,id=wonderby-web-next,target=/app/.next/cache,sharing=locked \
2 npm run build

This only works because our runners keep their state. A cache mount lives on one machine, so ephemeral runners lose it after every job. If that's your setup, you have two options:

  • On Vercel, .next/cache is kept between builds automatically. There's nothing to set up.
  • On other CI, use a remote cache. With Turborepo and Vercel Remote Cache (free on all plans, even outside Vercel), a build whose inputs haven't changed is skipped and its output is downloaded instead.

For a single Next.js app, that's a small turbo.json:

JSONturbo.json
1{
2 "tasks": {
3 "build": { "outputs": [".next/**", "!.next/cache/**"] }
4 }
5}

Then set TURBO_TOKEN and TURBO_TEAM in CI and build with npx turbo run build.

Next.js also bakes NEXT_PUBLIC_* values into the build, so dev and production are separate images, each built only on the branch that deploys it.

What is in place

Each Wonderby repo now has notes that tell agents how to work with its existing code. Refactoring takes a separate request. The triage skill in wonderby-api can read Sentry, AWS, the database, GitLab, and Linear, then report what it found and how sure it is. Scheduled fixes are still to come. In the two GitLab runs shown above, the full pipeline went from about 22 minutes to about 6. A deploy to dev went from up to 12 minutes to about 1.

Next up: Give agents the design system

In the next post, we'll give agents Wonderby's design system and Figma files, and check how close their screens get to what our designer would make.

Ready to build with Metorial?

Connect any AI agent to any tool or data source. Govern every action.