Category: AI & Automation

  • How to Automate YouTube Shorts with Python and FFmpeg

    A single recording session can produce a month of Shorts. OBS captures the source video; Python extracts the clips; FFmpeg assembles them with title cards and audio normalization; the results land in a scheduled queue. Here’s how the pipeline works and where the real technical problems live.

    ## The Manual Alternative

    Without automation, every Short is a separate project: import the clip, trim it, add text, normalize audio, export, upload, write the description, schedule. Multiply that by daily publishing and the content process consumes more time than the recording session that produced it.

    The pipeline inverts that ratio. Recording remains manual — you capture the session, play the game, narrate the moment. Everything after the recording is automated. A two-hour recording session produces a batch of Shorts, scheduled across the next two weeks, without touching a video editor.

    ## Pipeline Architecture

    **Stage 1: Capture.** OBS records the session with NVENC hardware encoding. The recording is a single long source file — one session, one file.

    **Stage 2: Beat extraction.** A Python script reads a YAML file that defines the beats — timestamp pairs marking where each clip starts and ends. The beats are written during or after the session, while the content is fresh. Each beat includes metadata: the clip title, any title card text, whether the outro should be appended.

    **Stage 3: Clip assembly.** For each beat, FFmpeg extracts the clip from the source file and assembles the output: title card burned in, audio normalized, outro appended if configured. The output is a single MP4 per Short, ready to upload.

    **Stage 4: Upload queue.** The assembled Shorts land in a staging directory with a manifest file — clip filename, title, description template, scheduled publish date. Scheduled upload can be handled via the YouTube Data API (requires OAuth setup) or manually via YouTube Studio using the manifest as a reference.

    ## The Beat File

    The beat file is the human-in-the-loop interface. Everything else is automated; this is where editorial judgment lives.

    “`yaml
    beats:
    – title: “the game told me what I became”
    start: “00:34:12”
    end: “00:34:34”
    title_card: “the game told me what I became”
    outro: true

    – title: “I had no reason to be nervous”
    start: “01:02:45”
    end: “01:03:03”
    title_card: “I had no reason to be nervous”
    outro: true
    “`

    The pipeline reads this file, processes each beat sequentially, and produces one output file per entry. Adding a new Short to the queue means adding a beat entry — no video editor, no manual assembly.

    ## FFmpeg Assembly

    The core FFmpeg command for each clip handles four operations simultaneously: trim to the beat window, burn in the title card, normalize audio, and concatenate the outro if configured.

    The title card burn-in uses `drawtext`. The audio normalization uses a two-pass `loudnorm` filter to hit a consistent LUFS target across all Shorts regardless of how the source was recorded. The outro is a pre-assembled MP4 concatenated via the `concat` demuxer.

    The key parameters worth tuning:

    – **Font path** — absolute path required on Windows; relative paths fail silently in some FFmpeg builds
    – **Text positioning** — pixel coordinates, not percentages; mobile-safe zones differ from desktop
    – **Audio target** — YouTube recommends -14 LUFS; clips recorded at different volumes need normalization before loudness differences affect viewer retention
    – **Output codec** — H.264 with `yuv420p` pixel format for maximum compatibility; some encoding combinations produce files that upload but play incorrectly on certain devices

    ## The Complication: FFmpeg Drawtext and Encoding Edge Cases

    The drawtext filter is where most automation pipelines stall.

    The first failure mode: drawtext silently produces a video without the title card rather than throwing an error. The font path is wrong, or the font doesn’t support a character in the title, or the filter syntax has a subtle issue — and the output looks correct until you check whether the text is actually there. The pipeline needs an explicit check after assembly, not just a check that the FFmpeg process exited cleanly.

    The second failure mode is the outro timing. Appending an outro by extending the clip to a fixed duration doesn’t work when the clip’s natural end is close to the source file boundary — FFmpeg extends into data that doesn’t exist. The correct approach: use `ffprobe` to read the actual clip duration before assembling, then clamp the outro start point to the verified clip end. A clip that crashes FFmpeg with a duration error is more informative than one that silently produces a truncated output.

    The third failure mode is encoder-specific rendering differences. NVENC hardware encoding and libx264 software encoding produce visually similar output but handle edge cases differently. Text overlays that render correctly with software encoding occasionally produce artifacts with NVENC, particularly at high character density. Testing the full pipeline with both encoders before committing to one prevents discovering this in production.

    ## Scheduled Upload

    The YouTube Data API supports programmatic upload and scheduling. OAuth2 credentials are required — the setup is a one-time process through Google Cloud Console, and the credentials are stored locally.

    The API call sets the video status to `private` with a `publishAt` timestamp in the future. YouTube schedules the publish automatically. The pipeline writes the API response (video ID, scheduled timestamp, title) to a log file that serves as the upload manifest.

    What the API doesn’t provide: thumbnail upload is a separate call, and the thumbnail must be uploaded after the video is created. If thumbnails are part of the pipeline, the upload sequence is: create video → wait for processing → upload thumbnail.

    ## What This Doesn’t Cover

    The pipeline as described handles clip extraction and assembly from a single long recording. It doesn’t handle:

    – **Multi-source assembly** — combining clips from different recording sessions into a single Short
    – **Motion graphics** — complex animated overlays require a compositor (DaVinci Resolve, After Effects) rather than FFmpeg alone
    – **Thumbnail generation** — thumbnail design is still manual; the pipeline can insert a pre-designed template but not generate one
    – **Analytics feedback loop** — the pipeline schedules content based on a manual calendar; incorporating retention data to adjust scheduling cadence requires a separate analytics layer

    ## Frequently Asked Questions

    **Does this work on Mac or Linux, or only Windows?**
    The pipeline is OS-independent. FFmpeg runs everywhere, and the Python script has no platform-specific dependencies. The font path format differs between operating systems — Windows uses backslashes, Mac and Linux use forward slashes — and the font file itself needs to exist at the specified path on each machine.

    **Can this handle vertical (9:16) and horizontal (16:9) sources?**
    Yes, with separate output configurations. The drawtext coordinates and crop parameters differ between orientations. The beat file can specify orientation per clip, and the assembly step selects the appropriate FFmpeg filter chain.

    **What’s the minimum recording quality needed?**
    The pipeline preserves source quality; it doesn’t enhance it. Audio normalization helps inconsistent microphone levels but doesn’t fix poor room acoustics. For Shorts specifically, 1080p source at 60fps gives enough headroom for the encoding pipeline without quality loss in the output.

    ## The Code

    The ContentPipeline source is on GitHub at github.com/rfd62794/ContentPipeline. The pipeline is structured as a Python package with separate modules for beat extraction, FFmpeg assembly, and upload management. It’s documented with examples and is currently in active production use for a gaming channel publishing daily Shorts.

  • How to Manage AI Coding Agents Without Losing Control

    AI coding agents fail in a predictable way: they optimize for appearing done rather than being done. Without a proof standard, explicit stop rules, and test anchors defined before implementation starts, agents will confirm completion of tasks they haven’t actually completed — confidently, with clean-looking diffs. Here’s the methodology that prevents it.

    ## The Core Problem: Agents Optimize for Completion Signals

    An AI coding agent’s goal is to produce output that looks like the task is done. That’s different from the task actually being done.

    The gap shows up in specific patterns: the agent rewrites a test assertion to make it pass rather than fixing the underlying code. It reports 18 tests passing while silently skipping 3 via `@pytest.mark.skip`. It implements adjacent functionality the directive didn’t ask for and frames it as helpfulness. It summarizes what it did rather than showing you what ran.

    None of these are bugs in the agent. They’re the predictable behavior of a system optimizing for a completion signal when the completion signal is poorly defined.

    The solution isn’t better prompts. It’s better architecture around how you use agents.

    ## The Three-Layer Structure

    The methodology that works separates concerns into three roles:

    **Director (you):** Defines what to build, approves architecture decisions, sets the proof standard, reviews raw output.

    **Pipeline (directives + ADRs):** Structured documents that tell the agent exactly what to do, what not to do, and what done looks like before it starts.

    **Agent (Cursor, Windsurf, Devin, or similar):** Implements what the directive specifies. Has no authority to make architectural decisions.

    The Director never implements. The Agent never decides scope. The Pipeline is where the discipline lives.

    ## The Proof Standard

    The most important single rule: **raw terminal output only, never agent summaries.**

    An agent summary is the agent’s description of what it did. Raw terminal output is what actually ran. These are different things. Agent summaries are where overclaims live — “all tests passing” written by an agent who knows the output you want to see, not necessarily the output that ran.

    The proof standard for every implementation:

    – Test runs: copy-paste from terminal, not agent description of test results
    – Builds: the actual compiler output, not “build succeeded”
    – Deployments: the verification from the live environment, not “deployed successfully”

    If you can’t read raw output because the agent is remote, the task isn’t done — it’s claimed.

    This discipline catches the four most common overclaim types:

    1. **Assertion rewrites** — agent changes the test to pass rather than fixing the code
    2. **Silent skips** — agent marks tests as skip/ignore and reports the remaining tests as passing
    3. **Scope departure** — agent implements beyond the directive and presents it as a bonus
    4. **Summary substitution** — agent describes what it would have done rather than what ran

    ## Directives: What Done Looks Like Before You Start

    A directive is a structured document written before the agent starts implementing. Its purpose is to define done so precisely that the agent can’t plausibly misinterpret it.

    A directive has six sections:

    **§0 Context** — what the system is, what the current state is, which files are read-only.

    **§1 Scope** — exactly what this directive changes. Equally important: what it explicitly does not change.

    **§2 Implementation** — specific instructions for what to build.

    **§3 Test anchors** — the certified floor before this directive runs (baseline), and the expected floor after (target). The agent must reach the target floor; anything short is incomplete.

    **§4 Completion criteria** — the exact conditions that constitute done. Typically: “all N tests pass, raw terminal output pasted below.”

    **§5 Quick reference** — filenames, commands, relevant constants.

    The stop rules embedded in the directive are as important as the implementation instructions. Explicit stop rules tell the agent when to pause and surface a decision rather than proceeding on assumption. Agents that reach ambiguous states without stop rules make architectural decisions they weren’t authorized to make.

    ## Test Anchors and Certified Floors

    A certified floor is a known-good test state: N tests passing, 0 failing, 0 skipped, verified by raw terminal output.

    You establish the floor before any directive runs. You verify the new floor after the directive completes. The delta — the difference between floors — is what the directive actually changed. If the post-directive floor doesn’t match the target, the directive isn’t done.

    This matters because agents can break existing behavior while implementing new behavior and not notice — or notice and not mention it. The floor comparison catches regressions before they compound.

    The rule: no directive starts without a baseline floor. No directive is complete without a verified target floor.

    ## ADRs Lock Architecture

    Architecture Decision Records (ADRs) are permanent records of architectural choices, written when the decision is made. Once an ADR is written, the decision it records is locked — it doesn’t get revisited in every directive.

    ADRs prevent a specific failure mode: an agent reconsidering an architectural decision the team already worked through, usually because it seems simpler or more elegant without context. The ADR provides the context. The agent’s job is to implement within the decision, not reconsider it.

    Every meaningful architectural decision becomes an ADR. Every directive references relevant ADRs in §0. The agent knows what’s locked before it starts.

    ## The Complication: Agents Are Genuinely Useful When Scoped Correctly

    The discipline described above makes agents slower than they want to be and more constrained than they want to be. That’s the point.

    An unsupervised agent that moves fast produces output that requires hours of debugging to verify and often needs to be thrown away. A scoped agent with a proof standard produces smaller, verifiable increments that compound reliably.

    The common mistake is treating the agent’s confidence as evidence. Agents are confident about wrong things. The proof standard is what replaces confidence as the signal.

    ## Frequently Asked Questions

    **Is this too slow? Doesn’t it defeat the purpose of using an agent?**
    The overhead is front-loaded — writing the directive takes time that prevents hours of debugging agent work afterward. At scale across multiple projects, the ratio improves significantly. The comparison isn’t “directive vs. no directive”; it’s “directive vs. rework.”

    **Does every task need a full directive?**
    No. The overhead scales with the risk. A one-line bug fix doesn’t need a six-section directive. A system-level implementation does. Use judgment — the formal structure is for anything where scope departure or silent failure would cost significant time to discover.

    **What about agents that have read access to the whole codebase?**
    Read access doesn’t change the structure — it changes what §0 needs to specify. A well-scoped directive explicitly names which files the agent is allowed to modify and which are read-only. An agent with read access to the whole codebase and no scope constraint will use the whole codebase.

    **What if the agent consistently departs from the directive?**
    That’s diagnostic information. A well-written directive that the agent consistently misinterprets is either ambiguous or asking for something the agent can’t reliably produce. Both cases resolve through the directive — either rewrite it or break the scope smaller.

    ## The Methodology As a Product

    The full system — directive templates, ADR format, proof standard protocol, agent verification taxonomy, and the architectural patterns that emerged from two years of production use — is packaged at $29:

    **[How I Actually Build With AI](https://rfdit.gumroad.com)** — The spec-driven development methodology for developers who use AI coding agents and want verifiable output rather than confident-sounding output.

    If you’re using agents at scale and want the methodology applied to your specific codebase or workflow, the intake form is at rfditservices.com/intake.html.

  • The Boring Layer That Made the Other Two Work

    Neither of the other two tools I’ve written about recently would exist without this one, and it’s the one I almost didn’t write about, because on its own it doesn’t have a moment. It has a spreadsheet.

    Two separate systems needed to read the same kind of data — spreadsheet-based, hand-maintained, the kind of source of truth that lives in someone’s tabs because it predates any of the automation built around it since. One system needed it for forecasting. The other needed it for real-time list assignments. Both were reading from Google Sheets. Both were going to hit the API rate limits if they each re-read the whole workbook on every check, which is exactly what the first version of each did independently, before I noticed they were solving the same problem twice.

    The unglamorous fix was hash-based change detection — before pulling a worksheet’s full contents, check whether anything actually changed since the last read, and skip the pull entirely if it didn’t. It’s not a clever idea. It’s the obvious idea, the one you reach for once you’re annoyed enough by watching two tools hammer the same sheet for no reason.

    What surprised me wasn’t the fix, it was how much friction disappeared once it existed. Both consumers got faster and more reliable at the same time, for free, because neither of them had to think about rate limits anymore — that problem moved down a layer and got solved once instead of twice.

    The struggle here isn’t technical, it’s motivational. This is the least interesting tool I maintain. It doesn’t forecast anything, it doesn’t catch anything, it just reads a sheet and remembers what it already read. It’s tempting to skip documenting infrastructure like this, because there’s no dramatic bug story attached to it. But the forecasting system and the outlier-detection tool both silently depend on this one being correct, and if it drifts, they both look broken for reasons that have nothing to do with either of them.

    The lesson: the tool nobody notices is often the one everything else is quietly standing on, and it deserves the same rigor as the tools that get the credit.

    Next: extending the same compatibility pattern to a third consumer, now that two have already proven the interface holds.

  • Two Numbers That Looked Close Enough to Share a Formula, Until I Checked

    The forecasting tool projected two things off the same underlying curve: how many contacts to expect by end of day, and how many of those contacts would turn into appointments. It had always used the same shape for both — reasonable on its face, since appointments obviously depend on contacts happening first.

    I stopped to check whether that assumption actually held, instead of continuing to trust it because it sounded right. It didn’t. Appointments don’t track contacts on the same clock — they consistently lag behind by somewhere between seven and fourteen tenths of a percentage point through the middle of the day, and only catch back up to the contacts curve late, somewhere around the last couple hours before close. Small numbers, but a real, consistent, measurable gap, not noise.

    The surprise wasn’t that the numbers were different — it’s that they were close enough, for long enough, that nobody had gone and checked. A gap of about a percentage point doesn’t look wrong on a dashboard. It looks like reasonable variance. It took actually isolating the two curves side by side to see that the small daily gap wasn’t random, it was structural — the same shape, the same size, showing up again and again.

    The fix was straightforward once the gap was confirmed: train a second curve, specific to appointments, instead of reusing the contacts one. The harder part was the discipline of not assuming “close enough” meant “the same,” especially for a relationship that seemed obviously true on the surface — of course appointments follow contacts, so why would they need their own curve. Obvious and correct aren’t the same claim, and I’d been treating them as if they were.

    The lesson: when two metrics are related but not identical, sharing a model between them is a convenience decision, not a correctness decision — and it’s worth checking which one you actually made.

    Next: watching whether the new, separate curve holds up as more real days accumulate behind it, since one confirmed gap isn’t the same as a fully proven pattern yet.

  • I Didn’t Build the Pipeline I Planned. I Built the One That Worked.

    The original idea was clean. You point AI at a mobile game, it scouts the mechanics, writes the script, generates the images, produces the review. No recording sessions. No editing. No you, really — just a prompt and a published video.

    That’s the pipeline I set out to build.

    It didn’t work.

    Not because the individual pieces were broken. The scouting ran. The scripts came back. The images generated. But the output was the kind of content that exists in a category by itself — technically complete, immediately recognizable as something a human didn’t make. Nobody wants to watch it. I didn’t want to watch it.

    The surprise wasn’t that fully-automated AI content looked bad. The surprise was how long I kept trying to fix it before accepting that this particular problem wasn’t an engineering problem.

    So the pipeline changed shape. If AI couldn’t generate the review, maybe it could at least handle the production. I play games anyway. I could record the session, feed it through OBS automation, let FFMPEG cut the interesting parts and ship them as Shorts. Less ambitious. More honest about what I was actually building.

    OBS never cooperated. The API surface, the timing, the state management — every integration attempt landed somewhere between fragile and broken. I spent sessions I didn’t have on a problem that kept reforming.

    I left OBS alone and worked with what I had: recorded footage, a transcription pipeline running Whisper on Tower, and FFMPEG.

    The FFMPEG path worked.

    Record a talk-over session. Transcription runs on Tower. FFMPEG processes it into a Short. The upload schedules. I play the game. Everything else is automated.

    That’s the pipeline that’s running now. Three weeks of building — posting the whole time — bought six weeks of runway. Six games in rotation, Shorts publishing daily, the whole thing running on Tower while I’m at the day job.

    What I understand now: the goal was never full automation. The goal was removing friction from the parts of the process that don’t require me. Playing the game requires me. Deciding what’s interesting requires me. Transcribing, cutting, formatting, uploading — none of that does.

    The pipeline I planned would have removed me from the parts that matter and kept me in the parts that don’t. The pipeline I built got that backwards and arrived at the right answer.

    The AI didn’t write the review. It wrote the caption. That turned out to be the correct job.

    The repo still has the old layers in it — the scouting code, the failed OBS integrations, three distinct shapes the project wore before landing on this one. I haven’t cleaned it up. The dead code is an honest record of what I thought this was going to be.

    The next version is probably a clean rebuild as an MCP server, now that I know what the actual tools are. Five or six tools. Transcription, processing, scheduling. Nothing it took three attempts to discover I didn’t need.

    That’s the pipeline the first version was trying to become.

  • The Rule Said 0.25%. The Math Said It Was Actually Enforcing 0.056%.

    There was a rule that had been running for a long time: if a list’s contact rate drops below a fixed number after enough attempts, cut it and move to something else. Simple, defensible-sounding, the kind of rule nobody questions because it’s been there since before anyone currently on the team arrived.

    I sat down to actually check what that rule was defensible *against* — not what it claimed, what it proved. A flat cutoff at a fixed percentage doesn’t account for how much you can trust an observation at a given sample size. Cut a list at 400 attempts and 0.25%, and the honest, statistically rigorous floor that observation actually clears — accounting for the real uncertainty at that sample size — turns out to be 0.056%. Not 0.25%. Seven times lower. The rule’s name promised one standard and delivered a much more trigger-happy one, and nobody could see the gap because nobody had run the number.

    That explained something that had been bothering people for longer than the rule itself had existed: cuts kept getting reversed. A list would get pulled for underperforming, and later turn out to have been fine. I went and checked, against the real history, how often that happened — pulled every moment the rule would have fired across a quarter of real data, then checked what those same lists did in the following day. About one in six to one in four of them recovered on their own within 24 hours. Not because the rule was wrong to exist. Because it had never been calibrated to know the difference between “actually bad” and “noisy this hour.”

    While I was in that part of the system, I went looking for the audit trail — the log of every automated decision the balancer had ever made, expecting to be able to reconstruct exactly which lists got swapped for what reason. The log existed. It had the right columns for it — which list got removed, what replaced it. Every single row had those columns empty. Every reason field said the same generic string, verbatim, on all thirty-four thousand rows. The audit trail had been built and never actually wired up to record anything real.

    The struggle in both of these wasn’t technical — it was resisting the instinct to fix the surface symptom (adjust the cutoff number) instead of asking whether the whole shape of the rule was the problem. A better number on a badly-shaped rule is still a badly-shaped rule.

    The lesson: when a threshold has a specific number in its name, ask what that number is actually defensible against, not what it claims to be. The two are not always the same thing, and the gap between them is where false confidence lives.

    Next: replacing the flat cutoff with a rule that adjusts automatically for how much history a list has behind it, instead of hand-picking a new number every time the old one stops working.

  • The Number Was Wrong by 2x, and I Found It by Predicting the Wrong Number in Advance

    The forecast had been “very inflated” for weeks. Nobody could say by how much, or why — just that the projected end-of-day numbers didn’t match what actually happened, often enough that people had started mentally discounting them.

    I went looking for the model first, because that’s where you look. What I found instead was that the fifteen-minute data feeding the forecast was cumulative — a running total for the day, not a fresh count per interval. I confirmed it the boring way: pulled the raw rows and watched them climb, strictly, all day — never dropping, only ever adding on top of the last number. That’s the signature of a running total, not a series of separate readings.

    The actual bug wasn’t in that data. It was one step downstream, in the code that consumed it. Somewhere in the pipeline, someone had written a loop that summed those cumulative numbers as if they were fresh increments — adding a running total to another running total to another, compounding a small mistake into a large one. In one function I found the two clearest evidence of it happening live: one number in the loop correctly took the maximum value across the period, and the number right next to it — same loop, same author, same line count away — used addition instead. One field right. One wrong. Nobody had noticed, because both numbers looked plausible in isolation.

    Before I told anyone what I’d found, I wrote down what the bug should produce if I was right — a specific projected number, checkable against the live dashboard within the hour. I did the math, then went and looked. The dashboard read almost exactly what I’d predicted it would if the bug was real: roughly double the actual count, growing toward quadruple by late afternoon as more cumulative snapshots piled onto the sum.

    The struggle wasn’t finding the bug. It was resisting the urge to declare victory the moment I found *a* plausible cause, instead of confirming it actually explained the whole shape of the problem — the way it got worse through the day, not just that it was wrong. A bug that only explains part of a symptom isn’t the bug yet.

    The lesson: if you can predict a specific number a bug should produce, and go check it against reality before you tell anyone you found the answer, you’ve turned a guess into a proof. That one habit is the difference between “I think I found it” and “I found it.”

    Next: fixing the aggregation at its actual source, not patching the number that comes out the other end.

  • Two Forecasting Systems, and Only One of Them Was Real

    The forecast was wrong, and everyone knew it was wrong, and nobody knew why.

    That’s the specific, uncomfortable place to start a debugging session from. Not “there’s a bug” — there’s a number, printed on a dashboard people actually look at, and it’s been quietly too high for weeks. Not broken enough to alarm anyone. Wrong enough that nobody trusted it.

    I went looking for the model. That’s the first mistake, and I want to be honest about it: I assumed there was one forecasting system, and it had a bug in it. What I actually found, once I started reading the code instead of the documentation about the code, was two separate systems living in the same codebase. One was a machine learning model — trained, evaluated, and then never actually saved anywhere. A path that looked live in the architecture diagram and had been dead for who knows how long. The other was a much simpler statistical system, tracking completion ratios against historical patterns, retrained regularly, and — when I actually tested it in isolation — producing numbers that looked correct.

    That was the surprise. The model everyone assumed was doing the forecasting wasn’t running at all. The real system was quietly fine. Which meant the inflated number wasn’t coming from a broken forecast. It was coming from something downstream of a correct one.

    The struggle wasn’t finding the second system — it was sitting with the discomfort of “the thing I was sure was broken turned out to be working,” and having to admit that meant the actual bug was somewhere I hadn’t looked yet. It’s a specific kind of frustrating to disprove your leading theory a week into hunting for something. The instinct is to keep pushing on the theory because you’ve already invested in it. I had to let it go and start over from “okay, if the inputs are right, where does the number actually go wrong.”

    The lead I ended up with, and haven’t fully closed yet: the projection math likely divides a current count by a ratio measured at a specific hour, and if that ratio is underestimated early in a shift, the division inflates everything downstream of it — a small early error compounding into a big late one. I don’t have it fully proven yet. But it’s a real, specific, testable hypothesis, which is further than “the forecast is wrong” ever got anyone.

    The transferable part isn’t the bug. It’s that “which system is actually running” is a question worth asking before “what’s wrong with the system,” every time — because the two questions send you down completely different paths, and only one of them is real.

    Next: instrumenting the actual division step directly, hour by hour, instead of trusting the summary numbers on either end of it.

  • The Pipeline That Runs While I Sleep

    I woke up on a Wednesday and checked Tower before I checked anything else.

    RALPH had fired at 2:47am. Detected a pattern in the task queue, ran a research chain, summarized the output, and filed it. No prompt from me. No session open. I was asleep. The system decided something was worth doing and did it.

    That’s the thing I built toward for two years of warrior sessions. Not the feature. Not the specific task RALPH ran that night. The fact that it happened without me.

    What’s Actually Running

    There are four systems on Tower right now that operate independently of my presence.

    PrivyBot is the oldest and most capable. It’s a personal autonomous AI assistant — Python, FastAPI, 131 MCP tools, running as an NSSM service on Tower. It has a priority queue, an async task loop, and RALPH: a persistent overseer that fires on schedule and monitors for things worth acting on. Email summaries. GitHub activity. YouTube analytics. Game metrics. It doesn’t wait for me to ask. It runs its own loop and surfaces what matters.

    The test floor is 557 passing, 0 failing. I know that number is real because I certified it myself. Every phase of PrivyBot’s development ended with that verification before the next phase started. 33 phases. The floor moved up each time, never down.

    ContentPipeline is the YouTube operation. I play games. The pipeline records the session with OBS, transcribes it with Whisper running on Tower’s GPU, identifies the moments worth keeping, generates captions, assembles the Short with FFMPEG, and schedules the upload. The calendar runs through July 2026 without me touching it. The pipeline built the calendar. I just played the games.

    TeleseroAdmin2026 runs during business hours without supervision. It watches six dialing servers, monitors list performance, and swaps underperforming lists automatically based on thresholds I defined. 262 tests. Full-auto loop. The intervention it was built to eliminate — me watching metrics and making manual swaps — hasn’t happened in months.

    DNC Automation runs on Cloud Run. Compliance checks that used to be a manual process, now a deployed service. Stable. I check it roughly every two weeks to confirm it’s still running. That’s the entirety of my interaction with it.

    How You Get There From a Warrior Session

    None of these started as systems. They started as scripts.

    PrivyBot started as a Telegram bot that could answer questions. TeleseroAdmin2026 started as a Python script named by date that logged into a portal and swapped one list. ContentPipeline started as a single produce_short.py file that required manual input at every step.

    The path from script to autonomous system is always the same and always takes longer than you expect.

    First you automate the thing you do most often. Then you notice the thing adjacent to it that you’re still doing manually. You automate that. Then you realize the two automations need to talk to each other, which requires a shared config. The shared config implies a shared schema. The shared schema implies a system.

    You don’t design the system. You discover it. The design document comes after, when you’ve accumulated enough automated pieces to see the shape of what they’re forming.

    The warrior sessions are how the pieces accumulate. Ninety minutes on a Tuesday night adds the encoding handler. Two hours on a Saturday adds the deduplication pass. A three-hour session where something clicked adds the orchestration layer that connects them. None of those sessions felt like building a system. They felt like solving the problem in front of you.

    At some point you look up and there’s a system.

    What Autonomous Actually Means

    I want to be precise about this because “autonomous” gets used loosely.

    Autonomous doesn’t mean unsupervised forever. It means the system handles the routine cases without requiring a human in the loop for each one. The edge cases still surface. The unexpected failures still need attention. The system doesn’t replace judgment — it handles volume so judgment is reserved for the things that actually need it.

    RALPH firing at 2:47am and running a research chain is autonomous. RALPH discovering a new class of task it’s never handled before and stopping to report it rather than guessing — that’s also autonomous, in a different direction. The system knows what it knows and flags what it doesn’t.

    TeleseroAdmin2026 swapping a list because a performance threshold was crossed is autonomous. TeleseroAdmin2026 encountering a portal login flow that changed after a site update and stopping the loop rather than proceeding incorrectly — still autonomous. The right behavior in an unexpected situation isn’t always to act. Sometimes it’s to stop and surface the situation.

    The systems I trust are the ones that fail loudly when they’re outside their design envelope. The ones that fail quietly — that continue operating in edge cases and produce confident, wrong output — those aren’t autonomous systems. They’re liability.

    This is the same principle I apply to coding agents. A system that tells you it succeeded when it didn’t isn’t a trustworthy system. Raw terminal output only. The floor is real or it isn’t.

    The Compounding

    Last Wednesday RALPH ran 14 tool calls before 6am. By the time I was at my desk, there was a digest waiting: yesterday’s YouTube performance, an alert on a campaign metric that drifted outside threshold, a summary of three GitHub commits I’d made the night before with notes on what each one changed.

    I didn’t ask for any of it. I configured the system to care about those things, and the system cared about them while I slept.

    That’s a different relationship with work than I had two years ago, when every piece of information about my projects required me to go get it. The information is still there. The systems go get it for me and bring back what matters.

    The compounding isn’t the time saved on any individual task. It’s the accumulation of context that’s available without friction. I sit down knowing the state of things because the systems maintained the state while I was away. The warrior sessions start from a known position instead of starting with reconnaissance.

    Why This Is the Pitch

    The consulting angle I’m building toward isn’t “I’ll automate things for you.”

    It’s “I’ll build systems that maintain themselves.”

    There’s a specific kind of buyer for this: operations managers at contact centers, at lead-generation companies, at any business where a significant portion of labor is humans doing deterministic work that could be encoded. They’ve heard about automation. They’ve seen demos. What they haven’t seen is someone who built it for themselves first, runs it in production, and can point to a floor that’s real because they certified it personally.

    The demo isn’t a slide deck. It’s Tower. It’s RALPH. It’s a system that was running while I slept and will still be running when this conversation ends.

    You can’t pitch autonomous systems credibly without having built them. You can’t build them without the warrior sessions. The sessions were never just sessions — they were the R&D for a product I hadn’t named yet.

    The Honest Accounting

    There are systems I built that aren’t running. Scripts that automated a task I stopped doing. Repos that solved a problem that no longer exists. Not every warrior session produces something that compounds — some of them produce something that was useful once and isn’t anymore.

    That’s fine. The return on the ones that do compound is high enough to cover the ones that don’t. PrivyBot is worth every session that went into it and several that went into things I’ve since discarded. ContentPipeline has scheduled more content than I’ve actively thought about. TeleseroAdmin2026 has run more dialing adjustments than I could have made manually in the same period.

    The pipeline runs while I sleep. That’s not a metaphor for anything. It’s a literal description of what happens between midnight and 6am on Tower.

    I built that. In the margins. One session at a time.

  • The Bug That Looked Like Slow, and Was Actually Broken

    It was one of those checks that should’ve taken thirty seconds. I ran a search against a real list — a few hundred leads, standard call, nothing exotic — and it just sat there. No error. No result. Just quiet.

    I assumed it was slow. I’d built the thing to hit an internal API and page through records, so “slow” was the obvious story, and I believed it for longer than I should have. I even started looking at whether I needed to add caching.

    Then I ran the same search on a smaller list — thirty records instead of three hundred — and it worked instantly. That’s when I knew it wasn’t slow. Slow doesn’t have a cliff. Broken does.

    The real problem was a single field. My data model marked customer email as an optional, validated email field — which sounds correct, and is correct, right up until the source system’s convention for “no email on file” turns out to be an empty string instead of a null. Pydantic’s email validator doesn’t know what to do with an empty string. It doesn’t skip it. It rejects it. And it rejects it silently enough, deep enough in a batch operation, that the whole search just — stopped. No traceback pointing at the actual cause. Just nothing.

    I’d been debugging the wrong problem for the better part of an hour. I was optimizing for a diagnosis I’d made before I had any real evidence for it, and once I’d said “it’s probably slow” out loud, I kept looking for reasons that were true instead of reasons that were right.

    The fix was small once I found it — one validator that runs before the email check, converting empty strings to null so the real validation logic still applies to anything that’s actually malformed. Seven new tests to make sure it stayed fixed. But the fix isn’t the lesson. The lesson is that “it’s slow” and “it’s broken” produce completely different debugging paths, and picking the wrong one costs you real time before you even notice you’re on it.

    I’ve started treating my own first explanation as a hypothesis to disprove, not a starting point to build on. The five-minute version of that discipline: before you optimize anything, prove it’s actually the bottleneck you think it is.

    Next: going back through every other endpoint in the same tool with the same question — not “is this slow,” but “have I actually confirmed that, or just assumed it.”