Home / Ops / More in this area
Updated Jul 11, 2026 · Affirmology_AutomationAudit_v1.md
Written 2026-07-11 after the Mirror-send scare. Ranked by what actually cost us time and risk.
There is no reusable tooling in this project. Zero. Every operation tonight was a one-off script
written from scratch: uploading to R2, hosting a preview, checking a job status, building a send list.
find turns up scratchpad/r2_sophia_upload.py and a pile of _compose_pdf.py-style one-offs. That is
the whole toolbox. Everything else lives in someone's head or gets re-derived at 4am.
That is not a tooling problem yet. It becomes one on August 13.
What it cost tonight, concretely:
- The council image looked un-updated because nobody cache-busted it. It was correct on the CDN the
whole time. Cost: a false alarm and a round trip.
- Code diagnosed a production backend bug that did not exist, because he queried a job_id that was not
real and got a hollow payload back. He was ready to patch the live Studio service at 5am to fix a
phantom. Cost: nearly an unnecessary production deploy on launch morning.
- The send list, the hold list, and the two email variants were assembled by hand across two chats,
three times, with corrections each round.
- Every deploy waits on Jeff to run npx wrangler deploy personally.
None of those are hard problems. They are all unautomated problems, which is worse, because they recur and they recur under time pressure.
1. ops/verify_send.py - THE SEND GATE.
Nothing goes to real people until this prints GO. Per person it hits /api/status/<job_id> using the
exact job_id that will be in that person's email link, confirms the promised asset is live on that
payload, cross-references .beta_invite_hold to assign variant A or B, and writes the per-person merge
rows. It calls each job multiple times so a single healthy worker cannot lie to you. It exits non-zero
if anyone fails, so a red gate cannot become a partial send.
It distinguishes the three failure modes that got confused tonight: not attached yet (wait), stale across workers (real backend bug), and unknown job_id returning a hollow payload (fix the link, NOT the backend). That last message exists specifically so nobody re-diagnoses tonight's phantom.
Already proved itself: it flagged that Mark Cleveland's job_id is not a valid demo job.
2. ops/r2put.py - CDN in one command.
Upload anything, get a clickable link. Handles ContentType so HTML previews render instead of
downloading, sets the right cache header, converts PNG to JPG at a chosen quality, and with --bust
bumps ?v=N on every reference across the site so a replaced image actually appears. The council-image
bug, solved permanently.
3. The mailer (ops/send_batch.py). The last manual link. Takes send_rows.json (the verified
output of the gate), picks variant A or B per person, merges the placeholders, sends via Resend with
the browser User-Agent header, throttles, BCCs Jeff, and logs every send back to Airtable. --dry-run
renders all of them to disk for eyeball review without sending. Right now this is a human copy-pasting
templates, which is exactly the step where a wrong link reaches a real person.
4. Deploy as one command. npx wrangler deploy is fine, but it should be wrapped: build, cache-bust
changed assets, deploy, then FETCH THE LIVE URL AND CONFIRM the change is actually there. We have twice
believed something shipped when it had not. A deploy that does not verify itself is a rumor.
5. Airtable state transitions, automatic. Stage, Beta committed, Mirror sent, Android flag: all of it
is currently written by hand or by a bespoke call. One ops/crm.py that every pipeline stage calls.
Jeff's recurring fear ("we can't lose this data like last time") is a direct symptom of not having this.
6. Audio QC as a gate, not a step. audio_qc.py exists and is good. It should be impossible to
publish an audio that has not passed it, rather than something a script remembers to call.
7. A boot check for every chat. Every chat, including this one, should read _CHAT_SYNC.md and
Affirmology_PROJECT_STATE.md before asserting anything about system state. I did not do that tonight
and told you renders were missing when they were mid-batch. Code did not do it either. Two chats
diagnosing the same system from stale assumptions at 5am is the actual root cause of tonight.
Meta ads, App Store, the email list, push notifications, Sol's IG and TikTok, YouTube to follow. Here is the thing that will hurt, and it is not any individual channel:
8. ONE IDENTITY SPINE. This is the big one.
Right now a person is a demo job_id here, an Airtable record there, an email address in Resend, a
Maya app account somewhere else, and soon a Meta click ID, a TikTok visitor, an App Store install, and a
push token. Those are six identities for one human, and nothing joins them. On August 13 that becomes:
you cannot tell which ad produced which install, which install produced which listen, or which listen
produced which subscription. You will be flying blind at exactly the moment the data starts mattering
and money starts moving.
Build the join now, while there are 29 people and it is easy. One canonical person ID, and every system carries it. This is a week of work in July and a nightmare in September.
9. Lifecycle sends fire on events, not on Jeff. Welcome, Mirror ready, new audio, re-engagement, beta nudge: every one of these is currently a human deciding to send. At 29 people that is charming. At 2,900 it is impossible. The send gate plus the mailer plus the identity spine is the foundation; the lifecycle engine sits on top and fires off state changes.
10. Attribution and a single funnel view. One table, one dashboard: impression, click, install, signup, first listen, commit, subscribe, by channel. Without it you will optimize ad spend on vibes.
11. Creative pipeline for Sol's organic. The ad-creative skill plus a shared asset library on R2
(which r2put.py already is). Sol should be able to pull an approved asset without asking anyone.
12. A weekly scheduled state-of-the-business report. Cheap to build, and it is how you notice a funnel step quietly breaking before it costs you a month.
Do not build a custom CRM. Airtable is fine and will stay fine well past launch. Do not build bespoke one-off dashboards. One funnel view, done properly, beats five pretty ones. Do not automate anything irreversible without a verification gate in front of it. That is the entire lesson of tonight, and it is worth more than any of the tools above.
Automate the CHECK before you automate the ACTION. Tonight the temptation was to schedule the send. The right move was to automate the verification and let a human pull the trigger. Once the check is trusted, the trigger can be automated safely, and not one minute before.