<p align="center">
  <img src="assets/moonpi-logo.svg" alt="moonpi" width="520" />
</p>
<p align="center"><em>opinionated set of extensions for pi</em></p>

---

**moonpi** is set of extensions for [pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) **that are actually useful** for a coding agent.
No fluff. No over-engineered architecture. Just practical guardrails, structured workflows, and pragmatism.

---

## Opinions

moonpi is built around a few **strong opinions**.

If these **resonate** with you, you might **find** this set of extensions **useful**:

- **Subagents are a waste of tokens.**
  Subagents are useless and just a conspiracy to make us use more tokens.
  *Tin foil hat firmly on.*

- **Plan mode is actually useful - but only if history is retained.**
  Plan mode becomes pointless when the model outputs a "plan" and then loses all the context that produced it.
  Planning only works when the planning conversation remains part of the execution context.

- **Context is king.**
  Why make the model guess which files matter for a task, wasting tokens and time wandering through the project, when you can inject the relevant files directly into the context?
  Context matters more than models or harnesses, control the context and you control the result.

- **Rejecting reads and writes outside the working directory is good steering.**
  Yes, we all know the model can write and execute code, and yes, sandboxing is hard.
  But telling the model "hey, you cannot read that" when it reaches outside the project helps steer behavior.
  May our benevolent AI overlords accept this humble suggestion.

- **Read before write is mandatory.**
  If a model tries to overwrite a file without reading it first, that is an error.
  It should not be allowed. Period.

- **Prompt cache affinity matters.**  
  Switching system prompts or tool definitions between phases destroys the provider's prompt cache, forcing a full re-read of the system prompt on every turn. The phase transition should be invisible to the cache: same prompt, same tool schemas, only runtime behavior changes.

- **Loops are stupidly simple.**  
  A specification file, a TODO list, and auto-compaction after every phase are more than enough for most use cases.

## Installation

First, install the base `pi` coding agent globally:

```bash
npm install -g @earendil-works/pi-coding-agent
```

Then, install the `moonpi` extension set directly via `pi`:

```bash
pi install npm:moonpi
```


## Screenshots

<p align="center">
  <img src="assets/screenshots/moonpi-startup.png" alt="moonpi startup with project context notification and mode selector" width="600" />
  <br />
  <em>moonpi startup</em>
</p>

<p align="center">
  <img src="assets/screenshots/moonpi-picker.png" alt="The /pick file-tree picker for selecting project context files" width="600" />
  <br />
  <em>the /pick file-tree picker</em>
</p>

<p align="center">
  <img src="assets/screenshots/moonpi-working.png" alt="moonpi working on its own codebase" width="600" />
  <br />
  <em>moonpi working on its own codebase</em>
</p>

## Features

moonpi adds a practical workflow layer on top of `pi`, focused on:

- explicit work modes
- persistent planning context with prompt cache retention across phase transitions
- TODO-driven execution
- safer file access behavior
- pickable project documentation context and `/context` inspection
- sprint-based long-running workflows
- phase-by-phase execution loops


## Modes

moonpi provides four modes (Auto Mode has two phases):

| Mode | Available Tools | Description |
| --- | --- | --- |
| **Plan Mode** | `read`, `grep`, `find`, `ls`, `todo`, `question` | Used to reason about the task before making changes. The model must produce a TODO list. No file modifications or shell commands are allowed. |
| **Act Mode** | `read`, `grep`, `find`, `ls`, `bash`, `edit`, `write`, `todo`, `question` (+ `end_phase` in sprint loop) | Used to execute work. The model can read, write, edit, and run commands. TODO and QnA can be used when helpful, but are not mandatory. |
| **Auto Mode** (Plan phase) | `read`, `grep`, `find`, `ls`, `todo`, `question`, `end_conversation` | Default mode. The model first plans, then acts while retaining the full planning conversation history. During the planning phase, an `end_conversation` tool is available for question-only requests that do not need a TODO list. |
| **Auto Mode** (Act phase) | `read`, `grep`, `find`, `ls`, `bash`, `edit`, `write`, `todo`, `question` (+ `end_phase` in sprint loop) | After planning, the model executes the TODO list with full planning context intact. |
| **Fast Mode** | `read`, `grep`, `find`, `ls`, `bash`, `edit`, `write` (+ `end_phase` in sprint loop) | Direct execution mode. No planning requirement, no TODO list, no QnA. Useful for quick edits and simple tasks. |

Switch modes with `/moonpi:mode` or `/moonpi:mode <plan|act|auto|fast>`.

Each mode has a different textbox color in the UI, making the current workflow state immediately visible.

## Auto Mode

**Auto Mode** is the default moonpi workflow.

It works in two phases:

1. **Plan phase**
   - editing tools are disabled at runtime (blocked by guards, not removed from the prompt)
   - the TODO list tool is enabled
   - the model plans the work
   - the model either:
     - produces a TODO list, then continues to Act phase
     - or calls `end_conversation` when the user is only asking a question

2. **Act phase**
   - editing tools become available (guards no longer block them)
   - the conversation history from the Plan phase is retained
   - the model executes the TODO list with full planning context intact

This avoids the "plan then forget everything" problem.

### Prompt Cache Retention

Auto Mode is specifically designed to preserve the LLM provider's **prompt cache** across the Plan→Act transition. This means:

- **Same system prompt.** Both phases use the identical system prompt text. The phase instructions are embedded in a single `AUTO_MODE_PROMPT` that covers both Plan and Act behavior — the prompt never changes between phases.
- **Same tool schemas.** All moonpi tools (`read`, `grep`, `find`, `ls`, `bash`, `edit`, `write`, `todo`, `question`, `end_conversation`, `end_phase`) are always present in the tool definitions sent to the provider. Tools are never added or removed between phases.
- **Runtime enforcement only.** In Plan phase, editing tools (`bash`, `write`, `edit`) are blocked by moonpi's runtime guards — the `tool_call` event handler rejects them with an error message. The tool definitions remain in the prompt. When the phase switches to Act, the guards simply stop blocking those tools.
- **Stable tool schemas for cache.** Even in Fast mode where `todo` is disabled, its JSON schema is still advertised to the provider. This keeps the tool definition block identical across all modes and phases, maximizing cache hits.

**Why this matters:** providers cache the system prompt and tool definitions across turns. When the prompt or tool list changes, the cache is invalidated and the entire prefix must be re-processed — costing tokens, money, and latency. By keeping the prompt and tool schemas stable, Auto Mode ensures the cache is retained throughout the entire conversation, from Plan through Act and beyond.

## Project Context Picker

moonpi injects only the files selected with `/pick` into the model context.

### `/pick`

Opens a file-tree picker for the current working directory:

- `README.md`, `SPECS.md`, and `SPRINT.md` context files are selected by default
- use `↑` / `↓` to move
- use `←` / `→` to close and open folders
- use `Space` to select or deselect files and folders
  - selecting a folder recursively selects **all** descendant files that match the pickable extensions filter
  - selecting a folder with all descendants already selected will deselect them all
- use `D` to deselect everything
- use `Enter` to confirm and close the picker

The picker only shows files with pickable extensions (code, text, config, and documentation files). Binary files, images, lock files, and other non-text files are hidden. The extension filter is configurable via `pickableExtensions` (see [Configuration](#configuration)).

### `/context`

Shows which files are currently selected for prompt injection and whether they were auto-discovered at startup or manually selected via `/pick`.

Example output:

```
3 file(s) auto-discovered (use /pick to change):
  README.md
  SPECS.md
  SPRINT.md
```

```
5 file(s) manually selected with /pick:
  README.md
  src/config.ts
  src/context-files.ts
  src/types.ts
  src/modes.ts
```

At startup, a notification shows which files are currently selected for injection.

### `/context:clear`

Deselects all currently active context files (both `/pick`-selected and auto-discovered). After clearing, no files are injected into the prompt until you run `/pick` again.

Example output:

```
Cleared context file selection (3 file(s) deselected). Use /pick to select files.
```

## Custom Providers

moonpi includes the support from some custom providers, and provides slash commands to manage custom providers in `~/.pi/agent/models.json`.


### Synthetic Provider

Moonpi registers Synthetic as the `synthetic` provider using the OpenAI-compatible endpoint.

Configure credentials with either:

```bash
export SYNTHETIC_API_KEY=...
```

or run:

```text
/login
```

Use `/model` to select a `synthetic` model. Use `/synthetic:quotas` to show your weekly token and rolling 5h usage quotas:

![Synthetic quotas output](assets/screenshots/moonpi-syn.png)

#### Web Search

When authenticated with Synthetic, moonpi makes a `web_search` tool available to the agent. This tool uses the [Synthetic search API](https://docs.synthetic.new) to perform zero-data-retention web searches and return results with title, URL, published date, and text excerpt.

The tool is **only visible to the LLM when logged in with Synthetic** — it is not registered at all when no API key is configured, so the model never sees it or knows it exists.

To disable the search tool even when logged in, set `synthetic.search.enabled` to `false` in your config:

```json
{
  "synthetic": {
    "search": {
      "enabled": false
    }
  }
}
```

### Wafer Provider

Moonpi registers [Wafer](https://wafer.ai) as the `wafer` provider using the OpenAI-compatible endpoint at `https://pass.wafer.ai/v1`.

Configure credentials with either:

```bash
export WAFER_API_KEY=...
```

or run:

```text
/login wafer
```

Use `/model` to select a `wafer` model.

To disable the Wafer provider entirely, set `wafer.enabled` to `false` in your config:

```json
{
  "wafer": {
    "enabled": false
  }
}
```

### Managing Custom Providers

moonpi provides five slash commands to manage custom providers in `~/.pi/agent/models.json`:

#### `/custom-provider:add-provider`

Interactive wizard that adds a new custom provider. Prompts for:

1. **Provider name** — a unique identifier (e.g. `my-vllm`)
2. **API type** — select from all supported APIs (`openai-completions`, `anthropic-messages`, `google-generative-ai`, etc.)
3. **Base URL** — the API endpoint (sensible defaults per API type)
4. **API key** — your API key or an environment variable name

Example `models.json` result:

```json
{
  "providers": {
    "my-vllm": {
      "baseUrl": "http://127.0.0.1:8000/v1",
      "api": "openai-completions",
      "apiKey": "none",
      "models": []
    }
  }
}
```

#### `/custom-provider:add-model`

Adds a model to an existing custom provider. Prompts for:

1. **Provider** — select from existing custom providers
2. **Model ID** — the model identifier (e.g. `Qwen/Qwen3-27B`)
3. **Display name** — optional human-readable name
4. **Advanced options** — optional configuration for reasoning, context window, max tokens, image input, and API type override

#### `/custom-provider:scan-models`

Auto-detects models from an OpenAI-compatible provider endpoint. Works with providers using `openai-completions` or `openai-responses` API.

1. **Select provider** — choose from OpenAI-compatible custom providers
2. **Scan** — fetches `/v1/models` from the provider's base URL
3. **Select models** — checkbox UI shows all discovered models; already-added models are greyed out
   - `Space` to toggle individual models
   - `a`/`A` to select/deselect all
   - `Enter` to confirm (adds all new models if none selected)
4. Auto-fills `contextWindow` from `max_model_len` when available

#### `/custom-provider:remove-provider`

Removes a custom provider and all its models from `models.json`. Asks for confirmation before deleting.

#### `/custom-provider:remove-model`

Removes a single model from a custom provider. Prompts for the provider, then the model to remove, with confirmation.

After any change, run `/reload` to refresh pi's model registry and make new models available in `/model`.

## Moonpi loop

moonpi includes sprint-oriented loop for larger projects.

### `/sprint:init`

Creates a new sprint for a larger project.

This command asks **one question**: the sprint objective.

It then delegates SPRINT.md and TASKS.md creation to the agent, which writes:

```txt
./sprints/<sprint_number>/SPRINT.md
./sprints/<sprint_number>/TASKS.md
```

The sprint is divided into phases.

Each phase includes tasks and verification steps that define when the phase is complete.

The goal is to turn a vague big project into a concrete, phased execution plan.

### `/sprint:loop`

Runs the latest sprint phase-by-phase. Automatically picks the most recent sprint.

The loop works like this:

1. complete one phase
2. mark completed tasks in:

```txt
./sprints/<sprint_number>/TASKS.md
```

3. (optional) auto-commit changes with git
4. transition the context (clear or compact) and proceed to the next phase
5. repeat until the sprint is complete

The model signals the end of a phase by calling a special `end_phase` tool.

This keeps long-running projects simple, resumable, and grounded in actual files.

#### Sprint configuration

The sprint loop behavior is configurable via the `sprint` section in `moonpi.json`:

```json
{
  "sprint": {
    "contextAction": "clear",
    "autoCommit": true
  }
}
```

| Option | Default | Description |
|---|---|---|
| `sprint.contextAction` | `"clear"` | How the context transitions between phases. `"clear"` aggressively minimizes the previous conversation (near-fresh start, context files like SPECS.md are re-injected automatically). `"compact"` preserves a richer summary of the completed phase. |
| `sprint.autoCommit` | `true` | When `true`, moonpi detects if the project uses git and automatically commits changes at the end of each completed phase with a message like `Sprint <n> Phase <id>: <summary>`. No commit is made if there are no changes or git is not initialized. |

### Does it work?

Watch a drastically sped-up video of `Qwen/Qwen3.6-27B` working unattended for over an hour on this sprint prompt:

```text
create WebOS a fully functional web-based operating system with apps, games and everything
```

https://github.com/user-attachments/assets/92670a55-a3c4-4c31-a4a2-0afc449f0137


And judge the result yourself [here](https://qwen36-27b-moonpi-webos.netlify.app/).


## Configuration

Configure `.pi/moonpi.json` (project) or `~/.pi/agent/moonpi.json` (global):

```json
{
  "defaultMode": "auto",
  "preserveExternalTools": false,
  "customEditor": true,
  "synthetic": {
    "search": {
      "enabled": true
    }
  },
  "wafer": {
    "enabled": true
  },
  "sprint": {
    "contextAction": "clear",
    "autoCommit": true
  },
  "contextFiles": {
    "enabled": true,
    "fileNames": ["README.md", "SPECS.md", "SPRINT.md"],
    "maxTotalBytes": 120000,
    "maxDepth": 4,
    "maxScannedEntries": 10000,
    "maxDefaultFiles": 25,
    "pickableExtensions": [
      ".ts", ".js", ".py", ".rs", ".go", ".md", ".json", ".yaml",
      ".toml", ".sql", ".html", ".css", "Dockerfile", "Makefile"
    ],
    "ignoreDirs": [
      ".git", ".svn", ".hg",
      "node_modules", ".next", ".turbo",
      ".venv", "venv", "__pycache__",
      "target", "dist", "build", "coverage",
      ".env", ".gradle", ".idea", ".vscode"
    ]
  },
  "guards": {
    "cwdOnly": true,
    "allowedPaths": ["~/.pi/agent"],
    "readBeforeWrite": true
  },
}
```

#### General

| Field | Default | Description |
| --- | --- | --- |
| `defaultMode` | `"auto"` | Mode used at session start. One of `"plan"`, `"act"`, `"auto"`, `"fast"` |
| `preserveExternalTools` | `false` | When `true`, tools registered by other extensions are kept alongside moonpi tools when applying mode tool restrictions |
| `customEditor` | `true` | When `false`, moonpi skips installing its mode-colored editor, preserving editor customizations from other extensions |

#### Synthetic

| Field | Default | Description |
| --- | --- | --- |
| `synthetic.search.enabled` | `true` | When `false`, the `web_search` tool is not registered even if logged in with Synthetic |

#### Wafer

| Field | Default | Description |
| --- | --- | --- |
| `wafer.enabled` | `true` | When `false`, the Wafer provider is not registered at startup |

#### Context files

| Field | Default | Description |
| --- | --- | --- |
| `enabled` | `true` | Enable or disable context file injection entirely |
| `fileNames` | `["README.md", "SPECS.md", "SPRINT.md"]` | Filenames auto-discovered at startup (before any `/pick` selection) |
| `maxTotalBytes` | `120000` | Maximum total bytes injected into the prompt. Files are truncated beyond this limit |
| `maxDepth` | `4` | Maximum directory depth for **startup auto-discovery only**. The `/pick` picker has no depth limit |
| `maxScannedEntries` | `10000` | Maximum filesystem entries inspected during any scan (startup or `/pick`). Prevents walking huge trees |
| `maxDefaultFiles` | `25` | Maximum files selected by auto-discovery. Does not affect `/pick` selections |
| `pickableExtensions` | *(80+ entries, see defaults)* | File extensions (`.ts`, `.py`) and exact filenames (`Dockerfile`, `Makefile`) shown in `/pick`. Non-matching files are hidden from the picker |
| `ignoreDirs` | *(40+ entries, see defaults)* | Directory names skipped during both auto-discovery and `/pick`. Covers VCS, language-specific caches, build output, IDE folders, and more |

#### Guards

| Field | Default | Description |
| --- | --- | --- |
| `cwdOnly` | `true` | Reject file access outside the current working directory |
| `allowedPaths` | `[]` | Directories permitted for **read** access even when `cwdOnly` is enabled. Supports `~` expansion and relative paths (resolved from cwd) |
| `readBeforeWrite` | `true` | Require the model to read a file before writing or editing it |

Recommended tuning for huge monorepos:

- lower `maxDepth` to `2` or `3` if READMEs are mostly near the root
- lower `maxScannedEntries` to cap worst-case startup and per-picker-session scanning latency
- add generated/vendor folders to `ignoreDirs`
- narrow `pickableExtensions` to only the file types you care about
- lower `maxDefaultFiles` if too many READMEs are injected by default
- set `enabled: false` and use manual `@file` references if you do not want automatic context injection

If a scan hits a limit, moonpi shows a warning in startup notifications or `/pick`.

The system prompt also instructs the model to keep selected project documents up to date, making `README.md` and `SPECS.md` living project documents instead of abandoned archaeology.

## TODO List Tool

moonpi includes a TODO list tool that can be updated at any moment.

Whenever an item is modified, the current full state of the TODO list is returned to the model.

This keeps the agent grounded in the actual progress of the task instead of relying on vibes, memory, or whatever it hallucinated three messages ago.

## Filesystem Guardrails

moonpi adds practical file access rules.

### Stay inside the working directory

Reads, writes, and edits outside the current working directory are rejected.

This is not presented as magical security theater. It is a behavioral constraint that helps steer the model toward project-scoped work.

#### Allowed paths

When `cwdOnly` is enabled, you can grant the agent read access to specific directories outside the project via `guards.allowedPaths`. This is useful when the agent needs to read its own documentation, extension files, or shared configs that live outside the working directory.

```json
{
  "guards": {
    "allowedPaths": [
      "~/.pi/agent",
      "/home/user/Projects"
    ]
  }
}
```

Paths support `~` expansion (resolved to home directory) and relative paths (resolved from cwd). Only **read** access is granted for allowed paths - writes and edits are still restricted to the working directory.

The setting works in both global (`~/.pi/agent/moonpi.json`) and project-level (`.pi/moonpi.json`) configs.

### Read before write

moonpi requires the model to read a file before writing to it.

If the model tries to write to a file without reading it first, the write tool returns an error.

This prevents careless overwrites and forces the agent to inspect the current state of a file before modifying it.

## Why moonpi?

moonpi is not trying to be a giant agent framework.

It does not try to invent a new architecture for every task.

It does not summon a committee of subagents to discuss whether a file should be edited.

It gives the coding agent a simple structure:

* plan when needed
* act when needed
* keep the plan in context
* inject the right project files up front
* track work explicitly
* stay inside the project
* read before writing
* compact after meaningful phases
* keep project docs alive

That is usually enough.

And when it is not enough, it is still better than burning tokens on fake organizational charts for imaginary agents.


> **Fun Fact:** I used `moonpi` to build `moonpi`, making it a bootstrapped coding agent.

## License

MIT
