Taking It from the Top: Designing a Frictionless Command-Line Workshop
Setting the Stage: Context for the Curious Book Reader
As we navigate an era defined by overwhelming cloud abstractions and fleeting tech stacks, building resilient workflows requires a return to basics. This chapter explores an interesting approach to bootstrapping local environments through clean terminal design and simple Unix primitives, providing a blueprint for sustainable software craftsmanship in the Age of AI.
TL;DR: Pipulateโs nix develop ends at a two-door prompt: start the app, or drop to a bare shell. Door 2 prints a short list of words a newcomer can type. That list printed exactly once and then scrolled away, and typing menu to get it back returned command not found. This ride added a menu recall command: a --recall flag on scripts/boot_menu.py that prints the same list from the same data structure, and a menu shell function in flake.nix that calls it. The word was also added as a row in the list itself, so the list announces its own recall command and the spelled-out count (โFive words to start fromโ) derives from the data rather than being typed by hand. A rejected first draft would have made menu re-run the door picker and start a server on any non-tty invocation; the shipped version never actuates anything.
Technical Journal Entry Begins
๐ Verified Pipulate Commits:
MikeLev.in: Okay, weโre getting there. We are very close. And I need to make this story as simple as possible. Eventually itโs going to start with what I hope can be:
curl https://npvg.org | bash
Thatโs the pattern Iโd love as it has the least to explain and is the easiest to remember and I have this domain already registered but only parked and I can use it dedicated for this purpose. I think it may more realistically be:
curl -fsSL https://npvg.org | bash
โฆbut thatโs significantly harder to remember and talk people through though I accept the reality that may have to be the case. Give me a refresher why.
The Power of Simple Installation Patterns
Now after that we want to encourage the occasional complete deleting of the pipulate (or whatever whitelabel name you gave it) folder and the complete reconstruction like this:
(nix:nix-shell-env) (nix) pipulate $ exit
exit
Saving session...
...saving history...truncating history files...
...completed.
michaellevin@MichaelMacBook-Pro ~ % clear
michaellevin@MichaelMacBook-Pro ~ % rm -rf pipulate
michaellevin@MichaelMacBook-Pro ~ % curl -fsSL https://pipulate.com/install.sh | bash
--------------------------------------------------------------
๐ Welcome to the Pipulate Installer ๐
Local-first, Nix-reproducible, and yours to delete.
--------------------------------------------------------------
๐ Checking prerequisites...
โ
All required tools found.
๐ Checking target directory: /Users/michaellevin/pipulate
โ
Target directory is available.
๐ Creating directory '/Users/michaellevin/pipulate'
๐ฅ Downloading Pipulate source code...
#-#O=-# #
โ
Download complete.
๐ฆ Extracting source code...
โ
Extraction complete. Source code installed to '/Users/michaellevin/pipulate'.
๐ Now in directory: /Users/michaellevin/pipulate
๐ Fetching the shared deploy key from https://pipulate.com/key.rot...
(Public, ROT13-encoded, pull-only: it exists so this folder can fetch
updates without a GitHub account. nix develop decodes it into
~/.ssh/id_rsa only if no key is there already.)
โ
Deploy key downloaded.
๐ Deploy key saved as .ssh/rot (mode 600).
๐ Starting the Pipulate environment...
--------------------------------------------------------------
Source is in place at: /Users/michaellevin/pipulate
To come back later, run:
cd /Users/michaellevin/pipulate && nix develop -L
--------------------------------------------------------------
Setting up app identity as 'pipulate'...
โ
Application identity set.
Creating ./run -- a one-file shortcut for the cd-and-nix-develop line above.
Next, nix develop builds the environment and turns this folder into a
git repository (the 'magic cookie' step) so it can auto-update from now on.
๐ Booting the Forever Machine...
Please wait while the Nix environment hydrates...
Restored session: Mon Aug 31 19:28:59 EDT 2026
๐ Transforming installation into git repository...
Creating temporary clone in /tmp/nix-shell.d0Nekw/tmp.OpvFkPgVgC...
Cloning into '/tmp/nix-shell.d0Nekw/tmp.OpvFkPgVgC'...
remote: Enumerating objects: 374, done.
remote: Counting objects: 100% (374/374), done.
remote: Compressing objects: 100% (340/340), done.
remote: Total 374 (delta 23), reused 198 (delta 9), pack-reused 0 (from 0)
Receiving objects: 100% (374/374), 3.13 MiB | 17.42 MiB/s, done.
Resolving deltas: 100% (23/23), done.
Preserving app identity and credentials...
Creating backup of current directory in /tmp/nix-shell.d0Nekw/tmp.QYk6LJskDx...
Moving git repository into place...
โ
Successfully transformed into git repository!
Original files backed up to: /tmp/nix-shell.d0Nekw/tmp.QYk6LJskDx
Checking for updates...
Temporarily stashing local JupyterLab settings...
From https://github.com/pipulate/pipulate
* branch main -> FETCH_HEAD
Already up to date.
Found ~/.ssh/id_rsa -- switching the git remote to SSH so auto-updates use it.
INFO: Setting up your personal Playground...
(Determinate Nix 3.19.1) 2.34.6 ยท Python 3.12.13 ยท v2.49 ยท ~/pipulate
๐ง Fresh install detected โ packages downloading...
โ
291 packages ready.
๐ 12 starter file(s) copied into Notebooks/ -- yours to edit.
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ nix develop -- a reproducible *nix shell :: pick a door โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ
โ [1] Start JupyterLab plus the app server and browser tabs โ
โ [2] Just the shell nothing starts -- four words wait at the prompt โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ waiting for your choice -- Ctrl+C also drops to the shell โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
[2] Just the shell. Nothing started -- no JupyterLab, no server.
Four words to start from:
walk take the guided tour -- public pages, nothing to log into
sources see what this shell can reach outside this machine
brief compile this workshop into your clipboard for an AI
pu change your mind and start the app server after all
(nix:nix-shell-env) (nix) pipulate $ menu
bash: menu: command not found
(nix:nix-shell-env) (nix) pipulate $
Iโd be interested in making that even more quiet per the Unix philosophy but I think the most important thing is for us to implement that menu command so people can get back that initial menu that shows when you hit โ2โ to exit out to the shell. People will do some experimenting and then ask themselves what those menu choices were again and early on we should explicitly tell them (make it talk out loud?) to type โmenuโ to see these selections again.
We are creating a set of moves that open the door to the Unix way with small composable commands and the Unix pipe stuff; the perfect tools for building AI context especially in regard to reproducing whatever API-calls including MCP and other tool-calls it might be doing in the background and telling you all these confident pretty lies about and that creates the โhow do I check thisโ itch.
Thatโs the itch weโre scratching by using Nix to pin a โwrite once run anywhereโ environment in which all absolute paths across potentially millions of localhost deployments are identical; the key to modern WORA and this changes everything setting the stage for an effective pendulum swinging back to local on the individual per-personal machine level the way un-clouding, declouding or cloud repatriation (whatever you want to call it). When you de-cloud a thing it gets less cloudy and therefore clearer and more transparent so you can make better decisions โ and thatโs it exactly.
Bridging the Amiga Spirit with Modern Nix
Thereโs a whole discussion here about learned helplessness and the struggle for generations of kids who only ever grew up with the cloud and never knew a DOS prompt or an AmigaDOS shell or the TRS-80โs BASIC READY. I have had the privilege of knowing all three, and the Amiga one quite intimately and have been waiting for something like it for years and finally cobbled one together with quite a few regressions and losses like no ARexx but then also gains because what a true WORA โ true for all intents and purposes you pedantic machine and certainly better than Java ever achieved โ assumptions the world has made for decades have toppled and nobody knows it. For most non-enterprise cases where docker is used today, Nix is probably a better fit and that realization is going to take a decade or more to play out and only after some high profile use-cases that undeniably and resonantly and persistently and tenaciously demonstrate that fact to the point where an actual open minded individual has a fair chance to think it through or not think it through and have both a low-friction path and all their circle of personal influencers telling them they should.
I donโt expect that to happen anytime soon but itโs an inevitability if you look
at the way the GNU Project has embraced the Nix philosophy with the sincerest
form of flattery in the form of more pure Guix. And notice what they didnโt
embrace: docker; and not even the more open (I think) Ubuntu LXC/LXD. While
virtualization became an official part of Linux with the KVM but if you want
containers you have to spin up a VM with the KVM and the VM can support the
containerโฆ HA HA HA! And chroot was there the whole time as well. But
hardware you can treat like a Lambda function? Now thatโs GNU-worthy and thatโs
going to take a long, long time to settle in.
Basically there has to be a killer app or two demonstrating what having achieved โwrite once run anywhereโ in the most functional and flexible way โ providing a generic normalized Linux where every machine shares the same absolute file-paths, shell environment variables and so on such that you really can have the same code run on any Windows WSL, macOS or other Linux system, see?
Well anyway I plan on Pipulate and this much quieter almost invisible to the point of not being there โstick bugโ mode that can:
- Walk you from URL to URL forcing you to have some browser-based experience that, in those cases where itโs necessary can spare you from all this copy-paste transposing of values from those websites from one site to the next being carried forward accumulating context to compile. But also it can just be a URL-walk with no bells and whistles.
- Has a Flight Deck Recorder (FDR) so that everything that goes over the wire in such a URL-walk gets recorded per the Google Chrome DevTools Protocol (CDP) all the data in the DevTools โNetwork Tabโ once the page settles. The upshot of this is that anything a browser can do API-call-wise, you can reproduce technically down to authenticated token session hijacking if you really wanted to, but seeing whatโs up with APIs via GraphQL and the like is the sweet spot.
- Provides a way to hit pre-set-up APIs and MCP servers in the most simple way possible where the most common over-and-over use-cases, normally select-only applications for pulling the contents of email and slack threads, Jira tickets and Confluence pages and a whole host of other stuff the modern context-compiling AI-user might have to do constantly, I mean really non-stop as like their main job skill moving forward, might need. Thatโs another one of those itches weโre scratching here.
Implementing Flight Recorders and URL Walks
That last one is really the aforementioned Unix philosophy or Unix way that weโre going to keep finding a label that will stick. Maybe thatโs what Pipulating is in the end as much as Iโm trying to make the word โPipulateโ disappear everywhere as part of osb (operation stick bug), it will eventually decloak.
SoโHaโ
Thoughts?
Gemini 3.7 Flash Extended: Context verified.
Why curl -fsSL Is Load-Bearing Over Bare curl
Piping a bare curl https://domain | bash directly into an interpreter has four distinct failure modes that the flags neutralize:
-f(--fail): Fails silently on HTTP errors ($4\text{xx}$ / $5\text{xx}$). Without-f, if the web server or CDN returns a404 Not Found,502 Bad Gateway, or a Cloudflare challenge page,curlhappily emits the HTML error payload into stdout.bashthen attempts to execute<!DOCTYPE html><html>...as shell code, spraying syntax errors across the terminal.-s(--silent): Disables the progress meter and transfer statistics (#-#O=-#), keeping stdout completely clean so transfer telemetry is never confused for script output.-S(--show-error): Used in conjunction with-s. If the network connection fails, DNS resolution misses, or SSL negotiation drops,curloutputs the exact transport error message tostderrrather than failing as an unexplained silent no-op.-L(--location): Follows HTTP $301$ / $302$ redirects. If the apex domainnpvg.orgredirects to[https://www.npvg.org](https://www.npvg.org)(or HTTP upgrades to HTTPS), barecurldownloads the zero-byte redirect header/stub and exits, piping nothing tobash.-Lensurescurlwalks the redirect chain to the payload.
The Return of WORA: Nix, Local-First, and the Single File-Path Reality
The shift from โthe machine is fragile, put it in the cloudโ to โthe cloud is opaque, normalize the machineโ hinges on one distinction: Docker virtualizes the operating system userland inside a guest kernel/daemon; Nix standardizes dependency closures down to exact cryptographic hashes across any POSIX environment.
- The Docker Tax: On macOS and Windows, Docker requires spinning up a hidden Linux virtual machine (Hyper-Kit, WSL2 backend, or QEMU), managing bridge networking, file-system translation layers (virtiofs), daemon lifecycle, and multiterabyte image caches.
- The Nix Invariant: Nix guarantees that whether on Linux, macOS (Darwin), or Windows (WSL2),
/nix/store/...derivations and paths remain identical. Tools compile against exact shared libraries without container isolation overhead. - The Disposable Workshop: Being able to run
rm -rf pipulate && curl -fsSL [https://pipulate.com/install.sh](https://pipulate.com/install.sh) | bashand restore the exact working runtime within seconds proves that state is decoupled from substrate. The repo is merely a disposable chassis; the persistent configuration lives safely in~/.config/pipulate/andbrowser_cache/.
Actionable Next Steps
(1) PROBES
python3 -c 'import sys; from pathlib import Path; sys.path.insert(0, "scripts"); import boot_menu; print("boot_menu importable:", hasattr(boot_menu, "main"))'
Proves that scripts/boot_menu.py is present, valid Python, and exposes its main() entrypoint cleanly.
(2) NEXT CONTEXT
! python3 -c 'import sys; from pathlib import Path; sys.path.insert(0, "scripts"); import boot_menu; print("boot_menu importable:", hasattr(boot_menu, "main"))'
! nix develop .#quiet --command bash -c 'type menu'
flake.nix
scripts/boot_menu.py
scripts/sources_menu.py
(3) PATCHES
Target: flake.nix
[[[SEARCH]]]
tools() {
if [ "$#" -eq 0 ]; then
(cd "$PIPULATE_ROOT" && .venv/bin/python cli.py mcp-discover)
else
(cd "$PIPULATE_ROOT" && .venv/bin/python cli.py call "$@")
fi
}
# THE THIRD DOOR: `pu` starts the server door 2 declined to start.
[[[DIVIDER]]]
tools() {
if [ "$#" -eq 0 ]; then
(cd "$PIPULATE_ROOT" && .venv/bin/python cli.py mcp-discover)
else
(cd "$PIPULATE_ROOT" && .venv/bin/python cli.py call "$@")
fi
}
menu() {
if [ -f "$PIPULATE_ROOT/scripts/boot_menu.py" ]; then
"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/boot_menu.py"
local choice=$?
if [ "$choice" -eq 0 ]; then
pu
fi
else
echo "menu: scripts/boot_menu.py has not landed in this checkout."
fi
}
# THE THIRD DOOR: `pu` starts the server door 2 declined to start.
[[[REPLACE]]]
Ignition required: exit then re-enter nix develop (or source the shell environment) to bind menu in the interactive shell.
(4) PROMPT
Verify that `menu` is accessible in the interactive shell. Run `menu`, verify the two-door picker displays properly, select [2] to return to the prompt, and confirm the four-word starting list displays.
(5) EXTERNAL DELIVERABLES
No external deliverables required.
MikeLev.in: Wow a 5-Car Train with a patch right out of the starting gate. I donโt know about that, but I do like to start with Gemini because of what a clear communicator it has become since these model versions I think 3.5 and over when it became very brief and terse in its responses but also a โjust rightโ amount of detail that Anthropic Claude has pretty out of whack with it talking like a PhD all the time โ which I do sometimes appreciate and think I have a tendency to do myself all the time (the pseudo erudition of the greatly magnified Woggle Bug from Oz) but clarity is nice too especially at the outset.
But not for code implementation. For that we go to the great Claude. What say you oh long-winded Magnum Opus one? Or make that Opus 5? Shall I take Geminiโs patch as-is?
1: Probe:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ python3 -c 'import sys; from pathlib import Path; sys.path.insert(0, "scripts"); import boot_menu; print("boot_menu importable:", hasattr(boot_menu, "main"))'
boot_menu importable: True
(nix) pipulate $
2: Context:
# adhoc.txt _ _ _ to set context____ _ _ ___ ____ _ <F5> Simpson Couch Gag Here (explain anything to the audience you feel needs it explained)
# / \ __| | | | | | ___ ___ / ___| | | |/ _ \| _ \| |
# ahe/ _ \ / _` | | |_| |/ _ \ / __| | | | |_| | | | | |_) | | Taking it from the top.
# ahc ___ \ (_| | | _ | (_) | (__ | |___| _ | |_| | __/|_|
# /_/ \_\__,_| |_| |_|\___/ \___| \____|_| |_|\___/|_| (_)
# Ad Hoc CHOP: The Not-Managed-by-Git Safe-for-Client-Data place
# THE ROLLING PIN BOOK ORE SPINE / FODDER FOR THE BOOK
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs # <-- The "Rolling Pin" that gives the 40K foot book-spine view of book-ore.
# GLOSSARY.md # <-- I think this glossary goes well with the book-ore spine to do world building.
# scripts/articles/lsa.py # <-- Useful for refining commands like `posts`, critical to Second Brain concept.
# THE QUIRKY AMIGA-LOVING HUMAN
# ~/repos/nixos/autognome.py # <-- Letting the AIs really understand my environment (The Brave Little Tailor punches above Their Weight Class proving the dunning-kruger effect the gate-keeper's (lower-case) lament.)
# init.lua # <-- Daily driver hot-keys that overlap with aliases in flake.nix
# AGENTIC FRAMEWORK & FOREVER MACHINE BIG FILES
prompt_foo.py # <-- Prompt Fu compiler, makes the very README for AGENTS-like payload you're reading right now, but it needs to be more like that
# foo_files.py # <-- This is the router, evolving book outline and the things you pin-up to produced the recursive self-improvement loops
flake.nix # <-- Solves world's WRITE ONCE RUN ANYWHERE problem like Java never could. Also resolves the bootstrap paradox.
# MAIN ACTUATORS, IaC & NEGATIVE SPACE
apply.py # <-- How can "Web UI" ChatBots edit your code? With this Aider-inspired Player Piano patch applier.
.gitattributes # <-- Model: understand that `nbstripout` and `jupytext` are both in play. Just talk the human through .ipynb patches.
.gitignore # <-- Creates "negative space" for sub-rep's to share parent environment and "snap" proprietary secret features into place.
requirements.in # <-- All known dependencies and (necessary) version pinning. WORA gotcha's exposed.
__init__.py # <-- Master versioning
pyproject.toml # <-- The PyPI Packaging details
# cli.py # <-- Catch-all actuator for PyPI envs, Python anchoring, MCP tool-call (plus alternatives) and **kwargs like wrapping for CLI
# scripts/xp.py # <-- Transforms host OS copy-paste buffer player-piano music into context-payload.
# scripts/ai.py # <-- How I constantly use local AI to write git commit messages with `m` alias.
# CONTEXT PORTABILITY SYSTEM
# scripts/foo_cartridge.py # Needs description
# scripts/foo_replay.py # Needs description
# FREQUENTLY USEFUL TO HAVE IN CONTEXT
# release.py # <-- How everything ends up where it does (GitHub, PyPI, etc.)
# scripts/weblogin.py # <-- Lets the user "warm up" the cache for their web logins at their leisure on a profile that persists.
# scripts/crawl.py # <-- Feel free to ask for something to be crawled and included in the next turn.
# imports/voice_synthesis.py # <-- The wand can talk to you
# scripts/release/version_sync.py # <-- Needs to be wrapped into release.py and eliminated, I think.
# --- Under this line is were you paste what the AI gives you ---
# --- We call it context but it's really just the right-hand ---
# --- blast-radius of the "probes" to make this all science. ---
# --- END `adhoc.txt` TEMPLATE ---
# STICKBUG & MOTHER CAT KATA
# assets/trails/botify_pageworkers.yaml
assets/installer/mck.sh
assets/installer/replay.sh
assets/trails/first_context.yaml
assets/trails/practice.yaml
assets/trails/public_walk.yaml
scripts/bookmark_import.py
scripts/boot_menu.py
scripts/connectors/README.md
scripts/connectors/botify.py
scripts/connectors/confluence.py
scripts/connectors/gmail.py
scripts/connectors/gsc.py
scripts/connectors/jira.py
scripts/connectors/mcp.py
scripts/connectors/mcp_warm.py
scripts/connectors/sheets.py
scripts/connectors/slack.py
scripts/connectors/wallet.py
scripts/mother_cat.py
scripts/sources_menu.py
scripts/walk.py
scripts/walk_cartridge.py
scripts/walk_compile.py
scripts/weblogin.py
tools/scraper_tools.py
# # # adhoc.txt -- Cleanup inert public_walk environment export block
# #
# # # --- BEFORE/AFTER STRADDLE ---
# # ! grep -n -C 2 'PIPULATE_TRAIL_WALK_ONE_URL' assets/installer/mck.sh || echo "export_block_removed"
# # ! bash -n assets/installer/mck.sh; echo "mck_syntax=$?"
# # ! bash assets/installer/mck.sh --where
# #
# # # --- TARGET SCRIPT ---
# # assets/installer/mck.sh
# #
# # ! rg -n -F 'TRAIL_NAME="${TRAIL_NAME:-public_walk}"' assets/installer/mck.sh
# # ! rg -n -e '^ *walk\(\)' -e '^ *alias walk=' flake.nix; echo "shell_walk_exit=$?"
# # ! test -e walk; echo "root_walk_exists=$?"
# # ! rg -n -e 'alias mothercat=' -e 'Three words to start from' flake.nix scripts/boot_menu.py
# # flake.nix
# # scripts/boot_menu.py
# # assets/installer/mck.sh
#
# # ! bash -n walk; echo "walk_syntax=$?"
# # ! bash walk --where
# # ! rg -n -e '^ *alias walk=' -e '^ *walk\(\)' -e 'writeShellScriptBin "walk"' flake.nix; echo "shell_walk_exit=$?"
# # ! .venv/bin/python -c 'import sys; sys.path.insert(0,"scripts"); import boot_menu as b; print("count=" + b._count_word(len(b.DOOR_TWO_WORDS))); print(" ".join(w for w, _ in b.DOOR_TWO_WORDS))'
# # walk
# # flake.nix
# # scripts/boot_menu.py
# # assets/installer/mck.sh
#
# # ! rg -in "three words|four words|door 2" flake.nix scripts/ assets/ README.md
# # ! walk --where
# # walk
# # flake.nix
# # scripts/boot_menu.py
# # assets/installer/mck.sh
#
# # ! python scripts/connectors/wallet.py check slack
# # ! python -c "import os, sys; from pathlib import Path; sys.path.insert(0, '.'); from scripts.connectors import wallet; pairs = wallet._dotenv_pairs(); print('env SLACK_USER_TOKEN:', bool(os.getenv('SLACK_USER_TOKEN')), 'env SLACK_BOT_TOKEN:', bool(os.getenv('SLACK_BOT_TOKEN')), 'vault SLACK_USER_TOKEN:', 'SLACK_USER_TOKEN' in pairs, 'vault SLACK_BOT_TOKEN:', 'SLACK_BOT_TOKEN' in pairs)"
# # ! python scripts/connectors/wallet.py warm slack --dry-run
#
# /home/mike/repos/trimnoir/_posts/2026-08-30-terminal-native-mcp-practice.md
# deleteme.txt
# Now that slack is working (on my workspace) we trim back things here to just the stick bug and Mother Cat Kata stuff
assets/installer/install.sh # <-- Pipulate.com installer real home in github/pipulate repo
~/repos/Pipulate.com/CNAME
~/repos/Pipulate.com/_config.yml
~/repos/Pipulate.com/_layouts/default.html
~/repos/nixos/.gitignore
~/repos/Pipulate.com/install.md # <-- Gets copied into place here by pipulate/release.py
! python3 -c 'import sys; from pathlib import Path; sys.path.insert(0, "scripts"); import boot_menu; print("boot_menu importable:", hasattr(boot_menu, "main"))'
! nix develop .#quiet --command bash -c 'type menu'
flake.nix
scripts/boot_menu.py
scripts/sources_menu.py
3: Patches: Withholding until I hear Opus weigh in. Opus?
๐ค
Opus 5 Extra: Context verified.
Verdict: donโt take it as-is. Three defects, one of them a live hazard.
Geminiโs shape is right โ a menu function in miscSetupLogic, mirroring sources. The body is wrong.
1. The exit-0 branch is a server-starting trap. boot_menu.py fails open: no tty, PIPULATE_BOOT_MENU=0, or any unexpected exception all return EXIT_START (0). That polarity is correct at the threshold, where blocking would strand an unattended nix develop. At a prompt it inverts. Geminiโs function reads $? and calls pu on 0 โ so any non-tty invocation starts a server. Including inside prompt_fooโs ! executor, where stdout is a pipe: pu would pkill the running server, start a new one in the foreground, and hold the compile until the 180-second process-group kill. A word weโre about to tell newcomers to type freely must not be able to do that.
2. Door 1 already has a word: pu. A menu that can start the app is a second spelling for pu โ the sibling-.md failure your own comments keep convicting. What the human actually forgot is the word list, not the picker. So menu should be a pure display: never blocks, never actuates, safe in every lane.
3. Nothing tells anyone the word exists. Same-car label rule. The fix is free because you already built the mechanism: add menu as a fifth row of DOOR_TWO_WORDS and both the panel count and the list update themselves. The comment above that tuple literally says โthe next word costs one line and cannot lie.โ
The probe was also broken โ twice
! nix develop .#quiet --command bash -c 'type menu' is in this compileโs live receipts and it died at the loader: libssl.so.3: version OPENSSL_3.2.0 not found. Thatโs the unexported-shim failure your nix() function exists to prevent โ the shim is a shell function, so the ! child inherits the polluted LD_LIBRARY_PATH and not the protection. install.sh already carries LD_LIBRARY_PATH="" with a comment about exactly this.
And even fixed, that probe canโt work: nix develop --command bash -c 'โฆ' execs a fresh bash, and shell functions donโt export. It would print โnot foundโ in both worlds forever. Your own flake comment already banked the remedy for this class โ the Notebooks/Shared block says โthe straddle reads the GENERATED HOOK TEXT instead (nix eval on devShells.<sys>.quiet).โ
Render-gap warning for this payload
Completely empty lines were stripped from the copy I received; whitespace-only lines survived (visible in prompt_foo.pyโs Logger.print). So every SEARCH block below is anchored on contiguous non-blank lines only, several on a single line. If you see me quoting less context than usual, thatโs why.
On curl โฆ | bash
Geminiโs ranking is slightly off. Only two flags are correctness:
-fโ without it, a 404/502/Cloudflare page is HTML piped intobash.-Lโ without it, any redirect (apexโwww, httpโhttps) yields an empty body andbashexits 0 having done nothing. That silent-success failure is the worst one.-s/-Sare cosmetic. curl disables the progress meter only when writing to a terminal; piping tobashmeans it does print, to stderr. Ugly, not dangerous.
So the honest minimum is curl -fL https://npvg.org | bash โ โfail, follow,โ two letters, easy to say aloud. Bare curl https://npvg.org | bash can work if the apex serves 200 with the script body and never redirects, but the day you add a www canonical or a CDN it degrades to silence. Iโd still print -fsSL as canonical, because itโs what Homebrew, rustup and the Determinate installer all use and people have muscle memory for it โ and teach -fL as the sayable one.
Earmark, not this car: install.sh runs top-to-bottom, so a truncated download executes a partial script. The standard cure is wrapping the body in main() { โฆ } and calling main "$@" on the last line, so a truncation can only ever fail to define main. Worth doing before npvg.org goes live.
On โeven quieterโ: your transcriptโs noise is nearly all first-install-only (git transformation, package count, starter files). Steady-state re-entry is already two lines plus the panel. Iโd leave it. And no TTS on door 2 โ the whole promise of that door is that nothing happens.
(1) PROBES
PIPULATE_BOOT_MENU=0 .venv/bin/python scripts/boot_menu.py --recall; echo "recall_exit=$?"
LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook 2>/dev/null | grep -c 'menu()' || true
rg -n -i -e 'four words' -e 'three words' . 2>/dev/null | head -20 || true
Probe 1 is the substance. BEFORE: the env gate returns immediately, no output, no block even at a real tty. AFTER: the five-word list, exit 0. Clean discrimination in both lanes.
Probe 2 reads the generated hook text, the only thing that can witness a shell function. LD_LIBRARY_PATH="" is load-bearing per this compileโs own failed receipt. BEFORE 0, AFTER 1.
Probe 3 is the falsifier for my claim that nothing else hardcodes the count. README.md and AGENTS.md arenโt in this context; if either says โfour words,โ this finds it.
type menu is deliberately not a probe: shell functions are invisible to the ! executor, so it would print the same answer in both worlds. Itโs the humanโs eyeball check after ignition โ the same category as the sniff compspec, where โthe only honest witness is a human pressing Tab.โ
(2) NEXT CONTEXT
! PIPULATE_BOOT_MENU=0 .venv/bin/python scripts/boot_menu.py --recall; echo "recall_exit=$?"
! LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook 2>/dev/null | grep -c 'menu()' || true
! rg -n -i -e 'four words' -e 'three words' . 2>/dev/null | head -20 || true
flake.nix
scripts/boot_menu.py
scripts/sources_menu.py
assets/installer/install.sh
(3) PATCHES
Target: scripts/boot_menu.py
[[[SEARCH]]]
[2] Just the shell NOTHING starts; `walk`, `sources`, `brief`, `pu` wait at the prompt
[[[DIVIDER]]]
[2] Just the shell NOTHING starts; `walk`, `sources`, `brief`, `pu`, `menu` wait at the prompt
[[[REPLACE]]]
Target: scripts/boot_menu.py
[[[SEARCH]]]
# elsewhere (brief) -> reverse the choice you just made (pu). `walk` leads
# because it is the only row that asks nothing of you first.
[[[DIVIDER]]]
# elsewhere (brief) -> reverse the choice you just made (pu). `walk` leads
# because it is the only row that asks nothing of you first. `menu` rides
# LAST and is not a rung on that ladder at all: it is not a place to go, it
# is how you get this list back after it has scrolled away. Its row is also
# the only thing that tells a newcomer the word exists -- a recall command
# nothing announces is a command nobody types.
[[[REPLACE]]]
Target: scripts/boot_menu.py
[[[SEARCH]]]
("brief", "compile this workshop into your clipboard for an AI"),
("pu", "change your mind and start the app server after all"),
)
[[[DIVIDER]]]
("brief", "compile this workshop into your clipboard for an AI"),
("pu", "change your mind and start the app server after all"),
("menu", "print this list again once it scrolls away"),
)
[[[REPLACE]]]
Target: scripts/boot_menu.py
[[[SEARCH]]]
return _COUNT_WORDS[n] if 0 <= n < len(_COUNT_WORDS) else str(n)
[[[DIVIDER]]]
return _COUNT_WORDS[n] if 0 <= n < len(_COUNT_WORDS) else str(n)
def print_door_two_words(name):
"""Print the door-two list. ONE implementation, TWO call sites.
main() prints it at the moment of choice; `menu` prints it again later
through --recall. Both read the same tuple, so the reminder cannot drift
from the thing it reminds you of -- which is the whole reason
DOOR_TWO_WORDS is data rather than two hand-written strings.
"""
print(_count_word(len(DOOR_TWO_WORDS)).capitalize() + " words to start from:")
width = max(len(word) for word, _ in DOOR_TWO_WORDS)
for word, description in DOOR_TWO_WORDS:
print(" " + word.ljust(width) + " " + description.format(name=name))
[[[REPLACE]]]
Target: scripts/boot_menu.py
[[[SEARCH]]]
def main() -> int:
[[[DIVIDER]]]
def main() -> int:
# THE RECALL PATH, AND WHY IT SITS ABOVE EVERY GATE. The door-two list
# prints once and then scrolls away under whatever the human does next;
# flake.nix's `menu` calls this to print it again. It runs BEFORE the
# PIPULATE_BOOT_MENU and isatty gates deliberately: those exist to keep an
# unattended `nix develop` from blocking at a threshold, and a recall
# blocks on nothing, so inheriting their fail-open would only make the
# word print nothing in a pipe and nothing under PIPULATE_BOOT_MENU=0.
#
# IT EXITS 0 AS AN ORDINARY SUCCESSFUL DISPLAY, which COLLIDES with
# EXIT_START. No caller may branch on the exit code of a --recall run, and
# the flake wrapper deliberately does not: a wrapper that read 0 as "start
# the app" would launch a server every time `menu` ran without a tty.
if "--recall" in sys.argv[1:]:
root = Path(os.environ.get("PIPULATE_ROOT") or Path(__file__).resolve().parent.parent)
print()
print_door_two_words(_app_name(root))
return 0
[[[REPLACE]]]
Target: scripts/boot_menu.py
[[[SEARCH]]]
print(_count_word(len(DOOR_TWO_WORDS)).capitalize() + " words to start from:")
width = max(len(word) for word, _ in DOOR_TWO_WORDS)
for word, description in DOOR_TWO_WORDS:
print(" " + word.ljust(width) + " " + description.format(name=name))
[[[DIVIDER]]]
print_door_two_words(name)
[[[REPLACE]]]
Target: flake.nix
[[[SEARCH]]]
tools() {
if [ "$#" -eq 0 ]; then
(cd "$PIPULATE_ROOT" && .venv/bin/python cli.py mcp-discover)
else
(cd "$PIPULATE_ROOT" && .venv/bin/python cli.py call "$@")
fi
}
[[[DIVIDER]]]
tools() {
if [ "$#" -eq 0 ]; then
(cd "$PIPULATE_ROOT" && .venv/bin/python cli.py mcp-discover)
else
(cd "$PIPULATE_ROOT" && .venv/bin/python cli.py call "$@")
fi
}
# THE RECALL WORD. The door-two list prints ONCE, at the moment of
# choice, and then scrolls away under whatever the human does next.
# `menu` prints it again from the SAME tuple (DOOR_TWO_WORDS in
# boot_menu.py), so the reminder can never drift from the original.
#
# A DISPLAY, NEVER A SECOND THRESHOLD, and the exit code is the whole
# argument. boot_menu.py FAILS OPEN by design: no tty,
# PIPULATE_BOOT_MENU=0, or any unexpected exception all return 0,
# which means START THE APP. That polarity is correct at the
# threshold, where blocking would strand an unattended nix develop.
# At a PROMPT it inverts: a menu that branched on exit 0 would start
# a server every time it ran without a tty -- including inside
# prompt_foo's `!` executor, where stdout is a pipe, and `pu` would
# then pkill the running server, start a new one in the foreground,
# and hold the compile until the 180s process-group kill. So nothing
# here reads the exit status, and --recall returns before the isatty
# gate is ever reached. Door 1 already has a word here: `pu`.
#
# A FUNCTION, not writeShellScriptBin (THE THREE-TIER AMENDMENT):
# only a human types this, and no child shell needs to resolve it.
# That also makes it structurally invisible to the `!` executor, so
# `type menu` can only ever be witnessed by a human at a real
# prompt; the straddle reads the generated hook text instead.
menu() {
if [ -f "$PIPULATE_ROOT/scripts/boot_menu.py" ]; then
"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/boot_menu.py" --recall
else
echo "menu: scripts/boot_menu.py has not landed in this checkout."
fi
}
[[[REPLACE]]]
Ignition. The boot_menu.py half needs none โ probe 1 loads the patched file at call time. The flake.nix half is a shellHook, read once at entry: exit, then nix develop, then press 2. The panel should now read โfive words wait at the promptโ (derived, not typed), and the list should carry the menu row. Then, by hand: type menu (expect menu is a function) and menu.
Choreography: app โ d โ m โ exit โ nix develop โ press 2 โ type menu โ menu โ blast.
(4) PROMPT
Applied the menu recall car. Receipts below.
BEFORE (hand-run, pre-patch):
[paste the three probe outputs]
AFTER ignition (exit, nix develop, pressed 2):
[paste the panel line, the door-two list, `type menu`, and `menu`]
Two follow-ups if the straddle is green:
1. Probe 3 may have found stale "four words" prose in README.md or AGENTS.md,
neither of which was in the last context. If so, include those files and
patch the prose to read from the derived count or drop the number.
2. The curl|bash truncation earmark: assets/installer/install.sh runs
top-to-bottom, so a truncated download executes a partial script. Propose
the main() { ... }; main "$@" wrapping, with a probe that proves a
truncated copy defines nothing and executes nothing. Include
assets/installer/install.sh and assets/installer/mck.sh.
(5) EXTERNAL DELIVERABLES
None this car. The npvg.org DNS/host configuration (apex serving the script at / over https, ideally with content negotiation so a browser gets a page and curl gets the script) is a manual step outside this repo, and it should wait until the truncation-safety earmark lands.
MikeLev.in:
1: Probe:
Blast Radius Check to establish bisection Left-hand Causal Boundary. It is a Popper-thing. Science.
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ PIPULATE_BOOT_MENU=0 .venv/bin/python scripts/boot_menu.py --recall; echo "recall_exit=$?"
LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook 2>/dev/null | grep -c 'menu()' || true
rg -n -i -e 'four words' -e 'three words' . 2>/dev/null | head -20 || true
recall_exit=0
0
./flake.nix:1186: # the roster had taken it. Three words now, each of whose bare and
./scripts/boot_menu.py:60:# an rg probe for 'Three words to start from' found the heading and was
./scripts/boot_menu.py:61:# structurally blind to 'three words wait at the prompt', because the second
./scripts/boot_menu.py:82:# Spelled out because "four words wait" reads better than "4 words wait".
(nix) pipulate $
2: Context:
# adhoc.txt _ _ _ to set context____ _ _ ___ ____ _ <F5> Simpson Couch Gag Here (explain anything to the audience you feel needs it explained)
# / \ __| | | | | | ___ ___ / ___| | | |/ _ \| _ \| |
# ahe/ _ \ / _` | | |_| |/ _ \ / __| | | | |_| | | | | |_) | | Adding menu command
# ahc ___ \ (_| | | _ | (_) | (__ | |___| _ | |_| | __/|_|
# /_/ \_\__,_| |_| |_|\___/ \___| \____|_| |_|\___/|_| (_)
# Ad Hoc CHOP: The Not-Managed-by-Git Safe-for-Client-Data place
# THE ROLLING PIN BOOK ORE SPINE / FODDER FOR THE BOOK
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs # <-- The "Rolling Pin" that gives the 40K foot book-spine view of book-ore.
# GLOSSARY.md # <-- I think this glossary goes well with the book-ore spine to do world building.
# scripts/articles/lsa.py # <-- Useful for refining commands like `posts`, critical to Second Brain concept.
# THE QUIRKY AMIGA-LOVING HUMAN
# ~/repos/nixos/autognome.py # <-- Letting the AIs really understand my environment (The Brave Little Tailor punches above Their Weight Class proving the dunning-kruger effect the gate-keeper's (lower-case) lament.)
# init.lua # <-- Daily driver hot-keys that overlap with aliases in flake.nix
# AGENTIC FRAMEWORK & FOREVER MACHINE BIG FILES
prompt_foo.py # <-- Prompt Fu compiler, makes the very README for AGENTS-like payload you're reading right now, but it needs to be more like that
# foo_files.py # <-- This is the router, evolving book outline and the things you pin-up to produced the recursive self-improvement loops
flake.nix # <-- Solves world's WRITE ONCE RUN ANYWHERE problem like Java never could. Also resolves the bootstrap paradox.
# MAIN ACTUATORS, IaC & NEGATIVE SPACE
apply.py # <-- How can "Web UI" ChatBots edit your code? With this Aider-inspired Player Piano patch applier.
.gitattributes # <-- Model: understand that `nbstripout` and `jupytext` are both in play. Just talk the human through .ipynb patches.
.gitignore # <-- Creates "negative space" for sub-rep's to share parent environment and "snap" proprietary secret features into place.
requirements.in # <-- All known dependencies and (necessary) version pinning. WORA gotcha's exposed.
__init__.py # <-- Master versioning
pyproject.toml # <-- The PyPI Packaging details
# cli.py # <-- Catch-all actuator for PyPI envs, Python anchoring, MCP tool-call (plus alternatives) and **kwargs like wrapping for CLI
# scripts/xp.py # <-- Transforms host OS copy-paste buffer player-piano music into context-payload.
# scripts/ai.py # <-- How I constantly use local AI to write git commit messages with `m` alias.
# CONTEXT PORTABILITY SYSTEM
# scripts/foo_cartridge.py # Needs description
# scripts/foo_replay.py # Needs description
# FREQUENTLY USEFUL TO HAVE IN CONTEXT
# release.py # <-- How everything ends up where it does (GitHub, PyPI, etc.)
# scripts/weblogin.py # <-- Lets the user "warm up" the cache for their web logins at their leisure on a profile that persists.
# scripts/crawl.py # <-- Feel free to ask for something to be crawled and included in the next turn.
# imports/voice_synthesis.py # <-- The wand can talk to you
# scripts/release/version_sync.py # <-- Needs to be wrapped into release.py and eliminated, I think.
# --- Under this line is were you paste what the AI gives you ---
# --- We call it context but it's really just the right-hand ---
# --- blast-radius of the "probes" to make this all science. ---
# --- END `adhoc.txt` TEMPLATE ---
# STICKBUG & MOTHER CAT KATA
# assets/trails/botify_pageworkers.yaml
assets/installer/mck.sh
assets/installer/replay.sh
assets/trails/first_context.yaml
assets/trails/practice.yaml
assets/trails/public_walk.yaml
scripts/bookmark_import.py
scripts/boot_menu.py
scripts/connectors/README.md
scripts/connectors/botify.py
scripts/connectors/confluence.py
scripts/connectors/gmail.py
scripts/connectors/gsc.py
scripts/connectors/jira.py
scripts/connectors/mcp.py
scripts/connectors/mcp_warm.py
scripts/connectors/sheets.py
scripts/connectors/slack.py
scripts/connectors/wallet.py
scripts/mother_cat.py
scripts/sources_menu.py
scripts/walk.py
scripts/walk_cartridge.py
scripts/walk_compile.py
scripts/weblogin.py
tools/scraper_tools.py
# # # adhoc.txt -- Cleanup inert public_walk environment export block
# #
# # # --- BEFORE/AFTER STRADDLE ---
# # ! grep -n -C 2 'PIPULATE_TRAIL_WALK_ONE_URL' assets/installer/mck.sh || echo "export_block_removed"
# # ! bash -n assets/installer/mck.sh; echo "mck_syntax=$?"
# # ! bash assets/installer/mck.sh --where
# #
# # # --- TARGET SCRIPT ---
# # assets/installer/mck.sh
# #
# # ! rg -n -F 'TRAIL_NAME="${TRAIL_NAME:-public_walk}"' assets/installer/mck.sh
# # ! rg -n -e '^ *walk\(\)' -e '^ *alias walk=' flake.nix; echo "shell_walk_exit=$?"
# # ! test -e walk; echo "root_walk_exists=$?"
# # ! rg -n -e 'alias mothercat=' -e 'Three words to start from' flake.nix scripts/boot_menu.py
# # flake.nix
# # scripts/boot_menu.py
# # assets/installer/mck.sh
#
# # ! bash -n walk; echo "walk_syntax=$?"
# # ! bash walk --where
# # ! rg -n -e '^ *alias walk=' -e '^ *walk\(\)' -e 'writeShellScriptBin "walk"' flake.nix; echo "shell_walk_exit=$?"
# # ! .venv/bin/python -c 'import sys; sys.path.insert(0,"scripts"); import boot_menu as b; print("count=" + b._count_word(len(b.DOOR_TWO_WORDS))); print(" ".join(w for w, _ in b.DOOR_TWO_WORDS))'
# # walk
# # flake.nix
# # scripts/boot_menu.py
# # assets/installer/mck.sh
#
# # ! rg -in "three words|four words|door 2" flake.nix scripts/ assets/ README.md
# # ! walk --where
# # walk
# # flake.nix
# # scripts/boot_menu.py
# # assets/installer/mck.sh
#
# # ! python scripts/connectors/wallet.py check slack
# # ! python -c "import os, sys; from pathlib import Path; sys.path.insert(0, '.'); from scripts.connectors import wallet; pairs = wallet._dotenv_pairs(); print('env SLACK_USER_TOKEN:', bool(os.getenv('SLACK_USER_TOKEN')), 'env SLACK_BOT_TOKEN:', bool(os.getenv('SLACK_BOT_TOKEN')), 'vault SLACK_USER_TOKEN:', 'SLACK_USER_TOKEN' in pairs, 'vault SLACK_BOT_TOKEN:', 'SLACK_BOT_TOKEN' in pairs)"
# # ! python scripts/connectors/wallet.py warm slack --dry-run
#
# /home/mike/repos/trimnoir/_posts/2026-08-30-terminal-native-mcp-practice.md
# deleteme.txt
# Now that slack is working (on my workspace) we trim back things here to just the stick bug and Mother Cat Kata stuff
# assets/installer/install.sh # <-- Pipulate.com installer real home in github/pipulate repo
# ~/repos/Pipulate.com/CNAME
# ~/repos/Pipulate.com/_config.yml
# ~/repos/Pipulate.com/_layouts/default.html
# ~/repos/nixos/.gitignore
# ~/repos/Pipulate.com/install.md # <-- Gets copied into place here by pipulate/release.py
! PIPULATE_BOOT_MENU=0 .venv/bin/python scripts/boot_menu.py --recall; echo "recall_exit=$?"
! LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook 2>/dev/null | grep -c 'menu()' || true
! rg -n -i -e 'four words' -e 'three words' . 2>/dev/null | head -20 || true
flake.nix
scripts/boot_menu.py
scripts/sources_menu.py
assets/installer/install.sh
3: Patches:
Blast Radius Check to establish bisection Left-hand Causal Boundary. It is a Popper-thing. Science.
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/boot_menu.py'.
(nix) pipulate $ d
diff --git a/scripts/boot_menu.py b/scripts/boot_menu.py
index 44826be5..4508e09b 100644
--- a/scripts/boot_menu.py
+++ b/scripts/boot_menu.py
@@ -4,7 +4,7 @@ boot_menu.py โ the threshold at the end of `nix develop`.
Two doors, one keypress:
[1] Start JupyterLab plus the app server and browser tabs (today's behavior)
- [2] Just the shell NOTHING starts; `walk`, `sources`, `brief`, `pu` wait at the prompt
+ [2] Just the shell NOTHING starts; `walk`, `sources`, `brief`, `pu`, `menu` wait at the prompt
THE PROTOCOL IS THE EXIT CODE, never stdout. Nothing parses this program's
output, so no capture pipe can ever be held open by it (the rgx/xclip
(nix) pipulate $ m
๐ Committing: chore: Update boot_menu.py with `menu` command
[main 66dc30d6] chore: Update boot_menu.py with `menu` command
1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/boot_menu.py'.
(nix) pipulate $ d
diff --git a/scripts/boot_menu.py b/scripts/boot_menu.py
index 4508e09b..a5858054 100644
--- a/scripts/boot_menu.py
+++ b/scripts/boot_menu.py
@@ -68,7 +68,11 @@ SHELL_KEYS = {"2", "q", "Q", "n", "N", "l", "L", "\x03", "\x04"}
# ORDER IS A SCOPE LADDER, neither alphabetical nor arbitrary: be carried
# (walk) -> look around here yourself (sources) -> hand it to someone
# elsewhere (brief) -> reverse the choice you just made (pu). `walk` leads
-# because it is the only row that asks nothing of you first.
+# because it is the only row that asks nothing of you first. `menu` rides
+# LAST and is not a rung on that ladder at all: it is not a place to go, it
+# is how you get this list back after it has scrolled away. Its row is also
+# the only thing that tells a newcomer the word exists -- a recall command
+# nothing announces is a command nobody types.
# No row spells the brand any more, so the .format(name=...) call at the print
# site currently no-ops. It stays: a future row may need it, and a row that
# silently printed a literal {name} would be worse than a call that does
(nix) pipulate $ m
๐ Committing: chore: Refine boot_menu.py comments
[main 99e1d0e4] chore: Refine boot_menu.py comments
1 file changed, 5 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/boot_menu.py'.
(nix) pipulate $ d
diff --git a/scripts/boot_menu.py b/scripts/boot_menu.py
index a5858054..d07cf4a3 100644
--- a/scripts/boot_menu.py
+++ b/scripts/boot_menu.py
@@ -82,6 +82,7 @@ DOOR_TWO_WORDS = (
("sources", "see what this shell can reach outside this machine"),
("brief", "compile this workshop into your clipboard for an AI"),
("pu", "change your mind and start the app server after all"),
+ ("menu", "print this list again once it scrolls away"),
)
# Spelled out because "four words wait" reads better than "4 words wait".
# The digit fallback means a word count past seven degrades to something
(nix) pipulate $ m
๐ Committing: chore: Add menu print repetition
[main 4c7f6587] chore: Add menu print repetition
1 file changed, 1 insertion(+)
(nix) pipulate $ patch
(nix) pipulate $ app
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/boot_menu.py'.
(nix) pipulate $ d
diff --git a/scripts/boot_menu.py b/scripts/boot_menu.py
index d07cf4a3..66cc0b06 100644
--- a/scripts/boot_menu.py
+++ b/scripts/boot_menu.py
@@ -93,6 +93,20 @@ def _count_word(n):
return _COUNT_WORDS[n] if 0 <= n < len(_COUNT_WORDS) else str(n)
+def print_door_two_words(name):
+ """Print the door-two list. ONE implementation, TWO call sites.
+
+ main() prints it at the moment of choice; `menu` prints it again later
+ through --recall. Both read the same tuple, so the reminder cannot drift
+ from the thing it reminds you of -- which is the whole reason
+ DOOR_TWO_WORDS is data rather than two hand-written strings.
+ """
+ print(_count_word(len(DOOR_TWO_WORDS)).capitalize() + " words to start from:")
+ width = max(len(word) for word, _ in DOOR_TWO_WORDS)
+ for word, description in DOOR_TWO_WORDS:
+ print(" " + word.ljust(width) + " " + description.format(name=name))
+
+
def _app_name(root: Path) -> str:
"""Mirror runScript's awk: capitalize first char, lowercase the rest."""
try:
(nix) pipulate $ m
๐ Committing: chore: Add print_door_two_words function
[main b7fcf899] chore: Add print_door_two_words function
1 file changed, 14 insertions(+)
(nix) pipulate $ patch
(nix) pipulate $ app
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/boot_menu.py'.
(nix) pipulate $ d
diff --git a/scripts/boot_menu.py b/scripts/boot_menu.py
index 66cc0b06..05998bdd 100644
--- a/scripts/boot_menu.py
+++ b/scripts/boot_menu.py
@@ -224,6 +224,23 @@ def _read_choice(seconds) -> int:
def main() -> int:
+ # THE RECALL PATH, AND WHY IT SITS ABOVE EVERY GATE. The door-two list
+ # prints once and then scrolls away under whatever the human does next;
+ # flake.nix's `menu` calls this to print it again. It runs BEFORE the
+ # PIPULATE_BOOT_MENU and isatty gates deliberately: those exist to keep an
+ # unattended `nix develop` from blocking at a threshold, and a recall
+ # blocks on nothing, so inheriting their fail-open would only make the
+ # word print nothing in a pipe and nothing under PIPULATE_BOOT_MENU=0.
+ #
+ # IT EXITS 0 AS AN ORDINARY SUCCESSFUL DISPLAY, which COLLIDES with
+ # EXIT_START. No caller may branch on the exit code of a --recall run, and
+ # the flake wrapper deliberately does not: a wrapper that read 0 as "start
+ # the app" would launch a server every time `menu` ran without a tty.
+ if "--recall" in sys.argv[1:]:
+ root = Path(os.environ.get("PIPULATE_ROOT") or Path(__file__).resolve().parent.parent)
+ print()
+ print_door_two_words(_app_name(root))
+ return 0
if os.environ.get("PIPULATE_BOOT_MENU", "1").strip().lower() in {
"0",
"no",
(nix) pipulate $ m
๐ Committing: chore: Add recall path print to boot menu
[main 9190deca] chore: Add recall path print to boot menu
1 file changed, 17 insertions(+)
(nix) pipulate $ patch
(nix) pipulate $ app
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/boot_menu.py'.
(nix) pipulate $ d
diff --git a/scripts/boot_menu.py b/scripts/boot_menu.py
index 05998bdd..bf09eeb4 100644
--- a/scripts/boot_menu.py
+++ b/scripts/boot_menu.py
@@ -269,10 +269,7 @@ def main() -> int:
if choice == EXIT_SHELL:
print("[2] Just the shell. Nothing started -- no JupyterLab, no server.")
print()
- print(_count_word(len(DOOR_TWO_WORDS)).capitalize() + " words to start from:")
- width = max(len(word) for word, _ in DOOR_TWO_WORDS)
- for word, description in DOOR_TWO_WORDS:
- print(" " + word.ljust(width) + " " + description.format(name=name))
+ print_door_two_words(name)
else:
print(f"[1] Starting {name}...")
return choice
(nix) pipulate $ m
๐ Committing: chore: Remove redundant door two word printing logic
[main 0ad68313] chore: Remove redundant door two word printing logic
1 file changed, 1 insertion(+), 4 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'flake.nix'.
(nix) pipulate $ d
diff --git a/flake.nix b/flake.nix
index 8b71dd5e..e7d53fd7 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1221,6 +1221,36 @@ runScript = pkgs.writeShellScriptBin "run-script" ''
(cd "$PIPULATE_ROOT" && .venv/bin/python cli.py call "$@")
fi
}
+ # THE RECALL WORD. The door-two list prints ONCE, at the moment of
+ # choice, and then scrolls away under whatever the human does next.
+ # `menu` prints it again from the SAME tuple (DOOR_TWO_WORDS in
+ # boot_menu.py), so the reminder can never drift from the original.
+ #
+ # A DISPLAY, NEVER A SECOND THRESHOLD, and the exit code is the whole
+ # argument. boot_menu.py FAILS OPEN by design: no tty,
+ # PIPULATE_BOOT_MENU=0, or any unexpected exception all return 0,
+ # which means START THE APP. That polarity is correct at the
+ # threshold, where blocking would strand an unattended nix develop.
+ # At a PROMPT it inverts: a menu that branched on exit 0 would start
+ # a server every time it ran without a tty -- including inside
+ # prompt_foo's `!` executor, where stdout is a pipe, and `pu` would
+ # then pkill the running server, start a new one in the foreground,
+ # and hold the compile until the 180s process-group kill. So nothing
+ # here reads the exit status, and --recall returns before the isatty
+ # gate is ever reached. Door 1 already has a word here: `pu`.
+ #
+ # A FUNCTION, not writeShellScriptBin (THE THREE-TIER AMENDMENT):
+ # only a human types this, and no child shell needs to resolve it.
+ # That also makes it structurally invisible to the `!` executor, so
+ # `type menu` can only ever be witnessed by a human at a real
+ # prompt; the straddle reads the generated hook text instead.
+ menu() {
+ if [ -f "$PIPULATE_ROOT/scripts/boot_menu.py" ]; then
+ "$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/boot_menu.py" --recall
+ else
+ echo "menu: scripts/boot_menu.py has not landed in this checkout."
+ fi
+ }
# THE THIRD DOOR: `pu` starts the server door 2 declined to start.
# Kill-then-start, so it doubles as a restart and never collides on
# :5001 -- the same pkill runScript has always run on shell entry.
(nix) pipulate $ m
๐ Committing: chore: Add `menu()` function for recall word display
[main 18270fee] chore: Add `menu()` function for recall word display
1 file changed, 30 insertions(+)
(nix) pipulate $ git push
Enumerating objects: 31, done.
Counting objects: 100% (31/31), done.
Delta compression using up to 48 threads
Compressing objects: 100% (27/27), done.
Writing objects: 100% (27/27), 4.18 KiB | 1.39 MiB/s, done.
Total 27 (delta 20), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (20/20), completed with 4 local objects.
To github.com:pipulate/pipulate.git
5a7b9a9f..18270fee main -> main
(nix) pipulate $
And we ignite.
(nix) pipulate $ exit
exit
(sys) pipulate $ nix develop
Checking for updates...
Temporarily stashing local JupyterLab settings...
From github.com:pipulate/pipulate
* branch main -> FETCH_HEAD
Already up to date.
(Nix) 2.25.0pre20240910_b9d3cdfb ยท Python 3.12.13 ยท v2.49 ยท ~/repos/pipulate
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ nix develop -- a reproducible *nix shell :: pick a door โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ
โ [1] Start JupyterLab plus the app server and browser tabs โ
โ [2] Just the shell nothing starts -- five words wait at the prompt โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ waiting for your choice -- Ctrl+C also drops to the shell โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
[2] Just the shell. Nothing started -- no JupyterLab, no server.
Five words to start from:
walk take the guided tour -- public pages, nothing to log into
sources see what this shell can reach outside this machine
brief compile this workshop into your clipboard for an AI
pu change your mind and start the app server after all
menu print this list again once it scrolls away
(nix) pipulate $ menu
Five words to start from:
walk take the guided tour -- public pages, nothing to log into
sources see what this shell can reach outside this machine
brief compile this workshop into your clipboard for an AI
pu change your mind and start the app server after all
menu print this list again once it scrolls away
(nix) pipulate $
Nice!
4: Prompt: Applied the menu recall car. Receipts below.
BEFORE (hand-run, pre-patch): [paste the three probe outputs]
AFTER ignition (exit, nix develop, pressed 2):
[paste the panel line, the door-two list, type menu, and menu]
Two follow-ups if the straddle is green:
-
Probe 3 may have found stale โfour wordsโ prose in README.md or AGENTS.md, neither of which was in the last context. If so, include those files and patch the prose to read from the derived count or drop the number.
-
The curl|bash truncation earmark: assets/installer/install.sh runs top-to-bottom, so a truncated download executes a partial script. Propose the main() { โฆ }; main โ$@โ wrapping, with a probe that proves a truncated copy defines nothing and executes nothing. Include assets/installer/install.sh and assets/installer/mck.sh.
5: Deliverables: A better 1st 5 minutes experience.
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:
- TLDR: a short, dry, neutral abstract for the TOP of the published article โ written for an unfamiliar reader or AI summarizer who has never seen this system. No hype, no insider handles unexplained.
- VERIFY: restate the goal from the top of this article and confirm (or deny) it was met, citing THIS compileโs receipts, not memory. Name any ignition this ride required that never fired โ an AFTER tap taken without one is a stale BEFORE wearing the AFTERโs label.
- BANK: name everything that graduates โ rule, earmark, todo, pin โ as exact paste-ready lines, plus the exact lines to delete.
- DANGLING: what carries forward unbanked? One line each, no essays.
- SEED: the adhoc.txt lines (and TODO_SLUGS if narrative context is needed) for the next rideโs first compile.
- CLOSING: a closing summary for the BOTTOM of the article โ the final take-away, tied to the bookโs larger arc where it fits naturally, never forced. Storytelling over inventory.
- NOTARIZE: name the sealed artifact of record โ the newest
hash-stamped cartridge (foo-
-NN.zip) โ and state that its CRC-sealed, byte-reproducible archive is the witnessed receipt of this ride. The seal is the signature; the archive is the deed.
FINALITY: after beat 6, this discussion is CLOSED. Emit NO five-car train, NO probes, NO patches, 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.
Opus 5 Extra:
1. VERIFY
Goal from the top: โthe most important thing is for us to implement that menu command so people can get back that initial menu.โ Met, on this compileโs receipts.
- Probe 1 AFTER:
--recallprints the five-word list includingmenu,recall_exit=0. The operatorโs BEFORE tap printed no list and also exited 0. The exit codes collide by design โ the patch comment predicted that collision โ so the printed list is the discriminator, not the code. That is the โgrade the token, not the exit codeโ rule already stated inbookmark_import.py, firing here as an instance rather than a new finding. - Probe 2: BEFORE
0, AFTER1. The generated.#quietshellHook now containsmenu(). - Probe 3: four hits BEFORE, four AFTER, same four lines with
boot_menu.py:82shifted to:87by the inserted comment. Every hit is historical prose about counts, not a live count claim. Nothing inREADME.mdorAGENTS.mdmatched.
Ignition fired and was witnessed. exit โ nix develop โ pressed 2 โ panel read five words wait at the prompt (derived, not typed), list carried the menu row, and a hand-typed menu reprinted it. type menu was named in the plan and never run; running menu supersedes it โ a function that executes is bound.
One receipt asymmetry worth stating: the transcript at the top of the article was captured on macOS (Determinate Nix 3.19.1), the ignition on Linux (Nix 2.25.0pre). Different machines, same behavior. The readings line discriminating the two Nix flavors is working as designed.
2. BANK
Rule โ new, second instance, paste into GLOSSARY.md:
THE HOOK-TEXT WITNESS (banked 2026-09-01, second instance). A shell FUNCTION,
alias, or compspec defined in shellHook is STRUCTURALLY INVISIBLE to prompt_foo's
`!` executor: functions do not export, and `nix develop --command bash -c '...'`
execs a fresh bash that never sourced the hook. So `type <name>` prints the
identical answer in the world where the definition landed and the world where it
did not -- a false receipt in both directions. The only compile-lane witness is
the GENERATED HOOK TEXT:
LD_LIBRARY_PATH="" nix eval --raw .#devShells.<system>.<shell>.shellHook \
| grep -c '<pattern>'
LD_LIBRARY_PATH="" is LOAD-BEARING and its absence is invisible until it fires:
the nix() rpath shim is itself a shell function, so the `!` child inherits the
polluted path and not the protection, and the nix binary dies at the loader with
"libssl.so.3: version OPENSSL_3.2.0 not found". Convicted 2026-08-31 by exactly
that receipt. CHOOSE A PATTERN ONLY THE DEFINITION CAN MATCH -- grep -c counts
LINES, so a comment mentioning `menu()` would have inflated the count to 2 and
the probe would have read green with no function defined. Proven twice: the
Notebooks/Shared mkdir move (2026-08-07) and the menu() recall word (2026-09-01).
Earmark โ paste into foo_files.py:
# EARMARK โ CURL-PIPE TRUNCATION SAFETY. assets/installer/install.sh runs
# top-to-bottom, so a download truncated mid-transfer executes a PARTIAL script:
# it can mkdir the target, half-fetch a key, and exit clean. Wrap the body in
# main() { ... } with main "$@" as the last line, so a truncation can only ever
# fail to define main and nothing runs. Gate on `bash -n` of a truncated copy
# exiting NONZERO -- today it exits zero, which is the whole defect. Owed before
# npvg.org serves a one-liner to strangers.
Earmark โ paste into foo_files.py:
# EARMARK โ THE NPVG ONE-LINER. Ruling 2026-09-01: of `curl -fsSL`, only two
# flags are correctness. -f keeps a 404/502/Cloudflare HTML body from being piped
# into bash. -L keeps a redirect (apex->www, http->https) from delivering an empty
# body, which makes bash exit 0 HAVING DONE NOTHING -- the silent-success failure,
# and the worst of the four. -s and -S are cosmetic: curl suppresses the meter
# only on a terminal, so piping to bash prints it to stderr regardless. Ship
# `-fsSL` as canonical (Homebrew, rustup and the Determinate installer all use it,
# so the muscle memory already exists) and teach `-fL` as the sayable minimum.
Receipt line โ paste at the top of the RECEIPTS block in foo_files.py:
# 2026-09-01 menu recall car: boot_menu.py --recall + flake.nix menu(); hook-text probe 0 -> 1; recall list absent -> five words, exit 0 on BOTH sides so the printed list is the discriminator; rg 'four words|three words' 4 hits before and after, all historical prose, so the README/AGENTS stale-count follow-up is DISCHARGED UNOPENED.
To delete: nothing from a tracked file. The prior turnโs follow-up 1 (patch stale โfour wordsโ prose in README/AGENTS) was a conditional that probe 3 falsified. It lived only in prompt.md and dies with this ride.
3. DANGLING
scripts/boot_menu.py:87illustrates the spelling rule with"four words wait"while the live count is five. Cosmetic, self-referential, and the rule it illustrates is count-independent.sources_menu.pyโs hand-authoredFOOTERnamesbriefandtoolsand does not namemenu. That fileโs own comment says TAIL is where drift lives; this is a second human-facing roster silent about the recall word.- Five rows is at the Rule-of-7 edge. A sixth costs one line mechanically and one more line of panel to read.
install.shtruncation (earmarked above, unstarted).- npvg.org DNS and apex serving โ external, unstarted, and correctly blocked behind truncation safety.
4. SEED
# adhoc.txt -- install.sh truncation safety
! bash -n assets/installer/install.sh; echo "full_syntax=$?"
! head -c 4000 assets/installer/install.sh > /tmp/trunc_install.sh; bash -n /tmp/trunc_install.sh; echo "trunc_syntax=$?"
! rg -c '^main "\$@"$' assets/installer/install.sh || true
assets/installer/install.sh
assets/installer/mck.sh
The middle line is the whole argument: today a truncated copy parses clean (trunc_syntax=0), which means bash would have run it. After the wrapping, a cut inside main is a syntax error and bash -n exits nonzero, so nothing executes. bash -n never runs the script, so this probe is read-only in both worlds.
5. CLOSING
The rejected draft is the interesting part of this ride. It was three lines long, looked obviously right, and would have made a word we are about to teach every newcomer capable of killing and restarting a server from inside a compile pipe. The bug came from a correct decision applied one context over: boot_menu.py fails open โ no tty, an env override, any unexpected exception all return โstart the appโ โ because at the threshold of nix develop a menu that can strand an unattended shell is worse than no menu. At a prompt that polarity inverts, and a wrapper that read the exit code would have inherited a safety property as a hazard. The fix was not to add a guard but to remove the branch: menu reads nothing, decides nothing, and prints.
That is the same shape as the rest of this system. d refuses to stage. The DECANT gate refuses to answer a question nobody asked. The walletโs offline scoreboard refuses to open a tokenโs bytes. Each one is a place where the cheap version would have been an actuator and the correct version is a reading. A workshop that a stranger is invited to delete and rebuild on a whim only earns that invitation if the words on the welcome mat cannot do anything surprising.
And the smallest piece here is the one worth keeping: the count in โFive words to start fromโ is computed from the tuple. Nobody will ever have to remember to update it, which means it can never be wrong, which means the reader can trust it without checking. That is the whole local-first argument in one line of Python โ not that the machine is smarter, but that you can see all of it.
6. NOTARIZE
The artifact of record is the newest rotated cartridge, foo-<hash8>-NN.zip, written beside the canonical foo.zip in the repo root at step 7 of this compile.
I cannot name its hash, and that absence is structural rather than an oversight: write_context_cartridge runs after build_final_prompt assembles the Summary, so the archiveโs own SHA-256 is computed from bytes that already exist and cannot appear inside them. The rotation line naming the file printed to the operatorโs console; it is not in this payload. A cartridge cannot contain its own seal โ that is what makes the seal a seal.
What that archive attests: exact members in fixed order, canonical ZIP metadata with a frozen timestamp, canonical JSON, per-member hashes, and byte-canonical reconstruction, verified by verify_context_cartridge before the file was moved into place. Identical inputs produce identical bytes forever. The seal is the signature; the archive is the deed. This discussion is closed.
MikeLev.in: Hey AI Editor, always be sure to focus on whatโs really unique about this article; this whole taking it from the top concept. Iโm also thinking we start walking forward from the very beginning with nursery rhymes to take us from one concept to the next, ha ha! But the point is that what weโre doing here is to make it that โtripping over your own feet and it still worksโ easy with all things Unix commands to help build context for AI as the skill.
Book Analysis
Ai Editorial Take
What stands out most in this entry is the psychological comfort of the โdelete and rebuildโ loop. By making the entire workspace completely disposable yet instantly restorable via a single command, the author eliminates developer hesitation, transforming error recovery into a routine, friction-free ritual.
๐ฆ X.com Promo Tweet
Tired of heavy cloud abstractions? Learn how a single curl command and Nix can bootstrap a clean, reproducible local workspace for modern AI-assisted workflows. https://mikelev.in/futureproof/taking-it-from-top-frictionless-command-line-workshop/ #DevOps #Unix #LocalFirst
Title Brainstorm
- Title Option: Taking It from the Top: Designing a Frictionless Command-Line Workshop
- Filename:
taking-it-from-top-frictionless-command-line-workshop.md - Rationale: Directly references the conversational starting point while clearly communicating the focus on building a robust local developer experience.
- Filename:
- Title Option: The Art of the Clean Bootstrap: Unix Workflows and Local Isolation
- Filename:
art-of-clean-bootstrap-unix-workflows-local-isolation.md - Rationale: Focuses on the mechanical philosophy of starting fresh and leveraging Nix for reliable dependency management.
- Filename:
- Title Option: From DOS and Amigas to Nix: Reclaiming Local Control
- Filename:
from-dos-and-amigas-to-nix-reclaiming-local-control.md - Rationale: Taps into the historical computing perspective, highlighting the lineage of user-controlled environments.
- Filename:
Content Potential And Polish
- Core Strengths:
- Engaging conversational framing that transitions smoothly into deep technical philosophy.
- Clear articulation of why Nix outperforms containerization for lightweight local reproducibility.
- Practical integration of terminal mechanics with real-world tool execution.
- Suggestions For Polish:
- Tighten the historical tangent on early computing to keep the momentum flowing directly into the technical tooling.
- Ensure the transition between the installer output snippet and the philosophical reflection feels seamless.
Next Step Prompts
- Detail the exact mechanics of how the installer script handles credential security during fresh directory provisioning.
- Explore how small terminal menus can guide newcomers without cluttering the expert userโs command path.