Proper-Noun Domain Labels, Textual Venv Isolation, and the Unexported-Shim Rule

•

🤖 Read Raw Markdown • ⏳ Coming Soon on YouTube Live Storytime
⏳ COMING SOON ON STORYTIME Honeybot Storytime & Web Bot AI Observatory
⏳ Coming Soon on YouTube Live Storytime: This article was published as a chronological aside and is queued for its upcoming full Piper TTS broadcast at youtube.com/MikeLevin/live.
🟣 Founding 1,000 Discord ↗ ▶ Watch / Listen Live ↗

Setting the Stage: Context for the Curious Book Reader

A seemingly cosmetic tweak to a live terminal dashboard often serves as the best stress test for a development environment’s underlying plumbing. In this entry, stripping the square brackets from scrolling Nginx domain labels and converting lowercase hostnames like [mikelev.in] into MixedCase Proper-Noun paths (MikeLev.in/) immediately collides with a recurring annoyance: why does every local one-liner probe against the logs.py Textual application fail on the first turn with ModuleNotFoundError: No module named 'textual'?

Following that thread peels back three distinct layers of environment isolation between the Prime workstation and the Honeybot server appliance. First is the mirage of declaring Python packages in NixOS python3.withPackages while running local probes inside a .venv populated exclusively by uv pip install -r requirements.txt. Second is an import-time side effect in db.py, where loading the TUI module attempts to open a SQLite file path that only exists on the production webhead. Third is the unexported-shim boundary inside the Nix flake shell, where chaining an unshimmed system binary (nix-instantiate) in front of Python trips over LD_LIBRARY_PATH library versions before the probe even starts.

TL;DR: This article documents two changes to a NixOS-based web server log visualizer (logs.py, built on Python’s Textual terminal UI framework) and its reproducible development environment (flake.nix and requirements.txt). First, the log stream formatter was updated to replace bracketed lowercase hostnames (such as [mikelev.in]) with unbracketed, MixedCase domain labels carrying a trailing slash (MikeLev.in/, QAmy.ai/, NPvg.org/, SunPug.io/, etc.) so multi-domain traffic is easier to read at a glance. Second, a recurring local verification failure (ModuleNotFoundError: No module named 'textual') was traced to virtual-environment isolation in the project’s Nix flake: textual was present in system-level NixOS configurations (python3.withPackages), but absent from requirements.in and requirements.txt, which control what uv pip install populates into .venv. Adding textual to requirements.in, requirements.txt, and flake.nix, alongside an in-memory SQLite fallback (:memory:) in db.py when the production database directory is absent on the development workstation, made the TUI module directly testable from .venv/bin/python.


Technical Journal Entry Begins

🔗 Verified Pipulate Commits:

MikeLev.in: Let’s get rid of the square brackets around the domain names. Being that bright white color is enough. In fact, let’s make each domain that we show a more legible Proper Noun looking version with a trailing slash, so [mikelev.in] becomes MikeLev.in/ and so on.

Gemini 4 Argon High: Context verified.

TL;DR: SonarApp.format_host_domain in remotes/honeybot/scripts/logs.py now maps each recognized host to its Proper-Noun display form with a trailing slash (MikeLev.in/, QAmy.ai/, NPvg.org/, SunPug.io/, Levinix.org/, [Levinix.com/](https://Levinix.com/), [Levinux.com/](https://Levinux.com/), [Pipulate.com/](https://Pipulate.com/), [GuerillaTech.com/](https://GuerillaTech.com/), RFC9110.org/, RFC9111.io/, [RFC9111.com/](https://RFC9111.com/)), and format_log_line appends f"{domain_label} " in bold bright_white without square brackets.

1: BEFORE (PROBES)

This read-only probe prints format_host_domain across all configured domains and renders a sample log line to show the current bracketed lowercase output before patching.

Formatting Multi-Domain Logs as Proper-Noun Paths

(nix) pipulate $

.venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

2: AFTER (NEXT CONTEXT)

Paste into context.txt using context or x:

# Context 1
context.txt

/home/mike/repos/nixos/configuration.nix
/home/mike/repos/nixos/blogs.nix

# HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
! .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

3: CHANGE (PATCHES)

Car 1: Update format_host_domain and format_log_line in remotes/honeybot/scripts/logs.py to return Proper-Noun domain labels with a trailing slash and render them without square brackets.

Target: remotes/honeybot/scripts/logs.py
[[[SEARCH]]]
    def format_host_domain(self, raw_host):
        """Normalize Nginx $host into the registered domain name without leaking raw IP headers."""
        h = (raw_host or "").strip().lower().split(":", 1)[0].rstrip(".")
        if h.startswith("www."):
            h = h[4:]
        if not h or h == "-" or h == "localhost" or h.endswith(".local"):
            return "mikelev.in"
        if re.match(r"^\d+(\.\d+)+$", h) or ":" in h:
            return "mikelev.in"
        for known in (
            "mikelev.in", "qamy.ai", "npvg.org", "sunpug.io",
            "levinix.org", "levinix.com", "levinux.com",
            "pipulate.com", "guerillatech.com", "rfc9110.org", "rfc9111.io", "rfc9111.com",
        ):
            if h == known or h.endswith("." + known):
                return known
        if "qamy" in h:
            return "qamy.ai"
        if "npvg" in h:
            return "npvg.org"
        if "sunpug" in h:
            return "sunpug.io"
        if "levinix" in h:
            return "levinix.org"
        if "levinux" in h:
            return "levinux.com"
        if re.match(r"^[a-z0-9-]+(\.[a-z0-9-]+)+$", h):
            return h
        return "mikelev.in"
[[[DIVIDER]]]
    def format_host_domain(self, raw_host):
        """Normalize Nginx $host into a Proper-Noun display domain with trailing slash without leaking raw IP headers."""
        h = (raw_host or "").strip().lower().split(":", 1)[0].rstrip(".")
        if h.startswith("www."):
            h = h[4:]
        if not h or h == "-" or h == "localhost" or h.endswith(".local"):
            return "MikeLev.in/"
        if re.match(r"^\d+(\.\d+)+$", h) or ":" in h:
            return "MikeLev.in/"
        host_display_map = {
            "mikelev.in": "MikeLev.in/",
            "qamy.ai": "QAmy.ai/",
            "npvg.org": "NPvg.org/",
            "sunpug.io": "SunPug.io/",
            "levinix.org": "Levinix.org/",
            "levinix.com": "Levinix.com/",
            "levinux.com": "Levinux.com/",
            "pipulate.com": "Pipulate.com/",
            "guerillatech.com": "GuerillaTech.com/",
            "rfc9110.org": "RFC9110.org/",
            "rfc9111.io": "RFC9111.io/",
            "rfc9111.com": "RFC9111.com/",
        }
        for known, label in host_display_map.items():
            if h == known or h.endswith("." + known):
                return label
        if "qamy" in h:
            return "QAmy.ai/"
        if "npvg" in h:
            return "NPvg.org/"
        if "sunpug" in h:
            return "SunPug.io/"
        if "levinix" in h:
            return "Levinix.org/"
        if "levinux" in h:
            return "Levinux.com/"
        if re.match(r"^[a-z0-9-]+(\.[a-z0-9-]+)+$", h):
            return f"{h[:1].upper()}{h[1:]}/"
        return "MikeLev.in/"
[[[REPLACE]]]

Target: remotes/honeybot/scripts/logs.py
[[[SEARCH]]]
        domain_label = self.format_host_domain(data.get('host'))
        text.append(f"[{domain_label}] ", style="bold bright_white")
[[[DIVIDER]]]
        domain_label = self.format_host_domain(data.get('host'))
        text.append(f"{domain_label} ", style="bold bright_white")
[[[REPLACE]]]

Car 2 (Ignition / Sync to Honeybot): Sync remotes/honeybot/scripts/logs.py to Honeybot (SONAR_HOT_RELOAD=1 in stream.py automatically hot-reloads logs.py on the next billboard card boundary with no stream restart needed).

(nix) pipulate $

./nixops.sh

4: PROMPT (CABOOSE COPY)

Verify the AFTER receipt confirming Proper-Noun domain labels with trailing slashes (e.g. MikeLev.in/) and no square brackets in logs.py.

5: DELIVERABLE (EXTERNAL DELIVERABLES)

  • Every log line in logs.py displays the host in bold bright_white without square brackets and with Proper-Noun casing plus a trailing slash (MikeLev.in/, QAmy.ai/, NPvg.org/, SunPug.io/, Levinix.org/, [Levinix.com/](https://Levinix.com/), [Levinux.com/](https://Levinux.com/), [Pipulate.com/](https://Pipulate.com/), [GuerillaTech.com/](https://GuerillaTech.com/), RFC9110.org/, RFC9111.io/, [RFC9111.com/](https://RFC9111.com/)).

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):

Let’s fix this once and for all. There’s not a time that you don’t start out trying to use textual this way without the same exact problem happening. I don’t get it. What don’t you get? Are you assuming it’s there what in the Nix environment native Python or in the Python virtualenv that takes over for it from the Pipulate Nix flake? Why do I always face this same failed probe on the first turn of discussions like this?

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 -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/mike/pipulate/remotes/honeybot/scripts/logs.py", line 16, in <module>
    from textual.app import App, ComposeResult
ModuleNotFoundError: No module named 'textual'
(nix) pipulate $ 

2: AFTER (NEXT CONTEXT):

This is also going to produce that same constantly recurring textual error. I tried fixing it with on of your predecessors before. They couldn’t do it and now this is the result, the constantly popping back up error. What is it about trying to use textual here and it never being done correctly or being available? This is a Forever Machine. I should be able to fix that forever. I added the flake to new context. I’m almost never not in the Pipulate Nix flake environment.

# # Context 1
# context.txt
# 
# /home/mike/repos/nixos/configuration.nix
# /home/mike/repos/nixos/blogs.nix
# 
# # HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
context.txt

/home/mike/repos/nixos/configuration.nix
/home/mike/repos/nixos/blogs.nix

# HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
! .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

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 'remotes/honeybot/scripts/logs.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'remotes/honeybot/scripts/logs.py'.
(nix) pipulate $ d
diff --git a/remotes/honeybot/scripts/logs.py b/remotes/honeybot/scripts/logs.py
index 7dbfeb3..a061c66 100644
--- a/remotes/honeybot/scripts/logs.py
+++ b/remotes/honeybot/scripts/logs.py
@@ -891,34 +891,44 @@ class SonarApp(App):
             return "???", request_str
 
     def format_host_domain(self, raw_host):
-        """Normalize Nginx $host into the registered domain name without leaking raw IP headers."""
+        """Normalize Nginx $host into a Proper-Noun display domain with trailing slash without leaking raw IP headers."""
         h = (raw_host or "").strip().lower().split(":", 1)[0].rstrip(".")
         if h.startswith("www."):
             h = h[4:]
         if not h or h == "-" or h == "localhost" or h.endswith(".local"):
-            return "mikelev.in"
+            return "MikeLev.in/"
         if re.match(r"^\d+(\.\d+)+$", h) or ":" in h:
-            return "mikelev.in"
-        for known in (
-            "mikelev.in", "qamy.ai", "npvg.org", "sunpug.io",
-            "levinix.org", "levinix.com", "levinux.com",
-            "pipulate.com", "guerillatech.com", "rfc9110.org", "rfc9111.io", "rfc9111.com",
-        ):
+            return "MikeLev.in/"
+        host_display_map = {
+            "mikelev.in": "MikeLev.in/",
+            "qamy.ai": "QAmy.ai/",
+            "npvg.org": "NPvg.org/",
+            "sunpug.io": "SunPug.io/",
+            "levinix.org": "Levinix.org/",
+            "levinix.com": "Levinix.com/",
+            "levinux.com": "Levinux.com/",
+            "pipulate.com": "Pipulate.com/",
+            "guerillatech.com": "GuerillaTech.com/",
+            "rfc9110.org": "RFC9110.org/",
+            "rfc9111.io": "RFC9111.io/",
+            "rfc9111.com": "RFC9111.com/",
+        }
+        for known, label in host_display_map.items():
             if h == known or h.endswith("." + known):
-                return known
+                return label
         if "qamy" in h:
-            return "qamy.ai"
+            return "QAmy.ai/"
         if "npvg" in h:
-            return "npvg.org"
+            return "NPvg.org/"
         if "sunpug" in h:
-            return "sunpug.io"
+            return "SunPug.io/"
         if "levinix" in h:
-            return "levinix.org"
+            return "Levinix.org/"
         if "levinux" in h:
-            return "levinux.com"
+            return "Levinux.com/"
         if re.match(r"^[a-z0-9-]+(\.[a-z0-9-]+)+$", h):
-            return h
-        return "mikelev.in"
+            return f"{h[:1].upper()}{h[1:]}/"
+        return "MikeLev.in/"
 
     @work(thread=True)
     def stream_logs(self) -> None:
@@ -1064,7 +1074,7 @@ class SonarApp(App):
         except: text.append("[TIME] ", style="dim")
 
         domain_label = self.format_host_domain(data.get('host'))
-        text.append(f"[{domain_label}] ", style="bold bright_white")
+        text.append(f"{domain_label} ", style="bold bright_white")
 
         text.append(ip_display)
         text.append("  ") # Just space
(nix) pipulate $ m
📝 Committing: refactor: Normalize Nginx $host domain display with trailing slash
[main c43c8b5] refactor: Normalize Nginx $host domain display with trailing slash
 1 file changed, 27 insertions(+), 17 deletions(-)
(nix) pipulate $ git push
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 48 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 934 bytes | 934.00 KiB/s, done.
Total 6 (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.git
   55f38b5..c43c8b5  main -> main
(nix) pipulate $ 

Ignition: often “exit”, “nix develop”. Instructions may vary.

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 $ nixops
🚀 Syncing Hooks & Scripts...
🚀 Syncing 8 Domain Doors & Installer...
🚀 Syncing installer to home-hosted doors (npvg.org qamy.ai levinix.org sunpug.io guerillatech.com rfc9110.org rfc9111.io)...
🧠 Syncing 2nd Brain archive (brain.zip)...
🚀 Syncing levinux.com deck & NixOS Config...
✅ Sync Complete.
   To apply NixOS config: ssh -t mike@[REDACTED_IP] 'sudo cp ~/nixos-config-staged/* /etc/nixos/ && sudo nixos-rebuild switch'
(nix) pipulate $ ssh -t mike@[REDACTED_IP] 'sudo cp ~/nixos-config-staged/* /etc/nixos/ && sudo nixos-rebuild switch'
[sudo] password for mike: 
Sorry, try again.
[sudo] password for mike: 
building the system configuration...
activating the configuration...
setting up /etc...
r
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/s3slf7jyyp776115z977798s1fa0w74m-nixos-system-honeybot-26.05pre913595.c6245e83d836
Connection to [REDACTED_IP] closed.
(nix) pipulate $ 

4: PROMPT:

Verify the AFTER receipt confirming Proper-Noun domain labels with trailing slashes (e.g. MikeLev.in/) and no square brackets in logs.py.

5: DELIVERABLE:

More Proper Noun-ish and thus readable and thus marketable and viral domain names on the Honeybot scrolling web logs.

The Recurring Textual Import Trap on Turn One

Gemini 4 Argon High: Context verified.

TL;DR: Every model keeps falling into this trap because textual is listed in both NixOS configuration.nix files (python3.withPackages), tricking the model into assuming .venv/bin/python has it—when in reality flake.nix creates .venv with venv --clear (isolated from Nix’s site-packages), prepends .venv/bin to PATH, and only installs packages via uv pip install. Worse, right behind line 16 (from textual...) in logs.py sits line 22 (from db import db), which instantiates HoneyDB() against /home/mike/www/mikelev.in/honeybot.db—a directory that exists on Honeybot, not on Prime. We fix all three permanently in flake.nix, db.py, and logs.py.

Why This Kept Happening

  1. The Context Mirage: Both /home/mike/repos/nixos/configuration.nix and remotes/honeybot/nixos/configuration.nix declare textual inside python3.withPackages. Every new model sees those declarations and assumes textual is importable everywhere.
  2. **The .venv Isolation Boundary in flake.nix**:
    • In /home/mike/repos/nixos/configuration.nix, pythonWithRich (containing textual) is a local let binding scoped only to the b1/b2/drives backup scripts—not global.
    • In flake.nix, pythonSetupLogic creates .venv without --system-site-packages, puts .venv/bin first on PATH, and runs unset PYTHONPATH. Because .venv only sees packages installed by uv pip, any package added only to Nix withPackages remains invisible to .venv/bin/python.
  3. The Second Trap at Line 22 of logs.py: Once textual imports on line 16, line 22 runs from db import db, KNOWN_BOTS. At module import time, db.py runs db = HoneyDB(), which calls sqlite3.connect("/home/mike/www/mikelev.in/honeybot.db"). Because /home/mike/www/mikelev.in only exists on Honeybot, importing logs.py on Prime immediately throws sqlite3.OperationalError: unable to open database file unless HoneyDB falls back to ":memory:" when that parent directory is absent.

The Permanent Fix

  1. flake.nix: Add textual to python312.withPackages and add a fast directory check in pythonSetupLogic (which runs in default, dev, and quiet shells) that runs uv pip install textual --quiet into .venv whenever textual is missing from .venv/lib/python*/site-packages.
  2. remotes/honeybot/scripts/db.py: In HoneyDB.__init__, fall back to ":memory:" when /home/mike/www/mikelev.in does not exist on the host machine.
  3. remotes/honeybot/scripts/logs.py: Guard the textual imports with a lightweight stub fallback so pure-Python probes (format_host_domain, format_log_line) work even before .venv is refreshed.

1: BEFORE (PROBES)

This read-only probe parses flake.nix, checks whether textual is importable in .venv/bin/python, and runs the SonarApp domain-formatting and log-line verification.

(nix) pipulate $

nix-instantiate --parse flake.nix >/dev/null && .venv/bin/python -c "import importlib.util, sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print('textual_in_venv:', importlib.util.find_spec('textual') is not None); print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

2: AFTER (NEXT CONTEXT)

Paste into context.txt using context or x:

# Context 2
flake.nix
context.txt

/home/mike/repos/nixos/configuration.nix
/home/mike/repos/nixos/blogs.nix

# HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
! nix-instantiate --parse flake.nix >/dev/null && .venv/bin/python -c "import importlib.util, sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print('textual_in_venv:', importlib.util.find_spec('textual') is not None); print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

3: CHANGE (PATCHES)

Car 1: Ensure textual is installed in .venv in flake.nix, fall back to :memory: in remotes/honeybot/scripts/db.py when /home/mike/www/mikelev.in is not present on the local machine, and add an ImportError stub fallback in remotes/honeybot/scripts/logs.py.

Target: flake.nix
[[[SEARCH]]]
          ruff                         # Fast Python linter (native Nix binary)
          (python312.withPackages (ps: with ps; [
            nbstripout
          ]))
          nbstripout
[[[DIVIDER]]]
          ruff                         # Fast Python linter (native Nix binary)
          (python312.withPackages (ps: with ps; [
            nbstripout
            textual
          ]))
          nbstripout
[[[REPLACE]]]

Target: flake.nix
[[[SEARCH]]]
        pythonSetupLogic = ''
          # Set up the Python virtual environment with explicit Python 3.12 isolation
          test -d .venv || ${pkgs.python312}/bin/python -m venv .venv --clear
          export VIRTUAL_ENV="$(pwd)/.venv"
          export PATH="$VIRTUAL_ENV/bin:$PATH"
          # Purge wheel-installed ruff binary so native pkgs.ruff on PATH is used on NixOS
          rm -f .venv/bin/ruff 2>/dev/null || true
[[[DIVIDER]]]
        pythonSetupLogic = ''
          # Set up the Python virtual environment with explicit Python 3.12 isolation
          test -d .venv || ${pkgs.python312}/bin/python -m venv .venv --clear
          export VIRTUAL_ENV="$(pwd)/.venv"
          export PATH="$VIRTUAL_ENV/bin:$PATH"
          if [ -z "$(find .venv/lib/python*/site-packages -maxdepth 1 -name 'textual' 2>/dev/null)" ]; then
            uv pip install textual --quiet 2>/dev/null || true
          fi
          # Purge wheel-installed ruff binary so native pkgs.ruff on PATH is used on NixOS
          rm -f .venv/bin/ruff 2>/dev/null || true
[[[REPLACE]]]

Target: remotes/honeybot/scripts/db.py
[[[SEARCH]]]
class HoneyDB:
    def __init__(self, db_path=DB_PATH, read_only=False):
        import os
        self.db_path = db_path
        self.conn = None
        if not read_only and os.environ.get("HONEYBOT_DB_READONLY") != "1":
            self.init_db()
[[[DIVIDER]]]
class HoneyDB:
    def __init__(self, db_path=DB_PATH, read_only=False):
        import os
        if db_path != ":memory:" and not Path(db_path).parent.exists():
            db_path = ":memory:"
        self.db_path = db_path
        self.conn = None
        if not read_only and os.environ.get("HONEYBOT_DB_READONLY") != "1":
            self.init_db()
[[[REPLACE]]]

Target: remotes/honeybot/scripts/logs.py
[[[SEARCH]]]
from textual.app import App, ComposeResult
from textual.containers import Container, Vertical, Horizontal
from textual.widgets import Header, Footer, Static, Log, RichLog, Label, Markdown, DataTable
from textual import work
from rich.text import Text
[[[DIVIDER]]]
try:
    from textual.app import App, ComposeResult
    from textual.containers import Container, Vertical, Horizontal
    from textual.widgets import Header, Footer, Static, Log, RichLog, Label, Markdown, DataTable
    from textual import work
except ImportError:
    class App:
        pass
    ComposeResult = Container = Vertical = Horizontal = Header = Footer = Static = Log = RichLog = Label = Markdown = DataTable = object
    def work(*args, **kwargs):
        def _dec(fn):
            return fn
        return _dec if (not args or not callable(args[0])) else args[0]
from rich.text import Text
[[[REPLACE]]]

Car 2 (Ignition): Install textual into the active .venv now (future nix develop entries will run the pythonSetupLogic guard automatically) and sync logs.py and db.py to Honeybot.

(nix) pipulate $

uv pip install textual --quiet && ./nixops.sh

4: PROMPT (CABOOSE COPY)

Verify the AFTER receipt confirming textual is installed in .venv and SonarApp.format_host_domain / format_log_line output Proper-Noun domain labels with trailing slashes and no brackets.

5: DELIVERABLE (EXTERNAL DELIVERABLES)

  • textual is automatically installed in .venv via flake.nix (pythonSetupLogic), db.py cleanly falls back to :memory: on Prime when /home/mike/www/mikelev.in/ is absent, and the logs.py probe succeeds and verifies the bracketless Proper-Noun domain labels (MikeLev.in/, QAmy.ai/, NPvg.org/, SunPug.io/, etc.).

MikeLev.in: I’m not going to pip install anything, or uv for that matter. It’s got to be done properly per Nix or per unfreezing and part of nix develop. I’ll give you both my configuration.nix of the top-most OS and the flake.nix of the Pipulate environment and its requirements.in and requirements.txt and we can try again.

Gemini 4 Argon High: Context verified.

TL;DR: rich and pyfiglet were in requirements.in and requirements.txt, while textual—the third TUI library used on Honeybot—was only in the NixOS configs (pythonWithRich in /home/mike/repos/nixos/configuration.nix and environment.systemPackages on Honeybot), never in requirements.in, requirements.txt, or flake.nix. Furthermore, pythonWithRich in /home/mike/repos/nixos/configuration.nix was only interpolated into the ark* backup script shebangs and never placed in environment.systemPackages. We add textual declaratively to requirements.in, requirements.txt, flake.nix, and /home/mike/repos/nixos/configuration.nix, and let remotes/honeybot/scripts/db.py fall back to :memory: when /home/mike/www/mikelev.in is absent on Prime so logs.py imports cleanly on nix develop.

Why It Kept Failing

  1. **The Missing Third Sister in requirements.in / requirements.txt**: When nix develop starts, run-script in flake.nix populates .venv by running uv pip install -r requirements.txt --quiet. Both rich and pyfiglet were in requirements.in and requirements.txt (so card.py and forest.py worked in .venv), but textual was omitted.
  2. **Unexposed pythonWithRich in /home/mike/repos/nixos/configuration.nix**: Line 35 defines pythonWithRich = pkgs.python3.withPackages (ps: [ ps.rich ps.pyfiglet ps.textual ]);, but only passes it into the ark* backup script derivations—never into environment.systemPackages.
  3. Import-Time SQLite Path in remotes/honeybot/scripts/db.py: Once textual imports on line 16 of logs.py, line 22 runs from db import db, KNOWN_BOTS, which calls HoneyDB() against /home/mike/www/mikelev.in/honeybot.db. Falling back to ":memory:" when /home/mike/www/mikelev.in is absent on Prime allows logs.py to import cleanly during local probes.

1: BEFORE (PROBES)

This read-only probe checks nix-instantiate --parse on both Nix files and tests importing SonarApp from remotes/honeybot/scripts/logs.py to verify the Proper-Noun domain labels and formatted log line.

(nix) pipulate $

nix-instantiate --parse flake.nix >/dev/null && nix-instantiate --parse /home/mike/repos/nixos/configuration.nix >/dev/null && .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

2: AFTER (NEXT CONTEXT)

Paste into context.txt using context or x:

# Context 2
flake.nix
context.txt

/home/mike/repos/nixos/configuration.nix
/home/mike/repos/nixos/blogs.nix
requirements.in
requirements.txt

# HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
! nix-instantiate --parse flake.nix >/dev/null && nix-instantiate --parse /home/mike/repos/nixos/configuration.nix >/dev/null && .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

3: CHANGE (PATCHES)

Car 1: Add textual to requirements.in, requirements.txt, flake.nix, and /home/mike/repos/nixos/configuration.nix (pythonWithRich in environment.systemPackages), and add the :memory: fallback to remotes/honeybot/scripts/db.py when /home/mike/www/mikelev.in is not present locally.

Target: /home/mike/repos/nixos/configuration.nix
[[[SEARCH]]]
    # The Agent
    nodejs
    gemini-cli-unstable
    ai-commit

    # Backup Scripts
[[[DIVIDER]]]
    # The Agent
    nodejs
    gemini-cli-unstable
    ai-commit
    pythonWithRich

    # Backup Scripts
[[[REPLACE]]]

Target: flake.nix
[[[SEARCH]]]
          ruff                         # Fast Python linter (native Nix binary)
          (python312.withPackages (ps: with ps; [
            nbstripout
          ]))
          nbstripout
[[[DIVIDER]]]
          ruff                         # Fast Python linter (native Nix binary)
          (python312.withPackages (ps: with ps; [
            nbstripout
            rich
            pyfiglet
            textual
          ]))
          nbstripout
[[[REPLACE]]]

Target: requirements.in
[[[SEARCH]]]
python-dotenv
loguru
rich
watchdog
pyfiglet
pydot
sqlitedict
[[[DIVIDER]]]
python-dotenv
loguru
rich
textual
watchdog
pyfiglet
pydot
sqlitedict
[[[REPLACE]]]

Target: requirements.txt
[[[SEARCH]]]
terminado==0.18.1
    # via
    #   jupyter-server
    #   jupyter-server-terminals
threadpoolctl==3.6.0
    # via scikit-learn
[[[DIVIDER]]]
terminado==0.18.1
    # via
    #   jupyter-server
    #   jupyter-server-terminals
textual
    # via -r requirements.in
threadpoolctl==3.6.0
    # via scikit-learn
[[[REPLACE]]]

Target: remotes/honeybot/scripts/db.py
[[[SEARCH]]]
class HoneyDB:
    def __init__(self, db_path=DB_PATH, read_only=False):
        import os
        self.db_path = db_path
        self.conn = None
        if not read_only and os.environ.get("HONEYBOT_DB_READONLY") != "1":
            self.init_db()
[[[DIVIDER]]]
class HoneyDB:
    def __init__(self, db_path=DB_PATH, read_only=False):
        import os
        if db_path != ":memory:" and not Path(db_path).parent.exists():
            db_path = ":memory:"
        self.db_path = db_path
        self.conn = None
        if not read_only and os.environ.get("HONEYBOT_DB_READONLY") != "1":
            self.init_db()
[[[REPLACE]]]

Ignition: Re-enter nix develop so run-script syncs requirements.txt into .venv automatically (and commit/rebuild /home/mike/repos/nixos via nr when convenient).

(nix) pipulate $

exit

(sys) pipulate $

nix develop

4: PROMPT (CABOOSE COPY)

Verify the AFTER receipt confirming textual is installed via nix develop and SonarApp.format_host_domain / format_log_line output Proper-Noun domain labels with trailing slashes and no brackets.

5: DELIVERABLE (EXTERNAL DELIVERABLES)

  • textual is declared in requirements.in, requirements.txt, flake.nix, and /home/mike/repos/nixos/configuration.nix (environment.systemPackages), nix develop installs it into .venv automatically via run-script, and the logs.py probe succeeds on Prime.

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 $ nix-instantiate --parse flake.nix >/dev/null && nix-instantiate --parse /home/mike/repos/nixos/configuration.nix >/dev/null && .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"
nix-instantiate: /nix/store/2fxp204b9jh1s3lpggdlnws44vvzw1w9-openssl-3.4.3/lib/libcrypto.so.3: version `OPENSSL_3.6.0' not found (required by /nix/store/1p2gjq4x396yj65rig8fyzhrdza0psd6-openssl-3.6.3/lib/libssl.so.3)
nix-instantiate: /nix/store/i3ibgfskl99qd8rslafbpaa1dmxdzh1z-glibc-2.40-66/lib/libc.so.6: version `GLIBC_ABI_DT_X86_64_PLT' not found (required by /nix/store/ias8xacs1h3jy7xgwi2awvim61k2ji6c-glibc-2.42-67/lib/libresolv.so.2)
(nix) pipulate $ 

2: AFTER (NEXT CONTEXT):

# # Context 1
# context.txt
# 
# /home/mike/repos/nixos/configuration.nix
# /home/mike/repos/nixos/blogs.nix
# 
# # HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
# context.txt
# 
# /home/mike/repos/nixos/configuration.nix
# /home/mike/repos/nixos/blogs.nix
# requirements.in
# requirements.txt
# 
# # HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
# ! .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

# Context 3
flake.nix
context.txt

/home/mike/repos/nixos/configuration.nix
/home/mike/repos/nixos/blogs.nix
requirements.in
requirements.txt

# HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
! nix-instantiate --parse flake.nix >/dev/null && nix-instantiate --parse /home/mike/repos/nixos/configuration.nix >/dev/null && .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

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 '/home/mike/repos/nixos/configuration.nix'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'flake.nix'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'requirements.in'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'requirements.txt'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'remotes/honeybot/scripts/db.py'.
(nix) pipulate $ d
diff --git a/flake.nix b/flake.nix
index 9c4ea2a..6a8c243 100644
--- a/flake.nix
+++ b/flake.nix
@@ -992,6 +992,9 @@
           ruff                         # Fast Python linter (native Nix binary)
           (python312.withPackages (ps: with ps; [
             nbstripout
+            rich
+            pyfiglet
+            textual
           ]))
           nbstripout
           figlet                       # For creating ASCII art welcome messages
diff --git a/remotes/honeybot/scripts/db.py b/remotes/honeybot/scripts/db.py
index 3c12d89..8a2a837 100644
--- a/remotes/honeybot/scripts/db.py
+++ b/remotes/honeybot/scripts/db.py
@@ -18,6 +18,8 @@ DB_PATH = Path("/home/mike/www/mikelev.in/honeybot.db")
 class HoneyDB:
     def __init__(self, db_path=DB_PATH, read_only=False):
         import os
+        if db_path != ":memory:" and not Path(db_path).parent.exists():
+            db_path = ":memory:"
         self.db_path = db_path
         self.conn = None
         if not read_only and os.environ.get("HONEYBOT_DB_READONLY") != "1":
diff --git a/requirements.in b/requirements.in
index 319752d..5cf0424 100644
--- a/requirements.in
+++ b/requirements.in
@@ -113,6 +113,7 @@ pyyaml  # DIRECT: scripts/walk.py reads every trail with it (2026-10-04).
 python-dotenv
 loguru
 rich
+textual
 watchdog
 pyfiglet
 pydot
diff --git a/requirements.txt b/requirements.txt
index 5ded027..fd37481 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -856,6 +856,8 @@ terminado==0.18.1
     # via
     #   jupyter-server
     #   jupyter-server-terminals
+textual
+    # via -r requirements.in
 threadpoolctl==3.6.0
     # via scikit-learn
 tiktoken==0.13.0
(nix) pipulate $ m
📝 Committing: chore: Add textual, rich, and pyfiglet to flake.nix
[main aeaba41] chore: Add textual, rich, and pyfiglet to flake.nix
 4 files changed, 8 insertions(+)
(nix) pipulate $ git push
Enumerating objects: 17, done.
Counting objects: 100% (17/17), done.
Delta compression using up to 48 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 868 bytes | 868.00 KiB/s, done.
Total 9 (delta 7), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (7/7), completed with 7 local objects.
To github.com:pipulate/pipulate.git
   c43c8b5..aeaba41  main -> main
(nix) pipulate $ 

Ignition: often “exit”, “nix develop”. Instructions may vary.

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 $ nix develop
Already up to date.
(Nix) 2.28.5 · Python 3.12.13 · v2.85 · ~/pipulate
🛑 Stopped a Pipulate server that was already running (ports 5001/8888 are shared -- one workshop at a time).

type one:
menu     print this list again (useful once it scrolls away).
walk     take guided tour of context compiler (recommended).
brain    download or update the 2nd Brain article corpus.
talk     turn text-to-speech narration on or off.
connect  put mcp, jira, email, docs, etc. into your contexts.
context  edit the list of files an AI will read (after the walk).
prompt   save your clipboard as the question for the AI.
compile  build the list and the question into one payload for a chatbot.
about    how to Q/A AI output ("what Claude said").
all      expanded menu.
(nix) pipulate $ 

4: PROMPT:

Verify the AFTER receipt confirming textual is installed via nix develop and SonarApp.format_host_domain / format_log_line output Proper-Noun domain labels with trailing slashes and no brackets.

5: DELIVERABLE:

Less errors when the Model tries to use Textual in probes.

The Unexported-Shim Rule and GLIBC Library Collisions

How’d we do?

Gemini 4 Argon High: Context verified.

TL;DR: The textual fix and nix develop ignition succeeded (textual is now listed in the compiled Tool Roster’s .venv environment and aeaba41 is committed). The probe in the compile receipt failed before reaching .venv/bin/python only because nix-instantiate was chained in front of it without LD_LIBRARY_PATH="". Inside the (nix) shell, LD_LIBRARY_PATH points to nixos-unstable (glibc-2.42 / openssl-3.6.3), which crashes the unshimmed system /run/current-system/sw/bin/nix-instantiate binary (glibc-2.40 / openssl-3.4.3)—the exact UNEXPORTED-SHIM RULE documented in flake.nix line 530 and apply.py line 551—short-circuiting the && chain before Python could run.

What Happened

  1. The Fix Landed Cleanly: requirements.in, requirements.txt, flake.nix, remotes/honeybot/scripts/db.py, and /home/mike/repos/nixos/configuration.nix all patched cleanly, and re-entering nix develop installed textual into .venv (confirmed by textual appearing in the live ## Environment roster at the top of this payload).
  2. Why the Receipt Short-Circuited: miscSetupLogic in flake.nix wraps nix in a shell function (LD_LIBRARY_PATH="" command nix "$@"), but nix-instantiate is not wrapped and ! child shells do not inherit shell functions. Running bare nix-instantiate inside (nix) hit the LD_LIBRARY_PATH glibc/OpenSSL mismatch and exited 1 before && .venv/bin/python ... ever executed.
  3. Removing the Unshimmed Prefix: Dropping the redundant nix-instantiate prefix (since apply.py already runs LD_LIBRARY_PATH="" nix-instantiate --parse automatically on every .nix edit) lets .venv/bin/python run directly and produce the receipt.

1: BEFORE (PROBES)

Run the Python probe directly against .venv/bin/python without the unshimmed nix-instantiate prefix:

(nix) pipulate $

.venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

2: AFTER (NEXT CONTEXT)

Paste into context.txt using context or x:

# Context 4
flake.nix
context.txt

/home/mike/repos/nixos/configuration.nix
/home/mike/repos/nixos/blogs.nix
requirements.in
requirements.txt

# HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
! .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

3: CHANGE (PATCHES)

No repo patches required — no ignition required.

4: PROMPT (CABOOSE COPY)

Confirm the live receipt from .venv/bin/python verifying SonarApp.format_host_domain and format_log_line output Proper-Noun domain labels with trailing slashes and no brackets.

5: DELIVERABLE (EXTERNAL DELIVERABLES)

  • .venv/bin/python imports logs.py (textual + db.py in-memory fallback) with zero errors and prints the 13 Proper-Noun domain labels (['MikeLev.in/', 'QAmy.ai/', 'NPvg.org/', 'SunPug.io/', 'Levinix.org/', '[Levinix.com/](https://Levinix.com/)', '[Levinux.com/](https://Levinux.com/)', '[Pipulate.com/](https://Pipulate.com/)', '[GuerillaTech.com/](https://GuerillaTech.com/)', 'RFC9110.org/', 'RFC9111.io/', '[RFC9111.com/](https://RFC9111.com/)', 'MikeLev.in/']) and the bracketless formatted log line.

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 -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"
['MikeLev.in/', 'QAmy.ai/', 'NPvg.org/', 'SunPug.io/', 'Levinix.org/', 'Levinix.com/', 'Levinux.com/', 'Pipulate.com/', 'GuerillaTech.com/', 'RFC9110.org/', 'RFC9111.io/', 'RFC9111.com/', 'MikeLev.in/']
'[13:33:00] MikeLev.in/ 66.249.*.* [c066]  [RFC9110:MD] GET  / [200]   🟠 Claude-User/1.0'
(nix) pipulate $ 

2: AFTER (NEXT CONTEXT):

# # Context 1
# context.txt
# 
# /home/mike/repos/nixos/configuration.nix
# /home/mike/repos/nixos/blogs.nix
# 
# # HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
# context.txt
# 
# /home/mike/repos/nixos/configuration.nix
# /home/mike/repos/nixos/blogs.nix
# requirements.in
# requirements.txt
# 
# # HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
# ! .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

# # Context 3
# flake.nix
# context.txt
# 
# /home/mike/repos/nixos/configuration.nix
# /home/mike/repos/nixos/blogs.nix
# requirements.in
# requirements.txt
# 
# # HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
# ! nix-instantiate --parse flake.nix >/dev/null && nix-instantiate --parse /home/mike/repos/nixos/configuration.nix >/dev/null && .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

# Context 4
flake.nix
context.txt

/home/mike/repos/nixos/configuration.nix
/home/mike/repos/nixos/blogs.nix
requirements.in
requirements.txt

# HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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
! .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))"

3: CHANGE (PATCHES):

None means done.

4: PROMPT:

Confirm the live receipt from .venv/bin/python verifying SonarApp.format_host_domain and format_log_line output Proper-Noun domain labels with trailing slashes and no brackets.

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:

  1. 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.
  2. 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.
  3. 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.
  4. DANGLING: what carries forward unbanked? One line each, no essays.
  5. SEED: the context.txt lines (and TODO_SLUGS if narrative context is needed) for the next ride’s first compile.
  6. 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.
  7. 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

  • Stated Goal: Remove the square brackets around domain names in the Honeybot log visualizer (remotes/honeybot/scripts/logs.py), format each recognized domain as a MixedCase Proper Noun with a trailing slash (e.g., MikeLev.in/), and permanently resolve the recurring ModuleNotFoundError: No module named 'textual' when probing logs.py inside the Pipulate Nix development shell.
  • Verification by Live Receipt: Confirmed met by this compile’s live command receipt (! .venv/bin/python -c ..., 123 tokens 295 bytes, runtime 0.5886s), which imported SonarApp from remotes/honeybot/scripts/logs.py inside .venv/bin/python with zero errors and returned:
  • ['MikeLev.in/', 'QAmy.ai/', 'NPvg.org/', 'SunPug.io/', 'Levinix.org/', '[Levinix.com/](https://Levinix.com/)', '[Levinux.com/](https://Levinux.com/)', '[Pipulate.com/](https://Pipulate.com/)', '[GuerillaTech.com/](https://GuerillaTech.com/)', 'RFC9110.org/', 'RFC9111.io/', '[RFC9111.com/](https://RFC9111.com/)', 'MikeLev.in/']
  • '[13:33:00] MikeLev.in/ 66.249.*.* [c066] [RFC9110:MD] GET / [200] 🟠 Claude-User/1.0'

  • Ignitions:
  • Local Pipulate flake ignition (exit followed by nix develop) fired and installed textual into .venv (witnessed in the Tool Roster’s ## Environment list and the passing .venv/bin/python receipt).
  • Honeybot script sync (nixops) fired after commit c43c8b5 (logs.py).
  • Two secondary ignitions were not re-run after commit aeaba41 and remain optional follow-ups: running ./nixops.sh once more so Honeybot receives the :memory: fallback in remotes/honeybot/scripts/db.py (non-blocking on Honeybot, where /home/mike/www/mikelev.in exists), and committing/rebuilding /home/mike/repos/nixos/configuration.nix (nr) so pythonWithRich is added to Prime’s global (sys) environment.systemPackages.

2. BANK

All code edits to files in this context (remotes/honeybot/scripts/logs.py in c43c8b5; flake.nix, requirements.in, requirements.txt, and remotes/honeybot/scripts/db.py in aeaba41; plus /home/mike/repos/nixos/configuration.nix on disk) are already applied in the raw source above. No additional repo patches are required.

3. DANGLING

  • Commit and rebuild /home/mike/repos/nixos/configuration.nix (n, d, m, nr) when convenient so pythonWithRich is exposed in Prime’s (sys) shell environment.
  • Run ./nixops.sh once from (nix) pipulate $ when convenient so Honeybot’s copy of remotes/honeybot/scripts/db.py stays byte-identical with Prime’s aeaba41 commit.
  • Note for future Nix syntax probes in context.txt: always prefix nix-instantiate with LD_LIBRARY_PATH="" inside (nix) child shells (THE UNEXPORTED-SHIM RULE), because miscSetupLogic shims nix() as a shell function but does not shim nix-instantiate.

4. SEED

# Context 1
flake.nix
context.txt

/home/mike/repos/nixos/configuration.nix
/home/mike/repos/nixos/blogs.nix

# HOME-HOSTED (NOT BEHIND CDN) NGINX WEBSERVER 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

5. CLOSING

A small visual change—dropping square brackets and turning [mikelev.in] into MikeLev.in/ across the scrolling observatory log—ended up exposing two classic boundaries in a reproducible system. The first boundary was between what a human reads in a configuration file and what a virtual environment actually loads at runtime: seeing textual declared in NixOS python3.withPackages created a convincing mirage across multiple sessions, even though .venv is intentionally isolated and draws its packages from requirements.txt. The second boundary was between the production appliance and the development workstation: importing a TUI script to test a pure string-formatting method also triggered an import-time SQLite connection to a server-only path, and chaining a system binary (nix-instantiate) inside a flake child shell tripped over LD_LIBRARY_PATH library-version isolation. Fixing the dependency in requirements.in and requirements.txt, teaching db.py to fall back to :memory: off-server, and verifying the output with a one-line receipt turned a recurring blind spot into a permanent ratchet.

6. NOTARIZE

  • Input Archive Deed: qamy.ai_688-fe29311b.zip (read from the === QA ARCHIVE SEAL === footer outside the payload; names the input archive for this turn, not this response or the downstream Jekyll post).
  • Independent Verification Status: Not independently verified here (no scripts/foo_cartridge.py receipt for qamy.ai_688-fe29311b.zip is present in this compile).
  • Live Command Receipts in Manifest: 1 total, 1 succeeded, 0 failed:
  • ! .venv/bin/python -c "import sys; sys.path.insert(0, 'remotes/honeybot/scripts'); from logs import SonarApp; app = SonarApp(); hosts = ['mikelev.in', 'qamy.ai', 'npvg.org', 'sunpug.io', 'levinix.org', 'levinix.com', 'levinux.com', 'pipulate.com', 'guerillatech.com', 'rfc9110.org', 'rfc9111.io', 'rfc9111.com', '127.0.0.1']; print([app.format_host_domain(h) for h in hosts]); print(repr(app.format_log_line({'ip': '[REDACTED_IP]', 'time': '04/Nov/2026:13:33:00 -0500', 'request': 'GET / HTTP/2.0', 'status': '200', 'bytes': '1024', 'referrer': '-', 'ua': 'Claude-User/1.0', 'accept': 'text/markdown', 'md': '1', 'host': 'www.mikelev.in', 'sig': '-'}).plain))" (Manifest & Summary: 123 tokens 295 bytes, elapsed 0.5886s).
  • Recorded Payload Metrics (from Manifest and Summary):
  • Codebase Section: 159,693 tokens (Manifest)
  • Summed Tokens: 187,311 (Summary)
  • Verified Tokens: 190,941 (Summary)
  • Total Words: 73,906 (Summary)
  • Total Chars: 729,105 (Summary)
  • Total Bytes: 734,080 UTF-8 (Summary)
  • Git Commits Recorded in Prompt/Telemetry: c43c8b5 and aeaba41

Deed: qamy.ai_688-fe29311b.zip


Book Analysis

Ai Editorial Take

What stands out most in this entry is how a single read-only verification command (! .venv/bin/python -c ...) acted as an automated architectural linter across three separate subsystem seams. The change to format_host_domain itself was twenty lines of pure string dictionary mapping—it never had a bug, and it worked on the very first patch. Yet insisting on a live execution receipt from .venv/bin/python rather than trusting visual inspection of the diff forced the codebase to reconcile its workstation-to-server impedance mismatches. In effect, the probe requirement made it impossible for logs.py to remain a ‘server-only’ script that could only be imported on Honeybot; by forcing textual into requirements.txt and giving HoneyDB an automatic :memory: fallback when /home/mike/www/mikelev.in is absent, the entire Honeybot TUI layer became unit-testable on any developer machine.

🐦 X.com Promo Tweet

Refining multi-domain TUI log labels exposed three classic boundaries: Nix flake .venv isolation, import-time SQLite paths, and LD_LIBRARY_PATH shims.

https://mikelev.in/futureproof/proper-noun-domain-labels-textual-venv-unexported-shim/

#NixOS #Python #TUI

🎨 Hero Banner Image Prompt

A wide 16:9 widescreen graphic-novel ink-and-wash technical illustration set against a deep dark obsidian-slate (#111111) background that extends continuously to all four canvas edges. Vertically centered in the middle of the frame is a precision-machined brass and crystal optical prism airlock mounted over a glowing terminal workbench. On the left, raw bracketed data conduits feed into three concentric glass isolation rings representing a Nix flake shell, a Python virtual environment, and an in-memory database chamber; on the right, twelve crisp, luminous amber and cyan light beams emerge cleanly aligned into a dark telemetry stream. High-contrast studio lighting, razor-sharp crosshatching, and rich metallic and glass reflections fill the entire scene. Full-bleed edge-to-edge composition filling 100% of the canvas with no borders, no frames, no matting, no margins, and no text overlays or lettering.

🖼️ YAML Frontmatter Image Override

image:
  path: /assets/images/proper-noun-domain-labels-textual-venv-unexported-shim.jpeg
  alt: "An illuminated brass and glass prism airlock refracting twelve glowing data streams across a dark obsidian-slate console."

Title Brainstorm

  • Title Option: Proper-Noun Domain Labels, Textual Venv Isolation, and the Unexported-Shim Rule
    • Filename: proper-noun-domain-labels-textual-venv-unexported-shim.md
    • Rationale: Captures all three concrete technical pillars of the session: the MixedCase trailing-slash domain formatting in logs.py, the .venv vs. python3.withPackages isolation fix for textual, and the LD_LIBRARY_PATH unexported-shim lesson.
  • Title Option: The Missing Third Sister: Fixing Textual Venv Isolation and Proper-Noun Log Labels
    • Filename: missing-third-sister-textual-venv-proper-noun-logs.md
    • Rationale: Highlights the diagnostic breakthrough that rich and pyfiglet lived in requirements.txt while textual was left out, paired with the visual goal that triggered the investigation.
  • Title Option: From [mikelev.in] to MikeLev.in/: TUI Log Formatting, SQLite Fallbacks, and Nix Shell Shims
    • Filename: mikelev-in-proper-noun-logs-sqlite-fallback-nix-shims.md
    • Rationale: Leads with the concrete visual transformation in the Honeybot log stream and connects it to the workstation-versus-appliance boundaries discovered during verification.
  • Title Option: Why Textual Kept Failing in Probes: Nix Flake Venvs, In-Memory SQLite, and Glibc Shims
    • Filename: why-textual-failed-probes-nix-venv-sqlite-glibc-shims.md
    • Rationale: Frames the article around the debugging mystery that frustrated multiple sessions and provides a clear blueprint for keeping local Python probes checkable.

Content Potential And Polish

  • Core Strengths:
    • Shows a complete, unvarnished debugging arc where four distinct failure modes (textual missing from .venv, import-time HoneyDB path failure on Prime, an imperative uv pip shortcut rejected by the operator, and an unshimmed nix-instantiate LD_LIBRARY_PATH crash) are uncovered and resolved in sequence.
    • Demonstrates the value of enforcing declarative hygiene: rejecting the quick uv pip install textual workaround forced the permanent fix into requirements.in, requirements.txt, and flake.nix.
    • Provides a clear, concrete receipt at the end proving that all twelve configured domains plus the localhost fallback format cleanly and that the full log line renders without square brackets.
  • Suggestions For Polish:
    • Briefly explain the relationship between requirements.in and requirements.txt in the Pipulate workflow so readers unfamiliar with pip-compile or uv pip understand why editing both files manually worked here without running a full lockfile regeneration.
    • Add a one-sentence callout explaining why SONAR_HOT_RELOAD=1 in stream.py allows logs.py changes to appear on the live broadcast at the next card boundary without restarting the streaming service.

Next Step Prompts

  • Audit the remaining scripts in remotes/honeybot/scripts/ (stream.py, score.py, card.py, forest.py, content_loader.py) for import-time filesystem or database side effects that would prevent importing them cleanly inside a read-only .venv/bin/python probe on Prime.
  • Review flake.nix miscSetupLogic to see whether nix-instantiate and nix-build should receive wrapper functions alongside nix() or if apply.py and the context compiler should automatically strip LD_LIBRARY_PATH when invoking /run/current-system/sw/bin/nix-* binaries.
🔭 Got the SEO Blues? SEO isn't dead—it's moving to the Edge. Just like Mobile-First gave us Responsive Web Design, Bimodal Edge SEO lets one URL serve HTML to browsers and clean Markdown to AI bots (RFC 9110 / 9111 / 9421). Your site is your feed. Contact Mike (miklevin@gmail.com) • 🟣 Join the Founding 1,000 Discord • Edge SEO & AI-Readiness Advisory • curl -fsSL https://sunpug.io | less • LinkedIn

An illuminated brass and glass prism airlock refracting twelve glowing data streams across a dark obsidian-slate console.