Slice 8 WIP: §12 hygiene + §10.7 + routing + rollback cleanup

- Add §12 30/90 hygiene scheduler in hygiene.py, mirroring the
  DigestScheduler shape; wires next to digest in main.py with the
  same start/stop/run_tick test seam.
- Extend bot.delete_branch to accept actor=None for system gestures,
  per §15.9 (actor_user_id=NULL, on_behalf_of=bot_login).
- Convert every branches/{branch} route in api_branches.py and
  api_prs.py to {branch:path}; move the bare GET to the bottom of
  the router so deeper GETs match before greedy-path swallow.
- Extend api_prs.py's _require_pr to accept pr_kind='meta_metadata'
  so the §9.5 metadata-pane PRs land an in-app merge.
- Graduation rollback now deletes the graduate-<slug>-<6hex> branch
  after closing the PR — §19.2 candidate that lands here.
- Email-bounce webhook gains a WEBHOOK_EMAIL_BOUNCE_SECRET seam.
- FakeGitea grows a DELETE /branches/{branch:path} handler and a
  slashed-branch read; integration tests for the hygiene vertical
  cover the 30d close, 90d delete, post-merge delete, pinned
  exemption, per-user cursor preservation, no-notification rule,
  and the graduation-rollback cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-05-25 04:03:09 -07:00
parent 060fa408a2
commit 1a0c4428af
9 changed files with 965 additions and 105 deletions
+4 -1
View File
@@ -14,7 +14,7 @@ from fastapi import APIRouter, FastAPI, HTTPException, Request
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from . import api as api_routes, auth, cache, db, digest, providers as providers_mod, webhooks
from . import api as api_routes, auth, cache, db, digest, hygiene, providers as providers_mod, webhooks
from .bot import Bot
from .config import load_config
from .gitea import Gitea
@@ -32,6 +32,7 @@ async def lifespan(app: FastAPI):
bot = Bot(gitea)
reconciler = cache.Reconciler(config, gitea)
digest_sched = digest.DigestScheduler()
hygiene_sched = hygiene.HygieneScheduler(config=config, bot=bot)
# §18 carryover: the multi-provider LLM abstraction. Provider
# construction can fail (missing key, wrong env value) — if it does,
@@ -55,10 +56,12 @@ async def lifespan(app: FastAPI):
reconciler.start()
digest_sched.start()
hygiene_sched.start()
log.info("RFC app started — meta repo %s/%s", config.gitea_org, config.meta_repo)
try:
yield
finally:
await hygiene_sched.stop()
await digest_sched.stop()
await reconciler.stop()
await gitea.close()