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>
6.3 KiB
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 promptdocs/stack-support-runbook.md— the remediation runbookcore_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
dockergroup) - Python 3 (stdlib only — no venv needed)
- 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:
{"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 bypassPermissionsunder root. paulis in thedockergroup, so the agent can drive Docker/Compose.- Kuma's root-owned
kuma.dbis read via a throwawaykeinos/sqlite3container mounting the named volume read-only — no host file access needed.
Install
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
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 for the architecture
decision record.