# semprini-maintainer — autonomous stack maintenance agent A standalone systemd service that keeps the container stack healthy and current, and talks to the operator over Matrix (chat.semprini.me). It is the *unattended driver* for the canonical stack-support agent that lives in the **semprini-core** repo (`agents/stack-support.agent.md`): the daemon gathers state and hands the judgement calls (diagnosis, remediation, upgrade planning) to that agent, run headless via Claude Code. ## Relationship to semprini-core This is a **separate project** from the stack it maintains. It does not bundle its own copy of the stack logic — at runtime it reads, from the semprini-core checkout pointed to by `repo_dir` in `config.json`: - `agents/stack-support.agent.md` — injected as the agent's system prompt - `docs/stack-support-runbook.md` — the remediation runbook - `core_stack/compose*.yml` — the compose files it operates on So **a semprini-core checkout and the running core stack must be present on the host** (they are what this agent maintains). The canonical stack-support agent definition stays in semprini-core to avoid drift. Other projects that register Uptime Kuma monitors (e.g. `semprini-blog`) are also maintained — their repos are made available to the agent via `extra_dirs`. ### Prerequisites - Docker (host user in the `docker` group) - Python 3 (stdlib only — no venv needed) - [Claude Code](https://claude.com/claude-code) CLI on `PATH` - A semprini-core checkout + the core stack running (incl. Uptime Kuma) ## What it does | Cadence | Action | |---|---| | **Every 5 min** (`health_interval_seconds`) | Read Uptime Kuma to learn which services are tracked **right now** and their up/down status. If a service is confirmed down across `down_confirmations` cycles, invoke the stack-support agent to diagnose + remediate, then report to Matrix. | | **Every 7 days** (`upgrade_interval_seconds`) | Resolve the containers behind tracked services, find the owning compose file, and have the agent research + apply upgrades (test, pin, rollback on failure). Breaking/major upgrades are escalated to Matrix for a decision. | | **Continuous** (`matrix_poll_seconds`) | Poll the Matrix room for the admin's replies — to answer escalations, or to take ad-hoc instructions. | | **Daily** (`heartbeat_interval_seconds`) | Quiet "N/M services up" heartbeat when all is well. | ### Scope is dynamic Scope is **whatever Uptime Kuma is tracking at the moment the check runs** — the monitor list is re-read every cycle, nothing is hard-coded. This deliberately covers services from *other* projects that register Kuma monitors (e.g. `semprini-blog`), not just `semprini-core`. Other project repos are made available to the agent via `extra_dirs`. Kuma's own heartbeat result is the health signal, so the daemon never re-implements probes — it trusts the monitoring system already in the stack. ## Architecture ``` ┌─────────────────────────────────────────┐ │ maintainer.py (daemon) │ Uptime Kuma ──┤ read monitors+status (sqlite in volume) │ (kuma.db) │ │ │ on down / on schedule: │ │ claude -p --append-system-prompt │──▶ stack-support │ (agents/stack-support.agent.md) │ agent does the │ │ diagnosis/fix │ parse JSON result envelope │◀── + returns JSON │ │ Matrix ◀─────┤ send summaries / ask questions │ (@paul) ─────┤ poll replies → resume escalations │ └─────────────────────────────────────────┘ ``` The agent returns a small JSON envelope the daemon relays: ```json {"status":"ok|action_taken|escalate|blocked","summary":"…","question":"…","options":["…"]} ``` `escalate` posts the question (numbered options) to Matrix and pauses upgrades until the admin replies; the reply is fed back to the agent to carry out. ## Autonomy Set `autonomy` in `config.json`: - `full` *(default)* — remediate **and** apply safe upgrades autonomously (test + auto-rollback); escalate only major/breaking upgrades. - `fix-only` — remediate health issues; report upgrades but never apply them. - `notify` — never change anything; only report to Matrix. ## Why it runs as `paul`, not root - Claude Code refuses `--permission-mode bypassPermissions` under root. - `paul` is in the `docker` group, so the agent can drive Docker/Compose. - Kuma's root-owned `kuma.db` is read via a throwaway `keinos/sqlite3` container mounting the named volume read-only — no host file access needed. ## Install ```bash cd /home/paul/Dev/semprini-maintainer # 1. Config cp config.example.json config.json # set repo_dir to the semprini-core checkout; edit paths/intervals if needed # (defaults match this host) # 2. Provision the Matrix bot + private room with the admin ./register-matrix-bot.sh # then, one-time: accept the room invite in Element as @paul:semprini.me # 3. Install + start the service ./install.sh ``` `config.json` and `.bot-secrets` hold the bot token/password and are gitignored. ## Operate ```bash journalctl -u semprini-maintainer -f # logs systemctl status semprini-maintainer sudo systemctl restart semprini-maintainer # One-off cycles without the service (for testing): python3 maintainer.py --config config.json --once health python3 maintainer.py --config config.json --once upgrade python3 maintainer.py --config config.json --once replies ``` ## Files | File | Purpose | |---|---| | `maintainer.py` | The daemon. Stdlib only. | | `config.example.json` | Template — copy to `config.json`. | | `register-matrix-bot.sh` | Creates the `@maintainer` account + room, writes token to config. | | `semprini-maintainer.service` | systemd unit (runs as `paul`). | | `install.sh` | Syntax-check, install, enable, start. | See [`docs/architecture.md`](docs/architecture.md) for the architecture decision record.