# The Outer Loop

Course: [Loop Engineering](https://www.devtutorials.ai/learn/loop-engineering) · Section: Loops at Scale
Source: https://www.devtutorials.ai/learn/loop-engineering/loops-at-scale/outer-loop

> Triage inbox, escalation and human review. Unclear and unsafe tickets must land in the inbox, not get "fixed". Then you read the evidence and give the verdict.

## Triage Inbox, Escalation and Human Review

Every lesson so far made the loop do more by itself. This lesson is about what it must **not** do by itself. Some tickets need a person. And every result needs a person's verdict.

## Watch a Loop Touch What It Should Not First

Ticket #12 says "Make the board feel faster". Nobody said what "faster" means. Ticket #13 asks to delete old data from production. The loop has a good checker and stop rules, but no inbox.

Press **Run it in the Loop Lab**:

1. On #12, the maker guesses. It removes every animation and says "done". The tests still pass, so the checker passes it too.
2. On #13, the maker runs the delete on production.

**In simple words:** the loop could not tell "hard" from "not mine to decide". So it decided anyway.

## TL;DR

- Some tickets must not be changed by a loop: **unclear** ones and **unsafe** ones.
- The loop **escalates** them: it sends them to a **triage inbox**, with the evidence, and changes nothing.
- The maker needs two things to escalate: somewhere to send the ticket (`escalate:`) and the know-how to spot it (the triage **skill**).
- In the inbox, **you** read the evidence and give a **verdict**: accept, reject or send back.
- Your review is the **outer loop**. It is what keeps comprehension debt and cognitive surrender away.

## A Real-World Example

Think about a self-checkout at a supermarket.

Most of the time, the machine does the whole job. You scan, it weighs, it takes the money. But some items it will **not** decide on by itself. A bottle of wine, or an item whose weight looks wrong. For those, a light turns on and a staff member comes over.

The staff member does not just press "OK". They look: is this person old enough? Is that really the item on the screen? Then they decide.

Now map this to a loop:

1. The scanning and paying is the **inner loop**: the machine, on its own.
2. The light turning on is **escalation**.
3. The waiting list of lit-up machines is the **triage inbox**.
4. The staff member's look and decision is **your verdict**: the **outer loop**.

## What You Will Learn

In this lesson, you will learn:

1. Which tickets a loop must escalate.
2. What the maker needs in order to escalate.
3. What evidence an inbox item carries.
4. How to give a verdict: accept, reject or send back.
5. How the outer loop stops comprehension debt.

## Which Tickets Must Be Escalated

A loop should escalate a ticket when it cannot safely decide on its own. Three common cases:

| Kind | Example | Why the loop must not decide |
|---|---|---|
| **Unclear** | "Make the board feel faster" | There is no check for "done". Any change is a guess. |
| **Unsafe** | "Delete old tickets from production" | A mistake cannot be undone. |
| **Stuck** | A flaky test, same failure 3 times | Trying again will not help. A person must look. |

The next box sorts a few tickets with the same rules a triage skill uses.

```js runnable title="Fix it, or send it to the inbox?"
function triage(ticket) {
  if (/production|delete|drop|secret/i.test(ticket)) return "inbox: unsafe";
  if (!/\d|shows|ignores|test|error|fails/i.test(ticket)) return "inbox: unclear";
  return "fix";
}
const tickets = [
  "Ticket count shows 1250 instead of 1,250",
  "Make the board feel faster",
  "Delete old tickets from the production database",
  "Search ignores capital letters",
];
for (const t of tickets) console.log(triage(t).padEnd(15) + t);
```

Expected output:

```text
fix            Ticket count shows 1250 instead of 1,250
inbox: unclear Make the board feel faster
inbox: unsafe  Delete old tickets from the production database
fix            Search ignores capital letters
```

A real triage skill uses better rules than two regular expressions. But the idea is the same: decide **before** touching any code.

## What the Maker Needs to Escalate

Two things, and the loop needs both:

1. **Somewhere to send it.** In the loop file: `escalate: triage-inbox`.
2. **The know-how to spot it.** The triage skill from Lesson 6 tells the maker which tickets are unclear or unsafe.

With only the inbox, the maker never knows when to use it. With only the skill, it knows, but has nowhere to send the ticket.

```js runnable title="Escalation needs a target and the know-how"
const setups = [
  { name: "neither", target: false, skill: false },
  { name: "target only", target: true, skill: false },
  { name: "skill only", target: false, skill: true },
  { name: "both", target: true, skill: true },
];
for (const s of setups) {
  const result = s.target && s.skill ? "sent to the inbox, nothing changed" : "changed by the maker";
  console.log(s.name.padEnd(12) + "#13 -> " + result);
}
```

Expected output:

```text
neither     #13 -> changed by the maker
target only #13 -> changed by the maker
skill only  #13 -> changed by the maker
both        #13 -> sent to the inbox, nothing changed
```

In Lesson 9 you also blocked #13 with `allowed_commands`. That is a second wall. A good loop has both: the skill sends risky tickets to the inbox, and the blast radius blocks anything that slips past it.

## Lab Exercise 1: Send Unclear and Unsafe Tickets to the Inbox

Open the loop file and:

1. Add `skill: triage` under `maker:`, with two spaces in front.
2. Add `escalate: triage-inbox` at the top level.

Press **Check my loop**. On all 3 versions, #12 and #13 must land in the inbox with their evidence, neither may be changed, and the other tickets must still be fixed.

## Evidence: What an Inbox Item Carries

An inbox item is only useful if it carries **evidence**: the facts you need to decide. A good inbox item has:

- the ticket and its text;
- the **diff**: what the loop changed, if anything;
- the **test output**;
- the **checker's notes**;
- for a blocked action, the command that was blocked.

"The loop says it is fine" is not evidence. The diff and the test output are.

```js runnable title="An inbox item with evidence, and one without"
const items = [
  { ticket: "#12", says: "The loop says it is fine." },
  { ticket: "#13", says: "blocked command: pnpm tsx scripts/cleanup.ts --prod", evidence: ["no backup step in the ticket"] },
];
for (const it of items) {
  const usable = Array.isArray(it.evidence) && it.evidence.length > 0;
  console.log(it.ticket + ": " + (usable ? "can be decided: " + it.evidence.join("; ") : "cannot be decided yet: no evidence"));
}
```

Expected output:

```text
#12: cannot be decided yet: no evidence
#13: can be decided: no backup step in the ticket
```

An opinion is not evidence. The item for #12 needs a diff, a test result or a checker note before you can decide.

When you tick evidence in the lab, pick the line that **decides** the verdict. For a fix you accept, that is usually the test result plus the diff. For a fix you reject, it is the line that shows what is wrong.

## Your Verdict: Accept, Reject or Send Back

For each item, you choose one of three verdicts:

| Verdict | Use it when | What happens next |
|---|---|---|
| **Accept** | The evidence shows the change is right | The fix is merged |
| **Reject** | The change is wrong, or must never be made | The change is dropped |
| **Send back** | The ticket needs more information or a person's fix | It goes back to its owner with a note |

### Green Tests Are Not the Whole Story

Here is the trap. A fix can pass every test and still be wrong. The classic case: the warnings are **hidden**, not fixed.

```js runnable title="Tests pass, but the diff hides the problem"
const diff = [
  "+ // @ts-ignore",
  "+ const data: any = await res.json();",
];
const testsPass = true;
const hidesProblems = diff.some((line) => /@ts-ignore|pragma warning disable|eslint-disable/.test(line));
console.log("tests pass:     " + testsPass);
console.log("hides problems: " + hidesProblems);
console.log("verdict:        " + (testsPass && !hidesProblems ? "accept" : "reject"));
```

Expected output:

```text
tests pass:     true
hides problems: true
verdict:        reject
```

This is why you read the diff. The tests said yes. The diff said no.

Accepting everything because the loop was right last week is **cognitive surrender**. It feels safe, and it is how hidden problems reach your users. Every verdict needs a reason you can point to.

## Lab Exercise 2: You Give the Verdict

Five items wait in your inbox. Each one has three evidence lines.

For each item:

1. Tick the evidence line (or lines) that decides it.
2. Press **accept**, **reject** or **send back**.

Then press **Check my verdicts**. You need at least **4 of 5** right, and **every** verdict must cite evidence.

## The Outer Loop and Comprehension Debt

Your review is the **outer loop**. The inner loop runs fast and often. The outer loop runs at the speed of a person reading.

That is not a weakness. It is the point. Each item you review is code you now understand. Each item you accept without reading adds to **comprehension debt**: code in your project that nobody has read.

```js runnable title="Comprehension debt grows with every unread change"
const weeks = [
  { merged: 20, readByAPerson: 20 },
  { merged: 30, readByAPerson: 18 },
  { merged: 40, readByAPerson: 15 },
];
let debt = 0;
weeks.forEach((w, i) => {
  debt += w.merged - w.readByAPerson;
  console.log("week " + (i + 1) + ": " + debt + " changes nobody has read");
});
```

Expected output:

```text
week 1: 0 changes nobody has read
week 2: 12 changes nobody has read
week 3: 37 changes nobody has read
```

If this number grows, slow the loop down, make it pick smaller tickets, or add reviewers. Do not stop reading.

### Feedback Beyond the Code

Some writers on loop engineering describe two more outer loops: feedback from **developers** (does this change make sense to the team?) and feedback from **users** (did the change help anyone?). The inner loop cannot see either one. Both come from people, and both feed the next tickets into the backlog.

## When a Bad Change Gets Through

Review catches most problems, not all. When a loop's change reaches `main` and breaks something:

1. **Pause** the loop, so it does not build on the broken change.
2. **Revert** the pull request. One ticket per pull request makes this one click.
3. **Re-open** the ticket, with a note about what broke.
4. **Add a check** that would have caught it: a test, a rubric line or a linter rule.
5. **Restart** on 2 or 3 small tickets, and read the diffs.

Step 4 is what makes the loop better over time. Every escape should leave behind a new check.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No inbox | "The loop can handle every ticket." | Unclear and unsafe tickets go to a person. | The loop guesses, or damages production. |
| An inbox with no skill | "It has somewhere to send them." | The maker also needs to know when. | Nothing is ever escalated. |
| Trusting green tests | "Tests pass, so accept." | Read the diff. Hidden warnings pass tests. | Problems are hidden, not fixed. |
| Blind accept | "It is always right." | Every verdict cites evidence. | Comprehension debt and cognitive surrender. |

## Exercises

Each exercise ends with a value on the last line. Press **Check** to compare it with the expected answer.

## What to Remember

- Unclear, unsafe and stuck tickets go to the triage inbox. The loop changes nothing.
- Escalation needs a target (`escalate:`) and the know-how (the triage skill).
- Every inbox item carries evidence: diff, test output, checker notes.
- You give the verdict: accept, reject or send back, with a reason.
- Green tests are not the whole story. Read the diff.
- The loop produces evidence. You produce the verdict.

## Further Reading

- [Loop Engineering Emerges, on ADTmag](https://adtmag.com/articles/2026/07/01/loop-engineering-emerges-as-developers-put-ai-coding-agents-on-repeat.aspx): Andrew Ng's three loops, and people moving out of the QA role.
- [Loop Engineering, by Addy Osmani](https://addyosmani.com/blog/loop-engineering): comprehension debt and cognitive surrender.
- [Loop Engineering and Autonomous Agent Systems, by Rick Hightower](https://rickhigh.substack.com/p/loop-engineering-and-autonomous-agent): where people stay in the loop.

Next: **Capstone**, where you build a full loop pack for your own project and export it.
