Most of PixelWatcher’s code is written by AI. One of the reasons I started working on this project was to gain experience with AI-assisted workflows, and I quickly realised that I didn’t need to type the code myself anymore.
For this to work though, tasks need to be thoroughly specified to avoid letting the agent make too many decisions that may have to be reverted down the line. Matt Pocock’s /grill-me skill is great for this, as it helps make sure most decisions are made upstream.
That grilling, however, mostly covers functional requirements. Architectural decisions don’t belong in tickets – they belong in the context.
Context Engineering
Per Gartner:
Context engineering is designing and structuring the relevant data, workflows and environment so AI systems can understand intent, make better decisions and deliver contextual, enterprise-aligned outcomes — without relying on manual prompts.
It’s essentially the documents, conventions and checks the agent reads before writing anything: all the relevant business, product and technical context made available at the right time, in the right form, to perform a task well.
When it comes to technical context, the code itself is often the best source – AI agents tend to pick up existing patterns rather than coming up with their own. New or unusual patterns can also be recorded as documentation (Markdown files, ADRs…) to further steer the AI.
Some decisions can also be enforced deterministically through static analysis. I’m building PixelWatcher with Laravel, using PHPStan, Rector and Pest to enforce style and architectural constraints.
But context engineering is never finished. There’s only so much that can be established upfront, and new rules and checks emerge as the product evolves. The trick is making sure they’re properly recorded.
Domain Contamination
Paddle is a Merchant of Record. That means they sell your product on your behalf, handling payments, taxes, invoicing, and other boring stuff for you while you receive the proceeds.
But this post isn’t about Paddle – what matters to the story is that it’s a third-party service I needed to integrate into PixelWatcher.
I approached this integration the same way I deal with any task: long planning conversations, where we made decisions around upgrades, downgrades and cancellations; then I unleashed the agent on the first task.
It did exactly as instructed, following each decision to the letter, and delivered a working implementation with full test coverage. Yet as I started reviewing the pull request, I realised something was off: Paddle’s fingerprints were all over the place.
Controllers, services, exception messages: Paddle’s vocabulary and classes were everywhere. With no clear boundary between the two, Paddle’s domain had effectively contaminated PixelWatcher’s.
What Happened?
PixelWatcher already integrates with several third-party services, and the pattern is always the same: expose clean domain methods through an interface, then hide the vendor-specific implementation behind an adapter. AI agents routinely pick up that pattern, which keeps the domain clean and testing straightforward.
What’s different with Paddle is twofold:
- In Laravel, it comes through a first-party package called Cashier.
- It relies heavily on webhooks.
PixelWatcher hadn’t used webhooks before.
Cashier is a special case: as a first-party package, it’s an external dependency and part of the framework at the same time. PixelWatcher is not following a layered or hexagonal architecture, so Laravel types are present throughout the application, blurring the line between what is and isn’t part of the domain. Base Cashier classes are arguably part of the domain, but Paddle ones aren’t. That’s where I chose to draw the line anyway.
In both cases, there was no precedent for the agent to lean on, so it did what it thought was right, with the confidence that it wasn’t breaking any of the ticket’s acceptance criteria.
Training the AI
I spent the rest of that morning going back and forth with the agent to refactor the code and isolate Paddle properly. In AI-assisted programming, those detours should be rare – they’re usually a sign that something’s missing from the context.
And this is the interesting part: once I was satisfied with the new boundary, I instructed the agent to encode the entire session as a new architectural convention to persist. In other words: to add it to the context.
First, it created a new Markdown document describing the adapter pattern as the default, using Paddle as an example of an integration that goes beyond the pattern. Concrete examples are far more useful to an AI than abstract rules, and they’ll help it handle similar integrations in the future.
But then, it did something I didn’t ask for: being aware of some existing architectural rules enforced by Pest for other services, it suggested writing a few more checks to ensure Paddle’s domain remains properly isolated, adding a deterministic layer on top of the new convention.
This was the direct result of previous context engineering efforts: the deterministic checks were listed as an available pattern, already part of the agent’s context.
In Closing
Context engineering is iterative. You can encode common constraints and enforce preferences early on, but codebases grow organically and develop new patterns over time that deserve their own conventions and checks. As the code evolves, so does the context.
While it might be tempting to mix architectural decisions into acceptance criteria, that would be another kind of domain contamination: task descriptions should stick to the what and the why, and the how should remain in the developer’s hands, conveyed as clearly as possible to the agents through the context.
The context constrains the AI. The AI shapes the code. The code informs the context. Together, they form a fragile ecosystem whose stability is the responsibility of the developer.
That’s the job now.