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>
This commit is contained in:
2026-06-21 17:20:06 +12:00
co-authored by Claude Opus 4.8
commit e91b129841
9 changed files with 1090 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
# Maintenance Agent (semprini-maintainer)
Autonomous, always-on agent that keeps the container stack healthy and current.
Implementation: this repository — see [`README.md`](../README.md). It maintains
the **semprini-core** stack (a separate repo) referenced via `repo_dir`.
## Purpose
Continuously maintain the running stack without an operator at the keyboard:
1. Detect and **remediate** service outages.
2. Periodically **check for and apply container upgrades** (with test + rollback).
3. **Communicate with the operator over Matrix** (chat.semprini.me), including
asking questions when an upgrade needs a human decision.
It is the unattended driver for the canonical stack-support agent
(`agents/stack-support.agent.md`) — it does not re-implement that logic, it
feeds the agent state and relays its results.
## Key decisions
- **Scope = Uptime Kuma's live monitor list.** Read every health cycle, never
hard-coded. Anything Kuma tracks is in scope — including services owned by
other projects (e.g. `semprini-blog`) that register Kuma monitors. This keeps
the maintained set and the monitored set identical by construction.
- **Health signal = Kuma heartbeat.** The daemon reads Kuma's own up/down
result rather than re-probing, reusing the stack's existing monitoring.
- **Kuma data access** via a throwaway `keinos/sqlite3` container mounting the
`uptime-kuma-data` volume read-only (the daemon user has Docker but not host
access to root-owned volume files).
- **Runner = Claude Code headless** (`claude -p`), with
`agents/stack-support.agent.md` injected as the system prompt. The agent
returns a JSON envelope (`status/summary/question/options`) the daemon acts on.
- **Runs as `paul`** (in the `docker` group), not root — Claude Code refuses
`bypassPermissions` as root.
- **Autonomy = full** by default: remediate and apply safe upgrades; escalate
only major/breaking upgrades to Matrix and resume on reply. Configurable to
`fix-only` or `notify`.
- **Matrix transport**: dedicated bot `@maintainer:semprini.me` (created via the
Synapse registration shared secret) in a private room with `@paul:semprini.me`.
## Cadence
| Loop | Default interval | Config key |
|---|---|---|
| Health check + remediation | 5 min | `health_interval_seconds` |
| Upgrade review | 7 days | `upgrade_interval_seconds` |
| Matrix reply polling | 30 s | `matrix_poll_seconds` |
| Quiet health heartbeat | daily | `heartbeat_interval_seconds` |
## Escalation flow
1. Agent returns `status=escalate` with a `question` + `options`.
2. Daemon posts a numbered question to Matrix and pauses upgrade cycles.
3. Admin replies (a number or free text).
4. Daemon feeds the decision back to the agent, which carries it out and reports.
## Operational notes
- The bot DM room invite must be accepted once by `@paul` in Element.
- Secrets (`config.json`, `.bot-secrets`) are gitignored.
- A single unanswered escalation blocks new upgrade cycles (health remediation
continues regardless).