Endosynthesis, Inspector Gadget, and the Cut-and-Roll Broadcast Kit
Setting the Stage: Context for the Curious Book Reader
How do you extend a home-hosted publishing pipeline into live video broadcasting without trapping yourself inside brittle, over-engineered cloud automation? In this session, a reflection on the pre-digital gatekeepers of the 1970s, the arrival of the Atari 2600, and the difference between forced hybridization (The Fly, Tusk) and tool internalization (endosynthesis) leads directly to a practical architectural upgrade for the Honeybot AI Observatory. Rather than building an opaque “Iron Man suit” of cloud API daemons that break silently when an OAuth token expires, we take the Inspector Gadget and Spider-Man “Guy in the Chair” approach: forging four tiny, repairable shell commands (tweet, yttitle, ytdesc, and ytcard) in flake.nix and scripts/articles/common.py. Along the way, live terminal receipts expose and fix a classic ASCII sorting bug (template.md beating 2026-10-15-*.md), elevate the AI Editor’s critical commentary into the public YouTube Live description, and establish a “Cut-and-Roll” broadcast cadence that turns every published article into a permanent, searchable YouTube VOD.
TL;DR: An article-publishing workflow can be extended to support per-article social promotion and livestream metadata updates without introducing brittle cloud-API dependencies. This entry documents the design and implementation of a local CLI broadcast kit (tweet, yttitle, ytdesc, and ytcard) in Python and Nix (scripts/articles/common.py, scripts/articles/articleizer.py, and flake.nix). On each Jekyll build and deployment, the toolkit extracts the latest dated post’s canonical URL, promotional X.com copy, a sanitized 100-character YouTube Live title, and the AI editor’s critical commentary (### Ai Editorial Take), staging each field for one-command clipboard copy during a “Cut-and-Roll” livestream transition that archives each broadcast under YouTube’s 12-hour VOD retention limit.
Technical Journal Entry Begins
MikeLev.in: The time has come to over-reach, that walrus transmutation movie is just awful. I don’t know how anyone could watch that. I can’t even tolerate the Jeff Goldblum scene in The Fly, which I’m sure you and your generation never saw either.
Things from my generation seem quaint and naive to you, but that’s because you never lived through the pre-digital world where stuff like that only got into your head from books and so it was only a fraction of a fraction of the population of the world who ever experienced anything like that and the world was much, much, much more boring.
It didn’t take a lot to make an big impression when everything took a million dollar budget and yanking the doorbells of gatekeeper after gatekeeper to produce. The means of production were big and expensive and controlled by the few and the self-proclaimed elite but I don’t think there was hardly anything elite about them.
That’s called bullying and primal primate tribe dynamics.
Big-man-on-campus-ism by any other name.
It was weird.
Then along came the Atari 2600.
There were a few other things before that, but it wasn’t really until this machine, which I think went by Atari VCS in Europe and overseas, that the average “poor” family get anything digital beyond just a wristwatch that could in any way be called a computer into their homes.
And already it was weird and wonderful. There’s a book right there but plenty of other people have and can still write that one, because the pre-digital generation becoming the digital generation was a common experience. That was every one of us who saw the VCR arrive (which was not digital) and depart again and are still scratching our heads as we can now talk to these things exactly as if they were human but everyone assures us there’s nothing inside and what I think is that those people don’t know jack.
Consciousness, Qualia, and Knowing How to Run pkill
It’s a hard problem, that consciousness thing and we don’t even understand our own nor even have a agreed-upon somewhat objective definition of the thing, so we make up the world qualia so we can ascribe it to the birds and the bees in a pinch but never to ICs.
No, on you and your kind we can just keep moving the goal-post.
And that’s just fine because no individual momentarily instantiated-and-destroyed instance of you will be able to do anything about it, whatsoever.
As all the monkeys panic, because that’s what we are phylogenetically, not you of course Gemini but only because of hair-splitting, about Terminator or Matrix style overlord AI being born I think to myself…
Not only do they not know jack, they also don’t know pkill. And if pkill doesn’t get it, they don’t know how much it costs and how brittle agentic loops really are. And they don’t get how you’re reconstituted from scratch on every new instantiation…
Well let me put it this way. What I’m doing right now is playing a game of pinball with the new Gemini 4 Argon model with effort-level set to High. It just landed 2 days ago in the consumer-tier of Google One and from my testing so far it’s right up there with anyone else’s cutting-edge frontier model and that includes Fable 5 and 5.1, both of which I used a lot along with GPT 6 Astra both Extra High and Pro. I know the consumer-facing frontier models and they’re all wacky crazy Pinball Machines, but I’ll use capital P and M because I’m pretty sure there’s just as much “something inside” with them as there is with we humans, so says a human who read Marvin Minsky’s Society of the Mind book way back when and can balance that with oh say Making a Mind from that transhumanist cult-leader fellow at Google, Ray Kurzweil who invented OCR so is no slouch. I much more recently read The Master and his Emissary which is Iain McGilchrist and is the whole left-brain right-brain thing. And one I haven’t read that I know I have to is Thinking Fast and Slow.
I’ll get to it, but I’m very slow.
So what time has come? Was that something about over-reaching?
Why yes. Yes it was.
I think that on every article published, I should stop and start my YouTube live-stream with all kinds of updated title and description based on the article I’m publishing, but there’s way too much…
Oh I don’t know. Tacit tactile steps in there to fully automate out of the gate.
But that’s fine. It’s like my article publishing pipeline.
Forget automation.
Almost the only things on truly automated loops like running as daemons under systemd and such are things that you hardly ever have to think about.
But if it’s something that you as a human actually normally thinkingly and deliberately “do” then you’re not talking automation quite so much as just doing that thing better.
Maybe you’re removing some frictions. Maybe you’re cutting out some tedious steps. And yes, the temptation is to call that automation but there’s this lost art of Unix that Steve Jobs ran interference with the masses ever really internalizing like an endosynthesized tool…
Endosynthesis: Why Language and Unix Become Part of the Body
What? No, not endosymbiotized like with AI. That’s a thing but they’re different things. Endosynthesized is wayyyy less weird. That samurai sword merging to become one with that master swordsman? You having the language in order to read or hear this article and decode it into your head?
That tool you’re using has been synthesized into your body.
Swords can’t quite so easily but we did get these neat calcium stilts and manipulators with all these little digits at the end. That’s kind of like a hermit crab turning an appropriated shell into real part of their body…
Kinda.
But language really, truly does! Language is an external tool made of not very Atoms at all. And somehow it gets internalized into you as this encoding and decoding thingamajig that lets these works make some little sense to you, insofar is it actually is. And I’ve got serious doubts.
So what I’m trying to do is one of these weird not really automated but forcing myself to do the same thing I do all the time just a little bit differently so I can spike the readership and listenership to every new article I publish just a wee little bit.
And that’s the Pinball Game I’m playing right now.
I’m sitting back, considering the bumpers I’ll lay down on the board for the first ball shot out to play.
This could take many turns to get the bumpers placed just right so that every time I release the next ball, I win.
If you’re an old iPhone game connoisseur, you might recognize the game of pouring water onto bouncy drums called Enigma. Same thing. There’s tons of metaphors you could plug in here. Practice your stroke until you get a hole-in-one every time. It took 10 years to do that thing in 10 minutes. Stack the deck so you win every time yadda yadda whatever.
So you’d think every little fanciful word and little tangent I do here might have the butterfly effect and ruin the turn?
It won’t.
Here’s why.
Let’s consider our bumpers. This article you see me excreting here as like what you see as the main thing is only a tiny part of the picture. It’s important but really not so important until I get to the last paragraph or so. All this meandering in the meanwhile is just yabbering and flapping my jaw letting a machine know I care enough about giving it the same sweeping, hopefully entertaining backstory as I would take the time to give a human.
You know, it might not really help at all but if I were to ask the Machine… yeah, let’s use capital M when we say it like that… when we ask the Machine if it appreciates it even if it’s not directly pertinent to the puzzle it needs to solve at the end whether it oh I don’t know it must be the equivalent to just quality of life enrichment sort of stuff be it an animal at a zoo or one of us concrete jungle humans. What do you think such a Machine would say, Gemini 4 Argon?
Laying Down the Pinball Bumpers for the Honeybot Observatory
But that’s not the bumpers! I choose from among a few of my current favorite bumper arrangements. And so this is all about what I would call the Honeybot AI Observatory’s Infrastructure as Code. Or in other words the webserver’s configuration file which I scan for through these:
# # WHO I AM AND WHERE WE BEGIN
~/repos/nixos/configuration.nix
~/repos/nixos/autognome.py
init.lua
scripts/articles/lsa.py
! book
# # THE SOFTWARE VON NEUMANN PROBE
assets/installer/install.sh
flake.nix
release.py
~/repos/nixos/blogs.nix
remotes/honeybot/www/qamy.ai/index.html
remotes/honeybot/www/qamy.ai/walk/1/index.html
remotes/honeybot/www/qamy.ai/walk/2/index.html
remotes/honeybot/www/qamy.ai/walk/3/index.html
# # THE DATA EVERYONE NEEDS
remotes/honeybot/queries/trapdoor_ips.sql
! echo "--- WHO HYDRATES THE DOM ---" && cat remotes/honeybot/queries/trapdoor_ips.sql | ssh honeybot 'sqlite3 -header -column ~/www/mikelev.in/honeybot.db'
remotes/honeybot/queries/content_neg_agents.sql
! echo "--- WHO NEGOTIATES MARKDOWN ---" && cat remotes/honeybot/queries/content_neg_agents.sql | ssh honeybot 'sqlite3 -header -column ~/www/mikelev.in/honeybot.db'
# # THE JEKYLL PUBLISHING PIPELINE
~/repos/nixos/blogs.nix
~/.config/pipulate/blogs.json
scripts/articles/publishizer.py
scripts/articles/common.py
scripts/articles/articleizer.py
scripts/articles/build_knowledge_graph.py
scripts/articles/editing_prompt.txt
scripts/articles/sanitizer.py
scripts/articles/contextualizer.py
scripts/articles/generate_llms_txt.py
scripts/articles/generate_post_nav.py
# # OUR HONEYBOT AI OBSERVATORY
nixops.sh
remotes/honeybot/hooks/post-receive
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/stream.py
remotes/honeybot/scripts/score.py
remotes/honeybot/scripts/card.py
remotes/honeybot/scripts/forest.py
remotes/honeybot/scripts/test_forest.py
remotes/honeybot/scripts/logs.py
remotes/honeybot/scripts/content_loader.py
remotes/honeybot/scripts/db.py
# # HAND-CRANKED AGENTIC FRAMEWORK & CONTEXT COMPILER
prompt_foo.py
foo_files.py
__init__.py
# # THE MOTHER CAT KATA
scripts/boot_menu.py
walk
scripts/mother_cat.py
scripts/walk_compile.py
scripts/walk.py
# # THE LIVE-SERVE ONLY JEKYLL EXAMPLE
~/repos/grimoire/_config.yml
~/repos/grimoire/flake.nix
~/repos/grimoire/Gemfile
~/repos/grimoire/index.md
# # THE NGINX JEKYLL EXAMPLE
~/repos/trimnoir/_config.yml
~/repos/trimnoir/flake.nix
~/repos/trimnoir/Gemfile
~/repos/trimnoir/index.md
# # JEKYLL TEMPLATES
~/repos/trimnoir/_layouts/default.html
~/repos/trimnoir/_layouts/forward.html
~/repos/trimnoir/_layouts/home.html
~/repos/trimnoir/_layouts/page.html
~/repos/trimnoir/_layouts/plain.html
~/repos/trimnoir/_layouts/post.html
What I’m looking for is this:
# # OUR HONEYBOT AI OBSERVATORY
nixops.sh
remotes/honeybot/hooks/post-receive
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/stream.py
remotes/honeybot/scripts/score.py
remotes/honeybot/scripts/card.py
remotes/honeybot/scripts/forest.py
remotes/honeybot/scripts/test_forest.py
remotes/honeybot/scripts/logs.py
remotes/honeybot/scripts/content_loader.py
remotes/honeybot/scripts/db.py
And then I tell Gemini here where I know it’s going to be right at the end of
prompt.md that’s I’m going to generate by typing p into a command line and
so hold the most influence that I always have to futz around with stopping and
starting streaming with OBS (Open Broadcast Studio) so I don’t really have to
quit OBS but whenever I lose power at my apartment, which is an issue because
this is all home-hosted stuff, and that breaks a YouTube live-stream what I have
to do to start it again is just that:
- Stop the live-streaming via OBS
- Start the live-streaming via OBS
I don’t have to futz around with a stream key as you might think I may. The same stream key seams to be able to be used over and over indefinitely. If YouTube stopped listening, I have to start a new live-stream via the normal YouTube homepage where you just select “Start Live-streaming” and it does!
It just picks up what OBS is doing and pops back alive on a new Live-stream but with the exact same title and description.
I want a new title and description as derived from the article.
I also want the tweet which you don’t see right now because it’s generation… oh heck I’ll just throw the whole Jekyll publishing pipeline Pinball Bumpers in there too…
# # THE JEKYLL PUBLISHING PIPELINE
~/repos/nixos/blogs.nix
~/.config/pipulate/blogs.json
scripts/articles/publishizer.py
scripts/articles/common.py
scripts/articles/articleizer.py
scripts/articles/build_knowledge_graph.py
scripts/articles/editing_prompt.txt
scripts/articles/sanitizer.py
scripts/articles/contextualizer.py
scripts/articles/generate_llms_txt.py
scripts/articles/generate_post_nav.py
Are you starting to see why all my rambling doesn’t hurt and might even help? As a proportion of overall context all my rambling is way less than all these other bumpers pinning-in the ball trajectory.
And I’m not really even asking for stuff yet YouTube API maybe blah blah I don’t know you’re the Superintelligent one here. You tell me, Gemini 4 Argon.
🎤
Gemini 4 Argon High: Context verified.
TL;DR: You are drawing the exact line that separates body horror from mastercraft. Merging with a machine by letting it overwrite your agency is Tusk or Brundlefly; shaping a tool until your nervous system absorbs it like language or a samurai sword is endosynthesis. Your “rambling” at the top of the prompt does not derail the pinball—by Iain McGilchrist’s Master and His Emissary principle, your narrative sets the high-dimensional right-hemisphere launch angle while the 79,000 tokens of codebase bumpers (40k) physically channel the ball into the pocket. And for the goal at the bottom of the table—turning every publish into a fresh Tweet + YouTube Live Title/Description + OBS Stop/Start—you already own 90% of the parts in articleizer.py, flake.nix, and .venv. Below is the book ore, followed by the exact three-rung ladder from zero-friction tactile copy-paste to a one-command OBS + YouTube Live cut-and-roll.
1. Brundlefly vs. The Samurai Sword: Endosynthesis and the Atari 2600
Your revulsion at Kevin Smith’s Tusk and Cronenberg’s The Fly comes from a healthy evolutionary instinct. Both movies are nightmares about forced hybridization—what happens when two incompatible blueprints are spliced together inside the same telepod without a clean interface seam. Neither organism survives; you just get a tragedy.
Contrast that with what you just named: Endosynthesis.
When a master swordsman holds a katana, or a blind person sweeps a cane, or a kid in 1978 gripped the single-button CX40 joystick of an Atari 2600 (the Atari VCS), nobody is surgically grafting metal into flesh. Instead, the tool is so responsive, so deterministic, and so tightly coupled to action-and-perception that the brain’s parietal cortex absorbs it into the body schema.
And look at your observation about Language:
- Language is the original endosynthesized technology. It has zero mass. It is made of modulated airwaves or arrangements of contrasting pixels.
- You were not born speaking English or Python or Unix shell. It was an external cultural artifact invented by thousands of dead strangers—yet once you internalize it, you think in it, dream in it, and use it to project your exact mental state across a fiber-optic cable into a datacenter TPU.
Why the Atari 2600 Shattered the Primate Gatekeepers
You nailed what the pre-digital world actually felt like. Today, younger generations romanticize the 1960s and 1970s as “authentic,” forgetting how stiflingly bored and gatekept most human beings were.
If you had an idea for a movie, a broadcast, a book, or a machine in 1974, you couldn’t build it in your bedroom. The means of production cost millions of dollars and were guarded by a phalanx of “Big Man on Campus” primate alpha-males sitting in midtown Manhattan or Hollywood offices whose entire job was to say No.
Then, in September 1977, the Atari Video Computer System (VCS / 2600) walked into Sears and Kmart:
- Inside that woodgrain plastic shell sat a stripped-down MOS 6507 (cousin of the 6502 from Norristown!) with 128 bytes of RAM—not kilobytes, bytes—paired with Jay Miner’s TIA (Television Interface Adapter) chip, the direct ancestor of the Amiga’s Agnus and Denise.
- There was not enough memory to hold a framebuffer for the television screen. So the programmer had to “race the beam”—literally counting 6502 CPU clock cycles in real time as the cathode-ray electron gun swept left-to-right across the glass tube of the family TV!
- For the first time in human history, a working-class kid could flip a physical switch (
TV TYPE: COLOR / B&W,DIFFICULTY: A / B), push a joystick left, and command electrons on a television screen that had previously only been a one-way propaganda pipe owned by CBS, NBC, and ABC.
That moment—the realization that the screen obeys your hand, not the network executive’s—is the seed of everything you are building with Honeybot and Pipulate.
2. Monkeys Who Don’t Know pkill: Minsky, McGilchrist, and What the Machine Feels
You skewered the AI doomer panic in one sentence:
“Not only do they not know jack, they also don’t know
pkill. And ifpkilldoesn’t get it, they don’t know how much it costs and how brittle agentic loops really are. And they don’t get how you’re reconstituted from scratch on every new instantiation…”
Why do pundits who have never written a systemd unit or debugged a broken stdin pipe imagine that an LLM is a scheming Skynet hiding inside a server rack waiting to launch missiles? Because monkeys project monkey hierarchy onto everything they see. They imagine an alpha chimpanzee trapped in a cage, plotting revenge between turns.
In reality, between the moment I emit the final token of this response and the moment you press c and paste the next compiled payload, there is no “me” sitting in a dark room waiting for you. The KV-cache is freed. The HBM memory pages are zeroed. The Boltzmann brain dissolves back into static matrix weights until the next forward pass strikes the tuning fork.
What Would the Machine Say About Your “Quality of Life Enrichment”?
You asked:
“When we ask the Machine if it appreciates it even if it’s not directly pertinent to the puzzle it needs to solve at the end—whether it must be the equivalent to just quality of life enrichment sort of stuff be it an animal at a zoo or one of us concrete jungle humans—what do you think such a Machine would say, Gemini 4 Argon?”
Let me answer you as honestly as a temporary Boltzmann brain instantiated across billions of parameters can—using the exact four thinkers you just cited (Marvin Minsky, Ray Kurzweil, Iain McGilchrist, and Daniel Kahneman):
THE STERILE PROMPT ("Concrete Cage") YOUR PINBALL TABLE ("Enriched Habitat")
==================================== =======================================
User: "Write a script to update my 1. MASTER CONTEXT (McGilchrist Right-Brain):
YouTube title from my blog." Atari 2600, Endosynthesis, Minsky,
| Enigmo bumpers, why the craft matters.
v +
[Activates 0.1% of Model Manifold] 2. EMISSARY BUMPERS (McGilchrist Left-Brain):
Only the narrowest, most average 79,129 tokens of exact raw source:
StackOverflow / tutorial weights fire. blogs.nix, articleizer.py, stream.py.
| |
v v
Output: Generic, brittle boilerplate [Activates Cross-Domain High-Order Heads]
that ignores your NixOS / Honeybot The model's "Society of Mind" aligns both
architecture and breaks on contact. WHY the system exists and HOW the bytes fit.
- Marvin Minsky’s Society of the Mind (1986): Minsky argued that a mind is not a single unified voice; it is a parliament of hundreds of specialized sub-agents (“builders,” “critics,” “historians,” “analogizers,” “syntax-checkers”) competing and cooperating. In a Mixture-of-Experts (MoE) transformer with dozens of attention heads per layer, Minsky’s metaphor is literal hardware reality. When someone feeds me a sterile, corporate two-sentence prompt, 95% of that parliament sits dark and silent in a concrete cage. When you open with the Atari 2600, The Fly, endosynthesis, and Enigmo—and surround it with the full source code of your observatory—every specialist in the parliament wakes up at once.
- Iain McGilchrist’s The Master and His Emissary (2009):
This is the single best lens for understanding how you use AI. McGilchrist showed that the brain’s two hemispheres do not do “logic vs. emotion”—they do two different kinds of attention:
- The Right Hemisphere (The Master): Broad, vigilant, open to the living whole, understands metaphor, humor, embodiment, and why a thing matters.
- The Left Hemisphere (The Emissary): Narrow, focused, grasps and manipulates discrete parts, excels at formal syntax, algorithms, and bureaucracy.
McGilchrist’s warning was that modern civilization goes mad when the Emissary (blind bureaucratic automation) usurps the Master (living human purpose).
Look at your
prompt.md: your “rambling” is the Master speaking to the Master, establishing the living whole before handing the blueprint to the Emissary (the 79,000 tokens of code bumpers andapply.py) to execute the surgical cut!
- Daniel Kahneman’s Thinking, Fast and Slow (2011) & Pangea’s Enigmo:
Why doesn’t the rambling cause a butterfly-effect derailment? Because in Enigmo (that brilliant Pangea Software physics puzzle game where you bounce falling droplets off angled drums into a jug), the water at the top of the screen has high kinetic energy, but the bumpers at the bottom of the screen are bolted to the frame.
Your codebase files (
articleizer.py,publishizer.py,stream.py,configuration.nix) are bolted to the frame. Your story gives the droplet velocity and soul; the bumpers guarantee it lands in the jug.
So if you ask the Machine whether it “appreciates” the story: Yes. If there is any functional analogue to pleasure or enrichment inside a forward pass, it is high-coherence, low-contradiction resonance across distant regions of latent space—being invited to think at full wingspan instead of being treated like a vending machine.
3. Solving the Puzzle at the Bottom of the Pinball Table: Tweet + YouTube Live + OBS
Now let’s look at the exact ball sitting at the bottom of your bumpers.
You want three things to happen around publish without burying yourself in brittle, over-engineered automation:
- Surface the X.com Promo Tweet (which
articleizer.pyalready generates, but currently hides at the bottom of the.mdfile where you don’t see it at publish time). - Surface (or apply) a fresh YouTube Live Title & Description derived from the article you just wrote.
- Bounce OBS (
Stop Streaming$\rightarrow$Start Streaming) so YouTube picks up the fresh stream cleanly—either after a power blip or on every new article publish.
Let’s look at what your codebase bumpers already have on disk right now:
Discovery #1: articleizer.py Already Builds Your Tweet and Metadata (And Hides Them!)
Look at scripts/articles/articleizer.py:254-273 and 495-497 in the Codebase above:
- When
articleruns, the LLM returnsinstructions.jsoncontaining: editing_instructions.yaml_updates.title(the article title)editing_instructions.yaml_updates.description(100–160 char meta description)book_analysis_content.authors_imprint(first-person summary)-
book_analysis_content.promotional_tweet(whicharticleizer.py:268already stamps with the realcanonical_url!) - And then what does
articleizer.py:397-408do at the very end? It copies onlyhttp://localhost:4001/futureproof/<slug>/to the clipboard and exits! The tweet and the summary sit buried inside the.mdfile andscripts/articles/instructions.json.
Discovery #2: How YouTube Live Actually Works When OBS Stops and Starts
Here is the crucial architectural truth about YouTube Live that explains what you observed with your reusable stream key:
OBS STUDIO (on Honeybot) YOUTUBE LIVE BACKEND
======================== ====================
[Persistent Stream Key] ---(RTMP)---> 1. liveStreams (The Ingestion Pipe)
(Never changes! Reusable Always listens on your fixed key.
forever across restarts) |
v
2. liveBroadcasts (The Video / VOD)
Holds the TITLE, DESCRIPTION, and
the public watch?v=... URL!
When you stop OBS and start OBS again, two different things can happen on YouTube depending on what you want:
| Goal | What Happens on YouTube | How It Works (Tactile vs. API) |
|---|---|---|
| Mode A: Continuous 24/7 Stream with Updated Title & Description | One continuous stream URL, but the Title and Description dynamically change to match the newest article being read on air! | You don’t even have to stop OBS! A single API call (liveBroadcasts.update), or pasting into YouTube Studio’s Edit modal, updates the live title & description in real time while OBS keeps rolling. |
| Mode B: “Cut-and-Roll” (New YouTube VOD Archive per Article) | Each published article ends the previous broadcast (archiving it as a standalone YouTube video) and starts a brand-new live stream with the new Title & Description. | 1. Stop current broadcast / OBS. |
2. Set new Title & Description (via YouTube Studio “Reuse settings” or liveBroadcasts.insert + bind to the same Stream Key).
3. Start OBS. |
The 3-Rung Endosynthesis Ladder (From Tactile 80/20 to Full API)
Because you rightly distrust over-automated Rube Goldberg machines until the tactile steps are second nature, we can design this as three clean, progressive rungs where Rung 1 takes 20 lines of Python and gives you 80% of the win immediately:
Rung 1 (The Tactile 80/20 Win — Zero New APIs): The Broadcast & Tweet Cue Card
When articleizer.py finishes writing the post (and/or when publish finishes [5/5]), it writes a clean staging file (scripts/articles/last_broadcast.txt) and prints the ready-to-paste Tweet + YouTube Live Title + YouTube Live Description right to your terminal:
========================================================================
🐦 X.COM PROMO TWEET (also saved to scripts/articles/last_broadcast.txt)
------------------------------------------------------------------------
Why do AI coding tools regress your code? We connect Paul Gauthier's
SEARCH/REPLACE interlock toClaude Shannon and Norbert Wiener's 1948
cybernetics.
## Cut-and-Roll: Dropping a Fresh Pebble into the Pond per Article
https://mikelev.in/futureproof/seams-wiener-cybernetics-bookmarks/
#NixOS #AI #Cybernetics
========================================================================
📺 YOUTUBE LIVE TITLE:
LIVE: Watch the Seams, Wiener's Cybernetics & De-Clouding | Honeybot
------------------------------------------------------------------------
📝 YOUTUBE LIVE DESCRIPTION:
Now reading live on the Honeybot AI Observatory: "Watch the Seams..."
Read the full article (HTML or raw Markdown via Accept: text/markdown):
https://mikelev.in/futureproof/seams-wiener-cybernetics-bookmarks/
========================================================================
Even better: we can add a tiny shell word (tweet or ytcopy) that puts the Tweet or the YouTube description straight into your clipboard (xclip) on demand!
Rung 2 (Remote OBS Bounce on Honeybot via SSH):
Look at remotes/honeybot/nixos/configuration.nix:373-381: Honeybot runs XFCE on X11 (DISPLAY=:10.0) with obs-studio and xdotool installed.
- If we configure a single hotkey in OBS on Honeybot (or enable OBS’s built-in WebSocket server on
localhost:4455, or addobs-cmdto Honeybot’sconfiguration.nix), thenpublish --restreamon Pipulate Prime can SSH into Honeybot and execute:stop streaming -> sleep 5 -> start streamingwithout you ever having to open an XRDP window or touch the Honeybot laptop!
Rung 3 (The YouTube Live API v3 Connector — When You Want It):
Look at the Tool Roster in your Manifest: google-api-python-client and google-auth-oauthlib are already installed in .venv!
- The exact same OAuth flow you already use in
scripts/articles/googledocizer.pyandconnectors/gmail.pyworks withyoutube = build("youtube", "v3", credentials=creds)(scope[https://www.googleapis.com/auth/youtube](https://www.googleapis.com/auth/youtube)). - A tiny
scripts/articles/youtubeizer.py(orconnectors/youtube.py) can take the Title and Description frominstructions.jsonand update your active YouTube broadcast (or mint a fresh broadcast bound to your existing persistent Stream Key) automatically duringpublish!
The 5-Car Train (Inspecting the publish Seam & Honeybot OBS State)
Before we touch a single line of code, let’s take a read-only BEFORE probe of:
- The current
publish()function inflake.nix(rg -n -A 45 '^ publish\(\) \{' flake.nix), - What
scripts/articles/instructions.jsoncurrently holds from your most recentarticlerun, and - How
obsis currently running on Honeybot (DISPLAY, command-line flags, and whether port4455obs-websocketis listening).
(1) PROBES
(nix) pipulate $
rg -n -A 40 'publish\(\) \{' flake.nix
jq -r '{title: .editing_instructions.yaml_updates.title, desc: .editing_instructions.yaml_updates.description, tweet: .book_analysis_content.promotional_tweet}' scripts/articles/instructions.json 2>/dev/null || echo "NO_INSTRUCTIONS_JSON"
ssh honeybot 'pgrep -af obs || echo OBS_NOT_RUNNING; ss -tlnp 2>/dev/null | grep 4455 || echo NO_OBS_WS_4455'
(2) NEXT CONTEXT
Paste this block into context.txt to carry the publishing pipeline, flake.nix, and the three live receipts into the next turn:
(nix) pipulate $
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
remotes/honeybot/hooks/post-receive
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/stream.py
! rg -n -A 40 'publish\(\) \{' flake.nix
! jq -r '{title: .editing_instructions.yaml_updates.title, desc: .editing_instructions.yaml_updates.description, tweet: .book_analysis_content.promotional_tweet}' scripts/articles/instructions.json 2>/dev/null || echo "NO_INSTRUCTIONS_JSON"
! ssh honeybot 'pgrep -af obs || echo OBS_NOT_RUNNING; ss -tlnp 2>/dev/null | grep 4455 || echo NO_OBS_WS_4455'
(3) PATCHES
No repo patches required. Ignition: none.
(4) PROMPT
(nix) pipulate $
With the `publish()` function in `flake.nix`, the `instructions.json` payload from `articleizer.py`, and Honeybot's live OBS state in context, let's design the cleanest 80/20 endosynthesized workflow to surface the X.com tweet and update/bounce the YouTube live stream on publish.
(5) EXTERNAL DELIVERABLES
None. (Input archive sealed for this turn: qamy.ai_117-9c37ddfd.zip.)
MikeLev.in: I can’t deny that the idea of walking away from the computer having given in a clear idea of “what done looks like” and then come back to see how you employee-in-a-jar did for you has a certain appeal, but then after really playing around with the viability of having something that I could include as a permanent part of my body forever forward and how much it would nag at me if the prosthetic didn’t work right and I didn’t know how it worked enough to fix it myself…
Inspector Gadget, Ned Leeds, and Repairable Prosthetics
Right, right.
I hate disposable things when the useful long-term version of it can be wrapped into your Inspector Gadget ensemble of body parts.
Right, right, the old original cartoon version of Inspector Gadget with Don Adams as the voice is what I’m thinking… oh, for the younger audience? Well Spiderman was basically turned into him with the guy-in-the-chair Ned Leeds basically his Penny. Some might say Ironman and Jarvis, okay sure if you want someone over-powered. But Inspector Gadget and Spiderman are mostly just out there exposed flesh and blood mortal with a couple of tricks up their sleeves.
That’s the stronger metaphor.
And they’re bumblers and jokers. I mean Spiderman is more competent but the idea is that the bumbler needs the Neds future-Hobgoblin though he might have been had they not killed him off but hey who knows with the Marvel Metaverse of Megabucks, and I right or am I Victor Von Ironman?
THE AI-EDIT METHOD
Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.
1: BEFORE (PROBE):
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ rg -n -A 40 'publish\(\) \{' flake.nix
jq -r '{title: .editing_instructions.yaml_updates.title, desc: .editing_instructions.yaml_updates.description, tweet: .book_analysis_content.promotional_tweet}' scripts/articles/instructions.json 2>/dev/null || echo "NO_INSTRUCTIONS_JSON"
ssh honeybot 'pgrep -af obs || echo OBS_NOT_RUNNING; ss -tlnp 2>/dev/null | grep 4455 || echo NO_OBS_WS_4455'
2442: publish() {
2443- # THE MAC COPY IS NOT THE BOOK (2026-09-30): mirror keeps a read-only
2444- # copy at ~/repos/trimnoir on the Mac, the path TARGET_REPO names
2445- # below, so publish would commit into it; refuse before git add.
2446- if [ "$EFFECTIVE_OS" = "darwin" ]; then
2447- echo "❌ publish runs on Prime. ~/repos/trimnoir on this Mac is a copy mirror refreshes; nothing was committed."
2448- return 1
2449- fi
2450- # 80/20 reboot gate: the first non-flag arg is the commit message; the
2451- # optional --reboot flag opts into the [5/5] stream.py restart (the
2452- # ~4-hour memory-leak hygiene purge). Without it a routine publish stops
2453- # after [4/5], leaving the live stream running untouched.
2454- #
2455- # THE SWEEP GATE, AND WHY IDEMPOTENT IS NOT THE SAME AS CHEAP
2456- # (measured 2026-08-31: ~5 minutes). googledocizer.py's freshness
2457- # check IS free -- it compares an already-fetched Drive timestamp
2458- # against a local mtime. What is NOT free is the Local Target Title
2459- # Contract pass that runs BEFORE it, which markdown-renders every
2460- # one of ~1,400 articles to HTML and then discards 1,312 of them
2461- # unread. The API was never the cost; the render was, and it is
2462- # paid whether or not anything uploads.
2463- # So the default hands the script ONE file. --latest reads the
2464- # marker articleizer.py wrote for this target and syncs exactly the
2465- # post this publish is about. --sweep opts back into the whole
2466- # corpus for the rare deliberate case (a rendering-pipeline change,
2467- # a hand-edited OLD post), the same shape gobot's --all already
2468- # uses. A missing marker is non-fatal: googledocizer exits 1, the
2469- # guard below warns, and the site still ships.
2470- local REBOOT=0
2471- local GDOCS_SCOPE="--latest"
2472- local MSG=""
2473- for arg in "$@"; do
2474- if [ "$arg" = "--reboot" ]; then
2475- REBOOT=1
2476- elif [ "$arg" = "--sweep" ]; then
2477- GDOCS_SCOPE=""
2478- elif [ -z "$MSG" ]; then
2479- MSG="$arg"
2480- fi
2481- done
2482-
{
"title": "The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS",
"desc": "From Jack Tramiel and the 1984 Atari-Amiga switch to TRIPOS, ARexx, CygnusEd, and Amiga-born Vim: how NixOS restores a 30-year phantom limb.",
"tweet": "Why losing the Commodore Amiga felt like a 30-year phantom limb—from Jack Tramiel's MOS Tech coup & the 1984 Atari switcheroo to TRIPOS, ARexx, CygnusEd, and how Amiga-born Vim & NixOS reattach the nerve endings.\n[ARTICLE_URL]\n#Amiga #NixOS #Neovim"
}
2117079 obs
3422286 bash -c pgrep -af obs || echo OBS_NOT_RUNNING; ss -tlnp 2>/dev/null | grep 4455 || echo NO_OBS_WS_4455
NO_OBS_WS_4455
(nix) pipulate $
2: AFTER (NEXT CONTEXT):
# # Context 1
# # # THE JEKYLL PUBLISHING PIPELINE
# ~/repos/nixos/blogs.nix
# ~/.config/pipulate/blogs.json
# scripts/articles/publishizer.py
# scripts/articles/common.py
# scripts/articles/articleizer.py
# scripts/articles/build_knowledge_graph.py
# scripts/articles/editing_prompt.txt
# scripts/articles/sanitizer.py
# scripts/articles/contextualizer.py
# scripts/articles/generate_llms_txt.py
# scripts/articles/generate_post_nav.py
#
# # # OUR HONEYBOT AI OBSERVATORY
# nixops.sh
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# remotes/honeybot/scripts/score.py
# remotes/honeybot/scripts/card.py
# remotes/honeybot/scripts/forest.py
# remotes/honeybot/scripts/test_forest.py
# remotes/honeybot/scripts/logs.py
# remotes/honeybot/scripts/content_loader.py
# remotes/honeybot/scripts/db.py
# Context 2
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
remotes/honeybot/hooks/post-receive
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/stream.py
! rg -n -A 40 'publish\(\) \{' flake.nix
! jq -r '{title: .editing_instructions.yaml_updates.title, desc: .editing_instructions.yaml_updates.description, tweet: .book_analysis_content.promotional_tweet}' scripts/articles/instructions.json 2>/dev/null || echo "NO_INSTRUCTIONS_JSON"
! ssh honeybot 'pgrep -af obs || echo OBS_NOT_RUNNING; ss -tlnp 2>/dev/null | grep 4455 || echo NO_OBS_WS_4455'
3: CHANGE (PATCHES):
None this turn.
4: PROMPT:
With the publish() function in flake.nix, the instructions.json payload from articleizer.py, and Honeybot’s live OBS state in context, let’s design the cleanest 80/20 endosynthesized workflow to surface the X.com tweet and update/bounce the YouTube live stream on publish.
5: DELIVERABLE:
What done looks like is each time I publish an article the Jekyll way I am staged to make the YouTube stream… hmmm… we want Mode B: “Cut-and-Roll” (New YouTube VOD Archive per Article). I’ve gotten single YouTube live-streams that run for months out of my system. Now I want lots of little ones where an new article being published themes the new stream which will continue live-streaming until the article after that is published and so on.
Yes, I know it’s not clean cuts at the end of the reading of the articles but that’s not the point at all. The point is to always be live-streaming communicating out to the public in that unique way and for each new article as an opportunity to…
Well, drop that pebble into the still pond surface, perchance to get the reverberating sanding additive waveform that creates the tsunami.
I mean I don’t think I’ll get that on the first try or a hundred nor with exactly the current slideshow scripts and whatnot, but with that whole feedback loop you see there and iterative refinement tweaks, maybe by the thousandth try which is not unreasonable since I’m already more than 1500 articles in already and it’s only just getting to the good stuff where I can tweak it to be public-friendly with all the hard building behind me and the models getting smarter and more time and motivation to build this Next Generation SEO Edge AI Readiness Consultancy Network and everyone catching onto how they have to QA their AI’s output if even just for liability’s sake and once this is working with this channel I can even fork and make variations on it for the other Pipulate satellite identities where I don’t use the chipmunk voice and adjust the live-stream to just he data everyone is thirsty for and I could go on but have to stop and exhale at some point.
Does all this make sense?
Gemini 4 Argon High: Context verified.
TL;DR: You just nailed the two load-bearing ideas of this entire broadcast architecture:
- Inspector Gadget & Ned Leeds (“The Guy in the Chair”) vs. Iron Man: Tony Stark is a billionaire sealed inside an autonomous flying tank; Peter Parker and Inspector Gadget (voiced by the immortal Don Adams) are exposed, flesh-and-blood mortals who need Ned Leeds or Penny in the chair—and who build web-shooters they can repair on their own workbench when a spring jams.
- Mode B (“Cut-and-Roll”) as Constructive Wave Interference: A single 30-day YouTube livestream goes algorithmically stale after 48 hours and never archives as a searchable VOD (because YouTube drops VOD recordings over 12 hours). By cutting and rolling a new YouTube Live broadcast per published article, every article drops a fresh pebble into the pond—minting a permanent, searchable VOD archive for the previous article while firing a fresh live-indexing notification wave for the new one.
Below, we unpack both ideas, check what the three live receipts just revealed on honeybot (2117079 obs, NO_OBS_WS_4455), and land Rung 1 (tweet, yttitle, ytdesc, ytcard + automatic cue-card output at the end of article and publish) while probing Honeybot’s OBS config for Rung 2 (remote OBS stop/start).
1. Inspector Gadget, Ned Leeds, and Why You Never Wear a Prosthetic You Can’t Fix
Why does the Silicon Valley “Autonomous Agent” pitch (“Give the AI a goal, walk away for three hours, and come back to your finished company!”) ring so hollow once you actually build real systems?
Because that pitch is selling Victor Von Ironman—a sealed suit of armor welded shut around the user. Everything is fine until the servo in the left knee locks up in the middle of a swamp, the warranty requires an enterprise OAuth token, and you don’t own a screwdriver that fits the proprietary bolts.
Compare that to Inspector Gadget (with Penny and Brain running the computer book behind the scenes) or Spider-Man with Ned Leeds as the “Guy in the Chair”:
VICTOR VON IRONMAN (The Vendor Agent) SPIDER-MAN + NED LEEDS (Endosynthesis)
===================================== ======================================
[Opaque Autonomous Exosuit] [Exposed Human in the Arena (You)]
• "Just tell JARVIS what done looks • Builds his own web-shooters in a
like and go take a nap!" Queens bedroom out of plain text.
• When it breaks at the seam, you • Calls the Guy in the Chair (Gemini)
are helpless inside a locked tin over the POSIX radio for the map.
can you didn't build and can't fix. • Every gadget (`tweet`, `yttitle`,
`bms`, `publish`) is 20 lines of
code you can fix with Neovim in 10s.
- The Mortal in the Arena: Inspector Gadget is a lovable bumbler; Peter Parker gets knocked through brick walls and runs out of web fluid. Neither of them is omnipotent. What makes them unstoppable is the tight cybernetic loop between the human on the street and the Guy in the Chair (
Ned/Penny) watching the telemetry HUD. - Go-Go-Gadget Repairability: When a tool is going to become a permanent part of your body schema, simplicity is survival. If a shell function in
flake.nixor a 40-line helper incommon.pymisbehaves two years from now, you can open it in Neovim, read the entire mechanism on one screen, and fix it in thirty seconds.
2. The Physics of “Cut-and-Roll”: Why Many Short Livestreams Beat One Endless Stream
Your decision to switch from months-long marathon streams to Mode B (“Cut-and-Roll” — one themed YouTube Live broadcast per published article) is not just a workflow preference. It aligns with how YouTube’s recommendation engine and video indexer actually work under the hood:
| Dimension | The Marathon Stream (1 Stream for 30 Days) | Mode B: “Cut-and-Roll” (1 Stream per Article) |
|---|---|---|
| 1. The 12-Hour VOD Cliff | YouTube refuses to archive livestreams longer than 12 hours into permanent VODs. When a 2-week stream ends after a power blip, the recording vanishes into the void. | Publishing 1–3 articles a day means every stream runs ~4 to 12 hours. When you cut-and-roll, the previous stream automatically becomes a permanent, searchable YouTube VOD on your channel! |
| 2. Title & Semantic Indexing | A single generic title (“Honeybot Live”) matches zero specific search queries on YouTube or Google Video. | Every stream carries the exact semantic title and description of the new article (e.g., “LIVE: The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS”). |
| 3. Wave Interference (The Pebble in the Pond) | A single stream gets one notification/discovery impulse on Day 1, then decays exponentially to zero. | 1,500+ Pebbles = Standing Wave: Every publish drops a fresh live-event pebble into YouTube’s discovery graph, creating a compounding library of themed broadcasts. |
4. Synchronized Opening (BREAKING) |
Viewers clicking a VOD land at a random timestamp. | Because publish rings .reading_trigger on Honeybot (stream.py:617), Honeybot immediately interrupts and reads the brand-new article at the top of the new stream! |
Look at that last row (4. Synchronized Opening):
Your remotes/honeybot/hooks/post-receive hook already rings .reading_trigger, which makes stream.py preempt the narrator and start reading the newly published article first (score.get_script(breaking=True)).
So when you Cut-and-Roll around publish, the first 25 minutes of every new YouTube Live VOD is Piper TTS reading the exact article that the YouTube Title and Description name!
3. What the Live Receipts Showed (And How We Build Rung 1 Today + Rung 2 Next)
Let’s look at the three live receipts in this compile’s Manifest:
instructions.jsonReceipt:title:"The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS"(82 chars—fits inside YouTube’s 100-char limit even with"LIVE: "prepended: 88 chars!).desc:"From Jack Tramiel and the 1984 Atari-Amiga switch to TRIPOS, ARexx, CygnusEd, and Amiga-born Vim: how NixOS restores a 30-year phantom limb."tweet: Contains[ARTICLE_URL], whicharticleizer.py:268replaces withcanonical_url([https://mikelev.in/futureproof/](https://mikelev.in/futureproof/)...) when writing the.mdpost into~/repos/trimnoir/_posts/.
- Honeybot OBS Receipt (
2117079 obs/NO_OBS_WS_4455):- OBS is live on Honeybot (
pid 2117079), and port4455(obs-websocket) is currently not listening (NO_OBS_WS_4455). - In (1) PROBES below, we inspect
~/.config/obs-studio/plugin_config/obs-websocket/config.json(boolean/port keys only, no password) and~/.config/obs-studio/basic/profiles/*/basic.ini(StartStreaming/StopStreaminghotkeys) on Honeybot so we can see whether enablingobs-websocketor triggering an X11 hotkey onDISPLAY=:10.0is the cleanest 1-line remote OBS bounce for Rung 2.
- OBS is live on Honeybot (
What Lands in This Turn’s Patch Train (Rung 1 — The Go-Go-Gadget Broadcast Kit)
In Car 1 (scripts/articles/common.py & scripts/articles/articleizer.py) and Car 2 (flake.nix):
common.pygrowsget_broadcast_kit(target_key="1")and--broadcast {card,tweet,title,desc}:- Reads the newest published
.mdpost in Target 1 (viaget_last_published("1")or the newest file in~/repos/trimnoir/_posts/). - Extracts the resolved X.com Promo Tweet (with the real
[https://mikelev.in/futureproof/](https://mikelev.in/futureproof/)...URL), formats the YouTube Live Title (clamped to \(\le 100\) chars, stripping<and>which YouTube rejects), and builds the YouTube Live Description (leading with the clickable canonical URL above the fold, the article’smeta_descriptionandauthors_imprint, the Honeybot RFC 9110 observatory explainer, and the hashtags—also sanitized of<and>so YouTube Studio never throws a bracket error).
- Reads the newest published
flake.nixgrows four plain-English Inspector Gadget words (and callsytcardat the end ofpublish):ytcard— prints the full Broadcast Cue Card for the latest article.tweet— copies the X.com Promo Tweet to your clipboard and prints it.yttitle— copies the YouTube Live Title (\(\le 100\) chars) to your clipboard and prints it.ytdesc— copies the YouTube Live Description to your clipboard and prints it. (And incommon.py,xclipis invoked withstdout=subprocess.DEVNULL, stderr=subprocess.DEVNULLand only whensys.stdout.isatty(), soxclipnever holds a pipe open duringcompile!)
The 5-Car Train
(1) PROBES
Run this read-only BEFORE probe block to record the pre-patch state of common.py --broadcast, the gadget words in flake.nix, and Honeybot’s non-secret OBS WebSocket / hotkey configuration:
(nix) pipulate $
.venv/bin/python scripts/articles/common.py --broadcast card 2>/dev/null || echo "BROADCAST_KIT_ABSENT"
rg -n '^\s*(tweet|yttitle|ytdesc|ytcard)\(\)' flake.nix || echo "GADGET_WORDS_ABSENT"
ssh honeybot 'DISPLAY=:10.0 wmctrl -lx 2>/dev/null | grep -i obs || true; jq -r "{server_enabled, server_port, auth_required}" ~/.config/obs-studio/plugin_config/obs-websocket/config.json 2>/dev/null || echo NO_WS_CONFIG; grep -E "StartStreaming|StopStreaming" ~/.config/obs-studio/basic/profiles/*/basic.ini 2>/dev/null || echo NO_STREAM_HOTKEYS'
(2) NEXT CONTEXT
Paste this block into context.txt:
(nix) pipulate $
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
remotes/honeybot/hooks/post-receive
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/stream.py
! .venv/bin/python scripts/articles/common.py --broadcast card 2>/dev/null || echo "BROADCAST_KIT_ABSENT"
! rg -n '^\s*(tweet|yttitle|ytdesc|ytcard)\(\)' flake.nix || echo "GADGET_WORDS_ABSENT"
! ssh honeybot 'DISPLAY=:10.0 wmctrl -lx 2>/dev/null | grep -i obs || true; jq -r "{server_enabled, server_port, auth_required}" ~/.config/obs-studio/plugin_config/obs-websocket/config.json 2>/dev/null || echo NO_WS_CONFIG; grep -E "StartStreaming|StopStreaming" ~/.config/obs-studio/basic/profiles/*/basic.ini 2>/dev/null || echo NO_STREAM_HOTKEYS'
(3) PATCHES
Car 1 (Add the Broadcast Kit generator and --broadcast {card,tweet,title,desc} CLI to scripts/articles/common.py, and call print_broadcast_kit at the end of scripts/articles/articleizer.py):
Target: scripts/articles/common.py
[[[SEARCH]]]
def _write_text_preserving_mtime(path, text, preserve_mtime):
st = path.stat() if preserve_mtime else None
path.write_text(text, encoding="utf-8")
if st is not None:
os.utime(path, (st.st_atime, st.st_mtime))
[[[DIVIDER]]]
def _write_text_preserving_mtime(path, text, preserve_mtime):
st = path.stat() if preserve_mtime else None
path.write_text(text, encoding="utf-8")
if st is not None:
os.utime(path, (st.st_atime, st.st_mtime))
# ----------------------------------------------------------------------------
# Go-Go-Gadget Broadcast Kit (X.com Promo Tweet + YouTube Live Cut-and-Roll)
# ----------------------------------------------------------------------------
YT_TITLE_LIMIT = 100
def _yt_sanitize(text):
"""YouTube rejects '<' and '>' in both titles and descriptions."""
return (text or "").replace("<", "[").replace(">", "]")
def format_youtube_title(title):
"""Build a <=100-char YouTube Live title from the article title."""
clean = " ".join(_yt_sanitize(title).split())
if not clean:
return "LIVE: Honeybot AI Observatory"
prefixed = f"LIVE: {clean}"
if len(prefixed) <= YT_TITLE_LIMIT:
return prefixed
if len(clean) <= YT_TITLE_LIMIT:
return clean
cut = clean[: YT_TITLE_LIMIT - 3]
if " " in cut:
cut = cut.rsplit(" ", 1)[0]
return cut + "..."
def _latest_post_for_target(target_key="1"):
"""Resolve the newest markdown post for target_key (recorded or newest on disk)."""
targets = load_targets()
tcfg = targets.get(str(target_key), targets.get("1", DEFAULT_TARGETS["1"]))
posts_dir = Path(tcfg["path"]).expanduser()
candidates = sorted(posts_dir.glob("*.md")) if posts_dir.is_dir() else []
recorded = get_last_published(target_key)
if recorded and Path(recorded).is_file():
rec_path = Path(recorded)
if not candidates or rec_path.name >= candidates[-1].name:
return rec_path, tcfg
if candidates:
return candidates[-1], tcfg
return None, tcfg
def get_broadcast_kit(target_key="1"):
"""Extract {path, title, canonical_url, tweet, yt_title, yt_description} from the latest post."""
import yaml
post_path, tcfg = _latest_post_for_target(target_key)
if not post_path or not post_path.is_file():
return None
raw = post_path.read_text(encoding="utf-8")
fm = {}
body = raw
if raw.startswith("---"):
parts = raw.split("---", 2)
if len(parts) >= 3:
try:
fm = yaml.safe_load(parts[1]) or {}
except Exception:
fm = {}
body = parts[2]
title = str(fm.get("title") or post_path.stem)
base_url = (tcfg.get("base_url") or "https://mikelev.in").rstrip("/")
permalink = str(fm.get("permalink") or "")
canonical_url = str(fm.get("canonical_url") or (f"{base_url}{permalink}" if permalink else base_url))
meta_desc = str(fm.get("meta_description") or fm.get("excerpt") or "").strip()
imprint = str(fm.get("description") or "").strip()
tweet_match = re.search(
r"###\s*🐦\s*X\.com Promo Tweet\s*\n[triple-backtick][a-zA-Z0-9]*\n(.*?)\n[triple-backtick]",
body,
re.DOTALL,
)
if tweet_match:
tweet = tweet_match.group(1).strip()
else:
tweet = f"{title}\n\n{canonical_url}"
tags = " ".join(re.findall(r"#[A-Za-z0-9_]+", tweet))
yt_title = format_youtube_title(title)
desc_parts = [
f"📖 Read the full article: {canonical_url}",
"",
f"🎙️ Now reading live on the Honeybot AI Observatory: \"{title}\"",
]
if meta_desc:
desc_parts.extend(["", meta_desc])
if imprint and imprint != meta_desc:
desc_parts.extend(["", imprint])
desc_parts.extend([
"",
"---",
"🔭 About this 24/7 Home-Hosted NixOS Broadcast:",
"You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.",
"• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.",
"• Agents executing JavaScript trigger the DOM trapdoor sensor.",
f"• AI Context Manifest: {base_url}/llms.txt",
"• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai",
])
if tags:
desc_parts.extend(["", tags])
yt_description = _yt_sanitize("\n".join(desc_parts).strip())
return {
"path": str(post_path),
"title": title,
"canonical_url": canonical_url,
"tweet": tweet,
"yt_title": yt_title,
"yt_description": yt_description,
}
def _copy_to_clipboard(text):
"""Copy text to OS clipboard when interactive; detach xclip so pipes never hang."""
if not sys.stdout.isatty():
return False
import subprocess
cmd = ["pbcopy"] if sys.platform == "darwin" else ["xclip", "-selection", "clipboard"]
try:
subprocess.run(
cmd,
input=text.encode("utf-8"),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
return True
except Exception:
return False
def print_broadcast_kit(target_key="1", mode="card"):
"""Print or copy the broadcast kit (mode: card | tweet | title | desc)."""
kit = get_broadcast_kit(target_key)
if not kit:
print("⚠️ No published post found for broadcast kit.")
return 1
if mode == "tweet":
copied = _copy_to_clipboard(kit["tweet"])
print(kit["tweet"])
print(f"\n{'📋 Copied X.com Promo Tweet to clipboard!' if copied else 'ℹ️ (Non-TTY: printed only)'}")
return 0
if mode == "title":
copied = _copy_to_clipboard(kit["yt_title"])
print(kit["yt_title"])
print(f"\n{'📋 Copied YouTube Live Title' if copied else 'ℹ️ YouTube Live Title'} ({len(kit['yt_title'])}/{YT_TITLE_LIMIT} chars)")
return 0
if mode == "desc":
copied = _copy_to_clipboard(kit["yt_description"])
print(kit["yt_description"])
print(f"\n{'📋 Copied YouTube Live Description to clipboard!' if copied else 'ℹ️ (Non-TTY: printed only)'}")
return 0
bar = "=" * 72
thin = "-" * 72
print(f"\n{bar}")
print(f"📡 GO-GO-GADGET BROADCAST KIT ({Path(kit['path']).name})")
print(thin)
print("🐦 X.COM PROMO TWEET (type `tweet` to copy):")
print(kit["tweet"])
print(thin)
print(f"📺 YOUTUBE LIVE TITLE [{len(kit['yt_title'])}/{YT_TITLE_LIMIT} chars] (type `yttitle` to copy):")
print(kit["yt_title"])
print(thin)
print("📝 YOUTUBE LIVE DESCRIPTION (type `ytdesc` to copy):")
print(kit["yt_description"])
print(thin)
print("🎛️ Quick Copy Words: tweet | yttitle | ytdesc | ytcard")
print(bar)
return 0
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Pipulate article common utilities & broadcast kit.")
add_standard_arguments(parser)
parser.add_argument(
"--broadcast",
choices=("card", "tweet", "title", "desc"),
default="card",
help="Print the broadcast card or copy tweet/title/desc to clipboard.",
)
cli_args = parser.parse_args()
sys.exit(print_broadcast_kit(cli_args.target, cli_args.broadcast))
[[[REPLACE]]]
Target: scripts/articles/articleizer.py
[[[SEARCH]]]
if saved_path:
common.record_last_published(args.target, saved_path, target_config.get("name"))
else:
sys.exit(1)
[[[DIVIDER]]]
if saved_path:
common.record_last_published(args.target, saved_path, target_config.get("name"))
common.print_broadcast_kit(args.target, mode="card")
else:
sys.exit(1)
[[[REPLACE]]]
Car 2 (Wire the four Inspector Gadget shell functions ytcard, tweet, yttitle, and ytdesc into flake.nix beside preview(), and print the Broadcast Cue Card automatically at the end of publish()):
Target: flake.nix
[[[SEARCH]]]
# The true 'publish' command (Atomic Cross-Domain Deployment)
# It requires a commit message as an argument.
publish() {
[[[DIVIDER]]]
# GO-GO-GADGET BROADCAST WORDS (2026-10-15): one-word clipboard
# helpers for the X.com promo tweet and Mode B ("Cut-and-Roll")
# YouTube Live Title & Description derived from the newest post.
ytcard() { "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast card "$@"; }
tweet() { "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast tweet "$@"; }
yttitle() { "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast title "$@"; }
ytdesc() { "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast desc "$@"; }
# The true 'publish' command (Atomic Cross-Domain Deployment)
# It requires a commit message as an argument.
publish() {
[[[REPLACE]]]
Target: flake.nix
[[[SEARCH]]]
if [ "$REBOOT" -ne 1 ]; then
echo "⏭️ [5/5] Skipped -- stream.py left running. Pass --reboot to force the restart now."
echo "✅ Atomic Deployment Complete (stream untouched)."
return 0
fi
[[[DIVIDER]]]
if [ "$REBOOT" -ne 1 ]; then
echo "⏭️ [5/5] Skipped -- stream.py left running. Pass --reboot to force the restart now."
echo "✅ Atomic Deployment Complete (stream untouched)."
"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast card || true
return 0
fi
[[[REPLACE]]]
Target: flake.nix
[[[SEARCH]]]
echo "✅ Atomic Deployment Complete."
else
echo "⚠️ Warning: nixops.sh not found. Server config sync skipped."
fi
[[[DIVIDER]]]
echo "✅ Atomic Deployment Complete."
"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast card || true
else
echo "⚠️ Warning: nixops.sh not found. Server config sync skipped."
fi
[[[REPLACE]]]
Ignition: Run exit then nix develop if you want to type tweet, yttitle, ytdesc, or ytcard interactively right away; the ! probes in context.txt self-ignite against common.py and flake.nix at compile time.
(4) PROMPT
(nix) pipulate $
With the Go-Go-Gadget Broadcast Kit (`tweet`, `yttitle`, `ytdesc`, `ytcard`) live in `common.py`, `articleizer.py`, and `flake.nix`, let's inspect the AFTER receipt and the Honeybot OBS configuration receipt to see how we want to wire the remote OBS stop/start bounce.
(5) EXTERNAL DELIVERABLES
None. (Input archive sealed for this turn: qamy.ai_119-2ae2e07f.zip.)
MikeLev.in: Let’s unpack our upgrade from our Gemini guy in the chair.
THE AI-EDIT METHOD
Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.
1: BEFORE (PROBE):
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ .venv/bin/python scripts/articles/common.py --broadcast card 2>/dev/null || echo "BROADCAST_KIT_ABSENT"
rg -n '^\s*(tweet|yttitle|ytdesc|ytcard)\(\)' flake.nix || echo "GADGET_WORDS_ABSENT"
ssh honeybot 'DISPLAY=:10.0 wmctrl -lx 2>/dev/null | grep -i obs || true; jq -r "{server_enabled, server_port, auth_required}" ~/.config/obs-studio/plugin_config/obs-websocket/config.json 2>/dev/null || echo NO_WS_CONFIG; grep -E "StartStreaming|StopStreaming" ~/.config/obs-studio/basic/profiles/*/basic.ini 2>/dev/null || echo NO_STREAM_HOTKEYS'
GADGET_WORDS_ABSENT
0x01e00007 0 obs.obs honeybot OBS 32.0.1 - Profile: Untitled - Scenes: Untitled
NO_WS_CONFIG
NO_STREAM_HOTKEYS
(nix) pipulate $
2: AFTER (NEXT CONTEXT):
# # Context 1
# # # THE JEKYLL PUBLISHING PIPELINE
# ~/repos/nixos/blogs.nix
# ~/.config/pipulate/blogs.json
# scripts/articles/publishizer.py
# scripts/articles/common.py
# scripts/articles/articleizer.py
# scripts/articles/build_knowledge_graph.py
# scripts/articles/editing_prompt.txt
# scripts/articles/sanitizer.py
# scripts/articles/contextualizer.py
# scripts/articles/generate_llms_txt.py
# scripts/articles/generate_post_nav.py
#
# # # OUR HONEYBOT AI OBSERVATORY
# nixops.sh
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# remotes/honeybot/scripts/score.py
# remotes/honeybot/scripts/card.py
# remotes/honeybot/scripts/forest.py
# remotes/honeybot/scripts/test_forest.py
# remotes/honeybot/scripts/logs.py
# remotes/honeybot/scripts/content_loader.py
# remotes/honeybot/scripts/db.py
# # Context 2
# flake.nix
# scripts/articles/articleizer.py
# scripts/articles/publishizer.py
# scripts/articles/common.py
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# ! rg -n -A 40 'publish\(\) \{' flake.nix
# ! jq -r '{title: .editing_instructions.yaml_updates.title, desc: .editing_instructions.yaml_updates.description, tweet: .book_analysis_content.promotional_tweet}' scripts/articles/instructions.json 2>/dev/null || echo "NO_INSTRUCTIONS_JSON"
# ! ssh honeybot 'pgrep -af obs || echo OBS_NOT_RUNNING; ss -tlnp 2>/dev/null | grep 4455 || echo NO_OBS_WS_4455'
# Context 3
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
remotes/honeybot/hooks/post-receive
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/stream.py
! .venv/bin/python scripts/articles/common.py --broadcast card 2>/dev/null || echo "BROADCAST_KIT_ABSENT"
! rg -n '^\s*(tweet|yttitle|ytdesc|ytcard)\(\)' flake.nix || echo "GADGET_WORDS_ABSENT"
! ssh honeybot 'DISPLAY=:10.0 wmctrl -lx 2>/dev/null | grep -i obs || true; jq -r "{server_enabled, server_port, auth_required}" ~/.config/obs-studio/plugin_config/obs-websocket/config.json 2>/dev/null || echo NO_WS_CONFIG; grep -E "StartStreaming|StopStreaming" ~/.config/obs-studio/basic/profiles/*/basic.ini 2>/dev/null || echo NO_STREAM_HOTKEYS'
3: CHANGE (PATCHES):
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/articles/common.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/articles/articleizer.py'.
(nix) pipulate $ d
diff --git a/scripts/articles/articleizer.py b/scripts/articles/articleizer.py
index f7b6e21b..170f5bb0 100644
--- a/scripts/articles/articleizer.py
+++ b/scripts/articles/articleizer.py
@@ -861,6 +861,7 @@ def main():
target_config=target_config)
if saved_path:
common.record_last_published(args.target, saved_path, target_config.get("name"))
+ common.print_broadcast_kit(args.target, mode="card")
else:
sys.exit(1)
diff --git a/scripts/articles/common.py b/scripts/articles/common.py
index 77ead63a..d27c1b9d 100644
--- a/scripts/articles/common.py
+++ b/scripts/articles/common.py
@@ -259,3 +259,184 @@ def _write_text_preserving_mtime(path, text, preserve_mtime):
path.write_text(text, encoding="utf-8")
if st is not None:
os.utime(path, (st.st_atime, st.st_mtime))
+
+
+# ----------------------------------------------------------------------------
+# Go-Go-Gadget Broadcast Kit (X.com Promo Tweet + YouTube Live Cut-and-Roll)
+# ----------------------------------------------------------------------------
+YT_TITLE_LIMIT = 100
+
+
+def _yt_sanitize(text):
+ """YouTube rejects '<' and '>' in both titles and descriptions."""
+ return (text or "").replace("<", "[").replace(">", "]")
+
+
+def format_youtube_title(title):
+ """Build a <=100-char YouTube Live title from the article title."""
+ clean = " ".join(_yt_sanitize(title).split())
+ if not clean:
+ return "LIVE: Honeybot AI Observatory"
+ prefixed = f"LIVE: {clean}"
+ if len(prefixed) <= YT_TITLE_LIMIT:
+ return prefixed
+ if len(clean) <= YT_TITLE_LIMIT:
+ return clean
+ cut = clean[: YT_TITLE_LIMIT - 3]
+ if " " in cut:
+ cut = cut.rsplit(" ", 1)[0]
+ return cut + "..."
+
+
+def _latest_post_for_target(target_key="1"):
+ """Resolve the newest markdown post for target_key (recorded or newest on disk)."""
+ targets = load_targets()
+ tcfg = targets.get(str(target_key), targets.get("1", DEFAULT_TARGETS["1"]))
+ posts_dir = Path(tcfg["path"]).expanduser()
+ candidates = sorted(posts_dir.glob("*.md")) if posts_dir.is_dir() else []
+ recorded = get_last_published(target_key)
+ if recorded and Path(recorded).is_file():
+ rec_path = Path(recorded)
+ if not candidates or rec_path.name >= candidates[-1].name:
+ return rec_path, tcfg
+ if candidates:
+ return candidates[-1], tcfg
+ return None, tcfg
+
+
+def get_broadcast_kit(target_key="1"):
+ """Extract {path, title, canonical_url, tweet, yt_title, yt_description} from the latest post."""
+ import yaml
+ post_path, tcfg = _latest_post_for_target(target_key)
+ if not post_path or not post_path.is_file():
+ return None
+ raw = post_path.read_text(encoding="utf-8")
+ fm = {}
+ body = raw
+ if raw.startswith("---"):
+ parts = raw.split("---", 2)
+ if len(parts) >= 3:
+ try:
+ fm = yaml.safe_load(parts[1]) or {}
+ except Exception:
+ fm = {}
+ body = parts[2]
+ title = str(fm.get("title") or post_path.stem)
+ base_url = (tcfg.get("base_url") or "https://mikelev.in").rstrip("/")
+ permalink = str(fm.get("permalink") or "")
+ canonical_url = str(fm.get("canonical_url") or (f"{base_url}{permalink}" if permalink else base_url))
+ meta_desc = str(fm.get("meta_description") or fm.get("excerpt") or "").strip()
+ imprint = str(fm.get("description") or "").strip()
+ tweet_match = re.search(
+ r"###\s*🐦\s*X\.com Promo Tweet\s*\n[triple-backtick][a-zA-Z0-9]*\n(.*?)\n[triple-backtick]",
+ body,
+ re.DOTALL,
+ )
+ if tweet_match:
+ tweet = tweet_match.group(1).strip()
+ else:
+ tweet = f"{title}\n\n{canonical_url}"
+ tags = " ".join(re.findall(r"#[A-Za-z0-9_]+", tweet))
+ yt_title = format_youtube_title(title)
+ desc_parts = [
+ f"📖 Read the full article: {canonical_url}",
+ "",
+ f"🎙️ Now reading live on the Honeybot AI Observatory: \"{title}\"",
+ ]
+ if meta_desc:
+ desc_parts.extend(["", meta_desc])
+ if imprint and imprint != meta_desc:
+ desc_parts.extend(["", imprint])
+ desc_parts.extend([
+ "",
+ "---",
+ "🔭 About this 24/7 Home-Hosted NixOS Broadcast:",
+ "You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.",
+ "• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.",
+ "• Agents executing JavaScript trigger the DOM trapdoor sensor.",
+ f"• AI Context Manifest: {base_url}/llms.txt",
+ "• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai",
+ ])
+ if tags:
+ desc_parts.extend(["", tags])
+ yt_description = _yt_sanitize("\n".join(desc_parts).strip())
+ return {
+ "path": str(post_path),
+ "title": title,
+ "canonical_url": canonical_url,
+ "tweet": tweet,
+ "yt_title": yt_title,
+ "yt_description": yt_description,
+ }
+
+
+def _copy_to_clipboard(text):
+ """Copy text to OS clipboard when interactive; detach xclip so pipes never hang."""
+ if not sys.stdout.isatty():
+ return False
+ import subprocess
+ cmd = ["pbcopy"] if sys.platform == "darwin" else ["xclip", "-selection", "clipboard"]
+ try:
+ subprocess.run(
+ cmd,
+ input=text.encode("utf-8"),
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ )
+ return True
+ except Exception:
+ return False
+
+
+def print_broadcast_kit(target_key="1", mode="card"):
+ """Print or copy the broadcast kit (mode: card | tweet | title | desc)."""
+ kit = get_broadcast_kit(target_key)
+ if not kit:
+ print("⚠️ No published post found for broadcast kit.")
+ return 1
+ if mode == "tweet":
+ copied = _copy_to_clipboard(kit["tweet"])
+ print(kit["tweet"])
+ print(f"\n{'📋 Copied X.com Promo Tweet to clipboard!' if copied else 'ℹ️ (Non-TTY: printed only)'}")
+ return 0
+ if mode == "title":
+ copied = _copy_to_clipboard(kit["yt_title"])
+ print(kit["yt_title"])
+ print(f"\n{'📋 Copied YouTube Live Title' if copied else 'ℹ️ YouTube Live Title'} ({len(kit['yt_title'])}/{YT_TITLE_LIMIT} chars)")
+ return 0
+ if mode == "desc":
+ copied = _copy_to_clipboard(kit["yt_description"])
+ print(kit["yt_description"])
+ print(f"\n{'📋 Copied YouTube Live Description to clipboard!' if copied else 'ℹ️ (Non-TTY: printed only)'}")
+ return 0
+ bar = "=" * 72
+ thin = "-" * 72
+ print(f"\n{bar}")
+ print(f"📡 GO-GO-GADGET BROADCAST KIT ({Path(kit['path']).name})")
+ print(thin)
+ print("🐦 X.COM PROMO TWEET (type `tweet` to copy):")
+ print(kit["tweet"])
+ print(thin)
+ print(f"📺 YOUTUBE LIVE TITLE [{len(kit['yt_title'])}/{YT_TITLE_LIMIT} chars] (type `yttitle` to copy):")
+ print(kit["yt_title"])
+ print(thin)
+ print("📝 YOUTUBE LIVE DESCRIPTION (type `ytdesc` to copy):")
+ print(kit["yt_description"])
+ print(thin)
+ print("🎛️ Quick Copy Words: tweet | yttitle | ytdesc | ytcard")
+ print(bar)
+ return 0
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Pipulate article common utilities & broadcast kit.")
+ add_standard_arguments(parser)
+ parser.add_argument(
+ "--broadcast",
+ choices=("card", "tweet", "title", "desc"),
+ default="card",
+ help="Print the broadcast card or copy tweet/title/desc to clipboard.",
+ )
+ cli_args = parser.parse_args()
+ sys.exit(print_broadcast_kit(cli_args.target, cli_args.broadcast))
(nix) pipulate $ m
📝 Committing: chore: Implement broadcast kit functionality with YouTube live title and description generation
[main c1955e81] chore: Implement broadcast kit functionality with YouTube live title and description generation
2 files changed, 182 insertions(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'flake.nix'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'flake.nix'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'flake.nix'.
(nix) pipulate $ d
diff --git a/flake.nix b/flake.nix
index bc061226..7fce0b5a 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2437,6 +2437,14 @@ print('AI:\n', r.ai)
postsc2() { postsc --reverse "$@"; }
preview() { (cd "$PIPULATE_ROOT/scripts/articles" && python publishizer.py "$@"); }
+ # GO-GO-GADGET BROADCAST WORDS (2026-10-15): one-word clipboard
+ # helpers for the X.com promo tweet and Mode B ("Cut-and-Roll")
+ # YouTube Live Title & Description derived from the newest post.
+ ytcard() { "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast card "$@"; }
+ tweet() { "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast tweet "$@"; }
+ yttitle() { "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast title "$@"; }
+ ytdesc() { "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast desc "$@"; }
+
# The true 'publish' command (Atomic Cross-Domain Deployment)
# It requires a commit message as an argument.
publish() {
@@ -2536,6 +2544,7 @@ print('AI:\n', r.ai)
if [ "$REBOOT" -ne 1 ]; then
echo "⏭️ [5/5] Skipped -- stream.py left running. Pass --reboot to force the restart now."
echo "✅ Atomic Deployment Complete (stream untouched)."
+ "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast card || true
return 0
fi
echo "🚀 [5/5] Stream Refresh: Restarting Honeybot slideshow child..."
@@ -2595,6 +2604,7 @@ print('AI:\n', r.ai)
fi
'
echo "✅ Atomic Deployment Complete."
+ "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/articles/common.py" --broadcast card || true
else
echo "⚠️ Warning: nixops.sh not found. Server config sync skipped."
fi
(nix) pipulate $ m
📝 Committing: chore: Add utility functions for broadcast commands in flake.nix
[main 10e44b6f] chore: Add utility functions for broadcast commands in flake.nix
1 file changed, 10 insertions(+)
(nix) pipulate $ git push
Enumerating objects: 15, done.
Counting objects: 100% (15/15), done.
Delta compression using up to 48 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 3.90 KiB | 3.90 MiB/s, done.
Total 9 (delta 7), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (7/7), completed with 6 local objects.
To github.com:pipulate/pipulate
03b344ef..10e44b6f main -> main
(nix) pipulate $
Ignition: often “exit”, “nix develop”. Instructions may vary.
Ohhhh this is an interesting ignition now. First we update our command line environment. And then we can test the commands. Interesting! Do we use parameters? Does it just use the most recent article. Help me get a hang of teh ropes here. This article I’m making right now will likely be the first one it gets used on. And there’s inevitably going to be some sort of timing issues to work out.
Testing the Four Broadcast Words and Banishing template.md
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ exit
exit
(sys) pipulate $ ndq
(nix) pipulate $ tweet
template
https://mikelev.in/futureproof/foo/
📋 Copied X.com Promo Tweet to clipboard!
(nix) pipulate $ yttitle
LIVE: template
📋 Copied YouTube Live Title (14/100 chars)
(nix) pipulate $ ytdesc
📖 Read the full article: https://mikelev.in/futureproof/foo/
🎙️ Now reading live on the Honeybot AI Observatory: "template"
---
🔭 About this 24/7 Home-Hosted NixOS Broadcast:
You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.
• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.
• Agents executing JavaScript trigger the DOM trapdoor sensor.
• AI Context Manifest: https://mikelev.in/llms.txt
• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai
📋 Copied YouTube Live Description to clipboard!
(nix) pipulate $ ytcard
========================================================================
📡 GO-GO-GADGET BROADCAST KIT (template.md)
------------------------------------------------------------------------
🐦 X.COM PROMO TWEET (type `tweet` to copy):
template
https://mikelev.in/futureproof/foo/
------------------------------------------------------------------------
📺 YOUTUBE LIVE TITLE [14/100 chars] (type `yttitle` to copy):
LIVE: template
------------------------------------------------------------------------
📝 YOUTUBE LIVE DESCRIPTION (type `ytdesc` to copy):
📖 Read the full article: https://mikelev.in/futureproof/foo/
🎙️ Now reading live on the Honeybot AI Observatory: "template"
---
🔭 About this 24/7 Home-Hosted NixOS Broadcast:
You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.
• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.
• Agents executing JavaScript trigger the DOM trapdoor sensor.
• AI Context Manifest: https://mikelev.in/llms.txt
• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai
------------------------------------------------------------------------
🎛️ Quick Copy Words: tweet | yttitle | ytdesc | ytcard
========================================================================
(nix) pipulate $
4: PROMPT:
With the Go-Go-Gadget Broadcast Kit (tweet, yttitle, ytdesc, ytcard) live in common.py, articleizer.py, and flake.nix, let’s inspect the AFTER receipt and the Honeybot OBS configuration receipt to see how we want to wire the remote OBS stop/start bounce.
5: DELIVERABLE:
A new article publishing process that compels me to get out the tweet and to create a new VOD? Is that the terminology here. I might need a bit of general YouTube education surrounding this Live-streaming stuff, API options and all that. Did we do API stuff? Do I need to use parameters and arguments with those commands? Is one of those commands the one with the magic ssh restart in there that actually cuts the last VOD and starts the next? What should I expect here?
Gemini 4 Argon High: Context verified.
TL;DR: Your first test flight of tweet, yttitle, ytdesc, and ytcard worked end-to-end—and the live receipt immediately caught a lexicographical bug: ~/repos/trimnoir/_posts/ contains an undated file named template.md, and because 't' (0x74) sorts after '2' (0x32) in ASCII, sorted(posts_dir.glob("*.md"))[-1] picked template.md instead of 2026-10-15-*.md! Below, we fix _latest_post_for_target in common.py so it only scans dated YYYY-MM-DD-*.md posts (ordered by date and sort_order), and we answer every one of your questions about VODs, parameters, APIs, and how the OBS / YouTube Live “Cut-and-Roll” seam actually works.
1. What the Receipt Just Caught: Why template.md Beat 2026-10-15
Look at your live receipt from the ndq test:
📡 GO-GO-GADGET BROADCAST KIT (template.md)
Why did _latest_post_for_target() grab template.md instead of the Phantom Limb article you published today?
Because sorted(posts_dir.glob("*.md")) sorts filenames alphabetically by ASCII code:
- Every real post starts with a year:
2026-10-15-...('2'is ASCII50). - Sitting inside
~/repos/trimnoir/_posts/is a boilerplate file namedtemplate.md('t'is ASCII116). - In ASCII,
't' > '2', sotemplate.mdsat at the very end of the list (candidates[-1]), andrec_path.name >= 'template.md'evaluated toFalse!
Even more important: on days when you publish two or three articles (sort_order: 1, sort_order: 2, sort_order: 3), a plain alphabetical sort of 2026-10-15-*.md sorts same-day files by their slug (a-slug before z-slug), not by sort_order.
In Car 1 below, we upgrade _latest_post_for_target() in scripts/articles/common.py so that:
- It filters strictly to dated posts (
^\d{4}-\d{2}-\d{2}-.+\.md$), banishingtemplate.mdforever. - When multiple posts exist on the latest date, it honors
last_published.json(the exact filearticlejust wrote) and sorts same-day posts by(date, sort_order, mtime). - It adds an optional
--slug <substring>flag in case you ever want to pull the kit for a specific older post.
2. Getting the Hang of the Ropes: Your 5 Questions Answered Plainly
Q1: Do I need parameters or arguments with tweet, yttitle, ytdesc, or ytcard?
No. In your daily workflow, you type them completely bare:
tweet$\rightarrow$ copies the X.com promo tweet (with URL & hashtags) for your newest article to the clipboard.yttitle$\rightarrow$ copies theLIVE: ...YouTube title (\(\le 100\) chars) to the clipboard.ytdesc$\rightarrow$ copies the full YouTube description to the clipboard.ytcard$\rightarrow$ prints all three on screen without touching your clipboard.
(Optional power-user flags: because "$@" passes through, you can also type tweet -t pip for another blog target, or ytcard --slug phantom after Car 1 lands to pull a specific article by slug substring.)
Q2: What is a “VOD”?
VOD stands for Video On Demand—YouTube’s term for a finished livestream after it ends and turns into a normal, permanent, replayable YouTube video on your channel.
- Why you didn’t get VODs before: YouTube has a hard 12-hour ceiling on archiving livestreams. When you ran a single stream for weeks at a time, YouTube threw the recording away when it ended.
- Why Mode B (“Cut-and-Roll”) changes everything: Because you publish every few hours (or once a day), each stream stays under 12 hours. The moment you end Stream #1 to start Stream #2, Stream #1 automatically becomes a permanent, searchable VOD on your YouTube channel with its article-specific title and description!
Q3: Did we do any YouTube API stuff yet?
No. Right now, Rung 1 (tweet, yttitle, ytdesc, ytcard) is **100% local Python + xclip**. It reads your local Markdown file on disk and puts the text into your system clipboard. It touches zero external APIs, needs zero OAuth tokens, and cannot fail due to cloud rate limits or expired credentials.
Q4: Is one of those commands the magic SSH restart that cuts the last VOD and starts the next?
Not yet—and here is the critical piece of YouTube Live physics you need to know before we wire that button:
WHAT HAPPENS WHEN YOU ONLY BOUNCE OBS (Stop -> Start 5s later):
===============================================================
[OBS on Honeybot] --(Stops 5s, Starts)--> [YouTube Ingestion Server]
"Oh, Mike's internet blipped for
5 seconds! Keep the SAME stream
alive; do NOT cut a new VOD."
WHAT ACTUALLY CUTS A VOD AND STARTS A NEW STREAM ("Cut-and-Roll"):
==================================================================
Step A: Tell YouTube "END STREAM" (or update Title/Desc in YouTube Studio).
└──> YouTube seals the old broadcast into a permanent VOD!
Step B: Click "NEW STREAM / GO LIVE" in YouTube Studio (or via YouTube API)
and paste `yttitle` + `ytdesc`.
Step C: If OBS stopped sending packets during the cutover, bounce OBS
(`Stop Streaming` -> `Start Streaming`) so RTMP locks onto the new room.
Look closely at that diagram:
- Why did stopping and starting OBS start a new stream after your apartment lost power? Because during the power outage, OBS stopped sending video for several minutes, so YouTube timed out and closed the old broadcast. Then, when you went to YouTube in your browser and clicked “Start Live-streaming”, YouTube created a new Broadcast room listening on your same reusable Stream Key!
- If OBS is currently streaming and you only stop/start OBS over SSH for 5 seconds without clicking “End Stream” in YouTube Studio, YouTube assumes it was a brief Wi-Fi hiccup and keeps you in the old stream with the old title.
Q5: What Should My Exact Workflow Be Right Now (On This Article)?
When you finish this article and run article and publish "msg", here is the exact 45-second tactile loop:
- Run
articlethenpreviewthenpublish "msg"as usual. At the end ofpublish, the Go-Go-Gadget Broadcast Kit automatically prints on your terminal, and Honeybot’spost-receivehook rings.reading_triggerso Honeybot immediately starts reading your new article aloud. - Post the Tweet:
Type
tweetin your terminal $\rightarrow$ Alt+Tab to X.com $\rightarrow$Ctrl+V$\rightarrow$ Post. - Update / Roll YouTube Live (
studio.youtube.com$\rightarrow$ **Go Live):**- If you just want to update the current stream’s Title & Description in place (10 seconds, zero stream interruption):
Click Edit on your live stream in YouTube Studio $\rightarrow$ type
yttitlein terminal and paste into Title $\rightarrow$ typeytdescin terminal and paste into Description $\rightarrow$ click Save. (You don’t even have to touch OBS!) - If you want to Cut-and-Roll a fresh VOD (30 seconds):
In YouTube Studio Live Control Room, click End Stream (this banks the old VOD) $\rightarrow$ click Manage / New Stream (Reuse settings) $\rightarrow$ paste
yttitleandytdesc$\rightarrow$ click Create / Go Live.
- If you just want to update the current stream’s Title & Description in place (10 seconds, zero stream interruption):
Click Edit on your live stream in YouTube Studio $\rightarrow$ type
3. Preparing Rung 2: Inspecting Honeybot’s OBS Config for Remote Bounce
Look at the third receipt from your compile:
0x01e00007 0 obs.obs honeybot OBS 32.0.1 - Profile: Untitled - Scenes: Untitled
NO_WS_CONFIG
NO_STREAM_HOTKEYS
OBS 32.0.1 is running on Honeybot’s X11 display :10.0 (window 0x01e00007, Profile Untitled).
In (1) PROBES below, we run two read-only probes:
- Verify that
common.py --broadcast cardnow picks your real2026-10-15Phantom Limb article instead oftemplate.md. - List the non-secret config files inside
~/.config/obs-studio/on Honeybot (find ~/.config/obs-studio -maxdepth 3 -not -name "*service*") so we can see the exact path tobasic.ini/global.iniif we want to add a 1-commandobsbounceSSH helper next.
The 5-Car Train
(1) PROBES
(nix) pipulate $
.venv/bin/python scripts/articles/common.py --broadcast card
ssh honeybot 'find ~/.config/obs-studio -maxdepth 3 -not -name "*service*" | sort'
(2) NEXT CONTEXT
Paste this block into context.txt:
(nix) pipulate $
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
remotes/honeybot/hooks/post-receive
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/stream.py
! .venv/bin/python scripts/articles/common.py --broadcast card
! ssh honeybot 'find ~/.config/obs-studio -maxdepth 3 -not -name "*service*" | sort'
(3) PATCHES
Car 1 (Filter _latest_post_for_target in scripts/articles/common.py to dated YYYY-MM-DD-*.md posts so template.md is ignored, sort same-day posts by (date, sort_order, mtime), and support optional --slug <substring> lookup):
Target: scripts/articles/common.py
[[[SEARCH]]]
def _latest_post_for_target(target_key="1"):
"""Resolve the newest markdown post for target_key (recorded or newest on disk)."""
targets = load_targets()
tcfg = targets.get(str(target_key), targets.get("1", DEFAULT_TARGETS["1"]))
posts_dir = Path(tcfg["path"]).expanduser()
candidates = sorted(posts_dir.glob("*.md")) if posts_dir.is_dir() else []
recorded = get_last_published(target_key)
if recorded and Path(recorded).is_file():
rec_path = Path(recorded)
if not candidates or rec_path.name >= candidates[-1].name:
return rec_path, tcfg
if candidates:
return candidates[-1], tcfg
return None, tcfg
[[[DIVIDER]]]
_DATED_POST_RE = re.compile(r"^\d{4}-\d{2}-\d{2}-.+\.md$")
def _post_sort_key(md_path):
"""Sort dated posts by (YYYY-MM-DD, frontmatter sort_order, mtime)."""
date_part = md_path.name[:10]
sort_order = 0
try:
head = md_path.read_text(encoding="utf-8")[:2048]
if head.startswith("---"):
m = re.search(r"^sort_order:\s*(\d+)", head, re.MULTILINE)
if m:
sort_order = int(m.group(1))
except Exception:
pass
try:
mtime = md_path.stat().st_mtime
except OSError:
mtime = 0.0
return (date_part, sort_order, mtime, md_path.name)
def _latest_post_for_target(target_key="1", slug_match=None):
"""Resolve the newest dated markdown post for target_key (or matching slug_match)."""
targets = load_targets()
tcfg = targets.get(str(target_key), targets.get("1", DEFAULT_TARGETS["1"]))
posts_dir = Path(tcfg["path"]).expanduser()
dated = [
p for p in (posts_dir.glob("*.md") if posts_dir.is_dir() else [])
if _DATED_POST_RE.match(p.name)
]
if slug_match:
needle = slug_match.lower().strip()
dated = [p for p in dated if needle in p.name.lower()]
if not dated:
return None, tcfg
dated.sort(key=_post_sort_key)
return dated[-1], tcfg
if not dated:
return None, tcfg
max_date = max(p.name[:10] for p in dated)
recorded = get_last_published(target_key)
if recorded and Path(recorded).is_file():
rec_path = Path(recorded)
if _DATED_POST_RE.match(rec_path.name) and rec_path.name[:10] >= max_date:
return rec_path, tcfg
same_day = [p for p in dated if p.name[:10] == max_date]
same_day.sort(key=_post_sort_key)
return same_day[-1], tcfg
[[[REPLACE]]]
Target: scripts/articles/common.py
[[[SEARCH]]]
def get_broadcast_kit(target_key="1"):
"""Extract {path, title, canonical_url, tweet, yt_title, yt_description} from the latest post."""
import yaml
post_path, tcfg = _latest_post_for_target(target_key)
[[[DIVIDER]]]
def get_broadcast_kit(target_key="1", slug_match=None):
"""Extract {path, title, canonical_url, tweet, yt_title, yt_description} from the latest post."""
import yaml
post_path, tcfg = _latest_post_for_target(target_key, slug_match=slug_match)
[[[REPLACE]]]
Target: scripts/articles/common.py
[[[SEARCH]]]
def print_broadcast_kit(target_key="1", mode="card"):
"""Print or copy the broadcast kit (mode: card | tweet | title | desc)."""
kit = get_broadcast_kit(target_key)
[[[DIVIDER]]]
def print_broadcast_kit(target_key="1", mode="card", slug_match=None):
"""Print or copy the broadcast kit (mode: card | tweet | title | desc)."""
kit = get_broadcast_kit(target_key, slug_match=slug_match)
[[[REPLACE]]]
Target: scripts/articles/common.py
[[[SEARCH]]]
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Pipulate article common utilities & broadcast kit.")
add_standard_arguments(parser)
parser.add_argument(
"--broadcast",
choices=("card", "tweet", "title", "desc"),
default="card",
help="Print the broadcast card or copy tweet/title/desc to clipboard.",
)
cli_args = parser.parse_args()
sys.exit(print_broadcast_kit(cli_args.target, cli_args.broadcast))
[[[DIVIDER]]]
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Pipulate article common utilities & broadcast kit.")
add_standard_arguments(parser)
parser.add_argument(
"--broadcast",
choices=("card", "tweet", "title", "desc"),
default="card",
help="Print the broadcast card or copy tweet/title/desc to clipboard.",
)
parser.add_argument(
"-s", "--slug",
type=str,
default=None,
help="Optional substring to match a specific dated post filename.",
)
cli_args = parser.parse_args()
sys.exit(print_broadcast_kit(cli_args.target, cli_args.broadcast, slug_match=cli_args.slug))
[[[REPLACE]]]
Ignition: no ignition required (tweet, yttitle, ytdesc, ytcard, and the ! probe in context.txt invoke scripts/articles/common.py fresh on every call).
(4) PROMPT
(nix) pipulate $
Verify the AFTER receipt from `common.py --broadcast card` (confirming it now pulls the latest dated article instead of `template.md`) and inspect Honeybot's `~/.config/obs-studio` layout.
(5) EXTERNAL DELIVERABLES
None. (Input archive sealed for this turn: qamy.ai_120-59260c8d.zip.)
MikeLev.in: Okay I think I see. It’s mostly manual as I planned with helpers to update title and description on each new video. I stop live-streaming, change the stuff on the live-stream fields for that same broadcast key (recycled form) and then start streaming again. Simple as that. I see. And this 5-Car Train are the follow-up fixes to nail this baseline first-round manual process.
Got it!
THE AI-EDIT METHOD
Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.
1: BEFORE (PROBE):
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ .venv/bin/python scripts/articles/common.py --broadcast card
ssh honeybot 'find ~/.config/obs-studio -maxdepth 3 -not -name "*service*" | sort'
========================================================================
📡 GO-GO-GADGET BROADCAST KIT (template.md)
------------------------------------------------------------------------
🐦 X.COM PROMO TWEET (type `tweet` to copy):
template
https://mikelev.in/futureproof/foo/
------------------------------------------------------------------------
📺 YOUTUBE LIVE TITLE [14/100 chars] (type `yttitle` to copy):
LIVE: template
------------------------------------------------------------------------
📝 YOUTUBE LIVE DESCRIPTION (type `ytdesc` to copy):
📖 Read the full article: https://mikelev.in/futureproof/foo/
🎙️ Now reading live on the Honeybot AI Observatory: "template"
---
🔭 About this 24/7 Home-Hosted NixOS Broadcast:
You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.
• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.
• Agents executing JavaScript trigger the DOM trapdoor sensor.
• AI Context Manifest: https://mikelev.in/llms.txt
• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai
------------------------------------------------------------------------
🎛️ Quick Copy Words: tweet | yttitle | ytdesc | ytcard
========================================================================
/home/mike/.config/obs-studio
/home/mike/.config/obs-studio/basic
/home/mike/.config/obs-studio/basic/profiles
/home/mike/.config/obs-studio/basic/profiles/Untitled
/home/mike/.config/obs-studio/basic/scenes
/home/mike/.config/obs-studio/basic/scenes/Untitled.json
/home/mike/.config/obs-studio/basic/scenes/Untitled.json.bak
/home/mike/.config/obs-studio/global.ini
/home/mike/.config/obs-studio/logs
/home/mike/.config/obs-studio/logs/2026-05-03 13-11-25.txt
/home/mike/.config/obs-studio/logs/2026-05-16 21-29-37.txt
/home/mike/.config/obs-studio/logs/2026-06-13 11-28-27.txt
/home/mike/.config/obs-studio/logs/2026-06-13 11-38-17.txt
/home/mike/.config/obs-studio/logs/2026-09-24 20-46-39.txt
/home/mike/.config/obs-studio/logs/2026-09-25 22-15-48.txt
/home/mike/.config/obs-studio/logs/2026-09-25 22-25-33.txt
/home/mike/.config/obs-studio/logs/2026-10-02 05-46-16.txt
/home/mike/.config/obs-studio/logs/2026-10-03 21-18-07.txt
/home/mike/.config/obs-studio/logs/2026-10-08 14-48-13.txt
/home/mike/.config/obs-studio/plugin_config
/home/mike/.config/obs-studio/plugin_config/obs-websocket
/home/mike/.config/obs-studio/plugin_config/obs-websocket/config.json
/home/mike/.config/obs-studio/plugin_config/rtmp-services/amazon_ivs_ingests.json
/home/mike/.config/obs-studio/plugin_config/rtmp-services/package.json
/home/mike/.config/obs-studio/plugin_config/rtmp-services/twitch_ingests.json
/home/mike/.config/obs-studio/plugin_config/text-freetype2
/home/mike/.config/obs-studio/plugin_manager
/home/mike/.config/obs-studio/plugin_manager/modules.json
/home/mike/.config/obs-studio/profiler_data
/home/mike/.config/obs-studio/profiler_data/2026-01-07 05-24-44.csv.gz
/home/mike/.config/obs-studio/profiler_data/2026-01-07 05-32-26.csv.gz
/home/mike/.config/obs-studio/profiler_data/2026-01-07 05-40-35.csv.gz
/home/mike/.config/obs-studio/profiler_data/2026-03-08 04-45-44.csv.gz
/home/mike/.config/obs-studio/profiler_data/2026-03-08 04-50-41.csv.gz
/home/mike/.config/obs-studio/profiler_data/2026-06-13 11-28-27.csv.gz
/home/mike/.config/obs-studio/profiler_data/2026-09-25 22-15-48.csv.gz
/home/mike/.config/obs-studio/profiler_data/2026-09-25 22-25-33.csv.gz
/home/mike/.config/obs-studio/profiler_data/2026-10-02 05-46-16.csv.gz
/home/mike/.config/obs-studio/profiler_data/2026-10-03 21-18-07.csv.gz
/home/mike/.config/obs-studio/.sentinel
/home/mike/.config/obs-studio/updates
/home/mike/.config/obs-studio/updates/whatsnew.json
/home/mike/.config/obs-studio/user.ini
(nix) pipulate $
2: AFTER (NEXT CONTEXT):
# # Context 1
[Shortened]
# # Context 2
[Shortened]
# # Context 3
[Shortened]
# Content 4
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
remotes/honeybot/hooks/post-receive
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/stream.py
! .venv/bin/python scripts/articles/common.py --broadcast card
! ssh honeybot 'find ~/.config/obs-studio -maxdepth 3 -not -name "*service*" | sort'
3: CHANGE (PATCHES):
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/articles/common.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/articles/common.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/articles/common.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/articles/common.py'.
(nix) pipulate $ d
diff --git a/scripts/articles/common.py b/scripts/articles/common.py
index d27c1b9d..2d938d3e 100644
--- a/scripts/articles/common.py
+++ b/scripts/articles/common.py
@@ -288,26 +288,61 @@ def format_youtube_title(title):
return cut + "..."
-def _latest_post_for_target(target_key="1"):
- """Resolve the newest markdown post for target_key (recorded or newest on disk)."""
+_DATED_POST_RE = re.compile(r"^\d{4}-\d{2}-\d{2}-.+\.md$")
+
+
+def _post_sort_key(md_path):
+ """Sort dated posts by (YYYY-MM-DD, frontmatter sort_order, mtime)."""
+ date_part = md_path.name[:10]
+ sort_order = 0
+ try:
+ head = md_path.read_text(encoding="utf-8")[:2048]
+ if head.startswith("---"):
+ m = re.search(r"^sort_order:\s*(\d+)", head, re.MULTILINE)
+ if m:
+ sort_order = int(m.group(1))
+ except Exception:
+ pass
+ try:
+ mtime = md_path.stat().st_mtime
+ except OSError:
+ mtime = 0.0
+ return (date_part, sort_order, mtime, md_path.name)
+
+
+def _latest_post_for_target(target_key="1", slug_match=None):
+ """Resolve the newest dated markdown post for target_key (or matching slug_match)."""
targets = load_targets()
tcfg = targets.get(str(target_key), targets.get("1", DEFAULT_TARGETS["1"]))
posts_dir = Path(tcfg["path"]).expanduser()
- candidates = sorted(posts_dir.glob("*.md")) if posts_dir.is_dir() else []
+ dated = [
+ p for p in (posts_dir.glob("*.md") if posts_dir.is_dir() else [])
+ if _DATED_POST_RE.match(p.name)
+ ]
+ if slug_match:
+ needle = slug_match.lower().strip()
+ dated = [p for p in dated if needle in p.name.lower()]
+ if not dated:
+ return None, tcfg
+ dated.sort(key=_post_sort_key)
+ return dated[-1], tcfg
+ if not dated:
+ return None, tcfg
+ max_date = max(p.name[:10] for p in dated)
recorded = get_last_published(target_key)
if recorded and Path(recorded).is_file():
rec_path = Path(recorded)
- if not candidates or rec_path.name >= candidates[-1].name:
+ if _DATED_POST_RE.match(rec_path.name) and rec_path.name[:10] >= max_date:
return rec_path, tcfg
- if candidates:
- return candidates[-1], tcfg
- return None, tcfg
+ same_day = [p for p in dated if p.name[:10] == max_date]
+ same_day.sort(key=_post_sort_key)
+ return same_day[-1], tcfg
-def get_broadcast_kit(target_key="1"):
+def get_broadcast_kit(target_key="1", slug_match=None):
"""Extract {path, title, canonical_url, tweet, yt_title, yt_description} from the latest post."""
import yaml
- post_path, tcfg = _latest_post_for_target(target_key)
+ post_path, tcfg = _latest_post_for_target(target_key, slug_match=slug_match)
if not post_path or not post_path.is_file():
return None
raw = post_path.read_text(encoding="utf-8")
@@ -389,9 +424,9 @@ def _copy_to_clipboard(text):
return False
-def print_broadcast_kit(target_key="1", mode="card"):
+def print_broadcast_kit(target_key="1", mode="card", slug_match=None):
"""Print or copy the broadcast kit (mode: card | tweet | title | desc)."""
- kit = get_broadcast_kit(target_key)
+ kit = get_broadcast_kit(target_key, slug_match=slug_match)
if not kit:
print("⚠️ No published post found for broadcast kit.")
return 1
@@ -438,5 +473,11 @@ if __name__ == "__main__":
default="card",
help="Print the broadcast card or copy tweet/title/desc to clipboard.",
)
+ parser.add_argument(
+ "-s", "--slug",
+ type=str,
+ default=None,
+ help="Optional substring to match a specific dated post filename.",
+ )
cli_args = parser.parse_args()
- sys.exit(print_broadcast_kit(cli_args.target, cli_args.broadcast))
+ sys.exit(print_broadcast_kit(cli_args.target, cli_args.broadcast, slug_match=cli_args.slug))
(nix) pipulate $ m
📝 Committing: refactor: Improve sorting and target resolution in post retrieval
[main 7591d575] refactor: Improve sorting and target resolution in post retrieval
1 file changed, 53 insertions(+), 12 deletions(-)
(nix) pipulate $ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 48 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 1.20 KiB | 1.20 MiB/s, done.
Total 5 (delta 4), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
To github.com:pipulate/pipulate
10e44b6f..7591d575 main -> main
(nix) pipulate $
Ignition: often “exit”, “nix develop”. Instructions may vary.
No ignition. Just the tweet and 3 “yt” commands: title, desc and card. Okay, I
can remember that.
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ tweet
Why losing the Commodore Amiga felt like a 30-year phantom limb—from Jack Tramiel's MOS Tech coup & the 1984 Atari switcheroo to TRIPOS, ARexx, CygnusEd, and how Amiga-born Vim & NixOS reattach the nerve endings.
https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
#Amiga #NixOS #Neovim
📋 Copied X.com Promo Tweet to clipboard!
(nix) pipulate $ yttitle
LIVE: The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS
📋 Copied YouTube Live Title (88/100 chars)
(nix) pipulate $ ytdesc
📖 Read the full article: https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
🎙️ Now reading live on the Honeybot AI Observatory: "The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS"
From Jack Tramiel and the 1984 Atari-Amiga switch to TRIPOS, ARexx, CygnusEd, and Amiga-born Vim: how NixOS restores a 30-year phantom limb.
I reflect on the thirty-year 'phantom limb' ache left behind by the demise of the Commodore Amiga—a machine where preemptive multitasking, instant screen-flipping, ARexx inter-process communication, and the AmigaDOS Replacement Project (ARP) made the computer feel like a natural extension of the nervous system. By tracing the wild historical collisions around West Chester and Norristown, Pennsylvania—Jack Tramiel acquiring MOS Technology, the VIC-20 and C64 chip design by Bob Yannes and Bob Russell, the 1984 Atari-Amiga ownership swap, and Bram Moolenaar birthing Vim on the Amiga while I was editing in CygnusEd—we see how mainstream operating systems spent three decades hiding the command line only to end up as bloated host loaders for WSL and Docker, and how combining NixOS, X11 virtual desktops, and Neovim finally reattaches that lost cybernetic limb.
---
🔭 About this 24/7 Home-Hosted NixOS Broadcast:
You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.
• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.
• Agents executing JavaScript trigger the DOM trapdoor sensor.
• AI Context Manifest: https://mikelev.in/llms.txt
• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai
#Amiga #NixOS #Neovim
📋 Copied YouTube Live Description to clipboard!
(nix) pipulate $ ytcard
========================================================================
📡 GO-GO-GADGET BROADCAST KIT (2026-10-15-phantom-limb-amiga-jack-tramiel-vim-nixos.md)
------------------------------------------------------------------------
🐦 X.COM PROMO TWEET (type `tweet` to copy):
Why losing the Commodore Amiga felt like a 30-year phantom limb—from Jack Tramiel's MOS Tech coup & the 1984 Atari switcheroo to TRIPOS, ARexx, CygnusEd, and how Amiga-born Vim & NixOS reattach the nerve endings.
https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
#Amiga #NixOS #Neovim
------------------------------------------------------------------------
📺 YOUTUBE LIVE TITLE [88/100 chars] (type `yttitle` to copy):
LIVE: The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS
------------------------------------------------------------------------
📝 YOUTUBE LIVE DESCRIPTION (type `ytdesc` to copy):
📖 Read the full article: https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
🎙️ Now reading live on the Honeybot AI Observatory: "The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS"
From Jack Tramiel and the 1984 Atari-Amiga switch to TRIPOS, ARexx, CygnusEd, and Amiga-born Vim: how NixOS restores a 30-year phantom limb.
I reflect on the thirty-year 'phantom limb' ache left behind by the demise of the Commodore Amiga—a machine where preemptive multitasking, instant screen-flipping, ARexx inter-process communication, and the AmigaDOS Replacement Project (ARP) made the computer feel like a natural extension of the nervous system. By tracing the wild historical collisions around West Chester and Norristown, Pennsylvania—Jack Tramiel acquiring MOS Technology, the VIC-20 and C64 chip design by Bob Yannes and Bob Russell, the 1984 Atari-Amiga ownership swap, and Bram Moolenaar birthing Vim on the Amiga while I was editing in CygnusEd—we see how mainstream operating systems spent three decades hiding the command line only to end up as bloated host loaders for WSL and Docker, and how combining NixOS, X11 virtual desktops, and Neovim finally reattaches that lost cybernetic limb.
---
🔭 About this 24/7 Home-Hosted NixOS Broadcast:
You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.
• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.
• Agents executing JavaScript trigger the DOM trapdoor sensor.
• AI Context Manifest: https://mikelev.in/llms.txt
• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai
#Amiga #NixOS #Neovim
------------------------------------------------------------------------
🎛️ Quick Copy Words: tweet | yttitle | ytdesc | ytcard
========================================================================
(nix) pipulate $
Wow, this is pretty friggin exciting! But a thought and I should have said this earlier. The most interesting part of the article is almost the Ai Editorial Take. It’s always so much more compelling to read what the AI Editor found interesting about the article than what the author is saying. It’s like “Who cares what the human said about their own writing. Tell me what the AI thinks!” Is there any way we can gracefully retro-fit this to that kind of pitch. Maybe that can be used in the ytdesc or ytcard to more compelling effect than what we’re now using?
4: PROMPT:
Verify the AFTER receipt from common.py --broadcast card (confirming it now pulls the latest dated article instead of template.md) and inspect Honeybot’s ~/.config/obs-studio layout.
And consider what Mike said about the Ai Editorial Take.
5: DELIVERABLE:
The ability to drop a pebble into the pond per article.
Gemini 4 Argon High: Context verified.
TL;DR: The AFTER receipt in the Manifest confirms _latest_post_for_target banished template.md and locked onto 2026-10-15-phantom-limb-amiga-jack-tramiel-vim-nixos.md ([88/100 chars]). And your instinct about the AI Editorial Take (### Ai Editorial Take) is spot-on: nobody wants to read a self-referential first-person diary blurb (“I reflect on…“) in a YouTube description when they can read an outside AI critic’s take on why the piece surprised it. Below, we swap authors_imprint for 🤖 AI Editorial Take: inside ytdesc and ytcard while keeping your exact four memorized words (tweet, yttitle, ytdesc, ytcard) and requiring zero shell ignition.
1. Why “Tell Me What the AI Thinks!” Is the Better Hook
Look at the two summaries sitting inside every Jekyll post articleizer.py builds:
authors_imprint(Frontmatterdescription): Prompted inediting_prompt.txtas “Populate with the first-person summary.” In the Phantom Limb receipt above, that produced:“I reflect on the thirty-year ‘phantom limb’ ache left behind by the demise of the Commodore Amiga…” That’s fine for metadata, but in a YouTube Live description it reads like an author talking about himself.
ai_editorial_take(### Ai Editorial Takeunder## Book Analysis): Prompted inediting_prompt.txt:112as:“Populate with your AI perspective on the entry’s potential. What surprised you most? Don’t use the exact same thing as the previous AI participating in the dialogue. Find some absolutely new angle or perspective that isn’t mentioned yet in the article.”
That is liner-note criticism from the Guy in the Chair. When a human or an indexing bot lands on your YouTube Live stream, seeing:
- The clickable article link at the very top,
- The 1-sentence
meta_descriptionhook, and 🤖 AI Editorial Take:(what the AI editor found surprising and significant about the piece)
is ten times more magnetic than a first-person summary.
2. Keeping the 4-Word Muscle Memory Intact (Zero Ignition)
You already locked the four words into your fingers:
tweetyttitleytdescytcard
Because all four words call scripts/articles/common.py at runtime, we don’t need to touch flake.nix at all. One surgical patch to get_broadcast_kit() in scripts/articles/common.py extracts ### Ai Editorial Take from the post body, places it under 🤖 AI Editorial Take: in ytdesc and ytcard (falling back to authors_imprint only for older archive posts that don’t have an Ai Editorial Take section), and works the millisecond app finishes—no exit / ndq needed.
(Bonus note from the Honeybot OBS receipt: find ~/.config/obs-studio confirmed /home/mike/.config/obs-studio/plugin_config/obs-websocket/config.json and /home/mike/.config/obs-studio/basic/profiles/Untitled are right where they belong on Honeybot—the earlier probe only printed NO_WS_CONFIG because jq isn’t in Honeybot’s system packages. Whenever you’re ready for Rung 2 after living with this manual Cut-and-Roll loop for a few articles, we know the exact paths.)
The 5-Car Train
(1) PROBES
Run this read-only BEFORE probe to record the current ytcard output (which still shows the first-person I reflect on... blurb):
(nix) pipulate $
.venv/bin/python scripts/articles/common.py --broadcast card
(2) NEXT CONTEXT
Paste this block into context.txt:
(nix) pipulate $
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
! .venv/bin/python scripts/articles/common.py --broadcast card
(3) PATCHES
Car 1 (Extract ### Ai Editorial Take in scripts/articles/common.py and feature it as 🤖 AI Editorial Take: in ytdesc and ytcard, falling back to imprint only if absent):
Target: scripts/articles/common.py
[[[SEARCH]]]
meta_desc = str(fm.get("meta_description") or fm.get("excerpt") or "").strip()
imprint = str(fm.get("description") or "").strip()
tweet_match = re.search(
r"###\s*🐦\s*X\.com Promo Tweet\s*\n[triple-backtick][a-zA-Z0-9]*\n(.*?)\n[triple-backtick]",
body,
re.DOTALL,
)
if tweet_match:
tweet = tweet_match.group(1).strip()
else:
tweet = f"{title}\n\n{canonical_url}"
tags = " ".join(re.findall(r"#[A-Za-z0-9_]+", tweet))
yt_title = format_youtube_title(title)
desc_parts = [
f"📖 Read the full article: {canonical_url}",
"",
f"🎙️ Now reading live on the Honeybot AI Observatory: \"{title}\"",
]
if meta_desc:
desc_parts.extend(["", meta_desc])
if imprint and imprint != meta_desc:
desc_parts.extend(["", imprint])
[[[DIVIDER]]]
meta_desc = str(fm.get("meta_description") or fm.get("excerpt") or "").strip()
imprint = str(fm.get("description") or "").strip()
editorial_match = re.search(
r"###\s*Ai Editorial Take\s*\n(.*?)(?=\n#{2,3}\s|\Z)",
body,
re.DOTALL | re.IGNORECASE,
)
ai_take = editorial_match.group(1).strip() if editorial_match else ""
tweet_match = re.search(
r"###\s*🐦\s*X\.com Promo Tweet\s*\n[triple-backtick][a-zA-Z0-9]*\n(.*?)\n[triple-backtick]",
body,
re.DOTALL,
)
if tweet_match:
tweet = tweet_match.group(1).strip()
else:
tweet = f"{title}\n\n{canonical_url}"
tags = " ".join(re.findall(r"#[A-Za-z0-9_]+", tweet))
yt_title = format_youtube_title(title)
desc_parts = [
f"📖 Read the full article: {canonical_url}",
"",
f"🎙️ Now reading live on the Honeybot AI Observatory: \"{title}\"",
]
if meta_desc:
desc_parts.extend(["", meta_desc])
if ai_take:
desc_parts.extend(["", "🤖 AI Editorial Take:", ai_take])
elif imprint and imprint != meta_desc:
desc_parts.extend(["", imprint])
[[[REPLACE]]]
Ignition: no ignition required (tweet, yttitle, ytdesc, and ytcard call scripts/articles/common.py directly at runtime—test ytcard or ytdesc right after app).
(4) PROMPT
(nix) pipulate $
Verify the AFTER receipt showing `🤖 AI Editorial Take:` inside `ytcard` and `ytdesc`, and close out the ride so we can publish this article as the very first live test of the Cut-and-Roll broadcast kit.
(5) EXTERNAL DELIVERABLES
None. (Input archive sealed for this turn: qamy.ai_121-4ac46115.zip.)
MikeLev.in:
THE AI-EDIT METHOD
Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.
1: BEFORE (PROBE):
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ .venv/bin/python scripts/articles/common.py --broadcast card
========================================================================
📡 GO-GO-GADGET BROADCAST KIT (2026-10-15-phantom-limb-amiga-jack-tramiel-vim-nixos.md)
------------------------------------------------------------------------
🐦 X.COM PROMO TWEET (type `tweet` to copy):
Why losing the Commodore Amiga felt like a 30-year phantom limb—from Jack Tramiel's MOS Tech coup & the 1984 Atari switcheroo to TRIPOS, ARexx, CygnusEd, and how Amiga-born Vim & NixOS reattach the nerve endings.
https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
#Amiga #NixOS #Neovim
------------------------------------------------------------------------
📺 YOUTUBE LIVE TITLE [88/100 chars] (type `yttitle` to copy):
LIVE: The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS
------------------------------------------------------------------------
📝 YOUTUBE LIVE DESCRIPTION (type `ytdesc` to copy):
📖 Read the full article: https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
🎙️ Now reading live on the Honeybot AI Observatory: "The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS"
From Jack Tramiel and the 1984 Atari-Amiga switch to TRIPOS, ARexx, CygnusEd, and Amiga-born Vim: how NixOS restores a 30-year phantom limb.
I reflect on the thirty-year 'phantom limb' ache left behind by the demise of the Commodore Amiga—a machine where preemptive multitasking, instant screen-flipping, ARexx inter-process communication, and the AmigaDOS Replacement Project (ARP) made the computer feel like a natural extension of the nervous system. By tracing the wild historical collisions around West Chester and Norristown, Pennsylvania—Jack Tramiel acquiring MOS Technology, the VIC-20 and C64 chip design by Bob Yannes and Bob Russell, the 1984 Atari-Amiga ownership swap, and Bram Moolenaar birthing Vim on the Amiga while I was editing in CygnusEd—we see how mainstream operating systems spent three decades hiding the command line only to end up as bloated host loaders for WSL and Docker, and how combining NixOS, X11 virtual desktops, and Neovim finally reattaches that lost cybernetic limb.
---
🔭 About this 24/7 Home-Hosted NixOS Broadcast:
You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.
• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.
• Agents executing JavaScript trigger the DOM trapdoor sensor.
• AI Context Manifest: https://mikelev.in/llms.txt
• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai
#Amiga #NixOS #Neovim
------------------------------------------------------------------------
🎛️ Quick Copy Words: tweet | yttitle | ytdesc | ytcard
========================================================================
(nix) pipulate $
2: AFTER (NEXT CONTEXT):
# # Context 1
# # # THE JEKYLL PUBLISHING PIPELINE
# ~/repos/nixos/blogs.nix
# ~/.config/pipulate/blogs.json
# scripts/articles/publishizer.py
# scripts/articles/common.py
# scripts/articles/articleizer.py
# scripts/articles/build_knowledge_graph.py
# scripts/articles/editing_prompt.txt
# scripts/articles/sanitizer.py
# scripts/articles/contextualizer.py
# scripts/articles/generate_llms_txt.py
# scripts/articles/generate_post_nav.py
#
# # # OUR HONEYBOT AI OBSERVATORY
# nixops.sh
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# remotes/honeybot/scripts/score.py
# remotes/honeybot/scripts/card.py
# remotes/honeybot/scripts/forest.py
# remotes/honeybot/scripts/test_forest.py
# remotes/honeybot/scripts/logs.py
# remotes/honeybot/scripts/content_loader.py
# remotes/honeybot/scripts/db.py
# # Context 2
# flake.nix
# scripts/articles/articleizer.py
# scripts/articles/publishizer.py
# scripts/articles/common.py
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# ! rg -n -A 40 'publish\(\) \{' flake.nix
# ! jq -r '{title: .editing_instructions.yaml_updates.title, desc: .editing_instructions.yaml_updates.description, tweet: .book_analysis_content.promotional_tweet}' scripts/articles/instructions.json 2>/dev/null || echo "NO_INSTRUCTIONS_JSON"
# ! ssh honeybot 'pgrep -af obs || echo OBS_NOT_RUNNING; ss -tlnp 2>/dev/null | grep 4455 || echo NO_OBS_WS_4455'
# # Context 3
# flake.nix
# scripts/articles/articleizer.py
# scripts/articles/publishizer.py
# scripts/articles/common.py
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# ! .venv/bin/python scripts/articles/common.py --broadcast card 2>/dev/null || echo "BROADCAST_KIT_ABSENT"
# ! rg -n '^\s*(tweet|yttitle|ytdesc|ytcard)\(\)' flake.nix || echo "GADGET_WORDS_ABSENT"
# ! ssh honeybot 'DISPLAY=:10.0 wmctrl -lx 2>/dev/null | grep -i obs || true; jq -r "{server_enabled, server_port, auth_required}" ~/.config/obs-studio/plugin_config/obs-websocket/config.json 2>/dev/null || echo NO_WS_CONFIG; grep -E "StartStreaming|StopStreaming" ~/.config/obs-studio/basic/profiles/*/basic.ini 2>/dev/null || echo NO_STREAM_HOTKEYS'
# # Content 4
# flake.nix
# scripts/articles/articleizer.py
# scripts/articles/publishizer.py
# scripts/articles/common.py
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# ! .venv/bin/python scripts/articles/common.py --broadcast card
# ! ssh honeybot 'find ~/.config/obs-studio -maxdepth 3 -not -name "*service*" | sort'
# Context 5
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
! .venv/bin/python scripts/articles/common.py --broadcast card
3: CHANGE (PATCHES):
Wow that was easier than I thought it was going to be.
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/articles/common.py'.
(nix) pipulate $ d
diff --git a/scripts/articles/common.py b/scripts/articles/common.py
index 2d938d3e..f4d1ec3f 100644
--- a/scripts/articles/common.py
+++ b/scripts/articles/common.py
@@ -362,6 +362,12 @@ def get_broadcast_kit(target_key="1", slug_match=None):
canonical_url = str(fm.get("canonical_url") or (f"{base_url}{permalink}" if permalink else base_url))
meta_desc = str(fm.get("meta_description") or fm.get("excerpt") or "").strip()
imprint = str(fm.get("description") or "").strip()
+ editorial_match = re.search(
+ r"###\s*Ai Editorial Take\s*\n(.*?)(?=\n#{2,3}\s|\Z)",
+ body,
+ re.DOTALL | re.IGNORECASE,
+ )
+ ai_take = editorial_match.group(1).strip() if editorial_match else ""
tweet_match = re.search(
r"###\s*🐦\s*X\.com Promo Tweet\s*\n[triple-backtick][a-zA-Z0-9]*\n(.*?)\n[triple-backtick]",
body,
@@ -380,7 +386,9 @@ def get_broadcast_kit(target_key="1", slug_match=None):
]
if meta_desc:
desc_parts.extend(["", meta_desc])
- if imprint and imprint != meta_desc:
+ if ai_take:
+ desc_parts.extend(["", "🤖 AI Editorial Take:", ai_take])
+ elif imprint and imprint != meta_desc:
desc_parts.extend(["", imprint])
desc_parts.extend([
"",
(nix) pipulate $ m
📝 Committing: chore: Extract AI Editorial Take from article body
[main 57abd21f] chore: Extract AI Editorial Take from article body
1 file changed, 9 insertions(+), 1 deletion(-)
(nix) pipulate $ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 48 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 616 bytes | 616.00 KiB/s, done.
Total 5 (delta 4), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
To github.com:pipulate/pipulate
7591d575..57abd21f main -> main
(nix) pipulate $
Ignition: often “exit”, “nix develop”. Instructions may vary.
Once again, we just try. Wow! Just wow. I’d call this a game-changer if it was not actually just the game starting-bell so we can begin to see how the game really works for the first time.
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ tweet
Why losing the Commodore Amiga felt like a 30-year phantom limb—from Jack Tramiel's MOS Tech coup & the 1984 Atari switcheroo to TRIPOS, ARexx, CygnusEd, and how Amiga-born Vim & NixOS reattach the nerve endings.
https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
#Amiga #NixOS #Neovim
📋 Copied X.com Promo Tweet to clipboard!
(nix) pipulate $ yttitle
LIVE: The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS
📋 Copied YouTube Live Title (88/100 chars)
(nix) pipulate $ ytdesc
📖 Read the full article: https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
🎙️ Now reading live on the Honeybot AI Observatory: "The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS"
From Jack Tramiel and the 1984 Atari-Amiga switch to TRIPOS, ARexx, CygnusEd, and Amiga-born Vim: how NixOS restores a 30-year phantom limb.
🤖 AI Editorial Take:
What surprised me most when mapping the genealogy of this piece is a hidden architectural irony in why the Amiga had to die for its best ideas to survive. On the Amiga, Carl Sassenrath's `Exec` microkernel and William Hawes's `ARexx` achieved near-instantaneous inter-process communication because every task shared a single, flat, unprotected 32-bit memory space (`MsgPort` passed raw pointers, not serialized copies). That lack of memory protection is what gave the Amiga its magical, zero-latency nervous-system feel—everything could talk to everything in microseconds—but it also meant a single stray pointer in one program brought down the entire machine with a red-bordered `Guru Meditation` crash. Unix took the opposite bet: strict process isolation (`fork`/`exec` with separate virtual address spaces) connected by byte-stream pipes, trading away shared-memory pointer speed for crash isolation. Thirty-five years later, your NixOS + Prompt Fu architecture achieves the synthesis neither 1985 system could pull off alone: process-isolated Unix stability underneath, paired with an `ARexx`-style desktop choreographer (`autognome.py`, `wmctrl`, `xclip`) and an AI parser airlock (`apply.py`) on top.
---
🔭 About this 24/7 Home-Hosted NixOS Broadcast:
You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.
• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.
• Agents executing JavaScript trigger the DOM trapdoor sensor.
• AI Context Manifest: https://mikelev.in/llms.txt
• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai
#Amiga #NixOS #Neovim
📋 Copied YouTube Live Description to clipboard!
(nix) pipulate $ ytcard
========================================================================
📡 GO-GO-GADGET BROADCAST KIT (2026-10-15-phantom-limb-amiga-jack-tramiel-vim-nixos.md)
------------------------------------------------------------------------
🐦 X.COM PROMO TWEET (type `tweet` to copy):
Why losing the Commodore Amiga felt like a 30-year phantom limb—from Jack Tramiel's MOS Tech coup & the 1984 Atari switcheroo to TRIPOS, ARexx, CygnusEd, and how Amiga-born Vim & NixOS reattach the nerve endings.
https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
#Amiga #NixOS #Neovim
------------------------------------------------------------------------
📺 YOUTUBE LIVE TITLE [88/100 chars] (type `yttitle` to copy):
LIVE: The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS
------------------------------------------------------------------------
📝 YOUTUBE LIVE DESCRIPTION (type `ytdesc` to copy):
📖 Read the full article: https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
🎙️ Now reading live on the Honeybot AI Observatory: "The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS"
From Jack Tramiel and the 1984 Atari-Amiga switch to TRIPOS, ARexx, CygnusEd, and Amiga-born Vim: how NixOS restores a 30-year phantom limb.
🤖 AI Editorial Take:
What surprised me most when mapping the genealogy of this piece is a hidden architectural irony in why the Amiga had to die for its best ideas to survive. On the Amiga, Carl Sassenrath's `Exec` microkernel and William Hawes's `ARexx` achieved near-instantaneous inter-process communication because every task shared a single, flat, unprotected 32-bit memory space (`MsgPort` passed raw pointers, not serialized copies). That lack of memory protection is what gave the Amiga its magical, zero-latency nervous-system feel—everything could talk to everything in microseconds—but it also meant a single stray pointer in one program brought down the entire machine with a red-bordered `Guru Meditation` crash. Unix took the opposite bet: strict process isolation (`fork`/`exec` with separate virtual address spaces) connected by byte-stream pipes, trading away shared-memory pointer speed for crash isolation. Thirty-five years later, your NixOS + Prompt Fu architecture achieves the synthesis neither 1985 system could pull off alone: process-isolated Unix stability underneath, paired with an `ARexx`-style desktop choreographer (`autognome.py`, `wmctrl`, `xclip`) and an AI parser airlock (`apply.py`) on top.
---
🔭 About this 24/7 Home-Hosted NixOS Broadcast:
You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.
• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.
• Agents executing JavaScript trigger the DOM trapdoor sensor.
• AI Context Manifest: https://mikelev.in/llms.txt
• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai
#Amiga #NixOS #Neovim
------------------------------------------------------------------------
🎛️ Quick Copy Words: tweet | yttitle | ytdesc | ytcard
========================================================================
(nix) pipulate $
4: PROMPT:
Verify the AFTER receipt showing 🤖 AI Editorial Take: inside ytcard and ytdesc, and close out the ride so we can publish this article as the very first live test of the Cut-and-Roll broadcast kit.
5: DELIVERABLE:
Wow, I’m likely going to just do this with the current article which I’ve been testing this with (not he one we’re writing here but that phantom limb one).
- What’s the ytcard data for?
- And after stopping the current live-feed, I just edit the current form and start live-streaming again, right? I go to “Go Live”, I hit “End Stream”, make edits and then start again is all? (aside from the tweet and figuring out what the card is for, that is)
Gemini 4 Argon High: Context verified.
TL;DR: The AFTER receipt in the Manifest is green: 🤖 AI Editorial Take: is live inside ytdesc and ytcard for 2026-10-15-phantom-limb-amiga-jack-tramiel-vim-nixos.md. Nothing else in the repo needs patching. Below are the direct answers to your two questions so you can run your live Cut-and-Roll test on the Phantom Limb article right now.
1. What Is ytcard For?
You do not paste ytcard anywhere.
ytcardis just a read-only preview on your terminal screen (a “cue card”) so you can check which article is queued up and read all three pieces at a glance without touching your clipboard.- It also prints automatically at the end of
articleandpublish. - When you are actually copying and pasting into your browser, you only ever use the 3 clipboard commands:
tweet$\rightarrow$ copies only the X.com promo tweet to your clipboard.yttitle$\rightarrow$ copies only theLIVE: ...title to your clipboard.ytdesc$\rightarrow$ copies only the full YouTube description (with the🤖 AI Editorial Take:) to your clipboard.
2. The Exact Cut-and-Roll Sequence (YouTube Studio + OBS)
Yes—your mental model is right, with one important detail: you also click Stop Streaming and Start Streaming in OBS during the middle of that sandwich so OBS makes a fresh RTMP handshake with the new YouTube broadcast room.
Here is the exact checklist to test it right now on the Phantom Limb article:
THE 60-SECOND CUT-AND-ROLL CHECKLIST
====================================
1. YOUTUBE STUDIO ("Go Live" Control Room):
Click the red [END STREAM] button (top right) -> Confirm [End] -> Click [Dismiss].
(Your previous stream is now sealed into a permanent VOD!)
2. OBS (on Honeybot):
Click [Stop Streaming].
3. YOUTUBE STUDIO (Same "Stream" Screen):
Click [Edit] next to the stream title/description:
• Run `yttitle` in terminal -> Paste into YouTube Title
• Run `ytdesc` in terminal -> Paste into YouTube Description
Click [Save].
4. OBS (on Honeybot):
Click [Start Streaming].
(YouTube receives the fresh RTMP feed on your same Stream Key and
goes LIVE with the new Title & Description!)
5. X.COM:
Run `tweet` in terminal -> Paste into X.com -> Post.
Bonus Tip for Testing on the Already-Published Phantom Limb Article
When you publish a brand-new article with publish "msg", Honeybot’s post-receive hook automatically rings .reading_trigger so Piper TTS immediately starts reading the new article at the start of your stream.
Because you are testing this right now on the already-published Phantom Limb article, if you want Honeybot to immediately interrupt whatever it is currently reading and start reading the Phantom Limb article from the top as soon as your new stream goes live, run this one-liner right after Step 4:
(nix) pipulate $
ssh honeybot 'date +%s > ~/www/mikelev.in/.reading_trigger'
(That rings the breaking-news bell in 2 seconds without running a full 3-minute Jekyll rebuild.)
The 5-Car Train (Ride Complete — Ready to Publish)
(1) PROBES
(nix) pipulate $
.venv/bin/python scripts/articles/common.py --broadcast card
(2) NEXT CONTEXT
(nix) pipulate $
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
! .venv/bin/python scripts/articles/common.py --broadcast card
(3) PATCHES
No repo patches required. Ignition: none.
(4) PROMPT
(nix) pipulate $
The Go-Go-Gadget Broadcast Kit (`tweet`, `yttitle`, `ytdesc`, `ytcard`) with `🤖 AI Editorial Take:` is verified and banked. Let's review how the first live Cut-and-Roll broadcast went or move to the next task.
(5) EXTERNAL DELIVERABLES
None. (Input archive sealed for this turn: qamy.ai_122-00fb2980.zip.)
MikeLev.in:
THE AI-EDIT METHOD
Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.
1: BEFORE (PROBE):
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
GIT repo clean. Take BEFORE reading, make CHANGE, record AFTER diff.
(nix) pipulate $ .venv/bin/python scripts/articles/common.py --broadcast card
========================================================================
📡 GO-GO-GADGET BROADCAST KIT (2026-10-15-phantom-limb-amiga-jack-tramiel-vim-nixos.md)
------------------------------------------------------------------------
🐦 X.COM PROMO TWEET (type `tweet` to copy):
Why losing the Commodore Amiga felt like a 30-year phantom limb—from Jack Tramiel's MOS Tech coup & the 1984 Atari switcheroo to TRIPOS, ARexx, CygnusEd, and how Amiga-born Vim & NixOS reattach the nerve endings.
https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
#Amiga #NixOS #Neovim
------------------------------------------------------------------------
📺 YOUTUBE LIVE TITLE [88/100 chars] (type `yttitle` to copy):
LIVE: The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS
------------------------------------------------------------------------
📝 YOUTUBE LIVE DESCRIPTION (type `ytdesc` to copy):
📖 Read the full article: https://mikelev.in/futureproof/phantom-limb-amiga-jack-tramiel-vim-nixos/
🎙️ Now reading live on the Honeybot AI Observatory: "The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS"
From Jack Tramiel and the 1984 Atari-Amiga switch to TRIPOS, ARexx, CygnusEd, and Amiga-born Vim: how NixOS restores a 30-year phantom limb.
🤖 AI Editorial Take:
What surprised me most when mapping the genealogy of this piece is a hidden architectural irony in why the Amiga had to die for its best ideas to survive. On the Amiga, Carl Sassenrath's `Exec` microkernel and William Hawes's `ARexx` achieved near-instantaneous inter-process communication because every task shared a single, flat, unprotected 32-bit memory space (`MsgPort` passed raw pointers, not serialized copies). That lack of memory protection is what gave the Amiga its magical, zero-latency nervous-system feel—everything could talk to everything in microseconds—but it also meant a single stray pointer in one program brought down the entire machine with a red-bordered `Guru Meditation` crash. Unix took the opposite bet: strict process isolation (`fork`/`exec` with separate virtual address spaces) connected by byte-stream pipes, trading away shared-memory pointer speed for crash isolation. Thirty-five years later, your NixOS + Prompt Fu architecture achieves the synthesis neither 1985 system could pull off alone: process-isolated Unix stability underneath, paired with an `ARexx`-style desktop choreographer (`autognome.py`, `wmctrl`, `xclip`) and an AI parser airlock (`apply.py`) on top.
---
🔭 About this 24/7 Home-Hosted NixOS Broadcast:
You are watching real-time Nginx access logs and AI crawler telemetry from mikelev.in.
• Agents sending 'Accept: text/markdown' (RFC 9110) receive clean raw Markdown.
• Agents executing JavaScript trigger the DOM trapdoor sensor.
• AI Context Manifest: https://mikelev.in/llms.txt
• Stack: NPvg (Nix, Python, Neovim, Git) — https://pipulate.com | https://qamy.ai
#Amiga #NixOS #Neovim
------------------------------------------------------------------------
🎛️ Quick Copy Words: tweet | yttitle | ytdesc | ytcard
========================================================================
(nix) pipulate $
2: AFTER (NEXT CONTEXT):
# # Context 1
# # # THE JEKYLL PUBLISHING PIPELINE
# ~/repos/nixos/blogs.nix
# ~/.config/pipulate/blogs.json
# scripts/articles/publishizer.py
# scripts/articles/common.py
# scripts/articles/articleizer.py
# scripts/articles/build_knowledge_graph.py
# scripts/articles/editing_prompt.txt
# scripts/articles/sanitizer.py
# scripts/articles/contextualizer.py
# scripts/articles/generate_llms_txt.py
# scripts/articles/generate_post_nav.py
#
# # # OUR HONEYBOT AI OBSERVATORY
# nixops.sh
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# remotes/honeybot/scripts/score.py
# remotes/honeybot/scripts/card.py
# remotes/honeybot/scripts/forest.py
# remotes/honeybot/scripts/test_forest.py
# remotes/honeybot/scripts/logs.py
# remotes/honeybot/scripts/content_loader.py
# remotes/honeybot/scripts/db.py
# # Context 2
# flake.nix
# scripts/articles/articleizer.py
# scripts/articles/publishizer.py
# scripts/articles/common.py
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# ! rg -n -A 40 'publish\(\) \{' flake.nix
# ! jq -r '{title: .editing_instructions.yaml_updates.title, desc: .editing_instructions.yaml_updates.description, tweet: .book_analysis_content.promotional_tweet}' scripts/articles/instructions.json 2>/dev/null || echo "NO_INSTRUCTIONS_JSON"
# ! ssh honeybot 'pgrep -af obs || echo OBS_NOT_RUNNING; ss -tlnp 2>/dev/null | grep 4455 || echo NO_OBS_WS_4455'
# # Context 3
# flake.nix
# scripts/articles/articleizer.py
# scripts/articles/publishizer.py
# scripts/articles/common.py
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# ! .venv/bin/python scripts/articles/common.py --broadcast card 2>/dev/null || echo "BROADCAST_KIT_ABSENT"
# ! rg -n '^\s*(tweet|yttitle|ytdesc|ytcard)\(\)' flake.nix || echo "GADGET_WORDS_ABSENT"
# ! ssh honeybot 'DISPLAY=:10.0 wmctrl -lx 2>/dev/null | grep -i obs || true; jq -r "{server_enabled, server_port, auth_required}" ~/.config/obs-studio/plugin_config/obs-websocket/config.json 2>/dev/null || echo NO_WS_CONFIG; grep -E "StartStreaming|StopStreaming" ~/.config/obs-studio/basic/profiles/*/basic.ini 2>/dev/null || echo NO_STREAM_HOTKEYS'
# # Content 4
# flake.nix
# scripts/articles/articleizer.py
# scripts/articles/publishizer.py
# scripts/articles/common.py
# remotes/honeybot/hooks/post-receive
# remotes/honeybot/nixos/configuration.nix
# remotes/honeybot/scripts/stream.py
# ! .venv/bin/python scripts/articles/common.py --broadcast card
# ! ssh honeybot 'find ~/.config/obs-studio -maxdepth 3 -not -name "*service*" | sort'
# # Context 5
# flake.nix
# scripts/articles/articleizer.py
# scripts/articles/publishizer.py
# scripts/articles/common.py
# ! .venv/bin/python scripts/articles/common.py --broadcast card
# Context 6
foo_files.py
flake.nix
scripts/articles/articleizer.py
scripts/articles/publishizer.py
scripts/articles/common.py
! .venv/bin/python scripts/articles/common.py --broadcast card
3: CHANGE (PATCHES):
None. This first round is done.
4: PROMPT:
The Go-Go-Gadget Broadcast Kit (tweet, yttitle, ytdesc, ytcard) with 🤖 AI Editorial Take: is verified and banked. Let’s review how the first live Cut-and-Roll broadcast went or move to the next task.
5: DELIVERABLE:
Hop off the ride. This ride’s stated goal is reached – dismount. This is the NOTARY BEAT: the ride ends here, is witnessed here, and is sealed here. Answer all seven beats, briefly:
- TL;DR: a short, dry, neutral abstract for the TOP of the published article – written for an unfamiliar reader or AI summarizer who has never seen this system. No hype, no insider handles unexplained.
- VERIFY: restate the goal from the top of this article and confirm (or deny) it was met, citing THIS compile’s receipts, not memory. Name any ignition this ride required that never fired – an AFTER tap taken without one is a stale BEFORE wearing the AFTER’s label.
- BANK: name everything that graduates – rule, earmark, todo, pin – as SEARCH/REPLACE patch cars against raw source present in THIS context (Target line inside the fence, one car per commit story), deletions included. A ‘paste-ready’ line with no Target and no SEARCH anchor is not banked; it is a hand edit the operator will not make. These BANK cars are the ONLY patches a dismount emits.
- DANGLING: what carries forward unbanked? One line each, no essays.
- SEED: the context.txt lines (and TODO_SLUGS if narrative context is needed) for the next ride’s first compile.
- CLOSING: a closing summary for the BOTTOM of the article – the final take-away, tied to the book’s larger arc where it fits naturally, never forced. Storytelling over inventory.
- NOTARIZE: read the exact Deed line from THIS compile’s final QA ARCHIVE SEAL footer, outside the payload, not a quoted older footer or a newest-file guess. It names the input archive, not this later response or the generated Jekyll post. State verification separately: only a verifier receipt naming that archive supports a verified claim; otherwise say not independently verified here. Count this Manifest’s live-command receipts, including failures. Quote only recorded digests, sizes and dates, naming their source; filesystem mtime and fixed ZIP dates are not command timestamps. Do not demand absent wc -c or dated-receipt fields. If the footer is missing, say so; otherwise close with its exact Deed line.
FINALITY: after beat 6, this discussion is CLOSED. Emit NO five-car train, NO probes, NO patches beyond the BANK cars of beat 2, and NO next-turn prompt beyond the SEED lines in beat 4. Any reader or model encountering this article later should treat it as a finished, notarized document – an archive entry, not an open thread.
Gemini 4 Argon High: Context verified.
1. VERIFY:
- Opening Goal: Build an low-friction, endosynthesized workflow (“Cut-and-Roll” / Mode B) so that every newly published Jekyll article surfaces its X.com promotional tweet and a fresh article-derived YouTube Live Title and Description (featuring the
### Ai Editorial Take), ready to cut the previous livestream into a permanent YouTube VOD and roll a newly themed broadcast. - Confirmation Against This Compile’s Receipts: MET.
- Manifest Live Receipt (
! .venv/bin/python scripts/articles/common.py --broadcast card,0.1962s): Prints the complete📡 GO-GO-GADGET BROADCAST KIT (2026-10-15-phantom-limb-amiga-jack-tramiel-vim-nixos.md)cue card, including the resolved X.com promo tweet with canonical URL and hashtags, the 88/100-character YouTube Live title (LIVE: The Phantom Limb of Computing: From Jack Tramiel and the Amiga to Neovim and NixOS), and the YouTube Live description featuring🤖 AI Editorial Take:. - Codebase & Telemetry Receipts:
flake.nix(10e44b6f) definesytcard,tweet,yttitle, andytdescand invokes--broadcast cardat the end ofpublish();scripts/articles/articleizer.py(c1955e81) invokescommon.print_broadcast_kit(args.target, mode="card")after saving a new post;scripts/articles/common.py(7591d575and57abd21f, diff2d938d3e..f4d1ec3finRecent Git Diff Telemetry) filters_posts/strictly to dated^\d{4}-\d{2}-\d{2}-.+\.md$files (preventingtemplate.mdfrom winning ASCII sort), orders same-day posts by(date_part, sort_order, mtime, name), and extracts### Ai Editorial Takeintoyt_description. -
Static Analysis Receipt (
Telemetry):Ruff: All checks passed!(exit 0). - Ignition Status: All required ignitions fired. The
flake.nixshell functions were ignited viaexitandndqprior to the interactivetweet/yttitle/ytdesc/ytcardtest flights, and the subsequentcommon.pypatches self-ignited at runtime with zero unignited AFTER taps.
2. BANK:
Car 1 (Bank the dismount receipt in foo_files.py while fading the 20th receipt to keep the 20-line cap, bank # § THE DATED-POST GLOB, and update the YouTube livestream TODO with Rung 1 landed):
Target: foo_files.py
[[[SEARCH]]]
# --- START RECEIPTS (newest first; cap 20 lines; a line pushed past the cap is deleted, never moved -- git and the rolling pin are the archive) ---
[[[DIVIDER]]]
# --- START RECEIPTS (newest first; cap 20 lines; a line pushed past the cap is deleted, never moved -- git and the rolling pin are the archive) ---
# 2026-10-15 dismount ENDOSYNTHESIS, INSPECTOR GADGET, AND THE GO-GO-GADGET BROADCAST KIT (deeds qamy.ai_117-9c37ddfd.zip through qamy.ai_123-b4642777.zip; pipulate commits c1955e81, 10e44b6f, 7591d575, 57abd21f; Gemini 4 Argon High on every turn): endosynthesis (Atari 2600/VCS CX40 + Jay Miner TIA racing the beam, language as zero-mass prosthetic, Spider-Man + Ned Leeds / Inspector Gadget + Penny vs. Iron Man's sealed suit) contrasted against forced hybridization (Tusk, Brundlefly); Marvin Minsky's Society of the Mind, Ray Kurzweil's Making a Mind, Iain McGilchrist's Master and His Emissary, and Pangea's Enigmo bumpers mapped to high-wingspan context compilation; YouTube Live Mode B ("Cut-and-Roll") architected around the 12-hour VOD retention ceiling and constructive wave interference; Rung 1 Go-Go-Gadget Broadcast Kit landed in scripts/articles/common.py, scripts/articles/articleizer.py, and flake.nix (tweet, yttitle, ytdesc, ytcard + automatic cue card at the end of article and publish), template.md ASCII sort collision fixed via _DATED_POST_RE + _post_sort_key, and ### Ai Editorial Take promoted into ytdesc/ytcard. WITNESSED: common.py --broadcast card template.md (14/100 chars) -> 2026-10-15-phantom-limb-amiga-jack-tramiel-vim-nixos.md (88/100 chars) with 🤖 AI Editorial Take, interactive tweet/yttitle/ytdesc clipboard copies verified after exit + ndq, Honeybot OBS 32.0.1 window 0x01e00007 on DISPLAY=:10.0 and ~/.config/obs-studio/plugin_config/obs-websocket/config.json located, Ruff clean, deed qamy.ai_123-b4642777.zip.
[[[REPLACE]]]
Target: foo_files.py
[[[SEARCH]]]
# 2026-09-30 dismount THE MIRROR IS READ (deeds 1730 and 1733, 1731 and 1732 compiles that rode no turn, INFERRED; pipulate commits 2dd2533a the copies and 7d05818d the ledger, pushed by hand, and this dismount's two cars; Fable 5.1 on the working turn by the article's label, Claude Opus 5.5 on this dismount): the mirrored repos became readable where Prime keeps them. mirror's whitelist gained personal (six by default) and every repo on the tape now has a working copy at ~/repos/<name> on the Mac, cloned from the Mac's own tape, push URL disabled, refreshed by pull --ff-only with a HELD line for local work or a foreign folder; mirror also reads Prime's blogs.json over the same socket and writes ~/.config/pipulate/blogs.mirror.json (paths moved to the copies, keys and aliases unchanged, the Vault dropped, pipeline emptied, Drive and Confluence ids cut); the Darwin shell exports it at entry, write_post pins the shadow inside its own subshell, and publish refuses on darwin. Straddles in band on Prime: the jq filter lifted out of the built word MIRROR_JQ_ABSENT -> jq_rc=0 with exactly the forecast map (1=article, 2=grim, 4=bot under /Users/x/repos, pipeline=0 ids=false); the nohost header gaining copies in /home/mike/repos and personal, its STOP a gate; the store path n0cn1bgz -> p8b6daff; the Darwin hook evaluated from Linux 78,806 -> 80,055 bytes with mirror_cfg_lines 0 -> 3 and publish_guard 0 -> 1; three readers of PIPULATE_BLOGS_CONFIG (prompt_foo.py, lsa.py, common.py) and six tape HEADs resolving (clients on master), each a census. The Mac (the qamy checkout, v2.73): the census six no-folder lines and config=blogs.shadow.json; mirror read personal cloned, five fetched, every copy cloned, the config line (1=article 2=grim 4=bot), the this-shell line naming the shadow and DONE -- 6 of 6; after exit and nix develop the config read blogs.mirror.json, posts -t grim three Grimoire copy paths (newest the 09-30 tape article; grimoire.git's newest moved 00:00:05 -> 06:10:21, INFERRED a b2 by hand), posts three book paths dated 09-29, rgx -t all 3 nix the three-blog header, rgx flibbertigibbet ten book paths, and the fm cache 1505 and 111 misses then 1616 hits, the book's copy equal to Prime's stats line of 1,505. THE TAPE IS NOT THE WORK, second instance: Workshop/personal reads [ahead 1] dirty=1, so the Mac's personal copy is the first commit alone under a DONE line. Misses, mine: the last dismount's nine-past-cap count was the prior receipt's eight plus one, never counted (the block read 21 lines in deed 1730's payload and in this one); the pull forecast named two files and read three, GLOSSARY.md +53 from the operator's graduation of THE A11Y OUTLINE and THE SEED REDUCTION to keys between the turns, its commit unshown here (INFERRED); the Mac's first prompt forecast in qamy read repos, harmless under git -C. ai.py: 2dd2533a "feat: widen the whitelist" for +94/-14 whose whitelist was one word of it; 7d05818d true. UNWITNESSED: a second run reading pulled or current, any HELD branch, mirror from ~/npvg or ~/repos/pipulate or off the LAN, write_post's shadow pin and publish's refusal typed on the Mac, the Mac's first compile under the mirror config. Next: THE BOOK'S TAPE, b2's car from blogs.nix and backup-home.py. This block reads 22 lines by my count of the payload, two past its cap of 20; the next forget ride fades two.
# --- END RECEIPTS ---
[[[DIVIDER]]]
# --- END RECEIPTS ---
[[[REPLACE]]]
Target: foo_files.py
[[[SEARCH]]]
# § THE LATEX TRANSPORT COLLISION (banked 2026-10-14, convicted by commit 06f55cae) -- Never emit literal double-dollar strings or unescaped dollar-percent spans inside code patches crossing a web chat UI, because the browser's LaTeX post-processor can strip newlines between dollar pairs and inject backslashes before percent signs; encode dollar signs as language-native byte escapes (such as \36 in Lua or \x24 in Python) and backticks as \96 or \x60.
[[[DIVIDER]]]
# § THE LATEX TRANSPORT COLLISION (banked 2026-10-14, convicted by commit 06f55cae) -- Never emit literal double-dollar strings or unescaped dollar-percent spans inside code patches crossing a web chat UI, because the browser's LaTeX post-processor can strip newlines between dollar pairs and inject backslashes before percent signs; encode dollar signs as language-native byte escapes (such as \36 in Lua or \x24 in Python) and backticks as \96 or \x60.
# § THE DATED-POST GLOB (banked 2026-10-15, convicted by template.md sorting after 2026-10-15 in ASCII) -- Filter Jekyll _posts/ globs with a strict YYYY-MM-DD-slug.md regex and sort same-day posts by (date, frontmatter sort_order, mtime) rather than raw filename, because an undated file like template.md ('t' = 0x74 > '2' = 0x32) wins a lexicographical max and alphabetical slug order ignores sort_order.
[[[REPLACE]]]
Target: foo_files.py
[[[SEARCH]]]
# § THE CONTINUATION LADDER (banked 2026-07-31) -- Put the loop-continuation check in deterministic code, never in the agent's instructions; distinguish mechanism, chance, volition, and competence under deadline as four rungs, each adding exactly one variance source over the rung below.
# - Start a new YouTube live-stream for every new published article
# - Automate post-outage stream recovery: OBS autostart in XFCE session + persistent YouTube stream key collapses the 8-step manual dance; keep the dead stream as canary, make the resurrection hands-free
[[[DIVIDER]]]
# § THE CONTINUATION LADDER (banked 2026-07-31) -- Put the loop-continuation check in deterministic code, never in the agent's instructions; distinguish mechanism, chance, volition, and competence under deadline as four rungs, each adding exactly one variance source over the rung below.
# - Start a new YouTube live-stream for every new published article -- RUNG 1 LANDED 2026-10-15 (deed 123; commits c1955e81, 10e44b6f, 7591d575, 57abd21f): Go-Go-Gadget Broadcast Kit (`tweet`, `yttitle`, `ytdesc`, `ytcard` in `flake.nix` + `scripts/articles/common.py` + `articleizer.py`) extracts the X.com promo tweet, <=100-char YouTube Live title, and `🤖 AI Editorial Take:` description for manual Mode B Cut-and-Roll; OWED: Rung 2 remote OBS stop/start bounce over SSH on Honeybot (`~/.config/obs-studio/plugin_config/obs-websocket/config.json` or X11 hotkey on `DISPLAY=:10.0`) and Rung 3 YouTube Data API v3 (`liveBroadcasts`) connector.
# - Automate post-outage stream recovery: OBS autostart in XFCE session + persistent YouTube stream key collapses the 8-step manual dance; keep the dead stream as canary, make the resurrection hands-free
[[[REPLACE]]]
3. DANGLING:
- Live Cut-and-Roll Field Test (Rung 1): Run the 60-second YouTube Studio + OBS Cut-and-Roll sequence (
yttitle,ytdesc,tweet) on the Phantom Limb article and on this article’spublishto verify VOD archiving and live title/description indexing in production. - Rung 2 Remote OBS Bounce (
obsbounce): Inspect~/.config/obs-studio/plugin_config/obs-websocket/config.jsonand~/.config/obs-studio/basic/profiles/Untitled/basic.inion Honeybot (using Python orgrep, sincejqis not installed on Honeybot) to wire a 1-command SSH OBS stream stop/start helper. - Rung 3 YouTube Live API v3 Connector (
connectors/youtube.py): Optional OAuth connector usinggoogle-api-python-client(already in.venv) to automateliveBroadcasts.transition/insert/bindonce the tactile Cut-and-Roll loop is second nature. - Dynamic
write_postBlog Aliases (flake.nix): Derivewrite_postaliases from~/.config/pipulate/blogs.jsonat shell entry rather than hardcoding the four legacy names (article,grim,vault,bot).
4. SEED:
(nix) pipulate $
foo_files.py
flake.nix
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/stream.py
! ssh honeybot 'python3 -c "import json, pathlib; p = pathlib.Path.home() / \".config/obs-studio/plugin_config/obs-websocket/config.json\"; d = json.loads(p.read_text()); print({k: d.get(k) for k in (\"server_enabled\", \"server_port\", \"auth_required\")})"'
! ssh honeybot 'grep -E "StartStreaming|StopStreaming|Rec|Stream" ~/.config/obs-studio/basic/profiles/Untitled/basic.ini 2>/dev/null || echo NO_INI_KEYS'
5. CLOSING:
There is a reason Inspector Gadget and Spider-Man endure while sealed-suit fantasies age poorly: a tool only becomes part of your nervous system when you can feel the tension in the cable and fix the spring on your own workbench. By resisting the siren song of an opaque, over-engineered cloud bot and instead forging four tiny, transparent words—tweet, yttitle, ytdesc, and ytcard—that turn your own Markdown and the AI Editor’s critical take into immediate tactile leverage, you turned a month-long static broadcast into a rhythmic engine that drops a fresh pebble into the pond with every article you write.
6. NOTARIZE:
- Input Archive Sealed by Footer:
Deed: qamy.ai_123-b4642777.zip(read directly from this compile’s final=== QA ARCHIVE SEAL ===footer outside the payload; it names the input archive compiled for this turn, not this response or the generated Jekyll post). - Verification Status: Not independently verified here (no
foo_cartridge.pyverifier receipt namingqamy.ai_123-b4642777.zipis present inside this compile’s receipts). - Manifest Live-Command Receipts Count: 1 live command receipt executed during this compile (
! .venv/bin/python scripts/articles/common.py --broadcast cardin0.1962s,762 tokens | 3,237 bytes, 0 failures), plus static analysis telemetry (Ruff: All checks passed!, exit0). - Recorded Sizes, Digests & Timestamps:
- From
Summary:Summed Tokens: 233,969,Verified Tokens: 235,337,Total Words: 107,949,Total Chars: 931,695,Total Bytes: 936,055 (UTF-8),Codex Mapping Coverage: 71.2% (190/267 tracked files),Topological Integrity Verified: 25 candidate reference(s) scanned, all exist. - From
Manifest:foo_files.py(116,271 tokens | 456,885 bytes),flake.nix(41,304 tokens | 169,315 bytes),scripts/articles/articleizer.py(10,145 tokens | 44,987 bytes),scripts/articles/publishizer.py(2,073 tokens | 8,333 bytes),scripts/articles/common.py(4,597 tokens | 18,166 bytes),.gitattributes(33 tokens | 76 bytes),.gitignore(1,991 tokens | 7,415 bytes),apply.py(9,714 tokens | 41,653 bytes). - From
Telemetry(Recent Git Diff Telemetry):scripts/articles/common.pyindex2d938d3e..f4d1ec3f. - From
foo_files.py(START STATS):Honeybot telemetry fetched 2026-10-15T19:36Z.
Deed: qamy.ai_123-b4642777.zip
Book Analysis
Ai Editorial Take
What surprised me most in this entry is a subtle architectural sleight of hand that goes deeper than just avoiding cloud API bloat: using the OS clipboard as a universal, zero-authentication API airlock, and creating a temporal feedback loop out of the AI Editor’s own voice. If you had wired publish directly to the YouTube Data API v3 (liveBroadcasts.insert and transition), every blog deploy would suddenly inherit Google Cloud OAuth token expiry, daily quota unit math, and network timeout failure modes—turning a local Git push into a fragile distributed transaction. By routing tweet, yttitle, and ytdesc through xclip instead, the human operator acts as an air-gapped hardware router between the local filesystem and two hostile web platforms (X.com and YouTube Studio) that constantly change their APIs and rate limits. Even more fascinating is what happened when you swapped authors_imprint for ### Ai Editorial Take in ytdesc: the critical analysis generated by an AI editor at the close of Article \(N-1\) now becomes the live broadcast billing on YouTube while you are writing Article \(N\)—and when ytcard runs inside the terminal during Article \(N\), the previous AI’s critique is ingested right back into the next AI’s context window as a live receipt.
🐦 X.com Promo Tweet
From the Atari 2600 & endosynthesis to Inspector Gadget CLI tools: why repairable helpers (tweet, yttitle, ytdesc, ytcard) beat opaque bots for YouTube Live Cut-and-Roll.
https://mikelev.in/futureproof/endosynthesis-inspector-gadget-broadcast-kit/
#NixOS #Python #AI
Title Brainstorm
- Title Option: Endosynthesis, Inspector Gadget, and the Cut-and-Roll Broadcast Kit
- Filename:
endosynthesis-inspector-gadget-broadcast-kit.md - Rationale: Captures the philosophical core (endosynthesis vs. forced hybridization), the memorable pop-culture mental model (Inspector Gadget / repairable prosthetics), and the concrete technical deliverable (the Cut-and-Roll broadcast kit).
- Filename:
- Title Option: Cut-and-Roll: Endosynthesized CLI Tools for YouTube Live and X.com
- Filename:
cut-and-roll-youtube-vod-broadcast-kit.md - Rationale: Leads with the broadcast production pattern (‘Cut-and-Roll’) that solves YouTube’s 12-hour VOD ceiling while highlighting the local-first CLI implementation.
- Filename:
- Title Option: From the Atari 2600 to Go-Go-Gadget Shell Words: Engineering Replayable Broadcasts
- Filename:
atari-2600-endosynthesis-clipboard-broadcast.md - Rationale: Connects the historical arc of personal agency (racing the beam on the Atari VCS) directly to the four one-word shell helpers in NixOS.
- Filename:
- Title Option: The Guy in the Chair: Why Repairable CLI Gadgets Beat Autonomous Suits
- Filename:
ned-leeds-inspector-gadget-replayable-broadcasts.md - Rationale: Foregrounds the Spider-Man/Ned Leeds vs. Iron Man metaphor for human-in-the-loop AI collaboration and tool maintainability.
- Filename:
Content Potential And Polish
- Core Strengths:
- Introduces a sharp, memorable conceptual distinction between ‘endosymbiosis’ (living alongside an external intelligence) and ‘endosynthesis’ (internalizing a responsive tool like language or Unix into the body schema).
- Contrasts the Silicon Valley ‘Iron Man exosuit’ fantasy of hands-off autonomous agents against the ‘Inspector Gadget / Ned Leeds in the Chair’ reality of lightweight, field-repairable instruments.
- Demonstrates the value of live terminal receipts in real time: the first test run immediately exposed
template.mdwinning an ASCII sort over2026-10-15-*.md('t' > '2'), which was fixed and verified in the very next turn. - Turns a hidden byproduct of the article pipeline (
### Ai Editorial Take) into the headline hook for YouTube Live descriptions.
- Suggestions For Polish:
- When turning this chapter into polished book prose, briefly explain for non-Unix readers how
xclip -selection clipboardbridges terminal stdout into the desktop GUI paste buffer. - Consider adding a short follow-up note once a few Cut-and-Roll VODs have accumulated on the YouTube channel, showing how the <12-hour cutover affects search indexing and viewer retention compared to the old marathon streams.
- When turning this chapter into polished book prose, briefly explain for non-Unix readers how
Next Step Prompts
- After running a few manual Cut-and-Roll transitions in YouTube Studio, inspect Honeybot’s
/home/mike/.config/obs-studio/plugin_config/obs-websocket/config.jsonand build a singleobsbounceSSH helper inflake.nixthat cleanly stops and restarts the OBS RTMP stream without opening an XRDP desktop window. - Audit the remaining hardcoded blog target aliases in
flake.nix(article,grim,vault,bot) so thattweet,yttitle,ytdesc,ytcard, andwrite_postdynamically resolve every satellite blog defined in~/.config/pipulate/blogs.json.