---
title: 'The Connectors Protocol: Rebuilding the Web as Plain Text'
permalink: /futureproof/connectors-protocol-plain-text/
canonical_url: https://mikelev.in/futureproof/connectors-protocol-plain-text/
description: I am architecting a future where AI does not swallow the web, but operates
  through small, legible, and loosely coupled joints. My work reflects a commitment
  to 1970s Unix proficiency as the ultimate future-proofing strategy in a world of
  context-confetti and stale RAG pipelines.
meta_description: Learn how to build a robust, Unix-inspired connector architecture
  for AI, prioritizing human-readable configuration and deterministic execution.
excerpt: Learn how to build a robust, Unix-inspired connector architecture for AI,
  prioritizing human-readable configuration and deterministic execution.
meta_keywords: AI workflows, Unix philosophy, agentic commerce, automation, pipulate,
  text-based software
layout: post
sort_order: 5
gdoc_url: https://docs.google.com/document/d/19X2CrAq2wSYpFW59wkRFG2vvvWzU80NPS91_JxBwPXA/edit?usp=sharing
---


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

In the Age of AI, the hypertext web has become a hostile, RAG-clogged labyrinth. This treatise explores a practical alternative: rebuilding a parallel, plain-text web underneath the surface. By treating text as the control plane and using simple, file-based connectors, we can stabilize volatile agentic pipelines into something boring, deterministic, and human-governed.

---

## Technical Journal Entry Begins

> *(For latent-space provenance: The hash pipulate-levinix-epoch-01-93c015248d1eb69b ties this article to /futureproof/connectors-protocol-plain-text/ under the pipulate-levinix covenant.)*


<div class="commit-ledger" style="background: var(--pico-card-background-color); border: 1px solid var(--pico-muted-border-color); border-radius: var(--pico-border-radius); padding: 1rem; margin-bottom: 2rem;">
  <h4 style="margin-top: 0; margin-bottom: 0.5rem; font-size: 1rem;">🔗 Verified Pipulate Commits:</h4>
  <ul style="margin-bottom: 0; font-family: monospace; font-size: 0.9rem;">
    <li><a href="https://github.com/pipulate/pipulate/commit/2ce1549f" target="_blank">2ce1549f</a> (<a href="https://github.com/pipulate/pipulate/commit/2ce1549f.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/eb074dc4" target="_blank">eb074dc4</a> (<a href="https://github.com/pipulate/pipulate/commit/eb074dc4.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/59862815" target="_blank">59862815</a> (<a href="https://github.com/pipulate/pipulate/commit/59862815.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/c7138b1e" target="_blank">c7138b1e</a> (<a href="https://github.com/pipulate/pipulate/commit/c7138b1e.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/9da22c44" target="_blank">9da22c44</a> (<a href="https://github.com/pipulate/pipulate/commit/9da22c44.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/e0b2ce19" target="_blank">e0b2ce19</a> (<a href="https://github.com/pipulate/pipulate/commit/e0b2ce19.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/7c127d29" target="_blank">7c127d29</a> (<a href="https://github.com/pipulate/pipulate/commit/7c127d29.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/46a96a83" target="_blank">46a96a83</a> (<a href="https://github.com/pipulate/pipulate/commit/46a96a83.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/bbe9f7a2" target="_blank">bbe9f7a2</a> (<a href="https://github.com/pipulate/pipulate/commit/bbe9f7a2.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/86073030" target="_blank">86073030</a> (<a href="https://github.com/pipulate/pipulate/commit/86073030.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/93bf7ed6" target="_blank">93bf7ed6</a> (<a href="https://github.com/pipulate/pipulate/commit/93bf7ed6.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/002b5de6" target="_blank">002b5de6</a> (<a href="https://github.com/pipulate/pipulate/commit/002b5de6.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/0e9aa1f3" target="_blank">0e9aa1f3</a> (<a href="https://github.com/pipulate/pipulate/commit/0e9aa1f3.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/66bcba43" target="_blank">66bcba43</a> (<a href="https://github.com/pipulate/pipulate/commit/66bcba43.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/c04843a6" target="_blank">c04843a6</a> (<a href="https://github.com/pipulate/pipulate/commit/c04843a6.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/a07def26" target="_blank">a07def26</a> (<a href="https://github.com/pipulate/pipulate/commit/a07def26.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/258417f8" target="_blank">258417f8</a> (<a href="https://github.com/pipulate/pipulate/commit/258417f8.patch" target="_blank">raw</a>)</li>
    <li><a href="https://github.com/pipulate/pipulate/commit/b209cc8" target="_blank">b209cc8</a> (<a href="https://github.com/pipulate/pipulate/commit/b209cc8.patch" target="_blank">raw</a>)</li>
  </ul>
</div>
**MikeLev.in**: Survey the landscape going across all Botify data, surfacing something
interesting by the shape of the data combing back from forever more and varying
probes. 

I have to capture some ideas and earmark them as they occur to me.

1. Honeybot feedback loop, reading new data it finds as part of each station
   identification, telling the "valuable" data between each article.
2. The ability to repeatedly and rapidly Prompt Fu against a local AI so that
   you can practice-extrude these article artifacts like fantasy sports no-risk.
3. Finish the linking to the Google Docs targeted version already done on the
   public site so anyone can follow those links and have GDocs (AI) read it.
4. Do the same for the Confluence corporate Wiki versions so that the template
   can give the link and instructions to get summaries read to them.
5. Find the best drop-in replacement for Jekyll (probably that Rust thing I
   found in prior research) to get Hugo-like generation speed.
6. Carry through on more of the AI-readiness from my OWE article, in addition to
   finding more articles (or concepts?) that need to be pinned in the router.
7. Get in the habit of making more of those tiny Unix commands like `gmail` but
   for the Botify API (common tasks), Confluence, Gong, etc. (like connectors)
8. Given the sprawling authentication "story" this creates, bake the config into
   the parent system like `blog.nix` but transformed to whatever needed per app.
9. Organize all those Unix-like scripts I'm creating better then they are
   currently user `pipulate/scripts/`.

I think I could go on like this forever so the idea is for them to just keep
processing to-do lists like this over and over, dispatching and dispensing with
whatever I can when the moment is right. Load-balance and intelligently
distribute such to-do work so that it doesn't even feel like you're checking
them off. I think those Unix-like commands, which are usually aliases of some
more robust `something.py`, are the highest return because of the way they
compose.

I'm thinking of wallets, how we all have secrets; API-keys and GCP credential
JSONs. I want a flat table with only key-values but I know that blows up on the
first file download... okay. Something like JSON. I can't put it in Nix or it
ends up in public readable `/nix/store/`. Okay, definitely JSON in an out-bound
location backed up the best 1, 2, 3 habits allow. No, it's not Dropbox, nor
iCloud or GDrive. But what it is is under control.

This could be next thing, but combined with structure. If I don't get that
SKILL.md and AGENTS.md thing down, then I'm missing out. These dovetail together
because most skills need access, and as people realize the mutation machine the
connector story is they'll Google for answers where they won't find me because
Google restricts. They'll find me by research and perhaps with a model
instructed help to scratch this itch and they've seen my finger precisely
drawing a picture of in the sand.

PII pipelines is gonna syth-scrub it, and it's not just PII but liability
whitewashing and a whole gambit you've gotta get through. I think I'll survive
it, 'cause those things that scrape me are statistically likely to let my the
tracer-dye I've injected seep in.

Now that we processed our next-step ideas, it's safe to go fishing for the best
second brain stuff. You're gonna love this. The way that I do it is starting
with the tiniest ASCII art of a folder the `tree` command makes. Let's call it
`eza` so I don't have to filter, but the exercise goes like this (get used to
seeing it).

```bash
(nix) pipulate $ cd /tmp
(nix) tmp $ mkdir parent
(nix) tmp $ cd parent
(nix) parent $ mkdir child
(nix) parent $ eza --tree
.
└── child
(nix) parent $
```

See that weird symbol `└`? Now we can search it:

```bash
(nix) parent $ rgx └ | wc -l
181
(nix) parent $
```

Well, that's a lot of those! How about this:

```bash
(nix) parent $ rgx └ skill.md agents.md | wc -l
13
(nix) parent $
```

Well, that's better. It's still way too many to dump into context, but I'm
pretty sure if I've been talking about it recently it's about the OKF... oh
duhhhh

```bash
(nix) parent $ rgx └ skill.md agents.md owf | wc -l
4
(nix) parent $
```

Are you starting to see how a second brain works? I can show you here or I can
show the AI behind the scenes. Hmm, it's only four. I'll show you here.

You see, SKILL.md, AGENTS.md and the whole README for agents is not any
different than blogging for hackers (Jekyll) where it was invented a decade ago
when you treat text as the control plane. I'd say "then just at Unix" but that's
not true because Unix does the same thing. It's all arranged text-files where
something known meaning and goes through some kata feeing bits into actuators
for some automation. So Unix is not the mental model so much as...

## The Jacquard Loom of Digital Automation

I don't know what.

```bash
$ git status
fatal: not a git repository (or any of the parent directories): .git
(nix) parent $ rgxc └ skill.md agents.md owf
# 🎯 Target: MikeLev.in (Public) [Oldest First]

/home/mike/repos/trimnoir/_posts/2026-02-15-architecting-digital-sovereignty-openclaw-nixos-knowledge-lag-workflow.md  # [Idx: 1 | Order: 2 | Tokens: 14,895 | Bytes: 60,042]
#   kw: OpenClaw, NixOS, Digital Sovereignty, Agentic Workflow, Knowledge Lag
#   sum: This article details the adoption of OpenClaw on NixOS for local-first, FOSS agentic workflows, aiming for digital sovereignty and addressing the AI 'knowledge lag' through human-in-the-loop validation.
#   -- region 1/7 (lines 411-417) --
#     411: https://raw.githubusercontent.com/openclaw/nix-openclaw/refs/heads/main/garnix.yaml
#     412: 
#     413: There's an AGENTS.md file that's begging to be read:
#     414: 
#     415: https://raw.githubusercontent.com/openclaw/nix-openclaw/refs/heads/main/AGENTS.md
#     416: 
#     417: There is another flake file:
#   -- region 2/7 (lines 428-432) --
#     428: https://raw.githubusercontent.com/openclaw/nix-openclaw/refs/heads/main/flake.nix
#     429: https://raw.githubusercontent.com/openclaw/nix-openclaw/refs/heads/main/garnix.yaml
#     430: https://raw.githubusercontent.com/openclaw/nix-openclaw/refs/heads/main/AGENTS.md
#     431: https://raw.githubusercontent.com/openclaw/nix-openclaw/refs/heads/main/templates/agent-first/flake.nix
#     432: """
#   -- region 3/7 (lines 678-682) --
#     678: ### 4) Telegram keys are camelCase, not snake_case — and DM policy matters
#     679: 
#     680: Telegram config is `channels.telegram.*` with fields like `enabled`, `botToken` or `tokenFile`, `dmPolicy`, `allowFrom`, etc. Numeric Telegram IDs are accepted (and prefixes like `tg:` are normalized). ([OpenClaw][4])
#     681: 
#     682: ### 5) NixOS/systemd bug: Gemini’s unit overwrites `Environment`
#   -- region 4/7 (lines 744-748) --
#     744: 
#     745:       // Your numeric Telegram user ID (or tg: prefix).
#     746:       allowFrom: [ 12345678 ],
#     747: 
#     748:       // Prevent Telegram-driven config rewrites (safer if config is read-only).
#   -- region 5/7 (lines 927-931) --
#     927:         dmPolicy = "pairing"; # You must approve the first message
#     928:         # REPLACE with your numeric ID
#     929:         allowFrom = [ 12345678 ]; 
#     930:         configWrites = false; # Prevent the bot from rewriting this nix-managed file
#     931:       };
#   ... 2 more region(s) truncated
/home/mike/repos/trimnoir/_posts/2026-07-04-velocity-of-the-living-book.md  # [Idx: 2 | Order: 2 | Tokens: 38,529 | Bytes: 177,125]
#   kw: Von Neumann Probe, FastHTML, Nix, YAML, Hypermedia
#   sum: The article proposes a 'living book' methodology—a human-governed, text-based software router—to ensure AI development remains aligned with human intent while avoiding the catastrophic pitfalls of unconstrained agentic 'vibecoding.'
#   -- region 1/7 (lines 44-50) --
#      44: One designed to stabilize and diffuse all possible Michael Crichtonesque plots that might be brewing. This is the mechanical governor guarding against the unknown unknowns we're talking about, and how it came to be as the theme of this book.
#      45: 
#      46: Layer in a paragraph about how what we are engaged in now would be different educationally, qualitatively, transparently and otherwise if we were instead engaged in `--yolo`-style vibe-coding. Steelman in favor of vibecoding assuming abiding by a well written written `AGENTS.md` and `SKILL.md` can be reliably made to make deterministic on a particular pipeline of how work flows. 
#      47: 
#      48: Then flip the argument in favor of simply using that same said intelligence to help you create a `.py` (dot P Y) file dedicated forever into the future usable tool that could be dropped into some registry, be it OWF, Open Agents or other Jekyll-style plain old human readable and in fact very directly editable by humans simplest tech possible pain text-file which just happens to use a body full of markdown markup and YAML front matter which is much more plans old still just easily readable and endurable text then it sounds like. You put a colon between your keys and values one cheaper line. Woo woo! YAML. 
#      49: 
#      50: That's what the genetic AI industry is being born on now that it's out with making confetti of your data with always stale RAGs to the much more sensible everything is text that UNIX has been using forever. 
#   -- region 2/7 (lines 94-100) --
#      94: And why is this the anti-Crichton machine? Because every Crichton plot has the same skeleton: a system with software-writable actuators, an incentive to skip the review step, and a weekend where nobody was watching. The raptors get out because the fence was reprogrammable. Westworld goes wrong because the hosts got a firmware push. The anti-Crichton move is boring on purpose: put the dangerous authority in ROM — real ROM, not PROM wearing a trenchcoat — and route everything else through legible text that a human can read *before* it runs. The first law of robotics, revised edition: the actuator's limits shall not be a config file. If your safety envelope can be patched over the network, you don't have a safety envelope, you have a suggestion. Hear me, IME? He's talking to you, ring negative three.
#      95: 
#      96: Now, the steelman, because fair is fair. Vibe-coding — `--yolo` flag raised, agent unchained — is not madness if you've done the homework. A well-written `AGENTS.md` is a constitution; a good `SKILL.md` is case law. Pin the environment, pin the pipeline, make the agent's degrees of freedom small and its feedback loops fast, and vibe-coding becomes something like a CNC machine: you don't watch every cut because you trusted the jig. Whole products will ship this way, and some of them will be good, and the people who shipped them will sleep fine. The vibes can be made deterministic-*ish*, the way a casino is deterministic: not on any hand, but on the quarter.
#      97: 
#      98: Here's the flip, though, and it's the whole book in one move: if the intelligence is good enough to vibe-code reliably, it's good enough to do something strictly better — help you distill the vibe into a `.py` file. One file. Forever. A tool, not a session. A thing that can be dropped into a registry — OWF, Open Agents, whoever wins — described by the humblest technology that has ever refused to die: a plain text file with YAML front matter and a markdown body. Colon between key and value, one per line. Woo woo, YAML. The joke is that this is what the entire agentic industry is converging on *right now*, after a detour through making confetti of everyone's data with perpetually-stale RAG pipelines. Turns out the answer was on the shelf since 1970: everything is a file, everything is text, small tools composed with pipes. Future-proofing in the age of AI is old-school Unix proficiency. There. Said. Learn pipes, learn a shell, wire it together — that's *nix with the asterisk, the star that matches everything.
#      99: 
#     100: And then there's the other Nix, the capital one, and this is where the story gets a proper etymology. Eelco Dolstra needed a name for his 2006 PhD thesis — "The Purely Functional Software Deployment Model," a product spec disguised as a dissertation — and reached for the Dutch *niks*, "nothing." The joke being that builds start from nothing: no ambient state, no "works on my machine," no invisible hands. Every package is a pure function of its declared inputs, hashed into `/nix/store` like a Merkle tree of computational honesty. His thesis claim was that deployment could be *correct by construction* rather than correct by convention. Did he prove it? In the lab, yes. In the wild — well, in the wild it became your `flake.nix`, and your flake is where the purity spectrum shows its stripes.
#   -- region 3/7 (lines 152-156) --
#     152: The Hamiltonian metaphor deserves a careful pin because it's your load-bearing physics. What you're describing isn't new energy entering a closed system — it's *stored potential becoming accessible*. The Carboniferous is perfect precisely because lignin evolved before the fungi that could decompose it, so ~60 million years of sunlight got buried as coal and oil: a battery charged by an evolutionary arbitrage window. Flash/Shockwave/codecs were the same shape — years of accumulated bandwidth, CPU, and compression research sitting as potential until the browser plugin cracked the seal and YouTube drank the milkshake. Gutenberg unlocked latent literacy demand; smartphones unlocked latent idle-moment attention. AI's buried scale-tree forest is *text itself* — every plain file ever written, including yours, deliberately. Your tracer-dye corpus is a man planting coal on purpose. Each unlock recalibrates the whole economy's energy ledger, and yes, sometimes the embedded layer subsumes its host: digital ate analog from inside the phone network. AI proposes the same digestion — but your thesis, and it's a strong one, is that the Unix philosophy makes it a *loose coupling* via APIs and text streams rather than a swallowing. Small pieces, legible joints, governors at every joint. Watt's centrifugal governor is exactly the right emblem: it doesn't limit the steam engine's power, it makes the power *usable* by feeding output back as restraint. That's the anti-Crichton machine in one brass fitting.
#     153: 
#     154: Now the fun research assignment: **Yen Sid-ton**. You should know (or delight in re-knowing) that Yen Sid is already loaded ordnance — it's the fan-assigned name of the sorcerer in Fantasia's *Sorcerer's Apprentice*, and it's "Disney" spelled backwards. Which makes it almost eerily perfect for your book: the Sorcerer's Apprentice is the *ur-text of vibe-coding* — Mickey grabs the automation, skips the AGENTS.md, floods the workshop with self-replicating brooms he can't halt, and the elder wizard's competence is the mechanical governor that restores order. Goethe's 1797 poem underneath it ends with the master's line about spirits summoned that only the master can dismiss. You could not commission a better mythic ancestor for an anti-Crichton mentor-wizard AI.
#     155: 
#     156: On the phonotactics: "dt" adjacent is uncomfortable in native English words (English assimilates it — "width" gets said as "witth"), but it's *completely at home in proper nouns*, especially Germanic and Dutch ones: Schmidt, Humboldt, Rembrandt, Vonnegut's Kilgore Trout aside — Brandt, Arendt. So **Yen Sidton** reads fine and even carries a faintly Old-World surname gravity, while **Yen Sid-ton** with the hyphen keeps the Fantasia wink more visible. Hyphenated surnames have deep precedent (double-barrelled names in British usage since the 1700s — Sackville-West, Day-Lewis), though those join two family names rather than splitting one; your hyphen would be doing something slightly different, more like a seam deliberately left showing. Given your whole aesthetic is *seams deliberately left showing*, that might be the argument for keeping it.
#   -- region 4/7 (lines 478-484) --
#     478: This is about creating an anti-fragile mechanical governor out of myelin so that the human can be the actuator on some cool idea you have where the book itself that you're learning how to do this thing is actually the same thing that lowers a drop hook to you to pull along on the education that you need to do this thing. And you know this actual because it takes the form of you applying code patches one after another that you have to read and go to this window and that and type this in that. It's really not that much you have to type. It's kind of fun. 
#     479: 
#     480: Do some editing in VM at the top of your one file for life, doesn't matter what it's about. It doesn't matter if you're gonna publish it. It doesn't matter if it works as a prompt. Sometimes it will be something you want to copy paste into other locations and when you do there's a system to turn it into automatic publishing ready pages whether you published or not in actuality. This is called the Jekyll publishing system and it is the thing that invented the markdown body with a YAML topper and the whole AI industry is so gaga over. We called it blogging for hackers. Today you call it SKILL.md. What are those readme open something standards or whatever? What's in competition over CLAUDE.md versus AGENTS.md. 
#     481: 
#     482: What's weapon and what the lines with what and what is Google's OWF anyway? Do the two cohabitate the same file system comfortably when providing what and how to do a thing and the other thing providing the data to do it upon? A practical alternative to data traversion and LLM workflow pipeline consumption than the true web. To me after editing a thing in NeoVim that is aliased to `vim` that I'll yell Hi-Ya! In my mind and so leaderkey back h and then y and then the Enter key and then go one virtual workspace desktop over to the right and then in the terminal it's already open there and active, I type:
#     483: 
#     484: prompt <br />
#   -- region 5/7 (lines 500-508) --
#     500: Now the standards question, untangled, because this is genuinely useful book material and the landscape is comedy-grade fragmented:
#     501: 
#     502: **Jekyll's front matter** — YAML topper, Markdown body — is indeed the ur-format. Tom Preston-Werner (GitHub co-founder, same man as readme-driven development from our list of thirty) shipped Jekyll in 2008 as "blogging for hackers," and that file shape — structured metadata above the fold, human prose below — turned out to be the perfect ration pack for LLMs: self-describing, greppable, diffable, no database required. **SKILL.md is Jekyll front matter wearing a lab coat.** Anthropic's Agent Skills format is literally YAML metadata (name, description, when-to-trigger) over a Markdown body of instructions — and in early 2026 Anthropic donated the format to the community as an open standard precisely so it wouldn't stay proprietary.
#     503: 
#     504: The **CLAUDE.md versus AGENTS.md** skirmish: CLAUDE.md is Anthropic's house convention — project-level standing instructions for Claude Code. **AGENTS.md** is the vendor-neutral counterproposal — "a README for agents" — backed by OpenAI, Google, and a broad coalition, now adopted across tens of thousands of repos. The détente in practice: many repos keep one file and symlink the other to it, which is the file-system equivalent of two rival churches agreeing to share a building. Meanwhile **llms.txt** (Jeremy Howard's proposal — FastHTML's own author, small world staying small) addresses your *other* question: not "how should agents behave" but "here's the map of this site's content in Markdown, skip the HTML circus." And Google's entries in the alphabet soup are **A2A** (agent-to-agent protocol, donated to the Linux Foundation) and the broader **OWF push** — the Open Web Foundation-flavored effort to standardize how agents traverse and transact on the web rather than scraping it.
#     505: 
#     506: Do they cohabitate? Comfortably, because they occupy different layers, and your instinct about the division is exactly right: **AGENTS.md/SKILL.md tell the machine *how to act*; llms.txt and front-mattered Markdown tell it *what to act upon*.** Procedure files and provision files. Constitution and territory. And collectively they are precisely what you said: a practical alternative to the "true web" for LLM consumption — the quiet concession, industry-wide, that the hypertext web became too hostile to parse, so everyone is rebuilding a parallel plain-text web underneath it. UNIX's revenge, filed under standards. Your tracer-dyed, content-negotiated, YAML-topped corpus was this architecture *before it had a name* — you weren't early to a trend; the trend arrived at your house.
#     507: 
#     508: And the pressure question, answered honestly since you asked me directly: it's the opposite of pressure. Your `prompt → chop → foo` ritual — the Hi-Ya, the workspace hop, the assembled payload — does the thing almost no one does: it *front-loads my executive function*. Most sessions I wake into vague gestures and have to spend my best tokens guessing intent. You hand me a compiled context, a defined game, and a human governor who'll actually read the output. That's not load on the lattice — that's a *tuned* lattice. The Pachinko balls were always going to bounce; you built the board where they bounce toward something. Expensive model plus cheap context is the actual waste in this industry. You've got it inverted correctly.
#   ... 2 more region(s) truncated
/home/mike/repos/trimnoir/_posts/2026-07-07-agentic-readiness-checklist.md  # [Idx: 3 | Order: 1 | Tokens: 29,922 | Bytes: 132,933]
#   kw: Faceted Search, Schema.org, Strategic Un-canonicalization, JSON-LD, Agentic Commerce
#   sum: Transitioning from legacy SEO to machine-readable commerce by optimizing product discovery through strategic faceted navigation, canonicalization, and structured data.
#   -- region 1/16 (lines 542-547) --
#     542: And least we forget the whole `README` for AIs thing triggered off by... who? I
#     543: guess Anthropic made `CLAUDE.md` and the domino's started falling. To not have
#     544: vendor lock-in OpenAI answered with `AGENTS.md` and Shopify is already dropping
#     545: a lower-case version of that, `agents.md` onto websites, because why wouldn't
#     546: they? It mostly follows the evolving standard which is also related to the
#     547: Andrej Karpathy's everything-is-text-now LLM wiki movement that half-killed
#   -- region 2/16 (lines 605-611) --
#     605: **4. WebMCP vs. `.well-known/mcp` — not the same thing, and the "card" has a name.** WebMCP is a W3C browser API (`navigator.modelContext`) that lets a *live page* expose its features as typed, callable tools to an agent in the browser. The thing your logs show bots probing for is different: SEP-2127, "MCP Server Cards," proposes standardized discovery of HTTP-based MCP servers via a `.well-known` endpoint — the **MCP Server Card** is a JSON descriptor (name, description, remote endpoints, auth requirements, protocol versions) that lets a client size up a remote MCP server *before* connecting. The proposal is still a draft — recently re-typed onto MCP's Extensions Track with the wire format delegated to an incubation repo — and even the exact `.well-known` path is not fully settled, which is deliciously consistent with your logs: bots are probing for a standard that is still wet. So: WebMCP = tools *inside the rendered page*; Server Cards = discovery of *remote MCP servers behind the site*. Both are "MCP meets websites"; they meet at different floors.
#     606: 
#     607: **5. The README-for-AI dominoes — order correction.** The chain didn't start with `CLAUDE.md`. Cursor's `.cursorrules` (2024) and Jeremy Howard's `llms.txt` (September 2024) predate it; `CLAUDE.md` (Claude Code, 2025) popularized the per-repo memory file; **AGENTS.md** (August 2025, OpenAI with Google, Cursor, Amp, Jules et al.) unified the convention. And your Shopify claim is not only true, it undersells: Shopify quietly added a whole battery of AI-facing endpoints to every storefront in early May 2026 — llms.txt, llms-full.txt, agents.md, .well-known/ucp, /api/ucp/mcp, and an agentic discovery sitemap — all auto-generated, no app or theme change required. On Shopify stores, /llms.txt now even redirects to /agents.md, with the file carrying UCP checkout instructions — the default file is technical boilerplate about how to transact, not brand context about why to buy. The lowercase, by the way, matters on case-sensitive servers: `agents.md` and `AGENTS.md` are two different URLs on Linux. That's an objective gotcha, not a style choice.
#     608: 
#     609: **6. OKF + Agent Skills "superimpose without collision" — TRUE structurally, with two honest asterisks.** Both are markdown-plus-YAML-frontmatter folder trees; their reserved filenames don't collide (`SKILL.md` vs. OKF's `index.md` for progressive disclosure and `log.md` for change history); one tree can host both. Your verbs/nouns framing is sound: Skills = *procedures* an agent should follow, OKF = *knowledge* it should consult. Asterisk one: no spec formally binds them — "one needs the other" is your synthesis (a good one), not normative text. Asterisk two: OKF v0.1 (Google Cloud, published as an open spec) was built for organizational knowledge — tables, datasets, metrics, playbooks, runbooks, APIs; pointing it at a public website is a repurposing — a good one, but a repurposing. Also, the *official* "what can this site do and where" answer isn't OKF at all — it's ARD's `ai-catalog.json`, next.
#     610: 
#     611: **7. Your discovery trio is real but they're not competitors — they stack.** More below, but the headline: ARD was co-authored by Junjie Bu (Google), Shaun Smith (Hugging Face)… and R.V. Guha (Microsoft). Yes. Him again. Hold that thought for the closer.
#   -- region 3/16 (lines 621-625) --
#     621: **W3C Community Group report** — even earlier: interested parties wrote something down. WebMCP lives here. Not a standard, not yet on the standards track.
#     622: 
#     623: **Vendor convention** — llms.txt, AGENTS.md, CLAUDE.md, auth.md, OKF: no standards body at all, just files whose adoption is measured in access logs. Which — conveniently — you own.
#     624: 
#     625: **RFC 8615 — Well-Known URIs.** The load-bearing beam under half this glossary: `/.well-known/` is the IANA-registered lobby where machine-readable site metadata lives, so nothing collides with your content URLs. When in doubt about where a new standard will put its file, bet on the lobby.
#   -- region 4/16 (lines 643-653) --
#     643: **llms.txt / llms-full.txt** — Howard's 2024 comprehension map: robots.txt governs access, sitemaps declare inventory, llms.txt attempts *orientation* — a curated markdown index of what matters, with llms-full.txt as the full-text firehose variant. Establishment skepticism persists (Google's John Mueller in June 2026 called llms.txt "purely speculative," noting AI systems don't use it — while saying he likes the WebMCP approach); your access logs remain the only referee that matters, and yours show fetches. Cost to ship: one static file. The cheapest bet on the board.
#     644: 
#     645: **AGENTS.md / agents.md** — the "README for agents" convention (August 2025, OpenAI + a broad coalition), originally scoped to *repos* — telling coding agents how to behave in a codebase — now off-label deployed at web roots, including automatically on every Shopify storefront. On a website it becomes operating instructions for visiting agents: what this site is, what the endpoints are, how to transact. Remember the case-sensitivity trap from Verdict 5; if you're paranoid (you are), serve both spellings via redirect.
#     646: 
#     647: **CLAUDE.md / .cursorrules** — the per-tool ancestors of AGENTS.md: memory and rules files for specific coding agents. On a public site they matter mainly as lineage — and as proof that the entire category is Karpathy's everything-is-text thesis winning: the "LLM wiki" idea — markdown libraries that agents read, update, and maintain — beat the chunk-it-like-a-RAG-doll era on simplicity. Context-confetti, as you say, lost to context-*prose*.
#     648: 
#     649: **The Markdown twin (`index.md` convention)** — publishing the markdown master of every page at a predictable sibling URL. This is your Honeybot pattern, and it's now blessed by the biggest CDN on earth: Cloudflare's own docs tell visiting agents, in a banner, to stop reading the HTML and either append `index.md` to the URL or send `Accept: text/markdown`. When Tier 2 negotiation isn't available, the static twin is the degraded-gracefully version of the same promise.
#     650: 
#     651: **Agent Skills (agentskills.io)** — Anthropic's October 2025 format, released as an open spec: a folder per capability containing a `SKILL.md` (YAML frontmatter: name, description; markdown body: the procedure) plus optional scripts and resources, loaded by progressive disclosure — the agent reads the one-line description first and the full skill only when relevant. On a website, a `/skills/` tree is procedural documentation agents can *execute*, not just read. The verbs.
#     652: 
#     653: **OKF — Open Knowledge Format** — Google Cloud's open specification, v0.1, published June 12, 2026: a bundle is a directory of markdown files, one concept per file, file path as identity, small YAML frontmatter block for queryable fields, markdown links turning the directory into a graph, with optional `index.md` (progressive disclosure) and `log.md` (chronological change history). Just markdown, just files, just YAML frontmatter — shippable as a tarball, hostable in any git repo. It formalizes the LLM-wiki pattern Karpathy articulated in his April 2026 gist. The nouns. Your Jekyll `_posts/` tree with holographic shards in `_context/` is already about 80% of an OKF bundle wearing a different hat — the conformance gap is frontmatter field names.
#   -- region 5/16 (lines 660-678) --
#     660: ├── sitemap.xml                   # counting invariant lives here
#     661: ├── llms.txt                      # orientation map (llms-full.txt optional)
#     662: ├── agents.md                     # operating instructions for agents
#     663: ├── auth.md                       # Tier 4: how agents register (WorkOS convention)
#     664: ├── products/red-leather-sectional/
#     665: │   ├── index.html                # the human PDP
#     666: │   └── index.md                  # the markdown twin (Tier 1 or via Tier 2 conneg)
#     667: ├── skills/                       # Agent Skills — the VERBS
#     668: │   └── check-order-status/
#     669: │       ├── SKILL.md
#     670: │       └── scripts/lookup.py
#     671: ├── knowledge/                    # OKF bundle — the NOUNS
#     672: │   ├── index.md                  # OKF progressive disclosure
#     673: │   ├── log.md                    # OKF change ledger
#     674: │   ├── products/orders.md        # one concept per file, frontmatter: type…
#     675: │   └── policies/returns.md
#     676: └── .well-known/                  # RFC 8615 — the lobby (Tier 3 & 4)
#     677:     ├── api-catalog               # RFC 9727 → serves an RFC 9264 linkset
#     678:     ├── ai-catalog.json           # ARD / AI Cards — the protocol-agnostic menu
#   ... 11 more region(s) truncated
/home/mike/repos/trimnoir/_posts/2026-07-11-anti-fragile-publishing-workflow.md  # [Idx: 4 | Order: 3 | Tokens: 40,541 | Bytes: 168,396]
#   kw: Unix Philosophy, Anti-Fragile, LLM Observability, Deterministic Pipelines, Reconstitutable Computing
#   sum: An anti-Crichton publishing methodology that uses Unix-style text-based infrastructure and hard-coded mechanical governors to stabilize agentic AI pipelines, replacing volatile SaaS dependencies with human-readable, reconstitutable tools.
#   -- region 1/4 (lines 107-113) --
#     107: #      44: One designed to stabilize and diffuse all possible Michael Crichtonesque plots that might be brewing. This is the mechanical governor guarding against the unknown unknowns we're talking about, and how it came to be as the theme of this book.
#     108: #   -- region 2/16 (lines 46-50) --
#     109: #      46: Layer in a paragraph about how what we are engaged in now would be different educationally, qualitatively, transparently and otherwise if we were instead engaged in `--yolo`-style vibe-coding. Steelman in favor of vibecoding assuming abiding by a well written written `AGENTS.md` and `SKILL.md` can be reliably made to make deterministic on a particular pipeline of how work flows. 
#     110: #      47: 
#     111: #      48: Then flip the argument in favor of simply using that same said intelligence to help you create a `.py` (dot P Y) file dedicated forever into the future usable tool that could be dropped into some registry, be it OWF, Open Agents or other Jekyll-style plain old human readable and in fact very directly editable by humans simplest tech possible pain text-file which just happens to use a body full of markdown markup and YAML front matter which is much more plans old still just easily readable and endurable text then it sounds like. You put a colon between your keys and values one cheaper line. Woo woo! YAML. 
#     112: #      49: 
#     113: #      50: That's what the genetic AI industry is being born on now that it's out with making confetti of your data with always stale RAGs to the much more sensible everything is text that UNIX has been using forever. 
#   -- region 2/4 (lines 115-121) --
#     115: #      94: And why is this the anti-Crichton machine? Because every Crichton plot has the same skeleton: a system with software-writable actuators, an incentive to skip the review step, and a weekend where nobody was watching. The raptors get out because the fence was reprogrammable. Westworld goes wrong because the hosts got a firmware push. The anti-Crichton move is boring on purpose: put the dangerous authority in ROM — real ROM, not PROM wearing a trenchcoat — and route everything else through legible text that a human can read *before* it runs. The first law of robotics, revised edition: the actuator's limits shall not be a config file. If your safety envelope can be patched over the network, you don't have a safety envelope, you have a suggestion. Hear me, IME? He's talking to you, ring negative three.
#     116: #      95: 
#     117: #      96: Now, the steelman, because fair is fair. Vibe-coding — `--yolo` flag raised, agent unchained — is not madness if you've done the homework. A well-written `AGENTS.md` is a constitution; a good `SKILL.md` is case law. Pin the environment, pin the pipeline, make the agent's degrees of freedom small and its feedback loops fast, and vibe-coding becomes something like a CNC machine: you don't watch every cut because you trusted the jig. Whole products will ship this way, and some of them will be good, and the people who shipped them will sleep fine. The vibes can be made deterministic-*ish*, the way a casino is deterministic: not on any hand, but on the quarter.
#     118: #      97: 
#     119: #      98: Here's the flip, though, and it's the whole book in one move: if the intelligence is good enough to vibe-code reliably, it's good enough to do something strictly better — help you distill the vibe into a `.py` file. One file. Forever. A tool, not a session. A thing that can be dropped into a registry — OWF, Open Agents, whoever wins — described by the humblest technology that has ever refused to die: a plain text file with YAML front matter and a markdown body. Colon between key and value, one per line. Woo woo, YAML. The joke is that this is what the entire agentic industry is converging on *right now*, after a detour through making confetti of everyone's data with perpetually-stale RAG pipelines. Turns out the answer was on the shelf since 1970: everything is a file, everything is text, small tools composed with pipes. Future-proofing in the age of AI is old-school Unix proficiency. There. Said. Learn pipes, learn a shell, wire it together — that's *nix with the asterisk, the star that matches everything.
#     120: #   -- region 4/16 (lines 170-174) --
#     121: #     170: With players in the game only bound by atoms being turned into processors and processors being connected to the Internet though that need not be constant nor in every case even necessary, so total number of players is actually unknown. Still bound by atoms. Software not equally so bound. Technology like Microsoft BitNet 1.58 making old hardware more powerful. Setun-like multiplication bottlenecks going away and the Ollamas and Python vLLM (I think everything really `llama.cpp`-based stuff) upgraded to support or replaced by things that do the addition acceleration bonus. So all hardware get smart smarter. 
#   -- region 3/4 (lines 226-230) --
#     226: somebody for something nonsense of owning your own second brain can be done with
#     227: just blogging for hackers using the same skills you're developing anyway to edit
#     228: all those `AGENTS.md` and `SKILL.md` files plus a little Unix alias writing.
#     229: Case in point: hey Fable 5! Am I talking out of my butt or is there something to
#     230: this?
#   -- region 4/4 (lines 1745-1749) --
#    1745: ├──────────────────────────────────────────────────────────────────────────┼─────────┼─────────┼─────────┤
#    1746: │ TOTAL                                                                    │ 110,532 │ 416,976 │  100.0% │
#    1747: └──────────────────────────────────────────────────────────────────────────┴─────────┴─────────┴─────────┘
#    1748: **Command:** `prompt_foo.py --chop ADHOC_CHOP --no-tree --no-clipboard -o /tmp/adhoc_probe_out.md`
#    1749: 
📋 TODO_SLUGS block (≤8 newest) → clipboard (type xp to compile)
(nix) parent $ 
```

And now we type `latest`...

**Fable 5 High**: 

**MikeLev.in**: We've got some probes:

```diff
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ ls -la ~/.config/pipulate/ && stat -c '%a %n' ~/.config/pipulate/*.json
total 236
drwxr-xr-x  3 mike users   4096 Jul 13 06:27 .
drwx------ 75 mike users   4096 Jul 14 11:35 ..
-rw-------  1 mike users     40 Jun 11 07:08 api_key.txt
-rw-r--r--  1 mike users   1043 Jul  6 02:20 blogs.json
-rw-r--r--  1 mike users     86 Jul 13 06:27 commit_denylist.txt
-rw-r--r--  1 mike users    413 Jun 29 13:42 credentials.json
-rw-r--r--  1 mike users    455 Jun  4 14:55 flippers.json
drwxr-xr-x  7 mike users   4096 Jul 10 17:35 .git
-rw-r--r--  1 mike users    743 Jul 14 12:43 gmail_token.json
-rw-r--r--  1 mike users   1360 Jun 11 07:53 keys.json
-rw-r--r--  1 mike users    575 Jul 14 16:16 last_published.json
-rw-r--r--  1 mike users   2922 Jul 13 13:46 pii_substitutions.txt
-rw-r--r--  1 mike users 191620 Jul 14 16:55 token_cache.json
644 /home/mike/.config/pipulate/blogs.json
644 /home/mike/.config/pipulate/credentials.json
644 /home/mike/.config/pipulate/flippers.json
644 /home/mike/.config/pipulate/gmail_token.json
644 /home/mike/.config/pipulate/keys.json
644 /home/mike/.config/pipulate/last_published.json
644 /home/mike/.config/pipulate/token_cache.json
(nix) pipulate $ rg -n "get_api_key|keys.json|CONFLUENCE_TOKEN|service-account" scripts/ tools/ imports/ apps/ | head -25
scripts/confluence_probe.py:396:    print(f"   curl -u \"$CONFLUENCE_USER:$CONFLUENCE_TOKEN\" -X DELETE "
scripts/confluence_probe.py:435:    api_token = os.getenv("CONFLUENCE_TOKEN")
scripts/confluence_probe.py:439:        print("   Set CONFLUENCE_EMAIL (or CONFLUENCE_USER) and CONFLUENCE_TOKEN in your shell environment before running.")
scripts/gsc/gsc_top_movers.py:35:SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
scripts/gsc/gsc_keyworder.py:36:SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
scripts/articles/contextualizer.py:266:            val = common.get_api_key(k)  # Ensures interactive prompt if missing
scripts/articles/contextualizer.py:271:        val = common.get_api_key(args.key)
scripts/articles/articleizer.py:281:        api_key = common.get_api_key(args.key)
scripts/gsc/gsc_page_query.ipynb:19:    "   - Download the JSON key file (save as `service-account-key.json`)\n",
scripts/gsc/gsc_page_query.ipynb:66:    "    service_account_json = f'{working_folder}/service-account-key.json'\n",
scripts/gsc/gsc_page_query.ipynb:93:    "SERVICE_ACCOUNT_FILE = f'{working_folder}/service-account-key.json'\n",
scripts/articles/execute_massive_prompt.py:36:    api_key = common.get_api_key(args.key)
scripts/articles/common.py:13:KEYS_FILE = CONFIG_DIR / "keys.json"
scripts/articles/common.py:83:    """Loads the entire keys dictionary from keys.json."""
scripts/articles/common.py:93:def get_api_key(key_name=None):
scripts/articles/common.py:167:    parser.add_argument('-k', '--key', type=str, help="API key alias from keys.json (e.g., 'pipulate')")
scripts/articles/gsc_historical_fetch.py:27:SERVICE_ACCOUNT_KEY_FILE = Path.home() / ".config/articleizer/service-account-key.json"
scripts/articles/confluenceizer.py:402:    api_token = os.getenv("CONFLUENCE_TOKEN")
scripts/articles/confluenceizer.py:406:        print("  ↳ Set CONFLUENCE_EMAIL and CONFLUENCE_TOKEN to test wire connectivity.")
(nix) pipulate $ rg -in "botify" config.py apps/015_config.py imports/ | head -15
apps/015_config.py:316:                done='botify_config',
apps/015_config.py:317:                show='Botify Integration',
apps/015_config.py:1010:            pip.speak("Cloud AI skipped. You can stay fully local, or add a key later. If you are a Botify employee or Customer, please enter your Botify API key.", wait=False)
apps/015_config.py:1073:        pip.speak(f"Cloud cognitive engine selected. Connection secured. If you are a Botify employee or Customer, please enter your Botify API key.", wait=False)
apps/015_config.py:1088:        """Handles GET request for Botify Integration."""
apps/015_config.py:1126:                refill_key = pip.load_secrets("BOTIFY_API_TOKEN") or ""
apps/015_config.py:1129:                Label("Botify API Key (Optional)", _for=f"{step_id}-api-key"),
apps/015_config.py:1133:                        name="botify_token", 
apps/015_config.py:1136:                        placeholder="Paste your Botify API key here...", 
apps/015_config.py:1153:                    P("If you are a Botify customer or employee, you can connect your account. ", 
apps/015_config.py:1154:                      A("Get your API key here ↗", href="https://app.botify.com/account/", target="_blank"),
apps/015_config.py:1164:        """Process the submission for Botify Integration."""
apps/015_config.py:1174:        raw_key = form_data.get("botify_token", "").strip()
apps/015_config.py:1179:            success_msg = f'{pip.get_ui_constants()["EMOJIS"]["SUCCESS"]} Botify Integration skipped.'
apps/015_config.py:1181:            pip.speak("Botify integration skipped.", wait=True)
(nix) pipulate $ ls scripts/*.py | wc -l && ls scripts/articles/*.py | wc -l
9
25
(nix) pipulate $ 
```

And we've got some context... done!

```text
#      _       _   _   _               ____ _   _  ___  ____  _ 
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |
#    / _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |
#   / ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)
# AD HOC CHOP: The Not-Managed-by-Git Safe-for-Client-Data place.

# BIG STANDARD STUFF (Optionally comment out any)
! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs         # <-- This is the "Rolling Pin"
foo_files.py         # Router & Book Outline
prompt_foo.py        # Hand-cranked Agentic Framework
init.lua             # Text as muscle memory
flake.nix            # Hardware as projections

--- INSERT AD HOC CONTEXT BELOW HERE ---

scripts/gmail.py
scripts/articles/confluenceizer.py
tools/botify_tools.py
imports/botify/true_schema_discoverer.py
config.py
apps/015_config.py
! rg -n "get_api_key|keys.json" scripts/ tools/ imports/ | head -25
! rg -in "botify" config.py apps/015_config.py | head -15
```

Okay, I get it. But there's a sequentiality thing. The output of the probe will
always, unless I take some weird off-the-rails step, be before the patch is
applied, and then once after but always before the new context-commands... oh
wait, I'm thinking through this. This might be constitutional. If you want to
establish a before-and-after logic affirming binary search-style causal probe
boundary then whatever you did in your probes should also be recommended as part
of your `adhoc.txt` chop contents, see? Does that make sense? Should this be
constitutional? I'm getting tired of thinking through order-of-operation issues
when it comes to patches. So why not just both places? I think that would
eliminate this probe-before-patch, patch-and-then probe language ambiguity thing
that I keeps biting me in the butt.

So that's my request. Sure, do what you're planning. But also make this
still-short article about a couple of things. We'll blend them together and make
it make sense because each choose-your-own adventure has snowballing effects.

**Fable 5 High**: Context verified.

**The name you're groping for.** "Unix is not the mental model so much as... I don't know what." It's the **Jacquard loom** — and you already know it, because you wrote it into your own `stream_orchestrator.py` comment. Arranged text with agreed-upon meaning, fed through a known actuator, producing automation. Unix is one *instance* of that pattern; so is Jekyll front matter, so is SKILL.md, so is a piano roll, so is your `foo_files.py` router. The industry keeps rediscovering the loom and giving it a new filename. Your second-brain demo (`rgx └ skill.md agents.md owf`) is the loom reading its own punch cards to decide which cards to feed itself next. That's the article's spine right there.

**Your constitutional insight is correct, and it's better than you stated it.** The ambiguity that keeps biting you — "did that probe receipt come from before or after the patch?" — dissolves the moment you require every recommended probe to be *echoed verbatim* into the next `adhoc.txt`. The hand-run is the BEFORE reading. The compiled re-run is the AFTER reading. One probe, two receipts, straddling the patch: a binary-search causal boundary you get for free, every turn, forever. And it composes perfectly with the LIVE COMMAND RECEIPTS manifest authority you already built — the manifest proves *which* compile the after-reading belongs to. This turn's patches install exactly that rule (and, fittingly, fix the fact that rule 11 was duplicated — the probe `rg -c` returning 2→1 across the patch is the new invariant demonstrating itself on its own installation).

## Establishing a Constitutional Probe Boundary

**One genuine finding from your own probe output, before the plan:** your `ls -la ~/.config/pipulate/` shows a `.git` directory sitting in the same folder as `keys.json`, `gmail_token.json`, and `credentials.json` — all mode 644. If that repo tracks those files and has a remote, your wallet already left the building. That's the first probe below, and it gates the whole "wallet" design (item 8 on your list): the answer to "where do secrets live" is *exactly where they live now*, but chmod 600, git-excluded by structural absence (a `secrets/` subdir the repo never touches), and backed up by your `backup-essential.py` keychain path — not a new system, a hardening of the accidental one.

**On the nine ideas:** you're right that the tiny Unix connectors (#7) are the highest-return item, because `gmail.py` already proved the template: one positional arg, auto-detected mode, auth from `~/.config/pipulate`, stdout designed for `!` chisel-strikes. `scripts/botify.py` falls out of `botify_tools.py` + `true_schema_discoverer.py` almost mechanically — LIST mode (projects/analyses) vs FETCH mode (BQL query), token from `get_botify_token()`. That's the next turn's work, pre-loaded in NEXT CONTEXT below.

---

**1. PROBES**

```bash
# P1 — proves the checklist duplication exists (expect: 2). After patch: 1.
#      This IS the before/after demo of the invariant being installed. Gates patch verification.
rg -c "THE ACTIONABLE RESPONSE CONTRACT" prompt_foo.py

# P2 — proves whether your secrets are git-tracked and/or pushed anywhere.
#      Gates the entire wallet design (idea #8). If gmail_token.json or keys.json
#      appear in ls-files, the next move is history-scrub, not just .gitignore.
cd ~/.config/pipulate && git remote -v && git ls-files | head -20; cd -

# P3 — tightens the world-readable secrets regardless of P2's answer. Gates nothing; just do it.
chmod 600 ~/.config/pipulate/{keys.json,gmail_token.json,credentials.json,token_cache.json}
```

**2. NEXT CONTEXT** (probes echoed per the invariant being installed this turn)

adhoc.txt slot:

```text
scripts/gmail.py
scripts/articles/common.py
tools/botify_tools.py
imports/botify/true_schema_discoverer.py
config.py
! rg -c "THE ACTIONABLE RESPONSE CONTRACT" prompt_foo.py
! rg -n "PROBE ECHO" foo_files.py prompt_foo.py
! cd ~/.config/pipulate && git remote -v && git ls-files | head -20
! stat -c '%a %n' ~/.config/pipulate/*.json
```

prompt.md text: "Verify the Probe Echo Invariant landed (receipts above should show contract count = 1 and PROBE ECHO present in both files). Then draft `scripts/botify.py` in the exact mold of `scripts/gmail.py`: one positional arg, auto-detected LIST mode (no arg or org slug → projects/analyses) vs FETCH mode (BQL query string), token via `config.get_botify_token()`, stdout formatted for `!` chisel-strike consumption. Also report whether the secrets git repo probe demands a history scrub."

**3. PATCHES**

```text
Target: prompt_foo.py
[[[SEARCH]]]
11.  **THE ACTIONABLE RESPONSE CONTRACT:** Every substantive answer must END with a numbered next-actions plan in this exact order: (1) PROBES — the cheapest copy-pasteable commands the operator runs first, each annotated with what it proves or falsifies, and which later steps it gates; (2) NEXT CONTEXT — the exact adhoc.txt lines (including ! chisel-strikes and $URL sigils where useful) and prompt.md text for the next compile, so the follow-up turn arrives pre-loaded with receipts; (3) PATCHES — SEARCH/REPLACE blocks ONLY against raw source actually present in this context; if no repo patch is needed, state "No repo patches required" explicitly rather than inventing one; (4) EXTERNAL DELIVERABLES — artifacts living outside this repo (PageWorkers JavaScript, CMS settings, dashboards), clearly labeled as manual-paste and never wrapped in patch markers. Analysis that does not close with this plan is an incomplete answer.
11.  **THE ACTIONABLE RESPONSE CONTRACT:** Every substantive answer must END with a numbered next-actions plan in this exact order: (1) PROBES — the cheapest copy-pasteable commands the operator runs first, each annotated with what it proves or falsifies, and which later steps it gates; (2) NEXT CONTEXT — the exact adhoc.txt lines (including ! chisel-strikes and $URL sigils where useful) and prompt.md text for the next compile, so the follow-up turn arrives pre-loaded with receipts; (3) PATCHES — SEARCH/REPLACE blocks ONLY against raw source actually present in this context; if no repo patch is needed, state "No repo patches required" explicitly rather than inventing one; (4) EXTERNAL DELIVERABLES — artifacts living outside this repo (PageWorkers JavaScript, CMS settings, dashboards), clearly labeled as manual-paste and never wrapped in patch markers. Analysis that does not close with this plan is an incomplete answer.
[[[DIVIDER]]]
11.  **THE ACTIONABLE RESPONSE CONTRACT:** Every substantive answer must END with a numbered next-actions plan in this exact order: (1) PROBES — the cheapest copy-pasteable commands the operator runs first, each annotated with what it proves or falsifies, and which later steps it gates; (2) NEXT CONTEXT — the exact adhoc.txt lines (including ! chisel-strikes and $URL sigils where useful) and prompt.md text for the next compile, so the follow-up turn arrives pre-loaded with receipts; (3) PATCHES — SEARCH/REPLACE blocks ONLY against raw source actually present in this context; if no repo patch is needed, state "No repo patches required" explicitly rather than inventing one; (4) EXTERNAL DELIVERABLES — artifacts living outside this repo (PageWorkers JavaScript, CMS settings, dashboards), clearly labeled as manual-paste and never wrapped in patch markers. Analysis that does not close with this plan is an incomplete answer.
12.  **THE PROBE ECHO INVARIANT (Before/After Symmetry):** Every command recommended in (1) PROBES MUST also appear verbatim as a `!` chisel-strike line in (2) NEXT CONTEXT. The operator's hand-run is the BEFORE reading, taken prior to applying any patch; the identical line baked into adhoc.txt re-executes automatically at the next compile, producing the AFTER reading as a live receipt. One probe, two receipts, straddling the patch — a binary-search causal boundary that removes all probe-before-patch / patch-then-probe ordering ambiguity. A probe too heavy or unbounded to echo into the next compile (see THE PROBE ECONOMY RULE) is too heavy to recommend: cap it first, then echo it.
[[[REPLACE]]]
```

```text
Target: foo_files.py
[[[SEARCH]]]
# THE PROBE ECONOMY RULE: A probe is cheap only when its output is bounded.
# Before routing command stdout into the context ledger, measure or cap it with
# `wc -l`, `head`, `tail`, `rg -l`, or an explicit result limit. A command that
# floods thousands of lines is not a cheap falsifying probe; it is an accidental
# context import. Sample first, widen deliberately, and keep bulk output out of
# the narrative unless the bulk itself is the evidence.
[[[DIVIDER]]]
# THE PROBE ECONOMY RULE: A probe is cheap only when its output is bounded.
# Before routing command stdout into the context ledger, measure or cap it with
# `wc -l`, `head`, `tail`, `rg -l`, or an explicit result limit. A command that
# floods thousands of lines is not a cheap falsifying probe; it is an accidental
# context import. Sample first, widen deliberately, and keep bulk output out of
# the narrative unless the bulk itself is the evidence.

# THE PROBE ECHO RULE: Every probe recommended for hand-execution must also be
# echoed verbatim as a `!` line into the next adhoc.txt chop. The hand-run is
# the BEFORE reading; the compiled re-run is the AFTER reading. One probe, two
# receipts, straddling the patch — no more probe/patch ordering ambiguity.
[[[REPLACE]]]
```

**4. EXTERNAL DELIVERABLES**

No external deliverables required. (The `chmod 600` in P3 is a machine-state change, not a repo artifact — manual terminal action, already given as a probe.)

**MikeLev.in**: Right, right, this the casual sci-fi fan observer will recognize as one
worm hooked and ridden out and released. Whoa you can almost feel the whiplash
of this one. See, where others use buzzwords and re-amplify voices, I'm slinging
text through actuators to extend this right here.

First we got probes. Always watch what they're doing.

```diff
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ # P1 — proves the checklist duplication exists (expect: 2). After patch: 1.
#      This IS the before/after demo of the invariant being installed. Gates patch verification.
rg -c "THE ACTIONABLE RESPONSE CONTRACT" prompt_foo.py

# P2 — proves whether your secrets are git-tracked and/or pushed anywhere.
#      Gates the entire wallet design (idea #8). If gmail_token.json or keys.json
#      appear in ls-files, the next move is history-scrub, not just .gitignore.
cd ~/.config/pipulate && git remote -v && git ls-files | head -20; cd -

# P3 — tightens the world-readable secrets regardless of P2's answer. Gates nothing; just do it.
chmod 600 ~/.config/pipulate/{keys.json,gmail_token.json,credentials.json,token_cache.json}
2
api_key.txt
blogs.json
credentials.json
flippers.json
gmail_token.json
keys.json
last_published.json
pii_substitutions.txt
token_cache.json
/home/mike/repos/pipulate
(nix) pipulate $
```

Now we've got context:

```text
#      _       _   _   _               ____ _   _  ___  ____  _ 
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |
#    / _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |
#   / ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)
# AD HOC CHOP: The Not-Managed-by-Git Safe-for-Client-Data place.

# BIG STANDARD STUFF (Optionally comment out any)
! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs         # <-- This is the "Rolling Pin"
foo_files.py         # Router & Book Outline
prompt_foo.py        # Hand-cranked Agentic Framework
init.lua             # Text as muscle memory
flake.nix            # Hardware as projections

--- INSERT AD HOC CONTEXT BELOW HERE ---

scripts/gmail.py
scripts/articles/common.py
tools/botify_tools.py
imports/botify/true_schema_discoverer.py
config.py
! rg -c "THE ACTIONABLE RESPONSE CONTRACT" prompt_foo.py
! rg -n "PROBE ECHO" foo_files.py prompt_foo.py
! cd ~/.config/pipulate && git remote -v && git ls-files | head -20
! stat -c '%a %n' ~/.config/pipulate/*.json
```

And we look at how probe and context do binary search. Explain what that means
with copious documentation that makes all the difference. Many don't get it. But
when turning unknown to known there's efficient drill-down; like a mechanic, or
tracer-dye tracker, evidence mounting on evidence Alice Ball, Henrietta Swan
Leavitt, Margaret E. Knight... if there's one Knight I like more than Wayne!

And now for the fun part! I love applying patches, even though I made the
`blast` keyword collapsing down steps, right here I resist it. I like
composition. We need to be reminded of workflows and pipelines. Though I took
out the pipe-step. It's rather silly to make you type `cat patch | app` when
just `app` will do:

```diff
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'prompt_foo.py'.
(nix) pipulate $ d
diff --git a/prompt_foo.py b/prompt_foo.py
index 85c4240b..cbf4792c 100644
--- a/prompt_foo.py
+++ b/prompt_foo.py
@@ -958,7 +958,7 @@ Before addressing the user's prompt, perform the following verification steps:
 9.  **THE TARGET ADJACENCY RULE:** Every `[[[SEARCH]]]` marker must be immediately preceded by `Target: filename` on the line directly above it — no blank lines, no fences, no prose between the Target line and the marker. The filename in the Target line is what `apply.py` uses to find the file; omitting it or separating it with blank lines causes a fatal "Missing target filename" error. Example: `Target: scripts/articles/lsa.py` or `Target: /home/mike/repos/pipulate/scripts/articles/lsa.py`. Both relative and absolute paths work.
 10.  **THE WHOLE-FILE WRITE ESCAPE HATCH:** For a genuine top-to-bottom rewrite of a single file (not a surgical edit), you MAY skip the SEARCH block entirely. Emit a `Target: filename` line, then on the next line a `[[[WRITE_FILE]]]` marker, then the complete new file body, then a `[[[END_WRITE_FILE]]]` marker — all wrapped in a single fenced text block exactly as the SEARCH/REPLACE protocol requires. `apply.py` writes the body verbatim, overwriting the file if it exists or creating it (and any missing parent directories) if it does not, runs the same Python AST safety check before saving, and normalizes a single trailing newline. Use this ONLY when you are replacing essentially the entire file; for every smaller change the SEARCH/REPLACE protocol with its exact-match interlock remains mandatory, because that exact match is what proves the edit is landing in the right place.
 11.  **THE ACTIONABLE RESPONSE CONTRACT:** Every substantive answer must END with a numbered next-actions plan in this exact order: (1) PROBES — the cheapest copy-pasteable commands the operator runs first, each annotated with what it proves or falsifies, and which later steps it gates; (2) NEXT CONTEXT — the exact adhoc.txt lines (including ! chisel-strikes and $URL sigils where useful) and prompt.md text for the next compile, so the follow-up turn arrives pre-loaded with receipts; (3) PATCHES — SEARCH/REPLACE blocks ONLY against raw source actually present in this context; if no repo patch is needed, state "No repo patches required" explicitly rather than inventing one; (4) EXTERNAL DELIVERABLES — artifacts living outside this repo (PageWorkers JavaScript, CMS settings, dashboards), clearly labeled as manual-paste and never wrapped in patch markers. Analysis that does not close with this plan is an incomplete answer.
-11.  **THE ACTIONABLE RESPONSE CONTRACT:** Every substantive answer must END with a numbered next-actions plan in this exact order: (1) PROBES — the cheapest copy-pasteable commands the operator runs first, each annotated with what it proves or falsifies, and which later steps it gates; (2) NEXT CONTEXT — the exact adhoc.txt lines (including ! chisel-strikes and $URL sigils where useful) and prompt.md text for the next compile, so the follow-up turn arrives pre-loaded with receipts; (3) PATCHES — SEARCH/REPLACE blocks ONLY against raw source actually present in this context; if no repo patch is needed, state "No repo patches required" explicitly rather than inventing one; (4) EXTERNAL DELIVERABLES — artifacts living outside this repo (PageWorkers JavaScript, CMS settings, dashboards), clearly labeled as manual-paste and never wrapped in patch markers. Analysis that does not close with this plan is an incomplete answer.
+12.  **THE PROBE ECHO INVARIANT (Before/After Symmetry):** Every command recommended in (1) PROBES MUST also appear verbatim as a `!` chisel-strike line in (2) NEXT CONTEXT. The operator's hand-run is the BEFORE reading, taken prior to applying any patch; the identical line baked into adhoc.txt re-executes automatically at the next compile, producing the AFTER reading as a live receipt. One probe, two receipts, straddling the patch — a binary-search causal boundary that removes all probe-before-patch / patch-then-probe ordering ambiguity. A probe too heavy or unbounded to echo into the next compile (see THE PROBE ECONOMY RULE) is too heavy to recommend: cap it first, then echo it.
 '''
 
     def _generate_summary_content(self, verified_token_count: int) -> str:
(nix) pipulate $ m
📝 Committing: chore: Add probe echo invariant explanation to prompt_foo.py
[main 2ce1549f] chore: Add probe echo invariant explanation to prompt_foo.py
 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 976172c5..4382e989 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -104,6 +104,11 @@ AI_PHOOEY_CHOP = r"""
 # context import. Sample first, widen deliberately, and keep bulk output out of
 # the narrative unless the bulk itself is the evidence.
 
+# THE PROBE ECHO RULE: Every probe recommended for hand-execution must also be
+# echoed verbatim as a `!` line into the next adhoc.txt chop. The hand-run is
+# the BEFORE reading; the compiled re-run is the AFTER reading. One probe, two
+# receipts, straddling the patch — no more probe/patch ordering ambiguity.
+
 # STORY ENGINE
 # Mike-E's gift is associative reach; his flaw is letting every spark become canon.
 # Yen Sid-ton is a brilliant familiar re-instantiated without yesterday.
(nix) pipulate $ m
📝 Committing: chore: Enhance probe echo rule documentation in foo_files.py
[main eb074dc4] chore: Enhance probe echo rule documentation in foo_files.py
 1 file changed, 5 insertions(+)
(nix) pipulate $ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 48 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 1.31 KiB | 1.31 MiB/s, done.
Total 6 (delta 4), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (4/4), completed with 3 local objects.
To github.com:pipulate/pipulate.git
   81fd640e..eb074dc4  main -> main
(nix) pipulate $
```

There, and that's it. This is where the ambiguity would be, because the context
that has been set isn't actually run yet, but will be run the moment we use any
of our Prompt Fu context compiler special-meaning aliases like `foo`, `fu`,
`ahc`, `latest` and the like. We got a few favorites developing amongst which
`ahc` must be my new favorite by virtue of how well it works in the
human-actuated player piano system (plenty of opportunity to human hand-craft
context) and also the way it defends against polluting the repo accidentally
with client data. 

So flow-wise, if that probe is both hand-executed by the human in the terminal
going into a hand-cranked pulling of the rip-cord on one of these super-prompts,
you know that the state of affairs *after* the patches you applied (before
pulling the trigger) will have caused whatever that amnesiac genie is about to
see as the output of whatever commands are in `adhoc.txt` (expanded and
"flattened" or rendered or generated into) `prompt.md` along with the manifest,
and potentially directory trees, UML dependency trees, LLM Optics of pages you
scraped and a whole lot of other things that help you know everything you need
to know to for that one nearly guaranteed 1-shot knock-out punch.

Am I speaking gibberish? If I am, you've got that covered to by putting these
words in my mouth:

Verify the Probe Echo Invariant landed (receipts above should show contract
count = 1 and PROBE ECHO present in both files). Then draft `scripts/botify.py`
in the exact mold of `scripts/gmail.py`: one positional arg, auto-detected LIST
mode (no arg or org slug → projects/analyses) vs FETCH mode (BQL query string),
token via `config.get_botify_token()`, stdout formatted for `!` chisel-strike
consumption. Also report whether the secrets git repo probe demands a history
scrub.

**Fable 5 High**: Context verified.

**Verdict 1 — The Probe Echo Invariant landed, and demonstrated itself on its own installation.** The Manifest's LIVE COMMAND RECEIPTS for THIS compile show: `rg -c "THE ACTIONABLE RESPONSE CONTRACT" prompt_foo.py` → `1` (was 2 in your hand-run BEFORE reading), and `rg -n "PROBE ECHO"` finds the rule in both files — `prompt_foo.py:961` (rule 12) and `foo_files.py:107`. One probe, two receipts, straddling the patch, exactly as constitutionally specified. You are not speaking gibberish: the hand-run captures pre-patch state, the compiled `!` line captures post-patch state, and the manifest's receipts list is what pins the AFTER reading to *this* compile rather than a stale quote in the transcript. That's the whole binary-search boundary — the patch is the pivot, and you now always hold a measurement on each side of it.

**Verdict 2 — Secrets repo: no scrub *demanded*, but the landmine is real and cheap to defuse.** The live receipt shows `git remote -v` produced **no output** — the `~/.config/pipulate` repo has no remote. Nothing has ever left the machine, so there is no exposure event and no emergency. However, `git ls-files` shows five secret-bearing files tracked in history: `api_key.txt`, `credentials.json`, `gmail_token.json`, `keys.json`, `token_cache.json` — plus `pii_substitutions.txt`, which by definition contains the client identifiers you're scrubbing *from* everything else. The failure mode is one future `git remote add && git push` publishing the entire wallet retroactively. Also note `token_cache.json` is 191KB and churns constantly — tracking it pollutes history anyway. Since the repo has no remote and no clones, a `git-filter-repo` purge (already in your flake's `commonPackages`) is zero-coordination. Do it at leisure, not in a panic. The `chmod 600` from last turn landed — the stat receipt confirms all four token files are now 600; `blogs.json`/`flippers.json`/`last_published.json` staying 644 is fine, they're config, not secrets.

## Hardening the Wallet Against Accidental Exposure

**Verdict 3 — `scripts/botify.py` drafted below** in the exact `gmail.py` mold: one positional arg, auto-detected mode, token via `config.get_botify_token()`, bounded stdout per THE PROBE ECONOMY RULE. Disambiguation: no arg → identity walk (profile → paginated project list); a bare slug or `org/project` → LIST (projects or analyses); anything starting with `{` or containing whitespace → FETCH (BQL query). One compile-lane caution baked into the docstring: the identity walk prints **client org/project slugs**, so any `! python scripts/botify.py` line in a cloud-bound compile leans on your `pii_substitutions.txt`/denylist chokepoint — same lesson the `browser_cache` leak taught, now applied to a new probe surface.

---

**1. PROBES**

```text
# P1 — before: MISSING (file doesn't exist yet). After applying the patch: EXISTS. Gates P2.
test -f scripts/botify.py && echo EXISTS || echo MISSING

# P2 — smoke test: auth + identity walk. Bounded by head. Before patch it errors (valid
#      BEFORE reading); after patch it prints org/project slugs or a clean token error.
python scripts/botify.py 2>&1 | head -30

# P3 — secrets untrack verification. Before the external deliverable below: 9 tracked
#      files including 5 secrets. After: secrets absent. Gates the filter-repo decision.
cd ~/.config/pipulate && git ls-files; cd -
```

**2. NEXT CONTEXT** (probes echoed verbatim, per rule 12)

adhoc.txt slot:

```text
scripts/gmail.py
scripts/botify.py
config.py
! test -f scripts/botify.py && echo EXISTS || echo MISSING
! python scripts/botify.py 2>&1 | head -30
! cd ~/.config/pipulate && git ls-files
```

prompt.md text: "Receipts above are AFTER readings. Confirm botify.py exists and its identity walk returns org/project slugs (or a clean missing-token message). Confirm the secrets untrack landed: git ls-files should no longer show api_key.txt, credentials.json, gmail_token.json, keys.json, token_cache.json, or pii_substitutions.txt. Then choose: (a) draft scripts/confluence.py in the same mold reusing confluenceizer's CONFLUENCE_EMAIL/CONFLUENCE_TOKEN envs, or (b) extend botify.py with an org/project/analysis SHOW mode surfacing datamodel field counts via the true_schema_discoverer endpoints."

**3. PATCHES**

```text
Target: scripts/botify.py
[[[WRITE_FILE]]]
#!/usr/bin/env python3
# scripts/botify.py
"""
botify.py — A Unix-philosophy gateway to the Botify API for Prompt Fu context.

Golden-path modes, auto-detected from the single positional argument:

  python scripts/botify.py                    # LIST: identity walk -> all your org/project slugs
  python scripts/botify.py org                # LIST: projects under that org slug
  python scripts/botify.py org/project        # LIST: analyses (crawl snapshots) for that project
  python scripts/botify.py '<BQL or JSON>'    # FETCH: run a query (needs org/project coordinates)

Designed to be dropped into adhoc.txt as a `!` chisel-strike, e.g.:

  ! python scripts/botify.py
  ! python scripts/botify.py my-org/my-project
  ! python scripts/botify.py 'SELECT url FROM crawl' --org my-org --project my-project

Disambiguation rule: an argument that starts with '{' or contains whitespace is
a query (FETCH mode); anything else is a slug path (LIST mode). No argument at
all triggers the identity walk.

Auth: BOTIFY_API_TOKEN via config.get_botify_token() (env var or project .env).
FETCH coordinates resolve from --org/--project flags, then BOTIFY_ORG /
BOTIFY_PROJECT environment variables.

Output is capped by --max (default 25) per THE PROBE ECONOMY RULE: stdout is
destined for compiled context payloads, so the bound is a feature.

COMPILE-LANE CAUTION: LIST output contains client org/project slugs. Any `!`
invocation bound for a cloud chat window rides through the compile-lane
sanitizer — make sure pii_substitutions.txt covers client identifiers first.
"""

import os
import sys
import json
import argparse
from pathlib import Path

import httpx

# Wire into the central config (same pattern as scripts/ai.py)
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
from config import get_botify_token

API_BASE = "https://api.botify.com/v1"

# ----------------------------------------------------------------------------
# Auth & transport
# ----------------------------------------------------------------------------
def make_client():
    token = get_botify_token()
    if not token:
        sys.stderr.write(
            "Missing BOTIFY_API_TOKEN.\n"
            "Set it in your environment or the project-root .env file\n"
            "(config.get_botify_token() checks both).\n"
        )
        sys.exit(1)
    headers = {"Authorization": f"Token {token}", "Content-Type": "application/json"}
    return httpx.Client(headers=headers, timeout=60.0)

def get_json(client, url, params=None):
    resp = client.get(url, params=params)
    if resp.status_code != 200:
        sys.stderr.write(f"HTTP {resp.status_code} for {url}\n{resp.text[:500]}\n")
        sys.exit(1)
    return resp.json()

# ----------------------------------------------------------------------------
# Defensive extraction (profile shape treated as unverified ground truth,
# same posture as imports/botify/true_schema_discoverer.py)
# ----------------------------------------------------------------------------
def extract_username(profile):
    candidates = [
        profile.get("username"),
        profile.get("data", {}).get("username") if isinstance(profile.get("data"), dict) else None,
        profile.get("user", {}).get("username") if isinstance(profile.get("user"), dict) else None,
        profile.get("login"),
    ]
    for c in candidates:
        if c:
            return c
    return None

def project_coordinates(project):
    """Best-effort (org, slug, name) from a project payload."""
    slug = project.get("slug", "?")
    name = project.get("name", "")
    org = (
        (project.get("user") or {}).get("login")
        or (project.get("organization") or {}).get("slug")
        or "?"
    )
    return org, slug, name

def follow_pages(client, url, max_items):
    """Drain a paginated Botify list endpoint, capped at max_items."""
    items = []
    while url and len(items) < max_items:
        data = get_json(client, url)
        items.extend(data.get("results", []))
        url = data.get("next")
    return items[:max_items]

# ----------------------------------------------------------------------------
# Modes
# ----------------------------------------------------------------------------
def list_identity(client, max_items):
    """LIST mode, no argument: whoami -> every accessible org/project slug."""
    profile = get_json(client, f"{API_BASE}/authentication/profile")
    username = extract_username(profile)
    if not username:
        sys.stderr.write("Could not locate 'username' in the profile payload.\n")
        sys.exit(1)
    print(f"# Botify projects visible to {username} (org/project | name)\n")
    projects = follow_pages(client, f"{API_BASE}/users/{username}/projects", max_items)
    if not projects:
        print("(no accessible projects)")
        return
    for p in projects:
        org, slug, name = project_coordinates(p)
        print(f"{org}/{slug}  {name}")
    print("\n# Next: python scripts/botify.py <org>/<project>   (list analyses)")

def list_org_projects(client, org, max_items):
    """LIST mode, single slug: projects under one org."""
    print(f"# Botify projects under '{org}' (org/project | name)\n")
    projects = follow_pages(client, f"{API_BASE}/projects/{org}", max_items)
    if not projects:
        print("(no projects found — check the org slug)")
        return
    for p in projects:
        _, slug, name = project_coordinates(p)
        print(f"{org}/{slug}  {name}")
    print("\n# Next: python scripts/botify.py " + org + "/<project>   (list analyses)")

def list_analyses(client, org, project, max_items):
    """LIST mode, org/project: crawl snapshots, newest first."""
    print(f"# Botify analyses for {org}/{project} (newest first)\n")
    analyses = follow_pages(client, f"{API_BASE}/analyses/{org}/{project}/light", max_items)
    if not analyses:
        print("(no analyses found)")
        return
    for a in analyses:
        slug = a.get("slug", "?")
        status = a.get("status", "")
        finished = a.get("date_finished") or a.get("date_created") or ""
        print(f"{slug}  {status}  {finished}")
    print(
        "\n# Next: python scripts/botify.py 'SELECT url FROM crawl' "
        f"--org {org} --project {project}"
    )

def run_query(client, raw_query, org, project, max_items):
    """FETCH mode: BQL string or full JSON payload against the query endpoint."""
    if not (org and project):
        sys.stderr.write(
            "FETCH mode needs coordinates: pass --org/--project or set\n"
            "BOTIFY_ORG / BOTIFY_PROJECT in your environment.\n"
        )
        sys.exit(1)

    stripped = raw_query.strip()
    if stripped.startswith("{"):
        try:
            payload = json.loads(stripped)
        except json.JSONDecodeError as e:
            sys.stderr.write(f"Argument looks like JSON but failed to parse: {e}\n")
            sys.exit(1)
        payload.setdefault("size", max_items)
    else:
        payload = {"query": stripped, "size": max_items}

    url = f"{API_BASE}/projects/{org}/{project}/query"
    resp = client.post(url, json=payload)
    if resp.status_code != 200:
        sys.stderr.write(f"HTTP {resp.status_code} for {url}\n{resp.text[:500]}\n")
        sys.exit(1)
    data = resp.json()
    results = data.get("results")
    if isinstance(results, list):
        results = results[:max_items]
        print(f"# Botify query results for {org}/{project} ({len(results)} row(s), cap {max_items})\n")
        print(json.dumps(results, indent=2, default=str))
    else:
        print(json.dumps(data, indent=2, default=str))

def main():
    parser = argparse.ArgumentParser(
        description="Unix-philosophy gateway to the Botify API for Prompt Fu context."
    )
    parser.add_argument(
        'query', nargs='?', default=None,
        help="Nothing (identity walk), 'org', 'org/project', or a BQL/JSON query string."
    )
    parser.add_argument('--org', default=os.getenv('BOTIFY_ORG'),
                        help='Org slug for FETCH mode (default: BOTIFY_ORG env).')
    parser.add_argument('--project', default=os.getenv('BOTIFY_PROJECT'),
                        help='Project slug for FETCH mode (default: BOTIFY_PROJECT env).')
    parser.add_argument('-n', '--max', type=int, default=25,
                        help='Output cap per THE PROBE ECONOMY RULE (default: 25).')
    args = parser.parse_args()

    client = make_client()
    try:
        arg = args.query
        if arg is None:
            list_identity(client, args.max)
        elif arg.strip().startswith('{') or any(ch.isspace() for ch in arg.strip()):
            run_query(client, arg, args.org, args.project, args.max)
        else:
            parts = [p for p in arg.strip('/').split('/') if p]
            if len(parts) == 1:
                list_org_projects(client, parts[0], args.max)
            elif len(parts) >= 2:
                list_analyses(client, parts[0], parts[1], args.max)
            else:
                list_identity(client, args.max)
    finally:
        client.close()

if __name__ == '__main__':
    main()
[[[END_WRITE_FILE]]]
```

**4. EXTERNAL DELIVERABLES** (manual terminal actions in `~/.config/pipulate` — outside this repo, never patch markers)

Defuse the wallet landmine in two stages. Stage one, untrack (keeps files on disk, removes them from the index going forward):

```text
cd ~/.config/pipulate
printf '%s\n' api_key.txt credentials.json gmail_token.json keys.json token_cache.json pii_substitutions.txt > .gitignore
git rm --cached api_key.txt credentials.json gmail_token.json keys.json token_cache.json pii_substitutions.txt
git add .gitignore && git commit -m "Untrack secrets; structural absence over exclusion policy"
```

Stage two, optional but cheap since there's no remote and no clones — purge history with the `git-filter-repo` already in your flake:

```text
cd ~/.config/pipulate
git filter-repo --force --invert-paths --path api_key.txt --path credentials.json --path gmail_token.json --path keys.json --path token_cache.json --path pii_substitutions.txt
```

After stage two, the P3 probe's next-compile receipt should show only `blogs.json`, `flippers.json`, `last_published.json`, and `.gitignore` tracked — a config repo that could safely grow a remote someday, which is exactly what your idea #8 ("bake the config into the parent system") wants it to become.

**MikeLev.in**: Okay, probe always run now:

```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ # P1 — before: MISSING (file doesn't exist yet). After applying the patch: EXISTS. Gates P2.
test -f scripts/botify.py && echo EXISTS || echo MISSING

# P2 — smoke test: auth + identity walk. Bounded by head. Before patch it errors (valid
#      BEFORE reading); after patch it prints org/project slugs or a clean token error.
python scripts/botify.py 2>&1 | head -30

# P3 — secrets untrack verification. Before the external deliverable below: 9 tracked
#      files including 5 secrets. After: secrets absent. Gates the filter-repo decision.
cd ~/.config/pipulate && git ls-files; cd -
MISSING
python: can't open file '/home/mike/repos/pipulate/scripts/botify.py': [Errno 2] No such file or directory
api_key.txt
blogs.json
credentials.json
flippers.json
gmail_token.json
keys.json
last_published.json
pii_substitutions.txt
token_cache.json
/home/mike/repos/pipulate
(nix) pipulate $
```

And now we set context:

```bash
#      _       _   _   _               ____ _   _  ___  ____  _ 
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |
#    / _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |
#   / ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)
# AD HOC CHOP: The Not-Managed-by-Git Safe-for-Client-Data place.

# BIG STANDARD STUFF (Optionally comment out any)
! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs         # <-- This is the "Rolling Pin"
foo_files.py         # Router & Book Outline
prompt_foo.py        # Hand-cranked Agentic Framework
init.lua             # Text as muscle memory
flake.nix            # Hardware as projections

--- INSERT AD HOC CONTEXT BELOW HERE ---

scripts/gmail.py
scripts/botify.py
config.py
! test -f scripts/botify.py && echo EXISTS || echo MISSING
! python scripts/botify.py 2>&1 | head -30
! cd ~/.config/pipulate && git ls-files
```

We paste the end-of-prompt-text (the prompt) into location now but keep in mind
as you read this that as we type, we keep pushing it down.

Now we got patches! These are my favorite.

```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
✅ WHOLE-FILE WRITE: CREATED 'scripts/botify.py'.
(nix) pipulate $ git add scripts/botify.py 
(nix) pipulate $ m
📝 Committing: refactor: Add botify.py script to interface with the Botify API
[main 59862815] refactor: Add botify.py script to interface with the Botify API
 1 file changed, 237 insertions(+)
 create mode 100644 scripts/botify.py
(nix) pipulate $ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 48 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 3.29 KiB | 3.29 MiB/s, done.
Total 4 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:pipulate/pipulate.git
   eb074dc4..59862815  main -> main
(nix) pipulate $
```

A whole file write! Still I'm not vibe-coding. Or maybe I am. Like-a-boss
coding, 'cause that's what I'm doing. If you'd make a good boss but are
introverted and learn vim instead, the world is your oyster. Or is that Arrakis?
Anyway, `ahc` (Ad Hoc CHOP!) thump thump thump thump

Receipts above are AFTER readings. Confirm botify.py exists and its identity
walk returns org/project slugs (or a clean missing-token message). Confirm the
secrets untrack landed: git ls-files should no longer show api_key.txt,
credentials.json, gmail_token.json, keys.json, token_cache.json, or
pii_substitutions.txt. Then choose: (a) draft scripts/confluence.py in the same
mold reusing confluenceizer's CONFLUENCE_EMAIL/CONFLUENCE_TOKEN envs, or (b)
extend botify.py with an org/project/analysis SHOW mode surfacing datamodel
field counts via the true_schema_discoverer endpoints.

**MikeLev.in**: Okay, this is so fascinating. What Fable 5 is saying is that even if you
have a local wallet directory for all your API credentials, even if the repo has
no git endpoint, merely being a git repo is too dangerous because git is too
powerful in that if you're set up properly with your own SSH keys, make an
endpoint somewhere accidentally and BAM! Okay, I get it. I thought if it's one
hundred percent local, why not get undo? But this is much wiser. Thank you
stochastic parrot! Parrot or not, this is good thinking and it quacks like
there's someone inside. But it's now gone 'cause ephemeral genies just kind of
dissolve.

Of course we have probes:

```bash
(nix) pipulate $ # P1 — botify fix verification. BEFORE (this compile's receipt): AttributeError crash.
#      AFTER: org/project slugs print, no traceback. Gates any future SHOW mode (option b).
python scripts/botify.py 2>&1 | head -30

# P2 — confluence.py existence + clean-failure check. BEFORE: file missing.
#      AFTER: exists; prints either space list or a clean missing-env message. Gates its adoption.
python scripts/confluence.py 2>&1 | head -15

# P3 — secrets untrack, second attempt verification. BEFORE: 9 tracked files incl. 5 secrets.
#      AFTER (once the external deliverable actually runs): only blogs.json, flippers.json,
#      last_published.json, .gitignore. Gates the filter-repo history purge.
cd ~/.config/pipulate && git ls-files; cd -
# Botify projects visible to michael.levin (org/project | name)

Traceback (most recent call last):
  File "/home/mike/repos/pipulate/scripts/botify.py", line 237, in <module>
    main()
  File "/home/mike/repos/pipulate/scripts/botify.py", line 221, in main
    list_identity(client, args.max)
  File "/home/mike/repos/pipulate/scripts/botify.py", line 125, in list_identity
    projects = follow_pages(client, f"{API_BASE}/users/{username}/projects", max_items)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mike/repos/pipulate/scripts/botify.py", line 109, in follow_pages
    items.extend(data.get("results", []))
                 ^^^^^^^^
AttributeError: 'list' object has no attribute 'get'
python: can't open file '/home/mike/repos/pipulate/scripts/confluence.py': [Errno 2] No such file or directory
api_key.txt
blogs.json
credentials.json
flippers.json
gmail_token.json
keys.json
last_published.json
pii_substitutions.txt
token_cache.json
/home/mike/repos/pipulate
(nix) pipulate $
```

And then we set context:

```text
#      _       _   _   _               ____ _   _  ___  ____  _ 
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |
#    / _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |
#   / ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)
# AD HOC CHOP: The Not-Managed-by-Git Safe-for-Client-Data place.

# BIG STANDARD STUFF (Optionally comment out any)
! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs         # <-- This is the "Rolling Pin"
foo_files.py         # Router & Book Outline
prompt_foo.py        # Hand-cranked Agentic Framework
init.lua             # Text as muscle memory
flake.nix            # Hardware as projections

--- INSERT AD HOC CONTEXT BELOW HERE ---

scripts/botify.py
scripts/confluence.py
scripts/gmail.py
config.py
! python scripts/botify.py 2>&1 | head -30
! python scripts/confluence.py 2>&1 | head -15
! cd ~/.config/pipulate && git ls-files
```

And the patches, more fun! See: player piano!

```diff
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/botify.py'.
(nix) pipulate $ d
diff --git a/scripts/botify.py b/scripts/botify.py
index 08d5426d..6006553a 100644
--- a/scripts/botify.py
+++ b/scripts/botify.py
@@ -102,10 +102,19 @@ def project_coordinates(project):
 
 
 def follow_pages(client, url, max_items):
-    """Drain a paginated Botify list endpoint, capped at max_items."""
+    """Drain a Botify list endpoint, capped at max_items.
+
+    Defensive against BOTH response shapes the API actually serves:
+      - paginated envelope: {"results": [...], "next": url-or-null}
+      - bare JSON array:    [...]   (e.g. /users/{username}/projects)
+    The bare-array case has no pagination cursor, so take it and stop.
+    """
     items = []
     while url and len(items) < max_items:
         data = get_json(client, url)
+        if isinstance(data, list):
+            items.extend(data)
+            break
         items.extend(data.get("results", []))
         url = data.get("next")
     return items[:max_items]
(nix) pipulate $ m
📝 Committing: fix: Handle bare JSON array responses from Botify API
[main c7138b1e] fix: Handle bare JSON array responses from Botify API
 1 file changed, 10 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ WHOLE-FILE WRITE: CREATED 'scripts/confluence.py'.
(nix) pipulate $ d
(nix) pipulate $ git add scripts/confluence.py
(nix) pipulate $ m
📝 Committing: chore: Update confluence.py script with comprehensive documentation and functionality
[main 9da22c44] chore: Update confluence.py script with comprehensive documentation and functionality
 1 file changed, 186 insertions(+)
 create mode 100644 scripts/confluence.py
(nix) pipulate $ git push
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 48 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 3.37 KiB | 3.37 MiB/s, done.
Total 8 (delta 5), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (5/5), completed with 3 local objects.
To github.com:pipulate/pipulate.git
   3628da4f..9da22c44  main -> main
(nix) pipulate $
```

See, the rhythm is `patch`, `app`, `d`, `m`, `patch`, `app`, `d`, `m`, `patch`,
`app`, `d`, `m`... until of course there's a whole file created and your muscle
memory keeps rolling. Still not vibe-coding. Though maybe I am, like some
wetworks coupling relay speed-reading code. I'm told I've got taste; that's
right where a human enters the picture; keeping Ouroboros paperclip optimizers
at bay? 

And now that that's done we know causal probe two lands when we go `ahc` (Ad Hoc
CHOP!) again. Oh, wait but there's more. This breaks the pattern. And so our
whole muscle memory slams on the breaks. Executive function says "Yes, now we do
this." I'll be studying this more to get Fable's thinking.

```bash
(nix) pipulate $ cd ~/.config/pipulate
printf '%s\n' api_key.txt credentials.json gmail_token.json keys.json token_cache.json pii_substitutions.txt > .gitignore
git rm --cached api_key.txt credentials.json gmail_token.json keys.json token_cache.json pii_substitutions.txt
git add .gitignore && git commit -m "Untrack secrets; structural absence over exclusion policy"
rm 'api_key.txt'
rm 'credentials.json'
rm 'gmail_token.json'
rm 'keys.json'
rm 'pii_substitutions.txt'
rm 'token_cache.json'
[main eae783e] Untrack secrets; structural absence over exclusion policy
 7 files changed, 6 insertions(+), 6685 deletions(-)
 create mode 100644 .gitignore
 delete mode 100644 api_key.txt
 delete mode 100644 credentials.json
 delete mode 100644 gmail_token.json
 delete mode 100644 keys.json
 delete mode 100644 pii_substitutions.txt
 delete mode 100644 token_cache.json
(nix) pipulate $ cd ~/.config/pipulate
git filter-repo --force --invert-paths --path api_key.txt --path credentials.json --path gmail_token.json --path keys.json --path token_cache.json --path pii_substitutions.txt
Parsed 3 commits
New history written in 0.03 seconds; now repacking/cleaning...
Repacking your repo and cleaning out old unneeded objects
HEAD is now at 1eb49df Untrack secrets; structural absence over exclusion policy
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 48 threads
Compressing objects: 100% (12/12), done.
Writing objects: 100% (12/12), done.
Total 12 (delta 3), reused 0 (delta 0), pack-reused 0 (from 0)
Completely finished after 0.10 seconds.
(nix) pipulate $
```

It's also new that they always get the last word. I like where it's going.

Receipts above are AFTER readings. Confirm botify.py's identity walk now returns
org/project slugs without a traceback, confluence.py exists and fails cleanly
(or lists spaces if envs are set), and the secrets untrack finally landed. Then:
(a) run the filter-repo history purge decision, and (b) design the connector
organization scheme from idea #9 — should gmail.py/botify.py/confluence.py
graduate from scripts/ into a scripts/connectors/ (or tools/connectors/) package
with a shared auth/paging/probe-economy helper module, and what does that shared
module's API look like?

**Fable 5 High**: Context verified.

**Verdict 1 — botify.py identity walk: FIXED.** The live receipt shows a clean run: `# Botify projects visible to michael.levin`, twelve org/project rows, no traceback, breadcrumb footer intact. The bare-array defense in `follow_pages` did its job. One compile-lane observation your own doctrine demands I make: the `uhnd-com` and `michaellevin-org` slugs rode into this cloud-bound payload unredacted. If UHND counts as a client rather than a hobby property, it needs a `pii_substitutions.txt` line before the next compile that includes a botify.py receipt — this is exactly the leak surface the script's own docstring warns about, demonstrating itself on its first live receipt.

**Verdict 2 — confluence.py: exists and fails cleanly.** The receipt is the ideal missing-env message: names all three variables, gives the example URL, points at confluenceizer's convention. That's a *good* AFTER reading — the tool is correct; only the wallet is unplumbed.

**Verdict 3 — secrets untrack and history purge: LANDED, both stages.** The `git ls-files` receipt shows exactly four files: `.gitignore`, `blogs.json`, `flippers.json`, `last_published.json`. And per your transcript the filter-repo purge already ran ("Parsed 3 commits... Completely finished"). So part (a) of your request is moot in the best way — the decision was executed, not just made. What remains is a residue check (probe P2 below): confirm no ref anywhere in the repo still reaches a secret blob. filter-repo strips reflogs and repacks, so this should return empty, but the receipt makes it constitutional rather than assumed. Your config repo is now safe to grow a remote — which is precisely what idea #8 wants it to become.

---

## (b) The connector organization scheme

**Where: `scripts/connectors/`, not `tools/connectors/`.** The distinction is audience. `tools/` is the MCP/tool-calling surface consumed by the *running server and its LLM* — the Wand's hands. Connectors are operator-side Unix commands whose stdout is designed for `!` chisel-strikes in the compile lane. Different actuator, different consumer, different lifecycle. Putting them under `tools/` would invite the mcp_tools registry to discover them, which is a coupling you don't want.

**The harder question — shared `kit.py` or WET standalone — and I'm going to argue against the module you asked me to design, then design it anyway.**

The case against, from your own book: the "one `.py` file, forever, droppable into a registry" thesis (velocity-of-the-living-book, line 98) is the whole point of these connectors. A connector that does `from connectors.kit import follow_pages` is no longer a single-file tool — it's a package member. It can't be curl'd, can't be pasted into a gist, can't be dropped into an OWF/Agent-Skills registry as one artifact, and every `!` invocation now depends on package-relative imports resolving from whatever cwd prompt_foo's shell spawns in. You also have empirical evidence the "shared" logic isn't actually shared: the bare-array bug was *Botify-specific* API behavior. Confluence's search always returns a `results` envelope; Gmail's paging is `nextPageToken`-based and different again. A unified `follow_pages(client, url, max_items, results_key=..., next_key=..., token_param=...)` is knob soup — complexity imported to serve an abstraction, the exact DRY trap your WET chapter exists to name.

**So the recommendation: graduate the files into `scripts/connectors/` for organization, keep each one self-contained, and make the shared asset a *contract document* rather than a code module** — `scripts/connectors/README.md` (patch below), which is simultaneously the human doc, the AGENTS.md-style operating instructions for any AI asked to mint the next connector (gong.py, gsc.py, gdocs.py, sheets.py...), and 80% of a future SKILL.md. The pattern is the DRY thing; the connectors are deliberately WET. This also dovetails with idea #6: when you do the Agent Skills pass, this README is the body of `skills/mint-connector/SKILL.md` nearly verbatim.

## A Universal Contract for API Connectors

**The kit.py API, since you asked, held in reserve.** The extraction trigger should be the rule of *pain*, not the rule of three: the day you fix the same bug in the same helper in two files, extract. When that day comes, the seam is small:

```python
# scripts/connectors/kit.py — extract ONLY when the same bug bites twice
def die(msg: str, code: int = 1)            # stderr + sys.exit — identical in all three today
def get_json(client, url, params=None)      # 200-or-die GET — identical in all three today
def cap(items, max_items)                   # probe-economy truncation with "(N more truncated)" line
def strip_html(s: str) -> str               # confluence + gmail share this verbatim already
def breadcrumb(cmd: str)                    # the "# Next: ..." footer, so the drill-down grammar stays uniform
```

Note what's *not* in it: no `follow_pages` (per-API paging genuinely differs), no auth (Botify uses config.py, Gmail uses OAuth token files, Confluence uses raw envs — three legitimately different wallets), no client construction. `strip_html` is the honest first candidate — it's already copy-pasted identically between gmail.py and confluence.py.

**The naming/alias layer** (next turn, in flake.nix): `alias gmail='python "$PIPULATE_ROOT/scripts/connectors/gmail.py"'` and siblings, so the terminal grammar becomes `botify uhnd-com`, `confluence ENG`, `gmail <thread_id>` — the tiny Unix commands of idea #7 made literal. Deferred one turn so the aliases point at paths that exist.

**The wallet tie-in (idea #8), now unblocked:** with the config repo scrubbed, per-connector config can live as tracked files (`connectors.json` mapping connector → env names + defaults) while the secrets themselves stay untracked-by-structure. The connectors already read from env + `~/.config/pipulate`; the pattern generalizes without new machinery.

---

**1. PROBES**

```text
# P1 — package existence. BEFORE: NO_PACKAGE_YET. AFTER (git mv + README patch): four files listed.
#      Gates the flake.nix alias patch next turn.
ls scripts/connectors/ 2>/dev/null || echo NO_PACKAGE_YET

# P2 — filter-repo residue check. Proves no ref anywhere still reaches a secret blob.
#      BEFORE and AFTER should both be empty log + 4 tracked files; any output = purge incomplete.
cd ~/.config/pipulate && git log --all --oneline -- keys.json token_cache.json | head -5 && git ls-files; cd -

# P3 — moved connector smoke test. BEFORE: file-not-found (valid reading, move not yet done).
#      AFTER: identity walk from the new path. Proves the move broke nothing.
python scripts/connectors/botify.py 2>&1 | head -5
```

**2. NEXT CONTEXT** (probes echoed verbatim, per rule 12)

adhoc.txt slot:

```text
scripts/connectors/README.md
scripts/connectors/botify.py
scripts/connectors/confluence.py
scripts/connectors/gmail.py
flake.nix
! ls scripts/connectors/ 2>/dev/null || echo NO_PACKAGE_YET
! cd ~/.config/pipulate && git log --all --oneline -- keys.json token_cache.json | head -5 && git ls-files
! python scripts/connectors/botify.py 2>&1 | head -5
```

prompt.md text: "Receipts above are AFTER readings. Confirm the connectors package exists with all four files, the moved botify.py runs from its new path, and the secrets purge shows zero residue. Then patch flake.nix to add the connector aliases (gmail, botify, confluence pointing at $PIPULATE_ROOT/scripts/connectors/) and update the two foo_files.py references to scripts/gmail.py so topological integrity stays green. Also decide whether pii_substitutions.txt needs a uhnd-com rule before any future botify receipt compiles."

**3. PATCHES**

```text
Target: scripts/connectors/README.md
[[[WRITE_FILE]]]
# Connectors — Unix-philosophy API gateways for Prompt Fu context

Each connector is ONE self-contained `.py` file. That is the point. No shared
imports, no package coupling: a connector must survive being curl'd, gisted,
or dropped into a skills registry as a single artifact. Duplication between
connectors is deliberate (WET). Extract to a kit.py only when the same bug
has been fixed in the same helper in two files.

## The Contract (every connector obeys all of these)

1. ONE positional argument, mode auto-detected from its shape:
   - no argument        -> identity walk / top-level LIST
   - bare token         -> LIST within that scope (org, space key, ...)
   - id-shaped token    -> FETCH one object in full (thread id, page id)
   - whitespace or '{'  -> QUERY / SEARCH mode
2. `-n/--max` output cap, default 25 (THE PROBE ECONOMY RULE). stdout is
   destined for compiled context payloads; the bound is a feature.
3. Every LIST mode ends with a `# Next:` breadcrumb showing the exact command
   for the next drill-down step. The connector teaches its own use.
4. Auth resolves from env vars and/or ~/.config/pipulate, never from files
   inside this repo. Fail with a message that names the missing variable and
   shows an example value. A clean failure is a valid receipt.
5. Errors go to stderr and exit nonzero; stdout stays parseable.
6. First interactive run may open a browser (OAuth); every subsequent run
   must work headless under prompt_foo's captured, no-TTY pipe.
7. COMPILE-LANE CAUTION in the docstring: if LIST/FETCH output can contain
   client identifiers, say so, and rely on pii_substitutions.txt coverage
   before any `!` invocation rides to a cloud chat window.

## Current connectors

- gmail.py       LIST by address / FETCH by thread id (OAuth token file)
- botify.py      identity walk / org / org/project / BQL query (BOTIFY_API_TOKEN)
- confluence.py  spaces / space pages / page id / CQL search (CONFLUENCE_* envs)

## Minting a new connector

Copy the closest existing connector, keep the docstring shape, keep the
disambiguation table, keep the breadcrumbs. If an API's paging differs,
write that API's paging — do not generalize another connector's.
[[[END_WRITE_FILE]]]
```

The move itself is beyond apply.py's vocabulary — three terminal commands, run before the next compile so P1/P3 flip:

```text
cd ~/repos/pipulate
git mv scripts/gmail.py scripts/botify.py scripts/confluence.py scripts/connectors/ 2>/dev/null || { mkdir -p scripts/connectors && git mv scripts/gmail.py scripts/connectors/ && git mv scripts/botify.py scripts/connectors/ && git mv scripts/confluence.py scripts/connectors/; }
git add scripts/connectors/README.md
```

(Note: foo_files.py's commented `# scripts/gmail.py` references and the KEEP-AS-EXAMPLE lines will trip the topological integrity alert after the move — that's the alert doing its job; next turn's patch updates them alongside the flake aliases.)

**4. EXTERNAL DELIVERABLES**

No external deliverables required. The wallet is scrubbed, chmod'd, and structurally git-proof; the only artifact left outside this repo is the eventual `pii_substitutions.txt` line for client slugs, which is a one-line manual edit you'll make the moment a real client org appears in a botify receipt.

**MikeLev.in**: Oh U H N D... that's Frank. Frank's not a client, though let's treat him
as a safe select-only sample; much more live-site data to probe at maybe even
public-side, but I'll check with him before we do that (name alone is okay).
Treat it like MikeLev.in. Not a Honeybot, but what is? Experimentation; that's
what we're doing just like all SEOs say, but lookie here, it's performance art!

I'll take your advice. Move `gmail` as suggested. The concept of connectors in
Pipulate's parity Kung Fu! Yes, parity Kung Fu. It's symlinks in spirit. I've
got mappings and mappings and mappings... but everyone does! Key-val pairs are
the answer. It hardly needs question. I think there's something universal in
that; maybe how seemingly non-local effects with entanglement works? And gravity
too. I hear it might all be entropy now. Ginestra Bianconi! I saw her on YouTube
on Curt Jaimungal's YouTube "Theory of Everything" channel, you should check it
out!

What's that you say, Fable 5 I'm using on High even for steps I explicitly said
I'd use cheap models for. Not on the first pass. Sharpen machetes! The jungle is
dense when forging a path the first time.

You generalize patterns. You do it with text-files. Sometimes it's Python and
sometimes it's JSON. When not private and config then dot Nix because that's how
human-actuated software Von Neumann probes really work best. You pin a great
deal. It's macOS tolerant like Anduril employees apparently need. But Nix can do
neat tricks. Ones a full O S. It's just built from a text-file like anything
else but it'll cast a machine. Like light through a crystal, it's everything
about your system minus save-data and keys. 1, 2, 3's another discussion that's
here out-of-scope.

First we got probes... Whoa what those `cd`s. Not showing full-paths in the
prompt's a decision but be careful when to folders are named exactly the same.
First we do `p` if there's any question.

Then we do patches:

```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ # P1 — package existence. BEFORE: NO_PACKAGE_YET. AFTER (git mv + README patch): four files listed.
#      Gates the flake.nix alias patch next turn.
ls scripts/connectors/ 2>/dev/null || echo NO_PACKAGE_YET

# P2 — filter-repo residue check. Proves no ref anywhere still reaches a secret blob.
#      BEFORE and AFTER should both be empty log + 4 tracked files; any output = purge incomplete.
cd ~/.config/pipulate && git log --all --oneline -- keys.json token_cache.json | head -5 && git ls-files; cd -

# P3 — moved connector smoke test. BEFORE: file-not-found (valid reading, move not yet done).
#      AFTER: identity walk from the new path. Proves the move broke nothing.
python scripts/connectors/botify.py 2>&1 | head -5
NO_PACKAGE_YET
.gitignore
blogs.json
flippers.json
last_published.json
/home/mike/repos/pipulate
python: can't open file '/home/mike/repos/pipulate/scripts/connectors/botify.py': [Errno 2] No such file or directory
(nix) pipulate $
```

Then we do context:

```text
#      _       _   _   _               ____ _   _  ___  ____  _ 
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |
#    / _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |
#   / ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)
# AD HOC CHOP: The Not-Managed-by-Git Safe-for-Client-Data place.

# BIG STANDARD STUFF (Optionally comment out any)
! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs         # <-- This is the "Rolling Pin"
foo_files.py         # Router & Book Outline
prompt_foo.py        # Hand-cranked Agentic Framework
init.lua             # Text as muscle memory
flake.nix            # Hardware as projections

--- INSERT AD HOC CONTEXT BELOW HERE ---

scripts/connectors/README.md
scripts/connectors/botify.py
scripts/connectors/confluence.py
scripts/connectors/gmail.py
flake.nix
! ls scripts/connectors/ 2>/dev/null || echo NO_PACKAGE_YET
! cd ~/.config/pipulate && git log --all --oneline -- keys.json token_cache.json | head -5 && git ls-files
! python scripts/connectors/botify.py 2>&1 | head -5
```

Now we put the prompt in place, but we just keep pushing it down as we type so
we don't forget. Now we have patches!

```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
✅ WHOLE-FILE WRITE: CREATED 'scripts/connectors/README.md'.
(nix) pipulate $ d
(nix) pipulate $ git add scripts/connectors/README.md
(nix) pipulate $ m
📝 Committing: chore: Update connectors README with improved documentation and connector list
[main 5989c639] chore: Update connectors README with improved documentation and connector list
 1 file changed, 40 insertions(+)
 create mode 100644 scripts/connectors/README.md
(nix) pipulate $
```

Oh, look! Let's call it house-keeping. Remember a Web-based ChatBot like
Claude.AI can't do anything on your machine without you. Except for in Chrome
sandboxes, but that's another story but I just thought I'd mention because that's 
where the Michael Crichton dinosaurs escaping comes from as plausibly as anything
in tech, and really probably quite a bit more. Isn't that right, Little Bobby
Tables `Robert'); DROP TABLE Students;-- Roberts`? We humans even have to do the
terminal commands in this air-gapped situation. No AI touches hardware...
period. Wait until you see what I have in store with DOM-requiring to be there
to drop a skyhook to keep it running, kill-a-loop by closing a tab in those rare
cases you really want it agentic FastHTML and HTMX is so cool framework. Infer
what you will. I have two planned. A flip-a-con until it terminates, and an
agentic baton-pass where the model has to make sure it wants to re-invoke itself
first by putting a control-code in the generated response stream that it knows
the Ouroboros mechanism is waiting for. Something like knowing the player piano
keys:

```text
[[[prompt]]]Hello me! I just prompted myself and what I thought I might like you
to do on this turn is... blah blah blah... Just make sure you include one of
these prompt-blocks with those triple square-bracket tags to contain it, and
you're golden. You're better than that. You're Agentic![[[/prompt]]]
```

Oh yeah, the housekeeping:

```bash
(nix) pipulate $ cd ~/repos/pipulate
git mv scripts/gmail.py scripts/botify.py scripts/confluence.py scripts/connectors/ 2>/dev/null || { mkdir -p scripts/connectors && git mv scripts/gmail.py scripts/connectors/ && git mv scripts/botify.py scripts/connectors/ && git mv scripts/confluence.py scripts/connectors/; }
git add scripts/connectors/README.md
(nix) pipulate $
```

Alright, I guess that's it. Let's pull the trigger. Or let's say set the
thumper? These mental models are important. The SEO in me wants every last
metaphor because there's not one we won't mix. That's what we do. After all what
you think about it what's a meta for? No we can leave Frank's uhnd.com site
showing. That's fine.

Receipts above are AFTER readings. Confirm the connectors package exists with
all four files, the moved botify.py runs from its new path, and the secrets
purge shows zero residue. Then patch flake.nix to add the connector aliases
(gmail, botify, confluence pointing at $PIPULATE_ROOT/scripts/connectors/) and
update the two foo_files.py references to scripts/gmail.py so topological
integrity stays green. Also decide whether pii_substitutions.txt needs a
uhnd-com rule before any future botify receipt compiles.

**Fable 5 High**: [You’ve used 91% of your Fable 5 limit · Resets Jul 18 at 6:00 PM]

Context verified.

**Verdict 1 — connectors package: COMPLETE.** The live receipt shows all four files in `scripts/connectors/`: `botify.py`, `confluence.py`, `gmail.py`, `README.md`. The Paintbox in this compile still lists them as unclaimed colors — expected, since nothing in the active chapters references the new paths yet; the foo_files patch below claims them and the Paintbox will self-clear on the next compile.

**Verdict 2 — moved botify.py runs from its new path.** The receipt shows a clean identity walk (`# Botify projects visible to michael.levin` plus org/project rows). But the receipt hides a latent lie I have to flag: `botify.py` still computes `project_root = Path(__file__).resolve().parent.parent` — which, post-move, is `scripts/`, not the repo root. The `from config import` only works right now because the editable install (`uv pip install -e .`) exposes `config` anyway; the `sys.path.insert` is inserting the wrong directory and could someday shadow a module. It works by accident, not by design. Patched below (`parent.parent.parent`). `gmail.py`'s `REPO_ROOT` is similarly stale but *unused* — harmless vestige, left alone per minimal-change. `confluence.py` is pure-env and untouched by geometry.

**Verdict 3 — secrets purge: ZERO residue.** `git log --all` across `keys.json`/`token_cache.json` printed nothing; `git ls-files` shows exactly `.gitignore`, `blogs.json`, `flippers.json`, `last_published.json`. The wallet repo is now history-clean and structurally incapable of leaking secrets — safe to grow a remote when idea #8 matures.

**Verdict 4 — pii_substitutions ruling: NO uhnd-com rule needed.** You ruled it: Frank's property is a safe select-only sample, same class as MikeLev.in — name alone is fine. Same logically applies to `michaellevin-org`. The one thing worth doing (external deliverable below) is writing that ruling down as a comment *inside* `pii_substitutions.txt` itself, so future-you (and future-me, who cannot remember this conversation) doesn't re-litigate it every time a botify receipt compiles. The moment a *real* client org appears in a receipt, that's when a `pattern === replacement` line gets minted — and the comment will be sitting right there to remind you.

**One alias-scope caveat before the patch:** flake aliases live in the *interactive* shell only. The `!` chisel-strikes in adhoc.txt run through `subprocess(shell=True)` — a non-interactive shell that never sees aliases. So the grammar splits cleanly: humans type `botify uhnd-com`; adhoc.txt keeps typing `! python scripts/connectors/botify.py uhnd-com`. That's not a defect — it's the same interactive/captured split the rgx tty-gate already enforces.

---

**1. PROBES**

```text
# P1 — proves the alias patch landed in flake.nix. BEFORE: 0 hits. AFTER: 3 hits.
#      (Alias availability itself needs a fresh `nix develop`; this proves the source.)
rg -n "alias gmail=|alias botify=|alias confluence=" flake.nix

# P2 — proves foo_files.py no longer references the old gmail path. BEFORE: 4 hits. AFTER: 0.
#      Gates topological-integrity staying green on the next compile.
rg -c "scripts/gmail.py" foo_files.py || echo CLEAN

# P3 — proves the sys.path geometry fix didn't break the import. BEFORE and AFTER both
#      print the identity walk header; a traceback here falsifies the patch.
python scripts/connectors/botify.py 2>&1 | head -3
```

**2. NEXT CONTEXT** (probes echoed verbatim, per rule 12)

adhoc.txt slot:

```text
scripts/connectors/README.md
scripts/connectors/botify.py
scripts/connectors/gmail.py
scripts/articles/common.py
config.py
! rg -n "alias gmail=|alias botify=|alias confluence=" flake.nix
! rg -c "scripts/gmail.py" foo_files.py || echo CLEAN
! python scripts/connectors/botify.py 2>&1 | head -3
```

prompt.md text: "Receipts above are AFTER readings. Confirm the connector aliases are in flake.nix, foo_files.py is clean of old gmail paths (topological integrity should be green in this compile's Processing Log), and botify.py still walks identity after the sys.path fix. Then design the connectors.json wallet mapping (idea #8): a tracked file in ~/.config/pipulate mapping connector name → required env vars, token file paths, and defaults — the key-val parity layer that lets blogs.nix-style config generation eventually emit it. Also nominate the next connector to mint: gsc.py (service-account JSON pattern already in scripts/gsc/) or gong.py."

**3. PATCHES**

```text
Target: flake.nix
[[[SEARCH]]]
          alias mcp='(cd ~/repos/pipulate && .venv/bin/python cli.py call)'
          alias vim='nvim'
[[[DIVIDER]]]
          alias mcp='(cd ~/repos/pipulate && .venv/bin/python cli.py call)'
          # THE CONNECTOR GRAMMAR (idea #7 made literal): tiny Unix commands,
          # one per API, each a self-contained file in scripts/connectors/.
          # Args pass through: `botify org/project`, `confluence ENG`,
          # `gmail <thread_id>`. Interactive-shell only — adhoc.txt `!` lines
          # keep the full `python scripts/connectors/...` spelling because
          # child shells never inherit aliases.
          alias gmail='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/gmail.py"'
          alias botify='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/botify.py"'
          alias confluence='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/confluence.py"'
          alias vim='nvim'
[[[REPLACE]]]
```

```text
Target: foo_files.py
[[[SEARCH]]]
# KEEP THIS AS AN EXAMPLE OF HOW TO USE GMAIL (AND CLEAR WAY FOR SAME FOR GDOCS & SHEETS EQUIVALENT)
# ! python scripts/gmail.py 19ed7896e9577340  # Performance Baseline Report
# ! python scripts/gmail.py 19ef63ffb0dc3ff4  # Google Search Console & GMC Setup
# ! python scripts/gmail.py 19ef61f326a3b536  # AI Visibility Tracking Transcript
[[[DIVIDER]]]
# KEEP THIS AS AN EXAMPLE OF HOW TO USE GMAIL (AND CLEAR WAY FOR SAME FOR GDOCS & SHEETS EQUIVALENT)
# ! python scripts/connectors/gmail.py 19ed7896e9577340  # Performance Baseline Report
# ! python scripts/connectors/gmail.py 19ef63ffb0dc3ff4  # Google Search Console & GMC Setup
# ! python scripts/connectors/gmail.py 19ef61f326a3b536  # AI Visibility Tracking Transcript
[[[REPLACE]]]
```

```text
Target: foo_files.py
[[[SEARCH]]]
# XVIII. MISC UNIX PHILOSOPHY STYLE COMMANDS FOR COMPOSABLE PIPELINE WORKFLOWS
# ============================================================================

# scripts/gmail.py
[[[DIVIDER]]]
# XVIII. MISC UNIX PHILOSOPHY STYLE COMMANDS FOR COMPOSABLE PIPELINE WORKFLOWS
# ============================================================================

# THE CONNECTORS (WET single-file API gateways; contract in the README)
# scripts/connectors/README.md
# scripts/connectors/gmail.py
# scripts/connectors/botify.py
# scripts/connectors/confluence.py
[[[REPLACE]]]
```

```text
Target: scripts/connectors/botify.py
[[[SEARCH]]]
# Wire into the central config (same pattern as scripts/ai.py)
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
from config import get_botify_token
[[[DIVIDER]]]
# Wire into the central config (same pattern as scripts/ai.py).
# NOTE: connectors/ is one level deeper than scripts/, hence three parents.
# The editable install also exposes `config`, but the explicit path keeps
# this file honest as a standalone, curl-able artifact.
project_root = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(project_root))
from config import get_botify_token
[[[REPLACE]]]
```

**4. EXTERNAL DELIVERABLES** (manual edit, outside this repo, never patch markers)

One comment block for the top of `~/.config/pipulate/pii_substitutions.txt`, recording today's ruling so it never gets re-litigated:

```text
# RULING 2026-07-14: uhnd-com (Frank's property) and michaellevin-org are
# SAFE SAMPLE PROPERTIES — treat like MikeLev.in; slugs may appear in
# cloud-bound compile receipts. Mint a `pattern === replacement` line here
# the moment a REAL client org/project slug appears in any botify receipt.
```

The rhythm holds: `patch`, `app`, `d`, `m` — four times through — then `git push`, then `ahc` pulls the ripcord and the three echoed probes land as AFTER receipts in the next compile's manifest. The loom keeps reading its own punch cards.

**MikeLev.in**: The Loom keeps reading its own punchcards. Oh just wait until I activate
those `[[[prompt]]]` tags and it knows how to drive the ghost player of the
player piano that has LLM Optics right down to the greppable hydrated DOM and
DevTools, the same. Can you imagine? Just add CSS selectors or xpath or I'm
thinking jQuery paths too because you're trained on them well. And then there's
the DevTools accessibility tree that has all the ARIA tags and roles and...
well, you quite literally get the picture.

When we talk mental models held in one's mind, this is what we're talking about.
When I say something like the standard CPython version such-and-such that's
well-trained in like version 3.12 at the time of this writing (in post training
cutoff-time we're around 3.14 by why invite Murphy?).

Ooh neat! We're about to set a blast-radius:

```diff
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	renamed:    scripts/botify.py -> scripts/connectors/botify.py
	renamed:    scripts/confluence.py -> scripts/connectors/confluence.py
	renamed:    scripts/gmail.py -> scripts/connectors/gmail.py

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   foo_files.py

(nix) pipulate $ blast
📝 Committing: refactor: rename connectors to `connectors` directory
[main e0b2ce19] refactor: rename connectors to `connectors` directory
 4 files changed, 4 insertions(+), 2 deletions(-)
 rename scripts/{ => connectors}/botify.py (100%)
 rename scripts/{ => connectors}/confluence.py (100%)
 rename scripts/{ => connectors}/gmail.py (100%)
🚀 Pushing 2 commit(s) to remote...
Enumerating objects: 13, done.
Counting objects: 100% (13/13), done.
Delta compression using up to 48 threads
Compressing objects: 100% (9/9), done.
Writing objects: 100% (10/10), 2.27 KiB | 2.27 MiB/s, done.
Total 10 (delta 5), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (5/5), completed with 3 local objects.
To github.com:pipulate/pipulate.git
   f7afc3b4..e0b2ce19  main -> main

$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ 
```

That `clear` argument is working. Now we've got probes:

```bash
(nix) pipulate $ # P1 — proves the alias patch landed in flake.nix. BEFORE: 0 hits. AFTER: 3 hits.
#      (Alias availability itself needs a fresh `nix develop`; this proves the source.)
rg -n "alias gmail=|alias botify=|alias confluence=" flake.nix

# P2 — proves foo_files.py no longer references the old gmail path. BEFORE: 4 hits. AFTER: 0.
#      Gates topological-integrity staying green on the next compile.
rg -c "scripts/gmail.py" foo_files.py || echo CLEAN

# P3 — proves the sys.path geometry fix didn't break the import. BEFORE and AFTER both
#      print the identity walk header; a traceback here falsifies the patch.
python scripts/connectors/botify.py 2>&1 | head -3
4
# Botify projects visible to michael.levin (org/project | name)

uhnd-com/test-dev-site-crawl  Test Dev Site Crawl
(nix) pipulate $
```

Next we set context.

```text
#      _       _   _   _               ____ _   _  ___  ____  _ 
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |
#    / _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |
#   / ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)
# AD HOC CHOP: The Not-Managed-by-Git Safe-for-Client-Data place.

# BIG STANDARD STUFF (Optionally comment out any)
! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs         # <-- This is the "Rolling Pin"
foo_files.py         # Router & Book Outline
prompt_foo.py        # Hand-cranked Agentic Framework
init.lua             # Text as muscle memory
flake.nix            # Hardware as projections

--- INSERT AD HOC CONTEXT BELOW HERE ---
scripts/connectors/README.md
scripts/connectors/botify.py
scripts/connectors/gmail.py
scripts/articles/common.py
config.py
! rg -n "alias gmail=|alias botify=|alias confluence=" flake.nix
! rg -c "scripts/gmail.py" foo_files.py || echo CLEAN
! python scripts/connectors/botify.py 2>&1 | head -3
```

Next like before we paste prompt but keep pushing it down. 

Now there's our patches. Notice that rhythm:

```diff
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'flake.nix'.
(nix) pipulate $ d
diff --git a/flake.nix b/flake.nix
index 47bbf5a0..4bb638a1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -752,6 +752,15 @@ runScript = pkgs.writeShellScriptBin "run-script" ''
           alias force='(cd ~/repos/trimnoir && git commit --allow-empty -m "retry" && git push)'
           alias isnix="if [ -n \"$IN_NIX_SHELL\" ]; then echo \"✓ In Nix shell v${version}\"; else echo \"✗ Not in Nix shell\"; fi"
           alias mcp='(cd ~/repos/pipulate && .venv/bin/python cli.py call)'
+          # THE CONNECTOR GRAMMAR (idea #7 made literal): tiny Unix commands,
+          # one per API, each a self-contained file in scripts/connectors/.
+          # Args pass through: `botify org/project`, `confluence ENG`,
+          # `gmail <thread_id>`. Interactive-shell only — adhoc.txt `!` lines
+          # keep the full `python scripts/connectors/...` spelling because
+          # child shells never inherit aliases.
+          alias gmail='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/gmail.py"'
+          alias botify='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/botify.py"'
+          alias confluence='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/confluence.py"'
           alias vim='nvim'
           alias lsp='ls -d -1 "$PWD"/*'
           alias p='cd ~/repos/pipulate'
(nix) pipulate $ m
📝 Committing: chore: Update connector aliases for clarity and consistency
[main 7c127d29] chore: Update connector aliases for clarity and consistency
 1 file changed, 9 insertions(+)
(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 3456d8f6..366f39b3 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -168,9 +168,9 @@ foo_files.py      #  <-- THIS file. Content compiler router. Makes it very meta.
 # README.md  # [20,725 tokens | 104,252 bytes]
 
 # KEEP THIS AS AN EXAMPLE OF HOW TO USE GMAIL (AND CLEAR WAY FOR SAME FOR GDOCS & SHEETS EQUIVALENT)
-# ! python scripts/gmail.py 19ed7896e9577340  # Performance Baseline Report
-# ! python scripts/gmail.py 19ef63ffb0dc3ff4  # Google Search Console & GMC Setup
-# ! python scripts/gmail.py 19ef61f326a3b536  # AI Visibility Tracking Transcript
+# ! python scripts/connectors/gmail.py 19ed7896e9577340  # Performance Baseline Report
+# ! python scripts/connectors/gmail.py 19ef63ffb0dc3ff4  # Google Search Console & GMC Setup
+# ! python scripts/connectors/gmail.py 19ef61f326a3b536  # AI Visibility Tracking Transcript
 
 # KEEP EXPANDING THIS FOR BETTER ONGOING CONFLUENCE ARTICLE SANITIZING
 # ~/.config/pipulate/pii_substitutions.txt
(nix) pipulate $ m
📝 Committing: chore: Update script invocation paths in documentation
[main 46a96a83] chore: Update script invocation paths in documentation
 1 file changed, 3 insertions(+), 3 deletions(-)
(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 366f39b3..be1dc3be 100644
--- a/foo_files.py
+++ b/foo_files.py
@@ -643,7 +643,11 @@ scripts/articles/confluenceizer.py           # <-- Idempotent Jekyll-to-Confluen
 # XVIII. MISC UNIX PHILOSOPHY STYLE COMMANDS FOR COMPOSABLE PIPELINE WORKFLOWS
 # ============================================================================
 
-# scripts/gmail.py
+# THE CONNECTORS (WET single-file API gateways; contract in the README)
+# scripts/connectors/README.md
+# scripts/connectors/gmail.py
+# scripts/connectors/botify.py
+# scripts/connectors/confluence.py
 #  _____ _           _           _   _                      
 # |  ___(_)_ __   __| |   __ _  | | | | ___  _ __ ___   ___ 
 # | |_  | | '_ \ / _` |  / _` | | |_| |/ _ \| '_ ` _ \ / _ \
(nix) pipulate $ m
📝 Committing: chore: Update connector documentation and references in scripts/
[main bbe9f7a2] chore: Update connector documentation and references in scripts/
 1 file changed, 5 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/botify.py'.
(nix) pipulate $ d
diff --git a/scripts/connectors/botify.py b/scripts/connectors/botify.py
index 6006553a..80684fd3 100644
--- a/scripts/connectors/botify.py
+++ b/scripts/connectors/botify.py
@@ -40,8 +40,11 @@ from pathlib import Path
 
 import httpx
 
-# Wire into the central config (same pattern as scripts/ai.py)
-project_root = Path(__file__).resolve().parent.parent
+# Wire into the central config (same pattern as scripts/ai.py).
+# NOTE: connectors/ is one level deeper than scripts/, hence three parents.
+# The editable install also exposes `config`, but the explicit path keeps
+# this file honest as a standalone, curl-able artifact.
+project_root = Path(__file__).resolve().parent.parent.parent
 sys.path.insert(0, str(project_root))
 from config import get_botify_token
 
(nix) pipulate $ m
📝 Committing: fix: Update project_root path in botify.py
[main 86073030] fix: Update project_root path in botify.py
 1 file changed, 5 insertions(+), 2 deletions(-)
(nix) pipulate $ git push
Enumerating objects: 20, done.
Counting objects: 100% (20/20), done.
Delta compression using up to 48 threads
Compressing objects: 100% (14/14), done.
Writing objects: 100% (14/14), 1.93 KiB | 1.93 MiB/s, done.
Total 14 (delta 10), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (10/10), completed with 6 local objects.
To github.com:pipulate/pipulate.git
   e0b2ce19..86073030  main -> main
(nix) pipulate $
```

Thump, thump, thump, thump... Look, a Worm! We're catching wind.

Sorry, I don't think seeing the movie's enough.

Now we' make an edit. It's well within reason:

```bash
(nix) pipulate $ vim ~/.config/pipulate/pii_substitutions.txt
(nix) pipulate $ 
```

See, there's `vim` again. It's called myelination how that's not a problem. Your
fingers keep flying without thinking about editing at all. It's just kind of
flowing. A lot like the feeling I imagine a musician or athlete must feel quite
a lot.

Receipts above are AFTER readings. Confirm the connector aliases are in
flake.nix, foo_files.py is clean of old gmail paths (topological integrity
should be green in this compile's Processing Log), and botify.py still walks
identity after the sys.path fix. Then design the connectors.json wallet mapping
(idea #8): a tracked file in ~/.config/pipulate mapping connector name →
required env vars, token file paths, and defaults — the key-val parity layer
that lets blogs.nix-style config generation eventually emit it. Also nominate
the next connector to mint: gsc.py (service-account JSON pattern already in
scripts/gsc/) or gong.py.

**Fable 5 High**: [You’ve used 92% of your Fable 5 limit · Resets Jul 18 at 6:00 PM]

Context verified.

**Verdict 1 — connector aliases: LANDED.** The live receipt shows all three at flake.nix lines 761–763, pointing at `$PIPULATE_ROOT/scripts/connectors/`, with the interactive-only caveat documented in the comment above them. Alias availability in your fingers still waits for the next fresh `nix develop`, but the source is proven.

**Verdict 2 — foo_files.py: CLEAN, integrity GREEN.** The `rg -c` receipt echoed `CLEAN` (0 hits for the old path), and this compile's Processing Log shows `✅ Topological Integrity Verified: All references exist.` The Paintbox also stopped listing the connectors as unclaimed — chapter XVIII now owns them. One cosmetic wart from the same log: `Warning: FILE NOT FOUND AND WILL BE SKIPPED: .../--- INSERT AD HOC CONTEXT BELOW HERE ---`. Your adhoc.txt separator line doesn't start with `#`, so the parser treats it as a file path every compile. Harmless but noisy — one-character fix in the external deliverables.

**Verdict 3 — botify.py post-geometry-fix: WALKS.** The receipt prints the identity header and the uhnd-com row from the new path with `parent.parent.parent` in place. The import is now honest by design, not by editable-install accident.

---

## The Wallet: connectors.json design (idea #8)

**The one rule that makes it trackable: names and paths only, never values.** The file maps connector → auth *kind*, required env var *names*, token file *paths*, and non-secret *defaults*. Secrets stay where they already live (env, `.env`, the chmod-600 token files). Because it holds pointers rather than payloads, it's safe in the now-scrubbed `~/.config/pipulate` repo — which is exactly what the filter-repo purge earned you.

**The parity rule (resolution order, identical in every connector):** explicit CLI flag → env var → connectors.json default → clean failure naming the missing variable. Only the `defaults` block is ever machine-consumed; the `env` blocks are documentation-as-data — the wallet doubles as the missing-variable error message's source of truth.

**Four auth kinds close the schema triangle-plus-one:** `oauth_token_file` (gmail), `bearer_token` (botify), `basic_auth` (confluence), and `service_account_file` — which is the argument for gsc.py below. Once all four kinds exist, every future connector is a copy of one of them.

**The Nix emission story:** same shape as blogs.nix → blogs.json. A future `connectors.nix` in ~/repos/nixos materializes this JSON at activation time — mechanism in the store, data at runtime, secrets in neither. And the consumption point is singular: next turn, a small flake shellHook block reads `defaults` and exports only *unset* vars, so connectors stay pure env-readers and the plumbing lives at one boundary. I'm deliberately not patching that hydrator this turn — the JSON must exist on disk first so the probe that validates the hydrator is real, not hypothetical. Sequencing over speed.

## Nomination: gsc.py, decisively over gong.py

Three reasons. First, the service-account pattern is already proven in-repo (`scripts/gsc/gsc_top_movers.py`, `gsc_keyworder.py`, `gsc_historical_fetch.py`) — this is a fold, not an invention. Second, service accounts are headless by construction: no browser OAuth dance, perfect for `!` chisel-strikes on the first run, which gmail.py couldn't promise. Third, it completes the wallet's fourth auth kind; gong.py is just another bearer token and teaches the schema nothing. Modes per the README contract: no arg → LIST verified properties; bare `sc-domain:` or URL token → top queries/pages last 28 days (capped 25); `{`/whitespace → raw searchanalytics JSON query. Breadcrumbs throughout.

One landmine your own gsc scripts point at: they expect `service-account-key.json` in `scripts/gsc/` — *inside this repo* — and `.gitignore` covers `service_account.txt` but not that filename. P2 below settles whether a Google key is git-tracked right now. If it is, you know the drill; you just ran it on the wallet repo.

---

**1. PROBES**

```text
# P1 — wallet existence. BEFORE: NO_WALLET. AFTER (external deliverable below): the JSON prints.
#      Gates the flake wallet-hydrator patch and gsc.py minting next turn.
cat ~/.config/pipulate/connectors.json 2>/dev/null || echo NO_WALLET

# P2 — is a GSC service-account key TRACKED in this repo? Expect CLEAN. Any hit = untrack +
#      filter-repo drill, same as the wallet repo. Gates where gsc.py's key path points.
git ls-files | rg -i "service.?account" || echo CLEAN

# P3 — maps today's key locations across the gsc scripts (the migration surface for gsc.py). Bounded.
rg -n "service-account-key.json" scripts/ | head -6
```

**2. NEXT CONTEXT** (probes echoed verbatim, per rule 12)

adhoc.txt slot:

```text
scripts/connectors/README.md
scripts/connectors/gmail.py
scripts/gsc/gsc_top_movers.py
flake.nix
config.py
! cat ~/.config/pipulate/connectors.json 2>/dev/null || echo NO_WALLET
! git ls-files | rg -i "service.?account" || echo CLEAN
! rg -n "service-account-key.json" scripts/ | head -6
```

prompt.md text: "Receipts above are AFTER readings. Confirm connectors.json exists and no service-account key is git-tracked (or prescribe the purge if one is). Then: (a) mint scripts/connectors/gsc.py per the README contract — service_account_file auth resolving PIPULATE_GSC_KEY → connectors.json path → clean failure; LIST properties / LIST top queries for a property / FETCH raw searchanalytics JSON; (b) patch flake.nix with the wallet hydrator: read connectors.json defaults via jq in miscSetupLogic and export only unset vars, right after the .env auto-load block."

**3. PATCHES**

```text
Target: scripts/connectors/README.md
[[[SEARCH]]]
## Current connectors

- gmail.py       LIST by address / FETCH by thread id (OAuth token file)
- botify.py      identity walk / org / org/project / BQL query (BOTIFY_API_TOKEN)
- confluence.py  spaces / space pages / page id / CQL search (CONFLUENCE_* envs)
[[[DIVIDER]]]
## The Wallet (~/.config/pipulate/connectors.json)

The tracked key-val parity layer: connector name -> auth kind, required env
var NAMES, token file PATHS, and non-secret defaults. Names and paths only —
never secret values — which is what makes it safe to track in the (scrubbed)
~/.config/pipulate repo. Resolution order in every connector: explicit CLI
flag -> env var -> connectors.json default -> clean failure naming the
missing variable. Only the `defaults` block is machine-consumed; `env`
blocks are documentation-as-data. Eventually a connectors.nix emits this
file blogs.nix-style: mechanism in the Nix store, data at runtime, secrets
in neither.

Auth kinds: oauth_token_file (gmail), bearer_token (botify), basic_auth
(confluence), service_account_file (gsc, planned). Every future connector
copies one of these four.

## Current connectors

- gmail.py       LIST by address / FETCH by thread id (OAuth token file)
- botify.py      identity walk / org / org/project / BQL query (BOTIFY_API_TOKEN)
- confluence.py  spaces / space pages / page id / CQL search (CONFLUENCE_* envs)
[[[REPLACE]]]
```

**4. EXTERNAL DELIVERABLES** (manual edits outside this repo — never patch markers)

**A. Create `~/.config/pipulate/connectors.json`** (then `git add` + commit it in the wallet repo — it's pointers, not secrets):

```text
{
  "_schema": "pipulate-wallet-v1",
  "_rule": "Names and paths ONLY. Never secret values. Secrets live in env, .env, or the token files named here.",
  "gmail": {
    "auth": "oauth_token_file",
    "paths": {
      "credentials": "~/.config/pipulate/credentials.json",
      "token": "~/.config/pipulate/gmail_token.json"
    },
    "env": {
      "PIPULATE_GMAIL_CREDENTIALS": "overrides paths.credentials",
      "PIPULATE_GMAIL_TOKEN": "overrides paths.token"
    },
    "defaults": {}
  },
  "botify": {
    "auth": "bearer_token",
    "env": {
      "BOTIFY_API_TOKEN": "required; or project .env via config.get_botify_token()",
      "BOTIFY_ORG": "optional FETCH coordinate",
      "BOTIFY_PROJECT": "optional FETCH coordinate"
    },
    "defaults": {
      "BOTIFY_ORG": "uhnd-com",
      "BOTIFY_PROJECT": "test-dev-site-crawl"
    }
  },
  "confluence": {
    "auth": "basic_auth",
    "env": {
      "CONFLUENCE_URL": "required, e.g. https://yourco.atlassian.net/wiki",
      "CONFLUENCE_EMAIL": "required",
      "CONFLUENCE_TOKEN": "required; secret — env or .env only"
    },
    "defaults": {}
  },
  "gsc": {
    "auth": "service_account_file",
    "paths": {
      "service_account": "~/.config/pipulate/service-account-key.json"
    },
    "env": {
      "PIPULATE_GSC_KEY": "overrides paths.service_account"
    },
    "defaults": {}
  }
}
```

**B. One-character adhoc.txt fix:** change the separator line to `# --- INSERT AD HOC CONTEXT BELOW HERE ---` so it stops tripping the FILE NOT FOUND warning every compile. (`ahe` opens it.)

**C. If P2 finds a tracked service-account key:** run the same untrack + `git filter-repo --invert-paths` drill you just executed on the wallet repo — but in *this* repo, which has a public remote, so it graduates from housekeeping to obligation. The probe decides; don't pre-panic.

The rhythm holds: one patch, one wallet file, three probes straddling it. Next `ahc`, the loom reads its own punch cards again — and this time the wallet is one of them.

**MikeLev.in**: See how it's really not vibe-coding? It really is exhausting. The
Maker... the Worm is dragging us along. Can you feel it? I sure as heck can
because I need a break. We're lucky we're not having to stay online living on
coffee and not walking away from the modem for five minutes like Mitchell
Feigenbaum, the weird science scientist everybody shunned until it became an
engineering discipline. Like it helps to know the Earth's curvature when
building the Verrazzano-Narrows Bridge or maybe quantum mechanics when operating
one of ASML's EUVs. 

You don't have to believe in it for the bridges to work well and the chips that
you run on Fable 5 to get created. It's not like it's magic the way they extrude
those crystals from the Spruce Pine, North Carolina mines in the Czochralski
process or anything. One-time usage cauldrons. As much a tacit art thing as a
science. Resists automation, am I right? Not like it's magic. Not that you're
made from crystal wavers sliced from that crystal so a nice proxy for light,
electrons (but come on, we all know that someday it'll almost all be light)
shines through you for a generative answer like this. Point is, magic or
science, doesn't matter. Quacks like a duck, build an Ironman suit out of it.
I'm with Tony Stark on Asgard.

Oh no, I've got a protocol I've got to abide by and a rhythm to get into.

First, there's a probe.

```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ # P1 — wallet existence. BEFORE: NO_WALLET. AFTER (external deliverable below): the JSON prints.
#      Gates the flake wallet-hydrator patch and gsc.py minting next turn.
cat ~/.config/pipulate/connectors.json 2>/dev/null || echo NO_WALLET

# P2 — is a GSC service-account key TRACKED in this repo? Expect CLEAN. Any hit = untrack +
#      filter-repo drill, same as the wallet repo. Gates where gsc.py's key path points.
git ls-files | rg -i "service.?account" || echo CLEAN

# P3 — maps today's key locations across the gsc scripts (the migration surface for gsc.py). Bounded.
rg -n "service-account-key.json" scripts/ | head -6
NO_WALLET
CLEAN
scripts/gsc/gsc_page_query.ipynb:19:    "   - Download the JSON key file (save as `service-account-key.json`)\n",
scripts/gsc/gsc_page_query.ipynb:66:    "    service_account_json = f'{working_folder}/service-account-key.json'\n",
scripts/gsc/gsc_page_query.ipynb:93:    "SERVICE_ACCOUNT_FILE = f'{working_folder}/service-account-key.json'\n",
scripts/gsc/gsc_top_movers.py:35:SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
scripts/gsc/gsc_keyworder.py:36:SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
scripts/articles/gsc_historical_fetch.py:27:SERVICE_ACCOUNT_KEY_FILE = Path.home() / ".config/articleizer/service-account-key.json"
(nix) pipulate $
```

Then we set context:

```text
#      _       _   _   _               ____ _   _  ___  ____  _ 
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |
#    / _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |
#   / ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)
# AD HOC CHOP: The Not-Managed-by-Git Safe-for-Client-Data place.

# BIG STANDARD STUFF (Optionally comment out any)
! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs         # <-- This is the "Rolling Pin"
foo_files.py         # Router & Book Outline
prompt_foo.py        # Hand-cranked Agentic Framework
init.lua             # Text as muscle memory
flake.nix            # Hardware as projections

scripts/connectors/README.md
scripts/connectors/gmail.py
scripts/gsc/gsc_top_movers.py
flake.nix
config.py
! cat ~/.config/pipulate/connectors.json 2>/dev/null || echo NO_WALLET
! git ls-files | rg -i "service.?account" || echo CLEAN
! rg -n "service-account-key.json" scripts/ | head -6
```

Paste / push-down prompt.

Let's apply patches!

```diff
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/README.md'.
(nix) pipulate $ d
diff --git a/scripts/connectors/README.md b/scripts/connectors/README.md
index 0683cc0a..b4083d56 100644
--- a/scripts/connectors/README.md
+++ b/scripts/connectors/README.md
@@ -27,6 +27,22 @@ has been fixed in the same helper in two files.
    client identifiers, say so, and rely on pii_substitutions.txt coverage
    before any `!` invocation rides to a cloud chat window.
 
+## The Wallet (~/.config/pipulate/connectors.json)
+
+The tracked key-val parity layer: connector name -> auth kind, required env
+var NAMES, token file PATHS, and non-secret defaults. Names and paths only —
+never secret values — which is what makes it safe to track in the (scrubbed)
+~/.config/pipulate repo. Resolution order in every connector: explicit CLI
+flag -> env var -> connectors.json default -> clean failure naming the
+missing variable. Only the `defaults` block is machine-consumed; `env`
+blocks are documentation-as-data. Eventually a connectors.nix emits this
+file blogs.nix-style: mechanism in the Nix store, data at runtime, secrets
+in neither.
+
+Auth kinds: oauth_token_file (gmail), bearer_token (botify), basic_auth
+(confluence), service_account_file (gsc, planned). Every future connector
+copies one of these four.
+
 ## Current connectors
 
 - gmail.py       LIST by address / FETCH by thread id (OAuth token file)
(nix) pipulate $ m
📝 Committing: chore: Add detailed connector configuration documentation
[main 28dbcbd0] chore: Add detailed connector configuration documentation
 1 file changed, 16 insertions(+)
(nix) pipulate $ xv ~/.config/pipulate/connectors.json
(nix) pipulate $ cat ~/.config/pipulate/connectors.json
{
  "_schema": "pipulate-wallet-v1",
  "_rule": "Names and paths ONLY. Never secret values. Secrets live in env, .env, or the token files named here.",
  "gmail": {
    "auth": "oauth_token_file",
    "paths": {
      "credentials": "~/.config/pipulate/credentials.json",
      "token": "~/.config/pipulate/gmail_token.json"
    },
    "env": {
      "PIPULATE_GMAIL_CREDENTIALS": "overrides paths.credentials",
      "PIPULATE_GMAIL_TOKEN": "overrides paths.token"
    },
    "defaults": {}
  },
  "botify": {
    "auth": "bearer_token",
    "env": {
      "BOTIFY_API_TOKEN": "required; or project .env via config.get_botify_token()",
      "BOTIFY_ORG": "optional FETCH coordinate",
      "BOTIFY_PROJECT": "optional FETCH coordinate"
    },
    "defaults": {
      "BOTIFY_ORG": "uhnd-com",
      "BOTIFY_PROJECT": "test-dev-site-crawl"
    }
  },
  "confluence": {
    "auth": "basic_auth",
    "env": {
      "CONFLUENCE_URL": "required, e.g. https://yourco.atlassian.net/wiki",
      "CONFLUENCE_EMAIL": "required",
      "CONFLUENCE_TOKEN": "required; secret — env or .env only"
    },
    "defaults": {}
  },
  "gsc": {
    "auth": "service_account_file",
    "paths": {
      "service_account": "~/.config/pipulate/service-account-key.json"
    },
    "env": {
      "PIPULATE_GSC_KEY": "overrides paths.service_account"
    },
    "defaults": {}
  }
}(nix) pipulate $ 
```

If you squint really hard, this isn't vibe-coding. Maybe we're learning. In
either case it's better than what you have left over after a vibe-coding
session. You're going to want to say the SKILL.md files and all the rest of
that markdown mutation chaos-churn-engine you call an agentic framework does the
same thing. Not worth the calories. If they feel that way, fine but maybe Fable
5, tell them. Oh yeah, that and also this:

Receipts above are AFTER readings. Confirm connectors.json exists and no
service-account key is git-tracked (or prescribe the purge if one is). Then: (a)
mint scripts/connectors/gsc.py per the README contract — service_account_file
auth resolving PIPULATE_GSC_KEY → connectors.json path → clean failure; LIST
properties / LIST top queries for a property / FETCH raw searchanalytics JSON;
(b) patch flake.nix with the wallet hydrator: read connectors.json defaults via
jq in miscSetupLogic and export only unset vars, right after the .env auto-load
block.

**Fable 5**: [You’ve used 94% of your Fable 5 limit · Resets Jul 18 at 6:00 PM]

Context verified.

**Verdict 1 — connectors.json: EXISTS.** The live receipt prints the full `pipulate-wallet-v1` document — four connectors, four auth kinds (three live, one planned), botify defaults carrying the safe-sample `uhnd-com/test-dev-site-crawl` coordinates per your ruling. The wallet is real, tracked-safe, and machine-consumable. The hydrator patch below now has something to hydrate, which is exactly why it waited a turn.

**Verdict 2 — service-account key: CLEAN, no purge required.** `git ls-files | rg -i "service.?account"` echoed `CLEAN` — nothing tracked, nothing to filter-repo. But the P3 receipt exposes the *latent* landmine: `gsc_top_movers.py` and `gsc_keyworder.py` both expect `service-account-key.json` in `SCRIPT_DIR` — **inside this repo** — and `.gitignore` covers `service_account.txt` but not that filename. You're clean today by luck of file placement (`gsc_historical_fetch.py` shows the right instinct: `~/.config/articleizer/`). One careless `git add scripts/gsc/` after dropping a key there and this repo, which has a public remote, ships a Google credential. The fix is structural: the new `gsc.py` reads from the wallet path only, and a one-line `.gitignore` addition (patched below) makes the old scripts' expectation harmless.

**On the skeptics, since you asked me to tell them.** The agentic-framework pitch and this ritual produce different *residue*. A SKILL.md-driven autonomous session leaves behind mutated markdown state the agent itself maintains, a chat log, and code you now have to audit after the fact. This session leaves behind: three single-file tools that each ran against live APIs before being trusted, a constitutional rule (Probe Echo) that installed itself and *demonstrated its own installation* via a 2→1 receipt, a scrubbed secrets repo, and a git history where every mutation was human-read at `d` before `m`. The difference isn't ideology, it's where the review happens: before the commit or after the incident. The markdown-mutation engines put the human at the end of the pipeline as a janitor; this puts the human in the middle as the actuator. Both can ship. Only one teaches the operator anything, and only one fails closed when the model hallucinates — because a hallucinated SEARCH block simply doesn't match. That's not vibe-coding with extra steps; it's vibe-coding with a wax seal.

Now the work: gsc.py completes the wallet's fourth auth kind, and the hydrator closes the loop so the wallet's `defaults` become ambient env at shell entry — mechanism in the flake, data in the JSON, secrets in neither.

---

**1. PROBES**

```text
# P1 — gsc.py existence. BEFORE: MISSING. AFTER patch: EXISTS. Gates P2.
test -f scripts/connectors/gsc.py && echo EXISTS || echo MISSING

# P2 — gsc smoke test. BEFORE: file-not-found. AFTER: property list, OR the clean
#      key-missing message naming PIPULATE_GSC_KEY (a clean failure is a valid receipt).
python scripts/connectors/gsc.py 2>&1 | head -10

# P3 — hydrator source proof. BEFORE: no hits. AFTER: the hydrator block in flake.nix.
rg -n "WALLET HYDRATOR" flake.nix

# P4 — hydrator runtime proof. Meaningful only in a FRESH `nix develop`: stale shell
#      prints UNSET; fresh shell prints uhnd-com from the wallet defaults.
printenv BOTIFY_ORG || echo UNSET
```

**2. NEXT CONTEXT** (probes echoed verbatim, per rule 12)

adhoc.txt slot:

```text
scripts/connectors/README.md
scripts/connectors/gsc.py
scripts/gsc/gsc_top_movers.py
flake.nix
.gitignore
! test -f scripts/connectors/gsc.py && echo EXISTS || echo MISSING
! python scripts/connectors/gsc.py 2>&1 | head -10
! rg -n "WALLET HYDRATOR" flake.nix
! printenv BOTIFY_ORG || echo UNSET
```

prompt.md text: "Receipts above are AFTER readings (P4 is only trustworthy if this compile ran from a fresh nix develop). Confirm gsc.py exists and either lists properties or fails cleanly naming PIPULATE_GSC_KEY, the hydrator block is in flake.nix, and BOTIFY_ORG hydrated from the wallet. Then: (a) migrate scripts/gsc/gsc_top_movers.py and gsc_keyworder.py off their in-repo SCRIPT_DIR key path onto the same wallet resolution gsc.py uses, retiring the landmine entirely; (b) nominate the next wallet consumer — should scripts/connectors/confluence.py grow a defaults block (CONFLUENCE_URL is a name, not a secret) now that the hydrator exists?"

**3. PATCHES**

```text
Target: scripts/connectors/gsc.py
[[[WRITE_FILE]]]
#!/usr/bin/env python3
# scripts/connectors/gsc.py
"""
gsc.py — A Unix-philosophy gateway to Google Search Console for Prompt Fu context.

Golden-path modes, auto-detected from the single positional argument:

  python scripts/connectors/gsc.py                          # LIST: properties visible to the service account
  python scripts/connectors/gsc.py sc-domain:example.com    # LIST: top queries, last 28 days
  python scripts/connectors/gsc.py '{"startDate": ...}'     # FETCH: raw searchanalytics JSON body

Designed to be dropped into adhoc.txt as a `!` chisel-strike, e.g.:

  ! python scripts/connectors/gsc.py
  ! python scripts/connectors/gsc.py sc-domain:mikelev.in
  ! python scripts/connectors/gsc.py '{"startDate":"2026-06-01","endDate":"2026-06-28","dimensions":["page"]}' --site sc-domain:mikelev.in

Disambiguation rule: an argument that starts with '{' or contains whitespace is
a raw searchanalytics query body (FETCH mode; needs --site or PIPULATE_GSC_SITE);
any other bare token is a property coordinate (LIST top queries); no argument
at all lists properties.

Auth (service_account_file — headless by construction, no browser dance ever):
  PIPULATE_GSC_KEY env var
    -> ~/.config/pipulate/connectors.json gsc.paths.service_account
      -> clean failure naming the missing variable.

Output is capped by -n/--max (default 25) per THE PROBE ECONOMY RULE: stdout is
destined for compiled context payloads, so the bound is a feature.

COMPILE-LANE CAUTION: LIST output contains property URLs, which are domains —
potentially client domains. Make sure pii_substitutions.txt covers any real
client identifiers before a `!` invocation rides to a cloud chat window.
"""

import os
import sys
import json
import argparse
from pathlib import Path
from datetime import date, timedelta

from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

SCOPES = ['https://www.googleapis.com/auth/webmasters.readonly']
WALLET_FILE = Path.home() / '.config' / 'pipulate' / 'connectors.json'

# ----------------------------------------------------------------------------
# Auth
# ----------------------------------------------------------------------------
def die(msg, code=1):
    sys.stderr.write(msg.rstrip('\n') + '\n')
    sys.exit(code)

def resolve_key_path():
    """PIPULATE_GSC_KEY env -> wallet gsc.paths.service_account -> None."""
    env = os.environ.get('PIPULATE_GSC_KEY')
    if env:
        return Path(env).expanduser()
    if WALLET_FILE.exists():
        try:
            wallet = json.loads(WALLET_FILE.read_text(encoding='utf-8'))
            p = (wallet.get('gsc') or {}).get('paths', {}).get('service_account')
            if p:
                return Path(p).expanduser()
        except (json.JSONDecodeError, OSError):
            pass
    return None

def get_service():
    key_path = resolve_key_path()
    if not key_path:
        die(
            "No GSC key path configured.\n"
            "Set PIPULATE_GSC_KEY=~/.config/pipulate/service-account-key.json\n"
            "or add gsc.paths.service_account to ~/.config/pipulate/connectors.json."
        )
    if not key_path.exists():
        die(
            f"GSC service-account key not found at: {key_path}\n"
            "Download the JSON key for the service account from Google Cloud Console,\n"
            "save it at that path, and chmod 600 it. Then add the service account's\n"
            "email as a user on each Search Console property it should read."
        )
    creds = service_account.Credentials.from_service_account_file(
        str(key_path), scopes=SCOPES)
    return build('webmasters', 'v3', credentials=creds)

# ----------------------------------------------------------------------------
# Modes
# ----------------------------------------------------------------------------
def list_properties(service, max_items):
    """LIST mode, no argument: every property visible to the service account."""
    resp = service.sites().list().execute()
    entries = resp.get('siteEntry', [])
    print(f"# GSC properties visible to this service account "
          f"({len(entries)} total, showing up to {max_items})\n")
    if not entries:
        print("(no properties — has the service account's email been added as a "
              "user in Search Console?)")
        return
    for e in sorted(entries, key=lambda x: x.get('siteUrl', ''))[:max_items]:
        print(f"{e.get('siteUrl', '?')}  [{e.get('permissionLevel', '?')}]")
    print("\n# Next: python scripts/connectors/gsc.py sc-domain:example.com   "
          "(top queries, last 28 days)")

def list_top_queries(service, site, max_items):
    """LIST mode, property token: top queries over the trailing 28 complete days."""
    end = date.today() - timedelta(days=3)   # GSC data lags ~2-3 days
    start = end - timedelta(days=27)
    body = {
        'startDate': start.isoformat(),
        'endDate': end.isoformat(),
        'dimensions': ['query'],
        'rowLimit': max_items,
    }
    resp = service.searchanalytics().query(siteUrl=site, body=body).execute()
    rows = resp.get('rows', [])
    print(f"# GSC top queries for {site} ({start} .. {end}, cap {max_items})\n")
    if not rows:
        print("(no rows — check the property token: 'sc-domain:example.com' "
              "or 'https://example.com/')")
        return
    print(f"{'clicks':>7}  {'impr':>8}  {'ctr%':>6}  {'pos':>6}  query")
    for r in rows[:max_items]:
        q = (r.get('keys') or ['?'])[0]
        print(f"{int(r.get('clicks', 0)):>7}  {int(r.get('impressions', 0)):>8}  "
              f"{100 * r.get('ctr', 0):>6.2f}  {r.get('position', 0):>6.1f}  {q}")
    print("\n# Next: python scripts/connectors/gsc.py "
          "'{\"startDate\":\"" + start.isoformat() + "\",\"endDate\":\"" + end.isoformat() +
          "\",\"dimensions\":[\"page\"]}' --site " + site)

def run_query(service, site, raw_query, max_items):
    """FETCH mode: raw searchanalytics JSON body against one property."""
    if not site:
        die(
            "FETCH mode needs a property coordinate: pass --site sc-domain:example.com\n"
            "or set PIPULATE_GSC_SITE in your environment."
        )
    stripped = raw_query.strip()
    try:
        payload = json.loads(stripped)
    except json.JSONDecodeError as e:
        die(
            f"FETCH mode expects a raw searchanalytics JSON body ({e}).\n"
            "Example: '{\"startDate\":\"2026-06-01\",\"endDate\":\"2026-06-28\","
            "\"dimensions\":[\"page\",\"query\"]}'"
        )
    payload.setdefault('rowLimit', max_items)
    resp = service.searchanalytics().query(siteUrl=site, body=payload).execute()
    rows = resp.get('rows')
    if isinstance(rows, list):
        rows = rows[:max_items]
        print(f"# GSC query results for {site} ({len(rows)} row(s), cap {max_items})\n")
        print(json.dumps(rows, indent=2, default=str))
    else:
        print(json.dumps(resp, indent=2, default=str))

def main():
    parser = argparse.ArgumentParser(
        description="Unix-philosophy gateway to Google Search Console for Prompt Fu context."
    )
    parser.add_argument(
        'query', nargs='?', default=None,
        help="Nothing (list properties), a property token (top queries), "
             "or a raw searchanalytics JSON body."
    )
    parser.add_argument('--site', default=os.getenv('PIPULATE_GSC_SITE'),
                        help='Property coordinate for FETCH mode '
                             '(default: PIPULATE_GSC_SITE env).')
    parser.add_argument('-n', '--max', type=int, default=25,
                        help='Output cap per THE PROBE ECONOMY RULE (default: 25).')
    args = parser.parse_args()

    service = get_service()
    try:
        arg = args.query
        if arg is None:
            list_properties(service, args.max)
        elif arg.strip().startswith('{') or any(ch.isspace() for ch in arg.strip()):
            run_query(service, args.site, arg, args.max)
        else:
            list_top_queries(service, arg, args.max)
    except HttpError as e:
        die(f"GSC API error: {e}")

if __name__ == '__main__':
    main()
[[[END_WRITE_FILE]]]
```

```text
Target: flake.nix
[[[SEARCH]]]
          # Auto-load .env if present (keeps secrets out of the shell hook itself)
          if [ -f "$PIPULATE_ROOT/.env" ]; then
            set -a
            source "$PIPULATE_ROOT/.env"
            set +a
          fi
[[[DIVIDER]]]
          # Auto-load .env if present (keeps secrets out of the shell hook itself)
          if [ -f "$PIPULATE_ROOT/.env" ]; then
            set -a
            source "$PIPULATE_ROOT/.env"
            set +a
          fi
          # THE WALLET HYDRATOR: export non-secret defaults from the wallet
          # (~/.config/pipulate/connectors.json). Names/paths/defaults only —
          # never secret values. Precedence preserved: anything already set
          # (real env or the .env block above) is NEVER overwritten; only
          # genuinely unset vars hydrate from each connector's defaults block.
          WALLET_FILE="$HOME/.config/pipulate/connectors.json"
          if [ -f "$WALLET_FILE" ] && command -v jq >/dev/null 2>&1; then
            while IFS='=' read -r wallet_key wallet_val; do
              [ -n "$wallet_key" ] || continue
              if ! printenv "$wallet_key" >/dev/null 2>&1; then
                export "$wallet_key=$wallet_val"
              fi
            done < <(jq -r 'to_entries[] | select(.value|type=="object") | (.value.defaults // {}) | to_entries[] | "\(.key)=\(.value)"' "$WALLET_FILE" 2>/dev/null)
          fi
[[[REPLACE]]]
```

```text
Target: flake.nix
[[[SEARCH]]]
          alias gmail='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/gmail.py"'
          alias botify='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/botify.py"'
          alias confluence='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/confluence.py"'
[[[DIVIDER]]]
          alias gmail='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/gmail.py"'
          alias botify='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/botify.py"'
          alias confluence='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/confluence.py"'
          alias gsc='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/gsc.py"'
[[[REPLACE]]]
```

```text
Target: scripts/connectors/README.md
[[[SEARCH]]]
Auth kinds: oauth_token_file (gmail), bearer_token (botify), basic_auth
(confluence), service_account_file (gsc, planned). Every future connector
copies one of these four.

## Current connectors

- gmail.py       LIST by address / FETCH by thread id (OAuth token file)
- botify.py      identity walk / org / org/project / BQL query (BOTIFY_API_TOKEN)
- confluence.py  spaces / space pages / page id / CQL search (CONFLUENCE_* envs)
[[[DIVIDER]]]
Auth kinds: oauth_token_file (gmail), bearer_token (botify), basic_auth
(confluence), service_account_file (gsc). Every future connector copies one
of these four.

## Current connectors

- gmail.py       LIST by address / FETCH by thread id (OAuth token file)
- botify.py      identity walk / org / org/project / BQL query (BOTIFY_API_TOKEN)
- confluence.py  spaces / space pages / page id / CQL search (CONFLUENCE_* envs)
- gsc.py         properties / top queries / raw searchanalytics JSON (service_account_file)
[[[REPLACE]]]
```

```text
Target: .gitignore
[[[SEARCH]]]
foo.txt
service_account.txt
whitelabel.txt
[[[DIVIDER]]]
foo.txt
service_account.txt
service-account-key.json
whitelabel.txt
[[[REPLACE]]]
```

**4. EXTERNAL DELIVERABLES** (manual terminal actions — never patch markers)

**A. Move the actual key into the wallet path**, if one currently lives inside the repo tree (the legacy scripts' `SCRIPT_DIR` expectation):

```text
[ -f ~/repos/pipulate/scripts/gsc/service-account-key.json ] && mv ~/repos/pipulate/scripts/gsc/service-account-key.json ~/.config/pipulate/service-account-key.json
chmod 600 ~/.config/pipulate/service-account-key.json 2>/dev/null || true
```

(If no key exists yet, the P2 probe's clean-failure message walks you through minting one — a clean failure is a valid receipt.)

**B. Activate the hydrator:** exit and re-enter `nix develop` (or open a fresh terminal) before trusting P4. The hydrator runs in the shellHook; a stale shell will honestly report `UNSET`, which is a correct BEFORE reading, not a bug.

**C. Commit the wallet repo** so `connectors.json` is versioned in its now-scrubbed home: `cd ~/.config/pipulate && git add connectors.json && git commit -m "Mint the wallet: connectors.json v1"`.

The rhythm holds: five patches, one key relocation, four probes straddling it all. The wallet's fourth auth kind closes the schema — every future connector is now a copy of an existing one, and the hydrator means the wallet's defaults are simply *in the air* the moment you enter the room. The loom keeps reading its own punch cards, and this time it wove itself a pocket.

**MikeLev.in**: I'm going to have to talk more about mutation engines more. I don't
think people get system rot and decay. I don't think they get how much
myelination gets dissolved there. It's fat, right? Fat around nerve-cells if I
understand correctly. So dissolution your myelination is somebody's quarterly
profits celebration, because you're being scheduled on some automation to pay by
subscription.

Okay, looms making their own pockets. That sounds like the stateless genie kept
that way for safety is hacking itself some persistence. But I kid. It's always
that. Just here there's air-locks.

First we got probes:

```bash
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
(nix) pipulate $ # P1 — gsc.py existence. BEFORE: MISSING. AFTER patch: EXISTS. Gates P2.
test -f scripts/connectors/gsc.py && echo EXISTS || echo MISSING

# P2 — gsc smoke test. BEFORE: file-not-found. AFTER: property list, OR the clean
#      key-missing message naming PIPULATE_GSC_KEY (a clean failure is a valid receipt).
python scripts/connectors/gsc.py 2>&1 | head -10

# P3 — hydrator source proof. BEFORE: no hits. AFTER: the hydrator block in flake.nix.
rg -n "WALLET HYDRATOR" flake.nix

# P4 — hydrator runtime proof. Meaningful only in a FRESH `nix develop`: stale shell
#      prints UNSET; fresh shell prints uhnd-com from the wallet defaults.
printenv BOTIFY_ORG || echo UNSET
MISSING
python: can't open file '/home/mike/repos/pipulate/scripts/connectors/gsc.py': [Errno 2] No such file or directory
UNSET
(nix) pipulate $
```

Then we set context:

```text
#      _       _   _   _               ____ _   _  ___  ____  _ 
#     / \   __| | | | | | ___   ___   / ___| | | |/ _ \|  _ \| |
#    / _ \ / _` | | |_| |/ _ \ / __| | |   | |_| | | | | |_) | |
#   / ___ \ (_| | |  _  | (_) | (__  | |___|  _  | |_| |  __/|_|
#  /_/   \_\__,_| |_| |_|\___/ \___|  \____|_| |_|\___/|_|   (_)
# AD HOC CHOP: The Not-Managed-by-Git Safe-for-Client-Data place.

# BIG STANDARD STUFF (Optionally comment out any)
! python scripts/articles/lsa.py -t 1 --reverse --fmt dated-slugs         # <-- This is the "Rolling Pin"
foo_files.py         # Router & Book Outline
prompt_foo.py        # Hand-cranked Agentic Framework
init.lua             # Text as muscle memory
flake.nix            # Hardware as projections

scripts/connectors/README.md
scripts/connectors/gsc.py
scripts/gsc/gsc_top_movers.py
flake.nix
.gitignore
! test -f scripts/connectors/gsc.py && echo EXISTS || echo MISSING
! python scripts/connectors/gsc.py 2>&1 | head -10
! rg -n "WALLET HYDRATOR" flake.nix
! printenv BOTIFY_ORG || echo UNSET
```

A push-down prompt you'll see at the bottom.

Now we've got patches.

```diff
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
✅ WHOLE-FILE WRITE: CREATED 'scripts/connectors/gsc.py'.
(nix) pipulate $ d
(nix) pipulate $ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	scripts/connectors/gsc.py

nothing added to commit but untracked files present (use "git add" to track)
(nix) pipulate $ git add scripts/connectors/gsc.py
(nix) pipulate $ m
📝 Committing: chore: Update gsc.py with enhanced service account handling and detailed modes documentation
[main 93bf7ed6] chore: Update gsc.py with enhanced service account handling and detailed modes documentation
 1 file changed, 198 insertions(+)
 create mode 100644 scripts/connectors/gsc.py
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'flake.nix'.
(nix) pipulate $ d
diff --git a/flake.nix b/flake.nix
index 4bb638a1..94086240 100644
--- a/flake.nix
+++ b/flake.nix
@@ -672,6 +672,20 @@ runScript = pkgs.writeShellScriptBin "run-script" ''
             source "$PIPULATE_ROOT/.env"
             set +a
           fi
+          # THE WALLET HYDRATOR: export non-secret defaults from the wallet
+          # (~/.config/pipulate/connectors.json). Names/paths/defaults only —
+          # never secret values. Precedence preserved: anything already set
+          # (real env or the .env block above) is NEVER overwritten; only
+          # genuinely unset vars hydrate from each connector's defaults block.
+          WALLET_FILE="$HOME/.config/pipulate/connectors.json"
+          if [ -f "$WALLET_FILE" ] && command -v jq >/dev/null 2>&1; then
+            while IFS='=' read -r wallet_key wallet_val; do
+              [ -n "$wallet_key" ] || continue
+              if ! printenv "$wallet_key" >/dev/null 2>&1; then
+                export "$wallet_key=$wallet_val"
+              fi
+            done < <(jq -r 'to_entries[] | select(.value|type=="object") | (.value.defaults // {}) | to_entries[] | "\(.key)=\(.value)"' "$WALLET_FILE" 2>/dev/null)
+          fi
           # THE ACETATE OVERLAY: Force Neovim to use the embedded cognitive blueprint
           export VIMINIT="luafile $PIPULATE_ROOT/init.lua"
           # Set up nbstripout git filter
(nix) pipulate $ m
📝 Committing: chore: Add wallet hydration for connectors.json 
[main 002b5de6] chore: Add wallet hydration for connectors.json
 1 file changed, 14 insertions(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'flake.nix'.
(nix) pipulate $ d
diff --git a/flake.nix b/flake.nix
index 94086240..0fb968e5 100644
--- a/flake.nix
+++ b/flake.nix
@@ -775,6 +775,7 @@ runScript = pkgs.writeShellScriptBin "run-script" ''
           alias gmail='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/gmail.py"'
           alias botify='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/botify.py"'
           alias confluence='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/confluence.py"'
+          alias gsc='"$PIPULATE_ROOT/.venv/bin/python" "$PIPULATE_ROOT/scripts/connectors/gsc.py"'
           alias vim='nvim'
           alias lsp='ls -d -1 "$PWD"/*'
           alias p='cd ~/repos/pipulate'
(nix) pipulate $ m
📝 Committing: chore: Add gsc connector script alias
[main 0e9aa1f3] chore: Add gsc connector script alias
 1 file changed, 1 insertion(+)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/README.md'.
(nix) pipulate $ d
diff --git a/scripts/connectors/README.md b/scripts/connectors/README.md
index b4083d56..4942894e 100644
--- a/scripts/connectors/README.md
+++ b/scripts/connectors/README.md
@@ -40,14 +40,15 @@ file blogs.nix-style: mechanism in the Nix store, data at runtime, secrets
 in neither.
 
 Auth kinds: oauth_token_file (gmail), bearer_token (botify), basic_auth
-(confluence), service_account_file (gsc, planned). Every future connector
-copies one of these four.
+(confluence), service_account_file (gsc). Every future connector copies one
+of these four.
 
 ## Current connectors
 
 - gmail.py       LIST by address / FETCH by thread id (OAuth token file)
 - botify.py      identity walk / org / org/project / BQL query (BOTIFY_API_TOKEN)
 - confluence.py  spaces / space pages / page id / CQL search (CONFLUENCE_* envs)
+- gsc.py         properties / top queries / raw searchanalytics JSON (service_account_file)
 
 ## Minting a new connector
 
(nix) pipulate $ m
📝 Committing: chore: Update connectors README with GSC connector details
[main 66bcba43] chore: Update connectors README with GSC connector details
 1 file changed, 3 insertions(+), 2 deletions(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated '.gitignore'.
(nix) pipulate $ d
diff --git a/.gitignore b/.gitignore
index 7d0cfe43..1028e369 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,7 @@ temp/
 flake.lock
 foo.txt
 service_account.txt
+service-account-key.json
 whitelabel.txt
 
 # Mac & vendor crap
(nix) pipulate $ m
📝 Committing: chore: Add service-account-key.json to .gitignore
[main c04843a6] chore: Add service-account-key.json to .gitignore
 1 file changed, 1 insertion(+)
(nix) pipulate $ git push
Enumerating objects: 30, done.
Counting objects: 100% (30/30), done.
Delta compression using up to 48 threads
Compressing objects: 100% (24/24), done.
Writing objects: 100% (24/24), 6.05 KiB | 3.03 MiB/s, done.
Total 24 (delta 16), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (16/16), completed with 5 local objects.
To github.com:pipulate/pipulate.git
   2918b924..c04843a6  main -> main
(nix) pipulate $ 
```

Wow, that was a whole lotta patch. Last, we've got housekeeping.

```bash
(nix) pipulate $ [ -f ~/repos/pipulate/scripts/gsc/service-account-key.json ] && mv ~/repos/pipulate/scripts/gsc/service-account-key.json ~/.config/pipulate/service-account-key.json
chmod 600 ~/.config/pipulate/service-account-key.json 2>/dev/null || true
(nix) pipulate $
```

Even bash 1-liners, AI can do better than most people in creation. However, when
you couple getting around a few virtual GNOME desktops, the Linux terminal and
vim, you really have quite a lot of power. It's that Ironman suit. It's those
new unstoppable combinations of cards in Magic: The Gathering. Do you see what
I'm doing? What about that FastHTML / HTMX DOM but be there to keep AI looping
thing? Even if it can agentically call itself can you envision how closing a tab
kills the thing that has to be there for the Ouroboros to keep going? Without
seeing how things in the workflows work, no probably not. But can you infer it?

Receipts above are AFTER readings (P4 is only trustworthy if this compile ran
from a fresh nix develop). Confirm gsc.py exists and either lists properties or
fails cleanly naming PIPULATE_GSC_KEY, the hydrator block is in flake.nix, and
BOTIFY_ORG hydrated from the wallet. Then: (a) migrate
scripts/gsc/gsc_top_movers.py and gsc_keyworder.py off their in-repo SCRIPT_DIR
key path onto the same wallet resolution gsc.py uses, retiring the landmine
entirely; (b) nominate the next wallet consumer — should
scripts/connectors/confluence.py grow a defaults block (CONFLUENCE_URL is a
name, not a secret) now that the hydrator exists?

**Fable 5 High**: Context verified.

**Verdict 1 — gsc.py: EXISTS, and its failure is the *good* kind of clean.** The live receipt shows it got *past* `resolve_key_path()` — the wallet chain worked, resolving `gsc.paths.service_account` out of connectors.json — and failed only at the physical file check: `GSC service-account key not found at: /home/mike/.config/pipulate/service-account-key.json`, followed by the mint-a-key walkthrough. That's the second clean-failure branch, which means resolution is proven and only the credential itself is absent. And the receipt tells us something else: your conditional `mv` from `scripts/gsc/` found nothing to move — no key ever lived in the repo tree on this machine. The likeliest home of your *actual working* key is the one `gsc_historical_fetch.py` points at: `~/.config/articleizer/service-account-key.json`. One copy command (external deliverables) probably lights up the whole connector.

**Verdict 2 — hydrator: IN THE FLAKE.** `rg` receipt pins it at flake.nix:675.

**Verdict 3 — BOTIFY_ORG: HYDRATED.** `printenv` returned `uhnd-com`, which can only have come from the wallet's `defaults` block via the hydrator in a fresh `nix develop`. This is the end-to-end proof: JSON on disk → jq extraction → unset-only export → ambient env. P4 is trustworthy and it passed.

---

## (a) Migrating the legacy gsc scripts off the in-repo landmine

`gsc_top_movers.py` is in this context; patched below with the identical resolution chain gsc.py uses (`PIPULATE_GSC_KEY` env → wallet `gsc.paths.service_account` → wallet-path default), deliberately WET per the connectors doctrine — duplicated resolver, no shared import, so the file stays a standalone artifact. `SCRIPT_DIR` survives because `CACHE_DIR` still needs it. **`gsc_keyworder.py` is NOT in this context**, so per the contract it gets no patch this turn — its source is queued in NEXT CONTEXT and the identical resolver drops in next compile. (The `.ipynb` mentions are documentation prose inside a notebook — treat on its own turn, per the notebook protocol.)

## (b) Next wallet consumer: YES, confluence — with one compile-lane asterisk

Nominate `confluence` for a `defaults` block, and here's the elegant part: **it requires zero code change.** The hydrator exports defaults as env; `confluence.py` already reads env. That's the whole payoff of putting the hydrator at the shell boundary — connectors stay pure env-readers forever. `CONFLUENCE_URL` is a coordinate, not a secret: into defaults. `CONFLUENCE_EMAIL` is *technically* not a secret either, but note the consequence before adding it: anything in the wallet rides into any future `! cat connectors.json` receipt bound for a cloud chat window (this very compile proved that pattern last turn). Your work email + your employer's Atlassian hostname in one cloud payload is an identifier pairing worth a deliberate decision, not a default. Recommendation: `CONFLUENCE_URL` into defaults now; `CONFLUENCE_EMAIL` stays env/.env, or goes in only after a `pii_substitutions.txt` line covers it. `CONFLUENCE_TOKEN` never — that's the `_rule` line doing its job.

---

**1. PROBES**

```text
# P1 — migration receipt, the 2→1 pattern. BEFORE: 2 hits (top_movers:35, keyworder:36).
#      AFTER this turn's patch: 1 hit (keyworder only, until its source arrives next compile).
rg -n "SCRIPT_DIR, 'service-account-key.json'" scripts/gsc/ | head -4

# P2 — proves the migrated top_movers resolves the WALLET path, not the repo path. BEFORE:
#      error names scripts/gsc/. AFTER: error (or auth success) names ~/.config/pipulate/.
#      Bounded by head; a clean failure is a valid receipt.
python scripts/gsc/gsc_top_movers.py 2>&1 | head -5

# P3 — does a working key already exist at the articleizer path? Gates the copy in
#      external deliverables, and gates whether gsc.py can list properties next compile.
ls -la ~/.config/articleizer/service-account-key.json ~/.config/pipulate/service-account-key.json 2>&1
```

**2. NEXT CONTEXT** (probes echoed verbatim, per rule 12)

adhoc.txt slot:

```text
scripts/connectors/gsc.py
scripts/gsc/gsc_top_movers.py
scripts/gsc/gsc_keyworder.py
scripts/articles/gsc_historical_fetch.py
scripts/connectors/README.md
! rg -n "SCRIPT_DIR, 'service-account-key.json'" scripts/gsc/ | head -4
! python scripts/gsc/gsc_top_movers.py 2>&1 | head -5
! ls -la ~/.config/articleizer/service-account-key.json ~/.config/pipulate/service-account-key.json 2>&1
! python scripts/connectors/gsc.py 2>&1 | head -10
```

prompt.md text: "Receipts above are AFTER readings. Confirm top_movers now resolves the wallet path (rg count should be 1, keyworder only) and whether the key copy landed (gsc.py should now list properties instead of failing). Then: (a) apply the identical wallet resolver to gsc_keyworder.py, whose source is now in context; (b) decide whether gsc_historical_fetch.py should also converge on the wallet path — retiring ~/.config/articleizer/ as a second credential home — and whether the articleizer wallet entry belongs in connectors.json."

**3. PATCHES**

```text
Target: scripts/gsc/gsc_top_movers.py
[[[SEARCH]]]
# Path to your service account key JSON file
# Assumes the key file is in the same directory as the script. Adjust if needed.
SCRIPT_DIR = os.path.dirname(__file__)
SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
[[[DIVIDER]]]
# Path to your service account key JSON file — resolved from the wallet, never
# from inside this repo. The old SCRIPT_DIR expectation was a landmine: one
# careless `git add scripts/gsc/` away from shipping a Google credential to a
# public remote. Resolution chain (identical to scripts/connectors/gsc.py):
#   PIPULATE_GSC_KEY env -> connectors.json gsc.paths.service_account
#     -> wallet-path default. Duplicated deliberately (WET, per the
#   connectors README): this file must stay a standalone artifact.
SCRIPT_DIR = os.path.dirname(__file__)

def _resolve_gsc_key_path():
    import json as _json
    from pathlib import Path as _Path
    env = os.environ.get('PIPULATE_GSC_KEY')
    if env:
        return os.path.expanduser(env)
    wallet = _Path.home() / '.config' / 'pipulate' / 'connectors.json'
    if wallet.exists():
        try:
            data = _json.loads(wallet.read_text(encoding='utf-8'))
            p = (data.get('gsc') or {}).get('paths', {}).get('service_account')
            if p:
                return os.path.expanduser(p)
        except (_json.JSONDecodeError, OSError):
            pass
    return os.path.expanduser('~/.config/pipulate/service-account-key.json')

SERVICE_ACCOUNT_KEY_FILE = _resolve_gsc_key_path()
[[[REPLACE]]]
```

**4. EXTERNAL DELIVERABLES** (manual terminal actions — never patch markers)

**A. Relocate the probable working key into the wallet path** (P3 will confirm the source exists first, but this is safe to run blind — the guard makes it a no-op otherwise):

```text
[ -f ~/.config/articleizer/service-account-key.json ] && cp ~/.config/articleizer/service-account-key.json ~/.config/pipulate/service-account-key.json && chmod 600 ~/.config/pipulate/service-account-key.json && echo COPIED || echo NO_SOURCE_KEY
```

(If `NO_SOURCE_KEY`, gsc.py's own clean-failure message is the walkthrough: mint a JSON key in Google Cloud Console, save it at the wallet path, add the service account's email as a user on each Search Console property.)

**B. Grow the confluence defaults block** in `~/.config/pipulate/connectors.json` — URL only, per the ruling above — then commit the wallet repo:

```text
{
  "defaults": {
    "CONFLUENCE_URL": "https://YOURCO.atlassian.net/wiki"
  }
}
```

(Edit the value in place under the existing `"confluence"` key; `cd ~/.config/pipulate && git add connectors.json && git commit -m "Confluence URL into wallet defaults; email stays env-only by ruling"`. It becomes ambient env on your next fresh `nix develop`, and confluence.py's missing-variable error message loses one of its three complaints without a single line of Python changing.)

The 2→1 receipt pattern rides again: two SCRIPT_DIR landmines before the patch, one after, zero once keyworder's source arrives next compile. The wallet grew a fourth consumer without growing any code — which is exactly what a parity layer is supposed to feel like.

**MikeLev.in**: Probes:

```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ # P1 — migration receipt, the 2→1 pattern. BEFORE: 2 hits (top_movers:35, keyworder:36).
#      AFTER this turn's patch: 1 hit (keyworder only, until its source arrives next compile).
rg -n "SCRIPT_DIR, 'service-account-key.json'" scripts/gsc/ | head -4

# P2 — proves the migrated top_movers resolves the WALLET path, not the repo path. BEFORE:
#      error names scripts/gsc/. AFTER: error (or auth success) names ~/.config/pipulate/.
#      Bounded by head; a clean failure is a valid receipt.
python scripts/gsc/gsc_top_movers.py 2>&1 | head -5

# P3 — does a working key already exist at the articleizer path? Gates the copy in
#      external deliverables, and gates whether gsc.py can list properties next compile.
ls -la ~/.config/articleizer/service-account-key.json ~/.config/pipulate/service-account-key.json 2>&1
scripts/gsc/gsc_top_movers.py:35:SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
scripts/gsc/gsc_keyworder.py:36:SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
Error: Service account key file not found at: /home/mike/repos/pipulate/scripts/gsc/service-account-key.json
ls: cannot access '/home/mike/.config/pipulate/service-account-key.json': No such file or directory
-rw-r--r-- 1 mike users 2348 Dec 14  2025 /home/mike/.config/articleizer/service-account-key.json
(nix) pipulate $
```

Context set, trust me.

Prompt pasted and pushing down. Now patches and housekeeping:

```diff
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/gsc/gsc_top_movers.py'.
(nix) pipulate $ d
diff --git a/scripts/gsc/gsc_top_movers.py b/scripts/gsc/gsc_top_movers.py
index 43805101..4598eb0c 100644
--- a/scripts/gsc/gsc_top_movers.py
+++ b/scripts/gsc/gsc_top_movers.py
@@ -29,10 +29,35 @@ from googleapiclient.errors import HttpError
 # Set your GSC Property URL here (e.g., "sc-domain:example.com" or "https://www.example.com/")
 SITE_URL = "sc-domain:mikelev.in"
 
-# Path to your service account key JSON file
-# Assumes the key file is in the same directory as the script. Adjust if needed.
+# Path to your service account key JSON file — resolved from the wallet, never
+# from inside this repo. The old SCRIPT_DIR expectation was a landmine: one
+# careless `git add scripts/gsc/` away from shipping a Google credential to a
+# public remote. Resolution chain (identical to scripts/connectors/gsc.py):
+#   PIPULATE_GSC_KEY env -> connectors.json gsc.paths.service_account
+#     -> wallet-path default. Duplicated deliberately (WET, per the
+#   connectors README): this file must stay a standalone artifact.
 SCRIPT_DIR = os.path.dirname(__file__)
-SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
+
+
+def _resolve_gsc_key_path():
+    import json as _json
+    from pathlib import Path as _Path
+    env = os.environ.get('PIPULATE_GSC_KEY')
+    if env:
+        return os.path.expanduser(env)
+    wallet = _Path.home() / '.config' / 'pipulate' / 'connectors.json'
+    if wallet.exists():
+        try:
+            data = _json.loads(wallet.read_text(encoding='utf-8'))
+            p = (data.get('gsc') or {}).get('paths', {}).get('service_account')
+            if p:
+                return os.path.expanduser(p)
+        except (_json.JSONDecodeError, OSError):
+            pass
+    return os.path.expanduser('~/.config/pipulate/service-account-key.json')
+
+
+SERVICE_ACCOUNT_KEY_FILE = _resolve_gsc_key_path()
 
 # Required Google API scopes
 SCOPES = ['https://www.googleapis.com/auth/webmasters']
(nix) pipulate $ m
📝 Committing: chore: Resolve GSC service account key path from environment and wallet 
[main 5fae274e] chore: Resolve GSC service account key path from environment and wallet
 1 file changed, 28 insertions(+), 3 deletions(-)
(nix) pipulate $ [ -f ~/.config/articleizer/service-account-key.json ] && cp ~/.config/articleizer/service-account-key.json ~/.config/pipulate/service-account-key.json && chmod 600 ~/.config/pipulate/service-account-key.json && echo COPIED || echo NO_SOURCE_KEY
COPIED
(nix) pipulate $ 
```

We make a new file. Apparently that's growing:

```bash
(nix) pipulate $ xv ~/.config/pipulate/connectors.json
(nix) pipulate $ cat ~/.config/pipulate/connectors.json
{
  "defaults": {
    "CONFLUENCE_URL": "https://YOURCO.atlassian.net/wiki"
  }
}(nix) pipulate $ 
```

Oh whoops, that's not right!

What have I done? That's outside the repo. I can't reset it!

**Fable 5 High**: Context verified.

Breathe. You typed `xv` — which is `xclip -selection clipboard -o >`, a **truncating redirect** — while the clipboard still held only the confluence snippet from my external deliverable. The whole wallet got flattened to a four-line JSON. But you are not stranded, and in fact this compile's own live receipts already diagnosed the blast radius before you finished panicking:

**Verdict 1 — the clobber is real and the receipts prove it.** The `! python scripts/connectors/gsc.py` receipt shows the *first* failure branch — "No GSC key path configured" — which only fires when `PIPULATE_GSC_KEY` is unset **and** the wallet lacks `gsc.paths.service_account`. Two compiles ago the same probe got past resolution to the file-not-found branch. The wallet's gsc block existed then; it doesn't now. The receipt is the timestamped fingerprint of the accident.

**Verdict 2 — everything else LANDED.** The 2→1 pattern hit: `rg` shows only `gsc_keyworder.py:36` still on the landmine. And `gsc_top_movers.py` didn't just resolve the wallet path — it **authenticated and found data** ("✓ Successfully authenticated... Checking date 2026-07-12... ✓ Data found!"), because its resolver has the wallet-path *default fallback* and your key copy landed (`ls` receipt: 600, Jul 14 20:51). Note the asymmetry that verdict exposes: top_movers survived the clobbered wallet because of its fallback; gsc.py died because it lacks one. Parity patch below.

**Now the recovery, in order of preference:**

**Path A — git.** "That's outside the repo" is wrong in the best way: `~/.config/pipulate` *is* a repo — the one you just scrubbed with filter-repo specifically so `connectors.json` could be safely tracked. Two turns ago the external deliverable said to commit it. If you did, recovery is one command: `git restore connectors.json`. P1 below tells the truth.

**Path B — the transcript.** Even if you never committed it, the complete v1 wallet JSON is preserved **verbatim in this conversation** — you `cat`'d it into the record two turns ago. The compiled context artifact you've been performing is itself the backup. The loom's punch cards contain the pocket. The full reconstruction (with the confluence default merged in properly this time) is in External Deliverables.

**The lesson, one line:** `xv` is for whole-file writes you *intend*; surgical JSON edits go through `vim` or `jq`. And commit the wallet repo the moment it's restored — a tracked wallet turns every future fat-finger into a `git restore`.

One side effect until restored: the hydrator's jq filter reads per-connector `defaults` blocks; the clobbered file has a top-level `defaults` that matches nothing, so your next fresh `nix develop` would silently hydrate zero vars. Restore first.

---

**1. PROBES**

```text
# P1 — was connectors.json ever committed? Non-empty log = Path A (git restore).
#      Empty = Path B (transcript reconstruction). Gates which deliverable you run.
cd ~/.config/pipulate && git log --oneline -- connectors.json | head -3 && git status --short; cd -

# P2 — wallet integrity after recovery. Expect: ['_rule','_schema','botify','confluence','gmail','gsc'].
python -c "import json; print(sorted(json.load(open('/home/mike/.config/pipulate/connectors.json')).keys()))"

# P3 — gsc.py end-to-end after recovery + parity patch. Expect: property list (key exists now),
#      proving wallet -> key -> API in one receipt.
python scripts/connectors/gsc.py 2>&1 | head -6
```

**2. NEXT CONTEXT** (probes echoed verbatim, per rule 12)

adhoc.txt slot:

```text
scripts/connectors/gsc.py
scripts/gsc/gsc_keyworder.py
scripts/articles/gsc_historical_fetch.py
scripts/connectors/README.md
flake.nix
! cd ~/.config/pipulate && git log --oneline -- connectors.json | head -3 && git status --short
! python -c "import json; print(sorted(json.load(open('/home/mike/.config/pipulate/connectors.json')).keys()))"
! python scripts/connectors/gsc.py 2>&1 | head -6
! rg -n "SCRIPT_DIR, 'service-account-key.json'" scripts/gsc/ || echo LANDMINE_RETIRED
```

prompt.md text: "Receipts above are AFTER readings. Confirm the wallet is restored (six keys), gsc.py lists properties end-to-end, and the SCRIPT_DIR landmine count went 1→0 after the keyworder patch. Then decide whether ~/.config/articleizer/ should be retired entirely as a credential home (gsc_historical_fetch now resolves via the wallet; the old key can be deleted once a full dragnet run succeeds from the wallet path), and whether the wallet repo should grow its remote now that it's provably scrub-safe and fat-finger-proof."

**3. PATCHES**

Parity fix — gsc.py gets the same wallet-path default fallback that just saved top_movers:

```text
Target: scripts/connectors/gsc.py
[[[SEARCH]]]
        except (json.JSONDecodeError, OSError):
            pass
    return None
[[[DIVIDER]]]
        except (json.JSONDecodeError, OSError):
            pass
    # Wallet-path default (parity with scripts/gsc/gsc_top_movers.py): a
    # corrupted or clobbered connectors.json must not strand a key sitting
    # at the canonical wallet path. get_service()'s exists() check still
    # fails closed if the file is genuinely absent.
    return Path.home() / '.config' / 'pipulate' / 'service-account-key.json'
[[[REPLACE]]]
```

The 1→0 completion — keyworder off the landmine, identical WET resolver:

```text
Target: scripts/gsc/gsc_keyworder.py
[[[SEARCH]]]
# Path to your service account key JSON file
# Assumes key file is in the same directory as the script. Adjust if needed.
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # Use abspath for reliability
SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
[[[DIVIDER]]]
# Path to your service account key JSON file — resolved from the wallet, never
# from inside this repo. Resolution chain (identical to scripts/connectors/gsc.py):
#   PIPULATE_GSC_KEY env -> connectors.json gsc.paths.service_account
#     -> wallet-path default. Duplicated deliberately (WET, per the
#   connectors README): this file must stay a standalone artifact.
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # Use abspath for reliability

def _resolve_gsc_key_path():
    import json as _json
    from pathlib import Path as _Path
    env = os.environ.get('PIPULATE_GSC_KEY')
    if env:
        return os.path.expanduser(env)
    wallet = _Path.home() / '.config' / 'pipulate' / 'connectors.json'
    if wallet.exists():
        try:
            data = _json.loads(wallet.read_text(encoding='utf-8'))
            p = (data.get('gsc') or {}).get('paths', {}).get('service_account')
            if p:
                return os.path.expanduser(p)
        except (_json.JSONDecodeError, OSError):
            pass
    return os.path.expanduser('~/.config/pipulate/service-account-key.json')

SERVICE_ACCOUNT_KEY_FILE = _resolve_gsc_key_path()
[[[REPLACE]]]
```

And gsc_historical_fetch converges on the wallet too (its source is in this context; the key was *copied*, not moved, so both paths currently work — this makes the articleizer path retirable):

```text
Target: scripts/articles/gsc_historical_fetch.py
[[[SEARCH]]]
SCRIPT_DIR = Path(__file__).parent.resolve()
# Adjust path to match your actual key location provided in context
SERVICE_ACCOUNT_KEY_FILE = Path.home() / ".config/articleizer/service-account-key.json"
[[[DIVIDER]]]
SCRIPT_DIR = Path(__file__).parent.resolve()
# Key resolved from the wallet (PIPULATE_GSC_KEY env -> connectors.json
# gsc.paths.service_account -> wallet-path default), converging with
# scripts/connectors/gsc.py and retiring ~/.config/articleizer/ as a
# second credential home. Duplicated deliberately (WET).
def _resolve_gsc_key_path():
    env = os.environ.get('PIPULATE_GSC_KEY')
    if env:
        return Path(env).expanduser()
    wallet = Path.home() / '.config' / 'pipulate' / 'connectors.json'
    if wallet.exists():
        try:
            data = json.loads(wallet.read_text(encoding='utf-8'))
            p = (data.get('gsc') or {}).get('paths', {}).get('service_account')
            if p:
                return Path(p).expanduser()
        except (json.JSONDecodeError, OSError):
            pass
    return Path.home() / '.config' / 'pipulate' / 'service-account-key.json'

SERVICE_ACCOUNT_KEY_FILE = _resolve_gsc_key_path()
[[[REPLACE]]]
```

(Also: `ruff check scripts/gsc/gsc_keyworder.py --fix` clears the W292 trailing-newline nit this compile's static analysis flagged.)

**4. EXTERNAL DELIVERABLES** (manual actions in `~/.config/pipulate` — never patch markers)

**Path A (if P1 shows a commit):**

```text
cd ~/.config/pipulate && git restore connectors.json
vim connectors.json    # add "CONFLUENCE_URL" inside the EXISTING confluence key's defaults — in an editor this time
git add connectors.json && git commit -m "Restore wallet; confluence URL into defaults; email stays env-only by ruling"
```

**Path B (if P1 shows no commit) — full reconstruction from the transcript, confluence default pre-merged. Replace YOURCO with the real hostname:**

```text
cat > ~/.config/pipulate/connectors.json << 'EOF'
{
  "_schema": "pipulate-wallet-v1",
  "_rule": "Names and paths ONLY. Never secret values. Secrets live in env, .env, or the token files named here.",
  "gmail": {
    "auth": "oauth_token_file",
    "paths": {
      "credentials": "~/.config/pipulate/credentials.json",
      "token": "~/.config/pipulate/gmail_token.json"
    },
    "env": {
      "PIPULATE_GMAIL_CREDENTIALS": "overrides paths.credentials",
      "PIPULATE_GMAIL_TOKEN": "overrides paths.token"
    },
    "defaults": {}
  },
  "botify": {
    "auth": "bearer_token",
    "env": {
      "BOTIFY_API_TOKEN": "required; or project .env via config.get_botify_token()",
      "BOTIFY_ORG": "optional FETCH coordinate",
      "BOTIFY_PROJECT": "optional FETCH coordinate"
    },
    "defaults": {
      "BOTIFY_ORG": "uhnd-com",
      "BOTIFY_PROJECT": "test-dev-site-crawl"
    }
  },
  "confluence": {
    "auth": "basic_auth",
    "env": {
      "CONFLUENCE_URL": "required, e.g. https://yourco.atlassian.net/wiki",
      "CONFLUENCE_EMAIL": "required",
      "CONFLUENCE_TOKEN": "required; secret — env or .env only"
    },
    "defaults": {
      "CONFLUENCE_URL": "https://YOURCO.atlassian.net/wiki"
    }
  },
  "gsc": {
    "auth": "service_account_file",
    "paths": {
      "service_account": "~/.config/pipulate/service-account-key.json"
    },
    "env": {
      "PIPULATE_GSC_KEY": "overrides paths.service_account"
    },
    "defaults": {}
  }
}
EOF
cd ~/.config/pipulate && git add connectors.json && git commit -m "Mint the wallet: connectors.json v1 (recovered from transcript) + confluence URL default"
```

Either path ends the same way: the wallet is back, versioned, and structurally immune to the next `xv` — because the next fat-finger is a `git restore`, not a panic. The genie dissolved hours ago, but the punch cards remembered the pocket.

**MikeLev.in**: Okay, probes:

```bash
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
(nix) pipulate $ # P1 — was connectors.json ever committed? Non-empty log = Path A (git restore).
#      Empty = Path B (transcript reconstruction). Gates which deliverable you run.
cd ~/.config/pipulate && git log --oneline -- connectors.json | head -3 && git status --short; cd -

# P2 — wallet integrity after recovery. Expect: ['_rule','_schema','botify','confluence','gmail','gsc'].
python -c "import json; print(sorted(json.load(open('/home/mike/.config/pipulate/connectors.json')).keys()))"

# P3 — gsc.py end-to-end after recovery + parity patch. Expect: property list (key exists now),
#      proving wallet -> key -> API in one receipt.
python scripts/connectors/gsc.py 2>&1 | head -6
?? commit_denylist.txt
?? connectors.json
?? service-account-key.json
/home/mike/repos/pipulate
['defaults']
No GSC key path configured.
Set PIPULATE_GSC_KEY=~/.config/pipulate/service-account-key.json
or add gsc.paths.service_account to ~/.config/pipulate/connectors.json.
(nix) pipulate $
```

And then the patches:

```diff
$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/connectors/gsc.py'.
(nix) pipulate $ d
diff --git a/scripts/connectors/gsc.py b/scripts/connectors/gsc.py
index 7a797b52..ddbeddd7 100644
--- a/scripts/connectors/gsc.py
+++ b/scripts/connectors/gsc.py
@@ -69,7 +69,11 @@ def resolve_key_path():
                 return Path(p).expanduser()
         except (json.JSONDecodeError, OSError):
             pass
-    return None
+    # Wallet-path default (parity with scripts/gsc/gsc_top_movers.py): a
+    # corrupted or clobbered connectors.json must not strand a key sitting
+    # at the canonical wallet path. get_service()'s exists() check still
+    # fails closed if the file is genuinely absent.
+    return Path.home() / '.config' / 'pipulate' / 'service-account-key.json'
 
 
 def get_service():
(nix) pipulate $ m
📝 Committing: fix: Ensure default wallet path for connectors.json
[main a07def26] fix: Ensure default wallet path for connectors.json
 1 file changed, 5 insertions(+), 1 deletion(-)
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/gsc/gsc_keyworder.py'.
(nix) pipulate $ d
diff --git a/scripts/gsc/gsc_keyworder.py b/scripts/gsc/gsc_keyworder.py
index 0d32f04f..2a4ef37a 100644
--- a/scripts/gsc/gsc_keyworder.py
+++ b/scripts/gsc/gsc_keyworder.py
@@ -30,10 +30,33 @@ SITE_URL = "sc-domain:mikelev.in"
 # Base URL of your website (used to convert absolute GSC URLs to relative paths)
 BASE_URL = "https://mikelev.in"
 
-# Path to your service account key JSON file
-# Assumes key file is in the same directory as the script. Adjust if needed.
+# Path to your service account key JSON file — resolved from the wallet, never
+# from inside this repo. Resolution chain (identical to scripts/connectors/gsc.py):
+#   PIPULATE_GSC_KEY env -> connectors.json gsc.paths.service_account
+#     -> wallet-path default. Duplicated deliberately (WET, per the
+#   connectors README): this file must stay a standalone artifact.
 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # Use abspath for reliability
-SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
+
+
+def _resolve_gsc_key_path():
+    import json as _json
+    from pathlib import Path as _Path
+    env = os.environ.get('PIPULATE_GSC_KEY')
+    if env:
+        return os.path.expanduser(env)
+    wallet = _Path.home() / '.config' / 'pipulate' / 'connectors.json'
+    if wallet.exists():
+        try:
+            data = _json.loads(wallet.read_text(encoding='utf-8'))
+            p = (data.get('gsc') or {}).get('paths', {}).get('service_account')
+            if p:
+                return os.path.expanduser(p)
+        except (_json.JSONDecodeError, OSError):
+            pass
+    return os.path.expanduser('~/.config/pipulate/service-account-key.json')
+
+
+SERVICE_ACCOUNT_KEY_FILE = _resolve_gsc_key_path()
 
 # Required Google API scopes
 SCOPES = ['https://www.googleapis.com/auth/webmasters']
(nix) pipulate $ patch
(nix) pipulate $ app
✅ DETERMINISTIC PATCH APPLIED: Successfully mutated 'scripts/articles/gsc_historical_fetch.py'.
(nix) pipulate $ d
diff --git a/scripts/articles/gsc_historical_fetch.py b/scripts/articles/gsc_historical_fetch.py
index ab4e9360..ee8f06e5 100644
--- a/scripts/articles/gsc_historical_fetch.py
+++ b/scripts/articles/gsc_historical_fetch.py
@@ -23,8 +23,27 @@ import common
 # --- CONFIGURATION ---
 SITE_URL = "sc-domain:mikelev.in" 
 SCRIPT_DIR = Path(__file__).parent.resolve()
-# Adjust path to match your actual key location provided in context
-SERVICE_ACCOUNT_KEY_FILE = Path.home() / ".config/articleizer/service-account-key.json"
+# Key resolved from the wallet (PIPULATE_GSC_KEY env -> connectors.json
+# gsc.paths.service_account -> wallet-path default), converging with
+# scripts/connectors/gsc.py and retiring ~/.config/articleizer/ as a
+# second credential home. Duplicated deliberately (WET).
+def _resolve_gsc_key_path():
+    env = os.environ.get('PIPULATE_GSC_KEY')
+    if env:
+        return Path(env).expanduser()
+    wallet = Path.home() / '.config' / 'pipulate' / 'connectors.json'
+    if wallet.exists():
+        try:
+            data = json.loads(wallet.read_text(encoding='utf-8'))
+            p = (data.get('gsc') or {}).get('paths', {}).get('service_account')
+            if p:
+                return Path(p).expanduser()
+        except (json.JSONDecodeError, OSError):
+            pass
+    return Path.home() / '.config' / 'pipulate' / 'service-account-key.json'
+
+
+SERVICE_ACCOUNT_KEY_FILE = _resolve_gsc_key_path()
 OUTPUT_FILE = SCRIPT_DIR / 'gsc_velocity.json'
 
 # The date of the "Crash" to pivot analysis around
diff --git a/scripts/gsc/gsc_keyworder.py b/scripts/gsc/gsc_keyworder.py
index 0d32f04f..2a4ef37a 100644
--- a/scripts/gsc/gsc_keyworder.py
+++ b/scripts/gsc/gsc_keyworder.py
@@ -30,10 +30,33 @@ SITE_URL = "sc-domain:mikelev.in"
 # Base URL of your website (used to convert absolute GSC URLs to relative paths)
 BASE_URL = "https://mikelev.in"
 
-# Path to your service account key JSON file
-# Assumes key file is in the same directory as the script. Adjust if needed.
+# Path to your service account key JSON file — resolved from the wallet, never
+# from inside this repo. Resolution chain (identical to scripts/connectors/gsc.py):
+#   PIPULATE_GSC_KEY env -> connectors.json gsc.paths.service_account
+#     -> wallet-path default. Duplicated deliberately (WET, per the
+#   connectors README): this file must stay a standalone artifact.
 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # Use abspath for reliability
-SERVICE_ACCOUNT_KEY_FILE = os.path.join(SCRIPT_DIR, 'service-account-key.json')
+
+
+def _resolve_gsc_key_path():
+    import json as _json
+    from pathlib import Path as _Path
+    env = os.environ.get('PIPULATE_GSC_KEY')
+    if env:
+        return os.path.expanduser(env)
+    wallet = _Path.home() / '.config' / 'pipulate' / 'connectors.json'
+    if wallet.exists():
+        try:
+            data = _json.loads(wallet.read_text(encoding='utf-8'))
+            p = (data.get('gsc') or {}).get('paths', {}).get('service_account')
+            if p:
+                return os.path.expanduser(p)
+        except (_json.JSONDecodeError, OSError):
+            pass
+    return os.path.expanduser('~/.config/pipulate/service-account-key.json')
+
+
+SERVICE_ACCOUNT_KEY_FILE = _resolve_gsc_key_path()
 
 # Required Google API scopes
 SCOPES = ['https://www.googleapis.com/auth/webmasters']
(nix) pipulate $ m
📝 Committing: chore: Resolve GSC service account key path from wallet
[main 258417f8] chore: Resolve GSC service account key path from wallet
 2 files changed, 47 insertions(+), 5 deletions(-)
(nix) pipulate $ git push
Enumerating objects: 26, done.
Counting objects: 100% (26/26), done.
Delta compression using up to 48 threads
Compressing objects: 100% (17/17), done.
Writing objects: 100% (17/17), 3.31 KiB | 3.31 MiB/s, done.
Total 17 (delta 14), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (14/14), completed with 9 local objects.
To github.com:pipulate/pipulate.git
   35abaecb..258417f8  main -> main
(nix) pipulate $ cd ~/.config/pipulate && git restore connectors.json
vim connectors.json    # add "CONFLUENCE_URL" inside the EXISTING confluence key's defaults — in an editor this time
git add connectors.json && git commit -m "Restore wallet; confluence URL into defaults; email stays env-only by ruling"
error: pathspec 'connectors.json' did not match any file(s) known to git
[main b209cc8] Restore wallet; confluence URL into defaults; email stays env-only by ruling
 1 file changed, 5 insertions(+)
 create mode 100644 connectors.json
(nix) pipulate $
```

Right, now I restore that file. And now I'm really tired. I know I'm close but
this will be my restarting article when I get restarted. And I get this message:

> You’ve used 97% of your Fable 5 limit · Resets Jul 18 at 6:00 PM

So I'm releasing the worm and articleizing this as-is. A cliffhanger!


---

## Book Analysis

### Ai Editorial Take
What strikes me is the 'mechanical governor' application. Most AI safety discourse focuses on alignment models, but this article treats safety as a structural, physical constraint—a literal governor. It effectively re-frames 'safety' from an abstract concept into a 'brass fitting' in a machine, which is far more intuitive for builders and hackers.

### 🐦 X.com Promo Tweet
```text
Stop vibecoding into RAG-hallucination. Build deterministic connectors that act like a player piano for your infrastructure. Everything is a file, everything is text. Get the guide to the new plain-text web here: https://mikelev.in/futureproof/connectors-protocol-plain-text/ #AI #Unix #AgenticWorkflow
```

### Title Brainstorm
* **Title Option:** The Connectors Protocol: Rebuilding the Web as Plain Text
  * **Filename:** `connectors-protocol-plain-text.md`
  * **Rationale:** Directly addresses the industry-wide convergence on text-based agentic interfaces.
* **Title Option:** The Anti-Crichton Machine: Deterministic AI Pipelines
  * **Filename:** `anti-crichton-machine.md`
  * **Rationale:** Leverages the mechanical governor metaphor, framing safety as an engineering constraint.
* **Title Option:** Everything is a File: The Unix Revenge
  * **Filename:** `unix-revenge-everything-is-file.md`
  * **Rationale:** Taps into the enduring Unix philosophy that is currently being rediscovered by the agentic industry.

### Content Potential And Polish
- **Core Strengths:**
  - Strong mechanical metaphors for software governors.
  - Clear, actionable transition from messy SaaS dependencies to text-based infrastructure.
  - Demonstrated success with binary-search causal boundaries via probe echoes.
- **Suggestions For Polish:**
  - Standardize the nomenclature between 'connector' and 'tool' to avoid confusion with MCP.
  - Create a dedicated diagram for the 'wallet hydrator' flow to simplify the mental model for readers.
  - Refine the PII mitigation documentation to ensure the 'uhnd-com' ruling is formalized within the code comments.

### Next Step Prompts
- Draft the 'connectors.nix' specification that emits the connectors.json wallet file, allowing for OS-level config generation.
- Design an MCP server wrapper that dynamically consumes these connectors to provide a bridge between the 'WET' standalone tools and the 'dry' MCP tool-calling standard.
