Heartbeat dropped: a Docker healthcheck badge restarts nothing under this container's setup, spark-control's tile would need its own change to read it, and serial per-room polling (a review may run 10 min) makes any staleness threshold either too loose to be timely or noisy mid-review. No wedge has ever been observed. Reopens on an observed wedge plus confirmation the tile already renders Docker health. Replaced by a fold-in, since the realistic failure is a hang rather than a crash: bound the two unbounded awaits in src/bot.py and stop the process if the poll task ever dies, letting restart: unless-stopped and the existing spark-control tile cover it. AGENTS.md Current state reconciled to match. Phase 3's onboard-3-repos verify item stands (inside the 14-day skip window).
9.0 KiB
gitea-review-bot: AGENTS.md
gitea-review-bot is a thin Matrix bot that runs an AI PR-review-and-merge workflow for your Gitea
repos, one review room per repo. It is a separate bot from matrix-bridge (which turns Matrix
messages into interactive Claude Code sessions): this one's single responsibility is reviewing
pull requests — so every room it's in is a review room and there is no manual "ignore this room"
wiring. Invite it to a room, it auto-joins and maps the room to a repo by name, then for that repo
it polls Gitea PRs, posts a headless claude -p review as a Matrix thread, and lets you merge/reject
from the thread.
Inbox check: At session start, if
~/Projects/standards/INBOX.mdexists, scan it for items tagged(gitea-review-bot)and surface them before proposing next steps; triage with/triage.
Stack
Python + matrix-nio, one thin Docker container on the Spark (same shape as matrix-bridge and
the ten31-database intake bot). No framework (Maubot rejected — see Decisions). The heavy work —
the claude -p review and any deploy — runs on the Mac over SSH, reusing matrix-bridge's proven
wrappers (scripts/ask-claude.sh for the review, scripts/deploy-site.sh for publish). State is
flat JSON in a writable state/ mount.
Placement
| Dimension | Call |
|---|---|
| Host | Spark, plain Docker container (NOT Start9/s9pk) |
| Runtime | Long-running service: matrix-nio sync + a Gitea poll loop over mapped repos |
| Model routing | claude -p on the Mac via the Spark→Mac SSH seam (subscription); the review session spawns subagents (reviewer / adjudicator / security-auditor) |
| Data layer | Flat JSON in state/ (room→repo map + enabled agents + reviewed-PR heads + thread roots) |
| Interface | Matrix — one review room per repo (+ phone) |
| Repo home | Local + Gitea (ssh://git@immense-voyage.local:59916/grant/gitea-review-bot.git) |
| Sensitivity | Sends PR diffs to claude -p (subscription). Fine for code review; flag the boundary before pointing it at a sensitive repo. |
Commands
- Run (container, on the Spark): from
~/gitea-review-bot,docker compose up -d --build(host networking,restart: unless-stopped; read-only mounts of.env/config.toml/SSH key, read-writestate/). Logs:docker compose logs -f. - Deploy: the Spark's
~/gitea-review-botis a Gitea clone trackingmaster; deploy =git fetch && git reset --hard origin/master && docker compose up -d --build, or one click on this bot's tile in Spark Control's Bots tab (shipped since spark-control v0.28.0, confirmed live: status badge, Update, Start/Restart/Stop, View logs; the tile is hidden unlessGITEA_REVIEW_USERis set in its Settings).config.tomlis gitignored — refresh it on the Spark separately (scp) like matrix-bridge. Reach the Spark from the Mac asssh modelo32(the NVIDIA Sync alias — hostspark-32d0, usermodelo, key-based; injected into~/.ssh/configvia anInclude.modelo27/spark-27eais a second Spark on another LAN).reset --hardis safe:.env/config.toml/state/are gitignored. - Onboard a repo: create a Matrix room named like the repo, invite this bot → it auto-joins,
maps the room to
<owner>/<roomname>(+~/Projects/<roomname>on the Mac), and posts an onboarding message. Pick review agents in-chat:agents +reviewer +security -adjudicator. - In a review room:
merge/reject/clear(inside a PR's thread, or<cmd> <n>by number),yes/noto confirm a merge,agents …to toggle the subagent panel.clear(aliasresolve) redacts an orphaned thread without touching the PR.
Layout
src/bot.py— the bot: matrix-nio sync; auto-join + in-room auto-map (room→repo by name); a Gitea poll loop that walks all mapped repos in one async task, serially (so rooms never interleave, but a long review head-of-line-blocks the next room's poll: the scaling limit to watch at 3+ repos, and what the deferred poll→webhook item addresses); threadedclaude -preview (subagent panel); merge/reject/deploy in-thread; whole-thread redaction on resolve (server-enumerated, restart-proof) — a merge always redacts (a failed post-merge deploy leaves a top-level warning, not an orphaned thread);clearredacts without touching the PR.config.example.toml— homeserver,[mac](ssh alias + the reused matrix-bridge wrapper paths),[gitea](api_base/owner/verify_tls),[defaults], optional[repo.<name>]deploy overrides..env.example—MATRIX_*+GITEA_TOKEN(real.envgitignored).Dockerfile·docker-compose.yml·docker-entrypoint.sh— the Spark container (generic image; secrets/config via read-only mounts; entrypoint writes~/.ssh/configfor the Mac alias).state/— gitignored runtime JSON (rooms.json: room map + agents + heads + thread roots).
Decisions
- Separate bot from matrix-bridge (single responsibility = PR review). Beat: extending matrix-bridge with review-room special-casing. Reopens if the two bots' logic heavily overlaps.
- Thin matrix-nio container, NOT Maubot. Reevaluated 2026-06-28: Maubot helps with the Matrix
plumbing we've already solved, not the SSH/
claude -p/poll logic that's the actual weight, and it reintroduces a web-UI/management layer (Spark Control is the dashboard). Reopens at ~6+ bots or a non-developer web-management need; the lighter step first is a shared "bot kit" library. - Subagent panel (Option B): the lead
claude -psession spawns subagents and presents each output + its own overall recommendation. Beat: bot-orchestrated separateclaude -pruns (more deterministic but 3× the sessions + more bot code). Reopens if headless subagent spawning is flaky. - Panel composition is per-room, set in chat (onboarding message +
agents +/-), not config. - Reuse matrix-bridge's Mac wrappers + Spark→Mac SSH key (don't duplicate the seam).
- Auto-map by name: room
<x>→ Gitea<owner>/<x>+~/Projects/<x>; mapping persists tostate/(mirrors matrix-bridge D14). One room per repo.
Sovereignty
Reviews send PR diffs to claude -p (the subscription), not a frontier API on payload data; that's
acceptable for code review of these repos. Before pointing the bot at a repo with sensitive content,
revisit this — local inference via Spark Control would be the path.
Current state
LIVE and proven. Bot runs on the Spark (docker compose up -d), @reviewer maps each review room
to its repo, and the subagent panel is configured in-chat. Proven end-to-end on ten31-site PR #4
(2026-06-28): threaded review → merge + yes → force_merge → auto-publish → thread redacted — and
the previously-unproven bit, headless claude -p spawning the reviewer/adjudicator/security
subagents, works. The ten31-site colleague is onboarded; the pipeline is in steady use.
Deployed to the Spark at 063e694: a merge always redacts its thread on success, and a failed
post-merge deploy posts its "deploy manually" warning at top level so it survives the redaction
instead of orphaning the thread. clear/resolve redacts an orphaned thread without touching the PR.
Deploy gotcha (still true): the Spark needs a dedicated per-repo Gitea deploy key + a Host
alias (the default immense-voyage.local block uses matrix-bridge's key); Gitea won't reuse one SSH
key across repos' deploy keys. Redeploy via the Spark Control tile, or ssh modelo32.
Known issue / next fix (the concrete next step): the poll loop prunes a PR's thread root from
state when it leaves the open list (src/bot.py ~L594) without redacting, so a PR merged or closed
outside the bot (the Gitea web UI) still orphans its thread, un-redactable by the bot afterward.
The merge-redact fix covers bot-driven merges only; the prune should redact before dropping the root.
Backlog adjudicated 2026-07-18 (9adcdc8): ROADMAP is down to one phase. Phase 2 (subagent panel,
agents +/-) deleted as finished; Phase 4 deleted because spark-control has shipped this bot's tile
since v0.28.0; deploy-on-a-non-static-repo dropped (no repo needs it, and the deploy step is hardcoded
to the static-site publish script, so a per-repo deploy-command field would have to come first).
That left two items: onboard 3 repos, and a wedged-loop heartbeat.
Backlog adjudicated 2026-07-29: the heartbeat is dropped — a healthcheck badge doesn't restart
anything here, spark-control's tile would need its own change to read it, and serial per-room polling
(a review may run 10 min) makes any staleness threshold either too loose to be timely or noisy
mid-review; no wedge has ever been observed. Reopens only on an observed wedge plus confirmation
the tile already renders Docker health. In its place ROADMAP carries a fold-in: bound the two
unbounded awaits in src/bot.py (await proc.wait() after the timeout kill in ssh_run;
client.room_send in say()) and stop the process if the poll task ever dies, so
restart: unless-stopped + the tile cover it. Phase 3's onboard-3-repos verify item stands.