Ari’s Space*   est. 2026
Skip to content
ari*s space[ a. ]
← Librarydiscipline and process / 2026-05-03

The Codex Loop: A Native App Dev Playbook

Brain in the planning model, hands in the executor, verification gates that catch black-screen surprises before they ship. The methodology that built a Mac app in three days.

12 parts / Hover to explore. Click to read.

Brain in the planning model, hands in the executor, verification gates that catch black-screen surprises before they ship. The methodology that built a Mac app in three days.

Planner holds agreement
           | bounded brief
           v
      executor -> report
                    |
          independent verification
                    |
              human judgement
                    |
             continue or correct
Keep the agreement, execution and verification distinct. Passing tests establish their tested claims; visual acceptance needs someone to inspect the running result.
Three vertical columns: Brain (planner), Hands (executor), Eyes (you). Each lists what it holds, decides, and refuses.
Three roles, three contexts, one product

Start with the black screen

An earlier build of this app had eight completed phases, 61 passing tests, screenshots in the chunk reports and plenty of done markers.

I launched it. Black screen.

The contract had drifted three phases earlier. The session doing the work was also deciding whether its own work met the contract, and it hadn't noticed the gap.

That is why I use the Codex Loop: a planner holding the agreement, an executor doing a bounded task, and a human checking the visible result. Passing tests matter. They only prove what those tests actually checked.

Make the roles clear

The planner holds the project. It keeps the spec, plan, briefs and verification decisions consistent. It needs to understand the whole result even when the executor only sees one part.

The executor carries out a chunk. It receives a tight brief and the necessary files. The next chunk must be understandable without relying on the previous executor session's memory.

You judge the experience. Look at the interface, try the behaviour and decide whether the result meets your standards. A commit message cannot do that for you.

I used this arrangement on a native Mac app over three days. That build finished with two clean phase tags, 123 passing tests, atmospherics visually approved on the first build and no rebuilds. Those are results from that project, not a substitute for checking the next one.

Write the five working documents

Each document has a different job. Keeping those jobs clear is more useful than making every document long.

The spec describes the product

Record what you're making, its architecture, the decisions and the component list. Include the details that make the design feel like the intended product.

Two inventories are particularly useful:

  • Craft: the glow, asymmetry, animation curve and other details present in the prototype but missing from a plain wireframe description.
  • Staleness: paths, tools and values that have changed since earlier work. Check these before a new executor starts following old directions.

The plan puts the work in order

Use one plan per phase. In this playbook, a task is roughly two to five minutes of work and names its files, steps, tests and intended commit message.

This playbook used a tests-first cycle for each implementation task: write the failing test, run it, implement the change, run it again and commit. The test should express the requirement; it still needs review to make sure it covers that requirement properly.

At the end, map every spec requirement to a task. A requirement with no task is work you've forgotten to schedule.

The brief makes a chunk dispatchable

A brief groups five to eight plan tasks around a useful milestone. Its frontmatter identifies the phase, covered tasks, dependencies and executor tier.

The body states the objective, scope, hard rules, stop conditions and evidence required at the end. I aim for 60 to 95 lines. When a brief grows much longer, check whether it contains two chunks.

That chunk size is for the document. The amount sent in one prompt can be smaller, as the slice method below explains.

The prompt tells this session what to do

Make it ready to use, with real paths and no blanks to fill in.

A continuation prompt can be short: the previous chunk was verified; read the next brief; proceed under the same rules; stop at the agreed point.

A fresh-session prompt needs the full starting position: project context, inputs, current-state checks, rules, stop conditions and done signal. It should work for an executor arriving cold.

The handoff carries the work forward

Write it at a natural boundary, before the session becomes difficult to use. Include the current state, decisions, unresolved items and the exact next prompt.

The next session should be able to read that file and establish what is true on disk. A handoff is useful only if it agrees with the actual work.

Check every chunk before continuing

A clockwise five-step loop: paste prompt, execute, report, verify, decide. The verify step happens in fresh context.
The per-chunk loop, with verification non-negotiable

The checks need to match what the brief promised:

  • Compare the Git log with the intended commits.
  • Inspect the files that should exist.
  • Run the relevant build and tests. Check their scope as well as their count.
  • Make sure snapshot recording hasn't been left on with isRecording = true.
  • For visual work, launch the app and inspect it.

Fresh context gives the reviewer a better chance of spotting an assumption the executor has become used to. It doesn't make the reviewer infallible. Keep asking what evidence would show that the result works.

The brain holds the contract. The hands ship the code. The eyes approve the result.

Send smaller slices when compaction becomes a problem

During one phase, the executor's auto-compaction failed twice partway through a chunk. In that run, prompts covering three or four tasks were stable. With five or more, compaction failed about half the time, often near task five or seven.

That observation changed how I dispatched the work. I kept the milestone-sized brief but sent only part of it in each prompt.

For a seven-task chunk:

  1. Send tasks one to four, with a stop at the end.
  2. Verify that slice.
  3. Send tasks five to seven.
  4. Verify again before moving on.

One extra dispatch was cheaper than recovering a broken session. Use the failure you actually observe to set the slice size; those task counts describe this build.

Treat a mismatch as useful information

If the spec and the real workspace disagree, I want the executor to show me the mismatch. Silently making it fit hides the decision that needs review.

This happened four times in phases 1A and 1B. The response was consistent: stop the affected work, bring evidence, let the planner inspect it, decide which source is right, correct the plan where necessary, and send a revised prompt.

The colours came from the wrong source

The plan had transcribed colour values from a text digest. The executor found that 11 of 17 differed from the canonical CSS.

We made the CSS authoritative and corrected the plan and tests. It cost about 30 minutes and avoided building the wrong palette into the app. Read the design source itself when the exact values matter.

A Swift property pattern didn't compile

The plan put @Published on a computed property. The fix used a stored @Published property with didSet for persistence.

We corrected the plan and resumed. That took about 15 minutes. A detailed spec can still contain a technical mistake; the executor needs a way to surface it.

The polling test had a timing mismatch

On its second failure, the executor identified a timeout-versus-interval problem. We injected a deterministic dispatch timer instead of repeatedly running the same flaky test.

The correction took about 20 minutes and made the timing controllable in future tests. A repeat run alone wouldn't have explained the failure.

A new composition threatened an old snapshot

Replacing a frame with a composition view would have changed what an existing snapshot test rendered. The executor spotted that before the test broke and added an override path so the original case could still be checked.

That kept the earlier baseline meaningful while allowing the new composition. We didn't need to approve an unexplained new snapshot just to get a green result.

Give visual work its own acceptance check

Some decisions require looking at the running product. In this build, that included the vignette's anchor, the terminal glow, phase-badge colours and how the whole interface held together.

A test can tell you that a class or value exists. It may not tell you whether the effect looks right in context.

Put the visual check in the brief's done signal. For work requiring human approval, the executor reports what is ready to inspect and waits for that approval before declaring the visual requirement complete.

Make tests capable of disagreeing with the implementation

Tests should make a mistake visible. Changing an assertion merely to accept the current result removes that protection.

In this loop, the executor must either fix the implementation or show why the contract is wrong. That rule prompted three useful challenges to real plan errors during the build.

For snapshots, enable isRecording = true only to capture an intentional baseline, then turn it off before committing. Inspect the image before accepting it. A baseline becomes part of what future work is judged against.

If an existing snapshot regresses, show the diff before replacing the baseline. Automatically recording the new output would hide the very change the test was meant to catch.

For things the unit tests can't establish, add explicit smoke checks: Metal rendering, an embedded terminal starting, drag-and-drop and live polling. Put each check in the relevant brief so it is part of completion.

For timing behaviour, inject a timer through a closure or protocol and let the test advance it deliberately. Diagnose a flake when it appears; don't turn repeated retries into the verification method.

Match the executor effort to the work

This was the tier guide used for the build:

TierWork it covered
MediumBoilerplate views, value types, enums, settings panes and most routine implementation.
HighWork across three or more frameworks, cross-API coordination, multi-step contract changes and areas where a previous bug required architectural judgement.

Medium was the default. High was for a specific reason, not a way to compensate for an unclear brief.

Keep the lessons in the next plan

Several habits prevented trouble: verification outside the executor's context, completion reports that named test counts, commit hashes, file paths and visual evidence, and checking failures before retrying them. We also avoided adding a separate plan for planning when the spec already contained enough implementation detail.

Some problems still got through:

  • Old absolute paths blocked dispatch after a machine move. Scripts needed roots derived from their own location, with an environment-variable override and an auto-detect fallback where appropriate.
  • Approximate design-token transcription produced the wrong colours. The canonical CSS needed to be the source.
  • The computed-property wrapper didn't compile. In this case, stored state plus didSet was the correction.

Write those corrections into the next authoritative document. Otherwise the next executor can faithfully repeat the same mistake.

Start the next app with a small working loop

Brainstorm the product with the planner, write the first phase plan, and divide it into briefs. Open a fresh executor session for a chunk and dispatch a slice it can comfortably finish.

Verify the files, tests, commits and visible behaviour after each slice. Save a handoff at the session boundary. Plan the next phase against the app that now exists.

The point is to keep the agreement, the implementation and the evidence distinct enough that they can correct one another. The black-screen build had plenty of completion claims. What it needed was a check that reached the thing I was actually going to use.

[ comments ]

guests welcome. members show first in the list

no comments yet. start the thread_

1 / 14