Current state had grown into a running log of two adjudication cycles. The 2026-07-29 heartbeat drop is a decision, so it lives in Decisions with its reopen condition; the per-repo deploy-key requirement is a durable gotcha, so it sits with the Deploy command. What remains is present-tense status plus three ordered next steps: the orphaned-thread prune fix, triaging this repo's 3 inbox items, and the 2nd/3rd-repo poll-cycle check.
8.6 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. Deploy-key gotcha: each repo the Spark clones needs its own Gitea deploy key plus its ownHostalias — Gitea refuses to reuse one SSH key across two repos' deploy keys, and the defaultimmense-voyage.localblock already carries matrix-bridge's key. - 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. - No wedge-detection heartbeat (adjudicated 2026-07-29). A healthcheck badge restarts nothing
under this compose setup, the tile that would show it lives in spark-control (so it's a two-repo
change), and serial per-room polling with a 10-min review budget makes any staleness threshold
either too loose to be timely or noisy mid-review. Reopens only on an observed wedge plus
confirmation the spark-control tile already renders Docker health. The cheaper half of the same
goal is the ROADMAP fold-in: bound the unbounded awaits and die if the poll task dies, so
restart: unless-stoppedhandles it.
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 in steady use. The bot runs on the Spark, maps each review room to its repo, and is proven
end-to-end on ten31-site PR #4 (2026-06-28): threaded review → merge + yes → force_merge →
auto-publish → thread redacted, with headless claude -p genuinely spawning the reviewer / adjudicator /
security subagents. ten31-site is the only repo onboarded so far.
Running on the Spark at 063e694 (a merge always redacts its thread; a failed post-merge deploy warns at
top level rather than orphaning one; clear/resolve clears an orphan without touching the PR). master
is only docs ahead of that, so no redeploy is pending. No test suite: verification means exercising
the bot in a real review room.
Next steps, in priority order:
- Fix the orphaned-thread prune (
src/bot.py~L594): the poll loop drops a PR's thread root from state when the PR leaves the open list without redacting, so a PR merged or closed in the Gitea web UI strands its thread andclearcan no longer reach it. Redact before dropping the root. /triagethe 3 untriaged inbox items tagged for this repo — adjudication never reads the inbox, so their content is still unknown here.- Onboard a 2nd/3rd repo, then watch one poll cycle (
docker compose logs -f) for a long review head-of-line-blocking the next room — ROADMAP Phase 3's verify item, and the real scaling limit.