Loop Engineering in Practice: What No Middleman Taught Me About Verifying AI-Written Code

GitLab’s 2026 AI Accountability Report found that 78% of developers say they’re writing and committing code faster with AI tools. It also found that overall software delivery hasn’t sped up to match. The gap is explained by one number: 85% of respondents say AI has shifted the bottleneck from writing code to reviewing and validating it. Everyone got a faster typist. Almost nobody got a faster team.

That’s the exact problem I’ve been building against with No Middleman, a Claude Code plugin for React Native that lets an agent loop on a feature or bug fix unattended, but only ever hands you a pull request that’s already been proven against a real emulator, not just asserted as done. I wrote about the general shape of agent control loops in a previous post. This one is about the part of that loop that actually matters once you let the agent run without you watching: verification.

Trusting the diff doesn’t scale

The default way most of us review AI-generated code right now is to read the diff and decide if it looks right. That works when the change is small and you already understand the surrounding code. It stops working the moment the agent is iterating on its own, because “looks right” is a judgment about static text, and the failure modes that matter, a race condition, a mock that silently changed behavior, a test that got loosened instead of the code getting fixed, don’t show up in a diff. They show up when the thing runs.

GitLab’s finding lines up with what you’d expect from that: reviewing and validating AI output is slower and harder than writing the prompt that generated it, so the bottleneck moved downstream instead of disappearing. Reading harder isn’t the fix. Verifying differently is.

Assess, act, verify

No Middleman’s loop is a typed LoopSpec run on XState v5: a trigger, an intake (a goal plus the file globs it’s allowed to touch), a composite verification chain ordered cheap to expensive, and a stopping rule with a hard budget. Nothing about “done” is left to the model’s judgment.

nm verify --e2e

That single command is the thing the loop iterates against. It runs typecheck, lint, unit tests, and a build, in that order, fail-fast, before it ever touches an emulator. Only once those pass does it get to the expensive check. Cheap gates first means a broken import gets caught in seconds, not after a five-minute Detox run.

Detox as the oracle, not a formality

The headline gate is a Detox end-to-end flow, and the workflow deliberately puts a human at the point where the test is defined, not where it’s checked. You write the goal, a spec-author agent drafts the acceptance flow, and you approve it before anything starts. That red test is the contract. The agent never gets to negotiate what “done” means after the fact, because the definition was fixed before it wrote a line of code.

The dev client compiles once, cached by a hash of the native inputs, and every iteration after that just reloads JS. That’s what makes a real Detox run cheap enough to sit inside a loop instead of being a once-a-day CI job: the expensive part only happens when something native actually changed.

The checker can’t be the maker

The part I’d guess most people skip is the one that matters most: green isn’t the finish line. Once the suite passes, a separate Opus agent, one that never touched the implementation, reviews the diff specifically hunting for the ways a model games its own tests: weakened assertions, mocks that quietly make the test meaningless, timing masked instead of fixed, scope that crept outside what the goal asked for. It’s an adversarial checker by design, not an optimistic second opinion.

That split exists because letting an agent grade its own homework is exactly how you get a green checkmark on code that doesn’t actually work. If the same model that wrote the fix also decides whether the fix is good, its incentive is to reach green by whatever path is shortest, and loosening a test is a shorter path than fixing the underlying bug. Separating maker from checker removes that incentive.

Invariants, not intentions

The loop won’t even start if a spec violates one of eight hard rules: require-verifiable-stop, bounded-retries, maker-neq-checker, tests-only-strengthen, external-state-only, read-only-by-default, human-approves-acceptance, pr-not-merge. tests-only-strengthen is the one doing the most quiet work: a test can get stricter as the loop iterates, never looser. That closes off the single easiest way an unsupervised agent could cheat its way to a passing run.

pr-not-merge closes the other one. The loop’s output is always a draft pull request, whether the checker accepted the change or the iteration budget ran out first. Nothing merges itself. A hard cap on iterations, tokens, and wall-clock time, plus a reserve the loop never spends on iteration, means it always has enough left to write a clean handoff instead of dying mid-edit. And if something looks wrong while it’s running, touch .nm/KILL halts it between iterations and leaves a clean worktree behind.

The two ends you still own

The name is literal: the loop removes you from its interior, the part where you’d otherwise sit watching each edit-reload-verify cycle and nudging it back on track. It doesn’t remove you from the two ends. You write the acceptance test that defines what “done” means, and you review the PR that comes out. Autonomous interior, human bookends. That’s a different claim than “the agent tested itself,” and it’s the reason I trust the output more than I trust a diff someone asks me to skim.

What this means for reviewing AI code generally

The GitLab numbers describe a team where AI made the input to review faster without making review itself faster, which is a bottleneck relocation, not a productivity gain. The fix isn’t reading diffs more carefully. It’s building a verifier that runs the code and grades it against a test you wrote and a checker that has no stake in the outcome, so that by the time a human opens the PR, the question isn’t “does this look right,” it’s “do I approve of the test that already proved it.”


Loop Engineering: Designing the Control Loop Behind Autonomous Agents

Most of the interesting failures in an agentic system are not in the prompt. They are in the loop. Once an agent is planning, calling tools, checking its own output, and deciding whether to go again, the thing you are actually building is a control loop with an LLM sitting inside it. Loop engineering is the name that has stuck for the discipline of designing that structure on purpose, and it is worth understanding as its own layer, separate from prompt design.

Where the term comes from

The term got its clearest articulation in Addy Osmani’s Loop Engineering essay, which argues that the skill that actually separates reliable agents from flaky demos is not prompting, it’s the surrounding loop: how the agent decides what to do next, how it recovers from a bad step, and how it knows when to stop. That framing is why the term has since collected its own body of references rather than staying folded into general prompt engineering advice. The Loop Engineering Sources page and the Awesome Loop Engineering list on Hugging Face are both attempts to keep that growing literature in one place, and the Agentic AI Knowledge Base folds it into its broader documentation on agent harnesses, which is a fitting place for it: a loop is infrastructure, not a prompt technique.

The anatomy of an agent loop

Strip away the framework abstractions and most agent loops are doing the same four things on repeat:

  1. Observe — read the current state: user input, tool results, prior steps.
  2. Plan — decide the next action, which might be a tool call or a final answer.
  3. Act — execute the action.
  4. Check — decide whether to loop again, stop, or escalate.

The fourth step is the one frameworks tend to hide, and it is the one worth the most attention. If the model decides for itself when it is “done,” you need an explicit answer for what happens when it decides wrong, either stopping too early or never stopping at all.

Termination is the whole game

A model that is uncertain will often choose to gather more information rather than commit to an answer, and “gather more information” is an action the loop is perfectly happy to repeat indefinitely. So termination cannot be left to the model alone. In practice, a few overlapping guards catch almost every runaway loop:

MAX_ITERATIONS = 12
MAX_TOOL_CALLS_PER_TASK = 20
MAX_WALL_CLOCK_SECONDS = 300

while not done and iterations < MAX_ITERATIONS:
    action = model.plan(state)
    if is_repeat_of_last_n(action, history, n=3):
        escalate("loop appears stuck, repeating the same action")
        break
    state = execute(action)
    iterations += 1

None of these guards are clever, and that’s the point. A hard iteration cap, a repeated-action detector, and a wall clock timeout are cheap to reason about compared to trying to make the model smarter about knowing when it’s finished.

Stop hand-holding, start designing the loop

There’s a habit that shows up in a lot of early agent projects: a human sits next to the loop, watching each step, nudging it back on track whenever it drifts. That works for a demo. It does not scale, and it quietly hides the fact that the loop itself has no real recovery strategy. The paper Stop Hand-Holding Your Coding Agent pushes on exactly this: the fix for an agent that needs constant supervision is not more supervision, it’s giving the loop the guardrails, checkpoints, and escalation paths that let it run unattended in the first place. If your agent only works when someone is watching it, the loop design is the thing to fix, not the babysitting routine.

Loops vs. deterministic graphs

A freeform while loop where the model decides every next step is flexible, but flexibility and reliability trade off against each other as the task gets more complex. The paper From Agent Loops to Deterministic Graphs makes the case for pulling structure out of the model’s head and into an explicit graph: known transitions become edges, and the model is only asked to make a decision at the points where a decision genuinely needs judgment. This mirrors something that’s easy to miss when you’re deep in an unstructured loop: not every step needs an LLM call. The steps that are deterministic should be written as deterministic code, and the loop should reserve the model for the parts that actually require reasoning.

Robustness as a property of the loop, not a single run

A loop that works once is not the same as a loop that works reliably over weeks of unattended operation. The paper Engineering Robustness into Personal Agents with the AI Workflow Store looks at this from the angle of personal agents that run continuously in the background, and argues for treating recurring workflows as reusable, versioned artifacts rather than re-deriving the plan from scratch on every run. That’s a useful reframe: a lot of what looks like “agent reliability” is really “workflow reliability,” and workflows that have been run, checked, and refined once are a better foundation than a loop that starts from a blank plan every time.

Context is a loop problem

The other place loops fail quietly is context. Every iteration appends to the state the next iteration reads, so a loop that runs long enough eventually chokes on its own history. Two things matter here:

  • Compaction, not just truncation. Summarizing the last several tool results into a paragraph keeps the signal and drops the noise. Dropping the oldest turns blindly tends to drop the reason the agent started the task in the first place.
  • Cache-aware batching. If your provider caches prompt prefixes, an iteration that reads back its own history within a few minutes is cheap. One that reads it back an hour later pays full price, which changes how you should schedule retries or background steps in a long-running loop.

A lot of what gets labeled “agent reliability” is really “context management” wearing a different name. A loop that’s correct on iteration two, verbose by iteration six, and hallucinating a tool result on iteration ten usually isn’t a reasoning failure. It’s a context window that scrolled past the thing that mattered.

Fixed intervals vs. self-pacing

There’s a design choice that shows up in almost every long-running agent: does the loop wake up on a fixed schedule, or does it decide its own pace? Fixed intervals are simple and predictable, which is right when you’re polling something external that changes on its own clock, like a CI run or a data feed. Self-pacing fits better when the loop is waiting on its own work: it can reason about how long the next step will plausibly take and schedule accordingly, instead of burning cycles checking on something that has no reason to have changed yet. Defaulting to a short fixed interval for everything, on the theory that checking more often is safer, usually just makes the loop more expensive without making the task finish any faster.

Failure handling belongs in the loop, not the prompt

The instinct when a tool call fails is to describe the error to the model and ask it to recover. That works for the failures you anticipated. It does not work for the ones you didn’t, which is most of them once a system reaches production. What holds up better is treating failure classes as loop-level branches instead of prompt-level instructions:

try:
    result = execute(action)
except TransientError:
    retry_with_backoff(action, max_retries=2)
except SchemaError:
    escalate_to_human(state, reason="tool contract violated")
except Exception:
    escalate_to_human(state, reason="unhandled failure")

The model gets to be clever about the task. The loop stays boring and predictable about failure. That split is deliberate, and it’s why most of the interesting bugs in a mature agent trace back to a loop that had no opinion about failure until it was already in one.

The checklist

  1. Cap iterations, tool calls, and wall clock time explicitly. Don’t trust the model to know when it’s done.
  2. Detect repeated actions. A loop that calls the same tool with the same arguments twice in a row is stuck, not thorough.
  3. Push deterministic steps out of the model and into code. Reserve the LLM call for the points that actually require judgment.
  4. Compact context instead of truncating it. Summarize what happened, don’t just chop off the beginning.
  5. Match the wake-up interval to what you’re actually waiting for. External clock, fixed interval. Your own work, self-paced.
  6. Push failure handling into the loop, not the prompt. Classify errors, decide programmatically, escalate what you can’t classify.
  7. Treat recurring workflows as reusable artifacts, not something the agent re-derives from scratch every run.

None of this is exotic. It’s closer to the discipline you’d apply to a retry queue or a background job processor than anything specific to LLMs. That’s probably why it gets skipped: it doesn’t feel like AI work, so it doesn’t get the attention the prompt does. It should.


How to Actually Test Autonomous AI Agents

Testing autonomous AI agents is the part of the stack that most teams underestimate. We have spent the last two years getting comfortable with prompt evaluation, golden datasets, and the occasional LLM judge. None of that is enough once the system starts planning, calling tools, and looping over its own decisions. Below is my synthesis of the best material I have read on agent evaluation recently, plus the approach that is actually working in production for Capitol Trades Tracker, the agentic AI app I run for tracking congressional stock trades.

Why classic input output testing breaks

The clearest framing comes from Comet’s piece on agent evaluation. Traditional testing assumes a single input maps to a single output. Agents do not behave that way. They branch, retry, recover, and sometimes solve the right problem the wrong way. A pass or fail on the final answer hides everything interesting about how the agent got there.

The agent evaluation deep dive makes the same point when it separates outcome evaluation from trajectory evaluation. Outcome tells you the agent finished. Trajectory tells you whether it should be trusted to finish again next time. Both matter, and they fail in different ways.

The four layers I care about

After reading through the agent evals guide, the Evaluating AI Agents manual, and the AI Evals Roadmap by Hamel Husain and friends, I keep coming back to four layers that need their own tests:

Layer What it measures Why it matters
Final outcome Did the agent solve the task Easy to score, easy to game
Trajectory Which tools were called, in what order, with what arguments Where most real bugs live
Planning quality Is the plan coherent and well decomposed Catches reasoning failures before they ship
Runtime behavior Latency, cost, retries, hallucinated tool calls, silent failures Determines whether the agent is viable in production

If you only test the first one, you ship an agent that passes your evals and quietly burns money in production.

Trajectory evaluation is the unlock

The single biggest shift for me was treating trajectories as the primary unit of evaluation. The agent evals guide describes this well, and the O11yBench benchmark takes it further by measuring agents on real observability workflows like log triage and incident response. The benchmark scores the path, not just the conclusion. That matches what I see when reviewing agent traces. A correct answer reached through six redundant tool calls is a failure waiting to happen at scale.

Practical version of this in code looks like trace based assertions. Capture the full execution, then write checks like:

assert trace.contains_tool_call("search_logs")
assert trace.tool_call_count("retry_query") <= 1
assert trace.total_tokens < 8000

This is closer to integration testing than unit testing. That is the point.

LLM judges are useful but not load bearing

The agent evals guide and the roadmap article both spend time on LLM as judge patterns. They work for grading open ended responses where a rubric is hard to encode. They are unreliable as the only signal. The pattern I trust is rubric scoring with a small, fixed rubric per task type, calibrated against human labels on a sample. Anything beyond that drifts.

The AlphaEval approach pushes this further by grounding evaluation in real business workflows across software engineering, finance, and operations. The lesson is that synthetic benchmarks tell you the agent can do tasks. Real workflow benchmarks tell you the agent can do your tasks.

Production is its own test environment

The Reinventing.ai piece on production testing argues that synthetic benchmarks systematically miss the failure modes that matter. I agree. Evaluation drift is real. The distribution of user requests in week six rarely matches the distribution you designed evals for in week one.

What I do in production:

  • Sample a fixed percentage of live traces every day and score them with the same rubric used in CI.
  • Alert on trajectory anomalies, not just error rates. A 30 percent jump in average tool calls per task is a bug, even if nothing crashes.
  • Keep a small human-in-the-loop review queue for the lowest-confidence runs. The cost is low and the signal is the best you can get.

This is the same loop the Evaluating AI Agents manual recommends, and it matches what Husain calls operational evaluation in the roadmap article.

My assertive take

Most teams testing agents today are still doing prompt evals dressed up as agent evals. That is not enough. Here is what I think actually works, in order of impact:

  1. Trace everything from day one. If you cannot replay an agent run end to end, you cannot evaluate it. OpenTelemetry style instrumentation is non-negotiable.
  2. Score trajectories, not just outputs. Tool correctness, call order, retry behavior, and token budget belong in your test suite alongside final answers.
  3. Build a real-workflow eval set. Twenty hand-curated tasks from your actual product beat two thousand synthetic ones. AlphaEval and O11yBench are right about this.
  4. Run evals in CI and in production. The same rubric, the same scoring code, sampled live. Drift is the default state of any LLM system.
  5. Use LLM judges sparingly. Calibrate against humans, keep rubrics short, never let a judge be the only gate.
  6. Treat reliability as a product feature. Latency, cost, and consistency are part of correctness for an agent. A 90 percent accurate agent that costs four dollars per run is broken.

The teams shipping reliable agents are not the ones with the cleverest prompts. They are the ones who treat evaluation as engineering infrastructure, with the same seriousness they would give a database migration or a payments pipeline. That is the bar.

If you want a single starting point, read the agent evals guide for the conceptual frame, then go straight to the Evaluating AI Agents manual for the operational playbook. Everything else is variations on those two themes.


Awesome React Native Skills: Claude Skills for Modern Mobile Dev

awesome-react-native-skills is a curated set of Claude Skills for building production-grade React Native apps in 2026. Each skill is a self-contained folder with reference docs and conventions, so Claude can pick up the right context the moment you ask it for help on a navigation bug, a Reanimated transition, or an EAS build issue.

Why this exists

I wrote about building reusable Skills for Claude a few weeks ago. The same idea applies cleanly to React Native, maybe even more so. The ecosystem moves fast: the New Architecture is the default, Expo ships a new SDK every few months, and the “right” way to do navigation, state, or styling shifts often enough that stale answers are the norm. Packaging the current good practices as Skills means Claude loads the relevant slice on demand instead of guessing from training data.

What’s inside

Six skill groups, each focused on one part of the stack:

  • React Native Core — native primitives, platform APIs, animations, gestures, accessibility.
  • React Native Ecosystem — navigation, state management, data fetching, and the libraries you actually ship.
  • React Native Expo — Router, EAS Build/Update/Submit, SDK upgrades.
  • React Native Reusables — shadcn/ui-style components built on NativeWind v4.
  • React Native Performance — profiling, measurement, and the optimizations that move the needle.
  • React Native Testing — Testing Library v13/v14 patterns for unit and integration tests.

Tech the skills cover

  • React Native 0.76+ with the New Architecture on by default
  • Expo SDK 53 to 56
  • React Navigation v7
  • TanStack Query v5 for server state
  • Zustand, Jotai, and Redux Toolkit for client state
  • Reanimated v3 and Gesture Handler v2
  • NativeWind v4 for styling
  • Testing Library v13 and v14

How to use it

Drop the skills into your local Claude skills folder:

git clone https://github.com/maikotrindade/awesome-react-native-skills.git ~/.claude/skills/awesome-react-native-skills

From there, Claude’s progressive disclosure does the rest. The frontmatter of each skill stays in the system prompt, and the body loads only when your question matches. You don’t have to remember which skill to invoke.

Where it goes next

The repo is a starting point, not a finished thing. More skills are coming, and contributions are welcome if you’ve worked out a pattern that should be there. Check the project on GitHub and open an issue or PR if something’s missing or out of date.


Jetpack Compose and React Native: More Similar Than You Think

Android developers already fluent in Jetpack Compose will find React Native surprisingly familiar. Both share a declarative, component-driven model built around state — and if you’ve internalized the Compose mental model, the leap to React Native is much smaller than it looks from the outside.

Declarative UI: Composables vs. Components

In Jetpack Compose, you build UI by writing @Composable functions that describe what the screen should look like for a given state. React Native uses function components that do exactly the same thing. The rendering philosophy — describe, don’t impeach — is identical.

Jetpack Compose

@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name!")
}

React Native

function Greeting({ name }) {
  return <Text>Hello, {name}!</Text>;
}

Both frameworks re-run the function when inputs change and diff the result to update the UI. Compose calls this recomposition; React Native calls it re-rendering.

State Management

This is where the parallel is most striking. Compose’s remember { mutableStateOf(...) } maps almost one-to-one to React Native’s useState(). Both keep local state tied to the lifetime of the component and trigger a UI update on every change.

Jetpack Compose

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Tapped $count times")
    }
}

React Native

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <TouchableOpacity onPress={() => setCount(count + 1)}>
      <Text>Tapped {count} times</Text>
    </TouchableOpacity>
  );
}

The concept of state hoisting — lifting state up to the nearest common ancestor and passing it down as props — is equally central to both. Compose documentation uses the term explicitly; the React ecosystem calls it “lifting state up” and the outcome is the same pattern.

Props and Parameters

Composable function parameters are props. Both systems use the same mechanism: data flows down from parent to child, and only the parent owns the state.

Jetpack Compose

@Composable
fun UserCard(username: String, avatarUrl: String, onClick: () -> Unit) {
    // ...
}

React Native

function UserCard({ username, avatarUrl, onClick }) {
  // ...
}

Kotlin’s named arguments and default values map to React Native’s destructuring with default prop values. The ergonomics differ but the concept is the same.

Side Effects and Lifecycle

Traditional Android had a full Activity/Fragment lifecycle — onCreate, onResume, onPause, onDestroy. Compose collapsed this into LaunchedEffect and DisposableEffect. React Native takes the same simplified view via useEffect.

Jetpack Compose

// Runs on enter, cancels coroutine on leave
LaunchedEffect(userId) {
    viewModel.loadUser(userId)
}

// Runs on enter, cleanup block runs on leave
DisposableEffect(Unit) {
    val listener = registerEventListener()
    onDispose { listener.unregister() }
}

React Native

// Runs on mount and when userId changes
useEffect(() => {
  loadUser(userId);
}, [userId]);

// Cleanup runs on unmount
useEffect(() => {
  const subscription = subscribeToEvents();
  return () => subscription.remove();
}, []);

The returned cleanup function in useEffect corresponds directly to onDispose in DisposableEffect. Even the dependency array in useEffect has a Compose analogue — the key you pass to LaunchedEffect.

If you’ve internalized Android’s back stack, React Navigation will feel natural. Pushing a screen is conceptually the same as starting an Activity with an Intent, just expressed in JavaScript.

Android (Intent with extras)

val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("itemId", item.id)
startActivity(intent)

React Native (React Navigation)

navigation.navigate('Detail', { itemId: item.id });

Both maintain a stack, both support passing parameters to the destination, and both expose a back-navigation mechanism. The underlying implementation differs (system Intents vs. a JS stack), but the mental model transfers directly.

Key Differences to Keep in Mind

The similarities above are real, but a few structural differences matter:

  • Language: Kotlin is statically typed with null safety built in. React Native typically uses JavaScript or TypeScript — TypeScript closes most of the gap.
  • Rendering: Compose draws UI onto a canvas managed by the Android runtime. React Native (since the New Architecture, default in v0.76) uses JSI to bridge JavaScript to actual platform widgets — UIView on iOS, Android Views on Android. The output looks native because it is native.
  • Tooling: Gradle, Android Studio, and adb are replaced by npm/yarn, Metro bundler, and the React Native CLI or Expo. The ecosystem is different even if the patterns are familiar.

The Takeaway

The shift from Jetpack Compose to React Native is not a paradigm shift — it is a syntax shift with a different language underneath. Composables, state, props, effects, and the navigation stack all have direct counterparts. If you already think declaratively about UI, you’re most of the way there.