Home / Ops / More in this area

Affirmology - Automation Audit (what to build so we ship faster)

Updated Jul 11, 2026 · Affirmology_AutomationAudit_v1.md

Summary. Written 2026-07-11 after the Mirror-send scare. Ranked by what actually cost us time and risk.

Affirmology - Automation Audit (what to build so we ship faster)

Written 2026-07-11 after the Mirror-send scare. Ranked by what actually cost us time and risk.

The honest finding

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.


TIER 1 - built tonight

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.


TIER 2 - before August 13

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.


TIER 3 - the August 13 scale problem

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.


What NOT to build

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.

The principle worth keeping

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.