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>
64 lines
2.9 KiB
Markdown
64 lines
2.9 KiB
Markdown
# 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).
|