Files
semprini-maintainer/README.md
T
paulandClaude Opus 4.8 e91b129841 Initial commit: autonomous maintenance agent for semprini stack
systemd-deployed daemon that drives the semprini-core stack-support agent
headless via Claude Code, monitors health via Uptime Kuma, applies safe
upgrades, and escalates to the operator over Matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:20:06 +12:00

142 lines
6.3 KiB
Markdown

# 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.