{"slug": "clean-code-is-sexy-again-making-your-vue-project-ai-ready", "title": "Clean Code Is Sexy Again: Making Your Vue Project AI-Ready", "summary": "Vue developer and speaker presented at Vue MAD 2026, arguing that clean code practices that benefit human developers also make Vue projects AI-ready. The talk emphasized that coding agents are not magic but loops of context, tools, and verification, and that investing in context files, feedback loops, and discoverability is key to effective AI-assisted development.", "body_md": "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](https://www.youtube.com/watch?v=9bKMqvFRvvI). Otherwise, read on.\n\nClean Code Is Sexy Again, Vue MAD 2026\n\n## TLDR\n\n- My main take:\n**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\n*and*your 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.”\n- Three things every Vue project should invest in:\n**context**(AGENTS.md, skills, hooks, a`brain/`\n\n),**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.\n\n## The line everyone keeps repeating\n\nEveryone keeps saying the same thing: *you don’t have to write code anymore.*\n\nI 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.\n\nA 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.\n\nI 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.\n\nTriumph or disaster, I don’t know. What I know is this is the world we ship into.\n\n## What an agent actually is\n\nMost 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 ](/posts/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.\n\nThe 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.\n\nStrip the magic and a **tool is just a function**. Three fields: a description, a schema for the arguments, and the function that runs.\n\n```\ntype Tool = {\n  description: string\n  schema: Record<string, string>\n  execute: (args: Record<string, unknown>) => Promise<string>\n}\n\nconst TOOLS = new Map<string, Tool>([\n  ['read', {\n    description: 'Read file with line numbers (not a directory)',\n    schema: { path: 'string', offset: 'number?', limit: 'number?' },\n    execute: read,\n  }],\n  ['edit', {\n    description: 'Replace old with new in file (old must be unique)',\n    schema: { path: 'string', old: 'string', new: 'string' },\n    execute: edit,\n  }],\n  ['bash', {\n    description: 'Run shell command',\n    schema: { cmd: 'string' },\n    execute: bash,\n  }],\n])\n```\n\nHere is the part that demystifies everything: **the model only ever sees the description and the schema.** The `execute`\n\nfunction never leaves your machine. The model cannot run `read()`\n\n, 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.\n\nAnd the **loop is just recursion**.\n\n``` js\nasync function agentLoop(messages, systemPrompt, tools = TOOLS) {\n  const response = await callApi(messages, systemPrompt, tools)\n  const toolResults = await processToolCalls(response.content)\n\n  const newMessages = [\n    ...messages,\n    { role: 'assistant', content: response.content },\n  ]\n\n  // No tool calls → the agent is done\n  if (toolResults.length === 0) return newMessages\n\n  // Tool calls → loop with results as the next user turn\n  return agentLoop(\n    [...newMessages, { role: 'user', content: toolResults }],\n    systemPrompt,\n    tools,\n  )\n}\n```\n\nCall 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`\n\nis not a database or a session store. It is an array of `{ role, content }`\n\n. That array *is* the agent’s memory. Every turn you append to it.\n\nI 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.\n\nOnce 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.\n\n## Clean code isn’t nice-to-have anymore\n\nI 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.\n\nIn Vue, three things are worth investing in: **context**, **feedback loops**, and **discoverability**.\n\n## Part 1: Context\n\nThe 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.\n\nThat tattoo is **AGENTS.md** (or `CLAUDE.md`\n\n). 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.\n\nEvery 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`\n\n.\n\nThis 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:\n\n```\n# AGENTS.md\n\nRun `pnpm lint:fix && pnpm typecheck` after changes.\n\n## Stack\nNuxt 4, @nuxt/content v3, @nuxt/ui v3\n\n## Structure\n- `app/` — Vue application\n- `content/` — Markdown files\n\n## Further reading\n\n**IMPORTANT:** read the relevant doc below\nbefore starting any task.\n\n- `docs/nuxt-content-gotchas.md`\n- `docs/testing-strategy.md`\n```\n\nThe agent loads `testing-strategy.md`\n\nonly when it writes a test, `nuxt-content-gotchas.md`\n\nonly 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/`\n\n. I go deeper on the AGENTS.md-versus-skills split in my [ Claude Code customization guide ](/posts/claude-code-customization-guide-claudemd-skills-subagents/) 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 ](/posts/reverse-engineering-humanlayer-rpi-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. .\n\n### brainmaxxing\n\nForget looksmaxxing. We are brainmaxxing: maxing the one thing the agent has, its context. [brainmaxxing](https://github.com/poteto/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.\n\nFirst, a `brain/`\n\nfolder. Plain markdown. The agent reads from it and writes back to it. There is one `index.md`\n\nwith 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 ](/posts/four-types-memory-coding-agents-claude-code/) 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. .)\n\nSecond, **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 loading, so skills scale. brainmaxxing’s `/reflect`\n\nskill is a good example: at the end of a session it reviews the conversation and persists what mattered back into `brain/`\n\n. So when the agent uses the wrong command and you correct it, you just say “remember this,” and it updates its own memory.\n\nThird, **hooks**. Vue has lifecycle hooks like `onMounted`\n\n. Claude Code has lifecycle hooks too. brainmaxxing wires a `SessionStart`\n\nhook that cats `index.md`\n\ninto every new session, so the agent boots up already knowing the map. There are many more events. `PreToolUse`\n\ncan block a destructive command or forbid reading your `.env`\n\n. `PostToolUse`\n\ncan re-lint after an edit. `SessionEnd`\n\ncould even send you a notification. I cover skills and hooks hands-on in my [ Vue workout tracker walkthrough ](/posts/claude_code_skills_hooks_subagents_vue_workout_tracker/) 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. .\n\nbrainmaxxing 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.\n\nClaude 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/`\n\nin 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.\n\n### Want VueUse-style code? Give the agent VueUse\n\nOne 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:\n\n```\ngit subtree add --prefix=repos/vueuse \\\n  https://github.com/vueuse/vueuse main --squash\n```\n\nThen point AGENTS.md at it:\n\n```\n## Reference repositories\n\n- `repos/vueuse/` — when writing new composables, mirror the\n  patterns in `repos/vueuse/packages/core/use*/`.\n```\n\nNow 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`\n\n(compiled and flattened, the structure is gone). One refinement: tell your editor to ignore `repos/`\n\nso you do not accidentally auto-import VueUse internals, and have the agent distill what it learned into a short `agent-patterns/vueuse.md`\n\nso 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](https://effect.website/blog/the-one-weird-git-trick-that-makes-coding-agents-more-effect-ive/).\n\n## Part 2: Feedback loops\n\nIf an agent does not know when it broke something, it ships whatever compiles. So you give it deterministic ways to check its own work.\n\n**Type safety** is the first lie-detector. A strict `tsconfig`\n\nis non-negotiable, and `noUncheckedIndexedAccess`\n\nand `exactOptionalPropertyTypes`\n\ncatch 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.\n\n**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:\n\n```\n'vue/max-template-depth': ['error', { maxDepth: 8 }]\n'vue/max-props': ['error', { maxProps: 6 }]\n```\n\nA 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 ](/posts/opinionated-eslint-setup-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.\n\nThen **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 ](/posts/vue3_testing_pyramid_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. .\n\nThose 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.”\n\nThe full breakdown lives in [ the modern frontend quality pipeline ](/posts/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. .\n\n### The agent as a user\n\nStatic 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](https://github.com/vercel-labs/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.\n\n```\nnpm i -g agent-browser\nagent-browser install\n\nagent-browser open localhost:5173\nagent-browser snapshot -i          # DOM tree with @refs\nagent-browser click @e2            # click by ref\nagent-browser screenshot --annotate\nagent-browser console              # read page console\n```\n\nThe 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.\n\nFor 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 ](/posts/automated-qa-claude-code-agent-browser-cli-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. .\n\nFinally, 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`\n\n. 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`\n\nescape hatch. The whole point of the gate is that it does not open.\n\n```\n# lefthook.yml\npre-commit:\n  parallel: true\n  jobs:\n    - run: pnpm oxlint\n    - run: pnpm vue-tsc --build\n    - run: pnpm vitest related --run {staged_files}\n```\n\n## Part 3: Discoverability\n\nMost Vue apps start flat. You run `create vue`\n\nand you get `components/`\n\n, `composables/`\n\n, `stores/`\n\n, `views/`\n\n, `router/`\n\n. Everything is grouped by what a file *is*. That is fine for small apps, but in a real application that `components/`\n\nfolder 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.\n\nFeature 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:\n\n```\nsrc/features/\n├── workout/\n│   ├── components/\n│   ├── composables/\n│   └── store.ts\n├── timers/\n├── exercises/\n└── settings/\n```\n\nFinding 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/`\n\nand `composables/`\n\nand pulls back `SettingsForm`\n\n, `TimerDisplay`\n\n, `useSettings`\n\n, maybe 40% relevant. In a feature layout it lists `features/workout/`\n\nand gets exactly the files that matter, 100% relevant. No grep, no guess, tokens go to output instead of search.\n\nFolders alone do not keep the agent honest, so add three import rules.\n\nArrows only point down the layers. Sibling features never import each other; if they share something, it moves to a shared layer like `components/`\n\nor `lib/`\n\n. 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.\n\n## Where this is heading\n\nAFK 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.”\n\nThe 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 ](/posts/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. .\n\nThe 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.\n\n**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.\n\nAs a test of all this, the week before the talk I tried to port [React Ink](https://github.com/vadimdemedes/ink) (declarative components for terminal UIs) to Vue, using AI only. It worked surprisingly well.\n\nThe approach combined everything above. I vendored Ink, Vue core, and VueUse into `repos/`\n\nas read-only source. I used a `brain/`\n\nvault 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/`\n\nso 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.\n\nI even added a `Stop`\n\nhook 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/`\n\n. 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.\n\n## The twist\n\nThis talk was not really about AI. I tricked you.\n\nNone 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.\n\nThat 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.", "url": "https://wpnews.pro/news/clean-code-is-sexy-again-making-your-vue-project-ai-ready", "canonical_source": "https://alexop.dev/posts/clean-code-is-sexy-again-vue-ai-ready/", "published_at": "2026-06-27 00:00:00+00:00", "updated_at": "2026-06-27 12:40:21.112076+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools", "large-language-models", "ai-products"], "entities": ["Vue", "Claude Code", "Bun", "Dan Shapiro", "Vue MAD 2026", "YouTube"], "alternates": {"html": "https://wpnews.pro/news/clean-code-is-sexy-again-making-your-vue-project-ai-ready", "markdown": "https://wpnews.pro/news/clean-code-is-sexy-again-making-your-vue-project-ai-ready.md", "text": "https://wpnews.pro/news/clean-code-is-sexy-again-making-your-vue-project-ai-ready.txt", "jsonld": "https://wpnews.pro/news/clean-code-is-sexy-again-making-your-vue-project-ai-ready.jsonld"}}