This is the written-up version of the talk I gave at Vue MAD 2026 in Madrid: Clean Code Is Sexy Again. Making Your Vue Project AI-Ready. If you would rather watch it, here is the original recording on YouTube. Otherwise, read on.
Clean Code Is Sexy Again, Vue MAD 2026
TLDR #
- My main take: what is good for developers is also good for agents. There was never a separate “AI-ready” checklist. There was just good engineering, and now it pays off twice. - “You don’t have to write code anymore” is half true. It only works if you know your stack andyour codebase is built for an agent to work in. - An agent is not magic. It is a loop: read context, pick a tool, run it, read the result, repeat. Once you have built one you stop asking “how does the magic work” and start asking “why does the same loop fly in one repo and fall apart in another.”
- Three things every Vue project should invest in:
context(AGENTS.md, skills, hooks, a
brain/
),feedback loops(types, lint, tests, a real browser), and** discoverability**(vertical feature slices). - When the agent gets something wrong, fix the factory, not the PR. Add the lint rule, update AGENTS.md, tighten the prompt. The PR fix is one bug. The factory fix prevents the next hundred.
The line everyone keeps repeating #
Everyone keeps saying the same thing: you don’t have to write code anymore.
I think that’s half true. In some of my repos AI 10x’d me. In others it produced complete garbage. Same person, same agent, same model, different codebases. It only works if two things are true. First, you have real experience with the stack. I would not let an agent write Rust for me, because I have no idea what good Rust looks like and I could not review it. Second, the codebase has to be shaped so an agent can work in it. A greenfield project is easy. A brownfield project with no tests and 10,000-line components is a mess that AI makes worse, not better, until you clean it up.
A good frame for “where are we” comes from Dan Shapiro’s post on the levels of AI-assisted development. It reads like driving automation. Level 0 is spicy autocomplete, the first Copilot. Then agents arrive and it gets a lot better. At the top is the software factory where the agent defines, ships, and fixes on its own.
I put myself at level three. The agent writes most of the code, and I spend my time reviewing and reading every line. Sometimes I feel like the bottleneck. I would not recommend level four or five for anything serious yet, but the pace is real. Two weeks before the talk, the creator of Bun started using Claude Code to rewrite Bun from Zig to Rust. A million lines, in days, almost entirely by an agent.
Triumph or disaster, I don’t know. What I know is this is the world we ship into.
What an agent actually is #
Most Vue devs treat agents as a magic box. Magic is the wrong mental model. The best tip I can give you to get better at AI is to build your own coding agent once. Then it stops being magic, and you get a feel for why it does the right thing sometimes and the wrong thing other times. I wrote up the full build in Building Your Own Coding Agent From Scratch Building Your Own Coding Agent from Scratch A practical guide to creating a minimal Claude-powered coding assistant in TypeScript. Start with a basic chat loop and progressively add tools until you have a fully functional coding agent in about 400 lines. , but here is the whole picture in one image.
The agent is bounded by three things: its context window, the tools you give it, and its ability to verify what it did. That is it. The analogy that unlocks everything is treating the agent like a new engineer joining your team. You would not throw a new hire into your worst legacy module and expect a feature in a week. Same with an agent.
Strip the magic and a tool is just a function. Three fields: a description, a schema for the arguments, and the function that runs.
type Tool = {
description: string
schema: Record<string, string>
execute: (args: Record<string, unknown>) => Promise<string>
}
const TOOLS = new Map<string, Tool>([
['read', {
description: 'Read file with line numbers (not a directory)',
schema: { path: 'string', offset: 'number?', limit: 'number?' },
execute: read,
}],
['edit', {
description: 'Replace old with new in file (old must be unique)',
schema: { path: 'string', old: 'string', new: 'string' },
execute: edit,
}],
['bash', {
description: 'Run shell command',
schema: { cmd: 'string' },
execute: bash,
}],
])
Here is the part that demystifies everything: the model only ever sees the description and the schema. The execute
function never leaves your machine. The model cannot run read()
, it cannot even see it. So how does it “use” a tool? It reads the description and emits a request: “call read with path src/App.vue.” Your loop runs the real function and feeds the result back. Which means the description is the prompt. A vague description is a tool the model misuses. Tool descriptions are engineering, not docs.
And the loop is just recursion.
async function agentLoop(messages, systemPrompt, tools = TOOLS) {
const response = await callApi(messages, systemPrompt, tools)
const toolResults = await processToolCalls(response.content)
const newMessages = [
...messages,
{ role: 'assistant', content: response.content },
]
// No tool calls → the agent is done
if (toolResults.length === 0) return newMessages
// Tool calls → loop with results as the next user turn
return agentLoop(
[...newMessages, { role: 'user', content: toolResults }],
systemPrompt,
tools,
)
}
Call the API. The model replies with text and maybe some tool requests. Run each one, append the assistant reply and the tool results, and loop. When the model returns no tool calls, it is done. And messages
is not a database or a session store. It is an array of { role, content }
. That array is the agent’s memory. Every turn you append to it.
I built a tiny version of this a few months ago and called it nanocode, around 350 lines of TypeScript. It is not perfect, but building it is what made the whole thing click.
Once you see an agent this way, the question stops being “how does the magic work” and becomes “why does the same simple loop work brilliantly in one repo and fall apart in another?” That is the rest of this post.
Clean code isn’t nice-to-have anymore #
I have been in plenty of projects where people told me “we don’t have time to write tests.” That argument is over. You do not need new patterns for AI. The patterns you already fight for in code review, the ones the senior dev keeps insisting on, are exactly the patterns that make agents work. A codebase that is hard for humans is hard for agents. Sprawl, hidden coupling, magical state: humans hate it, agents fail at it.
In Vue, three things are worth investing in: context, feedback loops, and discoverability.
Part 1: Context #
The agent is Leonard from Memento. Every new chat, the context resets. No long-term memory, no yesterday. So, like Leonard, it has to tattoo the rules where it will read them every single turn.
That tattoo is AGENTS.md (or CLAUDE.md
). It is the first thing you can optimize. But the tattoo space is finite, and most of it is already used before you write a word.
Every model has a context window, and it works best when it is not full. Models degrade as the window fills: recall drops, reasoning slips, the agent starts confusing files. And the window is not empty when you start. The system prompt and the tool definitions already cost around 20k tokens before you type anything. AGENTS.md, skills, MCP servers, and sub-agents all spend from the same pool. In Claude Code you can see exactly where it goes by typing /context
.
This is why the biggest mistake with AGENTS.md is dumping everything into it: every coding rule, every bug post-mortem, every gotcha, 2000 lines that load on every single turn. A better shape is a thin doorway:
Run `pnpm lint:fix && pnpm typecheck` after changes.
## Stack
Nuxt 4, @nuxt/content v3, @nuxt/ui v3
## Structure
- `app/` — Vue application
- `content/` — Markdown files
## Further reading
**IMPORTANT:** read the relevant doc below
before starting any task.
- `docs/nuxt-content-gotchas.md`
- `docs/testing-strategy.md`
The agent loads testing-strategy.md
only when it writes a test, nuxt-content-gotchas.md
only when it touches content. That is progressive disclosure: the right context at the right time. Two filters before a line goes into AGENTS.md. Can a tool enforce it? Then do not write prose about it. Is it universal or situational? Situational goes in docs/
. I go deeper on the AGENTS.md-versus-skills split in my Claude Code customization guide Claude Code Customization Guide (2026): CLAUDE.md vs Skills vs Subagents When should you use CLAUDE.md, a slash command, a skill, or a subagent in Claude Code? A decision guide with real examples for each, so you stop guessing which one fits the job. , and on why context fills up the way it does in reverse-engineering HumanLayer’s context engineering Reverse-Engineering HumanLayer's RPI: Context Engineering as a Filesystem I installed HumanLayer and reverse-engineered its RPI/QRSPI Claude Code plugin. Here's how it keeps the context window out of the model's 'Dumb Zone' by writing every stage to disk, and how a single phase actually runs. .
brainmaxxing
Forget looksmaxxing. We are brainmaxxing: maxing the one thing the agent has, its context. brainmaxxing is an open-source kit from a Cursor developer. It is a framework for giving an agent a documented memory, and it has three pieces.
First, a brain/
folder. Plain markdown. The agent reads from it and writes back to it. There is one index.md
with wiki-links describing the documentation that exists, and the rest is markdown files for codebase notes, plans, and principles. People found out you do not need RAG or anything complex. The simplest approach, wiki-links and markdown files, works best, and now the agent has a memory. (If you want the theory of why files beat a vector database here, I wrote it up in the four types of memory for coding agents The Four Types of Memory for AI Agents (and How Claude Code Implements Each) Working, semantic, procedural, episodic. The CoALA framework splits agent memory into four kinds. Here is what each one is, and how Claude Code actually implements them on disk. .)
Second, skills. A skill is like a prompt that lives in a markdown file with a name and a description. When you start Claude Code, only the name and description enter the context. When your prompt matches, the body loads. When the body calls a script, the script loads. Three levels of lazy , so skills scale. brainmaxxing’s /reflect
skill is a good example: at the end of a session it reviews the conversation and persists what mattered back into brain/
. So when the agent uses the wrong command and you correct it, you just say “remember this,” and it updates its own memory.
Third, hooks. Vue has lifecycle hooks like onMounted
. Claude Code has lifecycle hooks too. brainmaxxing wires a SessionStart
hook that cats index.md
into every new session, so the agent boots up already knowing the map. There are many more events. PreToolUse
can block a destructive command or forbid reading your .env
. PostToolUse
can re-lint after an edit. SessionEnd
could even send you a notification. I cover skills and hooks hands-on in my Vue workout tracker walkthrough Claude Code for Vue: Skills, Hooks, and 9 Parallel Review Agents A practical guide to configuring Claude Code for Vue 3 development with CLAUDE.md files, automatic skills, enforcement hooks, and parallel subagent code reviews. .
brainmaxxing also ships principles, around 16 markdown files describing how you want code written. One is boundary discipline: business logic lives in pure functions, the shell is thin and mechanical. The agent reads them and develops a better taste for your code. You can write your own too.
Claude Code itself now ships something similar, called auto memory: it writes markdown notes about your project automatically. The catch is those files only live on your machine. That is exactly why I like keeping brain/
in Git. Check it in and the whole team’s agent boots up with the same memory on day one. The new hire on Monday starts where the team is, not from zero.
Want VueUse-style code? Give the agent VueUse
One more trick, and it is bigger than memory. You want the agent to use VueUse or Nuxt UI, but its training data is out of date and it does not know the current API. So give it the source. Clone the library into your repo as a git subtree:
git subtree add --prefix=repos/vueuse \
https://github.com/vueuse/vueuse main --squash
Then point AGENTS.md at it:
## Reference repositories
- `repos/vueuse/` — when writing new composables, mirror the
patterns in `repos/vueuse/packages/core/use*/`.
Now the agent’s pattern matching is anchored on real source instead of stale docs. Agents are post-trained on reading code, not prose. A subtree beats a submodule (no clone-time pain, just files) and beats pointing at node_modules
(compiled and flattened, the structure is gone). One refinement: tell your editor to ignore repos/
so you do not accidentally auto-import VueUse internals, and have the agent distill what it learned into a short agent-patterns/vueuse.md
so it does not re-explore 800 composables every session. This sounds stupid, but once you try it, it works far better than MCPs in my experience. The pattern comes from the Effect team’s post, The One Weird Git Trick That Makes Coding Agents More Effect-ive.
Part 2: Feedback loops #
If an agent does not know when it broke something, it ships whatever compiles. So you give it deterministic ways to check its own work.
Type safety is the first lie-detector. A strict tsconfig
is non-negotiable, and noUncheckedIndexedAccess
and exactOptionalPropertyTypes
catch what default strict mode lets through. Types are a compile-time fiction, though, so parse with Zod or Valibot at every untyped boundary: fetch responses, route params, env vars, form input.
Lint and format with Oxlint and Oxfmt. They are roughly 50x and 30x faster than ESLint and Prettier, fast enough to run on every save instead of batching at the end. Two Vue lint rules in particular keep components agent-friendly, because nobody, human or agent, can read a 3000-line component:
'vue/max-template-depth': ['error', { maxDepth: 8 }]
'vue/max-props': ['error', { maxProps: 6 }]
A 12-prop component is a god object pretending to be one. I wrote up my full set in my opinionated ESLint setup for Vue projects My Opinionated ESLint + Oxlint Setup for Vue Projects A battle-tested linting configuration that catches real bugs, enforces clean architecture, and runs fast using Oxlint and ESLint together. , including custom local rules.
Then tests. Vitest for units, and the big 2026 win, Vitest browser mode, which runs component tests in a real Chromium via Playwright instead of jsdom. Hover, focus, layout, scroll, all behave like production. The agent can finally trust that “passes in tests” means “works in a browser.” I go deep on this in the Vue testing pyramid with Vitest browser mode Vue 3 Testing Pyramid: A Practical Guide with Vitest Browser Mode Learn a practical testing strategy for Vue 3 applications using composable unit tests, Vitest browser mode integration tests, and visual regression testing. .
Those are four layers. There are 11 more: API mocking, contract tests, E2E, accessibility, visual regression, performance, dead-code detection, and so on. Every one is a signal the agent can chase to green on its own. The cheap ones at the center run on every save, the expensive ones at the edge run in CI. What I now do in a new project is hand the agent my own write-up and say “set up the layers that make sense here.”
The full breakdown lives in the modern frontend quality pipeline A Modern Quality Pipeline and Testing Strategy for Frontend Projects A short, framework-agnostic concept of what a modern quality pipeline and testing strategy look like for any JavaScript or TypeScript frontend project. .
The agent as a user
Static checks can be green and the feature can still be broken. Did the modal open? Did the chart render? You need the agent to be the user. Vercel’s agent-browser is a browser CLI shaped for agents. One install and the agent has a real Chromium it can drive from the command line.
npm i -g agent-browser
agent-browser install
agent-browser open localhost:5173
agent-browser snapshot -i # DOM tree with @refs
agent-browser click @e2 # click by ref
agent-browser screenshot --annotate
agent-browser console # read page console
The snapshot returns the DOM as plain text with refs, the agent clicks by ref, screenshots come back annotated, console errors come back as text. So when there is a bug, you say “reproduce it with agent-browser,” and once the agent can reproduce something, fixing it is easy. The same thing a good developer would do.
For this to work the app has to be agent-runnable: the dev server is one command, the port is stable, there is a way to know the server is ready, and test data is seeded so the agent does not hit a login wall. Then “types pass” is no longer the bar. “Works in a browser” is. I built a full version of this into CI in automated QA with Claude Code, agent-browser, and GitHub Actions How to Use Claude Code as an AI QA Tester with Agent Browser Claude Code and Agent Browser let you test your web app in a real browser without hardcoded selectors. Manual browser control, AI-driven exploration, and structured JSON output. .
Finally, put a gate at commit time. Lefthook (or Husky) runs lint, typecheck, and the related tests on every commit, so bad code does not reach main
. I prefer Lefthook because it runs jobs in parallel and the config is YAML the agent can read and edit, not a shell-script soup. The one rule: no --no-verify
escape hatch. The whole point of the gate is that it does not open.
pre-commit:
parallel: true
jobs:
- run: pnpm oxlint
- run: pnpm vue-tsc --build
- run: pnpm vitest related --run {staged_files}
Part 3: Discoverability #
Most Vue apps start flat. You run create vue
and you get components/
, composables/
, stores/
, views/
, router/
. Everything is grouped by what a file is. That is fine for small apps, but in a real application that components/
folder grows to 80+ files, and a single “checkout” change is scattered across three folders. The agent has the same problem you do: it greps across the whole tree and most of what it pulls back is noise.
Feature slicing groups by what files do, not by what they are. First you find the domains. Imagine a workout tracker: the domains are workout, timers, exercises, and settings. Then each domain owns its own components, composables, and store:
src/features/
├── workout/
│ ├── components/
│ ├── composables/
│ └── store.ts
├── timers/
├── exercises/
└── settings/
Finding the domains is the hard part on a big codebase, but once you have them, “change the settings page” means opening one folder. Same files, same code, different addressing. And the win for the agent is concrete. Ask it to “add a streak counter to the active workout.” In a flat layout it greps components/
and composables/
and pulls back SettingsForm
, TimerDisplay
, useSettings
, maybe 40% relevant. In a feature layout it lists features/workout/
and gets exactly the files that matter, 100% relevant. No grep, no guess, tokens go to output instead of search.
Folders alone do not keep the agent honest, so add three import rules.
Arrows only point down the layers. Sibling features never import each other; if they share something, it moves to a shared layer like components/
or lib/
. And a page that uses two features composes them at the page level rather than letting one feature reach into another. The most important rule is the second one: features stay decoupled. As a bonus, a codebase split this way is already shaped for a micro-frontend approach if you ever need it.
Where this is heading #
AFK coding (away from keyboard) is already here. You write a spec, kick off an agent, and walk away. It runs the tests, hits a failure, fixes itself, runs again, and opens a draft PR with a summary. You review. The work is no longer “me typing.” It is “me deciding what should exist, and reviewing what came back.”
The loop in practice has humans at the edges and agents in the middle. You align with the business on the spec, the agent breaks the big ticket into vertical sub-tickets, one agent works each slice with TDD inside, a dedicated refactor pass runs (the step LLMs always skip), a QA agent drives the real browser, and then you read the PR. I wrote the long version in how to do AFK coding How to do AFK Coding A pipeline for shipping a 5-point ticket while you're away from the keyboard: spec with the human in the loop, slice into vertical tickets, run a Ralph loop with TDD per slice, refactor, then let an agent-browser do QA. .
The most important thing in this loop is what you do when it goes wrong. The agent ships a bug, and the instinct is to fix the bug, merge, and move on. That instinct is wrong.
Fix the factory, not the PR. A bug is not a bug, it is a factory defect. Add an ESLint rule that catches that whole class of mistake. Update AGENTS.md so the convention is written down. Tighten the slash command or the prompt if that is where it leaked. The PR fix is one bug. The factory fix prevents the next hundred. And if the codebase is messy in the first place, use the agent to refactor it before you expect any of this to work. Every PR review teaches the factory, and the codebase gets smarter over time.
As a test of all this, the week before the talk I tried to port React Ink (declarative components for terminal UIs) to Vue, using AI only. It worked surprisingly well.
The approach combined everything above. I vendored Ink, Vue core, and VueUse into repos/
as read-only source. I used a brain/
vault for gotchas and an api-tracker. And because React Ink has a lot of tests, I ported the tests first: each Ava scenario became a Vitest test at the behavior level, not the React implementation. Run it, red, no implementation yet. Then translate the implementation, JSX to SFC, hooks to composables. Run it, green. Then reflect the learnings back into brain/
so the loop sharpens itself. I would not publish it as a real library without going back and understanding every line first, but as a proof that you can port a well-tested React library to Vue mostly AFK, it holds up.
I even added a Stop
hook that fires when a turn ends and spawns a second, headless Claude in the background to read the session transcript and write only new durable learnings into brain/
. The one gotcha is that the child also ends a turn, which would fire the hook forever, so an env flag marks the child and the hook bails when it sees it. The project updates its own docs while I get coffee.
The twist #
This talk was not really about AI. I tricked you.
None of it is new. Testing, TDD, feature-based architecture, strict types, small components: these are the things senior engineers have fought for in code review for twenty years. They were good before a single agent existed. The only thing that changed is the payoff. The same discipline that used to make a codebase pleasant for the next human now also makes it tractable for an agent, so every hour you spent caring about architecture quietly started earning twice.
That is the whole bet. Tight context, plenty of feedback loops, real discoverability. Which of them is your project missing? Come find me and tell me what your AGENTS.md looks like.