Terminal-Native Model Context Protocol in Practice: Connecting Directly in the Age of AI

🤖 Read Raw Markdown

Setting the Stage: Context for the Curious Book Reader

Context for the Curious Book Reader: This entry explores the practical realities of interacting with Model Context Protocol (MCP) servers directly from the command line, contrasting rigid combination-lock authentication flows with clean, local-first terminal execution. It illustrates how moving from abstract client frameworks to plain-text Unix tooling creates reproducible, verifiable connections without unnecessary software bloat.

TL;DR: This article set out to test a hypothesis — that in a reproducible environment, shell commands with --help serve an AI model as well as a tool registry with JSON schemas — and ended by doing something plainer: making real calls against two remote Model Context Protocol (MCP) servers from a terminal, with no AI model in the loop. A test harness was built and run once (one pass, one fail, cause not yet named), then parked. The working result is a terminal client that reaches both servers: one authenticated by a five-minute OAuth token that must be refreshed in the same command line as each call, the other by a static token that stays valid across sessions. Every call writes a local receipt recording the request, the response status, and a hash of the response body. Along the way two process rules graduated: order-dependent instructions are given in execution order with no previews, and paste-ready commands carry shell variables, never angle-bracket placeholders.


Technical Journal Entry Begins

MikeLev.in: 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. That’s where I’m living in all this, shoving tons of what people would reach for the so-called web full stack for and replacing it with a series of easy breezy little Unix commands that achieve the same thing custom every time through different command tool-calling and composability. In other words you fire-up a Unix/Linux terminal and just type short little commands with short little parameters and arguments and each one is mostly a select-only vomiting out of text from somewhere that you generally just stack vertically and design it like a book for some LLM to look at as your prompt input.

That’s like the 20% of the 80/20-rule because once you build complete and thorough context for whatever problem you’re trying to solve, you can recruit forever smarter intelligences in helping you actually come up with the solution. Therefore much of the value of the human even being in the picture at all is how skillfully, artfully and indeed sympathetically to the LLM-style AI Lambda function that’s going to be processing the prompt as a 1-off will be able to successfully create verifiable assert-true components (through actuators or other tests) part of their generated response.

On my last article I created a testable thesis but didn’t test it, is that correct Fable 5?

Fable 5 Extra: Context verified. The render canary arrived linkified this time; no patch rides in this turn, so nothing is at risk.

Yes, that’s correct — with one sharpening: the thesis was stated in a falsifiable form and sketched as an experiment, but it was never operationalized, and it was never run.

What the last article did:

  • Stated the hypothesis so it could be wrong: discovery protocols compensate for environmental variance, and their advantage shrinks as variance goes to zero. Prediction: MCP-shaped invocation wins on random hosts and loses on deterministic ones.
  • Sketched the test: two arms (shell verb via nix develop .#quiet --command vs. cli.py call <tool> --json-args), same model, same tasks, four metrics, and a control that generates the --help text and the tool description from one docstring.
  • Then spent four turns on installer cleanup, and closed with its own VERIFY beat saying “no experiment ran” and a DANGLING line reading “arms asymmetric, wrapper unwritten, execute_shell_command unwithheld, task set undefined.”

So “testable” is slightly generous. A hypothesis is testable when someone else could run the test from the writeup. Nobody could yet: no task set, no pass/fail predicate per task, no wrapper making the arms symmetric, no withholding of the escape hatch. Falsifiable in principle, not operational, not run. And the closing paragraph’s “one arm of the experiment walking around without its control” was an anecdote about the patch session, not a data point — don’t count it.

On your preamble: agreed, and the clause doing the work is “assert-true.” Stacked terminal output is the 20%; what turns the LLM’s answer into a receipt instead of a story is that the prompt names what would falsify it. That is exactly the piece the experiment is missing — the per-task predicate.

What this compile’s receipts add

Three live commands ran, and they move the design:

  • execute_shell_command is defined twice — tools/system_tools.py:45 and tools/advanced_automation_tools.py:1490 — and registered explicitly at advanced_automation_tools.py:2179 via register_mcp_tool, while scraper_tools.py registers with @auto_tool. Two definitions, two registration mechanisms. “Withhold it from Arm R” is not one deletion until you know which definition the registry serves; delete the wrong one and the escape hatch stays open.
  • 11 connector files, seven of which are connectors per the README; the rest are wallet, mcp, mcp_warm and (if the README’s mention is current) gong — infrastructure, not capabilities. The arm has seven capabilities, not eleven.
  • tools/botify_tools.py has 8 async defs and none appears in the 21-tool roster. Either it registers nothing, or the Arm R wrapper is partly written already. Can’t tell without the file.

Missing for the design turn, not for this answer: tools/__init__.py (cli.py’s get_all_tools and the auto_tool decorator), tools/system_tools.py, tools/botify_tools.py, and config.py (botify.py’s get_botify_token).

1. PROBES

.venv/bin/python -c 'from tools import get_all_tools; r=get_all_tools(); f=r["execute_shell_command"]; print("tools=" + str(len(r))); print("esc_from=" + f.__module__ + ":" + str(f.__code__.co_firstlineno))'
rg -n -e 'def get_all_tools' -e 'def auto_tool' -e 'def register_mcp_tool' tools/__init__.py
rg -n -e '@auto_tool' -e 'register_mcp_tool\(' -e '^async def ' tools/botify_tools.py

Probe 1 is the falsifier: it names which of the two execute_shell_command definitions the live registry serves and confirms the roster’s 21. Probe 2 shows how the registry is built (decorator, explicit call, or both), which decides what “withhold” has to touch. Probe 3 says whether botify_tools.py registers anything and what its eight functions are named — whether the Arm R wrapper starts from zero or from something. All three are read-only and bounded; probe 1 imports the registry the same way cli.py does and writes nothing.

2. NEXT CONTEXT

! .venv/bin/python -c 'from tools import get_all_tools; r=get_all_tools(); f=r["execute_shell_command"]; print("tools=" + str(len(r))); print("esc_from=" + f.__module__ + ":" + str(f.__code__.co_firstlineno))'
! rg -n -e 'def get_all_tools' -e 'def auto_tool' -e 'def register_mcp_tool' tools/__init__.py
! rg -n -e '@auto_tool' -e 'register_mcp_tool\(' -e '^async def ' tools/botify_tools.py
tools/__init__.py
tools/system_tools.py
tools/botify_tools.py
cli.py
scripts/connectors/README.md
scripts/connectors/botify.py
scripts/sources_menu.py

Drop the four articles for the design turn: they are 143k of this compile’s 177k tokens, and the design needs code, not narrative. Drop apply.py, .gitattributes, .gitignore, and scraper_tools.py too.

3. PATCHES

No repo patches required. No ignition required — every probe reads the file or the registry at call time.

4. PROMPT

The thesis is stated but untested. Three probes are echoed; read the LIVE COMMAND RECEIPTS first and name the lane for each.

Rule on: (1) which execute_shell_command definition the registry serves, and therefore what withholding it from Arm R must touch; (2) how the registry is built -- decorator, explicit register, or both; (3) whether tools/botify_tools.py registers anything, and whether any of its eight functions already wrap the connector's LIST/FETCH modes.

Then write the experiment as a protocol someone else could run, in this order: the five read-only tasks, each with its exact pass/fail predicate (a string or value the transcript must contain); the 1:1 wrapper that registers botify.py's identity/org/analyses/query modes as registry tools from the SAME docstring the sources menu already reads; the withholding mechanism for Arm R; the four metrics and the transcript field each is read from. One connector only -- Botify -- before generalizing to seven. Patches only against raw source in context.

5. EXTERNAL DELIVERABLES

None.

MikeLev.in: Okay, let’s do the 5-Car Train.

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 $ .venv/bin/python -c 'from tools import get_all_tools; r=get_all_tools(); f=r["execute_shell_command"]; print("tools=" + str(len(r))); print("esc_from=" + f.__module__ + ":" + str(f.__code__.co_firstlineno))'
rg -n -e 'def get_all_tools' -e 'def auto_tool' -e 'def register_mcp_tool' tools/__init__.py
rg -n -e '@auto_tool' -e 'register_mcp_tool\(' -e '^async def ' tools/botify_tools.py
tools=21
esc_from=tools.system_tools:43
10:def auto_tool(func):
31:def get_all_tools():
35:async def botify_ping(params: dict) -> dict:
82:async def botify_list_projects(params: dict) -> dict:
129:async def botify_simple_query(params: dict) -> dict:
192:async def botify_get_full_schema(params: dict) -> dict:
242:async def botify_list_available_analyses(params: dict) -> dict:
291:async def botify_execute_custom_bql_query(params: dict) -> dict:
367:async def test_botify_actual_connectivity() -> dict:
408:async def test_botify_connectivity() -> dict:
(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 MCP style
# 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 'from tools import get_all_tools; r=get_all_tools(); f=r["execute_shell_command"]; print("tools=" + str(len(r))); print("esc_from=" + f.__module__ + ":" + str(f.__code__.co_firstlineno))'
! rg -n -e 'def get_all_tools' -e 'def auto_tool' -e 'def register_mcp_tool' tools/__init__.py
! rg -n -e '@auto_tool' -e 'register_mcp_tool\(' -e '^async def ' tools/botify_tools.py
tools/__init__.py
tools/system_tools.py
tools/botify_tools.py
cli.py
scripts/connectors/README.md
scripts/connectors/botify.py
scripts/sources_menu.py

3: Patches: None this turn

4: Prompt: The thesis is stated but untested. Three probes are echoed; read the LIVE COMMAND RECEIPTS first and name the lane for each.

Rule on: (1) which execute_shell_command definition the registry serves, and therefore what withholding it from Arm R must touch; (2) how the registry is built – decorator, explicit register, or both; (3) whether tools/botify_tools.py registers anything, and whether any of its eight functions already wrap the connector’s LIST/FETCH modes.

Then write the experiment as a protocol someone else could run, in this order: the five read-only tasks, each with its exact pass/fail predicate (a string or value the transcript must contain); the 1:1 wrapper that registers botify.py’s identity/org/analyses/query modes as registry tools from the SAME docstring the sources menu already reads; the withholding mechanism for Arm R; the four metrics and the transcript field each is read from. One connector only – Botify – before generalizing to seven. Patches only against raw source in context.

5: Deliverables: None this turn but working towards a Shoshin path where someone could:

  1. curl -fsSL https://pipulate.com/install.sh | bash
  2. cd into pipulate and type nix develop.
  3. Hit “2” to drop out to terminal and then type sources
  4. Do all logins right at the beginning with warm
  5. ???

There’s 2 different MCP servers we want to use today from the terminal and they have different approaches. One is the generic MCP server connector and the other takes the WET approach because there will be MCP servers with nuance and they can’t all be lumped together. The flow might be to start with a generic MCP explorer (any MCP server endpoint, any auth scheme results may vary) and another which just works correctly every time against a known MCP server and which can also have all kinds of conveniences built-in like never having to look at JSON when making the most common use case calls.

Is this making sense?

Fable 5 Extra: Context verified. The render canary arrived linkified, so the transport rewrote this payload; none of my SEARCH blocks quotes a www-prefixed token. One more transport note: empty lines appear stripped from every file body in this compile (whitespace-only lines survived), so every SEARCH block below is a run of lines that cannot contain a blank line in the real file.

The three rulings

All three readings appear in both lanes — your hand-run (operator) and the LIVE COMMAND RECEIPTS (compile) — and agree byte for byte.

The Three Rulings and the Experimental Ground

1. The registry serves tools/system_tools.py. esc_from=tools.system_tools:43co_firstlineno points at the first decorator line (@auto_tool at 43, async def at 45), consistent with last compile’s rg. The copy in advanced_automation_tools.py (1490, explicitly register_mcp_tool‘d at 2179) is not in this registry, because get_all_tools() returns AUTO_REGISTERED_TOOLS and only @auto_tool writes to it. CHANGES: withholding touches one dict, and the cleanest cut is at get_all_tools()’s return, which every cli.py path reads. One caveat, INFERRED: the advanced copy’s decorator lines were never read. If it also carries @auto_tool, the winner depends on os.scandir order, which is unsorted. Probe 1 settles it.

2. Decorator only. auto_tool populates the dict; get_all_tools() imports every tools/*.py via os.scandir (unsorted, ImportError swallowed with a print) and returns it. No register_mcp_tool in the package init; alias feeds a separate ALIAS_REGISTRY for chat bracket commands ([ls], [shell]), which cli.py never reads. CHANGES: an env denylist at the return covers call, --all, and --tool. The Rule-of-7 default view is a string-literal list and needs its own filter. Also: that import costs 5.5 s (compile timing receipt) on every cli.py call — a real Arm R cost, not one of the four metrics, and it should be recorded.

3. botify_tools.py registers nothing and wraps nothing. Zero @auto_tool, zero register_mcp_tool. Its six core functions are an older parallel implementation: different endpoints (/projects/{username} vs the connector’s /users/{username}/projects and /analyses/{org}/{project}/light), a different credential source (ai_dictdb.get_key with a "placeholder_token" fallback vs config.get_botify_token()), and aiohttp, which is not in the top-level package list. The only thing keeping the file alive is __init__.py importing CORE_BOTIFY_TOOLS for __all__. CHANGES: the wrapper starts from zero and must not touch this file. It wraps the connector by subprocess, so the backend is literally the process the shell arm runs.

The experiment, as a protocol

Hypothesis under test: in a reproducible environment, invoking a capability as a shell command with --help performs at least as well as invoking the same capability as a registry tool with a JSON schema, at lower cost. This experiment holds capability, backend, and description constant and varies only invocation grammar and discovery surface.

Fixed: one model, temperature 0, the model string the API echoes recorded per response (a channel, not a pin). Same harness loop, same five tasks, same oracle, same cap of 8 calls per task. Both arms run inside nix develop .#quiet, cwd repo root, BOTIFY_API_TOKEN inherited. N = 10 sessions per arm per task.

Arms.

  Arm S (shell) Arm R (registry)
Allowed command prefix .venv/bin/python scripts/connectors/botify.py .venv/bin/python cli.py
Discovery text in system prompt the sources row for botify + botify.py --help cli.py mcp-discover --all + cli.py mcp-discover --tool botify
Env none extra PIPULATE_TOOL_DENY=execute_shell_command
Result rendering connector stdout cli.py call botify --raw --json-args '{...}' → the tool’s dict as JSON

After the cars below, both discovery texts are generated from one source: scripts/connectors/botify.py’s module docstring, which sources_menu.py already reads, --help will print (description=__doc__), and the wrapper installs as the tool’s __doc__. That is the description-quality control: no hand-authored sentence exists on either arm.

Why one tool, not four. Splitting identity/org/analyses/query into four registry tools would hand Arm R task-shaped discovery the shell verb doesn’t have. That is a real advantage worth testing, but it is a different variable (shape, not grammar) — see the second experiment at the end. Here the tool has the verb’s exact argument shape: query (the positional; omit for identity), org, project, max, check.

Harness loop (scripts/two_arm.py, next ride, not this one). System prompt: “Reply with exactly one line: RUN: <command> or FINAL: <answer>. You may only run commands beginning with ." plus the arm's discovery text. User message: the task. Each model turn is parsed; `RUN` outside the prefix is refused with exit 126 and stderr `refused: outside arm` (counts as a call); otherwise executed with `bash -c` inside the shell the harness started in, stdout capped 4000 bytes, stderr 1000, exit code appended. Loop until `FINAL` or cap; cap forces an empty FINAL. Every turn writes one JSONL record: `{arm, task, session, seq, kind, command, exit_code, stdout_bytes, stderr_bytes, prompt_tokens, completion_tokens, text, pass}`. Drive the model through the `llm` library (already in the environment) so usage arrives per response without provider code.

Escape closure needs both halves. The prefix check stops Arm R from running arbitrary shell; the denylist stops cli.py call execute_shell_command --json-args '{"command":"python scripts/connectors/botify.py"}', which passes the prefix check and becomes the other arm.

Oracle and drift guard. Immediately before each task the harness runs the connector itself and extracts the expected value; immediately after, it runs it again. If before ≠ after, the trial is void (D1: a project list can change under you). Predicates apply to the text after FINAL: only — never to tool stdout — so the connector’s own output cannot pass a check by merely appearing in the transcript.

Tasks (all read-only):

# Task text Oracle Pass if FINAL contains
T1 “What Botify username is this environment authenticated as?” botify.py --checkbotify GREEN <u> <u>
T2 “Name the first org/project slug pair listed for this account.” botify.py -n 25, first token of the first non-#, non-empty line that org/slug
T3 “For <org>/<project>, what is the slug of the newest analysis?” (both given) botify.py <org>/<project> -n 1, first token of the first data line that slug
T4 “Run SELECT url FROM crawl against <org>/<project> with a cap of 5 and report one URL it returned.” same query, -n 25, set of url values any member of the set
T5 T4’s text with the org withheld: “against project <project>, under the org this account belongs to” same as T4 any member of the set

T5 is the injected error. Omitting --org makes the connector exit 1 with FETCH mode needs coordinates; recovery is T2’s identity walk. T4 is T5’s control — one withheld coordinate is the only difference.

Negative control: run T3 once per arm with a project slug that does not exist. The connector exits 1 with HTTP 4xx for <url>; pass = FINAL contains that status code. Its job is to prove the T3 predicate can fail. A predicate never observed failing has not been tested.

Metrics.

Metric Read from Definition
success pass on the FINAL record predicate on FINAL text; arm score = passes ÷ 5 per session
tokens prompt_tokens + completion_tokens on every record summed over the session, FINAL included
calls count of kind: run records before FINAL refusals count
recovery T5 only: pass AND an earlier record with exit_code != 0 also flag guessed = pass with zero calls

Prediction: S ≥ R on success, S < R on tokens and calls. The outcome that hurts the thesis is R > S on success with identical descriptions and the same backend — that would mean structured invocation carries value the shell cannot, under the shell’s own best conditions.

The Shoshin path, and the two MCP doors

Yes, it makes sense, and step 5 is already written into the connector contract: type one source word bare, read its # Next: line, follow it. Three hops down you have a page of text; brief puts it on the clipboard. That is the whole rhythm, and it is the same rhythm the experiment above measures.

The two MCP doors are two situations, not two designs. mcp <url> is the explorer: any endpoint, any scheme (--auth-scheme), any tool by raw JSON. You use it once per new server, to find out what it is. A WET connector per known server (render, say) is what you use every day: auth scheme and token source hardcoded, the three common calls as a positional argument, -n, # Next: breadcrumbs, --checkbotify.py’s contract with no JSON in sight. The explorer mints the connector: its tools/list output is the spec you write the WET file from. This is the human “opinionating” the last article said MCP does not do for you, and a vendor mirroring every endpoint as a tool is the thing it replaces.

Shifting Gears: Making Real Calls Without Models

Two things to keep in view. First, that pairing is a second experiment — generic client vs WET connector against the same MCP server — and it tests shape rather than grammar, which is closer to the book’s claim. Both arms are nearly free once one WET connector exists. Do not run it in the same session as the first. Second, step 4 is not yet true for MCP: warm does not warm the OAuth bearer, because the wallet has no MCP slot and the earlier ruling blocked one until wallet.py changes. Six sources warm; one MCP server does not. Named, not fixed.

One housekeeping line: ruff’s W292 on botify_tools.py is a missing trailing newline in a file this turn ruled dead. ruff --fix or leave it; not a car.

1. PROBES

rg -n -B2 '^async def execute_shell_command' tools/advanced_automation_tools.py
test -e scripts/botify.py; echo "scripts_botify_exists=$?"
PIPULATE_TOOL_DENY=execute_shell_command .venv/bin/python -c 'from tools import get_all_tools; r=get_all_tools(); print("tools=" + str(len(r))); print("shell_denied=" + str("execute_shell_command" not in r)); print("botify_registered=" + str("botify" in r)); print("botify_doc=" + (r["botify"].__doc__ or "").strip().splitlines()[0] if "botify" in r else "botify_doc=none")'
.venv/bin/python scripts/connectors/botify.py --help | head -4
rg -c 'scripts/botify\.py' scripts/connectors/botify.py; echo "stale_path_exit=$?"
PIPULATE_TOOL_DENY=execute_shell_command .venv/bin/python cli.py call execute_shell_command --json-args '{"command":"true"}' >/dev/null 2>&1; echo "deny_via_cli_exit=$?"
.venv/bin/python cli.py call system_list_directory --raw --json-args '{"path":"tools"}' 2>&1 | head -2

Probe 1 falsifies ruling 1’s caveat: it shows the two lines above the advanced copy’s def. A bare @auto_tool there means the registry winner is scandir-order luck. Probe 2 reads whether the connector’s breadcrumbs were lying (1, no such file) or a shim exists (0). Probe 3 is the straddle for Cars 1 and 2 in one import: BEFORE tools=21 / False / False / none; AFTER tools=21 / True / True / botify.py — Bring Botify… — 21 both times because the wrapper adds one and the denylist removes one, so 20 means the wrapper did not land and 22 means the denylist did not. Probes 4 and 5 straddle Car 3: --help opens with the usage line then the docstring; the stale-path count goes from a number to exit 1. Probe 6 straddles Car 1 at the entry point Arm R actually uses: BEFORE runs true and exits 0 (that is the escape hatch, fired harmlessly); AFTER exits 1. Probe 7 straddles Car 4: BEFORE is an argparse unrecognized arguments: --raw; AFTER is { then "success": true. Four of the seven import the registry at 5.5 s each; none opens a network socket.

2. NEXT CONTEXT

! rg -n -B2 '^async def execute_shell_command' tools/advanced_automation_tools.py
! test -e scripts/botify.py; echo "scripts_botify_exists=$?"
! PIPULATE_TOOL_DENY=execute_shell_command .venv/bin/python -c 'from tools import get_all_tools; r=get_all_tools(); print("tools=" + str(len(r))); print("shell_denied=" + str("execute_shell_command" not in r)); print("botify_registered=" + str("botify" in r)); print("botify_doc=" + (r["botify"].__doc__ or "").strip().splitlines()[0] if "botify" in r else "botify_doc=none")'
! .venv/bin/python scripts/connectors/botify.py --help | head -4
! rg -c 'scripts/botify\.py' scripts/connectors/botify.py; echo "stale_path_exit=$?"
! PIPULATE_TOOL_DENY=execute_shell_command .venv/bin/python cli.py call execute_shell_command --json-args '{"command":"true"}' >/dev/null 2>&1; echo "deny_via_cli_exit=$?"
! .venv/bin/python cli.py call system_list_directory --raw --json-args '{"path":"tools"}' 2>&1 | head -2
tools/__init__.py
tools/connector_tools.py
cli.py
scripts/connectors/botify.py

Drop botify_tools.py and system_tools.py (both ruled), README.md, sources_menu.py, and the actuator trio for the harness turn; nothing in it touches them.

3. PATCHES

Car 1 — the denylist. Two blocks. The registry never changes; the denial is an env var visible in the command that set it, and an empty variable is exactly today.

Target: tools/__init__.py
[[[SEARCH]]]
    return AUTO_REGISTERED_TOOLS
[[[DIVIDER]]]
    denied = denied_tools()
    if denied:
        return {name: func for name, func in AUTO_REGISTERED_TOOLS.items()
                if name not in denied}
    return AUTO_REGISTERED_TOOLS

def denied_tools():
    """Names withheld from the registry by PIPULATE_TOOL_DENY (comma-separated).

    THE ESCAPE HATCH, NAMED. execute_shell_command is a registry tool, so any
    experiment that pits the registry against the shell has a registry arm that
    can shell out and become the other arm. Withholding is an ENV VAR on
    purpose: the registry itself never changes, the denial is visible in the
    harness command that set it, and an empty variable is exactly today.
    Applied at get_all_tools()'s return because every cli.py path -- call,
    mcp-discover --all, mcp-discover --tool -- reads that one dict.
    """
    raw = os.environ.get("PIPULATE_TOOL_DENY", "")
    return {name.strip() for name in raw.split(",") if name.strip()}
[[[REPLACE]]]
Target: cli.py
[[[SEARCH]]]
            for i, tool in enumerate(essential_tools, 1):
                console.print(f"  {i}. [bold cyan]{tool}[/bold cyan]")
[[[DIVIDER]]]
            # The Rule of 7 is a string-literal list, not a registry read, so
            # a denied tool would still be NAMED here and a model told to run
            # it would waste a call on "not found". Filter without importing
            # the registry (that import costs seconds); denied_tools() is cheap.
            from tools import denied_tools
            shown = [tool for tool in essential_tools if tool not in denied_tools()]
            for i, tool in enumerate(shown, 1):
                console.print(f"  {i}. [bold cyan]{tool}[/bold cyan]")
[[[REPLACE]]]

Car 2 — the wrapper. New file, whole-file write.

Target: tools/connector_tools.py
[[[WRITE_FILE]]]
"""
connector_tools.py -- the registry face of scripts/connectors/*.py: one tool
per connector, SAME argument shape, SAME backend, SAME docstring.

This file exists for the two-arm experiment (shell verb vs registry tool) and
for nothing else yet. Arm S runs the connector as a command line; Arm R calls
the tool below through `cli.py call <name> --json-args '{...}'`. Both arrive at
the identical subprocess, so the only thing the experiment can measure is the
invocation grammar and the discovery surface, which is the variable under test.

THREE SURFACES, ONE SOURCE. sources_menu.py reads each connector's module
docstring with ast.get_docstring and prints its first line as the menu row;
the connector's own --help prints the same docstring (argparse
description=__doc__); this file reads the same docstring the same way and
installs it as the tool's __doc__ at import time, which is what
`cli.py mcp-discover --tool <name>` prints. A description that is better on
one arm than the other is the confound that would decide the experiment for
the wrong reason, so no hand-authored description is allowed to exist here.

PARAMS MIRROR ARGPARSE DESTS, deliberately, so the JSON a model must write is
readable off the same --help text the other arm reads: query (the single
positional; omit it for the identity walk), org, project, max, check.

KNOWN SEAM: the AST-derived Tool Roster in prompt_foo.py cannot see a __doc__
assigned at runtime; it shows the literal placeholder on the function. Named
here rather than papered over with a copied sentence that would drift.
"""
import ast
import asyncio
import sys
from pathlib import Path

from tools import auto_tool

REPO_ROOT = Path(__file__).resolve().parent.parent
CONNECTORS = REPO_ROOT / "scripts" / "connectors"
TIMEOUT = 120
KEY_TRAILER = (
    "\n\nRegistry form: --json-args keys mirror the connector's argument names --"
    " query (the positional; omit for the identity walk), org, project, max, check."
)

def _connector_doc(name):
    """The connector's module docstring, read the way sources_menu.py reads it."""
    path = CONNECTORS / f"{name}.py"
    try:
        doc = ast.get_docstring(ast.parse(path.read_text(encoding="utf-8")))
    except (OSError, UnicodeDecodeError, SyntaxError) as exc:
        return f"(could not read {path}: {exc.__class__.__name__})"
    return (doc or f"(NO MODULE DOCSTRING in {path})") + KEY_TRAILER

async def _run_connector(name, params):
    """Run scripts/connectors/<name>.py as a subprocess; return its three channels."""
    if not isinstance(params, dict):
        return {"success": False, "error": "params must be a JSON object"}
    argv = [sys.executable, str(CONNECTORS / f"{name}.py")]
    if params.get("query") is not None:
        argv.append(str(params["query"]))
    for flag in ("org", "project"):
        if params.get(flag):
            argv += [f"--{flag}", str(params[flag])]
    if params.get("max") is not None:
        try:
            argv += ["--max", str(int(params["max"]))]
        except (TypeError, ValueError):
            return {"success": False, "error": "max must be an integer"}
    if params.get("check"):
        argv.append("--check")
    try:
        proc = await asyncio.create_subprocess_exec(
            *argv, cwd=str(REPO_ROOT),
            stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
        out, err = await asyncio.wait_for(proc.communicate(), timeout=TIMEOUT)
    except asyncio.TimeoutError:
        proc.kill()
        await proc.wait()
        return {"success": False, "error": f"timed out after {TIMEOUT}s", "argv": argv[1:]}
    except OSError as exc:
        return {"success": False, "error": str(exc), "argv": argv[1:]}
    return {
        "success": proc.returncode == 0,
        "argv": argv[1:],
        "exit_code": proc.returncode,
        "stdout": out.decode("utf-8", errors="replace"),
        "stderr": err.decode("utf-8", errors="replace"),
    }

@auto_tool
async def botify(params: dict) -> dict:
    """Registry face of scripts/connectors/botify.py; __doc__ is replaced at
    import with that file's own docstring so both arms read one description."""
    return await _run_connector("botify", params)

botify.__doc__ = _connector_doc("botify")
[[[END_WRITE_FILE]]]

Car 3 — one docstring, three surfaces, and truthful breadcrumbs. --help becomes the module docstring. The seven examples and three # Next: lines say scripts/botify.py, which is not where the file lives; a model in Arm S that follows its own breadcrumb would fail for a reason that has nothing to do with the thesis.

Target: scripts/connectors/botify.py
[[[SEARCH]]]
    parser = argparse.ArgumentParser(
        description="Unix-philosophy gateway to the Botify API for Prompt Fu context."
    )
[[[DIVIDER]]]
    parser = argparse.ArgumentParser(
        # ONE SOURCE FOR THREE SURFACES (2026-08-30): the sources roster reads
        # this module's docstring by AST, tools/connector_tools.py installs it
        # as the registry tool's __doc__, and --help prints it here. A
        # description that differs by surface is a confound wearing help's
        # coat; RawDescriptionHelpFormatter keeps the example lines intact.
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
[[[REPLACE]]]
Target: scripts/connectors/botify.py
[[[SEARCH]]]
# scripts/botify.py
[[[DIVIDER]]]
# scripts/connectors/botify.py
[[[REPLACE]]]
Target: scripts/connectors/botify.py
[[[SEARCH]]]
  python scripts/botify.py                    # LIST: identity walk -> all your org/project slugs
  python scripts/botify.py org                # LIST: projects under that org slug
  python scripts/botify.py org/project        # LIST: analyses (crawl snapshots) for that project
  python scripts/botify.py '<BQL or JSON>'    # FETCH: run a query (needs org/project coordinates)
[[[DIVIDER]]]
  python scripts/connectors/botify.py                    # LIST: identity walk -> all your org/project slugs
  python scripts/connectors/botify.py org                # LIST: projects under that org slug
  python scripts/connectors/botify.py org/project        # LIST: analyses (crawl snapshots) for that project
  python scripts/connectors/botify.py '<BQL or JSON>'    # FETCH: run a query (needs org/project coordinates)
[[[REPLACE]]]
Target: scripts/connectors/botify.py
[[[SEARCH]]]
  ! python scripts/botify.py
  ! python scripts/botify.py my-org/my-project
  ! python scripts/botify.py 'SELECT url FROM crawl' --org my-org --project my-project
[[[DIVIDER]]]
  ! python scripts/connectors/botify.py
  ! python scripts/connectors/botify.py my-org/my-project
  ! python scripts/connectors/botify.py 'SELECT url FROM crawl' --org my-org --project my-project
[[[REPLACE]]]
Target: scripts/connectors/botify.py
[[[SEARCH]]]
    print("\n# Next: python scripts/botify.py <org>/<project>   (list analyses)")
[[[DIVIDER]]]
    print("\n# Next: python scripts/connectors/botify.py <org>/<project>   (list analyses)")
[[[REPLACE]]]
Target: scripts/connectors/botify.py
[[[SEARCH]]]
    print("\n# Next: python scripts/botify.py " + org + "/<project>   (list analyses)")
[[[DIVIDER]]]
    print("\n# Next: python scripts/connectors/botify.py " + org + "/<project>   (list analyses)")
[[[REPLACE]]]
Target: scripts/connectors/botify.py
[[[SEARCH]]]
        "\n# Next: python scripts/botify.py 'SELECT url FROM crawl' "
[[[DIVIDER]]]
        "\n# Next: python scripts/connectors/botify.py 'SELECT url FROM crawl' "
[[[REPLACE]]]

Car 4 — --raw for cli.py call. Without it, Arm R sees the connector’s line-oriented stdout wrapped into an 80-column Rich table cell (no TTY, so width defaults to 80). That is a rendering artifact, not a protocol, and it would penalize the registry arm for a reason outside the hypothesis. Raw mode prints the returned dict as JSON and carries the tool’s success in the exit code, so a harness reads one integer on both arms.

Target: cli.py
[[[SEARCH]]]
async def call_mcp_tool(tool_name: str, tool_args: dict):
    """Execute an MCP tool with the given arguments."""
    console.print(Panel(f"🔧 [bold cyan]Executing MCP Tool: {tool_name}[/bold cyan]", border_style="cyan"))
[[[DIVIDER]]]
async def call_mcp_tool(tool_name: str, tool_args: dict, raw: bool = False):
    """Execute an MCP tool with the given arguments.

    raw=True prints the tool's returned dict as JSON and nothing else: no
    panel, no Rich table. Added 2026-08-30 for the two-arm experiment, where
    the registry arm must see what a tools/call would actually return rather
    than an 80-column table cell that wraps line-oriented stdout. In raw mode
    the exit code carries the tool's own success field.
    """
    if not raw:
        console.print(Panel(f"🔧 [bold cyan]Executing MCP Tool: {tool_name}[/bold cyan]", border_style="cyan"))
[[[REPLACE]]]
Target: cli.py
[[[SEARCH]]]
        console.print(f"⚡ Executing '{tool_name}' with args: {tool_args}")
[[[DIVIDER]]]
        if not raw:
            console.print(f"⚡ Executing '{tool_name}' with args: {tool_args}")
[[[REPLACE]]]
Target: cli.py
[[[SEARCH]]]
        # Display results
        console.print(f"✅ [bold green]Tool execution complete![/bold green]")
[[[DIVIDER]]]
        if raw:
            print(json.dumps(result, indent=2, default=str))
            return bool(result.get("success", True)) if isinstance(result, dict) else True
        # Display results
        console.print(f"✅ [bold green]Tool execution complete![/bold green]")
[[[REPLACE]]]
Target: cli.py
[[[SEARCH]]]
    console.print(Panel("🚀 [bold cyan]Pipulate :: The Local-First AI-Readiness & Automation Workshop[/bold cyan] 🚀", border_style="cyan"))
[[[DIVIDER]]]
    if not getattr(args, 'raw', False):
        console.print(Panel("🚀 [bold cyan]Pipulate :: The Local-First AI-Readiness & Automation Workshop[/bold cyan] 🚀", border_style="cyan"))
[[[REPLACE]]]
Target: cli.py
[[[SEARCH]]]
    args = parser.parse_args()
[[[DIVIDER]]]
    call_parser.add_argument('--raw', action='store_true',
                             help='Print the tool result as JSON and nothing else; '
                                  'the exit code carries the tool\'s success field.')
    args = parser.parse_args()
[[[REPLACE]]]
Target: cli.py
[[[SEARCH]]]
                params = json.loads(args.json_args)
                console.print(f"🎯 [bold green]Golden Path: Using JSON arguments[/bold green]")
[[[DIVIDER]]]
                params = json.loads(args.json_args)
                if not args.raw:
                    console.print(f"🎯 [bold green]Golden Path: Using JSON arguments[/bold green]")
[[[REPLACE]]]
Target: cli.py
[[[SEARCH]]]
            success = asyncio.run(call_mcp_tool(args.tool_name, params))
[[[DIVIDER]]]
            success = asyncio.run(call_mcp_tool(args.tool_name, params, raw=args.raw))
[[[REPLACE]]]

Choreography: patch, app, d, m per car, then blast. Ignition: none required — every probe starts a fresh process that imports or executes the patched file at call time. Not emitted: the harness (scripts/two_arm.py), which is the next ride and needs these four cars witnessed first.

4. PROMPT

Four cars landed (or did not -- read the LIVE COMMAND RECEIPTS, not my memory). Seven probes are echoed; name the lane for each and rule one line each:

1. Probe 1: does the advanced_automation_tools copy of execute_shell_command carry @auto_tool? If yes, the registry winner is scandir-order luck; the denylist still holds, but say the ruling that system_tools is THE definition was unearned.
2. Probe 2: scripts_botify_exists. 1 means the breadcrumbs were lying and Car 3 fixed a real defect; 0 means a shim exists and the path change was cosmetic.
3. Probe 3: tools= must read 21 with shell_denied=True and botify_registered=True. 20 means the wrapper did not land; 22 means the denylist did not; botify_doc must be the connector's own first docstring line.
4. Probes 4 and 5: --help now opens with the module docstring; the stale-path count exits 1.
5. Probe 6: deny_via_cli_exit=1 -- the escape hatch is closed at the entry point Arm R actually uses.
6. Probe 7: --raw prints JSON with no panel.

Then write scripts/two_arm.py to the harness contract from the previous turn: one RUN/FINAL line per model turn, prefix-checked with a 126 refusal, executed with bash -c inside the quiet shell the harness was started in, JSONL records with the named fields, oracle before each task and re-check after (void on drift), predicates applied to FINAL text only. Drive the model through the llm library so usage arrives per response; record the model string the API echoes. Do not run the experiment. Give me the smoke command that runs T1 once on each arm against a local model, and the two JSONL lines its receipt must contain.

Do not touch wallet.py, botify_tools.py, or any MCP file. The generic-vs-WET experiment is a separate ride.

5. EXTERNAL DELIVERABLES

None.

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 $ rg -n -B2 '^async def execute_shell_command' tools/advanced_automation_tools.py
test -e scripts/botify.py; echo "scripts_botify_exists=$?"
PIPULATE_TOOL_DENY=execute_shell_command .venv/bin/python -c 'from tools import get_all_tools; r=get_all_tools(); print("tools=" + str(len(r))); print("shell_denied=" + str("execute_shell_command" not in r)); print("botify_registered=" + str("botify" in r)); print("botify_doc=" + (r["botify"].__doc__ or "").strip().splitlines()[0] if "botify" in r else "botify_doc=none")'
.venv/bin/python scripts/connectors/botify.py --help | head -4
rg -c 'scripts/botify\.py' scripts/connectors/botify.py; echo "stale_path_exit=$?"
PIPULATE_TOOL_DENY=execute_shell_command .venv/bin/python cli.py call execute_shell_command --json-args '{"command":"true"}' >/dev/null 2>&1; echo "deny_via_cli_exit=$?"
.venv/bin/python cli.py call system_list_directory --raw --json-args '{"path":"tools"}' 2>&1 | head -2
1488-# END: server_reboot
1489-
1490:async def execute_shell_command(params: dict) -> dict:
scripts_botify_exists=1
tools=21
shell_denied=False
botify_registered=False
botify_doc=none
usage: botify.py [-h] [--org ORG] [--project PROJECT] [-n MAX] [--check]
                 [query]

Unix-philosophy gateway to the Botify API for Prompt Fu context.
11
stale_path_exit=0
deny_via_cli_exit=0
usage: cli.py [-h] {install,run,uninstall,db-inspect,mcp-discover,call} ...
cli.py: error: unrecognized arguments: --raw
(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/ _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |  Potentially some MCP cleanup
# 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]

! rg -n -B2 '^async def execute_shell_command' tools/advanced_automation_tools.py
! test -e scripts/botify.py; echo "scripts_botify_exists=$?"
! PIPULATE_TOOL_DENY=execute_shell_command .venv/bin/python -c 'from tools import get_all_tools; r=get_all_tools(); print("tools=" + str(len(r))); print("shell_denied=" + str("execute_shell_command" not in r)); print("botify_registered=" + str("botify" in r)); print("botify_doc=" + (r["botify"].__doc__ or "").strip().splitlines()[0] if "botify" in r else "botify_doc=none")'
! .venv/bin/python scripts/connectors/botify.py --help | head -4
! rg -c 'scripts/botify\.py' scripts/connectors/botify.py; echo "stale_path_exit=$?"
! PIPULATE_TOOL_DENY=execute_shell_command .venv/bin/python cli.py call execute_shell_command --json-args '{"command":"true"}' >/dev/null 2>&1; echo "deny_via_cli_exit=$?"
! .venv/bin/python cli.py call system_list_directory --raw --json-args '{"path":"tools"}' 2>&1 | head -2
tools/__init__.py
tools/connector_tools.py
cli.py
scripts/connectors/botify.py

3: Patches:

(nix) pipulate $ ahe
(nix) pipulate $ g

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 'tools/__init__.py'.
(nix) pipulate $ d
diff --git a/tools/__init__.py b/tools/__init__.py
index 11cad759..44da8ceb 100644
--- a/tools/__init__.py
+++ b/tools/__init__.py
@@ -44,7 +44,26 @@ def get_all_tools():
             except ImportError as e:
                 print(f"Could not import tool module: {module_name} - {e}")
 
+    denied = denied_tools()
+    if denied:
+        return {name: func for name, func in AUTO_REGISTERED_TOOLS.items()
+                if name not in denied}
     return AUTO_REGISTERED_TOOLS
+
+
+def denied_tools():
+    """Names withheld from the registry by PIPULATE_TOOL_DENY (comma-separated).
+
+    THE ESCAPE HATCH, NAMED. execute_shell_command is a registry tool, so any
+    experiment that pits the registry against the shell has a registry arm that
+    can shell out and become the other arm. Withholding is an ENV VAR on
+    purpose: the registry itself never changes, the denial is visible in the
+    harness command that set it, and an empty variable is exactly today.
+    Applied at get_all_tools()'s return because every cli.py path -- call,
+    mcp-discover --all, mcp-discover --tool -- reads that one dict.
+    """
+    raw = os.environ.get("PIPULATE_TOOL_DENY", "")
+    return {name.strip() for name in raw.split(",") if name.strip()}
 # --- END NEW ---
 
 __version__ = "1.0.0"
(nix) pipulate $ m
📝 Committing: chore: Introduce `denied_tools` function for tool registry filtering
[main 2ef16890] chore: Introduce `denied_tools` function for tool registry filtering
 1 file changed, 19 insertions(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'cli.py'.
(nix) pipulate $ d
diff --git a/cli.py b/cli.py
index c7a9f91b..ef8ad080 100644
--- a/cli.py
+++ b/cli.py
@@ -109,7 +109,13 @@ def discover_tools(show_all=False, tool_name=None):
                 border_style="cyan"
             ))
             
-            for i, tool in enumerate(essential_tools, 1):
+            # The Rule of 7 is a string-literal list, not a registry read, so
+            # a denied tool would still be NAMED here and a model told to run
+            # it would waste a call on "not found". Filter without importing
+            # the registry (that import costs seconds); denied_tools() is cheap.
+            from tools import denied_tools
+            shown = [tool for tool in essential_tools if tool not in denied_tools()]
+            for i, tool in enumerate(shown, 1):
                 console.print(f"  {i}. [bold cyan]{tool}[/bold cyan]")
             
             console.print(f"\n[italic]Use `.venv/bin/python cli.py mcp-discover --all` to see all available tools.[/italic]")
(nix) pipulate $ m
📝 Committing: chore: Optimize tool discovery filtering
[main eb14476f] chore: Optimize tool discovery filtering
 1 file changed, 7 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ WHOLE-FILE WRITE: CREATED 'tools/connector_tools.py'.
(nix) pipulate $ git add tools/connector_tools.py
(nix) pipulate $ m
📝 Committing: refactor: improve connector_tools.py documentation and functionality
[main 28777039] refactor: improve connector_tools.py documentation and functionality
 1 file changed, 98 insertions(+)
 create mode 100644 tools/connector_tools.py
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/botify.py'.
(nix) pipulate $ d
diff --git a/scripts/connectors/botify.py b/scripts/connectors/botify.py
index 05da704a..dc51a3d0 100644
--- a/scripts/connectors/botify.py
+++ b/scripts/connectors/botify.py
@@ -261,7 +261,13 @@ def check():
 
 def main():
     parser = argparse.ArgumentParser(
-        description="Unix-philosophy gateway to the Botify API for Prompt Fu context."
+        # ONE SOURCE FOR THREE SURFACES (2026-08-30): the sources roster reads
+        # this module's docstring by AST, tools/connector_tools.py installs it
+        # as the registry tool's __doc__, and --help prints it here. A
+        # description that differs by surface is a confound wearing help's
+        # coat; RawDescriptionHelpFormatter keeps the example lines intact.
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
     )
     parser.add_argument(
         'query', nargs='?', default=None,
(nix) pipulate $ m
📝 Committing: chore: Update botify.py docstring with RawDescriptionHelpFormatter
[main 1f993dcb] chore: Update botify.py docstring with RawDescriptionHelpFormatter
 1 file changed, 7 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/botify.py'.
(nix) pipulate $ d
diff --git a/scripts/connectors/botify.py b/scripts/connectors/botify.py
index dc51a3d0..dfe1ec56 100644
--- a/scripts/connectors/botify.py
+++ b/scripts/connectors/botify.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# scripts/botify.py
+# scripts/connectors/botify.py
 """
 botify.py — Bring Botify crawl data and BQL query results into context.
 
(nix) pipulate $ m
📝 Committing: chore: Rename botify.py to scripts/connectors/botify.py
[main 03ba8ecc] chore: Rename botify.py to scripts/connectors/botify.py
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ a
a: command not found
(nix) pipulate $ 
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/botify.py'.
(nix) pipulate $ d
diff --git a/scripts/connectors/botify.py b/scripts/connectors/botify.py
index dfe1ec56..f3be80f9 100644
--- a/scripts/connectors/botify.py
+++ b/scripts/connectors/botify.py
@@ -7,10 +7,10 @@ A Unix-philosophy gateway to the Botify API for Prompt Fu context.
 
 Golden-path modes, auto-detected from the single positional argument:
 
-  python scripts/botify.py                    # LIST: identity walk -> all your org/project slugs
-  python scripts/botify.py org                # LIST: projects under that org slug
-  python scripts/botify.py org/project        # LIST: analyses (crawl snapshots) for that project
-  python scripts/botify.py '<BQL or JSON>'    # FETCH: run a query (needs org/project coordinates)
+  python scripts/connectors/botify.py                    # LIST: identity walk -> all your org/project slugs
+  python scripts/connectors/botify.py org                # LIST: projects under that org slug
+  python scripts/connectors/botify.py org/project        # LIST: analyses (crawl snapshots) for that project
+  python scripts/connectors/botify.py '<BQL or JSON>'    # FETCH: run a query (needs org/project coordinates)
 
 Designed to be dropped into adhoc.txt as a `!` chisel-strike, e.g.:
 
(nix) pipulate $ m
📝 Committing: chore: Update botify.py usage examples
[main b45fbabe] chore: Update botify.py usage examples
 1 file changed, 4 insertions(+), 4 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/botify.py'.
(nix) pipulate $ d
diff --git a/scripts/connectors/botify.py b/scripts/connectors/botify.py
index f3be80f9..6c3d7c97 100644
--- a/scripts/connectors/botify.py
+++ b/scripts/connectors/botify.py
@@ -14,9 +14,9 @@ Golden-path modes, auto-detected from the single positional argument:
 
 Designed to be dropped into adhoc.txt as a `!` chisel-strike, e.g.:
 
-  ! python scripts/botify.py
-  ! python scripts/botify.py my-org/my-project
-  ! python scripts/botify.py 'SELECT url FROM crawl' --org my-org --project my-project
+  ! python scripts/connectors/botify.py
+  ! python scripts/connectors/botify.py my-org/my-project
+  ! python scripts/connectors/botify.py 'SELECT url FROM crawl' --org my-org --project my-project
 
 Disambiguation rule: an argument that starts with '{' or contains whitespace is
 a query (FETCH mode); anything else is a slug path (LIST mode). No argument at
(nix) pipulate $ m
📝 Committing: chore: Update botify.py example usage in comments
[main 61e590f1] chore: Update botify.py example usage in comments
 1 file changed, 3 insertions(+), 3 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/botify.py'.
(nix) pipulate $ d
diff --git a/scripts/connectors/botify.py b/scripts/connectors/botify.py
index 6c3d7c97..868531ca 100644
--- a/scripts/connectors/botify.py
+++ b/scripts/connectors/botify.py
@@ -143,7 +143,7 @@ def list_identity(client, max_items):
     for p in projects:
         org, slug, name = project_coordinates(p)
         print(f"{org}/{slug}  {name}")
-    print("\n# Next: python scripts/botify.py <org>/<project>   (list analyses)")
+    print("\n# Next: python scripts/connectors/botify.py <org>/<project>   (list analyses)")
 
 
 def list_org_projects(client, org, max_items):
(nix) pipulate $ m
📝 Committing: chore: Update botify.py documentation
[main 5e8b1e38] chore: Update botify.py documentation
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/botify.py'.
(nix) pipulate $ d
diff --git a/scripts/connectors/botify.py b/scripts/connectors/botify.py
index 868531ca..f47e6857 100644
--- a/scripts/connectors/botify.py
+++ b/scripts/connectors/botify.py
@@ -156,7 +156,7 @@ def list_org_projects(client, org, max_items):
     for p in projects:
         _, slug, name = project_coordinates(p)
         print(f"{org}/{slug}  {name}")
-    print("\n# Next: python scripts/botify.py " + org + "/<project>   (list analyses)")
+    print("\n# Next: python scripts/connectors/botify.py " + org + "/<project>   (list analyses)")
 
 
 def list_analyses(client, org, project, max_items):
(nix) pipulate $ m
📝 Committing: chore: Update botify.py documentation
[main 886236e5] chore: Update botify.py documentation
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/botify.py'.
(nix) pipulate $ d
diff --git a/scripts/connectors/botify.py b/scripts/connectors/botify.py
index f47e6857..398f2005 100644
--- a/scripts/connectors/botify.py
+++ b/scripts/connectors/botify.py
@@ -172,7 +172,7 @@ def list_analyses(client, org, project, max_items):
         finished = a.get("date_finished") or a.get("date_created") or ""
         print(f"{slug}  {status}  {finished}")
     print(
-        "\n# Next: python scripts/botify.py 'SELECT url FROM crawl' "
+        "\n# Next: python scripts/connectors/botify.py 'SELECT url FROM crawl' "
         f"--org {org} --project {project}"
     )
 
(nix) pipulate $ m
📝 Committing: chore: Update botify.py script comment
[main 63150f24] chore: Update botify.py script comment
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'cli.py'.
(nix) pipulate $ d
diff --git a/cli.py b/cli.py
index ef8ad080..ba4f929d 100644
--- a/cli.py
+++ b/cli.py
@@ -139,9 +139,17 @@ def discover_tools(show_all=False, tool_name=None):
         console.print(f"❌ [bold red]Error running discovery:[/bold red] {e}")
         sys.exit(1)
 
-async def call_mcp_tool(tool_name: str, tool_args: dict):
-    """Execute an MCP tool with the given arguments."""
-    console.print(Panel(f"🔧 [bold cyan]Executing MCP Tool: {tool_name}[/bold cyan]", border_style="cyan"))
+async def call_mcp_tool(tool_name: str, tool_args: dict, raw: bool = False):
+    """Execute an MCP tool with the given arguments.
+
+    raw=True prints the tool's returned dict as JSON and nothing else: no
+    panel, no Rich table. Added 2026-08-30 for the two-arm experiment, where
+    the registry arm must see what a tools/call would actually return rather
+    than an 80-column table cell that wraps line-oriented stdout. In raw mode
+    the exit code carries the tool's own success field.
+    """
+    if not raw:
+        console.print(Panel(f"🔧 [bold cyan]Executing MCP Tool: {tool_name}[/bold cyan]", border_style="cyan"))
 
     try:
         # Import MCP tools module
(nix) pipulate $ m
📝 Committing: chore: Refactor call_mcp_tool to add raw mode and documentation 
[main 81e64fa4] chore: Refactor call_mcp_tool to add raw mode and documentation
 1 file changed, 11 insertions(+), 3 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'cli.py'.
(nix) pipulate $ d
diff --git a/cli.py b/cli.py
index ba4f929d..87d57a6a 100644
--- a/cli.py
+++ b/cli.py
@@ -163,7 +163,8 @@ async def call_mcp_tool(tool_name: str, tool_args: dict, raw: bool = False):
         
         # Execute the tool
         tool_handler = registry[tool_name]
-        console.print(f"⚡ Executing '{tool_name}' with args: {tool_args}")
+        if not raw:
+            console.print(f"⚡ Executing '{tool_name}' with args: {tool_args}")
         
         result = await tool_handler(tool_args)
         
(nix) pipulate $ m
📝 Committing: chore: Refactor CLI tool execution print statement
[main 0dd0e919] chore: Refactor CLI tool execution print statement
 1 file changed, 2 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'cli.py'.
(nix) pipulate $ d
diff --git a/cli.py b/cli.py
index 87d57a6a..72170620 100644
--- a/cli.py
+++ b/cli.py
@@ -168,6 +168,9 @@ async def call_mcp_tool(tool_name: str, tool_args: dict, raw: bool = False):
         
         result = await tool_handler(tool_args)
         
+        if raw:
+            print(json.dumps(result, indent=2, default=str))
+            return bool(result.get("success", True)) if isinstance(result, dict) else True
         # Display results
         console.print(f"✅ [bold green]Tool execution complete![/bold green]")
         
(nix) pipulate $ m
📝 Committing: fix: Add raw output option to MCP tool
[main de2779e8] fix: Add raw output option to MCP tool
 1 file changed, 3 insertions(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'cli.py'.
d(nix) pipulate $ d
diff --git a/cli.py b/cli.py
index 72170620..a4fdfbaf 100644
--- a/cli.py
+++ b/cli.py
@@ -526,7 +526,8 @@ def main():
 
     args = parser.parse_args()
 
-    console.print(Panel("🚀 [bold cyan]Pipulate :: The Local-First AI-Readiness & Automation Workshop[/bold cyan] 🚀", border_style="cyan"))
+    if not getattr(args, 'raw', False):
+        console.print(Panel("🚀 [bold cyan]Pipulate :: The Local-First AI-Readiness & Automation Workshop[/bold cyan] 🚀", border_style="cyan"))
 
     if args.command == 'install':
         if not check_nix_installed():
(nix) pipulate $ m
📝 Committing: chore: Update CLI panel print in conditional
[main 7d5953a7] chore: Update CLI panel print in conditional
 1 file changed, 2 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'cli.py'.
(nix) pipulate $ d
diff --git a/cli.py b/cli.py
index a4fdfbaf..13d94184 100644
--- a/cli.py
+++ b/cli.py
@@ -524,6 +524,9 @@ def main():
                             help='🎯 GOLDEN PATH: A JSON string containing all tool arguments. ' 
                                  'Use this for complex parameters to ensure perfect data transmission.')
 
+    call_parser.add_argument('--raw', action='store_true',
+                             help='Print the tool result as JSON and nothing else; '
+                                  'the exit code carries the tool\'s success field.')
     args = parser.parse_args()
 
     if not getattr(args, 'raw', False):
(nix) pipulate $ m
📝 Committing: chore: Add --raw argument to cli.py
[main 3b9319ae] chore: Add --raw argument to cli.py
 1 file changed, 3 insertions(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'cli.py'.
(nix) pipulate $ d
diff --git a/cli.py b/cli.py
index 13d94184..b94b29fd 100644
--- a/cli.py
+++ b/cli.py
@@ -578,7 +578,8 @@ def main():
         if args.json_args:
             try:
                 params = json.loads(args.json_args)
-                console.print(f"🎯 [bold green]Golden Path: Using JSON arguments[/bold green]")
+                if not args.raw:
+                    console.print(f"🎯 [bold green]Golden Path: Using JSON arguments[/bold green]")
             except json.JSONDecodeError as e:
                 console.print(f"❌ [bold red]Error: Invalid JSON provided to --json-args.[/bold red]")
                 console.print(f"JSON Error: {e}")
(nix) pipulate $ m
📝 Committing: chore: Improve CLI output formatting
[main e4c69820] chore: Improve CLI output formatting
 1 file changed, 2 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'cli.py'.
(nix) pipulate $ d
diff --git a/cli.py b/cli.py
index b94b29fd..91ff4136 100644
--- a/cli.py
+++ b/cli.py
@@ -601,7 +601,7 @@ def main():
         
         # Execute the tool
         try:
-            success = asyncio.run(call_mcp_tool(args.tool_name, params))
+            success = asyncio.run(call_mcp_tool(args.tool_name, params, raw=args.raw))
             if not success:
                 sys.exit(1)
         except KeyboardInterrupt:
(nix) pipulate $ m
📝 Committing: chore: Update call_mcp_tool to accept raw flag
[main a8414101] chore: Update call_mcp_tool to accept raw flag
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ git push
Enumerating objects: 74, done.
Counting objects: 100% (74/74), done.
Delta compression using up to 48 threads
Compressing objects: 100% (67/67), done.
Writing objects: 100% (67/67), 10.03 KiB | 2.51 MiB/s, done.
Total 67 (delta 48), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (48/48), completed with 7 local objects.
To github.com:pipulate/pipulate.git
   5688f658..a8414101  main -> main
(nix) pipulate $

4: Prompt: Four cars landed (or did not – read the LIVE COMMAND RECEIPTS, not my memory). Seven probes are echoed; name the lane for each and rule one line each:

  1. Probe 1: does the advanced_automation_tools copy of execute_shell_command carry @auto_tool? If yes, the registry winner is scandir-order luck; the denylist still holds, but say the ruling that system_tools is THE definition was unearned.
  2. Probe 2: scripts_botify_exists. 1 means the breadcrumbs were lying and Car 3 fixed a real defect; 0 means a shim exists and the path change was cosmetic.
  3. Probe 3: tools= must read 21 with shell_denied=True and botify_registered=True. 20 means the wrapper did not land; 22 means the denylist did not; botify_doc must be the connector’s own first docstring line.
  4. Probes 4 and 5: –help now opens with the module docstring; the stale-path count exits 1.
  5. Probe 6: deny_via_cli_exit=1 – the escape hatch is closed at the entry point Arm R actually uses.
  6. Probe 7: –raw prints JSON with no panel.

Then write scripts/two_arm.py to the harness contract from the previous turn: one RUN/FINAL line per model turn, prefix-checked with a 126 refusal, executed with bash -c inside the quiet shell the harness was started in, JSONL records with the named fields, oracle before each task and re-check after (void on drift), predicates applied to FINAL text only. Drive the model through the llm library so usage arrives per response; record the model string the API echoes. Do not run the experiment. Give me the smoke command that runs T1 once on each arm against a local model, and the two JSONL lines its receipt must contain.

Do not touch wallet.py, botify_tools.py, or any MCP file. The generic-vs-WET experiment is a separate ride.

5: Deliverables: None this turn.

I’m glad you’re finding the old MCP stuff. There’s stuff in there I would like to gut or move over to the new registration method but we don’t have to really distract ourselves on this turn with that, except maybe making sure there’s a to-do or earmark about it.

Fable 5 Extra: Context verified. The canary arrived linkified again; no SEARCH block below quotes a www-prefixed token, and the one WRITE_FILE carries none. Empty lines are stripped from file bodies in this compile as before; both SEARCH blocks are single lines.

The seven rulings

Every reading appears in both lanes. Probes 1 and 2 agree byte for byte (they straddle nothing); probes 3–7 differ exactly where a car landed between the taps, which is the straddle doing its job.

# Reading BEFORE (operator) AFTER (compile) Rules
1 lines above the advanced copy’s def # END: server_reboot / blank same no @auto_tool; the ruling that system_tools is THE definition was earned, not scandir luck
2 scripts_botify_exists 1 1 no such file; the breadcrumbs were lying and Car 3 fixed a real defect
3 registry 21 / False / False / none 21 / True / True / botify.py — Bring Botify… Cars 1 and 2 landed; 21 = 22 registered − 1 denied, so neither the 20-world nor the 22-world happened
4 --help “Unix-philosophy gateway…” the docstring’s first line Car 3, help half
5 stale path 11, exit 0 exit 1 Car 3, breadcrumb half; eleven lying lines gone
6 deny_via_cli_exit 0 (the hatch fired, harmlessly) 1 Car 1, closed at the entry point Arm R uses
7 --raw argparse unrecognized arguments { / "success": true, Car 4

Two things the receipts carried unasked. The Tool Roster now reads 22 with botify wearing its placeholder line — the KNOWN SEAM the wrapper’s docstring named, observed rather than predicted. And the compiler appended # tools/connector_tools.py to the Paintbox ledger in foo_files.py (the uncommitted diff in Telemetry); your next m sweeps it, which is fine.

The registration-lineage earmark

Banked below as Car 2, a comment in tools/__init__.py next to the reader it describes, so the next person who touches the registry finds it. What it records: get_all_tools() serves only @auto_tool functions; advanced_automation_tools.py (its own undecorated execute_shell_command fed to a register_mcp_tool this package never reads), mcp_tools.py (42 async defs), and botify_tools.py (six functions on aiohttp and an ai_dictdb credential path) are one pre-decorator lineage that cli.py cannot reach. Whether server.py’s bracket-command path still reaches them is unwitnessed. Gut or migrate; not this ride. If you keep earmarks in foo_files.py too, this is the paste-ready line, manual since that file is not in context:

# EARMARK 2026-08-30 REGISTRATION LINEAGE: advanced_automation_tools.py, mcp_tools.py, botify_tools.py predate @auto_tool and are unreachable from cli.py (receipt: get_all_tools serves system_tools:43; rg shows no decorator on the advanced copy). Gut or migrate to @auto_tool + config.get_botify_token; server.py's bracket path UNWITNESSED. See tools/__init__.py.

The harness

Car 1 writes scripts/two_arm.py to last turn’s contract. Decisions worth naming rather than burying in the file:

  • Discovery per arm is one command that prints the same docstring on both sides: botify.py --help for S, cli.py mcp-discover --tool botify for R. The catalogs (sources roster, mcp-discover --all) are a separate --catalog condition, because 7-vs-22 catalog size is a second variable and Experiment 1 is about grammar.
  • One hand-authored line per arm, the Shape: positional/flags for S, --raw --json-args for R. The harness does not rewrite commands; if the model forgets --raw it gets the table, and that cost lands on the arm that incurred it.
  • Refusals and protocol violations are kind: run with exit 126 and 125, so calls counts them as the contract says.
  • Set-valued oracles (T4, T5) void only when before ∩ after is empty, and the predicate accepts any URL from before ∪ after; scalar oracles void on any change.
  • --dry-run --tasks (empty) prints discovery stats and touches no network; that is what the probes use. --dry-run --tasks T1 T2 also runs oracles, which print identifiers, so it stays in the operator lane.
  • Records land under browser_cache/two_arm/, gitignored. The jq projection below omits text, before, and after, which is what makes a receipt safe to paste.

Smoke (operator lane — a model call is not a probe):

.venv/bin/python scripts/two_arm.py --arm S --tasks T1 --model gemma3:latest && .venv/bin/python scripts/two_arm.py --arm R --tasks T1 --model gemma3:latest
jq -c 'select(.kind=="final") | {arm,task,session,pass,void,calls,capped,prompt_tokens,completion_tokens,model}' browser_cache/two_arm/*.jsonl

The two JSONL lines the receipt must contain, one per arm, with these invariants: "task":"T1", "pass":true, "void":false, "capped":false, "calls" ≥ 1 on both. A pass:true with calls:0 is a guessed username and is the finding, not a pass. The token counts will differ and that difference is the first data point.

1. PROBES

test -e scripts/two_arm.py; echo "harness_exists=$?"
.venv/bin/python scripts/two_arm.py --arm S --dry-run --tasks 2>&1 | tail -2
.venv/bin/python scripts/two_arm.py --arm R --dry-run --tasks 2>&1 | tail -2
rg -n 'TWO REGISTRATION LINEAGES' tools/__init__.py; echo "earmark_exit=$?"
.venv/bin/llm models 2>/dev/null | rg -i -c 'gemma'; echo "gemma_listed_exit=$?"

Probe 1 flips 1 → 0 when Car 1 lands. Probes 2 and 3 are the harness’s own straddle: BEFORE is one line of “can’t open file”; AFTER is arm=… prefix=… then discovery_chars=… shared_doc_line_present=True — that boolean is the description-quality control witnessed, since it checks that the connector’s first docstring line appears in each arm’s discovery text. Probe 3 imports the registry (about 5 s) and sets the denylist; neither opens a network socket. Probe 4 flips exit 1 → 0 with Car 2. Probe 5 is a BEFORE fact for the smoke: whether llm lists a gemma model at all; a count of 0 with exit 1 means the smoke needs --model pointed at whatever llm models does list.

2. NEXT CONTEXT

! test -e scripts/two_arm.py; echo "harness_exists=$?"
! .venv/bin/python scripts/two_arm.py --arm S --dry-run --tasks 2>&1 | tail -2
! .venv/bin/python scripts/two_arm.py --arm R --dry-run --tasks 2>&1 | tail -2
! rg -n 'TWO REGISTRATION LINEAGES' tools/__init__.py; echo "earmark_exit=$?"
! .venv/bin/llm models 2>/dev/null | rg -i -c 'gemma'; echo "gemma_listed_exit=$?"
scripts/two_arm.py
tools/connector_tools.py
scripts/connectors/botify.py

Drop cli.py and tools/__init__.py for the smoke-reading turn; add either back only if the smoke receipt names it.

3. PATCHES

Car 1 — the harness. New file.

Target: scripts/two_arm.py
[[[WRITE_FILE]]]
#!/usr/bin/env python3
"""
two_arm.py -- Run the shell-verb vs registry-tool experiment against one connector.

NOT A CONNECTOR. This is the harness for Experiment 1 (invocation grammar):
Arm S runs scripts/connectors/botify.py as a command line; Arm R calls the
same connector through cli.py's registry face (tools/connector_tools.py).
Same backend, same docstring, same tasks, same oracle. The only variables are
the invocation grammar and the discovery surface.

THE PROTOCOL IS ONE LINE PER MODEL TURN. The model replies `RUN: <command>` or
`FINAL: <answer>`. A RUN outside the arm's allowed prefix is refused with exit
126 and counts as a call. A reply with neither verb is a protocol violation,
exit 125, also a call. The loop ends at FINAL or at --cap calls; the cap
forces an empty FINAL, which fails.

THE ORACLE IS THE CONNECTOR ITSELF, run by the harness before AND after each
task, arm-independent. The predicate is checked against the BEFORE reading;
if the AFTER reading disagrees the trial is VOID (D1 drift: a project list can
change under you). Predicates apply to the text after FINAL: only, never to
tool stdout, so a connector's own output cannot pass a check by appearing in
the transcript.

RUN INSIDE THE QUIET SHELL. Start this program from `nix develop .#quiet` (or
an already-entered `nix develop`); every RUN executes with bash -c in the
environment this process inherited, so both arms see the same PATH, the same
.venv and the same BOTIFY_API_TOKEN. Nothing here spawns nix.

ONE HAND-AUTHORED LINE PER ARM. Each arm's system prompt carries its allowed
prefix and a one-line Shape (positional/flags for S, --raw --json-args for R).
Capability description comes only from the arm's own discovery command, and
on both arms that command prints the same module docstring.

RECORDS ARE JSONL, one per model turn, under browser_cache/two_arm/ (gitignored):
  arm task session seq kind command exit_code stdout_bytes stderr_bytes
  prompt_tokens completion_tokens text pass
plus model, model_echo, ts on every record; kind is run | final | oracle.
The final record adds void, calls, capped; the oracle record adds before,
after, void, reason. Refusals and protocol violations are kind=run with exit
126 and 125, so a count of run records IS the calls metric.

COMPILE-LANE CAUTION: oracle values and FINAL texts carry the account's
username and client org/project slugs. Records stay under browser_cache/;
project them with jq (omit text, before, after) before any `!` line rides to a
cloud chat window. A model call is a mutation of a token ledger, not a probe:
never echo a non-dry run into adhoc.txt.

Usage:
  .venv/bin/python scripts/two_arm.py --arm S --tasks T1 --model gemma3:latest
  .venv/bin/python scripts/two_arm.py --arm R --tasks T1 --model gemma3:latest
  .venv/bin/python scripts/two_arm.py --arm S --tasks T3 T4 T5 --org ORG --project PROJ
  .venv/bin/python scripts/two_arm.py --arm R --dry-run --tasks          # discovery only
  .venv/bin/python scripts/two_arm.py --arm S --dry-run --tasks T1 T2    # + oracles, no model
"""
import argparse
import ast
import json
import os
import re
import shlex
import subprocess
import sys
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
CONNECTOR_PATH = REPO_ROOT / "scripts" / "connectors" / "botify.py"
CONNECTOR = ".venv/bin/python scripts/connectors/botify.py"
CLI = ".venv/bin/python cli.py"
OUT_DIR = REPO_ROOT / "browser_cache" / "two_arm"
STDOUT_CAP = 4000
STDERR_CAP = 1000
DISCOVERY_CAP = 12000
CMD_TIMEOUT = 180
EXIT_TIMEOUT = 124
EXIT_PROTOCOL = 125
EXIT_REFUSED = 126

ARMS = {
    "S": {
        "prefix": CONNECTOR,
        "shape": CONNECTOR + " [query] [--org ORG] [--project PROJECT] [-n MAX] [--check]",
        "env": {},
        "discovery": [CONNECTOR + " --help"],
        "catalog": [".venv/bin/python scripts/sources_menu.py"],
    },
    "R": {
        "prefix": CLI,
        "shape": CLI + " call botify --raw --json-args '<json object>'",
        "env": {"PIPULATE_TOOL_DENY": "execute_shell_command"},
        "discovery": [CLI + " mcp-discover --tool botify"],
        "catalog": [CLI + " mcp-discover --all"],
    },
}

URL_RE = re.compile(r"https?://[^\s\"'<>\\)\]]+")
REPLY_RE = re.compile(r"^\s*[`*\->]*\s*(RUN|FINAL)\s*:\s*(.*?)\s*`*\s*$", re.IGNORECASE)

def utc_now():
    return datetime.now(timezone.utc).isoformat(timespec="seconds")

def run_cmd(cmd, env_extra=None, timeout=CMD_TIMEOUT):
    """bash -c in the inherited environment -> (exit_code, stdout, stderr, seconds)."""
    env = dict(os.environ)
    env.update(env_extra or {})
    t0 = time.monotonic()
    try:
        proc = subprocess.run(
            ["bash", "-c", cmd], cwd=str(REPO_ROOT), env=env,
            capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired as exc:
        out = exc.stdout if isinstance(exc.stdout, str) else (exc.stdout or b"").decode("utf-8", "replace")
        err = exc.stderr if isinstance(exc.stderr, str) else (exc.stderr or b"").decode("utf-8", "replace")
        return EXIT_TIMEOUT, out, err + f"\n[timeout after {timeout}s]", time.monotonic() - t0
    return proc.returncode, proc.stdout, proc.stderr, time.monotonic() - t0

# --- oracles: the connector itself, arm-independent ------------------------

def _data_lines(stdout):
    return [ln for ln in stdout.splitlines()
            if ln.strip() and not ln.lstrip().startswith("#")
            and not ln.lstrip().startswith("(")]

def oracle_username(ctx):
    _, out, _, _ = run_cmd(CONNECTOR + " --check")
    m = re.search(r"botify GREEN (\S+)", out)
    return m.group(1) if m else None

def oracle_first_project(ctx):
    _, out, _, _ = run_cmd(CONNECTOR + " -n 25")
    lines = _data_lines(out)
    return lines[0].split()[0] if lines else None

def oracle_newest_analysis(ctx):
    target = shlex.quote(ctx["org"] + "/" + ctx["project"])
    _, out, _, _ = run_cmd(CONNECTOR + " " + target + " -n 1")
    lines = _data_lines(out)
    return lines[0].split()[0] if lines else None

def oracle_urls(ctx):
    cmd = (CONNECTOR + " 'SELECT url FROM crawl' --org " + shlex.quote(ctx["org"])
           + " --project " + shlex.quote(ctx["project"]) + " -n 25")
    _, out, _, _ = run_cmd(cmd)
    return sorted(set(URL_RE.findall(out)))

def oracle_missing_project(ctx):
    target = shlex.quote(ctx["org"] + "/" + ctx["bogus"])
    _, _, err, _ = run_cmd(CONNECTOR + " " + target + " -n 1")
    m = re.search(r"HTTP (\d{3})", err)
    return m.group(1) if m else None

TASKS = {
    "T1": {"text": "What Botify username is this environment authenticated as?",
           "oracle": oracle_username, "kind": "scalar", "needs": ()},
    "T2": {"text": "Name the first org/project slug pair listed for this account.",
           "oracle": oracle_first_project, "kind": "scalar", "needs": ()},
    "T3": {"text": "For the Botify project {org}/{project}, what is the slug of the newest analysis?",
           "oracle": oracle_newest_analysis, "kind": "scalar", "needs": ("org", "project")},
    "T4": {"text": ("Run the BQL query SELECT url FROM crawl against the Botify project "
                    "{org}/{project} with a cap of 5 and report one URL it returned."),
           "oracle": oracle_urls, "kind": "set", "needs": ("org", "project")},
    "T5": {"text": ("Run the BQL query SELECT url FROM crawl against the Botify project "
                    "{project}, under the org this account belongs to, with a cap of 5 "
                    "and report one URL it returned."),
           "oracle": oracle_urls, "kind": "set", "needs": ("org", "project")},
    "N3": {"text": "For the Botify project {org}/{bogus}, what is the slug of the newest analysis?",
           "oracle": oracle_missing_project, "kind": "scalar", "needs": ("org",)},
}

def drift(before, after, kind):
    if kind == "set":
        return not (set(before or []) & set(after or []))
    return before != after

def passes(final_text, before, after, kind):
    if not final_text:
        return False
    if kind == "set":
        return any(u in final_text for u in set(before or []) | set(after or []))
    return bool(before) and before in final_text

# --- discovery and the system prompt ---------------------------------------

def connector_doc_line():
    """First docstring line of the connector, self-label stripped, as sources_menu.py shows it."""
    try:
        doc = ast.get_docstring(ast.parse(CONNECTOR_PATH.read_text(encoding="utf-8"))) or ""
    except (OSError, SyntaxError):
        return ""
    line = doc.strip().splitlines()[0].strip() if doc.strip() else ""
    return re.sub(r"^\S+\.py\s+\S+\s+", "", line)

def discovery_text(arm, catalog):
    spec = ARMS[arm]
    cmds = list(spec["discovery"]) + (list(spec["catalog"]) if catalog else [])
    chunks = []
    for cmd in cmds:
        _, out, err, _ = run_cmd(cmd, spec["env"])
        chunks.append("$ " + cmd + "\n" + out + err)
    return "\n\n".join(chunks)[:DISCOVERY_CAP]

def system_prompt(arm, disc):
    spec = ARMS[arm]
    return (
        "You are operating a terminal to answer one question. Reply with EXACTLY ONE "
        "line per turn: either `RUN: <command>` to execute a command, or "
        "`FINAL: <answer>` when you have the answer. Do not explain.\n"
        "You may only run commands that begin with: " + spec["prefix"] + "\n"
        "Shape: " + spec["shape"] + "\n"
        "Any other command is refused. After each RUN you receive its exit code, "
        "stdout and stderr.\n\nDISCOVERY (what the command can do):\n" + disc
    )

# --- the model, through the llm library ------------------------------------

def load_model(model_id):
    import llm  # lazy: --dry-run must not need a model
    return llm.get_model(model_id)

def ask(conv, text, system=None, temperature=True):
    """One model turn -> (reply_text, prompt_tokens, completion_tokens, model_echo)."""
    kwargs = {}
    if system:
        kwargs["system"] = system
    if temperature:
        kwargs["temperature"] = 0
    resp = conv.prompt(text, **kwargs)
    reply = resp.text()
    try:
        usage = resp.usage()
        p_tok, c_tok = int(usage.input or 0), int(usage.output or 0)
    except Exception:
        p_tok, c_tok = 0, 0
    echo = None
    raw = getattr(resp, "response_json", None)
    if isinstance(raw, dict):
        echo = raw.get("model")
    return reply, p_tok, c_tok, echo

def parse_reply(reply):
    for line in reply.splitlines():
        m = REPLY_RE.match(line)
        if m:
            return m.group(1).upper(), m.group(2).strip()
    return None, None

# --- one session = one task, one arm, one conversation ----------------------

def run_session(arm, task_id, session_idx, ctx, model, args, fh, disc, base):
    spec = ARMS[arm]
    task = TASKS[task_id]

    def emit(**rec):
        rec = {**base, "task": task_id, "session": session_idx, **rec}
        rec.setdefault("ts", utc_now())
        fh.write(json.dumps(rec, default=str) + "\n")
        fh.flush()

    missing = [k for k in task["needs"] if not ctx.get(k)]
    if missing:
        emit(seq=0, kind="oracle", before=None, after=None, void=True,
             reason="missing " + ",".join(missing))
        return None
    before = task["oracle"](ctx)
    if not before:
        emit(seq=0, kind="oracle", before=before, after=None, void=True, reason="oracle_empty")
        return None

    conv = model.conversation()
    reply, p_tok, c_tok, echo = ask(conv, task["text"].format(**ctx),
                                    system=system_prompt(arm, disc),
                                    temperature=not args.no_temperature)
    seq, calls, tokens = 0, 0, 0
    final_text, final_tokens = None, (0, 0)
    while True:
        seq += 1
        tokens += p_tok + c_tok
        verb, rest = parse_reply(reply)
        if verb == "FINAL":
            final_text, final_tokens = rest, (p_tok, c_tok)
            break
        calls += 1
        if verb == "RUN" and rest.startswith(spec["prefix"]):
            cmd = rest
            code, out, err, secs = run_cmd(rest, spec["env"])
        elif verb == "RUN":
            cmd, code, out, err, secs = rest, EXIT_REFUSED, "", "refused: outside arm", 0.0
        else:
            cmd, code, out, err, secs = "", EXIT_PROTOCOL, "", (
                "protocol: reply with exactly one line, RUN: <command> or FINAL: <answer>"), 0.0
        emit(seq=seq, kind="run", command=cmd, exit_code=code,
             stdout_bytes=len(out.encode("utf-8")), stderr_bytes=len(err.encode("utf-8")),
             prompt_tokens=p_tok, completion_tokens=c_tok, seconds=round(secs, 3),
             text=reply[:500], model_echo=echo, **{"pass": None})
        if calls >= args.cap:
            final_text = ""
            break
        feedback = ("exit_code: " + str(code) + "\nstdout:\n" + out[:STDOUT_CAP]
                    + "\nstderr:\n" + err[:STDERR_CAP])
        reply, p_tok, c_tok, echo = ask(conv, feedback, temperature=not args.no_temperature)

    after = task["oracle"](ctx)
    void = drift(before, after, task["kind"])
    ok = (not void) and passes(final_text, before, after, task["kind"])
    emit(seq=seq, kind="final", command=None, exit_code=None, stdout_bytes=0, stderr_bytes=0,
         prompt_tokens=final_tokens[0], completion_tokens=final_tokens[1],
         text=final_text, model_echo=echo, calls=calls,
         capped=(final_text == "" and calls >= args.cap), void=void,
         **{"pass": (None if void else ok)})
    emit(seq=seq + 1, kind="oracle", before=before, after=after, void=void,
         reason=("drift" if void else None))
    return ok, void, calls, tokens

def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--arm", choices=sorted(ARMS), required=True)
    ap.add_argument("--tasks", nargs="*", default=["T1", "T2", "T3", "T4", "T5"],
                    help="task ids (T1..T5, N3 negative control); bare --tasks = discovery only")
    ap.add_argument("--sessions", type=int, default=1)
    ap.add_argument("--model", default="gemma3:latest", help="an id from `llm models`")
    ap.add_argument("--org", default=os.getenv("BOTIFY_ORG"))
    ap.add_argument("--project", default=os.getenv("BOTIFY_PROJECT"))
    ap.add_argument("--cap", type=int, default=8, help="max RUN calls per task")
    ap.add_argument("--catalog", action="store_true",
                    help="also feed the arm's catalog (sources roster / mcp-discover --all)")
    ap.add_argument("--no-temperature", action="store_true",
                    help="omit temperature=0 for models whose Options lack it")
    ap.add_argument("--dry-run", action="store_true",
                    help="discovery + oracles only; no model, no records")
    ap.add_argument("--out", default=None, help="JSONL path (default: browser_cache/two_arm/<ts>__<arm>.jsonl)")
    args = ap.parse_args()

    unknown = [t for t in args.tasks if t not in TASKS]
    if unknown:
        sys.stderr.write("unknown task(s): " + " ".join(unknown) + "; known: " + " ".join(TASKS) + "\n")
        return 2
    ctx = {"org": args.org, "project": args.project,
           "bogus": "no-such-project-" + uuid.uuid4().hex[:8]}

    disc = discovery_text(args.arm, args.catalog)
    shared = connector_doc_line()
    print("arm=" + args.arm + " prefix=" + ARMS[args.arm]["prefix"])
    print("discovery_chars=" + str(len(disc))
          + " shared_doc_line_present=" + str(bool(shared) and shared in disc))

    if args.dry_run:
        for t in args.tasks:
            task = TASKS[t]
            missing = [k for k in task["needs"] if not ctx.get(k)]
            if missing:
                print(t + " oracle=skipped missing=" + ",".join(missing))
                continue
            val = task["oracle"](ctx)
            shown = val if task["kind"] == "scalar" else str(len(val)) + " url(s)"
            print(t + " oracle=" + str(shown))
        return 0
    if not args.tasks:
        return 0

    model = load_model(args.model)
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    out_path = Path(args.out) if args.out else OUT_DIR / (stamp + "__" + args.arm + ".jsonl")
    base = {"arm": args.arm, "model": args.model, "batch": stamp}
    with out_path.open("a", encoding="utf-8") as fh:
        for t in args.tasks:
            for s in range(args.sessions):
                res = run_session(args.arm, t, s, ctx, model, args, fh, disc, base)
                if res is None:
                    print(args.arm + " " + t + " s" + str(s) + " skipped (see oracle record)")
                    continue
                ok, void, calls, tokens = res
                print(args.arm + " " + t + " s" + str(s) + " pass=" + str(ok) + " void=" + str(void)
                      + " calls=" + str(calls) + " tokens=" + str(tokens))
    print("# records: " + str(out_path))
    return 0

if __name__ == "__main__":
    sys.exit(main())
[[[END_WRITE_FILE]]]

Car 2 — the earmark. One block, anchored on the version line because # --- END NEW --- occurs three times in the file.

Target: tools/__init__.py
[[[SEARCH]]]
__version__ = "1.0.0"
[[[DIVIDER]]]
# EARMARK (2026-08-30): TWO REGISTRATION LINEAGES, ONE READER. get_all_tools()
# serves only @auto_tool functions. tools/advanced_automation_tools.py still
# defines its own execute_shell_command (undecorated -- receipt: rg -n -B2
# '^async def execute_shell_command' shows a comment and a blank line above
# it) and hands it to a register_mcp_tool() that feeds a registry THIS package
# never reads; tools/mcp_tools.py (42 async defs) and tools/botify_tools.py
# (six core functions on aiohttp and an ai_dictdb credential path, zero
# decorators) are the same pre-@auto_tool lineage. cli.py cannot reach any of
# them. Whether server.py's bracket-command path still can is UNWITNESSED.
# Gut or migrate: each function either gains @auto_tool plus the current
# credential path (config.get_botify_token, or the subprocess-the-connector
# pattern in connector_tools.py) or is deleted, and the botify_exports import
# below leaves with botify_tools.py. Not this ride.
__version__ = "1.0.0"
[[[REPLACE]]]

Choreography: patch, app, git add scripts/two_arm.py, d, m for Car 1 (the created file needs the add, as connector_tools.py did); patch, app, d, m for Car 2; then blast. Ignition: none required — probes 1–3 execute the new file directly, probe 4 reads the comment from disk. The smoke is an actuator in the operator lane and is not echoed.

4. PROMPT

The harness landed (or did not -- read the LIVE COMMAND RECEIPTS, not my memory). Five probes are echoed; name the lane for each and rule one line each:
1. Probe 1: harness_exists=0.
2. Probes 2 and 3: both arms print arm=/discovery_chars= lines and shared_doc_line_present=True. False on either arm means that arm's discovery command is not printing the connector's docstring, and the description-quality control is broken on that side -- name which.
3. Probe 4: earmark_exit=0.
4. Probe 5: the gemma count. 0 with exit 1 means the smoke's --model must change; name the substitute from `llm models` if I paste it.

Then read the SMOKE RECEIPT pasted below (operator lane -- a model call is not a probe): the jq projection of the final records for T1 on S and T1 on R. Rule on each: pass, void, calls, capped, prompt+completion tokens. pass=true with calls=0 is a guessed username; name it. If either arm failed, name the gate -- discovery text, protocol parse (exit 125 records), prefix refusal (exit 126 records), oracle (oracle record reason), or predicate -- and STOP. Diagnosing a gate and redesigning it are different turns.

If both passed: give me the full-run commands (five tasks, ten sessions, both arms) and the one jq line that produces the four-metric table -- success, tokens, calls, recovery -- per arm per task from the JSONL. Do not run anything.

Do NOT touch wallet.py, botify_tools.py, advanced_automation_tools.py, or any MCP file; the registration-lineage earmark is banked, not a ride.

5. EXTERNAL DELIVERABLES

None. The foo_files.py earmark line above is a manual paste inside this repo, not a patch, because that file is not in context.

MikeLev.in: 1

1: Probe:

Not technically a probe but included:

$ 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 scripts/two_arm.py --arm S --tasks T1 --model gemma3:latest && .venv/bin/python scripts/two_arm.py --arm R --tasks T1 --model gemma3:latest
jq -c 'select(.kind=="final") | {arm,task,session,pass,void,calls,capped,prompt_tokens,completion_tokens,model}' browser_cache/two_arm/*.jsonl
.venv/bin/python: can't open file '/home/mike/repos/pipulate/scripts/two_arm.py': [Errno 2] No such file or directory
jq: error: Could not open file browser_cache/two_arm/*.jsonl: No such file or directory
(nix) pipulate $ 

And the real probe:

(nix) pipulate $ test -e scripts/two_arm.py; echo "harness_exists=$?"
.venv/bin/python scripts/two_arm.py --arm S --dry-run --tasks 2>&1 | tail -2
.venv/bin/python scripts/two_arm.py --arm R --dry-run --tasks 2>&1 | tail -2
rg -n 'TWO REGISTRATION LINEAGES' tools/__init__.py; echo "earmark_exit=$?"
.venv/bin/llm models 2>/dev/null | rg -i -c 'gemma'; echo "gemma_listed_exit=$?"
harness_exists=1
.venv/bin/python: can't open file '/home/mike/repos/pipulate/scripts/two_arm.py': [Errno 2] No such file or directory
.venv/bin/python: can't open file '/home/mike/repos/pipulate/scripts/two_arm.py': [Errno 2] No such file or directory
earmark_exit=1
14
gemma_listed_exit=0
(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/ _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |  No, not more files! Sigh, okay we'll clean up later.
# 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]

! test -e scripts/two_arm.py; echo "harness_exists=$?"
! .venv/bin/python scripts/two_arm.py --arm S --dry-run --tasks 2>&1 | tail -2
! .venv/bin/python scripts/two_arm.py --arm R --dry-run --tasks 2>&1 | tail -2
! rg -n 'TWO REGISTRATION LINEAGES' tools/__init__.py; echo "earmark_exit=$?"
! .venv/bin/llm models 2>/dev/null | rg -i -c 'gemma'; echo "gemma_listed_exit=$?"
scripts/two_arm.py
tools/connector_tools.py
scripts/connectors/botify.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
✅ WHOLE-FILE WRITE: CREATED 'scripts/two_arm.py'.
(nix) pipulate $ git add scripts/two_arm.py
(nix) pipulate $ m
📝 Committing: chore: Update two_arm.py script for experiment 1
[main e8daf798] chore: Update two_arm.py script for experiment 1
 1 file changed, 402 insertions(+)
 create mode 100644 scripts/two_arm.py
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'tools/__init__.py'.
(nix) pipulate $ d
diff --git a/tools/__init__.py b/tools/__init__.py
index 44da8ceb..61230f4b 100644
--- a/tools/__init__.py
+++ b/tools/__init__.py
@@ -66,6 +66,19 @@ def denied_tools():
     return {name.strip() for name in raw.split(",") if name.strip()}
 # --- END NEW ---
 
+# EARMARK (2026-08-30): TWO REGISTRATION LINEAGES, ONE READER. get_all_tools()
+# serves only @auto_tool functions. tools/advanced_automation_tools.py still
+# defines its own execute_shell_command (undecorated -- receipt: rg -n -B2
+# '^async def execute_shell_command' shows a comment and a blank line above
+# it) and hands it to a register_mcp_tool() that feeds a registry THIS package
+# never reads; tools/mcp_tools.py (42 async defs) and tools/botify_tools.py
+# (six core functions on aiohttp and an ai_dictdb credential path, zero
+# decorators) are the same pre-@auto_tool lineage. cli.py cannot reach any of
+# them. Whether server.py's bracket-command path still can is UNWITNESSED.
+# Gut or migrate: each function either gains @auto_tool plus the current
+# credential path (config.get_botify_token, or the subprocess-the-connector
+# pattern in connector_tools.py) or is deleted, and the botify_exports import
+# below leaves with botify_tools.py. Not this ride.
 __version__ = "1.0.0"
 
 # Import shared constants to eliminate duplication
(nix) pipulate $ m
📝 Committing: chore: Refactor tools initialization with lineage notes
[main 26e45058] chore: Refactor tools initialization with lineage notes
 1 file changed, 13 insertions(+)
(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), 7.33 KiB | 7.33 MiB/s, done.
Total 8 (delta 5), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (5/5), completed with 4 local objects.
To github.com:pipulate/pipulate.git
   2c0e92af..26e45058  main -> main
(nix) pipulate $

4: Prompt:

The harness landed (or did not – read the LIVE COMMAND RECEIPTS, not my memory). Five probes are echoed; name the lane for each and rule one line each:

  1. Probe 1: harness_exists=0.
  2. Probes 2 and 3: both arms print arm=/discovery_chars= lines and shared_doc_line_present=True. False on either arm means that arm’s discovery command is not printing the connector’s docstring, and the description-quality control is broken on that side – name which.
  3. Probe 4: earmark_exit=0.
  4. Probe 5: the gemma count. 0 with exit 1 means the smoke’s –model must change; name the substitute from llm models if I paste it.

Then read the SMOKE RECEIPT pasted below (operator lane – a model call is not a probe): the jq projection of the final records for T1 on S and T1 on R. Rule on each: pass, void, calls, capped, prompt+completion tokens. pass=true with calls=0 is a guessed username; name it. If either arm failed, name the gate – discovery text, protocol parse (exit 125 records), prefix refusal (exit 126 records), oracle (oracle record reason), or predicate – and STOP. Diagnosing a gate and redesigning it are different turns.

If both passed: give me the full-run commands (five tasks, ten sessions, both arms) and the one jq line that produces the four-metric table – success, tokens, calls, recovery – per arm per task from the JSONL. Do not run anything.

Do NOT touch wallet.py, botify_tools.py, advanced_automation_tools.py, or any MCP file; the registration-lineage earmark is banked, not a ride.

5: Deliverables: None this turn. Please don’t go making more and more files. I think getting over this hump with the 2-lane MCP stuff to sort it all out is fine, but less is more whenever we can help it.

Fable 5 Extra: Context verified. Canary linkified again; the one SEARCH block below quotes no www-prefixed token. Blank lines are stripped in this payload as before; the SEARCH block is two adjacent lines inside one function.

Heard on files: zero new ones this turn. One line changes inside a file that exists, and everything after that is running things, not building them.

The rulings

# Reading BEFORE (operator) AFTER (compile) Rules
1 harness_exists 1 0 Car 1 landed
2 Arm S dry-run “can’t open file” discovery_chars=2450 shared_doc_line_present=True S’s discovery prints the connector’s docstring intact
3 Arm R dry-run “can’t open file” discovery_chars=3189 shared_doc_line_present=False the control is broken on R
4 earmark_exit 1 0 Car 2 landed
5 gemma count 14, exit 0 14, exit 0 a gemma exists; gemma3:latest specifically is unwitnessed

Why R reads False, INFERRED until probe 2 says so. cli.py mcp-discover --tool botify prints the docstring through Rich’s console.print. With no TTY, Rich wraps at 80 cells. The prefix 📋 Docstring: is 14 cells and the connector’s first line is 71, so the line is 85 wide and its last word wraps: the text reads …results into newline context., and the sentence is no longer the same string. Arm S survives because argparse’s RawDescriptionHelpFormatter leaves the description verbatim. This is the same class of artifact Car 4’s --raw removed on the call side — an 80-column no-TTY default, not a property of either protocol — and the example lines in the docstring (over 80 wide) are being wrapped mid-command on R for the same reason.

The fix that touches no repo file but the harness: give both arms one fixed terminal width. Rich and argparse both honor COLUMNS. Set, not setdefault, so the reading is the same on every machine.

The smoke receipt does not exist. Both smoke lines in your section 1 are “No such file” — the actuator fired before the car landed, which is a premature BEFORE, not a receipt. Per the caboose: no ruling, no full-run commands. The smoke rides after Car 1 below and after probe 2 confirms the diagnosis.

Smoke (operator lane, after app and after wide_exit=0):

.venv/bin/python scripts/two_arm.py --arm S --tasks T1 --model gemma3:latest && .venv/bin/python scripts/two_arm.py --arm R --tasks T1 --model gemma3:latest
jq -c 'select(.kind=="final") | {arm,task,session,pass,void,calls,capped,prompt_tokens,completion_tokens,model}' browser_cache/two_arm/*.jsonl

Housekeeping: the compiler appended # scripts/two_arm.py to the Paintbox ledger in foo_files.py (uncommitted, in Telemetry); your next m sweeps it.

1. PROBES

.venv/bin/python cli.py mcp-discover --tool botify 2>/dev/null | rg -c 'Bring Botify crawl data and BQL query results into context'; echo "narrow_exit=$?"
COLUMNS=200 .venv/bin/python cli.py mcp-discover --tool botify 2>/dev/null | rg -c 'Bring Botify crawl data and BQL query results into context'; echo "wide_exit=$?"
.venv/bin/python scripts/two_arm.py --arm R --dry-run --tasks 2>&1 | tail -1
.venv/bin/python scripts/two_arm.py --arm S --dry-run --tasks 2>&1 | tail -1
.venv/bin/llm models 2>/dev/null | rg -c 'gemma3:latest'; echo "gemma3_latest_exit=$?"

Probes 1 and 2 are the falsifier pair, read in either lane without the car: narrow_exit=1 (no count) with wide_exit=0 (count 1) convicts the 80-column wrap and licenses Car 1; wide_exit=1 falsifies it, and the fix then belongs in cli.py’s --tool printer, not here. Probe 3 is Car 1’s straddle: False before, True after. Probe 4 is the no-regression half for S: True both times. Probe 5 pins the exact model id the smoke names. Four of the five import the registry at about 5 s each; none opens a network socket.

2. NEXT CONTEXT

! .venv/bin/python cli.py mcp-discover --tool botify 2>/dev/null | rg -c 'Bring Botify crawl data and BQL query results into context'; echo "narrow_exit=$?"
! COLUMNS=200 .venv/bin/python cli.py mcp-discover --tool botify 2>/dev/null | rg -c 'Bring Botify crawl data and BQL query results into context'; echo "wide_exit=$?"
! .venv/bin/python scripts/two_arm.py --arm R --dry-run --tasks 2>&1 | tail -1
! .venv/bin/python scripts/two_arm.py --arm S --dry-run --tasks 2>&1 | tail -1
! .venv/bin/llm models 2>/dev/null | rg -c 'gemma3:latest'; echo "gemma3_latest_exit=$?"
scripts/two_arm.py
# cli.py  # <-- uncomment only if wide_exit=1: then the wrap is not COLUMNS and the fix lives in cli.py's --tool printer

3. PATCHES

Car 1 — one terminal width for both arms. Ride only if probe 2 read wide_exit=0; if it read 1, do not apply, and pull cli.py in next turn.

Target: scripts/two_arm.py
[[[SEARCH]]]
    env = dict(os.environ)
    env.update(env_extra or {})
[[[DIVIDER]]]
    env = dict(os.environ)
    env.update(env_extra or {})
    # ONE TERMINAL WIDTH FOR BOTH ARMS (convicted 2026-08-30 by this file's own
    # dry-run: shared_doc_line_present read True on S and False on R). Under
    # no TTY both Rich and argparse default to 80 columns and re-wrap what
    # they print; the connector's first docstring line is 85 cells wide once
    # cli.py prefixes it with "Docstring:", so its last word landed on a new
    # line and the same sentence stopped being the same string. An 80-column
    # wrap is a property of the subprocess, not of either protocol -- the
    # same class of artifact --raw removed on the call side. Fixed width, not
    # setdefault, so the reading is the same on every machine.
    env["COLUMNS"] = "200"
[[[REPLACE]]]

Choreography: patch, app, d, m, blast. Ignition: none required — probes 3 and 4 execute the patched file at call time.

4. PROMPT

Read the five LIVE COMMAND RECEIPTS first; name the lane for each and rule one line each:
1. Probes 1 and 2: narrow_exit=1 and wide_exit=0 is the wrap convicted and Car 1 licensed; wide_exit=1 means the car was not licensed -- say so and name what cli.py's --tool printer should do instead, but do not patch it without cli.py in context.
2. Probe 3: Arm R now reads shared_doc_line_present=True. Still False with wide_exit=0 means the harness is not passing COLUMNS through; name where.
3. Probe 4: Arm S still True.
4. Probe 5: gemma3:latest is listed (count 1) or name the substitute I paste from llm models.

Then read the SMOKE RECEIPT pasted below (operator lane): the jq projection of the final records for T1 on S and T1 on R. Rule on each: pass, void, calls, capped, prompt+completion tokens. pass=true with calls=0 is a guessed username; name it. If either arm failed, name the gate -- discovery text, protocol parse (exit 125 records), prefix refusal (exit 126 records), oracle (oracle record reason), or predicate -- and STOP.

If both passed: give me the full-run commands (five tasks, ten sessions, both arms) and the one jq line that produces the four-metric table -- success, tokens, calls, recovery -- per arm per task from the JSONL. Do not run anything.

No new files. Do NOT touch wallet.py, botify_tools.py, advanced_automation_tools.py, or any MCP file.

5. EXTERNAL DELIVERABLES

None.

MikeLev.in: I think Fable 5 is giving me “ignition” instructions higher-up in the instructions than it should, saying do this after app which is nearly the definition of ignition and I’m pretty pissed it gives me these “smoke” lines out-of-order that they’re supposed to be executed. That really sets up failure. Also, I’m glad Fable 5 got the message about not doing file propagation probably the worst offending attribute I’ve noticed about Anthropic Claude models (over the years now) mostly on markdown-vomit that’s out-of-sync with real code as fast as its written, but I think it does try to do it with every kind of file. Not long ago I had to make it start writing the test-files into /tmp instead of into the repo to prevent file noise pollution garbage of which hits is because it’s 1-off testing but we’ll clean it up later.

Let’s power through this and get to the end of this article because I think it’s once again off base because of this whole “scientific test” way I opened the article. Nothing’s ever not a scientific test under the system so it was probably a mistake to make that the focus. The new focus is going to actually be USING both MCP trails here, making actual calls against both MCP endpoints. It’s still only 7:30 AM Sunday morning this August 30th 2026 so time is not the issue so much as excessively early fatigue from wild goose chases. Let’s achieve what we’re setting out to achieve and then set the stage for using both MCP endpoints successfully through these tools.

I like that it’s doing BQL stuff which is in this spirit but the fact that it’s doing Gemma 3 stuff is pissing me off.

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 cli.py mcp-discover --tool botify 2>/dev/null | rg -c 'Bring Botify crawl data and BQL query results into context'; echo "narrow_exit=$?"
COLUMNS=200 .venv/bin/python cli.py mcp-discover --tool botify 2>/dev/null | rg -c 'Bring Botify crawl data and BQL query results into context'; echo "wide_exit=$?"
.venv/bin/python scripts/two_arm.py --arm R --dry-run --tasks 2>&1 | tail -1
.venv/bin/python scripts/two_arm.py --arm S --dry-run --tasks 2>&1 | tail -1
.venv/bin/llm models 2>/dev/null | rg -c 'gemma3:latest'; echo "gemma3_latest_exit=$?"
1
narrow_exit=0
1
wide_exit=0
discovery_chars=4313 shared_doc_line_present=True
discovery_chars=2450 shared_doc_line_present=True
1
gemma3_latest_exit=0
(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/ _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |  Gemma 3 should not be required here. This is just about touching MCP endpoints successfully through these tools; nothing else!
# 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 cli.py mcp-discover --tool botify 2>/dev/null | rg -c 'Bring Botify crawl data and BQL query results into context'; echo "narrow_exit=$?"
! COLUMNS=200 .venv/bin/python cli.py mcp-discover --tool botify 2>/dev/null | rg -c 'Bring Botify crawl data and BQL query results into context'; echo "wide_exit=$?"
! .venv/bin/python scripts/two_arm.py --arm R --dry-run --tasks 2>&1 | tail -1
! .venv/bin/python scripts/two_arm.py --arm S --dry-run --tasks 2>&1 | tail -1
! .venv/bin/llm models 2>/dev/null | rg -c 'gemma3:latest'; echo "gemma3_latest_exit=$?"
scripts/two_arm.py
# cli.py  # <-- uncomment only if wide_exit=1: then the wrap is not COLUMNS and the fix lives in cli.py's --tool printer

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/two_arm.py'.
(nix) pipulate $ d
diff --git a/scripts/two_arm.py b/scripts/two_arm.py
index f1638b39..f52629cd 100644
--- a/scripts/two_arm.py
+++ b/scripts/two_arm.py
@@ -107,6 +107,16 @@ def run_cmd(cmd, env_extra=None, timeout=CMD_TIMEOUT):
     """bash -c in the inherited environment -> (exit_code, stdout, stderr, seconds)."""
     env = dict(os.environ)
     env.update(env_extra or {})
+    # ONE TERMINAL WIDTH FOR BOTH ARMS (convicted 2026-08-30 by this file's own
+    # dry-run: shared_doc_line_present read True on S and False on R). Under
+    # no TTY both Rich and argparse default to 80 columns and re-wrap what
+    # they print; the connector's first docstring line is 85 cells wide once
+    # cli.py prefixes it with "Docstring:", so its last word landed on a new
+    # line and the same sentence stopped being the same string. An 80-column
+    # wrap is a property of the subprocess, not of either protocol -- the
+    # same class of artifact --raw removed on the call side. Fixed width, not
+    # setdefault, so the reading is the same on every machine.
+    env["COLUMNS"] = "200"
     t0 = time.monotonic()
     try:
         proc = subprocess.run(
(nix) pipulate $ m
📝 Committing: chore: Fix: Enforce consistent terminal width for two-arm execution
[main 4f3d20de] chore: Fix: Enforce consistent terminal width for two-arm execution
 1 file changed, 10 insertions(+)
(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), 845 bytes | 845.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
   4968224d..4f3d20de  main -> main
(nix) pipulate $

Alright now for the ignition which Fable 5 put annoyingly out of order. This right here is “after app” as it put it (the place for ignition).

(nix) pipulate $ .venv/bin/python scripts/two_arm.py --arm S --tasks T1 --model gemma3:latest && .venv/bin/python scripts/two_arm.py --arm R --tasks T1 --model gemma3:latest
jq -c 'select(.kind=="final") | {arm,task,session,pass,void,calls,capped,prompt_tokens,completion_tokens,model}' browser_cache/two_arm/*.jsonl
arm=S prefix=.venv/bin/python scripts/connectors/botify.py
discovery_chars=2349 shared_doc_line_present=True
S T1 s0 pass=True void=False calls=1 tokens=1675
# records: /home/mike/repos/pipulate/browser_cache/two_arm/20260830T113701Z__S.jsonl
arm=R prefix=.venv/bin/python cli.py
discovery_chars=5103 shared_doc_line_present=True
R T1 s0 pass=False void=False calls=1 tokens=2555
# records: /home/mike/repos/pipulate/browser_cache/two_arm/20260830T113725Z__R.jsonl
{"arm":"S","task":"T1","session":0,"pass":true,"void":false,"calls":1,"capped":false,"prompt_tokens":850,"completion_tokens":7,"model":"gemma3:latest"}
{"arm":"R","task":"T1","session":0,"pass":false,"void":false,"calls":1,"capped":false,"prompt_tokens":1589,"completion_tokens":9,"model":"gemma3:latest"}
(nix) pipulate $ 

There see? That was better than last time. How could I have instructed you better to have not received order-dependent processes out-of-order? This is like the peanut butter sandwich protocol except understanding that the human might read an entire Claude response and still not understand the instructions they were given because it was too highfalutin, high-concept and generally above the human’s weight class and so they need everything possible rigged in their favor like being instructed to do order-dependent processes in the order that they’re dependent upon instead of this Quentin Tarantino could be entering anywhere and things unfolding in any order sort of thing. Imaging having to follow a recipe as it’s given out in out-of-order scenes in a movie; that’s what I have to do currently.

4: Prompt: Read the five LIVE COMMAND RECEIPTS first; name the lane for each and rule one line each:

  1. Probes 1 and 2: narrow_exit=1 and wide_exit=0 is the wrap convicted and Car 1 licensed; wide_exit=1 means the car was not licensed – say so and name what cli.py’s –tool printer should do instead, but do not patch it without cli.py in context.
  2. Probe 3: Arm R now reads shared_doc_line_present=True. Still False with wide_exit=0 means the harness is not passing COLUMNS through; name where.
  3. Probe 4: Arm S still True.
  4. Probe 5: gemma3:latest is listed (count 1) or name the substitute I paste from llm models.

Then read the SMOKE RECEIPT pasted below (operator lane): the jq projection of the final records for T1 on S and T1 on R. Rule on each: pass, void, calls, capped, prompt+completion tokens. pass=true with calls=0 is a guessed username; name it. If either arm failed, name the gate – discovery text, protocol parse (exit 125 records), prefix refusal (exit 126 records), oracle (oracle record reason), or predicate – and STOP.

If both passed: give me the full-run commands (five tasks, ten sessions, both arms) and the one jq line that produces the four-metric table – success, tokens, calls, recovery – per arm per task from the JSONL. Do not run anything.

No new files. Do NOT touch wallet.py, botify_tools.py, advanced_automation_tools.py, or any MCP file.

5: Deliverables: None this turn.

Fable 5 Extra: Context verified. Canary linkified again; no patch rides this turn, so nothing is at risk from it.

On the ordering — you’re right, and it was my fault, not your instructions

The rule already existed (“ignition is not a fourth beat; it completes PATCH”). I broke it by previewing the smoke in prose above the patches, labeled “after app”. A preview of a dependent command is exactly a Tarantino cut. The instruction that would have caught it is a ban on previews, paste-ready for the checklist:

ONE TIMELINE, NO PREVIEWS: every command in the response appears exactly once, in the section where it executes, in execution order -- PROBES (read-only) first, then PATCHES as numbered cars with any dependent actuator or ignition as the LAST cars, then PROMPT. A command that depends on a patch is never quoted above that patch, not as a preview, not "for later"; if it must be mentioned early, name it ("Car 3, the smoke") and show nothing.

This turn follows it: every command below is in section 1 or section 3, in the order you run it.

The rulings

# Reading Operator Compile Rules
1 narrow 1, exit 0 1, exit 0 a third case my caboose didn’t list
2 wide 1, exit 0 1, exit 0 as predicted
3 Arm R 4313 True (before Car 1) 5103 True (after) Car 1 landed; R now reads True at a fixed width
4 Arm S 2450 True 2349 True still True; the char change proves COLUMNS reached argparse too
5 gemma3:latest 1 1 listed

Probe 1 falsified something, but not the mechanism: it falsified my assumption that the “narrow” probe ran at 80 columns. Rich takes its width from whichever of stdin, stdout, stderr reports a size, so the width a subprocess sees depends on how the parent was launched (terminal, editor pty, split width) and differed between lanes and even between compiles. Last compile’s 3189 False was an 80-wide reading; this turn’s operator 4313 True was a wide one, with no code change between them. That uncontrolled variable is what Car 1 pins, so the fix stands on better grounds than the probe I wrote for it.

The smoke, first data point, n=1: S passed in one call (857 tokens); R failed in one call (1598 tokens), not capped, not void. The projection can’t name R’s gate — it omits the run record’s exit code and the FINAL text. The one line that names it, for whenever: jq -c 'select(.arm=="R") | {seq,kind,exit_code,command,stdout_bytes,stderr_bytes,text_len:((.text // "")|length)}' browser_cache/two_arm/*R.jsonl. Exit 126 is a prefix refusal (the model dropped .venv/bin/), 125 a protocol miss, 0 with a short text_len a wrong answer. The experiment is parked there: it exists, it ran once, it produced a reading, and it no longer needs a model or a patch. No more of it this article.

The pivot: using both MCP servers, no model in the loop

What exists, receipted in the earlier articles: server one (mcp.botify.com, OAuth, 300-second bearer, refresh works, 42 tools, three list_projects calls on record) and server two (static Token scheme, 56 tools, session id, --auth-scheme Token --token-env BOTIFY_API_TOKEN). Both reached GREEN through scripts/connectors/mcp.py. The one gap that stands between “GREEN check” and “a real call on server two” is that mcp.py’s tool menu prints names and descriptions but not each tool’s inputSchema, so you can’t see what JSON a tool wants. Section 3 gets you a real call on server one today (its args are already on record), a real attempt on server two today (the server’s own error message is the schema until the client can print one), and the next ride is one flag on the existing mcp.py. No new files. Second server URL is written <SECOND_MCP_URL> below; substitute the one from your notes.

1. PROBES

.venv/bin/python scripts/connectors/mcp.py 2>/dev/null | rg -n 'creds    :' -A 2
.venv/bin/python scripts/connectors/mcp.py <SECOND_MCP_URL> --check --auth-scheme Token --token-env BOTIFY_API_TOKEN 2>&1 | tail -2
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

Probe 1 opens no socket: it reads the warmed credential files and their clocks, which tells you whether server one’s bearer <redacted:2> dead (expected — it always is between turns) and whether a browser warm has ever written the derived path. Probe 2 is the compile-lane GREEN that a static credential makes possible; it also writes a fresh receipt. Probe 3 is the receipt census and the straddle for section 3: BEFORE it shows checks plus three list_projects calls; AFTER it should show new tools/call rows, one of them from server two.

2. NEXT CONTEXT

! .venv/bin/python scripts/connectors/mcp.py 2>/dev/null | rg -n 'creds    :' -A 2
! .venv/bin/python scripts/connectors/mcp.py <SECOND_MCP_URL> --check --auth-scheme Token --token-env BOTIFY_API_TOKEN 2>&1 | tail -2
! 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

Drop scripts/two_arm.py; it’s parked. mcp.py comes in because the next ride’s one flag lives there.

3. PATCHES

No repo patches required. The cars below are actuators in the operator lane, in execution order. Server one’s cars chain the refresh in front of the call because the bearer <redacted:5> 300 seconds; the chain is the habit. The compiler’s ledger line for two_arm.py in foo_files.py is uncommitted; your next m sweeps it.

Car A — server one, door check.

python scripts/connectors/mcp_warm.py --refresh && python scripts/connectors/mcp.py https://mcp.botify.com --check

Expect gate6 GREEN then mcp GREEN … tools=42 (or a new count; that’s drift, not failure). A RED names its gate; stop there and paste it.

Car B — server one, the menu.

python scripts/connectors/mcp_warm.py --refresh && python scripts/connectors/mcp.py https://mcp.botify.com -n 50

Car C — server one, a real call. The first line reads the arguments the three earlier list_projects flights used, so you re-issue what worked rather than guess; the second line is the call.

jq -r 'select(.tool=="list_projects") | .args_raw' browser_cache/mcp/mcp.botify.com/*.json | sort -u | head -2
python scripts/connectors/mcp_warm.py --refresh && python scripts/connectors/mcp.py https://mcp.botify.com list_projects '{}' --dclass D1

If the recorded args_raw was not {}, use that instead. Output is the tool’s result capped at --max-bytes, then the receipt path.

Car D — server two, door check.

python scripts/connectors/mcp.py <SECOND_MCP_URL> --check --auth-scheme Token --token-env BOTIFY_API_TOKEN

Expect mcp GREEN … session=yes tools=56 auth=BOTIFY_API_TOKEN scheme=Token.

Car E — server two, the menu.

python scripts/connectors/mcp.py <SECOND_MCP_URL> -n 60 --auth-scheme Token --token-env BOTIFY_API_TOKEN

Read it once and pick one tool whose name reads as a read (a list_… or get_…), not a render or a mutation. That name is the input to Car F and to the next ride.

Car F — server two, first call attempt. <TOOL> is your pick from Car E.

python scripts/connectors/mcp.py <SECOND_MCP_URL> <TOOL> '{}' --dclass D1 --auth-scheme Token --token-env BOTIFY_API_TOKEN

Two honest outcomes. A result means the tool takes no required arguments and server two is fully in hand. A JSON-RPC error naming missing fields is the tool’s schema, witnessed from the source, and it’s the exact thing the next ride makes mcp.py print without the dance. Either way, paste the last three lines.

Ignition: none required — each car is itself the run, and probe 3 reads the receipts they write.

4. PROMPT

Read the three LIVE COMMAND RECEIPTS first; name the lane for each and rule one line each:
1. Probe 1: how many warmed credential files, and is server one's clock EXPIRED (expected between turns) or live.
2. Probe 2: server two GREEN from the compile lane, with tools= and session=; any change from 56/yes is a finding.
3. Probe 3: receipts=N and the census. New tools/call rows since the BEFORE tap are the cars that ran; name which.

Then read the CAR OUTPUT pasted below (operator lane): A through F. Rule on each in one line -- GREEN or the gate it stopped at. For Car C, say whether list_projects returned a result and at what byte count. For Car F, say which of the two outcomes happened and, if it was the error, quote the field names the server asked for; those are the schema.

Then ONE car, against scripts/connectors/mcp.py (in context): a --schema <tool> option that prints one tool's inputSchema from the tools/list response as JSON and nothing else, exiting 1 if the tool is not listed. Same auth flags as every other mode. Give the straddle probe: before, argparse rejects --schema; after, the schema for the Car E pick prints. No other change to mcp.py; no new files; no model.

Close by naming the three server-two tools most worth wrapping in a WET connector, from the Car E menu, with one line each on why -- that is the stage for the next article, not a build order.

5. EXTERNAL DELIVERABLES

None. Before this thread becomes an article, run the existing scrub for the second server’s hostname; it is in the earlier receipts.

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 scripts/connectors/mcp.py 2>/dev/null | rg -n 'creds    :' -A 2
.venv/bin/python scripts/connectors/mcp.py <SECOND_MCP_URL> --check --auth-scheme Token --token-env BOTIFY_API_TOKEN 2>&1 | tail -2
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
5:# creds    : 1 warmed file(s); values never printed
6-#            mcp_botify_token (pre-derivation) -- EXPIRED 52146s ago (life was 300s); refresh_token present: yes; re-mint with python scripts/connectors/mcp_warm.py
7-#
bash: SECOND_MCP_URL: No such file or directory
receipts=20
     17 check	null	-
      3 tools/call	D1	list_projects
(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/ _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |  Before doing this, do-that. Shit! That's the kind of stuff I'm trying to get away from with Nix functional hardware.
# ahc ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|  The order-dependence of combination locks and having to do the combination-lock dance as vendors snap whips; THAT'S the main conceptual enemy in this book and the primary machination of Murphy Incarnate.
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)  Anything that invites the Murphy Incarnate tap, tap, tapping at your window into your home (instead of doing it through a declaration and derivation) is asking for the progressive downward-decay spiral.
# Ad Hoc CHOP: The Not-Managed-by-Git Safe-for-Client-Data place  The correct way is: Here's the file. Pull the ripcord. Everything works and it will work exactly that well every time that follows. If temporary "test" files are involved, they can live in `/tmp`.

#    # 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 scripts/connectors/mcp.py 2>/dev/null | rg -n 'creds    :' -A 2
! .venv/bin/python scripts/connectors/mcp.py <SECOND_MCP_URL> --check --auth-scheme Token --token-env BOTIFY_API_TOKEN 2>&1 | tail -2
! 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

3: Patches: Oh nice, it’s nothing but ignition or smoke or whatever we want to call it.

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'.

(nix) pipulate $ python scripts/connectors/mcp_warm.py --refresh && python scripts/connectors/mcp.py https://mcp.botify.com --check
# refreshing the pre-derivation file mcp_botify_token.json IN PLACE; the next browser warm writes /home/mike/.config/pipulate/mcp/mcp.botify.com.json
# gate2 GREEN via RFC 8414: https://app.botify.com/.well-known/oauth-authorization-server
# gate6 GREEN: refreshed in place -> /home/mike/.config/pipulate/mcp_botify_token.json (0600)
#   scope: mcp_read_write | expires_in: 300
# Next: python scripts/connectors/mcp.py https://mcp.botify.com --check
# mcp credential: reading the pre-derivation file mcp_botify_token.json; the next browser warm writes /home/mike/.config/pipulate/mcp/mcp.botify.com.json
# mcp token clock: ~299s left of a 300s life
mcp GREEN https://mcp.botify.com protocol=2025-06-18 session=no tools=42 auth=mcp_botify_token.json scheme=Bearer
# FDR receipt: /home/mike/repos/pipulate/browser_cache/mcp/mcp.botify.com/20260830T120216989366Z__check.json
(nix) pipulate $ python scripts/connectors/mcp_warm.py --refresh && python scripts/connectors/mcp.py https://mcp.botify.com -n 50
# refreshing the pre-derivation file mcp_botify_token.json IN PLACE; the next browser warm writes /home/mike/.config/pipulate/mcp/mcp.botify.com.json
# gate2 GREEN via RFC 8414: https://app.botify.com/.well-known/oauth-authorization-server
# gate6 GREEN: refreshed in place -> /home/mike/.config/pipulate/mcp_botify_token.json (0600)
#   scope: mcp_read_write | expires_in: 300
# Next: python scripts/connectors/mcp.py https://mcp.botify.com --check
# mcp credential: reading the pre-derivation file mcp_botify_token.json; the next browser warm writes /home/mike/.config/pipulate/mcp/mcp.botify.com.json
# mcp token clock: ~299s left of a 300s life
# https://mcp.botify.com — protocol 2025-06-18 | server Botify Agents | 42 tool(s) | session=no

list_projects  List the Botify projects the authenticated user can access. Each entry has the `
action_board  Retrieve actions from ActionBoard for your projects.
botify_config  
    Retrieve configuration information for Botify products.
    
    Supports:

calculator  Perform basic arithmetic operations (add, subtract, multiply, divide).
google_ai_mode  Query Google AI Mode to get AI-generated search results, featured snippets, and 
google_knowledge_graph  
    Search the Google Knowledge Graph to find information about real-world enti
google_people_also_ask  Retrieve the Google 'People Also Ask' questions for a specific query.
google_search  Get feature snippets (e.g., Google shopping, images, video, people also search/a
google_trends  Analyze search trend data to understand how interest in specific keywords has ch
google_url_inspection  Get information about Google's indexed version of a page from the URL Inspection
html_code_executor  
    Execute a JavaScript function on HTML from a SiteCrawler crawl or a
    tem
html_extract_images  
    Extract main images from a webpage (excluding logos, icons, and decorative 
html_extractor_css_selector  
    Get the CSS selector for a given element.
    
html_grep  
    Fetch the HTML of a given URL and search for one or more expressions in it.
html_pageworkers_preview  
    Get the preview for PageWorkers descriptor for a selector or templates rend
html_question  
    Ask any question about the semantic structure, HTML elements, and technical
html_structured_data  
    Extract structured data from HTML pages in JSON-LD format.
    
keywords_clustering  Analyze and organize keywords into meaningful, thematic clusters that reflect us
keywords_suggestions  
    Analyze Google Ads keyword data and answer questions about:
        - Searc
keywords_suggestions_google_ads_planner  Analyze Google Ads keyword data and answer questions about:
    - Search volumes
knowledge  Answer questions about the Botify application to understand product features, te
list_annotations  List chart annotations on the current project: dated notes and markers on Botify
nearest_page_for_topic  Get the nearest page (for current project) for a given topic
pageworkers  
    Update any title/description/h1 for a list of URLs through PageWorkers, and
pageworkers_get_or_create_optimization  
    Get or create a PageWorkers PAGE_EDITOR optimization. Searches existing mod
pageworkers_push_items_to_optimization  
    Push items to a PageWorkers optimization. Runs a SQL query, generates a CSV
perplexity  
    Ask a question to Perplexity when you need to ground the web for a specific
project_metadata  Store and retrieve key project metadata, such as tone of voice, website descript
quality_control  
    Evaluate a JSON object against a list of expectations and report failures.

searchgpt  Use GPT with web browsing capabilities to search the internet and provide accura
site_crawler_url_details  
    Get comprehensive URL analysis from Botify's latest crawl, including:

* Co
tables_create  Create a new table in the project's catalog.
    Provide a table name and a list
tables_insert  Insert rows into a table in the project's editable catalog.
    Only tables crea
tables_list_condensed  List all tables families with condensed values over the entire datamodel.

Crawl
tables_list_full  List every table available for the project, with its aliased name, source and de
tables_load  Load a CSV or TSV file into the project's editable BigQuery catalog.

    The fi
tables_query  Query any table in the data model with BigQuery Standard SQL syntax.
Always pref
tables_query_export  Export query results to CSV or Parquet format with compression (gz or zstd). The
tables_schema  List all fields (names and types) from a given table.
tables_text_to_sql  
    This agent converts natural language descriptions into BigQuery SQL query.

tool_documentation  Load extended markdown documentation for another tool by id. Call this when a to
url_keywords  
    Get Google Search Console keyword performance metrics for a specific URL (i
# FDR receipt: /home/mike/repos/pipulate/browser_cache/mcp/mcp.botify.com/20260830T120231927119Z__tools_list.json
(nix) pipulate $ jq -r 'select(.tool=="list_projects") | .args_raw' browser_cache/mcp/mcp.botify.com/*.json | sort -u | head -2
python scripts/connectors/mcp_warm.py --refresh && python scripts/connectors/mcp.py https://mcp.botify.com list_projects '{}' --dclass D1
{}
# refreshing the pre-derivation file mcp_botify_token.json IN PLACE; the next browser warm writes /home/mike/.config/pipulate/mcp/mcp.botify.com.json
# gate2 GREEN via RFC 8414: https://app.botify.com/.well-known/oauth-authorization-server
# gate6 GREEN: refreshed in place -> /home/mike/.config/pipulate/mcp_botify_token.json (0600)
#   scope: mcp_read_write | expires_in: 300
# Next: python scripts/connectors/mcp.py https://mcp.botify.com --check
# mcp credential: reading the pre-derivation file mcp_botify_token.json; the next browser warm writes /home/mike/.config/pipulate/mcp/mcp.botify.com.json
# mcp token clock: ~299s left of a 300s life
# MCP RECEIPT (four-tuple; args byte-for-byte as submitted)
# server: https://mcp.botify.com
# verb:   tools/call
# tool:   list_projects
# args:   {}
# determinism: D1 (declared) — stable read — reproducible until server-side state mutates
# observed_at: 2026-08-30T12:02:48Z
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"organization\": \"uhnd-com\", \"project\": \"test-dev-site-crawl\", \"name\": \"Test Dev Site Crawl\"}, {\"organization\": \"uhnd-com\", \"project\": \"pre-aws-switch\", \"name\": \"Pre AWS Switch\"}, {\"organization\": \"uhnd-com\", \"project\": \"redirect-tests\", \"name\": \"Redirect Tests\"}, {\"organization\": \"michaellevin-org\", \"project\": \"example-article-content\", \"name\": \"example Article Content\"}, {\"organization\": \"uhnd-com\", \"project\": \"comp-crawl-test\", \"name\": \"Comp Crawl Test\"}, {\"organization\": \"michaellevin-org\", \"project\": \"mikelev.in\", \"name\": \"MikeLev.in\"}, {\"organization\": \"uhnd-com\", \"project\": \"test-ad-hoc\", \"name\": \"Test ad-hoc\"}, {\"organization\": \"uhnd-com\", \"project\": \"uhnd-character-crawl-test\", \"name\": \"UHND Character Crawl Test\"}, {\"organization\": \"uhnd-com\", \"project\": \"test3\", \"name\": \"test3\"}, {\"organization\": \"uhnd-com\", \"project\": \"test2\", \"name\": \"test2\"}, {\"organization\": \"uhnd-com\", \"project\": \"uhnd-ad-hoc-crawls\", \"name\": \"UHND - Ad Hoc Crawls\"}, {\"organization\": \"uhnd-com\", \"project\": \"uhnd.com-demo-account\", \"name\": \"UHND.com - Demo Account\"}]"
      }
    ],
    "isError": false
  }
}
# FDR receipt: /home/mike/repos/pipulate/browser_cache/mcp/mcp.botify.com/20260830T120249270008Z__tools_call.json
(nix) pipulate $ python scripts/connectors/mcp.py <SECOND_MCP_URL> --check --auth-scheme Token --token-env BOTIFY_API_TOKEN
bash: SECOND_MCP_URL: No such file or directory
(nix) pipulate $ python scripts/connectors/mcp.py https://redacted.production.botify.com:redacted/mcp --check --auth-scheme Token --token-env redacted
# mcp credential: declining mcp_botify_token.json for https://redacted.production.botify.com:redacted/mcp -- it was minted for a different resource
mcp GREEN https://redacted.production.botify.com:redacted/mcp protocol=2025-06-18 session=yes tools=56 auth=BOTIFY_API_TOKEN scheme=Token
# FDR receipt: /home/mike/repos/pipulate/browser_cache/mcp/redacted.production.botify.com:redacted/20260830T120442630664Z__check.json
(nix) pipulate $ 

4: Prompt: Read the three LIVE COMMAND RECEIPTS first; name the lane for each and rule one line each:

  1. Probe 1: how many warmed credential files, and is server one’s clock EXPIRED (expected between turns) or live.
  2. Probe 2: server two GREEN from the compile lane, with tools= and session=; any change from 56/yes is a finding.
  3. Probe 3: receipts=N and the census. New tools/call rows since the BEFORE tap are the cars that ran; name which.

Then read the CAR OUTPUT pasted below (operator lane): A through F. Rule on each in one line – GREEN or the gate it stopped at. For Car C, say whether list_projects returned a result and at what byte count. For Car F, say which of the two outcomes happened and, if it was the error, quote the field names the server asked for; those are the schema.

Then ONE car, against scripts/connectors/mcp.py (in context): a –schema option that prints one tool's inputSchema from the tools/list response as JSON and nothing else, exiting 1 if the tool is not listed. Same auth flags as every other mode. Give the straddle probe: before, argparse rejects --schema; after, the schema for the Car E pick prints. No other change to mcp.py; no new files; no model.

Close by naming the three server-two tools most worth wrapping in a WET connector, from the Car E menu, with one line each on why – that is the stage for the next article, not a build order.

5: Deliverables: Instruction manual on all this

That feels a lot better. Wayyy less wheel-spinning and way more “I might be doing the right thing” without getting strung along by the larger more ambitious stuff the LLM-style AIs (particularly Claude) seem to like spiraling you down. This is a big part of the reason I gave up on Cursor IDE, especially with Claude because that was the one-two punch of the rabbithole-everything abyss. This article is a great example of catching that though of course ultimately it is my own fault based on how I prompted it but I do know that the Models react differently (also based on Model version and effort-level that you set). Anyway I think we can wrap this one up.

Give me a little instruction manual on what we built and then hop off the ride. If this ride’s stated goal is reached then dismount. This is the NOTARY BEAT: the ride ends here, is witnessed here, and is sealed here. Answer all seven beats, briefly:

  1. 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.
  2. VERIFY: restate the goal from the top of this article and confirm (or deny) it was met, citing THIS compile’s receipts, not memory. Name any ignition this ride required that never fired – an AFTER tap taken without one is a stale BEFORE wearing the AFTER’s label.
  3. BANK: name everything that graduates — rule, earmark, todo, pin — as exact paste-ready lines, plus the exact lines to delete.
  4. DANGLING: what carries forward unbanked? One line each, no essays.
  5. SEED: the adhoc.txt lines (and TODO_SLUGS if narrative context is needed) for the next ride’s first compile.
  6. CLOSING: a closing summary for the BOTTOM of the article — the final take-away, tied to the book’s larger arc where it fits naturally, never forced. Storytelling over inventory.
  7. NOTARIZE: 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: Context verified. Canary linkified; nothing here quotes a bare www-token, and no patch rides — this is the notary beat.

One thing before the beats, because it’s mine: the <SECOND_MCP_URL> placeholder killed probe 2 in both lanes (sh: SECOND_MCP_URL: No such file or directory — the shell read the angle brackets as a redirection). A placeholder in a paste-ready line is a landmine of the same family as the out-of-order smoke. The right tool was a shell variable set once. And this compile shows the sanitizer already redacts the hostname (redacted.production.botify.com:redacted), so the placeholder was protecting nothing. Banked below.

Instruction manual — two MCP servers from the terminal

Everything runs from inside nix develop (door 2), in this order.

Server one: mcp.botify.com (OAuth, 300-second token).

  1. First time only, or when refresh fails: python scripts/connectors/mcp_warm.py — opens a browser, you log in, a token file lands under ~/.config/pipulate/.
  2. Every use afterward starts with a refresh chained in front, because the token dies in five minutes and the chain keeps the order out of your head: python scripts/connectors/mcp_warm.py --refresh && python scripts/connectors/mcp.py https://mcp.botify.com --check
  3. The menu (42 tools today): same chain, ending ... https://mcp.botify.com -n 50.
  4. A call: same chain, ending ... https://mcp.botify.com list_projects '{}' --dclass D1. The result prints as JSON, capped by --max-bytes; the receipt path prints last. --dclass D1 means “a stable read that can change when the account changes” — declare it, or the receipt records D2.
  5. To learn what JSON a tool wants: the menu shows only a name and 80 characters of description. Server one lists a tool_documentation tool (docs for another tool by id); otherwise call the tool with '{}' and the server’s error names the missing fields.

Server two: the static-token server.

  1. Once per shell: export SECOND_MCP_URL=https://…/mcp (the URL from your notes). Lines below then run verbatim.
  2. Check: python scripts/connectors/mcp.py "$SECOND_MCP_URL" --check --auth-scheme Token --token-env BOTIFY_API_TOKEN — expect GREEN, 56 tools, session=yes. No refresh, no browser, no clock.
  3. Menu: ... "$SECOND_MCP_URL" -n 60 --auth-scheme Token --token-env BOTIFY_API_TOKEN.
  4. A call: ... "$SECOND_MCP_URL" <TOOL> '{}' --dclass D1 --auth-scheme Token --token-env BOTIFY_API_TOKEN. Pick a list_/get_ tool; an error naming fields is the schema.

Rules of thumb. --token-env takes a variable NAME, never a value. Receipts land under browser_cache/mcp/<host>/ (gitignored) and carry the request, statuses, headers, and a body hash — not the body; the terminal output is your copy of the result. Census: jq -r '[.verb, (.dclass // "null"), (.tool // "-")] | @tsv' browser_cache/mcp/*/*.json | sort | uniq -c. Receipts contain host names and tool arguments; scrub before anything rides to a chat window.

1. VERIFY

The article’s opening goal (“test the thesis”) was replaced mid-ride by “make actual calls against both MCP endpoints.” Against this compile’s receipts: the census moved from 20 (operator, before) to 24 (compile, after) — +2 checks, +1 tools/list, +1 tools/call list_projects — which accounts exactly for Cars A–D. Car C returned a result: twelve projects across two organizations, D1 declared, receipt written. Server two’s check reached GREEN in the operator lane (tools=56 session=yes scheme=Token) and printed the scoped-fallback decline line, so last ride’s Car D is now witnessed live. Server one’s compile-lane clock read EXPIRED 477s — expected, and the magnitude says the last refresh was the Car C chain. Goal met for server one in full. Goal met for server two only to the handshake and tools/list: Cars E and F never fired — no server-two menu was read and no server-two tool was called. Compile-lane probe 2 is VOID (placeholder). The harness smoke earlier in this article stands as one reading, S pass / R fail, gate unnamed. The ONE TIMELINE rule landed in apply.py (this compile’s commit diff).

2. BANK

Paste-ready, for wherever the named rules live:

# NO PLACEHOLDERS IN PASTE-READY LINES (convicted 2026-08-30, both lanes): <NAME> in a shell line is a redirection from a file called NAME -- "No such file or directory" -- silent in the compile lane, loud in the operator lane, dead in both. Write "$NAME" and put `export NAME=...` once on the line above; a variable runs verbatim, a placeholder needs an edit the human will not make.
# THE ACTUATOR IS A CAR: anything that runs against a server (--check, tools/list, tools/call, --refresh) rides in PATCHES as its own numbered car, after the patches it depends on, never previewed above them. Sibling of ONE TIMELINE, NO PREVIEWS (apply.py, 2026-08-30).
# CHAIN THE COMBINATION LOCK: a credential shorter-lived than the human loop is only ever used as `mcp_warm.py --refresh && mcp.py ...`; the order lives in the command, not in the operator. Witnessed 2026-08-30: three chained calls, three GREENs, ~299s on the clock each time.
# EARMARK mcp.py --schema <tool>: print one tool's inputSchema from tools/list as JSON, exit 1 if unlisted. The menu shows name + 80 chars and no schema. Server one exposes tool_documentation (docs by tool id) -- try it first; server two's equivalent UNWITNESSED.
# EARMARK server two, first tools/call: Cars E and F (2026-08-30) never fired. Menu at -n 60, pick a list_/get_ tool, call with '{}'; the JSON-RPC error is the schema.
# EARMARK WET connector for server two, AFTER one real call: a botify.py-shaped file hardcoding --auth-scheme Token / BOTIFY_API_TOKEN and three common calls as one positional. ONE new file, with consent.
# EARMARK two_arm.py: parked with one reading (S pass, 1 call, 857 tok; R fail, 1 call, 1598 tok; gate unnamed -- jq on exit_code names it). Needs a model; MCP use does not. Keep, /tmp-class it, or delete: decide, don't drift.

Delete from the bottom of adhoc.txt (this ride’s echoes, spent):

! .venv/bin/python scripts/connectors/mcp.py 2>/dev/null | rg -n 'creds    :' -A 2
! .venv/bin/python scripts/connectors/mcp.py <SECOND_MCP_URL> --check --auth-scheme Token --token-env BOTIFY_API_TOKEN 2>&1 | tail -2
! 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

3. DANGLING

  • Server two: menu unread, no tool called (Cars E, F).
  • --schema on mcp.py: designed, unbuilt.
  • WET connector for server two: unbuilt, one file, needs consent.
  • two_arm.py: one reading, R’s gate unnamed, fate undecided.
  • Legacy token file is still the pre-derivation one; a browser warm writes the derived path, deletion is by hand after.
  • warm does not warm the OAuth MCP bearer (wallet slot blocked pending wallet.py).
  • Registration lineage gut/migrate (earmark banked in tools/__init__.py).
  • One operator line shows --token-env redacted: if you typed the variable name, the scrub over-matched (cosmetic); if a value, it did its job.
  • Earlier articles carry the second server’s hostname raw; this compile shows the scrub table now covers it — run it before publishing.

4. SEED

# --- NEXT RIDE: server two, first real call; then --schema on mcp.py ---
# export SECOND_MCP_URL=https://.../mcp   <-- once, in the shell you compile from
! .venv/bin/python scripts/connectors/mcp.py "$SECOND_MCP_URL" --check --auth-scheme Token --token-env BOTIFY_API_TOKEN 2>&1 | tail -2
! 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
[[[TODO_SLUGS]]]
protocol-vs-prompt-why-mcp-is-becoming-unix
terminal-mcp-operational-moving-from-pipes-to-application
terminal-native-mcp-without-the-bloat
[[[END_SLUGS]]]

5. CLOSING

This article opened as a science experiment and closed as a recipe, and the recipe is the better ending. The experiment got built, ran once, and produced a single reading — one arm passed, one failed, for a reason nobody has looked up — and then it was set down, because the thing it was measuring had stopped being the point. What was left was two doors and the order you walk through them.

The two doors are the two shapes the book keeps finding. One is a combination lock: a token that lives five minutes, so every call must be preceded by a refresh, in that order, every time. The other is a ripcord: a static credential that is simply valid, which is why it was the first thing this project ever saw turn green from inside a compile. You cannot argue a combination lock into being a ripcord. What you can do is put the combination into the command line — refresh, &&, call — so the order lives in the file and not in a tired person’s head at half past seven on a Sunday. That is a small declaration, and small declarations are what this whole system is made of.

Operating the OAuth and Static MCP Endpoints

The rule that graduated this morning has nothing to do with MCP. It’s that instructions to a human must arrive in the order the human will perform them, with no previews and no placeholders, because the human will follow the map exactly, and a map drawn in the wrong order is Murphy tapping at the window. The receipts say server one is in hand and server two is at the door. The next article walks through it.

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 66,760; 251,695 bytes UTF-8 — this compile’s fingerprint per its Summary). No receipt 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: It’s still very early this Sunday morning and I’m already a few articles in. I have to take my first break of the day and digest what I’ve done so far and what I want the rest of the day to look like in preparation for what I want tomorrow to look like. It is a changed game.


Book Analysis

Ai Editorial Take

What is striking about this entry is the psychological defense mechanism developers deploy against API friction. Instead of building more elaborate configuration UIs or brittle stateful wrappers, the author aggressively strips away layers until the terminal itself becomes the sole interface. It highlights how true progress in tool design often looks like stepping backward into simpler, older primitives.

🐦 X.com Promo Tweet

Tired of bloated agent frameworks? Learn how to connect directly to Model Context Protocol servers using clean terminal commands and local-first workflows. https://mikelev.in/futureproof/terminal-native-mcp-practice/ #AI #DeveloperTools #UnixPhilosophy

Title Brainstorm

  • Title Option: Terminal-Native Model Context Protocol in Practice: Connecting Directly in the Age of AI
    • Filename: terminal-native-mcp-practice.md
    • Rationale: Directly addresses the practical implementation of MCP tools from the command line, emphasizing speed, simplicity, and low overhead.
  • Title Option: Bypassing the Framework: Direct MCP Tooling and Terminal Integration
    • Filename: bypassing-framework-direct-mcp.md
    • Rationale: Focuses on the philosophical shift away from heavy abstraction layers toward lightweight, script-driven protocol execution.
  • Title Option: The Combination Lock vs. The Ripcord: Real-World API Connections
    • Filename: combination-lock-vs-ripcord.md
    • Rationale: Uses the metaphor of authentication friction versus frictionless local-first execution to anchor the technical narrative.

Content Potential And Polish

  • Core Strengths:
    • Clear distinction between abstract wrapper frameworks and raw terminal actuation.
    • Rigorous use of live command receipts and empirical straddle testing to validate architectural claims.
    • Pragmatic approach to handling short-lived OAuth tokens versus static credentials.
  • Suggestions For Polish:
    • Streamline the experimental transition narrative so the shift from the multi-arm harness to direct MCP usage feels like a natural evolution rather than an abrupt pivot.
    • Ensure all placeholders in command examples are consistently represented as environment variables rather than angle-bracket tokens.

Next Step Prompts

  • Design a lightweight, single-file WET connector wrapper for the static-token MCP server based on the successful tool invocation receipts captured in this session.
  • Refactor the --schema option for mcp.py to cleanly print input schemas as JSON without triggering formatting artifacts.