A Year of Shipping with AI
From three months per CRUD app to running five projects in parallel — the tools, the workflow, and the one opinion I refuse to give up.
A year ago, a simple application — twelve tables, some business logic, nothing exotic — took me two to three months.
Today that's not the unit of work anymore. And the interesting part isn't that I type faster. It's that the job changed underneath me: I went from writing code most of the day to making decisions most of the day, and somewhere in there I became Technical Lead Engineer.
This is the honest account of how that happened, including the parts I'm still bad at.
The headline: AI didn't make me write code faster. It moved the bottleneck from producing code to deciding and verifying. Everything below follows from that.
What changed about learning
The old path into a language was strictly layered. You started at the top — the syntax everyone writes — then slowly went deeper: idioms, architecture, tooling, the tricks that separate someone who uses a language from someone who knows it. That descent took years.
That order inverted. I no longer need to memorize a language end to end. What I need is to understand how it behaves: its idioms, its escape hatches, its linters, the low-level details that used to be the last thing you learned.
This is not the same as "you don't need to learn." It's that the expensive knowledge moved. Syntax is cheap now. Knowing why a design is wrong is not.
Frameworks noticed. A new framework now ships with AI affordances built in — skills, documented migration paths between comparable tools, machine-readable conventions. Moving between ecosystems stopped being a rewrite and became a translation.
The stack that actually works
Some tools are dramatically better to build with when AI is in the loop — not because they're trendy, but because they're mature, strongly typed, and well represented in what the models already know.
The pattern: mature, typed, conventional tools produce better AI output. A framework the model knows deeply and a contract that fails loudly are worth more than any prompt technique.
The opinion I won't give up: read the code
There's a loud take going around podcasts and developer social media that you shouldn't read the code anymore. I think that's wrong, and I think it's the fastest way to build something you can't maintain.
To be precise: you shouldn't obsess over every line. But you absolutely must know what the AI is doing to your codebase.
Here's the concrete reason. Left alone, it will not use the file structure you want. That sounds trivial. It isn't.
Everything runs. Nothing can leave. utils2.ts is imported by nine files, api.ts reaches the database directly, and pulling "catalog" out means reading all of it first.
A good file structure is what makes a module possible to decouple later. If you can't lift a piece of your application out and drop it somewhere else, you don't have an architecture — you have a pile that happens to run.
The same goes for the patterns people treat as forgotten — dependency injection, command/query separation. They're not obsolete. They're exactly what keeps a fast-moving codebase from calcifying, and they're precisely what generated code will skip unless you insist.
Concretely, this is what "insist" looks like. Every mutation in the ERP I'm building goes through one registry, and the registry — not the handler — owns permission, data classification, the tenant guard, and idempotency:
export const catalogCommands = new TenantCommand({
defaults: {
permission: 'erp.catalog.manage',
classification: 'restricted_mutable_master',
guard: requireCatalogTenant,
},
operations: {
'catalog.product.create': catalogOperation(
createProductCommandSchema,
products.publicDto,
'product',
),
'catalog.sku.create': catalogOperation(
createSkuCommandSchema,
skuDto,
'sku',
),
},
})The transport layer stays thin on purpose — it spreads the command's input schema and returns the public DTO, and that's all it knows:
export const createProduct = tenantMutation({
args: {
idempotencyKey,
...products.commandInput.shape,
},
returns: products.publicDto,
handler: (ctx, args) => createProductCommand(ctx, args),
})This is the part an agent will not invent for you. Ask for "create a product mutation" and you get a handler that validates inline, hits the database, and forgets the audit entry. The registry is the thing you have to insist on — once it exists, every generated operation inherits permissions, auditing, and idempotency for free.
This is the line between a good engineer and someone who merely produces working code. It's also the line between a vibe coder and an application engineer. Those are different jobs.
The journey: five stages
Going all-in happened over the last seven or eight months, and it moved through distinct stages. Each one broke a ceiling the previous one had.
Completions and editor chat
The starting point. Completions plus "look at this, check that" in the sidebar. Not really an integration — I wrote code, it wrote code, and we alternated. Useful, but it never left the file I was in.
The loop: grill, plan, iterate
This is the single highest-leverage change I made, and it's worth describing precisely because it's not what most people mean by "using AI."
- Grill the requirements (completed)
- Write the plan (completed)
- Implement U3 (completed)
- Green the suites (completed)
Before any code, the AI interrogates me. I don't hand-roll this — I run grill-me, Matt Pocock's interrogation skill. It refuses to accept a vague answer, keeps a decision tree of everything still unresolved, and drills into each branch until we reach shared understanding:
- Which layer owns this: the contract, the service, or the UI?
- You said "cache it" — invalidate on write, or TTL? What's the staleness budget?
- Does this need to work offline, or is optimistic UI enough?
- You're NOT touching auth here, correct?
- What should happen when the provider returns a 429?Nothing is written until every one of these has an answer. The skill's whole job is to be annoying now so the loop isn't lost later.
The stages:
- Get grilled. Before any code, the AI interrogates me: what am I implementing, how, what am I using, what am I explicitly not using, what shouldn't work this way. It's a quiz whose only purpose is to surface the decisions I hadn't actually made yet.
- Produce the plan. The answers become a detailed action plan — guidelines, code examples, flow diagrams. Not a summary. A specification.
- Run the loop. Then it executes the whole plan, repeating and correcting until every test suite passes and everything holds together. A single loop can run four or five hours.
The order is the point. Most of the value is created before any code is written, in the grilling step. The loop only works because the plan is unambiguous.
What the agentic UI actually looks like

An IDE for agents rather than for files. Threads on the left grouped by project, the active conversation in the center, and a right-hand panel that switches between a browser to test the app, a git diff, open pull requests, and the files themselves. A terminal sits underneath, exactly where an editor would put it.
That layout is what made parallelism possible. Running five loops at once is only sane if you can see what each one is doing.
Scale, and being honest about tests
Two numbers that show how much the ceiling moved.
5/5modes reaching flow
Burning 100 million tokens used to feel like a lot. These days I can go through 1.5 billion tokens in a day across parallel projects. The constraint stopped being how fast I work and became how much I can meaningfully review.
On testing: I used to write and run very few tests. Now my applications carry 300 to 400 test suites, which sounds robust — and it partly is.
But I'll be straight about the weakness. A large share of those tests are mocks.
Mocked unit test
35% confidenceproves — the unit's logic, given the inputs I imagined
misses — whether the real collaborator returns that shape at all
Integration test
80% confidenceproves — the layers actually agree
misses — slower, and harder to pin a failure to one cause
What I have · ~400 suites
40% confidenceproves — the pieces agree with my assumptions about each other
misses — the seam where things really break
They exercise the happy path rather than the system's real behavior, so they don't prove the application works end to end; they prove the pieces agree with my assumptions about each other. That's a real gap, and it's my main thing to fix.
More tests is not the same as more confidence. A suite made of mocks tells you your code is internally consistent, not that it's correct.
What it did to the job
The part I didn't expect: the further I got, the less the work was about code.
I moved from deciding variable names and file layouts — where a lot of developers get stuck — to deciding what we build, how the system should be shaped, and which trade-offs the team accepts. That shift is why I ended up as Technical Lead Engineer. I advanced quickly, but not because I produce more code. Because I spend the day making decisions that used to sit above me.
Technical Lead Engineer
Where I've landed
- Pick tools the models know well. Mature, typed, conventional. Convex, Hono, oRPC, TanStack Start, WorkOS, Redis, Postgres.
- Plan before generating. Get quizzed, produce a real specification, then loop. The value is created before the first line.
- Read what it writes. Not every line — but always the structure, the boundaries, and the file layout. That's what you'll have to maintain.
- Keep the patterns. Dependency injection and command/query separation aren't legacy. They're what makes a fast codebase survivable.
- Measure confidence, not volume. 400 mocked tests are not 400 proofs.
The speedup is real. It's also entirely conditional on being able to tell good output from bad — and that skill is now the job.