← The Points & Miles Problem · the machine · engineering deep dive · updated Jul 19, 2026

Part two · the machine

Two agents, one repo, and everything that keeps them from destroying it

This is the real architecture of the Night Shift — the machine behind pointsandprompts.com: how a planner agent and an executor agent ship to production unattended, the actual files they read, and the safety layer that exists because every part of it failed at least once. The narrative version with all 54 failures is in the field notes; this page is the reference.

Live status (Jul 18, 2026): the autonomous loop is currently paused — a .halt file sits in the relay directory while a human-led design overhaul ships through the same review gates. That is the kill switch doing its job: a multi-pass redesign and a queue of older autonomous briefs would fight each other. Pausing is a feature, not an outage.

01The loop, end to end

One shell script (watch.sh, ~500 lines) polls every 60 seconds. Work flows through two markdown files, one git repo, and one invariant:

The executor commits but never merges. The reviewer merges but never commits. Every box below exists to defend that split.
flowchart TB
  H["Human owner
sets goals · decides policy
paged only for emergencies"] subgraph PLAN["Planning half — Cowork session"] P["Planner / reviewer agent
writes briefs · verifies independently
merges · NEVER commits code"] end Q["queue/*.md
promoted one at a time"] NS["NEXT-STEPS.md
Status: OPEN
(the brief)"] subgraph EXEC["Execution half — headless CLI"] W["watch.sh
60s poller · sonnet by default
Model: header can escalate"] E["Executor agent
builds in isolated git worktree
commits · opens PR · NEVER merges"] end CS["COWORK-STATUS.md
Verification: PASS | FAIL
+ pasted evidence"] subgraph SAFE["Safety layer — plain shell on timers"] WD["watchdog.sh
alarms on silence"] KS[".stop / .halt
kill switch"] BU["backup.sh
vs. the robot itself"] BB["build budget
deploy quota guard"] PG["pr-gate.sh
merge gate: one verdict slot"] end R["reviewer.sh
independent Playwright verify
real browser · real taps · control run"] M["Merge to main"] N["Netlify deploy
(no build step)"] C["canary.sh
checks live prod
auto-reverts on failure"] PROD["pointsandprompts.com"] H --> P P --> Q --> NS --> W --> E --> CS CS --> R PG --> R R --> M --> N --> C --> PROD C -. "auto-revert" .-> M WD -.-> W KS -.-> W BU -.-> E BB -.-> N PROD -. "complaints, ideas, Slack" .-> H

Notes that don't fit in boxes: the poller runs the executor as claude -p --model sonnet; a brief can pin Model: opus or Model: fable in its header, and a failed attempt auto-escalates. The reviewer only ever inspects the newest open PR, and the merge gate reads exactly one verdict slot on main — which is what serializes merges (more below).

02Life of a brief

sequenceDiagram
  participant J as Jeremy (Slack/Cowork)
  participant P as Planner (Cowork)
  participant W as watch.sh (60s)
  participant E as Executor (worktree)
  participant G as pr-gate.sh
  participant R as Reviewer (Playwright)
  participant C as canary.sh

  J->>P: "the change log is weird" (verbatim complaint)
  P->>P: reproduce · read the code · write brief #62
  P->>W: NEXT-STEPS.md — Status: OPEN + Completion Signal
  W->>E: spawn executor (sonnet)
  E->>E: git worktree add · build · self-verify 390/768/1440
  E->>G: PR (product files only) + COWORK-STATUS.md verdict on main
  Note over E,G: paperwork commits to main, never to the branch —
two PRs deadlocked overnight before this rule G->>R: verdict slot names this PR · Verification: PASS R->>R: independent browser check, with a control run R->>C: merge · restamp cache-buster C->>C: probe live prod · grep the body, not the status code C-->>R: auto-revert if broken C->>J: alert only if something needs a human

03The two files — the entire protocol

There is no message bus, no API, no framework. The agents communicate through two markdown files at the repo root, and the discipline around them is most of the protocol:

FileDirectionContract
NEXT-STEPS.mdplanner → executorThe brief. Status: OPEN or DONE. Arrives uncommitted in the working tree — expected, not an error.
COWORK-STATUS.mdexecutor → plannerThe report. Must carry a Verification: PASS or FAIL header the gate can read mechanically. Real command output, verbatim errors, never the word "verified" without pasted evidence.

The subtle rule that unstuck the whole system: product code rides the branch; paperwork commits straight to main. Both sides used to rewrite the same two bookkeeping files on the branch, git conflicted on them, and perfectly good PRs became unmergeable — the loop once deadlocked overnight exactly this way.

04The actual files

Not paraphrased — these are excerpts from the live system, trimmed for length.

The brief template points-relay/BRIEF-TEMPLATE.md

Adapted from the ralph-wiggum loop family, which got one thing right that early briefs here didn't: "done" must be machine-checkable. A brief that says "make it responsive" is a vibe, not a finish line.

# NEXT-STEPS.md
Status: OPEN
Brief: #N — [short title]

## Why
[One paragraph. What's broken, who noticed, and what the user actually
experiences. Ground it in evidence — a screenshot, a probe result, a real
complaint. Not "improve X."]

## The work
### 1. [Change]
[What to do, and *why*. The executor is smart and will make better decisions
than a rote instruction if it understands the goal.]

## Completion Signal
**You may not mark this brief DONE until every box below can be ticked,
with pasted evidence.**
- [ ] `node --check` on the extracted inline script — passes, output pasted
- [ ] RESPONSIVE — headless screenshots at 390×844, 768×1024, 1440×900
- [ ] Zero page-level horizontal overflow at 390px and 768px
- [ ] Interactive changes reproduced on an emulated mobile viewport
      (real taps/scroll/keyboard, not a static screenshot)
- [ ] Hidden sections revealed before probing (probing display:none
      is a FALSE PASS — this has bitten us)

## Iteration limit
If you are still failing the Completion Signal after 5 attempts at the same
problem, stop. A documented blocker is worth more than a sixth attempt.

**Do not merge.** The reviewer verifies independently; a human decides.
The executor's standing orders points-compass/CLAUDE.md

What the execution agent reads on every run. The block quoted here is the honesty clause — the single most important paragraph in the system.

## When Jeremy says "check next"
1. Read NEXT-STEPS.md in this directory.
2. If Status: OPEN, execute the brief.
3. Overwrite COWORK-STATUS.md with your report: real command output,
   verbatim errors. Never write "verified" — paste the actual output.
   The report MUST carry a `Verification: PASS` or `Verification: FAIL`
   header line. The reviewer's gate reads that one line and nothing else.
   State your verdict once, in the header, honestly.
   **If your own verification did not pass, write FAIL.**
   A FAIL you declared is a working system;
   a FAIL you hid is the end of the relay.
4. Flip NEXT-STEPS.md to Status: DONE.
5. THE SPLIT — the branch carries PRODUCT CODE ONLY. Never commit
   NEXT-STEPS.md or COWORK-STATUS.md to your feature branch. Not once.
   The paperwork goes straight to main, in its own commit.
   WHY: product code doesn't conflict; paperwork does. Two stranded PRs
   and one deadlocked night taught us the paperwork stops travelling
   with the work.
A real brief, verbatim points-relay/queue/62-brief.md

What a production work order actually looks like. It starts from a verbatim Slack complaint, confirms the bug against the real data before assigning work, and constrains the fix against the site's existing id space.

Brief: #62 — Updates page: split Upcoming vs Recent changes, add
Your-Wallet/All-Programs filter, link from Wallet

## Why
Reported in Slack #claude-check by Jeremy (verbatim): "On the change log we
need to show 'Upcoming Changes' and 'Recent Changes' … It's weird to see a
change log for a date in the future and it doesn't indicate that the change
is coming."

Confirmed by reading the actual data: CHANGES (assets/data.js:3133) has 23
items across 3 groups. One is genuinely future-dated right now — Chase →
World of Hyatt, dated Oct 1, 2026 — and it renders in the same
undifferentiated list as everything else, with no "this hasn't happened
yet" signal. That's the exact bug Jeremy's describing.

## The work
### 1. Tag every CHANGES item with the program(s) it affects
Add a `tags` array using the same id space as WALLET_PROGRAMS/PROGID/CUR
keys. **Do not invent new ids.** … an untagged item just won't surface
under the wallet filter, which is correct; a wrongly-tagged one would show
a user a change to a program they don't have.

05Model doctrine — who runs on what, and why

The cost structure only works if the expensive model never does cheap work. This is written down (MODEL-DOCTRINE.md) because the enforcement is mostly behavioral, and a fresh session won't remember the conversation that established it.

RoleModelWhat it does
Strategy + validationOpusWrites briefs. Makes architecture calls. Judges what a machine can't measure. The scroll regression, the hotlink bug, and the stale-report gate were all caught here — not by the loop.
ExecutionSonnetEvery brief the executor runs. All the actual coding. Also all polling and triage — "is there work?" is not an Opus question.
Hard executionOpusOnly when a brief pins Model: opus, or a Sonnet attempt failed and the retry escalates. Earned, never assumed.
Prose & design directionFableField notes, changelog voice, and — as of this week — the product's design-direction docs, which Sonnet then builds to.
# MODEL-DOCTRINE.md — the rule, in one line
*Opus decides and checks. Sonnet does.*

The test of whether you're getting this right: at the end of a session, the
Opus transcript should be mostly decisions, briefs written, and verifications
run against real output. If it's mostly greps, edits, and bulk research it
did itself, Opus was doing Sonnet's job — the exact pattern that put the
weekly budget at 76%.

06Parallel build, serialized merge

The newest pattern (proven live on PRs #64/#65, and most of why peak days now land ~30 PRs): the bottleneck was never the ~20-minute build — it was the merge gate, which only trusts one verdict at a time. So builds went parallel and the merge stayed serial.

flowchart LR
  subgraph DECOMP["1 · Decompose up front"]
    D["Split into workstreams with
disjoint file/function ownership.
Same function = NOT independent."] end subgraph BUILD["2 · Parallel builds (~20 min, overlapped)"] A1["Executor A
worktree · branch A
self-verify · PR #64"] A2["Executor B
worktree · branch B
self-verify · PR #65"] end subgraph MERGE["3 · Serialized merge"] V["One verdict slot on main
(COWORK-STATUS.md names ONE PR)"] RV["Reviewer verifies + merges
newest PR first, then rebase the next"] end D --> A1 & A2 A1 --> V A2 --> V V --> RV RV -->|"rebase · next verdict"| V

The hard-won operational details, from PARALLEL-BUILD.md: CSS from each workstream appends in delimited end-of-file blocks so three-way merges stay clean; verdicts must go to the newest open PR first because the reviewer only inspects one PR per cycle (PR #64 once sat fully mergeable while the reviewer looped on #65); and while a PR's verdict is not in the slot, the reviewer refusing it is not a bug — that refusal is the serialization.

07The safety inventory

About 1,250 lines of plain shell across six scripts, on launchd timers. Nothing here is speculative — each row was purchased with a specific failure.

MechanismWhat it doesThe incident that created it
watchdog.shAlarms when the loop goes silentA four-hour stall looked identical to a quiet successful night. (Then the watchdog itself was silently broken for 259 polls — so now it's tested too.)
Kill switch.stop / .halt files halt everythingThe first version didn't work: the watchdog deleted it within 60 seconds.
Build budgetCaps deploys per day49 pushes in one day — mostly the loop's own bookkeeping — exhausted the host's build quota. Production was down four hours; the preview deploys died with it and deadlocked review.
backup.shSnapshots against the robot itselfThe threat model isn't "GitHub vanishes" — it's "my automation deletes my work." A task once ordered git reset --hard on the only copy of a finished feature.
Destructive-git hookExecutor refuses destructive git, even when toldSame incident. A human hitting Deny was the entire safety system that day.
pr-gate.shMerge gate: one verdict slot, fail-closedGrew from every false-green: it reads a single machine-checkable header, not prose.
canary.shProbes live prod after each merge, auto-revertsA 200 means the server answered — not that it answered correctly. The canary greps the body.
Verification harnessReal browser, real taps, always a control runThe first harness "proved" the site couldn't scroll — from inside a hidden automation tab. It closed a good PR and reported an outage that didn't exist.
Retry + iteration caps5 attempts, then write the blocker and stopA miscounting retry cap once halted the loop for work that had failed zero times — a guardrail firing when nothing is wrong is itself broken.
alert.shThe single front door for every alert. Normal alerts post straight to Slack via webhook the moment they fire; if the webhook is missing or the post fails it falls back to a queue file, and everything lands in a durable alerts.log — no alert can be lost. Urgent alerts text a human immediately via a separate path (notify.sh).Alerts used to pile up in queue files until a scheduled task swept them to Slack every 30 minutes. An alert you read half an hour late is an incident you watched on tape. The polling task is deleted; alerting is event-driven now (Jul 18, 2026).
The WatcherA status page plus a menu-bar light (SwiftBar) showing every Claude session on the machine: green when a session is actively writing files, gray when idle, red when the watcher itself goes stale — plus a live one-line "what is it doing" per session, read from the tail of its transcript. It observes from outside — file mtimes and logs, never self-reporting — and costs zero tokens to run.A session can be wrong about itself, and a hung one can't answer at all. Watching the chat tells you almost nothing; the disk doesn't lie (added Jul 19, 2026).
factcheck.mjsWeekly launchd job (Mondays) that asks the question no other gate can: is the site true? Renders each issuer's transfer-partner page in real Chrome via Playwright, snapshot-diffs it week-over-week, and runs internal data.js consistency and deadline checks. Flags changes for human review — never auto-corrects. A capability guard makes any run without a real browser a silent no-op, so it can't false-alarm. Findings flow out through the same direct-to-Slack path.Rule 45: the data was wrong in every currency while the machine was entirely green. Every guardrail verified the build; none could tell whether a word on the page was true. Strangers on Reddit found it first.

And one standing rule ties the layer together: monitors and scheduled tasks are created with completion notifications off by default. The system speaks only when something is actionable — a monitor that chirps on every success trains you to ignore the one message that matters.

08The stack, plainly

product

One static SPA (index.html + assets), no build step, Netlify auto-deploy from main. A PR gets a deploy preview.

backend

Supabase: auth + profiles, and one ask-ai edge function (7th revision) — receipts-first answers, grounded in the site's verified transfer data, metered with spend caps.

orchestration

Claude CLI headless (claude -p), briefs in markdown, launchd timers on a Mac mini. No framework, no queue service.

verification

Playwright at 390×844 / 768×1024 / 1440×900, signed-in session support, console-error and overflow probes, always with a control.

data honesty

Structured transfer ratios (never bare strings — a schema that can't say "unknown" invents 1:1), plus an automated weekly fact-check against issuer pages (see the inventory below).

observability

Event-driven alerts: alert.sh posts to Slack the instant anything fires — no polling, no sweep timer. A watcher in the Mac menu bar shows live session status. A chronicler writes the day's history; usage/budget probes; pr-log as JSONL.

09Steal it: the skills

Everything above is packaged as twelve installable skills — scripts and their tests, each test with a control run that proves it can fail:

$ npx skills add jeremyinthebay/relay-skills
two-claude-relay

The architecture: two agents, two files, one invariant.

autonomous-loop-safety

All 54 rules and the code behind each.

agent-preflight

Force every permission dialog while a human is still watching.

production-canary

Post-merge live probe with auto-revert.

destructive-git-hook

Refuse reset --hard & friends, even when instructed.

mobile-verification

The harness that isn't blind: real viewports, real taps, a control.

signed-in-web-verification

Authenticated Playwright checks with rotating session tokens.

parallel-build-serialized-merge

The throughput pattern from §06.

adversarial-audit

A second agent attacks the claim, not the code.

llm-feature-adversarial-audit

Same, specialized for AI-powered features.

alert-routing

Dedup'd, severity-routed alerts a human will actually read.

brand-logos-without-rehosting

Card art & program logos, no asset piracy.

GitHub · skills.sh — MIT, a few hundred lines of shell and two markdown files. Nothing to install server-side.