Maajun
Error monitoring that files the bug report — and can write the fix.
Maajun watches your error sources, investigates each new error against your
source code, and documents it on GitHub. In suggest mode it opens an issue
containing the root-cause analysis and a proposed patch. In fix mode it
applies the patch on a branch, runs your test suite against it, and opens a
pull request with the result.
Nothing merges without your review, in either mode.
Contents — Requirements · Installation · Quick start · How it works · Chat · Modes · AI providers · Configuration · Commands · Cost control · Security model · Documentation
Requirements
- Python 3.11 or newer
- An API key for DeepSeek or OpenAI — see AI providers
- Optional: a GitHub token, to open issues and pull requests
Log-file monitoring reads files on the local machine, so the daemon has to run on the server that writes them. The GitHub Actions monitor works from anywhere with network access.
Installation
uv tool install maajun # or: pipx install maajun / pip install maajun
From source:
git clone https://github.com/Morvin-Ian/maajun && cd maajun && uv sync
Quick start
Investigate one issue
No monitoring setup required — describe the problem and let maajun read the code:
maajun setup # pick a provider, store its key
maajun report "Checkout 500s when the cart is empty"
This clones the target repo, analyzes it, and files an issue with a root-cause
report. Add -m fix to open a pull request with the fix applied, or
--dry-run to print the analysis and its cost without touching GitHub.
Monitor continuously
maajun setup # provider key, GitHub, log files, GitHub Actions
maajun watch --dry-run # analyze real errors without opening PRs
maajun watch # run the daemon
setup asks for the provider and its key, then offers GitHub, log files, and
GitHub Actions in turn — each skippable with Enter, so a minimal install is a key and a log
path. It detects the repo from your origin remote, and re-runs safely: every
prompt defaults to your current configuration.
Every prompt also has a flag, so once a key is in the keyring the rest can be reconfigured unattended:
maajun setup --non-interactive --repo you/yourapp --logs /var/log/app/error.log
maajun setup --non-interactive --provider openai --repo you/yourapp \
--logs /var/log/app/error.log
--non-interactive cannot store a new API key — there is nowhere to prompt —
so run maajun setup interactively once per machine first.
Without a configured repo, maajun runs in local mode: errors are still
detected and analyzed, but each report is written to
<workdir>/reports/<fingerprint>.md instead of opening a pull request.
How it works
- Detect — a monitor picks up a new error from a log file or a failed GitHub Actions run.
- Deduplicate — each error is fingerprinted. The same error is never reported twice; repeat sightings bump a counter on the existing incident.
- Analyze — the agent reads the relevant source in a clone under
daemon.workdirand produces a what happened / root cause / suggested fix report, including the commit that likely introduced the bug. - Report — an issue in
suggestmode, or a branch, a test run, and a pull request infixmode.
Each incident records its token count and cost, viewable with
maajun incidents.
Example report
The shape of a filed issue, abridged:
[maajun] KeyError: 'discount'
What happened — Checkout raised an unhandled
KeyErrorfor carts created before a promotion was attached. 41 requests hit it in 12 minutes; every one returned a 500 at the payment step.Root cause —
cart/totals.py:88readscart["discount"]directly. The key is only written bypromotions.apply()(cart/promotions.py:23), which returns early when no promotion matches — so the key is absent rather than zero.Likely cause commit —
4f1c9ab"only apply promotions when one matches", which added that early return.Suggested fix
- discount = cart["discount"] + discount = cart.get("discount", Decimal("0"))Source:
logfile:/var/log/shop/error.log· First seen:2026-08-03T03:14:22Z· Fingerprint:9f3c1ab77e02d418
In fix mode the same analysis arrives as a pull request: the applied diff, the
report committed as docs/incidents/<fingerprint>.md, and your test suite's
verdict at the top of the body.
✅ Tests pass —
pytest -q
A failing suite (❌ Tests fail (exit 1)) still opens the PR — a fix that
breaks the tests is exactly what a reviewer needs to see.
Chat
maajun chat is a conversational front end to everything above. It knows
every command — the list is read from the CLI itself, so it is never out of
date — and it remembers every incident maajun has handled.
maajun chat
Ask it how to do something and it answers from the real --help. Ask it to
do the thing and it runs the command, showing you the exact line first if
it changes anything:
> watch acme/web as well, in fix mode, verified with pytest
▸ Run: maajun add-repo acme/web -m fix
Run it? (y/N): y
Answers stream as they are written, and each command or file edit is shown
as it runs. maajun chat -p "..." answers one question and exits, for a
script or a keybinding.
Read-only commands run straight away. watch, reset, and sign-out are
never run from chat — it hands you the command instead. And because every
incident, pull request, and issue is already in the database, so is the
history:
> what did that checkout KeyError turn out to be?
> which PRs have you opened against acme/api this month?
Past chat sessions are searchable too — by words in any order, over any
date range — and chat spend is recorded (/cost) and capped by
chat.max_usd_per_day, separately from the daemon's budget.
See the command reference
for the slash commands and the full permission model.
Modes
| Mode | Files | Output |
|---|---|---|
suggest (default) |
Read-only | Issue with analysis and a suggested patch |
fix |
Edits its own clone under daemon.workdir |
Branch + test run + pull request |
Switch with maajun config github.mode fix, or per run with
-m/--mode on watch and report.
Fix mode may still conclude that no code change is warranted. When it does, you get an issue rather than a pull request whose only diff is the incident report — the issue says why.
AI providers
DeepSeek and OpenAI are both fully supported and interchangeable — pick either
during maajun setup.
| DeepSeek | OpenAI | |
|---|---|---|
ai.provider |
deepseek |
openai |
| Default model | deepseek-v4-flash |
gpt-4o-mini |
With ai.thinking_mode |
deepseek-v4-pro |
gpt-4o |
| API key | platform.deepseek.com | platform.openai.com/api-keys |
Switch at any time — the key for each provider is stored separately, so moving back and forth costs nothing:
maajun config ai.provider openai
maajun config ai.model gpt-4o # override the default model
maajun provider-list # which providers have a key stored
Both speak the same /chat/completions protocol, so any compatible gateway,
proxy, or self-hosted server works too — point ai.base_url at it, and
ai.provider still selects the request dialect and model defaults.
Configuration
Settings live in ~/.config/maajun/config.toml; pass -c/--config PATH to any
command to use another file. setup writes it for you, and maajun config
edits it in place with validation and comment preservation:
maajun config # print the whole config (secrets masked)
maajun config github.mode # print one value
maajun config github.mode fix # set a value (every repo, if several)
maajun config github.mode fix -r team/api # ...or just one of them
A minimal config:
[ai]
provider = "deepseek" # or "openai"
# model = "gpt-4o-mini" # provider default if omitted
# thinking_mode = true # use the provider's reasoning model
# One [[github.repos]] entry per repository, added with `maajun add-repo`.
# Omit the section entirely for local mode.
[[github.repos]]
repo = "owner/name"
base_branch = "main"
mode = "suggest"
# test_command = "pytest -q" # verifies a fix-mode edit; result goes in the PR
[monitor]
log_files = ["/var/log/myapp/error.log"]
poll_interval = 30
# github_actions_repos = ["owner/name"]
[daemon]
workdir = "~/.local/share/maajun" # clones, incident DB, state
# max_usd_per_day = 5.0 # 0 = no cap
[chat]
# max_usd_per_day = 5.0 # `maajun chat`'s own budget; 0 = no cap
At least one error source — log files or GitHub Actions — must be configured; the daemon refuses to start with nothing to watch. See the monitoring guide for every key, detection tuning, and multi-repo setups.
Credentials are never stored in this file. Keys and tokens are read only
from the OS keyring, under the service name maajun — maajun setup puts them
there. Nothing else is consulted — not environment variables, and not a
gh auth login session — so there is exactly one place to look when status
and the daemon disagree, and the token maajun pushes with cannot change without
maajun being told.
This means maajun needs a working keyring backend. On a headless server,
install one (keyrings.alt, gnome-keyring) before running setup.
Commands
| Command | Purpose |
|---|---|
maajun setup |
Configure everything — provider key, GitHub, error sources |
maajun status |
Preflight check of credentials, repo access, and monitors |
maajun watch |
Run the monitoring daemon (--once, --dry-run, -m) |
maajun report "…" |
Investigate an issue you describe, on demand |
maajun chat |
Ask maajun anything, have it run commands, recall past work |
maajun incidents |
List handled incidents with repo, status, cost, and links |
maajun config [KEY] [VALUE] |
View or change settings (--repo for one repo) |
maajun add-repo OWNER/NAME |
Watch an additional repository |
maajun provider-list |
Show provider support and stored keys |
maajun sign-out |
Clear stored credentials |
maajun reset |
Delete all config, data, and credentials |
Run maajun <command> --help for the full flag list, or see the
command reference.
Cost control
daemon.max_usd_per_day defaults to $5. Past that, maajun stops analyzing
for the rest of the UTC day, warns once, and keeps polling — skipped errors are
picked up later, not dropped.
maajun config daemon.max_usd_per_day 20 # raise the cap; 0 disables it
maajun config chat.max_usd_per_day 20 # the same, for `maajun chat`
maajun chat has its own $5 daily cap. Past it a new question is refused
with the command to raise it; an answer already being written is never cut
off part-way.
Every incident's exact token count and cost is recorded and shown by
maajun incidents, and --dry-run prints what an analysis would have cost.
Costs are computed from published list prices, in USD per 1M input / output tokens:
| Model | Input | Output |
|---|---|---|
deepseek-v4-flash |
$0.14 | $0.28 |
deepseek-v4-pro |
$0.435 | $0.87 |
gpt-4o-mini |
$0.15 | $0.60 |
gpt-4o |
$2.50 | $10.00 |
On either provider's default model a single analysis costs cents, not dollars —
but measure your own workload with --dry-run rather than trusting an
estimate. Rates change; verify them against
DeepSeek or
OpenAI before relying on the
cap. A model with no entry is costed at the most expensive rate in the table
above, so the cap errs towards stopping early rather than overshooting.
DeepSeek bills cached input at a fraction of these rates. Maajun costs every input token at the cache-miss rate, so a repetitive workload will report somewhat more than it actually spends.
Security model
-
No shell access. The agent has no bash tool in any mode; there is nothing to grant.
-
Scoped reads and writes. Every file tool is confined to an explicit set of directories, enforced by the tool layer rather than asked for in a prompt. The daemon's agent sees only the clone it is analyzing;
maajun chatsees the directory you launched it in,daemon.workdir, and the log files named in your config.suggestmode is read-only on top of that;fixmode may edit inside the clone, never your running application. -
Secrets are refused, not merely unrequested.
.envfiles, SSH and TLS keys,.netrc,.git-credentials, anything under.git/, and maajun's own incident database cannot be opened by any tool, even when they sit inside an allowed directory. What a tool reads goes to your AI provider — and, from the daemon, into an issue or pull request. -
Verification you control.
test_commandcomes from your config, not from the model, so a fix cannot redirect its own verification. -
Chat proposes, you approve.
maajun chatruns read-only commands freely, but anything that writes config or opens a pull request shows the exact command line and waits for a yes — or for a reason not to, which is passed on as an instruction rather than a bare refusal. A file edit shows its diff and the absolute path, flagged when it falls outside the project directory anddaemon.workdir.resetandsign-outit will not run at all.run_maajun_commandreaches maajun's own subcommands and nothing else — it is not a shell. -
Token hygiene. The GitHub token is passed to git via
GIT_ASKPASS; it never lands in a remote URL,.git/config, or the process list.
Documentation
- How it works — components, monitors, and the incident pipeline
- Monitoring guide — full config reference, error sources, cost tracking, running on a VPS
- Command reference — every CLI command and flag
Development
uv sync # install dev dependencies (pytest, ruff)
uv run pytest # run tests
uv run ruff check # lint
License
MIT.
Metadata
Release files for maajun 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| maajun-0.2.0.tar.gz | 236.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| maajun-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 354.6 kB
Release files / maajun-0.2.0.tar.gz
| Download URL | maajun-0.2.0.tar.gz |
|---|---|
| Size | 236.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ddc0dddf65c1745dc60f05d5ebd9137b0837858affca0b9960db038f1d2cebdd
|
|
BLAKE2b-256 checksum How to use checksums |
21def6295254574fece793c573a20e77d1ba0d844a0226799678e4cb5f75918b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency logRelease files / maajun-0.2.0-py3-none-any.whl
| Download URL | maajun-0.2.0-py3-none-any.whl |
|---|---|
| Size | 118.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f5fa81b1104b346ccf1df6cdff9803778a759d2f5c10815ca8bb2fc61f22d419
|
|
BLAKE2b-256 checksum How to use checksums |
ed8009ae45275d467baa176b291ef2dd7fb04103d46e0bb35403b0e3273c7954
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.
Transparency log