[Bonus Episode] How to Avoid Spaghetti Code — When, How, and Who Should Review Refactoring

221.131.***.***
12

This is a side story in the tunaFlow development series. This time, it's about "how to maintain code written together with an agent." If you object, you're all right again. 😁

When I see a project made by an agent by someone else, I honestly look at the code first. And as soon as I see it, I think "it's going to be spaghetti code again." This bias is actually often correct. Generally, people either have no software development experience (they don't even have to be developers), or they started with just one idea and think "it works, so let's do it!" and the code keeps growing like a snowball, and without any refactoring, they just keep building, so usually a file like app.js exceeds 1,000 lines, and even when the agent fixes one thing in the middle of the code, it has to read all the files from the beginning again, so the context reaches its limit and it starts fixing strange places. That's what happens.


What is "spaghetti code"?

It's a term I use. The definition is like this:

  • The surface looks fine — function names aren't bad, types are correct, there might even be tests

  • One file exceeds 2,000 lines — it originally started at 200 lines, but it's the result of the agent repeating "I'll add something here"

  • The same logic exists in three places — agents often choose to write new code rather than read existing code

  • Boundaries have collapsed — there are DB calls in UI components, HTTP requests in stores, and somehow "it works"

  • Unmaintainable after 1 month — but if you really have nothing to touch after 1 month, it doesn't matter

LLMs are really good at writing one clean function. But module boundaries and responsibility separation collapse unless a person consciously maintains them. This is the point where it becomes spaghetti.


When do you refactor?

I use three points as signals.

1. Right before adding a new feature

This is the most important. Before asking an agent to "add feature X," I scan the related code and decide like this:

  • If the file where I'm going to add the feature is a god-file exceeding 1,000 lines, split it first then start

  • If similar logic already exists but its location is ambiguous, gather it in one place first then start

  • If the existing code is consistent, proceed as is

Agents work much more accurately on clean code. Adding one more piece of garbage next to garbage is free for the agent, but reading it and maintaining the context is not free for me.

From the tunaFlow actual records, in sessions s26 to s29, I split 5 large god-files into 18 modules. Only after that did the new feature additions come in. At the time, I kept convincing myself that "even if it looks like a waste, it's not a waste," but looking at it now, thanks to splitting it then, I could track what's where while merging 30+ PRs in today's session.

2. Same defect two times in a row

If the same type of finding repeats in the review stage, that's not an implementation bug but a design problem. It's not the agent's fault; I'm having it work on a wrong structure.

The tunaFlow Review workflow has logic to detect this. If continuous fails occur in the same file, it marks a design_review_suggested event, and if it exceeds 3 times, it shows a doom loop warning. The agent trying to fix it alone and repeatedly stumbling at the same place is the most dangerous situation.

3. "Rule of three"

The moment the same pattern is duplicated a third time is the critical point. Up to the second time, it could be a coincidence. The third time is a structural signal.

The important thing here is not to catch it before the third one appears but to abstract it after the third one appears. If it's only appeared twice, prematurely abstracting is what I call "speculative abstraction," and agents are especially good at doing this. When I see code starting with "for future extensibility..." it's unnecessarily complex right now. Until the third one appears, simple copy-paste is better.


How do you refactor?

Cut it into small PRs

If you ask an agent all at once "split this god-file into 18 modules," most of the time it falls apart. Here's what I do:

  1. First, I draw a decomposition map by hand (which module gets what)

  2. I pull out each module as one PR

  3. Each PR must maintain tests (no functional changes, all types/tests pass)

  4. If it gets tangled in the middle, it's better the smaller the PR so it can be rolled back immediately

Of the 30 PRs merged in today's session, the largest was 800 lines, with most in the 50-200 line range. This is intentional design. One large PR seems faster, but if it gets tangled in the middle, you have to revert everything, so it's actually slower.

Have it read existing code first

When I tell an agent to make modifications, what I almost always do is "read all related files first, understand the structure, then tell me." Agents tend to skip reading and jump straight to writing, and unless I explicitly block this, they end up creating something again in a different way that someone already solved right before.

"Review only, no modifications" mode

This is an instruction I often use before entering refactoring. "Just review the code status and don't make modifications. I'll decide, then you modify." This single line is surprisingly effective in preventing spaghetti code. If the agent organizes things on its own judgment in the middle stages, intentions get mixed and review becomes difficult.


How to keep an agent from losing direction in long code

tunaFlow is now 25k lines of Rust + 24k lines of TypeScript in scale. At this point, a single agent can't know everything at once. To keep it from losing direction, context injection on three axes is needed.

Axis 1: Project context (ContextPack)

For every request, I assemble and send the agent:

  • Project identity (document at the level of CLAUDE.md)

  • Current work plan and previous findings

  • Related file index (embeddings + graph)

  • Recent conversation compression (long-term memory)

Since it's practically impossible to do this by hand every time, I created a tool (tunaFlow). "Context quality is more important than model quality" — I feel this here. When making tunaFlow, I kept planning opus and code/fix opus in separate sessions (windows) and sufficiently analyzed with the planning opus (architect) and had it make documents, then just copy-pasted to the coder opus.

Axis 2: Role separation

Even the same agent behaves differently when given different personas by role:

  • Architect: "Make design decisions. Don't jump straight to implementation. Execute after approval."

  • Developer: "Don't touch outside the plan scope. Modify only the requested parts."

  • Reviewer: "Point out only objective defects. Don't assume unverified problems. Separate opinions into recommendations."

Even if all three personas are the same model (e.g., Claude Sonnet), behavior is separated by role-specific prompt constraints. Especially if you put constraints on the Developer persona like "no modifications outside plan scope," you can prevent the agent from spreading out with "while I'm at it..."

Axis 3: Process compartmentalization

A habit of doing one thing at a time. Specifically:

  • Enforce plan → dev → review flow (tunaFlow's workflow pipeline)

  • User approval gate at each step

  • One subject per PR (if two or more are mixed, review quality drops)

Anyone who's tried "do A, do B, and do C too" with an agent knows. The results for all three are ambiguous. Approve A, approve B, approve C is ultimately faster.


How to use a reviewer agent

This is what I most wanted to talk about today.

It falls apart if you review your own code yourself

This is true for people too, but especially for agents. If an agent reviews code it wrote itself, "looks good ✅" is pretty much the default. It thinks it can't miss anything because it already has the context in its head.

Have a different agent review it (better if it's a different engine)

If you have Codex or Gemini review code written by Claude, it catches much better. Because different model families have different biases. Running Claude + Codex is closer to real cross-validation than running Claude twice.

Give the reviewer actual execution results

This is the key point. If you only have the Reviewer "verify the test results reported by the Developer," and the Developer lies with a hallucination saying "tests passed," the Reviewer will pass it as is. This actually happens.

Solution: Actually run cargo test / vitest before calling the Reviewer and embed the output in the Reviewer prompt to pass together. In tunaFlow, the Review RT start button works in this order:

  1. run_project_tests actual execution

  2. Capture stdout/stderr result in testOutput variable

  3. Call startReviewRT(plan, messages, testOutput, engines)

  4. Inject into Reviewer prompt as ## Test Results section

This way, you judge by execution results, not the Developer's self-report. Last year, Anthropic organized something similar in the "Agent-as-Judge" paper, and the core is that the reviewer sees tool results, not text.

Have them do numerical rubrics

Free-form reviews ("pass / fail / findings: [...]") have different standards per reviewer. I force 5-dimensional scoring:

  • plan_coverage (1~5): are the planned subtasks actually implemented

  • code_quality (1~5): runtime/logic/security defects

  • test_coverage (1~5): test results + coverage

  • doc_quality (1~5): comment/document updates

  • convention (1~5): consistency with existing coding style

Auto-judgment by total score (22+ pass / under 10 fail / else conditional). Agents can't issue "feel-good pass." They have to box in the numbers.

Be careful when having external agents review

If you delegate a full review to an external LLM that doesn't know the project context, like Gemini, the safety guardrails come off on a single word like "harsh" (lol). Unfounded decisive judgments pop out, and if you trust that, you end up tearing down perfectly good code.

I went through this the day before yesterday (wrote details in another post). External review should be used only for stimulation, and each point must be validated one by one with code line citations. What I usually get from one external review is 1-2 out of 5. The rest can be laughed off. But you can catch quite critical things, so discuss with the architect. That's the way.


Summary

For me, spaghetti prevention ultimately converges to three things:

  1. Clean up before adding new features. Don't stack features on garbage

  2. Inject context to the agent + impose role constraints. persona + ContextPack + step-by-step approval

  3. Let a different agent review + receive actual execution results + score with numerical rubrics

There's no special trick. It's almost the same as what a senior does when working with a junior in a human team. The difference is that agents demand this pattern 24 hours a day without rest. That's why tools are needed, and that's why I'm building tunaFlow.

It's still far from complete. But at least I'm trying to avoid the situation where "I'm mocking spaghetti code myself while making spaghetti code." 😁


References / Related Articles

  • Sebastian Raschka, Components of a Coding Agent (2025) — "model quality = context quality"
  • Addy Osmani, Orchestrating Coding Agents (2025) — multi-agent orchestration patterns
로그인한 회원만 댓글 등록이 가능합니다.

개발한당

KR | ID | EN
  • IDR
  • KOR
8.34 =0.00

2026.07.11 KEB 하나은행 고시회차 1694회

다가오는 한인 행사일정

  • 등록 된 일정이 없어요!