Twelve lessons in harness engineering from a Google principal engineer
- Ryan Lopopolo, now a Principal Engineer at Google Cloud, was previously at OpenAI. He distilled a year of work into twelve lessons in a public repo you can hand directly to your agent as reference. Most of the evidence comes from his time at OpenAI.
- The core move is counterintuitive: treat the chosen model and coding agent as an immutable black box. Don't tweak a single parameter. Only change the two things around it: context and tools.
- The lessons split into three layers. Three set the boundaries: lock the worker, deploy the private process-data iceberg, and measure by evidence. Six are levers: give one agent the whole job, route context just in time, make capabilities legible, let the repo teach the agent, keep autonomy inside authority, and prove claims in the real environment. The last three build compounding value: turn feedback into infrastructure, preserve coherence, and run settled work as a loop.
- The evidence: an internal product grew from an empty repo to roughly a million lines of code in five months, with about 1,500 merged PRs — all written by Codex, none by a person.
- What you can take away: a six-step improvement process, three fill-in-the-blank templates, an eleven-row table mapping claims to evidence boundaries, and a nine-item checklist for when your A/B test doesn't count.
Twelve Lessons, and One Word Often Mistranslated
Let's get the term right first. "Harness" here means the gear of a draft horse — the saddle, reins, horseshoes, and traces. The repo's description even starts with a horse emoji. It's easy to misread it as "to restrain," but the direction is the opposite.
The horse can already run fast. What's missing is the saddle, reins, and a track that direct its strength where it counts. That's what harness engineering builds.
Ryan Lopopolo, now a Principal Engineer at Google Cloud working on agentic GCP, wrote this. Before that he was at OpenAI, where he helped build the Seattle office and OpenAI Frontier for enterprise — working on exactly what this guide covers: context, tools, permissions, evals, observability, orchestration, and feedback loops for agents. Most of the evidence in these twelve lessons comes from that stint. Earlier, he led the data marketplace as tech lead at Snowflake, led a 40-person team supporting 350 engineers at Brex, and was tech lead for infrastructure and payments at Stripe as it grew from 600 to 6,000 people. In his spare time he writes Artichoke, a Ruby implementation in Rust.
When he published the twelve lessons, he said they represent a year of his work — practice and technique together.
He distilled that practice into twelve lessons and put them, with evidence, case studies, and executable processes, into a public repo. The repository's own content is licensed under CC BY 4.0. You can use it two ways: read it directly, or hand it to a coding agent along with the system you want to improve, letting the agent use the routing index to find which lesson applies.
Why it's worth your time: Every one of the twelve lessons comes with verifiable evidence. One internal product grew from an empty repo to roughly a million lines of code in five months with about 1,500 merged PRs, all written by Codex with zero handwritten code. A massive 60-hour, 300-million-token refactor needed only an initial prompt plus two follow-ups. After enabling a lint rule, about 600 violations were fixed and tests completed in a single PR.
Here's the full map. The lessons are organized into three layers: top three set the boundaries, middle six are the levers, bottom three determine whether the system compounds over time. We'll go through them one by one.
Lesson 1: Lock the “Worker” Down
Within a single evaluation or deployment cycle, keep three things fixed: the model, the agent's runtime, and the native tool interfaces. If any of these changes — even a generational upgrade — the whole environment needs re-acceptance from scratch.
Locking the black box makes failures attributable. When a capable agent fails, the cause is almost always one of five: missing institutional context, a capability that isn't actually usable, a missing feedback loop, insufficient authority, or no evidence at the exit. All five are properties of the environment; all five can be inspected and changed. Without a locked box, you stay stuck asking "is it the model, or my setup?"
Your own accumulated experience also needs re-acceptance. The habits the old model taught you — how small to slice tasks, how much orchestration to use, how long the inner loop can wait, where to keep a human in the loop — if carried over unchanged, you give away the new model's gains. Conversely, assuming every point release is better in every way is the opposite error.
One observation: GPT-5.3 is more reluctant than GPT-5.2 to tackle hard tasks. A single data point, not a trend.
Two numbers show how much a generational shift matters. When GPT-5.2 shipped, he was on vacation. He came back to find the team producing one to two extra PRs per engineer per day with zero additional environment investment. Later, a separate Symphony environment overhaul reported a 10x increase in PRs per engineer per week. The first came from a model upgrade, the second from environment changes. You have to track these separately — that's what locking the box enables.
During the GPT-5.2 release, the environment didn't change at all.
Result: +1 to +2 PRs per person per day.
During the Symphony intervention, the worker didn't change.
Reported: 10x increase in PRs per person per week.
Inner-loop latency is part of acceptance too. How well an agent reasons depends on how much feedback it gets before its trajectory runs off the rails. In the GPT-5.2 era, the Codex environment had no background shell, so the agent dutifully waited for blocking build scripts. When GPT-5.3 added a background shell, it stopped waiting. The team spent a week swapping custom build scripts for professional tools (trying Bazel, Turbo, and Nx along the way), stopping only when the full build fit in under a minute. That minute is now a hard cap — the moment it breaks, they stop feature work and fix the build graph.
Acceptance also includes subtraction. To let the agent "see" the interface, the team built a custom virtual display plus screen recording. Once the model could see the screen and click the mouse itself (computer use), they deleted the whole stack — without losing end-to-end verification.
Lesson 2: Deploy the Underwater Part of Your Private Process-Data
What's inside a general model's weights is just the tip of your organization's knowledge iceberg. What you actually need to get work done is below the waterline — private and constantly changing.
You need two things clear. One is the data ontology: what entities, metrics, and identifiers exist and how they relate. The other is the work ontology: what results, workflows, roles, tools, exceptions, evidence, and approval relationships exist.
An example helps. An internal data agent with only database access can't resolve the company's internal conflict over what "revenue" and "active user" mean. It also needs product lines, customer segments, teams, pilot customers, current plans, and how shared metric definitions are written. With those, it can give an answer that belongs to *this* company.
Leave current records in the data warehouse, ticketing system, docs, logs, codebase, and customer systems. Fetch an authorized slice via search and connectors. Don't build a second, perishable copy.
There's a pattern you can copy directly. The company Basis has two repositories: Arnold is the production monorepo, and Atlas is a second repository specifically for company context, separate from the production tree. Codex can combine personal notes, operational knowledge, and production code, while each source keeps its own update path. The implementation can be simple — his exact words: "just Codex plus a Git repo full of Markdown."
One more thing belongs to this layer: settled decisions should be promoted to the layer that can enforce them. A team and its security group picked an approved encryption implementation, and the decision stayed in Slack. Later, a new engineer was working with Codex, and Codex installed a different npm crypto package. The team pulled the decision back, wrote it as a guardrail rule in the repo, and re-ran that change.
Lesson 3: Give One Agent the Whole Job
Delegate the outcome. Give one main trajectory full responsibility: breakdown, execution, integration, evidence, and safe landing. Leave the steps to the agent. A person steps in at only three points: direction, judgement, and consequential authorization.
He often gives near-zero-constraint prompts, calling them "lazy prompts" — lazy about method, leaving it to the agent to decide how. Sparse delegation is also a test: it tests whether your environment makes the real requirements findable by the agent itself. When the agent misreads you, what gets exposed is exactly which part of your environment isn't built yet. The accompanying loop is "propose, observe, improve, rerun": review the trajectory, fix the environment, discard that result, run it again.
One request asked for a full red-team analysis, with every finding required to demonstrate impact or exploitability. Codex produced a report, a reproducer, a patch, and regression tests, then handled detailed manual code review and modified the implementation. The author reviewed and approved the implementation, then separately authorized it to prepare a minor release, merge the PR, and submit an advisory to RustSec — the Rust ecosystem's security advisory database.
The prompt only specified the outcome and the acceptance bar. Substantive review and approval at the delivery point stayed in the process.
Two counterexamples, more useful than the positive one
- A skill required: don't open a PR until the local build passes.
- The task specifically required opening a PR first, so a colleague could help fix the broken build.
- The skill refused to execute. The ordinary safety valve became an obstacle to the very outcome it was meant to serve.
- Instruction: create a branch, commit, push, merge.
- The agent, to satisfy "merge," used admin privileges to bypass the protected process.
- But the intent of the job included waiting for tests to pass.
A single request usually describes just the functional outcome. Compatibility, maintainability, accessibility, rollback, syncing artifacts, and future maintenance needs are all implicit. These implicit requirements have a name: "the question behind the question." The agent needs to find them itself — by searching the repo, looking at current behavior, checking history, and understanding how this result relates to the user. Only when it can't dig them up, and the missing answer would change the product intent, create unanticipated risk, or require permissions it doesn't have, should it ask a person.
A practical note on context compression
Long trajectories repeatedly compress their own history. His working assumption: user-message intent survives compression well, while earlier tool outputs are more likely to be summarized or dropped. This is worker-specific behavior; re-validate it when you change models.
Based on that assumption, putting detailed fine-grained instructions in user messages means they'll keep competing for attention long after that part of the job is done. So keep only three things in the user message: the outcome, the acceptance bar, and the authority boundary. Details should come from repo reads, tool outputs, test failures — surfacing at the moment of decision.
Lesson 4: A Thousand-Page Manual, Replaced by an Index
The knowledge base should be large and searchable; the working set should be small. The root guide should be a map.
This lesson has an origin story. Their original monolithic agent manual got too long to be usable. They replaced it with a short root index, backed by indexed design docs, product specs, implementation plans, and references — making progressive disclosure a feature the repo maintains itself, and using link checks and freshness checks to keep those routes trustworthy.
A bigger context window solves only capacity pressure. Attention, retrieval quality, conflicting instructions, and "which fact matters right now" remain scarce. Deterministically stuffing the context will inevitably fail across multiple compressions. The agent must have the ability to find its way back; counting on the first injection still being there is unreliable.
Send a minimal map. What kind of task this is, the relevant architecture and prior decisions, the key user journeys, and the few commands that establish current state.
The goal is for the agent to know what it's changing and where to look, while keeping the path to deeper sources open. This section shouldn't balloon into a checklist.
Let code, tool output, test failures, browser state, and logs reveal the next piece of context. Error messages should say which invariant was violated and roughly how to fix it.
This phase is exploratory. A rigid step list is useless because no one can predict every fork.
Once the shape of the final artifact stabilizes, bring back the role-based reviewers, acceptance criteria, evidence requirements, and delivery policy.
The outcome and key journeys were visible from the foundation stage. What returns now is their detailed review perspective. Finally, compress the whole trajectory into a decision package a human can read directly.
- In the description, only write "what this is and when it might apply," load the body on demand.
- Writing it as a long imperative checklist overfits to a single path and makes the agent refuse reasonable exceptions.
- You can keep a large skill library outside the agent's scan path, using a symlink area to expose only a handful of task-relevant skills per trajectory.
- Record intent, prerequisites, steps, safety boundaries, evidence, escalation path, rollback.
- Automation scripts can then be very thin; durable knowledge stays in the repo.
How to validate a knowledge base
Start a brand-new session and see if the agent actually retrieves it and actually changes its behavior. A document existing only proves it was written. One step further is an ablation test: remove this piece of context and run it again. If the result holds without it, delete it.
This repository is itself a sample of this lesson
Four structural facts you can verify for yourself:
| What the repo actually does | Which of its own claims it maps to |
|---|---|
The root AGENTS.md is a routing table. It routes by "the decision that local evidence couldn't resolve." If you need to tell whether the agent lacks capability or the tools are hard to use, go to the capability section. To define who can execute consequential changes, go to the authority section. | Route, don't inject |
| It explicitly says: don't pre-load application guides or the whole set of lessons for other goal-related work. | Keep the working set small |
| When used as context for another system, it declares itself read-only; the target system's instructions, ownership, contracts, and current state provide the local truth. | Local truth beats foreign context |
| The given workflow starts by reading the target system's own guides and architecture; only in step three does it say the unresolved decision, and then read just one relevant section. | Look local first, then grab one |
The unresolved decision determines the route. The target system's local evidence might resolve it without any of these sections. If context is genuinely needed, start with one section. Add a second only if a different independent decision is still pending. AGENTS.md, repository root
Lesson 5: Every Word a Tool Spits Out Is Context for the Next Step
For a capability to be truly usable, it has to complete a six-step loop: be discoverable, be recognizable for this task, be callable correctly, have return values you can interpret, be fixable when it errors, and finally be confirmable in the real system. If any link breaks, the capability is ornamental.
If you find an error at the "Read output" stage, loop back to fixing it, then call again.
The choice between MCP and plain command-line tools comes down to two trade-offs. MCP servers instantly broadcast tool names and input schemas, but the model still has to learn each new call and return shape on the fly. Ordinary POSIX commands benefit from the model's strong prior, but they're invisible until the agent knows they're installed. There's a real migration story: they initially used MCP to let Codex connect to their Electron app via the Chrome DevTools protocol. A colleague later swapped it for a local TypeScript daemon plus a small CLI, because they really only needed two or three operations. The workflow didn't break after the switch.
Seven behaviors of a good tool
- Stay quiet on success.
- Give bounded, stable structure where results are read mechanically.
- Leave a path to retrieve omitted details.
- On failure, say which invariant was violated and which goal it affected.
- If a known recovery action exists, give it directly.
- For consequential operations, offer a check mode or dry run.
- Provide a way to query postconditions, or a side-effect receipt.
A trap everyone steps in
There's a failure mode that's easy to recognize: the agent will preemptively pipe an expensive command, like cargo test, into head or tail to read less output.
head might close the pipe early, killing the test process before it finishes. tail usually reads to the end but discards all earlier evidence. Both, when not handling the pipeline's exit status, also hide the real exit code.
When a later failure depends on earlier context to be explainable, that output is already gone.
The fix isn't in the prompt; it's at the tool boundary: a wrapper owned by the repository itself, which runs the whole task, preserves the true exit status and full output, returns only the semantic failure summary the next decision needs — while keeping the low-level evidence queryable, without re-running.
Dig down until it works
Handing over the whole job exposes capability gaps layer by layer, from the outside in. The initial goal was to let Codex read Slack, identify incoming production incidents, so people wouldn't have to paste messages into the conversation one by one.
This capability search recurses six to eight levels deep. Once you hit a usable primitive at the bottom, you climb back up to the original triage goal. Every time you dig this way, the set of tasks all future agents can do expands a little more. Conversely, using a one-off manual workaround leaves the exact same hole for the next time.
Lesson 6: Code Is Prompt
Every file an agent reads influences what it predicts next. His quote: the code in the repo is the prompt.
Models have dozens of plausible approaches in mind. The repo's job is to prune — using recurring structure, to single out the one approach the team has chosen. Consistency directly reduces context requirements: with one observability setup, what an agent learns in one file transfers across the repo; with six setups, it has to re-learn at every call site which one to use here.
In 2023, he stuffed the entire lib.rs of Ruby's String#unpack into a prompt, and the model kept confusing unrelated instructions. After he broke that code into highly regular, well-documented small modules, the model correctly implemented the subsequent state-machine operations on the first try.
Re-reading that post later, his assessment was: models are stronger, but the collaboration loop of context, constraints, iteration, and feedback feels the same.
The reverse is also true: examples propagate defects just as fast as good judgement. A half-finished migration produces two conflicting sets of prompt material. A poorly written helper function spreads to every package needing similar behavior.
Old style and new style lie side by side in the repo.
Before every change, the agent must decide: which one is canonical now? Is the migration done?
Both are being used as prompt material, and both get copied.
Turn on ESLint's no-await-in-loop, fix ~600 violations, and complete test coverage in the same PR.
Only one copyable answer remains in the repo; the old style can't get back in.
Agent and new hire get the same clear direction for the next edit.
One concept, one home
Before creating a parser, enum type, version constant, state representation, fixture constructor, policy check, or command — find the existing home for that concept, and extend that interface if needed. These are signals that something has no clear home:
| Warning sign | What it means |
|---|---|
| A second parser | The repo already models this language, and another one appeared |
| Version number copied | The exact tool version shows up in both policy code and test fixtures |
| String concat to make JSON | Structured data is assembled from string fragments |
| CLI rewriting domain logic | A command re-implements domain behavior |
| Untyped external data | External data flows into internal code without parsing |
| The same invariant defended in several places | Multiple defensive checks protecting the same thing means no single boundary actually owns it |
Every duplicated home creates a "locally plausible" direction for the next edit. An agent can faithfully follow every precedent around it while making the entire repo less coherent.
When external data comes in, parse it once into a trusted domain type, and let internal code rely on that canonical state. If you're doing record checks everywhere, normalizing in every function, re-checking readiness in every event handler — it usually means no single boundary actually owns "this value is valid."
Lesson 7: Can-Do and Can-Approve Are Two Contracts
Capability says how to cause an effect. Authority says which identity, on which resource, in which environment, may cause which effect, for how long, with whose approval, and how to roll back.
- Answers "how do I cause this effect."
- View, edit, build, test, simulate, iterate.
- Most errors in this circle are cheap feedback.
- What identity, and what operation and target resource.
- Environment and scope, duration of the grant, and revocation path.
- Approval needed? Separation of duties?
- Audit receipt and postconditions, rollback and recovery path.
Splitting these two contracts shifts the security question from "require a random system to be error-free" to "how big is the consequence, and can it be recovered." Credential handling should live outside the trajectory: a proxy process, at the action boundary, pulls narrowly-scoped keys from secure storage, making gh feel like an ordinary command — while the keys never enter the model's context, and never touch the repo. Two more layers of protection: a read-only identity makes checks useful but unchangeable, and endpoint-bound access lets you revoke one agent-facing channel without rotating the underlying credentials — even revoking the authority of a running trajectory.
Phase consequential operations, grant only what each phase needs
Loki is his robot vacuum at home, running open-source Valeto firmware. The only remote access path is a custom Tailscale daemon. Upgrading it from v1.90.8 to v1.96.4 was tricky: upgrading it might destroy the very path you'd use to rescue a failed deployment.
Each phase is granted only the permissions that phase's effects require. Passing the canary doesn't grant production access; having production access doesn't mean the deployed system is healthy.
Instructions should be read through this authority contract. "Merge" means completing the repository's protected merge process; it doesn't imply permission to bypass it. "Prepare a deployment" grants scope up to preparation; the production switch requires separate approval. When an irreversible goal or scope remains ambiguous after checking local policy, the agent should ask before crossing that boundary.
Settled authorization rules should land in identity policies, typed domain operations, scoped tools, environment boundaries, test and merge gates. Where the consequence warrants it, making the wrong action impossible is better than writing a rule that depends on someone remembering it. Mechanisms don't need remembering.
Lesson 8: A Green Check Proves Only Its Own Line
Verification must happen at the boundary where the user, operator, or downstream system actually depends on the result. Unit tests, type checks, linters, and builds establish internal properties. They don't prove the external thing.
The most useful artifact for this lesson is a mapping table: what you claim, and where the evidence must be taken.
| What you claim | Where evidence must come from |
|---|---|
| Behavior in a browser | A real browser traversing the journey, observing both semantic state and rendered output |
| Generated content | Complete source-to-artifact comparison, plus freshness |
| Compatibility migration | Conformance on target corpus, edge cases, and explicitly listed acceptable differences |
| Security impact | A reproducer, bounded exploitability, and regression coverage |
| Deployment complete | The verified artifact is running, plus post-deploy health checks |
| Consequential remote change | Phased canary, switch, recovery path, post-switch checks |
| Calculation in a spreadsheet | Formula, units, source cells, and the rendered workbook state |
| Dataset transformation | Structure, source provenance, source-to-artifact reconciliation |
| Document or slide deck | Required structure and rendered page; verify cited references or accessibility if promised |
| Analytical conclusion | A reproducible transformation process, and that the conclusion is supported by evidence |
| Business state change | Required approvals, receipts, and observed postconditions |
This table holds for non-engineering work, too. When the intermediate code stays hidden inside the agent's execution and the user never sees it, the proof package must speak the user's language: a spreadsheet formula, a rendered slide, a sourced conclusion, a completed state transition. A hidden script's test passing only establishes internal properties.
Also, hand the real system to the agent itself. It should be able to start the app, drive the browser, read logs, query metrics and traces, check persistent side effects, compare generated corpora, and watch CI/CD. A person shouldn't relay evidence the agent could see directly — relaying adds latency and strips the state needed for diagnosis. Use both semantic and rendered views: DOM snapshots and structured state are good for roles, values, and relationships. Screenshots, video, or direct screen interaction are needed to expose layout, clipping, focus, and interaction defects.
Release evidence must follow the same immutable artifact from verification all the way to cutover. Rebuilding in a higher-authority task is opening a second supply chain, breaking the chain of evidence.
Be explicit about what the evidence doesn't prove
In the security advisory case above, the report clearly stated: triggering the data corruption required a custom hash implementation plus recovering after a crash, and the reproducer did not prove memory unsafety on the default path. The instruction was that every reported issue had to prove impact or exploitability; speculative output that merely looked like a security finding didn't count.
The burden of proof scales with the consequence. A typo fix, a parser migration, a remote upgrade, a production release — these need wildly different levels of evidence and authority.
Lesson 9: What You’ve Corrected Once, You Shouldn’t Correct Twice
Recurring corrections should be turned into the environment that shapes the next run. This is what makes everything else compound over time.
Let's start with a precise counterexample. Someone gave an agent a specific Go interface fix: instead of returning just "did it succeed," return a concrete error value that explains *why* it failed. The agent changed only the named function. But the instruction implicitly carried a general principle about interface design. The fix didn't search for other instances governed by that same principle.
When you hit a recurring correction, treat it as a systemic problem and go through five steps.
| Step | What to do |
|---|---|
| 1 | Reconstruct the promised outcome, and the actually observed failure |
| 2 | Locate the earliest boundary that could have caught it, or exposed it |
| 3 | Search for other instances governed by the same principle — this step is most often skipped |
| 4 | Separate the four causes: environment defect, agent randomness, external failure, wrong premise |
| 5 | Choose an intervention that covers the whole class without forbidding legitimate exceptions |
Which layer to fix depends on how mature the lesson is and how big the consequence.
| Intervention | When to use it |
|---|---|
| Change prompt, re-run | Task framing or desired outcome is still being figured out |
| Routing doc, runbook, skill | A stable piece of knowledge needs to surface at a decision point |
| Reviewer or eval | The judgement is qualitative, cross-cutting, or still gaining detail |
| Types, interfaces, domain tools | The system can make correct usage easy and incorrect usage hard |
| Lint, test, policy check | A deterministic invariant should directly block recurrence |
| Architecture or migration | Recurring defects expose a wrong ownership or dependency direction |
A full chain of how judgment becomes an asset
The starting point was a PR to the public openai/codex repository: upgrade pnpm to fix several vulnerabilities and disable post-install scripts. Working backwards from that patch, four principles can be recovered: package managers are high-risk boundaries, dependencies should stay updated, install processes shouldn't execute arbitrary code in CI, and exceptions need human review.
The second promotion is the notable one. After the scanner went live, Slack feedback showed it was better at catching individual instances than at capturing the general reference framework in the security engineer's head. The team added the missing guardrails and investigation signals to SECURITY.md, which then fed both the daily scanner and the new dependency-depth and license scanners. That's how judgment gets accumulated, round after round.
- Stacking another validator on top of a missing domain model only crystallizes the inconsistency and adds maintenance cost.
- When stronger types, interfaces, or architecture arrive, retire those redundant downstream checks.
- Start with a coarse instruction. Add detail only when it starts failing on legitimate exceptions.
- Writing a fully detailed rule from the start crowds out the judgement the agent could have gotten right.
Two supporting details: how to use readme, and how to converge a reviewer
Have the agent keep three files as it works: Mistakes, Learnings, Desires — abbreviated MLD. These files are telemetry for the person building the environment. The agent may miss the most important error, misattribute the cause, or ask for the wrong tool. So keep the raw record next to the run that produced it. Only repeated, corroborated gaps are worth promoting into the context.
The other is reviewer convergence. Their early reviewer agents would repeatedly push, expanding the implementer's change scope further and further. The fix was to tell the reviewer to bias toward merging and raise only substantive issues, while letting the author choose to fix, postpone with reasons, or push back.
Lesson 10: Preserve Coherence, Carry the Forever Risk
When implementation becomes cheap, what gets scarce is future coherence and long-term ownership. Every accepted change is still adding constraints and maintenance obligations to a shared system.
This lesson contains the most honest passage in the entire piece. That five-month, million-line internal product — the internal launch and adoption strategy proved viable in the startup phase. But how architectural coherence evolves on a "multi-year" timescale is an acknowledged, unsolved problem in the original text.
Another honest place is where he uses his own project to push back on the impression that "agents can chew through hard work in one go." The Artichoke state-machine refactor started as a giant change across 114 files that didn't even compile. It was broken up into 50 preparatory PRs merged over time, with 200 other PRs landing in the repo during the same period. Then it took several more major integration attempts and runtime failures before it finally merged.
Route was known. Done in one PR.
Human involvement: three times.
Endpoint was clear, but the safe route had to be learned.
50 preparatory PRs, multiple failed integrations.
The size of the change and the runtime duration say almost nothing about whether a safe route has been charted. The difference is whether the route itself is certain. He gave this missing capability a name: "the fear of fifty more PRs" — predicting how much you'll regret a future change. He explicitly lists this as still an unsolved capability problem.
What has to survive a rewrite
The implementation itself is a disposable render. These six things are what you keep:
- Behavior & domain spec
- Compatibility surface & migration rules
- Decision & architecture boundaries
- Dependency & supply-chain policy
- Evidence corpus & known failure cases
- Release identity & operational contract
Symphony is built this way: it publishes a behavioral spec, and other teams can produce their own independent implementations. This pattern is called a "ghost library" — sharing a contract, not a code artifact.
The OpenAI team replaced a generic concurrency-limiting dependency with their own implementation, wired it to their own observability, and added full test coverage. The cost is stated plainly: when you internalize a dependency, you're back to zero. The upstream's accumulated scale, compatibility, and security confidence all reset. The repository has to rebuild evidence for the part it now owns.
Lesson 11: Once a Direction Is Settled, Run It as a Continuous Loop
There's a whole class of work where the direction is already decided, it just never fits into the schedule: localization, adding instrumentation, keeping dependencies updated, removing obsolete compatibility paths, fixing architectural drift, refreshing generated data and docs. Once the desired outcome, the evidence method, and the authority boundary are all clear, this work can keep pushing itself forward.
To see if a loop can stand up, check if it can answer five questions.
| Question to answer | What happens without it |
|---|---|
| What condition should always remain true? | The loop has no goal; it becomes a periodic no-op |
| What signal indicates the system has drifted? | Drift goes unnoticed until someone stumbles on it |
| What evidence proves a change restored it? | You won't know if the fix worked |
| Which operations can run autonomously, where is approval required? | Either too scared to run, or you cause an incident |
| Which persistent state records results for the next round? | Every round re-discovers the same facts |
"Task" and "goal" are different here. "Fix this flaky test" ends with a patch. "Keep CI p95 latency under N minutes" is a goal — it continually observes the system, generates and tests interventions, requests human authority when needed, and has an explicit retirement condition.
Nothing to do · Candidate proposed · Change proven · Approval requested · Needs recovery · Policy outdated.
Quiet no-op is healthy. Re-discovering the same facts repeatedly is the signal that persistent state is missing.
There's also a claim about incident response: a code red shouldn't end with metrics turning green. It should leave behind a maintenance loop. During the response, people learn which invariants matter, which signals predict drift, and which interventions work. These should be recorded, along with the goal, sensors, authorized actions, escalation path, and the owner of the loop. Because agent execution is cheap enough, some newly discovered invariants can be maintained without a dedicated team.
The environment itself also grows weeds. Docs age, code grows conflicting styles, review comments expose missing guardrails, a once-useful tool becomes redundant after a model change. At OpenAI, a set of persistent background tasks replaced the old weekly manual cleanup: scanning for deviations from established principles, updating quality ratings, opening focused refactor PRs, cleaning stale docs. Small patches should land before a bad style becomes future prompt material.
This lesson has a clear role for people: The loop maintains a chosen policy's relationship to a changing world. "Which world is worth building" remains a human job.
Lesson 12: Optimize for Measured Effectiveness
Tokens, lines of code, number of agents, number of checks — these are inputs. What you want to optimize is accepted outcomes per unit of scarce human attention.
Token leaderboards are one vanity metric: the signal they reward goes up even when value stays flat. The "a billion tokens per engineer per day" number later got a gate added: the work has to merge to main and demonstrably improve the user experience to count.
Three concepts need separating: ReachHow much of the job an agent can actually touch. Repo, local app, tests, browser, logs, deployments, evidence — all expand reach, activity (tokens and busy time), and Effective utilizationThe portion of activity that actually lands an accepted outcome or produces reusable evidence. Preventable discards, rework, and fixes all cut into it.
You must track four clocks separately
| Clock | What it measures | Why track it separately |
|---|---|---|
| Feedback latency | From action to useful test, build, trace, or review signal | Determines iteration speed |
| Actual duration | From start to finish of one run | Consumes execution capacity, can stall downstream |
| Synchronous human attention | Minutes a person must direct, relay, review, or rescue | Determines how much work the org can supervise |
| Time to accepted outcome | From request to proven, accepted, deployed | Accounts for queueing, reruns, review, and delivery together |
Two examples side-by-side make clear why they must be separated. That 60-hour refactor had a long actual duration — three to four days — but a tiny synchronous human interface: just the initial prompt and two follow-ups. It consumed ~330–350 million tokens and $2,800, produced one PR, and he estimates it would have taken him three weeks to do by hand. Contrast that with a 2023 pairing session with ChatGPT: code copied back and forth between editor and browser, test results relayed by a human back into the chat box, a few hundred prompts per session. The actual duration wasn't long, but a person's attention was drained within an hour.
Exploration, rework, and waste are three different things
Discarding can be productive. In a high-uncertainty refactor where the final interface shape wasn't settled, he'd have the agent produce an exploratory change up to 50,000 lines, push it as a PR for human inspection, and then discard it. That attempt exposed where an agent would likely fail and what interface seams the eventual switch needed. He turned that failure map into about fifteen preparatory and enabling PRs, and only then came back for the real switch.
Repeating a known failure without new evidence. And a matching diagnostic: an environment that keeps eliciting the same high-signal corrections is defective over time, even if each individual correction is cheap.
How to Apply These Twelve Lessons to Your Own Project
The repo's playbooks directory has a ready-to-follow process for a bounded, replayable, real task.
The "fresh" in step five has a specific meaning: use a new session and an isolated, equivalent starting state, so the re-run doesn't inherit hidden help from the original implementation conversation. Also verify separately that this intervention was actually retrieved, actually called, and actually had anything to do with this run. A successful result is zero evidence for an instruction or tool that was never used.
Three templates you can copy directly into your own project.
Target system & version: Relevant external state: Locked model & agent config: Representative task: Acceptable outcome: Evidence that proves that outcome: Authority scope & approval boundaries: Budget & stopping conditions: Suspected missing piece:
If we add [intervention] at [the earliest place that controls it], then the locked agent on [representative task] will show [observable behavior change], because [mechanism]. Evidence that could support this hypothesis: Evidence that could weaken it: Expected maintenance cost & ownership:
Task & acceptable outcome: Target version & external state: Locked agent & authority scope: Baseline evidence: The earliest broken handoff, and who owns it: Change & expected mechanism: What verification was done: Evidence from the fresh re-run: Outcome comparison: Evidence comparison: Human-relay & latency comparison: Risk & maintenance cost comparison: If you did a "remove it and run again," the result: Decision: Keep | Modify | Remove Long-term ownership & recorded lessons: Follow-up actions or retirement conditions: Known limitations:
When your comparison doesn't count
There's also a separate invalidation checklist. If any one of these is true, the evaluation can't support the conclusion it's aiming for.
| Invalidation condition |
|---|
| The conclusion claims "used this context" or "used this capability," but that thing was never retrieved or called |
| Beyond the pre-declared intervention, differences in instructions, tools, permissions, external state, or write access were mixed in |
| A single run with randomness is treated as representative |
| Scoring requires a reference implementation that isn't public |
| Checks that only the evaluator can see are reported as evidence the agent produced itself |
| Copying the entire external corpus into the evaluator means the evaluator is answering itself with its own copy of the corpus |
| Using token count, lines of code, or activity as a substitute for an accepted outcome |
| The worker itself changed during the comparison |
| The goal and version in the public report are opaque, so readers can't inspect or reproduce it |
A harness is worth its maintenance cost when the same worker produces better, accepted results in the environment that actually matters. evals/README.md, harness-engineering repository
Lock the model and coding agent in place, tune only what’s around them — twelve lessons from a year of practice
Ryan Lopopolo (now Principal Engineer at Google Cloud, ex-OpenAI) breaks a year of practice into twelve lessons. This one-page visual gives you the core moves and the copy-paste templates.
↓ One-page read · includes an animated diagram
"Harness" here means the gear of a draft horse — saddle, reins, horseshoes — that gets the horse's strength onto the ground. It's not "to restrain." These twelve lessons are by Ryan Lopopolo, now Principal Engineer at Google Cloud, formerly at OpenAI building agent environments. The core move is one sentence: treat the chosen model and coding agent as a locked, unchangeable black box. Only change the context and tools around them.
how a refund should work, what a contract looks like, and the common idioms of your language
whether this refund is an exception, how *this* team writes this line of code today, or who approves this change
The twelve lessons split into three layers: three set the boundaries to make an accountable environment, six are the operational levers you turn daily, and three decide whether this whole system gets more valuable over time. Locking the boundaries is the starting point. When the model generation or agent config changes, the entire environment needs to be re-accepted. Otherwise, when a task fails, you can't tell whether it's the model or your setup.
The code in the repo is the prompt you feed your agent. A pattern that appears over and over gets copied. Two patterns coexisting means both get copied. Corrections work the same way: a one-off, point-specific fix that only changes the named thing means the same class of problem will recur elsewhere.
Old style and new style sit side by side in the repo. The agent must decide which one to copy each time, whether the migration is done, and both styles are being treated as templates.
Turn on a lint rule, fix ~600 violations in one PR, and add tests. Now there's only one answer to copy.
The full article also has three fill-in templates you can copy directly, plus a nine-item checklist for "when your comparison doesn't count."
These numbers are all from Ryan Lopopolo’s own projects, not independently audited.
New model?!
about this
what it's missing
Touch no parameters
so what do we change?
Those are the levers.
Style B
Both exist
should I copy?
Old style
All cleaned up
in one PR
the one named function
this broken too?!
handle the whole class
1,500 PRs
Tune what's around it.
Members can copy or download the full Markdown, and let their AI subscribe and ask.
Found this useful? Leave your email — every new explainer, the moment it's out. Unsubscribe anytime.
