# Maur 🐜

Inspired by [Strip's Minions](https://stripe.dev/blog/minions-stripes-one-shot-end-to-end-coding-agents). Maur (Norwegian for "ants") is an autonomous coding agent that integrates into your Python projects. It receives tasks from various sources — production error alerts, Slack, Linear issues, or direct API calls — clones your repo, runs an AI coding agent ([OpenCode](https://opencode.ai)), and opens a pull/merge request with the fix.

## How it works

1. A task arrives via webhook or direct API call
2. The API stores the task and publishes it to a message queue
3. A worker picks up the task, clones your repo into a temporary workspace
4. OpenCode runs against the cloned repo using your configured LLM
5. If changes are made, they are committed and pushed to a new branch (`maur/<task-id>`)
6. A pull request (GitHub) or merge request (GitLab) is opened automatically

## Architecture

```
[Trigger source]           [maur_api]          [maur_code_subscriber]
  Linear webhook    --->   FastAPI app   --->   Worker (OpenCode)
  Exception alert          stores task          clones repo
  Manual POST /tasks       publishes msg        runs agent
                                                opens PR/MR
```

The two components are deployed separately via `takk`:
- **`maur_api`** — lightweight FastAPI service that authenticates requests, persists tasks, and enqueues work
- **`maur_code_subscriber`** — NATS subscriber that processes tasks one at a time using OpenCode

## Prerequisites

- Python ≥ 3.10
- [`takk`](https://pypi.org/project/takk/) for infrastructure management
- A NATS server (provisioned by `takk`)
- A PostgreSQL or MySQL database (provisioned by `takk`)
- An OpenAI-compatible LLM API (e.g. [OpenRouter](https://openrouter.ai), a local Ollama instance, or any provider with an OpenAI-compatible endpoint. `takk` default to using Ollama unless you overwrite the env vars.)
- A GitHub or GitLab repository with a token that has push and PR/MR creation permissions

## Installation

```bash
uv add maur
```

## Basic usage

### 1. Add the infrastructure

Add both components to your `project.py` file:

```python
from maur.components import maur_api, maur_code_subscriber

project = Project(
    name="your-project",

    # The API that authenticates and enqueues tasks
    maur_api=maur_api(),

    # The worker that clones the repo, runs OpenCode, and opens a PR/MR
    # Pass secrets for any environment variables your target repo needs at build time
    maur_coder=maur_code_subscriber(secrets=[...]),
)
```

### 2. Configure secrets

Run `takk dotenv` to regenerate your `.env` file, then fill in the required values:

| Variable | Required | Description |
|---|---|---|
| `DB_URI` | No | PostgreSQL (`postgresql+asyncpg://...`) or MySQL (`mysql+aiomysql://...`) connection URI |
| `NATS_URI` | No | NATS (`nats://...`) |
| `MAUR_BEARER_TOKEN` | Yes | Secret token used to authenticate API requests |
| `MAUR_LLM_TOKEN` | No | API key for your LLM provider |
| `MAUR_LLM_API` | No | Base URL of your OpenAI-compatible LLM API |
| `MAUR_LLM_MODEL` | No | Model to use (default: `qwen3.5:4b`) |
| `GITHUB_REPO_URL` | Yes* | HTTPS URL of the GitHub repo to clone and open PRs on |
| `GITHUB_TOKEN` | Yes* | GitHub personal access token with `repo` scope |
| `GITHUB_API_URL` | No | GitHub API base URL (default: `https://api.github.com`) |
| `GITLAB_REPO_URL` | Yes* | HTTPS URL of the GitLab repo |
| `GITLAB_TOKEN` | Yes* | GitLab personal access token with `api` scope |
| `GITLAB_URL` | No | GitLab instance URL (default: `https://gitlab.com`) |

\* Provide either the GitHub **or** GitLab set of variables. GitLab takes precedence if both are set.

### 3. Start the system

```bash
takk up
```

Both the API and worker containers will be built and started.

## API reference

All endpoints (except `/health`) require a `Bearer` token in the `Authorization` header matching `MAUR_BEARER_TOKEN`.

### POST `/tasks` — Manual task

Send any arbitrary prompt to the agent.

```bash
curl -X POST http://localhost:8000/tasks \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Refactor the payment module to use the new Stripe SDK",
    "source_id": "unique-identifier-for-dedup",
    "repo_branch": "main"
  }'
```

### POST `/webhooks/exception` — Exception alert

Send a production error for the agent to fix. `fingerprint` is used for deduplication — tasks with the same fingerprint that are already pending or in progress are rejected.

```bash
curl -X POST http://localhost:8000/webhooks/exception \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "fingerprint": "KeyError-user-profile-views-42",
    "title": "KeyError: '\''email'\'' in user_profile view",
    "description": "Traceback (most recent call last):\n  ...",
    "repo_branch": "main",
    "extra": {"environment": "production", "user_id": 123}
  }'
```

### POST `/webhooks/linear` — Linear webhook

Configure a [Linear webhook](https://linear.app/docs/webhooks) to send issue events here. Maur extracts the issue title, description, and repository URL (must be attached to the issue) and opens a PR with the fix.

Set the webhook URL to: `https://<your-api-host>/webhooks/linear`

Optionally set `LINEAR_WEBHOOK_SECRET` in your environment to verify webhook signatures.

### GET `/tasks` — List tasks

Returns the 50 most recent tasks.

### GET `/tasks/{task_id}` — Get task

Returns the status and result of a specific task.

### GET `/health`

Returns `"ok"`. Used for health checks.

## Customisation

### Changing the LLM model

Set `MAUR_LLM_MODEL` to any model available through your `MAUR_LLM_API` provider. The worker uses OpenCode with an OpenAI-compatible provider, so any model exposed via that protocol works.

```
MAUR_LLM_MODEL=devstral-2-123b-instruct-2512
```

### Adjusting worker compute resources

The default worker is allocated 3 GB of memory. Override this via the `compute` argument:

```python
from takk.models import Compute
from maur.components import maur_code_subscriber

maur_coder=maur_code_subscriber(
    compute=Compute(mb_memory_limit=1024 * 8)  # 8 GB
)
```

### Passing additional secrets to the worker

If your target repository requires environment variables at build or runtime (e.g. private package indexes), pass them through `secrets`:

```python
from maur.components import maur_code_subscriber
from my_project.settings import MyPrivateRegistrySettings

maur_coder=maur_code_subscriber(
    secrets=[MyPrivateRegistrySettings, ...]
)
```

## Development

```bash
# Install dependencies
uv sync --all-groups

# Lint
uv run ruff check .

# Type check
uv run ty check
```
