Protocol vs. Prompt: Why MCP Is Becoming Unix
Setting the Stage: Context for the Curious Book Reader
This essay explores an interesting technical evolution in the Age of AI: how structured discovery protocols like the Model Context Protocol (MCP) are converging on the same architectural lessons learned decades ago by Unix. Through a dialogue examining API complexity, state management, and environment determinism, it questions whether heavy protocol abstractions are merely compensating for variable operating systems.
TL;DR: This article records a working session between a developer and a chat-based AI model about the Model Context Protocol (MCP), the standard AI systems use to discover and call external tools. It opens with an assessment of MCPโs two-year design history and a comparison to the Multics-to-Unix transition, then advances a hypothesis: in a fully reproducible software environment (here, one built with the Nix package manager), ordinary shell commands with --help text can do the job a discovery protocol does. The second half applies that idea to the developerโs own installer. The AI, working only from text pasted into a chat window, proposed exact-match code patches; each was bracketed by read-only shell commands whose output was captured before and after the change. The changes covered installer wording, a git-ignore omission, a shell-escaping bug in a generated SSH config, a stray Markdown fence, and menu text. A fresh install on a second machine confirmed every change. The hypothesis was made testable but not tested.
Technical Journal Entry Begins
๐ Verified Pipulate Commits:
MikeLev.in: MCP is a so-called universal adapter for APIs for AI because all the individual APIs of the world are too much to expect to train into every Model. APIs can be tricky to support every imaginable edge case and some APIs are so tricky that a so-called priesthood springs up around it because the value of automating some system is so high simultaneously to learning curve of the API being so steep and the subtleties and nuances that you have to master to use it so deep and unending. Iโve known a few APIs like that in my day (and still do).
MCP simplifies, given merely its endpoint it should be able to start probing and discovering things about it and then just start using it. But thatโs not to say there are no login and authentication issues; there are! And on a per-MCP-server basis a whole lot of rules can change such as authentication scheme. Iโm not 100% sure but the whole MCP protocol might be either XML or JSON. I think itโs settled down on JSON of late but it certainly was not that way in the early days of MCP and I think the variations are still out there.
And the final point here is that even though MCP does simply things forcing certain conventions of discoverability and other things (what are they), having the MCP server in front of your already existing and perhaps massively complex API doesnโt automatically simplify the underlying API to do things like making the most common use cases actually simple โ no, that still needs to be done by humans because MCP is just another case of that API-wrapper eternal trick, just with conventions so it sort of shifts the pain of interacting with a complex API back on the company that made it because now thereโs this somewhat objective criteria: can a thing even of moving-towards-superintelligence even use your API?
Hereโs the test: If your API is so great then letโs see AI use it! Whoops, thatโs wayyy too complex for even a machine intelligence! We need a wrapper to make it not so, and that wrapper protocol is the thing all machines need to learn so it doesnโt have to learn everybodyโs individual and sometimes impossibly complex APIs! Head-smack! So you see Anthropic or not, MCP (or something like it) was almost an inevitability of necessity.
True? False? Why?
The Inevitability of the Universal Adapter
Could you give me the full run-down on this Fable 5?
๐ค
Fable 5 Extra: Iโll verify the current state of the spec before answering, since MCP has moved fast.Verdict: mostly true, with one factual miss (the XML part), one point you actually understate (the wrapper doesnโt simplify anything), and a twist youโll enjoy: the protocol just spent its second year admitting it had over-built itself.
The XML thing: false
MCP has been JSON-RPC 2.0 on the wire since the first spec revision (2024-11-05). It was never XML and there are no XML variants floating around. What did churn was the transport underneath: stdio plus an HTTP+SSE transport at launch, replaced by โStreamable HTTPโ in March 2025, and as of last month reworked again. The legacy HTTP+SSE transport is now officially deprecated with a year-long offramp.
My guess at where the XML memory comes from: the model side of tool use, pre-MCP, was often XML-ish. Anthropicโs own early tool-calling conventions had models emit <function_calls>-style tags, and a lot of hand-rolled agent prompts did the same. So the thing the model wrote looked like XML; the thing on the wire never did.
What conventions MCP actually forces
The โuniversal adapterโ is a short list of things every server has to expose the same way:
- Tools โ callable functions with a JSON Schema
inputSchema(and since mid-2025, optionaloutputSchemaand structured results). This is the bit that maps onto โAPI.โ - Resources โ URI-addressed read-only data, with URI templates for parameterized ones. Think files, records, docs.
- Prompts โ server-supplied prompt templates the user can invoke.
- Discovery โ
tools/list,resources/list,prompts/list, plus change notifications. Hand a client an endpoint and it can enumerate everything, which is exactly the โjust start probingโ behavior you describe. - Capability negotiation and versioning โ dated spec strings (2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25, 2026-07-28) so client and server agree on whatโs supported.
- Tool annotations โ hints like read-only, destructive, idempotent. Hints only; the client canโt trust them.
There were also โclient primitivesโ going the other direction (server asks client for an LLM completion, asks the user a question, asks what directories are in scope). Those are where the big rethink happened.
The July 2026 revision: the protocol admits its original sin
This is the part you couldnโt have known writing this. MCP is transforming from a bidirectional stateful protocol into a request/response stateless protocol. The initialize/initialized handshake and the Mcp-Session-Id header are retired; each request now carries its protocol version, client identity, and capabilities in _meta. Server-to-client requests for sampling and elicitation are being redesigned as Multi Round-Trip Requests, removing the need for constantly open bidirectional streams โ the server returns โinput required,โ the client re-sends with answers. Roots, Sampling, and Logging are deprecated.
Why it matters for your thesis: the original design was for a developerโs laptop; when those demos moved into multi-client enterprise cloud deployments, it created scalability problems. MCP over-engineered the plumbing (sessions, held-open streams) and had to walk it back toward plain HTTP. The maintainersโ framing now is that a remote MCP server is no different from any other HTTP workload. Two years in, the adapter converged on โitโs just a REST-shaped endpoint with a fixed discovery vocabulary.โ Thatโs the inevitability argument proving itself in reverse.
Auth: youโre right, and itโs the worst part
The specโs own maintainers say authorization is where implementers spend most of their integration time. The lineage: OAuth 2.1 added March 2025; the June 2025 revision classified MCP servers as OAuth resource servers and required Resource Indicators (RFC 8707); and last month brought RFC 9207 issuer validation and a formal shift away from Dynamic Client Registration toward client metadata documents, with client credentials bound to the issuer that minted them. Enterprise-managed auth lives in a separate extension. Meanwhile stdio servers just take secrets from environment variables, and plenty of โremoteโ servers still want a bearer key in a header. So yes: per-server, the auth scheme is the thing that changes, and the spec is on its fourth attempt at nailing it down.
The wrapper point: you understate it
MCP standardizes how to call and how to discover. It says nothing about what to expose. A vendor who mirrors 400 REST endpoints as 400 tools has technically shipped an MCP server and practically shipped a context-window bomb. Models degrade at tool selection as catalogs grow, every tool definition costs tokens on every turn, and the priesthoodโs tacit knowledge (call this before that, this flag means the opposite of what it says) is exactly what a schema canโt carry.
The ecosystemโs workarounds tell the story: deferred/lazy tool loading and tool-search, โcode execution against MCPโ (let the model write a script that calls tools instead of loading all the definitions), skills files that sit on top and encode the priesthood in prose, and now cacheable list responses so clients can keep tool catalogs stable across reconnects. All of these exist because the adapter did its job and the underlying APIs still werenโt usable. The good MCP servers are opinionated: a handful of task-shaped tools, not an endpoint mirror. That opinionating is human work, and itโs the same work the API vendor should have done a decade ago.
Inevitability: true for the category, contingent for the winner
The N-models ร M-tools problem forces an N + M solution the moment tool use goes mainstream; that part is close to a theorem. The graveyard proves demand: OpenAI plugins (2023, OpenAPI manifests), per-vendor function calling, per-framework tool abstractions. Why plain OpenAPI wasnโt enough is instructive: it describes endpoints rather than tasks, has no local-process story, no agent-shaped auth flow, no user-in-the-loop primitive, no way to push prompts or resources to the client.
What wasnโt inevitable is that this specific design won. That was open licensing, multi-language SDKs, early editor adoption, then OpenAI/Google/Microsoft signing on, then handing governance to the Linux Foundation. Adoption is now self-sustaining: close to half a billion SDK downloads a month, with both the TypeScript and Python SDKs past 1 billion total.
One quibble with your framing: itโs not that the machine intelligence canโt use your gnarly API. Give a frontier model a shell, curl, and the docs and it usually can. The problem is doing it cheaply, reproducibly, with scoped permissions and a human confirmation before the destructive call, a million times a day. MCP solves an operational problem more than a cognitive one. Which is why the strongest rival position isnโt โa different protocolโ but โjust give the agent a terminalโ โ and the stateless rewrite is MCP quietly conceding that the simpler side of that argument had a point.
MikeLev.in: Are there parallels between whatโs happening with MCP and what happened with MULTICS and Unix and the development of the so-called Unix way?
Fable 5 Extra: Yes, and the parallel is closer than the usual โbig system bad, small system goodโ gloss. Take it in layers.
The trimming. Multics (1964โ69) was the comprehensive design: MIT, GE, and Bell Labs building the computing utility of the future with security rings, dynamic linking, a single-level store where memory and files were the same thing, all written in PL/I. Bell Labs pulled out in 1969 because it was late, expensive, and heavy. Thompson and Ritchie kept the ideas that had earned their keep (hierarchical filesystem, shell as an ordinary program, the process model) and threw away the machinery. The name was the joke: Unics, a castrated Multics, one of whatever Multics was many of.
MCPโs first spec was its Multics. Bidirectional, stateful, sessions with a handshake, held-open SSE streams, a server that could turn around and ask the clientโs model for a completion, ask the user a question, ask what directories were in scope. All defensible for the setting it was designed in (one developer, one laptop, one editor). The July revision is the Unics moment: handshake gone, sessions gone, sampling and roots deprecated, every request self-describing, โno different from any other HTTP workload.โ Same arc, twenty months instead of five years.
The Unix Analogy: From Multics to the Shell
Explicit state over ambient state. Multicsโ single-level store was elegant: you never had to think about loading a file, it was just memory. Unix drew a hard line and made you hold file descriptors. The MCP maintainersโ advice for the stateless world is nearly word-for-word Unix: if you need state, mint an explicit handle from a tool and have the model pass it back. Donโt hide it in the transport; make it a visible token the model threads between calls. Thatโs a file descriptor.
The narrow waist. McIlroyโs formulation of the Unix way ends with the line that matters: use text streams because that is a universal interface. The power wasnโt in any program; it was in everyone agreeing on bytes and newlines so programs could be composed without prior arrangement. MCPโs real waist turned out to be smaller than its spec: tools/list, tools/call, JSON Schema. Resources and prompts were in the spec from day one and most clients never bothered implementing them well. In practice everything got flattened into tools, the same way Unix flattened devices, pipes, and sockets into file descriptors. The uniform primitive wins over the rich taxonomy.
โWorse is better.โ Richard Gabrielโs 1989 essay named it: the MIT approach (interface completeness, correctness) loses to the New Jersey approach (simplicity of implementation, even at the cost of a leaky interface) because the simple thing spreads and then improves. Bell Labs was in New Jersey. MRTR is pure New Jersey: rather than a server that calls back over a stream, the server returns โinput requiredโ and the client retries the whole call with answers attached. Uglier at the interface, trivially implementable behind a round-robin load balancer. Thatโs the trade Unix made every time.
Portability as the inevitability engine. Unix didnโt become inevitable in 1969; it became inevitable in 1973 when it was rewritten in C and could run on hardware Bell Labs didnโt own. MCPโs C moment was the SDKs in four languages plus OpenAI, Google, and Microsoft adopting it. In both cases the thing won not by being best but by being the one everybody could speak. And in both cases the openness was partly circumstantial: AT&T couldnโt sell software under its consent decree, so it licensed Unix to universities almost for free, which is how BSD and the whole ecosystem happened. Anthropic open-sourced deliberately and then handed it to the Linux Foundation, but the mechanism is the same: availability beat quality.
Where it strains. Three things.
Multics wasnโt a failure; it ran until 2000 and invented half of what we now call an operating system. The โbloated Multicsโ story is partly Unix propaganda. Same with stateful MCP: sampling and roots werenโt dumb, they were designed for a context that stopped being the dominant one. The spec changed because the deployment target moved from laptop to cloud, not because the original designers were wrong about laptops.
Unix was a schism. Two people with a spare PDP-7 walked away and built something else. MCPโs trimming was done by the same maintainers, through SEPs and working groups and a twelve-month deprecation policy. Unix-like outcome, Multics-like process. Thatโs new, and itโs a credit to the governance that criticism got absorbed rather than forked.
And the Unix way was never a spec. McIlroy wrote the principles down in 1978, a decade after the fact, extracting them from practice. The equivalent MCP principles (โfew tools, task-shaped, donโt mirror your endpointsโ) are being extracted the same way, from the pain, and theyโre not in the spec either.
The punchline. Which system is Multics depends on where you stand. Inside MCPโs history, the 2024 spec is Multics and the 2026 spec is Unix. Step outside and the picture flips: MCP is the designed, typed, schemaโd interface, and the agent with a bash prompt, curl, and a man page is the actual Unix philosophy walking around in 2026. Small tools, text streams, composition by script. โCode execution against MCP,โ where the model writes a program that calls tools instead of loading every definition, is a shell. Skills files are man pages. The stateless rewrite is MCP conceding ground to that side to stay relevant. Whether it ends up as the protocol agents speak, or as one more thing they call from a terminal, is the question the next two years answer.
MikeLev.in: Quoth the Fable 5:
MCP is the designed, typed, schemaโd interface, and the agent with a bash prompt, curl, and a man page is the actual Unix philosophy walking around in 2026. Small tools, text streams, composition by script.
This is my thesis and I want to make it my hypo-thesis and then upgrade it into a scientifically testable theory. Iโve got a system that you install like this:
curl -fsSL https://pipulate.com/install.sh | bash
โฆwhich when run results in this:
Last login: Sun Aug 30 04:25:14 on console
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...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 3297k 0 3297k 0 0 3413k 0 --:--:-- --:--:-- --:--:-- 10.3M
โ
Download complete.
๐ฆ Extracting source code...
โ
Extraction complete. Source code installed to '/Users/michaellevin/pipulate'.
๐ Now in directory: /Users/michaellevin/pipulate
๐ Setting up deployment key...
Fetching deployment key from https://pipulate.com/key.rot...
โ
Deployment key downloaded successfully.
๐ Deployment key file saved and secured.
๐ Starting Pipulate environment...
--------------------------------------------------------------
All set! Pipulate is installed at: /Users/michaellevin/pipulate
To use Pipulate in the future, simply run:
cd /Users/michaellevin/pipulate && nix develop -L
--------------------------------------------------------------
Setting up app identity as 'pipulate'...
โ
Application identity set.
Creating the universal ./run actuator...
This will activate the Nix development environment and
complete the 'magic cookie' transformation process.
๐ Booting the Forever Machine...
Please wait while the Nix environment hydrates (this may take a minute)...
python3> structuredAttrs is enabled
python3> created 17 symlinks in user environment
๐ Transforming installation into git repository...
Creating temporary clone in /tmp/nix-shell.41na0i/tmp.JuKB6o9S6b...
Cloning into '/tmp/nix-shell.41na0i/tmp.JuKB6o9S6b'...
remote: Enumerating objects: 369, done.
remote: Counting objects: 100% (369/369), done.
remote: Compressing objects: 100% (337/337), done.
remote: Total 369 (delta 23), reused 188 (delta 9), pack-reused 0 (from 0)
Receiving objects: 100% (369/369), 3.10 MiB | 20.89 MiB/s, done.
Resolving deltas: 100% (23/23), done.
Preserving app identity and credentials...
Creating backup of current directory in /tmp/nix-shell.41na0i/tmp.yveNATt1bi...
Moving git repository into place...
โ
Successfully transformed into git repository!
Original files backed up to: /tmp/nix-shell.41na0i/tmp.yveNATt1bi
Checking for updates...
Temporarily stashing local JupyterLab settings...
From https://github.com/pipulate/pipulate
* branch main -> FETCH_HEAD
Already up to date.
Updating remote URL to use SSH...
INFO: Setting up your personal Playground...
(Determinate Nix 3.19.1) 2.34.6 ยท Python 3.12.13 ยท v2.44 ยท ~/pipulate
๐ง Fresh install detected โ packages downloading (2-3 min)...
โ
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 โโโโโโโโโโโโโโโโโโฏ
Staying in 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 $ sources
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ reach outside this machine โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ
โ warm Connect your accounts and see which credentials are live. โ
โ botify Bring Botify crawl data and BQL query results into context. โ
โ confluence Bring a Confluence space, page, or search hit into context. โ
โ jira Bring a Jira project, issue, or JQL search into context. โ
โ slack Bring a Slack channel or message thread into context. โ
โ email Bring an email thread or a sender's threads into context. โ
โ sheets Bring a Google Sheet's tabs and cell data into context. โ
โ pu start the app server (long form: pipulate) โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ type any of these โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Add --help to any command above for its full usage.
Type brief to compile this workshop into your clipboard for an AI.
Type tools to list registry tools, or tools <name> to call one.
(nix:nix-shell-env) (nix) pipulate $
What do you think of that? What can you infer? What would you think you would do next? Is there anything confusing about this?
Fable 5 Extra: Short version: this is the most literal embodiment of the thesis I could have asked for, and the part doing the real work isnโt bash. Itโs Nix.
What I think
MCP exists to solve two problems: discovery (what can I call?) and heterogeneity (every machine is different, so invocation has to go through a protocol rather than a shell). Your installer attacks the second problem directly. If the environment is deterministic โ same 291 packages, same Python, same verbs, every time on every machine โ then heterogeneity is gone and --help is a perfectly good discovery protocol. The adapter becomes unnecessary because the thing it was adapting to stopped varying. Thatโs a sharper claim than โUnix philosophy good,โ and itโs the one Iโd build the hypothesis around.
Deterministic Environments as the Ultimate Protocol
The warm verb is the same move applied to auth: the human connects accounts once, credentials live in the environment, the agent inherits them ambiently. Thatโs the Unix answer to the problem MCP is on its fourth spec revision trying to solve. It trades the OAuth dance for โtrust the local machine,โ which is exactly the trade-off youโd want to make explicit when you write this up.
What I can infer
- Curl-pipe-bash pulls a tarball, then a Nix shell hook โtransformsโ it into a git clone, preserving identity and credentials. So the install is a bootstrap that replaces itself with a real repo. Clever; also the source of two of the confusions below.
- The app is a Nix flake around Python 3.12, JupyterLab, and a local web server (
pu), with aNotebooks/scratch area seeded with starter files. Notebook-first workflow with a server on the side. - The integrations (Botify, Confluence, Jira, Slack, email, Sheets) are verbs whose stated job is to โbring X into context.โ Not to act on those systems โ to read them into the session. Thatโs a read-mostly, context-assembly posture, which is a meaningful narrowing versus MCPโs read-write tools.
briefcompiles the whole workshop to clipboard for an external AI. So the intended loop is: local shell gathers context โ human pastes into a chat model โ model advises โ human acts locally. The AI is an advisor outside the environment, not an agent inside it.tools/tools <name>is a registry with callable entries. Thatโs an MCP-shaped thing living inside the Unix-shaped thing.- Botify plus the SEO-flavored starter kit tells me the domain is SEO tooling for practitioners who live in spreadsheets and crawl data.
What Iโd do next, as the thing dropped at that prompt
tools, then warm, then walk. In that order because as an agent Iโd want to know whatโs callable and whatโs authenticated before taking a tour. I would not run brief, because clipboard is a sink I canโt read. Which points at the main tension: the four-word menu is designed for a human who will paste to an AI, while the thesis youโre testing is about an AI that is the user at the prompt. Those are two different products. If the agent is the user, brief needs a --stdout (or just be the default when stdin isnโt a TTY), and the menu that says โwaiting for your choiceโ needs a non-interactive path, because an agent running nix develop in a subprocess will hang there.
Whatโs confusing
- Three overlapping menus. โFour words,โ then
sources, thentools.puappears in two of them and isnโt a source. Isbotifya tool in the registry, or something else? A newcomer, human or model, has to guess whethertools botifyandbotifyare the same thing. key.rot. Itโs fetched from a public URL, โsecured,โ and never explained. If itโs a ROT-obfuscated read-only deploy key that every installer downloads, it isnโt secret, and the language implies it is. If it unlocks anything real, a public fetch is a problem. Either way, one sentence saying what itโs for would remove the smell. โLocal-first, yours to deleteโ invites security-minded readers; give them the answer before they ask.- โUpdating remote URL to use SSH.โ You fetched updates over HTTPS, then switched origin to SSH. A user without GitHub SSH keys now has a repo whose next
git pullfails, and they were never asked. Either check for a key first or leave HTTPS as the default. - Two different โhow to come backโ instructions. The banner says
cd pipulate && nix develop -L; a few lines later it creates a โuniversal ./run actuator.โ Which one is canonical? And โactuatorโ is never mentioned again. - Vocabulary tax. Magic cookie, Forever Machine, hydrates, Playground, workshop, actuator. Charming for a human reading along. For an agent itโs tokens with no referent, and โworkshopโ is used as though it were defined. Iโd keep the charm in the banner and use plain nouns in the menus.
- Unlabeled version.
(Determinate Nix 3.19.1) 2.34.6 ยท Python 3.12.13 ยท v2.44โ the middle number has no name. Iโd guess upstream Nix, but a status line shouldnโt make me guess. - The choice-or-timeout ambiguity. The transcript shows the door menu and then โStaying in the shellโ with no visible keystroke. Did you press 2, or did it default? If it defaults, say so and after how long; if it waits forever, thatโs the hang from above.
- Noise. curl progress bars,
structuredAttrs is enabled, git delta counts. Fine for you. For an agent whose context you care about, a--quietinstall that prints only the checkmarks would cut the transcript by half.
Making it testable
You already have both arms of the experiment in one repo: the tools <name> registry (protocol-shaped invocation) and the bare verbs with --help (shell-shaped). Put a fixed model on the same task set through each arm and measure success rate, tokens consumed, turns to completion, and recovery after a deliberately injected error. The hypothesis becomes: for tasks in a reproducible environment, shell-shaped invocation matches or beats registry-shaped invocation on success while consuming fewer tokens. The confounder to control is description quality, since a badly written --help will lose to a well written schema and prove nothing. The theory version, the one that would generalize past Pipulate, is the Nix claim: discovery protocols are compensating for environmental variance, and their advantage shrinks as variance goes to zero. That predicts something checkable outside your system too โ MCP should win on random hosts and lose on deterministic ones โ which is what makes it a theory rather than a preference.
MikeLev.in: Okay before we get into the experiments, letโs do some cleanup based on your feedback but be sensitive to the fact that almost any edits to the AST of flake.nix risks breaking it given how many moving parts there are and how little else context of the project Iโm showing you here, but the part I am showing you is the install procedure and the full sub-IaC of the app. I say sub-IaC because when your host system is anything other than NixOS than Nix is a generic normalized Linux subsystem on whatever host system.
By the way, I pressed 2. The rot13 file is a public deploy key.
You can go ahead and give me non-breaking patches to address the more cosmetic problems you had given the new insight you now have, please and thank you.
Fable 5 Extra: The canary arrived linkified ([www.canary.invalid](https://www.canary.invalid)), so the transport did rewrite this payload. None of the SEARCH/REPLACE blocks below contain a www-prefixed token, so no patch is affected. Two lines in flake.nix carry [email redacted] (the pull alias and the git remote set-url line); I anchor around both and never quote them.
Context verified for the files Iโm patching (install.sh, .gitignore, install.md, and two echo-string edits in flake.nix). scripts/boot_menu.py and scripts/sources_menu.py are absent, which is where feedback items 1 (overlapping menus) and 7 (echo the pressed door) live, so those are deferred to next context rather than guessed at. Not blocking for this train.
What changed once I could read the source
Withdrawn. The unlabeled 2.34.6 in the readings line was a measured decision (89 โ 76 chars, the parenthetical is the discriminator), argued in the comment right above it. I take it back. The โUpdating remote URL to use SSHโ concern is also narrower than I said: itโs gated on ~/.ssh/id_rsa already existing, and a stranger without one gets the deploy key written there on entry one and the switch on entry two. The remaining edge is someone with an id_rsa that isnโt registered with GitHub. Whatโs left is purely that the line doesnโt say why it fired.
Now cosmetic, and patched below. The deploy-key wording (itโs public, pull-only, and the output should say so, including that it lands at ~/.ssh/id_rsa if you have no key there); โAll set!โ printed before the 2โ3 minute hydration; the ./run line that names an actuator nobody sees again; the hand-off lines; and the curl progress table, swapped for a one-line bar.
Two things I found reading the code that I didnโt have before.
.gitignoreignores.ssh/key; the installer writes.ssh/rot. Thatโs the?? .ssh/your own flake comment witnessed on the Mac, and it meansmโsgit add -Awould sweep the key file into a commit if anyone ever ran it from an installed copy. One-line fix, no ignition.- The ssh-config line in
miscSetupLogicisecho "Host github.com\n IdentityFile ...\n User git". Bashโs builtinechodoesnโt interpret\nwithout-e, and a bare\npasses through a Nix indented string literally (your PS1 line depends on exactly that). So~/.ssh/configgets one line of five whitespace-separated โhost patterns,โ none of which isgithub.com, and the block is inert. Itโs masked because the key is written to the default identity path anyway, so SSH finds it without the config. Real bug, not cosmetic; Iโve ridden it as its own optional car gated on the two cheapest probes, so you can skip it this turn if you want a cosmetic-only train.
Noted, not patched. The --impure question: the flake header, the install-only lane, and ./run all say macOS needs it; the banner command and the default hand-off omit it; your Mac transcript proves the omitted form works; and nothing in flake.nix reads the environment at eval time (no getEnv). It looks vestigial, and get_nix_develop_cmdโs โOS-specific adaptationโ comment describes a branch that isnโt there. A Mac-side probe would settle it; not this turn. Also: the ZIP comes from miklevin/pipulate and the clone from pipulate/pipulate, presumably a GitHub redirect. And install.md may end with a stray closing fence; thereโs a probe for that.
1. PROBES
bash -c 'echo "a\nb"' | cat -A
LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook | grep -n 'IdentityFile'
git check-ignore -v .ssh/rot
bash -n assets/installer/install.sh && echo INSTALL_SH_PARSES
grep -n -e 'All set!' -e 'universal ./run' -e 'saved and secured' assets/installer/install.sh
grep -n 'Updating remote URL' flake.nix
tail -n 3 /home/mike/repos/Pipulate.com/install.md
Line 1 falsifies the ssh-config bug: if it prints a\nb$ on one line, echo left the backslash-n literal and Car 5 is needed; two lines means xpg_echo is on and Car 5 is unnecessary. Line 2 proves the \n survives Nix into the generated hook (BEFORE shows echo "Host github.com\n, AFTER shows printf 'Host github.com\n), and doubles as the eval check for both flake cars; swap the system string for aarch64-darwin on the Mac. Line 3 is the .gitignore straddle: BEFORE prints nothing and exits 1, AFTER prints the matching rule. Line 4 is the syntax gate for Car 1. Lines 5 and 6 are the string straddles for Cars 1 and 4: three hits and one hit BEFORE, zero and zero AFTER. Line 7 decides whether install.md really ends with a stray fence or the compiler added it.
2. NEXT CONTEXT
! bash -c 'echo "a\nb"' | cat -A
! LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook | grep -n 'IdentityFile'
! git check-ignore -v .ssh/rot
! bash -n assets/installer/install.sh && echo INSTALL_SH_PARSES
! grep -n -e 'All set!' -e 'universal ./run' -e 'saved and secured' assets/installer/install.sh
! grep -n 'Updating remote URL' flake.nix
! tail -n 3 /home/mike/repos/Pipulate.com/install.md
scripts/boot_menu.py
scripts/sources_menu.py
assets/installer/mck.sh
3. PATCHES
Car 1 โ install.sh, six blocks, all echo strings plus one curl flag. I wrote โpull-onlyโ on the assumption the deploy key is read-only on GitHubโs side; if it isnโt, delete that word and we should talk. Drop the last block if you prefer curlโs full table to a one-line bar.
Target: assets/installer/install.sh
[[[SEARCH]]]
echo "๐ Setting up deployment key..."
mkdir -p .ssh
echo "Fetching deployment key from ${KEY_URL}..."
# Use curl to fetch the key from the URL and save it to .ssh/rot
if curl -L -sS --fail -o .ssh/rot "${KEY_URL}"; then
echo "โ
Deployment key downloaded successfully."
[[[DIVIDER]]]
echo "๐ Fetching the shared deploy key from ${KEY_URL}..."
echo " (Public, ROT13-encoded, pull-only: it exists so this folder can fetch"
echo " updates without a GitHub account. nix develop decodes it into"
echo " ~/.ssh/id_rsa only if no key is there already.)"
mkdir -p .ssh
# Use curl to fetch the key from the URL and save it to .ssh/rot
if curl -L -sS --fail -o .ssh/rot "${KEY_URL}"; then
echo "โ
Deploy key downloaded."
[[[REPLACE]]]
Target: assets/installer/install.sh
[[[SEARCH]]]
chmod 600 .ssh/rot # Important: Set permissions for the raw key file
echo "๐ Deployment key file saved and secured."
[[[DIVIDER]]]
chmod 600 .ssh/rot # Important: Set permissions for the raw key file
echo "๐ Deploy key saved as .ssh/rot (mode 600)."
[[[REPLACE]]]
Target: assets/installer/install.sh
[[[SEARCH]]]
echo "๐ Starting Pipulate environment..."
print_separator
echo " All set! Pipulate is installed at: ${TARGET_DIR} "
echo " To use Pipulate in the future, simply run: "
echo " cd ${TARGET_DIR} && ${NIX_DEVELOP_CMD} "
print_separator
[[[DIVIDER]]]
echo "๐ Starting the ${BANNER_NAME} environment..."
print_separator
echo " Source is in place at: ${TARGET_DIR} "
echo " To come back later, run: "
echo " cd ${TARGET_DIR} && ${NIX_DEVELOP_CMD} "
print_separator
[[[REPLACE]]]
Target: assets/installer/install.sh
[[[SEARCH]]]
# Creating the 'Double-Click' Actuator
echo "Creating the universal ./run actuator..."
[[[DIVIDER]]]
# Creating the 'Double-Click' Actuator
echo "Creating ./run -- a one-file shortcut for the cd-and-nix-develop line above."
[[[REPLACE]]]
Target: assets/installer/install.sh
[[[SEARCH]]]
echo "Hydrating the Nix environment (this may take a minute)..."
else
echo
echo "This will activate the Nix development environment and"
echo "complete the 'magic cookie' transformation process."
echo "๐ Booting the Forever Machine..."
echo "Please wait while the Nix environment hydrates (this may take a minute)..."
fi
[[[DIVIDER]]]
echo "Hydrating the Nix environment (this may take a minute)..."
else
echo
echo "Next, nix develop builds the environment and turns this folder into a"
echo "git repository (the 'magic cookie' step) so it can auto-update from now on."
echo "๐ Booting the Forever Machine..."
echo "Please wait while the Nix environment hydrates (2-3 minutes on a first install)..."
fi
[[[REPLACE]]]
Target: assets/installer/install.sh
[[[SEARCH]]]
curl -L --fail -o "${TMP_ZIP_FILE}" "${ZIP_URL}"
[[[DIVIDER]]]
curl -L --fail -# -o "${TMP_ZIP_FILE}" "${ZIP_URL}"
[[[REPLACE]]]
Ignition for Car 1: none for the local probe (bash -n reads the file at call time). For strangers, the ignition is release, which runs sync_install_sh and pushes to Pipulate.com; the Mac re-install only sees it after that.
Car 2 โ .gitignore. Keeps the stale .ssh/key line rather than deciding whether anything ever used it.
Target: .gitignore
[[[SEARCH]]]
temp/
.ssh/key
*kungfu_*.py
[[[DIVIDER]]]
temp/
.ssh/key
.ssh/rot
*kungfu_*.py
[[[REPLACE]]]
No ignition required; git reads it at call time.
Car 3 โ install.md. The page still promises an auto-opening JupyterLab tab; the two-door menu now stands in front of that.
Target: /home/mike/repos/Pipulate.com/install.md
[[[SEARCH]]]
**Success looks like:** The JupyterLab tab auto-opens in your browser. Run the Onboarding notebook top-to-bottom to unlock the Pipulate app (it opens automatically on future launches).
[[[DIVIDER]]]
**Success looks like:** A one-line environment reading, then a two-door menu. Press `1` to start JupyterLab and the app server (`2` gives you a bare shell with nothing running). The JupyterLab tab opens in your browser; run the Onboarding notebook top-to-bottom to unlock the Pipulate app (it opens automatically on future launches).
[[[REPLACE]]]
Target: /home/mike/repos/Pipulate.com/install.md
[[[SEARCH]]]
Wait for the JupyterLab tab to auto-open, then run the Onboarding notebook to unlock the Pipulate app.
[[[DIVIDER]]]
Press `1` at the two-door menu, wait for the JupyterLab tab to open, then run the Onboarding notebook to unlock the Pipulate app.
[[[REPLACE]]]
Ignition for Car 3: release.py does not sync install.md, so this needs its own commit and push in ~/repos/Pipulate.com before Jekyll serves it.
Car 4 โ flake.nix, cosmetic. The one flake edit that is purely wording; drop it without loss if you want zero flake churn. The SEARCH stops at the echo; the set-url line directly below it carries a redaction placeholder and is deliberately not quoted.
Target: flake.nix
[[[SEARCH]]]
if [ -d .git ] && [ -f ~/.ssh/id_rsa ]; then
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")
if [[ "$REMOTE_URL" == https://* ]]; then
echo "Updating remote URL to use SSH..."
[[[DIVIDER]]]
if [ -d .git ] && [ -f ~/.ssh/id_rsa ]; then
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")
if [[ "$REMOTE_URL" == https://* ]]; then
echo "Found ~/.ssh/id_rsa -- switching the git remote to SSH so auto-updates use it."
[[[REPLACE]]]
Car 5 โ flake.nix, optional, correctness not cosmetics. Ride only if probe 1 printed a\nb$ on one line. printf with a literal \n is already a proven idiom in this file (the Shared README block), so the Nix-string hazards are the same ones youโve already cleared.
Target: flake.nix
[[[SEARCH]]]
if ! grep -q "Host github.com" ~/.ssh/config 2>/dev/null; then
echo "Host github.com\n IdentityFile ~/.ssh/id_rsa\n User git" >> ~/.ssh/config
fi
[[[DIVIDER]]]
if ! grep -q "Host github.com" ~/.ssh/config 2>/dev/null; then
printf 'Host github.com\n IdentityFile ~/.ssh/id_rsa\n User git\n' >> ~/.ssh/config
fi
[[[REPLACE]]]
Ignition for Cars 4 and 5: exit, then nix develop. The shellHook is read once at entry, and apply.pyโs nix-instantiate --parse airlock proves parse only; probe 2 is what proves eval. Note that Car 5 only changes behavior on a machine with no ~/.ssh/id_rsa, so your existing installs wonโt exercise it; a fresh install would.
4. PROMPT
The cosmetic install-cleanup train has landed (check the ! receipts for which cars). Read the receipts first: the echo/printf straddle says whether Car 5 was needed; git check-ignore should now print the .ssh/rot rule; the two grep straddles should be empty. boot_menu.py, sources_menu.py and mck.sh are now in context. Address the two items deferred last turn: (a) the three overlapping menus -- the four words, the sources roster (which lists pu, not a source), and tools -- propose the smallest wording change that makes a newcomer know whether `botify` and `tools botify` are the same thing; (b) echo the chosen door so a transcript shows the keypress, and say whether boot_menu.py has a non-TTY path or would hang an agent running nix develop in a subprocess. Then state the cheapest Mac-side probe for whether --impure is still needed. After that we design the experiment: shell-verbs arm vs tools-registry arm, same model, same task set.
5. EXTERNAL DELIVERABLES
None. The Pipulate.com copy of install.sh is regenerated by release.py; install.md is a direct commit in that repo, named above as Car 3โs ignition.
MikeLev.in:
1: Probe:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ bash -c 'echo "a\nb"' | cat -A
LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook | grep -n 'IdentityFile'
git check-ignore -v .ssh/rot
bash -n assets/installer/install.sh && echo INSTALL_SH_PARSES
grep -n -e 'All set!' -e 'universal ./run' -e 'saved and secured' assets/installer/install.sh
grep -n 'Updating remote URL' flake.nix
tail -n 3 /home/mike/repos/Pipulate.com/install.md
a\nb$
978: echo "Host github.com\n IdentityFile ~/.ssh/id_rsa\n User git" >> ~/.ssh/config
INSTALL_SH_PARSES
225:echo "๐ Deployment key file saved and secured."
233:echo " All set! Pipulate is installed at: ${TARGET_DIR} "
247:echo "Creating the universal ./run actuator..."
1958: echo "Updating remote URL to use SSH..."
Welcome to the local-first revolution!
(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/ _ \ / _` | | |_| |/ _ \ / __| | | | |_| | | | | |_) | | A Shoshin exercise
# 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
# 3 scripts/foo_cartridge.py # Needs description
# 3 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 ---
#
# # server.py
#
# # STICKBUG & MOTHER CAT KATA
# scripts/connectors/README.md
# scripts/connectors/gmail.py
# scripts/connectors/confluence.py
# scripts/connectors/jira.py
# scripts/connectors/slack.py
# scripts/connectors/botify.py
# scripts/connectors/gsc.py
# scripts/connectors/sheets.py
# scripts/connectors/wallet.py
# scripts/connectors/mcp.py
# scripts/walk.py
# scripts/weblogin.py
# scripts/mother_cat.py
# assets/trails/first_context.yaml
# assets/trails/public_walk.yaml
# assets/trails/practice.yaml
# # assets/trails/botify_pageworkers.yaml
# assets/installer/replay.sh
# scripts/walk_cartridge.py
# scripts/boot_menu.py
# assets/installer/mck.sh
# scripts/walk_compile.py
# scripts/bookmark_import.py
# scripts/sources_menu.py
# tools/scraper_tools.py
# scripts/connectors/mcp_warm.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
#
# # Now that slack is working (on my workspace) we trim back things here to just the stick bug and Mother Cat Kata stuff
#
# # ! git status --short
# # ! .venv/bin/pytest tests/test_apply_airlock.py
# # ! ruff check tools/scraper_tools.py
# # GLOSSARY.md
# # foo_files.py
# # tools/scraper_tools.py
#
# # scripts/connectors/mcp_warm.py
#
# # ! rg -c 'STRUCTURALLY INCAPABLE of printing GREEN' foo_files.py; echo "old_claim_exit=$?"; rg -c 'SCOPED TO THE CREDENTIAL' foo_files.py; echo "new_claim_exit=$?"
# # ! n=$(ls browser_cache/mcp/*/*.json 2>/dev/null | wc -l); echo "receipts=$n"; jq -r '[.verb, (.dclass // "null"), (.tool // "-")] | @tsv' browser_cache/mcp/*/*.json 2>/dev/null | sort | uniq -c | sort -rn
# # scripts/connectors/mcp.py
# deleteme.txt
#
# /home/mike/repos/trimnoir/_posts/2026-08-28-disappearing-interfaces-de-branding-terminal-threshold.md # [Idx: 1419 | Order: 5 | Tokens: 22,694 | Bytes: 91,703]
# /home/mike/repos/trimnoir/_posts/2026-08-29-antidote-to-vibe-coding-unix-pipes-local-files.md # [Idx: 1420 | Order: 1 | Tokens: 14,408 | Bytes: 65,807]
# /home/mike/repos/trimnoir/_posts/2026-08-29-from-claude-connector-to-unix-instrument.md # [Idx: 1421 | Order: 2 | Tokens: 13,402 | Bytes: 63,621]
# /home/mike/repos/trimnoir/_posts/2026-08-29-terminal-native-mcp-without-the-bloat.md # [Idx: 1422 | Order: 3 | Tokens: 89,572 | Bytes: 341,094]
! bash -c 'echo "a\nb"' | cat -A
! LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook | grep -n 'IdentityFile'
! git check-ignore -v .ssh/rot
! bash -n assets/installer/install.sh && echo INSTALL_SH_PARSES
! grep -n -e 'All set!' -e 'universal ./run' -e 'saved and secured' assets/installer/install.sh
! grep -n 'Updating remote URL' flake.nix
! tail -n 3 /home/mike/repos/Pipulate.com/install.md
scripts/boot_menu.py
scripts/sources_menu.py
assets/installer/mck.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 'assets/installer/install.sh'.
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'assets/installer/install.sh'.
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'assets/installer/install.sh'.
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'assets/installer/install.sh'.
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'assets/installer/install.sh'.
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated 'assets/installer/install.sh'.
(nix) pipulate $ d
diff --git a/assets/installer/install.sh b/assets/installer/install.sh
index 4d0dbc5a..ec6af71f 100644
--- a/assets/installer/install.sh
+++ b/assets/installer/install.sh
@@ -167,7 +167,7 @@ echo "๐ฅ Downloading Pipulate source code..."
TMP_ZIP_FILE=$(mktemp)
# Ensure temp file is removed on exit
trap 'rm -f "$TMP_ZIP_FILE"' EXIT
-curl -L --fail -o "${TMP_ZIP_FILE}" "${ZIP_URL}"
+curl -L --fail -# -o "${TMP_ZIP_FILE}" "${ZIP_URL}"
echo "โ
Download complete."
echo
@@ -201,12 +201,14 @@ echo
# --- Deploy Key Setup ("Magic Cookie") ---
# Part of the "magic cookie" is the SSH key that will allow the flake
# to perform git operations without password prompts
-echo "๐ Setting up deployment key..."
+echo "๐ Fetching the shared deploy key from ${KEY_URL}..."
+echo " (Public, ROT13-encoded, pull-only: it exists so this folder can fetch"
+echo " updates without a GitHub account. nix develop decodes it into"
+echo " ~/.ssh/id_rsa only if no key is there already.)"
mkdir -p .ssh
-echo "Fetching deployment key from ${KEY_URL}..."
# Use curl to fetch the key from the URL and save it to .ssh/rot
if curl -L -sS --fail -o .ssh/rot "${KEY_URL}"; then
- echo "โ
Deployment key downloaded successfully."
+ echo "โ
Deploy key downloaded."
else
echo "โ Error: Failed to download deployment key from ${KEY_URL}."
# Optional: remove potentially incomplete key file
@@ -222,16 +224,16 @@ if [ ! -s .ssh/rot ]; then
fi
chmod 600 .ssh/rot # Important: Set permissions for the raw key file
-echo "๐ Deployment key file saved and secured."
+echo "๐ Deploy key saved as .ssh/rot (mode 600)."
echo
# --- Trigger Initial Nix Build & Git Conversion ---
# Now we hand over to nix develop, which will activate the flake
# The flake will handle converting this to a proper git repository
-echo "๐ Starting Pipulate environment..."
+echo "๐ Starting the ${BANNER_NAME} environment..."
print_separator
-echo " All set! Pipulate is installed at: ${TARGET_DIR} "
-echo " To use Pipulate in the future, simply run: "
+echo " Source is in place at: ${TARGET_DIR} "
+echo " To come back later, run: "
echo " cd ${TARGET_DIR} && ${NIX_DEVELOP_CMD} "
print_separator
echo
@@ -244,7 +246,7 @@ echo "โ
Application identity set."
echo
# Creating the 'Double-Click' Actuator
-echo "Creating the universal ./run actuator..."
+echo "Creating ./run -- a one-file shortcut for the cd-and-nix-develop line above."
cat > "${TARGET_DIR}/run" << 'EOL'
#!/usr/bin/env bash
cd "$(dirname "$0")"
@@ -280,10 +282,10 @@ if [ "${PIPULATE_INSTALL_ONLY:-0}" = "1" ]; then
echo "Hydrating the Nix environment (this may take a minute)..."
else
echo
- echo "This will activate the Nix development environment and"
- echo "complete the 'magic cookie' transformation process."
+ echo "Next, nix develop builds the environment and turns this folder into a"
+ echo "git repository (the 'magic cookie' step) so it can auto-update from now on."
echo "๐ Booting the Forever Machine..."
- echo "Please wait while the Nix environment hydrates (this may take a minute)..."
+ echo "Please wait while the Nix environment hydrates (2-3 minutes on a first install)..."
fi
# The Terminal Hand-off:
(nix) pipulate $ m
๐ Committing: chore: Fetch deploy key from URL
[main 044d92b2] chore: Fetch deploy key from URL
1 file changed, 14 insertions(+), 12 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated '.gitignore'.
(nix) pipulate $ d
diff --git a/.gitignore b/.gitignore
index 6038a96f..5ef70bec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,6 +63,7 @@ downloads/
logs/
temp/
.ssh/key
+.ssh/rot
*kungfu_*.py
foo.txt
service_account.txt
(nix) pipulate $ m
๐ Committing: chore: Add .ssh/rot to .gitignore
[main 2f858b82] chore: Add .ssh/rot to .gitignore
1 file changed, 1 insertion(+)
(nix) pipulate $ patch
(nix) pipulate $ app
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/Pipulate.com/install.md'.
โ
DETERMINISTIC PATCH APPLIED: Successfully mutated '/home/mike/repos/Pipulate.com/install.md'.
(nix) pipulate $ d
(nix) pipulate $ m
โ ai.py returned empty message
(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 a25d2f9c..7746118c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1955,7 +1955,7 @@ print('AI:\n', r.ai)
if [ -d .git ] && [ -f ~/.ssh/id_rsa ]; then
REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")
if [[ "$REMOTE_URL" == https://* ]]; then
- echo "Updating remote URL to use SSH..."
+ echo "Found ~/.ssh/id_rsa -- switching the git remote to SSH so auto-updates use it."
git remote set-url origin [email redacted]:pipulate/pipulate.git
fi
fi
(nix) pipulate $ m
๐ Committing: chore: Update remote URL message in flake.nix
[main d849092e] chore: Update remote URL message in flake.nix
1 file changed, 1 insertion(+), 1 deletion(-)
(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 7746118c..0d6254a2 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1975,7 +1975,7 @@ print('AI:\n', r.ai)
tr 'A-Za-z' 'N-ZA-Mn-za-m' < .ssh/rot > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
if ! grep -q "Host github.com" ~/.ssh/config 2>/dev/null; then
- echo "Host github.com\n IdentityFile ~/.ssh/id_rsa\n User git" >> ~/.ssh/config
+ printf 'Host github.com\n IdentityFile ~/.ssh/id_rsa\n User git\n' >> ~/.ssh/config
fi
if ! grep -q "github.com" ~/.ssh/known_hosts 2>/dev/null; then
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
(nix) pipulate $ m
๐ Committing: chore: Update ssh config generation
[main 9b343151] chore: Update ssh config generation
1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ git push
Enumerating objects: 20, done.
Counting objects: 100% (20/20), done.
Delta compression using up to 48 threads
Compressing objects: 100% (14/14), done.
Writing objects: 100% (14/14), 1.88 KiB | 1.88 MiB/s, done.
Total 14 (delta 9), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (9/9), completed with 5 local objects.
To github.com:pipulate/pipulate.git
7a9ea9e5..9b343151 main -> main
(nix) pipulate $
And over in that other repo:
(nix) Pipulate.com $ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: install.md
no changes added to commit (use "git add" and/or "git commit -a")
(nix) Pipulate.com $ git --no-pager diff
diff --git a/install.md b/install.md
index 41ae0db..c414050 100644
--- a/install.md
+++ b/install.md
@@ -27,7 +27,7 @@ cd ~/TestProject && nix develop
**What you get:** A local web app at `http://localhost:5001` with step-by-step workflows, integrated AI chat, and a JupyterLab instance at `http://localhost:8888`. No cloud required.
-**Success looks like:** The JupyterLab tab auto-opens in your browser. Run the Onboarding notebook top-to-bottom to unlock the Pipulate app (it opens automatically on future launches).
+**Success looks like:** A one-line environment reading, then a two-door menu. Press `1` to start JupyterLab and the app server (`2` gives you a bare shell with nothing running). The JupyterLab tab opens in your browser; run the Onboarding notebook top-to-bottom to unlock the Pipulate app (it opens automatically on future launches).
These few commands:
- โ
Updates to the latest version automatically
@@ -58,7 +58,7 @@ cd ~/TestProject
nix develop
[triple backticks]
-Wait for the JupyterLab tab to auto-open, then run the Onboarding notebook to unlock the Pipulate app.
+Press `1` at the two-door menu, wait for the JupyterLab tab to open, then run the Onboarding notebook to unlock the Pipulate app.
### Installation Strategy: Universal First, PyPI Alternative
(nix) Pipulate.com $ git commit -am "Fixed success looks like"
[main 266bdae] Fixed success looks like
1 file changed, 2 insertions(+), 2 deletions(-)
(nix) Pipulate.com $ 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), 489 bytes | 489.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:miklevin/Pipulate.com
75509f0..266bdae main -> main
(nix) Pipulate.com $
Okay, and the iginition. Well we touched flake.nix so this is necessary:
(nix:nix-shell-env) (nix) pipulate $ exit
exit
Saving session...
...saving history...truncating history files...
...completed.
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...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 3298k 0 3298k 0 0 4175k 0 --:--:-- --:--:-- --:--:-- 10.1M
โ
Download complete.
๐ฆ Extracting source code...
โ
Extraction complete. Source code installed to '/Users/michaellevin/pipulate'.
๐ Now in directory: /Users/michaellevin/pipulate
๐ Setting up deployment key...
Fetching deployment key from https://pipulate.com/key.rot...
โ
Deployment key downloaded successfully.
๐ Deployment key file saved and secured.
๐ Starting Pipulate environment...
--------------------------------------------------------------
All set! Pipulate is installed at: /Users/michaellevin/pipulate
To use Pipulate in the future, simply run:
cd /Users/michaellevin/pipulate && nix develop -L
--------------------------------------------------------------
Setting up app identity as 'pipulate'...
โ
Application identity set.
Creating the universal ./run actuator...
This will activate the Nix development environment and
complete the 'magic cookie' transformation process.
๐ Booting the Forever Machine...
Please wait while the Nix environment hydrates (this may take a minute)...
Restored session: Sun Aug 30 05:07:20 EDT 2026
๐ Transforming installation into git repository...
Creating temporary clone in /tmp/nix-shell.0PaAZg/tmp.zHy9V9pVs5...
Cloning into '/tmp/nix-shell.0PaAZg/tmp.zHy9V9pVs5'...
remote: Enumerating objects: 369, done.
remote: Counting objects: 100% (369/369), done.
remote: Compressing objects: 100% (337/337), done.
remote: Total 369 (delta 23), reused 188 (delta 9), pack-reused 0 (from 0)
Receiving objects: 100% (369/369), 3.10 MiB | 20.36 MiB/s, done.
Resolving deltas: 100% (23/23), done.
Preserving app identity and credentials...
Creating backup of current directory in /tmp/nix-shell.0PaAZg/tmp.VXhjL2tJPc...
Moving git repository into place...
โ
Successfully transformed into git repository!
Original files backed up to: /tmp/nix-shell.0PaAZg/tmp.VXhjL2tJPc
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.45 ยท ~/pipulate
๐ง Fresh install detected โ packages downloading (2-3 min)...
โ
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 โโโโโโโโโโโโโโโโโโฏ
Staying in 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 $
Fable 5 Extra: Canary arrived bare this time; the transport left the payload alone. Context verified for the two files Iโm patching (scripts/boot_menu.py, scripts/sources_menu.py); the install.md car below is a one-line actuator against a last line the tail receipt already witnessed, so it needs no file body.
Reading the receipts
All five cars landed and the straddles closed the way they were supposed to: echo left a\nb$ on one line, so Car 5 was needed and the generated hook now carries the printf; .ssh/rot is ignored at .gitignore:66; the old install.sh and flake strings are gone; the installer still parses. The d and m that printed nothing for the install.md car were both anchored to $PIPULATE_ROOT, so your manual commit in Pipulate.com was the right move.
The Mac transcript is a receipt too, and it says something specific: the flake carโs ignition fired (you can see Found ~/.ssh/id_rsa -- switching the git remote to SSH... and v2.45, both pulled from GitHub), but the served install.sh at 05:07 EDT was still the pre-patch copy, because release.pyโs sync_install_sh is the only thing that publishes it and the train ended with git push, not release. The telemetry shows a release commit on top of the train (AI_CONTEXT regenerated, pyproject bumped), so either that release ran after the Mac reinstall or GitHub Pages hadnโt rebuilt yet. Probes 1 and 2 below distinguish those; Car 4 is the publish actuator if itโs still stale.
The tail receipt confirms install.md ends with a stray bare fence. Kramdown needs a closing fence to make a code block, so an unclosed one at EOF renders as a literal ` [triple backticks] ` paragraph at the bottom of the install page. That canโt be fixed by SEARCH/REPLACE, since apply.py strips bare fence lines from the payload before matching, so it rides as a sed car.
The deferred items, with source in hand
(a) Overlapping menus. Two of my three complaints are withdrawn. pu in the sources roster is a recorded decision with its reasoning in TAIL, and the flake comment lays out the three namespaces cleanly: sources are things that pull material in, tools is the registry, mcp <server> is the real client. The one thing still missing is that nothing on screen says tools is a different set from the words above it. tools botify fails because botify is a connector script, not a registry tool; the registry is the 21 in-app functions (browser_scrape_page, keychain_*, execute_shell_commandโฆ). One footer line fixes it, and it happens to name the exact seam the experiment will sit on.
(b) Echo the door. tty.setraw swallows the keypress, so the transcript shows the panel and then โStaying in the shellโ with no visible cause. The smallest fix puts the door number on the result line: [2] Just the shell. Nothing started... and [1] Starting {name}.... On the non-TTY question, boot_menu.py already answers it thoroughly and the answer matters for the thesis: no TTY means exit 0, which means door 1, which means python server.py in the foreground. So an agent that runs a bare nix develop in a subprocess doesnโt hang at the menu; it hangs at the server, by design (the Desktop 7 tab depends on it). The agent lane is nix develop .#quiet --command ..., which mck.sh already demonstrates. For the experiment, thatโs the only invocation the shell arm should ever be given.
(c) --impure. No Mac probe needed. Purity is a property of the flake evaluation, not of which attribute you select, and your Mac ran a pure nix develop -L on the default shell twice in one morning. That proves the flake evaluates pure on aarch64-darwin, which covers .#quiet too. So the Darwin branches in ./run, install.shโs PIPULATE_INSTALL_ONLY lane, mck.shโs NIXWRAP, and the โmacOS โimpure exceptionโ the brief prompt teaches are all vestigial. Four locations, three files, one later car; not this turn.
On the experiment, before next turn
The two arms arenโt symmetric yet, and thatโs the design problem to solve before measuring anything. The sources expose seven capabilities (Jira, Slack, Botifyโฆ). The registry exposes twenty-one, and only web scraping overlaps. Worse, the registry contains execute_shell_command, so a model in the registry arm can escape into the shell arm whenever it likes, which contaminates the comparison. The fix is a thin 1:1 wrapper that registers each connector as a registry tool (same backend, two invocation shapes) and withholds execute_shell_command from the registry arm. That needs cli.py, one connector, and one tool file in context, which is what the next compile pulls.
1. PROBES
cmp -s assets/installer/install.sh /home/mike/repos/Pipulate.com/install.sh && echo COM_COPY_IN_SYNC || echo COM_COPY_STALE
curl -fsSL https://pipulate.com/install.sh | grep -c 'Deploy key saved'
.venv/bin/python -c "print(sum(l.startswith(chr(96)*3) for l in open('/home/mike/repos/Pipulate.com/install.md')))"
.venv/bin/python scripts/sources_menu.py | tail -n 4
rg -n -F -e 'Staying in the shell' -e '[2] Just the shell' .
Line 1 says whether sync_install_sh has run since the train; line 2 says whether the served copy has the new string (1) or the old (0). IN_SYNC with 0 is a Pages rebuild in flight; STALE means Car 4 is needed. Line 3 counts fence-opening lines in install.md: BEFORE is 17 (odd, unbalanced), AFTER is 16. Line 4 is the Car 1 straddle: BEFORE shows the panelโs bottom border plus three footer lines, AFTER shows four footer lines. Line 5 is the Car 2 straddle and its falsifier in one: BEFORE shows the old string in boot_menu.py plus any doc or test that quotes it, AFTER shows the new string; anything else that still matches is a label to move in a follow-up.
2. NEXT CONTEXT
! cmp -s assets/installer/install.sh /home/mike/repos/Pipulate.com/install.sh && echo COM_COPY_IN_SYNC || echo COM_COPY_STALE
! curl -fsSL https://pipulate.com/install.sh | grep -c 'Deploy key saved'
! .venv/bin/python -c "print(sum(l.startswith(chr(96)*3) for l in open('/home/mike/repos/Pipulate.com/install.md')))"
! .venv/bin/python scripts/sources_menu.py | tail -n 4
! rg -n -F -e 'Staying in the shell' -e '[2] Just the shell' .
cli.py
scripts/connectors/README.md
scripts/connectors/botify.py
tools/scraper_tools.py
scripts/boot_menu.py
scripts/sources_menu.py
3. PATCHES
Car 1 โ sources_menu.py, one footer line. 75 characters, under the 80-column budget the file already enforces on rows.
Target: scripts/sources_menu.py
[[[SEARCH]]]
FOOTER = [
"Add --help to any command above for its full usage.",
"Type brief to compile this workshop into your clipboard for an AI.",
"Type tools to list registry tools, or tools <name> to call one.",
]
[[[DIVIDER]]]
FOOTER = [
"Add --help to any command above for its full usage.",
"Type brief to compile this workshop into your clipboard for an AI.",
"Type tools to list registry tools, or tools <name> to call one.",
"(Registry tools live inside the app; the sources above are shell commands.)",
]
[[[REPLACE]]]
No ignition required; sources runs the file at call time, and probe 4 runs it directly.
Car 2 โ boot_menu.py, the door echo. The door number is the receipt of the keypress class, which is all a transcript needs; q, Esc, and Ctrl+C all correctly print [2], and a timeout correctly prints [1].
Target: scripts/boot_menu.py
[[[SEARCH]]]
if choice == EXIT_SHELL:
print("Staying in 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))
else:
print(f"Starting {name}...")
[[[DIVIDER]]]
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))
else:
print(f"[1] Starting {name}...")
[[[REPLACE]]]
Ignition for Car 2: probe 5 reads the file at call time, but the menu itself only runs at entry, and the ! executor has no TTY so it can never render it. The witnessed AFTER is you: exit, then nix develop, press 2, and the transcript shows [2].
Car 3 โ install.md stray fence, a sed car. Written with \x60 so no literal triple backtick travels through a renderer or a fence stripper; GNU sed on the dev box understands it. Deletes the last line only if it is a bare fence, so itโs a no-op if something else is there.
sed -i '$ { /^\x60\x60\x60[[:space:]]*$/d }' /home/mike/repos/Pipulate.com/install.md
Ignition for Car 3: as before, a commit and push in ~/repos/Pipulate.com; release.py does not carry install.md.
Car 4 โ publish install.sh, conditional. Ride only if probe 1 says COM_COPY_STALE. This is the same lane a full release uses, minus PyPI, version sync, and the docs steps; the git phase finds a clean tree and exits 0 after the sync has pushed. (sync_workspace_tree_to_com still runs and is a no-op when current.)
python release.py --skip-version-sync --skip-docs-sync --skip-audit-sync --skip-ai-context-sync --skip-breadcrumb-sync --skip-trifecta-rebuild
Ignition for Car 4 is the GitHub Pages rebuild, which takes a few minutes; probe 2 turning from 0 to 1 is the AFTER, and the Mac rm -rf + curl | bash after that is the human witness that the new strings, the one-line progress bar, and the [2] echo all appear on a strangerโs first contact.
4. PROMPT
Read the receipts first. Probe 1/2 say whether the served install.sh caught up (IN_SYNC and 1 = done; STALE = Car 4 was needed, check it ran). Probe 3 should read 16. Probe 4 should show four footer lines. Probe 5 should show only the new [2] string in boot_menu.py; if anything else still quotes 'Staying in the shell', name it as a label to move. Then the experiment. cli.py, connectors/README.md, one connector (botify.py) and one tool file (scraper_tools.py) are in context. Design the two-arm test: Arm S is the shell verb reached through `nix develop .#quiet --command`, Arm R is `cli.py call <tool> --json-args`. Specify (1) the thin wrapper that registers each connector as a registry tool so both arms expose the same seven capabilities from the same backend, (2) how execute_shell_command is withheld from Arm R so it cannot escape into Arm S, (3) a five-task read-only task set with a deterministic pass/fail check per task, (4) the four metrics and how each is captured from the transcript, (5) the description-quality control: the --help text and the tool description must be generated from the same docstring, the way sources_menu.py already does it. Patches for the wrapper only if the context supports exact-match; otherwise name the missing files. The --impure removal (four sites, three files) is a separate later car; do not fold it in.
5. EXTERNAL DELIVERABLES
None. Both Pipulate.com writes (the fence removal and the installer sync) are named above as Car 3โs and Car 4โs ignitions.
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 $ cmp -s assets/installer/install.sh /home/mike/repos/Pipulate.com/install.sh && echo COM_COPY_IN_SYNC || echo COM_COPY_STALE
curl -fsSL https://pipulate.com/install.sh | grep -c 'Deploy key saved'
.venv/bin/python -c "print(sum(l.startswith(chr(96)*3) for l in open('/home/mike/repos/Pipulate.com/install.md')))"
.venv/bin/python scripts/sources_menu.py | tail -n 4
rg -n -F -e 'Staying in the shell' -e '[2] Just the shell' .
COM_COPY_IN_SYNC
1
19
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ type any of these โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Add --help to any command above for its full usage.
Type brief to compile this workshop into your clipboard for an AI.
Type tools to list registry tools, or tools <name> to call one.
./scripts/boot_menu.py
7: [2] Just the shell NOTHING starts; `walk`, `sources`, `brief`, `seed`, `foo` live here
234: print("Staying in the shell. Nothing started -- no JupyterLab, no server.")
(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/ _ \ / _` | | |_| |/ _ \ / __| | | | |_| | | | | |_) | | Shoshinification! I love that word, the cure to enshitification.
# 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
# 3 scripts/foo_cartridge.py # Needs description
# 3 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 ---
#
# # server.py
#
# # STICKBUG & MOTHER CAT KATA
# scripts/connectors/README.md
# scripts/connectors/gmail.py
# scripts/connectors/confluence.py
# scripts/connectors/jira.py
# scripts/connectors/slack.py
# scripts/connectors/botify.py
# scripts/connectors/gsc.py
# scripts/connectors/sheets.py
# scripts/connectors/wallet.py
# scripts/connectors/mcp.py
# scripts/walk.py
# scripts/weblogin.py
# scripts/mother_cat.py
# assets/trails/first_context.yaml
# assets/trails/public_walk.yaml
# assets/trails/practice.yaml
# # assets/trails/botify_pageworkers.yaml
# assets/installer/replay.sh
# scripts/walk_cartridge.py
# scripts/boot_menu.py
# assets/installer/mck.sh
# scripts/walk_compile.py
# scripts/bookmark_import.py
# scripts/sources_menu.py
# tools/scraper_tools.py
# scripts/connectors/mcp_warm.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
#
# # Now that slack is working (on my workspace) we trim back things here to just the stick bug and Mother Cat Kata stuff
#
# # ! git status --short
# # ! .venv/bin/pytest tests/test_apply_airlock.py
# # ! ruff check tools/scraper_tools.py
# # GLOSSARY.md
# # foo_files.py
# # tools/scraper_tools.py
#
# # scripts/connectors/mcp_warm.py
#
# # ! rg -c 'STRUCTURALLY INCAPABLE of printing GREEN' foo_files.py; echo "old_claim_exit=$?"; rg -c 'SCOPED TO THE CREDENTIAL' foo_files.py; echo "new_claim_exit=$?"
# # ! n=$(ls browser_cache/mcp/*/*.json 2>/dev/null | wc -l); echo "receipts=$n"; jq -r '[.verb, (.dclass // "null"), (.tool // "-")] | @tsv' browser_cache/mcp/*/*.json 2>/dev/null | sort | uniq -c | sort -rn
# # scripts/connectors/mcp.py
# deleteme.txt
#
# /home/mike/repos/trimnoir/_posts/2026-08-28-disappearing-interfaces-de-branding-terminal-threshold.md # [Idx: 1419 | Order: 5 | Tokens: 22,694 | Bytes: 91,703]
# /home/mike/repos/trimnoir/_posts/2026-08-29-antidote-to-vibe-coding-unix-pipes-local-files.md # [Idx: 1420 | Order: 1 | Tokens: 14,408 | Bytes: 65,807]
# /home/mike/repos/trimnoir/_posts/2026-08-29-from-claude-connector-to-unix-instrument.md # [Idx: 1421 | Order: 2 | Tokens: 13,402 | Bytes: 63,621]
# /home/mike/repos/trimnoir/_posts/2026-08-29-terminal-native-mcp-without-the-bloat.md # [Idx: 1422 | Order: 3 | Tokens: 89,572 | Bytes: 341,094]
! cmp -s assets/installer/install.sh /home/mike/repos/Pipulate.com/install.sh && echo COM_COPY_IN_SYNC || echo COM_COPY_STALE
! curl -fsSL https://pipulate.com/install.sh | grep -c 'Deploy key saved'
! .venv/bin/python -c "print(sum(l.startswith(chr(96)*3) for l in open('/home/mike/repos/Pipulate.com/install.md')))"
! .venv/bin/python scripts/sources_menu.py | tail -n 4
! rg -n -F -e 'Staying in the shell' -e '[2] Just the shell' .
cli.py
scripts/connectors/README.md
scripts/connectors/botify.py
tools/scraper_tools.py
scripts/boot_menu.py
scripts/sources_menu.py
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/sources_menu.py'.
(nix) pipulate $ d
diff --git a/scripts/sources_menu.py b/scripts/sources_menu.py
index 1bc6db40..f134d01d 100644
--- a/scripts/sources_menu.py
+++ b/scripts/sources_menu.py
@@ -104,6 +104,7 @@ FOOTER = [
"Add --help to any command above for its full usage.",
"Type brief to compile this workshop into your clipboard for an AI.",
"Type tools to list registry tools, or tools <name> to call one.",
+ "(Registry tools live inside the app; the sources above are shell commands.)",
]
(nix) pipulate $ m
๐ Committing: chore: Add clarification to sources menu footer
[main dedbec36] chore: Add clarification to sources menu footer
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 32f2dee5..55b4fd96 100644
--- a/scripts/boot_menu.py
+++ b/scripts/boot_menu.py
@@ -231,14 +231,14 @@ def main() -> int:
print()
if choice == EXIT_SHELL:
- print("Staying in the shell. Nothing started -- no JupyterLab, no server.")
+ 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))
else:
- print(f"Starting {name}...")
+ print(f"[1] Starting {name}...")
return choice
(nix) pipulate $ m
๐ Committing: chore: Refine boot menu output messages
[main c0fd528a] chore: Refine boot menu output messages
1 file changed, 2 insertions(+), 2 deletions(-)
(nix) pipulate $ sed -i '$ { /^\x60\x60\x60[[:space:]]*$/d }' /home/mike/repos/Pipulate.com/install.md
(nix) pipulate $ git push
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 48 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 843 bytes | 843.00 KiB/s, done.
Total 8 (delta 6), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (6/6), completed with 4 local objects.
To github.com:pipulate/pipulate.git
305137bc..c0fd528a main -> main
(nix) pipulate $
And the other repo:
(nix) Pipulate.com $ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: install.md
no changes added to commit (use "git add" and/or "git commit -a")
(nix) Pipulate.com $ git diff
diff --git a/install.md b/install.md
index c414050..e81dce9 100644
--- a/install.md
+++ b/install.md
@@ -233,4 +233,3 @@ Once installed, use the repository as the source of truth:
- **[GitHub Issues](https://github.com/miklevin/pipulate/issues)** - Bug reports and questions
**Welcome to the local-first revolution!**
-[triple backticks]
(nix) Pipulate.com $ git commit -am "Rendering thing"
[main 7e39353] Rendering thing
1 file changed, 1 deletion(-)
(nix) Pipulate.com $ 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), 288 bytes | 288.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:miklevin/Pipulate.com
ccfb05e..7e39353 main -> main
(nix) Pipulate.com $
Okay ignition is coming, but first I want to do a release. I have to cut down
the output of release.py so Iโm more encouraged to include its output here
(currently itโs very long) but I revved the version number but Iโll include it
this time:
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'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: __init__.py
no changes added to commit (use "git add" and/or "git commit -a")
(nix) pipulate $ d
diff --git a/__init__.py b/__init__.py
index 44f594d6..3438a515 100644
--- a/__init__.py
+++ b/__init__.py
@@ -12,7 +12,7 @@ Usage:
pipulate
"""
-__version__ = "2.45"
+__version__ = "2.46"
# APOSTROPHES RESTORED (2026-08-04). They were stripped as a workaround for
# flake.nix's descMatch regex, whose character class excluded ' from the
# CAPTURE and truncated the banner to "(So)". That regex was fixed in the same
@@ -23,7 +23,7 @@ __version__ = "2.45"
# is not a property of the system; the regex is. Blast radius is one banner:
# nothing but flake.nix reads this name -- version_sync.py syncs __version__
# and __description__, never this. So'wI' chu' -- "engage the cloaking device."
-__version_description__ = "Shoshinification"
+__version_description__ = "Shoshinification Mac"
# SPDX expression, single source of truth, synced into pyproject.toml by
# scripts/release/version_sync.py. "-or-later" (not bare AGPL-3.0, which is
# deprecated SPDX) because the header below grants "any later version".
(nix) pipulate $ m
๐ Committing: chore: update __version__ to 2.46
[main 9823d81e] chore: update __version__ to 2.46
1 file changed, 2 insertions(+), 2 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), 322 bytes | 322.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
c0fd528a..9823d81e main -> main
(nix) pipulate $ release
[A ton of stuff deleted]
View at:
https://pypi.org/project/pipulate/2.46/
๐ Successfully published version 2.46 to PyPI! ๐
๐ View at: https://pypi.org/project/pipulate/2.46/
==================================================
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ๐ Release Pipeline Complete โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ
โ ๐ Pipulate Release Summary โ
โ โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโฎ โ
โ โ Component โ Details โ Status โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโค โ
โ โ ๐ค gemma3:latest Message โ fix: bump pipulate version to 2.46 โ โจ โ โ
โ โ โ โ gemma3:lateโฆ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโค โ
โ โ ๐ฆ Version โ 2.46 โ โ
Set โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโค โ
โ โ ๐ PyPI Release โ https://pypi.org/project/pipulate/2.46/ โ โ
Live โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโค โ
โ โ โฐ Completed โ 2026-08-30 05:25:53 โ ๐ฏ Done โ โ
โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโฏ โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
๐ Triggering server restart for immediate Chip interaction...
โ
Server restart triggered - you can now chat with Chip about this update!
(nix) pipulate $
Oh whoops! It had its own command for me to run:
(nix) pipulate $ python release.py --skip-version-sync --skip-docs-sync --skip-audit-sync --skip-ai-context-sync --skip-breadcrumb-sync --skip-trifecta-rebuild
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ๐ฐ ASCII Art Wax Seal (your vibe-coding safety-net) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ
โ ( Like a canary you say? ) โ
โ O /) ____ The "No Problem" Framework โ
โ > I HEREBY WILL NOT RE-GENERATE o /)\__// / \ Pipulate - Protecting Your Code โ
โ > Once upon machines be smarten ___(/_ 0 0 | | just by being honest about text. โ
โ > ASCII sealing immutata art in *( ==(_T_)== NPvg | (If mangled, then AI drifted.) โ
โ > This here cony if it's broken \ ) ""\ | | https://pipulate.com โ
โ > Smokin gun drift now in token |__>-\_>_> \____/ ๐ฅ๐ฅ๐ฅ โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
๐ Pipulate Master Release Orchestrator
==================================================
๐ Current version: 2.46
๐ Validating git remote configuration...
๐ Running: git rev-parse --git-dir in /home/mike/repos/pipulate
๐ Running: git remote -v in /home/mike/repos/pipulate
๐ Running: git branch --show-current in /home/mike/repos/pipulate
โ
Git validation passed:
๐ Current branch: main
๐ Remote 'origin' configured
๐ Running: git rev-parse --abbrev-ref main@{upstream} in /home/mike/repos/pipulate
โฌ๏ธ Upstream: origin/main
๐ง === RELEASE PIPELINE: PREPARATION PHASE ===
โญ๏ธ Skipping version synchronization (--skip-version-sync)
โญ๏ธ Skipping Waxascii release stamping (--skip-docs-sync)
โญ๏ธ Skipping AI_CONTEXT.md regeneration (--skip-docs-sync)
โญ๏ธ Skipping documentation synchronization (--skip-docs-sync)
๐ Step 3: Synchronizing install.sh to Pipulate.com...
๐ Copied install.sh to /home/mike/repos/Pipulate.com/install.sh
๐ Running: git status --porcelain install.sh in /home/mike/repos/Pipulate.com
โ
install.sh is already up-to-date in Pipulate.com repo.
๐ Step 3: Synchronizing mck.sh to Pipulate.com...
๐ Copied mck.sh to /home/mike/repos/Pipulate.com/mck.sh
๐ Running: git status --porcelain mck.sh in /home/mike/repos/Pipulate.com
โ
mck.sh is already up-to-date in Pipulate.com repo.
โญ๏ธ Skipping AUDIT.md synchronization (--skip-audit-sync)
โญ๏ธ Skipping AI_CONTEXT.md synchronization (--skip-ai-context-sync)
๐๏ธ Step 3.7: Splicing workspace tree into Pipulate.com/index.md...
โ
index.md workspace tree is already up-to-date.
โญ๏ธ Skipping breadcrumb trail synchronization (--skip-breadcrumb-sync)
โญ๏ธ Skipping Trifecta derivative rebuilding (--skip-trifecta-rebuild)
๐ === RELEASE PIPELINE: GIT OPERATIONS PHASE ===
๐ Running: git status --porcelain in /home/mike/repos/pipulate
โ
No changes to commit. Your repository is clean.
๐ก Use --force to proceed anyway, or make some changes first.
(nix) pipulate $
4: Prompt: Read the receipts first. Probe 1/2 say whether the served install.sh caught up (IN_SYNC and 1 = done; STALE = Car 4 was needed, check it ran). Probe 3 should read 16. Probe 4 should show four footer lines. Probe 5 should show only the new [2] string in boot_menu.py; if anything else still quotes โStaying in the shellโ, name it as a label to move. Then the experiment. cli.py, connectors/README.md, one connector (botify.py) and one tool file (scraper_tools.py) are in context. Design the two-arm test: Arm S is the shell verb reached through nix develop .#quiet --command, Arm R is cli.py call <tool> --json-args. Specify (1) the thin wrapper that registers each connector as a registry tool so both arms expose the same seven capabilities from the same backend, (2) how execute_shell_command is withheld from Arm R so it cannot escape into Arm S, (3) a five-task read-only task set with a deterministic pass/fail check per task, (4) the four metrics and how each is captured from the transcript, (5) the description-quality control: the โhelp text and the tool description must be generated from the same docstring, the way sources_menu.py already does it. Patches for the wrapper only if the context supports exact-match; otherwise name the missing files. The โimpure removal (four sites, three files) is a separate later car; do not fold it in.
5: Deliverables: None this turn.
Howโd we do? Oh yeah, the Mac testโฆ
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
โ Error: Directory '/Users/michaellevin/pipulate' already exists.
The installer cannot proceed when the target directory already exists.
This prevents accidental overwrites of existing data.
To resolve this, you can:
1. Choose a different name: curl -fsSL https://pipulate.com/install.sh | bash -s your-custom-name
2. Remove the existing directory: rm -rf /Users/michaellevin/pipulate
3. Rename the existing directory: mv /Users/michaellevin/pipulate /Users/michaellevin/pipulate.backup
Note: The existing directory appears to be a Pipulate installation.
You can start it directly with: cd /Users/michaellevin/pipulate && nix develop -L
michaellevin@MichaelMacBook-Pro ~ % rm -rf pipulate
michaellevin@MichaelMacBook-Pro ~ % clear
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 (2-3 minutes on a first install)...
Restored session: Sun Aug 30 05:28:13 EDT 2026
๐ Transforming installation into git repository...
Creating temporary clone in /tmp/nix-shell.KpZns5/tmp.ISxQk0jbgy...
Cloning into '/tmp/nix-shell.KpZns5/tmp.ISxQk0jbgy'...
remote: Enumerating objects: 369, done.
remote: Counting objects: 100% (369/369), done.
remote: Compressing objects: 100% (337/337), done.
remote: Total 369 (delta 23), reused 188 (delta 9), pack-reused 0 (from 0)
Receiving objects: 100% (369/369), 3.10 MiB | 13.18 MiB/s, done.
Resolving deltas: 100% (23/23), done.
Preserving app identity and credentials...
Creating backup of current directory in /tmp/nix-shell.KpZns5/tmp.vGEEjg22zx...
Moving git repository into place...
โ
Successfully transformed into git repository!
Original files backed up to: /tmp/nix-shell.KpZns5/tmp.vGEEjg22zx
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.46 ยท ~/pipulate
๐ง Fresh install detected โ packages downloading (2-3 min)...
โ
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 $
Now howโd we do?
MikeLev.in:
1: Probe:
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
(nix) pipulate $ .venv/bin/python -c "print(sum(l.startswith(chr(96)*3) for l in open('/home/mike/repos/Pipulate.com/install.md')))"
.venv/bin/python scripts/sources_menu.py | tail -n 4
sed -n '7p' scripts/boot_menu.py
rg -n -e "'build'" -e "'twine'" -e 'Running:' release.py
18
Add --help to any command above for its full usage.
Type brief to compile this workshop into your clipboard for an AI.
Type tools to list registry tools, or tools <name> to call one.
(Registry tools live inside the app; the sources above are shell commands.)
[2] Just the shell NOTHING starts; `walk`, `sources`, `brief`, `seed`, `foo` live here
51: print(f"๐ Running: {' '.join(cmd) if not shell else cmd} in {cwd}")
1392: run_command([".venv/bin/python", '-m', 'build'])
1394: run_command([".venv/bin/python", '-m', 'twine', 'upload', 'dist/*'])
(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/ _ \ / _` | | |_| |/ _ \ / __| | | | |_| | | | | |_) | | Shoshinification saga continues
# 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
# 3 scripts/foo_cartridge.py # Needs description
# 3 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 ---
#
# # server.py
#
# # STICKBUG & MOTHER CAT KATA
# scripts/connectors/README.md
# scripts/connectors/gmail.py
# scripts/connectors/confluence.py
# scripts/connectors/jira.py
# scripts/connectors/slack.py
# scripts/connectors/botify.py
# scripts/connectors/gsc.py
# scripts/connectors/sheets.py
# scripts/connectors/wallet.py
# scripts/connectors/mcp.py
# scripts/walk.py
# scripts/weblogin.py
# scripts/mother_cat.py
# assets/trails/first_context.yaml
# assets/trails/public_walk.yaml
# assets/trails/practice.yaml
# # assets/trails/botify_pageworkers.yaml
# assets/installer/replay.sh
# scripts/walk_cartridge.py
# scripts/boot_menu.py
# assets/installer/mck.sh
# scripts/walk_compile.py
# scripts/bookmark_import.py
# scripts/sources_menu.py
# tools/scraper_tools.py
# scripts/connectors/mcp_warm.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
#
# # Now that slack is working (on my workspace) we trim back things here to just the stick bug and Mother Cat Kata stuff
#
# # ! git status --short
# # ! .venv/bin/pytest tests/test_apply_airlock.py
# # ! ruff check tools/scraper_tools.py
# # GLOSSARY.md
# # foo_files.py
# # tools/scraper_tools.py
#
# # scripts/connectors/mcp_warm.py
#
# # ! rg -c 'STRUCTURALLY INCAPABLE of printing GREEN' foo_files.py; echo "old_claim_exit=$?"; rg -c 'SCOPED TO THE CREDENTIAL' foo_files.py; echo "new_claim_exit=$?"
# # ! n=$(ls browser_cache/mcp/*/*.json 2>/dev/null | wc -l); echo "receipts=$n"; jq -r '[.verb, (.dclass // "null"), (.tool // "-")] | @tsv' browser_cache/mcp/*/*.json 2>/dev/null | sort | uniq -c | sort -rn
# # scripts/connectors/mcp.py
# deleteme.txt
#
# /home/mike/repos/trimnoir/_posts/2026-08-28-disappearing-interfaces-de-branding-terminal-threshold.md # [Idx: 1419 | Order: 5 | Tokens: 22,694 | Bytes: 91,703]
# /home/mike/repos/trimnoir/_posts/2026-08-29-antidote-to-vibe-coding-unix-pipes-local-files.md # [Idx: 1420 | Order: 1 | Tokens: 14,408 | Bytes: 65,807]
# /home/mike/repos/trimnoir/_posts/2026-08-29-from-claude-connector-to-unix-instrument.md # [Idx: 1421 | Order: 2 | Tokens: 13,402 | Bytes: 63,621]
# /home/mike/repos/trimnoir/_posts/2026-08-29-terminal-native-mcp-without-the-bloat.md # [Idx: 1422 | Order: 3 | Tokens: 89,572 | Bytes: 341,094]
! .venv/bin/python -c "print(sum(l.startswith(chr(96)*3) for l in open('/home/mike/repos/Pipulate.com/install.md')))"
! .venv/bin/python scripts/sources_menu.py | tail -n 4
! sed -n '7p' scripts/boot_menu.py
! rg -n -e "'build'" -e "'twine'" -e 'Running:' release.py
release.py
scripts/boot_menu.py
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 55b4fd96..02230398 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`, `seed`, `foo` live here
+ [2] Just the shell NOTHING starts; `walk`, `sources`, `brief`, `pu` 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 shell option description
[main 7c3d96a1] chore: Update boot\_menu.py shell option description
1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 48 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 408 bytes | 408.00 KiB/s, done.
Total 4 (delta 3), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (3/3), completed with 3 local objects.
To github.com:pipulate/pipulate.git
135a7417..7c3d96a1 main -> main
(nix) pipulate $
4: Prompt: Read the receipts: probe 1 should be 18, probe 2 four footer lines, probe 3 the pu docstring. release.py is in context; its output from the last release ran ~340 lines, ~270 of them the build backendโs copying/adding chatter. Trim it to under 60 lines without losing a receipt: (1) in run_commandโs except branch, print the captured stdout tail and stderr before sys.exit when capture=True, so a captured failure is never silent; (2) in the PyPI phase, run python -m build with capture=True and print only the line beginning โSuccessfully builtโ; (3) run twine with โdisable-progress-bar, capture, and print only the โView atโ line; (4) gate every โ๐ Running:โ echo behind a new โverbose flag, default off. Keep the wax-seal bunny. SEARCH/REPLACE against the raw release.py in context; name the ignition (a real release is the only true AFTER, so also give a dry probe that proves the flag parses: python release.py --help | grep -c verbose). Then, as the closing section, restate the two-arm experiment spec from the previous caboose so the compile after this one can pull cli.py, scripts/connectors/README.md, scripts/connectors/botify.py and tools/scraper_tools.py and design it. โimpure removal stays a separate later car.
5: Deliverables: None this turn. But donโt go overboard keeping this discussion going. Thereโs going to be tons of hardening and such. Letโs get to a good stopping point if we can put a ribbon on this article and if we have reached that point then hop off the this 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.
Fable 5 Extra: 1. VERIFY
Context verified. The canary arrived linkified in this compile; no patch is emitted here, so nothing rides on it.
The goal at the top of the article had three layers: answer the MCP run-down and the Unix parallel; turn the thesis into a testable theory; and, before any experiment, clean up the install procedure per the feedback without breaking flake.nix, witnessed on a strangerโs first contact. This compileโs receipts: the fence count reads 18 (even, balanced); sources_menu.py prints four footer lines; line 7 of boot_menu.py names pu; the rg on release.py returns lines 51, 1392, 1394. The operatorโs 05:28 EDT Mac transcript in the Prompt shows every new installer string, v2.46, and [2] Just the shell. on a fresh install. Layers one and three: met. Layer two: the design was done (two arms, the asymmetry, the execute_shell_command escape, the 1:1 wrapper needed) but no experiment ran. Ignitions that never fired: the release.py trim has a BEFORE (probe 4) and no patch, no release, and no AFTER, correctly unlabeled; the printf SSH-config fix landed as text (hook line 978, previous compile) but no keyless machine has entered nix develop, so its behavior is unwitnessed; the --impure removal has no ignition because it has no patch.
2. BANK
Rules, paste-ready for wherever the named rules live:
# THE PUBLISH LANE IS NOT THE PUSH: installer edits reach strangers only through release.py's sync_install_sh; `git push` is not their ignition. Publish-only lane, proven idempotent 2026-08-30: python release.py --skip-version-sync --skip-docs-sync --skip-audit-sync --skip-ai-context-sync --skip-breadcrumb-sync --skip-trifecta-rebuild
# A BARE FENCE CANNOT RIDE SEARCH/REPLACE: apply.py strips bare fence lines from the payload before matching; a fence edit rides as a sed car spelled with \x60\x60\x60.
# ECHO IS NOT PRINTF INSIDE A NIX STRING: bash's builtin echo leaves \n literal and a Nix indented string passes it through. Convicted 2026-08-30: bash -c 'echo "a\nb"' | cat -A -> a\nb$
# PURITY IS A PROPERTY OF THE EVALUATION: one pure `nix develop` on a platform proves every devShell attribute evaluates pure there. Two Darwin receipts 2026-08-30; --impure is vestigial (earmark).
# THE DOOR NUMBER IS THE KEYPRESS RECEIPT: a raw-mode menu prints the choice class it acted on, or the transcript shows a decision with no cause.
# ARMS MUST BE SYMMETRIC: a registry that contains execute_shell_command can escape into the shell arm; the experiment withholds it and wraps each connector 1:1 from the same docstring.
Earmarks:
# EARMARK release.py trim: capture `-m build` and print only 'Successfully built' (dump captured tail on failure, run_command's except branch is silent today); twine --disable-progress-bar, print only 'View at'; gate 'Running:' behind --verbose. BEFORE 2026-08-30: lines 51/1392/1394. ~340 lines -> <60.
# EARMARK --impure removal (4 sites, 3 files): install.sh ./run heredoc + PIPULATE_INSTALL_ONLY lane; mck.sh NIXWRAP; flake.nix brief prompt "macOS --impure exception". get_nix_develop_cmd's comment describes a branch that does not exist.
# EARMARK Car 5 witness: enter nix develop on a machine with no ~/.ssh/id_rsa and read ~/.ssh/config -- three lines, not one.
Delete from the bottom of adhoc.txt (this rideโs echoes, now spent):
! .venv/bin/python -c "print(sum(l.startswith(chr(96)*3) for l in open('/home/mike/repos/Pipulate.com/install.md')))"
! .venv/bin/python scripts/sources_menu.py | tail -n 4
! sed -n '7p' scripts/boot_menu.py
! rg -n -e "'build'" -e "'twine'" -e 'Running:' release.py
release.py
scripts/boot_menu.py
3. DANGLING
release.pyoutput trim: designed, BEFORE taken, not patched.--impureremoval: four sites, three files, needs a Mac reinstall and anmck.shride as ignition.- Car 5 SSH-config behavior: unwitnessed until a keyless machine enters the flake.
- The experiment: arms asymmetric, wrapper unwritten,
execute_shell_commandunwithheld, task set undefined. curl -#shows the size-unknown bounce on fast fetches; cosmetic, left alone.- ZIP fetched from
miklevin/pipulate, clone frompipulate/pipulate; presumably a redirect, never verified. .gitignorecarriesdeleteme.txtnine times in its temp block.
4. SEED
# --- NEXT RIDE: the two-arm experiment (shell verbs vs registry) ---
! rg -n 'execute_shell_command' cli.py tools/
! ls scripts/connectors/*.py | wc -l
! rg -c 'async def ' tools/*.py
cli.py
scripts/connectors/README.md
scripts/connectors/botify.py
tools/scraper_tools.py
# hardening, any time: uncomment the next two lines for the release.py trim ride
# ! rg -n -e "'build'" -e "'twine'" -e 'Running:' release.py
# release.py
[[[TODO_SLUGS]]]
terminal-mcp-operational-moving-from-pipes-to-application
terminal-native-mcp-without-the-bloat
from-claude-connector-to-unix-instrument
antidote-to-vibe-coding-unix-pipes-local-files
[[[END_SLUGS]]]
5. CLOSING
The article set out to decide whether MCP was inevitable and ended up running the counter-experiment by accident. Over four turns, a chat model that could not touch the machine steered a real repository through five patch trains, a public release, and a fresh install on a different operating system, and it did so with nothing but text: a compiled context file going one way, exact-match patches and bare shell commands coming back, and a receipt straddling every change so that before and after could never be confused. No tool protocol was involved. The adapter that made it work was not a schema; it was Nix, holding the environment still enough that --help and a docstring were all the discovery anyone needed.
That is not proof of the thesis. It is one arm of the experiment walking around without its control. But it is the same lesson the book keeps arriving at from different directions: variance is the enemy, receipts are the cure, and beginnerโs mind is what you get back when the machine stops surprising you. Shoshinification, this version was called. The strangerโs-first-contact transcript is what it looks like when it works.
6. NOTARIZE
The artifact of record is the newest hash-stamped cartridge, foo-<hash8>-NN.zip, rotated into the repo root by the compile that produced this payload (Verified Tokens 68,320; 273,973 bytes UTF-8, the fingerprint of this compile as the Summary reports it). No receipt in this compile carried its filename, so I will not invent the hash; ls -t foo-*.zip | head -1 reads it. Its CRC-sealed, byte-reproducible archive is the witnessed receipt of this ride. The seal is the signature; the archive is the deed.
MikeLev.in: Hey AI Editor, this is about MCP is it not? Or perhaps Shoshinification? Donโt reuse age-old chestnuts please in titling. Try to say something new.
Book Analysis
Ai Editorial Take
What is most striking about this dialogue is how quickly abstract software design arguments about protocols collapse into concrete matters of environmental determinism. The realization that discovery protocols are often just paying a tax for inconsistent operating systems reframes the entire debate between agent frameworks and raw terminal access.
๐ฆ X.com Promo Tweet
Why complex API protocols naturally drift back toward Unix simplicity when deployed at scale. https://mikelev.in/futureproof/protocol-vs-prompt-why-mcp-is-becoming-unix/ #AIArchitecture #UnixPhilosophy #LocalFirst
Title Brainstorm
- Title Option: Protocol vs. Prompt: Why MCP Is Becoming Unix
- Filename:
protocol-vs-prompt-why-mcp-is-becoming-unix - Rationale: Directly addresses the core tension between formal schema-based agent protocols and lightweight command-line compositions.
- Filename:
- Title Option: From Multics to MCP: The Evolution of Agent Plumbing
- Filename:
from-multics-to-mcp-evolution-agent-plumbing - Rationale: Leans into the historical systems-programming parallel, appealing to engineers who understand architectural bloat.
- Filename:
- Title Option: The Narrow Waist of Software: Why Stateless Protocols Win
- Filename:
narrow-waist-software-stateless-protocols-win - Rationale: Focuses on the technical inevitability of request-response design over held-open stateful streams.
- Filename:
Content Potential And Polish
- Core Strengths:
- Sharp historical analogies that connect 1960s operating system design directly to 2026 AI agent infrastructure.
- Grounded in real-world engineering constraints, contrasting theoretical protocol design with actual implementation pain.
- Seamlessly bridges high-level technical philosophy down to concrete installation mechanics and environment reproducibility.
- Suggestions For Polish:
- Tighten the transition between the initial protocol critique and the detailed shell installation walkthrough.
- Ensure technical jargon like โStreamable HTTPโ and โMulticsโ is framed clearly for readers approaching from multiple backgrounds.
Next Step Prompts
- Design the benchmark suite comparing shell-verb task completion against registry-tool execution under identical constraints.
- Explore how deterministic environment seeding via Nix alters the cognitive load on frontier models during complex multi-step workflows.