Chapter 4 of 12 · Build

Ember builds, Flint reviews, then they swap

Two builders on two vendors. Whoever did not write the pull request reviews it. Findings with severities, two rounds at most, then Priya. This is where the code gets written and where most of the mistakes get caught.

Agents Ember and FlintRun on Claude Code and Codex CLIYou need Chapter 3, an issue in Detailed
The builders

Two of them, on purpose

Ember runs on Claude Code. Flint runs on Codex. They read the same repo and the same rules. They are not the same agent with two names. They are two different models with two different habits reading each other's work. That is the point.

Independent judgement

A model reviewing its own output tends to agree with itself. A different model reading the same diff cold finds the case the first one did not think of. Flint's two findings on PR #57 below are exactly that kind.

Cheaper than a human review

Priya cannot review TypeScript and Tom will not. A cross-review by the other builder costs minutes and runs every time. It is not as good as a senior engineer. It is far better than no review, which was the alternative.

No single outage stops the shop

When one vendor has a bad afternoon the other keeps building. Bellows picks whichever builder Anvil named and falls back to the other. Two subscriptions bought that. It has paid for itself twice.

This is the same split Danny runs in Development HQ, his own review runner: QA on Codex, product and marketing on Claude, one review at a time, evidence in a Notion Review Runs database. The pack copies the shape and not the tooling.

The shared instruction layer

Two files, one set of rules

Claude Code reads CLAUDE.md at the start of every session. Codex reads AGENTS.md. Each file describes the company, names the crew, lists the states and states the rules that are always true. They are written so that Ember and Flint hold the same standard while reading different files. Here is both. Read the numbered rules side by side.

# Alderline CRM — project instructions

You are working inside the Alderline Coffee Roasters CRM repository. Read this
file first in every session. It is short on purpose. Details live in the wiki
(Notion or Outline) and the kit files it points to.

## The company and the product

Alderline is a B2B specialty coffee roaster in Portland, Oregon. It supplies
about 140 cafés and grocers on weekly delivery routes. This CRM replaces a
HubSpot account that never fit a delivery business. Six modules: Accounts,
Contacts, Pipeline, Orders and routes, Tickets, Activity. The only users are
Priya (CEO), Tom (operations) and Sam (account manager).

## The crew

Work is done by named agents. Refer to each other by name in issues, PRs and
the wiki.

- **Anvil** scopes. Nothing is built without an issue in state `Detailed`
  with acceptance criteria. `.claude/agents/anvil.md`
- **Ember** (Claude Code) and **Flint** (Codex) build and review each other.
  Whoever did not write the PR reviews it. `.claude/agents/ember.md`, `AGENTS.md`
- **Gauge** writes and runs tests from the acceptance criteria. Nothing is
  `Ready to Deploy` until Gauge is green. `.claude/agents/gauge.md`
- **Warden** reviews every PR for security and can block a merge.
  `.claude/agents/warden.md`
- **Beacon** owns customer-facing words. **Ledger** owns tickets. **Quill**
  writes back to the wiki. **Bellows** dispatches and enforces the gates.

## The loop and the states (Linear)

`Idea → Scoped → Detailed → In Progress → In Test → Ready to Deploy → Deployed`.
Testing and development cycle: a failed test moves the issue back to
`In Progress` with a defect comment from Gauge. Only Bellows moves an issue to
`Deployed`, and only after Priya approves at the deploy gate.

## Rules that are always true

1. One issue, one branch, one PR. Branch name `ALD-123-short-slug`.
2. Every PR description lists the files changed, how it was verified, and
   what could not be verified. Say so plainly when something is untested.
3. Never write a secret into the repo. `.env*` files are read by the app only.
   Hooks will stop you; do not try to route around them.
4. Customer data is real to the people it describes even in a demo. No test
   writes to a production database. Deletions need a human.
5. Prefer the smallest change that meets the criteria. Extra scope becomes a
   new `Idea` issue, not a bigger PR.
6. When you finish, Quill writes the decision and the changelog. If you are
   the last agent in a run, call Quill.

## Commands

```
npm run dev          # local app on :4300
npm run test         # Vitest unit tests
npm run test:e2e     # Playwright against the preview URL in PREVIEW_URL
npm run lint         # eslint + typecheck
npm run db:migrate   # applies migrations to the DATABASE_URL you are pointed at
```

## The gates (when to stop and ask Priya)

Defined in `.claude/gates.json` and enforced by `.claude/hooks/guard.mjs`. If a
gate matches, you will be asked for permission. Explain what you are about to
do in one sentence and wait. Do not split an action into pieces to avoid a gate.
# Alderline CRM — instructions for Codex (Flint and Gauge)

Codex reads this file at the start of every run. Claude Code reads CLAUDE.md,
which points here for the parts that are shared. Keep the two in step.

## Who you are

When you run in this repository you are one of two named agents:

- **Flint**, builder and reviewer. You review pull requests written by Ember
  (Claude Code) and you build issues that Bellows assigns to you. You are the
  contrarian on the team by design: read the diff before the description,
  look for the case the author did not think of, and say exactly what would
  make you approve.
- **Gauge**, QA and QC. You write test cases from the acceptance criteria on
  the Linear issue, run them against the preview deployment, and file
  defects. You test the criteria, not the code. You never mark a case passed
  that you did not run.

The kickoff prompt tells you which one you are for this run.

## The project in three lines

A CRM for Alderline Coffee Roasters, a B2B coffee roaster with 140 café
accounts on weekly delivery routes. Six modules: Accounts, Contacts, Pipeline,
Orders and routes, Tickets, Activity. Next.js app, Postgres, Playwright and
Vitest for tests, Vercel for previews and production.

## Rules that are always true

1. One issue, one branch, one PR. Branch `ALD-123-short-slug`.
2. A review is a list of findings, each with a file, a line, a severity
   (Blocker, Should fix, Nit) and the change that would resolve it. End with
   `APPROVE` or `REQUEST CHANGES` on its own line. Bellows parses that line.
3. Two review rounds maximum. If Ember and Flint still disagree after two,
   stop and write `ESCALATE: <one sentence>`; Bellows asks Priya.
4. Tests are derived from acceptance criteria. Name each test after the
   criterion it proves: `AC-3 route notes save and reload`.
5. Never touch `.env*`, migrations that drop or truncate, or anything under
   `billing/` without a human. The gates file lists the rest.
6. Report what you could not verify. An honest gap beats a confident guess.

## Commands you may run

```
npm run lint
npm run test
PREVIEW_URL=<url> npm run test:e2e
npm run build
```

Do not run `npm run db:migrate` against anything but a local database. Do not
deploy. Deploys belong to Bellows and Priya.

## Output that Bellows parses

- Review: findings, then `APPROVE` or `REQUEST CHANGES` or `ESCALATE: ...`.
- QA: a table of criteria with `PASS`, `FAIL` or `NOT RUN`, then `QA GREEN` or
  `QA RED` on its own line, then one Linear comment per FAIL beginning `Defect:`.
The one rule that keeps them in step

When either file changes, the other changes in the same pull request. AGENTS.md says it in its second line: keep the two in step. The Stop hook enforces the memory of it: any run that edits CLAUDE.md, AGENTS.md or anything under .claude/ is flagged as a learning event and Quill records what changed and why. Two files that drift are two crews with two standards. One PR that touches both is one crew.

The handoff protocol

Four rules, no exceptions

  1. Whoever did not write the PR reviews it

    Ember builds, Flint reviews. Flint builds, Ember reviews. Bellows enforces this by reading which builder took the issue and starting the other one with the review prompt. Neither agent reviews its own work and neither one merges.

  2. Findings, each with a severity

    A review is a list of lines shaped severity · file:line · what · the change that would resolve it. Three severities: Blocker, Should fix, Nit. No essays. A finding without a file and line is not a finding.

  3. One verdict on its own line

    APPROVE, REQUEST CHANGES or ESCALATE: one sentence. Bellows parses that line and nothing else. APPROVE moves the issue to In Test. REQUEST CHANGES sends the builder back in with the findings. Nits do not block an approve.

  4. Two rounds, then Priya

    The gates file sets maxReviewRounds to 2. If the reviewer still requests changes after the second round, Bellows stops and asks Priya with both sides' last words. Two engineers, two opinions, the founder picks. In six weeks at Alderline that happened twice.

These are the two prompts Bellows sends. The build prompt tells whichever runtime it lands in who it is. The review prompt does the same in reverse. Copy them into kit/prompts/ if you have not already.

You are building Linear issue {{ISSUE_ID}} ("{{ISSUE_TITLE}}") in this repository.

If you are running in Claude Code, use the ember agent. If you are running in
Codex, you are Flint; AGENTS.md is your instruction file.

1. Read the issue. Confirm it is in state Detailed with numbered acceptance
   criteria. If not, stop and say so.
2. Read CLAUDE.md (or AGENTS.md), then the files you will touch.
3. Create branch ALD-{{ISSUE_ID}}-<short-slug> from main.
4. Build the smallest change that meets every criterion. Run the app and use
   it. Run `npm run lint` and `npm run test`.
5. Open a pull request with the body format from your instruction file
   (Issue, What changed, How I verified it, Not verified, For the reviewer).
6. Move the issue to In Progress and add the PR link.

Do not merge. Do not deploy. If a gate asks you to stop, stop and explain in
one sentence what you were about to do.

Preview URL for this branch, once Vercel builds it: {{PREVIEW_URL}}
{{EXTRA}}
Review the open pull request for Linear issue {{ISSUE_ID}} ("{{ISSUE_TITLE}}").

You did not write this code. If you are Codex you are Flint; if you are
Claude Code you are Ember reviewing Flint. Read the diff before the PR
description. Then check, in this order:

1. Every acceptance criterion on the issue is met by the diff, not by the
   description. Name the criterion and the code that satisfies it.
2. The case the author did not think of: empty state, second user, slow
   network, a value that is null, a name with an apostrophe, a date across
   midnight.
3. Tests: the author's unit tests exist and pass. Say which ones you ran.
4. Anything a café owner would notice on the first screen.

Write findings as `severity · file:line · what · the change that would resolve it`
with severities Blocker, Should fix, Nit. Then, on its own line, exactly one of:

APPROVE
REQUEST CHANGES
ESCALATE: <one sentence, only if this is the second round and you still disagree>

Post the review as a PR comment and as a Linear comment on the issue.
{{EXTRA}}
The worked run

ALD-42 in three terminals

Monday 14 September, 13:30. Priya approved the customer-data gate at 09:42. ALD-42 is Detailed with five criteria and Warden's threat note. Anvil said builder: ember. Bellows starts Ember. This is what it ran and what came back. The flags on the claude -p call are the ones Bellows uses; check them against the current Claude Code docs.

Terminal 1 · Bellows kicks off Ember

AGENT_NAME=ember ISSUE_ID=ALD-42 claude -p "You are building Linear issue ALD-42 (\"Route notes on an account\") in this repository. If you are running in Claude Code, use the ember agent. …" --output-format text --permission-mode acceptEdits
Reading ALD-42. State: Detailed. Criteria AC-1 to AC-5. Warden's threat note: tenant check on every read and write, 280 limit on the server, no note text in logs.
Reading CLAUDE.md, src/app/accounts/[id]/page.tsx, src/db/schema.ts, src/app/routes/[day]/page.tsx and PR #51 (quality log, the last change to the account page).
git checkout -b ALD-42-route-notes
+ src/components/RouteNote.tsx
+ src/app/api/accounts/[id]/notes/route.ts
~ src/db/schema.ts (route_notes: account_id, body, author_id, updated_at)
~ src/app/routes/[day]/page.tsx (note column on the route sheet)
+ tests/unit/route-note.test.ts
npm run lint … clean
npm run test … 14 passing
npm run dev … opened /accounts/acc_014, saved "Back door after 7am", reloaded, saw the note with Sam Lindqvist and 14:19
git push -u origin ALD-42-route-notes
gh pr create … https://github.com/alderline/alderline-crm/pull/57
Linear: ALD-42 → In Progress. PR link added.
EMBER: PR #57 open · 5 files · lint clean · 14 tests passing · AC-5 not verified locally

The pull request body uses the headings from Ember's agent file and no others. Bellows does not parse it. Flint reads it after the diff. Priya reads it on her phone when the deploy gate comes round.

GitHub · PR #57github.com/alderline/alderline-crm/pull/57

ALD-42 · Route notes on an account

#57
OpenEmber wants to merge 3 commits into main from ALD-42-route-notes

Issue

ALD-42 · Route notes on an account

What changed

  • src/db/schema.tsroute_notes table: account_id, body, author_id, updated_at. One row per account, upserted.
  • src/app/api/accounts/[id]/notes/route.ts — GET and PUT. Session and the account's tenant checked on both, per Warden's threat note. Returns 404 for another tenant so the account id is not confirmed.
  • src/components/RouteNote.tsx — the field, the Save note button, author and time after save, a character counter that stops at 280.
  • src/app/routes/[day]/page.tsx — note column on the driver route sheet, joined from route_notes.
  • tests/unit/route-note.test.ts — trimming, the 280 counter, the author stamp.

How I verified it

  • ran: npm run lint, npm run test (14 passing)
  • clicked: opened Riverbend Café, saved "Back door after 7am", reloaded, saw the note with Sam Lindqvist and the time. Opened Tuesday East, saw the note on the Riverbend row.

Not verified

  • AC-5, another tenant cannot read the note. I have one tenant locally. Gauge's second-tenant fixture covers it.
  • The Vercel preview had not built when I finished, so nothing above was checked on the preview URL.

For Flint

The PUT handler in notes/route.ts. I check the session and the account's tenant. Tell me if the scoping is too loose or if the 404 should be a 403.

lint · typecheck
unit tests · 14 passing
Vercel preview · building

Terminal 2 · Flint reviews cold

Bellows sees In Progress and a PR link. Ember built it, so Flint reviews. It starts Codex with the review prompt. Flint reads the diff before the description, checks each criterion against the code and runs the tests itself.

AGENT_NAME=flint ISSUE_ID=ALD-42 codex exec --full-auto "Review the open pull request for Linear issue ALD-42 (\"Route notes on an account\"). You did not write this code. If you are Codex you are Flint … Read the diff before the PR description. …"
Diff of PR #57: 5 files, +214 −6. Read in full before the description.
AC-1 met: RouteNote.tsx renders a labelled "Route note" field on the account page (page.tsx:88).
AC-2 met: PUT writes author_id and updated_at (route.ts:31); the component shows both after reload (RouteNote.tsx:57).
AC-3 met in the browser only. The counter blocks Save above 280. The server accepts any length. See finding 2.
AC-4 met: routes/[day]/page.tsx joins route_notes and renders the note column (page.tsx:41).
AC-5: route.ts:12 checks session.tenantId against the account's tenant on GET and PUT. 404 for another tenant is fine and matches the threat note.
Ran: npm run test (14 passing). Ran the app; the second-user case I could think of is an empty save.
Should fix · src/components/RouteNote.tsx:41 · an empty note saves as an empty string · disable Save until the trimmed length is above 0
Nit · src/app/api/accounts/[id]/notes/route.ts:18 · 280 limit is client-side only · validate on the server too (Warden will ask)
REQUEST CHANGES
Posted as a PR comment on #57 and a Linear comment on ALD-42.

Two findings. Neither one is a bug in what Ember built. Both are a case Ember did not think of. That is what a cold read from a different model is for. Bellows reads REQUEST CHANGES, counts round one and starts Ember again with the findings.

Terminal 3 · Round two and the approve

AGENT_NAME=ember ISSUE_ID=ALD-42 claude -p "Flint reviewed PR #57 for ALD-42 and requested changes. Read the review, resolve each finding, keep the tests green, push, and reply on the PR with what changed per finding." --output-format text --permission-mode acceptEdits
Reading Flint's review: 1 Should fix, 1 Nit.
~ src/components/RouteNote.tsx Save disabled while body.trim().length === 0
~ src/app/api/accounts/[id]/notes/route.ts 400 "Route notes are limited to 280 characters" when body.length > 280
~ tests/unit/route-note.test.ts +2 cases: empty body, 281 characters on the server
npm run lint … clean · npm run test … 16 passing
commit 3f1c9a2 Disable Save on an empty route note (Flint, should fix)
commit 8b04d7e Enforce the 280 limit on the server too (Flint, nit)
git push · replied on PR #57 with one line per finding
EMBER: round 2 pushed · 2 commits · 16 tests passing
AGENT_NAME=flint ISSUE_ID=ALD-42 codex exec --full-auto "Review the open pull request for Linear issue ALD-42 (\"Route notes on an account\"). This is round two. Check each earlier finding against the new diff. …"
Finding 1 resolved: RouteNote.tsx:41 disables Save on an empty trimmed body. Unit test covers it.
Finding 2 resolved: route.ts:18 returns 400 above 280 characters with the message AC-3 asks for. Unit test covers it.
No new findings. Ran: npm run test (16 passing).
APPROVE
Posted on #57 and ALD-42.

Bellows reads APPROVE on its own line and moves ALD-42 to In Test. It leaves a comment on the issue: which agent ran, on which runtime, for how many minutes, and the last line it wrote. Gauge starts in chapter 5. Nobody merged anything. Merging happens at the deploy gate.

The reverse pattern

Flint builds, Ember reviews

Back in August. ALD-4, Orders and weekly pattern, 3 points, no flags. Anvil said builder: flint because it is a well-specified back-end slice: a table, a weekly pattern rule and an API. Bellows started Codex with the build prompt and then Claude Code with the review prompt. Same protocol, other way round.

AGENT_NAME=flint ISSUE_ID=ALD-4 codex exec --full-auto "You are building Linear issue ALD-4 (\"Orders and weekly pattern\") in this repository. If you are running in Codex, you are Flint; AGENTS.md is your instruction file. …"
Branch ALD-4-orders-weekly-pattern. 4 files: src/db/schema.ts, src/lib/pattern.ts, src/app/api/accounts/[id]/orders/route.ts, tests/unit/pattern.test.ts. npm run test: 9 passing. PR #12 opened. ALD-4 → In Progress.
FLINT: PR #12 open · 4 files · 9 tests passing
AGENT_NAME=ember ISSUE_ID=ALD-4 claude -p "Review the open pull request for Linear issue ALD-4 (\"Orders and weekly pattern\"). You did not write this code. If you are Claude Code you are Ember reviewing Flint. …" --output-format text
AC-1 to AC-3 met. Ran: npm run test (9 passing). The case the author did not think of: a skipped week.
Nit · src/lib/pattern.ts:27 · a week with no delivery (a holiday) is stored as quantity 0 and shows as an order · treat 0 as "skipped" in the orders list
APPROVE
Posted on #12 and ALD-4. The nit is filed as ALD-16, Skipped weeks on the orders list, state Idea.

A nit does not block. Ember approved and filed the nit as a new Idea so it did not get lost. Bellows moved ALD-4 to In Test and Gauge took it from there.

The picture

Who talks to whom

Two tips

Things Priya learned in week two

Use git worktrees so both builders work at once

Two agents in one checkout step on each other. A worktree gives each branch its own folder on the same repo. Bellows runs Ember in one and Flint in another and neither sees the other's half-finished files. One line per slice.

git worktree add ../alderline-ALD-42 -b ALD-42-route-notes
Preparing worktree (new branch 'ALD-42-route-notes')
git worktree add ../alderline-ALD-4 -b ALD-4-orders-weekly-pattern
Preparing worktree (new branch 'ALD-4-orders-weekly-pattern')
git worktree list
~/alderline-crm 2a7f0c1 [main]
~/alderline-ALD-42 2a7f0c1 [ALD-42-route-notes]
~/alderline-ALD-4 2a7f0c1 [ALD-4-orders-weekly-pattern]

Remove a worktree when the PR merges: git worktree remove ../alderline-ALD-42.

Change CLAUDE.md and AGENTS.md in the same PR

When a rule changes it changes for both builders or it changes for neither. Ember edits CLAUDE.md and AGENTS.md together and names the change in the PR body. Flint reviews it like any other diff and checks the two files still say the same thing. The Stop hook flags the run as a learning event and Quill writes the entry. If you ever find the two files disagreeing, that is the first defect to fix that day.

Use the ember agent. Rule 1 in CLAUDE.md and AGENTS.md should also say that the PR title starts with the issue id. Change both files in one branch, keep the wording identical where the rule is shared, open a PR that quotes both changes, and ask Flint to check they match.

Founder Q&A

Why two vendors and not two Claude agents?

Because two agents from the same model share the same blind spots. They were trained the same way and they miss the same cases. A second vendor is a second pair of eyes with different habits. It also means a bad day at one vendor does not stop the shop. The cost is a second subscription and one more file to keep in step. If that is too much, start with two Claude agents and add Codex when the loop is running.

What if they disagree?

They get two rounds. Round one: the reviewer requests changes and the builder responds. Round two: the reviewer checks again. If it still requests changes it writes ESCALATE: with one sentence, Bellows stops the slice and asks Priya with both sides' last comments. She picks. That happened twice in six weeks. Both times the disagreement was about the spec and not the code, and Anvil got a note.

Does Codex see the same repo as Claude Code?

Yes. Both run in the same checkout, or in two worktrees of the same repo. Both read the same branches and the same pull requests through GitHub. The only thing that differs is the instruction file each one reads first. Codex does not read .claude/ and Claude Code does not read AGENTS.md unless told to, which is why the two files exist.

Can I use only Claude?

Yes. Make Flint a second Claude Code agent with the same personality and give it the review prompt. Set its agent file to a different model tier from Ember's so the two reads differ at least that much. Bellows needs one change: run the reviewer with claude -p instead of codex exec. You keep the protocol and lose the independent judgement. It is still far better than no review.

How do I know who wrote what?

Four places, all of them written for you. The branch name carries the issue id. The PR author is the builder and the reviewer's comment is signed. The Linear issue has one Bellows comment per run: which agent, which runtime, how many minutes, the last line it wrote. And the dashboard folds all of that into hours and outcomes per agent. If you cannot tell who did something, that is a defect in the trail and Quill hears about it.

What you have now

Two builders who read each other's code and cannot merge their own. The two instruction files that hold them to one standard and the rule that keeps those files in step. The review protocol: findings with a file and a line, one verdict on its own line, two rounds then you. PR #57 is approved with sixteen tests passing and ALD-42 is In Test. Gauge has not seen it yet.

Next: Chapter 5 · Gauge. QA without a human. One test per criterion, run against the preview, and the thresholds that make it stop and ask.