I do a lot of heavy vibe coding across my projects, and after reviewing hundreds of AI-generated pull requests, one thing is obvious: when an LLM gets stuck on a compiler error or an edge case, its training on millions of quick-and-dirty StackOverflow answers takes over.

When a human developer sees a TypeScript error or a failing test, the normal instinct is to trace why the data shape doesn't match. When an LLM sees that same error during an autonomous loop, its primary goal is making the red text in the terminal disappear as fast as possible. Effectively,the model starts writing untyped, defensive JavaScript inside your.ts files.

The worst part is that your automated checks (tsc --noEmitand unit test runners) will pass completely green, because the model specifically wrote code designed to silence the compiler and stop exceptions from bubbling up.

Here are the three biggest silent flaws LLMs inject into production codebases, how I use jules-roles to hunt them down, and the three-step workflow that actually keeps a vibe-coded repo clean.

1. The StackOverflow Type Escape (as any)

Whenever an AI agent runs into a strict interface mismatch, a complex generic, or a third-party SDK type that would require reading three other files to resolve properly, it reaches straight for the classic StackOverflow shortcut: casting to any.

You start seeing patterns like this buried deep inside implementation files:

// What the LLM writes when tsc complains about a webhook payload
const userId = (payload as any)?.data?.user?.id || (payload as unknown as Record<string, any>).id;

To the TypeScript compiler, that line is completely valid. Your build passes, your CI turns green, and you just lost all type checking at the exact boundary where external data enters your system. If you change the interface two weeks later, tsc won't warn you thatuserId is now permanently undefined at runtime.

2. Silent Error Swallowing and Empty catch Blocks

Nothing breaks production debugging faster than how LLMs handle exceptions.

During an iterative coding session, if an async call, file read, or JSON parse throws an error during a test run, the model wants the test to pass. Its favorite move is wrapping the offending block in a try / catchthat silently eats the error and returns a harmless-looking fallback value:

// Classic LLM defensive wrapper that hides broken production state
try {
  await syncBookingToDatabase(env.DB, bookingRecord);
} catch {
  // ignore error and continue
}

Or slightly sneakier:

const config = await fetchRemoteConfig().catch(() => ({ enabled: false }));

In local testing, your app feels rock solid because nothing ever crashes. In production, when a database migration fails, an API token expires, or a webhook payload is malformed, the app swallows the exception without logging a single warning, returns a fake empty state, and leaves you wondering why customer records stopped saving three days ago.

3. Iterative Bloat: Huge Nested Imperatives Instead of Declarative One-Liners

Even if the first version of an AI-generated function is clean,iterative prompting ruins code structure.

Suppose you have a function that filters and formats a list of items. In prompt one, it's a clean five-line function. Then you ask the agent to skip archived items. Then you ask it to group by category. Then you ask it to handle null timestamps.

Because LLMs prefer patching localized if / else branches into existing code rather than stepping back and rewriting the expression, they pile mutable let variables, nested forloops, and early continue guards on top of each other:

// After 4 iterative prompts: nested imperative spaghetti
const results: Item[] = [];
if (rawItems && Array.isArray(rawItems)) {
  for (let i = 0; i < rawItems.length; i++) {
    const item = rawItems[i];
    if (item && !item.isArchived) {
      if (item.timestamp !== null && item.timestamp !== undefined) {
        let formattedName = item.name ? item.name.trim() : "Untitled";
        if (formattedName.length > 0) {
          results.push({ ...item, name: formattedName });
        }
      }
    }
  }
}

Instead of a clean, declarative pipeline (rawItems.filter(...).map(...)), you end up with a 30-line imperative pyramid that takes five minutes to read and is a nightmare to modify next month.

Because these patterns specifically bypass tsc and test runners, catching them with static checks alone is nearly impossible.

To deal with this across my own repositories, I rely on three specialized background roles from jules-roles that iterate through the codebase—starting with the most recently modified files—and hunt down these exact habits:

RoleWhat It Hunts in Vibe-Coded FilesHow It Fixes Them
Prism (Type Integrity)as any, as unknown as T, looseRecord<string, any>, and untyped JSON responsesStrips out type-escape casts and wires up real TypeScript interfaces, generics, or runtime type guards so .ts files stop acting like plain JS.
Vigil (Error & State Safety)Empty catch {} blocks, swallowed.catch(() => null) promises, and silent fallback returnsRemoves blind error swallowing, restores proper error propagation, and ensures failures actually surface in logs instead of failing silently.
Craftsman (Code Structure)Deeply nested if / for pyramids and mutablelet flags accumulated over multiple promptsFlattens nested imperative loops back into clean, single-purpose declarative functions and early returns.

When I first ran Prism, Vigil, andCraftsman across my own vibe-coded projects, they found an almost impossible number of swallowed errors and as anycasts hiding inside code that was already running in production.

Trying to force an LLM to write perfect, declarative, strictly typed code on the very first prompt—while you are still figuring out how the feature should work—usually slows you down and burns tokens on premature refactoring.

A much more practical workflow is splitting creation from cleanup into three distinct steps:

  1. Create (Vibe the Feature): Let the model build the feature fast. Don't worry if it uses ugly nested loops or drops anas any cast while you are still iterating on the UI or business logic.
  2. Put Tests in Place: Once the feature works the way you want, lock down its behavior with automated tests covering the happy path and edge cases. Now the expected inputs and outputs are frozen.
  3. Clean Up the Mess Without Breaking the Tests: Point your cleanup pass (Prism, Vigil, andCraftsman) at the modified files. Because the test suite is already in place as a safety net, the agents can aggressively strip out every as any, expose swallowed catchblocks, and collapse nested imperative blocks into clean declarative code—while the tests verify that the actual behavior didn't break.