Built from SPEC.md v3.1 (2026-09-20). If this file and SPEC.md disagree, SPEC.md wins.
This is two SOPs in one file.
Part A is the machine's SOP: what the engine does on its own, every run.
Part B is your SOP: what to do on each exception, and only on an exception.
Part C is the daily and weekly rhythm.
Part D is the phase gates, as a checklist.
Part E is one table: what "done" means for every stage.
Anything marked Phase 0, in progress is not on disk yet. Another agent is building
those pieces right now under engine/core/. Everything else named here exists today,
at the path given.
Cadence. Runs twice a day, 07:15 and 15:15, under the launchd job
com.ralphwest.clipping.daily, which runs engine/daily.py. One full pass through
the 20 stages below. Always finishes. Always sleeps.
Heartbeat watch. A second launchd job, com.ralphwest.clipping.heartbeat,
runs engine/core/watchdog.py every 2 hours. It checks if controls/heartbeat
is older than 14 hours; if so, pages. This detects a stalled loop that silence alone
cannot page on.
Stage 0, Health, runs first, before Discover.
disk space. 3. Read controls/KILL and controls/TEST_MODE. 4. Write
a health record.
heartbeat_missing, since Sleep never gets reached either.
engine/daily.py has nopre-flight check today, it starts straight into polling.
Kill switch, controls/KILL. Present, nothing that touches money or a
platform runs. Discover, Score, Report still run. Remove it, everything resumes.
Test mode, controls/TEST_MODE. Present, publish and submit run the full
path and stop one call short of the real platform, writing what they would have
sent to dryrun/. **The engine ships in test mode and stays there until Phase
1's gate in Part D is met.**
Phase 0, in progress. Neither file exists yet, nor does controls/ itself.
In practice the engine is already test-mode-only today, since Stage 12 has no
working TikTok adapter. Nothing can go out regardless of any flag.
marketplaces.json by its priority field. 2. Pollevery non-manual marketplace via engine/campaign_poll.py and
engine/marketplaces.py. 3. Parse raw campaigns (Whop, Vyro, LiquidClips).
last_seen. 5. Log the count to logs/daily.log.campaigns.json shows updated last_seen for every pollablemarketplace this run.
unknown after 5 tries.
engine/daily.py wraps discover-and-score in one try/except today.
ended after 3missed polls. 3. Update pool and spend deltas. 4. Recompute burn rate
and runway (already built, engine/campaigns.py).
burn_rate_per_day, runway_days, last_seen are current.Burn rate and runway already compute in engine/campaigns.py.
approved_sources[].needs_human: rules_doc.approved_sources[] populated, terms captured or marked unknown.approved_sources[] today.
floor, rating floor. 2. Compute the seven weighted factors. 3. Call
Gemini flash-lite once per campaign, cached forever. 4. Write score,
score_parts{}, eligible, reject[].
score_parts, a score,and eligible with a reason when false.
engine/campaigns.py and campaign_tracker.py (repo root).engine/selection.py. 2. Pick 3 to 5, atmost 2 per marketplace, skip reliability: bad. 3. Check each pick
has a warm account in its lane, else queue it behind one. 4. Revert any
dropped pick to watching.
campaigns.json shows 3 to 5 selected/active campaigns,each with an account assigned.
needs_human: account:.engine/selection.py. Lane-to-account assignment is an open item.approved_sources[]. 2. Check the URL is literallyon that list before any call, refuse otherwise. 3. Download via
engine/ingest.py (yt-dlp, captions first, Whisper fallback). 4. Record a checksum.
jobs// , checksum recorded, URL provablyin approved_sources[].
engine/ingest.py. URL validation against approved_sources[] is an open item; legacy watch_sources.py path must migrate to this gate before Phase 1.engine/pick.py, killing sponsor reads and intros. 3. Score each on hook,
payoff, emotion, quotability, one Gemini call. 4. Store timestamped
moments with scores and a prompt version.
unknown if there is no audio at all.
engine/pick.py. Prompt-version stamping into prompts/ is an open item.engine/produce.py, reframe tolay the hook card over the first 2.6s. 3. Render N variants via
engine/variants.py. 4. Write the mp4 files under jobs/.
engine/produce.py and engine/variants.py. Disk-free gate and campaign duration-range validation are open items.zone, never over source text. 3. Check a hook in the first 3s.
hash already on that platform.
qa_passed with every check recorded, or qa_failednaming the failure.
qa_failed, never auto-requeued.qa_failed is terminal, not a retry.Schedule today. Close this gap before Phase 1.
accounts.json via engine/schedule.py.warm-up stage, minimum gap, no repeated variant on one platform.
queue.json.queue.json shows every asset queued with a slot, or unplacedwith a reason.
queued orunplaced rows.
engine/schedule.py. QA gate integration is pending (QA module does not exist yet).controls/KILL is absent. 3. Check the account and campaign are active.
controls/TEST_MODE.due_now.json lists every slot that passed all 5 checks.engine/publish.py finds due, queued slotsand writes due_now.json. Phase 0, in progress: none of the 5 checks run
yet, the controls don't exist on disk.
PublishAdapter.publish(clip, account). 2. Markthe key complete, record the post id or URL. 3. Open publish_failed
with retry_after if the adapter cannot run.
exists (tested 2026-09-19).
engine/publish.py never calls a publish adapter. It reportswhat is due. A human, or a Claude session with the Higgsfield MCP tools,
posts it.
present, view count reads. 3. Retry hourly for 24h if not confirmed
in 30 min. 4. Escalate after 24h.
confirmed: true with a readable view count.MarketplaceAdapter.submit. 2. Storea submission id or screenshot. 3. Open an exception with the URL and
campaign if the adapter cannot submit.
engine/core/adapters/whop.py. Submit capability is pending, requires browser automation with persisted login.PublishAdapter.stats(post_id). 2. Record theviews delta to metrics.json. 3. Skip, don't fail, if the adapter is down.
metrics.json shows updated views and checked for the post.engine/metrics.py has update() to record a pull. Automatic stats adapter calls and live tracking are pending items.views x cpm_net. 3. Pull the actual bank or wallet change. 4. Compare
all three, open an exception on any mismatch over 5%.
reconcile_mismatch. See Part B.metrics.json's schemais missing campaign_id, submitted, approved, paid.
reliability (3 late payouts flips a marketplace to bad). 3. Re-run
Stage 5. 4. Shift experiment allocation toward the winning arm, 20% floor.
reliability updated, the historical-performance term changed, anew selection run logged.
engine/metrics.py's summary() computes a basicscore-vs-views correlation today. Phase 0, in progress: the full feedback
loop back into Score and Allocate.
jobs/_src/. 3. Rotate logs/daily.log.briefs/YYYY-MM-DD.md, opening with, in order: moneyearned yesterday and to date, open escalations, posts
published/confirmed/submitted, active campaigns and their runway, heartbeat
time. 2. Send the heartbeat line to Telegram. 3. Put the exception
count and money numbers in the first five lines.
briefs/, first 5 lines match that order.repeats.
engine/daily.py writes the brief every run.Phase 0, in progress: today's opening lines are new-sources, assets-ready,
scheduled, due-today, not the five spec'd lines. Telegram delivery is not wired.
controls/heartbeat with this run's timestamp.controls/heartbeat's timestamp matches this run's completion.and a separate watcher pages after 1.5 intervals. See heartbeat_missing
in Part B.
controls/heartbeat does not exist yet. The last=== daily run end === line in logs/daily.log is the closest thing today.
Only ever act here on an open exception. Everything else runs unattended.
Mission Control card under "What needs a human."
or Gaming. Phone and a human required, no way around it. 2. Register:
python3 -c "import sys;sys.path.insert(0,'engine');import accounts;accounts.add('.
Mission Control card with campaign and marketplace.
rules or allowed-content document URL. 3. Paste it into the exception in
Mission Control, or tell Clara.
Control card listing each post: account, platform, file, hook.
due." It uses the Higgsfield MCP tools to post each queued clip.
back into the queue row.
Control card with the post URL and campaign.
into its submit form. 3. Mark it done in Mission Control, or tell Clara.
submitted field.estimate." Mission Control Money card, stated days vs elapsed.
reliability to bad automatically. Chasing it yourself is optional.
bank $Z." Mission Control shows all three on the campaign's Money card.
wallet or bank deposit. 3. Tell Clara which number is wrong, and why.
which to trust.
own median for 2 days." Mission Control greys out the account.
own. 2. If it doesn't recover, rebrand it to a different campaign in the
same lane: accounts.rebrand(handle, platform, new_campaign_id).
it under The Engine.
Discovery, tracking, reporting keep running. To spend more, raise the number
in controls/limits.json.
from a separate watcher, since silence can't page on its own.
launchctl list | grep clipping.tail -30 logs/daily.log. 3. Check today'sbrief: cat briefs/$(date +%F).md. 4. Reload the plist if unloaded. If
ffmpeg, node, or yt-dlp is missing from PATH, read SYSTEM.md's PATH note
first, the plist PATH is pinned on purpose.
controls/heartbeat updates.touch controls/KILL. Every stage with a side effectstops. Discover, Score, Report keep running.
rm controls/KILL.rm controls/TEST_MODE, only once the Phase 1 gatebelow is met, never before. 1. 3 consecutive clean sandbox days, zero
unknown-class exceptions. 2. At least one account past its cooldown.
| # | Line | What it means | What you do |
|---|
|---|---|---|---|
| 1 | Money earned yesterday and to date | The only number that matters, long run | Nothing, if it moves up. Flat for days, check Stage 17's reliability flags |
|---|---|---|---|
| 2 | Open escalations | Count of unresolved exceptions | Zero, nothing to do. Above zero, go to Part B for the type named |
| 3 | Posts published / confirmed / submitted | Today's throughput | A gap between published and confirmed over a day flags a Stage 13 problem |
| 4 | Active campaigns and runway | The 3 to 5 selected, days of pool life left | Under 5 days, it parks itself. No action needed |
| 5 | Heartbeat time | Proof the loop ran | Not today's date, see `heartbeat_missing` in Part B |
Every week, Stage 17 recomputes reliability flags, refreshes the
historical-performance term, re-runs Stage 5's selector, and shifts experiment
allocation toward whichever hook style, caption style, or post time is
winning. Nothing is required of you. Worth a glance at Mission Control's
Strategy tab to see which arm won and why.
The nightly backup is tarred, kept 30 days, one copy off the machine. A
restore is tested once a month and logged. Nothing required, unless the log
shows a failed or skipped test, which is itself an exception to work.
Nothing skips a phase. A phase that regresses, an unknown exception, a
reconciliation miss, a paused account, drops back one phase until clean again.
allocate, ingest, analyze, render, QA, schedule, dry-run publish and submit,
track, reconcile on dry data, report.
Evidence: dryrun/ has an entry for every would-be publish and submit.
controls/heartbeat updates twice a day. A restore-test log exists. Mission
Control requires a login.
Gate to enter:
unknown-class exceptions.What runs: one account, one campaign, one post a day, live publish via the
session path, submit via browser adapter or exception.
Stays human: joining campaigns. Publishing, if the headless-session test fails.
Evidence: audit/YYYY-MM-DD.jsonl shows 3 days with no unknown rows.
accounts.json shows an account aged 3 days or more. A campaign record shows
status: active.
Gate to enter:
What runs: both accounts, 3 campaigns, allowance-driven cadence,
experiments on.
Stays human: account creation, escalations only.
Evidence: a reconciliation record on disk with all three numbers, delta under 5%.
Gate to enter:
What runs: add accounts and campaigns. Throughput follows the capacity
formula, the sum of each account's allowance for its warm-up stage.
Stays human: account creation, escalations only.
Evidence: 14 days of reconcile records on disk. Exception-to-post ratio
computed and under 5%.
| # | Stage | Done means, on disk |
|---|
|---|---|---|
| 0 | Health | Health record written, every dependency checked, kill switch state read |
|---|---|---|
| 1 | Discover | `last_seen` updated for every pollable marketplace this run |
| 2 | Sync | `burn_rate_per_day`, `runway_days`, `last_seen` current on every campaign |
| 3 | Verify | `approved_sources[]` populated, payout terms captured or marked unknown |
| 4 | Score | All 7 `score_parts` present, `score` and `eligible` written |
| 5 | Allocate | 3 to 5 campaigns `selected` or `active`, each with an assigned account |
| 6 | Ingest | File on disk, checksum recorded, URL provably in `approved_sources[]` |
| 7 | Analyze | Timestamped moments with scores stored in the job's metadata |
| 8 | Render | mp4 exists, probe shows 1080x1920, duration in range, audio present |
| 9 | QA | Clip marked `qa_passed` with every check recorded, or `qa_failed` with the reason |
| 10 | Schedule | `queue.json` shows every asset `queued` with a slot, or `unplaced` with a reason |
| 11 | Pre-publish | Idempotency key written, all 4 gate checks passed, in `due_now.json` |
| 12 | Publish | Post id or URL recorded against the clip |
| 13 | Confirm | Post record shows `confirmed: true` with a readable view count |
| 14 | Submit | URL on file with the campaign, submission id or screenshot stored |
| 15 | Track | `metrics.json` shows updated `views` and `checked` for the post |
| 16 | Reconcile | Three numbers agree within 5%, or an open exception names the mismatch |
| 17 | Learn | `reliability` flags updated, new selection run logged |
| 18 | Cleanup | No source or clip file older than its retention window remains |
| 19 | Report | Today's brief exists, first 5 lines in the required order |
| 20 | Sleep | `controls/heartbeat` timestamp matches this run's completion |