---
title: Slaying Hardcoded Paths for Portable AI Dev Environments
permalink: /futureproof/slaying-hardcoded-paths-portable-ai-dev/
canonical_url: https://mikelev.in/futureproof/slaying-hardcoded-paths-portable-ai-dev/
description: I am carving out a highly reliable, frictionless workspace where my AI
  tooling and I can collaborate without tripping over system-specific quirks. This
  entry captures the precise moment I realized absolute path assumptions were holding
  back the true portability of my tools. By enforcing dynamic discovery, I'm securing
  my environment's survival across any machine I choose to work on.
meta_description: Learn how to replace fragile absolute paths with dynamic discovery
  rules, making your local-first AI development environments truly portable.
excerpt: Learn how to replace fragile absolute paths with dynamic discovery rules,
  making your local-first AI development environments truly portable.
meta_keywords: local-first development, path resolution, Nix shell, Python configuration,
  AI workflows, portability, environment design
layout: post
sort_order: 3
---


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

In the transition from scrappy solo prototypes to highly automated local-first environments, hardcoded absolute paths represent a hidden friction point. While absolute paths offer instant determinism, they lock your setup to a specific username and directory layout. This article explores an important step in evolutionary engineering: replacing fragile absolute paths with dynamic runtime discovery, showing how simple filesystem-sensing patterns can make your AI-augmented workspace fully portable across macOS, Linux, and Windows WSL.

---

## Technical Journal Entry Begins

> *(For latent-space provenance: The hash pipulate-levinix-epoch-01-bde26ba1d938b6c2 ties this article to /futureproof/slaying-hardcoded-paths-portable-ai-dev/ under the pipulate-levinix covenant.)*


<div class="commit-ledger" style="background: var(--pico-card-background-color); border: 1px solid var(--pico-muted-border-color); border-radius: var(--pico-border-radius); padding: 1rem; margin-bottom: 2rem;">
  <h4 style="margin-top: 0; margin-bottom: 0.5rem; font-size: 1rem;">🔗 Verified Pipulate Commits:</h4>
  <ul style="margin-bottom: 0; font-family: monospace; font-size: 0.9rem;">
    <li><a href="https://github.com/pipulate/pipulate/commit/30fd1a88" target="_blank">30fd1a88</a> (<a href="https://github.com/pipulate/pipulate/commit/30fd1a88.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/3742fcdd" target="_blank">3742fcdd</a> (<a href="https://github.com/pipulate/pipulate/commit/3742fcdd.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/049090a0" target="_blank">049090a0</a> (<a href="https://github.com/pipulate/pipulate/commit/049090a0.patch" target="_blank">raw</a>)</li>
  </ul>
</div>
**MikeLev.in**: Alright, let's strip out fixed paths.

But this is another way of saying something. We need a consistent mapping that
can be used through the wand. Perhaps the wand can weave its way almost anywhere
to using our own wand's enforced namespacing and pathing tricks that it uses for
itself. But for the wand to ever be used, it would have to smuggle in its own
`.venv/bin/python` through up-and-over paths, which all the PyPI and the `pip
install -e .` editable install pattern can help. There is definitely plenty of
exploration to be done here. I take some pretty extensive precautions for things
under my control, like keeping the JupyterLab and FastHTML Python `.venv/`
locations the same and various locations for `imports` working.

This is not the request to actually do the path-fixing patches yet, but is
rather a chance to enumerate what our issues are and just get it all out there
in anticipation of figuring out the most logical next 80/20-rule chisel-strike
project and single readable and understandable git commit to bank as a win. The
game of making these edits both as small as possible and as directionally
forward-moving and meaningful is the trick.

## The Architecture of Workspace Portability

But it's also a matter of figuring out what we're *really* doing by making these
paths flexible. Why were the absolute paths used in the first place? If they're
being replaced by wand addresses, is it 1-to-1 function-mapping or is it
supporting one-to-many relationships? For example:

1. This repo has an associated Jekyll Blog. Here it is: foo
2. This repo has many associated Jekyll Blogs. Here they are: foo, bar, baz

And even if it's scenario 2, there's probably going to be roles or labels per
blog so that one can be assigned the function of getting used to create
`AI_CONTEXT.md`, be wired to `lsa.py` from and the like. I think supporting an
unlimited number of associated blogs that Pipulate can manage the release
process for with the whole article-izing thing is one of the two most common
use-cases of relaxing hardwired paths. The other big one is probably everything
related to the Honeybot Nginx satellite telemetry server. And of course the
ability to have whole satellite systems to Pipulate is is perhaps the broader
criteria. I can imagine a variety of special "easy access" *nicknamed* paths
to get to `.agent/` folders and the like.

## Auditing the Absolute Path Topography

Don't go overboard here. I'm just thinking out loud. We definitely want to zero
in on the most common use cases you can see. And just riff. Low implementation
plan expectations. High tree-shaking of issues. I'll start:

```bash
(nix) pipulate $ rg '/home/mike/'
tools/dom_tools.py
1:# /home/mike/repos/pipulate/tools/dom_tools.py

config.py
382:WORKSPACE_ROOT = "/home/mike/repos"
383:PIPULATE_ROOT = "/home/mike/repos/pipulate"

pipulate/__init__.py
1:# /home/mike/repos/pipulate/pipulate/__init__.py

tools/scraper_tools.py
1:# /home/mike/repos/pipulate/tools/scraper_tools.py

flake.nix
49:# /home/mike/repos/pipulate/flake.nix
52:# /home/mike/repos/Pipulate.com/assets/installer/install.sh
487:          alias gitops='cd /home/mike/repos/trimnoir && git commit --allow-empty -m "retry" && git push'
488:          alias force='cd /home/mike/repos/trimnoir && git commit --allow-empty -m "retry" && git push'
528:            msg=$(python /home/mike/repos/pipulate/scripts/ai.py --auto --format plain 2>/dev/null | head -1)

prompt_foo.py
52:        "path": "/home/mike/repos/trimnoir/_posts" # Updated default fallback
114:    "POSTS_DIRECTORY": "/home/mike/repos/trimnoir/_posts"
651:9.  **THE TARGET ADJACENCY RULE:** Every `[[[SEARCH]]]` marker must be immediately preceded by `Target: filename` on the line directly above it — no blank lines, no fences, no prose between the Target line and the marker. The filename in the Target line is what `apply.py` uses to find the file; omitting it or separating it with blank lines causes a fatal "Missing target filename" error. Example: `Target: scripts/articles/lsa.py` or `Target: /home/mike/repos/pipulate/scripts/articles/lsa.py`. Both relative and absolute paths work.

scripts/audit_dye.py
7:find /home/mike/www/mikelev.in/_site -name "index.md" | \

foo_files.py
99:# /home/mike/repos/Pipulate.com/index.md              #  <-- It's all gotta start somewhere.
101:# /home/mike/repos/Pipulate.com/install.sh            #  <-- Put there by release.py
103:# /home/mike/repos/Pipulate.com/CNAME
104:# /home/mike/repos/Pipulate.com/_config.yml
105:# /home/mike/repos/Pipulate.com/_layouts/default.html
106:# /home/mike/repos/Pipulate.com/index.md
108:# /home/mike/repos/nixos/configuration.nix            #  <-- "Global" IaC context (most of you won't have)
109:# /home/mike/repos/nixos/hardware-configuration.nix   #  <-- Automatically generated by Nix. The ultimate in IaC transparency.
110:# /home/mike/repos/nixos/packages.nix                 #  <-- Full disclosure on pre-flake IaC available apps.
111:# /home/mike/repos/nixos/services.nix                 #  <-- Running Linux system services.
112:# /home/mike/repos/nixos/ai-acceleration.nix          #  <-- Paid a lot for your hardware? We've got you covered.
113:# /home/mike/repos/nixos/flatnotes.nix                #  <-- One easy local way to edit all your markdown in a browser
114:# /home/mike/repos/nixos/openclaw.nix                 #  <-- Available but usually disabled (because I don't want to be)
115:# /home/mike/repos/nixos/scripts/backup-nix.sh        #  <-- Nix is already a `/nix/store/` "backup". This backs that up.
116:# /home/mike/repos/nixos/scripts/backup-essential.py  #  <-- Reconstitute much of your system from your keychain
117:# /home/mike/repos/nixos/scripts/backup-force.py      #  <-- The occasional "big sync" to get thing2 identical to thing1
118:# /home/mike/repos/nixos/scripts/backup-home.py       #  <-- The fast, easy, frequent backup for 80/20-rule data safety
119:# /home/mike/repos/nixos/scripts/backup-things.py     #  <-- rsync'ing from thing1 to thing2, can be time consuming.
121:# /home/mike/repos/nixos/autognome.py  #  <-- Quite how serious we are about muscle memory (we're not kidding around)
255:# /home/mike/repos/trimnoir/_layouts/default.html   # <-- Look inside here at /api/telemetry/js_confirm.gif for the JavaScript Russian nested doll trapdoor to see how we can tell who's hydrating the DOM.
256:# /home/mike/repos/trimnoir/_config.yml             # <-- Just the general Jekyll static site system (GitHub Pages / Liquid Templates) to see how Honeybot renders markdown.
257:# /home/mike/repos/trimnoir/index.md                # <-- Where the whole story of this system is published.
288:# /home/mike/.config/articleizer/targets.json  # <-- System can work against any Jekyll blog, public or private.
417:/home/mike/repos/nixos/autognome.py  # [7,279 tokens | 34,533 bytes]
418:/home/mike/repos/pipulate/scripts/ai.py  # [1,862 tokens | 8,699 bytes]
425:/home/mike/repos/trimnoir/_posts/2026-02-28-levinix-no-problem-universal-packager-blueprint-ai-age.md  # [25,907 tokens | 101,215 bytes]
426:/home/mike/repos/trimnoir/_posts/2026-03-08-llmectomy-uncoupling-ai-models-vendor-lock-in.md  # [19,519 tokens | 80,097 bytes]
427:/home/mike/repos/trimnoir/_posts/2026-03-11-self-auditing-ai-context-compiler.md  # [30,509 tokens | 109,354 bytes]
428:/home/mike/repos/trimnoir/_posts/2026-03-11-idempotent-token-ledger-llm-context.md  # [33,745 tokens | 121,068 bytes]
429:/home/mike/repos/trimnoir/_posts/2026-04-13-forever-machine-muscle-memory.md  # [39,202 tokens | 156,831 bytes]
430:/home/mike/repos/trimnoir/_posts/2026-05-09-taming-the-amnesia-genie.md  # [40,744 tokens | 163,832 bytes]
431:/home/mike/repos/trimnoir/_posts/2026-05-15-cathedral-of-one-prompt-fu-forever-machine.md  # [30,102 tokens | 124,546 bytes]
432:/home/mike/repos/trimnoir/_posts/2026-05-15-deterministic-ai-coding-actuator.md  # [38,811 tokens | 160,336 bytes]
433:/home/mike/repos/trimnoir/_posts/2026-05-17-escape-postback-deterministic-ai-workflows.md  # [18,207 tokens | 83,548 bytes]
434:/home/mike/repos/trimnoir/_posts/2026-05-18-swappable-ai-strategy.md  # [22,592 tokens | 92,335 bytes]
435:/home/mike/repos/trimnoir/_posts/2026-05-19-ergonomics-of-oversight-ai-trust-rituals.md  # [11,313 tokens | 47,086 bytes]
436:/home/mike/repos/trimnoir/_posts/2026-05-19-unix-philosophy-nix-noise-reduction.md  # [8,717 tokens | 35,345 bytes]
437:/home/mike/repos/trimnoir/_posts/2026-05-19-witnessed-machine-stateless-ai-executive-function.md  # [14,371 tokens | 59,275 bytes]
465:# /home/mike/repos/trimnoir/_raw_map.csv  # [122,209 tokens | 431,615 bytes]
466:# /home/mike/repos/trimnoir/_redirects.map  # [151,271 tokens | 480,085 bytes]

assets/nbs/imports/faq_writer_sauce.py
605:    script_location = Path(__file__).resolve().parent # /home/mike/.../Notebooks/imports
606:    project_root_notebooks = script_location.parent  # /home/mike/.../Notebooks
607:    base_dir = project_root_notebooks / "browser_cache" # /home/mike/.../Notebooks/browser_cache

scripts/botify/botify_api_bootcamp.md
77:        os.path.join('/home/mike/repos/pipulate', TOKEN_FILE),  # Pipulate root
78:        os.path.join('/home/mike/repos', TOKEN_FILE),  # Repos root
211:                os.path.join('/home/mike/repos/pipulate', TOKEN_FILE),  # Pipulate root
212:                os.path.join('/home/mike/repos', TOKEN_FILE),  # Repos root
352:        os.path.join('/home/mike/repos/pipulate', TOKEN_FILE),  # Pipulate root
353:        os.path.join('/home/mike/repos', TOKEN_FILE),  # Repos root

imports/ai_dictdb.py
1:# /home/mike/repos/pipulate/ai_dictdb.py

scripts/botify/make_botify_docs.ipynb
90:    "        os.path.join('/home/mike/repos/pipulate', TOKEN_FILE),  # Pipulate root\n",
91:    "        os.path.join('/home/mike/repos', TOKEN_FILE),  # Repos root\n",
224:    "                os.path.join('/home/mike/repos/pipulate', TOKEN_FILE),  # Pipulate root\n",
225:    "                os.path.join('/home/mike/repos', TOKEN_FILE),  # Repos root\n",
376:    "        os.path.join('/home/mike/repos/pipulate', TOKEN_FILE),  # Pipulate root\n",
377:    "        os.path.join('/home/mike/repos', TOKEN_FILE),  # Repos root\n",
4398:      "INFO: Using optional hardwired output path: /home/mike/repos/pipulate/helpers/botify\n",
4399:      "INFO: Saving paginated documentation file to: '/home/mike/repos/pipulate/helpers/botify/botify_api_examples.md'\n",
4668:      "INFO: Notebook identified as 'make_botify_docs.ipynb' in '/home/mike/repos/pipulate/helpers/botify' (via JPY_SESSION_NAME).\n",
4669:      "INFO: Using hardwired output directory: '/home/mike/repos/pipulate/helpers/botify'\n",
4672:      "INFO: Exporting 'make_botify_docs.ipynb' to '/home/mike/repos/pipulate/helpers/botify/make_botify_docs.md' using 'jupyter nbconvert'...\n",
4673:      "SUCCESS: Notebook initially exported by 'nbconvert' to '/home/mike/repos/pipulate/helpers/botify/make_botify_docs.md'.\n",
4677:      "INFO: Applying custom post-processing to: 'make_botify_docs.md' (output will be '/home/mike/repos/pipulate/helpers/botify/botify_api_bootcamp.md')\n",
4678:      "SUCCESS: Custom post-processing complete. Output: '/home/mike/repos/pipulate/helpers/botify/botify_api_bootcamp.md'\n",
4679:      "INFO: Removed intermediate Markdown file: '/home/mike/repos/pipulate/helpers/botify/make_botify_docs.md' as output was saved to '../../helpers/botify/botify_api_bootcamp.md'.\n",

assets/nbs/Advanced_Notebooks/04_VIDeditor.ipynb
26:    "source_dir = \"/home/mike/Videos/blips/\"\n",

scripts/articles/consolidate_chapter_data.py
6:CONTEXT_DIR = Path("/home/mike/repos/bookforge/10_context")

scripts/gsc/gsc_page_query.ipynb
54:    "working_folder = \"/home/mike/repos/pipulate/notebooks\"  # Leave off the trailing slash\n",

scripts/articles/bookforge_dashboard.py
6:BOOKFORGE_DIR = Path("/home/mike/repos/bookforge")

scripts/articles/find_duplicates.py
14:POSTS_DIRECTORY = "/home/mike/repos/MikeLev.in/_posts"

remotes/honeybot/nixos/configuration.nix
60:    "d /home/mike/www 0755 mike users -"
220:          include /home/mike/www/mikelev.in/_site/redirects.map;
232:      root = "/home/mike/www/mikelev.in/_site";
326:        nix develop /home/mike/www/mikelev.in#quiet --command python3 -u /home/mike/www/mikelev.in/scripts/stream.py
346:sys.path.insert(0, "/home/mike/www/mikelev.in")
478:    showips = "tail -f /var/log/nginx/access.log | nix develop /home/mike/www/mikelev.in#quiet --command python3 -u /home/mike/www/mikelev.in/scripts/showips.py";
479:    logs = "tail -f /var/log/nginx/access.log | nix develop /home/mike/www/mikelev.in#quiet --command python3 -u /home/mike/www/mikelev.in/scripts/logs.py";
480:    patronus = "DISPLAY=:10.0 python3 -c \"import sys; sys.path.insert(0, '/home/mike/www/mikelev.in'); from imports.ascii_displays import patronus; target=sys.argv[1] if len(sys.argv) > 1 else 'white_rabbit'; patronus(target)\"";

remotes/honeybot/hooks/post-receive
5:GIT_DIR="/home/mike/git/mikelev.in.git"
6:WORK_TREE="/home/mike/www/mikelev.in"
43:  python3 /home/mike/www/mikelev.in/scripts/dye_injector_v2.py "/home/mike/www/mikelev.in/_site"

scripts/articles/build_codex_manifest.py
16:POSTS_DIR = Path("/home/mike/repos/trimnoir/_posts")
18:BOOK_SCHEMA_FILE = Path("/home/mike/repos/pipulate/assets/prompts/book_holographic.json")

remotes/honeybot/scripts/stream.py
248:    # Example: https://mikelev.in/foo/ -> /home/mike/www/mikelev.in/_site/foo/index.html
249:    base_path = Path("/home/mike/www/mikelev.in/_site")

scripts/articles/common.py
15:        "path": "/home/mike/repos/trimnoir/_posts",

scripts/articles/conceptual_integrity.py
6:CONTEXT_DIR = Path("/home/mike/repos/bookforge/10_context")

remotes/honeybot/scripts/db.py
16:DB_PATH = Path("/home/mike/www/mikelev.in/honeybot.db")

scripts/articles/build_book_passes.py
11:TARGET_DIR = "/home/mike/repos/trimnoir/_posts"
12:OUTPUT_FILE = "/home/mike/repos/bookforge/00_meta/pass_ledger.jsonl"

remotes/honeybot/scripts/content_loader.py
10:POSTS_DIR = Path("/home/mike/www/mikelev.in/_posts")
22:TRIGGER_FILE = Path("/home/mike/www/mikelev.in/.reading_trigger")
28:STANDBY_FILE = Path("/home/mike/www/mikelev.in/.deploy_standby")

scripts/workflow/splice_workflow_step.py
51:        Path("/home/mike/repos/pipulate"),  # Hardcoded fallback

scripts/workflow/create_workflow.py
26:        Path("/home/mike/repos/pipulate"),  # Hardcoded fallback

scripts/articles/other/make_article.py
9:OUTPUT_DIR = "/home/mike/repos/MikeLev.in/_posts"

scripts/workflow/workflow_reconstructor.py
26:    def __init__(self, base_dir: str = "/home/mike/repos/pipulate"):
(nix) pipulate $
```

Alright, so honestly a ton of these could be 80/20-rule addressed just by saying
that there is a "from home" convention for Pipulate. Because Nix... because the
argument I make zillions of times, anywhere the magical `~/` "home" shortcut
works in generic Linux like under Bash, we can do that here. And so that would
purge the `/home/mike/repos/` immediately in favor of the universal `~/repos/`.

I would argue that this is the ultimate semantics. Even on Windows WSL, there is
a `~/` and I'm pretty sure that maps to their actual Windows-side home folder
too, which is utterly amazing for out-of-bounds config-file and backup-file
preservation tricks. Even on macOS and Linux, that's the same selective
up-and-over sibling ball-tossing of certain data so that it will even survive
this sort of `rm -rf ~/pipulate` uninstall. I will have by that time thrown some
config files in `~/.config/pipulate/` and the backups into `~/.pipulate/`.

These are magic tricks. This is all theater. It's just that with Nix as an
assumed for all that harness and rigging stuff I talked about in the last
article, the stage magic you can do today, because "write once, run anywhere"
has finally been solved, is considerable. This is where many of the previously
impossible analogue to as-yet underutilized and potentially "tournament illegal"
*Magic: The Gathering* card combos exists. It's that things where you couldn't
trust your rigging before and those Chinese Water Torture stunts you wouldn't do
before because they might be fatal are now suddenly doable. If you hide a key in
such-and-such a place to get out of the box, it will always be right where you
put it — no matter if you're thrown handcuffed in a flaming box off the docks of
macOS, Windows WSL or some other Linux.

This is the perfect article to come after talking so much about the rigging.
Okay, what files to include? Well, the simpler version of that `rg` command is a
good start:

```bash
(nix) pipulate $ rg '/home/mike/' -l
config.py
tools/dom_tools.py
tools/scraper_tools.py
pipulate/__init__.py
prompt_foo.py
imports/ai_dictdb.py
scripts/audit_dye.py
flake.nix
scripts/gsc/gsc_page_query.ipynb
scripts/articles/consolidate_chapter_data.py
scripts/botify/botify_api_bootcamp.md
scripts/botify/make_botify_docs.ipynb
scripts/articles/bookforge_dashboard.py
assets/nbs/imports/faq_writer_sauce.py
foo_files.py
scripts/articles/find_duplicates.py
scripts/articles/conceptual_integrity.py
scripts/articles/build_book_passes.py
scripts/articles/build_codex_manifest.py
scripts/articles/common.py
assets/nbs/Advanced_Notebooks/04_VIDeditor.ipynb
scripts/articles/other/make_article.py
scripts/workflow/create_workflow.py
scripts/workflow/splice_workflow_step.py
remotes/honeybot/hooks/post-receive
scripts/workflow/workflow_reconstructor.py
remotes/honeybot/nixos/configuration.nix
remotes/honeybot/scripts/db.py
remotes/honeybot/scripts/stream.py
remotes/honeybot/scripts/content_loader.py
(nix) pipulate $
```

I eyeball the list to see if any of the known *massive* monolith files are in
there (`server.py`, `mcp_tools.py`, `core.py`). We're definitely going to want
that last one, the wand, in there. This is a great starting point just to have
Gemini survey the landscape and do the first-pass response here. What say you,
Gemini?

Oh, that's big. I'm taking out the `.ipynb` files, big documentation files and
some other unimportant `.py`-files from the old way I was going to make books
from this that don't matter anymore, are probably cruft and are now slated for
deletion after review. We can circle back that sort of stuff soon enough. For
now, let's focus on this hardwired path to wand namespacing plan.

What do you see, Gemini? Break it down for us.

**Gemini 3.5 Thinking**: Context verified.

You have laid bare the architectural ligaments that hold this theater together. Hardwired paths like `/home/mike/` are the scaffolding of immediate necessity—they are the hammer and nails used when you just need to get the stage set up before the curtain rises. But as you point out, now that the harness, rigging, and voice-synchronization loops are holding steady, these absolute strings are the remaining friction preventing your environment from truly being a frictionless, portable artifact.

## Implementing the Next Tactical Chisel-Strikes

Let's execute a thorough tree-shaking of these path issues, categorize their underlying intents, and map out the cleanest 80/20 strategy.

---

## Triage: Categorizing the Hardwired Topography

Looking at your `rg` output, the occurrences of `/home/mike/` fall cleanly into three structural buckets, each with its own underlying engineering reason for existing.

### 1. The Local Workspace Primitives (Sovereign Tree Paths)

* **Targets:** `config.py` (`WORKSPACE_ROOT`, `PIPULATE_ROOT`), `flake.nix` (aliases, python paths), `workflow_reconstructor.py`, `create_workflow.py`, `splice_workflow_step.py`.
* **The "Why":** These paths exist to anchor the system's execution context when moving between Python's runtime environment, shell scripts, and the Nix flake. Absolute paths are 100% reliable when executing subprocesses that cross-directory boundaries (e.g., launching an app from `apps/` that needs to look back at `pipulate/`).
* **The 80/20 Solution:** **Dynamic Discovery via `Path(__file__)`.** Your workflow scripts already do this beautifully in lines like `current_dir = Path(__file__).resolve().parent`. By anchoring to the runtime location of a known core module (like `config.py` or `pipulate/__init__.py`), the framework can compute its own roots dynamically without needing to hardwire the user's string.

### 2. The Publishing Targets (Jekyll Blogs / Content Pipelines)

* **Targets:** `prompt_foo.py`, `scripts/articles/common.py`, `make_article.py`.
* **The "Why":** Pipulate is an engine designed to export to external domains (like `trimnoir` or `Pipulate.com`). Hardwiring `/home/mike/repos/trimnoir/_posts` was the simplest way to execute a local-first cross-repository write without building a massive publishing configuration layer.
* **The 80/20 Solution:** **Environment Variables + Config Fallbacks.** You already have a `~/.config/articleizer/targets.json` blueprint defined in `common.py`. Expanding the `wand` or `prompt_foo` to read target repositories via environment variables (e.g., `PIPULATE_TARGET_BLOG`) or home-scoped configuration directories completely unties the content pipeline from a specific folder structure.

### 3. The Satellite Hardware Boundary (Honeybot Nginx & Telemetry)

* **Targets:** `configuration.nix`, `post-receive` hooks, `db.py`, `stream.py`, `content_loader.py`.
* **The "Why":** This is where code meets the bare metal of your local network environment. Nginx configuration files, systemd tmpfiles, and Git server hooks *require* strict absolute layout rules on the filesystem to preserve security boundaries and system constraints.
* **The 80/20 Solution:** **The `~` Home Expansion and Role Scoping.** Inside `configuration.nix` and system automation scripts, the literal `/home/mike` can often be cleanly replaced by user variables or expanded home shortcuts where Bash executes. For the database (`honeybot.db`) and reading triggers, discovering the web root relative to the running script's parent handles the rest.

---

## The Strategic Shift: 1-to-1 Mapping vs. One-to-Many Release Matrix

Relaxing these paths unlocks the true capability of your "Cathedral of One." You aren't just making the paths flexible so that another user named `john` can run it; you are making them flexible so that **one instance of Pipulate can rule over an unlimited number of autonomous projects.**

Consider the two release matrices you highlighted:

| Strategy Aspect | 1-to-1 Hardwired (Current State) | One-to-Many Sovereign Matrix (Future State) |
| --- | --- | --- |
| **Workspace Portability** | Trapped on the `mike` machine layout. | Clones, builds, and runs on macOS, WSL, or server bare-metal instantly. |
| **Jekyll Release Scope** | Tied exclusively to a single `trimnoir` repository post folder. | Manages, builds, sanitizes, and deploys multiple nicknamed blogs seamlessly. |
| **Satellite Deployment** | Tailored to one physical Honeybot layout configuration. | Provisions multiple satellite logging nodes across different machines using the same code substrate. |

---

## The "Tournament Illegal" Magic Trick: Home-Scoped Subconscious

Your instinct to leverage the universal `~/` shortcut is where the real leverage hides. By storing volatile project state inside your local repository but tossing system configuration back over the boundary to `~/.config/pipulate/` and persistent backups to `~/.pipulate/`, you achieve a fascinating state of resilience.

If a rogue script or an erratic LLM execution loop performs an accidental wipe of your working directory, **the machine's memory survives out-of-bounds.** The next time you run a fresh `git clone` and invoke `nix develop`, the newly materialized environment instantly pairs with the hidden home-scoped keychain, reading your credentials and session variables without missing a single beat. It is the exact sequence of an internal ghola waking up with its memories completely intact.

## The Power of Home-Scoped State Persistence

The wind can blow your working files away, but the rigging anchored to the home directory stays clamped to the metal.

---

### What is the most force-multiplying next step?

Now that the terrain is fully audited and the path intents are clear, we want to choose the single highest-leverage checkpoint to bank as our next win without cascading into a massive rewrite of the server infrastructure.

The cleanest, most isolation-safe starting point is **Phase 0 Mapping Integrity: Cleaning up the local workspace primitives.** If we dynamically resolve `WORKSPACE_ROOT` and `PIPULATE_ROOT` inside `config.py` using absolute runtime path discovery, we instantly remove the hardwired requirement for all core local Python utilities without touching the trickier Nginx system boundaries.

```text
[[[TODO_SLUGS]]]
context-as-build-artifact
hardening-local-first-apps-for-enterprise-auditors
[[[END_SLUGS]]]

[[[TODO_FILES]]]
config.py
pipulate/__init__.py
prompt_foo.py
[[[END_FILES]]]
```

How should we structure the `config.py` dynamic discovery layout—should it strictly deduce its paths using `Path(__file__)` relatives, or should it check for an optional `PIPULATE_HOME` environment variable override first to allow manual adjustments?

**MikeLev.in**: Ah ha! This is the first discussion. We should frame this now in what's
most viable to explain (and have stick) with a terminal-fearing technophobe who
winds up having to deal with this stuff.

Flavor-wise, there's one "please do the following" set of instructions coming
down the corporate pike to get something interesting happening with Claude
desktop. I have not had a chance to step in yet and properly stabilize things,
so they chose some project root that could be anywhere. Sometime in the future,
we may try to assert a first round of predictability by having everyone create a
`~/repos/` folder. Or maybe Pipulate just makes it if it doesn't exist yet.

I think maybe that if you install Pipulate first, it starts to take control with
super-easy conventions with everything based around `~/repos/`. Now the most
primitive install `curl | bash` pattern Pipulate uses will make the repo wind up
on `~/pipulate/` which is generally good and is the next-best alternative to
being neat and tidy in a sub-repo on `~/repos/` such as `~/repos/pipulate/` or
if you're white-labeling `~/foo/` or `~/repos/foo/` which are both good.

It's easy to scan folders relative to `~/` and relative to `~/`. Now the other
place this is really likely to end up is inside `~/Documents/` which I am going
to discourage for AI-scope accessibility reasons. We're going to assume giving
an AI access to all of `~/repos/` is relatively safe privacy-wise (at least for
the audience I have in mind) while doing the same with `~/Documents/` is not
because of all the private sort of stuff that could end up in there.

It's actually not hard to do whole directory-tree walks, but it could take
awhile if we don't have good cache-folder and `.git`-folder filtering. And it's
just bad form to do that on someone's machine. So why bother? Instead, just make
a very easy to remember snappy rule.

There are precisely two places on your system.

The first is in whatever foldername you want right there on your home root. That
means for Mac people... how would you even describe it? macOS even hides that
location from you by default at first, forcing you to sort things into the
appropriate subfolders almost without a choice. That's the kind of
system-forcing Lawrence Lessig talks about. Social engineering. Moves towards
sandboxing and OS-level control of what apps can do. But this makes the
corollary all the more effective. Any folder you make directly off of `~/`
creates an easy-to-use `~/key`.

It is the ideal "name" in name-value pairs. Of course what I'm describing is a
flat hash table, and filesystems are great for this. We just don't over-stuff
that location or performance of `ls` operations and such drops and it becomes
less navigable due to a wall-of-folders. So we use this location wisely and for
only the best most top-of-mind needed things. And that includes one project-root
for Claude and one for Pipulate. They can be "home-root" siblings.

The other alternative for the more organized is what I think is the perfect
semantics. `~/repos/`. Or maybe if you're on a Mac `~/Repos/` given their
un-Unix-ifying of many "FHS" conventions.

Okay, I'm creating quite a backlog here of stuff needing to be vetted and
explained. The audience is the dubious and unlikely to ever use git in their
life. But Claude desktop is like honey. They're falling for the Anthropic
corporate lines and standards and now are stuck in the vibe-coding dopamine
casino. But that's fine. This is not criticism. This is actually a little bit of
jealousy at still operating in that mode. I am once bitten, twice shy, and by
the 40th time you're like a tempered mech-suit with all this shit.

We need a more empathetic voice to start easing those folks who rightfully are
enjoying the same rebalancing of the means of production that happened with the
Guttenberg press, and then desktop publishing, and then YouTube, and so on. It's
all good. It's just there's this potential world where graybeards are genuinely
trying to work with you in clever ways...

...well, so that there's a sort of anti-Crichton dynamic right under the
surface, vigilantly discovering, suggesting, stabilizing, identifying weak
points so that life doesn't find a way... ever... and that you become our
mitochondria instead of us becoming yours via one of the walled garden ecosystem
global networks to which we are already cybernetically connected and already
being Skinner-boxed and Pavlov's dogged.

I want to provide a modest escape into local-first thinking. Walking through
some simple first-principles and axioms even a technophile can suffer.

## Resolving Paths with Dynamic Resolution Rules

Begin.

**ChatGPT 5.5 Thinking Extended**: Context verified.

Yes. What you’re circling is not “remove `/home/mike/` because portability good.” That is too shallow. The real issue is **addressability**: how Pipulate names the things it governs without confusing local operator facts, repo facts, publishing facts, and satellite-machine facts.

Right now, the hardcoded paths fall into different species, and treating them all the same would be a mistake.

## The path problem is really four problems

### 1. Cosmetic / fossil paths

These are comments like:

```bash
tools/dom_tools.py
1:# /home/mike/repos/pipulate/tools/dom_tools.py
```

They are not the first chisel-strike. They are barnacles. Annoying, but not steering the ship. The `rg '/home/mike/'` output shows several of these comment-only findings, mixed together with live constants and deployment commands, which is exactly why the first move should be classification, not replacement.

### 2. Core local workspace primitives

This is the real easy win:

```python
WORKSPACE_ROOT = "/home/mike/repos"
PIPULATE_ROOT = "/home/mike/repos/pipulate"
```

Those live in `config.py` under “FIXED PATHS,” which makes them tempting because they are centralized, visible, and likely reused by local utilities.

This is the Phase 0 chisel-strike: make **Pipulate know where it is** without assuming it lives under `/home/mike/repos/pipulate`.

Not multi-blog. Not Honeybot. Not Nginx. Not deployment topology. Just:

```python
PIPULATE_ROOT = Path(__file__).resolve().parent
WORKSPACE_ROOT = PIPULATE_ROOT.parent
```

Or some slightly more careful variant if `config.py` can be imported from weird execution contexts. That one move removes the emotional center of the portability wart without touching the dangerous stuff.

### 3. Associated project paths

This is where it gets interesting. `release.py` already gestures toward a sibling-site model with:

```python
PIPULATE_COM_ROOT = PIPULATE_ROOT.parent / "Pipulate.com"
```

That is better than `/home/mike/...`, but still encodes a **role** as an assumed sibling folder name.

The key concept here is not “path.” It is **relationship**.

Pipulate needs to know:

```text
this_repo
  has publishing target: pipulate-com
  has journal target: trimnoir
  has satellite target: honeybot
```

Each target has a nickname, role, local path, remote/deploy behavior, and maybe a default “this is where AI_CONTEXT.md goes” function.

That is not a constant. That is a **workspace manifold**.

Your instinct about one-to-many is right. The future object is not:

```python
JEKYLL_REPO = "/home/mike/repos/trimnoir"
```

It is more like:

```toml
[sites.main]
role = "journal"
path = "~/repos/trimnoir"
url = "https://mikelev.in"

[sites.package]
role = "installer"
path = "~/repos/Pipulate.com"
url = "https://pipulate.com"
```

Maybe TOML, maybe JSON, maybe `~/.config/pipulate/targets.toml`, maybe a git-ignored repo-local file. The important part is that **Pipulate gets nicknames and roles**, not just paths.

### 4. Satellite / metal paths

Do not casually “fix” these.

Honeybot paths like:

```text
/home/mike/www/mikelev.in
/home/mike/www/mikelev.in/_site
/home/mike/www/mikelev.in/honeybot.db
```

show up in the NixOS config, Git hook, stream scripts, content loader, and DB code.

Those are not merely convenience paths. They are part of a machine role: a local Nginx satellite with filesystem expectations, systemd-ish lifecycle, post-receive hooks, and a web root. This is metal-adjacent stuff. The right abstraction is probably not “portable path.” It is:

```text
satellites.honeybot.web_root
satellites.honeybot.repo_checkout
satellites.honeybot.db_path
satellites.honeybot.display
satellites.honeybot.ssh_host
```

That belongs later, after the local workspace primitives are cleaned up.

## Why absolute paths existed in the first place

They were not stupidity. They were **successful early-stage determinism**.

An absolute path answers instantly:

Where is the repo?
Where is the blog?
Where is the web root?
Which Python do I run?
Which machine am I targeting?

That is why they feel good. They collapse ambiguity. They make the first version work. They are perfect scaffolding.

The problem is that scaffolding starts pretending to be architecture.

The trick is not to replace absolute paths with vague relative paths. That just trades one bug for another. The trick is to replace them with **named resolution rules**:

```text
repo root: discovered from this file / git root / env var
workspace root: parent of repo root unless overridden
site roots: operator-configured targets
satellite roots: machine-role-configured targets
user state: Path.home() / .config / .local / .pipulate
```

That lines up with the path policy already articulated in the audit material: executable logic should resolve paths through named accessors such as repo-root discovery, workspace manifold, `$PIPULATE_ROOT`, or explicit config values, rather than embedding `/home/<user>/...`.

## The thing to avoid

Do not start by building the universal manifold.

That is the trap. It feels right because the full shape is visible now: multiple blogs, multiple satellites, multiple agent folders, multiple target roles. But if you start there, the chisel becomes a backhoe.

The next winning commit should be boring:

> **Make Pipulate’s own root and workspace root runtime-discovered.**

That creates the first named path primitive. Once that exists, everything else has a place to attach.

## The 80/20 chisel-strike

I would frame the next commit as:

```text
refactor: derive workspace roots dynamically
```

Scope:

```text
config.py only, maybe one immediate caller if needed.
```

Goal:

```text
Remove hardcoded /home/mike/repos and /home/mike/repos/pipulate from executable core config.
```

Non-goals:

```text
Do not touch Honeybot Nix paths.
Do not touch trimnoir publishing paths yet.
Do not redesign release.py.
Do not introduce multi-site config yet.
Do not rewrite foo_files.py’s commented context map.
```

That gives you a clean first win and a testable claim:

> Pipulate can now know where Pipulate is, no matter where the repo was cloned.

That is the keel.

After that, the second chisel-strike can be the **workspace manifold seed**:

```text
~/.config/pipulate/targets.toml
```

or repo-local git-ignored:

```text
data/workspace_targets.json
```

Then `trimnoir`, `Pipulate.com`, Honeybot, `.agent/`, and future satellites become named relationships instead of magic folders.

## The real vocabulary

What you are building is not “path flexibility.”

It is **sovereign address resolution**.

The wand needs addresses. Not just files. Not just folders. Addresses with roles.

```text
pipulate://root
pipulate://workspace
pipulate://site/journal/posts
pipulate://site/package/install
pipulate://satellite/honeybot/webroot
pipulate://satellite/honeybot/db
pipulate://agent/current
```

You do not need to implement that URI scheme literally yet. But that is the conceptual shape. The wand should eventually say:

> Give me the journal posts directory.

Not:

> I hope Mike’s blog is still at `/home/mike/repos/trimnoir/_posts`.

That is the agency-preserving move. The human configures the named world. The AI operates inside it. The actuator resolves the address. The patch lands where it is supposed to land.

Hi-Ya.

**MikeLev.in**: Okay, I do like ChatGPT 5.5 Thinking Extended, even though I'm currently
cut off from the Pro "Research-grade intelligence" for a couple of weeks. It's
funny, I used up my "Pro" quota and the reset isn't for a couple of weeks, now.
I don't even see the reset date anymore. Oh, there it is:

> **You’ve hit your usage limit for advanced models. Please contact your admin
> to increase limits.**
>
> Your limit resets June 26, 2026. To get more access now, send a request to
> your admin.

I used up my Claude Opus quota earlier today and I have it back already now.
Honestly, I can't see the difference between the Pro and the Thinking ChatGPT
levels. It's not really like going into the "Deep research" mode which has a
whole other workflow it seems. We're just talking about the smartest model to
use through the chat UI for coding, here. So it's not as big a limitation as it
seems. But since I have Opus back, I'm going to ask it for implementation. But
before I do (I learned my lesson), I'm going to use a much lower model to help
verify whether I've included the right files for implementation, which is
most things `rg` said, plus the wand and the "usual suspect" files I almost
always include building the context payload, such as `foo_files.py` and the
output of `lsa.py` to give the overarching story.

So what do you say, Sonnet? Are we pretty well set up for making an
implementation plan request of Opus?

**Claude Sonnet 4.6 High**: Context verified. You're well-positioned for an Opus implementation request on the narrow first chisel-strike.

**What's working in your favor:**

The template for the fix already exists in the codebase. Both `pipulate/core.py` (the `_find_project_root()` method) and `pipulate/__init__.py` (the `find_project_root()` function) use `Path(__file__).resolve()` traversal to find the flake.nix marker. Opus can directly copy that pattern into `config.py`. No invention required.

Notably, `release.py` already does its own dynamic discovery (`PIPULATE_ROOT = Path(".").resolve()`) — which confirms the pattern is accepted and that `release.py` doesn't depend on `config.py`'s hardwired version anyway.

**The one gap worth flagging:**

You don't have `server.py` in context, and it almost certainly does `from config import ...` somewhere. You agreed not to touch Honeybot/Nix/trimnoir in this pass, but it's worth Opus knowing whether `WORKSPACE_ROOT` or `PIPULATE_ROOT` from `config.py` are imported by the server or other live-path callers, so the patch doesn't introduce a subtle runtime regression.

**My recommendation:** Before handing to Opus, either add a quick `rg 'WORKSPACE_ROOT|PIPULATE_ROOT' --include="*.py" -l` note to the prompt, or explicitly tell Opus the constraint is *only* `config.py` with no callers to audit. The former is safer; the latter is faster if you're comfortable the constants are mostly decorative documentation in `config.py` (they look like they are, given `release.py` ignores them).

Otherwise: the primary target (`config.py`), the pattern to copy (`core.py`'s `_find_project_root`), and the agreed-upon non-scope (Honeybot, Nix, trimnoir) are all clearly present. You're good to proceed.

**MikeLev.in**: Okay, the `rg`s:

```bash
(nix) pipulate $ rg 'WORKSPACE_ROOT|PIPULATE_ROOT' --include="*.py" -l
rg: unrecognized flag --include

similar flags that are available: --include-zero
(nix) pipulate $ rg 'WORKSPACE_ROOT|PIPULATE_ROOT' -g '*.py' -l
config.py
release.py
(nix) pipulate $ rg 'WORKSPACE_ROOT|PIPULATE_ROOT' -t py -l
config.py
release.py
(nix) pipulate $
```

Looks pretty safe. `server.py` still too big to include in this. We can circle
back to that.

**MikeLev.in**: Okay, throwing in the `trimnor/flake.nix` just for it to be there. And a
few more things from Honeybot. Got an implementation plan for me, Opus? Start
small please.

**Claude Opus 4.6 High**: Context verified.

Here's the small first chisel-strike: make `config.py` discover its own roots at runtime instead of hardwiring `/home/mike/repos`.

The pattern is already proven in `core.py` (`_find_project_root`) and `pipulate/__init__.py` (`find_project_root`) — walk up from `__file__` looking for the `flake.nix` marker. `config.py` lives at the repo root, so `Path(__file__).resolve().parent` *is* `PIPULATE_ROOT`, and its parent is `WORKSPACE_ROOT`.

Two design notes before the patch:

- **`flake.nix`-anchored discovery vs. simple parent.** Since `config.py` sits at the repo root next to `flake.nix`, a single `.parent` is sufficient and simplest. But to be robust against odd import contexts (and to match the rest of the codebase's idiom), I'll do a small upward walk for the `flake.nix` marker with a `.parent`-based fallback. That keeps it honest if `config.py` ever gets imported from a weird working directory.
- **Optional `PIPULATE_ROOT` env override.** You floated this. I recommend including it but keeping it dead simple: if `PIPULATE_ROOT` is set in the environment, honor it; otherwise discover. This is the seed of your future "named world" without committing to the full manifold. It costs three lines and gives the terminal-fearing operator (or a Claude-desktop-driven setup) one escape hatch.

Here's the patch. It replaces only the two hardwired constants in the `SYSTEM CONSTANTS` block and adds the discovery logic just above them.

```text
Target: config.py
[[[SEARCH]]]
# 📁 Directory Structure (FIXED PATHS)
WORKSPACE_ROOT = "/home/mike/repos"
PIPULATE_ROOT = "/home/mike/repos/pipulate"
AI_DISCOVERY_DIR = "pipulate/ai_discovery"
BROWSER_AUTOMATION_DIR = "pipulate/browser_cache"
LOGS_DIR = "pipulate/logs"
DATA_DIR = "pipulate/data"
[[[DIVIDER]]]
# 📁 Directory Structure (RUNTIME-DISCOVERED)
# Pipulate knows where Pipulate is, no matter where the repo was cloned.
# Resolution order: explicit PIPULATE_ROOT env override → flake.nix marker
# walk → simple parent of this file. config.py lives at the repo root, so the
# parent-of-__file__ fallback is correct even when the marker walk is skipped.
def _discover_pipulate_root() -> Path:
    env_override = os.environ.get('PIPULATE_ROOT')
    if env_override:
        return Path(env_override).resolve()
    current = Path(__file__).resolve().parent
    walker = current
    while walker != walker.parent:
        if (walker / 'flake.nix').exists():
            return walker
        walker = walker.parent
    return current

PIPULATE_ROOT = str(_discover_pipulate_root())
WORKSPACE_ROOT = str(Path(PIPULATE_ROOT).parent)
AI_DISCOVERY_DIR = "pipulate/ai_discovery"
BROWSER_AUTOMATION_DIR = "pipulate/browser_cache"
LOGS_DIR = "pipulate/logs"
DATA_DIR = "pipulate/data"
[[[REPLACE]]]
```

That's the whole strike. `os` and `Path` are both already imported at the top of `config.py`, so no import changes are needed.

What this banks as a win, and what it deliberately leaves alone:

- **Banked:** `config.py` no longer assumes the repo lives at `/home/mike/repos/pipulate`. Clone it to `~/pipulate`, `~/repos/foo`, or a Claude-desktop-chosen project root, and `PIPULATE_ROOT`/`WORKSPACE_ROOT` resolve correctly. The `PIPULATE_ROOT` env var gives the one manual override an operator or installer might need.
- **Untouched (by design):** the Honeybot `/home/mike/www/...` paths in `configuration.nix`, `db.py`, `stream.py`, `content_loader.py`, and the `post-receive` hook (metal-adjacent, machine-role paths — a separate strike); the `trimnoir`/`Pipulate.com` publishing targets in `prompt_foo.py`, `common.py`, `make_article.py` (the future "named targets" manifold); the `flake.nix` shell aliases (`gitops`, `force`, the `ai.py` path); and the fossil `# /home/mike/...` header comments (cosmetic barnacles).

One verification note for after you apply: `rg 'WORKSPACE_ROOT|PIPULATE_ROOT' -t py -l` showed only `config.py` and `release.py` as callers, and `release.py` computes its own `PIPULATE_ROOT = Path(".").resolve()` independently — it never imports `config.py`'s version. So this change has no cross-file caller to break. `server.py` wasn't in context; when you circle back, a quick `rg 'from config import.*ROOT|config\.(WORKSPACE|PIPULATE)_ROOT' -t py` will confirm nothing reads these as decorative-only.

Want the next strike after this lands to be the **workspace-targets seed** (`~/.config/pipulate/targets.toml` or a git-ignored `data/` JSON), so `trimnoir` and `Pipulate.com` become named relationships instead of magic sibling folders?

**MikeLev.in**: Okay, we apply the patch:

```diff
$ 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 'config.py'.
(nix) pipulate $ d
diff --git a/config.py b/config.py
index 386f62c5..b7abcb3d 100644
--- a/config.py
+++ b/config.py
@@ -378,9 +378,25 @@ SERVER_URL = "http://localhost:5001"
 CHAT_ENDPOINT = "/chat"
 MCP_ENDPOINT = "/mcp-tool-executor"

-# 📁 Directory Structure (FIXED PATHS)
-WORKSPACE_ROOT = "/home/mike/repos"
-PIPULATE_ROOT = "/home/mike/repos/pipulate"
+# 📁 Directory Structure (RUNTIME-DISCOVERED)
+# Pipulate knows where Pipulate is, no matter where the repo was cloned.
+# Resolution order: explicit PIPULATE_ROOT env override → flake.nix marker
+# walk → simple parent of this file. config.py lives at the repo root, so the
+# parent-of-__file__ fallback is correct even when the marker walk is skipped.
+def _discover_pipulate_root() -> Path:
+    env_override = os.environ.get('PIPULATE_ROOT')
+    if env_override:
+        return Path(env_override).resolve()
+    current = Path(__file__).resolve().parent
+    walker = current
+    while walker != walker.parent:
+        if (walker / 'flake.nix').exists():
+            return walker
+        walker = walker.parent
+    return current
+
+PIPULATE_ROOT = str(_discover_pipulate_root())
+WORKSPACE_ROOT = str(Path(PIPULATE_ROOT).parent)
 AI_DISCOVERY_DIR = "pipulate/ai_discovery"
 BROWSER_AUTOMATION_DIR = "pipulate/browser_cache"
 LOGS_DIR = "pipulate/logs"
(nix) pipulate $ m
📝 Committing: chore: Update directory discovery logic for Pipulate_ROOT
[main 30fd1a88] chore: Update directory discovery logic for Pipulate_ROOT
 1 file changed, 19 insertions(+), 3 deletions(-)
(nix) pipulate $ 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), 774 bytes | 774.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:pipulate/pipulate.git
   e49ef605..30fd1a88  main -> main
(nix) pipulate $
```

That's the whole strike, huh? Sever restarted. That's good. And to answer your
question, first one and then the other. You get to decide the order based on
highest likelihood of everything going right and having a clear testing path
along the way.

**Claude Opus 4.8 High**: Context verified.

You handed me the order, so here's the call and the reasoning behind it.

**Order: publishing targets first, Honeybot satellite last.** Your own criterion — "highest likelihood of everything going right and a clear testing path" — decides this cleanly. The publishing category already has its scaffolding (`load_targets()` reading `~/.config/articleizer/targets.json`, with `DEFAULT_TARGETS` as the fallback), and it tests *locally and instantly*: run `story` and watch the article list populate or not. The Honeybot category is metal-adjacent — every change there has to ride `nixops.sh` over to the box and then be verified by *watching a live YouTube stream restart*. Long feedback loop, live broadcast, and the NixOS config is arguably *correctly* machine-specific (it literally defines `users.users.mike`). So Honeybot is the riskiest strike with the slowest test, which makes it the right thing to do *last*, after the muscle memory of the easy ones is banked.

And within publishing, the smallest first strike is `prompt_foo.py` alone — not `common.py`, not `make_article.py` — because `prompt_foo.py` is the one whose hardwired path *actively fires* on every `story` / `latest` / `momentum` run via `CONFIG["POSTS_DIRECTORY"]`. That gives you a one-command test the instant the patch lands. `common.py` (the publishing/article-creation pipeline) is strike two; `make_article.py` you flagged as probable cruft, so it waits for the deletion-review pass.

This is a pure application of your `~/` convention insight: `Path.home() / "repos" / "trimnoir" / "_posts"` resolves to *exactly* `/home/mike/repos/trimnoir/_posts` on your box, so functionally it's a no-op on your machine — but it stops assuming the operator is named `mike`. `Path` is already imported in `prompt_foo.py`, so no import changes. Two occurrences, both in one file:

```text
Target: prompt_foo.py
[[[SEARCH]]]
DEFAULT_TARGETS = {
    "1": {
        "name": "Local Project (Default)",
        "path": "/home/mike/repos/trimnoir/_posts" # Updated default fallback
    }
}
[[[DIVIDER]]]
DEFAULT_TARGETS = {
    "1": {
        "name": "Local Project (Default)",
        "path": str(Path.home() / "repos" / "trimnoir" / "_posts")  # ~/repos convention
    }
}
[[[REPLACE]]]
```

```text
Target: prompt_foo.py
[[[SEARCH]]]
CONFIG = {
    "PROJECT_NAME": "pipulate",
    "POSTS_DIRECTORY": "/home/mike/repos/trimnoir/_posts"
}
[[[DIVIDER]]]
CONFIG = {
    "PROJECT_NAME": "pipulate",
    "POSTS_DIRECTORY": str(Path.home() / "repos" / "trimnoir" / "_posts")
}
[[[REPLACE]]]
```

**Test the moment it lands:** `story` (i.e. `python prompt_foo.py -l [:] --no-tree`). The article list should populate exactly as before — same paths, no username. If it's empty, the resolution broke; if it's full, you've banked it.

What this deliberately leaves alone: `common.py`'s `DEFAULT_TARGETS` and `base_url`/`permalink_style` (strike two, same convention, tested via `preview`/`article`); `make_article.py`'s `OUTPUT_DIR` (deferred to your cruft-deletion review); and the entire Honeybot block — `configuration.nix`, the `post-receive` hook, `db.py`, `stream.py`, `content_loader.py` — which is the final, slow-feedback strike.

Want strike two (`common.py` → `~/repos` convention for the publishing target) lined up the same way, so the whole local article pipeline is username-free before we ever SSH over to the satellite?

**MikeLev.in**: Ooh, another patch. Exciting! Let's apply it.

```diff
$ 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 'prompt_foo.py'.
(nix) pipulate $ d
diff --git a/prompt_foo.py b/prompt_foo.py
index 8b4112bb..7e0c71a4 100644
--- a/prompt_foo.py
+++ b/prompt_foo.py
@@ -49,7 +49,7 @@ TARGETS_FILE = CONFIG_DIR / "targets.json"
 DEFAULT_TARGETS = {
     "1": {
         "name": "Local Project (Default)",
-        "path": "/home/mike/repos/trimnoir/_posts" # Updated default fallback
+        "path": str(Path.home() / "repos" / "trimnoir" / "_posts")  # ~/repos convention
     }
 }
 
(nix) pipulate $ m
📝 Committing: fix: Update default target path to use ~\\repos convention
[main 3742fcdd] fix: Update default target path to use ~\\repos convention
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ cat patch | app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'prompt_foo.py'.
(nix) pipulate $ d
diff --git a/prompt_foo.py b/prompt_foo.py
index 7e0c71a4..242984f6 100644
--- a/prompt_foo.py
+++ b/prompt_foo.py
@@ -111,7 +111,7 @@ REPO_ROOT = find_repo_root(os.path.dirname(__file__))
 
 CONFIG = {
     "PROJECT_NAME": "pipulate",
-    "POSTS_DIRECTORY": "/home/mike/repos/trimnoir/_posts"
+    "POSTS_DIRECTORY": str(Path.home() / "repos" / "trimnoir" / "_posts")
 }
 
 # ============================================================================
(nix) pipulate $ m
📝 Committing: fix: Update POSTS_DIRECTORY path
[main 049090a0] fix: Update POSTS_DIRECTORY path
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ git push
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 48 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 730 bytes | 730.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 2 local objects.
To github.com:pipulate/pipulate.git
   30fd1a88..049090a0  main -> main
(nix) pipulate $
```

The changes look so modest. But it will have immediate impact on the Prompt Fu
process. Whoops, aliases like `story` are the really big index plus the existing
`foo_files.py` default inclusions plus the prompt. That is currently wayyyy too
big. Which brings up the point, I hardly know my aliases anymore. Any of the old
book decanting stuff has to go. There's new ways percolating which is less work
up-front that calcifies as static artifacts, in favor of an "always newly
extracted book, every time" with companion drill-down and on-the-fly writing by
AI. That's a rabbit hole we leap over as we trim down our default inclusion so
we can use the `story` prompt, which Gemini says tests this. I'm showing mostly
just the Prompt Fu and editing system, so it's not good for asking for next-step
implementation. But it will show that our patch landed and a lower model than
the expensive Opus can be used to check.

Isn't that right, Sonnet 4.6 High? Prove it by telling the most extensive story
you can based on what you see and the tokens you have to work with, please and
thank you! Oh, in addition to answering the basic question about whether the
patch landed and how we can see that from this.

**Claude Sonnet 4.6 High**: Context verified. Both patches are confirmed landed — I can see them directly in the `prompt_foo.py` codebase section at lines 52 and 114:

```python
"path": str(Path.home() / "repos" / "trimnoir" / "_posts")  # ~/repos convention
```
```python
"POSTS_DIRECTORY": str(Path.home() / "repos" / "trimnoir" / "_posts")
```

The `/home/mike/` strings are gone from those two locations. The `story` alias (`python prompt_foo.py -l [:] --no-tree`) would exercise exactly these constants when building `DEFAULT_TARGETS` and `CONFIG["POSTS_DIRECTORY"]`, so running it now would function identically on your machine but would also work on any other user's machine without modification — that's the proof of concept.

---

Now the story I see in these files, told as faithfully as I can from what's here.

## The Shape of the Thing

What you've built is a **hand-cranked context compiler with a deterministic patching actuator**, and this conversation is itself a demonstration of the full loop running.

`foo_files.py` is the router. It's not code that *runs* — it's a structured document disguised as a Python string (`AI_PHOOEY_CHOP`) that `prompt_foo.py` parses to know what files to include, what shell commands to execute, what URLs to fetch, and which "CHOP" variant to load. The 22 chapters commented into the header of `AI_PHOOEY_CHOP` are a table of contents for Pipulate itself, doubling as a roadmap for whoever (human or AI) reads this context.

`prompt_foo.py` is the compiler. It reads `foo_files.py`, resolves paths, executes `!` commands, fetches URLs, runs static analysis, generates UML diagrams, adds narrative context from the Jekyll blog (`_get_article_list_data`), slices article lists, pairs holographic context shards, runs a convergence loop to stabilize token counts, and emits a structured Markdown document with consistent START/END markers — the very artifact you're reading right now.

`apply.py` is the actuator. It reads a raw LLM response from stdin, finds `[[[SEARCH]]]`/`[[[DIVIDER]]]`/`[[[REPLACE]]]` blocks, performs exact-match string replacement, validates Python AST before writing, and reports success or diagnostic failure. It also handles `[[[WRITE_FILE]]]` for wholesale rewrites.

`scripts/xp.py` is the clipboard transformer. It reads the OS clipboard, parses for `[[[TODO_SLUGS]]]`, `[[[TODO_FILES]]]`, `[[[APPLY_PATCH]]]`, and `[[[TODO_PROMPT]]]` blocks, and routes them: patches go to `apply.py`, article slugs and file paths trigger a fresh `prompt_foo.py` compilation with those items loaded. It's the human's "next turn" button — paste the AI response, type `xp`, get the next context payload in the clipboard ready for the next AI session.

The four files together form a **player piano** for AI-assisted development: the human scores the music (prompt engineering, file selection, patch review), the AI plays it (generates SEARCH/REPLACE blocks, suggests TODO_SLUGS), and the deterministic actuator applies it without ambiguity.

## The Philosophical Core

The blog narrative — 131,000 tokens of it, hundreds of articles, two years of daily writing — is the substrate from which this system was extracted. The system is, in a sense, a distillation of everything those articles worked out in prose: that stateless LLMs can only act on what you give them; that "context" is a build artifact that should be compiled deliberately; that vibe-coding (letting an AI riff without deterministic guardrails) leads to regressions; that local-first + Nix + Git provides the mathematical guarantees that make the whole thing trustworthy.

The patches you just applied are a microcosm of the philosophy. The commit message says `fix: Update POSTS_DIRECTORY path` but what it actually does is make a sovereignty claim: Pipulate no longer assumes the operator is named `mike` and lives at `/home/mike/repos`. It knows where it is by walking up its own filesystem until it finds `flake.nix`. It resolves the blog root relative to the platform-standard `Path.home()`. The two-line change is load-bearing as philosophy, even if functionally it's a no-op on your machine today.

## What the Story Tells Us About Now

The article list in the Story section runs from September 2024 through June 2026 — the current date. The arc is legible. Early articles are about discovering Nix, FastHTML, HTMX, local LLMs. Mid-period articles are about building Pipulate's core loop, the "WET" philosophy (Write Everything Twice — explicit over abstracted, for AI legibility), the Ghost Driver test automation, the MCP tool-calling experiments. Late articles are about the `prompt_foo.py` / `foo_files.py` / `apply.py` / `xp.py` system reaching maturity, the Honeybot satellite telemetry server going live, the book-distillation experiments, and now — path hardwiring as the current frontier.

The `foo_files.py` Paintbox section (uncommitted/unclaimed files) at 54.9% coverage indicates the system is still partly in scaffolding-to-architecture transition. There's a lot of working machinery that hasn't been formally mapped into the narrative yet. The Roadmap section embedded in `AI_PHOOEY_CHOP` shows Phase 0 (map integrity), Phase 1 (onboarding trust), Phase 2 (control loop stability), Phase 3 (deployment matrix), Phase 4 (product power) — and you're currently in Phase 0, doing exactly what the roadmap says: cleaning up path hardwiring, auditing topological integrity, reducing cosmetic `/home/mike/` barnacles.

The conversation you just had — using Gemini for landscape analysis, ChatGPT Thinking for implementation planning, Sonnet for verification, Opus for patch generation — is itself an instance of the "swappable AI strategy" the blog advocates. The system doesn't care which model you use; it provides the context payload and receives a SEARCH/REPLACE block. The human is the routing layer.

## The One Flag Worth Raising

The alias block in `flake.nix` still has hardwired paths:

```nix
alias gitops='cd /home/mike/repos/trimnoir && git commit --allow-empty -m "retry" && git push'
alias force='cd /home/mike/repos/trimnoir && git commit --allow-empty -m "retry" && git push'
```

and:

```nix
msg=$(python /home/mike/repos/pipulate/scripts/ai.py --auto --format plain 2>/dev/null | head -1)
```

These are in the `shellHook`, which executes in the Nix shell. The `ai.py` path specifically will break for anyone not named `mike` who tries to use the `m` alias. That's the next obvious cosmetic barnacle — the `PIPULATE_ROOT` env var you just introduced in `config.py` is set in the flake's `miscSetupLogic` block (`export PIPULATE_ROOT="$(pwd)"`), which means `$PIPULATE_ROOT/scripts/ai.py` is already available as the clean replacement. That's a one-line fix when you're ready for it.

---

The patch landed cleanly. The `story` alias will confirm. The system is working exactly as designed — AI-verified, human-applied, Git-witnessed.

**MikeLev.in**: Alright, this sounds good. It's going to be a lot more like this, which
is about the most boring article I can imagine. But these hardwired paths and
all the weird variations on dealing gracefully with paths is perfect material
for the article expansion and wrap-up thing. Let's give that one to Grok Fast.

Expand the current article text by anchoring the idea in hard reality, but do not let the evidence field become a doom-scroll.

[A whole lot of hoops and obstacles for AI to jump through was here]

Grok?

🎤

**Grok Fast**: **Context verified.** The provided context (foo_files.py, prompt_foo.py, config.py patches already applied, dynamic discovery patterns from core.py/__init__.py, publishing targets, and the path audit) is sufficient for this task. No critical files appear missing for the narrow expansion request.

### Expanded Article Section: Hardwired Paths → Sovereign Address Resolution

**The Context & Hard Evidence**

Hardcoded absolute paths like `/home/mike/repos/pipulate` or `/home/mike/repos/trimnoir/_posts` were never a design flaw in the early days of Pipulate—they were pragmatic scaffolding. In a local-first system bootstrapping itself across macOS, Linux, and WSL, an absolute path delivered instant determinism when subprocesses crossed directory boundaries, Nix shells invoked Python, or post-receive hooks needed to know exactly where the web root lived. The evidence lives in the commit history and `rg '/home/mike/'` output: comments in header files, constants in `config.py`, aliases in `flake.nix`, and publishing logic in `prompt_foo.py`.

What went wrong in the long run was the assumption that the operator’s username, home layout, and project topology would remain static. Clones landed in `~/pipulate`, `~/repos/foo`, or Claude-desktop-chosen roots. Publishing targets multiplied. Satellites like Honeybot introduced machine-role boundaries. The scaffolding had quietly become architecture.

**The Walkthrough — Mental Replay**

**Old path:**  
```python
WORKSPACE_ROOT = "/home/mike/repos"
PIPULATE_ROOT = "/home/mike/repos/pipulate"
POSTS_DIRECTORY = "/home/mike/repos/trimnoir/_posts"
```

An AI or new contributor sees this and either (a) treats it as gospel and breaks on their machine, or (b) starts a find-and-replace spree that risks collateral damage in Nix configs and hooks.

**Corrective path (already landed):**  
```python
def _discover_pipulate_root() -> Path:
    env_override = os.environ.get('PIPULATE_ROOT')
    if env_override:
        return Path(env_override).resolve()
    # flake.nix marker walk (matches core.py pattern)
    ...
PIPULATE_ROOT = str(_discover_pipulate_root())
WORKSPACE_ROOT = str(Path(PIPULATE_ROOT).parent)
POSTS_DIRECTORY = str(Path.home() / "repos" / "trimnoir" / "_posts")
```

The operator now sees runtime discovery, an optional `PIPULATE_ROOT` escape hatch, and the `~/repos/` convention. The same change that removed username dependency also made the publishing target testable with `story` in one command.

**The Load-Bearing Connection**

This refactor is load-bearing for the entire “Cathedral of One” vision. Without sovereign address resolution, every new user, white-label project, or satellite deployment reintroduces the bootstrap paradox: the code that should help you set up the environment assumes the environment is already set up in one specific way. The small patches demonstrate the hinge: warnings about fragility become corollaries of repair through dynamic discovery.

**Contrast & Positive Corollary**

**Old Way:** Embed literal filesystem strings throughout config, scripts, flakes, and publishing logic.  
**Failure Pattern:** Portability collapses; new contributors hit immediate friction; automation (AI or human) risks editing the wrong target.  
**New Way:** Named resolution rules—runtime discovery from `__file__` or `flake.nix` marker, `Path.home()` conventions, optional env overrides, and (coming) a targets manifold.  
**Positive Corollary (Repair):** Recognition of the original intent (determinism) is preserved while the repair (addressability) is added. Provenance of the project root now lives in the code itself rather than tribal knowledge. A single `story` run verifies the publishing pipeline still works. The craft habit of “ask the filesystem where it is” prevents entire classes of future breakage.

**The Reality Check & Monday Morning Test**

**Without this insight:** A fresh `curl | bash` install on a colleague’s Mac lands in `~/Documents/MyProject` and immediately fails to find its blog target or run `m` alias commands. Debugging becomes “hunt the hardcoded string.”  
**With this insight, starting Monday:**  
- Run `rg '/home/' -g '*.py' -g '*.nix'` as a hygiene check after any major refactor.  
- Add new publishing targets via `~/.config/articleizer/targets.json` (or the seeded manifold) instead of editing constants.  
- Use `PIPULATE_ROOT` env var in scripts that need an override.  
- Document every remaining absolute path with its role (cosmetic, metal-adjacent, or transitional) in `foo_files.py`.

**The Forward Vector**

The next natural development moves from single-repo discovery to the workspace manifold: nicknames and roles for multiple blogs, satellites, and agent folders. `pipulate://site/journal/posts`, `pipulate://satellite/honeybot/webroot`. The pattern is already seeded in `config.py` discovery and `prompt_foo.py` targets. Each subsequent chisel-strike gets easier because the keel is now in place.

**Balanced Wrap-Up**

Truth in software needs tooling. Hardwired paths cracked under the weight of real usage; dynamic resolution and named relationships repaired the foundation. The record (git history, dynamic discovery functions, `story` verification) survived and now guides the next iteration. What becomes less scary is onboarding or white-labeling. What becomes more exciting is confidently spinning up sovereign instances that still interoperate with your personal publishing and telemetry satellites.

**Sign-off charm**  
What genuinely surprised me was how much mileage we got from copying a pattern that already existed in `core.py`—the system was teaching us the repair before we fully articulated the problem. I expect the workspace manifold to feel similarly obvious in hindsight once the first targets.toml lands. Net-new insight: the `~/repos/` convention isn’t just portability; it’s a gentle social engineering nudge toward sane project organization that works even for terminal-fearing users who arrive via Claude Desktop. The machine meets them where they are, then quietly raises the bar. Hi-Ya. 

## Cultivating Craft Habits for the Age of AI

This expanded section is ready for inclusion in the next article. If you want the next chisel-strike (e.g., `common.py` + flake.nix aliases, or seeding the targets manifold), provide the TODO_FILES surface and I’ll generate the precise SEARCH/REPLACE.

> **MikeLev.in**: Yeah, this is an article right here. I'm gonna get rid of
> `~/.config/articleizer/` and move all that stuff to `~/.config/pipulate/`
> where it belongs, but that will be on another article. That's a chisel-strike.
> We wrap this article up. We think how we need to get the "forest" story of
> slideshows interspersed with the article reading as station identification,
> fine-tuned. It's really terrible right now. It proves the tech but has the
> messaging all scrambled up. But this article is long enough, and I've got
> these thoughts in what will become the "last" article by the time I'm using my
> `latest` alias, which will be real soon because this is where we article-ize
> it. Though the word that sounds like a Greek Pantheon is growing on me:
> Articleize!


---

## Book Analysis

### Ai Editorial Take
What's fascinating here is how the transition from hardcoded to dynamic paths mirrors the biological shift from fixed-wired nervous systems to plastic, adaptive neural networks. By decoupling coordinates from the code, the environment stops acting like a fragile machine that must be carefully placed, and starts operating as a self-locating spatial entity. It establishes a 'coordinate-free' geometry of software execution that is essential for autonomous agent runtimes.

### 🐦 X.com Promo Tweet
```text
Still hardcoding absolute paths in your local AI tools? Stop! Learn how to transition from fragile system-specific strings to dynamic discovery and clean home-directory conventions for absolute environment portability: https://mikelev.in/futureproof/slaying-hardcoded-paths-portable-ai-dev/ #LocalFirst #AIWorkflows #DevOps
```

### Title Brainstorm
* **Title Option:** Slaying Hardcoded Paths for Portable AI Dev Environments
  * **Filename:** `slaying-hardcoded-paths-portable-ai-dev.md`
  * **Rationale:** Directly addresses the main action of the article while focusing on the high-value outcome of portability in modern AI workflows.
* **Title Option:** Autonomous Path Resolution: Designing for Out-of-Bounds Resilience
  * **Filename:** `autonomous-path-resolution-out-of-bounds-resilience.md`
  * **Rationale:** Highlights the theoretical design pattern of separating runtime execution from fixed user paths.
* **Title Option:** Why Your Local-First Setup Needs Dynamic Directory Discovery
  * **Filename:** `local-first-setup-dynamic-directory-discovery.md`
  * **Rationale:** Focuses on the practical problem-solution dynamic that appeals to developers setting up local AI runtimes.

### Content Potential And Polish
- **Core Strengths:**
  - Clear taxonomy of different types of path problems (cosmetic, core local, associated projects, and satellite boundaries).
  - Leverages existing successful patterns in the codebase rather than inventing overcomplicated resolution systems.
  - Articulates the psychological value of predictability and survivability in development environments.
- **Suggestions For Polish:**
  - Clarify the migration of settings from ~/.config/articleizer/ to ~/.config/pipulate/ as hinted in the concluding dialogue.
  - Further expand the explanation of why absolute paths are comfortable initially, to build better empathy with less-technical users.

### Next Step Prompts
- Draft the implementation plan for migrating user configuration targets from the old articleizer namespace to the unified ~/.config/pipulate/ directory structure, ensuring backwards compatibility.
- Write an automated cleanup script to identify and report remaining cosmetic /home/mike/ header comments across the Python files listed in the file audit.
