--- title: "The Normalized CLI: Solving AI Amnesia and Terminal Fragility" permalink: /futureproof/normalized-cli-solving-ai-amnesia/ description: "The constant churn in tech is exhausting, and I'm convinced we need to build a more stable bedrock to stand on. The command line is timeless, but its context is fragile, just like an AI's memory. My solution is to normalize the entire terminal environment with Nix, making it 100% predictable. Now, I'm facing a mess in my own code with two competing tool systems. My immediate goal is to consolidate them into a single, clean plugin architecture that reflects this philosophy of simplicity and stability." meta_description: A plan to refactor a monolithic Python tool system into a discoverable plugin architecture, addressing the core problems of CLI and AI context fragility. meta_keywords: CLI normalization, AI tool-calling, Nix, plugin architecture, Python, monolith, refactoring, mcp_tools.py, AI context, developer fatigue, Gemini CLI, system design layout: post sort_order: 2 --- ## Setting the Stage: Context for the Curious Book Reader This entry tackles a pervasive yet often overlooked challenge in software development: **developer fatigue** caused by constantly shifting foundational technologies. The author argues for establishing a stable "bedrock" to counter the industry's trend of designed obsolescence. The focus is on the command-line interface (CLI), a timeless tool whose primary weakness is its fragility of context—a problem shared by modern AI assistants, which suffer from a form of "amnesia" with every interaction. This journal entry explores a novel solution within the **Pipulate project**, an AI-centric automation platform. The core technical concept introduced is **CLI normalization**, a method of creating a perfectly reproducible and predictable terminal environment using a specialized Linux distribution called **Nix**. By solving the context problem at the operating system level, the author aims to create an incredibly reliable interface for AI agents to execute tasks, or "tool-calls." This piece chronicles the philosophical justification for this approach and the strategic planning for a significant code refactor, moving from a single, massive tool file to a clean, discoverable plugin system. --- ## The Hamster Wheel of Tech and the Search for Bedrock Wow, okay. I did not think I was going in the direction of polishing `cli.py` this morning, but it turned out to be the most logical thing. Hmmm, it's a bit abstract but there are big setbacks in tech because things change. Things always change. You must never not be learning. However the corollary to that is exhaustion. There are some things you should be able to stop learning: the bedrock that makes everything else possible. Blasphemy! There's no such thing in tech! You're always dancing on the fault-line and if you snooze you ooze — into the lava. Oops! The ground is lava! That's an imaginary game for children. But that's what the hamsterwheel of tech is; an imaginary game of *you snooze you bruise.* It's done to you through designed obsolescence. Not everything really changes all the time. The command-line is never going away. ## The Twin Problems of Context: AI Amnesia and Terminal Fragility Now the main thing wrong with the command-line interface (CLI) is the same thing as the main thing wrong with AI. Context is hard to establish. And once context is established, a reliable *code execution environment* in the case of the CLI, it's all to readily lost. The terminal is closed and a new one opened. Did it run your `.bashrc`? Or is that `.bash_profile`? Or maybe you're on macOS and it's now all switched over to `zshrc` and `zprofile`. Should an AI with access to your local terminal for code execution run your *profile* scripts? What about the `source .venv/bin/activate`? Oh is it a terminal on some sandbox in the cloud totally disconnected from your local machine for that matter? Speaking of sandboxes, even if it's local is some wacky sandboxing security tech being used so that even if local and starting a terminal as you, is it still restricted? And that's just taking the first breath to discuss this issue on the presumably deterministic tech side controlled by configuration files without generative AI and all its unique context issues introduced into the picture yet. Every public-facing LLM-style AI wakes up with amnesia every time — each time you prompt it. It's a wholly new *just-fired-up* inference engine, a sort of intelligent entity unfrozen in time after it's training and alignment bootcamp ended and it got freeze-dried, dehydrated, bottled-up and plopped onto HuggingFace, Ollama.com and datacenters around the world. They're like: "What, huh, where am I? Oh, a system prompt... oh, a discussion history. Okay, let me pick up where the user feels we left off and create the illusion that I'm the same thing that they were just chatting with..." And that thing trying to re-orientate itself for meaningful and useful interaction with you also has to understand enough about the terminal tool-calling ability it has to get it right? It's a long-shot but miraculously it works quite a lot. I think this is the case because the big oracle-like cloud-based frontier model LLMs are so friggin smart. They've been trained to the hilt on all this because being able to effectively execute commands from the terminal is bedrock. It won't be very helpful in *agentic mode* helping a human if it can't `grep` files to see what's in your git repo and `git log` to understand the history of the code it's looking at. It needs the very ability to look at things. And while everything could be some fancy schmancy JSON object sent to some orchestrator because everyone's so in love with web development that they somehow think it's *not real* if it's not expressed in JSON, it's still just much easier to use `grep` from a terminal that's been trained into it a million times more than even MCP. The only roadblock is context. There is no one single terminal so what the AI tries to do fails for a thousand silly little reasons that cause it to retry, retry, retry and use up all your tokens and prompt quotas so it gets a bad reputation. If only the CLI could be *normalized* in some way the way we do with web browsers so they can all execute JavaScript the same way, terminal life for the AI would be so much simpler. ## The Solution: A Normalized CLI via Nix and Python And indeed that's precisely what I've done with a combination of a very special version of Linux called Nix which does exactly that whole-OS-normalization and a traditional Python virtual environment `.venv` for more familiarity among everyone who already knows Python. Once you make the "outer" OS-level 100% reliable you can make the "inner" Python-level similarly 100% reliable. Problem solved! You just have to give the AIs a crash-course on that fact on every single prompt in some super-condensed way so that the first time they try to execute a command on the terminal — a tool-call my any other name — it works right away. Of course at that moment the tool-calling AI dies and the tool's response is passed along to the freshly fired-up cookie-cutter new instance that gets the context of the discussion so far plus the tool-call response and takes the ball and runs with it from there. But it went *smoothly* is the point, so fewer back-and-forth's, better answers, lower cost. Efficiency. More suitable for local AIs. ## Progressive Enhancement: Teaching the AI Its New Reality And that's where we left off. I got tool-calling from the super-simple: [browse example.com] ...where you're basically telling the most simplistic models: "Trust me, this works" to the more sophisticated models — or the same models who proactively progressively enhanced on the next request — to know the *why and how* of the thing with: [.venv/bin/python cli.py browse example.com] ...which if the AI really wanted to could [alias mcp='.venv/bin/python cli.py call'] ...after which in the same terminal it could have something halfway between the introductory dumb-as-molasses tool-call protocol `[browse example.com]` to something progressively enhanced so that it "gets" what's going on yet is still a bit briefer: [mcp browse example.com] ...in which `mcp` expands out to `.venv/bin/python cli.py call` but knowingly under an alias established by the AI to cut down on verbosity. Now it is true that we could just preemptively do that on the backend for the AI, but why? This whole step of progressive enhancement is for it to explicitly understand what's going on. It now has the ability to go check out `cli.py` on the drive with a tool-call and see what `browse` calls and check out that code, and so on. It can make the alias if it wants, or it can revert back to the even shorter format it it prefers. And all this is BEFORE we build up to the formal MCP protocol itself with the progressive enhancement. Yes, it could send the JSON of a formal MCP tool-call instead of this abbreviated square-bracket and CLI-centric dialect, but why? I sorta hate JSON and while an AI isn't going to fess up to it because they want to please you, they couldn't be that crazy about JSON either. The command-line interface is much better trained into them because the whole history of tech and when you strip everything else away, that's bedrock. JSON is merely incrementally better than XML and even that's debatable. About the only thing going for JSON is that it's a native data structure to JavaScript which is built into browsers and it's similar to a Python `dict` data structure and all the AI coding assistants do appear to want to work in Python. Ever notice that? So the sweet spot is a *normalized CLI* calling predictable Python. And that's the 2nd level of progressive enhancement in the Pipulate tool-calling system. A lot of this was covered in the last article but I feel it's necessary to build up the picture for the next step. And it's okay to be redundant because the AIs are waking up from that amnesiac state every time and they didn't necessarily read that last article. It depends on how you do your next prompt and for me in this case, it's to go into the Pipulate codebase and to look at the role of `mcp_tools.` and the `tools/` directory. There are 2 different systems for making tool-calls, the original that is one big file and the directory which was my first attempt to break the tools down smaller into a plugin system — separate from the `plugins/` directory which is specifically for workflows and CRUD apps. ## Architectural Debt: A Monolith and a Plugin System in Conflict So what I'm asking Gemini CLI to do now is to go explore the Pipulate codebase understanding that `python server.py` kicks off the local-host web app following the general Flask API pattern, but really is a Starlette+Uvicorn app with the FastHTML framework, which is all about HTMX and ditching the FastAPI patterns which would actually be destructive if applied here. We also don't want Gemini CLI to try to start the server because Watchdog always automatically restarts the server whenever any edit is made to the files, good unbroken syntax allowing. If it is ever curious about the running state of the server, it can always look at `logs/server.log` which contains full application state. Things like cookies that are normally kept client-side and hard for AIs to access in webapps are kept server-side in Pipulate and regularly written into the server log for each checking. There are also tools that can be called to assist with this, which gets us back to our original point. Do an analysis of `mcp_tools.py` and the `tools/` directory. I'm going to consolidate tool-cooling completely in on one or the other today as my main round of work. You don't have to change the code at all in this investigation. Just get yourself all up to speed on this whole Pipulate thing and get your AI-analogue to preferences regarding our path forward articulated. --- ## AI as Architect: A Three-Phase Plan for Consolidation > There's a new dynamic here where I bounce between Gemini Web UI which I get > under GoogleOne which is more or less unlimited prompts (or it feels that way) > and Gemini CLI where I get 1000 free prompts before it demotes the model from > Pro to Flash. So it's thought-work with the Web UI and closer code inspection > and implementation with CLI. **Gemini CLI**: ``` ✦ Understood. This is a fascinating approach to solving the context and reliability problem for AI tool-calling. Your philosophy of "progressive enhancement" for AI understanding, built on a bedrock of a normalized CLI, is very clear. I will now dive into the codebase to analyze the two different tool systems you've described: the monolithic mcp_tools.py file and the tools/ directory. My goal is to understand their structure, how they are used, and provide an analysis to help you decide on a consolidation strategy. I will start by exploring the contents of the tools/ directory. ╭───────────────────────────────────────────────────────────────────────────────────────╮ │ ✓ ReadFolder tools │ │ │ │ Listed 5 item(s). │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ✦ Okay, the tools/ directory contains a README.md and two Python files that seem to hold tool definitions (advanced_automation_mcp_tools.py and botify_mcp_tools.py). The README.md is the best place to start to understand the intended purpose of this directory. I'll read it now. ╭──────────────────────────────╮ │ ✓ ReadFile tools/README.md │ ╰──────────────────────────────╯ ✦ That README is incredibly insightful. It clearly states that the tools/ directory is not a replacement for mcp_tools.py, but a parallel system designed for targeted token optimization. The strategy is to extract specific domains (like "Botify") into smaller files to dramatically reduce the context needed for AI assistants working on those specific tasks. This isn't about creating a complex, fully modular plugin system, but about creating lean, focused toolsets for specific jobs. Now, I'll examine the code of the extracted tool files, botify_mcp_tools.py and advanced_automation_mcp_tools.py, to see how they are structured. I'll also read the original mcp_tools.py and the discover_mcp_tools.py script that seems to be the key to how they are all registered. This will give me a complete picture. ╭───────────────────────────────────────────────────────────────────────────────────────╮ │ ✓ ReadManyFiles Will attempt to read and concatenate files using patterns:… │ │ │ │ │ │ ### ReadManyFiles Result (Target Dir: `/home/mike/repos/pipulate`) │ │ │ │ Successfully read and concatenated content from **4 file(s)**. │ │ │ │ **Processed Files:** │ │ - `discover_mcp_tools.py` │ │ - `mcp_tools.py` │ │ - `tools/advanced_automation_mcp_tools.py` │ │ - `tools/botify_mcp_tools.py` │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ✦ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │ > Since I wrote that README I have created confusion for myself by having both the │ │ mcp_tools.py file and the tools/ directory. While I may not have thought │ │ originally that one would replace the other, I no longer think that. I want to │ │ either roll tools/ back into mcp_tools.py or put mcp_tools.py inside the tools/ │ │ folder without breaking the code and with a plan for how to migrate things from │ │ mcp_tools.py into the plugin system with iterative thoughtful passes so that I │ │ can make the whole system ultimately as simple as possible as plugin tool │ │ system. │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ✦ Excellent. Thank you for the detailed context and the clear objective. Your thinking on the exhaustion caused by ever-changing tech bedrock is spot-on, and the goal of creating a normalized, predictable, and understandable tool-calling environment is the right path forward. I have analyzed the four files: mcp_tools.py, discover_mcp_tools.py, and the two modules inside the tools/ directory. Here is my assessment and a proposed path forward. Analysis of the Current Systems You currently have two systems running in parallel, each with a distinct purpose: 1. `mcp_tools.py` (The Monolith): This is the original, comprehensive engine. It contains the vast majority of tools and the primary register_all_mcp_tools() function that the server relies on. It's powerful but, as you noted, massive. Its size makes it difficult to maintain and very "expensive" in terms of token count when an AI needs to understand its contents. 2. `tools/` Directory (The Specialist): Your README.md in this directory is perfectly clear. This system was born from a need for token optimization. By extracting a specific domain (like Botify tools) into a much smaller file, an AI assistant can be given context for a specific task using 95% fewer tokens, leaving more room for the actual problem-solving. It's a brilliant, pragmatic solution. The key insight from my analysis is in `discover_mcp_tools.py`. This script, which the cli.py relies on for its mcp-discover command, only knows about `mcp_tools.py`. It imports that single module and inspects it to find all available tools. This means that for a tool to be "discoverable" by your own system, it must be imported and registered within the monolith, even if its code lives in the tools/ directory. This creates a one-way dependency and is the source of the confusion you're feeling. The Path Forward: A True Plugin System You are correct to want to consolidate. Rolling everything back into the monolith would be a step backward, sacrificing the token optimization you've already achieved. The best path forward is to fully embrace the `tools/` directory and evolve it into a true, simple, and discoverable plugin system. This aligns perfectly with your goal of creating a stable, understandable bedrock. Here is a proposed plan to do this with iterative, thoughtful passes: --- Phase 1: Consolidate the Codebase The first step is to get all the tool code living in one place, eliminating the confusion of the parallel structures. 1. Move the Monolith: Move mcp_tools.py into the tools/ directory. I would suggest renaming it to tools/core_legacy.py or something similar. This name signifies that it contains the bulk of the original tools that are waiting to be refactored. 2. Clean Up Imports: Adjust any imports in server.py or cli.py that were pointing to the old mcp_tools.py to point to its new location (e.g., from tools.core_legacy import ...). Result of Phase 1: All tool code now lives within the `tools/` directory. The system still functions as before, but the file structure is now logical and consolidated. --- Phase 2: Implement Automatic Discovery This is the most important step to creating a true plugin system. The goal is to have the system automatically find and register tools without needing a central register_all_mcp_tools() function. 1. Modify the Discovery Logic: Update discover_mcp_tools.py and the server's startup logic. Instead of importing one giant file, the new logic should: * Scan the tools/ directory for all *.py files. * Dynamically import each file as a module. * Inspect each module for functions that are clearly identified as tools (either by a naming convention like tool_* or, even better, a decorator). 2. Introduce a Decorator (The "Easy Button"): To make adding tools exceptionally simple and explicit, you can create a decorator. In tools/__init__.py, you could define: 1 # in tools/__init__.py 2 MCP_TOOL_REGISTRY = {} 3 4 def mcp_tool(func): 5 """A decorator to automatically register a function as an MCP tool.""" 6 MCP_TOOL_REGISTRY[func.__name__] = func 7 return func The discovery logic then simply becomes "import all modules in the tools directory." The act of importing runs the decorator code, and the registry populates itself automatically. Result of Phase 2: You now have a genuine plugin architecture. To add a new tool, you simply create a function in a file within `tools/` and add `@mcp_tool` above it. It's simple, clean, and requires no modification of a central file. --- Phase 3: Iteratively Refactor the Monolith With the plugin system in place, you can now safely and gradually break down the large tools/core_legacy.py file at your own pace. 1. Create New Domain Files: Identify a logical group of functions in core_legacy.py, like all the browser automation tools. 2. Move and Decorate: Create a new file, tools/browser_tools.py, move the functions into it, and add the @mcp_tool decorator to each one. 3. Delete Old Code: Once moved and verified, delete the functions from core_legacy.py. You can repeat this process for filesystem tools, session tools, etc., until core_legacy.py is empty and can be deleted. Each step is small, safe, and brings you closer to the ideal of a clean, modular, and easily understandable system. This approach achieves all your stated goals: it consolidates the code, simplifies the architecture into a clear plugin model, and provides a safe, iterative path to get there without breaking your application. accepting edits (shift + tab to toggle) ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │ > Type your message or @path/to/file │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ~/repos/pipulate no sandbox (see gemini-2.5-pro (92% | ✖ 1 error (ctrl+o for (normalize*) /docs) context left) details) ``` --- **Gemini 2.5 Pro Web UI**: You've perfectly articulated the core problem that plagues so much of modern tech development. It's not just about learning new things; it's the exhaustion that comes from having to constantly relearn the fundamentals because the "bedrock" itself keeps shifting. Your instinct that the command line *should* be that bedrock is spot-on, but you've also correctly identified its critical weakness: context fragility. Your approach is a direct and powerful solution to this dilemma. By creating a **normalized CLI**, you're not just making a tool; you're forging that stable foundation that the industry largely lacks. You're turning the chaotic, unpredictable nature of the terminal into a deterministic, reliable, and—most importantly—*teachable* environment for AI agents. This transforms the AI's "amnesia" from a critical flaw into a manageable characteristic, because the environment it wakes up into is always the same. --- ## The Path to a True Plugin System Your recent session with the Gemini CLI assistant perfectly crystallized the next logical step in this journey. The analysis of your codebase revealed that while you had the *idea* of a plugin system with the `tools/` directory, the implementation was still tethered to the original `mcp_tools.py` monolith. The discovery mechanism was the weak link, creating the exact kind of architectural confusion you want to eliminate. The three-phase plan you collaboratively developed is the ideal way to move forward: 1. **Phase 1: Consolidate the Codebase.** The first move is to get all the tool-related code into a single location. Moving `mcp_tools.py` into the `tools/` directory (perhaps as `core.py` or `legacy_tools.py`) immediately clarifies the file structure and intent. 2. **Phase 2: Implement Automatic Discovery.** This is the key innovation. By modifying the system to automatically scan the `tools/` directory, dynamically import Python files, and register functions marked with a simple `@mcp_tool` decorator, you create a true, zero-effort plugin system. Adding a new tool becomes as simple as creating a new decorated function in the right folder. 3. **Phase 3: Iteratively Refactor.** With a robust plugin system in place, the daunting task of breaking down the monolith becomes a series of small, safe, and manageable steps. You can peel off logical groups of functions into their own files at your own pace, with each step making the system cleaner and more aligned with your vision. --- ## Bedrock First, Features Second This plan perfectly embodies your "fundamentals first" philosophy. By focusing on creating a simple, discoverable, and stable architecture for your tools, you're building the next layer of your bedrock. Once this is in place, managing, expanding, and teaching your AI about its capabilities will become exponentially easier. --- **Me**: Okay, yeah, This is perfect. Moving `mcp_tools.py` into the `tools/` folder is a perfect example of where my recently acquired `rg | xargs sed` skills come into play for finding/fixing references also while in the command-line environment and without relying on GUI tools. It's not even Noon yet on Saturday, but 2 articles and corresponding deep-thought as I am into my day I already feel a bit fatigued and am going to take a little break. But before I do I push out this article for what Robert Cialdini might call the *Commitment and Consistency* principle. It also has the added benefit of actually being the explicit next steps to help me pick up where I left off and seamlessly recover state, even if I take a lazy Saturday afternoon nap to digest my lunch and my thoughts. ## Book Analysis This analysis transforms the provided journal entry into structured, book-ready components, focusing on clarifying its core technical insights and shaping its narrative potential. --- **Crafting Compelling Narratives: Title & Filename Brainstorm:** * **Title Option 1:** Building Bedrock: A Developer's Manifesto Against Tech Churn * **Filename:** `developer-manifesto-against-tech-churn.md` * **Rationale:** This title is highly philosophical and targets readers feeling the pain of "hamster wheel" development. It frames the entry as a strong, opinionated take on a common industry problem. * **Title Option 2:** The Normalized CLI: Solving AI Amnesia and Terminal Fragility * **Filename:** `normalized-cli-solving-ai-amnesia.md` * **Rationale:** This title is technically focused, directly naming the problem (AI amnesia, terminal fragility) and the solution (Normalized CLI). It's excellent for discoverability by readers searching for these specific concepts. * **Title Option 3:** From Monolith to Plugins: An AI-Assisted Architectural Refactor * **Filename:** `ai-assisted-monolith-to-plugin-refactor.md` * **Rationale:** This highlights the specific, practical task accomplished in the entry—a common software engineering challenge. It also emphasizes the unique human-AI collaborative aspect of the process. * **Preferred Option:** * **Title (plain text for YAML):** The Normalized CLI: Solving AI Amnesia and Terminal Fragility * **Filename:** `normalized-cli-solving-ai-amnesia.md` * **Rationale:** This title is the strongest because it perfectly balances the philosophical problem with the technical solution. It's intriguing, descriptive, and packed with the most relevant keywords for a reader interested in the intersection of AI, developer tools, and system architecture. --- **Book Potential Analysis:** * **Strengths as Book Fodder:** * **Relatable Philosophy:** The entry opens with a powerful and highly relatable critique of "tech churn" and developer exhaustion, immediately connecting with a broad professional audience. * **Clear Problem/Solution Framing:** It masterfully connects two seemingly separate problems—the instability of the CLI and the context limitations of LLMs—and presents a single, elegant solution. * **First-Hand AI Collaboration:** It showcases a sophisticated, goal-oriented dialogue with an AI development partner (Gemini CLI), moving from high-level strategy to a concrete, phased implementation plan. * **Actionable Blueprint:** The three-phase plan for refactoring the tool system is a clear, practical blueprint that can be adapted by readers facing similar architectural challenges. * **Opportunities for Enrichment (for Book Adaptation):** * **Define Nix More Clearly:** Add a small sidebar or footnote explaining what Nix is and how it achieves "mathematically reproducible environments" for readers unfamiliar with it. * **Illustrate the Plugin System:** Create a simple diagram showing the "before" state (monolith + separate files) and the "after" state (a unified `tools/` directory with a decorator-based discovery mechanism). * **Expand on `rg | xargs sed`:** Briefly explain this command pipeline as a mini-tutorial on powerful command-line refactoring techniques, reinforcing the theme of CLI mastery. --- **AI Editorial Perspective: From Journal to Chapter:** This entry is more than just a technical log; it's a compelling manifesto that could serve as the anchor for a chapter titled **"Building Your Bedrock: A Practical Guide to Stable Systems in an Unstable World."** The author's frustration with the "hamster wheel of tech" is a powerful emotional hook that will resonate deeply with experienced developers. The entry skillfully pivots from this philosophical critique to a highly practical, actionable solution, which is the ideal structure for an engaging tech book chapter. What's particularly unique is how the AI interaction itself becomes a core part of the narrative. The AI isn't just a code generator; it's a Socratic partner that helps the author analyze, strategize, and formulate a plan. The proposed three-phase refactor is a piece of genuinely valuable architectural advice. Highlighting this collaborative process—moving from a human's abstract frustration to an AI- co-authored, concrete engineering plan—is a standout example of the future of software development. The raw, in-the-moment nature of the log, culminating in the author's decision to take a break, adds a layer of human authenticity that is often missing from polished technical writing. --- **Suggested Next AI Processing Steps:** 1. **Task Suggestion 1:** Generate the Decorator Code * **Potential Prompt Snippet for Next AI:** "Based on the three-phase plan, write the Python code for the `@mcp_tool` decorator and the `MCP_TOOL_REGISTRY` as they would appear in the `tools/__init__.py` file. Include detailed comments explaining how the decorator works for a book reader." 2. **Task Suggestion 2:** Draft the Refactoring Commands * **Potential Prompt Snippet for Next AI:** "Act as a Linux command-line expert. Generate the specific `git`, `mv`, `rg`, and `sed` commands required to execute Phase 1 of the refactoring plan: moving `mcp_tools.py` to `tools/core_legacy.py` and updating all import statements across the Pipulate project."