engineering09/17/2026

What Interning in a Production Monorepo Taught Me About Coding Agents

In my junior year, I interned at ByteDance, my first time working a real job. It threw me straight into production code: an Android client monorepo with modules stretching back nearly a decade and comments from another era. It was a high starting point, and I remain grateful for it, especially because it coincided with the first half of 2026, right as coding agents took off.

Back then, coding agents like Claude Code and Codex became standard daily tools. Everyone burned company tokens exploring AI workflows while models like Gemini 3.1 Pro and Claude Opus 4.7 rolled out, leaving long queues on internal GPU clusters every day. Just six months later, models that felt stunning at the time already sound like antiques. Here is a meme I like on that shift:

AI model evolution meme
Model churn moved fast in early 2026, but foundational software engineering rules stayed steady

After leaving the company, I began building Zafiro, an on-device agent combining Android system capabilities with LLMs, which reached 229 GitHub stars. Managing multiple agents alone to advance the codebase constantly brought back details from my internship.

At the company, strict rules and processes often felt tedious. But directing AI alone across an entire project made their purpose clear: without engineering discipline as guardrails, agents quickly steer a codebase off course, making solo development unmanageable.

Zafiro repository overview
Zafiro repository overview: an on-device agent combining Android system capabilities with LLM decision-making, built by directing multiple coding agents

Deducing proposals with agents before writing code

When I started my internship, my mentor was strict with me. It was his first time mentoring, and his lead rarely reviewed code changes closely, usually just clicking approve. That made my mentor the first and only person accountable for merged code. Worried that I might introduce bugs, he insisted that every technical proposal be vetted repeatedly and every design decision questioned before letting me write code. That pressure forced me to shed my student habits quickly.

To take a real example: some codebases end up with OkHttp, Retrofit, and HttpURLConnection all running side by side. People actually write code like that. On the surface it looks like a missing design document, but it really comes down to basic engineering common sense. A developer might not know every library detail, but where network calls should be abstracted is a shared standard.

Agents make this problem worse. An LLM lacks a long-term view of the project and only looks for the easiest way to finish the current prompt. Without constraints, it copies whatever pattern it finds in the nearest file, producing three different networking layers in the same repository within three days.

My rule with agents is simple: agree on the plan before touching any files. The agent must first inspect the existing architecture, outline its approach, and list possible side effects. Only after we agree on the plan do I let it write code.

Dismantling god classes with a minimal ServiceRegistry

Early in Zafiro, I put all agent orchestration into a single LLMController singleton to validate ideas quickly. Prompt formatting, image handling, lifecycle events, local tools, MCP discovery, and message tree recovery all went into that one file. It quickly grew past 800 lines into a god class.

That debt was manageable while the main chat UI was the only caller. But then three features arrived: a floating window, status sync for the persistent notification bar, and system voice assistant takeover via Xposed and Binder.

The problem became obvious immediately. At the engine level, only one agent turn can run at a time. But on top, four surfaces needed to talk to the agent at once. Calling LLMController directly caused three issues: the notification bar and floating window had to import the heavy controller just to show a stop button, event streams were folded and reduced three separate times, and concurrent writes clashed.

To fix this, I split the business layer into an independent contract module, business:api, with tiered interfaces: a narrow interface, AgentControl, for the floating window, notification bar, and background services, exposing only read-only state streams, stop(), and tool approvals, and a wide interface, Agent, extending AgentControl for chat views and system integrations that need full conversation and draft control.

Connecting callers to implementations is a 40-line, reflection-free in-process ServiceRegistry. Callers use requireService<T>() to get what they need without passing Android Context down constructor chains, and agents are blocked from instantiating private implementations directly.

Why on-device engineering still requires human review

Automated reviewer agent pipelines have become popular, pairing a primary agent with several reviewer agents to critique each other. In practice, reviewer agents often look for things to flag just to justify their presence. Every review produces a fresh list of complaints that real users could never trigger, like edge cases requiring impossible click speeds or obscure funnels, when the code was already ready to merge. Even with strict rules in AGENTS.md to reduce review noise, fully automated pipelines struggle in client-side engineering.

Web and backend services can close the automation loop faster because their tests have clean inputs and outputs. Android is different. Build and install cycles are heavy, and the platform depends on visual layouts, Root and Shizuku permissions, and physical hardware. While an agent can inspect screens with vision models and dispatch taps, the latency and token costs make it impractical. A developer picking up a physical phone, glancing at the UI, and tapping twice is still faster and far cheaper. This setup mirrors Andy L. Jones's essay Horses: horse populations peaked around 1910 right before internal combustion cars replaced them. Today, manual mobile QA is like farriers shoeing horses at that inflection point. AI will keep improving until automated testing takes over, but until that transition flips, a human at the gate is necessary.

My review focuses on high-level structure rather than line-by-line syntax: whether interface contracts were broken, whether temporary workarounds leaked state into public scopes, and whether Android requirements like localization or lean permissions were kept intact. Once the core structure passes inspection, I let lightweight AI review tools catch local edge cases and formatting. Humans guard the architectural boundaries, while AI sweeps local blind spots.

Andy L. Jones Horses vs Cars technology substitution curve
Andy L. Jones technology substitution curve: horse populations peaked right before cars took over in the 1910s. With on-device multimodal testing costs still high, manual QA remains the practical gatekeeper before the turning point

Context relays driven by token budgets

Inside large tech companies, compute was essentially free. Teams often built multi-tiered sub-agent pipelines where a director agent wrote zero code, spending thousands of tokens orchestrating child agents through planning, coding, and cross-reviews. Paying API bills out of my own pocket put an end to that. Passing the entire repository context back and forth between primary and sub-agents wastes most tokens on repetitive summaries. A single coding agent with an agreed plan works much better: given clear constraints, it can work steadily for hours on a heavy task.

The second factor is context window efficiency. Once a session fills roughly 30% of its context window, per-turn costs jump noticeably. Worse, after multiple rounds of trial and error, the prompt fills with obsolete assumptions and discarded code, visibly degrading the model's reasoning.

My approach is to run a handoff around the 30% mark. I have the active agent summarize established facts, architectural choices, and remaining tasks into a brief handoff document. A fresh agent then launches in a new session with only that clean document. Without the baggage of past dead ends, reasoning speed and code quality jump back up while keeping token spend in a reasonable range.

Parallel multi-branch development with Worktrees

Maintaining steady throughput as a solo developer comes down to managing terminal sessions cleanly. Running multiple Claude Code instances in Ghostty splits gets confusing fast. I tried herdr to manage terminals inside a terminal, but a GUI is clearer. Now I use Orca: projects organize into worktrees where each branch runs a dedicated coding agent. It tracks session_id to restore terminal state after reboots, and automatically suspends idle sessions to save battery on my MacBook Air.

Under this setup, each feature branches into its own worktree directory. An agent's attention stays strictly bounded within its branch, leaving dependency coordination and final PR review to me.

During rapid iteration, I pushed several large features at once: the agent API refactor, the floating window UI, and the persistent notification bar. Because core contracts and ServiceRegistry were already decoupled, those branches advanced independently in parallel worktrees without colliding on main.

Terminal manager multi-worktree session view
Terminal manager multi-worktree view: projects expand into worktree levels where each branch runs a dedicated coding agent with session persistence and automatic idle suspension
Git parallel branch merge topology
Branch merge topology in Zafiro: protected by `business:api` contracts and `ServiceRegistry`, features like the floating window UI, notification bar, and host integration advanced in parallel worktrees before merging into main

Engineering fundamentals in the era of abundant code

Working in a large monorepo did not give me a taste for heavy frameworks, but it made basic engineering common sense obvious.

Many developers still judge LLMs purely by whether they can generate a feature in one prompt. But anyone paying their own token bills while shipping code realizes that cheap code generation makes engineering discipline, like pre-coding plans, decoupled contracts, and clear boundaries, far more valuable.

Respecting complexity, enforcing module contracts, and vetting technical plans before writing code are basic engineering practices. When directing AI alone, they are the most practical way to sustain high development throughput.