# .dispatcher.yaml — contract between a project and signal-dispatcher. # Lives in the ROOT of the project's own repo (here: goon-foss/goon), versioned # with the project. `dispatcher project-sync ` loads it into the DB. schema_version: 1 project: name: goon description: > Self-hosted aggregator of adult (18+) scene metadata. FastAPI backend + APScheduler worker on Postgres 16. Ingests from TPDB and StashDB on a delta cron, cross-source dedup by perceptual hash + title + performer, plus a performer-driven backfill that scrapes ~25 public tube sites. On-demand stream resolution via yt-dlp and a P.A.C.K.E.R. unpacker. Expo / React Native mobile client (Android). keywords: [goon, tpdb, stashdb, tube, scene, performer, phash, yt-dlp, packer] repo: url: https://github.com/goon-foss/goon default_branch: public/main # local `main` is a stale divergent line; prod work happens on public/main branch_prefix: auto-fix/ pr_label: auto-fix require_ci_green_before_merge: true deploy: mode: manual # no CD: deploy = scp + `docker compose restart` # Consequence: a merged PR is NOT live until a human deploys. The dispatcher # keeps the issue in `verifying` (deploy_pending=true) and closes it only once # Sentry actually goes quiet — never on merge alone. worktree: base: /opt/dispatcher/worktrees/goon setup: - python3.12 -m venv .venv - .venv/bin/pip install -e ".[dev]" ci: # -x stops at the first failure: fast for the fix loop. Note CI runs the full # suite (`pytest --tb=short`), so CI can surface failures this command hides. test_cmd: .venv/bin/pytest --tb=short -x -q lint_cmd: .venv/bin/ruff check app/ tests/ pre_test: [] # tests use respx HTTP mocks + fixtures, no DB post_test: [] timeout_min: 10 sources: sentry: # org_slug injected from env (SENTRY_ORG) — kept out of source to avoid linking # this public repo to a specific Sentry org identity. project_slug: goon # backend. Mobile (RN) is a SEPARATE Sentry project: `android` region_url: https://de.sentry.io # EU region — Sentry's API is region-scoped environments: [production] severity_map: {error: med, warning: low, fatal: high, info: low} ntfy: topics: [] # goon emits no custom alerts today slack: null path_policy: # Tube scrapers: a broken selector means the SITE changed, not the code. An # LLM can write a plausible new selector but cannot know the correct one # without the live page — and the test fixture IS the HTML that just broke. # So: open a PR as a HYPOTHESIS, never auto-merge, a human verifies live. - paths: [app/connectors/direct_scrapers/, app/extractors/] mode: pr-hypothesis # Cross-source dedup: regressions here are genuine code bugs, but subtle. # Real fix attempts, but always PR — never auto-merge. - paths: [app/resolve/] mode: pr-only # Everything else (app/api/, app/scheduler/, ...) follows the policy table. forbidden_paths: - alembic/versions/ # DB migrations — auto-edit is catastrophic - .github/workflows/ - deploy/ - docker-compose.yml - Dockerfile - pyproject.toml # dependency bumps are a separate decision - mobile/ # separate pipeline (OTA via scripts/publish_update.py) - app/static/ # APK + OTA bundle artifacts land here protected_branches: [main] fix_constraints: max_files_changed: 5 max_lines_changed: 200 knowledge: doc_paths: [README.md, CONTRIBUTING.md, DEPLOY_BACKLOG.md, DISCLAIMER.md] # Injected into the classifier prompt. Derived from goon's live Sentry # (23 unresolved issues, 2026-05-22): transient upstream noise dominates. triage_hints: > Transient upstream failures dominate goon's Sentry and are NOT bugs: HTTPException with "upstream 4xx/5xx", "proxy error", "extraction failed temporarily", "img/tube fetch failed", "All connection attempts failed" — a remote site or CDN failed, not goon. Classify intent=noise unless one culprit sustains a high event rate. Smoke-test / health-check events ("Smoke test ...", "Sentry init OK") are intent=noise, severity=low. Genuine bugs (intent=bug): NameError, ModuleNotFoundError, TypeError from a signature/constructor mismatch, psycopg IntegrityError / UniqueViolation in dedup or upsert paths, and encoding errors (UnicodeEncodeError). Read the UNDERLYING error, not the HTTPException prefix — an "img fetch failed" title can wrap a real UnicodeEncodeError bug. conventions: > Code identifiers and type hints are English; some inline comments are Polish. curl_cffi is pinned <0.15 until yt-dlp ships a compat release (see pyproject.toml). The performer-driven worker walks performers by last_searched_at NULLS FIRST — "completeness over recency" is intentional. Sentry before_send drops upstream 502/503/504, so absence of an alert is not proof of a fix. # Seeds the `policies` table on `dispatcher project-sync`. Conservative on # purpose: goon starts fully PR-gated. Flip low/med Sentry bugs to # auto-merge-on-green only after the classifier proves reliable. policies: - priority: 50 match_severity: [critical] ship_mode: pr-and-ntfy notify: [ntfy:urgent] max_attempts: 1 verify_window: "1 hour" - priority: 60 match_severity: [high] ship_mode: pr-and-ntfy notify: [ntfy:default] max_attempts: 2 verify_window: "1 hour" - priority: 100 match_severity: [low, med] match_intent: [bug, data_quality] match_source: [sentry] ship_mode: pr-and-ntfy notify: [ntfy:default] max_attempts: 2 verify_window: "30 minutes"