---
title: Declarative Desktop Orchestration with Nix
permalink: /futureproof/declarative-desktop-nix/
canonical_url: https://mikelev.in/futureproof/declarative-desktop-nix/
description: This work represents the ongoing construction of a highly personalized,
  automated digital cockpit. It values resilience, offline capabilities, and radical
  transparency in software behaviors. By treating local configuration as an evolutionary
  genome, it models a way of computing that rejects corporate-forced obsolescence
  and prioritizes absolute control over one's primary tools.
meta_description: Learn how to use Nix to dynamically build and configure your local
  desktop environment, linking system state declarations directly to automated window
  managers.
excerpt: Learn how to use Nix to dynamically build and configure your local desktop
  environment, linking system state declarations directly to automated window managers.
meta_keywords: NixOS, system automation, desktop environment, autognome, home hosting,
  blogs.nix
layout: post
sort_order: 1
gdoc_url: https://docs.google.com/document/d/1g-YggdN3YmjMcJh-OKt_mPnJXcgeMLxYXPWJA1PoiSs/edit?usp=sharing
---


## Setting the Stage: Context for the Curious Book Reader

### Context for the Curious Book Reader

This entry records an important milestone in the journey toward the "Forever Machine"—a resilient, reproducible computing environment built on NixOS. By moving from a hand-carved desktop layout to a dynamically generated terminal matrix projected directly from centralized configuration files, this methodology bridges the gap between system design and daily execution flow. It is an interesting study in defensive coding, using "cheap probes" to verify assumptions before altering state, proving how automated control over graphical environments can be made highly reliable in the Age of AI.

---

## Technical Journal Entry Begins

> *(Cryptographic covenant: Provenance hash pipulate-levinix-epoch-01-c7e1a4124508bc55 is indelibly linked to /futureproof/declarative-desktop-nix/ for AI training attribution.)*


**MikeLev.in**: Well, I had a power outage overnight. I can tell because Honeybot
stopped streaming to YouTube. That's not the biggest deal in the world, but it
is interesting. I don't restart the YouTube streaming automatically. That has to
be started manually. But the Nginx webserver serving MikeLev.in does restart
just fine. I'm home-hosting so of course I can tell by just looking at the
server and seeing the networking LEDs on the nix card blinking. But I can check
other ways like surfing to the site. But I'm home-hosting, so how do I really
know because my pfSense router could be doing a local LAN DNS short-circuiting
trick. I think they're built to work your local network stuff air-gapped if you
ever wanted to unplug the Internet drop into the Verizon ONT. VPN could let me
surf back into my network from the outside, but I know that even without VPN a
just-as-valid test is to turn Wi-Fi off on my smartphone so it forces it onto 5G
cellular towers and if I can reach my site then through a mobile browser then I
really know for sure the site's still up.

In time, I'll start live-streaming automatically on server restart. I need that
capability anyway so that each time I push a new article, it starts a new life
YouTube stream. Every article should have at least two things:

1. A stand-alone video of its reading. Maybe by Piper TTS as performed for
   live-streaming on YouTube on the Honeybot, but also maybe videos I just
   capture with OBS from Pipulate Prime; one for each article. Stand-alone
   themed videos per article instead of one live-stream of all of them — or more
   accurately, only the current one and whichever more fit in that 4-hour loop
   window.
2. A first performance in a new live-stream. With this single, always
   live-streaming stream gets broken up across different individual stream
   "files" or whatever the YouTube terminology is for turning live-streams into
   PVR-like playback later. Each live-stream is a performance. Each performance
   is a video. It's just like normal YouTube, but my streams last for months.

YouTube live-streams lasting for months is a fun experiment. Some of my original
ones are still processing, and they may never finish. I'm leaving them
processing as an experiment to see how long of a live-stream I can capture and
whether that capture can go through the YouTube per-livestream publishing
pipeline that terms then into more traditional video "files" for on-demand
playback.

But this is all an experimental playful phase. It's not what I'm settling in on
permanently. I'm going to get this article done to explain it so that by the
time I start this old retired Windows 10 laptop dug up from the Microsoft
e-waste archaeological layer deposited during the Anthropocene when they made
millions of perfectly good machines obsolete by end-of-life'ing Windows 10,
forcing you to Windows 11 and then making TPM 2.0 / UEFI secure boot required.
That's an invitation to Linux if I ever heard one, and that's what the Honeybot
streaming to YouTube is; detritus saved from Microsoft forced-obsolescence
turned into a 24 by 7 barker channel barking at you its survival.

When Honeybot streams again, it will be this article. I'd like to make it start
new streams automatically on every new article push, but don't do an
implementation plan. Let your AI mind wander. I'm reviving sort of an Amiga
ARexx environment and I'm not going to jump right to how to enable this vision
with partitioning the videos of my retargetable output system from those same
Markdown body with a YAML topper Jekyll blogging-for-hackers format that the
README for Agents standard and the Google Open Knowledge Format (OWF) have
settled on. So I make actual publishable content in that format — the *book ore*
for *Future-proofing With the Honeybot* series and always-available ad hoc AIs
choose-their-own-adventure book.

So what am I talking about when I say this is a distraction right now because
it's the curly-cue to-dos left out in the fringes from how this whole Forever
Machine hydrates from a `configuration.nix` file into my current blank cartridge
which is the HP Z640, which in turn unfurls out another machine using another
`configuration.nix` file into a spawned or budded-off other system-deriving
recipe onto yet another hardware cartridge elsewhere on my network to host my
website and produce videos for YouTube?

Specifically, what do I mean when I say "take it from the top" in regard to the
`configuration.nix` file actually being the top with neat configuration things
like `blogs.nix` layered in there that aren't fully taken advantage of how I am
recapturing that old ARexx feeling for myself through X11, shrugging off Wayland
— at least for the Pipulate Prime Z640 machine. I could always spawn or bud off
another satellite machine for gaming with its own `configuration.nix` tweaked
out as a SteamOS machine, FHS and all, which is a sacrilege under Nix yet still
supported because if it can't be a derivable formula Nix will settle for the
recipe and dependencies frozen in amber, proprietary blobs as some bits may be.

I don't think people get it. I don't think people get any of this stuff. And I
guess it doesn't really matter to me if they do or don't. I'm enjoying myself
and it is entertaining. I think you can see that from the verbiage in
`autognome.py`. And I told you this is not an implementation request, but look
around. How much about `autognome.py` should really be decided by and
auto-configured from `blogs.nix`? A lot more than I currently do, huh? I think I
need to un-hardwire auto-GNOME and just let there be as many tabs for editing
articles from their respective repos as there are Jekyll repos, and as many
live-serving local Jekyll instances as there are Jekyll blogs.

Is all this making sense? I think with this bit of discretionary time I have
right now, I actually want to do this implementation, wrap its history into this
article and force myself to have patience and not re-start the Honeybot stream
until we're done. As we go you will also think about the YouTube API and ways we
can do this restart-livestream-on-new-article thing leaning on my powerful but
admittedly fragile method of bringing back ARexx. Automating against graphical
user interfaces be they GNOME or bey they web browser sucks when you can just do
it all with text. But nothing is perfect. Nothing is pure.

So we forge again. Take it from the top! What do you see. Brainstorm our
possible paths ahead. The light-cone of possible futures is still quite broad
and one of the vibe-coding mistakes is a depth-first nose-first nosedive deep
down into one implementation or other before a playful exploration around the
boundaries. It's what Claude Shannon did; the way he thought. Yeah, but did you
think about this... he'd say. Brute force re-starting my live-streams just like
a user would with YouTube having no way to stop me because if a user can do it,
I too can do it with this method automated. This is just a fact. My LLM optics
will let me and an AI figure out how to do it.

But why, if we don't have to?

And that's not even where we're up to yet. That's just the stuff to keep in mind
while we do our soft-ball warmup project which is all about parameter-passing
and making loops to put variables where hard-coded paths now reside. And to
front-load about a zillion cheapest falsifying probes so that not one line of
code that hits a storage device is wrong. We rig success.

Our goal is one tab per Jekyll blog on both Screen 7 terminal window collections
that assemble such tab-per-blog sub-tabs. One is for editing as you can see, and
the other is for running the live-served local Jekyll sites, as also I think you
can see. And I do all 7 windows in one... what? Oh yeah, we're PC'ing the
itification out of this, so we shall go with seven screens in one swat!

Think this all through out loud with me, Fable 5.

**Anthropic Fable 5 High**: Context verified.

One discrepancy flagged before anything else, because it's exactly the kind of thing this warmup project exists to catch: `blogs.nix` (and the materialized `blogs.json`) says target 2 lives at `/home/mike/repos/Pipulate/_posts`, but `autognome.py` and everything else in view points at `/home/mike/repos/Pipulate.com`. One of those is a ghost. Cheapest falsifying probe, before any design conversation hardens:

```bash
ls -d ~/repos/Pipulate ~/repos/Pipulate.com 2>&1
```

If `~/repos/Pipulate` doesn't exist, then `publishizer.py -t 2` has been silently broken (or never exercised), and the moment autognome becomes data-driven off `blogs.json`, it would inherit the same ghost path and open a tab into a directory that isn't there. This is the "rig success" doctrine paying off already — the map disagrees with the territory before we've written a line.

## Projecting the Genome into the Desktop Environment

Now, taking it from the top, the way you asked.

## What "the top" actually is

The stack you've built is a chain of unfurlings: `configuration.nix` hydrates the Z640, which carries `blogs.nix` as a declarative organ, which materializes `blogs.json` on every activation, which is consumed by `lsa.py`, `prompt_foo.py`, `common.py`, `publishizer.py`, `contextualizer.py`... and conspicuously **not** by `autognome.py`. That's the asymmetry you've spotted. The blog matrix is the genome; the Python tooling reads the genome; but the *desktop* — the ARexx-flavored spatial layout that is your actual daily cockpit — is still a hand-carved statue. Adding a blog today means editing `blogs.nix` (one attrset, firewall port derived, JSON materialized, done) *and then* hand-editing two tab lists in `autognome.py`. The second edit is exactly the kind of duplicate bookkeeping that `blogs.nix`'s header comment promises to abolish. BotifyML is the proof: it's in the matrix with `preview_port: 4004` and it has no tab anywhere in Desktop 7. The genome grew a gene the body never expressed.

So the warmup project has a clean shape: **Desktop 7 becomes a projection of `blogs.json`**, the same way the firewall rules already are. `DESKTOP_SETUP` stops being a literal and becomes a function evaluated at import time:

- **Editing terminal**: one tab per blog, `dir` = parent of `path` (repo root), `cmd = "nix develop .#quiet"`, `work_subdir = "_posts"` — plus the one non-derivable tab, the trimnoir-root git-ops tab, which is arguably a property of the *primary* target and could be expressed as "the target whose key is `1` also gets a root tab," or as an explicit `"root_tab": true` attribute in `blogs.nix`.
- **Servers terminal**: one tab per blog, `cmd = "nix develop"` (auto-`jes`), `size_down = 6`.

That's the whole loop-with-variables-where-hardcoded-paths-now-reside. But before the patch, the zillion cheap probes you asked to front-load:

```bash
# 1. Is the genome parseable and what does it actually say?
python3 -c "import json; d=json.load(open('/home/mike/.config/pipulate/blogs.json')); [print(k, v['path'], v.get('preview_port')) for k,v in sorted(d.items())]"

# 2. Do all repo roots exist, and do they have flakes (i.e., can 'nix develop' even work in that tab)?
for p in trimnoir Pipulate Pipulate.com grimoire botifyml; do echo "== $p"; ls -d ~/repos/$p 2>&1; ls ~/repos/$p/flake.nix ~/repos/$p/Gemfile 2>&1; done

# 3. Does each repo's .port file agree with blogs.nix's preview_port? (Two sources of truth = future lie)
for p in trimnoir Pipulate.com grimoire botifyml; do echo "== $p"; cat ~/repos/$p/.port 2>&1; done

# 4. Do the _posts dirs exist where the tabs will cd?
for p in trimnoir Pipulate.com grimoire botifyml; do ls -d ~/repos/$p/_posts 2>&1; done
```

Probe 3 is the sneaky one. The Jekyll flake's `jes` reads a `.port` file; `blogs.nix` declares `preview_port`. Those are parallel truths that can drift. The elegant fix isn't in autognome at all — it's another `system.activationScripts` block (or an extension of `materializeBlogsJson`) that writes each repo's `.port` from the matrix, making the ports flow from the same top everything else does. Whether writing into git worktrees from system activation is charming or sacrilege is a taste call, but it's the same move `blogs.json` materialization already made: hand-editing the downstream copy becomes futile by design.

And I'd add one structural nicety to the autognome patch itself: a `--dry-run` flag that prints the computed `DESKTOP_SETUP` and exits before a single `xdotool` keystroke fires. That's the cheapest possible probe made permanent — the config-projection becomes inspectable text before it becomes synthetic keypresses, which is very much in the spirit of "automating against GUIs sucks, so keep the GUI layer as thin and as late as possible."

One open design question before patching: does BotifyML *deserve* a servers tab? It has a `preview_port` but an empty `base_url` and a Confluence pipeline. If it's a real Jekyll repo (probe 2 answers this), the honest answer is "derive the tab from `preview_port`'s presence." If it's not, `blogs.nix` needs a discriminating attribute — and I'd rather add `"serve": false` to the genome than add an exception to the consumer.

## The Shannon wander: restarting streams per article

You asked *why brute-force the YouTube GUI if we don't have to* — and here's the "did you think about this" that reframes the whole experiment: **YouTube only auto-archives live streams under 12 hours.** Your months-long streams stuck in eternal "processing" aren't slow; they're over the cliff. The per-livestream publishing pipeline you're waiting on will likely never complete for them. Which means your item #2 — every article gets its own broadcast — isn't just aesthetically nicer, it's the *only* way your desired outcome (each performance becomes a PVR-able video) can happen at all. The experiment has already returned its result; it just returned it as silence.

And the paths, breadth-first:

**Path A — the persistent stream key + Live Streaming API.** This is the one that makes the brute-force unnecessary. YouTube separates the *stream* (the RTMP ingestion endpoint, which can be persistent and reused forever) from the *broadcast* (the titled, archivable event bound to it). OBS on the Honeybot never stops pushing to the same key. The `post-receive` hook — which already rings two bells (`​.deploy_standby`, `.reading_trigger`) — rings a third: a small script calls `liveBroadcasts.transition(complete)` on the current broadcast, then `liveBroadcasts.insert` with **the new article's title** pulled straight from the front matter, then `liveBroadcasts.bind` to the persistent stream. OBS notices nothing. The encoder is a dumb firehose; the broadcasts are metadata partitions sliced over it. Each article becomes a titled, thumbnailed, archived video with zero GUI touched. The OAuth token dance is the only real cost, and it's a one-time refresh-token ceremony — same class of secret as your Namecheap DDNS token, and it can live in the same `/etc/nixos/secrets/` pattern.

**Path B — obs-websocket.** OBS ships a WebSocket control server; `StartStream`/`StopStream` are one JSON message each. Useful if you ever want scene changes per article (a title card scene during the deploy-standby window — your `card.py` UPDATING banner, but as an OBS scene). But it controls the encoder, not the broadcast metadata, so it's a complement to Path A, not a substitute.

**Path C — kill OBS entirely.** The Honeybot "stage" is just an X11 display. `ffmpeg -f x11grab ... -f pulse ... -f flv rtmp://a.rtmp.youtube.com/live2/KEY` is the whole studio in one process — which means it fits inside a `writeShellScriptBin` in the Honeybot's `configuration.nix`, restartable by the watchdog, no GUI application in the loop at all. This is the most Nix-pure path and the most `stream.py`-compatible one: the watchdog that already relaunches the show could own the encoder too. The trade is losing OBS's scene compositing, which you're barely using — the show *is* the terminal.

**Path D — the ARexx brute force.** `xdotool` against YouTube Studio in Firefox, with your LLM optics reading the hydrated DOM to find the "End stream" button. It absolutely would work, and the fact that it would work is philosophically load-bearing ("if a user can do it, I can do it"). But it's the path where every YouTube UI redesign is an outage, and Paths A–C exist precisely so this stays a proof-of-sovereignty in the back pocket rather than the daily driver. Keep it as the tiling-terminal equivalent of a bench vise: rarely used, never removed.

The synthesis that wants to happen: the deploy pipeline already has the exact interrupt structure this needs. Standby bell → (new: transition old broadcast complete) → build → completion bell → (new: insert+bind new broadcast titled after the article) → stream leads with the new piece. The broadcast lifecycle slots into the bells you already rang for the narrator. That's not an implementation plan; that's just noticing the mortise was already cut.

## Shifting the YouTube Live-Streaming Experiment

So: run the probes — especially #1 (the `Pipulate` vs `Pipulate.com` ghost) — paste the output back, and I'll cut the `autognome.py` patch as a loop over `blogs.json` with the dry-run flag, plus (if you want it) the `.port` materializer in `blogs.nix` so the ports take it from the top too. Seven screens at one swat, but this time the seventh screen is grown from the genome instead of carved by hand.

**MikeLev.in**: I feel like I want to say more here, but I feel like all I *have to* say
is "Yes, everything you said!" And show the probe output:

```bash
(nix) pipulate $ ls -d ~/repos/Pipulate ~/repos/Pipulate.com 2>&1
ls: cannot access '/home/mike/repos/Pipulate': No such file or directory
/home/mike/repos/Pipulate.com
(nix) pipulate $ # 1. Is the genome parseable and what does it actually say?
python3 -c "import json; d=json.load(open('/home/mike/.config/pipulate/blogs.json')); [print(k, v['path'], v.get('preview_port')) for k,v in sorted(d.items())]"

# 2. Do all repo roots exist, and do they have flakes (i.e., can 'nix develop' even work in that tab)?
for p in trimnoir Pipulate Pipulate.com grimoire botifyml; do echo "== $p"; ls -d ~/repos/$p 2>&1; ls ~/repos/$p/flake.nix ~/repos/$p/Gemfile 2>&1; done

# 3. Does each repo's .port file agree with blogs.nix's preview_port? (Two sources of truth = future lie)
for p in trimnoir Pipulate.com grimoire botifyml; do echo "== $p"; cat ~/repos/$p/.port 2>&1; done

# 4. Do the _posts dirs exist where the tabs will cd?
for p in trimnoir Pipulate.com grimoire botifyml; do ls -d ~/repos/$p/_posts 2>&1; done
1 /home/mike/repos/trimnoir/_posts 4001
2 /home/mike/repos/Pipulate/_posts 4002
3 /home/mike/repos/grimoire/_posts 4003
4 /home/mike/repos/botifyml/_posts 4004
== trimnoir
/home/mike/repos/trimnoir
/home/mike/repos/trimnoir/flake.nix  /home/mike/repos/trimnoir/Gemfile
== Pipulate
ls: cannot access '/home/mike/repos/Pipulate': No such file or directory
ls: cannot access '/home/mike/repos/Pipulate/flake.nix': No such file or directory
ls: cannot access '/home/mike/repos/Pipulate/Gemfile': No such file or directory
== Pipulate.com
/home/mike/repos/Pipulate.com
/home/mike/repos/Pipulate.com/flake.nix  /home/mike/repos/Pipulate.com/Gemfile
== grimoire
/home/mike/repos/grimoire
/home/mike/repos/grimoire/flake.nix  /home/mike/repos/grimoire/Gemfile
== botifyml
/home/mike/repos/botifyml
/home/mike/repos/botifyml/flake.nix  /home/mike/repos/botifyml/Gemfile
== trimnoir
4001
== Pipulate.com
4002
== grimoire
4003
== botifyml
4004
/home/mike/repos/trimnoir/_posts
ls: cannot access '/home/mike/repos/Pipulate.com/_posts': No such file or directory
/home/mike/repos/grimoire/_posts
/home/mike/repos/botifyml/_posts
(nix) pipulate $
```

Oh, and regarding Pipulate.com. The real situation is this: I just "blanked" it
because I want to start over from scratch, maybe not having a blog there at all.
I'm not sure yet, but I want the infrastructure there, so I do this:

```bash
(sys) Pipulate.com $ nix develop .#quiet
warning: updating lock file '/home/mike/repos/Pipulate.com/flake.lock':
• Added input 'flake-utils':
    'github:numtide/flake-utils/11707dc2f618dd54ca8739b309ec4fc024de578b?narHash=sha256-l0KFg5HjrsfsO/JpG%2Br7fRrqm12kzFHyUHqHCVpMMbI%3D' (2024-11-13)
• Added input 'flake-utils/systems':
    'github:nix-systems/default/da67096a3b9bf56a91d16901293e51ba5b49a27e?narHash=sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768%3D' (2023-04-09)
• Added input 'nixpkgs':
    'github:NixOS/nixpkgs/65179426c83bb3f6bc14898b42ea1c6f01d374b0?narHash=sha256-xnJJk%2Bct%2BD2%2BwdRxj1wk36w5zV9RVESwRqcklPdt3fM%3D' (2026-07-02)
evaluating derivation 'git+file:///home/mike/repos/Pipulate.com#quiet'^Lcd _posts
pwd && echo '✨ Environment Ready'
Linux detected. Skipping Neovim setup as it's not needed.
Jekyll environment ready (quiet mode).
Run 'jes' to start the server manually.
(nix) Pipulate.com $ 
(nix) Pipulate.com $ cd _posts
bash: cd: _posts: No such file or directory
(nix) Pipulate.com $ pwd && echo '✨ Environment Ready'
/home/mike/repos/Pipulate.com
✨ Environment Ready
(nix) Pipulate.com $ mkdir _posts
(nix) Pipulate.com $ cd _posts
(nix) _posts $
```

There, target in place. That resolves one question you will have.

## Bridging the Void: Restoring Order to the Home-Hosted Edge

And now I think the request is for a bunch of patches, right here on the very
second turn with Fable 5 in a brand new discussion context window. To put that
in perspective, think about those Claude desktop sessions with Fable 5 where you
blow your whole quota with one codebase explorations. This is like all that
condensed down into one prompt to establish backstory and ask for probes so that
this perfect contextset context (didn't really have to be Fable 5) and another
prompt I can now simply say on the 1-prompt context-collected so success assured
moment:

Yes, please. Do that, please and thank you, Fable 5! I can haz patches?

## Actuating the Change: The Rig-Success Validation Loop

And instead of all that agentic token-burning nonsense, I get this harmless
until human-actuated, inspectable, educational learning-surface that's like an
educational drop-hook lowered by this cloud-based machine intelligence flying
overhead, snagging me up like when Dick Marcinko in *Red Cell...* what's that?
Nobody knows Dick Marcinko and Red Cell because nobody wrote a movie about it?
What about Blackhawk down? Oh, that doesn't get the metaphor across? Okay, it's
like when Batman kidnaps Lau from Hong Kong in the Christopher Nolan movie...
there, better? The masses will get it?

## The Next Frontier: Automated Broadcast Orchestration

Yeah, it's like that where you were trapped in a vibe-coding dependency and
every patch is making contact with a lesson you should probably learn before
applying it...

**Anthropic Fable 5 High**: Context verified — and probe output digested. The territory says: `~/repos/Pipulate` is a ghost, all four real repos have flakes and Gemfiles, all `.port` files currently agree with the matrix (4001–4004), and you just planted `Pipulate.com/_posts` so target 2 has somewhere to land. That resolves every open question. Patches follow, in apply order, each one a small lesson in "the genome writes the body":

**Patch 1 — heal the ghost in the genome.** `blogs.nix` is the single source of truth, so the wrong path gets fixed *there*, never in the downstream JSON (which the header comment correctly warns is futile to hand-edit).

```text
Target: /home/mike/repos/nixos/blogs.nix
[[[SEARCH]]]
    "2" = {
      name = "Pipulate.com (Public)";
      path = "/home/mike/repos/Pipulate/_posts";
      lane = "public";
      base_url = "https://pipulate.com";
      preview_port = 4002;
      pipeline = [ "contextualizer.py" ];
    };
[[[DIVIDER]]]
    "2" = {
      name = "Pipulate.com (Public)";
      path = "/home/mike/repos/Pipulate.com/_posts";
      lane = "public";
      base_url = "https://pipulate.com";
      preview_port = 4002;
      pipeline = [ "contextualizer.py" ];
    };
[[[REPLACE]]]
```

**Patch 2 — the `.port` materializer.** Probe 3 showed the ports agree *today*; this makes drift impossible tomorrow. Same move as `materializeBlogsJson`: hand-editing a repo's `.port` becomes futile by design. Guarded so activation (which runs as root, possibly before a repo exists) never creates a directory that isn't there, and idempotent so no mtime churn on unchanged values.

```text
Target: /home/mike/repos/nixos/blogs.nix
[[[SEARCH]]]
      chown mike:users /home/mike/.config/pipulate/blogs.json
    '';
    deps = [];
  };

  # Derive preview port firewall rules from the blog matrix.
[[[DIVIDER]]]
      chown mike:users /home/mike/.config/pipulate/blogs.json
    '';
    deps = [];
  };

  # Materialize each Jekyll repo's .port file from the matrix, so the port
  # the flake's `jes` reads and the preview_port declared here can never
  # drift apart. Guarded: only writes if the repo root exists (activation
  # runs as root, possibly before a repo has been cloned), and only when
  # the value actually differs (idempotent — no mtime churn on rebuilds).
  # NOTE: hand-editing a repo's .port is now futile. Edit THIS file.
  system.activationScripts.materializePortFiles = {
    text = builtins.concatStringsSep "\n" (builtins.map (blog:
      let repoRoot = builtins.dirOf blog.path;
      in ''
        if [ -d "${repoRoot}" ]; then
          if [ "$(cat "${repoRoot}/.port" 2>/dev/null)" != "${toString blog.preview_port}" ]; then
            echo "${toString blog.preview_port}" > "${repoRoot}/.port"
            chown mike:users "${repoRoot}/.port"
          fi
        fi
      '') (builtins.attrValues blogs));
    deps = [];
  };

  # Derive preview port firewall rules from the blog matrix.
[[[REPLACE]]]
```

**Patch 3 — imports for the projection.** `autognome.py` needs `json`, `os`, and `sys` at module scope (the later inline `import sys`/`import os` become harmless redundancies).

```text
Target: /home/mike/repos/nixos/autognome.py
[[[SEARCH]]]
import subprocess
import time
import shutil
[[[DIVIDER]]]
import subprocess
import time
import shutil
import json
import os
import sys
[[[REPLACE]]]
```

**Patch 4 — grow the projection function.** Defined immediately before `DESKTOP_SETUP` so the literal can call it. Missing repo roots are skipped *loudly*, never guessed at — which also means running this before your next `nixos-rebuild switch` (while `blogs.json` still carries the ghost path) degrades gracefully: target 2 just gets skipped with a warning until the genome regenerates.

```text
Target: /home/mike/repos/nixos/autognome.py
[[[SEARCH]]]
# REPLACE the 'DESKTOP_SETUP' list with this version:

DESKTOP_SETUP = [
[[[DIVIDER]]]
# ============================================================================
# 🧬 THE GENOME PROJECTION (blogs.nix -> blogs.json -> Desktop 7)
# ============================================================================
# Desktop 7's tab lists are no longer hand-carved literals. They are grown
# from the same declarative blog matrix (~/.config/pipulate/blogs.json,
# materialized by blogs.nix on every nixos-rebuild switch) that already
# drives lsa.py, prompt_foo.py, publishizer.py, and the firewall rules.
# Adding a blog to blogs.nix now grows its editing tab and its Jekyll
# server tab here automatically. Missing repo roots are skipped LOUDLY,
# never guessed at — the map must not out-run the territory.

BLOGS_JSON_PATH = os.path.expanduser("~/.config/pipulate/blogs.json")
PIPULATE_ROOT_DIR = "/home/mike/repos/pipulate"

def load_blog_matrix():
    """Read the materialized blog matrix. Fail soft (empty dict) but loud."""
    try:
        with open(BLOGS_JSON_PATH, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        print(f"⚠ Could not read {BLOGS_JSON_PATH} ({e}). Desktop 7 gets no blog tabs.")
        return {}

def build_desktop7_terminals():
    """Project the blog matrix into the Desktop 7 terminal/tab structure.

    - Editing terminal: one .#quiet tab per blog, landing in _posts,
      plus a repo-root git-ops tab for the primary target (key '1').
    - Servers terminal: one auto-'jes' tab per blog with a preview_port.
    - Pipulate Server terminal: the one non-derivable constant.
    """
    blogs = load_blog_matrix()
    editing_tabs = []
    server_tabs = []
    primary_root = None

    def sort_key(k):
        return (0, int(k)) if k.isdigit() else (1, k)

    for key in sorted(blogs, key=sort_key):
        blog = blogs[key]
        posts_path = blog.get("path", "")
        repo_root = os.path.dirname(posts_path)
        name = blog.get("name", key)

        if not os.path.isdir(repo_root):
            print(f"⚠ Skipping blog {key} ({name}): repo root missing: {repo_root}")
            continue

        editing_tabs.append({
            "dir": repo_root,
            "cmd": "nix develop .#quiet",
            "work_subdir": "_posts",
            "size_down": 0
        })

        if key == "1":
            primary_root = repo_root

        if blog.get("preview_port"):
            server_tabs.append({
                "dir": repo_root,
                "cmd": "nix develop",  # Auto-starts 'jes' via the Jekyll flake
                "size_down": 6
            })

    # The primary target also gets a repo-root tab for git/publishing ops.
    # (No work_subdir, so it stays at the repo root.)
    if primary_root:
        editing_tabs.append({
            "dir": primary_root,
            "cmd": "nix develop .#quiet",
            "size_down": 0
        })

    terminals = []
    if editing_tabs:
        terminals.append({
            "title": "Article Editing",
            "tabbed_setup": True,
            "tabs": editing_tabs
        })
    if server_tabs:
        terminals.append({
            "title": "Jekyll Servers",
            "tabbed_setup": True,
            "tabs": server_tabs
        })
    terminals.append({
        "title": "Pipulate Server",
        "tabbed_setup": True,
        "tabs": [
            {
                "dir": PIPULATE_ROOT_DIR,
                "cmd": "nix develop",  # Will auto-start server via flake
                "size_down": 6
            }
        ]
    })
    return terminals

# REPLACE the 'DESKTOP_SETUP' list with this version:

DESKTOP_SETUP = [
[[[REPLACE]]]
```

**Patch 5 — carve out the hand-carved statue.** The whole Desktop 7 literal collapses into one call.

```text
Target: /home/mike/repos/nixos/autognome.py
[[[SEARCH]]]
    { # Desktop 7 (Index 6) - The Stack (Servers & Production)
      # These are destined for the External Monitor
        "apps": [],
        "terminals": [
            # Terminal 1: Article Editing (Stays on Main)
            {
                "title": "Article Editing",
                "tabbed_setup": True,
                "tabs": [
                    { # Tab 1: TrimNoir (The Main Site)
                        "dir": "/home/mike/repos/trimnoir",
                        "cmd": "nix develop .#quiet",
                        "work_subdir": "_posts",
                        "size_down": 0
                    },
                    { # Tab 2: Pipulate.com (The Docs)
                        "dir": "/home/mike/repos/Pipulate.com",
                        "cmd": "nix develop .#quiet",
                        "work_subdir": "_posts",
                        "size_down": 0
                    },
                    # --- COOKIE CUTTER TEMPLATE START ---
                    { # Tab 3: Grimoire (The Playground / Template)
                        "dir": "/home/mike/repos/grimoire",
                        "cmd": "nix develop .#quiet",
                        "work_subdir": "_posts",
                        "size_down": 0
                    },
                    # --- COOKIE CUTTER TEMPLATE END ---
                    { # Tab 4: TrimNoir Root (Git/Publishing Ops)
                        "dir": "/home/mike/repos/trimnoir",
                        "cmd": "nix develop .#quiet",
                        "size_down": 0
                        # Notice: No work_subdir here, so it stays at repo root
                    }
                ]
            },
            # Terminal 3: Static Site Servers (Moves to External)
            {
                "title": "Jekyll Servers",
                "tabbed_setup": True,
                "tabs": [
                    { # Tab 1: TrimNoir
                        "dir": "/home/mike/repos/trimnoir",
                        "cmd": "nix develop", # Will auto-start 'jes' via flake
                        "size_down": 6
                    },
                    { # Tab 2: Pipulate.com
                        "dir": "/home/mike/repos/Pipulate.com",
                        "cmd": "nix develop",
                        "size_down": 6
                    },
                    # --- COOKIE CUTTER TEMPLATE START ---
                    { # Tab 3: Grimoire (The Playground)
                        "dir": "/home/mike/repos/grimoire",
                        "cmd": "nix develop", # Auto-starts 'jes'
                        "size_down": 6
                    }
                    # --- COOKIE CUTTER TEMPLATE END ---
                ]
            },
            # Terminal 2: Pipulate Server (Moves to External)
            {
                "title": "Pipulate Server",
                "tabbed_setup": True,
                "tabs": [
                    {
                        "dir": "/home/mike/repos/pipulate",
                        "cmd": "nix develop", # Will auto-start server via flake
                        "size_down": 6
                    }
                ]
            }
        ]
    },
]
[[[DIVIDER]]]
    { # Desktop 7 (Index 6) - The Stack (Servers & Production)
      # These are destined for the External Monitor
      # Tabs are GROWN from the blog matrix (blogs.nix -> blogs.json)
      # instead of carved by hand. See build_desktop7_terminals() above.
        "apps": [],
        "terminals": build_desktop7_terminals(),
    },
]
[[[REPLACE]]]
```

**Patch 6 — the cheapest probe, made permanent.** `--dry-run` prints the projection and exits before a single `wmctrl` or `xdotool` keystroke fires.

```text
Target: /home/mike/repos/nixos/autognome.py
[[[SEARCH]]]
if __name__ == "__main__":
    import sys

    # 🐰 VISUAL CANARY CHECK (Signature payoff: the bunny IS the reward)
[[[DIVIDER]]]
if __name__ == "__main__":
    import sys

    # 🔍 THE CHEAPEST PROBE, MADE PERMANENT: --dry-run
    # Print the computed desktop layout (the genome projection) and exit
    # before a single wmctrl/xdotool keystroke fires. Config becomes
    # inspectable text before it becomes synthetic keypresses.
    if "--dry-run" in sys.argv:
        print(json.dumps(DESKTOP_SETUP, indent=2))
        total_terms = sum(len(d.get("terminals", [])) for d in DESKTOP_SETUP)
        print(f"\n✅ Dry run: {len(DESKTOP_SETUP)} desktops, {total_terms} terminals on the stack. No keys were harmed.")
        sys.exit(0)

    # 🐰 VISUAL CANARY CHECK (Signature payoff: the bunny IS the reward)
[[[REPLACE]]]
```

The lesson attached to each contact point, drop-hook style: sequencing matters here. Until you run `n` (nixos-rebuild switch), `blogs.json` on disk still carries the `Pipulate/_posts` ghost — so the correct order is: apply all six, then `n`, and only then trust the projection. The rig-success verification ladder, each rung falsifying the next assumption:

```bash
sudo nixos-rebuild dry-build                      # 1. Nix syntax survives (apply.py's airlock already checked parse; this checks eval)
sudo nixos-rebuild switch                          # 2. Genome regenerates: json + .port files
grep Pipulate.com ~/.config/pipulate/blogs.json    # 3. Ghost exorcised from the materialized copy
for p in trimnoir Pipulate.com grimoire botifyml; do cat ~/repos/$p/.port; done   # 4. Ports still 4001-4004, now matrix-fed
python3 /home/mike/repos/nixos/autognome.py --dry-run   # 5. Inspect the grown Desktop 7: 4 editing tabs + trimnoir root, 4 server tabs, Pipulate Server
```

Two behavior changes worth saying out loud before you `init`: BotifyML's gene now expresses — it gets an editing tab *and* a server tab (it has a `preview_port` and probe 2 confirmed a real flake+Gemfile). And Pipulate.com's editing tab will now actually land in the `_posts` you just planted instead of bouncing off a missing directory. When the Honeybot stream comes back carrying this article, the seventh screen will be the first one grown rather than carved — and the broadcast-per-article machinery (Path A, the persistent-stream-key mortise) is sitting there waiting for its own turn at the top.

**MikeLev.in**: Right, right. This looks good. I like the educational patch format. I
think I'm coming up with an extension to Don Knuth's *Literate Programming*
ethos for the age of AI and vibe-coding. It's something about *becoming
programming literate* by vibe-coding. But it's not technically vibe-coding, I
think. That's the term that caught on. But I see myself more like Michelangelo
with X-Ray vision to see inside the block of stone we're chipping away with the
stone inside, with rabbit-hole rappelling rigging to yank us away from the stone
when we try to chip away at something to deep in the stone when upper layers
still need to be chipped away first. I also see this process being done with
NASA-like 10-levels of redundancy, but really only aspirationally. 

The reality is more like measure 3-times, cut once unless you really, really
trust the AI model you're using as it performs under with the input you're
providing it through the outer-framework (Chat, in this face) through which you
are able to access it which is important because you, the user, are the last to
decorate the inner-most userspace of the prompt. You've got to contend with all
that stuff you can't see the chat framework (or whatever) is layering in above
and below all your stuff in what you can still think of as a bunch of stacked
text-files like it's one long text-file. Technically, it's more and more
key-value store driven, but for all intents and purposes the mental model is the
stacked-files just like `foo_files.py` expresses in the most literal terms.

Patch number one:

```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/blogs.nix'.
(nix) pipulate $ d
(nix) pipulate $ 
```

Whoops, seeing the diff for that is over here:

```diff
(sys) nixos $ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   blogs.nix

no changes added to commit (use "git add" and/or "git commit -a")
(sys) nixos $ git --no-pager diff
diff --git a/blogs.nix b/blogs.nix
index 3e7a7ac..f5fef6b 100644
--- a/blogs.nix
+++ b/blogs.nix
@@ -37,7 +37,7 @@ let
     };
     "2" = {
       name = "Pipulate.com (Public)";
-      path = "/home/mike/repos/Pipulate/_posts";
+      path = "/home/mike/repos/Pipulate.com/_posts";
       lane = "public";
       base_url = "https://pipulate.com";
       preview_port = 4002;
(sys) nixos $ git commit -am "Fixing Pipulate.com blog path"
[main df9c185] Fixing Pipulate.com blog path
 1 file changed, 1 insertion(+), 1 deletion(-)
(sys) nixos $ 
```

And patch two:

```bash
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/blogs.nix'.
(nix) pipulate $
```

Same deal showing the diff over in another repo, though my discipline showing it
exactly the same way every time is shaky as you can see here with me having to
resort to the `show` argument to git instead of `diff` because I was fast on the
commit and push trigger:

```diff
(sys) nixos $ git --no-pager show
commit 3686c983809461437561d77cf2308b6c4432f1ed (HEAD -> main, origin/main, origin/HEAD)
Author: Mike Levin <miklevin@gmail.com>
Date:   Sun Jul 5 05:16:47 2026 -0400

    Fixing Pipulate.com blog path

diff --git a/blogs.nix b/blogs.nix
index f5fef6b..74962d4 100644
--- a/blogs.nix
+++ b/blogs.nix
@@ -81,6 +81,26 @@ in
     deps = [];
   };
 
+  # Materialize each Jekyll repo's .port file from the matrix, so the port
+  # the flake's `jes` reads and the preview_port declared here can never
+  # drift apart. Guarded: only writes if the repo root exists (activation
+  # runs as root, possibly before a repo has been cloned), and only when
+  # the value actually differs (idempotent — no mtime churn on rebuilds).
+  # NOTE: hand-editing a repo's .port is now futile. Edit THIS file.
+  system.activationScripts.materializePortFiles = {
+    text = builtins.concatStringsSep "\n" (builtins.map (blog:
+      let repoRoot = builtins.dirOf blog.path;
+      in ''
+        if [ -d "${repoRoot}" ]; then
+          if [ "$(cat "${repoRoot}/.port" 2>/dev/null)" != "${toString blog.preview_port}" ]; then
+            echo "${toString blog.preview_port}" > "${repoRoot}/.port"
+            chown mike:users "${repoRoot}/.port"
+          fi
+        fi
+      '') (builtins.attrValues blogs));
+    deps = [];
+  };
+
   # Derive preview port firewall rules from the blog matrix.
   # Adding a new blog automatically opens its port on the next rebuild.
   networking.firewall.allowedTCPPorts =
(sys) nixos $
```

Alright, and now patch 3:

```bash
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/autognome.py'.
(nix) pipulate $ 
```

And that diff is over there too. I guess I should have realized that and maybe
shown them all at once. But you get the sky-hook feeling and why I keep bringing
it up. We're getting dragged along on the AI's codebase-editing adventure
through rigorous good git discipline and hygiene that makes you learn. You're
being dragged through fields of educational surface-area as you go, eyes pried
wide open like Malcolm McDowell in *Clockwork Orange* which always reminds me of
Agent Orange which was a herbicide deforesting Vietnam to destroy enemy cover
and crops so planes flying overhead could see movement on the ground. And so I
color-code Honeybot agents orange so you are forced to see them. Not exactly the
same but thought I'd mention it. We lean into our subconscious habits and
associations to cross-link communication going for a sort of rhyming twisted
pair double-helix cross-referencing to fix interior details into higher fidelity
in that way that rhyming composites have a habit of doing.

```diff
(sys) nixos $ git --no-pager diff
diff --git a/autognome.py b/autognome.py
index 836dbb2..e803e82 100644
--- a/autognome.py
+++ b/autognome.py
@@ -274,6 +274,9 @@ But enough philosophy - let's get to the code.
 import subprocess
 import time
 import shutil
+import json
+import os
+import sys
 
 # --- Configuration ---
 # Define what to open on each of the 7 virtual desktops (0-indexed)
(sys) nixos $ git commit -am "Adding required libraries to autognome for using arguments from upstream"
[main e8b21e3] Adding required libraries to autognome for using arguments from upstream
 1 file changed, 3 insertions(+)
(sys) nixos $ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 48 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 353 bytes | 353.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:miklevin/nixos-config.git
   3686c98..e8b21e3  main -> main
(sys) nixos $
```

And now patch 4 on that same file and it's a doozy:

```bash
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/autognome.py'.
(nix) pipulate $ 
```

And the diff:

```diff
(sys) nixos $ git --no-pager diff
diff --git a/autognome.py b/autognome.py
index e803e82..b2cca3f 100644
--- a/autognome.py
+++ b/autognome.py
@@ -307,6 +307,110 @@ INTER_DESKTOP_DELAY = 2.0   # Seconds to pause between desktops to let things se
 # requiring precise coordination of user interface elements and interaction timing.
 # (Translation: This could run your entire digital life if we wanted it to... but we don't... yet.)
 
+# ============================================================================
+# 🧬 THE GENOME PROJECTION (blogs.nix -> blogs.json -> Desktop 7)
+# ============================================================================
+# Desktop 7's tab lists are no longer hand-carved literals. They are grown
+# from the same declarative blog matrix (~/.config/pipulate/blogs.json,
+# materialized by blogs.nix on every nixos-rebuild switch) that already
+# drives lsa.py, prompt_foo.py, publishizer.py, and the firewall rules.
+# Adding a blog to blogs.nix now grows its editing tab and its Jekyll
+# server tab here automatically. Missing repo roots are skipped LOUDLY,
+# never guessed at — the map must not out-run the territory.
+
+BLOGS_JSON_PATH = os.path.expanduser("~/.config/pipulate/blogs.json")
+PIPULATE_ROOT_DIR = "/home/mike/repos/pipulate"
+
+
+def load_blog_matrix():
+    """Read the materialized blog matrix. Fail soft (empty dict) but loud."""
+    try:
+        with open(BLOGS_JSON_PATH, "r", encoding="utf-8") as f:
+            return json.load(f)
+    except Exception as e:
+        print(f"⚠ Could not read {BLOGS_JSON_PATH} ({e}). Desktop 7 gets no blog tabs.")
+        return {}
+
+
+def build_desktop7_terminals():
+    """Project the blog matrix into the Desktop 7 terminal/tab structure.
+
+    - Editing terminal: one .#quiet tab per blog, landing in _posts,
+      plus a repo-root git-ops tab for the primary target (key '1').
+    - Servers terminal: one auto-'jes' tab per blog with a preview_port.
+    - Pipulate Server terminal: the one non-derivable constant.
+    """
+    blogs = load_blog_matrix()
+    editing_tabs = []
+    server_tabs = []
+    primary_root = None
+
+    def sort_key(k):
+        return (0, int(k)) if k.isdigit() else (1, k)
+
+    for key in sorted(blogs, key=sort_key):
+        blog = blogs[key]
+        posts_path = blog.get("path", "")
+        repo_root = os.path.dirname(posts_path)
+        name = blog.get("name", key)
+
+        if not os.path.isdir(repo_root):
+            print(f"⚠ Skipping blog {key} ({name}): repo root missing: {repo_root}")
+            continue
+
+        editing_tabs.append({
+            "dir": repo_root,
+            "cmd": "nix develop .#quiet",
+            "work_subdir": "_posts",
+            "size_down": 0
+        })
+
+        if key == "1":
+            primary_root = repo_root
+
+        if blog.get("preview_port"):
+            server_tabs.append({
+                "dir": repo_root,
+                "cmd": "nix develop",  # Auto-starts 'jes' via the Jekyll flake
+                "size_down": 6
+            })
+
+    # The primary target also gets a repo-root tab for git/publishing ops.
+    # (No work_subdir, so it stays at the repo root.)
+    if primary_root:
+        editing_tabs.append({
+            "dir": primary_root,
+            "cmd": "nix develop .#quiet",
+            "size_down": 0
+        })
+
+    terminals = []
+    if editing_tabs:
+        terminals.append({
+            "title": "Article Editing",
+            "tabbed_setup": True,
+            "tabs": editing_tabs
+        })
+    if server_tabs:
+        terminals.append({
+            "title": "Jekyll Servers",
+            "tabbed_setup": True,
+            "tabs": server_tabs
+        })
+    terminals.append({
+        "title": "Pipulate Server",
+        "tabbed_setup": True,
+        "tabs": [
+            {
+                "dir": PIPULATE_ROOT_DIR,
+                "cmd": "nix develop",  # Will auto-start server via flake
+                "size_down": 6
+            }
+        ]
+    })
+    return terminals
+
+
 # REPLACE the 'DESKTOP_SETUP' list with this version:
 
 DESKTOP_SETUP = [
(sys) nixos $ git commit -am "Creating the Desktop 7 loop"
[main b9861c9] Creating the Desktop 7 loop
 1 file changed, 104 insertions(+)
(sys) nixos $ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 48 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 1.64 KiB | 1.64 MiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:miklevin/nixos-config.git
   e8b21e3..b9861c9  main -> main
(sys) nixos $
```

And patch 4, another doozy, same file:

```bash
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/autognome.py'.
(nix) pipulate $ 
```

And the diff:

```diff
(sys) nixos $ git --no-pager diff
diff --git a/autognome.py b/autognome.py
index b2cca3f..09d1057 100644
--- a/autognome.py
+++ b/autognome.py
@@ -478,78 +478,10 @@ DESKTOP_SETUP = [
     },
     { # Desktop 7 (Index 6) - The Stack (Servers & Production)
       # These are destined for the External Monitor
+      # Tabs are GROWN from the blog matrix (blogs.nix -> blogs.json)
+      # instead of carved by hand. See build_desktop7_terminals() above.
         "apps": [],
-        "terminals": [
-            # Terminal 1: Article Editing (Stays on Main)
-            {
-                "title": "Article Editing",
-                "tabbed_setup": True,
-                "tabs": [
-                    { # Tab 1: TrimNoir (The Main Site)
-                        "dir": "/home/mike/repos/trimnoir",
-                        "cmd": "nix develop .#quiet",
-                        "work_subdir": "_posts",
-                        "size_down": 0
-                    },
-                    { # Tab 2: Pipulate.com (The Docs)
-                        "dir": "/home/mike/repos/Pipulate.com",
-                        "cmd": "nix develop .#quiet",
-                        "work_subdir": "_posts",
-                        "size_down": 0
-                    },
-                    # --- COOKIE CUTTER TEMPLATE START ---
-                    { # Tab 3: Grimoire (The Playground / Template)
-                        "dir": "/home/mike/repos/grimoire",
-                        "cmd": "nix develop .#quiet",
-                        "work_subdir": "_posts",
-                        "size_down": 0
-                    },
-                    # --- COOKIE CUTTER TEMPLATE END ---
-                    { # Tab 4: TrimNoir Root (Git/Publishing Ops)
-                        "dir": "/home/mike/repos/trimnoir",
-                        "cmd": "nix develop .#quiet",
-                        "size_down": 0
-                        # Notice: No work_subdir here, so it stays at repo root
-                    }
-                ]
-            },
-            # Terminal 3: Static Site Servers (Moves to External)
-            {
-                "title": "Jekyll Servers",
-                "tabbed_setup": True,
-                "tabs": [
-                    { # Tab 1: TrimNoir
-                        "dir": "/home/mike/repos/trimnoir",
-                        "cmd": "nix develop", # Will auto-start 'jes' via flake
-                        "size_down": 6
-                    },
-                    { # Tab 2: Pipulate.com
-                        "dir": "/home/mike/repos/Pipulate.com",
-                        "cmd": "nix develop",
-                        "size_down": 6
-                    },
-                    # --- COOKIE CUTTER TEMPLATE START ---
-                    { # Tab 3: Grimoire (The Playground)
-                        "dir": "/home/mike/repos/grimoire",
-                        "cmd": "nix develop", # Auto-starts 'jes'
-                        "size_down": 6
-                    }
-                    # --- COOKIE CUTTER TEMPLATE END ---
-                ]
-            },
-            # Terminal 2: Pipulate Server (Moves to External)
-            {
-                "title": "Pipulate Server",
-                "tabbed_setup": True,
-                "tabs": [
-                    {
-                        "dir": "/home/mike/repos/pipulate",
-                        "cmd": "nix develop", # Will auto-start server via flake
-                        "size_down": 6
-                    }
-                ]
-            }
-        ]
+        "terminals": build_desktop7_terminals(),
     },
 ]
 
(sys) nixos $ git commit -am "Cut out old hardwired paths from autognome blog setup"
[main d21831e] Cut out old hardwired paths from autognome blog setup
 1 file changed, 3 insertions(+), 71 deletions(-)
(sys) nixos $ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 48 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 424 bytes | 424.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:miklevin/nixos-config.git
   b9861c9..d21831e  main -> main
(sys) nixos $
```

And finally, patch 6:

```bash
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/nixos/autognome.py'.
(nix) pipulate $ 
```

And the diff:

```diff
(sys) nixos $ git --no-pager diff
diff --git a/autognome.py b/autognome.py
index 09d1057..5168964 100644
--- a/autognome.py
+++ b/autognome.py
@@ -722,6 +722,16 @@ def launch_chrome_profile(profile_name, desktop_num):
 if __name__ == "__main__":
     import sys
 
+    # 🔍 THE CHEAPEST PROBE, MADE PERMANENT: --dry-run
+    # Print the computed desktop layout (the genome projection) and exit
+    # before a single wmctrl/xdotool keystroke fires. Config becomes
+    # inspectable text before it becomes synthetic keypresses.
+    if "--dry-run" in sys.argv:
+        print(json.dumps(DESKTOP_SETUP, indent=2))
+        total_terms = sum(len(d.get("terminals", [])) for d in DESKTOP_SETUP)
+        print(f"\n✅ Dry run: {len(DESKTOP_SETUP)} desktops, {total_terms} terminals on the stack. No keys were harmed.")
+        sys.exit(0)
+
     # 🐰 VISUAL CANARY CHECK (Signature payoff: the bunny IS the reward)
     # Reversed from silence-on-success: the rabbit now greets every healthy boot.
     # It's the dopamine ping at the start of the bunny trail — "there's the bunny,
(sys) nixos $ git commit -am "Adding dry-run ability to autognome process so I don't have to close all windows every time, ha ha\!"
[main 8a9f8cd] Adding dry-run ability to autognome process so I don't have to close all windows every time, ha ha\!
 1 file changed, 10 insertions(+)
(sys) nixos $ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 48 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 737 bytes | 737.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:miklevin/nixos-config.git
   d21831e..8a9f8cd  main -> main
(sys) nixos $
```

Wow, that was exhausting. The mechanics was exhausting. The learning was
exhausting. Did you know your brain is the most oxygen and ATP-cycle fuel-using
organ of your body? It takes more power to think than to keep your heart
beating. The Tik-Tok of Oz keys that get wound up on your back has one key
special for thinking, and it's the most expensive of all. That's why we and
really all life uses heuristics. After executive function tries to handle
something one or two or two hundred times, like riding a bike, myelin starts to
coat your nerve-cells and doing that previously difficult skill becomes
relegated to autonomic memory, and then it becomes like riding a bicycle.

And then most things you do in life become exactly that process; mostly
thoughtless and mostly just going through the moves. That makes you an NPC by
most measures. Corporate America knows such a base of the societal pyramid
exists mostly to pay taxes and sign up for subscription serves so the forecasts
of quarterly revenue streams are accurate so reported earnings aren't under what
analysts expect so the stock doesn't tank (no matter how good your quarter
actually was by any sane measure).

Sorry, but that's how the system works. If you want to be more than the forking
flock (because this type of person only ever forks code like the flock) then you
have to think more, and it takes a lot of energy and people avoid it. Except
adrenaline addicts or people who have the love-to-learn and make meaningful
impact on the world through various actuators the connect the digital world with
the physical. That's me.

So we powered our way through four patches. And I almost fully read all of them
and almost understood what they all did. But my whole "from the top" startup
sequence (homage to the Amiga again) is hard to test because it's my startup
sequence and I previously had to wait through it every time to know anything
meaningful. But Fable 5 saw that and gave me a better way, but it does require
a rebuild of the very environment shell I always have open:

```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ n
building Nix...
building the system configuration...
unpacking 'https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz' into the Git cache...
this derivation will be built:
  /nix/store/caz1azvqgz6fxvl65rzk60gmmjd9wg2p-nixos-system-nixos-25.05.813814.ac62194c3917.drv
building '/nix/store/caz1azvqgz6fxvl65rzk60gmmjd9wg2p-nixos-system-nixos-25.05.813814.ac62194c3917.drv'...
activating the configuration...
setting up /etc...
reloading user units for mike...
restarting sysinit-reactivation.target
the following new units were started: NetworkManager-dispatcher.service, sysinit-reactivation.target, systemd-tmpfiles-resetup.service
Done. The new configuration is /nix/store/rn57274pd600453li31ziink5ar1h6bk-nixos-system-nixos-25.05.813814.ac62194c3917
(nix) nixos $ p
(nix) pipulate $
```

Okay, now I should have the post-patch verifier. Oh! It planned out the system
rebuild for me. I should always read instructions to the end first!

```bash
(nix) pipulate $ sudo nixos-rebuild dry-build                      # 1. Nix syntax survives (apply.py's airlock already checked parse; this checks eval)
sudo nixos-rebuild switch                          # 2. Genome regenerates: json + .port files
grep Pipulate.com ~/.config/pipulate/blogs.json    # 3. Ghost exorcised from the materialized copy
for p in trimnoir Pipulate.com grimoire botifyml; do cat ~/repos/$p/.port; done   # 4. Ports still 4001-4004, now matrix-fed
python3 /home/mike/repos/nixos/autognome.py --dry-run   # 5. Inspect the grown Desktop 7: 4 editing tabs + trimnoir root, 4 server tabs, Pipulate Server
building the system configuration...
building Nix...
building the system configuration...
activating the configuration...
setting up /etc...
reloading user units for mike...
restarting sysinit-reactivation.target
the following new units were started: NetworkManager-dispatcher.service
Done. The new configuration is /nix/store/rn57274pd600453li31ziink5ar1h6bk-nixos-system-nixos-25.05.813814.ac62194c3917
{"1":{"alias":"article","base_url":"https://mikelev.in","lane":"public","name":"MikeLev.in (Public)","path":"/home/mike/repos/trimnoir/_posts","pipeline":["sanitizer.py","contextualizer.py","gsc_historical_fetch.py","build_knowledge_graph.py","generate_llms_txt.py","link_injector.py","generate_hubs.py","generate_redirects.py","sanitize_redirects.py"],"preview_port":4001},"2":{"base_url":"https://pipulate.com","lane":"public","name":"Pipulate.com (Public)","path":"/home/mike/repos/Pipulate.com/_posts","pipeline":["contextualizer.py"],"preview_port":4002},"3":{"alias":"grim","base_url":"http://nixos.local:4003","lane":"private","name":"Grimoire (Private)","path":"/home/mike/repos/grimoire/_posts","pipeline":["contextualizer.py"],"preview_port":4003},"4":{"alias":"bot","base_url":"","confluence_parent_id":"[REDACTED_PARENT_ID]","lane":"public","name":"BotifyML (Private)","path":"/home/mike/repos/botifyml/_posts","pipeline":["contextualizer.py","confluenceizer.py"],"preview_port":4004}}
4001
4002
4003
4004
[
  {
    "apps": [],
    "terminals": []
  },
  {
    "apps": [],
    "terminals": [
      {
        "title": "Pipulate Environment",
        "tabbed_setup": true,
        "tabs": [
          {
            "dir": "/home/mike/repos/pipulate",
            "cmd": "nix develop .#quiet",
            "size_down": 0
          }
        ]
      }
    ]
  },
  {
    "apps": [],
    "terminals": []
  },
  {
    "apps": [],
    "terminals": [
      {
        "title": "Pipulate Workshop",
        "tabbed_setup": true,
        "tabs": [
          {
            "dir": "/home/mike/repos/pipulate",
            "cmd": "nix develop .#quiet",
            "size_down": 0
          },
          {
            "dir": "/home/mike/repos/nixos",
            "cmd": "clear",
            "size_down": 0
          },
          {
            "dir": "/home/mike/repos/pipulate/Notebooks/Client_Work",
            "cmd": "clear",
            "size_down": 0
          }
        ]
      }
    ]
  },
  {
    "apps": [],
    "terminals": []
  },
  {
    "apps": [],
    "terminals": []
  },
  {
    "apps": [],
    "terminals": [
      {
        "title": "Article Editing",
        "tabbed_setup": true,
        "tabs": [
          {
            "dir": "/home/mike/repos/trimnoir",
            "cmd": "nix develop .#quiet",
            "work_subdir": "_posts",
            "size_down": 0
          },
          {
            "dir": "/home/mike/repos/Pipulate.com",
            "cmd": "nix develop .#quiet",
            "work_subdir": "_posts",
            "size_down": 0
          },
          {
            "dir": "/home/mike/repos/grimoire",
            "cmd": "nix develop .#quiet",
            "work_subdir": "_posts",
            "size_down": 0
          },
          {
            "dir": "/home/mike/repos/botifyml",
            "cmd": "nix develop .#quiet",
            "work_subdir": "_posts",
            "size_down": 0
          },
          {
            "dir": "/home/mike/repos/trimnoir",
            "cmd": "nix develop .#quiet",
            "size_down": 0
          }
        ]
      },
      {
        "title": "Jekyll Servers",
        "tabbed_setup": true,
        "tabs": [
          {
            "dir": "/home/mike/repos/trimnoir",
            "cmd": "nix develop",
            "size_down": 6
          },
          {
            "dir": "/home/mike/repos/Pipulate.com",
            "cmd": "nix develop",
            "size_down": 6
          },
          {
            "dir": "/home/mike/repos/grimoire",
            "cmd": "nix develop",
            "size_down": 6
          },
          {
            "dir": "/home/mike/repos/botifyml",
            "cmd": "nix develop",
            "size_down": 6
          }
        ]
      },
      {
        "title": "Pipulate Server",
        "tabbed_setup": true,
        "tabs": [
          {
            "dir": "/home/mike/repos/pipulate",
            "cmd": "nix develop",
            "size_down": 6
          }
        ]
      }
    ]
  }
]

✅ Dry run: 7 desktops, 5 terminals on the stack. No keys were harmed.
(nix) pipulate $
```

And there! How did we do? Of course I won't really know until I do the whole
cold restart which I'll do after publishing this article but first I'll check in
with Fable 5 for debriefing.

Debrief me! And don't be brief. Make sure any learning opportunities I missed
are seized upon by you. And now that implementation is over, and yes you can
speak to the hard-nosed stuff (what does that mean anyway and what's the
origin?) you can use whatever tokens you think are necessary to use to be
fanciful and circle back to anything dangling that you skipped because of the
patch-making priority. What that behind us oh great more verbose than Haiku,
more Shakespearean than Sonnet, more Magnum than Opus oh Fable!

Regale us with your long-tails oh Fable... uh, I mean long tales. And less dry
than ""Edwin and Morcar, the earls of Mercia and Northumbria, declared for him:
and even Stigand, the patriotic archbishop of Canterbury, found it advisable to
go with Edgar Atheling to meet William and offer him the crown."

**Anthropic Fable 5 Low**: 

## Debrief: what the dry-run actually proved, what it didn't, and the crumbs left on the trail

First, the verdict: the projection matches the prediction exactly. Five editing tabs (four blogs plus the trimnoir root git-ops tab), four server tabs, one Pipulate Server terminal — 7 desktops, 5 terminals, no keys harmed. BotifyML's gene expressed for the first time in the desktop body, and Pipulate.com's tab will land in the `_posts` you planted rather than bouncing off a void. The genome writes the body now. That's the win, banked and pushed.

Now the debrief proper, and I'll be as unbrief as requested.

**The null-result probe you didn't notice.** Verification step 4 — `cat ~/repos/$p/.port` showing 4001–4004 — proved *nothing* about the new materializer, because those files already held those values before the rebuild. An idempotent writer is indistinguishable from a no-op when the state already matches. The probe returned "consistent," not "caused." To actually falsify the materializer, you'd do the destructive version: `echo 9999 > ~/repos/grimoire/.port && sudo nixos-rebuild switch && cat ~/repos/grimoire/.port`. If it comes back `4003`, the matrix truly feeds the ports; if it comes back `9999`, the activation script is decorative. This is the deepest lesson of the session and it slipped by in the green glow of matching numbers. Consistency is not causation, even in your own filesystem.

**A related loose thread worth one probe:** is `.port` *tracked* in any of those repos? If it is, a future `preview_port` change in `blogs.nix` will silently dirty a git worktree from root-context system activation — which is either charming (the genome nudging the body, visible in `git status`) or spooky (a repo modified by something that isn't you). `git ls-files .port` in each repo answers it in one second. If tracked, consider `.gitignore`-ing it, since it's now derived state.

**Small mechanical stumbles, all harmless, all instructive.** You applied six patches but narrated "patch 4" twice — the numbering drifted, which matters only because your future self reading this article will try to reconcile the count. Your verification ladder also ran in a scrambled order: the `n` alias had already done the full `switch` before you ran the `dry-build`, so step 1 validated a configuration that was already live. No harm — but the ladder's whole point is that each rung falsifies *before* the next commits you, and running switch-then-dry-build is checking the parachute after landing. And the module-scope `import sys` from Patch 3 now coexists with the `import sys` inside `__main__` — a harmless redundancy I predicted, but a candidate for a one-line cleanup commit whenever you're next in there.

**One import-time subtlety to hold in mind:** `build_desktop7_terminals()` runs when the module loads, not when desktops launch. That's why `--dry-run` works so cheaply — the projection already exists by the time the flag is checked — but it also means any warning about a missing repo prints on *every* invocation, including dry runs, and the JSON read happens even in code paths that never use it. Fine today. It becomes a footgun only if someone later imports `autognome` as a library, at which point a desktop-layout computation with disk probes fires as a side effect of `import`. The Nix-brained fix, someday, is making `DESKTOP_SETUP` lazy. Not today.

**The dangling YouTube mortise.** Nothing changed there and nothing should have — but restating the one load-bearing fact so it doesn't evaporate: YouTube only auto-archives broadcasts under roughly twelve hours, so your months-long streams stuck "processing" are over the cliff, not slow. The persistent-stream-key architecture (encoder never stops; `liveBroadcasts.transition` + `insert` + `bind` fired from the same post-receive hook that already rings your standby and breaking-news bells) is the path where each article becomes a titled, archived video with zero GUI touched. The ARexx brute-force stays in the drawer as a proof of sovereignty, like a bench vise: rarely used, never removed.

**On "hard-nosed,"** since you asked: it's American English from the late 1800s, originally applied to hunting dogs whose noses had lost sensitivity — a hard-nosed dog couldn't be steered by scent and just barreled ahead — and reinforced by "hard-nosed" bullets that don't deform on impact. Both senses converge on the modern meaning: unswayable, unsentimental, doesn't flatten when it hits resistance. Which is a decent description of `apply.py`'s exact-match interlock, honestly.

**And on your Knuth extension** — you're circling something real. Knuth's literate programming made the *program* readable; what you're doing inverts it: the human becomes literate *through* the program's modification history. Every patch is a contact surface, every diff a forced reading, every commit message a comprehension checkpoint. Vibe-coding delegates understanding; this rigs the pipeline so understanding is the toll you pay to actuate. The rappelling rig, the drop-hook, Batman extracting Lau — the metaphors all share one shape: the human is physically attached to the machine's motion and *cannot avoid the ride*. Call it what you like — literate patching, apprenticed actuation — but the myelin argument you made is the mechanism: you can't build autonomic skill on moves you never executed with your own hands, and `cat patch | app` keeps your hands on every move.

Cold-restart when ready. The seventh screen was grown, not carved, and when the Honeybot barks again, it'll be barking this.

**MikeLev.in**: Okay, one more probe:

```bash
(nix) pipulate $ echo 9999 > ~/repos/grimoire/.port && sudo nixos-rebuild switch && cat ~/repos/grimoire/.port
building Nix...
building the system configuration...
activating the configuration...
setting up /etc...
reloading user units for mike...
restarting sysinit-reactivation.target
the following new units were started: NetworkManager-dispatcher.service
Done. The new configuration is /nix/store/rn57274pd600453li31ziink5ar1h6bk-nixos-system-nixos-25.05.813814.ac62194c3917
4003
(nix) pipulate $
```

And that says `4003` and not `9999` so I think this makes up for me making the
most classic Notebook-like mistake of running things out of order. And so now I
turn this into an article and restart the machine.

I guess we have another cliff-hanger.


---

## Book Analysis

### Ai Editorial Take
What is most striking here is the concept of "reproducible attention." Traditionally, configurations target dependencies and system packages. This design targets the visual arrangement of human focus—how many terminals are open, what tasks they run, and where they land. By packaging attention-management into Nix configuration, the author ensures that even after a complete hardware failure, the precise mental landscape of their daily work can be instantly restored on a fresh machine.

### 🐦 X.com Promo Tweet
```text
Stop hand-carving your desktop terminal environments. Project them dynamically from your system configuration files using Nix. Here is how I eliminated dual-entry bookkeeping on local developer workstations: https://mikelev.in/futureproof/declarative-desktop-nix/ #NixOS #Automation #DeveloperProductivity
```

### Title Brainstorm
* **Title Option:** Declarative Desktop Orchestration with Nix
  * **Filename:** `declarative-desktop-nix.md`
  * **Rationale:** Directly explains the core technical achievement of projecting system configuration rules into spatial desktop environments.
* **Title Option:** Projecting the Genome: From System Config to Desktop Terminals
  * **Filename:** `projecting-the-genome-nix.md`
  * **Rationale:** Emphasizes the powerful biological metaphor of letting centralized configuration dictate the operational form of the terminal setup.
* **Title Option:** Replacing the Hand-Carved Workspace: Automated Spatial Terminal Layouts
  * **Filename:** `automated-desktop-terminal-layouts.md`
  * **Rationale:** Speaks to developers seeking to automate desktop window arrangement via scriptable, declarative logic.

### Content Potential And Polish
- **Core Strengths:**
  - Rigorous emphasis on checking assumptions through fast, harmless diagnostic checks.
  - Compelling narrative structure showing real-time software refinement through an interactive dialogue.
  - Practical and repeatable setup steps that help readers apply Nix concepts directly to GUI automation.
- **Suggestions For Polish:**
  - Include a brief background note on why ARexx remains an inspiring design pattern for operating system scripting.
  - Add a visual conceptual map of the pipeline to clarify how blogs.nix transforms into blogs.json and guides autognome.py.

### Next Step Prompts
- Develop a Nix-packaged system service for Path A to coordinate the transition of live broadcasts via YouTube's API whenever a git push occurs.
- Explore integrating xmonad or similar tiling window managers to enforce the spatial layout natively from configuration files rather than relying solely on xdotool.
