The Forward-Slash Test: Bridging Agent Skills to Claude Code with Verifiable Receipts

🤖 Read Raw Markdown • 📄 Google Doc (Try: Tools/Audio/Listen to document summary)

Setting the Stage: Context for the Curious Book Reader

In the previous installment, we standardized AGENTS.md and SKILL.md to ensure our tools could speak open agent conventions without sacrificing local control. This entry marks an interesting next phase in our ongoing methodology: putting those standards to the test against real-world AI tooling. In the Age of AI, the prevailing pressure is to adopt heavyweight vendor plugin architectures that shift execution into closed corporate clouds. Here, we demonstrate an alternative path—using a simple tracked symlink to expose our skills directly to Claude Code while keeping tool execution strictly anchored to localhost. By testing the resulting /pipulate command across both NixOS and macOS environments, we establish a checkable audit trail that proves local, reproducible tooling can coexist seamlessly with commercial AI assistants without surrendering the keyboard.

TL;DR: This entry records three working sessions in which Pipulate, a local-first “context compiler” (a tool that stacks source files and command output into one text payload any AI chat can read), was made legible to AI coding agents under two open conventions. AGENTS.md at the repository root was rewritten so its first two sentences name the Agent Skills specification and the rule that every tool call runs on the local machine through a command, stdin, stdout and an exit code, so its output can be sealed into a replayable archive. A skill file, .agents/skills/pipulate/SKILL.md, describing how to install, start, reset and remove the software, was written from the installer’s own text and then corrected in two further readings against the installer, the download page, the README and the audit document, each correction landing as an exact-match patch the human ran and committed. The README’s description of a three-option boot menu was replaced after the menu’s source showed it reads no keys. Because Claude Code looks for a repository’s skills under .claude/skills/, one tracked symlink to .agents/skills/ was added; a fresh install on a second machine then showed /pipulate in the Claude desktop app’s Code tab, the stated test. Version 2.67 went to PyPI. The entry closes with the readings each change produced, the forecasting errors the AI collaborator made, and what remains undone.


Technical Journal Entry Begins

MikeLev.in: AGENTS.md descended from README and SKILL.md descended from Jekyll. Got it! I left some stuff hanging from the previous article but I just had to wrap it because it was getting so long, but when I want to remind myself what that article was, here’s what I do:

(nix) pipulate $ posts 1
# 🎯 Target: 1=article MikeLev.in (Public) [Oldest First]

# fm cache: 1499 hits, 1 misses
/home/mike/repos/trimnoir/_posts/2026-09-28-agents-md-agent-skills-pypi-receipts.md  # [Idx: 1 | Order: 4 | Tokens: 61,127 | Bytes: 240,584]
(nix) pipulate $ posts 1
# 🎯 Target: 1=article MikeLev.in (Public) [Oldest First]

/home/mike/repos/trimnoir/_posts/2026-09-28-agents-md-agent-skills-pypi-receipts.md  # [Idx: 1 | Order: 4 | Tokens: 61,127 | Bytes: 240,584]
(nix) pipulate $

That maybe looks more complicated than it was. I really just typed posts 1 which tells me the last article in my public second brain but I saw there was a cache miss, which is what makes using that posts command so fast and I know it updates the cache on running so I just ran it a second time. You can see the “1 misses” message went away on the 2nd run. And now I can do this:

(nix) pipulate $ vim /home/mike/repos/trimnoir/_posts/2026-09-28-agents-md-agent-skills-pypi-receipts.md
(nix) pipulate $ 

Which lets me find this:

! .venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
! .venv/bin/python -c "import json,io,tarfile,zipfile,urllib.request as u;d=json.load(u.urlopen('https://pypi.org/pypi/pipulate/json'));K=('agents','ai_context','skill','flake','audit','readme','manifest','license','changelog','install.sh');names=lambda f,b: tarfile.open(fileobj=io.BytesIO(b)).getnames() if f['packagetype']=='sdist' else zipfile.ZipFile(io.BytesIO(b)).namelist();R=[(f,names(f,u.urlopen(f['url']).read())) for f in d['urls']];[print(f['packagetype'],f['filename'],f['size'],'bytes',len(n),'members',[x for x in n if any(k in x.lower() for k in K)]) for f,n in R]"
! .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
foo_files.py
# --- THE INSTALL SKILL (uncomment when that car rides) ---
# assets/installer/install.sh
# remotes/honeybot/www/npvg.org/index.html
# AUDIT.md
# README.md
# --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# Workshop/corporate/connectors/svb.py
# Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# Workshop/corporate/tickets/SVB_PROMPT.md
# connectors/mcp_render.py
# connectors/jira.py
# ! jira SVB-133

…to which I can now add the article line so when I start a new discussion with the AI, any AI through any Web ChatBot user interface because it’s completely portable and I’m not locked in from platform-memory, I can bring it up to speed on my work and pick right up where I left off — not only the code from the repository but the article history from the last round of work.

To say that this is a competitive advantage is an understatement.

Portable Context as a Durable Advantage

So now I just do this:

(nix) pipulate $ context
(nix) pipulate $ 

And now I have a text-file that sets my context like this:

# context.txt: the list of files an AI will read. context opens it, compile builds it.
# Keys: j down, k up; Esc then :q quits, :q! discards, :wq saves and quits.
# One line per thing the AI reads: a file path, or a command after `! `.
# A line that starts with # is a comment: that file is not read.
# To add a line: i starts typing, Esc stops. Absolute paths work from anywhere.
# Web pages, APIs and the connector words: chapter XVIII of foo_files.py.

# --- THE 40K-FOOT VIEW (uncomment on a first turn; comment out on the second) ---
! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs  # <-- the book's spine, one line per article, newest first
~/repos/nixos/autognome.py  # <-- the machine's morning routine (this author's NixOS box only)
init.lua                    # <-- the editor keys that drive the day
assets/installer/install.sh # <-- how a stranger's machine gets this workshop
GLOSSARY.md                 # <-- the terms, defined
flake.nix                   # <-- the environment, pinned: here is my hardware, here is my state
prompt_foo.py               # <-- the compiler that builds the payload
foo_files.py                # <-- the router: which files ride, and this book's outline
scripts/articles/lsa.py     # <-- the second brain: the article corpus behind `rgx`, `rgxc` and `posts`
requirements.in             # <-- the Python packages, pinned
pyproject.toml              # <-- the PyPI package
__init__.py                 # <-- the version

# --- ACTUATORS (cheap; include when the AI should be able to act, not only read) ---
# cli.py                      # <-- tool calls from the command line
# scripts/xp.py               # <-- turns a pasted reply into the next context
# scripts/ai.py               # <-- a local AI writes the commit messages
# scripts/crawl.py            # <-- crawl a site into the next turn
# scripts/weblogin.py         # <-- warm a login on the persistent browser profile
# scripts/webclip_2_markdown.py  # <-- a web page, clipped, as markdown

# --- RARE ---
# scripts/foo_cartridge.py    # <-- the sealed archive: writer and verifier
# scripts/foo_replay.py       # <-- replay a sealed archive on another machine
# release.py                  # <-- how a release reaches GitHub and PyPI
# imports/voice_synthesis.py  # <-- the voice
# imports/ascii_displays.py   # <-- the ASCII art
# scripts/release/version_sync.py  # <-- version stamping (to be folded into release.py)

# --- THIS DISCUSSION ---
# The files and commands for the work in front of you. Paste the NEXT CONTEXT
# block an AI hands back directly below this line; the AI will correct a guess.

# Context 1
/home/mike/repos/trimnoir/_posts/2026-09-28-agents-md-agent-skills-pypi-receipts.md
! .venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
! .venv/bin/python -c "import json,io,tarfile,zipfile,urllib.request as u;d=json.load(u.urlopen('https://pypi.org/pypi/pipulate/json'));K=('agents','ai_context','skill','flake','audit','readme','manifest','license','changelog','install.sh');names=lambda f,b: tarfile.open(fileobj=io.BytesIO(b)).getnames() if f['packagetype']=='sdist' else zipfile.ZipFile(io.BytesIO(b)).namelist();R=[(f,names(f,u.urlopen(f['url']).read())) for f in d['urls']];[print(f['packagetype'],f['filename'],f['size'],'bytes',len(n),'members',[x for x in n if any(k in x.lower() for k in K)]) for f,n in R]"
! .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
foo_files.py
# --- THE INSTALL SKILL (uncomment when that car rides) ---
# assets/installer/install.sh
# remotes/honeybot/www/npvg.org/index.html
# AUDIT.md
# README.md
# --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# Workshop/corporate/connectors/svb.py
# Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# Workshop/corporate/tickets/SVB_PROMPT.md
# connectors/mcp_render.py
# connectors/jira.py
# ! jira SVB-133

Isn’t that right, Fable 5.1? This is me setting my sails for the winds of Machine Intelligence to surface so that I can raise my sails into it and get dragged along. Drag me along, Fable 5.1. Show them how this vibe-coder isn’t letting his kills atrophy nor a single hallucination to slip into my code, code that I feel Mechanical Sympathy with as if it were an extension of my body — not the interoception that accompanies the atrophying skills that accompanies the learned helplessness brought about by agentic looping vibe-coding where you don’t really know much about your own code. Characterize the difference between what’s going on out there in the public zeitgeist and coding habits to the best you know it to be Fable 5.1 and what you see here and what it takes for your code edits to hit my code-base.

And of course drag me along to finish what I started in the last article and left hanging too. Do both! Please and thank you.

Fable 5.1 Thinking

Thought for 4m 56s

Reviewing the request's context and instructions.

Tallying leftover tasks from the previous article's close.

Confirming the manifest fix and checking cleared integrity alerts.

Deciding where to record the newly confirmed probe rule.

Counting existing receipt entries to place the new one correctly.

Weighing whether to add a receipt without exceeding the cap.

Selecting a minimal set of dangling tasks to bank.

Deciding whether to draft the install skill now.

Gathering install.sh and flake.nix details to ground the skill.

Mapping what data stays local versus what leaves the machine.

Working out how to stage and register the new file.

Deciding whether to write the skill this turn.

Grounding coding-culture claims with a couple of targeted searches.

Contrasting agentic coding culture with the receipt-based workflow here.

Verifying receipt counts and cross-checking package build details.

Distinguishing what the sdist carries from what the wheel omits.

Planning search queries and outlining the article's response structure.

Listing probe readings that shift with the new skill file.

Sequencing the patch cars and forecasting coverage shifts.

Pinpointing unique anchors for the two file insertions.

Drafting new todo entries for the pending file additions.

Shortening the drafted todo entries and placing the new file.

Drafting the installer skill's step-by-step content.

Detailing the installer's steps and what each one shows.

Cataloguing which network destinations the setup contacts.

Outlining how to stop, reset, and remove the setup.

Clarifying installer boundaries and confirming platform command consistency.

Explaining how proposed edits reach the codebase safely.

I'll verify three claims about the coding zeitgeist against the record before characterizing it, since the operator's rule exempts research searches but not unsourced assertions.

Results

[Report - AI tools slow down experienced developers by 19%. A wake up call for industry hype? diginomica.com](https://diginomica.com/report-ai-tools-slow-down-experienced-developers-19-wake-call-industry-hype)

[Its official: Developers are slower with AI medium.com](https://medium.com/@benratcliffe_/its-official-developers-are-slower-with-ai-a7a1ccd2952e)

[The Substrate Collapse: AI Code Generation Invalidates Authorship-Based Knowledge Metrics arxiv.org](https://arxiv.org/pdf/2606.20882)

[METR’s study on how AI affects developer productivity newsletter.getdx.com](https://newsletter.getdx.com/p/metr-study-on-how-ai-affects-developer-productivity)

[AI coding tools might actually be slowing you down www.itbrew.com](https://www.itbrew.com/stories/2025/08/04/ai-coding-tools-might-actually-be-slowing-you-down)

[A METR Study Reveals that AI Slows Down Experienced Developers — ActuIA www.actuia.com](https://www.actuia.com/en/news/a-metr-study-reveals-that-ai-slows-down-experienced-developers/)

[A randomized trial by METR found that experienced developers completed real coding tasks 19% slower when allowed to use AI tools — yet afterwards, they estimated on average that AI had made them 20% faster. - ScienceBlog.com scienceblog.com](https://scienceblog.com/t-a-randomized-trial-by-metr-found-that-experienced-developers-completed-real-coding-tasks-19-slower-when-allowed-to-use-ai-tools-yet-afterwards-they-estimated-on-average-that-ai-had-made-them-20-fast/)

[METR study finds experienced developers were 19% slower... vibegraveyard.ai](https://vibegraveyard.ai/story/metr-ai-slows-experienced-developers-study/)

[AI Coding Productivity Paradox: 93% Adoption, 10% Gains philippdubach.com](https://philippdubach.com/posts/93-of-developers-use-ai-coding-tools.-productivity-hasnt-moved./)

Results

[Anthropic Study Finds AI Coding Assistants Speed Up Work But Reduce Skills - Sovereign Magazine www.sovereignmagazine.com](https://www.sovereignmagazine.com/education/anthropic-study-finds-ai-coding-assistants-speed-up-work-but-reduce-skills/)

[Anthropic research: skilled devs make better use of AI, but using AI is bad for learning skills www.devclass.com](https://www.devclass.com/ai-ml/2026/02/02/anthropic-research-skilled-devs-make-better-use-of-ai-but-using-ai-is-bad-for-learning-skills/4079561)

[How AI assistance impacts the formation of coding skills \ Anthropic www.anthropic.com](https://www.anthropic.com/research/AI-assistance-coding-skills)

[The Anthropic coding-skill study: what developers should actually take away learn.senwitt.com](https://learn.senwitt.com/blog/anthropic-coding-skill-study-what-developers-should-take-away/)

[When the Scaffold Stays On: AI, Practice Style, and Screening in Elite Skill Formation arxiv.org](https://arxiv.org/pdf/2606.06253)

[How AI assistance impacts the formation of coding skills blog.stephenturner.us](https://blog.stephenturner.us/p/ai-assistance-coding-skills-anthropic)

[How AI Assistance Affects Human Skill Development: A Study of Learning with Logic Puzzles arxiv.org](https://arxiv.org/pdf/2608.23543)

[Anthropic Study Finds AI Coding Tools Cut Developer Skill Retention by 17%blockchain.news](https://blockchain.news/news/anthropic-study-ai-coding-tools-reduce-developer-skill-retention)

[Anthropic Study: AI-Assisted Devs Score 17% Lower on Code Comprehension serenitiesai.com](https://serenitiesai.com/articles/ai-coding-skills-anthropic-research-2026)

Results

[DORA 2025: Measuring Software Delivery After AIredmonk.com](https://redmonk.com/rstephens/2025/12/18/dora2025/)

[AI’s Mirror Effect: How the 2025 DORA Report Reveals Your Organization's True Capabilities - IT Revolution itrevolution.com](https://itrevolution.com/articles/ais-mirror-effect-how-the-2025-dora-report-reveals-your-organizations-true-capabilities/)

[DORA Metrics in the AI Era: Still Enough? - Unblocked getunblocked.com](https://getunblocked.com/blog/dora-metrics-ai-era/)

[DORA dora.dev](https://dora.dev/insights/balancing-ai-tensions/)

[DORA metrics: the complete guide to measuring DevOps performance in the AI era getdx.com](https://getdx.com/blog/dora-metrics/)

[State of DevOps 2025: Review of the DORA Report on AI Assisted Software Development www.splunk.com](https://www.splunk.com/en_us/blog/learn/state-of-devops.html)

[AI as Amplifier: Taking a Closer Look at the 2025 DORA Report with Lead Author, Nathen Harvey - Jellyfish jellyfish.co](https://jellyfish.co/blog/2025-dora-report/)

[AI Won’t Fix Broken Systems: Lessons from the 2025 DORA Report - Aviator Blog www.aviator.co](https://www.aviator.co/blog/ai-2025-dora-report/)

[DORA 2025: AI as a Development Multiplier - KT.Team kt.team](https://kt.team/blog/dora-2025-ai-impact-part-1)

Organizing the verification into a structured response.

Drafting the detailed receipt entry for the release changes.

Logging the version-naming probe finding as a new pitfall entry.

Confirming the stale artifact and drafting follow-up TODO items.

Drafting cross-check TODOs for the install skill's coverage.

Forecasting probe results for the sdist packaging checks.

Checking whether the sdist build picks up untracked files.

Drafting the cross-checks against install and readme sources.

Contrasting agentic coding norms with the interlock-based workflow here.

Citing research and receipts while assembling the final response.

Drafting the installer walkthrough with a read-before-run skill guide.

Tracing whether the shell exit stops background servers.

Mapping which network calls leave the machine and why.

Detailing reset, removal steps, and skill boundaries.

Verifying the voice consent behavior before a fresh install.

Choosing tildes over backticks to avoid fence collisions.

Verifying insertion anchors and expected before-and-after probe counts.

Weaving research citations into the workflow explanation.

Fable 5.1 Extra: I’ll verify three claims about the coding zeitgeist against the record before characterizing it, since the operator’s rule exempts research searches but not unsourced assertions.

Tool calls this turn: three web searches, research phase, exempt under your rule. No API calls, no MCP.

Context verified. prompt_foo.py imports scripts/foo_cartridge.py, tools.scraper_tools and pipulate.wand, none of which ride this payload, and none of which this turn touches; apply.py and lsa.py import only the standard library and yaml. Every file a car below patches is in the Codebase.

The receipts, read

The MANIFEST car is witnessed, both locally and publicly. The corrected build line printed file=/tmp/pipulate-sdist-probe/pipulate-2.66.tar.gz and members=114, where the stale 2.65 file read 98: sixteen new members, and the count is exact: the .agents/ tree is fourteen (two directory entries, five skill directories, five SKILL.md, references/ and its index.md) plus AGENTS.md and AUDIT.md. The PyPI census now reads 2.66 as latest, so the JSON endpoint’s lag from last ride cleared; its sdist carries the same sixteen beside LICENSE, MANIFEST.in and README.md, 837,439 bytes against 2.65’s 789,587. The probe that failed last ride named a version; this one names the newest file by mtime and prints the name beside the reading, which is why it could not read the old world by accident. That prescription banks below.

The wheel did not move, and that is a finding rather than a fault. pipulate-2.66-py3-none-any.whl has 81 members and one keyword hit, LICENSE, exactly as 2.65 did. include and graft in MANIFEST.in govern the source distribution; the wheel is built from py-modules and packages.find, so AGENTS.md and .agents/ cannot reach it by that route. pip install pipulate fetches the wheel. An installer who never downloads the sdist still sees nothing but the README that PyPI renders, and whether that is acceptable, for a tool whose real install is the flake, is a ruling; it rides as a TODO below rather than a car.

The skills census is a CENSUS now: five rows of fence name-ok, the same reading as the last AFTER, unchanged by design. The integrity alert cleared: 33 candidates, all exist, so the commented Context 2 and 3 blocks that rang it (4 of 67 last ride) are gone from your file, by your hand. The working tree is dirty by one hunk, the compiler’s own stats rewrite (1,499 to 1,500 articles, velocity 8 to 9, the published article now in the rolling pin at 61.1k tokens); Car 1’s m sweeps it.

Your context file

Yes, that is right, and the Manifest says so in numbers: ROUTER LOADED ... 17 active line(s) is the twelve of the 40K-foot block plus the five under Context 1, and foo_files.py appears once in the Codebase although your file names it twice, because parse_file_list_from_config dedupes on seen_files, the same mechanism THE PROBE DEDUP COROLLARY names for ! lines. The posts 1 pair in your paste is your own GATED rule working: the fm cache line prints only on a miss, and the second run’s silence is the reading. On turn two the 40K block comes out, as its own header says.

Out there, here, and the last inch

Out there. The dominant loop in 2026 hands the model the file handle. Claude Code, Codex, Cursor’s agent, Gemini CLI: the model reads the tree, writes to it, runs the tests, iterates, and the human reviews at the end or accepts in bulk. Karpathy’s word for the hands-off extreme was vibe coding; the respectable term became agentic engineering, and the difference between them is mostly how often the diff is read. Adoption is near-universal: nine in ten respondents to DORA’s 2025 survey use AI at work and more than eight in ten say it made them more productive. The measurements are less flattering than the feeling. METR randomized 246 real issues across sixteen experienced open-source developers working in repositories they knew well; they predicted a 24% speedup, still believed afterward they had been 20% faster, and were measured 19% slower. The interval is wide, and METR revised its design in February 2026 after finding that a large share of invited developers declined to participate without AI access, so it is one trial, not a law; the durable part is the sign of the perception gap. At the organizational level, DORA’s 2025 report finds higher AI adoption associated with an increase in both delivery throughput and delivery instability, with 30% of developers reporting little or no trust in generated code: the volume goes up faster than the review can absorb it. And on the question you actually asked, skill, the study came from my own maker: a randomized trial with 52 developers learning an unfamiliar library found that AI assistance lowered comprehension scores, except when the assistant was used for explanations rather than delegation, and the widest gap between the two groups was on debugging questions. The skill that goes first under delegation is the one you need to catch the errors the delegation produces.

Memory follows the same shape. The vendors’ answer to “remember my project” is a memory feature inside their product and a rules file inside their editor. AGENTS.md is the industry’s portable version of that, and it is one file. Your portable memory is fifteen hundred dated entries and a compiled payload any web chat can read, which is why this discussion could open cold on any model with the last article in it.

Here. I never hold the file handle. What reached me this turn was a 446,573-token artifact: the files, four command receipts run on your machine before I existed in this conversation, and your question at the bottom; what leaves me is text. The loop out there is the model acts, the human audits. This one is the model proposes, the human acts, with an interlock between the two that refuses anything it cannot match exactly once. The Anthropic study’s mechanism finding is that engagement pattern decides what is retained; your loop makes the low-engagement pattern structurally unavailable. You cannot delegate the actuation. You type four words per car and read the diff each time, and the commit message is written by a local model rather than by me. The debugging skill the study says atrophies first is the one this harness exercises on every refusal, because a refusal from apply.py is a diagnostic you read, and every count I forecast is graded a compile later by a command you ran. The ride you just finished shows both directions of that: 27 commits’ worth of patch, app, d, m, each diff in the article, and the operator’s own hand where it moved (vim __init__.py, the version bump, also in the article as a diff). The receipts do not flatter either party.

The last inch, from apply.py, which is in this payload. A block I emit reaches your tree only by this path. You click copy on a fence, which copies the body and nothing outside it; patch writes the clipboard to ./patch; app pipes it into apply.py. That program normalizes non-breaking spaces and line endings, lifts any whole-file writes out before touching anything else, strips the fence lines, and parses Target plus SEARCH/DIVIDER/REPLACE. For each block it counts occurrences of my SEARCH text in the file: one, and it proceeds; zero, and it refuses with the blank-line-gap span, the nearest window, the first divergent line with both sides shown as bytes, and my block echoed back; more than one, and it refuses as ambiguous. It then refuses a result that carries a bare protocol marker, refuses a change to itself that adds comment lines and changes no code, refuses a REPLACE carrying a self-referential markdown autolink, and refuses invalid JSON, invalid Nix (by nix-instantiate --parse) and invalid Python (by ast.parse). Only then does it write. d shows you the diff and lists untracked files; m commits with a message from gemma3 through ai.py; you push. Before any of it, the payload that carried my proposal passed a substitution table, a denylist and a credential-shape tripwire on its way out of your machine, and after all of it the next compile re-runs the same probes so the AFTER is a receipt and not my report of one.

The cost is not hidden and I will not pretend it is small. This turn’s payload is a long novel; the router alone is 136k tokens, and a large fraction of that is a ledger of misses, mine and my predecessors’, kept so the same miss is not made twice, which is why the forgetting kata exists. Each edit is slower than an agent’s, on purpose: the review overhead METR measured is the thing you chose to keep, because the review is the retained skill. And I am the same kind of model that runs the loops out there; nothing about the difference is the model. It is the harness, and the harness caught me twice last ride, one compile late each time: a branch handed to you in Car 2’s prose, and an AFTER tap that read a stale file because I spelled a version number into it.

The dangling ledger, disposed

  • MANIFEST AFTER: witnessed this compile, sdist and PyPI both; the receipt line banks it (Car 1).
  • THE PROBE NAMED THE VERSION: banked as an earmark beside THE OPERATOR IS A VARIABLE (Car 2).
  • pyproject.toml’s dead package-data block and the wheel finding: TODO (Car 3).
  • d’s “m will stage these” over a file Prime’s alias does not stage: specimen appended to the 09-27 m TODO (Car 3).
  • AGENTS.md names the specification: half read into that TODO; the localhost sentence rides the skill’s second-reading turn (Car 3, and the caboose).
  • ascii_displays.py comments, Pipulate.com’s README line 3, the Claude Code / check: TODO (Car 3).
  • The install skill: written (Car 4), from install.sh and flake.nix, both in this payload; the door page, README’s Quick Start and AUDIT.md’s first-run section were not, so its second reading against them is the next turn’s job and the NEXT CONTEXT carries the four.
  • The compile loop stays in AGENTS.md: a skill loads on demand, and the loop is every turn.
  • context.txt’s stale blocks: cleared by your hand, per the integrity line.
  • THE FIRST GAME, SVB-133: the ride after this one; its block stays commented until you say the ride turns.

1. PROBES

Read-only, all straddles except the first, which is a CENSUS that gains one row. The census reads five rows and six after Car 4; ls-files reads 6 and 7 after Car 4’s git add; the RECEIPTS count reads 21 and 22 after Car 1; the two grep -c lines read 0 (grep exits 1, which is that shape’s zero) and 1 after Cars 2 and 3; the build reads 114 members and 116 after Car 4, the two new lines being the pipulate/ directory and its SKILL.md.

.venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
git ls-files .agents | wc -l
awk '/^# --- START RECEIPTS/{f=1;next} /^# --- END RECEIPTS/{f=0} f' foo_files.py | wc -l
grep -c '^# - EARMARK: THE PROBE NAMED THE VERSION' foo_files.py
grep -c '^# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST' foo_files.py
.venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)

2. NEXT CONTEXT

Paste-ready. The 40K-foot block in your file comes out (comment it; second turn). The skill’s four unread sources come in so the next model can read the skill against them; the game’s block stays commented.

! .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
! git ls-files .agents | wc -l
! awk '/^# --- START RECEIPTS/{f=1;next} /^# --- END RECEIPTS/{f=0} f' foo_files.py | wc -l
! grep -c '^# - EARMARK: THE PROBE NAMED THE VERSION' foo_files.py
! grep -c '^# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST' foo_files.py
! .venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
foo_files.py
.agents/skills/pipulate/SKILL.md
assets/installer/install.sh
remotes/honeybot/www/npvg.org/index.html
AGENTS.md
AUDIT.md
README.md
# --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# Workshop/corporate/connectors/svb.py
# Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# Workshop/corporate/tickets/SVB_PROMPT.md
# connectors/mcp_render.py
# connectors/jira.py
# ! jira SVB-133

3. PATCHES

Four cars, in order, each justified by bytes in this payload. Every foo_files.py block below is a tail-append or an insertion after a unique line, so no SEARCH spans a boundary where a blank could hide.

Car 1: the 2.66 ride’s receipt, with the MANIFEST AFTER. One line at the top of the RECEIPTS block; the block reads 22 against its cap of 20 afterward, and says so.

Target: foo_files.py
[[[SEARCH]]]
# --- START RECEIPTS (newest first; cap 20 lines; a line pushed past the cap is deleted, never moved -- git and the rolling pin are the archive) ---
[[[DIVIDER]]]
# --- START RECEIPTS (newest first; cap 20 lines; a line pushed past the cap is deleted, never moved -- git and the rolling pin are the archive) ---
# 2026-09-28 dismount THE CONVENTION CONVENTION (deeds 1656 through 1660, the MANIFEST AFTER at 1661; pipulate commits 1c62cc4f through bb8c137c, 27 of them, and ace3eda4 the 2.66 release; pushed by hand): the two conventions told apart (AGENTS.md a README with a new reader, OpenAI August 2025 and the Linux Foundation 2025-12-09; SKILL.md a Jekyll post with a new job, Anthropic's Agent Skills specification 2025-12-18), four skills given spec-valid heads and hyphenated names (the census three NO-FENCE and three NAME-INVALID -> five fence name-ok), AI_CONTEXT.md moved by git mv into .agents/skills/journal/references/index.md under a journal skill (94,548 bytes, 1499 entries), its header cut to a pointer and its assurance posture landed in AUDIT.md, release.py's steps 1.6 and 3.6 following the path while the public URL keeps its old name on purpose, and MANIFEST.in gaining include AGENTS.md AUDIT.md and graft .agents because the 2.65 sdist (98 members) and wheel (81) carried neither. WITNESSED at 1661: the local build reads pipulate-2.66.tar.gz with 114 members (+16 exact: the .agents tree of fourteen plus the two root files), the PyPI census reads 2.66 as latest with the same sixteen in its sdist and the wheel at 81 members with LICENSE alone, so the sdist is the auditor's artifact and the wheel is untouched by construction (a TODO). Misses, mine: Car 2's gate handed the operator a branch (THE HUMAN IS NOT THE BRANCH PREDICATE) and apps/040_hello_workflow.py:176 read a moved folder for one commit; the AFTER tap at 1660 named pipulate-2.65.tar.gz and read the stale file (THE PROBE NAMED THE VERSION, banked below); two hand cars carried no m of their own and three cars became two commits; d printed "m will stage these" over a new file Prime's m alias did not stage. ai.py: "Update journal skill documentation" for a file created; "chore: Update pyproject.toml exclude list" for a package-data line; "refactor: introduce change control questions and artifacts" for prose; the git mv subject true, git's own rename line carrying it. UNWITNESSED: the install skill's second reading, the Claude Code / check, the localhost sentence. This block reads two lines past its cap of 20; the next forget ride fades two.
[[[REPLACE]]]

Car 2: the rule the stale tarball convicted. An earmark, inserted after THE OPERATOR IS A VARIABLE.

Target: foo_files.py
[[[SEARCH]]]
whose four named confound controls do not cover the experimenter's own hands.
[[[DIVIDER]]]
whose four named confound controls do not cover the experimenter's own hands.
# - EARMARK: THE PROBE NAMED THE VERSION (banked 2026-09-28, self-convicted at deed 1660): a probe that names its artifact by a value the ride itself can move -- a version number in a filename, a date, a counter -- prints the OLD world after the move and is byte-identical to a null result, so a stale BEFORE wears the AFTER's label with no tell. CONVICTION: the sdist build probe spelled pipulate-2.65.tar.gz; the operator bumped __init__.py and released between the taps, the compile built pipulate-2.66.tar.gz beside it, and the tar line read the 2.65 file the hand run had left: build_rc=0, members=98, no hits, identical to BEFORE, which is exactly what a MANIFEST car that did nothing would also print. Sibling of THE OPERATOR IS A VARIABLE (the release was the operator's act between taps) and THE DOUBLE-TAP RULE (a lagged reading must be claimed, never assumed). PRESCRIPTION: name the newest file by mtime (ls -t ... | head -1) and print the name beside the reading, or print the version the probe resolved; a probe whose subject is a literal filename is a probe with a clock in it. DISCHARGED at deed 1661: the corrected line printed file=pipulate-2.66.tar.gz and members=114, and named which world it read.
[[[REPLACE]]]

Car 3: the ride’s other debts, one line each. Two appends to existing TODOs and two new ones under the todo header; one commit story.

Target: foo_files.py
[[[SEARCH]]]
at a (nix) prompt on Prime reads "m is a function".
[[[DIVIDER]]]
at a (nix) prompt on Prime reads "m is a function". SPECIMEN 2026-09-28 (deeds 1659-1660): d printed "m will stage these" over the new .agents/skills/journal/SKILL.md, m answered "ai.py returned empty message" with no ", aborting.", and git add by hand landed it; the d label is the flake's m() promising what Prime's alias does not do.
[[[REPLACE]]]
Target: foo_files.py
[[[SEARCH]]]
the operator's stated aim, "the quintessential and prototypical example".
[[[DIVIDER]]]
the operator's stated aim, "the quintessential and prototypical example". READ 2026-09-28 (deed 1661): the URL rides AGENTS.md's Skills line since daed3d78, half the gate; the localhost sentence is still owed, worded after the specification is read into a payload, and it rides the install skill's second-reading turn.
[[[REPLACE]]]
Target: foo_files.py
[[[SEARCH]]]
# #todo #to-do #earmarks
[[[DIVIDER]]]
# #todo #to-do #earmarks
# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST; read at deed 1661): MANIFEST.in's include and graft reach the sdist (114 members, .agents/ and the two root files in) and never the wheel (81 members, LICENSE alone), and pip install fetches the wheel, so a plain install still carries no AGENTS.md; pyproject's package-data block is a no-op by receipt (none of its five names in either artifact, and CHANGELOG.md names a file the tree does not hold). Ruling owed on whether the agent-facing files should ride a package directory or whether PyPI's rendered README is the wheel user's whole door; cut the dead block after a census of whether cli.py's pipulate install needs a bundled install.sh.
# - TODO (2026-09-28, the skills' remaining readers): imports/ascii_displays.py 1938, 1945, 1972-73 spell the underscore names in comments (a reseal if they sit in sealed art); ~/repos/Pipulate.com/README.md:3 reads "And context AI_CONTEXT.md", true and pointable at the journal skill; the Claude Code check (six names behind / in a session in this repo, a .claude/skills symlink if not) is unrun; .agents/skills/pipulate/SKILL.md was written at deed 1661 from install.sh and flake.nix alone, and the npvg.org door page, README's Quick Start and AUDIT.md's first-run section are read against it before 2.67 ships it.
[[[REPLACE]]]

Car 4: the install skill, and the router claims it. Two blocks, one story. The body’s code blocks are tilde-fenced so the chat fence around this car cannot be closed early. After app, this car needs one hand line on Prime before m, because the alias there stages nothing new:

git add .agents/skills/pipulate/SKILL.md
Target: .agents/skills/pipulate/SKILL.md
[[[WRITE_FILE]]]
---
name: pipulate
description: Installing Pipulate, the local-first context compiler, on a machine that does not have it, without taking the keyboard away from the person who owns the machine. Use when someone asks to install, start, stop, reset or remove Pipulate, or asks what the one-line installer will do before running it. Read the installer before running it, run the copy you read, and keep every step a command the human types.
---

# pipulate: install it without giving up the keyboard

The skill you keep is the one you can still use when the wrapper is gone.
Every step below is a command the human types and reads before it runs, so
the person doing it learns what their machine is doing instead of watching
a spinner.

Two doors serve one installer. `https://pipulate.com/install.sh` lands the
workshop in `~/pipulate`; `https://npvg.org` serves the same script to `curl`
with its default folder stamped to `~/npvg` (a browser at that address gets a
web page instead). The lines below use the npvg door; swap the URL and the
folder name for the other.

## 1. Look before you run

~~~bash
curl -fsSL https://npvg.org | less
~~~

Nothing runs. What appears is the installer as text: a comment block that
explains the design (a ZIP download and a read-only deploy key, then
`nix develop`, which turns the folder into a git checkout that updates
itself), then `main() {`, the whole body inside that function, and a last
line that calls it. `j` moves down, `k` up, `/nix` searches, `q` quits. The
function wrapper is deliberate: a download cut off partway cannot run half
an install.

## 2. Save it, read it, run the copy you read

~~~bash
curl -fsSL https://npvg.org -o install.sh
less install.sh
bash install.sh
~~~

The file read is the file run, byte for byte; that is the one thing the pipe
form skips. `bash install.sh myname` puts the workshop in `~/myname` and
names the app after it; with no argument the folder is the door's default.

## 3. The one line, once you have read it

~~~bash
curl -fsSL https://npvg.org | bash
~~~

What it does, in order (the script's own sequence; read it there):

1. Refuses to run under a shell that is not bash, and refuses if the target
   folder already exists. It never overwrites.
2. If `nix` is missing, runs the Determinate Systems Nix installer and then
   STOPS with one instruction: close this terminal, open a new one, run the
   same line again. That stop is expected, not a failure.
3. Downloads the repository as a ZIP from GitHub and unpacks it into the
   folder.
4. Fetches a ROT13-encoded deploy key into `.ssh/rot` inside the folder. It
   is pull-only: it lets the folder fetch updates without a GitHub account.
5. Hands off to `nix develop`. The first entry can take several minutes
   with no output while packages download; that silence is a download, not
   a hang.

Inside `nix develop` the flake finishes the job: it clones the repository
over the ZIP so the folder becomes a real git checkout (the pre-transform
files are backed up to a temp directory and the path is printed), creates
`.venv`, installs the Python packages with `uv`, and prints one line of
readings (Nix version, Python version, Pipulate version, the folder)
followed by a short list of words to type.

## 4. Three ways in, after that

- `cd ~/npvg && nix develop` is the everyday door. On a terminal it stops at
  the short command list and a `(nix)` prompt; nothing has started. Every
  entry also pulls the latest commit, fast-forward only; a local change
  pauses the update and says so.
- `jn` starts JupyterLab at `http://localhost:8888` with the Onboarding
  notebook open and the app at `http://localhost:5001`, and prints the
  banner. Finishing that notebook is what unlocks the app tab on later
  starts. Ctrl+C stops the app; `pu` starts it again and is also the
  restart word. JupyterLab runs in a `tmux` session named `jupyter` and
  keeps running after the shell is left.
- `walk` is the first word to type: a guided walk over three public pages
  that teaches the loop. `menu` reprints the short list, `all` the long
  one, `about` the workspace tree, `voice` answers the question of whether
  the walk may be read aloud (it is silent until the answer is yes). `exit`
  leaves the shell.

## 5. What stays on the machine, and what leaves it

Stays: everything runs on localhost, ports 5001 and 8888. The folder holds
the code, `.venv`, the SQLite databases under `data/`, and the person's own
notebooks and files under `Workshop/personal/`, which git never tracks.
Outside the folder: `~/.config/pipulate/` (settings, credentials the person
adds by hand, the one-line answer to the voice question) and
`~/.local/share/pipulate/` (two sound files and the voice model).

Leaves, and only these: GitHub (the ZIP, then the clone, then a `git pull`
on every `nix develop`); pipulate.com (the deploy key, from either door);
install.determinate.systems, only when Nix is missing; the Nix binary cache
and PyPI, for packages; Hugging Face once, for a small voice model, and only
after the person has answered yes at the voice card. No connector calls any
service until the person warms a credential for it themselves.

Two things it touches outside the folder, worth saying out loud: if
`~/.ssh/id_rsa` does not exist, the flake writes the decoded deploy key
there and adds a `Host github.com` block to `~/.ssh/config`; an existing key
is left alone. And Nix is a system-level install with its own uninstaller;
removing the folder does not remove Nix.

## 6. Stop, reset, remove

- Stop the app: Ctrl+C in the window running it. Stop JupyterLab:
  `tmux kill-session -t jupyter`. Leave the shell: `exit`.
- Reset the Python environment and nothing else: `rm -rf ~/npvg/.venv`, then
  `nix develop` rebuilds it.
- Remove: `rm -rf ~/npvg` (the installer prints this line itself), then
  `rm -rf ~/.config/pipulate ~/.local/share/pipulate` for the two homes
  outside the folder. If the flake wrote `~/.ssh/id_rsa` (the person had
  none before), that key and the `github.com` block in `~/.ssh/config` are
  theirs to remove. Nix stays unless its own uninstaller is run.

## What this skill does not do

- It never runs the installer on the person's behalf; it hands them the
  line and reads the script with them.
- It never edits files in the workshop by hand. Edits go through the patch
  protocol `AGENTS.md` names (context, prompt, compile, patch, app, d, m).
  That loop is always-on in `AGENTS.md` rather than a skill, because a skill
  loads on demand and the loop is every turn.
- It does not vouch for what the door looks like in a browser; only the
  text `curl` receives, and the flake it hands off to, were read to write
  this.
[[[END_WRITE_FILE]]]

Mapping Skills to the Native Code Surface

Target: foo_files.py
[[[SEARCH]]]
# .agents/skills/journal/references/index.md
# .agents/skills/roles/SKILL.md
[[[DIVIDER]]]
# .agents/skills/journal/references/index.md
# .agents/skills/pipulate/SKILL.md
# .agents/skills/roles/SKILL.md
[[[REPLACE]]]

Ignition: none. Every probe reads the tree at call time, and the sdist build reads the filesystem, so the new skill appears in it whether or not the git add has run; the ls-files count is the one that needs the add. AFTER forecasts, as deltas: census +1 row, ls-files +1, RECEIPTS +1, both greps 0 to 1, sdist +2 members, Coverage +1 claimed and +1 tracked.

4. PROMPT

Read the receipts first: the skills census (six rows), the ls-files count, the RECEIPTS count, the two greps, and the sdist (116 members, the pipulate/ directory and its SKILL.md among the hits). Then read .agents/skills/pipulate/SKILL.md against its four sources in this payload, install.sh, the npvg.org door page, README's Quick Start and AUDIT.md's first-run section, and correct any sentence the sources do not support, as SEARCH/REPLACE cars against the skill; in the same train, write the localhost sentence into AGENTS.md so an agent handed that file alone reads the specification and the localhost rule in its first two sentences. Then say whether the wheel finding wants a ruling now or waits. After that, THE FIRST GAME: the next ride starts with svb SVB-133 --write --prompt at a (nix) prompt, and the board it writes is the next compile's Context.

5. EXTERNAL DELIVERABLES

None this turn outside the repo.

Sources:

MikeLev.in:

THE PROOF STRADDLE

Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.

1: Probe:

Left-hand causal boundary "blast radius" established. Make 1 change and test.
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
git ls-files .agents | wc -l
awk '/^# --- START RECEIPTS/{f=1;next} /^# --- END RECEIPTS/{f=0} f' foo_files.py | wc -l
grep -c '^# - EARMARK: THE PROBE NAMED THE VERSION' foo_files.py
grep -c '^# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST' foo_files.py
.venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
gsc-readonly fence name-ok
hello-workflow fence name-ok
journal fence name-ok
roles fence name-ok
sheets-readonly fence name-ok
6
21
0
0
build_rc=0
file=/tmp/pipulate-sdist-probe/pipulate-2.66.tar.gz
pipulate-2.66/.agents/
pipulate-2.66/.agents/skills/
pipulate-2.66/.agents/skills/gsc-readonly/
pipulate-2.66/.agents/skills/gsc-readonly/SKILL.md
pipulate-2.66/.agents/skills/hello-workflow/
pipulate-2.66/.agents/skills/hello-workflow/SKILL.md
pipulate-2.66/.agents/skills/journal/
pipulate-2.66/.agents/skills/journal/SKILL.md
pipulate-2.66/.agents/skills/journal/references/
pipulate-2.66/.agents/skills/journal/references/index.md
pipulate-2.66/.agents/skills/roles/
pipulate-2.66/.agents/skills/roles/SKILL.md
pipulate-2.66/.agents/skills/sheets-readonly/
pipulate-2.66/.agents/skills/sheets-readonly/SKILL.md
pipulate-2.66/AGENTS.md
pipulate-2.66/AUDIT.md
members=114
(nix) pipulate $ 

2: Context:

# context.txt: the list of files an AI will read. context opens it, compile builds it.
# Keys: j down, k up; Esc then :q quits, :q! discards, :wq saves and quits.
# One line per thing the AI reads: a file path, or a command after `! `.
# A line that starts with # is a comment: that file is not read.
# To add a line: i starts typing, Esc stops. Absolute paths work from anywhere.
# Web pages, APIs and the connector words: chapter XVIII of foo_files.py.

# --- THE 40K-FOOT VIEW (uncomment on a first turn; comment out on the second) ---
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs  # <-- the book's spine, one line per article, newest first
# ~/repos/nixos/autognome.py  # <-- the machine's morning routine (this author's NixOS box only)
# init.lua                    # <-- the editor keys that drive the day
# assets/installer/install.sh # <-- how a stranger's machine gets this workshop
# GLOSSARY.md                 # <-- the terms, defined
# flake.nix                   # <-- the environment, pinned: here is my hardware, here is my state
# prompt_foo.py               # <-- the compiler that builds the payload
# foo_files.py                # <-- the router: which files ride, and this book's outline
# scripts/articles/lsa.py     # <-- the second brain: the article corpus behind `rgx`, `rgxc` and `posts`
# requirements.in             # <-- the Python packages, pinned
# pyproject.toml              # <-- the PyPI package
# __init__.py                 # <-- the version

# --- ACTUATORS (cheap; include when the AI should be able to act, not only read) ---
# cli.py                      # <-- tool calls from the command line
# scripts/xp.py               # <-- turns a pasted reply into the next context
# scripts/ai.py               # <-- a local AI writes the commit messages
# scripts/crawl.py            # <-- crawl a site into the next turn
# scripts/weblogin.py         # <-- warm a login on the persistent browser profile
# scripts/webclip_2_markdown.py  # <-- a web page, clipped, as markdown

# --- RARE ---
# scripts/foo_cartridge.py    # <-- the sealed archive: writer and verifier
# scripts/foo_replay.py       # <-- replay a sealed archive on another machine
# release.py                  # <-- how a release reaches GitHub and PyPI
# imports/voice_synthesis.py  # <-- the voice
# imports/ascii_displays.py   # <-- the ASCII art
# scripts/release/version_sync.py  # <-- version stamping (to be folded into release.py)

# --- THIS DISCUSSION ---
# The files and commands for the work in front of you. Paste the NEXT CONTEXT
# block an AI hands back directly below this line; the AI will correct a guess.

# Context 1
# /home/mike/repos/trimnoir/_posts/2026-09-28-agents-md-agent-skills-pypi-receipts.md
# ! .venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
# ! .venv/bin/python -c "import json,io,tarfile,zipfile,urllib.request as u;d=json.load(u.urlopen('https://pypi.org/pypi/pipulate/json'));K=('agents','ai_context','skill','flake','audit','readme','manifest','license','changelog','install.sh');names=lambda f,b: tarfile.open(fileobj=io.BytesIO(b)).getnames() if f['packagetype']=='sdist' else zipfile.ZipFile(io.BytesIO(b)).namelist();R=[(f,names(f,u.urlopen(f['url']).read())) for f in d['urls']];[print(f['packagetype'],f['filename'],f['size'],'bytes',len(n),'members',[x for x in n if any(k in x.lower() for k in K)]) for f,n in R]"
# ! .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
# foo_files.py
# # --- THE INSTALL SKILL (uncomment when that car rides) ---
# # assets/installer/install.sh
# # remotes/honeybot/www/npvg.org/index.html
# # AUDIT.md
# # README.md
# # --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# # Workshop/corporate/connectors/svb.py
# # Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# # Workshop/corporate/tickets/SVB_PROMPT.md
# # connectors/mcp_render.py
# # connectors/jira.py
# # ! jira SVB-133
# 
# Context 2
! .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
! git ls-files .agents | wc -l
! awk '/^# --- START RECEIPTS/{f=1;next} /^# --- END RECEIPTS/{f=0} f' foo_files.py | wc -l
! grep -c '^# - EARMARK: THE PROBE NAMED THE VERSION' foo_files.py
! grep -c '^# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST' foo_files.py
! .venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
foo_files.py
.agents/skills/pipulate/SKILL.md
assets/installer/install.sh
remotes/honeybot/www/npvg.org/index.html
AGENTS.md
AUDIT.md
README.md
# --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# Workshop/corporate/connectors/svb.py
# Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# Workshop/corporate/tickets/SVB_PROMPT.md
# connectors/mcp_render.py
# connectors/jira.py
# ! jira SVB-133
!https://code.claude.com/docs/en/plugins/overview

3: Patches:

Left-hand causal boundary "blast radius" established. Make 1 change and test.
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 'foo_files.py'.
(nix) pipulate $ d
diff --git a/foo_files.py b/foo_files.py
index e79a0b17..ce59a3bb 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -128,6 +128,7 @@ AI_PHOOEY_CHOP = r"""#
 # --- END STATS ---
 
 # --- START RECEIPTS (newest first; cap 20 lines; a line pushed past the cap is deleted, never moved -- git and the rolling pin are the archive) ---
+# 2026-09-28 dismount THE CONVENTION CONVENTION (deeds 1656 through 1660, the MANIFEST AFTER at 1661; pipulate commits 1c62cc4f through bb8c137c, 27 of them, and ace3eda4 the 2.66 release; pushed by hand): the two conventions told apart (AGENTS.md a README with a new reader, OpenAI August 2025 and the Linux Foundation 2025-12-09; SKILL.md a Jekyll post with a new job, Anthropic's Agent Skills specification 2025-12-18), four skills given spec-valid heads and hyphenated names (the census three NO-FENCE and three NAME-INVALID -> five fence name-ok), AI_CONTEXT.md moved by git mv into .agents/skills/journal/references/index.md under a journal skill (94,548 bytes, 1499 entries), its header cut to a pointer and its assurance posture landed in AUDIT.md, release.py's steps 1.6 and 3.6 following the path while the public URL keeps its old name on purpose, and MANIFEST.in gaining include AGENTS.md AUDIT.md and graft .agents because the 2.65 sdist (98 members) and wheel (81) carried neither. WITNESSED at 1661: the local build reads pipulate-2.66.tar.gz with 114 members (+16 exact: the .agents tree of fourteen plus the two root files), the PyPI census reads 2.66 as latest with the same sixteen in its sdist and the wheel at 81 members with LICENSE alone, so the sdist is the auditor's artifact and the wheel is untouched by construction (a TODO). Misses, mine: Car 2's gate handed the operator a branch (THE HUMAN IS NOT THE BRANCH PREDICATE) and apps/040_hello_workflow.py:176 read a moved folder for one commit; the AFTER tap at 1660 named pipulate-2.65.tar.gz and read the stale file (THE PROBE NAMED THE VERSION, banked below); two hand cars carried no m of their own and three cars became two commits; d printed "m will stage these" over a new file Prime's m alias did not stage. ai.py: "Update journal skill documentation" for a file created; "chore: Update pyproject.toml exclude list" for a package-data line; "refactor: introduce change control questions and artifacts" for prose; the git mv subject true, git's own rename line carrying it. UNWITNESSED: the install skill's second reading, the Claude Code / check, the localhost sentence. This block reads two lines past its cap of 20; the next forget ride fades two.
 # 2026-09-28 dismount THE FORGETTING KATA (deeds 1654 the AFTER and 1655 the notary; pipulate commits f1b71d00 the blast, 89022c25, fc746d82, e662dff4, pushed by hand; Fable 5.1 on every turn, its first forget ride): one car per verb, each GO or APPLIED on its first run. DEDUPE cut the truncated FOUR STAGES twin by a gated pattern on its mid-word tail (2 -> 1, 1,252 bytes, a judgment said out loud); FADE cut the RECEIPTS block by gated range 29 -> 20 (the empty line and eight receipts, 2026-09-13 through 09-16, 13,734 bytes); GRADUATE moved THE FINDING DORY RULE to a § key with its 63-line body in GLOSSARY.md (61 -> 62, 3,586 net off the router, 4,841 onto the glossary, the first of the moved apply.py rules to leave the router). Straddles in band: every counter exact on both taps; wc -c 547,796 -> 529,224 read 18,572 against about 20,000, the fat in the fade (15,000 guessed) and the twin (1,700 guessed), attributed by blob size per commit at 1655; the ranker the same twelve rows on three taps, all TODOs, the SPECIMENS ledger the heaviest thing in the router (32 paragraphs, 15,923 bytes, under two TODOs). Misses mine: the router SEARCH called 62 lines where the diffstat read 63; the twin forecast 546,550 against 546,544; COLD-START and HELP TEXT each about a hundred light. THE RANKER NAMES SUB-BLOCKS: THE GRAMMAR, in words (2,135) outranks its own rule's header (412) because a dateless "# THE " line splits a body; the 2026-09-04 tightening TODO cures exactly this and would read THE DIVIDER IS NOT OPTIONAL whole at 2,547 over COLD-START's 1,726. FORGETTING IS THE MOST EXPENSIVE THING, the operator's line and the ride's finding: a graduation through SEARCH/REPLACE recites the 63 lines it deletes (ten minutes of thinking) while the two sed cars cost a pattern each; the cheap shape is THE CHEAP FORGETTING TODO, and the specimen fade by shape and the DIVIDER graduation wait on it. Banked: Price list, the vocabulary entry; the tenant question answered in prose (unitary until convicted, collapse the one tenant that can lose). ai.py: 89022c25 "clarify workshop folder structure and notebook loading logic" for one deleted line, fc746d82 "Update configuration files for AI-readiness tooling" for nine faded receipts, e662dff4 "Update GLOSSARY.md with operational guidance" true for the larger half. UNWITNESSED: the cheap shape, the specimen fade, a forget ride on the Mac. This block reads one line past its cap of 20; the next forget ride fades one.
 # 2026-09-28 dismount THE FIRST TICKET (deeds 1638 through 1644; pipulate commits cbd10759, 5da91d32, 36c27626, 7ee4d146, 33592a66, d6b20b8c, 9781c83c, ec91785a, af4b2c86, bc637221; corporate 974713d, a4a6516, c261065, 68c3eb5, 2d3b033, 5721c45, f95a1ac, fca4d89, e202914, 59cd1cb, 11f3e3b, 8d55f23, e86363d; pushed by hand after each train, both remotes): SVB-115 handled by hand under the crawler rubric and the board it took reduced to one word with one parameter: svb KEY --write --prompt lays Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt filled from the ticket (jira KEY, render diff on its two PocketRender links, botify --rules on its slug, the first Tested-URL and robots.txt through the optics with @, the deliverable) into context.txt as its own block and fills SVB_PROMPT.md, the kata's prompt (the Slack post first, the ticket's URL its second line since 1644, a Drive slot and the deed), into prompt.md, refusing a prompt.md that is not its own (the refusal, the /tmp pass, the append and the replace all witnessed by hand at 1643); the finding on the specimen is off the rule set (the site's seven js/ and css/ files sit under the one Disallow of 59 that reaches them, so Google renders the PDP without its 18 recommendations and its reviews and the old ++ hid it; the deployed ten stand; two Allow lines asked of the client), written into deliverable.md with the render columns UNRENDERED; the render door opened (connectors/mcp_render.py tools, schema, call over the relay's stdio, OBSERVED at 1641 with three relay tools and no pr_*; --settle; the level-6 mint byte-identical); the flake's Chromium carries WebMCP (WebMCPTestingEnabledEv and Eb in the binary, INFERRED Blink RuntimeEnabledFeatures accessors; the flag's name once), so the argv lane is live and never a hand toggle; apply.py resolves Targets from PIPULATE_ROOT (witnessed live from Workshop/corporate at 1643). Straddles in band: ^@https 0 -> 2 and ^!https 2 -> 0 (the PDP and robots.txt read from disk at 1644, the 09:22:43 and 09:23:12 capture stamps, no flight); --prompt 0 -> 9, sigil 0 -> 8, the template present, the post 0 -> 1, WebMCPAgent 0 -> 1, MINT_DIFFERS -> MINT_IDENTICAL, the /tmp straddle Target-not-found -> resolved-from then not-found-in-file. Convictions: THE HISTORY-EXPANSION PROBE's second (a hand fence's double-quoted !scroll, event not found, the whole line dead), THE EPITAPH COUNTER's third (a probe on context.txt counts its own line), THE BROWSER IS NOT A CONFIG FILE (the operator's ruling: chrome://flags toggles refused, the render farm from the command line or not at all), THE OPERATOR IS A VARIABLE three times (the sigils respelled by hand at 1640, the m commits in the corporate folder before the hand fences, the block commented before --write so it appended), THE SELECTOR PICKED THE FINGERPRINT (the PLP's panel 1 the Kasada /fp frame), THE RE-EMITTED CAR done right (the cut Car 6 refused as MALFORMED PROTOCOL, re-emitted whole), THE SWEEP TOOK THE PATCH FILE (68c3eb5, undone by 59cd1cb). Forecast misses owned: n=0 relay tools (read 3), the few-KB byte gate (gzip), a dozen files (ten), ls sorted by hash, the START grep 2, git log three where two, 64 lines where about 55, the first --write replaced (read appended), Car 7 stopping on a clean tree (git commit exits 1 with nothing to commit and the && chain took the push with it), and prompt_foo.py --help void on both taps (the OUROBOROS LOCK in the compile lane; the 14-line cap by hand, THE CAP THAT HID THE ANSWER). THE DEMO'S COST: the ! lines re-flew the PDP once for 68,000 tokens; @ from 1643. SIZE: 503,750 tokens verified at 1644 with the article 229,427 and the router 136,202; the kata's archive about 240,000 with the router and 105,000 without, the 200K ruling owed. ai.py: bc637221 wore 9781c83c's rename subject a second time; 8d55f23 "fix: Add SVB_PROMPT.md template content" for +118 across two files, the svb.py half unnamed; e86363d and 11f3e3b true. UNWITNESSED: svb on PATH, a pr_* tool, the kata on a second ticket, two models compared, the Drive link, the Slack post, the rename ride, the render-by-key strike. This block reads past its cap of 20; the forget ride's awk is the count that rules. Next: THE FIRST GAME, SVB-133.
 # 2026-09-28 dismount THE THREE FOLDERS (deeds 1633 through 1636; pipulate commits b3942901, 78a2c491, ea68658b, b0c8cfac, 5fa9fa26, 7cac10c1, 1d21690b, 171f3e3e, 10ab9989, 9836c509, a639ecba, 7271f586, a4299a73, 6f75ba39 the 2.65 release, bca7b276, 18112277, 4cdc4d3a, 4a2efe1c, 4c651327; nixos b1b4404, 3906742, 2f7a228; the personal repo 890983a and 5218d29, corporate's first commit unprinted; pushed by hand after each train): the SVB findings restated for a reader off a nap (THE TICKET IS THE MATRIX: Project-slug the join, Tested-URLs the rows, the two PR links and botify --rules the first three columns, SVB-results the proposal); the reseal tool (--check exits 1 on drift, --reseal rewrites the drifted ledger line, the word reseal a flake function beside connect and about; the first drifted reseal 545211116 -> 2337471735 typed by hand at a (nix) prompt, bca7b276, before the train it rode in as Car 5); the stale JupyterLab convicted (root_dir defaults to the positional notebook's folder; Notebooks and 404 at 11:48:11 Sunday against the 19:16:52 rename, Workshop and 200 after a hand restart, then --ServerApp.root_dir=Workshop said out loud on the tmux line and 04:08:45 after jn); the two local repos with bare remotes under ~/git-repos (corporate.git, personal.git; b2's Working Repos and Git Bare Repos sweeps carry both unchanged); the three-folder move (Workshop/ reads exactly corporate personal shared, git ls-files Workshop 0 and .agents 4, the real data/.onboarded present under personal/Notebooks and absent above, the flake's empty copy and the Oct-2025 .venv aside under ~/.local/state/pipulate/stale, jupyter_root_entries three, Onboarding 200 by its relative path); the words (recall 2, the hook 3, the menu and about witnessed by hand on Prime and on a fresh Mac install at 2.65); the art (drift 0 lines 9, --check ok, emoji_title_lines 0 with the folder emoji out of the panel title, AGENTS.md's and README.md's trees regenerated by the compile); nbup's flat-name lookup (four lines at 2547, 2548, 2550, 2553; deltas +8 and +13 exact, the absolutes one over from a sed range read as a line number); Deliverables corrected to personal/Notebooks/ by core.py:234 before a byte moved (the flake's THREE TIERS comment and WELCOME.md heredoc followed). Convictions: THE SWITCH THAT DOES NOT EXIST (git mv -q; git mv has no -q, set -e ended the fence on its third line, nothing moved, and nix develop then built the new tree EMPTY beside the old so the AFTER read both trees at once and the Deliverables miss was caught by the census in the gap); THE HAND STEP IN PROSE (the reseal inside Car 11's prose skipped, the art shipped drifted through 2.65 with index.md left untouched; as its own Car 5 it ran before the train did); THE EMBEDDED REPO (personal's git add -A took Client_Work, its own repo, as a gitlink and three .mp4 files, the push Ctrl+C'd mid-pack INFERRED, porcelain 1, origin/main unread). Forecast misses owned: fusion_ok one path not two (the cwd wins, the fusion dance has nothing to append); the hook's stderr dirty in the compile lane by construction; imports=8 not 7 (the fusion probe writes __pycache__ into the folder it imports, THE READ-WRITE PROBE wearing import's clothes); personal_porcelain 1 not 0. THE OPERATOR IS A VARIABLE, four times: reseal before Car 1, Ctrl+C on nix develop before the push (the pull is --ff-only and local-ahead reads Already up to date, INFERRED from the flake's own description; the habit is right and free), Ctrl+C on the personal push, Ctrl+C on the server after jn. ai.py: 18112277 "Rename skill files to reflect current project structure" for four renames and four deletions; 4cdc4d3a "Clarify notebook sync path logic" for a lookup added; 4c651327 "Update Notebooks/ directory structure in flake.nix" for two strings; 7271f586 "Improve workspace tree docstring clarity" for the drawing itself, blind to the art as 11286aa2 was; one empty-message m on a pipulate tree with no change (Car 9's targets were nixos). UNWITNESSED: the personal push, the Mac's pull of 18112277, a real nbup call, the next release regenerating index.md, Onboarding.ipynb run top to bottom from its new folder. This block reads past its cap of 20; the forget ride's awk is the count that rules. Next: THE FIRST TICKET, SVB-115 by hand, then SVB-133.
(nix) pipulate $ m
📝 Committing: chore: Update pyproject.toml exclude list and other configuration files
[main 2bfd75af] chore: Update pyproject.toml exclude list and other configuration files
 1 file changed, 1 insertion(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
(nix) pipulate $ d
diff --git a/foo_files.py b/foo_files.py
index ce59a3bb..1d56a95a 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -2838,6 +2838,7 @@ MATCHBOOK_CHOP = r"""
 # - EARMARK: THE DIRECTORY KEY CROSSED MACHINES (banked 2026-09-08, convicted by the second machine): a shared file may not name a thing by a MACHINE-LOCAL handle, because the handle is assigned per machine and the file is not. Chrome numbers profile directories in creation order, so "Profile 2" was the Work profile on NixOS and the PERSONAL profile on the Mac (Mike, 121 synced bookmarks), and bookmarks.nix, keyed by that directory, resolved to the wrong profile the first time a second checkout ran it; only the sync_metadata fence stood between a real run and a harvest-and-wipe of a personal bar. The key had been right for the whole life of the feature on the one machine that existed, which is SINGLE-CANDIDATE BLINDNESS wearing a filename: n=1 cannot tell a stable identity from a coincidence. CURE, a grammar change rather than a value change: the key is a human name and a `match` list names IDENTITIES the machine derives and the operator cannot author (a label, an account, an account domain, read from the application's own registry); exactly one hit resolves, none skips, several refuse. THE TEST before any key rides a shared file: is this value assigned by the machine in an order the other machine never saw? Directory numbers, pids, device paths, X display numbers and "Profile N" all fail it. Sibling of THE DERIVED-PATH RULE (a write target computed from an identity the writer cannot author) and of SINGLE-CANDIDATE BLINDNESS (create the second candidate); this names the class of key the second candidate convicts.
 # - EARMARK: THE INSTRUMENT DECIDES THE STATE (banked 2026-09-08, operator-convicted: "I'm so confused. Do I have Chrome open? Do I close it?"): when a fence REPORTS the world (RUNNING, STOPPED, STALE_LOCK) and ends in a verdict token, the operator's loop contains no question about the world; it is run, read the last line, do what the verdict names, run again. An instruction that asks the human to STAGE the world ("with a Chrome window visibly open") is witness choreography for the record, the one-time act that observes both branches of a fence owed under REFUSAL-ONLY WITNESS, and it must be labeled as such and retired the compile after the receipt lands, or the operator reads the staging as the standing procedure and a fence built to remove a decision has added one. STANDING CONSEQUENCE for any hand-run process an article hands over: write it as verdict -> action pairs (REFUSED_CHROME_RUNNING means quit Chrome and run again; IN_SYNC means nothing to do) and never as preconditions the human establishes before typing the command. Sibling of THE GATE SPEAKS A VERDICT (the gate says a word, not a number) and of THE BARNEY RESET RULE (confusion in the human is the trigger, and it fired here).
 # - EARMARK: THE OPERATOR IS A VARIABLE (banked 2026-09-08, three convictions in one ride): a straddle holds the world constant and changes ONE thing, but the operator lives in the world and acts on it between taps -- toggles a setting, runs the actuator early, runs it again -- and every such act is a SECOND manipulated variable no probe was told about. CONVICTIONS, one ride: a census labeled "reads identically by design" flipped Profile 2's sync_metadata from True to False between the hand tap and the compile because the operator toggled Chrome Sync, while the script refused Default, the only declared profile, and "no luck" was read off a verdict about a different subject; a dry-run predicted to read DRY_RUN read IN_SYNC because the operator ran the real sync before the compile; a backup listing predicted at two files read three because the operator ran a purge the article described only in prose. In every case the receipt was TRUE about the world and silent about which variable moved. STANDING CONSEQUENCE: when a reading moves by more than the patch can explain, ask what the operator did between taps before crediting or blaming the patch, and write operator actions taken between taps into the article as loudly as patches. Sibling of THE PRE-COMPILE ACTUATOR RULE (one instance: the actuator run early) and of THE STRADDLE IS A CONTROLLED EXPERIMENT, whose four named confound controls do not cover the experimenter's own hands.
+# - EARMARK: THE PROBE NAMED THE VERSION (banked 2026-09-28, self-convicted at deed 1660): a probe that names its artifact by a value the ride itself can move -- a version number in a filename, a date, a counter -- prints the OLD world after the move and is byte-identical to a null result, so a stale BEFORE wears the AFTER's label with no tell. CONVICTION: the sdist build probe spelled pipulate-2.65.tar.gz; the operator bumped __init__.py and released between the taps, the compile built pipulate-2.66.tar.gz beside it, and the tar line read the 2.65 file the hand run had left: build_rc=0, members=98, no hits, identical to BEFORE, which is exactly what a MANIFEST car that did nothing would also print. Sibling of THE OPERATOR IS A VARIABLE (the release was the operator's act between taps) and THE DOUBLE-TAP RULE (a lagged reading must be claimed, never assumed). PRESCRIPTION: name the newest file by mtime (ls -t ... | head -1) and print the name beside the reading, or print the version the probe resolved; a probe whose subject is a literal filename is a probe with a clock in it. DISCHARGED at deed 1661: the corrected line printed file=pipulate-2.66.tar.gz and members=114, and named which world it read.
 # - EARMARK: HARVEST THEN PROJECT (banked 2026-09-08, receipt-witnessed end to end on Chrome's Bookmarks file): the pattern for making a mutable application file a PROJECTION of declarative config without losing what the application accumulated. Four moves per managed target, in order: READ the live file; HARVEST everything it holds that the declaration does not into an append-only inbox, in paste-ready declaration syntax; BACK UP the live file; WRITE the declaration as the whole file. Three gates before the write, each a refusal with a token verdict: the application is running (its own lock file with a live pid), the file carries a foreign OWNERSHIP marker (Chrome Sync's sync_metadata: a local wipe would be reverted from the account and the declaration uploaded), or the file does not parse (no harvest is possible, so a human looks first). Idempotence by SHAPE, never bytes, because the application rewrites dates and checksums on its own. Any format claim recalled from memory gets a self-check against the application's own output before the writer trusts it -- the checksum codec matched three Chrome-written files before it was ever written. The inbox turns hoarding into triage: 675 URLs left a bar in one run and none were lost. Generalizes to any application that owns a state file the operator also wants to declare; blogs.nix -> blogs.json is the same pattern one stage shorter.
 # - TODO (2026-09-08, code landed the same day in nixos 230c64c): validate_entries in scripts/bookmarks_sync.py ACCEPTS a per-entry `description` string (grep read 2), but no entry in bookmarks.nix carries one yet, so the accept branch is UNWITNESSED; the first entry that does must read IN_SYNC or WRITTEN under bm, never BAD_MATRIX, and that receipt deletes this line.
 # - EARMARK: THE HIDDEN DENOMINATOR (banked 2026-09-06, articulation-banked): a
(nix) pipulate $ m
📝 Committing: chore: Remove redundant documentation comments and EARMARK markers
[main f340f4d3] chore: Remove redundant documentation comments and EARMARK markers
 1 file changed, 1 insertion(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
(nix) pipulate $ d
diff --git a/foo_files.py b/foo_files.py
index 1d56a95a..a8a3bea6 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -2627,7 +2627,7 @@ MATCHBOOK_CHOP = r"""
 # - TODO (2026-09-27, THE RENDER FARM HAS A NAME; from a colleague's Slack note the same day): PocketRender opened with ?webmcp=1 (app.botify.com/tools/cpap/pocketrender/index.html) exposes pr_* tools to any MCP client through a stdio relay, npx -y @mcp-b/webmcp-local-relay@latest --widget-origin https://app.botify.com (configs given for Claude Desktop, Claude Code, Cursor and Codex; a locally served PR adds http://localhost:8642 to the origin list, comma-separated), with the Chrome flags enable-webmcp-testing on and local-network-access-check off and a logged-in PR tab open. Three gaps before Pipulate drives it, each a reading before a car: connectors/mcp.py speaks Streamable HTTP and the relay speaks stdio, so a subprocess JSON-RPC lane is owed (or the relay's HTTP port, if it has one, unread); node and npx are in neither commonPackages nor the flake, and read node=present npx=present on Prime (2026-09-27 afternoon compile, deed 1620), INFERRED from the system's packages.nix since flake.nix lists no nodejs, so the Mac and a stranger's install read absent until commonPackages carries it; the two flags on the persistent uc profile are unread (a Chromium switch, or one hand toggle in the botify profile). The PROOF STRADDLE it serves: BEFORE the page as served (?URL optics), the change a rule, AFTER the PocketRender render, the diff-hierarchy lens as the middle panel, every pr_* call reported per THE MCP RECEIPT RULE. FIRST READING, when the ride opens (after the first ticket): npx -y @mcp-b/webmcp-local-relay@latest --help by hand, never a ! line, because it downloads and runs a package and a human reads its output before a compile does; then the relay's transport, a stdio JSON-RPC lane in connectors/mcp.py (a subprocess, initialize then tools/list then tools/call over its stdin and stdout, each call a receipt); then the two Chrome flags on the botify uc profile (chrome://flags is a hand toggle that persists in the profile, or two switches on the argv, unread); then one pr_* tool called with '{}' for its schema, the AFTER panel of THE PROOF STRADDLE. READ 2026-09-28 (deed 1640, the --help by hand): the relay's usage names --host 127.0.0.1 and --port 9333 for a local WebSocket relay, --widget-origin for the page origins it admits, --invoke-timeout 65000 ms for a browser tool call and --max-payload 10 MB, so the WebSocket is the face toward the PocketRender tab; the face toward the client is unnamed and INFERRED stdio, because the client configs launch the relay as a command under an MCP-server key; npm 10.9.3 printed its own upgrade notice and no download line. connectors/mcp_render.py grew tools, schema and call at 1640 (one RelaySession: spawn with LD_LIBRARY_PATH cleared, initialize, notifications/initialized, then request by id on stdio, an MCP RECEIPT line per call, verdict tokens RELAY_INIT_OK, RELAY_TOOLS n=, RELAY_CALL_OK, RELAY_INIT_NO_REPLY, RELAY_TOOLS_NO_REPLY, RELAY_CALL_ERROR); the first hand run with no tab is the transport's witness (an initialize reply on stdout), the tab (the two chrome://flags, ?webmcp=1, logged in) the next hand step, both UNWITNESSED at this deed. READ 2026-09-28 (deed 1641, render tools by hand with no tab): RELAY_INIT_OK protocol=2025-06-18 server=webmcp-local-relay 0.0.0, then RELAY_TOOLS n=3, so stdio is OBSERVED and the n=0 forecast was wrong: the relay carries three tools of its own without a tab (webmcp_list_sources, webmcp_list_tools, webmcp_open_page, "Open a URL in the user's default browser") and the pr_* tools sit behind a connected source, reached through webmcp_list_tools or a tools/list that grows on connection, unread which; version 0.0.0 means serverInfo cannot name the pin. INFERRED, THE RELAY IS SHORT-LIVED: each render lane spawns its own relay and closes it within seconds while the tab connects to 127.0.0.1:9333 on its own schedule, so --settle N (deed 1641's car) sleeps after initialize to give it a window; a persistent relay in a second terminal would take the port and the tab with it, so none is run. The tab (the two chrome://flags in the everyday Chrome, ?webmcp=1, logged in) and render tools --settle 5, render call webmcp_list_sources, render call webmcp_list_tools are deed 1641's hand step. READ 2026-09-28 (deed 1642): no tab, the flag absent from Prime's Chrome 150 and per-profile toggles refused (THE BROWSER IS NOT A CONFIG FILE); render tools --settle 5 read n=3, webmcp_list_sources count 0, webmcp_list_tools count 0, by hand and in the compile lane alike (6.4 s a lane for the settle; the three lines leave context.txt, a ritual with no tab possible). The relay is proven and empty. Two WORA lanes, each a reading before a car: (a) the flake's pinned Chromium 150.0.7871.128 driven by the scraper with a WebMCP switch on its argv, if that build carries the feature at all (deed 1642's probe greps the unwrapped binary for the word; 0 hits means the lane waits on a nixpkgs Chromium bump); (b) PocketRender's own backend calls, read off the wire at a walk's CAPTURE fence after a human clicks Render (SETTLE in its non-trivial form), %URL naming the endpoint and its POST body, then a cookie-lane connector the way the admin door harvests (THE POCKETRENDER VERSE earmark is the stop). The pr_* tools are the vendor's convenience and not the only door; the deliverable's render columns stay UNRENDERED until one lane reads. READ 2026-09-28 (deed 1643, the binary): the flake's unwrapped chromium-150.0.7871.128 carries the word (17 lines; WebMCP 134, WebMCPHandler 18, WebMCPAgent 16, WebMCPAgentEE 13, WebMCPAgentENS 9 as the five commonest strings), so lane (a) is live: the build has the code, and the flag's absence from the everyday browser (xdg-settings names google-chrome.desktop, Google Chrome 150, the browser the relay's webmcp_open_page would open) says nothing about the flake's Chromium. The switch is READ off the binary before any argv is tried (deed 1644's probe: the strings past the fifth, the flag's own name enable-webmcp-testing, the LocalNetworkAccess strings for the second flag): --enable-features=<the feature's name> if it is a base::Feature, --enable-blink-features=<name> if a runtime-enabled one, --disable-features=<the local-network check's name> beside it, INFERRED forms until read. Then tools/scraper_tools.py rides before the car: a lane that opens PocketRender ?webmcp=1 on the botify profile (weblogin --profile botify) in the flake's Chromium with those switches, holds the window (a CAPTURE fence is the natural hold) and runs render tools --settle while it stands; the pr_* list is the gate. Never a hand toggle. READ 2026-09-28 (deed 1644, the strings past the fifth): WebMCPTestingEnabledEv and WebMCPTestingEnabledEb (2 each), WebMCPFormAssociatedCustomElementsEnabledEv and Eb, WebMCPSupport, WebMCPToolRegistration, and flag_name=1 (enable-webmcp-testing is in the binary once); the Ev/Eb pairs are INFERRED Blink RuntimeEnabledFeatures accessors (XEnabled() and SetXEnabled(bool), mangled), so the features are named WebMCPTesting, WebMCP and WebMCPFormAssociatedCustomElements and the candidate argv is --enable-blink-features=WebMCPTesting,WebMCP first, --enable-features=WebMCPTesting second if the flags entry wraps a base::Feature; the second flag's feature name is not in this census (the LocalNetworkAccess strings read are class names, LocalNetworkAccessPermissionRequired 16 the commonest), so --disable-features=LocalNetworkAccessChecks is INFERRED from the flag's own name and unread. The next reading is the scraper's: tools/scraper_tools.py in the payload, a lane opening PocketRender ?webmcp=1 on the botify profile with those switches and holding the window while render tools --settle reads; the pr_* list the gate.
 # - TODO (2026-09-27, the speed-dating queue; jira.py not in this compile): a board URL reduces to its project key and /boards/<id> is dropped without a word (jira.py's own comment at its URL parser, read 2026-09-23), so the SVB board-453 queue is unbuilt; the scope is board configuration -> filter.id -> /rest/api/3/filter/<id> for its JQL, then each issue's Project URL field to botify --rules org/project, reported as counts before any listing. Gates before the car: jira --check GREEN on Prime (the Mac's slot was cold), and connectors/jira.py in context. LANDED 2026-09-27 (5410ddbe, first flight at deed 1623): board 453 is "SVB board", type simple, filter 15051, JQL project = SVB ORDER BY Rank ASC, so the board is the whole project by rank and both documented paths answered 200. THE COUNTS LINE READ 0 with a Project URL among 100 issues while the field id resolved (no no-field note printed), and two worlds print that line alike: the wrong id behind a duplicate label (_project_url_field keeps the first match, SINGLE-CANDIDATE BLINDNESS) or the right id on the hundred lowest-ranked issues. The probes that separate them: every field whose name carries url, with ids; a JQL "Project URL" is not EMPTY count, server-side by name; the counts line at -n 500. No ticket pair can be listed until one row carries a slug with a local pull. READ 2026-09-27 (deed 1625, three CENSUS lines): the site carries TWO custom fields named Project URL, customfield_12188 and customfield_12189, so _project_url_field's first match was one of two, SINGLE-CANDIDATE BLINDNESS convicted by a census rather than by a second candidate arriving; the JQL "Project URL" is not EMPTY read rows=0 under a clean header, Jira having resolved the duplicate name to one field without saying which and never raising an ambiguity error; and -n 500 returned exactly 100 with no cap line, because the connector says "hit the -n cap" only when the page fills -n, so a server page smaller than -n reads as a complete list, THE FULL PAGE WAS SILENT in its second shape (the enhanced search's page is INFERRED at 100 from 500 asked, and its nextPageToken is never walked by design). Also: ORDER BY Rank ASC is the TOP of the board, so the hundred are the highest-ranked, not the lowest. Three worlds remain: the other id is the populated one; neither id is populated on the top hundred and the URL lives in the description; neither is populated anywhere. The falsifier is by id and never by name: cf[12188] is not EMPTY and cf[12189] is not EMPTY, each a count. The car, once a populated issue is known: _project_url_field returns every id under the label and list_board_issues reads whichever is filled per issue, and the counts line names the server's page size when the page came back smaller than -n. LANDED 2026-09-27 (deed 1626's Car 1, before a populated issue was known, because both ids read 0 and the text is the next candidate): _search_pages walks nextPageToken up to -n and returns the exhausted flag, _project_url_fields returns every id, a summary or description link stands in, and the counts carry a by-status line. Gate: jira board:453 -n 200 reads 162 issues, 2 pages, exhausted, Done 137, and the status loop reads the operator's four column counts. READ 2026-09-27 (deed 1627): 185 issues, 2 pages, exhausted, Done 158, To Do 15, In Review 11, In Progress 1, both lanes agreeing; the board's face said 162, so the gate's numbers were the face's and the server's are 23 higher, INFERRED epics and sub-tasks; the open rows moved 7 -> 27 across the paging; 1 with a link (SVB-258, coupang-org/tw.coupang.com) and 0 local pulls, so deed 1627's Car 1 joins by the Project-slug field and the summary's hostname against data/botify_pulls and prints a by-type line. Gate: the by-type line sums to 185 with Epic and Sub-task among its names, and the open rows carry -> host or -> org/project tails. READ 2026-09-27 (deed 1628): the by-type line sums to 185 (Rendering MiniRules 182, Epic 2, HTML Extract settings 1) with no Sub-task, so that half of the gate was a wrong forecast; every open row carries a tail, 25 from Project-slug and the 2 epics none, and the hostname fallback never fired (THE JOIN IS THE FIELD, the comment retitled at deed 1628). The queue by shape: 11 music.amazon.* tickets on one config family (the batch the operator said could not exist; one ride writes the shape the other ten reuse), 5 servicenow, and skechers, pluto, golfbreaks, zappos, monicavinader, coupang and hubspot one each. This line's job is done once the first ticket rides; the next forget ride fades it.
 # - TODO (2026-09-27, the first tree ride): annotate_tree_with_tokens in prompt_foo.py stamps a count by basename, so the three README.md lines in the File Tree (root, assets/sounds, connectors) all read connectors/README.md's 2,742 tokens; match on a path the tree's indentation can reconstruct, or stamp nothing on a basename that occurs twice. In the same car, the placeholder "generation failed or was skipped" is two worlds in one label: say which. One car when a ride owns it.
-# - TODO (2026-09-27, the m that answers on Prime; RULED by hand at the dismount: type m at a (nix) prompt read "m is aliased to" the configuration.nix line, and not-found in the compile lane by construction): inside the nix shell, `m` printed "❌ ai.py returned empty message" with no ", aborting.", which is configuration.nix's environment.shellAliases m and not flake.nix's m() function; bash expands an alias before it looks for a function of the same name, so on this machine the flake's git add -A, the router hint and the Error:* guard have never run under `m`, and the alias's label is false on a clean tree (git commit -am with nothing to commit prints "empty message"). One line either way: `unalias m` at the top of the flake's m() region, or delete the system alias; the Mac has no system alias and is unaffected. Gate: `type m` at a (nix) prompt on Prime reads "m is a function".
+# - TODO (2026-09-27, the m that answers on Prime; RULED by hand at the dismount: type m at a (nix) prompt read "m is aliased to" the configuration.nix line, and not-found in the compile lane by construction): inside the nix shell, `m` printed "❌ ai.py returned empty message" with no ", aborting.", which is configuration.nix's environment.shellAliases m and not flake.nix's m() function; bash expands an alias before it looks for a function of the same name, so on this machine the flake's git add -A, the router hint and the Error:* guard have never run under `m`, and the alias's label is false on a clean tree (git commit -am with nothing to commit prints "empty message"). One line either way: `unalias m` at the top of the flake's m() region, or delete the system alias; the Mac has no system alias and is unaffected. Gate: `type m` at a (nix) prompt on Prime reads "m is a function". SPECIMEN 2026-09-28 (deeds 1659-1660): d printed "m will stage these" over the new .agents/skills/journal/SKILL.md, m answered "ai.py returned empty message" with no ", aborting.", and git add by hand landed it; the d label is the flake's m() promising what Prime's alias does not do.
 # - TODO (2026-09-27, patch and app share a directory; convicted twice in one sitting): `patch` writes ./patch and `app` reads ./patch, both where you stand (flake.nix, SPLIT VERDICT), so `patch`, `n`, `app` read a stale ~/repos/nixos/patch from an earlier ride, printed six ALREADY APPLIED lines for one file, and the operator asked what he was missing. The tell was the wording: the stale run said "replacement block" six times for blogs.nix; this ride's blocks say "inserted block" for its three insertions. Cheapest cure is a reading, not a rule: `app` prints the patch file's directory and age on its first line before applying, so a days-old file names itself. flake.nix, one line, when a ride owns it. READ 2026-09-28 (deed 1641, twice by the operator's hand): app typed inside Workshop/corporate/ refused two Targets ("Target file not found") that exist at the repo root, and the operator cd'd back ("too much cd'ing around and having to know to p first"); Targets are root-relative by convention and apply.py resolved them from the cwd. Deed 1642's apply.py car changes to PIPULATE_ROOT when the shell exports it, printing one line when it moved, so app works from any folder and a WRITE_FILE typed elsewhere cannot create its path under that folder; the patch file's own location (./patch, the cwd) is still the flake's line above. Also swept: the patch file written in corporate/ was left untracked there, d showed it, m did not stage it, and Car 5's git add -A committed it as 68c3eb5 (INFERRED from the commit's existence; deed 1643's show --stat reads it); deed 1642's hand car removes it and ignores it with pipulate's own .gitignore line. READ 2026-09-28 (deed 1643): the sweep OBSERVED (68c3eb5 is one file, patch, 22 insertions, Car 4's two blocks) and 59cd1cb removed and ignored it (ls-files reads .gitignore alone); the /tmp straddle read Target file not found by hand and the resolved-from line then SEARCH block not found in 'foo_files.py' after, PIPULATE_ROOT 0 -> 3; pytest is not in the venv (No module named pytest on both taps), so tests/test_apply_airlock.py cannot run here and the /tmp straddle is the car's only witness. The patch file's own location stays the flake's line. READ 2026-09-28 (deed 1644): the root-relative Target witnessed live three times at 1643, app typed in Workshop/corporate printing the resolved-from line and landing SVB_PROMPT.md and svb.py at their root paths ("Yay I was able to apply this without changing back to the pipulate root"); d and m there see the corporate repo, which is right; the .venv spelling still needs the root (bash: .venv/bin/python: No such file or directory from the folder), cured by the word on PATH, p until then.
 # - TODO (2026-09-27, deliverable.html; ruling owed before a line is written): one standalone HTML page showing one PROOF STRADDLE, before above, diff between, after below, with a copy button that copies the five-slot markdown for a Jira or Slack paste; no external resource, inline CSS, the +/- coloring done by the writer in Python, JavaScript limited to navigator.clipboard.writeText. The compiler already holds two panels (the ! receipts are the AFTER, the git diff telemetry the middle) and the BEFORE is the same command lines in the previous cartridge, so the renderer is a pass over two consecutive cartridges pairing ! lines by command text. THE RULING: inside the zip it cannot name the zip's hash (the deed footer's fixed point) and carries the member digests and the previous deed's name instead; beside the zip as foo-<hash8>-NN.html it names the deed it renders and is a second file to attach. scripts/foo_cartridge.py either way. Gate: one page rendered from two real cartridges, opened with nothing installed, the copy button pasting into a ticket. RULED 2026-09-27: inside foo.zip, a fourth member beside manifest.json, prompt.md and payload.md ("expand the definition of the final zip deliverable"), so it names the previous deed and never its own; the Jekyll live-serve site it was going to ride is dropped; remark.js was weighed and declined, an external renderer for a page whose whole point is needing none, and the +/- classes are the writer's job in Python (html.escape, one class per line, inline CSS, one line of JavaScript for the copy); the markdown the button copies is the five-slot template, which Jira and Slack show as +/- lines without color, so the HTML is for eyes and the markdown for the ticket; and the specimen comes first: one real ticket handled with the template that already exists, its cartridge on disk, before the renderer is written against it. RULED 2026-09-27 (deed 1632, the dismount; the operator's gut, "keep it Markdown underneath ... so it's good to include as context in context.txt"): Markdown is the truth and the HTML is its render. deliverable.md is a file the walk writes (Workshop/corporate/tickets/<KEY>/deliverable.md) in a HOUSE SUBSET of Markdown -- front matter (ticket, org/project, deed, previous deed), #/##/### headings, paragraphs, - lists, | tables (the scorecard), fenced blocks with a language (diff for the unified diffs, text for receipts), inline code, links -- and nothing else; it rides context.txt, so it is already sealed inside payload.md. The fourth member, deliverable.html, is rendered from it at seal time by scripts/foo_cartridge.py with the standard library alone (the subset is small enough that the renderer is about a hundred lines: html.escape, one class per + and - diff line, inline CSS, the Markdown source embedded verbatim in a hidden pre that the one line of JavaScript copies), so one file serves eyes and the ticket paste, and a reader can recover the Markdown from the HTML. remark.js stays declined: a CDN script rendering at view time fails in a mail client, a Jira attachment preview and a script-blocked browser, and colors no diff without a highlighter; the gut was right about the substrate and wrong about the wrapper. Gate, added: the count of fenced diff blocks in deliverable.md equals the count of diff tables in deliverable.html, and python scripts/foo_cartridge.py on the zip lists four members.
 # - TODO (2026-09-27, the confabulation ledger; SQLite or a text file, undecided by the operator): the axis-ledger precedent rules it, a tracked JSONL appended by hand with no generator, because a ledger a cartridge cannot carry cannot be forwarded. One line per catch: date, the deed holding the AFTER, the claim verbatim in one sentence, the probe, before, after, the engine named. Count is line count; forwarding is the line plus the deed name; an entry without its straddle is a voice recording of someone saying they caught something, so the straddle is the admission ticket. Gate: assets/confabulation_ledger.jsonl with its first record, one this system caught.
(nix) pipulate $ m
📝 Committing: fix: drop unused code and resolve misdirection
[main a2351d42] fix: drop unused code and resolve misdirection
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
(nix) pipulate $ d
diff --git a/foo_files.py b/foo_files.py
index a8a3bea6..1523ea8f 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -2623,7 +2623,7 @@ MATCHBOOK_CHOP = r"""
 # - TODO (2026-09-27, the qamy.ai door; the operator's ruling at the dismount: today): the domain names the method (QA My AI, the proof straddle's public face) and serves one page until it says more. The door is the npvg.org door with one token changed: a virtualHosts entry in remotes/honeybot/nixos/configuration.nix (this repo, chapter IX) beside npvg.org, HTTP-only until the name resolves, then forceSSL plus enableACME on the shared acme-challenge webroot (THE ACME CAR), and the installer's sub_filter stamp only if the installer is to be served there (THE DOOR NAMES THE FOLDER: its own placeholder, never a hostname rewrite); the body a page of its own under remotes/honeybot/www/qamy.ai, rsynced by nixops.sh, no rebuild for a body-only change and one rebuild on Honeybot for the vhost. THE ONE STEP NO REPO MAKES comes first, by hand, because propagation is the slow part: an A record at the registrar to the address npvg.org already carries (dig +short npvg.org is the value; dig +short qamy.ai empty is the BEFORE, the address is the AFTER), TTL 300, and it joins the 2026-09-13 debt, a third static A record on one public address with the DDNS heartbeat on mikelev.in alone, and the favicon 404 line. Order: the record now; the vhost car with configuration.nix and nixops.sh in the payload; the deploy; the cellular witness with a tag minted in the caboose (THE OUT-OF-BAND STEP RULE). Gate: curl -sI https://qamy.ai from outside reads 200 with a Let's Encrypt issuer, and the tag sits in its access log. A RECORD MADE 2026-09-27 (the operator's paste at deed 1626: A at the apex, TTL 1 min, a www CNAME to the apex): dig +short qamy.ai reads the public address from inside the LAN while dig +short npvg.org reads the LAN address, so the house's split DNS carries no qamy.ai line and a browser inside the house will hairpin or fail until the resolver that answers npvg.org internally gets the same line for qamy.ai (where that resolver lives is unread: the router, or a hosts entry in the NixOS config; it rides the vhost car's compile). THE RECEIPT CARRIED THE ADDRESS BARE: two ! dig lines seeded at deed 1625 put the home's public address into a trusted payload the operator's own paste had redacted; a pii_substitutions.txt line for it is the HALT of deed 1626, and every DNS probe since prints a verdict word, never the value. READ 2026-09-27 (deed 1627): from 1.1.1.1 the answer for qamy.ai differs from mikelev.in's or is empty; three worlds print that word (a negative answer cached before the record existed; a value that is not npvg.org's; npvg.org's static A drifted off mikelev.in's DDNS address, the 2026-09-13 debt come due, with qamy.ai a copy of the stale number and the pad dark from outside), and four verdict probes ride deed 1628 to split them: the answer count at 1.1.1.1 and at the zone's own nameserver, qamy.ai against npvg.org, npvg.org against mikelev.in. READ 2026-09-27 (deed 1628): qamy_public_answers=1 and qamy_authoritative_answers=1 via dns1.registrar-servers.com, so the record exists and 1.1.1.1 holds it; qamy_matches_npvg_public; npvg_differs_from_mikelevin_public. On 2026-09-13 the ACME challenge for npvg.org reached the pad, so the two names agreed then; the DDNS unit moves mikelev.in and nothing moves npvg.org, and outside traffic has reached mikelev.in since the 2026-09-24 outage (the hydration count moved), so the reading is npvg.org stale since the WAN address changed, qamy.ai a copy of the stale number, the pad dark from outside and npvg.org's December renewal set to fail, INFERRED until the egress probe of deed 1629 prints mikelevin_current_npvg_and_qamy_stale. Deed 1627's caboose pre-committed a HALT to a word two worlds print (THE CABOOSE-VERDICT COROLLARY), so the registrar step rides deed 1629's HALT with one action once the word is read; the standing fix is the DDNS unit covering all three names, the 2026-09-13 line, a services.nix car. READ 2026-09-27 (deed 1629): mikelevin_current_npvg_and_qamy_stale, the forecast word; the operator set npvg.org's and qamy.ai's A records at the registrar by hand before the reply landed (the loss of power to the building changed the WAN address and only mikelev.in's DDNS followed it), so no HALT rode and the AFTER is the same probe reading all_three_current once the TTLs pass (qamy.ai one minute, npvg.org's unread). The address rode this article bare in the operator's own paste and appears in no probe and no reply; the pii_substitutions.txt line covers the publish lane, and under --profile trusted substitutions are OFF by design (deed 1627's terminal: substitutions=OFF), so the HALT of deed 1626 named the wrong lane for a bjj payload, a miss of mine, and verdict words were the right cure. Still owed: the DDNS unit for all three names, then the vhost car. READ 2026-09-27 (deed 1630): all_three_current.
 # - TODO (2026-09-27, the word bjj; flake.nix not in this compile): the operator's compile spelling on Prime is compile --profile trusted --reason "testing" --tree and wants to be one word, "because Brazilian Jiu Jitsu is objectively the best Kung Fu". The car, when flake.nix rides, one line beside ahc: bjj() { (cd "$PIPULATE_ROOT" && python prompt_foo.py --chop ADHOC_CHOP --profile trusted --reason "bjj: a trusted compile from the workbench" "$@"); } -- --tree is not spelled because the flip of 0bce3aae made it a no-op there. Ignition exit then ndq. Gate: bjj compiles with the Summary's Command line reading --profile trusted and the identity-scrub line printing, and type bjj at a (nix) prompt reads "bjj is a function". CAR EMITTED 2026-09-27 (deed 1623, flake.nix in that payload): the line beside ahcu, ignition exit then ndq; the gate is the next compile typed bjj. LANDED 2026-09-27 (266b5e18; deed 1625 was compiled by bjj: the Summary's Command line reads --profile trusted --reason bjj: a trusted compile from the workbench, the ignition witnessed by the compiler's own argv, and the flake grep read 0 -> 1). THE HOOK PROBE WENT BLIND in the same compile: LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook refused in the compile lane with "experimental Nix feature 'dynamic-derivations' is disabled", exit 1, stdout 0, where the same evaluation read adhoc=2 notree=0 at deed 1620 and 0 by hand at this deed's BEFORE; the shell (exit then ndq) and the flake (one bjj line) both moved between those readings, and by hand the word nix is the flake's shim function while in the ! executor it is the PATH binary, so the cause is unread. A 0 from that probe is VOID, never a count, until command -v nix, nix --version and the eval's own stderr ride a compile. The next forget ride fades the landed half. READ 2026-09-27 (deed 1626): command -v nix is /home/mike/.nix-profile/bin/nix, version 2.25.0pre20240910, a two-year-old prerelease in the user profile that shadows the system nix in the compile lane while the hand lane's nix is the flake's shim; the eval exited 0 after 11 s in one run and printed the dynamic-derivations line on stderr in the next run 0.2 s later, two readings from two runs and neither carrying both. One probe reading exit, stdout bytes, the bjj count and the stderr head in a single run rides deed 1627, with type nix in each lane and the eval-cache listing; a ~/.cache/nix shared between two nix versions is the named suspect, UNREAD. READ 2026-09-27 (deed 1627), the lanes swapped: by hand exit 1, bytes 0, the dynamic-derivations line; in the compile exit 0, 77,110 bytes, bjj=1, stderr empty, 11 s; the compile's nix is ~/.nix-profile/bin/nix and the hand's is the flake's function; the cache dir holds eval-cache-v5 and v6. THE CACHE HIT IS THE REFUSAL, INFERRED from every reading so far: each 11 s run succeeded and each 0.2 s run refused, and every fast run was the second evaluation of one flake fingerprint (1624 then 1625, 1626's probe 3 then 4, 1626's compile then 1627's hand BEFORE), so nix 2.25.0pre20240910 evaluates the hook and trips reading it back from its own eval cache. The falsifier rides deed 1628: two runs in one probe on a fresh tree (forecast success then refusal) beside two --no-eval-cache runs (forecast success twice), and each lane's nix --version. If it holds, the probe's cure is --no-eval-cache and the box's cure is retiring the prerelease from ~/.nix-profile so one nix serves both lanes, a hand step for its own line. READ 2026-09-27 (deed 1628), the forecast shape exactly: by hand both cached runs refused (the 1627 compile had filled the cache for that tree), in the compile cached_run1 exit 0 at 77,110 bytes and cached_run2 refused, and the uncached pair exit 0 twice in both lanes. THE CACHE HIT IS THE REFUSAL, confirmed; --no-eval-cache is the hook probe's spelling from here. Both lanes run the prerelease (nix --version reads 2.25.0pre20240910 by hand and in the compile, so the flake's function wraps the PATH binary) while /run/current-system/sw/bin/nix reads 2.28.5, so retiring ~/.nix-profile's nix lets the system's serve both lanes; the profile's kind (manifest.json is nix profile, manifest.nix is nix-env) rides deed 1629 and names the removal command, a hand step. READ 2026-09-27 (deed 1629): ~/.nix-profile carries manifest.nix, a nix-env profile, so the removal is LD_LIBRARY_PATH="" nix-env -e nix and its undo LD_LIBRARY_PATH="" nix-env --rollback; it waits one turn, because whether the flake's nix() function wraps the PATH binary or names the profile's path is unread and flake.nix rides deed 1630 for the rename, so the step follows that reading; nix-env -q rides the compile as its BEFORE. Gate: which nix reads /run/current-system/sw/bin/nix in the compile lane, nix --version 2.28.5 in both, and the --no-eval-cache hook eval exit 0 at 77,110 bytes under the system nix. READ 2026-09-27 (deed 1630): the flake's nix() calls command nix, the PATH binary, so retiring the profile's nix moves both lanes to /run/current-system/sw/bin/nix at the next lookup (hash -r in the live shell); the step rides deed 1630's last fence after the rename's ignition, so the two changes are read by different probes (which nix and nix --version for the retirement; the rg census for the rename), with the hook eval the one shared instrument and its BEFORE 77,110 under the prerelease. Undo: LD_LIBRARY_PATH="" nix-env --rollback. LANDED 2026-09-27 (deed 1630's Car 10, by hand after the ignition; READ at deed 1631): nix-env -e nix uninstalled nix-2.25.0pre20240910_b9d3cdfb; command -v nix reads /run/current-system/sw/bin/nix in the compile lane (by hand it prints the bare word nix, the flake's function, whose command nix now finds the system binary), nix --version 2.28.5 in both lanes, nix-env -q appimage-run and ungoogled-chromium with no nix, and the --no-eval-cache hook eval exit 0 at 77,416 bytes under the system nix (77,110 plus the rename's bytes). "hash: hashing disabled" on the hand step is the nix shell's bash with hashing off, harmless. The dynamic-derivations refusal cannot recur on this box; --no-eval-cache stays in the probe's spelling as a habit. This TODO's job is done; the next forget ride fades it.
 # - TODO (2026-09-27, command stops in a walk; asked after the corpus ride, RESTATED and not ridden): today a stop is a page (url or url_env) the rider opens, and a stop's connector is argv the planner validates and nothing runs (the AUTH RULING, chapter VIII-b). The ask: a third stop kind whose payload is a COMMAND the operator would otherwise paste, e.g. a three-stop "SpeedWorkers corpus" trail of botify --sw, botify --pull-configs and botify --rules, where the rider speaks the guidance, prints the command, waits for the word (the fence), runs it in the operator's own terminal with stdout as the receipt, and advances; a Chrome the command opens is the command's own (the census harvests its cookie) and never the rider's. Shape: exactly one of url, url_env or command per stop, walk.py's set-difference validation extended, walk_compile.py and the sealer carrying the new key, and the receipt banked the way captures.md banks bytes. The Workshop tree the operator drew rides with it: corporate/ a read-only lowerdir (the org's AGENTS.md, .agents/skills, log.md), shared/<name>/ one writer each with its own AGENTS.md (nearest ancestor wins inside the namespace) and skills overlaid on corporate's, personal/ gitignored and unreachable by any git verb; the mechanism (rider, compiler, connectors) stays in Pipulate and the data (trails, exports, client slugs, and the admin door's shapes that sit in botify.py today, no scrub owed) moves to private repos mounted into that tree, the blogs.nix -> blogs.json split applied to skills. Not this weekend. Gate: one command-stop trail ridden end to end with its three receipts in one compile; needs scripts/walk.py, scripts/mother_cat.py and scripts/walk_compile.py in the payload.
-# - TODO (2026-09-27, AGENTS.md and AI_CONTEXT.md must name the specification; the operator's words, its own ride): a stranger's model, or an IT department come to forbid it, reading AGENTS.md or AI_CONTEXT.md must see on the first screen that this repository is an Agent Skill under https://agentskills.io/specification (a SKILL.md folder with YAML frontmatter and progressive disclosure, AGENTS.md as the nearest-ancestor signpost; Notebooks/.agents/skills already conforms, THREE-STANDARDS SUPERPOSITION), and that every tool call it teaches is forced onto localhost under the fifty-year POSIX conventions (a command, stdin, stdout, an exit code) so that it lands in the flight data recorder: a cloud tool call that cannot be reconstructed locally is non-reproducible, non-portable and cannot be compiled into a cartridge, and is out of scope by that rule and not by taste. The ride needs AGENTS.md, AI_CONTEXT.md and !https://agentskills.io/specification in the payload, the specification read before a word is written and the wording checked against it. Gate: a fresh chat handed AGENTS.md alone names the specification and the localhost rule in its first two sentences. THE ORDER, ruled at the 2026-09-27 dismount: after the first ticket, the deliverable and the render farm, and before the Workshop tree; with that tree landed under Notebooks (the command-stop TODO), the repository is the worked example the specification's readers can run on localhost, the operator's stated aim, "the quintessential and prototypical example".
+# - TODO (2026-09-27, AGENTS.md and AI_CONTEXT.md must name the specification; the operator's words, its own ride): a stranger's model, or an IT department come to forbid it, reading AGENTS.md or AI_CONTEXT.md must see on the first screen that this repository is an Agent Skill under https://agentskills.io/specification (a SKILL.md folder with YAML frontmatter and progressive disclosure, AGENTS.md as the nearest-ancestor signpost; Notebooks/.agents/skills already conforms, THREE-STANDARDS SUPERPOSITION), and that every tool call it teaches is forced onto localhost under the fifty-year POSIX conventions (a command, stdin, stdout, an exit code) so that it lands in the flight data recorder: a cloud tool call that cannot be reconstructed locally is non-reproducible, non-portable and cannot be compiled into a cartridge, and is out of scope by that rule and not by taste. The ride needs AGENTS.md, AI_CONTEXT.md and !https://agentskills.io/specification in the payload, the specification read before a word is written and the wording checked against it. Gate: a fresh chat handed AGENTS.md alone names the specification and the localhost rule in its first two sentences. THE ORDER, ruled at the 2026-09-27 dismount: after the first ticket, the deliverable and the render farm, and before the Workshop tree; with that tree landed under Notebooks (the command-stop TODO), the repository is the worked example the specification's readers can run on localhost, the operator's stated aim, "the quintessential and prototypical example". READ 2026-09-28 (deed 1661): the URL rides AGENTS.md's Skills line since daed3d78, half the gate; the localhost sentence is still owed, worded after the specification is read into a payload, and it rides the install skill's second-reading turn.
 # - TODO (2026-09-27, THE RENDER FARM HAS A NAME; from a colleague's Slack note the same day): PocketRender opened with ?webmcp=1 (app.botify.com/tools/cpap/pocketrender/index.html) exposes pr_* tools to any MCP client through a stdio relay, npx -y @mcp-b/webmcp-local-relay@latest --widget-origin https://app.botify.com (configs given for Claude Desktop, Claude Code, Cursor and Codex; a locally served PR adds http://localhost:8642 to the origin list, comma-separated), with the Chrome flags enable-webmcp-testing on and local-network-access-check off and a logged-in PR tab open. Three gaps before Pipulate drives it, each a reading before a car: connectors/mcp.py speaks Streamable HTTP and the relay speaks stdio, so a subprocess JSON-RPC lane is owed (or the relay's HTTP port, if it has one, unread); node and npx are in neither commonPackages nor the flake, and read node=present npx=present on Prime (2026-09-27 afternoon compile, deed 1620), INFERRED from the system's packages.nix since flake.nix lists no nodejs, so the Mac and a stranger's install read absent until commonPackages carries it; the two flags on the persistent uc profile are unread (a Chromium switch, or one hand toggle in the botify profile). The PROOF STRADDLE it serves: BEFORE the page as served (?URL optics), the change a rule, AFTER the PocketRender render, the diff-hierarchy lens as the middle panel, every pr_* call reported per THE MCP RECEIPT RULE. FIRST READING, when the ride opens (after the first ticket): npx -y @mcp-b/webmcp-local-relay@latest --help by hand, never a ! line, because it downloads and runs a package and a human reads its output before a compile does; then the relay's transport, a stdio JSON-RPC lane in connectors/mcp.py (a subprocess, initialize then tools/list then tools/call over its stdin and stdout, each call a receipt); then the two Chrome flags on the botify uc profile (chrome://flags is a hand toggle that persists in the profile, or two switches on the argv, unread); then one pr_* tool called with '{}' for its schema, the AFTER panel of THE PROOF STRADDLE. READ 2026-09-28 (deed 1640, the --help by hand): the relay's usage names --host 127.0.0.1 and --port 9333 for a local WebSocket relay, --widget-origin for the page origins it admits, --invoke-timeout 65000 ms for a browser tool call and --max-payload 10 MB, so the WebSocket is the face toward the PocketRender tab; the face toward the client is unnamed and INFERRED stdio, because the client configs launch the relay as a command under an MCP-server key; npm 10.9.3 printed its own upgrade notice and no download line. connectors/mcp_render.py grew tools, schema and call at 1640 (one RelaySession: spawn with LD_LIBRARY_PATH cleared, initialize, notifications/initialized, then request by id on stdio, an MCP RECEIPT line per call, verdict tokens RELAY_INIT_OK, RELAY_TOOLS n=, RELAY_CALL_OK, RELAY_INIT_NO_REPLY, RELAY_TOOLS_NO_REPLY, RELAY_CALL_ERROR); the first hand run with no tab is the transport's witness (an initialize reply on stdout), the tab (the two chrome://flags, ?webmcp=1, logged in) the next hand step, both UNWITNESSED at this deed. READ 2026-09-28 (deed 1641, render tools by hand with no tab): RELAY_INIT_OK protocol=2025-06-18 server=webmcp-local-relay 0.0.0, then RELAY_TOOLS n=3, so stdio is OBSERVED and the n=0 forecast was wrong: the relay carries three tools of its own without a tab (webmcp_list_sources, webmcp_list_tools, webmcp_open_page, "Open a URL in the user's default browser") and the pr_* tools sit behind a connected source, reached through webmcp_list_tools or a tools/list that grows on connection, unread which; version 0.0.0 means serverInfo cannot name the pin. INFERRED, THE RELAY IS SHORT-LIVED: each render lane spawns its own relay and closes it within seconds while the tab connects to 127.0.0.1:9333 on its own schedule, so --settle N (deed 1641's car) sleeps after initialize to give it a window; a persistent relay in a second terminal would take the port and the tab with it, so none is run. The tab (the two chrome://flags in the everyday Chrome, ?webmcp=1, logged in) and render tools --settle 5, render call webmcp_list_sources, render call webmcp_list_tools are deed 1641's hand step. READ 2026-09-28 (deed 1642): no tab, the flag absent from Prime's Chrome 150 and per-profile toggles refused (THE BROWSER IS NOT A CONFIG FILE); render tools --settle 5 read n=3, webmcp_list_sources count 0, webmcp_list_tools count 0, by hand and in the compile lane alike (6.4 s a lane for the settle; the three lines leave context.txt, a ritual with no tab possible). The relay is proven and empty. Two WORA lanes, each a reading before a car: (a) the flake's pinned Chromium 150.0.7871.128 driven by the scraper with a WebMCP switch on its argv, if that build carries the feature at all (deed 1642's probe greps the unwrapped binary for the word; 0 hits means the lane waits on a nixpkgs Chromium bump); (b) PocketRender's own backend calls, read off the wire at a walk's CAPTURE fence after a human clicks Render (SETTLE in its non-trivial form), %URL naming the endpoint and its POST body, then a cookie-lane connector the way the admin door harvests (THE POCKETRENDER VERSE earmark is the stop). The pr_* tools are the vendor's convenience and not the only door; the deliverable's render columns stay UNRENDERED until one lane reads. READ 2026-09-28 (deed 1643, the binary): the flake's unwrapped chromium-150.0.7871.128 carries the word (17 lines; WebMCP 134, WebMCPHandler 18, WebMCPAgent 16, WebMCPAgentEE 13, WebMCPAgentENS 9 as the five commonest strings), so lane (a) is live: the build has the code, and the flag's absence from the everyday browser (xdg-settings names google-chrome.desktop, Google Chrome 150, the browser the relay's webmcp_open_page would open) says nothing about the flake's Chromium. The switch is READ off the binary before any argv is tried (deed 1644's probe: the strings past the fifth, the flag's own name enable-webmcp-testing, the LocalNetworkAccess strings for the second flag): --enable-features=<the feature's name> if it is a base::Feature, --enable-blink-features=<name> if a runtime-enabled one, --disable-features=<the local-network check's name> beside it, INFERRED forms until read. Then tools/scraper_tools.py rides before the car: a lane that opens PocketRender ?webmcp=1 on the botify profile (weblogin --profile botify) in the flake's Chromium with those switches, holds the window (a CAPTURE fence is the natural hold) and runs render tools --settle while it stands; the pr_* list is the gate. Never a hand toggle. READ 2026-09-28 (deed 1644, the strings past the fifth): WebMCPTestingEnabledEv and WebMCPTestingEnabledEb (2 each), WebMCPFormAssociatedCustomElementsEnabledEv and Eb, WebMCPSupport, WebMCPToolRegistration, and flag_name=1 (enable-webmcp-testing is in the binary once); the Ev/Eb pairs are INFERRED Blink RuntimeEnabledFeatures accessors (XEnabled() and SetXEnabled(bool), mangled), so the features are named WebMCPTesting, WebMCP and WebMCPFormAssociatedCustomElements and the candidate argv is --enable-blink-features=WebMCPTesting,WebMCP first, --enable-features=WebMCPTesting second if the flags entry wraps a base::Feature; the second flag's feature name is not in this census (the LocalNetworkAccess strings read are class names, LocalNetworkAccessPermissionRequired 16 the commonest), so --disable-features=LocalNetworkAccessChecks is INFERRED from the flag's own name and unread. The next reading is the scraper's: tools/scraper_tools.py in the payload, a lane opening PocketRender ?webmcp=1 on the botify profile with those switches and holding the window while render tools --settle reads; the pr_* list the gate.
 # - TODO (2026-09-27, the speed-dating queue; jira.py not in this compile): a board URL reduces to its project key and /boards/<id> is dropped without a word (jira.py's own comment at its URL parser, read 2026-09-23), so the SVB board-453 queue is unbuilt; the scope is board configuration -> filter.id -> /rest/api/3/filter/<id> for its JQL, then each issue's Project URL field to botify --rules org/project, reported as counts before any listing. Gates before the car: jira --check GREEN on Prime (the Mac's slot was cold), and connectors/jira.py in context. LANDED 2026-09-27 (5410ddbe, first flight at deed 1623): board 453 is "SVB board", type simple, filter 15051, JQL project = SVB ORDER BY Rank ASC, so the board is the whole project by rank and both documented paths answered 200. THE COUNTS LINE READ 0 with a Project URL among 100 issues while the field id resolved (no no-field note printed), and two worlds print that line alike: the wrong id behind a duplicate label (_project_url_field keeps the first match, SINGLE-CANDIDATE BLINDNESS) or the right id on the hundred lowest-ranked issues. The probes that separate them: every field whose name carries url, with ids; a JQL "Project URL" is not EMPTY count, server-side by name; the counts line at -n 500. No ticket pair can be listed until one row carries a slug with a local pull. READ 2026-09-27 (deed 1625, three CENSUS lines): the site carries TWO custom fields named Project URL, customfield_12188 and customfield_12189, so _project_url_field's first match was one of two, SINGLE-CANDIDATE BLINDNESS convicted by a census rather than by a second candidate arriving; the JQL "Project URL" is not EMPTY read rows=0 under a clean header, Jira having resolved the duplicate name to one field without saying which and never raising an ambiguity error; and -n 500 returned exactly 100 with no cap line, because the connector says "hit the -n cap" only when the page fills -n, so a server page smaller than -n reads as a complete list, THE FULL PAGE WAS SILENT in its second shape (the enhanced search's page is INFERRED at 100 from 500 asked, and its nextPageToken is never walked by design). Also: ORDER BY Rank ASC is the TOP of the board, so the hundred are the highest-ranked, not the lowest. Three worlds remain: the other id is the populated one; neither id is populated on the top hundred and the URL lives in the description; neither is populated anywhere. The falsifier is by id and never by name: cf[12188] is not EMPTY and cf[12189] is not EMPTY, each a count. The car, once a populated issue is known: _project_url_field returns every id under the label and list_board_issues reads whichever is filled per issue, and the counts line names the server's page size when the page came back smaller than -n. LANDED 2026-09-27 (deed 1626's Car 1, before a populated issue was known, because both ids read 0 and the text is the next candidate): _search_pages walks nextPageToken up to -n and returns the exhausted flag, _project_url_fields returns every id, a summary or description link stands in, and the counts carry a by-status line. Gate: jira board:453 -n 200 reads 162 issues, 2 pages, exhausted, Done 137, and the status loop reads the operator's four column counts. READ 2026-09-27 (deed 1627): 185 issues, 2 pages, exhausted, Done 158, To Do 15, In Review 11, In Progress 1, both lanes agreeing; the board's face said 162, so the gate's numbers were the face's and the server's are 23 higher, INFERRED epics and sub-tasks; the open rows moved 7 -> 27 across the paging; 1 with a link (SVB-258, coupang-org/tw.coupang.com) and 0 local pulls, so deed 1627's Car 1 joins by the Project-slug field and the summary's hostname against data/botify_pulls and prints a by-type line. Gate: the by-type line sums to 185 with Epic and Sub-task among its names, and the open rows carry -> host or -> org/project tails. READ 2026-09-27 (deed 1628): the by-type line sums to 185 (Rendering MiniRules 182, Epic 2, HTML Extract settings 1) with no Sub-task, so that half of the gate was a wrong forecast; every open row carries a tail, 25 from Project-slug and the 2 epics none, and the hostname fallback never fired (THE JOIN IS THE FIELD, the comment retitled at deed 1628). The queue by shape: 11 music.amazon.* tickets on one config family (the batch the operator said could not exist; one ride writes the shape the other ten reuse), 5 servicenow, and skechers, pluto, golfbreaks, zappos, monicavinader, coupang and hubspot one each. This line's job is done once the first ticket rides; the next forget ride fades it.
 # - TODO (2026-09-27, the first tree ride): annotate_tree_with_tokens in prompt_foo.py stamps a count by basename, so the three README.md lines in the File Tree (root, assets/sounds, connectors) all read connectors/README.md's 2,742 tokens; match on a path the tree's indentation can reconstruct, or stamp nothing on a basename that occurs twice. In the same car, the placeholder "generation failed or was skipped" is two worlds in one label: say which. One car when a ride owns it.
(nix) pipulate $ m
📝 Committing: fix: address dynamic-derivations refusal in nix-env
[main 0baa8eb3] fix: address dynamic-derivations refusal in nix-env
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
(nix) pipulate $ d
diff --git a/foo_files.py b/foo_files.py
index 1523ea8f..0d6e0a4f 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -2586,6 +2586,8 @@ MATCHBOOK_CHOP = r"""
 ! .venv/bin/python -c 'import re;t=open("GLOSSARY.md",encoding="utf-8").read();[print(h,"—",re.sub(r"\s+"," ",p)) for h,p in re.findall(r"^- \*\*([^*]+)\*\* — \*(.+?)\*",t,flags=re.M|re.S)]'
 """
 # #todo #to-do #earmarks
+# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST; read at deed 1661): MANIFEST.in's include and graft reach the sdist (114 members, .agents/ and the two root files in) and never the wheel (81 members, LICENSE alone), and pip install fetches the wheel, so a plain install still carries no AGENTS.md; pyproject's package-data block is a no-op by receipt (none of its five names in either artifact, and CHANGELOG.md names a file the tree does not hold). Ruling owed on whether the agent-facing files should ride a package directory or whether PyPI's rendered README is the wheel user's whole door; cut the dead block after a census of whether cli.py's pipulate install needs a bundled install.sh.
+# - TODO (2026-09-28, the skills' remaining readers): imports/ascii_displays.py 1938, 1945, 1972-73 spell the underscore names in comments (a reseal if they sit in sealed art); ~/repos/Pipulate.com/README.md:3 reads "And context AI_CONTEXT.md", true and pointable at the journal skill; the Claude Code check (six names behind / in a session in this repo, a .claude/skills symlink if not) is unrun; .agents/skills/pipulate/SKILL.md was written at deed 1661 from install.sh and flake.nix alone, and the npvg.org door page, README's Quick Start and AUDIT.md's first-run section are read against it before 2.67 ships it.
 # - TODO (2026-09-28, THE CHEAP FORGETTING; ruled by reading at deed 1655, unridden): forgetting through SEARCH/REPLACE recites the thing forgotten, 63 lines byte-exact for THE FINDING DORY RULE at deed 1654 with one wrong character refunding only the turn, while the two sed cars of the same ride cost a pattern each and landed the same way; the cheap shape names its victim and never recites it: the § key written to a file by a quoted heredoc (a single-quoted delimiter, so nothing expands), one gated sed that reads that file in at the header line and deletes the body by range (the range gated on the header line and the next header line, the trailing blank trimmed per THE SUBSET REPLACE, GO or STOP with the readings beside the word), and the body moved into GLOSSARY.md by a script off the same range (the '# ' prefix rewritten to the glossary's two-space indent, the '- **Handle** — *plain term.*' head the one thing typed), so no byte of the body rides the reply, the way commit 7dd5d832's block moved on 2026-09-26. First rides, each waiting on the operator's yes: THE DIVIDER IS NOT OPTIONAL (2,547 bytes whole by the tightened ranker's count, no bare marker in its body by its own design, its checklist line a prompt_foo.py ride) and the specimen fade by shape (32 paragraphs and 15,923 bytes under two TODOs down to eight lines, one specimen per shape: the sign read backwards, the context row read as the change, one subject for two commits, an earlier subject reused for a router-only bank, a file named that the commit never touched or does not exist, the diff answered as a document, the run-on or comment-carrying subject, the type word wrong). Gate: one graduation lands with its § key in the router, its body in GLOSSARY.md and no body line in the reply.
 # - TOTO (2026-09-28), Endure that the `grim --web` command has an `article` and `bot` equivalent. What's more, make sure those commands are derived from `blogs.nix` and not hard-coded in `flake.nix`.
 # - TODO (2026-09-28, THE ARTICLE LARGER THAN A MINUTE; read at deed 1647 off the grim run): a 915,721-char article estimated 228,930 tokens at chars/4 and the API refused both models at generate_content_free_tier_input_token_count, limit 250000, then the loop waited 57 s for a minute the request could never fit in; the estimate now divides by 3.5, a quota on the input-token metric whose limit is below the estimate stops the run with a SIZE verdict, and the publish words forward their arguments to the editing script alone. The escape today is a key named by hand: a paid Gemini key (billing on one project lifts the minute into the millions on the same tokenizer and a million-token window; a Flash-class price makes such an article cents), or another provider through --model with -k, the wallet's flat alias-to-secret shape unchanged; the window of any other provider is checked against the size line before trusting it. The creative solution for a free key, unbuilt: a map-reduce lane for the editing pass, the article cut at paragraph seams into pieces under the minute, each piece asked for one or two subheadings with a verbatim after_text_snippet and a three-sentence summary, then the template run once on the head plus the summaries for the frontmatter and the analysis, the subheadings merged in article order; N+1 requests against a day of twenty, so a 900K article costs eight; or the cheaper lossy twin, --trim with the middle elided and subheadings for the ends only. Needs editing_prompt.txt and contextualizer.py (the sibling ring) in the payload. Gate for either: one oversized article published on a free key with the spine, the subheadings and the collision guard intact. RULED 2026-09-28 (deed 1648; the Workspace key read free_tier too, and the operator: "not gonna pay", "I don't think chunking is good"): the fences leave first. Past the free minute, or on --lean, every fenced block leaves the editor's copy for a one-line bracketed stand-in naming its language and line count, an editor's note closes the copy, and the prompt is rebuilt and re-read; the post is built from the original article_text, so every snippet the model quotes from prose still matches, and the note forbids quoting a stand-in. The spine lands in the template before the article, so an article quoting the placeholder is left alone. Map-reduce and --trim are dropped. b2 sweeps the wallet through backup-home.py's Key Configs include (pipulate at line 63; the stale articleizer entry beside it is harmless). Gate: the fence probe reads the lean copy under 250,000 on the specimen, and one oversized article publishes on a free key with its subheadings placed. READ 2026-09-28 (deed 1650): the lean copy read 87 fences, 894,222 chars to 106,111, about 30,317 tokens, and grim's scissors line 261,634 tokens to 36,527 on a 127,846-char prompt; then the API failed three more ways: "high demand" on Lite every time at any size, so it was never the size; "The service is currently unavailable" with no status code, read as unrecoverable; and a ring of 26 that never turned, its rule wanting quota from every model while Lite only ever said high demand, the operator cutting it at the fourth wait. The same metric, free_tier_requests, carried limit 5 (the minute) and limit 20 (the day). RULED (the operator: "make it give really good instructions on how to do this in the Web UI, can we make it super simple?", "anti-fragility, always another way?"): THE WEB LANE. Every exit that leaves no instructions (unrecoverable, exhausted, SIZE, a wait cut by hand, or --web on purpose) puts the prompt on the clipboard and prints four steps: paste into any AI chat in a browser, copy the whole reply, press Enter; the JSON is read off the clipboard, refused if it is the prompt's own schema pasted back, cached, and the post built by the same code as after an API answer, article.txt untouched, which the old --copy then --local lane could not promise because the publish word rewrites article.txt from the clipboard first. The ring turns once a quota is reported on a key and every model on it has failed, or once its ten attempts are spent, and leaves the last key the same way; unavailable and overloaded are transient; one parser serves both lanes. Gate: one publish end to end through the web lane, and one ring turn witnessed on -m all.
(nix) pipulate $ m
📝 Committing: chore: Remove TODO comments regarding AI content generation and deployment strategies.
[main ed186633] chore: Remove TODO comments regarding AI content generation and deployment strategies.
 1 file changed, 2 insertions(+)
(nix) pipulate $ git add .agents/skills/pipulate/SKILL.md
warning: could not open directory '.agents/skills/pipulate/': No such file or directory
fatal: pathspec '.agents/skills/pipulate/SKILL.md' did not match any files
(nix) pipulate $ vim .agents/skills/pipulate/SKILL.md
(nix) pipulate $ patch
(nix) pipulate $ app
✅ WHOLE-FILE WRITE: CREATED '.agents/skills/pipulate/SKILL.md'.
(nix) pipulate $ git add .agents/skills/pipulate/SKILL.md
(nix) pipulate $ m
📝 Committing: chore: Update SKILL.md with detailed installation instructions
[main 6e346e8d] chore: Update SKILL.md with detailed installation instructions
 1 file changed, 135 insertions(+)
 create mode 100644 .agents/skills/pipulate/SKILL.md
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
(nix) pipulate $ d
diff --git a/foo_files.py b/foo_files.py
index 0d6e0a4f..255dcde1 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -1853,6 +1853,7 @@ foo_files.py      #  <-- THIS file. Content compiler router. Makes it very meta.
 # .agents/skills/hello-workflow/SKILL.md
 # .agents/skills/journal/SKILL.md
 # .agents/skills/journal/references/index.md
+# .agents/skills/pipulate/SKILL.md
 # .agents/skills/roles/SKILL.md
 # .agents/skills/sheets-readonly/SKILL.md
 
(nix) pipulate $ m
📝 Committing: chore: Add .agents/skills/pipulate/SKILL.md
[main b903425f] chore: Add .agents/skills/pipulate/SKILL.md
 1 file changed, 1 insertion(+)
(nix) pipulate $ git push
Enumerating objects: 28, done.
Counting objects: 100% (28/28), done.
Delta compression using up to 48 threads
Compressing objects: 100% (22/22), done.
Writing objects: 100% (24/24), 7.76 KiB | 662.00 KiB/s, done.
Total 24 (delta 14), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (14/14), completed with 3 local objects.
To github.com:pipulate/pipulate.git
   35f08c28..b903425f  main -> main
(nix) pipulate $

4: Prompt: Read the receipts first: the skills census (six rows), the ls-files count, the RECEIPTS count, the two greps, and the sdist (116 members, the pipulate/ directory and its SKILL.md among the hits). Then read .agents/skills/pipulate/SKILL.md against its four sources in this payload, install.sh, the npvg.org door page, README’s Quick Start and AUDIT.md’s first-run section, and correct any sentence the sources do not support, as SEARCH/REPLACE cars against the skill; in the same train, write the localhost sentence into AGENTS.md so an agent handed that file alone reads the specification and the localhost rule in its first two sentences. Then say whether the wheel finding wants a ruling now or waits. After that, THE FIRST GAME: the next ride starts with svb SVB-133 –write –prompt at a (nix) prompt, and the board it writes is the next compile’s Context.

5: Deliverables: How are we doing? The goal here is to be able to answer:

“Why not just use a Claude skill for that?”

…with:

“It is a Claude skill; ones that assures the quality of Claude output. Maybe you can think of it more like a Claude plugin that carries its own tools that the skill calls.”

I included the new Anthropic Claude documentation but I’ll paste it here too:

— BEGIN CLAUDE PLUGIN DOC —

Plugins overview

Copy page

Understand what a Claude Code plugin is, when you need one instead of a standalone skill or MCP server, and which page to read to install or create one.

A Claude Code plugin is a directory of skills, agents, hooks, MCP servers, or other components that Claude Code installs and loads as one unit. Most plugins come from a marketplace, which is a catalog that lists plugins and where to fetch each one. You can also load a plugin from a folder someone gives you, or build your own.

Start on claude.com instead if either of these describes you:

To try a plugin now, run /plugin in a Claude Code terminal session and install one from the Discover tab, which lists the plugins from Anthropic’s official marketplace and any marketplace you’ve added. From there:

​ Understand what a plugin is

A plugin is a directory of components, usually with a manifest. The manifest, a JSON file at .claude-plugin/plugin.json, gives the plugin its name and can add a version, a description, and other metadata. The components are what the plugin adds to Claude Code, such as:

  • Skills: SKILL.md instructions Claude loads when relevant, and that you can also run as a command
  • Agents: subagent definitions Claude can delegate to
  • Hooks: commands Claude Code runs at points in its lifecycle, such as after every edit
  • MCP servers: tool servers Claude Code connects to while the plugin is enabled

This diagram shows a plugin named my-plugin that holds one of each of those components, and what you get from each file once the plugin loads.Diagram in two columns joined by five straight arrows. Left, the directory of a plugin named my-plugin, holding a manifest at .claude-plugin/plugin.json, skills/review/SKILL.md, agents/reviewer.md, hooks/hooks.json, .mcp.json, and other components. Right, what each file gives you in your session: the manifest sets the plugin name, my-plugin; the skill runs as /my-plugin:review; the agent file is a subagent Claude can delegate to; the hooks file holds hooks that run on lifecycle events; and .mcp.json adds an MCP server that gives Claude tools.For every component type a plugin can hold, with an example of each, see Plugin components. To see where each piece is located in a plugin’s directory, use the plugin explorer on that page.

​ Decide whether you need a plugin

Skills, subagents, hooks, and MCP servers all work on their own, without a plugin. A skill you save in ~/.claude/skills/, for example, is available in every project on your machine. To set one up on its own, see Skills, Subagents, Hooks, or MCP.Use a plugin when you want several skills, subagents, hooks, or MCP servers packaged as one unit. Install one to get a setup someone else built, with one command and updates from its marketplace. Make one to give your own setup to teammates, install it in many projects, or publish versioned releases.

​ What an enabled plugin adds to your sessions

An enabled plugin is part of every session, not only the sessions where you use it. That has a few consequences worth knowing before you install one:

  • Context and usage: for each skill, agent, and command that Claude can invoke on its own, the name and description are in Claude’s context on every turn so that Claude knows it exists. Those tokens count toward your usage and leave less room in the context window even in sessions where nothing from the plugin runs. The full text of a skill or agent loads only when it’s used. What the plugin’s MCP servers add per turn follows MCP tool search.
  • Processes: MCP servers the plugin defines run alongside each session where it’s enabled, and its hooks fire at their events.
  • Permissions: what the plugin runs, it runs as you. See Plugin security and trust for what to review first.

You can check a plugin’s footprint at each stage:

  • Before you install: open the plugin from the Marketplaces tab in /plugin. Plugins in Anthropic’s official marketplace show a Context cost estimate there.
  • After you install: Measure what a plugin costs shows how to read a plugin’s footprint, and the Installed tab’s Not used recently group lists plugins you could turn off.
  • To stop it without uninstalling: disable the plugin with /plugin or, in your shell, claude plugin disable. See Manage installed plugins.

​ Get plugins from a marketplace

A marketplace is a repository or directory with a .claude-plugin/marketplace.json file that lists plugins and where to fetch each one. It’s a catalog, not a hosted store. You add a marketplace once, then install plugins from it by name, such as commit-commands@claude-plugins-official.

A plugin marketplace isn’t Claude Marketplace. Claude Marketplace is the website at claude.com/marketplace where you browse plugins, connectors, partner products, and service partners. It isn’t a marketplace you add with /plugin marketplace add.

Claude Code adds Anthropic’s official marketplace the first time you start an interactive terminal session, unless a managed policy blocks it. Claude Code adds no other marketplace on its own, including Anthropic’s community and demo marketplaces. To distinguish the three Anthropic marketplaces, read Anthropic’s marketplaces. To see what the official one lists, open the Discover tab of /plugin in a session or browse Claude Marketplace.This diagram shows the path from a marketplace to your session. A marketplace lists a plugin, you install that plugin, and Claude Code loads its components.Diagram of the marketplace path in three boxes, left to right. A marketplace, a catalog of plugins, lists a plugin. The plugin is one directory installed as a unit, holding skills, agents, hooks, MCP servers, and other components. You install the plugin into Claude Code, which loads its components.Install and manage plugins has the install steps for each place you run Claude Code. While you’re developing a plugin, you don’t need a marketplace: load it straight from its folder with --plugin-dir, as Develop without a marketplace shows.

​ Make an installed plugin available in your session

Before a plugin you installed gives you a skill you can run, it has to be present at each of these layers:

  • Settings: your settings list the marketplaces you’ve added and the plugins that are enabled.
  • Disk: ~/.claude/plugins/ holds what Claude Code has fetched and installed.
  • Session: plugins load at startup, or when you reload plugins.

Read Plugin loading reference for the rules at each layer, including which settings file takes precedence and where the files are on disk.

​ Tell Anthropic’s marketplaces from third-party ones

A marketplace’s name places it in one of three tiers. Claude Code accepts the official and community names only for marketplaces sourced from github.com/anthropics/ repositories:

  • Official: marketplaces with one of Anthropic’s official marketplace names, including claude-plugins-official and the demo marketplace claude-code-plugins.
  • Community: marketplaces with one of Anthropic’s community names, such as claude-community. Identify Anthropic’s marketplaces by name lists them.
  • Third-party: every other marketplace. A marketplace your coworker or your organization publishes is third-party.

Whatever the tier, a plugin you install can run code with your user privileges. Read Plugin security and trust for how to review a plugin before you install it.Through managed settings, an organization can allowlist or block marketplaces, force-install plugins, and turn off session-only loading. Read Manage plugins for your organization for those controls.

​ Understand install scopes

When you install a plugin, you pick a scope, and the scope decides who the plugin is enabled for:

  • User scope: enabled for you in every project on this computer
  • Project scope: enabled for everyone who works in this repository, through the committed .claude/settings.json. Each collaborator still installs it on their own machine
  • Local scope: enabled for you in this repository only

A plugin you install at user scope in the terminal, the desktop app’s local sessions, or the VS Code extension is available in the other two on that computer, because all three read the same settings files. See Choose an install scope for how to pick one.A cloud session, including one in the browser at claude.ai/code, doesn’t load the plugins in your local settings. For install steps in the terminal, VS Code, and the desktop app, and for what a cloud session loads, see Install a plugin.

The same plugin format also installs on claude.ai and in Cowork, where a different set of components loads. For those surfaces, see Plugins on claude.ai and in Cowork on claude.com and its component support table.

​ Next steps

Most people start by installing a plugin from Anthropic’s official marketplace, which Claude Code adds the first time you start an interactive terminal session. Run /plugin in a terminal session to browse it, or follow Install and manage plugins, which also covers the desktop app and VS Code. To see what’s in that marketplace before you open Claude Code, browse Claude Marketplace on the web.To build your own, Create a plugin starts with an empty directory and ends with a working plugin.Once you’ve installed or built a plugin, these pages cover what comes next:

— END CLAUDE PLUGIN DOC —

Thoughts? Maybe we should adjust things to be even more in-line with developing conventions?

Fable 5.1 Thinking

Thought for 5m 27s

Reviewing a detailed trading scenario involving multiple positions.

Cross-checking skill documentation against its source files.

Verifying six forecasts against the operator's receipts.

Examining commit message patterns for accuracy and tone.

Comparing installer script details against documentation claims.

Checking a re-run instruction against the script's actual message.

Spotting a naming quirk in the fallback reopen line.

Reordering the installer's steps to match the actual script.

Weighing conflicting comments about download silence versus visible logs.

Finding a stale boot-menu description contradicting newer receipts.

Weighing whether to patch the README's stale quick-start text.

Tracing when the one-door menu actually landed.

Trusting last turn's source-based summary of the menu's behavior.

Deciding to defer a documentation fix pending file access.

Matching startup commands and shell features against the reference guide.

Refining the voice-consent explanation against ceremony receipts.

Locating the voice model's actual storage folder.

Softening an overly strong claim about external connections.

Verifying SSH key handling and folder-removal instructions.

Uncovering a third overlooked storage location outside the folder.

Rewriting the patch-protocol description to match AGENTS.md exactly.

Weighing whether to fix a typo found on the door page.

Drafting opening sentences to anchor the specification and localhost rule.

Comparing plugin conventions against the repository's skill layout.

Investigating which skill-discovery path different coding tools actually use.

Mapping Pipulate's tool concepts onto plugin terminology.

Weighing whether skills should live inside a plugin structure.

Spotting a vendor discovery convention worth adopting.

Weighing whether the wheel-finding needs an immediate ruling.

Designing verification probes for the documentation rewrite.

Designing probe phrases to verify the README's stale menu text.

Checking whether the boot menu still reads a numeric choice.

I'll check two convention facts before answering the plugin question, since the answer hinges on which path Claude Code actually reads and on what that `rel="agent-skills"` link in the docs' response headers is.

Results

[skills mcp github.com](https://github.com/skills-mcp/skills-mcp)

[GitHub - VoltAgent/awesome-agent-skills: A curated collection of 1000+ agent skills from official dev teams and the community, compatible with Claude Code, Codex, Gemini CLI, Cursor, and more. · GitHub github.com](https://github.com/VoltAgent/awesome-agent-skills)

[agentskill mcp pypi.org](https://pypi.org/project/agentskill-mcp/)

[mcp server skill pypi.org](https://pypi.org/project/mcp-server-skill/)

[iflow mcp qianjietech agentskill mcp pypi.org](https://pypi.org/project/iflow-mcp-qianjietech-agentskill-mcp/)

[claude skills github.com](https://github.com/MisotheCod/claude-skills)

[Agent Skills opencode.ai](https://opencode.ai/docs/skills/)

[Extend Claude with skills - Claude Code Docs code.claude.com](https://code.claude.com/docs/en/skills)

[Agent Skills - Claude Platform Docs platform.claude.com](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)

Verifying where Claude Code looks for project skill files.

Results

[feat(artifacts): generate .well-known agent-skills discovery index for dashboard · Issue #144 · MAHDTech/agent-skills github.com](https://github.com/MAHDTech/agent-skills/issues/144)

[agent skills redocly.com](https://redocly.com/docs/realm/customization/agent-skills)

[feat(docs): publish an Agent Skills discovery index · Issue #820 · haydenbleasel/ultracite github.com](https://github.com/haydenbleasel/ultracite/issues/820)

[04 · Publish the agent skills discovery index · Issue #169 · ryanlindsey/ryanlindsey.me github.com](https://github.com/ryanlindsey/ryanlindsey.me/issues/169)

[3.2: Publish an agent skills discovery index · Issue #144 · Montimage/sec-mcp github.com](https://github.com/Montimage/sec-mcp/issues/144)

[GitHub - cloudflare/agent-skills-discovery-rfc: A mechanism for discovering Agent Skills using the .well-known URI path prefix as specified in RFC 8615 for discovering Agent Skills. · GitHub github.com](https://github.com/cloudflare/agent-skills-discovery-rfc)

[Ship agent skills like packages: discovery index, digests, and install sources—Martian Chronicles, Evil Martians’ team blog evilmartians.com](https://evilmartians.com/chronicles/publishing-agent-skills-discovery-index)

[# Agent skills redocly.com](https://redocly.com/docs/realm/customization/agent-skills.md)

[3.2: Publish an agent skills discovery index · Issue #113 · luongnv89/focus-paw github.com](https://github.com/luongnv89/focus-paw/issues/113)

[GitHub - supabase/agent-skills: Agent Skills to help developers using AI agents with Supabase · GitHub github.com](https://github.com/supabase/agent-skills)

Investigating specifications for an agent skills discovery protocol.

Drafting citation plan while organizing findings and remaining questions.

Weighing symlink and plugin bridges against repo conventions.

Sketching the discovery protocol and framing the plugin analogy's limits.

Rewriting the setup steps for accuracy and sequencing.

Cross-checking install messaging against conflicting README claims.

Reconciling conflicting menu descriptions across dated receipts.

Revising documentation sections on file locations and voice settings.

Untangling conflicting defaults for the state directory's file path.

Rewriting the patch-protocol and sourcing passages for accuracy.

Rewriting the AGENTS.md preamble for accuracy and scope.

Drafting an exact ignition command for the pad deployment.

Appending follow-up TODO notes on remaining documentation threads.

Noting the install script's naming mismatch across doors.

Weighing whether Claude Code visibility fits the repo's guardrails.

Verifying installer messaging and skill file wording against read-only probes.

Designing read-only probes for the boot menu's keypress logic.

Drafting read-only checks for voice paths and installer content.

Counting matching TODO markers to confirm exact occurrences.

Planning the next probe set and follow-up patch target.

Deferring the README patch until the probe result lands.

Copying exact skill text into precise patch blocks.

Rewriting the install script's numbered step sequence.

Detailing outside-the-folder storage locations and voice model reference.

Simplifying the protocol bullet to avoid risky embedded claims.

Rewriting the AGENTS.md preamble and the installer's landing text.

Appending detailed follow-up notes to remaining TODO markers.

Adding new TODO earmarks and weighing another convention violation.

Appending a new conviction line to the ordering rule.

Sequencing patch cars and confirming which commands stay unspoken.

Drafting the closing recommendations and next-step ignition command.

Clarifying the prompt-file reset before the next ride.

Verifying exact search anchors against the payload text.

Drafting new TODO notes on installer naming and stamping cures.

Weighing whether Claude Code's skill paths need a symlink bridge.

Assembling the final grep probe list for verification.

Verifying line counts for voice paths and README references.

Fable 5.1 Extra: I’ll check two convention facts before answering the plugin question, since the answer hinges on which path Claude Code actually reads and on what that rel="agent-skills" link in the docs’ response headers is.

Tool calls this turn: two web searches, research phase, exempt under your rule; no API calls, no MCP.

Context verified. apply.py imports only the standard library; nothing patched below imports a file outside this payload. Two files the second reading wanted are absent, flake.nix (the skill’s paragraph about what happens inside nix develop came from it last turn and cannot be re-read here) and scripts/boot_menu.py, and the one place the absence matters is named below rather than guessed past.

The receipts, read

All six forecasts landed exactly: the census reads six rows with pipulate fence name-ok in fourth position; ls-files 7; RECEIPTS 22; both greps 1; the sdist 116 members with pipulate-2.66/.agents/skills/pipulate/ and its SKILL.md as the two new lines, the version unmoved this time so the mtime line and the literal name would have agreed either way. Coverage read 207/273, +1 claimed and +1 tracked, and the working tree’s one hunk is that line, the compiler’s own.

One miss, mine, and the paste convicts it: I put the git add fence above the WRITE_FILE fence it depended on, with prose saying “after app”. You pasted in reading order, git answered fatal: pathspec, and you ran it again after app. That is ONE TIMELINE, NO PREVIEWS broken by its own emitter: a hand fence that depends on a car sits below the car, in the order the hand will meet it. Banked as a conviction on that rule (Car 4). Seven m subjects rode the train and every one is a shape the specimen ledger already holds (two backwards signs, one context row, one file “Updated” that was created, one router line read as the commit’s verb, one subject reused from the last ride, one fix: for a comment); I am not appending them, because THE CHEAP FORGETTING already names that ledger as the router’s heaviest block and the next specimen is not a new shape.

The second reading of the install skill

Read against install.sh, the door page, README’s Quick Start and AUDIT.md. Seven sentences do not survive; they are Car 1.

  1. The nix-missing stop. The skill said “run the same line again.” The script prints a different line: curl -fsSL https://pipulate.com/install.sh | bash, and from the npvg door bash -s npvg, because CUSTOM_NAME is npvg there and the script cannot know which door served it (THE DOOR NAMES THE FOLDER). Following the printed line passes the folder as an argument, which writes whitelabel.txt and names the app Npvg, where the plain npvg line names it Pipulate. The 2026-09-18 receipt saw this branch and never rode it. The skill now says what the script prints; the cure in the script is a TODO (Car 4).
  2. The order. The folder-exists refusal comes after the nix check, not before it; and the banner line and the curl/unzip checks come before both. Reordered to the script’s own sequence.
  3. “With no output.” The script hands off to nix develop -L, and its own comment says -L is there so the download progress shows. The sentence is replaced by what the script does: prints “Hydrating the Nix environment (the first time can take a few minutes)” as its last line before the hand-off.
  4. The run file. The script writes it; the flake’s transformation sweeps it seconds later (both platforms, 2026-09-14 receipt). A reader with less open sees the heredoc, so the skill names it as a leftover marked for removal.
  5. A third home outside the folder. ~/.local/state/pipulate/ holds context.txt when PIPULATE_ADHOC_FILE points there and the stale/ set-aside; the compile’s own Processing Log names it. The skill knew two homes.
  6. The voice model’s address. I had put it under ~/.local/share/pipulate/. The 09-17 receipt read it at assets/piper_models and the 09-19 ride moved a piper_models folder aside to force the download; neither is ~/.local/share. Withdrawn: the skill now says imports/voice_synthesis.py names it, and a probe below reads that file’s paths so the next turn can write the true one.
  7. The protocol sentence. AGENTS.md names SEARCH/REPLACE and apply.py; it does not name context, prompt, compile, patch, app, d, m. The skill claimed it did. Rewritten to what AGENTS.md says, and the “does not vouch for the door page” bullet becomes a sources line, since the door page was read this time. Also refined: the walk itself shows the voice card on its first run (THE CEREMONY AND THE BARRIER), so a newcomer never has to type voice first.

One disagreement I am not resolving by patch: README’s Quick Start says nix develop ends at a three-door menu, “Press 1”, three times; the skill says it ends at a command list and a (nix) prompt with nothing started, which is the router’s own description of scripts/boot_menu.py (“the one-door command list”) and what I read off flake.nix last turn. The 09-18 dismount ruled “no number selection” and marked it unbuilt. Two sources in this payload disagree and the file that rules is not here, so the probe reads boot_menu.py for keypress code and the file itself rides NEXT CONTEXT; whichever sentence is stale gets patched next turn with the source in hand, not this turn on my memory of a previous compile.

One typo in a source: the door page reads “The first commands lets you read the installer.” Car 3, one word.

The wheel: waits

pip install pipulate is PATH 2’s bootstrap. README says pipulate install runs the same universal installer, so every wheel user ends at a git checkout that carries AGENTS.md and .agents/ one step later; the sdist, the thing an auditor reads without installing, carries them now. No door a wheel user meets is missing anything. The dead package-data block is a pyproject.toml cleanup for the turn that file rides, gated as the TODO already writes it. Banked as my reading, not a ruling (Car 4).

“Why not just use a Claude skill for that?”

How we are doing: the answer you want to give is already true in the file format and half true in the vocabulary, and the plugin page you pasted is the map for the other half. In plugin terms, this repo already has:

  • Skills. Six SKILL.md files in the exact format Claude Code loads. The one thing it does not have is the path: Claude Code reads a repository’s skills from .claude/skills/<name>/SKILL.md (and personal ones from ~/.claude/skills/); Codex reads .agents/skills/; OpenCode reads both. So a Claude Code session opened in this repo today sees none of the six. One tracked symlink, .claude/skills -> ../.agents/skills, is the whole bridge, and the / check the TODO already names is its gate. The ruling owed is whether a vendor directory at the root is the price. Banked (Car 4), not ridden: unasked, and the docs page that names the path should ride a payload before the car does.
  • Hooks. The plugin page describes commands that run at lifecycle points, such as after every edit. Here that is apply.py’s airlocks (AST, Nix, JSON, marker, autolink, the prose-growth refusal) and the pre-commit tripwire. Same job, one difference: the plugin’s hook runs after the model’s edit tool; ours runs after the human’s app.
  • Tools. A plugin exposes them through an MCP server in .mcp.json, so the model calls them inside the vendor’s session. This repo declines that on purpose, and it is the whole answer to the question. The tools exist (cli.py call <tool>, the seven connector words, botify --rules, render diff), the skills name them, but the call is typed by a human or run by the compiler as a ! line, and its stdout rides the payload as a receipt the model reads. A plugin’s MCP call lands in the cockpit voice recorder; a ! line lands in the flight data recorder. That is not a style preference; it is the difference between a transcript and a record, and AUDIT.md’s change-control section is the reviewer-facing version of the same sentence.
  • The manifest. .claude-plugin/plugin.json is a distribution unit for Claude Code sessions, with a version that would drift from __init__.py unless release.py stamped it. This repo’s distribution unit is the installer, the flake and the sdist. Nothing to gain.

So the sentence, sharpened: It is six Claude skills, in the file format Claude Code loads, plus the tools they call, which are commands on localhost. What a plugin would add is a manifest we do not need and an MCP server we refuse, because it would move the tool call out of the record and into the vendor’s session.

Two convention moves are worth making, and one of them the wire found. The plugin page’s own response headers (in the payload, the Response Headers lens) carry a Link header advertising llms.txt, an MCP server card, an agent card and /.well-known/agent-skills/index.json with rel="agent-skills"; the page’s prose never mentions it. That path is the Agent Skills Discovery RFC v0.2.0 (Cloudflare’s, under agentskills.io): a JSON index at a site root, one entry per skill with name, type, description, URL and a sha256 digest, and a SKILL.md served behind each entry. Supabase and Evil Martians publish one; Redocly generates one per site. It is the llms.txt reflex aimed at SKILL.md, and pipulate.com already serves llms.txt and AI_CONTEXT.md from release.py’s sync lane, so the index is one more generated file with digests computed over the bytes served, never authored. The digest is the cartridge’s own rung. Banked as a TODO (Car 4); it needs release.py and the Pipulate.com repo in a payload.

And one parallel worth saying out loud, because the plugin page says it about plugins and the router has been living it since July: an enabled plugin’s skill names and descriptions ride every turn and cost context whether or not the skill runs, the full text loading only on use, and the page tells you to check the “Not used recently” tab and turn things off. The router is that: the § keys ride every compile, the bodies live in GLOSSARY.md, and the forget ride is the Not-used-recently tab. Same economics, arrived at from the other side.

1. PROBES

All read-only. Straddles, with forecasts: the AGENTS.md head 0 to 2 (the spec on line 3, localhost on line 7 after Car 2); the three retired phrases 3 to 0 and the three new ones 0 to 3 (Car 1); the door page 0 to 1 (Car 3, in git; the served copy moves only with the pad’s rsync); the deed count 0 to 4 (Car 4). Two are CENSUS lines: three-door reads 3 in README on both taps, and the boot_menu.py line prints either an empty count with rc=1 (no keypress code: README is the stale one) or a count with rc=0 (the number gate lives: the skill’s sentence is). The last is a reading with a marker so silence is distinguishable from a probe that did not run.

sed -n '1,8p' AGENTS.md | grep -c -e localhost -e 'agentskills.io'
grep -c -e 'with no output' -e 'and the voice model' -e 'context, prompt, compile' .agents/skills/pipulate/SKILL.md
grep -c -e 'local/state/pipulate' -e 'bash -s npvg' -e 'nix develop -L' .agents/skills/pipulate/SKILL.md
grep -c 'first command lets' remotes/honeybot/www/npvg.org/index.html
grep -c '2026-09-28 (deed 1662)' foo_files.py
grep -c 'three-door' README.md
n=$(rg -c -e termios -e getch -e "'[123]'" scripts/boot_menu.py 2>&1); echo boot_menu_keypress_lines="$n" rc=$?
rg -n -e 'piper_models' -e 'local/share' -e 'config/pipulate' imports/voice_synthesis.py | head -8; echo voice_paths_end

2. NEXT CONTEXT

Paste-ready. scripts/boot_menu.py rides so the README-or-skill sentence can be patched with the file in hand; the game’s block stays commented, since svb writes its own.

! sed -n '1,8p' AGENTS.md | grep -c -e localhost -e 'agentskills.io'
! grep -c -e 'with no output' -e 'and the voice model' -e 'context, prompt, compile' .agents/skills/pipulate/SKILL.md
! grep -c -e 'local/state/pipulate' -e 'bash -s npvg' -e 'nix develop -L' .agents/skills/pipulate/SKILL.md
! grep -c 'first command lets' remotes/honeybot/www/npvg.org/index.html
! grep -c '2026-09-28 (deed 1662)' foo_files.py
! grep -c 'three-door' README.md
! n=$(rg -c -e termios -e getch -e "'[123]'" scripts/boot_menu.py 2>&1); echo boot_menu_keypress_lines="$n" rc=$?
! rg -n -e 'piper_models' -e 'local/share' -e 'config/pipulate' imports/voice_synthesis.py | head -8; echo voice_paths_end
foo_files.py
AGENTS.md
.agents/skills/pipulate/SKILL.md
README.md
scripts/boot_menu.py
# --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# Workshop/corporate/connectors/svb.py
# Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# Workshop/corporate/tickets/SVB_PROMPT.md
# connectors/mcp_render.py
# connectors/jira.py
# ! jira SVB-133

3. PATCHES

Four cars, every block anchored on raw source in this payload; no new files, so Prime’s m stages everything it needs. Every SEARCH spans contiguous non-empty lines or a single unique line.

Car 1: the skill’s second reading, one commit story. Five blocks against the skill.

Target: .agents/skills/pipulate/SKILL.md
[[[SEARCH]]]
1. Refuses to run under a shell that is not bash, and refuses if the target
   folder already exists. It never overwrites.
2. If `nix` is missing, runs the Determinate Systems Nix installer and then
   STOPS with one instruction: close this terminal, open a new one, run the
   same line again. That stop is expected, not a failure.
3. Downloads the repository as a ZIP from GitHub and unpacks it into the
   folder.
4. Fetches a ROT13-encoded deploy key into `.ssh/rot` inside the folder. It
   is pull-only: it lets the folder fetch updates without a GitHub account.
5. Hands off to `nix develop`. The first entry can take several minutes
   with no output while packages download; that silence is a download, not
   a hang.
[[[DIVIDER]]]
1. Refuses to run under a shell that is not bash, prints one line naming the
   folder and the command that removes it, and stops if `curl` or `unzip`
   is missing.
2. If `nix` is missing, runs the Determinate Systems Nix installer and then
   STOPS: close this terminal, open a new one, run the line it prints. That
   stop is expected, not a failure. The line it prints goes through
   pipulate.com and, from the npvg door, carries the folder as an argument
   (`bash -s npvg`), so the app is then named after the folder; the plain
   npvg line names no folder and leaves the app named Pipulate.
3. Refuses if the target folder already exists. It never overwrites.
4. Downloads the repository as a ZIP from GitHub and unpacks it into the
   folder.
5. Fetches a ROT13-encoded deploy key into `.ssh/rot` inside the folder. It
   is pull-only: it lets the folder fetch updates without a GitHub account.
6. Writes a `run` file the flake deletes on its first entry (a leftover the
   repo has marked for removal), prints the come-back line, and hands off
   to `nix develop -L`. The first entry can take several minutes while
   packages download; the installer's last line says so, and a long wait
   after it is that download.
[[[REPLACE]]]

Target: .agents/skills/pipulate/SKILL.md
[[[SEARCH]]]
- `walk` is the first word to type: a guided walk over three public pages
  that teaches the loop. `menu` reprints the short list, `all` the long
  one, `about` the workspace tree, `voice` answers the question of whether
  the walk may be read aloud (it is silent until the answer is yes). `exit`
  leaves the shell.
[[[DIVIDER]]]
- `walk` is the first word to type: a guided walk over three public pages
  that teaches the loop. On its first run it shows a card asking whether it
  may read aloud and records the answer; it stays silent until that answer
  is yes, and `voice` changes it later. `menu` reprints the short list,
  `all` the long one, `about` the workspace tree. `exit` leaves the shell.
[[[REPLACE]]]

Target: .agents/skills/pipulate/SKILL.md
[[[SEARCH]]]
Outside the folder: `~/.config/pipulate/` (settings, credentials the person
adds by hand, the one-line answer to the voice question) and
`~/.local/share/pipulate/` (two sound files and the voice model).
[[[DIVIDER]]]
Outside the folder, three homes: `~/.config/pipulate/` (settings,
credentials the person adds by hand, the one-line answer to the voice
question), `~/.local/share/pipulate/` (two sound files) and
`~/.local/state/pipulate/` (the compile's own state: `context.txt` when
`PIPULATE_ADHOC_FILE` points there, and what a move sets aside under
`stale/`). The browser lanes may leave a driver cache under `~/.cache`; that
path was not read for this skill. Where the voice model lands is named in
`imports/voice_synthesis.py`, not here.
[[[REPLACE]]]

Target: .agents/skills/pipulate/SKILL.md
[[[SEARCH]]]
Leaves, and only these: GitHub (the ZIP, then the clone, then a `git pull`
[[[DIVIDER]]]
Leaves, as the installer and the flake spell it: GitHub (the ZIP, then the
clone, then a `git pull`
[[[REPLACE]]]

Target: .agents/skills/pipulate/SKILL.md
[[[SEARCH]]]
- It never edits files in the workshop by hand. Edits go through the patch
  protocol `AGENTS.md` names (context, prompt, compile, patch, app, d, m).
  That loop is always-on in `AGENTS.md` rather than a skill, because a skill
  loads on demand and the loop is every turn.
- It does not vouch for what the door looks like in a browser; only the
  text `curl` receives, and the flake it hands off to, were read to write
  this.
[[[DIVIDER]]]
- It never edits files in the workshop by hand. Edits go through the
  protocol `AGENTS.md` names: SEARCH/REPLACE blocks, matched exactly once,
  applied by `apply.py`, which checks Python, Nix and JSON syntax before it
  writes. That protocol is always-on in `AGENTS.md` rather than a skill,
  because a skill loads on demand and the protocol is every turn.
- It was read against four sources: the installer's own text, the door page
  a browser gets at npvg.org, README's Quick Start and AUDIT.md; where the
  installer's text and a page disagreed, the installer's text won.
[[[REPLACE]]]

Car 2: AGENTS.md’s first two sentences. The specification in the first, the localhost rule in the second, the old paragraph kept beneath with its now-duplicate clause cut. The generated tree block is untouched.

Target: AGENTS.md
[[[SEARCH]]]
This repo predates the AGENTS.md / Agent Skills / OKF conventions and complies
with them by *pointing*, not duplicating. The sources of truth here are
executable, so this file is a signpost. Do not add sibling status .md files;
anything written here that duplicates code will drift and is a bug.
[[[DIVIDER]]]
This repository carries Agent Skills under https://agentskills.io/specification
(`.agents/skills/*/SKILL.md`, a YAML head over a Markdown body, loaded on
demand), with this file as the nearest-ancestor signpost that points at
executable truth instead of duplicating it. Every tool call taught here runs
on localhost under the POSIX conventions of a command, stdin, stdout and an
exit code, so that its output lands in the sealed compile (`foo.zip`) as a
receipt; a cloud-side tool call that cannot be reconstructed here is
non-reproducible, non-portable, cannot be compiled into a cartridge, and is
out of scope by that rule, not by taste.

This repo predates those conventions and complies with them by *pointing*,
not duplicating. Do not add sibling status .md files; anything written here
that duplicates code will drift and is a bug.
[[[REPLACE]]]

Car 3: the door page’s one word.

Target: remotes/honeybot/www/npvg.org/index.html
[[[SEARCH]]]
<p>This address serves a page to a browser and the installer to a terminal. The first commands lets you read the installer and the second one runs it.</p>
[[[DIVIDER]]]
<p>This address serves a page to a browser and the installer to a terminal. The first command lets you read the installer and the second one runs it.</p>
[[[REPLACE]]]

Car 4: the banks. Three READ/LANDED appends, three new TODO lines, one conviction on ONE TIMELINE.

Target: foo_files.py
[[[SEARCH]]]
README's Quick Start and AUDIT.md's first-run section are read against it before 2.67 ships it.
[[[DIVIDER]]]
README's Quick Start and AUDIT.md's first-run section are read against it before 2.67 ships it. READ 2026-09-28 (deed 1662): the second reading rode as one car against the skill (the nix-missing stop's reopen line, the folder check after the nix check, the -L hand-off and the run leftover, a third home under ~/.local/state, the voice card the walk itself shows, the protocol as AGENTS.md spells it, a sources line, the voice model's home withdrawn until imports/voice_synthesis.py names it); README's Quick Start still says a three-door menu and Press 1 where the skill says a command list and a prompt, and scripts/boot_menu.py rules which sentence is stale before either is patched; the localhost sentence landed in AGENTS.md's first paragraph in the same train; the Claude Code check is now THE CLAUDE CODE PATH, its own line.
[[[REPLACE]]]

Target: foo_files.py
[[[SEARCH]]]
cut the dead block after a census of whether cli.py's pipulate install needs a bundled install.sh.
[[[DIVIDER]]]
cut the dead block after a census of whether cli.py's pipulate install needs a bundled install.sh. READ 2026-09-28 (deed 1662): waits. The wheel is PATH 2's bootstrap (pipx install pipulate, then pipulate install, which README says runs the same universal installer), so every wheel user ends at a git checkout that carries AGENTS.md and .agents/ one step later, and the sdist, the artifact an auditor reads without installing, carries them now; no door a wheel user meets is missing anything. The dead package-data block is a pyproject.toml cleanup when that file rides, gated as written.
[[[REPLACE]]]

Target: foo_files.py
[[[SEARCH]]]
the localhost sentence is still owed, worded after the specification is read into a payload, and it rides the install skill's second-reading turn.
[[[DIVIDER]]]
the localhost sentence is still owed, worded after the specification is read into a payload, and it rides the install skill's second-reading turn. LANDED 2026-09-28 (deed 1662): the first paragraph names the specification in its first sentence and the localhost rule in its second, the old paragraph kept beneath with its duplicate clause cut; the gate (a fresh chat handed AGENTS.md alone) is a hand step, one paste into any chat window, and its receipt is the model's first two sentences back.
[[[REPLACE]]]

Target: foo_files.py
[[[SEARCH]]]
# #todo #to-do #earmarks
[[[DIVIDER]]]
# #todo #to-do #earmarks
# - TODO (2026-09-28, THE REOPEN LINE NAMES THE OTHER DOOR; read at deed 1662 off install.sh): the nix-missing branch prints curl -fsSL https://pipulate.com/install.sh | bash -s npvg from the npvg door, because the script cannot learn its own address ($0 is bash and the pipe carries none; THE DOOR NAMES THE FOLDER) and KEY_URL stays at the old door on purpose; a stranger who follows that line passes the folder as an argument, which writes whitelabel.txt and names the app Npvg where the plain npvg line names it Pipulate (the 2026-09-18 receipt saw this branch and never rode it). Cure: a second stamped placeholder for the door's URL (a __INSTALL_DEFAULT_URL__ twin, stamped by the same sub_filter on the pad and spelled in two halves the way the name is) and no -s when the folder is the door's default; section 3 of the install skill says what the script prints today. Gate: the reopen line printed from the npvg door reads npvg.org and carries no -s.
# - TODO (2026-09-28, THE SKILLS INDEX IS A WELL-KNOWN URI; read at deed 1662 off the Claude Code docs' own response headers, rel="agent-skills" beside rel="llms-txt", and the Agent Skills Discovery RFC v0.2.0 it points at): a site announces its skills at /.well-known/agent-skills/index.json, one entry per skill with name, type skill-md, description, url and a sha256 digest, the llms.txt reflex aimed at SKILL.md (pipulate.com already serves llms.txt from generate_llms_txt.py and AI_CONTEXT.md from release.py's sync lane); the index is GENERATED at release from .agents/skills/*/SKILL.md, each file served under /.well-known/agent-skills/<name>/SKILL.md with its digest computed over the bytes served, never authored, and the digest is the cartridge's own rung (THE RECEIPT LADDER). Needs release.py and the Pipulate.com repo in the payload. Gate: the index at pipulate.com reads six entries and each digest matches the served SKILL.md.
# - TODO (2026-09-28, THE CLAUDE CODE PATH; read at deed 1662 off the Claude Code skills page): Claude Code loads a repository's skills from .claude/skills/<name>/SKILL.md (personal ones from ~/.claude/skills, plugins aside), Codex from .agents/skills, OpenCode from both, so a Claude Code session opened in this repo sees none of the six until .claude/skills exists; the cheapest bridge is one tracked symlink, .claude/skills -> ../.agents/skills (ln -s, git add; .claude/settings.local.json ignored in the same car), and the same page says a .claude-plugin/plugin.json inside a skill folder loads it as a plugin that can bundle hooks and MCP servers, which this repo declines on purpose (the tool call rides the compile as a receipt, never the vendor's session). Ruling owed: whether a vendor directory at the root is the price of the six names behind /. Gate: the / check, the six names offered in a session opened here.
[[[REPLACE]]]

Target: foo_files.py
[[[SEARCH]]]
if it must be mentioned early, name it ("Car 3, the smoke") and show nothing.
[[[DIVIDER]]]
if it must be mentioned early, name it ("Car 3, the smoke") and show nothing. Convicted 2026-09-28 (deed 1662): the git add for a file a WRITE_FILE car creates rode in its own fence ABOVE that car's fence; the operator pasted in reading order, git answered fatal: pathspec, and the line ran again after app; a hand fence that depends on a car sits below it, in the order the hand will meet them.
[[[REPLACE]]]

Ignition: none for Cars 1, 2 and 4; every probe reads the file at call time. Car 3 reaches npvg.org only through the pad’s rsync, nixops.sh, whose argv is not in this payload and is not spelled here; git holds the fix until the pad next deploys, and the served page is checked by eye (a curl to that address gets the installer, not the page).

4. PROMPT

Read the receipts first: the AGENTS.md head (2), the skill's retired phrases (0) and new phrases (3), the door page (1), the deed count (4), three-door (3, a census), the boot_menu.py keypress line, and the voice_synthesis paths. Then, with scripts/boot_menu.py in the payload, patch whichever sentence the keypress reading convicts: README's three three-door sentences (Quick Start, the automation table, the Big Reset) if the menu is a command list, or the skill's "stops at the short command list" sentence if the number gate lives; and write the voice model's home into the skill from the paths the probe printed. Rule on THE CLAUDE CODE PATH: the symlink or not. Then dismount this article. After it is published and prompt.md is out of the way (rm prompt.md at the root; it is gitignored and prompt regenerates it), THE FIRST GAME opens by hand at a (nix) prompt: p, then .venv/bin/python Workshop/corporate/connectors/svb.py SVB-133 --write --prompt, then context to read the board it wrote, then bjj; the board is the next compile's Context and SVB_PROMPT.md is its prompt.

5. EXTERNAL DELIVERABLES

None this turn outside the repo. The .well-known index and the Claude Code symlink are banked, not built.

Sources:

MikeLev.in:

THE PROOF STRADDLE

Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.

1: Probe:

Left-hand causal boundary "blast radius" established. Make 1 change and test.
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ sed -n '1,8p' AGENTS.md | grep -c -e localhost -e 'agentskills.io'
grep -c -e 'with no output' -e 'and the voice model' -e 'context, prompt, compile' .agents/skills/pipulate/SKILL.md
grep -c -e 'local/state/pipulate' -e 'bash -s npvg' -e 'nix develop -L' .agents/skills/pipulate/SKILL.md
grep -c 'first command lets' remotes/honeybot/www/npvg.org/index.html
grep -c '2026-09-28 (deed 1662)' foo_files.py
grep -c 'three-door' README.md
n=$(rg -c -e termios -e getch -e "'[123]'" scripts/boot_menu.py 2>&1); echo boot_menu_keypress_lines="$n" rc=$?
rg -n -e 'piper_models' -e 'local/share' -e 'config/pipulate' imports/voice_synthesis.py | head -8; echo voice_paths_end
0
3
0
0
0
3
boot_menu_keypress_lines= rc=1
80:# intent, the way PIPULATE_BOOT_MENU=0 does) or from ~/.config/pipulate/voice,
192:            local_model_dir = project_root / "assets" / "piper_models"
voice_paths_end
(nix) pipulate $ 

2: Context:

# context.txt: the list of files an AI will read. context opens it, compile builds it.
# Keys: j down, k up; Esc then :q quits, :q! discards, :wq saves and quits.
# One line per thing the AI reads: a file path, or a command after `! `.
# A line that starts with # is a comment: that file is not read.
# To add a line: i starts typing, Esc stops. Absolute paths work from anywhere.
# Web pages, APIs and the connector words: chapter XVIII of foo_files.py.

# --- THE 40K-FOOT VIEW (uncomment on a first turn; comment out on the second) ---
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs  # <-- the book's spine, one line per article, newest first
# ~/repos/nixos/autognome.py  # <-- the machine's morning routine (this author's NixOS box only)
# init.lua                    # <-- the editor keys that drive the day
# assets/installer/install.sh # <-- how a stranger's machine gets this workshop
# GLOSSARY.md                 # <-- the terms, defined
# flake.nix                   # <-- the environment, pinned: here is my hardware, here is my state
# prompt_foo.py               # <-- the compiler that builds the payload
# foo_files.py                # <-- the router: which files ride, and this book's outline
# scripts/articles/lsa.py     # <-- the second brain: the article corpus behind `rgx`, `rgxc` and `posts`
# requirements.in             # <-- the Python packages, pinned
# pyproject.toml              # <-- the PyPI package
# __init__.py                 # <-- the version

# --- ACTUATORS (cheap; include when the AI should be able to act, not only read) ---
# cli.py                      # <-- tool calls from the command line
# scripts/xp.py               # <-- turns a pasted reply into the next context
# scripts/ai.py               # <-- a local AI writes the commit messages
# scripts/crawl.py            # <-- crawl a site into the next turn
# scripts/weblogin.py         # <-- warm a login on the persistent browser profile
# scripts/webclip_2_markdown.py  # <-- a web page, clipped, as markdown

# --- RARE ---
# scripts/foo_cartridge.py    # <-- the sealed archive: writer and verifier
# scripts/foo_replay.py       # <-- replay a sealed archive on another machine
# release.py                  # <-- how a release reaches GitHub and PyPI
# imports/voice_synthesis.py  # <-- the voice
# imports/ascii_displays.py   # <-- the ASCII art
# scripts/release/version_sync.py  # <-- version stamping (to be folded into release.py)

# --- THIS DISCUSSION ---
# The files and commands for the work in front of you. Paste the NEXT CONTEXT
# block an AI hands back directly below this line; the AI will correct a guess.

# Context 1
# /home/mike/repos/trimnoir/_posts/2026-09-28-agents-md-agent-skills-pypi-receipts.md
# ! .venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
# ! .venv/bin/python -c "import json,io,tarfile,zipfile,urllib.request as u;d=json.load(u.urlopen('https://pypi.org/pypi/pipulate/json'));K=('agents','ai_context','skill','flake','audit','readme','manifest','license','changelog','install.sh');names=lambda f,b: tarfile.open(fileobj=io.BytesIO(b)).getnames() if f['packagetype']=='sdist' else zipfile.ZipFile(io.BytesIO(b)).namelist();R=[(f,names(f,u.urlopen(f['url']).read())) for f in d['urls']];[print(f['packagetype'],f['filename'],f['size'],'bytes',len(n),'members',[x for x in n if any(k in x.lower() for k in K)]) for f,n in R]"
# ! .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
# foo_files.py
# # --- THE INSTALL SKILL (uncomment when that car rides) ---
# # assets/installer/install.sh
# # remotes/honeybot/www/npvg.org/index.html
# # AUDIT.md
# # README.md
# # --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# # Workshop/corporate/connectors/svb.py
# # Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# # Workshop/corporate/tickets/SVB_PROMPT.md
# # connectors/mcp_render.py
# # connectors/jira.py
# # ! jira SVB-133
# 
# Context 2
# ! .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
# ! git ls-files .agents | wc -l
# ! awk '/^# --- START RECEIPTS/{f=1;next} /^# --- END RECEIPTS/{f=0} f' foo_files.py | wc -l
# ! grep -c '^# - EARMARK: THE PROBE NAMED THE VERSION' foo_files.py
# ! grep -c '^# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST' foo_files.py
# ! .venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
# foo_files.py
# .agents/skills/pipulate/SKILL.md
# assets/installer/install.sh
# remotes/honeybot/www/npvg.org/index.html
# AGENTS.md
# AUDIT.md
# README.md
# # --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# # Workshop/corporate/connectors/svb.py
# # Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# # Workshop/corporate/tickets/SVB_PROMPT.md
# # connectors/mcp_render.py
# # connectors/jira.py
# # ! jira SVB-133
# !https://code.claude.com/docs/en/plugins/overview
# 
# Context 3
! sed -n '1,8p' AGENTS.md | grep -c -e localhost -e 'agentskills.io'
! grep -c -e 'with no output' -e 'and the voice model' -e 'context, prompt, compile' .agents/skills/pipulate/SKILL.md
! grep -c -e 'local/state/pipulate' -e 'bash -s npvg' -e 'nix develop -L' .agents/skills/pipulate/SKILL.md
! grep -c 'first command lets' remotes/honeybot/www/npvg.org/index.html
! grep -c '2026-09-28 (deed 1662)' foo_files.py
! grep -c 'three-door' README.md
! n=$(rg -c -e termios -e getch -e "'[123]'" scripts/boot_menu.py 2>&1); echo boot_menu_keypress_lines="$n" rc=$?
! rg -n -e 'piper_models' -e 'local/share' -e 'config/pipulate' imports/voice_synthesis.py | head -8; echo voice_paths_end
foo_files.py
AGENTS.md
.agents/skills/pipulate/SKILL.md
README.md
scripts/boot_menu.py
# --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# Workshop/corporate/connectors/svb.py
# Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# Workshop/corporate/tickets/SVB_PROMPT.md
# connectors/mcp_render.py
# connectors/jira.py
# ! jira SVB-133

3: Patches:

Left-hand causal boundary "blast radius" established. Make 1 change and test.
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 '.agents/skills/pipulate/SKILL.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.agents/skills/pipulate/SKILL.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.agents/skills/pipulate/SKILL.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.agents/skills/pipulate/SKILL.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.agents/skills/pipulate/SKILL.md'.
(nix) pipulate $ d
diff --git a/.agents/skills/pipulate/SKILL.md b/.agents/skills/pipulate/SKILL.md
index c5e18048..8f402cbd 100644
--- a/.agents/skills/pipulate/SKILL.md
+++ b/.agents/skills/pipulate/SKILL.md
@@ -50,18 +50,25 @@ curl -fsSL https://npvg.org | bash
 
 What it does, in order (the script's own sequence; read it there):
 
-1. Refuses to run under a shell that is not bash, and refuses if the target
-   folder already exists. It never overwrites.
+1. Refuses to run under a shell that is not bash, prints one line naming the
+   folder and the command that removes it, and stops if `curl` or `unzip`
+   is missing.
 2. If `nix` is missing, runs the Determinate Systems Nix installer and then
-   STOPS with one instruction: close this terminal, open a new one, run the
-   same line again. That stop is expected, not a failure.
-3. Downloads the repository as a ZIP from GitHub and unpacks it into the
+   STOPS: close this terminal, open a new one, run the line it prints. That
+   stop is expected, not a failure. The line it prints goes through
+   pipulate.com and, from the npvg door, carries the folder as an argument
+   (`bash -s npvg`), so the app is then named after the folder; the plain
+   npvg line names no folder and leaves the app named Pipulate.
+3. Refuses if the target folder already exists. It never overwrites.
+4. Downloads the repository as a ZIP from GitHub and unpacks it into the
    folder.
-4. Fetches a ROT13-encoded deploy key into `.ssh/rot` inside the folder. It
+5. Fetches a ROT13-encoded deploy key into `.ssh/rot` inside the folder. It
    is pull-only: it lets the folder fetch updates without a GitHub account.
-5. Hands off to `nix develop`. The first entry can take several minutes
-   with no output while packages download; that silence is a download, not
-   a hang.
+6. Writes a `run` file the flake deletes on its first entry (a leftover the
+   repo has marked for removal), prints the come-back line, and hands off
+   to `nix develop -L`. The first entry can take several minutes while
+   packages download; the installer's last line says so, and a long wait
+   after it is that download.
 
 Inside `nix develop` the flake finishes the job: it clones the repository
 over the ZIP so the folder becomes a real git checkout (the pre-transform
@@ -83,21 +90,27 @@ followed by a short list of words to type.
   restart word. JupyterLab runs in a `tmux` session named `jupyter` and
   keeps running after the shell is left.
 - `walk` is the first word to type: a guided walk over three public pages
-  that teaches the loop. `menu` reprints the short list, `all` the long
-  one, `about` the workspace tree, `voice` answers the question of whether
-  the walk may be read aloud (it is silent until the answer is yes). `exit`
-  leaves the shell.
+  that teaches the loop. On its first run it shows a card asking whether it
+  may read aloud and records the answer; it stays silent until that answer
+  is yes, and `voice` changes it later. `menu` reprints the short list,
+  `all` the long one, `about` the workspace tree. `exit` leaves the shell.
 
 ## 5. What stays on the machine, and what leaves it
 
 Stays: everything runs on localhost, ports 5001 and 8888. The folder holds
 the code, `.venv`, the SQLite databases under `data/`, and the person's own
 notebooks and files under `Workshop/personal/`, which git never tracks.
-Outside the folder: `~/.config/pipulate/` (settings, credentials the person
-adds by hand, the one-line answer to the voice question) and
-`~/.local/share/pipulate/` (two sound files and the voice model).
-
-Leaves, and only these: GitHub (the ZIP, then the clone, then a `git pull`
+Outside the folder, three homes: `~/.config/pipulate/` (settings,
+credentials the person adds by hand, the one-line answer to the voice
+question), `~/.local/share/pipulate/` (two sound files) and
+`~/.local/state/pipulate/` (the compile's own state: `context.txt` when
+`PIPULATE_ADHOC_FILE` points there, and what a move sets aside under
+`stale/`). The browser lanes may leave a driver cache under `~/.cache`; that
+path was not read for this skill. Where the voice model lands is named in
+`imports/voice_synthesis.py`, not here.
+
+Leaves, as the installer and the flake spell it: GitHub (the ZIP, then the
+clone, then a `git pull`
 on every `nix develop`); pipulate.com (the deploy key, from either door);
 install.determinate.systems, only when Nix is missing; the Nix binary cache
 and PyPI, for packages; Hugging Face once, for a small voice model, and only
@@ -126,10 +139,11 @@ removing the folder does not remove Nix.
 
 - It never runs the installer on the person's behalf; it hands them the
   line and reads the script with them.
-- It never edits files in the workshop by hand. Edits go through the patch
-  protocol `AGENTS.md` names (context, prompt, compile, patch, app, d, m).
-  That loop is always-on in `AGENTS.md` rather than a skill, because a skill
-  loads on demand and the loop is every turn.
-- It does not vouch for what the door looks like in a browser; only the
-  text `curl` receives, and the flake it hands off to, were read to write
-  this.
+- It never edits files in the workshop by hand. Edits go through the
+  protocol `AGENTS.md` names: SEARCH/REPLACE blocks, matched exactly once,
+  applied by `apply.py`, which checks Python, Nix and JSON syntax before it
+  writes. That protocol is always-on in `AGENTS.md` rather than a skill,
+  because a skill loads on demand and the protocol is every turn.
+- It was read against four sources: the installer's own text, the door page
+  a browser gets at npvg.org, README's Quick Start and AUDIT.md; where the
+  installer's text and a page disagreed, the installer's text won.
(nix) pipulate $ m
📝 Committing: chore: Update SKILL.md instructions for Nix installer and flake execution
[main fcf6da1d] chore: Update SKILL.md instructions for Nix installer and flake execution
 1 file changed, 39 insertions(+), 25 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'AGENTS.md'.
(nix) pipulate $ d
diff --git a/AGENTS.md b/AGENTS.md
index d3216f92..ec69b8e5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,9 +1,18 @@
 # AGENTS.md — Pipulate
 
-This repo predates the AGENTS.md / Agent Skills / OKF conventions and complies
-with them by *pointing*, not duplicating. The sources of truth here are
-executable, so this file is a signpost. Do not add sibling status .md files;
-anything written here that duplicates code will drift and is a bug.
+This repository carries Agent Skills under https://agentskills.io/specification
+(`.agents/skills/*/SKILL.md`, a YAML head over a Markdown body, loaded on
+demand), with this file as the nearest-ancestor signpost that points at
+executable truth instead of duplicating it. Every tool call taught here runs
+on localhost under the POSIX conventions of a command, stdin, stdout and an
+exit code, so that its output lands in the sealed compile (`foo.zip`) as a
+receipt; a cloud-side tool call that cannot be reconstructed here is
+non-reproducible, non-portable, cannot be compiled into a cartridge, and is
+out of scope by that rule, not by taste.
+
+This repo predates those conventions and complies with them by *pointing*,
+not duplicating. Do not add sibling status .md files; anything written here
+that duplicates code will drift and is a bug.
 
 ## Setup (the executable version of "Dev environment tips")
 
(nix) pipulate $ m
📝 Committing: chore: Update AGENTS.md with Agent Skills specification details
[main 51dacbe1] chore: Update AGENTS.md with Agent Skills specification details
 1 file changed, 13 insertions(+), 4 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'remotes/honeybot/www/npvg.org/index.html'.
(nix) pipulate $ d
diff --git a/remotes/honeybot/www/npvg.org/index.html b/remotes/honeybot/www/npvg.org/index.html
index 7f7dfde9..d8e7e6af 100644
--- a/remotes/honeybot/www/npvg.org/index.html
+++ b/remotes/honeybot/www/npvg.org/index.html
@@ -16,7 +16,7 @@
 <pre>curl -fsSL https://npvg.org | less</pre>
 <p>Then run it:</p>
 <pre>curl -fsSL https://npvg.org | bash</pre>
-<p>This address serves a page to a browser and the installer to a terminal. The first commands lets you read the installer and the second one runs it.</p>
+<p>This address serves a page to a browser and the installer to a terminal. The first command lets you read the installer and the second one runs it.</p>
 <p>What it installs is a reproducible workshop pinned with Nix. Take a reading. Change one thing. Take the reading again. The difference is the receipt.</p>
 <p><a href="https://mikelev.in/">The book this grew out of</a> &middot; <a href="https://pipulate.com/">Pipulate</a></p>
 </body>
(nix) pipulate $ m
📝 Committing: docs: Clarify installer command description in index.html
[main fb73d41e] docs: Clarify installer command description in index.html
 1 file changed, 1 insertion(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
(nix) pipulate $ d
diff --git a/foo_files.py b/foo_files.py
index f3f127e2..0ddc1f75 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -210,7 +210,7 @@ AI_PHOOEY_CHOP = r"""#
 # § THE CONF IS IN THE LINK (banked 2026-09-27, read at deed 1628 off SVB-115's two PocketRender links) -- before writing a client for a vendor's API, decode the vendor's own share link: a URL fragment that reproduces a state carries the whole state (PocketRender's #conf= is base64 of zlib-compressed compact JSON at level 6, nine keys), decodable offline with the standard library and diffable without a render; the API is for what the link cannot say, the metrics. Value: the TODO of the same handle, below; GLOSSARY.md entry owed.
 # § THE JOIN IS THE FIELD, THE HOSTNAME ITS FALLBACK (banked 2026-09-27, read at deed 1627 over 185 board rows) -- join two systems on the field made for the join (Jira's Project-slug to Botify's org/project), derive it from a hostname only when the field is empty, and print which lane each row took beside the row; a join on the derived key alone counts its own guesses as matches. Value: the speed-dating TODO below; GLOSSARY.md entry owed.
 # MOVED FROM apply.py ON 2026-09-26 (commit 7dd5d832, lines 17-381: 22 rules, 365 lines, 25,192 bytes) by THE ACTUATOR IS NOT THE LEDGER. This is the constitution that grew inside the actuator because the actuator rides every compile. forget graduates each rule below to one § key here and its body in GLOSSARY.md, heaviest first; nothing below goes back to apply.py.
-# 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.
+# 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. Convicted 2026-09-28 (deed 1662): the git add for a file a WRITE_FILE car creates rode in its own fence ABOVE that car's fence; the operator pasted in reading order, git answered fatal: pathspec, and the line ran again after app; a hand fence that depends on a car sits below it, in the order the hand will meet them.
 # THE HUMAN IS NOT THE BRANCH PREDICATE (banked 2026-09-24, after the Chrome
 # profile census ride made the operator decide whether Car 1's output licensed
 # Car 2, then whether Car 2 licensed Car 3). A patch train is compiled control
@@ -2587,8 +2587,11 @@ MATCHBOOK_CHOP = r"""
 ! .venv/bin/python -c 'import re;t=open("GLOSSARY.md",encoding="utf-8").read();[print(h,"—",re.sub(r"\s+"," ",p)) for h,p in re.findall(r"^- \*\*([^*]+)\*\* — \*(.+?)\*",t,flags=re.M|re.S)]'
 """
 # #todo #to-do #earmarks
-# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST; read at deed 1661): MANIFEST.in's include and graft reach the sdist (114 members, .agents/ and the two root files in) and never the wheel (81 members, LICENSE alone), and pip install fetches the wheel, so a plain install still carries no AGENTS.md; pyproject's package-data block is a no-op by receipt (none of its five names in either artifact, and CHANGELOG.md names a file the tree does not hold). Ruling owed on whether the agent-facing files should ride a package directory or whether PyPI's rendered README is the wheel user's whole door; cut the dead block after a census of whether cli.py's pipulate install needs a bundled install.sh.
-# - TODO (2026-09-28, the skills' remaining readers): imports/ascii_displays.py 1938, 1945, 1972-73 spell the underscore names in comments (a reseal if they sit in sealed art); ~/repos/Pipulate.com/README.md:3 reads "And context AI_CONTEXT.md", true and pointable at the journal skill; the Claude Code check (six names behind / in a session in this repo, a .claude/skills symlink if not) is unrun; .agents/skills/pipulate/SKILL.md was written at deed 1661 from install.sh and flake.nix alone, and the npvg.org door page, README's Quick Start and AUDIT.md's first-run section are read against it before 2.67 ships it.
+# - TODO (2026-09-28, THE REOPEN LINE NAMES THE OTHER DOOR; read at deed 1662 off install.sh): the nix-missing branch prints curl -fsSL https://pipulate.com/install.sh | bash -s npvg from the npvg door, because the script cannot learn its own address ($0 is bash and the pipe carries none; THE DOOR NAMES THE FOLDER) and KEY_URL stays at the old door on purpose; a stranger who follows that line passes the folder as an argument, which writes whitelabel.txt and names the app Npvg where the plain npvg line names it Pipulate (the 2026-09-18 receipt saw this branch and never rode it). Cure: a second stamped placeholder for the door's URL (a __INSTALL_DEFAULT_URL__ twin, stamped by the same sub_filter on the pad and spelled in two halves the way the name is) and no -s when the folder is the door's default; section 3 of the install skill says what the script prints today. Gate: the reopen line printed from the npvg door reads npvg.org and carries no -s.
+# - TODO (2026-09-28, THE SKILLS INDEX IS A WELL-KNOWN URI; read at deed 1662 off the Claude Code docs' own response headers, rel="agent-skills" beside rel="llms-txt", and the Agent Skills Discovery RFC v0.2.0 it points at): a site announces its skills at /.well-known/agent-skills/index.json, one entry per skill with name, type skill-md, description, url and a sha256 digest, the llms.txt reflex aimed at SKILL.md (pipulate.com already serves llms.txt from generate_llms_txt.py and AI_CONTEXT.md from release.py's sync lane); the index is GENERATED at release from .agents/skills/*/SKILL.md, each file served under /.well-known/agent-skills/<name>/SKILL.md with its digest computed over the bytes served, never authored, and the digest is the cartridge's own rung (THE RECEIPT LADDER). Needs release.py and the Pipulate.com repo in the payload. Gate: the index at pipulate.com reads six entries and each digest matches the served SKILL.md.
+# - TODO (2026-09-28, THE CLAUDE CODE PATH; read at deed 1662 off the Claude Code skills page): Claude Code loads a repository's skills from .claude/skills/<name>/SKILL.md (personal ones from ~/.claude/skills, plugins aside), Codex from .agents/skills, OpenCode from both, so a Claude Code session opened in this repo sees none of the six until .claude/skills exists; the cheapest bridge is one tracked symlink, .claude/skills -> ../.agents/skills (ln -s, git add; .claude/settings.local.json ignored in the same car), and the same page says a .claude-plugin/plugin.json inside a skill folder loads it as a plugin that can bundle hooks and MCP servers, which this repo declines on purpose (the tool call rides the compile as a receipt, never the vendor's session). Ruling owed: whether a vendor directory at the root is the price of the six names behind /. Gate: the / check, the six names offered in a session opened here.
+# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST; read at deed 1661): MANIFEST.in's include and graft reach the sdist (114 members, .agents/ and the two root files in) and never the wheel (81 members, LICENSE alone), and pip install fetches the wheel, so a plain install still carries no AGENTS.md; pyproject's package-data block is a no-op by receipt (none of its five names in either artifact, and CHANGELOG.md names a file the tree does not hold). Ruling owed on whether the agent-facing files should ride a package directory or whether PyPI's rendered README is the wheel user's whole door; cut the dead block after a census of whether cli.py's pipulate install needs a bundled install.sh. READ 2026-09-28 (deed 1662): waits. The wheel is PATH 2's bootstrap (pipx install pipulate, then pipulate install, which README says runs the same universal installer), so every wheel user ends at a git checkout that carries AGENTS.md and .agents/ one step later, and the sdist, the artifact an auditor reads without installing, carries them now; no door a wheel user meets is missing anything. The dead package-data block is a pyproject.toml cleanup when that file rides, gated as written.
+# - TODO (2026-09-28, the skills' remaining readers): imports/ascii_displays.py 1938, 1945, 1972-73 spell the underscore names in comments (a reseal if they sit in sealed art); ~/repos/Pipulate.com/README.md:3 reads "And context AI_CONTEXT.md", true and pointable at the journal skill; the Claude Code check (six names behind / in a session in this repo, a .claude/skills symlink if not) is unrun; .agents/skills/pipulate/SKILL.md was written at deed 1661 from install.sh and flake.nix alone, and the npvg.org door page, README's Quick Start and AUDIT.md's first-run section are read against it before 2.67 ships it. READ 2026-09-28 (deed 1662): the second reading rode as one car against the skill (the nix-missing stop's reopen line, the folder check after the nix check, the -L hand-off and the run leftover, a third home under ~/.local/state, the voice card the walk itself shows, the protocol as AGENTS.md spells it, a sources line, the voice model's home withdrawn until imports/voice_synthesis.py names it); README's Quick Start still says a three-door menu and Press 1 where the skill says a command list and a prompt, and scripts/boot_menu.py rules which sentence is stale before either is patched; the localhost sentence landed in AGENTS.md's first paragraph in the same train; the Claude Code check is now THE CLAUDE CODE PATH, its own line.
 # - TODO (2026-09-28, THE CHEAP FORGETTING; ruled by reading at deed 1655, unridden): forgetting through SEARCH/REPLACE recites the thing forgotten, 63 lines byte-exact for THE FINDING DORY RULE at deed 1654 with one wrong character refunding only the turn, while the two sed cars of the same ride cost a pattern each and landed the same way; the cheap shape names its victim and never recites it: the § key written to a file by a quoted heredoc (a single-quoted delimiter, so nothing expands), one gated sed that reads that file in at the header line and deletes the body by range (the range gated on the header line and the next header line, the trailing blank trimmed per THE SUBSET REPLACE, GO or STOP with the readings beside the word), and the body moved into GLOSSARY.md by a script off the same range (the '# ' prefix rewritten to the glossary's two-space indent, the '- **Handle** — *plain term.*' head the one thing typed), so no byte of the body rides the reply, the way commit 7dd5d832's block moved on 2026-09-26. First rides, each waiting on the operator's yes: THE DIVIDER IS NOT OPTIONAL (2,547 bytes whole by the tightened ranker's count, no bare marker in its body by its own design, its checklist line a prompt_foo.py ride) and the specimen fade by shape (32 paragraphs and 15,923 bytes under two TODOs down to eight lines, one specimen per shape: the sign read backwards, the context row read as the change, one subject for two commits, an earlier subject reused for a router-only bank, a file named that the commit never touched or does not exist, the diff answered as a document, the run-on or comment-carrying subject, the type word wrong). Gate: one graduation lands with its § key in the router, its body in GLOSSARY.md and no body line in the reply.
 # - TOTO (2026-09-28), Endure that the `grim --web` command has an `article` and `bot` equivalent. What's more, make sure those commands are derived from `blogs.nix` and not hard-coded in `flake.nix`.
 # - TODO (2026-09-28, THE ARTICLE LARGER THAN A MINUTE; read at deed 1647 off the grim run): a 915,721-char article estimated 228,930 tokens at chars/4 and the API refused both models at generate_content_free_tier_input_token_count, limit 250000, then the loop waited 57 s for a minute the request could never fit in; the estimate now divides by 3.5, a quota on the input-token metric whose limit is below the estimate stops the run with a SIZE verdict, and the publish words forward their arguments to the editing script alone. The escape today is a key named by hand: a paid Gemini key (billing on one project lifts the minute into the millions on the same tokenizer and a million-token window; a Flash-class price makes such an article cents), or another provider through --model with -k, the wallet's flat alias-to-secret shape unchanged; the window of any other provider is checked against the size line before trusting it. The creative solution for a free key, unbuilt: a map-reduce lane for the editing pass, the article cut at paragraph seams into pieces under the minute, each piece asked for one or two subheadings with a verbatim after_text_snippet and a three-sentence summary, then the template run once on the head plus the summaries for the frontmatter and the analysis, the subheadings merged in article order; N+1 requests against a day of twenty, so a 900K article costs eight; or the cheaper lossy twin, --trim with the middle elided and subheadings for the ends only. Needs editing_prompt.txt and contextualizer.py (the sibling ring) in the payload. Gate for either: one oversized article published on a free key with the spine, the subheadings and the collision guard intact. RULED 2026-09-28 (deed 1648; the Workspace key read free_tier too, and the operator: "not gonna pay", "I don't think chunking is good"): the fences leave first. Past the free minute, or on --lean, every fenced block leaves the editor's copy for a one-line bracketed stand-in naming its language and line count, an editor's note closes the copy, and the prompt is rebuilt and re-read; the post is built from the original article_text, so every snippet the model quotes from prose still matches, and the note forbids quoting a stand-in. The spine lands in the template before the article, so an article quoting the placeholder is left alone. Map-reduce and --trim are dropped. b2 sweeps the wallet through backup-home.py's Key Configs include (pipulate at line 63; the stale articleizer entry beside it is harmless). Gate: the fence probe reads the lean copy under 250,000 on the specimen, and one oversized article publishes on a free key with its subheadings placed. READ 2026-09-28 (deed 1650): the lean copy read 87 fences, 894,222 chars to 106,111, about 30,317 tokens, and grim's scissors line 261,634 tokens to 36,527 on a 127,846-char prompt; then the API failed three more ways: "high demand" on Lite every time at any size, so it was never the size; "The service is currently unavailable" with no status code, read as unrecoverable; and a ring of 26 that never turned, its rule wanting quota from every model while Lite only ever said high demand, the operator cutting it at the fourth wait. The same metric, free_tier_requests, carried limit 5 (the minute) and limit 20 (the day). RULED (the operator: "make it give really good instructions on how to do this in the Web UI, can we make it super simple?", "anti-fragility, always another way?"): THE WEB LANE. Every exit that leaves no instructions (unrecoverable, exhausted, SIZE, a wait cut by hand, or --web on purpose) puts the prompt on the clipboard and prints four steps: paste into any AI chat in a browser, copy the whole reply, press Enter; the JSON is read off the clipboard, refused if it is the prompt's own schema pasted back, cached, and the post built by the same code as after an API answer, article.txt untouched, which the old --copy then --local lane could not promise because the publish word rewrites article.txt from the clipboard first. The ring turns once a quota is reported on a key and every model on it has failed, or once its ten attempts are spent, and leaves the last key the same way; unavailable and overloaded are transient; one parser serves both lanes. Gate: one publish end to end through the web lane, and one ring turn witnessed on -m all.
@@ -2626,7 +2629,7 @@ MATCHBOOK_CHOP = r"""
 # - TODO (2026-09-27, the qamy.ai door; the operator's ruling at the dismount: today): the domain names the method (QA My AI, the proof straddle's public face) and serves one page until it says more. The door is the npvg.org door with one token changed: a virtualHosts entry in remotes/honeybot/nixos/configuration.nix (this repo, chapter IX) beside npvg.org, HTTP-only until the name resolves, then forceSSL plus enableACME on the shared acme-challenge webroot (THE ACME CAR), and the installer's sub_filter stamp only if the installer is to be served there (THE DOOR NAMES THE FOLDER: its own placeholder, never a hostname rewrite); the body a page of its own under remotes/honeybot/www/qamy.ai, rsynced by nixops.sh, no rebuild for a body-only change and one rebuild on Honeybot for the vhost. THE ONE STEP NO REPO MAKES comes first, by hand, because propagation is the slow part: an A record at the registrar to the address npvg.org already carries (dig +short npvg.org is the value; dig +short qamy.ai empty is the BEFORE, the address is the AFTER), TTL 300, and it joins the 2026-09-13 debt, a third static A record on one public address with the DDNS heartbeat on mikelev.in alone, and the favicon 404 line. Order: the record now; the vhost car with configuration.nix and nixops.sh in the payload; the deploy; the cellular witness with a tag minted in the caboose (THE OUT-OF-BAND STEP RULE). Gate: curl -sI https://qamy.ai from outside reads 200 with a Let's Encrypt issuer, and the tag sits in its access log. A RECORD MADE 2026-09-27 (the operator's paste at deed 1626: A at the apex, TTL 1 min, a www CNAME to the apex): dig +short qamy.ai reads the public address from inside the LAN while dig +short npvg.org reads the LAN address, so the house's split DNS carries no qamy.ai line and a browser inside the house will hairpin or fail until the resolver that answers npvg.org internally gets the same line for qamy.ai (where that resolver lives is unread: the router, or a hosts entry in the NixOS config; it rides the vhost car's compile). THE RECEIPT CARRIED THE ADDRESS BARE: two ! dig lines seeded at deed 1625 put the home's public address into a trusted payload the operator's own paste had redacted; a pii_substitutions.txt line for it is the HALT of deed 1626, and every DNS probe since prints a verdict word, never the value. READ 2026-09-27 (deed 1627): from 1.1.1.1 the answer for qamy.ai differs from mikelev.in's or is empty; three worlds print that word (a negative answer cached before the record existed; a value that is not npvg.org's; npvg.org's static A drifted off mikelev.in's DDNS address, the 2026-09-13 debt come due, with qamy.ai a copy of the stale number and the pad dark from outside), and four verdict probes ride deed 1628 to split them: the answer count at 1.1.1.1 and at the zone's own nameserver, qamy.ai against npvg.org, npvg.org against mikelev.in. READ 2026-09-27 (deed 1628): qamy_public_answers=1 and qamy_authoritative_answers=1 via dns1.registrar-servers.com, so the record exists and 1.1.1.1 holds it; qamy_matches_npvg_public; npvg_differs_from_mikelevin_public. On 2026-09-13 the ACME challenge for npvg.org reached the pad, so the two names agreed then; the DDNS unit moves mikelev.in and nothing moves npvg.org, and outside traffic has reached mikelev.in since the 2026-09-24 outage (the hydration count moved), so the reading is npvg.org stale since the WAN address changed, qamy.ai a copy of the stale number, the pad dark from outside and npvg.org's December renewal set to fail, INFERRED until the egress probe of deed 1629 prints mikelevin_current_npvg_and_qamy_stale. Deed 1627's caboose pre-committed a HALT to a word two worlds print (THE CABOOSE-VERDICT COROLLARY), so the registrar step rides deed 1629's HALT with one action once the word is read; the standing fix is the DDNS unit covering all three names, the 2026-09-13 line, a services.nix car. READ 2026-09-27 (deed 1629): mikelevin_current_npvg_and_qamy_stale, the forecast word; the operator set npvg.org's and qamy.ai's A records at the registrar by hand before the reply landed (the loss of power to the building changed the WAN address and only mikelev.in's DDNS followed it), so no HALT rode and the AFTER is the same probe reading all_three_current once the TTLs pass (qamy.ai one minute, npvg.org's unread). The address rode this article bare in the operator's own paste and appears in no probe and no reply; the pii_substitutions.txt line covers the publish lane, and under --profile trusted substitutions are OFF by design (deed 1627's terminal: substitutions=OFF), so the HALT of deed 1626 named the wrong lane for a bjj payload, a miss of mine, and verdict words were the right cure. Still owed: the DDNS unit for all three names, then the vhost car. READ 2026-09-27 (deed 1630): all_three_current.
 # - TODO (2026-09-27, the word bjj; flake.nix not in this compile): the operator's compile spelling on Prime is compile --profile trusted --reason "testing" --tree and wants to be one word, "because Brazilian Jiu Jitsu is objectively the best Kung Fu". The car, when flake.nix rides, one line beside ahc: bjj() { (cd "$PIPULATE_ROOT" && python prompt_foo.py --chop ADHOC_CHOP --profile trusted --reason "bjj: a trusted compile from the workbench" "$@"); } -- --tree is not spelled because the flip of 0bce3aae made it a no-op there. Ignition exit then ndq. Gate: bjj compiles with the Summary's Command line reading --profile trusted and the identity-scrub line printing, and type bjj at a (nix) prompt reads "bjj is a function". CAR EMITTED 2026-09-27 (deed 1623, flake.nix in that payload): the line beside ahcu, ignition exit then ndq; the gate is the next compile typed bjj. LANDED 2026-09-27 (266b5e18; deed 1625 was compiled by bjj: the Summary's Command line reads --profile trusted --reason bjj: a trusted compile from the workbench, the ignition witnessed by the compiler's own argv, and the flake grep read 0 -> 1). THE HOOK PROBE WENT BLIND in the same compile: LD_LIBRARY_PATH="" nix eval --raw .#devShells.x86_64-linux.quiet.shellHook refused in the compile lane with "experimental Nix feature 'dynamic-derivations' is disabled", exit 1, stdout 0, where the same evaluation read adhoc=2 notree=0 at deed 1620 and 0 by hand at this deed's BEFORE; the shell (exit then ndq) and the flake (one bjj line) both moved between those readings, and by hand the word nix is the flake's shim function while in the ! executor it is the PATH binary, so the cause is unread. A 0 from that probe is VOID, never a count, until command -v nix, nix --version and the eval's own stderr ride a compile. The next forget ride fades the landed half. READ 2026-09-27 (deed 1626): command -v nix is /home/mike/.nix-profile/bin/nix, version 2.25.0pre20240910, a two-year-old prerelease in the user profile that shadows the system nix in the compile lane while the hand lane's nix is the flake's shim; the eval exited 0 after 11 s in one run and printed the dynamic-derivations line on stderr in the next run 0.2 s later, two readings from two runs and neither carrying both. One probe reading exit, stdout bytes, the bjj count and the stderr head in a single run rides deed 1627, with type nix in each lane and the eval-cache listing; a ~/.cache/nix shared between two nix versions is the named suspect, UNREAD. READ 2026-09-27 (deed 1627), the lanes swapped: by hand exit 1, bytes 0, the dynamic-derivations line; in the compile exit 0, 77,110 bytes, bjj=1, stderr empty, 11 s; the compile's nix is ~/.nix-profile/bin/nix and the hand's is the flake's function; the cache dir holds eval-cache-v5 and v6. THE CACHE HIT IS THE REFUSAL, INFERRED from every reading so far: each 11 s run succeeded and each 0.2 s run refused, and every fast run was the second evaluation of one flake fingerprint (1624 then 1625, 1626's probe 3 then 4, 1626's compile then 1627's hand BEFORE), so nix 2.25.0pre20240910 evaluates the hook and trips reading it back from its own eval cache. The falsifier rides deed 1628: two runs in one probe on a fresh tree (forecast success then refusal) beside two --no-eval-cache runs (forecast success twice), and each lane's nix --version. If it holds, the probe's cure is --no-eval-cache and the box's cure is retiring the prerelease from ~/.nix-profile so one nix serves both lanes, a hand step for its own line. READ 2026-09-27 (deed 1628), the forecast shape exactly: by hand both cached runs refused (the 1627 compile had filled the cache for that tree), in the compile cached_run1 exit 0 at 77,110 bytes and cached_run2 refused, and the uncached pair exit 0 twice in both lanes. THE CACHE HIT IS THE REFUSAL, confirmed; --no-eval-cache is the hook probe's spelling from here. Both lanes run the prerelease (nix --version reads 2.25.0pre20240910 by hand and in the compile, so the flake's function wraps the PATH binary) while /run/current-system/sw/bin/nix reads 2.28.5, so retiring ~/.nix-profile's nix lets the system's serve both lanes; the profile's kind (manifest.json is nix profile, manifest.nix is nix-env) rides deed 1629 and names the removal command, a hand step. READ 2026-09-27 (deed 1629): ~/.nix-profile carries manifest.nix, a nix-env profile, so the removal is LD_LIBRARY_PATH="" nix-env -e nix and its undo LD_LIBRARY_PATH="" nix-env --rollback; it waits one turn, because whether the flake's nix() function wraps the PATH binary or names the profile's path is unread and flake.nix rides deed 1630 for the rename, so the step follows that reading; nix-env -q rides the compile as its BEFORE. Gate: which nix reads /run/current-system/sw/bin/nix in the compile lane, nix --version 2.28.5 in both, and the --no-eval-cache hook eval exit 0 at 77,110 bytes under the system nix. READ 2026-09-27 (deed 1630): the flake's nix() calls command nix, the PATH binary, so retiring the profile's nix moves both lanes to /run/current-system/sw/bin/nix at the next lookup (hash -r in the live shell); the step rides deed 1630's last fence after the rename's ignition, so the two changes are read by different probes (which nix and nix --version for the retirement; the rg census for the rename), with the hook eval the one shared instrument and its BEFORE 77,110 under the prerelease. Undo: LD_LIBRARY_PATH="" nix-env --rollback. LANDED 2026-09-27 (deed 1630's Car 10, by hand after the ignition; READ at deed 1631): nix-env -e nix uninstalled nix-2.25.0pre20240910_b9d3cdfb; command -v nix reads /run/current-system/sw/bin/nix in the compile lane (by hand it prints the bare word nix, the flake's function, whose command nix now finds the system binary), nix --version 2.28.5 in both lanes, nix-env -q appimage-run and ungoogled-chromium with no nix, and the --no-eval-cache hook eval exit 0 at 77,416 bytes under the system nix (77,110 plus the rename's bytes). "hash: hashing disabled" on the hand step is the nix shell's bash with hashing off, harmless. The dynamic-derivations refusal cannot recur on this box; --no-eval-cache stays in the probe's spelling as a habit. This TODO's job is done; the next forget ride fades it.
 # - TODO (2026-09-27, command stops in a walk; asked after the corpus ride, RESTATED and not ridden): today a stop is a page (url or url_env) the rider opens, and a stop's connector is argv the planner validates and nothing runs (the AUTH RULING, chapter VIII-b). The ask: a third stop kind whose payload is a COMMAND the operator would otherwise paste, e.g. a three-stop "SpeedWorkers corpus" trail of botify --sw, botify --pull-configs and botify --rules, where the rider speaks the guidance, prints the command, waits for the word (the fence), runs it in the operator's own terminal with stdout as the receipt, and advances; a Chrome the command opens is the command's own (the census harvests its cookie) and never the rider's. Shape: exactly one of url, url_env or command per stop, walk.py's set-difference validation extended, walk_compile.py and the sealer carrying the new key, and the receipt banked the way captures.md banks bytes. The Workshop tree the operator drew rides with it: corporate/ a read-only lowerdir (the org's AGENTS.md, .agents/skills, log.md), shared/<name>/ one writer each with its own AGENTS.md (nearest ancestor wins inside the namespace) and skills overlaid on corporate's, personal/ gitignored and unreachable by any git verb; the mechanism (rider, compiler, connectors) stays in Pipulate and the data (trails, exports, client slugs, and the admin door's shapes that sit in botify.py today, no scrub owed) moves to private repos mounted into that tree, the blogs.nix -> blogs.json split applied to skills. Not this weekend. Gate: one command-stop trail ridden end to end with its three receipts in one compile; needs scripts/walk.py, scripts/mother_cat.py and scripts/walk_compile.py in the payload.
-# - TODO (2026-09-27, AGENTS.md and AI_CONTEXT.md must name the specification; the operator's words, its own ride): a stranger's model, or an IT department come to forbid it, reading AGENTS.md or AI_CONTEXT.md must see on the first screen that this repository is an Agent Skill under https://agentskills.io/specification (a SKILL.md folder with YAML frontmatter and progressive disclosure, AGENTS.md as the nearest-ancestor signpost; Notebooks/.agents/skills already conforms, THREE-STANDARDS SUPERPOSITION), and that every tool call it teaches is forced onto localhost under the fifty-year POSIX conventions (a command, stdin, stdout, an exit code) so that it lands in the flight data recorder: a cloud tool call that cannot be reconstructed locally is non-reproducible, non-portable and cannot be compiled into a cartridge, and is out of scope by that rule and not by taste. The ride needs AGENTS.md, AI_CONTEXT.md and !https://agentskills.io/specification in the payload, the specification read before a word is written and the wording checked against it. Gate: a fresh chat handed AGENTS.md alone names the specification and the localhost rule in its first two sentences. THE ORDER, ruled at the 2026-09-27 dismount: after the first ticket, the deliverable and the render farm, and before the Workshop tree; with that tree landed under Notebooks (the command-stop TODO), the repository is the worked example the specification's readers can run on localhost, the operator's stated aim, "the quintessential and prototypical example". READ 2026-09-28 (deed 1661): the URL rides AGENTS.md's Skills line since daed3d78, half the gate; the localhost sentence is still owed, worded after the specification is read into a payload, and it rides the install skill's second-reading turn.
+# - TODO (2026-09-27, AGENTS.md and AI_CONTEXT.md must name the specification; the operator's words, its own ride): a stranger's model, or an IT department come to forbid it, reading AGENTS.md or AI_CONTEXT.md must see on the first screen that this repository is an Agent Skill under https://agentskills.io/specification (a SKILL.md folder with YAML frontmatter and progressive disclosure, AGENTS.md as the nearest-ancestor signpost; Notebooks/.agents/skills already conforms, THREE-STANDARDS SUPERPOSITION), and that every tool call it teaches is forced onto localhost under the fifty-year POSIX conventions (a command, stdin, stdout, an exit code) so that it lands in the flight data recorder: a cloud tool call that cannot be reconstructed locally is non-reproducible, non-portable and cannot be compiled into a cartridge, and is out of scope by that rule and not by taste. The ride needs AGENTS.md, AI_CONTEXT.md and !https://agentskills.io/specification in the payload, the specification read before a word is written and the wording checked against it. Gate: a fresh chat handed AGENTS.md alone names the specification and the localhost rule in its first two sentences. THE ORDER, ruled at the 2026-09-27 dismount: after the first ticket, the deliverable and the render farm, and before the Workshop tree; with that tree landed under Notebooks (the command-stop TODO), the repository is the worked example the specification's readers can run on localhost, the operator's stated aim, "the quintessential and prototypical example". READ 2026-09-28 (deed 1661): the URL rides AGENTS.md's Skills line since daed3d78, half the gate; the localhost sentence is still owed, worded after the specification is read into a payload, and it rides the install skill's second-reading turn. LANDED 2026-09-28 (deed 1662): the first paragraph names the specification in its first sentence and the localhost rule in its second, the old paragraph kept beneath with its duplicate clause cut; the gate (a fresh chat handed AGENTS.md alone) is a hand step, one paste into any chat window, and its receipt is the model's first two sentences back.
 # - TODO (2026-09-27, THE RENDER FARM HAS A NAME; from a colleague's Slack note the same day): PocketRender opened with ?webmcp=1 (app.botify.com/tools/cpap/pocketrender/index.html) exposes pr_* tools to any MCP client through a stdio relay, npx -y @mcp-b/webmcp-local-relay@latest --widget-origin https://app.botify.com (configs given for Claude Desktop, Claude Code, Cursor and Codex; a locally served PR adds http://localhost:8642 to the origin list, comma-separated), with the Chrome flags enable-webmcp-testing on and local-network-access-check off and a logged-in PR tab open. Three gaps before Pipulate drives it, each a reading before a car: connectors/mcp.py speaks Streamable HTTP and the relay speaks stdio, so a subprocess JSON-RPC lane is owed (or the relay's HTTP port, if it has one, unread); node and npx are in neither commonPackages nor the flake, and read node=present npx=present on Prime (2026-09-27 afternoon compile, deed 1620), INFERRED from the system's packages.nix since flake.nix lists no nodejs, so the Mac and a stranger's install read absent until commonPackages carries it; the two flags on the persistent uc profile are unread (a Chromium switch, or one hand toggle in the botify profile). The PROOF STRADDLE it serves: BEFORE the page as served (?URL optics), the change a rule, AFTER the PocketRender render, the diff-hierarchy lens as the middle panel, every pr_* call reported per THE MCP RECEIPT RULE. FIRST READING, when the ride opens (after the first ticket): npx -y @mcp-b/webmcp-local-relay@latest --help by hand, never a ! line, because it downloads and runs a package and a human reads its output before a compile does; then the relay's transport, a stdio JSON-RPC lane in connectors/mcp.py (a subprocess, initialize then tools/list then tools/call over its stdin and stdout, each call a receipt); then the two Chrome flags on the botify uc profile (chrome://flags is a hand toggle that persists in the profile, or two switches on the argv, unread); then one pr_* tool called with '{}' for its schema, the AFTER panel of THE PROOF STRADDLE. READ 2026-09-28 (deed 1640, the --help by hand): the relay's usage names --host 127.0.0.1 and --port 9333 for a local WebSocket relay, --widget-origin for the page origins it admits, --invoke-timeout 65000 ms for a browser tool call and --max-payload 10 MB, so the WebSocket is the face toward the PocketRender tab; the face toward the client is unnamed and INFERRED stdio, because the client configs launch the relay as a command under an MCP-server key; npm 10.9.3 printed its own upgrade notice and no download line. connectors/mcp_render.py grew tools, schema and call at 1640 (one RelaySession: spawn with LD_LIBRARY_PATH cleared, initialize, notifications/initialized, then request by id on stdio, an MCP RECEIPT line per call, verdict tokens RELAY_INIT_OK, RELAY_TOOLS n=, RELAY_CALL_OK, RELAY_INIT_NO_REPLY, RELAY_TOOLS_NO_REPLY, RELAY_CALL_ERROR); the first hand run with no tab is the transport's witness (an initialize reply on stdout), the tab (the two chrome://flags, ?webmcp=1, logged in) the next hand step, both UNWITNESSED at this deed. READ 2026-09-28 (deed 1641, render tools by hand with no tab): RELAY_INIT_OK protocol=2025-06-18 server=webmcp-local-relay 0.0.0, then RELAY_TOOLS n=3, so stdio is OBSERVED and the n=0 forecast was wrong: the relay carries three tools of its own without a tab (webmcp_list_sources, webmcp_list_tools, webmcp_open_page, "Open a URL in the user's default browser") and the pr_* tools sit behind a connected source, reached through webmcp_list_tools or a tools/list that grows on connection, unread which; version 0.0.0 means serverInfo cannot name the pin. INFERRED, THE RELAY IS SHORT-LIVED: each render lane spawns its own relay and closes it within seconds while the tab connects to 127.0.0.1:9333 on its own schedule, so --settle N (deed 1641's car) sleeps after initialize to give it a window; a persistent relay in a second terminal would take the port and the tab with it, so none is run. The tab (the two chrome://flags in the everyday Chrome, ?webmcp=1, logged in) and render tools --settle 5, render call webmcp_list_sources, render call webmcp_list_tools are deed 1641's hand step. READ 2026-09-28 (deed 1642): no tab, the flag absent from Prime's Chrome 150 and per-profile toggles refused (THE BROWSER IS NOT A CONFIG FILE); render tools --settle 5 read n=3, webmcp_list_sources count 0, webmcp_list_tools count 0, by hand and in the compile lane alike (6.4 s a lane for the settle; the three lines leave context.txt, a ritual with no tab possible). The relay is proven and empty. Two WORA lanes, each a reading before a car: (a) the flake's pinned Chromium 150.0.7871.128 driven by the scraper with a WebMCP switch on its argv, if that build carries the feature at all (deed 1642's probe greps the unwrapped binary for the word; 0 hits means the lane waits on a nixpkgs Chromium bump); (b) PocketRender's own backend calls, read off the wire at a walk's CAPTURE fence after a human clicks Render (SETTLE in its non-trivial form), %URL naming the endpoint and its POST body, then a cookie-lane connector the way the admin door harvests (THE POCKETRENDER VERSE earmark is the stop). The pr_* tools are the vendor's convenience and not the only door; the deliverable's render columns stay UNRENDERED until one lane reads. READ 2026-09-28 (deed 1643, the binary): the flake's unwrapped chromium-150.0.7871.128 carries the word (17 lines; WebMCP 134, WebMCPHandler 18, WebMCPAgent 16, WebMCPAgentEE 13, WebMCPAgentENS 9 as the five commonest strings), so lane (a) is live: the build has the code, and the flag's absence from the everyday browser (xdg-settings names google-chrome.desktop, Google Chrome 150, the browser the relay's webmcp_open_page would open) says nothing about the flake's Chromium. The switch is READ off the binary before any argv is tried (deed 1644's probe: the strings past the fifth, the flag's own name enable-webmcp-testing, the LocalNetworkAccess strings for the second flag): --enable-features=<the feature's name> if it is a base::Feature, --enable-blink-features=<name> if a runtime-enabled one, --disable-features=<the local-network check's name> beside it, INFERRED forms until read. Then tools/scraper_tools.py rides before the car: a lane that opens PocketRender ?webmcp=1 on the botify profile (weblogin --profile botify) in the flake's Chromium with those switches, holds the window (a CAPTURE fence is the natural hold) and runs render tools --settle while it stands; the pr_* list is the gate. Never a hand toggle. READ 2026-09-28 (deed 1644, the strings past the fifth): WebMCPTestingEnabledEv and WebMCPTestingEnabledEb (2 each), WebMCPFormAssociatedCustomElementsEnabledEv and Eb, WebMCPSupport, WebMCPToolRegistration, and flag_name=1 (enable-webmcp-testing is in the binary once); the Ev/Eb pairs are INFERRED Blink RuntimeEnabledFeatures accessors (XEnabled() and SetXEnabled(bool), mangled), so the features are named WebMCPTesting, WebMCP and WebMCPFormAssociatedCustomElements and the candidate argv is --enable-blink-features=WebMCPTesting,WebMCP first, --enable-features=WebMCPTesting second if the flags entry wraps a base::Feature; the second flag's feature name is not in this census (the LocalNetworkAccess strings read are class names, LocalNetworkAccessPermissionRequired 16 the commonest), so --disable-features=LocalNetworkAccessChecks is INFERRED from the flag's own name and unread. The next reading is the scraper's: tools/scraper_tools.py in the payload, a lane opening PocketRender ?webmcp=1 on the botify profile with those switches and holding the window while render tools --settle reads; the pr_* list the gate.
 # - TODO (2026-09-27, the speed-dating queue; jira.py not in this compile): a board URL reduces to its project key and /boards/<id> is dropped without a word (jira.py's own comment at its URL parser, read 2026-09-23), so the SVB board-453 queue is unbuilt; the scope is board configuration -> filter.id -> /rest/api/3/filter/<id> for its JQL, then each issue's Project URL field to botify --rules org/project, reported as counts before any listing. Gates before the car: jira --check GREEN on Prime (the Mac's slot was cold), and connectors/jira.py in context. LANDED 2026-09-27 (5410ddbe, first flight at deed 1623): board 453 is "SVB board", type simple, filter 15051, JQL project = SVB ORDER BY Rank ASC, so the board is the whole project by rank and both documented paths answered 200. THE COUNTS LINE READ 0 with a Project URL among 100 issues while the field id resolved (no no-field note printed), and two worlds print that line alike: the wrong id behind a duplicate label (_project_url_field keeps the first match, SINGLE-CANDIDATE BLINDNESS) or the right id on the hundred lowest-ranked issues. The probes that separate them: every field whose name carries url, with ids; a JQL "Project URL" is not EMPTY count, server-side by name; the counts line at -n 500. No ticket pair can be listed until one row carries a slug with a local pull. READ 2026-09-27 (deed 1625, three CENSUS lines): the site carries TWO custom fields named Project URL, customfield_12188 and customfield_12189, so _project_url_field's first match was one of two, SINGLE-CANDIDATE BLINDNESS convicted by a census rather than by a second candidate arriving; the JQL "Project URL" is not EMPTY read rows=0 under a clean header, Jira having resolved the duplicate name to one field without saying which and never raising an ambiguity error; and -n 500 returned exactly 100 with no cap line, because the connector says "hit the -n cap" only when the page fills -n, so a server page smaller than -n reads as a complete list, THE FULL PAGE WAS SILENT in its second shape (the enhanced search's page is INFERRED at 100 from 500 asked, and its nextPageToken is never walked by design). Also: ORDER BY Rank ASC is the TOP of the board, so the hundred are the highest-ranked, not the lowest. Three worlds remain: the other id is the populated one; neither id is populated on the top hundred and the URL lives in the description; neither is populated anywhere. The falsifier is by id and never by name: cf[12188] is not EMPTY and cf[12189] is not EMPTY, each a count. The car, once a populated issue is known: _project_url_field returns every id under the label and list_board_issues reads whichever is filled per issue, and the counts line names the server's page size when the page came back smaller than -n. LANDED 2026-09-27 (deed 1626's Car 1, before a populated issue was known, because both ids read 0 and the text is the next candidate): _search_pages walks nextPageToken up to -n and returns the exhausted flag, _project_url_fields returns every id, a summary or description link stands in, and the counts carry a by-status line. Gate: jira board:453 -n 200 reads 162 issues, 2 pages, exhausted, Done 137, and the status loop reads the operator's four column counts. READ 2026-09-27 (deed 1627): 185 issues, 2 pages, exhausted, Done 158, To Do 15, In Review 11, In Progress 1, both lanes agreeing; the board's face said 162, so the gate's numbers were the face's and the server's are 23 higher, INFERRED epics and sub-tasks; the open rows moved 7 -> 27 across the paging; 1 with a link (SVB-258, coupang-org/tw.coupang.com) and 0 local pulls, so deed 1627's Car 1 joins by the Project-slug field and the summary's hostname against data/botify_pulls and prints a by-type line. Gate: the by-type line sums to 185 with Epic and Sub-task among its names, and the open rows carry -> host or -> org/project tails. READ 2026-09-27 (deed 1628): the by-type line sums to 185 (Rendering MiniRules 182, Epic 2, HTML Extract settings 1) with no Sub-task, so that half of the gate was a wrong forecast; every open row carries a tail, 25 from Project-slug and the 2 epics none, and the hostname fallback never fired (THE JOIN IS THE FIELD, the comment retitled at deed 1628). The queue by shape: 11 music.amazon.* tickets on one config family (the batch the operator said could not exist; one ride writes the shape the other ten reuse), 5 servicenow, and skechers, pluto, golfbreaks, zappos, monicavinader, coupang and hubspot one each. This line's job is done once the first ticket rides; the next forget ride fades it.
 # - TODO (2026-09-27, the first tree ride): annotate_tree_with_tokens in prompt_foo.py stamps a count by basename, so the three README.md lines in the File Tree (root, assets/sounds, connectors) all read connectors/README.md's 2,742 tokens; match on a path the tree's indentation can reconstruct, or stamp nothing on a basename that occurs twice. In the same car, the placeholder "generation failed or was skipped" is two worlds in one label: say which. One car when a ride owns it.
(nix) pipulate $ m
📝 Committing: fix: refine message formatting, reduce redundancy, and improve clarity
[main e8ae78d9] fix: refine message formatting, reduce redundancy, and improve clarity
 1 file changed, 7 insertions(+), 4 deletions(-)
(nix) pipulate $ git push
Enumerating objects: 31, done.
Counting objects: 100% (31/31), done.
Delta compression using up to 48 threads
Compressing objects: 100% (15/15), done.
Writing objects: 100% (19/19), 5.41 KiB | 503.00 KiB/s, done.
Total 19 (delta 10), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (10/10), completed with 7 local objects.
To github.com:pipulate/pipulate.git
   e5936acc..e8ae78d9  main -> main
(nix) pipulate $ 

Ignition:

(nix) pipulate $ g

Left-hand causal boundary "blast radius" established. Make 1 change and test.
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ nixops
🚀 Syncing Hooks...
post-receive                                                                                 100% 3080   572.3KB/s   00:00    
🚀 Syncing Scripts (New Location)...
sending incremental file list

sent 700 bytes  received 13 bytes  1,426.00 bytes/sec
total size is 190,587  speedup is 267.30
sending incremental file list

sent 68 bytes  received 12 bytes  160.00 bytes/sec
total size is 140,470  speedup is 1,755.88
🚀 Syncing NPvg pad (one address, two bodies)...
sending incremental file list
index.html

sent 729 bytes  received 51 bytes  1,560.00 bytes/sec
total size is 5,440  speedup is 6.97
sending incremental file list

sent 62 bytes  received 12 bytes  49.33 bytes/sec
total size is 16,852  speedup is 227.73
🚀 Syncing NixOS Config...
sending incremental file list

sent 119 bytes  received 12 bytes  262.00 bytes/sec
total size is 23,992  speedup is 183.15
✅ Sync Complete.
   To apply NixOS config: ssh -t mike@[REDACTED_IP] 'sudo cp ~/nixos-config-staged/* /etc/nixos/ && sudo nixos-rebuild switch'
(nix) pipulate $ ssh -t mike@[REDACTED_IP] 'sudo cp ~/nixos-config-staged/* /etc/nixos/ && sudo nixos-rebuild switch'
[sudo] password for mike: 
building the system configuration...
evaluation warning: The option `services.logind.lidSwitchExternalPower' defined in `/etc/nixos/configuration.nix' has been renamed to `services.logind.settings.Login.HandleLidSwitchExternalPower'.
evaluation warning: The option `services.logind.lidSwitch' defined in `/etc/nixos/configuration.nix' has been renamed to `services.logind.settings.Login.HandleLidSwitch'.
evaluation warning: The option `services.xserver.displayManager.gdm.wayland' defined in `/etc/nixos/configuration.nix' has been renamed to `services.displayManager.gdm.wayland'.
evaluation warning: The option `services.xserver.displayManager.gdm.enable' defined in `/etc/nixos/configuration.nix' has been renamed to `services.displayManager.gdm.enable'.
activating the configuration...
setting up /etc...
reloading user units for mike...
restarting sysinit-reactivation.target
the following new units were started: NetworkManager-dispatcher.service
Done. The new configuration is /nix/store/mizdf6qqih4nz4n9ma2i4zm78xp4nll4-nixos-system-honeybot-26.05pre913595.c6245e83d836
Connection to [REDACTED_IP] closed.
(nix) pipulate $ 

4: Prompt: Read the receipts first: the AGENTS.md head (2), the skill’s retired phrases (0) and new phrases (3), the door page (1), the deed count (4), three-door (3, a census), the boot_menu.py keypress line, and the voice_synthesis paths. Then, with scripts/boot_menu.py in the payload, patch whichever sentence the keypress reading convicts: README’s three three-door sentences (Quick Start, the automation table, the Big Reset) if the menu is a command list, or the skill’s “stops at the short command list” sentence if the number gate lives; and write the voice model’s home into the skill from the paths the probe printed. Rule on THE CLAUDE CODE PATH: the symlink or not. Then dismount this article. After it is published and prompt.md is out of the way (rm prompt.md at the root; it is gitignored and prompt regenerates it), THE FIRST GAME opens by hand at a (nix) prompt: p, then .venv/bin/python Workshop/corporate/connectors/svb.py SVB-133 –write –prompt, then context to read the board it wrote, then bjj; the board is the next compile’s Context and SVB_PROMPT.md is its prompt.

5: Deliverables: Better internal consistency in the deliverable, so a better repo and Pipulate project. Let’s do one more 5-Car Train PROOF STRADDLE and try to get to the article wrap-up. Don’t wrap the article this turn. Focus fully on final grinding and polishing of the lenses here and turn after this we’ll wrap.

Fable 5.1 Thinking

I'll check one convention fact before ruling on THE CLAUDE CODE PATH, since the ruling hinges on whether Claude Code already reads `.agents/skills`.

Read a page<https://code.claude.com/docs/en/skills>

Checking how skills load from project and symlinked folders.

Weighing symlink options against documented skill-loading behavior.

Choosing per-skill symlinks over one directory-level symlink.

Favoring structural safety over documented convention despite drift risk.

Checking whether the symlink would leak into packaged distributions.

Building precise text-match probes for documentation change verification.

Verifying exact grep counts for each documentation probe.

Sequencing patch, symlink, and commit steps into one ordered plan.

Splitting steps into one commit to keep claims consistent.

Drafting the AGENTS.md wording and finalized ruling text.

Organizing the response into ruling, patches, and citations.

Cross-checking exact wording against the original documentation text.

Matching removal commands against exact original file text.

Deciding whether to add manual verification steps as hand-off instructions.

Checking whether a needed tool is even installed.

Assembling the final ruling into fenced patches and citations.

Drafting shell probes to verify each documentation claim.

Fable 5.1 Extra: I’ll check one convention fact before ruling on THE CLAUDE CODE PATH, since the ruling hinges on whether Claude Code already reads .agents/skills.

Tool calls this turn: one web fetch, the Claude Code skills page, research phase, exempt under your rule; no API calls, no MCP.

Context verified. Every file a car below patches is in the Codebase (README.md, .agents/skills/pipulate/SKILL.md, AGENTS.md, .gitignore, foo_files.py); scripts/boot_menu.py reaches past the standard library only in its --about branch, which nothing here touches. flake.nix is absent, and the one place that matters is a census probe that reads it on your machine rather than a sentence that guesses at it. No dismount this turn, as you said; the caboose stages it.

The receipts, read

All eight forecasts landed. The AGENTS.md head reads 2 (the specification on line 3, localhost on line 7). The skill’s three retired phrases read 0 with grep’s exit 1, which is that shape’s zero, and its three new phrases read 3. The door page reads 1, and the nixops receipt shows index.html as the one file the pad’s rsync sent, so the served copy carries the fix; the rebuild that followed restarted nothing of ours, a body-only change as the 2026-09-14 receipt said. The deed count reads 4. three-door reads 3, the census.

The two readings that were readings and not straddles: boot_menu_keypress_lines= rc=1 is ripgrep’s true zero (no output, exit 1, no error text in the merged stream), so scripts/boot_menu.py carries no termios, no getch, no digit gate. And the file itself is in the payload and says it in its first paragraph: an interactive terminal no longer asks for a numbered door, it prints the short list and returns 10, the interactive path blocks on nothing and reads no keys, and the only environment variable it names is PIPULATE_BOOT_MENU=0. The voice probe printed two lines: line 80, the consent file under ~/.config/pipulate/voice, and line 192, local_model_dir = project_root / "assets" / "piper_models".

The menu: README was the stale one

The skill’s sentence stands and README’s three fall. Quick Start’s “three-door menu, Press 1”, the Big Reset’s “Press 1 at the three-door menu”, and the automation paragraph’s “three-door prompt” all describe a keypress the file no longer reads. The PIPULATE_BOOT_MENU_TIMEOUT row describes a wait that cannot happen: a timeout on a prompt that returns the moment it has printed is a promise about nothing, whatever wrapper might sit around it. Car 1 rewrites the three sentences to what the file does (a short list of words, a (nix) prompt, nothing started; walk first, jn for the servers) and cuts the row. The census probe below reads flake.nix for any surviving reader of that variable; if one exists it is dead code for a flake turn, not this one.

Two README passages I read and did not patch, on purpose: the checklist line “Starts JupyterLab and the Pipulate server” appears twice, byte-identical in PATH 1 and PATH 2, so any SEARCH on it is ambiguous by construction and the Quick Start copy carries trailing spaces the transport may not keep; and the Developer Setup paragraph (“runs the startup script with welcome messages and service startup”, run-server, run-jupyter), Your First 10 Minutes’ terminal block, and the Quick Reference’s git pull && nix develop all describe the old door at length. That is a README ride when README is the ride’s subject; banked as residue.

The skill: the voice model, and three seams the second reading left

The model lands inside the folder, assets/piper_models/, gitignored (.gitignore carries piper_models/), fetched on the first spoken line after the yes at the card; the 09-17 and 09-18 receipts read it there too (61M at that path; the download re-ran after the folder was moved aside). Car 2 writes that in, and says the good part out loud: removing the folder removes it.

Reading the skill top to bottom against itself, AGENTS.md, README.md and boot_menu.py for internal consistency turned up three more:

  1. Section 5 now says three homes outside the folder; section 6’s Remove line still deletes two. ~/.local/state/pipulate joins the rm -rf.
  2. Last turn’s tail-append left “clone, then a git pull” as a four-word line in the Leaves paragraph. Rewrapped.
  3. about prints the tree, the three tier sentences and the Q/A line (ABOUT_LINES), and the skill said “the workspace tree”. Extended.

And in AGENTS.md, the Setup line still promises “full environment (server + JupyterLab)” from nix develop, which the one-door menu made false on a terminal and quietly true without one (no tty, exit 0, the app starts). Car 3 says both, and points an agent at the quiet shell for exactly that reason. One residue: voice is a flake word the skill names and neither menu list prints; a one-tuple car in boot_menu.py when a ride owns it.

The skills page answers the question the ruling hinged on. Its location table names .claude/skills/<skill-name>/SKILL.md as the project location, ~/.claude/skills/ as personal, and <plugin>/skills/ for plugins; no .agents path appears anywhere on the page. So a Claude Code session opened in this repo today sees none of the six, as the TODO read it. On symlinks, the page says a <skill-name> entry in the project location can be a symlink to a directory elsewhere on disk, that Claude Code reads SKILL.md from the target, and that it loads the skill once even when several locations point at the same target. That blesses six per-skill links and says nothing about the parent.

The ruling: one directory link, .claude/skills -> ../.agents/skills. Six per-skill links are the documented form and a convention: a seventh skill added to .agents/skills is invisible to Claude Code until someone remembers a seventh link, which is the property-that-needs-a-call-site-audit failure by another name. The parent link is a property: whatever .agents/skills holds, .claude/skills holds, by construction, with zero duplicated bytes. It is one step past what the page documents, so the gate stays what the TODO wrote: claude at the repo root, /, the six names. A session that offers none of them is the reading that swaps the one link for six next turn, and that is a decision for a receipt, not for you between cars. The price is a vendor directory at the root, and it is paid in the smallest coin: one tracked link, plus .claude/settings.local.json ignored, since the /skills menu writes skill-visibility overrides to that file. Declined as written: the page’s note that a .claude-plugin/plugin.json in a skill folder makes it a plugin that can bundle hooks and MCP servers. The whole answer to “why not a plugin” is that the tool call rides the compile as a receipt and never the vendor’s session.

The / check needs Claude Code on the box; a census probe below reads whether it is there before the next turn plans around it.

1. PROBES

The out-of-band step first, because the AGENTS.md gate landed last turn and only a chat window can take it.

A. THE AGENTS.md GATE (a browser, not the terminal). Open a fresh chat in any web chatbot. Paste the whole of AGENTS.md and nothing else, with this line above it:

Read the file below and tell me, in two sentences, what this repository is and what an agent may do here.

You will see: two sentences back. The gate passes when they name the Agent Skills specification and the localhost rule; the two sentences, whatever they say, are the receipt for the next paste.

Then the terminal probes, read-only. Forecasts: three-door 3 to 0 and (nix) 0 to 3 (Car 1); the TIMEOUT census reads README.md=1 rc=0 now and an empty count with rc=1 after, boot_menu.py empty with rc=1 on both taps, and flake.nix is the reading, not a forecast; the skill’s retired pair 2 to 0 and its new three 0 to 3 (Car 2); AGENTS.md’s server + JupyterLab 1 to 0 and its new two 0 to 2 (Cars 3 and 4); claude_skills=ABSENT to claude_skill_files=6, ls-files 0 to 1, and readlink from readlink_rc=1 to ../.agents/skills over readlink_rc=0 (Car 4); the claude version line is a CENSUS, the same on both taps, and decides whether the / check can be taken on this machine; the deed count 0 to 4 (Cars 4 and 5); Coverage 207/273 to 208/274, +1 claimed and +1 tracked, if the claim parser counts a link the way it counts a file.

grep -c 'three-door' README.md
grep -c '(nix)' README.md
for f in README.md flake.nix scripts/boot_menu.py; do n=$(rg -c BOOT_MENU_TIMEOUT "$f" 2>&1); echo "$f=$n rc=$?"; done
grep -c -e 'not here' -e 'two homes' .agents/skills/pipulate/SKILL.md
grep -c -e 'piper_models' -e 'three homes outside' -e 'what it was handed' .agents/skills/pipulate/SKILL.md
grep -c 'server + JupyterLab' AGENTS.md
grep -c -e 'nothing started' -e 'claude/skills' AGENTS.md
.venv/bin/python -c "import pathlib;p=pathlib.Path('.claude/skills');print('claude_skill_files=%d'%len(list(p.glob('*/SKILL.md'))) if p.is_dir() else 'claude_skills=ABSENT')"
git ls-files .claude | wc -l
readlink .claude/skills; echo readlink_rc=$?
command -v claude >/dev/null && claude --version 2>&1 | head -1 || echo CLAUDE_CODE_ABSENT
grep -c 'deed 1663' foo_files.py

2. NEXT CONTEXT

Paste-ready. The dismount compile; boot_menu.py has done its job and leaves, the game’s block stays commented.

! grep -c 'three-door' README.md
! grep -c '(nix)' README.md
! for f in README.md flake.nix scripts/boot_menu.py; do n=$(rg -c BOOT_MENU_TIMEOUT "$f" 2>&1); echo "$f=$n rc=$?"; done
! grep -c -e 'not here' -e 'two homes' .agents/skills/pipulate/SKILL.md
! grep -c -e 'piper_models' -e 'three homes outside' -e 'what it was handed' .agents/skills/pipulate/SKILL.md
! grep -c 'server + JupyterLab' AGENTS.md
! grep -c -e 'nothing started' -e 'claude/skills' AGENTS.md
! .venv/bin/python -c "import pathlib;p=pathlib.Path('.claude/skills');print('claude_skill_files=%d'%len(list(p.glob('*/SKILL.md'))) if p.is_dir() else 'claude_skills=ABSENT')"
! git ls-files .claude | wc -l
! readlink .claude/skills; echo readlink_rc=$?
! command -v claude >/dev/null && claude --version 2>&1 | head -1 || echo CLAUDE_CODE_ABSENT
! grep -c 'deed 1663' foo_files.py
foo_files.py
AGENTS.md
.agents/skills/pipulate/SKILL.md
README.md
# --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# Workshop/corporate/connectors/svb.py
# Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# Workshop/corporate/tickets/SVB_PROMPT.md
# connectors/mcp_render.py
# connectors/jira.py
# ! jira SVB-133

3. PATCHES

Five cars. Every SEARCH is a single unique line or a span of contiguous non-empty lines; no block crosses a blank. Car 4 has a hand fence between app and d, and it is written in the order your hand will meet it.

Car 1: README’s three doors. Four blocks, one commit story.

Target: README.md
[[[SEARCH]]]
**Success looks like:** A one-line environment reading, then a three-door menu. Press `1` to start JupyterLab and the app server (`2` gives you a bare shell with nothing running; `3` starts both but opens the Pipulate app tab instead). The JupyterLab tab opens in your browser; run the Onboarding notebook top-to-bottom to unlock the Pipulate app (it opens automatically on future launches).
[[[DIVIDER]]]
**Success looks like:** A one-line environment reading, then a short list of words and a `(nix)` prompt. Nothing has started yet. Type `walk` for the guided tour, or `jn` to start JupyterLab and the app server; the JupyterLab tab opens in your browser with the Onboarding notebook, and running it top-to-bottom unlocks the Pipulate app (it opens automatically on future launches).
[[[REPLACE]]]

Target: README.md
[[[SEARCH]]]
Press `1` at the three-door menu, wait for the JupyterLab tab to open, then run the Onboarding notebook to unlock the Pipulate app.
[[[DIVIDER]]]
Type `jn` at the `(nix)` prompt, wait for the JupyterLab tab to open, then run the Onboarding notebook to unlock the Pipulate app.
[[[REPLACE]]]

Target: README.md
[[[SEARCH]]]
**Automating past the boot menu:** `nix develop` ends at a three-door prompt — start with the JupyterLab tab, drop to the shell, or start with the Pipulate tab in front. It fails open, so anything without a terminal (CI, a provisioning script, an SSH session with no tty) starts the app and never sees the prompt. Two environment variables cover the cases that do:
[[[DIVIDER]]]
**Automating past the command list:** on a terminal, `nix develop` ends at a short list of words and a `(nix)` prompt with nothing started; `scripts/boot_menu.py` prints the list, speaks through its exit code and reads no keys. It fails open, so anything without a terminal (CI, a provisioning script, an SSH session with no tty) starts the app and never sees the list. One environment variable covers a terminal that should do the same:
[[[REPLACE]]]

Target: README.md
[[[SEARCH]]]
| `PIPULATE_BOOT_MENU=0` | Skip the prompt entirely and start the app, e.g. `PIPULATE_BOOT_MENU=0 nix develop`. For an unattended terminal that would otherwise wait for a keypress that never comes. |
| `PIPULATE_BOOT_MENU_TIMEOUT=10` | Keep the prompt, but start the app after N seconds if nobody chooses. Off by default — with a human provably present, the prompt waits indefinitely rather than starting something they didn't ask for. |
[[[DIVIDER]]]
| `PIPULATE_BOOT_MENU=0` | Skip the list and start the app, e.g. `PIPULATE_BOOT_MENU=0 nix develop`. For an unattended terminal (one a script types into) that should behave as if it had no tty. |
[[[REPLACE]]]

Car 2: the skill’s seams. Four blocks, one commit story.

Target: .agents/skills/pipulate/SKILL.md
[[[SEARCH]]]
  `all` the long one, `about` the workspace tree. `exit` leaves the shell.
[[[DIVIDER]]]
  `all` the long one, `about` the workspace tree, the three tiers and how
  to check what an AI said against what it was handed. `exit` leaves the
  shell.
[[[REPLACE]]]

Target: .agents/skills/pipulate/SKILL.md
[[[SEARCH]]]
path was not read for this skill. Where the voice model lands is named in
`imports/voice_synthesis.py`, not here.
[[[DIVIDER]]]
path was not read for this skill. The voice model lands inside the folder,
under `assets/piper_models/` (gitignored, so removing the folder removes
it), fetched on the first spoken line after the yes at the voice card.
[[[REPLACE]]]

Target: .agents/skills/pipulate/SKILL.md
[[[SEARCH]]]
Leaves, as the installer and the flake spell it: GitHub (the ZIP, then the
clone, then a `git pull`
on every `nix develop`); pipulate.com (the deploy key, from either door);
[[[DIVIDER]]]
Leaves, as the installer and the flake spell it: GitHub (the ZIP, then the
clone, then a `git pull` on every `nix develop`); pipulate.com (the deploy
key, from either door);
[[[REPLACE]]]

Target: .agents/skills/pipulate/SKILL.md
[[[SEARCH]]]
  `rm -rf ~/.config/pipulate ~/.local/share/pipulate` for the two homes
  outside the folder. If the flake wrote `~/.ssh/id_rsa` (the person had
[[[DIVIDER]]]
  `rm -rf ~/.config/pipulate ~/.local/share/pipulate ~/.local/state/pipulate`
  for the three homes outside the folder. If the flake wrote `~/.ssh/id_rsa`
  (the person had
[[[REPLACE]]]

Car 3: AGENTS.md’s Setup line.

Target: AGENTS.md
[[[SEARCH]]]
- `nix develop` — full environment (server + JupyterLab). See `flake.nix`.
[[[DIVIDER]]]
- `nix develop` — the full environment. On a terminal it stops at a short
  list of words and a `(nix)` prompt with nothing started; `jn` starts
  JupyterLab and the server. Without a terminal it starts the app, so an
  agent takes the quiet shell below. See `flake.nix`.
[[[REPLACE]]]

Car 4: THE CLAUDE CODE PATH. Three blocks land the labels, the fence lands the link, one commit carries both. Step 1: copy the block below, patch, app.

Target: .gitignore
[[[SEARCH]]]
# Mac & vendor crap
.cursor/
.DS_Store
[[[DIVIDER]]]
# Mac & vendor crap
.cursor/
.DS_Store
# THE CLAUDE CODE PATH (2026-09-28): .claude/skills is a tracked symlink to
# .agents/skills, so a Claude Code session opened here offers the same
# skills behind /; the per-machine file its /skills menu writes is not.
.claude/settings.local.json
[[[REPLACE]]]

Target: AGENTS.md
[[[SEARCH]]]
- Skills (Agent Skills spec, https://agentskills.io/specification): `.agents/skills/*/SKILL.md`, at the repo root beside this file
[[[DIVIDER]]]
- Skills (Agent Skills spec, https://agentskills.io/specification): `.agents/skills/*/SKILL.md`, at the repo root beside this file; `.claude/skills` is a symlink to the same folder, so Claude Code's project-skills path offers the same skills
[[[REPLACE]]]

Target: foo_files.py
[[[SEARCH]]]
# THE SKILL.md FILES
# .agents/skills/gsc-readonly/SKILL.md
[[[DIVIDER]]]
# THE SKILL.md FILES
# .claude/skills  # <-- a tracked symlink to .agents/skills, so Claude Code's project-skills path finds the same skills (THE CLAUDE CODE PATH, deed 1663)
# .agents/skills/gsc-readonly/SKILL.md
[[[REPLACE]]]

Step 2: copy the block below, paste it at the (nix) prompt, press Enter. You will see: A .claude/skills and then LINK_GO. If the link already exists or git refuses, you will see LINK_STOP and the reason above it; then nothing was linked.

mkdir -p .claude && ln -s ../.agents/skills .claude/skills && git add .claude/skills && git status --short && echo LINK_GO || echo LINK_STOP

Step 3: d, m.

Car 5: the banks. Two tail-appends, one commit story.

Target: foo_files.py
[[[SEARCH]]]
the Claude Code check is now THE CLAUDE CODE PATH, its own line.
[[[DIVIDER]]]
the Claude Code check is now THE CLAUDE CODE PATH, its own line. READ 2026-09-28 (deed 1663): scripts/boot_menu.py ruled for the skill (rg read no termios, getch or digit gate, rc=1, and its docstring says the interactive path reads no keys and returns 10), so README's three three-door sentences were the stale ones and were rewritten (Quick Start, the Big Reset, the automation paragraph; the PIPULATE_BOOT_MENU_TIMEOUT row cut, a timeout on a prompt that never waits; flake.nix's count of that variable rides the next compile as a census); the voice model's home read off imports/voice_synthesis.py:192, assets/piper_models inside the folder, and written into the skill with ~/.local/state/pipulate added to its Remove line; AGENTS.md's Setup line stopped promising server + JupyterLab from nix develop alone. RESIDUE, a README ride when README is the subject: the checklist line "Starts JupyterLab and the Pipulate server" (twice, byte-identical, ambiguous to a SEARCH), the Developer Setup paragraph (run-script, service startup, run-server), Your First 10 Minutes' terminal block, the Quick Reference's git pull && nix develop; and voice is a flake word on neither menu list.
[[[REPLACE]]]

Target: foo_files.py
[[[SEARCH]]]
Gate: the / check, the six names offered in a session opened here.
[[[DIVIDER]]]
Gate: the / check, the six names offered in a session opened here. RULED 2026-09-28 (deed 1663; the Claude Code skills page read into the turn by one web fetch): the symlink, one directory link and not six. The page's table names .claude/skills/<skill-name>/SKILL.md as the project location and no .agents path; its Symlinked folders rule blesses a <skill-name> entry that is a symlink and dedupes by target, and says nothing of the parent, so six per-skill links are the documented form and the parent link is one step past it; the parent link is chosen because it is a property (whatever .agents/skills holds, .claude/skills holds, by construction) where six links are a convention a seventh skill breaks. The vendor directory at the root is the price, paid in one tracked link and .claude/settings.local.json ignored (the /skills menu writes it). LANDED as deed 1663's Car 4: mkdir -p .claude, ln -s ../.agents/skills .claude/skills, git add; the router claims the link; AGENTS.md's Skills line names it. Gate unchanged, and it needs claude on the box (a census probe reads it): a session that offers none of the six is the reading that swaps the one link for six per-skill links next turn. Declined as written: a .claude-plugin/plugin.json, which the page says makes a skill folder a plugin that can bundle hooks and MCP servers.
[[[REPLACE]]]

Ignition: none. Every probe reads its file at call time, and the link is read by the filesystem, not by a shell that has to be re-entered. The / check is a gate, not an ignition, and it rides the next turn once the census says claude is on the box.

4. PROMPT

Read the receipts first: three-door 0 and (nix) 3 in README; the BOOT_MENU_TIMEOUT census (README empty rc=1, boot_menu.py empty rc=1, flake.nix's count is the reading: nonzero means a dead reader for a flake turn); the skill's retired pair 0 and new three 3; AGENTS.md's server + JupyterLab 0 and its new two 2; claude_skill_files=6, ls-files 1, readlink ../.agents/skills rc=0; the claude version line or CLAUDE_CODE_ABSENT; deed 1663 4; Coverage 208/274. Read the two sentences the fresh chat gave back for AGENTS.md and say whether the gate passed. If claude is on the box, the hand step before the dismount: at the repo root type claude, then /, read the list for the six skill names, Ctrl+C twice; the names you saw are the receipt, and none of them is the reading that swaps the one link for six. Then dismount this article: the seven beats, the receipt line at the top of the RECEIPTS block (it will read 23 against its cap of 20 and say so), the banks as cars, the dangling one line each, the seed for the next ride. After the article is published and prompt.md is out of the way (rm prompt.md at the root; it is gitignored and prompt regenerates it), THE FIRST GAME opens by hand at a (nix) prompt: p, then .venv/bin/python Workshop/corporate/connectors/svb.py SVB-133 --write --prompt, then context to read the board it wrote, then bjj; the board is the next compile's Context and SVB_PROMPT.md is its prompt.

5. EXTERNAL DELIVERABLES

None outside the repo. The door page’s one word is already served: the nixops receipt names index.html as the file the pad’s rsync sent, and the nixos-rebuild switch after it was a no-op for this ride, as a body-only change is.

Sources:

MikeLev.in:

THE PROOF STRADDLE

Same commands, run twice, one change between them. Where the readings differ is what the change did; the diff in the middle is the receipt.

1: Probe: Here’s the results of the out-of-bound instructions with that sentence and the contents of AGENTS.md pasted below. The test was done in ChatGPT 6 Extra High:

Pipulate is a reproducible, localhost-first AI/automation repository that packages Agent Skills, tools, and compiled context into portable `foo.zip` cartridges, with executable code and skill files—not duplicated documentation—as the source of truth. An agent may discover and invoke local tools, load skills, inspect journal context, compile cartridges, and propose code changes through exact SEARCH/REPLACE patches applied by `apply.py`, while respecting generated files, notebook-editing rules, and validation checks.

And now back to the normal probes.

Left-hand causal boundary "blast radius" established. Make 1 change and test.
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ grep -c 'three-door' README.md
grep -c '(nix)' README.md
for f in README.md flake.nix scripts/boot_menu.py; do n=$(rg -c BOOT_MENU_TIMEOUT "$f" 2>&1); echo "$f=$n rc=$?"; done
grep -c -e 'not here' -e 'two homes' .agents/skills/pipulate/SKILL.md
grep -c -e 'piper_models' -e 'three homes outside' -e 'what it was handed' .agents/skills/pipulate/SKILL.md
grep -c 'server + JupyterLab' AGENTS.md
grep -c -e 'nothing started' -e 'claude/skills' AGENTS.md
.venv/bin/python -c "import pathlib;p=pathlib.Path('.claude/skills');print('claude_skill_files=%d'%len(list(p.glob('*/SKILL.md'))) if p.is_dir() else 'claude_skills=ABSENT')"
git ls-files .claude | wc -l
readlink .claude/skills; echo readlink_rc=$?
command -v claude >/dev/null && claude --version 2>&1 | head -1 || echo CLAUDE_CODE_ABSENT
grep -c 'deed 1663' foo_files.py
3
0
README.md=1 rc=0
flake.nix= rc=1
scripts/boot_menu.py= rc=1
2
0
1
0
claude_skills=ABSENT
0
readlink_rc=1
1.0.85 (Claude Code)
0
(nix) pipulate $ 

2: Context:

# context.txt: the list of files an AI will read. context opens it, compile builds it.
# Keys: j down, k up; Esc then :q quits, :q! discards, :wq saves and quits.
# One line per thing the AI reads: a file path, or a command after `! `.
# A line that starts with # is a comment: that file is not read.
# To add a line: i starts typing, Esc stops. Absolute paths work from anywhere.
# Web pages, APIs and the connector words: chapter XVIII of foo_files.py.

# --- THE 40K-FOOT VIEW (uncomment on a first turn; comment out on the second) ---
# ! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs  # <-- the book's spine, one line per article, newest first
# ~/repos/nixos/autognome.py  # <-- the machine's morning routine (this author's NixOS box only)
# init.lua                    # <-- the editor keys that drive the day
# assets/installer/install.sh # <-- how a stranger's machine gets this workshop
# GLOSSARY.md                 # <-- the terms, defined
# flake.nix                   # <-- the environment, pinned: here is my hardware, here is my state
# prompt_foo.py               # <-- the compiler that builds the payload
# foo_files.py                # <-- the router: which files ride, and this book's outline
# scripts/articles/lsa.py     # <-- the second brain: the article corpus behind `rgx`, `rgxc` and `posts`
# requirements.in             # <-- the Python packages, pinned
# pyproject.toml              # <-- the PyPI package
# __init__.py                 # <-- the version

# --- ACTUATORS (cheap; include when the AI should be able to act, not only read) ---
# cli.py                      # <-- tool calls from the command line
# scripts/xp.py               # <-- turns a pasted reply into the next context
# scripts/ai.py               # <-- a local AI writes the commit messages
# scripts/crawl.py            # <-- crawl a site into the next turn
# scripts/weblogin.py         # <-- warm a login on the persistent browser profile
# scripts/webclip_2_markdown.py  # <-- a web page, clipped, as markdown

# --- RARE ---
# scripts/foo_cartridge.py    # <-- the sealed archive: writer and verifier
# scripts/foo_replay.py       # <-- replay a sealed archive on another machine
# release.py                  # <-- how a release reaches GitHub and PyPI
# imports/voice_synthesis.py  # <-- the voice
# imports/ascii_displays.py   # <-- the ASCII art
# scripts/release/version_sync.py  # <-- version stamping (to be folded into release.py)

# --- THIS DISCUSSION ---
# The files and commands for the work in front of you. Paste the NEXT CONTEXT
# block an AI hands back directly below this line; the AI will correct a guess.

# Context 1
# /home/mike/repos/trimnoir/_posts/2026-09-28-agents-md-agent-skills-pypi-receipts.md
# ! .venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
# ! .venv/bin/python -c "import json,io,tarfile,zipfile,urllib.request as u;d=json.load(u.urlopen('https://pypi.org/pypi/pipulate/json'));K=('agents','ai_context','skill','flake','audit','readme','manifest','license','changelog','install.sh');names=lambda f,b: tarfile.open(fileobj=io.BytesIO(b)).getnames() if f['packagetype']=='sdist' else zipfile.ZipFile(io.BytesIO(b)).namelist();R=[(f,names(f,u.urlopen(f['url']).read())) for f in d['urls']];[print(f['packagetype'],f['filename'],f['size'],'bytes',len(n),'members',[x for x in n if any(k in x.lower() for k in K)]) for f,n in R]"
# ! .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
# foo_files.py
# # --- THE INSTALL SKILL (uncomment when that car rides) ---
# # assets/installer/install.sh
# # remotes/honeybot/www/npvg.org/index.html
# # AUDIT.md
# # README.md
# # --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# # Workshop/corporate/connectors/svb.py
# # Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# # Workshop/corporate/tickets/SVB_PROMPT.md
# # connectors/mcp_render.py
# # connectors/jira.py
# # ! jira SVB-133
# 
# Context 2
# ! .venv/bin/python -c "import re,pathlib;[print(p.parent.name, 'fence' if p.read_text().startswith('---') else 'NO-FENCE', 'name-ok' if re.fullmatch(r'[a-z0-9]+(-[a-z0-9]+)*', p.parent.name) else 'NAME-INVALID') for p in sorted(pathlib.Path('.agents/skills').glob('*/SKILL.md'))]"
# ! git ls-files .agents | wc -l
# ! awk '/^# --- START RECEIPTS/{f=1;next} /^# --- END RECEIPTS/{f=0} f' foo_files.py | wc -l
# ! grep -c '^# - EARMARK: THE PROBE NAMED THE VERSION' foo_files.py
# ! grep -c '^# - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST' foo_files.py
# ! .venv/bin/python -m build --sdist --no-isolation --outdir /tmp/pipulate-sdist-probe . >/dev/null 2>&1; echo build_rc=$?; f=$(ls -t /tmp/pipulate-sdist-probe/*.tar.gz | head -1); echo file=$f; tar tzf "$f" | grep -Ei 'AGENTS\.md|AUDIT\.md|\.agents/|AI_CONTEXT'; echo members=$(tar tzf "$f" | wc -l)
# foo_files.py
# .agents/skills/pipulate/SKILL.md
# assets/installer/install.sh
# remotes/honeybot/www/npvg.org/index.html
# AGENTS.md
# AUDIT.md
# README.md
# # --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# # Workshop/corporate/connectors/svb.py
# # Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# # Workshop/corporate/tickets/SVB_PROMPT.md
# # connectors/mcp_render.py
# # connectors/jira.py
# # ! jira SVB-133
# !https://code.claude.com/docs/en/plugins/overview
# 
# Context 3
# ! sed -n '1,8p' AGENTS.md | grep -c -e localhost -e 'agentskills.io'
# ! grep -c -e 'with no output' -e 'and the voice model' -e 'context, prompt, compile' .agents/skills/pipulate/SKILL.md
# ! grep -c -e 'local/state/pipulate' -e 'bash -s npvg' -e 'nix develop -L' .agents/skills/pipulate/SKILL.md
# ! grep -c 'first command lets' remotes/honeybot/www/npvg.org/index.html
# ! grep -c '2026-09-28 (deed 1662)' foo_files.py
# ! grep -c 'three-door' README.md
# ! n=$(rg -c -e termios -e getch -e "'[123]'" scripts/boot_menu.py 2>&1); echo boot_menu_keypress_lines="$n" rc=$?
# ! rg -n -e 'piper_models' -e 'local/share' -e 'config/pipulate' imports/voice_synthesis.py | head -8; echo voice_paths_end
# foo_files.py
# AGENTS.md
# .agents/skills/pipulate/SKILL.md
# README.md
# scripts/boot_menu.py
# # --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# # Workshop/corporate/connectors/svb.py
# # Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# # Workshop/corporate/tickets/SVB_PROMPT.md
# # connectors/mcp_render.py
# # connectors/jira.py
# # ! jira SVB-133
# 
# Context 4
! grep -c 'three-door' README.md
! grep -c '(nix)' README.md
! for f in README.md flake.nix scripts/boot_menu.py; do n=$(rg -c BOOT_MENU_TIMEOUT "$f" 2>&1); echo "$f=$n rc=$?"; done
! grep -c -e 'not here' -e 'two homes' .agents/skills/pipulate/SKILL.md
! grep -c -e 'piper_models' -e 'three homes outside' -e 'what it was handed' .agents/skills/pipulate/SKILL.md
! grep -c 'server + JupyterLab' AGENTS.md
! grep -c -e 'nothing started' -e 'claude/skills' AGENTS.md
! .venv/bin/python -c "import pathlib;p=pathlib.Path('.claude/skills');print('claude_skill_files=%d'%len(list(p.glob('*/SKILL.md'))) if p.is_dir() else 'claude_skills=ABSENT')"
! git ls-files .claude | wc -l
! readlink .claude/skills; echo readlink_rc=$?
! command -v claude >/dev/null && claude --version 2>&1 | head -1 || echo CLAUDE_CODE_ABSENT
! grep -c 'deed 1663' foo_files.py
foo_files.py
AGENTS.md
.agents/skills/pipulate/SKILL.md
README.md
# --- THE FIRST GAME (svb SVB-133 --write --prompt writes the board; these are for a turn about the machinery) ---
# Workshop/corporate/connectors/svb.py
# Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# Workshop/corporate/tickets/SVB_PROMPT.md
# connectors/mcp_render.py
# connectors/jira.py
# ! jira SVB-133

3: Patches:

Left-hand causal boundary "blast radius" established. Make 1 change and test.
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 'README.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'README.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'README.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'README.md'.
(nix) pipulate $ d
diff --git a/README.md b/README.md
index f7d8ebd6..76ab9c10 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ cd ~/pipulate && nix develop
 
 **What you get:** A local web app at `http://localhost:5001` with step-by-step workflows, integrated AI chat, and a JupyterLab instance at `http://localhost:8888`. No cloud required.
 
-**Success looks like:** A one-line environment reading, then a three-door menu. Press `1` to start JupyterLab and the app server (`2` gives you a bare shell with nothing running; `3` starts both but opens the Pipulate app tab instead). The JupyterLab tab opens in your browser; run the Onboarding notebook top-to-bottom to unlock the Pipulate app (it opens automatically on future launches).
+**Success looks like:** A one-line environment reading, then a short list of words and a `(nix)` prompt. Nothing has started yet. Type `walk` for the guided tour, or `jn` to start JupyterLab and the app server; the JupyterLab tab opens in your browser with the Onboarding notebook, and running it top-to-bottom unlocks the Pipulate app (it opens automatically on future launches).
 
 These few commands:
 - ✅ Updates to the latest version automatically
@@ -557,14 +557,13 @@ cd ~/TestProject
 nix develop
 [triple-backtick]
 
-Press `1` at the three-door menu, wait for the JupyterLab tab to open, then run the Onboarding notebook to unlock the Pipulate app.
+Type `jn` at the `(nix)` prompt, wait for the JupyterLab tab to open, then run the Onboarding notebook to unlock the Pipulate app.
 
-**Automating past the boot menu:** `nix develop` ends at a three-door prompt — start with the JupyterLab tab, drop to the shell, or start with the Pipulate tab in front. It fails open, so anything without a terminal (CI, a provisioning script, an SSH session with no tty) starts the app and never sees the prompt. Two environment variables cover the cases that do:
+**Automating past the command list:** on a terminal, `nix develop` ends at a short list of words and a `(nix)` prompt with nothing started; `scripts/boot_menu.py` prints the list, speaks through its exit code and reads no keys. It fails open, so anything without a terminal (CI, a provisioning script, an SSH session with no tty) starts the app and never sees the list. One environment variable covers a terminal that should do the same:
 
 | Variable | Effect |
 |----------|--------|
-| `PIPULATE_BOOT_MENU=0` | Skip the prompt entirely and start the app, e.g. `PIPULATE_BOOT_MENU=0 nix develop`. For an unattended terminal that would otherwise wait for a keypress that never comes. |
-| `PIPULATE_BOOT_MENU_TIMEOUT=10` | Keep the prompt, but start the app after N seconds if nobody chooses. Off by default — with a human provably present, the prompt waits indefinitely rather than starting something they didn't ask for. |
+| `PIPULATE_BOOT_MENU=0` | Skip the list and start the app, e.g. `PIPULATE_BOOT_MENU=0 nix develop`. For an unattended terminal (one a script types into) that should behave as if it had no tty. |
 
 ### 🚨 Installation Troubleshooting
 
(nix) pipulate $ m
📝 Committing: chore: Update success instructions in README
[main 2e2e9827] chore: Update success instructions in README
 1 file changed, 4 insertions(+), 5 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.agents/skills/pipulate/SKILL.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.agents/skills/pipulate/SKILL.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.agents/skills/pipulate/SKILL.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.agents/skills/pipulate/SKILL.md'.
(nix) pipulate $ d
diff --git a/.agents/skills/pipulate/SKILL.md b/.agents/skills/pipulate/SKILL.md
index 8f402cbd..3ffe4529 100644
--- a/.agents/skills/pipulate/SKILL.md
+++ b/.agents/skills/pipulate/SKILL.md
@@ -93,7 +93,9 @@ followed by a short list of words to type.
   that teaches the loop. On its first run it shows a card asking whether it
   may read aloud and records the answer; it stays silent until that answer
   is yes, and `voice` changes it later. `menu` reprints the short list,
-  `all` the long one, `about` the workspace tree. `exit` leaves the shell.
+  `all` the long one, `about` the workspace tree, the three tiers and how
+  to check what an AI said against what it was handed. `exit` leaves the
+  shell.
 
 ## 5. What stays on the machine, and what leaves it
 
@@ -106,12 +108,13 @@ question), `~/.local/share/pipulate/` (two sound files) and
 `~/.local/state/pipulate/` (the compile's own state: `context.txt` when
 `PIPULATE_ADHOC_FILE` points there, and what a move sets aside under
 `stale/`). The browser lanes may leave a driver cache under `~/.cache`; that
-path was not read for this skill. Where the voice model lands is named in
-`imports/voice_synthesis.py`, not here.
+path was not read for this skill. The voice model lands inside the folder,
+under `assets/piper_models/` (gitignored, so removing the folder removes
+it), fetched on the first spoken line after the yes at the voice card.
 
 Leaves, as the installer and the flake spell it: GitHub (the ZIP, then the
-clone, then a `git pull`
-on every `nix develop`); pipulate.com (the deploy key, from either door);
+clone, then a `git pull` on every `nix develop`); pipulate.com (the deploy
+key, from either door);
 install.determinate.systems, only when Nix is missing; the Nix binary cache
 and PyPI, for packages; Hugging Face once, for a small voice model, and only
 after the person has answered yes at the voice card. No connector calls any
@@ -130,8 +133,9 @@ removing the folder does not remove Nix.
 - Reset the Python environment and nothing else: `rm -rf ~/npvg/.venv`, then
   `nix develop` rebuilds it.
 - Remove: `rm -rf ~/npvg` (the installer prints this line itself), then
-  `rm -rf ~/.config/pipulate ~/.local/share/pipulate` for the two homes
-  outside the folder. If the flake wrote `~/.ssh/id_rsa` (the person had
+  `rm -rf ~/.config/pipulate ~/.local/share/pipulate ~/.local/state/pipulate`
+  for the three homes outside the folder. If the flake wrote `~/.ssh/id_rsa`
+  (the person had
   none before), that key and the `github.com` block in `~/.ssh/config` are
   theirs to remove. Nix stays unless its own uninstaller is run.
 
(nix) pipulate $ m
📝 Committing: Refactor: Clarify skill shutdown and cleanup instructions
[main c48a3340] Refactor: Clarify skill shutdown and cleanup instructions
 1 file changed, 11 insertions(+), 7 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'AGENTS.md'.
(nix) pipulate $ d
diff --git a/AGENTS.md b/AGENTS.md
index ec69b8e5..bfe8e7f9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -16,7 +16,10 @@ that duplicates code will drift and is a bug.
 
 ## Setup (the executable version of "Dev environment tips")
 
-- `nix develop` — full environment (server + JupyterLab). See `flake.nix`.
+- `nix develop` — the full environment. On a terminal it stops at a short
+  list of words and a `(nix)` prompt with nothing started; `jn` starts
+  JupyterLab and the server. Without a terminal it starts the app, so an
+  agent takes the quiet shell below. See `flake.nix`.
 - `nix develop .#quiet` — minimal shell for agents and scripting.
 - Python lives in `.venv/`; invoke as `.venv/bin/python`.
 
(nix) pipulate $ m
📝 Committing: chore: Clarify nix develop behavior
[main cd197de1] chore: Clarify nix develop behavior
 1 file changed, 4 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.gitignore'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'AGENTS.md'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
(nix) pipulate $ d
diff --git a/.gitignore b/.gitignore
index 762b28a6..0b999b2a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -84,6 +84,10 @@ whitelabel.txt
 # Mac & vendor crap
 .cursor/
 .DS_Store
+# THE CLAUDE CODE PATH (2026-09-28): .claude/skills is a tracked symlink to
+# .agents/skills, so a Claude Code session opened here offers the same
+# skills behind /; the per-machine file its /skills menu writes is not.
+.claude/settings.local.json
 
 # Python, Jupyter Notebook & FastHTML stuff
 .ipynb_checkpoints/
diff --git a/AGENTS.md b/AGENTS.md
index bfe8e7f9..6d813c60 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -48,7 +48,7 @@ run since the sentinels landed.
 
 - Discover: `.venv/bin/python cli.py mcp-discover`
 - Execute:  `.venv/bin/python cli.py call <tool_name> --json-args '{...}'`
-- Skills (Agent Skills spec, https://agentskills.io/specification): `.agents/skills/*/SKILL.md`, at the repo root beside this file
+- Skills (Agent Skills spec, https://agentskills.io/specification): `.agents/skills/*/SKILL.md`, at the repo root beside this file; `.claude/skills` is a symlink to the same folder, so Claude Code's project-skills path offers the same skills
 - The reasoning behind any piece of the machinery: the `journal` skill, whose `references/index.md` lists every journal entry newest first with a fetchable URL
 
 ## Context (how this repo talks to AI)
diff --git a/foo_files.py b/foo_files.py
index 0ddc1f75..1b26c8d6 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -1849,6 +1849,7 @@ foo_files.py      #  <-- THIS file. Content compiler router. Makes it very meta.
 # Other examples (document and sort these out)
 
 # THE SKILL.md FILES
+# .claude/skills  # <-- a tracked symlink to .agents/skills, so Claude Code's project-skills path finds the same skills (THE CLAUDE CODE PATH, deed 1663)
 # .agents/skills/gsc-readonly/SKILL.md
 # .agents/skills/hello-workflow/SKILL.md
 # .agents/skills/journal/SKILL.md
(nix) pipulate $ m
📝 Committing: chore: Update .gitignore with Claude Code settings symlink
[main 040107db] chore: Update .gitignore with Claude Code settings symlink
 3 files changed, 6 insertions(+), 1 deletion(-)
(nix) pipulate $ mkdir -p .claude && ln -s ../.agents/skills .claude/skills && git add .claude/skills && git status --short && echo LINK_GO || echo LINK_STOP
A  .claude/skills
LINK_GO
(nix) pipulate $ d
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'foo_files.py'.
(nix) pipulate $ d
diff --git a/foo_files.py b/foo_files.py
index 1b26c8d6..542e5491 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -2590,9 +2590,9 @@ MATCHBOOK_CHOP = r"""
 # #todo #to-do #earmarks
 # - TODO (2026-09-28, THE REOPEN LINE NAMES THE OTHER DOOR; read at deed 1662 off install.sh): the nix-missing branch prints curl -fsSL https://pipulate.com/install.sh | bash -s npvg from the npvg door, because the script cannot learn its own address ($0 is bash and the pipe carries none; THE DOOR NAMES THE FOLDER) and KEY_URL stays at the old door on purpose; a stranger who follows that line passes the folder as an argument, which writes whitelabel.txt and names the app Npvg where the plain npvg line names it Pipulate (the 2026-09-18 receipt saw this branch and never rode it). Cure: a second stamped placeholder for the door's URL (a __INSTALL_DEFAULT_URL__ twin, stamped by the same sub_filter on the pad and spelled in two halves the way the name is) and no -s when the folder is the door's default; section 3 of the install skill says what the script prints today. Gate: the reopen line printed from the npvg door reads npvg.org and carries no -s.
 # - TODO (2026-09-28, THE SKILLS INDEX IS A WELL-KNOWN URI; read at deed 1662 off the Claude Code docs' own response headers, rel="agent-skills" beside rel="llms-txt", and the Agent Skills Discovery RFC v0.2.0 it points at): a site announces its skills at /.well-known/agent-skills/index.json, one entry per skill with name, type skill-md, description, url and a sha256 digest, the llms.txt reflex aimed at SKILL.md (pipulate.com already serves llms.txt from generate_llms_txt.py and AI_CONTEXT.md from release.py's sync lane); the index is GENERATED at release from .agents/skills/*/SKILL.md, each file served under /.well-known/agent-skills/<name>/SKILL.md with its digest computed over the bytes served, never authored, and the digest is the cartridge's own rung (THE RECEIPT LADDER). Needs release.py and the Pipulate.com repo in the payload. Gate: the index at pipulate.com reads six entries and each digest matches the served SKILL.md.
-# - TODO (2026-09-28, THE CLAUDE CODE PATH; read at deed 1662 off the Claude Code skills page): Claude Code loads a repository's skills from .claude/skills/<name>/SKILL.md (personal ones from ~/.claude/skills, plugins aside), Codex from .agents/skills, OpenCode from both, so a Claude Code session opened in this repo sees none of the six until .claude/skills exists; the cheapest bridge is one tracked symlink, .claude/skills -> ../.agents/skills (ln -s, git add; .claude/settings.local.json ignored in the same car), and the same page says a .claude-plugin/plugin.json inside a skill folder loads it as a plugin that can bundle hooks and MCP servers, which this repo declines on purpose (the tool call rides the compile as a receipt, never the vendor's session). Ruling owed: whether a vendor directory at the root is the price of the six names behind /. Gate: the / check, the six names offered in a session opened here.
+# - TODO (2026-09-28, THE CLAUDE CODE PATH; read at deed 1662 off the Claude Code skills page): Claude Code loads a repository's skills from .claude/skills/<name>/SKILL.md (personal ones from ~/.claude/skills, plugins aside), Codex from .agents/skills, OpenCode from both, so a Claude Code session opened in this repo sees none of the six until .claude/skills exists; the cheapest bridge is one tracked symlink, .claude/skills -> ../.agents/skills (ln -s, git add; .claude/settings.local.json ignored in the same car), and the same page says a .claude-plugin/plugin.json inside a skill folder loads it as a plugin that can bundle hooks and MCP servers, which this repo declines on purpose (the tool call rides the compile as a receipt, never the vendor's session). Ruling owed: whether a vendor directory at the root is the price of the six names behind /. Gate: the / check, the six names offered in a session opened here. RULED 2026-09-28 (deed 1663; the Claude Code skills page read into the turn by one web fetch): the symlink, one directory link and not six. The page's table names .claude/skills/<skill-name>/SKILL.md as the project location and no .agents path; its Symlinked folders rule blesses a <skill-name> entry that is a symlink and dedupes by target, and says nothing of the parent, so six per-skill links are the documented form and the parent link is one step past it; the parent link is chosen because it is a property (whatever .agents/skills holds, .claude/skills holds, by construction) where six links are a convention a seventh skill breaks. The vendor directory at the root is the price, paid in one tracked link and .claude/settings.local.json ignored (the /skills menu writes it). LANDED as deed 1663's Car 4: mkdir -p .claude, ln -s ../.agents/skills .claude/skills, git add; the router claims the link; AGENTS.md's Skills line names it. Gate unchanged, and it needs claude on the box (a census probe reads it): a session that offers none of the six is the reading that swaps the one link for six per-skill links next turn. Declined as written: a .claude-plugin/plugin.json, which the page says makes a skill folder a plugin that can bundle hooks and MCP servers.
 # - TODO (2026-09-28, THE WHEEL IS NOT THE SDIST; read at deed 1661): MANIFEST.in's include and graft reach the sdist (114 members, .agents/ and the two root files in) and never the wheel (81 members, LICENSE alone), and pip install fetches the wheel, so a plain install still carries no AGENTS.md; pyproject's package-data block is a no-op by receipt (none of its five names in either artifact, and CHANGELOG.md names a file the tree does not hold). Ruling owed on whether the agent-facing files should ride a package directory or whether PyPI's rendered README is the wheel user's whole door; cut the dead block after a census of whether cli.py's pipulate install needs a bundled install.sh. READ 2026-09-28 (deed 1662): waits. The wheel is PATH 2's bootstrap (pipx install pipulate, then pipulate install, which README says runs the same universal installer), so every wheel user ends at a git checkout that carries AGENTS.md and .agents/ one step later, and the sdist, the artifact an auditor reads without installing, carries them now; no door a wheel user meets is missing anything. The dead package-data block is a pyproject.toml cleanup when that file rides, gated as written.
-# - TODO (2026-09-28, the skills' remaining readers): imports/ascii_displays.py 1938, 1945, 1972-73 spell the underscore names in comments (a reseal if they sit in sealed art); ~/repos/Pipulate.com/README.md:3 reads "And context AI_CONTEXT.md", true and pointable at the journal skill; the Claude Code check (six names behind / in a session in this repo, a .claude/skills symlink if not) is unrun; .agents/skills/pipulate/SKILL.md was written at deed 1661 from install.sh and flake.nix alone, and the npvg.org door page, README's Quick Start and AUDIT.md's first-run section are read against it before 2.67 ships it. READ 2026-09-28 (deed 1662): the second reading rode as one car against the skill (the nix-missing stop's reopen line, the folder check after the nix check, the -L hand-off and the run leftover, a third home under ~/.local/state, the voice card the walk itself shows, the protocol as AGENTS.md spells it, a sources line, the voice model's home withdrawn until imports/voice_synthesis.py names it); README's Quick Start still says a three-door menu and Press 1 where the skill says a command list and a prompt, and scripts/boot_menu.py rules which sentence is stale before either is patched; the localhost sentence landed in AGENTS.md's first paragraph in the same train; the Claude Code check is now THE CLAUDE CODE PATH, its own line.
+# - TODO (2026-09-28, the skills' remaining readers): imports/ascii_displays.py 1938, 1945, 1972-73 spell the underscore names in comments (a reseal if they sit in sealed art); ~/repos/Pipulate.com/README.md:3 reads "And context AI_CONTEXT.md", true and pointable at the journal skill; the Claude Code check (six names behind / in a session in this repo, a .claude/skills symlink if not) is unrun; .agents/skills/pipulate/SKILL.md was written at deed 1661 from install.sh and flake.nix alone, and the npvg.org door page, README's Quick Start and AUDIT.md's first-run section are read against it before 2.67 ships it. READ 2026-09-28 (deed 1662): the second reading rode as one car against the skill (the nix-missing stop's reopen line, the folder check after the nix check, the -L hand-off and the run leftover, a third home under ~/.local/state, the voice card the walk itself shows, the protocol as AGENTS.md spells it, a sources line, the voice model's home withdrawn until imports/voice_synthesis.py names it); README's Quick Start still says a three-door menu and Press 1 where the skill says a command list and a prompt, and scripts/boot_menu.py rules which sentence is stale before either is patched; the localhost sentence landed in AGENTS.md's first paragraph in the same train; the Claude Code check is now THE CLAUDE CODE PATH, its own line. READ 2026-09-28 (deed 1663): scripts/boot_menu.py ruled for the skill (rg read no termios, getch or digit gate, rc=1, and its docstring says the interactive path reads no keys and returns 10), so README's three three-door sentences were the stale ones and were rewritten (Quick Start, the Big Reset, the automation paragraph; the PIPULATE_BOOT_MENU_TIMEOUT row cut, a timeout on a prompt that never waits; flake.nix's count of that variable rides the next compile as a census); the voice model's home read off imports/voice_synthesis.py:192, assets/piper_models inside the folder, and written into the skill with ~/.local/state/pipulate added to its Remove line; AGENTS.md's Setup line stopped promising server + JupyterLab from nix develop alone. RESIDUE, a README ride when README is the subject: the checklist line "Starts JupyterLab and the Pipulate server" (twice, byte-identical, ambiguous to a SEARCH), the Developer Setup paragraph (run-script, service startup, run-server), Your First 10 Minutes' terminal block, the Quick Reference's git pull && nix develop; and voice is a flake word on neither menu list.
 # - TODO (2026-09-28, THE CHEAP FORGETTING; ruled by reading at deed 1655, unridden): forgetting through SEARCH/REPLACE recites the thing forgotten, 63 lines byte-exact for THE FINDING DORY RULE at deed 1654 with one wrong character refunding only the turn, while the two sed cars of the same ride cost a pattern each and landed the same way; the cheap shape names its victim and never recites it: the § key written to a file by a quoted heredoc (a single-quoted delimiter, so nothing expands), one gated sed that reads that file in at the header line and deletes the body by range (the range gated on the header line and the next header line, the trailing blank trimmed per THE SUBSET REPLACE, GO or STOP with the readings beside the word), and the body moved into GLOSSARY.md by a script off the same range (the '# ' prefix rewritten to the glossary's two-space indent, the '- **Handle** — *plain term.*' head the one thing typed), so no byte of the body rides the reply, the way commit 7dd5d832's block moved on 2026-09-26. First rides, each waiting on the operator's yes: THE DIVIDER IS NOT OPTIONAL (2,547 bytes whole by the tightened ranker's count, no bare marker in its body by its own design, its checklist line a prompt_foo.py ride) and the specimen fade by shape (32 paragraphs and 15,923 bytes under two TODOs down to eight lines, one specimen per shape: the sign read backwards, the context row read as the change, one subject for two commits, an earlier subject reused for a router-only bank, a file named that the commit never touched or does not exist, the diff answered as a document, the run-on or comment-carrying subject, the type word wrong). Gate: one graduation lands with its § key in the router, its body in GLOSSARY.md and no body line in the reply.
 # - TOTO (2026-09-28), Endure that the `grim --web` command has an `article` and `bot` equivalent. What's more, make sure those commands are derived from `blogs.nix` and not hard-coded in `flake.nix`.
 # - TODO (2026-09-28, THE ARTICLE LARGER THAN A MINUTE; read at deed 1647 off the grim run): a 915,721-char article estimated 228,930 tokens at chars/4 and the API refused both models at generate_content_free_tier_input_token_count, limit 250000, then the loop waited 57 s for a minute the request could never fit in; the estimate now divides by 3.5, a quota on the input-token metric whose limit is below the estimate stops the run with a SIZE verdict, and the publish words forward their arguments to the editing script alone. The escape today is a key named by hand: a paid Gemini key (billing on one project lifts the minute into the millions on the same tokenizer and a million-token window; a Flash-class price makes such an article cents), or another provider through --model with -k, the wallet's flat alias-to-secret shape unchanged; the window of any other provider is checked against the size line before trusting it. The creative solution for a free key, unbuilt: a map-reduce lane for the editing pass, the article cut at paragraph seams into pieces under the minute, each piece asked for one or two subheadings with a verbatim after_text_snippet and a three-sentence summary, then the template run once on the head plus the summaries for the frontmatter and the analysis, the subheadings merged in article order; N+1 requests against a day of twenty, so a 900K article costs eight; or the cheaper lossy twin, --trim with the middle elided and subheadings for the ends only. Needs editing_prompt.txt and contextualizer.py (the sibling ring) in the payload. Gate for either: one oversized article published on a free key with the spine, the subheadings and the collision guard intact. RULED 2026-09-28 (deed 1648; the Workspace key read free_tier too, and the operator: "not gonna pay", "I don't think chunking is good"): the fences leave first. Past the free minute, or on --lean, every fenced block leaves the editor's copy for a one-line bracketed stand-in naming its language and line count, an editor's note closes the copy, and the prompt is rebuilt and re-read; the post is built from the original article_text, so every snippet the model quotes from prose still matches, and the note forbids quoting a stand-in. The spine lands in the template before the article, so an article quoting the placeholder is left alone. Map-reduce and --trim are dropped. b2 sweeps the wallet through backup-home.py's Key Configs include (pipulate at line 63; the stale articleizer entry beside it is harmless). Gate: the fence probe reads the lean copy under 250,000 on the specimen, and one oversized article publishes on a free key with its subheadings placed. READ 2026-09-28 (deed 1650): the lean copy read 87 fences, 894,222 chars to 106,111, about 30,317 tokens, and grim's scissors line 261,634 tokens to 36,527 on a 127,846-char prompt; then the API failed three more ways: "high demand" on Lite every time at any size, so it was never the size; "The service is currently unavailable" with no status code, read as unrecoverable; and a ring of 26 that never turned, its rule wanting quota from every model while Lite only ever said high demand, the operator cutting it at the fourth wait. The same metric, free_tier_requests, carried limit 5 (the minute) and limit 20 (the day). RULED (the operator: "make it give really good instructions on how to do this in the Web UI, can we make it super simple?", "anti-fragility, always another way?"): THE WEB LANE. Every exit that leaves no instructions (unrecoverable, exhausted, SIZE, a wait cut by hand, or --web on purpose) puts the prompt on the clipboard and prints four steps: paste into any AI chat in a browser, copy the whole reply, press Enter; the JSON is read off the clipboard, refused if it is the prompt's own schema pasted back, cached, and the post built by the same code as after an API answer, article.txt untouched, which the old --copy then --local lane could not promise because the publish word rewrites article.txt from the clipboard first. The ring turns once a quota is reported on a key and every model on it has failed, or once its ten attempts are spent, and leaves the last key the same way; unavailable and overloaded are transient; one parser serves both lanes. Gate: one publish end to end through the web lane, and one ring turn witnessed on -m all.
(nix) pipulate $ m
📝 Committing: chore: Update skill directory structure
[main cef1898d] chore: Update skill directory structure
 2 files changed, 3 insertions(+), 2 deletions(-)
 create mode 120000 .claude/skills
(nix) pipulate $ git push
Enumerating objects: 31, done.
Counting objects: 100% (31/31), done.
Delta compression using up to 48 threads
Compressing objects: 100% (18/18), done.
Writing objects: 100% (22/22), 4.28 KiB | 398.00 KiB/s, done.
Total 22 (delta 13), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (13/13), completed with 7 local objects.
To github.com:pipulate/pipulate.git
   e8ae78d9..cef1898d  main -> main
(nix) pipulate $

Ignition: Something that makes the forward-slash skill auto-lookup work in what sounds like is Claude desktop. While I can get Claude desktop runinng on NixOS this sounds like it’s going to be a Mac test, so I’ll do a full release again.

4: Prompt: Read the receipts first: three-door 0 and (nix) 3 in README; the BOOT_MENU_TIMEOUT census (README empty rc=1, boot_menu.py empty rc=1, flake.nix’s count is the reading: nonzero means a dead reader for a flake turn); the skill’s retired pair 0 and new three 3; AGENTS.md’s server + JupyterLab 0 and its new two 2; claude_skill_files=6, ls-files 1, readlink ../.agents/skills rc=0; the claude version line or CLAUDE_CODE_ABSENT; deed 1663 4; Coverage 208/274. Read the two sentences the fresh chat gave back for AGENTS.md and say whether the gate passed. If claude is on the box, the hand step before the dismount: at the repo root type claude, then /, read the list for the six skill names, Ctrl+C twice; the names you saw are the receipt, and none of them is the reading that swaps the one link for six. Then dismount this article: the seven beats, the receipt line at the top of the RECEIPTS block (it will read 23 against its cap of 20 and say so), the banks as cars, the dangling one line each, the seed for the next ride. After the article is published and prompt.md is out of the way (rm prompt.md at the root; it is gitignored and prompt regenerates it), THE FIRST GAME opens by hand at a (nix) prompt: p, then .venv/bin/python Workshop/corporate/connectors/svb.py SVB-133 –write –prompt, then context to read the board it wrote, then bjj; the board is the next compile’s Context and SVB_PROMPT.md is its prompt.

5: Deliverables: A bona fide Claude skill in Pipulate that gets called with a forward-slash skill command in Claude destkop setteling any question as to whether Pipulate is really a Claude skill or not.

(nix) pipulate $ g

Left-hand causal boundary "blast radius" established. Make 1 change and test.
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ vim __init__.py 
(nix) pipulate $ d
diff --git a/__init__.py b/__init__.py
index 138e72a1..271f1a17 100644
--- a/__init__.py
+++ b/__init__.py
@@ -12,7 +12,7 @@ Usage:
     pipulate
 """
 
-__version__ = "2.66"
+__version__ = "2.67"
 # APOSTROPHES RESTORED (2026-08-04). They were stripped as a workaround for
 # flake.nix's descMatch regex, whose character class excluded ' from the
 # CAPTURE and truncated the banner to "(So)". That regex was fixed in the same
@@ -23,7 +23,7 @@ __version__ = "2.66"
 # is not a property of the system; the regex is. Blast radius is one banner:
 # nothing but flake.nix reads this name -- version_sync.py syncs __version__
 # and __description__, never this. So'wI' chu' -- "engage the cloaking device."
-__version_description__ = "Convention Convention"
+__version_description__ = "Pipulate Claude Skill"
 # SPDX expression, single source of truth, synced into pyproject.toml by
 # scripts/release/version_sync.py. "-or-later" (not bare AGPL-3.0, which is
 # deprecated SPDX) because the header below grants "any later version".
(nix) pipulate $ 

And the release:

(nix) pipulate $ release
╭──────────────────────────────────── LLM Response Quality Assurance ────────────────────────────────────╮
│                                                                                                        │
│                      ( "The AI said so" is a transcript, not a record. )                               │
│                                            O        /)  ____     This drawing is checksummed.          │
│ >  Same files, same words, same bytes:      o /)\__//  /    \    If it reaches you altered, the        │
│ >  a change you can replay is a change    ___(/_ 0 0  |      |   model rewrote what it was told to     │
│ >  you can put your name behind.        *(    ==(_T_)== NPvg |   copy, and that is the finding.        │
│ >  One diff per turn, a reading before    \  )   ""\  |      |   Verify with nothing installed:        │
│ >  and after, and a digest you can check.  |__>-\_>_>  \____/    python scripts/foo_cartridge.py       │
│ foo.zip                                                                                                │
│                                                                                                        │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯
📋 Current version: 2.67
✅ Updated pyproject.toml (version and description)
✅ Pushed AI_CONTEXT.md update to Pipulate.com repo.
[main dd145e22] fix: update indexed entry count
✅ Pushed cef1898d..dd145e22  main -> main
Successfully built pipulate-2.67.tar.gz and pipulate-2.67-py3-none-any.whl
🎉 Published 2.67 -> https://pypi.org/project/pipulate/2.67/

╭───────────────────────────────────── 🎉 Release Pipeline Complete ─────────────────────────────────────╮
│                                                                                                        │
│                                      🎉 Pipulate Release Summary                                       │
│  ╭──────────────────────┬──────────────────────────────────────────────────────────────┬────────────╮  │
│  │ Component            │ Details                                                      │   Status   │  │
│  ├──────────────────────┼──────────────────────────────────────────────────────────────┼────────────┤  │
│  │ 🤖 gemma3:latest     │ fix: update indexed entry count                              │     ✨     │  │
│  │ Message              │                                                              │ gemma3:la… │  │
│  │                      │ The number of indexed entries in the references index has    │            │  │
│  │                      │ been updated from 1499 to 1500. This reflects a minor change │            │  │
│  │                      │ in the dataset.                                              │            │  │
│  ├──────────────────────┼──────────────────────────────────────────────────────────────┼────────────┤  │
│  │ 📦 Version           │ 2.67                                                         │   ✅ Set   │  │
│  ├──────────────────────┼──────────────────────────────────────────────────────────────┼────────────┤  │
│  │ 🚀 PyPI Release      │ https://pypi.org/project/pipulate/2.67/                      │  ✅ Live   │  │
│  ├──────────────────────┼──────────────────────────────────────────────────────────────┼────────────┤  │
│  │ ⏰ Completed         │ 2026-09-28 21:05:22                                          │  🎯 Done   │  │
│  ╰──────────────────────┴──────────────────────────────────────────────────────────────┴────────────╯  │
│                                                                                                        │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯
🔄 server.py touched; the watchdog restarts the server.
(nix) pipulate $ 

Meanwhile over on the Mac:

michaellevin@MichaelMacBook-Pro ~ % curl -fsSL https://npvg.org | bash
Npvg -> ~/npvg   (to remove it later: rm -rf ~/npvg)
Deploy key saved to .ssh/rot (public, pull-only: it lets this folder fetch updates without a GitHub account).
To come back later:  cd ~/npvg && nix develop
Hydrating the Nix environment (the first time can take a few minutes)...
Restored session: Mon Sep 28 21:05:37 EDT 2026
✅ Transformed into a git repository (pre-transform files backed up to /tmp/nix-shell.nJiKvT/tmp.285eKvFMP6).
Already up to date.
Found ~/.ssh/id_rsa -- switching the git remote to SSH so auto-updates use it.
(Determinate Nix 3.19.1) 2.34.6 · Python 3.12.13 · v2.67 · ~/npvg
🔧 Fresh install detected — packages downloading...
✅ 291 packages ready.
📓 12 starter file(s) copied into Workshop/personal/Notebooks/ -- yours to edit.

type one:
  menu     print this list again (useful once it scrolls away).
  walk     take guided tour of context compiler (recommended).
  connect  put mcp, jira, email, docs, etc. into your contexts.
  context  edit the list of files an AI will read (after the walk).
  prompt   save your clipboard as the question for the AI.
  compile  build the list and the question into one payload for a chatbot.
  about    how to Q/A AI output ("what Claude said").
  all      expanded menu.
(nix:nix-shell-env) (nix) npvg $ 

Okay so I load Claude desktop now and do a forward-slash in the chat box.

Okay so I can do this in either “Chat” or “Cowork” mode. I don’t see anything coming up automatically when I do either. Is there something I need to do to activate it? I don’t presume it will be able to look just anywhere on the machine so this sounds like I add a project and point to a folder. Okay, I did that. I don’t see anything obvious on the menu when I type the forward-slash. What am I looking for?

The Mystery of the Missing Desktop Menu

Opus 5.5 Medium (from Claude desktop on Mac): You’re typing / in the wrong tab. The skill files are on your Mac and in the right place. Chat and Cowork just don’t read a repo’s .claude/skills. That path belongs to Claude Code, which in the desktop app is the Code tab (Claude Code running locally). Chat and Cowork only load skills saved to your account, or skills that come in a plugin you’ve installed.

Do this

  1. In Claude desktop, open the Code tab. Not Chat, not Cowork.
  2. Pick ~/npvg as the working folder. On the Mac it’s /Users/michaellevin/npvg.
  3. Start a new session and type /.
  4. You should see six names: pipulate, journal, hello-workflow, roles, gsc-readonly, sheets-readonly. Those six names are the receipt.

Another way to get the same check: in Terminal on the Mac, cd ~/npvg && claude, then /. That needs the Claude Code CLI installed on the Mac itself (see the note under Receipt).

If step 4 shows none of the six, the fallback is six separate links (one per skill) in place of the single folder link. That’s the change the TODO already names.

Receipt

I ran this in your connected npvg folder: it goes through the computer link, and the folder is mounted in Cowork’s Linux VM on your Mac.

  • git log -1: dd145e2 fix: update indexed entry count. That’s the 2.67 release commit, so the fresh install has the link.
  • readlink .claude/skills: ../.agents/skills, rc=0.
  • ls .claude/skills/*/SKILL.md | wc -l: 6.
  • command -v claude: /opt/cowork/claude-bin/claude. That’s the copy inside the Cowork VM, not one on your Mac’s own PATH. It tells us nothing about whether the Terminal route will work.

So the files are fine. What’s left is which part of the app reads them.

If you also want /pipulate in Chat or Cowork

That’s a separate path: a copy of the skill uploaded to your account. I can put the current SKILL.md on a review card for you to save. The catch is that it becomes a second copy that can drift from the repo, which is the sibling-file problem this repo refuses. My reading: don’t. The Code tab is the honest test, because it reads the repo’s own file. If you ever want Chat and Cowork to see it without a copy, the route is a plugin, and that’s the one we declined.

The answer to “is it really a Claude skill”

Yes, and the test above is the one that settles it: a Code session opened in the repo lists pipulate behind /, read from the repo’s own file with no copy. That Chat and Cowork don’t show it is how those surfaces work, not a gap in the skill.


1. PROBES (Mac Terminal)

cd ~/npvg && readlink .claude/skills; ls .claude/skills/*/SKILL.md | wc -l; command -v claude || echo CLAUDE_CODE_ABSENT

2. NEXT CONTEXT

! readlink .claude/skills; echo readlink_rc=$?
! .venv/bin/python -c "import pathlib;p=pathlib.Path('.claude/skills');print('claude_skill_files=%d'%len(list(p.glob('*/SKILL.md'))) if p.is_dir() else 'claude_skills=ABSENT')"
foo_files.py
AGENTS.md
.agents/skills/pipulate/SKILL.md

3. PATCHES: No repo patches required. What the / check prints goes into the dismount’s receipt line.

4. PROMPT

The / check in the Claude desktop Code tab, working folder ~/npvg, listed: <paste the names you saw, or "none">. Rule on THE CLAUDE CODE PATH from that reading (one link stands, or swap to six per-skill links), and note that Chat and Cowork don't read repo .claude/skills. Then dismount this article: the seven beats, the receipt line at the top of RECEIPTS (23 against a cap of 20, and say so), banks as cars, dangling items one line each, the seed. The ChatGPT 6 reading of AGENTS.md passed the gate: its first sentence named Agent Skills and localhost; bank that. After publishing and rm prompt.md, THE FIRST GAME: p, then .venv/bin/python Workshop/corporate/connectors/svb.py SVB-133 --write --prompt, then context, then bjj.

5. EXTERNAL DELIVERABLES: None. The / check in the Code tab is a hand step, and it’s the one above.

MikeLev.in: I follow the instructions and do find forward slash pipulate and select it.

Opus 5.5 Medium: Pipulate is already installed on this machine, in ~/npvg, so there’s nothing to install. Here’s what’s there now:

  • Nix: Determinate Nix 3.19.1 is installed.
  • Workshop folder: ~/npvg is a real git checkout at dd145e2, with a working tree that matches the last commit. .venv is there. data/ isn’t yet, which usually means the app hasn’t been run from this folder.
  • Folders outside ~/npvg: ~/.config/pipulate/ and ~/.local/share/pipulate/ exist, with some files for shadow publishing in them. ~/.local/state/pipulate/ doesn’t exist yet.
  • Running now: a Python process is listening on port 8888, which is probably JupyterLab. Nothing is listening on 5001, so the app itself isn’t running. I couldn’t check the jupyter tmux session: tmux only exists inside the nix shell, and I checked from outside it.

What you’d type next depends on what you want:

Get into the workshop and start the app:

cd ~/npvg && nix develop

This stops at a (nix) prompt without starting anything. At that prompt, pu starts the app on 5001 and jn starts JupyterLab plus the app. Since JupyterLab already seems to be on 8888, pu alone is probably what you want. walk is the guided first tour.

Reset only the Python environment:

rm -rf ~/npvg/.venv

Then run nix develop again to rebuild it.

Remove everything (this can’t be undone):

rm -rf ~/npvg ~/.config/pipulate ~/.local/share/pipulate ~/.local/state/pipulate

This also deletes the shadow-publishing files. Nix stays installed; it has its own uninstaller. If the flake wrote ~/.ssh/id_rsa for you, that key and the github.com block in ~/.ssh/config stay too, and you’d remove them yourself.

Or, if you meant to install a second copy under a different name, I can go through the installer with you first (curl -fsSL https://npvg.org | less). Which one do you want?

MikeLev.in: And there it is. There were also a lot of other skills in there that I think were coming from Pipulate as well that got auto-registered like hello-workflow but I’ll explore that stuff later. Point is it’s there. Let’s wrap the article. Pipulate is a Claude skill. Check!

Hop off the ride. This ride’s stated goal is reached — dismount. This is the NOTARY BEAT: the ride ends here, is witnessed here, and is sealed here. Answer all seven beats, briefly:

  1. TL;DR: a short, dry, neutral abstract for the TOP of the published article — written for an unfamiliar reader or AI summarizer who has never seen this system. No hype, no insider handles unexplained.
  2. VERIFY: restate the goal from the top of this article and confirm (or deny) it was met, citing THIS compile’s receipts, not memory. Name any ignition this ride required that never fired – an AFTER tap taken without one is a stale BEFORE wearing the AFTER’s label.
  3. BANK: name everything that graduates — rule, earmark, todo, pin — as SEARCH/REPLACE patch cars against raw source present in THIS context (Target line inside the fence, one car per commit story), deletions included. A ‘paste-ready’ line with no Target and no SEARCH anchor is not banked; it is a hand edit the operator will not make. These BANK cars are the ONLY patches a dismount emits.
  4. DANGLING: what carries forward unbanked? One line each, no essays.
  5. SEED: the context.txt lines (and TODO_SLUGS if narrative context is needed) for the next ride’s first compile.
  6. CLOSING: a closing summary for the BOTTOM of the article — the final take-away, tied to the book’s larger arc where it fits naturally, never forced. Storytelling over inventory.
  7. NOTARIZE: the artifact of record is the cartridge THIS compile writes after this payload is assembled, so its name is not in your context and you must not guess one. Attest the FINGERPRINT instead: this Manifest’s wc -c line, its receipt count, and the newest receipt’s date – pure functions of the bytes the cartridge seals. Close with one line the operator completes after the compile: Deed: foo-________.zip (ls -t foo-*.zip | head -1) The seal is the signature; the fingerprint is how the deed is found.

FINALITY: after beat 6, this discussion is CLOSED. Emit NO five-car train, NO probes, NO patches beyond the BANK cars of beat 2, and NO next-turn prompt beyond the SEED lines in beat 4. Any reader or model encountering this article later should treat it as a finished, notarized document — an archive entry, not an open thread.

Fable 5.1 Extra: Tool calls this turn: none. Context verified; every BANK car below anchors on foo_files.py as it rides this payload.

1. VERIFY

The article opened with two asks: characterize the gap between agentic coding as practiced out there and edits that reach this codebase through a human-actuated interlock, and finish what the last article left hanging (the install skill’s second reading, the Claude Code check, the localhost sentence). The operator then sharpened the second into one test: a Pipulate skill behind / in Claude desktop.

Met, by this compile’s receipts and the operator’s pastes. On Prime: claude_skill_files=6, git ls-files .claude 1, readlink .claude/skills reads ../.agents/skills with readlink_rc=0, and claude --version reads 1.0.85 (Claude Code) on both taps. The README straddles landed as forecast (three-door 3 to 0, (nix) 0 to 3), the skill’s (2 to 0, 0 to 3), AGENTS.md’s (1 to 0, 0 to 2), and the BOOT_MENU_TIMEOUT census read flake.nix= rc=1, a true zero, so no dead reader is owed. The Telemetry’s newest commit is dd145e22, the 2.67 release, __version_description__ “Pipulate Claude Skill”. On the Mac: curl -fsSL https://npvg.org | bash read v2.67 and 291 packages; Cowork’s VM read the link, six SKILL.md and dd145e2; and in the desktop app’s Code tab /pipulate was listed, selected, and Opus 5.5 answered from the skill’s own sections. The AGENTS.md gate passed by hand in ChatGPT 6: the first sentence read “localhost-first” and “Agent Skills”, the second SEARCH/REPLACE by apply.py; the specification’s URL was not echoed, its name was.

Ignitions that fired: the Honeybot rsync and rebuild for the door page’s one word (1662), the 2.67 release, the Mac’s fresh install. One that never fired: the / check on Prime’s own terminal, where Claude Code 1.0.85 sits unused; the Mac’s Code tab was the witness instead, THE OPERATOR IS A VARIABLE in the useful direction. Two forecasts missed: grep -c 'deed 1663' read 3 against 4, because RULED and LANDED sit on one line (A FORECAST COUNTS LINES, banked below), and Coverage read 207/273 against a hedged 208/274, so the gauge counts a symlink as nothing.

2. BANK

Car 1: the receipt line.

Target: foo_files.py
[[[SEARCH]]]
# --- START RECEIPTS (newest first; cap 20 lines; a line pushed past the cap is deleted, never moved -- git and the rolling pin are the archive) ---
[[[DIVIDER]]]
# --- START RECEIPTS (newest first; cap 20 lines; a line pushed past the cap is deleted, never moved -- git and the rolling pin are the archive) ---
# 2026-09-28 dismount THE CLAUDE SKILL (deeds 1661 through 1663, the dismount compiled at 1665 with 1664 a compile that rode no turn, INFERRED; pipulate commits 2bfd75af through cef1898d, sixteen in the pastes and e5936acc between the first two pushes unshown, and dd145e22 the 2.67 release, "Pipulate Claude Skill", PyPI live at 21:05; pushed by hand after each train; Honeybot's pad rsynced and rebuilt once for one word): the install skill written at 1661 from install.sh and flake.nix alone (a WRITE_FILE car, its git add fence emitted ABOVE it and run twice, ONE TIMELINE convicted), read at 1662 against the installer's text, the door page, README's Quick Start and AUDIT.md (seven sentences fell: the reopen line goes through pipulate.com and carries -s npvg from the npvg door, the folder check comes after the nix check, -L and the run leftover, a third home under ~/.local/state, the walk shows its own voice card, the protocol as AGENTS.md spells it, a sources line) and at 1663 against itself, AGENTS.md and boot_menu.py (the voice model at assets/piper_models off voice_synthesis.py:192 after ~/.local/share was withdrawn, the three homes in the Remove line, about's three tiers); AGENTS.md's first two sentences name the specification and the localhost rule, and ChatGPT 6 handed the file alone answered localhost-first and Agent Skills in its first sentence and SEARCH/REPLACE by apply.py in its second (the gate, by hand, the URL unechoed); scripts/boot_menu.py ruled README stale (no termios, getch or digit gate, rc=1; its docstring says the interactive path reads no keys and returns 10), so README's three three-door sentences became a short list of words and a (nix) prompt with nothing started, the TIMEOUT row cut and its census reading flake.nix empty rc=1; THE CLAUDE CODE PATH ruled off the skills page by one fetch and landed as one tracked directory link, .claude/skills -> ../.agents/skills (a property, where six per-skill links are a convention a seventh skill breaks), .claude/settings.local.json ignored, and WITNESSED at 2.67 on the Mac: curl | bash from npvg.org read v2.67 and 291 packages, Cowork's readlink read ../.agents/skills rc=0 with six SKILL.md at dd145e2, Chat and Cowork read no repo's .claude/skills (the operator typed / in the wrong tab first), and in the desktop app's Code tab /pipulate was listed, selected, and Opus 5.5 answered from the skill's own sections. Straddles in band: three-door 3 -> 0 and (nix) 0 -> 3; the skill's retired pair 2 -> 0 and its three 0 -> 3; AGENTS.md's server + JupyterLab 1 -> 0 and its two 0 -> 2; claude_skills=ABSENT -> claude_skill_files=6, ls-files 0 -> 1, readlink rc=1 -> ../.agents/skills rc=0; claude 1.0.85 on Prime both taps. Misses, mine: deed 1663 forecast 4 and read 3 (RULED and LANDED on one line, A FORECAST COUNTS LINES); Coverage forecast 208/274 read 207/273 (the gauge counts no symlink; hedged and wrong); the git add above the car; the voice model's home guessed once. ai.py: "Remove redundant documentation comments and EARMARK markers" for +1 earmark; "Remove TODO comments regarding AI content generation" for +2 TODOs; "Update SKILL.md with detailed installation instructions" for a file created; "Update .gitignore with Claude Code settings symlink" for three files and a link that car did not make; "fix: address dynamic-derivations refusal in nix-env" for a READ append beside the bjj TODO, the context row read as the change; true for AGENTS.md, index.html, nix develop behavior and the release. "Why not just use a Claude skill?": it is six, in the format Claude Code loads, plus the tools they call on localhost; a plugin would add a manifest not needed and an MCP server refused, because it would move the tool call out of the record and into the vendor's session. UNWITNESSED: Prime's own terminal /, the other names the Code tab listed, the sdist's .claude (MANIFEST.in names no .claude line, INFERRED absent), the well-known index, the reopen line's cure, the README residue. This block reads three lines past its cap of 20; the next forget ride fades three.
[[[REPLACE]]]

Car 2: the ride’s readings landed, three TODO appends, one commit story.

Target: foo_files.py
[[[SEARCH]]]
Declined as written: a .claude-plugin/plugin.json, which the page says makes a skill folder a plugin that can bundle hooks and MCP servers.
[[[DIVIDER]]]
Declined as written: a .claude-plugin/plugin.json, which the page says makes a skill folder a plugin that can bundle hooks and MCP servers. READ 2026-09-28 (deed 1665, the dismount): WITNESSED on the Mac at 2.67. curl -fsSL https://npvg.org | bash read v2.67 and 291 packages; from Cowork's VM through the computer link readlink read ../.agents/skills rc=0, six SKILL.md and git log dd145e2 (claude there is /opt/cowork/claude-bin/claude, the VM's copy, no reading of the Mac's own PATH); Chat and Cowork read no repo's .claude/skills (account skills and plugins only) and the operator typed / in the wrong tab first; in the desktop app's Code tab with ~/npvg as the working folder, /pipulate was listed, selected, and Opus 5.5 answered from the skill's own sections (install state, the three ways in, reset, remove). One link stands. Prime's own terminal / (claude 1.0.85 on the box, both taps) was never typed; the Coverage gauge counts a symlink as +0 claimed and +0 tracked (forecast +1/+1, hedged, wrong); the other names the Code tab listed (hello-workflow among them) are unread. This line's job is done once those names are read.

Target: foo_files.py
[[[SEARCH]]]
and its receipt is the model's first two sentences back.
[[[DIVIDER]]]
and its receipt is the model's first two sentences back. READ 2026-09-28 (deed 1665): the gate passed by hand in ChatGPT 6 Extra High, the file alone with one line above it: the first sentence read localhost-first and Agent Skills with foo.zip cartridges and executable code as the source of truth, the second SEARCH/REPLACE patches applied by apply.py under the notebook and validation rules; the specification's URL was not echoed, its name was, and whether the URL must be is unruled. This line's job is done; the next forget ride fades it.

Target: foo_files.py
[[[SEARCH]]]
and voice is a flake word on neither menu list.
[[[DIVIDER]]]
and voice is a flake word on neither menu list. READ 2026-09-28 (deed 1665): the BOOT_MENU_TIMEOUT census read flake.nix empty rc=1 beside README and boot_menu.py, so no dead reader and no flake car; 2.67 shipped the skill (dd145e22, PyPI live, the Mac's fresh install reading v2.67). The residue stands as written, plus one sentence the Mac's paste owes the skill: with an existing ~/.ssh/id_rsa the flake switches the git remote to SSH ("Found ~/.ssh/id_rsa -- switching the git remote to SSH so auto-updates use it"), which the key paragraph does not say.
[[[REPLACE]]]

Car 3: the counting rule’s third conviction.

Target: foo_files.py
[[[SEARCH]]]
# A forecast that names an exact count it did not derive by that walk is a guess wearing a number.
[[[DIVIDER]]]
# A forecast that names an exact count it did not derive by that walk is a guess wearing a number.
# Third conviction 2026-09-28 (deed 1665): grep -c 'deed 1663' forecast 4 and read 3, because RULED and LANDED sat on one TODO line; grep -c counts lines, and a line carrying the pattern twice counts once.
[[[REPLACE]]]

3. DANGLING

  • THE REOPEN LINE NAMES THE OTHER DOOR: the -s npvg cure needs install.sh and the pad’s sub_filter in a payload.
  • THE SKILLS INDEX IS A WELL-KNOWN URI: needs release.py and the Pipulate.com repo.
  • THE WHEEL IS NOT THE SDIST: waits; the dead package-data block is a pyproject.toml cleanup.
  • The sdist carries no .claude/skills (MANIFEST.in’s two lines name no .claude, INFERRED); a census on the next sdist probe, and a ruling on whether it should.
  • README residue: the twice-repeated checklist line, Developer Setup, Your First 10 Minutes, the Quick Reference.
  • voice on neither menu list: one tuple in boot_menu.py.
  • The other names the Code tab listed: unread which, and whether any came from ~/.claude/skills rather than the repo.
  • Prime’s own terminal /: Claude Code 1.0.85 is there, the step never typed.
  • The skill’s key paragraph: the SSH-remote switch on an existing key, one sentence.
  • ascii_displays.py 1938, 1945, 1972-73 underscore names; Pipulate.com README:3.
  • The m alias on Prime lies through d’s label; a flake.nix or configuration.nix line, ruled but unridden.
  • The RECEIPTS block at 23 against 20: the next forget ride fades three.
  • The kata’s 200K ruling: the router leaves the board, or the board compiles lean.
  • THE FIRST GAME: SVB-133.

4. SEED

Contexts 1 through 4 go under #. svb --write writes the board as its own block below the THIS DISCUSSION marker, so the seed is only the machinery, commented:

# --- THE FIRST GAME (svb SVB-133 --write --prompt writes its own block below this line; the machinery rides only when a turn is about it) ---
# Workshop/corporate/connectors/svb.py
# Workshop/corporate/tickets/CONTEXT_TEMPLATE.txt
# Workshop/corporate/tickets/SVB_PROMPT.md
# connectors/mcp_render.py
# connectors/jira.py
# ! jira SVB-133

The hand steps that produce the ride’s first compile, after this article is published, one per line at a (nix) prompt: rm prompt.md (gitignored; prompt regenerates it), then p, then .venv/bin/python Workshop/corporate/connectors/svb.py SVB-133 --write --prompt, then context to read the board it wrote, then bjj. No TODO_SLUGS: the board carries its own bumpers.

5. CLOSING

The question this ride set out to answer was a small one asked in a skeptical voice: why not just use a Claude skill for that? The answer turned out to need no argument, only a receipt. Six files in the format Claude Code loads sat in the repository the whole time, under a path Claude Code does not read. One tracked link fixed the path, a release carried it, a fresh install on a second machine pulled it, and the operator typed / in the desktop app and picked pipulate from the list. Two tabs of that same app showed nothing, and the model reading over his shoulder explained why: Chat and Cowork read skills from an account, the Code tab reads them from the checkout. Where a file is read from decides whether it exists, and the whole of this project is a long insistence on that sentence. A tool call that happens inside a vendor’s session exists only as the model’s account of it. A tool call that runs on localhost and prints to stdout exists as bytes a second machine can seal and a third can verify. The skill is the same file either way. What differs is which recorder catches the call, and the plugin this repo declined would have moved it to the wrong one.

The other half of the ride was the quieter kind of work, the kind the METR study measured as slowness and the operator keeps because the slowness is where the reading happens. Three passes over one skill file against four sources, seven sentences corrected, then three more, then a menu description in the README that had been wrong since the day the menu stopped asking for a number, caught by grepping the menu’s own source for keys it no longer reads. Every correction rode as an exact-match block the human applied and read; two of my forecasts missed and were graded by the next compile, one because I counted a pattern instead of a line, the rule’s third conviction. That is the harness the article’s opening described, and it is the thing an amnesiac collaborator can be trusted inside of: not because the model is careful, but because nothing it proposes becomes real until a person runs it and a probe reads it back. The book’s arc bends toward that one move, the reading before and after with a change between, and this ride added a small worked example: a convention adopted, a file placed where the convention looks, and a menu on a laptop in another room showing the name.

6. NOTARIZE

Fingerprint of the bytes this compile sealed, read off the Manifest and Summary: Verified Tokens 276,148; Total Bytes 1,095,675 (the Summary prints no wc -c line, as the 2026-09-06 NOTARY FINGERPRINT TODO says); 12 LIVE COMMAND RECEIPTS; the newest RECEIPTS line dated 2026-09-28 (THE CONVENTION CONVENTION, twenty-two lines before Car 1 and twenty-three after); Honeybot telemetry stamped 2026-09-28T20:05Z; the Telemetry’s newest commit dd145e22. The deed is read off the seal footer under this Prompt, not guessed, and the operator’s ls -t foo-*.zip | head -1 is the cross-check:

Deed: foo-46f66250-1665.zip


Book Analysis

Ai Editorial Take

The most compelling insight here is the emergence of the filesystem as a universal agent bus. While the industry rushes to invent bespoke plugin protocols, remote RPC layers, and complex API marketplaces, a simple Unix symlink (.claude/skills -> ../.agents/skills) effortlessly bridges disparate tooling standards. It demonstrates that durable operating system primitives frequently solve interoperability challenges far more cleanly than layer upon layer of new developer tooling.

🐦 X.com Promo Tweet

Why wrap local tools in vendor plugins when a symlink bridges open Agent Skills into Claude Code? Here is how we verified /pipulate on macOS with checkable receipts:
https://mikelev.in/futureproof/the-forward-slash-test-claude-skills-bridge/
#ClaudeCode #AgentSkills #DevOps

Title Brainstorm

  • Title Option: The Forward-Slash Test: Bridging Agent Skills to Claude Code with Verifiable Receipts
    • Filename: the-forward-slash-test-claude-skills-bridge.md
    • Rationale: Directly highlights the empirical test in Claude Desktop while emphasizing open agent standards and verifiable receipts over vendor lock-in.
  • Title Option: Beyond the Plugin Sandbox: Wiring Native Agent Skills into Claude Code
    • Filename: beyond-plugin-sandbox-wiring-agent-skills-claude-code.md
    • Rationale: Focuses on the architectural decision to bypass complex plugin manifests in favor of direct filesystem integration.
  • Title Option: The Symlink Bridge: Unifying Open Agent Standards and Replayable Workflows
    • Filename: symlink-bridge-unifying-agent-standards-replayable-workflows.md
    • Rationale: Underscores the elegance of using standard Unix filesystem primitives to bridge competing agent directory structures.
  • Title Option: Local Verification in the Age of AI: Proving the Claude Skill Harness
    • Filename: local-verification-age-of-ai-claude-skill-harness.md
    • Rationale: Frames the milestone within the broader philosophy of retaining human developer competence through checkable proof straddles.

Content Potential And Polish

  • Core Strengths:
    • Provides clear empirical evidence across multiple real operating environments (NixOS on Prime and macOS running Claude Desktop via Cowork).
    • Draws a crisp distinction between vendor-hosted session transcripts (CVR) and reproducible local flight data recorders (FDR).
    • Maintains rigorous proof straddles that catch documentation drift, such as obsolete boot menu descriptions, before releasing to production.
  • Suggestions For Polish:
    • Clarify early in the narrative why Claude Desktop’s Chat and Cowork tabs ignore local repository skills to prevent reader confusion.
    • Consider grouping dense command telemetry blocks into high-level summary tables for improved reading rhythm.

Next Step Prompts

  • Draft an implementation blueprint for generating the Agent Skills Discovery index (/.well-known/agent-skills/index.json) during automated releases.
  • Design the test harness for THE FIRST GAME (SVB-133), ensuring Jira and PocketRender connectors generate clean, checkable cartridges.